@descix/frqtl-node 0.6.0-p4.1 → 0.6.0

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 (63) hide show
  1. package/CLAUDE.md +2 -2
  2. package/README.md +5 -3
  3. package/dist/dist-manifest.json +58 -54
  4. package/dist/fat/pkg-node/package.json +2 -10
  5. package/dist/frqtl/compat/providers/wasm-engine.js +3 -4
  6. package/dist/frqtl/compat/renderer/providers/p5-provider.js +1 -1
  7. package/dist/frqtl/objects/FrqtlApp.js +5 -5
  8. package/dist/frqtl/objects/RecipeContext.js +106 -1
  9. package/dist/frqtl/objects/constants/WallBehaviors.js +1 -1
  10. package/dist/frqtl/objects/instruments/capture.js +1 -24
  11. package/dist/frqtl/objects/instruments/hosted-eyes.js +17 -1
  12. package/dist/frqtl/objects/objects/makeControlsForObject.js +5 -5
  13. package/dist/frqtl/objects/record/Record.js +25 -5
  14. package/dist/frqtl/objects/schema/engine-scopes.js +2 -2
  15. package/dist/frqtl/objects/schema/scopes.js +2 -2
  16. package/dist/frqtl/objects/setup/_worldInit.js +1 -2
  17. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +1 -2
  18. package/dist/frqtl/parts/CameraControls.js +8 -0
  19. package/dist/frqtl/parts/DetectorWall.js +1 -1
  20. package/dist/frqtl/parts/GraphingDetectorWall.js +7 -7
  21. package/dist/frqtl/parts/LargeObject.js +2 -2
  22. package/dist/frqtl/parts/LiveChart.js +2 -2
  23. package/dist/frqtl/parts/LogicGate.js +3 -3
  24. package/dist/frqtl/parts/OvenBox.js +1 -1
  25. package/dist/frqtl/parts/PointSource.js +1 -1
  26. package/dist/frqtl/parts/SlitLightSource.js +1 -1
  27. package/dist/frqtl/parts/SlitScreen.js +1 -1
  28. package/dist/frqtl/parts/SphereEmitter.js +12 -12
  29. package/dist/frqtl/parts/Trace.js +1 -1
  30. package/dist/frqtl/parts/Tunnel.js +11 -11
  31. package/dist/frqtl/parts/Wall.js +20 -10
  32. package/dist/frqtl/scenes/circuit-sat.js +1 -0
  33. package/dist/frqtl/scenes/dse-one-emitter.js +32 -34
  34. package/dist/frqtl/scenes/dse-two-emitters.js +33 -34
  35. package/dist/frqtl/scenes/mirror-route.js +96 -79
  36. package/dist/shim/bundle/index.js +6 -0
  37. package/dist/shim/controls/index.js +1 -1
  38. package/dist/shim/engine/artifact-shell.js +1 -1
  39. package/dist/shim/engine/router.js +3 -3
  40. package/dist/shim/host/host-view.js +228 -0
  41. package/dist/shim/host/index.js +18 -0
  42. package/dist/shim/index.js +2 -4
  43. package/dist/shim/notebook/kernel.js +1 -1
  44. package/dist/shim/notebook/setup-builder.js +2 -2
  45. package/dist/shim/package.json +1 -0
  46. package/dist/shim/page-params/index.js +2 -30
  47. package/dist/shim/project/fork.js +293 -0
  48. package/dist/shim/project/identity.js +3 -1
  49. package/dist/shim/project/link.js +43 -3
  50. package/dist/shim/project/parts-writer.js +62 -5
  51. package/dist/shim/project/read-text.js +43 -0
  52. package/dist/shim/project/specifiers.js +104 -15
  53. package/dist/shim/registry/exhibits.js +15 -34
  54. package/dist/shim/registry/index.js +2 -2
  55. package/dist/shim/registry/scene-roster.js +35 -0
  56. package/dist/shim/registry/scene-source.js +6 -36
  57. package/dist/shim/registry/walkthrough.js +2 -3
  58. package/dist/shim/render-state/apply.js +66 -31
  59. package/dist/shim/render-state/index.js +6 -2
  60. package/dist/shim/runtime/index.js +4 -4
  61. package/llms.txt +4 -3
  62. package/package.json +19 -1
  63. package/dist/shim/registry/experiments.js +0 -215
