Spaces:
Running
Running
File size: 6,588 Bytes
e38621f | 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 140 141 142 | /* 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
*/
"use strict";
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);
|