Spaces:
Running
Running
| /* Bet 9 battery — eraser / backdrop / band gains, tested headless. | |
| 1. bandOf edges match the declared bands | |
| 2. band-gain linearity: sum_b gain_b * render(band_b) == render(gain-scaled colors), exactly | |
| 3. blendOverBackdrop: identities (a=1 field wins, a=0 backdrop wins), image backdrop path | |
| 4. eraseMask geometry + skip veto | |
| 5. erase -> undo round-trip: rendered field identical (order-free re-append) | |
| 6. burn-to-template: template loses exactly the erased refs; undo restores them | |
| */ | |
| ; | |
| const C = require("./studio/core.js"); | |
| const SC = require("./studio/studio_core.js"); | |
| let go = 0, fail = 0; | |
| function T(name, ok, note) { | |
| console.log((ok ? "GO " : "FAIL") + " " + name + (note ? " [" + note + "]" : "")); | |
| ok ? go++ : fail++; | |
| } | |
| const rnd = (() => { let s = 424242; return () => (s = (s * 1103515245 + 12345) & 0x7fffffff) / 0x7fffffff; })(); | |
| function randAtoms(n) { | |
| const out = []; | |
| for (let i = 0; i < n; i++) { | |
| const su = 0.03 + 0.1 * rnd(); | |
| out.push([-0.8 + 1.6 * rnd(), -0.8 + 1.6 * rnd(), Math.PI * rnd(), | |
| su, su * (0.4 + 0.5 * rnd()), 0.6 + 9 * rnd(), 2 * Math.PI * rnd(), | |
| -1 + 2 * rnd(), -1 + 2 * rnd(), -1 + 2 * rnd()]); | |
| } | |
| return out; | |
| } | |
| const H = 64; | |
| /* 1 ------------------------------------------------------------------ */ | |
| T("bandOf edges", SC.bandOf(1.99) === 0 && SC.bandOf(2.0) === 1 && | |
| SC.bandOf(4.99) === 1 && SC.bandOf(5.0) === 2, "edges at f=2,5 (4 & 10 c/img)"); | |
| /* 2 band-gain linearity ---------------------------------------------- */ | |
| { | |
| const atoms = randAtoms(40); | |
| const gains = [0.3, 1.7, 0.0]; | |
| // path A: per-band buffers scaled at composite time | |
| const bands = [new Float32Array(3 * H * H), new Float32Array(3 * H * H), new Float32Array(3 * H * H)]; | |
| for (const a of atoms) C.renderPre([a], H, bands[SC.bandOf(a[5])]); | |
| const A = new Float32Array(3 * H * H); | |
| for (let b = 0; b < 3; b++) for (let i = 0; i < A.length; i++) A[i] += gains[b] * bands[b][i]; | |
| // path B: gain baked into atom colors, rendered once | |
| const scaled = atoms.map(a => { | |
| const g = gains[SC.bandOf(a[5])], c = a.slice(); | |
| c[7] *= g; c[8] *= g; c[9] *= g; return c; | |
| }); | |
| const B = C.renderPre(scaled, H); | |
| let maxd = 0; | |
| for (let i = 0; i < A.length; i++) maxd = Math.max(maxd, Math.abs(A[i] - B[i])); | |
| T("band gains exact by render linearity", maxd < 1e-6, "max |A-B| = " + maxd.toExponential(1)); | |
| } | |
| /* 3 backdrop blending -------------------------------------------------*/ | |
| { | |
| const atoms = randAtoms(25); | |
| const pre = C.renderPre(atoms, H); | |
| const img0 = C.sigmoidField(pre, H); | |
| const alpha = SC.alphaFromPre(pre, H); | |
| // a) alpha forced to 1 -> field unchanged | |
| const one = new Float32Array(H * H).fill(1); | |
| const imgA = new Uint8ClampedArray(img0); | |
| SC.blendOverBackdrop(imgA, one, 1, [10, 200, 30], H); | |
| let d = 0; for (let i = 0; i < imgA.length; i++) d = Math.max(d, Math.abs(imgA[i] - img0[i])); | |
| T("alpha=1: field wins everywhere", d === 0, "max delta " + d); | |
| // b) opacity 0 -> pure backdrop color | |
| const imgB = new Uint8ClampedArray(img0); | |
| SC.blendOverBackdrop(imgB, alpha, 0, [10, 200, 30], H); | |
| let ok = true; | |
| for (let i = 0; i < H * H; i++) | |
| if (imgB[4 * i] !== 10 || imgB[4 * i + 1] !== 200 || imgB[4 * i + 2] !== 30) { ok = false; break; } | |
| T("opacity=0: backdrop wins everywhere", ok); | |
| // c) image backdrop path: where alpha==0 the backdrop image shows verbatim | |
| const bg = new Uint8ClampedArray(4 * H * H); | |
| for (let i = 0; i < H * H; i++) { bg[4 * i] = i % 251; bg[4 * i + 1] = (2 * i) % 251; bg[4 * i + 2] = (3 * i) % 251; bg[4 * i + 3] = 255; } | |
| const imgC = new Uint8ClampedArray(img0); | |
| SC.blendOverBackdrop(imgC, alpha, 1, bg, H); | |
| ok = true; | |
| let checked = 0; | |
| for (let i = 0; i < H * H; i++) if (alpha[i] === 0) { | |
| checked++; | |
| if (imgC[4 * i] !== bg[4 * i] || imgC[4 * i + 1] !== bg[4 * i + 1] || imgC[4 * i + 2] !== bg[4 * i + 2]) { ok = false; break; } | |
| } | |
| T("image backdrop shows through empty pixels", ok && checked > 100, checked + " empty px checked"); | |
| } | |
| /* 4 eraseMask ----------------------------------------------------------*/ | |
| { | |
| const obs = [[0, 0, 0, .05, .05, 3, 0, 1, 0, 0], [0.09, 0, 0, .05, .05, 3, 0, 1, 0, 0], | |
| [0.3, 0.3, 0, .05, .05, 3, 0, 1, 0, 0]]; | |
| const m = SC.eraseMask(obs, 0, 0, 0.12, null); | |
| T("brush circle membership", m[0] === true && m[1] === true && m[2] === false); | |
| const m2 = SC.eraseMask(obs, 0, 0, 0.12, i => i === 1); | |
| T("skip veto respected", m2[0] === true && m2[1] === false && m2[2] === false); | |
| } | |
| /* 5 erase -> undo round trip (order-free re-append) --------------------*/ | |
| { | |
| const obs = randAtoms(60); | |
| const before = C.renderPre(obs, H); | |
| const mask = SC.eraseMask(obs, 0.1, -0.1, 0.35, null); | |
| const removed = [], kept = []; | |
| for (let i = 0; i < obs.length; i++) (mask[i] ? removed : kept).push(obs[i]); | |
| T("stroke removed something and kept something", | |
| removed.length > 0 && kept.length > 0, removed.length + " erased / " + kept.length + " kept"); | |
| const mid = C.renderPre(kept, H); | |
| let changed = 0; for (let i = 0; i < mid.length; i++) if (mid[i] !== before[i]) changed++; | |
| T("erase changes the rendered field", changed > 0, changed + " px"); | |
| const after = C.renderPre(kept.concat(removed), H); // undo = append at the end | |
| let maxd = 0; for (let i = 0; i < after.length; i++) maxd = Math.max(maxd, Math.abs(after[i] - before[i])); | |
| T("undo restores the field exactly (order-free)", maxd < 1e-6, "max delta " + maxd.toExponential(1)); | |
| } | |
| /* 6 burn-to-template ----------------------------------------------------*/ | |
| { | |
| const template = randAtoms(30); | |
| // scene build records src by REFERENCE, like the studio does | |
| const src = template.map(ref => ({ li: 0, ref })); | |
| const layer = { template: template.slice() }; | |
| const mask = SC.eraseMask(template, 0, 0, 0.4, null); | |
| const burned = []; | |
| for (let i = 0; i < template.length; i++) if (mask[i]) { | |
| const idx = layer.template.indexOf(src[i].ref); | |
| if (idx >= 0) { layer.template.splice(idx, 1); burned.push(src[i]); } | |
| } | |
| const nGone = mask.filter(Boolean).length; | |
| T("burn removes exactly the erased refs", burned.length === nGone && | |
| layer.template.length === 30 - nGone, nGone + " burned"); | |
| T("burned refs really left the template", | |
| burned.every(b => !layer.template.includes(b.ref))); | |
| for (const b of burned) layer.template.push(b.ref); // undo | |
| T("undo restores template membership", layer.template.length === 30 && | |
| burned.every(b => layer.template.includes(b.ref))); | |
| } | |
| console.log("\n" + go + " GO, " + fail + " FAIL"); | |
| process.exit(fail ? 1 : 0); | |