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

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 (58) hide show
  1. package/CLAUDE.md +2 -2
  2. package/README.md +7 -3
  3. package/dist/dist-manifest.json +54 -48
  4. package/dist/frqtl/assets/vendor/acorn.LICENSE +21 -0
  5. package/dist/frqtl/assets/vendor/acorn.PINNED.json +9 -0
  6. package/dist/frqtl/assets/vendor/acorn.mjs +6266 -0
  7. package/dist/frqtl/compat/renderer/lo-drawn-center.js +41 -0
  8. package/dist/frqtl/experiments/_shared/boot-failure.js +60 -0
  9. package/dist/frqtl/objects/FrqtlApp.js +109 -30
  10. package/dist/frqtl/objects/ObjectFactory.js +4 -5
  11. package/dist/frqtl/objects/RecipeContext.js +451 -9
  12. package/dist/frqtl/objects/instruments/hosted-eyes.js +68 -28
  13. package/dist/frqtl/objects/objects/QuantumEmitter.js +1 -1
  14. package/dist/frqtl/objects/schema/index.js +72 -0
  15. package/dist/frqtl/parts/DetectorWall.js +1 -1
  16. package/dist/frqtl/parts/FrqtlControlsPanel.js +4 -2
  17. package/dist/frqtl/parts/Wall.js +2 -0
  18. package/dist/frqtl/scenes/absorbing-wall-3d.js +18 -14
  19. package/dist/frqtl/scenes/absorbing-wall.js +18 -14
  20. package/dist/frqtl/scenes/atomic-model.js +3 -0
  21. package/dist/frqtl/scenes/big-bang.js +3 -0
  22. package/dist/frqtl/scenes/blackbody.js +14 -11
  23. package/dist/frqtl/scenes/circuit-sat.js +7 -4
  24. package/dist/frqtl/scenes/collision-matrix-3d.js +3 -0
  25. package/dist/frqtl/scenes/counting-wall-3d.js +19 -15
  26. package/dist/frqtl/scenes/counting-wall.js +19 -15
  27. package/dist/frqtl/scenes/double-slit.js +3 -0
  28. package/dist/frqtl/scenes/dse-one-emitter.js +64 -58
  29. package/dist/frqtl/scenes/dse-one-slit.js +59 -55
  30. package/dist/frqtl/scenes/dse-photoelectric.js +28 -24
  31. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +124 -99
  32. package/dist/frqtl/scenes/dse-two-emitters.js +64 -58
  33. package/dist/frqtl/scenes/dse-two-slits.js +69 -65
  34. package/dist/frqtl/scenes/empty.js +3 -0
  35. package/dist/frqtl/scenes/laser-buildup.js +23 -16
  36. package/dist/frqtl/scenes/mirror-3d.js +31 -27
  37. package/dist/frqtl/scenes/mirror-route.js +4 -1
  38. package/dist/frqtl/scenes/mirror.js +24 -20
  39. package/dist/frqtl/scenes/omni-3d.js +7 -4
  40. package/dist/frqtl/scenes/omni-source.js +7 -4
  41. package/dist/frqtl/scenes/self-interference.js +13 -10
  42. package/dist/frqtl/scenes/smoking-gun.js +15 -11
  43. package/dist/frqtl/scenes/straight-walk.js +7 -4
  44. package/dist/frqtl/scenes/sun.js +11 -8
  45. package/dist/frqtl/scenes/two-source.js +22 -17
  46. package/dist/frqtl/scenes/wave.js +10 -7
  47. package/dist/shim/bundle/manifest.js +1 -1
  48. package/dist/shim/engine/router.js +2 -2
  49. package/dist/shim/page-params/index.js +2 -2
  50. package/dist/shim/project/index.js +7 -5
  51. package/dist/shim/project/link.js +12 -5
  52. package/dist/shim/project/parts-module.js +67 -0
  53. package/dist/shim/project/parts-writer.js +453 -0
  54. package/dist/shim/project/specifiers.js +41 -13
  55. package/dist/shim/registry/walkthrough.js +9 -9
  56. package/dist/shim/render-state/apply.js +11 -14
  57. package/llms.txt +2 -2
  58. package/package.json +1 -1
@@ -5,31 +5,35 @@
5
5
  *
6
6
  * A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world
7
7
  * (its co-init call, `exp.createCanvas` / `exp.createScene` — the one place its size is written),
8
- * the parts (`exp.addObject`) and the render intent are all here; the sugar
8
+ * the parts (`export const parts` + `exp.declare`, or `exp.addObject`) and the render intent are all here; the sugar
9
9
  * objects, renderer and engine are imports. Registered by lib/shim/registry/walkthrough.js
10
10
  * (rung cl1c-counting-wall-3d).
11
11
  */
12
12
 
13
- import { SphereEmitter } from '@descix/frqtl-web/parts';
13
+ import { SphereEmitter, Wall } from '@descix/frqtl-web/parts';
14
14
 