@@ -178,10 +178,20 @@ import { wallBehaviorV2ValuesMap as _wallBehaviorV2ValuesMap, bounceDirectionVal
178
178
  }
179
179
 
180
180
  function _rectFromOpts(opts) {
181
- // Normalize an OBJECT-bag rect ({left,top,w,h} / {left,top,z,w,h,d} — the shape a
182
- // codegen round-trip captures from the live LO, restart-bug fix 2026-07-25) to the
183
- // canonical ARRAY form FIRST, so the 3D checks below (_rectIs3D/_rectDepth are
184
- // Array-based) see a captured 3D wall as 3D. Mirrors LargeObject's own rectToArray.
181
+ // ONE spelling of the z-slab: a rect that already carries z/depth (the 6-element form, or an
182
+ // object rect with z/d) beside the z / d / depth props would be two statements of one fact,
183
+ // one of them silently ignored. Refused, naming both.
184
+ var slabProps = ['z', 'd', 'depth'].filter(function (k) { return opts[k] !== undefined; });
185
+ if (slabProps.length && opts.rect && (Array.isArray(opts.rect) ? opts.rect.length === 6
186
+ : (opts.rect.z !== undefined || opts.rect.d !== undefined))) {
187
+ throw new Error('[Wall] rect is already the z/depth form ' + ValueNormalizer.rectTupleText(6) +
188
+ ' and the props also give ' + slabProps.join(', ') + ' — write the z-slab ONE way: the 6-element rect, ' +
189
+ 'or a 4-element rect beside z/depth. (FAIL-LOUD.)');
190
+ }
191
+ // Normalize an OBJECT-bag rect ({left,top,w,h} / {left,top,z,w,h,d} — the shape a live LO's
192
+ // rect reads as) to the canonical ARRAY form FIRST, so the 3D checks below
193
+ // (_rectIs3D/_rectDepth are Array-based) see such a 3D wall as 3D. Mirrors LargeObject's
194
+ // own rectToArray.
185
195
  if (opts.rect && !Array.isArray(opts.rect) && opts.rect.left !== undefined && opts.rect.w !== undefined) {
186
196
  var ob = opts.rect;
187
197
  var arr = (ob.z !== undefined || ob.d !== undefined)
@@ -382,7 +392,7 @@ import { wallBehaviorV2ValuesMap as _wallBehaviorV2ValuesMap, bounceDirectionVal
382
392
  self._radiationTickFn = tickFn;
383
393
  // The radiator OWNS the live emissivity (including the default when none was
384
394
  // stated). Read the facade prop back off it rather than leaving it undefined —
385
- // captureProps/codegen and the Inspector read `wall.emissivity`, and a facade that
395
+ // captureProps (the parts writer) and the Inspector read `wall.emissivity`, and a facade that
386
396
  // says nothing while the radiator says 0.5 is a second, wrong answer.
387
397
  self.emissivity = tickFn.emissivity();
388
398
  if (universe && Array.isArray(universe.experimentTickFunctions)) {
@@ -399,10 +409,10 @@ import { wallBehaviorV2ValuesMap as _wallBehaviorV2ValuesMap, bounceDirectionVal
399
409
  function Wall(universe, opts) {
400
410
  opts = opts || {};
401
411
  this.universe = universe;
402
- this._schemaKind = 'Wall';
412
+ this._schemaKind = this.constructor.name;
403
413
 
404
- // A `preset` expands to the v2 descriptor fields (construction sugar; codegen
405
- // round-trips the EXPANDED fields, never the preset name).
414
+ // A `preset` expands to the v2 descriptor fields (construction sugar; the parts
415
+ // writer captures the EXPANDED fields, never the preset name).
406
416
  if (opts.preset) {
407
417
  var expanded = expandWallPreset(opts.preset);
408
418
  var merged = {}; Object.keys(opts).forEach(function (k) { if (k !== 'preset') { merged[k] = opts[k]; } });
@@ -437,7 +447,7 @@ import { wallBehaviorV2ValuesMap as _wallBehaviorV2ValuesMap, bounceDirectionVal
437
447
  // ── Descriptor-field read surface (captureProps / Inspector) ─────────────
438
448
  // captureProps(wall, WallDescriptor) reads wall[field.name] directly; these
439
449
  // getters expose the LIVE inner-LO state so a dragged/edited wall Saves at its
440
- // current state (the codegen round-trip authority). `rect` is the single
450
+ // current state (what the parts writer captures). `rect` is the single
441
451
  // captured position authority (the LO's own Rectangle, whose toJSON pins the
442
452
  // {left,top,w,h} wire shape).
443
453
  Object.defineProperty(Wall.prototype, 'rect', { get: function () { return this.lo ? this.lo.rect : undefined; } });
@@ -573,7 +583,7 @@ import { wallBehaviorV2ValuesMap as _wallBehaviorV2ValuesMap, bounceDirectionVal
573
583
  */
574
584
  Wall.prototype.setV2Prop = function (prop, value) {
575
585
  // EMISSIVITY — live radiating efficiency. The running radiator OWNS the value (one
576
- // live copy); the facade prop is a ferried mirror for the Inspector/codegen read.
586
+ // live copy); the facade prop is a ferried mirror for the Inspector and capture read.
577
587
  // Validation (fullness fraction in (0,1]) belongs to the owner and throws from there.
578
588
  if (prop === 'emissivity') {
579
589
  if (this.collisionBehavior !== 'absorb' || !this._radiationTickFn) {
@@ -22,6 +22,7 @@
22
22
  import { FrqtlControlOverlay, FrqtlControlsPanel, LogicGate, SphereEmitter, Trace } from '@descix/frqtl-web/parts';
23
23
  import * as ControlRegistry from '@descix/frqtl-web/shim/controls/registry.js';
24
24
 
25
+ // Code-declared on purpose: the emitter centres and rail traces derive from the gates' port geometry and RAIL (a knob rewrites it), and a Trace has no kind — a literal block would restate derived geometry by hand.
25
26
  export const parts = {};
26
27
 
27
28
  export default function buildExperiment(exp) {
@@ -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' +
@@ -148,7 +148,7 @@ var DIST_PROFILE = buildDistProfile('../..');
148
148
  // lib/shim/engine/test/router.test.mjs asserts them against the two
149
149
  // package.json#version fields so a forgotten bump fails `make verify`, not a
150
150
  // 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
151
+ var CDN_SDK_VERSION = '0.6.0'; // sdk/frqtl-web/package.json#version (the ONE version owner) — bound by router.test.mjs
152
152
  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
153
 
154
154
  /**
@@ -382,7 +382,7 @@ function documentSiteBuild() {
382
382
  }
383
383
 
384
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),
385
+ // index.html: a site's index.html is the publisher's front page (lib/shim/host/host-view.js::HOST_FRONT_PAGE),
386
386
  // which the IDE brand links home to.
387
387
  var STATIC_APP_PAGE = 'app.html';
388
388
 
@@ -507,7 +507,7 @@ var _assertProfileParity = (function () {
507
507
  * // CDN mode (static hosting — the artifact host shell has no server, no page anchor):
508
508
  * const cdnRouter = engineRouter({ mode: 'cdn', engine: 'wasm' });
509
509
  * 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'
510
+ * // 'https://cdn.jsdelivr.net/npm/@descix/frqtl-web@0.6.0/dist/frqtl/compat/engine.js'
511
511
  *
512
512
  * // CDN mode, self-hosted: a copy of dist/ served from the consumer's own origin:
513
513
  * const selfHosted = engineRouter({ mode: 'cdn', engine: 'wasm', base: 'https://example.org/vendor/frqtl-web/dist' });