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);