15
- export default function buildExperiment(exp) {
16
- // Collision Lab 1C · 3D — a volumetric source into a 3D Detector Screen slab
17
- // (destroy+count) + a LIVE in-scene chart, textured in three; orbit to view.
18
- var W = 600, H = 400; // THE WORLD this scene states (its co-init call below)
19
- var cy = Math.round(H / 2);
20
- var SRC = {
15
+ // Collision Lab 1C · 3D — a volumetric source into a 3D Detector Screen slab
16
+ // (destroy+count) + a LIVE in-scene chart, textured in three; orbit to view.
17
+ const W = 600, H = 400; // THE WORLD this scene states (its co-init call below)
18
+ const cy = Math.round(H / 2);
19
+
20
+ export const parts = {
21
+ source: [SphereEmitter, {
21
22
  preset: 'spherical', is2D: false, center: [Math.round(W * 0.15), cy],
22
23
  chargeMode: 'random', fundamentalDimension: 2, wavelength: 32,
23
24
  burstSize: 24, emitEveryTicks: 1
24
- };
25
- exp.createScene(W, H, 600, SphereEmitter.worldCfg(SRC)); // THE WORLD: a 3D scene + universe, 600x400x600 — first, then parts
26
- exp.addObject('SphereEmitter', SRC);
27
- var screen = exp.addObject('Wall', {
25
+ }],
26
+ screen: [Wall, {
28
27
  rect: [Math.round(W * 0.6), 0, 20, H], z: -80, depth: 160,
29
- preset: 'detector-screen', name: 'screen', color: [120, 200, 120]
30
- });
28
+ preset: 'detector-screen', color: [120, 200, 120]
29
+ }],
30
+ };
31
+
32
+ export default function buildExperiment(exp) {
33
+ exp.createScene(W, H, 600, SphereEmitter.worldCfg(parts.source[1])); // THE WORLD: a 3D scene + universe, 600x400x600 — first, then parts
34
+ const p = exp.declare(parts);
31
35
  exp.addObject('LiveChart', {
32
- source: screen,
36
+ source: p.screen,
33
37
  rect: [Math.round(W * 0.66), Math.round(H * 0.08), Math.round(W * 0.28), Math.round(H * 0.5)],
34
38
  series: 'bar', label: 'arrivals by y', refreshMs: 100, color: [24, 32, 46]
35
39
  });
@@ -5,33 +5,37 @@
5
5
  *
6
6
  * A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world
7
7
  * (its co-init call, `exp.createCanvas` / `exp.createScene` — the one place its size is written),
8
- * the parts (`exp.addObject`) and the render intent are all here; the sugar
8
+ * the parts (`export const parts` + `exp.declare`, or `exp.addObject`) and the render intent are all here; the sugar
9
9
  * objects, renderer and engine are imports. Registered by lib/shim/registry/walkthrough.js
10
10
  * (rung cl1c-counting-wall).
11
11
  */
12
12
 
13
- import { SphereEmitter } from '@descix/frqtl-web/parts';
13
+ import { SphereEmitter, Wall } from '@descix/frqtl-web/parts';
14
14
 
15
- export default function buildExperiment(exp) {
16
- // Collision Lab 1C (Counting Wall) — the Detector Screen preset (destroy + count),
17
- // with a LIVE in-scene LiveChart of the running y-histogram placed beside it.
18
- var W = 600, H = 400; // THE WORLD this scene states (its co-init call below)
19
- var cy = Math.round(H / 2);
20
- var SRC = {
15
+ // Collision Lab 1C (Counting Wall) — the Detector Screen preset (destroy + count),
16
+ // with a LIVE in-scene LiveChart of the running y-histogram placed beside it.
17
+ const W = 600, H = 400; // THE WORLD this scene states (its co-init call below)
18
+ const cy = Math.round(H / 2);
19
+
20
+ export const parts = {
21
+ source: [SphereEmitter, {
21
22
  preset: 'spherical', is2D: true, center: [Math.round(W * 0.15), cy],
22
23
  chargeMode: 'random', fundamentalDimension: 2, wavelength: 32,
23
24
  burstSize: 24, emitEveryTicks: 1
24
- };
25
- exp.createCanvas(W, H, SphereEmitter.worldCfg(SRC)); // THE WORLD: a 2D canvas + universe — first, then parts
26
- exp.addObject('SphereEmitter', SRC);
27
- var screen = exp.addObject('Wall', {
25
+ }],
26
+ screen: [Wall, {
28
27
  rect: [Math.round(W * 0.6), 0, 20, H],
29
- preset: 'detector-screen', name: 'screen', color: [120, 200, 120]
30
- });
28
+ preset: 'detector-screen', color: [120, 200, 120]
29
+ }],
30
+ };
31
+
32
+ export default function buildExperiment(exp) {
33
+ exp.createCanvas(W, H, SphereEmitter.worldCfg(parts.source[1])); // THE WORLD: a 2D canvas + universe — first, then parts
34
+ const p = exp.declare(parts);
31
35
  // The interactive counting demo: a live chart of the detector's histogram,
32
36
  // placed to the RIGHT of the screen (past where the quanta are absorbed).
33
37
  exp.addObject('LiveChart', {
34
- source: screen,
38
+ source: p.screen,
35
39
  rect: [Math.round(W * 0.66), Math.round(H * 0.08), Math.round(W * 0.28), Math.round(H * 0.5)],
36
40
  series: 'bar', label: 'arrivals by y', refreshMs: 100, color: [24, 32, 46]
37
41
  });
@@ -17,10 +17,13 @@
17
17
 
18
18
  import { CollisionTypes, DetectorWall, LargeObject, SlitLightSource, SlitScreen } from '@descix/frqtl-web/parts';
19
19
 
20
+ export const parts = {};
21
+
20
22
  export default function buildExperiment(exp) {
21
23
  var S = 480, WAVELENGTH = 64; // THE WORLD this scene states (its co-init call below) and its one geometry knob
22
24
 
23
25
  exp.createCanvas(S, S, { dimension: 2, wavelengthScale: Math.floor(WAVELENGTH / 16) }); // a 2D canvas + universe, S x S
26
+ exp.declare(parts);
24
27
  var u = exp.universe;
25
28
 
26
29
  var slits = new SlitScreen(u, WAVELENGTH * 1.5, WAVELENGTH, S, WAVELENGTH, true, 1);
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world
7
7
  * (its co-init call, `exp.createCanvas` / `exp.createScene` — the one place its size is written),
8
- * the parts (`exp.addObject`) and the render intent are all here; the sugar
8
+ * the parts (`export const parts` + `exp.declare`, or `exp.addObject`) and the render intent are all here; the sugar
9
9
  * objects, renderer and engine are imports. Registered by lib/shim/registry/walkthrough.js
10
10
  * (rung dse-one-emitter).
11
11
  */
@@ -71,19 +71,70 @@
71
71
  // the title lines and the `var NSRC` line — lib/frqtl/scenes/test/scene-files.test.mjs
72
72
  // fails if any other line diverges, so the A/B the pair exists to make stays an A/B.
73
73
 
74
- import { SphereEmitter } from '@descix/frqtl-web/parts';
74
+ import { SphereEmitter, Wall } from '@descix/frqtl-web/parts';
75
+
76
+ const CW = 600, H = 400; // THE WORLD this scene states (its co-init call below)
77
+ // ── THE ONLY LENGTH ─────────────────────────────────────────────────────
78
+ // lamPx is lambda_eff in pixels. 320 is chosen for RESOLUTION, not for how many
79
+ // orders fit: the feature this rung exists to show (the minimum just inside the
80
+ // band and the peak riding outside it) is 0.025*lambda wide, which is 8 px at
81
+ // lambda 320 and 3 px at lambda 128. A stage that cannot separate them shows a
82
+ // smooth hump and leaves you unable to say whether the physics or the sampling
83
+ // was at fault. One well-resolved pair beats three mushy ones.
84
+ const lamPx = 320;
85
+ const sinkW = Math.round(lamPx / 16); // 20 — far-edge sink thickness
86
+
87
+ // ── THE DETECTOR LEG — ONE NAMED CONSTANT ───────────────────────────────
88
+ // D = legFrac * lambda, measured from the SOURCE (there is no barrier, so the
89
+ // emitter itself is the reference point of the ladder). legFrac = 1/4 is the
90
+ // leg the ring formula was written for; see the block comment above.
91
+ const legFrac = 1 / 4;
92
+ const D = Math.round(lamPx * legFrac); // 80
93
+ const srcX = Math.round(lamPx / 16); // 20 — launch room; the canvas edge is the back sink
94
+ const detX = srcX + D; // 100 — the plate's FRONT FACE
95
+ // PLATE THICKNESS IS AN OPTICAL CHOICE UNDER A FLUX DETECTOR, not trim. A flux
96
+ // probe counts every tick a quantum overlaps it, so it integrates the density
97
+ // ACROSS its thickness — and the ring crest is a CIRCLE about the source, not a
98
+ // plane. At the m=0 band the crest's slope is dy/dx = -D/y = -1, so a slab t px
99
+ // thick smears the band over ~t px of y. Against an 8 px feature a lambda/8 (40
100
+ // px) plate would guarantee mush. lambda/64 = 5 px keeps the smear sub-feature
101
+ // and is still unjumpable (a quantum steps at most 2 px/axis/tick). A 'hits'
102
+ // plate would not care — it counts at first contact, i.e. at the front face —
103
+ // which is exactly why the reference H1 runs could afford a lambda/8 slab.
104
+ const detW = Math.round(lamPx / 64); // 5 — plate thickness
105
+ // ── THE PLATE — A COUNTING WALL THAT DOES NOT ABSORB ────────────────────
106
+ // ONE NAMED CONSTANT, so the reading can be swapped in a single edit and the two
107
+ // instruments compared rather than argued about:
108
+ // 'transparent-probe' = collisionBehavior 'nothing' + mark/count. FLUX. The
109
+ // quantum is counted on every tick it overlaps the plate and then flies on,
110
+ // so the reading is the OCCUPANCY INTEGRAL at the plate plane — the density
111
+ // at that plane. This is the shipped choice.
112
+ // 'detector-screen' = destroy + count. HITS. Each quantum is counted once, at
113
+ // arrival, and removed: a transit census, the headcount a photographic plate
114
+ // would report. It is what the H1 reference runs used.
115
+ // NEITHER ABSORBS. 'absorb' stores the arriving mass and re-radiates it — that is
116
+ // the photoelectric experiment, and it saturates.
117
+ const detectorPreset = 'transparent-probe';
118
+
119
+ export const parts = {
120
+ // FAR-EDGE SINK. The plate below counts WITHOUT removing, so quanta fly on past
121
+ // it; this is the experiment-boundary sink downstream of the plate. It is not
122
+ // load-bearing (wrap is false, so the canvas edge would remove them anyway) — it
123
+ // is where the population bound is STATED rather than inferred.
124
+ far_sink: [Wall, { rect: [CW - sinkW, 0, sinkW, H], collisionBehavior: 'destroy',
125
+ color: [120, 120, 120], alpha: 40 }],
126
+ // markTag — the plate's PROVENANCE stamp. The mark hook fires ONLY when the
127
+ // collision trigger does (genuine overlap), whereas the engine buffers every
128
+ // frame inside the LO's mb_w-PADDED search rect whether it triggered or not. So a
129
+ // raw frame count off the buffer is a padded-occupancy reading and the
130
+ // mark-filtered count is the trigger-gated census: two different instruments, and
131
+ // an offline reader needs the tag to tell them apart.
132
+ screen: [Wall, { rect: [detX, 0, detW, H],
133
+ preset: detectorPreset, markTag: 'plate', color: [120, 200, 120] }],
134
+ };
75
135
 
76
136
  export default function buildExperiment(exp) {
77
137
  var NSRC = 1;
78
- var CW = 600, H = 400; // THE WORLD this scene states (its co-init call below)
79
- // ── THE ONLY LENGTH ─────────────────────────────────────────────────────
80
- // lamPx is lambda_eff in pixels. 320 is chosen for RESOLUTION, not for how many
81
- // orders fit: the feature this rung exists to show (the minimum just inside the
82
- // band and the peak riding outside it) is 0.025*lambda wide, which is 8 px at
83
- // lambda 320 and 3 px at lambda 128. A stage that cannot separate them shows a
84
- // smooth hump and leaves you unable to say whether the physics or the sampling
85
- // was at fault. One well-resolved pair beats three mushy ones.
86
- var lamPx = 320;
87
138
  var lamArg = 3.2; // capacity/mass -> fullness 1/3.2 = 0.3125 (yellow)
88
139
  var fwl = 4 * (lamPx / lamArg); // 400 -> wavelengthScale floor(400/4) = 100
89
140
  var wlScale = Math.floor(fwl / 4); // 100
@@ -91,26 +142,7 @@ export default function buildExperiment(exp) {
91
142
  throw new Error('[dse-h1] lamPx ' + lamPx + ' is not realisable as lambda_eff: the engine ' +
92
143
  'can only produce wavelengthArg x floor(fwl/4) = ' + (lamArg * wlScale) + '. (FAIL-LOUD.)');
93
144
  }
94
- // ── THE DETECTOR LEG — ONE NAMED CONSTANT ───────────────────────────────
95
- // D = legFrac * lambda, measured from the SOURCE (there is no barrier, so the
96
- // emitter itself is the reference point of the ladder). legFrac = 1/4 is the
97
- // leg the ring formula was written for; see the block comment above.
98
- var legFrac = 1 / 4;
99
- var D = Math.round(lamPx * legFrac); // 80
100
- var srcX = Math.round(lamPx / 16); // 20 — launch room; the canvas edge is the back sink
101
145
  var cy = Math.round(H / 2); // 200 — the source plane's midline
102
- var detX = srcX + D; // 100 — the plate's FRONT FACE
103
- // PLATE THICKNESS IS AN OPTICAL CHOICE UNDER A FLUX DETECTOR, not trim. A flux
104
- // probe counts every tick a quantum overlaps it, so it integrates the density
105
- // ACROSS its thickness — and the ring crest is a CIRCLE about the source, not a
106
- // plane. At the m=0 band the crest's slope is dy/dx = -D/y = -1, so a slab t px
107
- // thick smears the band over ~t px of y. Against an 8 px feature a lambda/8 (40
108
- // px) plate would guarantee mush. lambda/64 = 5 px keeps the smear sub-feature
109
- // and is still unjumpable (a quantum steps at most 2 px/axis/tick). A 'hits'
110
- // plate would not care — it counts at first contact, i.e. at the front face —
111
- // which is exactly why the reference H1 runs could afford a lambda/8 slab.
112
- var detW = Math.round(lamPx / 64); // 5 — plate thickness
113
- var sinkW = Math.round(lamPx / 16); // 20 — far-edge sink thickness
114
146
  // TWO-SOURCE SEPARATION. Two sources reinforce when their separation is a
115
147
  // multiple of lambda/2, and the coupling falls as (lambda/d)^2, so lambda/2 is
116
148
  // the strongest case. At this leg it also puts a band of EACH source on the
@@ -137,33 +169,7 @@ export default function buildExperiment(exp) {
137
169
  for (var si = 0; si < srcYs.length; si++) {
138
170
  exp.addObject('SphereEmitter', Object.assign({}, SRC, { center: [srcX, Math.round(srcYs[si])] }));
139
171
  }
140
- // FAR-EDGE SINK. The plate below counts WITHOUT removing, so quanta fly on past
141
- // it; this is the experiment-boundary sink downstream of the plate. It is not
142
- // load-bearing (wrap is false, so the canvas edge would remove them anyway) — it
143
- // is where the population bound is STATED rather than inferred.
144
- exp.addObject('Wall', { rect: [CW - sinkW, 0, sinkW, H], collisionBehavior: 'destroy',
145
- name: 'far_sink', color: [120, 120, 120], alpha: 40 });
146
- // ── THE PLATE — A COUNTING WALL THAT DOES NOT ABSORB ────────────────────
147
- // ONE NAMED CONSTANT, so the reading can be swapped in a single edit and the two
148
- // instruments compared rather than argued about:
149
- // 'transparent-probe' = collisionBehavior 'nothing' + mark/count. FLUX. The
150
- // quantum is counted on every tick it overlaps the plate and then flies on,
151
- // so the reading is the OCCUPANCY INTEGRAL at the plate plane — the density
152
- // at that plane. This is the shipped choice.
153
- // 'detector-screen' = destroy + count. HITS. Each quantum is counted once, at
154
- // arrival, and removed: a transit census, the headcount a photographic plate
155
- // would report. It is what the H1 reference runs used.
156
- // NEITHER ABSORBS. 'absorb' stores the arriving mass and re-radiates it — that is
157
- // the photoelectric experiment, and it saturates.
158
- var detectorPreset = 'transparent-probe';
159
- // markTag — the plate's PROVENANCE stamp. The mark hook fires ONLY when the
160
- // collision trigger does (genuine overlap), whereas the engine buffers every
161
- // frame inside the LO's mb_w-PADDED search rect whether it triggered or not. So a
162
- // raw frame count off the buffer is a padded-occupancy reading and the
163
- // mark-filtered count is the trigger-gated census: two different instruments, and
164
- // an offline reader needs the tag to tell them apart.
165
- var screen = exp.addObject('Wall', { rect: [detX, 0, detW, H],
166
- preset: detectorPreset, markTag: 'plate', name: 'screen', color: [120, 200, 120] });
172
+ const p = exp.declare(parts);
167
173
  // ── THE INSTRUMENT — the live count-by-y histogram, REGISTERED TO THE PLATE ──
168
174
  // The panel takes the plate's OWN rect y-origin (0) and height (H), so the two
169
175
  // are co-extensive on the bin axis at any canvas size, and it starts just past
@@ -189,7 +195,7 @@ export default function buildExperiment(exp) {
189
195
  // profile's floor rises the curve compresses toward the left edge. Read positions off
190
196
  // it, not magnitudes; magnitudes come from getCollisionHistogram().
191
197
  exp.addObject('LiveChart', {
192
- source: screen, orientation: 90,
198
+ source: p.screen, orientation: 90,
193
199
  rect: [detX + detW + 8, 0, Math.max(60, CW - (detX + detW + 8) - 8), H],
194
200
  series: 'line', label: 'count by y', refreshMs: 100, color: [24, 32, 46], alpha: 225
195
201
  });
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world
7
7
  * (its co-init call, `exp.createCanvas` / `exp.createScene` — the one place its size is written),
8
- * the parts (`exp.addObject`) and the render intent are all here; the sugar
8
+ * the parts (`export const parts` + `exp.declare`, or `exp.addObject`) and the render intent are all here; the sugar
9
9
  * objects, renderer and engine are imports. Registered by lib/shim/registry/walkthrough.js
10
10
  * (rung dse-one-slit).
11
11
  */
@@ -36,78 +36,82 @@
36
36
  // diffraction hump" expectation, which this engine's per-source ring mechanism
37
37
  // does NOT predict — if that is what appears instead, it is a real null.)
38
38
 
39
- import { SphereEmitter } from '@descix/frqtl-web/parts';
39
+ import { SphereEmitter, Tunnel, Wall } from '@descix/frqtl-web/parts';
40
40
 
41
- export default function buildExperiment(exp) {
42
- // One Slit -- the SAME apparatus as dse-two-slits with slit 2 WALLED OFF. Barrier =
43
- // Wall segments (destroy); the surviving slit 1 = a directed Tunnel with the exit
44
- // gate collapsed + phase-reset (identical to dse-two-slits' slit1_channel).
45
- var CW = 600, H = 400; // THE WORLD this scene states (its co-init call below)
46
- // ── THE ONLY LENGTH — IDENTICAL to the canonical-leg pair and dse-two-slits ─────
47
- var lamPx = 320;
48
- var lamArg = 3.2; // fullness 1/3.2 = 0.3125 -> yellow
49
- var fwl = 4 * (lamPx / lamArg); // 400 -> wavelengthScale floor(400/4) = 100
50
- var wlScale = Math.floor(fwl / 4); // 100
51
- if (Math.abs(lamArg * wlScale - lamPx) > 1e-9) {
52
- throw new Error('[dse-one-slit] lamPx ' + lamPx + ' is not realisable as lambda_eff: the ' +
53
- 'engine can only produce wavelengthArg x floor(fwl/4) = ' + (lamArg * wlScale) + '. (FAIL-LOUD.)');
54
- }
55
- // ── SLIT 1's POSITION — IDENTICAL to dse-two-slits' slit-1 aperture ─────────────
56
- // sep is kept only to DERIVE slitCY1 at the exact same y dse-two-slits used (120) —
57
- // there is no slit 2 in this rung, so it is never used to place a second aperture.
58
- var legFrac = 1 / 4;
59
- var D = Math.round(lamPx * legFrac); // 80 — the SAME leg, measured from the
60
- // (single) slit EXIT plane
61
- var sep = lamPx / 2; // 160 — dse-two-slits' source separation,
62
- // reused ONLY to derive slit 1's y below
63
- var cy = Math.round(H / 2); // 200
64
- var slitCY1 = cy - sep / 2; // 120 — IDENTICAL y to dse-two-slits' slit 1
65
- // ── THE BARRIER — one opaque plate, ONE slit aperture (slit 2 walled off) ───────
66
- var barrierW = Math.round(lamPx / 8); // 40 — plate thickness
67
- var slitH = Math.round(lamPx / 8); // 40 — slit aperture height (before liners)
68
- var linerH = Math.round(lamPx / 64); // 5 — liner thickness (unjumpable: max step 2px/tick)
69
- var bore = slitH - 2 * linerH; // 30 — the clear aperture between the liners
70
- var srcX = Math.round(lamPx / 16); // 20 — the emitter, well behind the barrier
71
- var barrierL = srcX + Math.round(lamPx / 2); // 180 — half a wavelength of travel room
72
- var slit1Top = slitCY1 - slitH / 2; // 100 -- slit 1 spans y 100..140
73
- var exitX = barrierL + barrierW; // 220 — the slit EXIT plane (barrier back face)
74
- var detX = exitX + D; // 300 — SAME leg as dse-two-slits
75
- var detW = Math.round(lamPx / 64); // 5 — plate thickness (flux probe, sub-feature)
76
- var sinkW = Math.round(lamPx / 16); // 20 — far-edge sink thickness
77
- // ── THE LIGHT SOURCE — ONE emitter, behind the barrier, lighting the one slit ───
78
- var SRC = {
41
+ // One Slit -- the SAME apparatus as dse-two-slits with slit 2 WALLED OFF. Barrier =
42
+ // Wall segments (destroy); the surviving slit 1 = a directed Tunnel with the exit
43
+ // gate collapsed + phase-reset (identical to dse-two-slits' slit1_channel).
44
+ const CW = 600, H = 400; // THE WORLD this scene states (its co-init call below)
45
+ // ── THE ONLY LENGTH — IDENTICAL to the canonical-leg pair and dse-two-slits ─────
46
+ const lamPx = 320;
47
+ const lamArg = 3.2; // fullness 1/3.2 = 0.3125 -> yellow
48
+ const fwl = 4 * (lamPx / lamArg); // 400 -> wavelengthScale floor(400/4) = 100
49
+ const wlScale = Math.floor(fwl / 4); // 100
50
+ // ── SLIT 1's POSITION — IDENTICAL to dse-two-slits' slit-1 aperture ─────────────
51
+ // sep is kept only to DERIVE slitCY1 at the exact same y dse-two-slits used (120) —
52
+ // there is no slit 2 in this rung, so it is never used to place a second aperture.
53
+ const legFrac = 1 / 4;
54
+ const D = Math.round(lamPx * legFrac); // 80 — the SAME leg, measured from the
55
+ // (single) slit EXIT plane
56
+ const sep = lamPx / 2; // 160 — dse-two-slits' source separation,
57
+ // reused ONLY to derive slit 1's y below
58
+ const cy = Math.round(H / 2); // 200
59
+ const slitCY1 = cy - sep / 2; // 120 — IDENTICAL y to dse-two-slits' slit 1
60
+ // ── THE BARRIER — one opaque plate, ONE slit aperture (slit 2 walled off) ───────
61
+ const barrierW = Math.round(lamPx / 8); // 40 — plate thickness
62
+ const slitH = Math.round(lamPx / 8); // 40 — slit aperture height (before liners)
63
+ const linerH = Math.round(lamPx / 64); // 5 — liner thickness (unjumpable: max step 2px/tick)
64
+ const bore = slitH - 2 * linerH; // 30 — the clear aperture between the liners
65
+ const srcX = Math.round(lamPx / 16); // 20 — the emitter, well behind the barrier
66
+ const barrierL = srcX + Math.round(lamPx / 2); // 180 — half a wavelength of travel room
67
+ const slit1Top = slitCY1 - slitH / 2; // 100 -- slit 1 spans y 100..140
68
+ const exitX = barrierL + barrierW; // 220 — the slit EXIT plane (barrier back face)
69
+ const detX = exitX + D; // 300 — SAME leg as dse-two-slits
70
+ const detW = Math.round(lamPx / 64); // 5 — plate thickness (flux probe, sub-feature)
71
+ const sinkW = Math.round(lamPx / 16); // 20 — far-edge sink thickness
72
+
73
+ export const parts = {
74
+ source: [SphereEmitter, {
79
75
  preset: 'hemisphere', is2D: true, chargeMode: 'random', center: [srcX, cy],
80
76
  wavelength: lamArg, fundamentalDimension: 2, fundamentalWaveLength: fwl,
81
77
  burstSize: 8, emitEveryTicks: 1
82
- };
83
- exp.createCanvas(CW, H, SphereEmitter.worldCfg(SRC)); // THE WORLD: a 2D canvas + universe — first, then parts
84
- exp.addObject('SphereEmitter', SRC);
78
+ }],
85
79
  // Barrier segments — opaque destroy sinks, leaving ONE slit gap (slit 1 only). Where
86
80
  // dse-two-slits had wall2 (a short strip between the two slits) + a slit-2 Tunnel +
87
81
  // wall3 (below slit 2), this rung has ONE solid wall from slit 1's bottom liner all
88
82
  // the way to the far edge — slit 2's aperture is WALLED, not narrowed or moved.
89
- exp.addObject('Wall', { rect: [barrierL, 0, barrierW, slit1Top],
90
- collisionBehavior: 'destroy', name: 'wall1', color: [200, 60, 60] });
91
- exp.addObject('Wall', { rect: [barrierL, slit1Top + slitH, barrierW, H - (slit1Top + slitH)],
92
- collisionBehavior: 'destroy', name: 'wall2_blocked_slit2', color: [150, 40, 40] });
83
+ wall1: [Wall, { rect: [barrierL, 0, barrierW, slit1Top],
84
+ collisionBehavior: 'destroy', color: [200, 60, 60] }],
85
+ wall2_blocked_slit2: [Wall, { rect: [barrierL, slit1Top + slitH, barrierW, H - (slit1Top + slitH)],
86
+ collisionBehavior: 'destroy', color: [150, 40, 40] }],
93
87
  // The one surviving slit channel — directed Tunnel, exit gate COLLAPSED + PHASE-RESET
94
88
  // (the dse-tunnel-rebuild composition): its own mouth stands in for a point source at
95
89
  // y=120, phase-locked — what a SphereEmitter would do at that pixel.
96
- exp.addObject('Tunnel', {
90
+ slit1_channel: [Tunnel, {
97
91
  left: barrierL, top: slit1Top + linerH, length: barrierW, bore: bore,
98
92
  wallThickness: linerH, axis: 'x', mode: 'directed',
99
93
  exitCollapse: true, exitRecenter: true, exitPhaseReset: true,
100
- name: 'slit1_channel', color: [80, 140, 220]
101
- });
94
+ color: [80, 140, 220]
95
+ }],
102
96
  // FAR-EDGE SINK — the same experiment-boundary sink the canonical-leg pair uses.
103
- exp.addObject('Wall', { rect: [CW - sinkW, 0, sinkW, H], collisionBehavior: 'destroy',
104
- name: 'far_sink', color: [120, 120, 120], alpha: 40 });
97
+ far_sink: [Wall, { rect: [CW - sinkW, 0, sinkW, H], collisionBehavior: 'destroy',
98
+ color: [120, 120, 120], alpha: 40 }],
105
99
  // ── THE PLATE — SAME instrument as dse-two-slits: a counting wall that does NOT
106
100
  // absorb, at the SAME leg D past the new source plane (the one slit exit).
107
- var screen = exp.addObject('Wall', { rect: [detX, 0, detW, H],
108
- preset: 'transparent-probe', markTag: 'plate', name: 'screen', color: [120, 200, 120] });
101
+ screen: [Wall, { rect: [detX, 0, detW, H],
102
+ preset: 'transparent-probe', markTag: 'plate', color: [120, 200, 120] }],
103
+ };
104
+
105
+ export default function buildExperiment(exp) {
106
+ if (Math.abs(lamArg * wlScale - lamPx) > 1e-9) {
107
+ throw new Error('[dse-one-slit] lamPx ' + lamPx + ' is not realisable as lambda_eff: the ' +
108
+ 'engine can only produce wavelengthArg x floor(fwl/4) = ' + (lamArg * wlScale) + '. (FAIL-LOUD.)');
109
+ }
110
+ // ── THE LIGHT SOURCE — ONE emitter, behind the barrier, lighting the one slit ───
111
+ exp.createCanvas(CW, H, SphereEmitter.worldCfg(parts.source[1])); // THE WORLD: a 2D canvas + universe — first, then parts
112
+ const p = exp.declare(parts);
109
113
  exp.addObject('LiveChart', {
110
- source: screen, orientation: 90,
114
+ source: p.screen, orientation: 90,
111
115
  rect: [detX + detW + 8, 0, Math.max(60, CW - (detX + detW + 8) - 8), H],
112
116
  series: 'line', label: 'count by y', refreshMs: 100, color: [24, 32, 46], alpha: 225
113
117
  });
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * A sketch file: the WHOLE scene as one ES module, `export default function buildExperiment(exp)`. The world
7
7
  * (its co-init call, `exp.createCanvas` / `exp.createScene` — the one place its size is written),
8
- * the parts (`exp.addObject`) and the render intent are all here; the sugar
8
+ * the parts (`export const parts` + `exp.declare`, or `exp.addObject`) and the render intent are all here; the sugar
9
9
  * objects, renderer and engine are imports. Registered by lib/shim/registry/walkthrough.js
10
10
  * (rung dse-photoelectric).
11
11
  */
@@ -24,46 +24,50 @@
24
24
  // counts the RADIATED quanta (the photoelectron detector) — and its own live arrivals chart
25
25
  // shows them being counted. SU1 — the LO5 absorb/radiate golden, composed, zero new physics.
26
26
 
27
- import { SphereEmitter } from '@descix/frqtl-web/parts';
27
+ import { SphereEmitter, Wall } from '@descix/frqtl-web/parts';
28
28
 
29
- export default function buildExperiment(exp) {
30
- // 2B (Photoelectric) — light stream → ABSORB wall (charges + radiates) → a Detector
31
- // Screen BEHIND counting the radiated photoelectrons. Two live charts: the wall's
32
- // charge ledger (absorberStats) and the rear detector's arrivals.
33
- var W = 600, H = 400; // THE WORLD this scene states (its co-init call below)
34
- var cy = Math.round(H / 2);
35
- // The incoming light stream: a thin +x beam of oppositely-charged dim-2 leaves. A
36
- // SPARSE flux (1 leaf / 2 ticks) keeps the slow absorb/radiate drip radiation-dominated
37
- // (a dense beam saturates the wall, which then leaks past it — the deferred emergent-
38
- // probability threshold would gate that; see 2B prose).
39
- var SRC = {
29
+ // 2B (Photoelectric) — light stream → ABSORB wall (charges + radiates) → a Detector
30
+ // Screen BEHIND counting the radiated photoelectrons. Two live charts: the wall's
31
+ // charge ledger (absorberStats) and the rear detector's arrivals.
32
+ const W = 600, H = 400; // THE WORLD this scene states (its co-init call below)
33
+ const cy = Math.round(H / 2);
34
+
35
+ export const parts = {
36
+ source: [SphereEmitter, {
40
37
  preset: 'particle-walk', is2D: true, center: [Math.round(W * 0.1), cy],
41
38
  chargeMode: 'random', fundamentalDimension: 2, wavelength: 32,
42
39
  burstSize: 1, emitEveryTicks: 2
43
- };
44
- exp.createCanvas(W, H, SphereEmitter.worldCfg(SRC)); // THE WORLD: a 2D canvas + universe — first, then parts
45
- exp.addObject('SphereEmitter', SRC);
40
+ }],
46
41
  // The absorbing wall (ABSORB v2): charges up to capacity, then radiates lower-dim
47
42
  // leaves out its back (mass conserved) — the photocathode.
48
- var absorber = exp.addObject('Wall', {
43
+ absorber: [Wall, {
49
44
  rect: [Math.round(W * 0.45), Math.round(H * 0.25), 20, Math.round(H * 0.5)],
50
- collisionBehavior: 'absorb', capacity: 64, name: 'absorber', color: [150, 90, 210]
51
- });
45
+ collisionBehavior: 'absorb', capacity: 64, color: [150, 90, 210]
46
+ }],
52
47
  // The photoelectron detector: a Detector Screen (destroy + count) BEHIND the absorber,
53
48
  // counting the radiated quanta that emerge past it.
54
- var photodetector = exp.addObject('Wall', {
49
+ photodetector: [Wall, {
55
50
  rect: [Math.round(W * 0.72), Math.round(H * 0.2), 20, Math.round(H * 0.6)],
56
- preset: 'detector-screen', name: 'photodetector', color: [120, 200, 120]
57
- });
51
+ preset: 'detector-screen', color: [120, 200, 120]
52
+ }],
53
+ };
54
+
55
+ export default function buildExperiment(exp) {
56
+ // The incoming light stream: a thin +x beam of oppositely-charged dim-2 leaves. A
57
+ // SPARSE flux (1 leaf / 2 ticks) keeps the slow absorb/radiate drip radiation-dominated
58
+ // (a dense beam saturates the wall, which then leaks past it — the deferred emergent-
59
+ // probability threshold would gate that; see 2B prose).
60
+ exp.createCanvas(W, H, SphereEmitter.worldCfg(parts.source[1])); // THE WORLD: a 2D canvas + universe — first, then parts
61
+ const p = exp.declare(parts);
58
62
  // The wall's charge ledger (stored / absorbed / radiated) — the absorb-stats series.
59
63
  exp.addObject('LiveChart', {
60
- source: absorber, projector: 'absorberStats',
64
+ source: p.absorber, projector: 'absorberStats',
61
65
  rect: [Math.round(W * 0.04), Math.round(H * 0.03), Math.round(W * 0.32), Math.round(H * 0.28)],
62
66
  series: 'bar', label: 'wall charge (stored/absorbed/radiated)', refreshMs: 100, color: [30, 22, 44], alpha: 230
63
67
  });
64
68
  // The photoelectron count beyond the wall — the rear detector's live arrivals.
65
69
  exp.addObject('LiveChart', {
66
- source: photodetector,
70
+ source: p.photodetector,
67
71
  rect: [Math.round(W * 0.63), Math.round(H * 0.03), Math.round(W * 0.32), Math.round(H * 0.28)],
68
72
  series: 'bar', label: 'photoelectrons counted', refreshMs: 100, color: [22, 40, 30], alpha: 230
69
73
  });