@descix/frqtl-node 0.6.0-p4.1 → 0.6.1-p1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CLAUDE.md +2 -2
  2. package/LICENSE.md +79 -63
  3. package/README.md +6 -3
  4. package/dist/dist-manifest.json +60 -54
  5. package/dist/fat/pkg-node/package.json +2 -10
  6. package/dist/frqtl/compat/providers/wasm-engine.js +3 -4
  7. package/dist/frqtl/compat/renderer/providers/p5-provider.js +1 -1
  8. package/dist/frqtl/objects/FrqtlApp.js +5 -5
  9. package/dist/frqtl/objects/RecipeContext.js +106 -1
  10. package/dist/frqtl/objects/constants/WallBehaviors.js +1 -1
  11. package/dist/frqtl/objects/instruments/capture.js +1 -24
  12. package/dist/frqtl/objects/instruments/hosted-eyes.js +17 -1
  13. package/dist/frqtl/objects/objects/makeControlsForObject.js +5 -5
  14. package/dist/frqtl/objects/record/Record.js +25 -5
  15. package/dist/frqtl/objects/schema/engine-scopes.js +2 -2
  16. package/dist/frqtl/objects/schema/scopes.js +2 -2
  17. package/dist/frqtl/objects/setup/_worldInit.js +1 -2
  18. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +1 -2
  19. package/dist/frqtl/parts/CameraControls.js +8 -0
  20. package/dist/frqtl/parts/DetectorWall.js +1 -1
  21. package/dist/frqtl/parts/GraphingDetectorWall.js +7 -7
  22. package/dist/frqtl/parts/LargeObject.js +2 -2
  23. package/dist/frqtl/parts/LiveChart.js +2 -2
  24. package/dist/frqtl/parts/LogicGate.js +3 -3
  25. package/dist/frqtl/parts/OvenBox.js +1 -1
  26. package/dist/frqtl/parts/PointSource.js +1 -1
  27. package/dist/frqtl/parts/SlitLightSource.js +1 -1
  28. package/dist/frqtl/parts/SlitScreen.js +1 -1
  29. package/dist/frqtl/parts/SphereEmitter.js +12 -12
  30. package/dist/frqtl/parts/Trace.js +1 -1
  31. package/dist/frqtl/parts/Tunnel.js +11 -11
  32. package/dist/frqtl/parts/Wall.js +20 -10
  33. package/dist/frqtl/scenes/circuit-sat.js +1 -0
  34. package/dist/frqtl/scenes/dse-one-emitter.js +32 -34
  35. package/dist/frqtl/scenes/dse-two-emitters.js +33 -34
  36. package/dist/frqtl/scenes/mirror-route.js +96 -79
  37. package/dist/shim/bundle/index.js +6 -0
  38. package/dist/shim/controls/index.js +1 -1
  39. package/dist/shim/engine/artifact-shell.js +1 -1
  40. package/dist/shim/engine/router.js +10 -17
  41. package/dist/shim/engine/site-graph.js +36 -0
  42. package/dist/shim/host/host-view.js +232 -0
  43. package/dist/shim/host/index.js +18 -0
  44. package/dist/shim/index.js +2 -4
  45. package/dist/shim/notebook/kernel.js +1 -1
  46. package/dist/shim/notebook/setup-builder.js +2 -2
  47. package/dist/shim/package.json +1 -0
  48. package/dist/shim/page-params/index.js +2 -30
  49. package/dist/shim/project/fork.js +293 -0
  50. package/dist/shim/project/identity.js +3 -1
  51. package/dist/shim/project/link.js +43 -3
  52. package/dist/shim/project/parts-writer.js +62 -5
  53. package/dist/shim/project/read-text.js +43 -0
  54. package/dist/shim/project/specifiers.js +107 -16
  55. package/dist/shim/registry/exhibits.js +16 -35
  56. package/dist/shim/registry/index.js +5 -2
  57. package/dist/shim/registry/scene-roster.js +35 -0
  58. package/dist/shim/registry/scene-source.js +6 -36
  59. package/dist/shim/registry/showcase-link.js +61 -0
  60. package/dist/shim/registry/walkthrough.js +2 -3
  61. package/dist/shim/render-state/apply.js +66 -31
  62. package/dist/shim/render-state/index.js +6 -2
  63. package/dist/shim/runtime/index.js +4 -4
  64. package/llms.txt +4 -3
  65. package/package.json +21 -2
  66. package/dist/shim/registry/experiments.js +0 -215
@@ -67,9 +67,10 @@
67
67
  // 2D ONLY, deliberately. There is no `modes` strip and no `exp.is3D` guard: these
68
68
  // rungs declare one world each. A second dimensional variant would be a separate
69
69
  // recipe declaration, not a mode.
70
- // THE PAIR. `dse-two-emitters.js` is the SAME apparatus with NSRC = 2. The two files differ ONLY in
71
- // the title lines and the `var NSRC` line — lib/frqtl/scenes/test/scene-files.test.mjs
72
- // fails if any other line diverges, so the A/B the pair exists to make stays an A/B.
70
+ // THE PAIR. `dse-two-emitters.js` is the SAME apparatus with TWO source entries. The two files differ ONLY in the
71
+ // title lines, this note, and their `source*` entries of `parts` (with the world call naming the first
72
+ // of them) — lib/frqtl/scenes/test/scene-files.test.mjs fails if anything else diverges, so the A/B the
73
+ // pair exists to make stays an A/B.
73
74
 
74
75
  import { SphereEmitter, Wall } from '@descix/frqtl-web/parts';
75
76
 
@@ -116,7 +117,34 @@ const detW = Math.round(lamPx / 64); // 5 — plate thickness
116
117
  // the photoelectric experiment, and it saturates.
117
118
  const detectorPreset = 'transparent-probe';
118
119
 
