File size: 13,243 Bytes
794b22f ee84700 794b22f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | /* test-designer.mjs: the designer's state <-> DSSL core (dssl-vscode/media/designer-core.js).
* What the panel writes must (1) compile with the real compiler, (2) read back to the same
* state, and (3) leave out anything that is just the default. A seeded random sweep hammers
* all three. The panel itself is exercised in a browser by test-designer-browser.mjs. */
import * as core from '../dssl-vscode/media/designer-core.js';
import { compile } from '../public/dssl/index.js';
import { rng } from '../public/dssl/texture.js';
let pass = 0, fail = 0;
const ok = (name, cond, extra) => { if (cond) { pass++; console.log(` ok ${name}`); } else { fail++; console.log(`FAIL ${name}${extra !== undefined ? '; ' + extra : ''}`); } };
const section = (t) => console.log(`\n== ${t}`);
const compiles = (snippet) => { const r = compile(core.wrapForCompile(snippet)); return { ok: r.ok, errors: r.errors.map((e) => e.message) }; };
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
section('aura');
{
const d = core.defaultAuraState();
for (const style of ['separate', 'single']) {
const text = core.auraToDssl(d, style);
const c = compiles(text);
ok(`the default aura (${style}) compiles`, c.ok, c.errors.join(' | ') + '\n' + text);
const back = core.auraFromDssl(text).state;
ok(`and reads back to the same state (${style})`, same(back.particles, d.particles) && same(back.glow, d.glow), JSON.stringify(back.particles) + '\n' + JSON.stringify(d.particles));
}
const off = core.defaultAuraState(); off.glow.on = false; off.particles.on = false;
ok('with both layers off it writes aura: none, which compiles', core.auraToDssl(off) === 'aura: none' && compiles('aura: none').ok);
const g = core.defaultAuraState(); g.particles.on = false;
ok('glow only writes only aura-glow', core.auraToDssl(g) === `aura-glow: #ff7a00 ${core.GLOW_DEFAULTS.radius}` && !/particle/.test(core.auraToDssl(g)));
const plain = core.defaultAuraState();
plain.glow.radius = 24; plain.particles = { ...plain.particles, ...core.PARTICLE_DEFAULTS, stops: [{ hex: '#ffffff', a: 1 }, { hex: '#ffffff', a: 0 }], count: 40, glowing: false, behind: false, dir: 'up', texture: '', blend: '', size: [4, 4], life: [900, 900], speed: [40, 40], spread: 30, gravity: 0, drag: 0, turbulence: 0, from: 'edge', shape: 'circle', grow: 0.3 };
ok('settings left at their defaults are not written', core.auraToDssl(plain).split('\n')[1] === 'aura-particle: #ffffff -> rgba(255,255,255,0) count 40', core.auraToDssl(plain).split('\n')[1]);
ok('a see-through colour is written as rgba, an opaque one as hex', core.colorToDssl({ hex: '#ff0000', a: 0.5 }) === 'rgba(255,0,0,0.5)' && core.colorToDssl({ hex: '#ff0000', a: 1 }) === '#ff0000');
const withTok = core.auraFromDssl('aura-glow: color.accent 24');
ok('a token colour cannot go on a picker: it is kept as the default and the user is told', withTok.state.glow.on && withTok.notes.some((n) => /token or variable/.test(n)));
ok('text that is not an aura is ignored, not an error', (() => { const r = core.auraFromDssl('.card { color: red }'); return !r.state.glow.on && !r.state.particles.on && r.notes.length === 0; })());
ok('a broken aura says what is wrong instead of throwing', core.auraFromDssl('aura-glow: #fff radius 99999').notes.length === 1);
ok('degrees round-trip', (() => { const x = core.defaultAuraState(); x.particles.dir = 'deg'; x.particles.deg = -45; const t = core.auraToDssl(x); return /dir -45/.test(t) && core.auraFromDssl(t).state.particles.deg === -45; })());
ok('multi-line aura: declarations are read as one', core.auraFromDssl(core.auraToDssl(d, 'single')).state.particles.on === true);
}
section('texture');
{
const bad = [];
for (const name of core.PRESET_NAMES) {
const st = { layers: [core.defaultTextureLayer(name)], base: '#fff' };
const text = core.textureToDssl(st);
const c = compiles(text);
const back = core.textureFromDssl(text).state;
if (!c.ok || text !== `texture: ${name}` || back.layers[0].name !== name || back.layers.length !== 1) bad.push(name);
}
ok(`all ${core.PRESET_NAMES.length} presets write as just their name, compile, and read back`, bad.length === 0, bad.join(','));
const custom = { layers: [{ name: 'wood', set: { scale: 9, intensity: 0.8, stretch: [20, 2], blend: 'multiply' }, color: { hex: '#331100', a: 1 }, color2: { hex: '#ddaa66', a: 1 } }], base: '#fff' };
const t = core.textureToDssl(custom);
ok('overrides are written, and only the ones that differ from the preset', /scale 9/.test(t) && /intensity 0.8/.test(t) && /stretch 20 2/.test(t) && !/blend/.test(t) && /color #331100/.test(t) && /color2 #ddaa66/.test(t), t);
ok('custom textures compile', compiles(t).ok, compiles(t).errors.join('|'));
const back = core.textureFromDssl(t).state.layers[0];
ok('and read back with the same overrides', back.name === 'wood' && back.set.scale === 9 && back.set.intensity === 0.8 && same(back.set.stretch, [20, 2]) && back.color.hex === '#331100', JSON.stringify(back));
const stack = { layers: [core.defaultTextureLayer('grain'), { name: 'lines', set: { angle: 30 }, color: null, color2: null }], base: '#fff' };
ok('stacked layers use commas and read back as two layers', (() => { const s = core.textureToDssl(stack); const r = core.textureFromDssl(s).state; return /grain, lines angle 30/.test(s) && r.layers.length === 2 && r.layers[1].set.angle === 30 && compiles(s).ok; })());
ok('a generator with no preset round-trips', (() => { const s = core.textureToDssl({ layers: [{ name: 'noise', set: { scale: 3, octaves: 4 }, color: null, color2: null }], base: '#fff' }); const r = core.textureFromDssl(s).state.layers[0]; return compiles(s).ok && r.name === 'noise' && r.set.scale === 3 && r.set.octaves === 4; })());
ok('no layers writes texture: none', core.textureToDssl({ layers: [], base: '#fff' }) === 'texture: none' && compiles('texture: none').ok);
ok('a texture with an unknown name reports a note, never throws', core.textureFromDssl('texture: grian').notes.length === 1);
}
section('slider');
{
const d = core.defaultSliderState();
const t = core.sliderToDssl(d);
ok('the default slider compiles', compiles(t).ok, compiles(t).errors.join('|') + '\n' + t);
const back = core.sliderFromDssl(t).state;
ok('and reads back to the same state', same({ ...back, bars: null }, { ...d, bars: null }), JSON.stringify(back));
const full = { ...d, shape: 'diamond', ring: true, glow: true, ticks: true, graph: true, trackRadius: 'px', trackRadiusPx: 3, fill: false };
const ft = core.sliderToDssl(full);
ok('every clause together compiles', compiles(ft).ok, compiles(ft).errors.join('|') + '\n' + ft);
ok('and reads back with every clause on', (() => { const r = core.sliderFromDssl(ft).state; return r.shape === 'diamond' && r.ring && r.glow && r.ticks && r.graph && r.fill === false && r.trackRadius === 'px' && r.trackRadiusPx === 3 && same(r.bars, d.bars); })());
ok('a token colour on a slider is kept as the default and flagged', core.sliderFromDssl('slider: fill color.accent').notes.some((n) => /token or variable/.test(n)));
}
section('random sweep: everything the panel can write compiles and reads back');
{
const r = rng(20261024);
const pick = (a) => a[Math.floor(r() * a.length)];
const num = (lo, hi, step = 1) => +(Math.round((lo + r() * (hi - lo)) / step) * step).toFixed(4);
const col = () => ({ hex: '#' + [0, 0, 0].map(() => Math.floor(r() * 256).toString(16).padStart(2, '0')).join(''), a: r() < 0.3 ? +r().toFixed(2) : 1 });
const range = (lo, hi) => { const a = num(lo, hi), b = num(lo, hi); return [Math.min(a, b), Math.max(a, b)]; };
let n = 0, bad = [];
for (let i = 0; i < 300; i++) {
// aura
const a = core.defaultAuraState();
a.glow = { on: r() < 0.8, color: col(), radius: num(1, 200), intensity: num(0, 3, 0.1), pulse: r() < 0.5 ? 0 : num(100, 5000, 100), depth: num(0, 1, 0.05), flicker: r() < 0.5 ? 0 : num(0, 1, 0.05), texture: r() < 0.3 ? pick(['grain', 'dots', 'lines', 'cloud']) : '', seed: num(1, 50), essential: r() < 0.2 };
a.particles = { on: r() < 0.9, stops: Array.from({ length: num(1, 4) }, col), count: num(1, 300), size: range(1, 60), life: range(100, 4000), speed: range(0, 400), dir: pick(['up', 'down', 'left', 'right', 'out', 'in', 'deg']), deg: num(-180, 180), spread: num(0, 360), gravity: num(-300, 300), drag: num(0, 5, 0.1), turbulence: num(0, 300), from: pick(core.FROM_WORDS), shape: pick(core.SHAPES), grow: num(0, 4, 0.1), glowing: r() < 0.5, behind: r() < 0.5, blend: pick(['', 'add', 'normal']), texture: r() < 0.3 ? pick(['grain', 'dots', 'lines']) : '', seed: num(1, 50), essential: r() < 0.2 };
for (const style of ['separate', 'single']) {
const text = core.auraToDssl(a, style);
const c = compiles(text); n++;
if (!c.ok) { bad.push(`aura ${style}: ${c.errors[0]} :: ${text.replace(/\n/g, ' | ')}`); continue; }
const back = core.auraFromDssl(text).state;
// depth only means something with a pulse, so it is not written (and reads back as the default) when pulse is 0
const expGlow = { ...a.glow, depth: a.glow.pulse ? a.glow.depth : core.GLOW_DEFAULTS.depth };
if (a.glow.on && !same(back.glow, expGlow)) bad.push(`aura glow round trip ${JSON.stringify(expGlow)} vs ${JSON.stringify(back.glow)}`);
if (a.particles.on) {
const exp = { ...a.particles }; if (exp.stops.length === 1) exp.stops = [exp.stops[0], { ...exp.stops[0], a: 0 }];
if (exp.dir !== 'deg') exp.deg = back.particles.deg;
else if (exp.deg === -90) { exp.dir = 'up'; } else if (exp.deg === 90) exp.dir = 'down'; else if (exp.deg === 180) exp.dir = 'left'; else if (exp.deg === 0) exp.dir = 'right';
if (!same(back.particles, exp)) bad.push(`aura particle round trip\n ${JSON.stringify(exp)}\n ${JSON.stringify(back.particles)}`);
}
}
// texture
const layer = { name: pick([...core.PRESET_NAMES, ...core.GENERATORS]), set: {}, color: r() < 0.4 ? col() : null, color2: r() < 0.4 ? col() : null };
if (r() < 0.6) layer.set.scale = num(1, 300); if (r() < 0.5) layer.set.intensity = num(0, 2, 0.05); if (r() < 0.3) layer.set.octaves = num(1, 8);
if (r() < 0.3) layer.set.stretch = [num(1, 60), num(1, 60)]; if (r() < 0.3) layer.set.blend = pick(['multiply', 'screen', 'overlay', 'soft-light']);
const ts = { layers: [layer], base: '#fff' }; const tt = core.textureToDssl(ts); const tc = compiles(tt); n++;
if (!tc.ok) bad.push(`texture: ${tc.errors[0]} :: ${tt}`);
else if (core.textureFromDssl(tt).state.layers[0].name !== layer.name) bad.push(`texture name lost :: ${tt}`);
// slider
const sl = core.defaultSliderState();
Object.assign(sl, { trackH: num(2, 30), trackColor: col(), trackRadius: pick(['full', 'px']), trackRadiusPx: num(0, 12), fill: r() < 0.8, fillColor: col(), thumb: num(8, 48), thumbColor: col(), shape: pick(core.THUMB_SHAPES), ring: r() < 0.5, ringW: num(1, 8), ringColor: col(), glow: r() < 0.5, glowColor: col(), glowR: num(2, 40), ticks: r() < 0.5, tickCount: num(2, 20), tickColor: col(), tickLen: num(2, 14), graph: r() < 0.5, bars: Array.from({ length: num(2, 30) }, () => num(0, 100)), graphH: num(16, 90), graphGap: num(0, 8) });
const st = core.sliderToDssl(sl); const sc = compiles(st); n++;
if (!sc.ok) bad.push(`slider: ${sc.errors[0]} :: ${st.replace(/\n/g, ' | ')}`);
else { const b = core.sliderFromDssl(st).state; if (b.shape !== sl.shape || b.trackH !== sl.trackH || b.graph !== sl.graph || b.fill !== sl.fill || (sl.graph && !same(b.bars, sl.bars))) bad.push(`slider round trip :: ${st.replace(/\n/g, ' | ')}`); }
}
ok(`${n} random designs all compile and read back (aura both styles, texture, slider)`, bad.length === 0, `${bad.length} problems; first: ${bad[0]}`);
}
// 0.11/0.12: the wood settings and the mirrored tile survive the designer both ways
{
const mk = (set) => ({ layers: [{ name: 'oak', set, color: null, color2: null }], base: '#b8a58a' });
const t1 = core.textureToDssl(mk({ planks: 120, knots: 2, figure: 0.8, symmetrical: 'both' }));
ok('wood settings are written (planks, knots, figure, symmetrical)', /^texture: oak /.test(t1) && /planks 120/.test(t1) && /knots 2/.test(t1) && /figure 0.8/.test(t1) && /symmetrical(?! (x|y|both))/.test(t1), t1);
ok('and the result compiles', compiles(t1).ok, JSON.stringify(compiles(t1)));
const back = core.textureFromDssl(t1).state.layers[0];
ok('and reads back into the same controls', back.name === 'oak' && back.set.planks === 120 && back.set.knots === 2 && back.set.figure === 0.8 && back.set.symmetrical === 'both', JSON.stringify(back));
ok('x and y are kept', /symmetrical x/.test(core.textureToDssl(mk({ symmetrical: 'x' }))) && core.textureFromDssl('texture: oak symmetrical y').state.layers[0].set.symmetrical === 'y');
ok('a preset that already says nothing special writes just its name', core.textureToDssl(mk({})) === 'texture: oak');
ok('RINGS_ONLY lists exactly the settings that only mean something for wood', JSON.stringify(core.RINGS_ONLY) === JSON.stringify(['figure', 'warp', 'pores', 'knots', 'planks']));
}
console.log(`\n${pass} passed, ${fail} failed`);
process.exit(fail ? 1 : 0);
|