120
+ const lamArg = 3.2; // capacity/mass -> fullness 1/3.2 = 0.3125 (yellow)
121
+ const fwl = 4 * (lamPx / lamArg); // 400 -> wavelengthScale floor(400/4) = 100
122
+ const wlScale = Math.floor(fwl / 4); // 100
123
+ const cy = Math.round(H / 2); // 200 — the source plane's midline
124
+ // TWO-SOURCE SEPARATION. Two sources reinforce when their separation is a
125
+ // multiple of lambda/2, and the coupling falls as (lambda/d)^2, so lambda/2 is
126
+ // the strongest case. At this leg it also puts a band of EACH source on the
127
+ // midline y = H/2 (source 1's lower band and source 2's upper band coincide
128
+ // there) — the doubly-reinforced central peak, equidistant from both emitters,
129
+ // exactly where the one-source rung has no peak at all.
130
+ const sep = lamPx / 2; // 160 -> sources at y 120 and 280
131
+ // ── THE LIGHT SOURCE ────────────────────────────────────────────────────
132
+ // preset 'hemisphere' = 180 degrees of FORWARD (+X) emission: the SAME
133
+ // ball-rejected sampler as the omni source with the forward axis' range halved,
134
+ // so nothing about the walk changes, it is simply pointed at the plate. The
135
+ // default emission box is 0 for this preset, so the source is a single PIXEL —
136
+ // load-bearing, because any spread at birth blurs every band by that spread.
137
+ // NO START WALL: the backward population is empty by construction, and `wrap` is
138
+ // false, so the canvas edge kills whatever transverse wiggle out-runs its
139
+ // forward component. One sink, not two.
140
+ const SRC = {
141
+ preset: 'hemisphere', is2D: true, chargeMode: 'random',
142
+ wavelength: lamArg, fundamentalDimension: 2, fundamentalWaveLength: fwl,
143
+ burstSize: 8, emitEveryTicks: 1
144
+ };
145
+
119
146
  export const parts = {
147
+ source: [SphereEmitter, { ...SRC, center: [srcX, cy] }],
120
148
  // FAR-EDGE SINK. The plate below counts WITHOUT removing, so quanta fly on past
121
149
  // it; this is the experiment-boundary sink downstream of the plate. It is not
122
150
  // load-bearing (wrap is false, so the canvas edge would remove them anyway) — it
@@ -134,41 +162,11 @@ export const parts = {
134
162
  };
135
163
 
136
164
  export default function buildExperiment(exp) {
137
- var NSRC = 1;
138
- var lamArg = 3.2; // capacity/mass -> fullness 1/3.2 = 0.3125 (yellow)
139
- var fwl = 4 * (lamPx / lamArg); // 400 -> wavelengthScale floor(400/4) = 100
140
- var wlScale = Math.floor(fwl / 4); // 100
141
165
  if (Math.abs(lamArg * wlScale - lamPx) > 1e-9) {
142
166
  throw new Error('[dse-h1] lamPx ' + lamPx + ' is not realisable as lambda_eff: the engine ' +
143
167
  'can only produce wavelengthArg x floor(fwl/4) = ' + (lamArg * wlScale) + '. (FAIL-LOUD.)');
144
168
  }
145
- var cy = Math.round(H / 2); // 200 — the source plane's midline
146
- // TWO-SOURCE SEPARATION. Two sources reinforce when their separation is a
147
- // multiple of lambda/2, and the coupling falls as (lambda/d)^2, so lambda/2 is
148
- // the strongest case. At this leg it also puts a band of EACH source on the
149
- // midline y = H/2 (source 1's lower band and source 2's upper band coincide
150
- // there) — the doubly-reinforced central peak, equidistant from both emitters,
151
- // exactly where the one-source rung has no peak at all.
152
- var sep = lamPx / 2; // 160 -> sources at y 120 and 280
153
- var srcYs = (NSRC === 2) ? [cy - sep / 2, cy + sep / 2] : [cy];
154
- // ── THE LIGHT SOURCE ────────────────────────────────────────────────────
155
- // preset 'hemisphere' = 180 degrees of FORWARD (+X) emission: the SAME
156
- // ball-rejected sampler as the omni source with the forward axis' range halved,
157
- // so nothing about the walk changes, it is simply pointed at the plate. The
158
- // default emission box is 0 for this preset, so the source is a single PIXEL —
159
- // load-bearing, because any spread at birth blurs every band by that spread.
160
- // NO START WALL: the backward population is empty by construction, and `wrap` is
161
- // false, so the canvas edge kills whatever transverse wiggle out-runs its
162
- // forward component. One sink, not two.
163
- var SRC = {
164
- preset: 'hemisphere', is2D: true, chargeMode: 'random',
165
- wavelength: lamArg, fundamentalDimension: 2, fundamentalWaveLength: fwl,
166
- burstSize: 8, emitEveryTicks: 1
167
- };
168
- exp.createCanvas(CW, H, SphereEmitter.worldCfg(SRC)); // THE WORLD: a 2D canvas + universe — first, then parts
169
- for (var si = 0; si < srcYs.length; si++) {
170
- exp.addObject('SphereEmitter', Object.assign({}, SRC, { center: [srcX, Math.round(srcYs[si])] }));
171
- }
169
+ exp.createCanvas(CW, H, SphereEmitter.worldCfg(parts.source[1])); // THE WORLD: a 2D canvas + universe — first, then parts
172
170
  const p = exp.declare(parts);
173
171
  // ── THE INSTRUMENT — the live count-by-y histogram, REGISTERED TO THE PLATE ──
174
172
  // The panel takes the plate's OWN rect y-origin (0) and height (H), so the two
@@ -67,9 +67,10 @@
67
67
  // 2D ONLY, deliberately. There is no `modes` strip and no `exp.is3D` guard: these
68
68
  // rungs declare one world each. A second dimensional variant would be a separate
69
69
  // recipe declaration, not a mode.
70
- // THE PAIR. `dse-one-emitter.js` is the SAME apparatus with NSRC = 1. The two files differ ONLY in
71
- // the title lines and the `var NSRC` line — lib/frqtl/scenes/test/scene-files.test.mjs
72
- // fails if any other line diverges, so the A/B the pair exists to make stays an A/B.
70
+ // THE PAIR. `dse-one-emitter.js` is the SAME apparatus with ONE source entry. The two files differ ONLY in the
71
+ // title lines, this note, and their `source*` entries of `parts` (with the world call naming the first
72
+ // of them) — lib/frqtl/scenes/test/scene-files.test.mjs fails if anything else diverges, so the A/B the
73
+ // pair exists to make stays an A/B.
73
74
 
74
75
  import { SphereEmitter, Wall } from '@descix/frqtl-web/parts';
75
76
 
@@ -116,7 +117,35 @@ const detW = Math.round(lamPx / 64); // 5 — plate thickness
116
117
  // the photoelectric experiment, and it saturates.
117
118
  const detectorPreset = 'transparent-probe';
118
119
 
120
+ const lamArg = 3.2; // capacity/mass -> fullness 1/3.2 = 0.3125 (yellow)
121
+ const fwl = 4 * (lamPx / lamArg); // 400 -> wavelengthScale floor(400/4) = 100
122
+ const wlScale = Math.floor(fwl / 4); // 100
123
+ const cy = Math.round(H / 2); // 200 — the source plane's midline
124
+ // TWO-SOURCE SEPARATION. Two sources reinforce when their separation is a
125
+ // multiple of lambda/2, and the coupling falls as (lambda/d)^2, so lambda/2 is
126
+ // the strongest case. At this leg it also puts a band of EACH source on the
127
+ // midline y = H/2 (source 1's lower band and source 2's upper band coincide
128
+ // there) — the doubly-reinforced central peak, equidistant from both emitters,
129
+ // exactly where the one-source rung has no peak at all.
130
+ const sep = lamPx / 2; // 160 -> sources at y 120 and 280
131
+ // ── THE LIGHT SOURCE ────────────────────────────────────────────────────
132
+ // preset 'hemisphere' = 180 degrees of FORWARD (+X) emission: the SAME
133
+ // ball-rejected sampler as the omni source with the forward axis' range halved,
134
+ // so nothing about the walk changes, it is simply pointed at the plate. The
135
+ // default emission box is 0 for this preset, so the source is a single PIXEL —
136
+ // load-bearing, because any spread at birth blurs every band by that spread.
137
+ // NO START WALL: the backward population is empty by construction, and `wrap` is
138
+ // false, so the canvas edge kills whatever transverse wiggle out-runs its
139
+ // forward component. One sink, not two.
140
+ const SRC = {
141
+ preset: 'hemisphere', is2D: true, chargeMode: 'random',
142
+ wavelength: lamArg, fundamentalDimension: 2, fundamentalWaveLength: fwl,
143
+ burstSize: 8, emitEveryTicks: 1
144
+ };
145
+
119
146
  export const parts = {
147
+ source1: [SphereEmitter, { ...SRC, center: [srcX, Math.round(cy - sep / 2)] }],
148
+ source2: [SphereEmitter, { ...SRC, center: [srcX, Math.round(cy + sep / 2)] }],
120
149
  // FAR-EDGE SINK. The plate below counts WITHOUT removing, so quanta fly on past
121
150
  // it; this is the experiment-boundary sink downstream of the plate. It is not
122
151
  // load-bearing (wrap is false, so the canvas edge would remove them anyway) — it
@@ -134,41 +163,11 @@ export const parts = {
134
163
  };
135
164
 
136
165
  export default function buildExperiment(exp) {
137
- var NSRC = 2;
138
- var lamArg = 3.2; // capacity/mass -> fullness 1/3.2 = 0.3125 (yellow)
139
- var fwl = 4 * (lamPx / lamArg); // 400 -> wavelengthScale floor(400/4) = 100
140
- var wlScale = Math.floor(fwl / 4); // 100
141
166
  if (Math.abs(lamArg * wlScale - lamPx) > 1e-9) {
142
167
  throw new Error('[dse-h1] lamPx ' + lamPx + ' is not realisable as lambda_eff: the engine ' +
143
168
  'can only produce wavelengthArg x floor(fwl/4) = ' + (lamArg * wlScale) + '. (FAIL-LOUD.)');
144
169
  }
145
- var cy = Math.round(H / 2); // 200 — the source plane's midline
146
- // TWO-SOURCE SEPARATION. Two sources reinforce when their separation is a
147
- // multiple of lambda/2, and the coupling falls as (lambda/d)^2, so lambda/2 is
148
- // the strongest case. At this leg it also puts a band of EACH source on the
149
- // midline y = H/2 (source 1's lower band and source 2's upper band coincide
150
- // there) — the doubly-reinforced central peak, equidistant from both emitters,
151
- // exactly where the one-source rung has no peak at all.
152
- var sep = lamPx / 2; // 160 -> sources at y 120 and 280
153
- var srcYs = (NSRC === 2) ? [cy - sep / 2, cy + sep / 2] : [cy];
154
- // ── THE LIGHT SOURCE ────────────────────────────────────────────────────
155
- // preset 'hemisphere' = 180 degrees of FORWARD (+X) emission: the SAME
156
- // ball-rejected sampler as the omni source with the forward axis' range halved,
157
- // so nothing about the walk changes, it is simply pointed at the plate. The
158
- // default emission box is 0 for this preset, so the source is a single PIXEL —
159
- // load-bearing, because any spread at birth blurs every band by that spread.
160
- // NO START WALL: the backward population is empty by construction, and `wrap` is
161
- // false, so the canvas edge kills whatever transverse wiggle out-runs its
162
- // forward component. One sink, not two.
163
- var SRC = {
164
- preset: 'hemisphere', is2D: true, chargeMode: 'random',
165
- wavelength: lamArg, fundamentalDimension: 2, fundamentalWaveLength: fwl,
166
- burstSize: 8, emitEveryTicks: 1
167
- };
168
- exp.createCanvas(CW, H, SphereEmitter.worldCfg(SRC)); // THE WORLD: a 2D canvas + universe — first, then parts
169
- for (var si = 0; si < srcYs.length; si++) {
170
- exp.addObject('SphereEmitter', Object.assign({}, SRC, { center: [srcX, Math.round(srcYs[si])] }));
171
- }
170
+ exp.createCanvas(CW, H, SphereEmitter.worldCfg(parts.source1[1])); // THE WORLD: a 2D canvas + universe — first, then parts
172
171
  const p = exp.declare(parts);
173
172
  // ── THE INSTRUMENT — the live count-by-y histogram, REGISTERED TO THE PLATE ──
174
173
  // The panel takes the plate's OWN rect y-origin (0) and height (H), so the two
@@ -19,102 +19,119 @@
19
19
  // captured descriptor DATA) keeps the population bounded so FPS stays healthy, Restart-safe.
20
20
  //
21
21
  // ONE MODE-AWARE SOURCE. The mode strip selects the RENDERER the host boots
22
- // (renderState.renderer), and this scene reads it ONCE, before its world: `three` ⇒
22
+ // (renderState.renderer), and this scene reads it ONCE, inside buildExperiment: `three` ⇒
23
23
  // exp.createScene (a 600x400x600 world), else exp.createCanvas (600x400). The ring geometry,
24
- // tilts, colours, laser and gate are the SAME in both modes (2D≡3D); `IS3D` guards exactly
25
- // these deltas:
24
+ // tilts, colours, laser and gate are the SAME in both modes (2D≡3D). `parts` is written in the
25
+ // 3D default mode, and flat2D (below) derives the 2D block from it — the scene's own statement of
26
+ // two of its deltas:
26
27
  // (a) every mirror is a 3D SLAB — a z-slab centred on the beam plane z=0, rendered as a plate;
27
28
  // (b) the laser is VOLUMETRIC (is2D:false — it braids in y AND z; the slab depth catches the
28
- // z-braid; its tilted xy faces reflect by the 2θ law, its near/far z-faces bounce vz);
29
+ // z-braid; its tilted xy faces reflect by the 2θ law, its near/far z-faces bounce vz).
30
+ // `IS3D` guards the rest, in code after exp.declare:
29
31
  // (c) two invisible destroy z-collimators flush with the slab z-band;
30
- // (d) the population counter carries a z-slab covering the WHOLE universe z-band;
32
+ // (d) the population counter carries a z-slab covering the WHOLE universe z-band (the emitter's
33
+ // gateCounter names it, so it stays a code declaration);
31
34
  // (e) MOVE/LOOK camera joysticks (the OMNI_3D pattern).
32
35
  // The 3D mode's render intent — god-rays at the sun's settings, computed from the light
33
36
  // particles only (godraySource 'leaf'), and the 'table-top' camera — rides the registry mode's
34
37
  // renderState (lib/shim/registry/walkthrough.js), whose god-ray VALUES are the sun's, owned by
35
38
  // lib/shim/render/sun-look.js.
36
39
 
37
- import { CameraControls, SphereEmitter } from '@descix/frqtl-web/parts';
40
+ import { CameraControls, SphereEmitter, Wall } from '@descix/frqtl-web/parts';
38
41
  import * as FrqtlDisplay from '@descix/frqtl-web/compat/renderer/display-slot.js';
39
42
 
40
- export const parts = {};
43
+ const W = 600, H = 400, WORLD_D = 600; // THE WORLD this scene states (its co-init call below)
44
+ const f = Math.min(W, H) / 600;
45
+ // R 225·f: adjacent decagon vertices sit 2R·sin18° ≈ 0.618R apart vs slab
46
+ // height ~110·f, so the mirrors separate with clear gaps. Fit: R + rotated-slab half-diagonal
47
+ // (≈55·f) stays inside min(W,H)/2 with margin.
48
+ const CX = Math.round(W / 2), CY = Math.round(H / 2), R = Math.round(225 * f), HOP = 3, ROT = 12;
49
+ // CAP 6400 / burst 8 (brightness retune): a VERY BRIGHT braid. The cap is
50
+ // LIVE-controllable off the population counter (select 'population' in Layers → Inspector
51
+ // 'Population Gate').
52
+ const SW = Math.max(10, Math.round(14 * f)), SH = Math.max(70, Math.round(110 * f)), CAP = 6400;
53
+ // SD — the 3D slab z-depth: a 2:1 tile (twice the slab height, z-centred on the beam plane).
54
+ // The scene z-collimator clamps ALL quanta to this band, so every mirror provably faces every
55
+ // alive quantum.
56
+ const SD = 2 * SH;
57
+ // Each mirror a DIFFERENT colour = its identity (a red→magenta rainbow); the recolor hook
58
+ // stamps that colour onto every particle it reflects (string_map['render.color']).
59
+ const PAL = [[255,60,60],[255,140,0],[255,220,0],[150,230,40],[40,210,90],[0,210,190],[0,170,255],[70,90,255],[150,70,235],[235,50,190]];
60
+ // Hit-order vertices of the {10/3} star (m1..m10 named IN the order the beam visits them).
61
+ const order = []; for (var k = 0; k < 10; k++) { order.push((k * HOP) % 10); }
62
+ const D2R = Math.PI / 180;
63
+ const vpos = function (v) { var a = (v * 36 + ROT) * D2R; return [CX + R * Math.cos(a), CY + R * Math.sin(a)]; };
64
+ const pos = order.map(vpos);
65
+ const deg = function (a) { return ((a * 180 / Math.PI) % 360 + 360) % 360; };
66
+ const angOf = function (p, q) { return deg(Math.atan2(q[1] - p[1], q[0] - p[0])); };
67
+ // Per-mirror IN-PLANE orientation from the in/out chords: ori = (in+out−180)/2, branch chosen
68
+ // so the engine reflect law out = 180 + 2·ori − in reproduces the outgoing chord exactly.
69
+ // Orientation is z-axis rotation, so the 3D slab depth adds NO new angle.
70
+ const ORI = [];
71
+ for (var j = 0; j < 10; j++) {
72
+ var pr = pos[(j + 9) % 10], cu = pos[j], nx = pos[(j + 1) % 10];
73
+ var inA = angOf(pr, cu), outA = angOf(cu, nx);
74
+ var o = (((inA + outA - 180) / 2) % 360 + 360) % 360;
75
+ var op = ((180 + 2 * o - inA) % 360 + 360) % 360;
76
+ if (Math.abs(((op - outA + 540) % 360) - 180) > 0.5) { o = (o + 90) % 360; }
77
+ ORI.push(o);
78
+ }
79
+ // The NARROW STRAIGHT-WALK laser (the exemplar): the SAME emitter as Rung 1
80
+ // (Straight Walk) — preset 'particle-walk', every walk-character field INHERITED from the same
81
+ // factory defaults (chargeMode 'positive', dim 0 = 'light' P=1, fundamentalWaveLength 64 → the
82
+ // λ16 braided transverse ripple) — with ONLY the deliberate loop deltas: (a) a 2px source line
83
+ // (emissionBoxSide 2 vs r1's default 10), (b) seated on the m10→m1 leg JUST CLEAR of m10 (70·f
84
+ // past its vertex > the rotated-slab half-diagonal ≈55·f) and aimed along the leg (the longest
85
+ // clear throw: leg 1.618R ≈ 364·f), (c) a burst of 8/tick, and (d) the DECLARED emission gate.
86
+ const pS = pos[9], p0 = pos[0];
87
+ const dx = p0[0] - pS[0], dy = p0[1] - pS[1], L = Math.hypot(dx, dy), ux = dx / L, uy = dy / L;
88
+ const emit = [Math.round(pS[0] + 70 * f * ux), Math.round(pS[1] + 70 * f * uy)];
89
+ // EMISSION-CAP GATE as DECLARED DATA (the blackbody-thermostat pattern):
90
+ // emissionCap + gateCounter compile at CONSTRUCTION into a host tick-fn reading
91
+ // Wall.getLiveCount on the background counter named 'population' (placed below — the by-name
92
+ // reference resolves lazily on the first tick). The closed loop has NO sink, so the gate pauses
93
+ // emission at the cap and resumes below it: the population plateaus. Restart-safe: the parts
94
+ // writer keeps the two fields in the entry and the rebuilt emitter recompiles the gate (no free recipe tick-fn).
95
+ // is2D: false (the 3D default) opens activeAxes.z — the volumetric laser; flat2D writes it true.
96
+ // The parts, written in the 3D default mode (the mode strip's default): the laser, then the ten mirrors at the
97
+ // star vertices — each its own colour + the recolor tag, each a 3D SLAB (z-slab -SD/2..SD/2). 2D is derived from
98
+ // this block by flat2D below.
99
+ export const parts = {
100
+ source: [SphereEmitter, { preset: 'particle-walk', is2D: false, center: emit, orientationDeg: angOf(pS, p0), emissionBoxSide: 2, burstSize: 8, emissionCap: CAP, gateCounter: 'population' }],
101
+ m1: [Wall, { rect: [Math.round(pos[0][0] - SW / 2), Math.round(pos[0][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[0], color: PAL[0], hooks: ['recolor'] }],
102
+ m2: [Wall, { rect: [Math.round(pos[1][0] - SW / 2), Math.round(pos[1][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[1], color: PAL[1], hooks: ['recolor'] }],
103
+ m3: [Wall, { rect: [Math.round(pos[2][0] - SW / 2), Math.round(pos[2][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[2], color: PAL[2], hooks: ['recolor'] }],
104
+ m4: [Wall, { rect: [Math.round(pos[3][0] - SW / 2), Math.round(pos[3][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[3], color: PAL[3], hooks: ['recolor'] }],
105
+ m5: [Wall, { rect: [Math.round(pos[4][0] - SW / 2), Math.round(pos[4][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[4], color: PAL[4], hooks: ['recolor'] }],
106
+ m6: [Wall, { rect: [Math.round(pos[5][0] - SW / 2), Math.round(pos[5][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[5], color: PAL[5], hooks: ['recolor'] }],
107
+ m7: [Wall, { rect: [Math.round(pos[6][0] - SW / 2), Math.round(pos[6][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[6], color: PAL[6], hooks: ['recolor'] }],
108
+ m8: [Wall, { rect: [Math.round(pos[7][0] - SW / 2), Math.round(pos[7][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[7], color: PAL[7], hooks: ['recolor'] }],
109
+ m9: [Wall, { rect: [Math.round(pos[8][0] - SW / 2), Math.round(pos[8][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[8], color: PAL[8], hooks: ['recolor'] }],
110
+ m10: [Wall, { rect: [Math.round(pos[9][0] - SW / 2), Math.round(pos[9][1] - SH / 2), SW, SH], z: -Math.round(SD / 2), depth: SD, collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[9], color: PAL[9], hooks: ['recolor'] }],
111
+ };
112
+
113
+ /** This scene's 2D delta, stated once: the same keys in the same order, each props copied with its z-slab
114
+ * (z, depth) dropped and is2D written true where it is spelled. Pure; adds no key. */
115
+ function flat2D(parts) {
116
+ const out = {};
117
+ for (const key of Object.keys(parts)) {
118
+ const props = Object.assign({}, parts[key][1]);
119
+ delete props.z;
120
+ delete props.depth;
121
+ if ('is2D' in props) { props.is2D = true; }
122
+ out[key] = [parts[key][0], props];
123
+ }
124
+ return out;
125
+ }
41
126
 
42
127
  export default function buildExperiment(exp) {
43
128
  // The booted display (the host's renderState.renderer) decides which world this mode makes.
44
129
  var IS3D = FrqtlDisplay.name() === 'three';
45
- var W = 600, H = 400, WORLD_D = 600; // THE WORLD this scene states (its co-init call below)
46
- var f = Math.min(W, H) / 600;
47
- // R 225·f: adjacent decagon vertices sit 2R·sin18° ≈ 0.618R apart vs slab
48
- // height ~110·f, so the mirrors separate with clear gaps. Fit: R + rotated-slab half-diagonal
49
- // (≈55·f) stays inside min(W,H)/2 with margin.
50
- var CX = Math.round(W / 2), CY = Math.round(H / 2), R = Math.round(225 * f), HOP = 3, ROT = 12;
51
- // CAP 6400 / burst 8 (brightness retune): a VERY BRIGHT braid. The cap is
52
- // LIVE-controllable off the population counter (select 'population' in Layers → Inspector
53
- // 'Population Gate').
54
- var SW = Math.max(10, Math.round(14 * f)), SH = Math.max(70, Math.round(110 * f)), CAP = 6400;
55
- // SD — the 3D slab z-depth: a 2:1 tile (twice the slab height, z-centred on the beam plane).
56
- // The scene z-collimator clamps ALL quanta to this band, so every mirror provably faces every
57
- // alive quantum.
58
- var SD = 2 * SH;
59
- // Each mirror a DIFFERENT colour = its identity (a red→magenta rainbow); the recolor hook
60
- // stamps that colour onto every particle it reflects (string_map['render.color']).
61
- var PAL = [[255,60,60],[255,140,0],[255,220,0],[150,230,40],[40,210,90],[0,210,190],[0,170,255],[70,90,255],[150,70,235],[235,50,190]];
62
- // Hit-order vertices of the {10/3} star (m1..m10 named IN the order the beam visits them).
63
- var order = []; for (var k = 0; k < 10; k++) { order.push((k * HOP) % 10); }
64
- var D2R = Math.PI / 180;
65
- var vpos = function (v) { var a = (v * 36 + ROT) * D2R; return [CX + R * Math.cos(a), CY + R * Math.sin(a)]; };
66
- var pos = order.map(vpos);
67
- var deg = function (a) { return ((a * 180 / Math.PI) % 360 + 360) % 360; };
68
- var angOf = function (p, q) { return deg(Math.atan2(q[1] - p[1], q[0] - p[0])); };
69
- // Per-mirror IN-PLANE orientation from the in/out chords: ori = (in+out−180)/2, branch chosen
70
- // so the engine reflect law out = 180 + 2·ori − in reproduces the outgoing chord exactly.
71
- // Orientation is z-axis rotation, so the 3D slab depth adds NO new angle.
72
- var ORI = [];
73
- for (var j = 0; j < 10; j++) {
74
- var pr = pos[(j + 9) % 10], cu = pos[j], nx = pos[(j + 1) % 10];
75
- var inA = angOf(pr, cu), outA = angOf(cu, nx);
76
- var o = (((inA + outA - 180) / 2) % 360 + 360) % 360;
77
- var op = ((180 + 2 * o - inA) % 360 + 360) % 360;
78
- if (Math.abs(((op - outA + 540) % 360) - 180) > 0.5) { o = (o + 90) % 360; }
79
- ORI.push(o);
80
- }
81
- // The NARROW STRAIGHT-WALK laser (the exemplar): the SAME emitter as Rung 1
82
- // (Straight Walk) — preset 'particle-walk', every walk-character field INHERITED from the same
83
- // factory defaults (chargeMode 'positive', dim 0 = 'light' P=1, fundamentalWaveLength 64 → the
84
- // λ16 braided transverse ripple) — with ONLY the deliberate loop deltas: (a) a 2px source line
85
- // (emissionBoxSide 2 vs r1's default 10), (b) seated on the m10→m1 leg JUST CLEAR of m10 (70·f
86
- // past its vertex > the rotated-slab half-diagonal ≈55·f) and aimed along the leg (the longest
87
- // clear throw: leg 1.618R ≈ 364·f), (c) a burst of 8/tick, and (d) the DECLARED emission gate.
88
- var pS = pos[9], p0 = pos[0];
89
- var dx = p0[0] - pS[0], dy = p0[1] - pS[1], L = Math.hypot(dx, dy), ux = dx / L, uy = dy / L;
90
- var emit = [Math.round(pS[0] + 70 * f * ux), Math.round(pS[1] + 70 * f * uy)];
91
- // EMISSION-CAP GATE as DECLARED DATA (the blackbody-thermostat pattern):
92
- // emissionCap + gateCounter compile at CONSTRUCTION into a host tick-fn reading
93
- // Wall.getLiveCount on the background counter named 'population' (placed below — the by-name
94
- // reference resolves lazily on the first tick). The closed loop has NO sink, so the gate pauses
95
- // emission at the cap and resumes below it: the population plateaus. Restart-safe: codegen
96
- // captures the two fields and the rebuilt emitter recompiles the gate (no free recipe tick-fn).
97
- // is2D:!IS3D opens activeAxes.z in 3D — the volumetric laser.
98
- var SRC = {
99
- preset: 'particle-walk', is2D: !IS3D, center: emit, orientationDeg: angOf(pS, p0),
100
- emissionBoxSide: 2, burstSize: 8,
101
- emissionCap: CAP, gateCounter: 'population'
102
- };
130
+ var P = IS3D ? parts : flat2D(parts);
103
131
  // THE WORLD — first, then parts: a 3D scene + universe (600x400x600) or a 2D canvas + universe.
104
- if (IS3D) { exp.createScene(W, H, WORLD_D, SphereEmitter.worldCfg(SRC)); }
105
- else { exp.createCanvas(W, H, SphereEmitter.worldCfg(SRC)); }
106
- exp.declare(parts);
107
- exp.addObject('SphereEmitter', SRC);
108
- // The ten mirrors at the star vertices — each its own colour + the recolor tag. In 3D each is a
109
- // SLAB (z-slab −SD/2..SD/2); in 2D the bag is EMPTY, so the wall is the flat mirror.
110
- var ZSLAB = IS3D ? { z: -Math.round(SD / 2), depth: SD } : {};
111
- for (var i = 0; i < 10; i++) {
112
- var p = pos[i];
113
- exp.addObject('Wall', Object.assign(
114
- { rect: [Math.round(p[0] - SW / 2), Math.round(p[1] - SH / 2), SW, SH] }, ZSLAB,
115
- { collisionBehavior: 'bounce', bounceDirection: 'reflect', orientationDeg: ORI[i],
116
- name: 'm' + (i + 1), color: PAL[i], hooks: ['recolor'] }));
117
- }
132
+ if (IS3D) { exp.createScene(W, H, WORLD_D, SphereEmitter.worldCfg(P.source[1])); }
133
+ else { exp.createCanvas(W, H, SphereEmitter.worldCfg(P.source[1])); }
134
+ exp.declare(P);
118
135
  if (IS3D) {
119
136
  // SCENE Z-COLLIMATOR (without it the z lets things pass through instead of
120
137
  // reflecting): the legs' z-braid diffuses lap over lap; any quantum beyond the mirrors'
@@ -81,6 +81,12 @@ function _fromZip(bytes) {
81
81
  _fail('the archive carries ' + retired.join(' and ') + ': ' + RETIRED + '.');
82
82
  }
83
83
  if (names.indexOf(MANIFEST_FILE) === -1) {
84
+ var tops = names.map(function (n) { return n.split('/')[0]; }).filter(function (t, i, a) { return a.indexOf(t) === i; });
85
+ if (tops.length === 1 && names.indexOf(tops[0] + '/' + MANIFEST_FILE) !== -1 &&
86
+ names.every(function (n) { return n.indexOf(tops[0] + '/') === 0; })) {
87
+ _fail('the archive carries no ' + MANIFEST_FILE + ' at its root — the archive holds the folder `' + tops[0] + '/` rather ' +
88
+ 'than its contents — zip the folder\'s CONTENTS (' + MANIFEST_FILE + ' at the root), as an Export writes it.');
89
+ }
84
90
  _fail('the archive carries no ' + MANIFEST_FILE + ' at its root — a project archive is the folder zipped ' +
85
91
  '(frqtl.json + sketch.js + …), as an Export writes it.');
86
92
  }
@@ -101,7 +101,7 @@
101
101
  // action — a MOMENTARY verb: {id,label,action} where `action` is a member of
102
102
  // the closed ACTIONS vocabulary below. Stateless (no `value`, no
103
103
  // read-back) and serializable by NAME, so a declared array of them
104
- // round-trips through codegen with no new machinery. Actions are
104
+ // round-trips through the scene's text with no new machinery. Actions are
105
105
  // deliberately NOT expressed as a `bind` — a bind is a property
106
106
  // read/write pair, and `get()` of "restart" is meaningless.
107
107
  //
@@ -164,7 +164,7 @@ export function renderArtifactShellHtml(opts) {
164
164
  return (
165
165
  '<!DOCTYPE html>\n' +
166
166
  '<!-- Copyright 2023-2026 by Essam Abadir -->\n' +
167
- '<!-- FRQTL — claude.ai Artifact host shell (ws-artifact-showcase Bite A2).\n' +
167
+ '<!-- FRQTL — claude.ai Artifact host shell.\n' +
168
168
  ' Generated by lib/shim/engine/artifact-shell.js — DO NOT hand-edit.\n' +
169
169
  ' Paste this ENTIRE file into a claude.ai chat as an Artifact (type: HTML). -->\n' +
170
170
  '<html lang="en">\n' +
@@ -35,6 +35,10 @@
35
35
 
36
36
  'use strict';
37
37
 
38
+ // The static site's shape (graph dir, build stamp, build id, entry page) — owned by site-graph.js, imported from
39
+ // there by every consumer; the router uses them and does not re-export them.
40
+ import { SITE_GRAPH_DIR, SITE_BUILD_ATTR, SITE_BUILD_ID_RE, STATIC_APP_PAGE } from './site-graph.js';
41
+
38
42
  // ── Path profiles ────────────────────────────────────────────────────────────
39
43
  //
40
44
  // Each profile maps a frqtl-relative prefix → URL prefix.
@@ -148,7 +152,7 @@ var DIST_PROFILE = buildDistProfile('../..');
148
152
  // lib/shim/engine/test/router.test.mjs asserts them against the two
149
153
  // package.json#version fields so a forgotten bump fails `make verify`, not a
150
154
  // silent stale-CDN-URL bug.
151
- var CDN_SDK_VERSION = '0.6.0-p4.1'; // sdk/frqtl-web/package.json#version (the ONE version owner) — bound by router.test.mjs
155
+ var CDN_SDK_VERSION = '0.6.1-p1.2'; // sdk/frqtl-web/package.json#version (the ONE version owner) — bound by router.test.mjs
152
156
  var CDN_MATH_SDK_VERSION = '0.3.2'; // sdk/egpt-math-sdk/package.json#version — bound by router.test.mjs; bumped with each npm publish
153
157
 
154
158
  /**
@@ -302,15 +306,9 @@ function buildStaticProfile(graphBase, entryBase) {
302
306
  };
303
307
  }
304
308
 
305
- // The static site's sdk directory, relative to the site's document. The ONLY place it is
306
- // spelled; the entries sit directly in it, the graph under its build-stamped child.
307
- var STATIC_SDK_DIR = './pub';
308
-
309
- // The <html> attribute carrying the static site's build id — the cross-repo contract
310
- // (egpt-frqtl scripts/refresh-site.sh reads it). Every reader and writer names it through here.
311
- var SITE_BUILD_ATTR = 'data-frqtl-site-build';
312
- // A build id: the 8-hex-digit short sha of the commit the bundle was built from.
313
- var SITE_BUILD_ID_RE = /^[0-9a-f]{8}$/;
309
+ // The static site's sdk directory, relative to the site's document (site-graph.js::SITE_GRAPH_DIR);
310
+ // the entries sit directly in it, the graph under its build-stamped child.
311
+ var STATIC_SDK_DIR = './' + SITE_GRAPH_DIR;
314
312
 
315
313
  /**
316
314
  * The validated build id, or a thrown error naming what was supplied.
@@ -381,11 +379,6 @@ function documentSiteBuild() {
381
379
  return assertSiteBuildId(attr);
382
380
  }
383
381
 
384
- // The static IDE bundle's entry document, beside its front page. The bundle ships THIS name and no
385
- // index.html: a site's index.html is the publisher's front page (host-view.js::HOST_FRONT_PAGE),
386
- // which the IDE brand links home to.
387
- var STATIC_APP_PAGE = 'app.html';
388
-
389
382
  // The static IDE bundle's own chunk directory (vite assetsDir), relative to the document. Not
390
383
  // `assets/`: the DeSciX platform routes `/assets/` on an app subdomain to app MEDIA, not the site.
391
384
  var STATIC_APP_ASSETS_DIR = 'app-assets';
@@ -507,7 +500,7 @@ var _assertProfileParity = (function () {
507
500
  * // CDN mode (static hosting — the artifact host shell has no server, no page anchor):
508
501
  * const cdnRouter = engineRouter({ mode: 'cdn', engine: 'wasm' });
509
502
  * console.log(cdnRouter.resolve('compat/engine.js'));
510
- * // 'https://cdn.jsdelivr.net/npm/@descix/frqtl-web@0.6.0-p4.1/dist/frqtl/compat/engine.js'
503
+ * // 'https://cdn.jsdelivr.net/npm/@descix/frqtl-web@0.6.1-p1.2/dist/frqtl/compat/engine.js'
511
504
  *
512
505
  * // CDN mode, self-hosted: a copy of dist/ served from the consumer's own origin:
513
506
  * const selfHosted = engineRouter({ mode: 'cdn', engine: 'wasm', base: 'https://example.org/vendor/frqtl-web/dist' });
@@ -687,4 +680,4 @@ export function engineRouter(opts) {
687
680
 
688
681
 
689
682
  // Export profiles for the sdk build to consume.
690
- export { LIB_IDENTITY_PREFIXES, DEV_CORE_PROFILE, DIST_PROFILE, CDN_PROFILE, STATIC_SDK_DIR, STATIC_APP_PAGE, STATIC_APP_ASSETS_DIR, STATIC_HOST_VIEW_ENTRY, SITE_BUILD_ATTR, SITE_BUILD_ID_RE, CDN_SDK_VERSION, CDN_MATH_SDK_VERSION, CDN_BASE, cdnDistBaseFor, CDN_MATH_BASE, buildCdnProfile, buildDistProfile, buildStaticProfile, staticGraphDir, staticSdkBase, staticEntryBase };
683
+ export { LIB_IDENTITY_PREFIXES, DEV_CORE_PROFILE, DIST_PROFILE, CDN_PROFILE, STATIC_SDK_DIR, STATIC_APP_ASSETS_DIR, STATIC_HOST_VIEW_ENTRY, CDN_SDK_VERSION, CDN_MATH_SDK_VERSION, CDN_BASE, cdnDistBaseFor, CDN_MATH_BASE, buildCdnProfile, buildDistProfile, buildStaticProfile, staticGraphDir, staticSdkBase, staticEntryBase };
@@ -0,0 +1,36 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * site-graph.js — the shape of a static FRQTL site: the names a site's documents carry and the
5
+ * directory its build-stamped graph sits in. The ONE owner and the ONE import door of each: lib/shim/engine/router.js
6
+ * and every other consumer import them from here (the router never re-exports them), and a node consumer that reads a SERVED
7
+ * site (a publisher's gate) imports them through the package export `@descix/frqtl-web/site-graph`
8
+ * (or `@descix/frqtl-node/site-graph`), never typing them.
9
+ *
10
+ * SITE_GRAPH_DIR 'pub' — the site directory holding the fixed entries and, under
11
+ * SITE_GRAPH_DIR/<build>/, the build-stamped module graph
12
+ * SITE_BUILD_ATTR the <html> attribute of the IDE document carrying <build>
13
+ * SITE_BUILD_ID_RE a <build>: the 8-hex-digit short sha of the commit the bundle was built from
14
+ * STATIC_APP_PAGE the IDE bundle's entry document, beside the publisher's front page
15
+ * SITE_GRAPH_EXPORTS the published names (the parity gate holds both packages' ./site-graph to this table)
16
+ *
17
+ * Host-neutral ESM with no imports: it loads unchanged in node and in a page.
18
+ */
19
+
20
+ /** The names `./site-graph` publishes. */
21
+ export const SITE_GRAPH_EXPORTS = Object.freeze(['SITE_GRAPH_DIR', 'SITE_BUILD_ATTR', 'SITE_BUILD_ID_RE', 'STATIC_APP_PAGE']);
22
+
23
+ /** The static site's graph directory name (the entries sit in it, the graph under its build-stamped child). */
24
+ export const SITE_GRAPH_DIR = 'pub';
25
+
26
+ /** The <html> attribute carrying the static site's build id — the cross-repo contract. */
27
+ export const SITE_BUILD_ATTR = 'data-frqtl-site-build';
28
+
29
+ /** A build id: the 8-hex-digit short sha of the commit the bundle was built from. */
30
+ export const SITE_BUILD_ID_RE = /^[0-9a-f]{8}$/;
31
+
32
+ /**
33
+ * The static IDE bundle's entry document, beside its front page. The bundle ships THIS name and no
34
+ * index.html: a site's index.html is the publisher's front page (lib/shim/host/host-view.js::HOST_FRONT_PAGE).
35
+ */
36
+ export const STATIC_APP_PAGE = 'app.html';