@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.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 (217) hide show
  1. package/CLAUDE.md +87 -0
  2. package/README.md +84 -2
  3. package/dist/dist-manifest.json +218 -0
  4. package/dist/eyes/index.js +109 -0
  5. package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
  6. package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
  7. package/dist/fat/pkg-node/package.json +12 -0
  8. package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
  9. package/dist/frqtl/compat/boot-node.js +34 -0
  10. package/dist/frqtl/compat/engine.js +127 -0
  11. package/dist/frqtl/compat/package-root.js +24 -0
  12. package/dist/frqtl/compat/providers/wasm-engine.js +2501 -0
  13. package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
  14. package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
  15. package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
  16. package/dist/frqtl/compat/renderer/Filters.js +163 -0
  17. package/dist/frqtl/compat/renderer/RenderFamily.js +85 -0
  18. package/dist/frqtl/compat/renderer/RenderMesh.js +100 -0
  19. package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
  20. package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
  21. package/dist/frqtl/compat/renderer/captureStill-common.js +259 -0
  22. package/dist/frqtl/compat/renderer/display-slot.js +96 -0
  23. package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
  24. package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
  25. package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
  26. package/dist/frqtl/compat/renderer/providers/index.js +22 -0
  27. package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
  28. package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
  29. package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
  30. package/dist/frqtl/compat/renderer/providers/three-provider.js +2474 -0
  31. package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
  32. package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
  33. package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
  34. package/dist/frqtl/compat/vendor.js +196 -0
  35. package/dist/frqtl/node-entry.js +17 -0
  36. package/dist/frqtl/objects/FrqtlApp.js +2922 -0
  37. package/dist/frqtl/objects/ObjectFactory.js +297 -0
  38. package/dist/frqtl/objects/RecipeContext.js +322 -0
  39. package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
  40. package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
  41. package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
  42. package/dist/frqtl/objects/instruments/capture-host.js +453 -0
  43. package/dist/frqtl/objects/instruments/capture.js +711 -0
  44. package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -0
  45. package/dist/frqtl/objects/kinds-entry.js +14 -0
  46. package/dist/frqtl/objects/kinds.js +190 -0
  47. package/dist/frqtl/objects/objects/Box.js +273 -0
  48. package/dist/frqtl/objects/objects/Bullet.js +155 -0
  49. package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
  50. package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
  51. package/dist/frqtl/objects/objects/DirectedTunnel.js +412 -0
  52. package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
  53. package/dist/frqtl/objects/objects/LightSource.js +354 -0
  54. package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
  55. package/dist/frqtl/objects/objects/Tube.js +471 -0
  56. package/dist/frqtl/objects/objects/absorbRadiation.js +310 -0
  57. package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
  58. package/dist/frqtl/objects/objects/controlAction.js +89 -0
  59. package/dist/frqtl/objects/objects/controlBind.js +278 -0
  60. package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
  61. package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
  62. package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
  63. package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
  64. package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
  65. package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
  66. package/dist/frqtl/objects/objects/makeControlsForObject.js +824 -0
  67. package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
  68. package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
  69. package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
  70. package/dist/frqtl/objects/objects/tagger.js +228 -0
  71. package/dist/frqtl/objects/schema/engine-scopes.js +469 -0
  72. package/dist/frqtl/objects/schema/index.js +256 -0
  73. package/dist/frqtl/objects/schema/scopes.js +457 -0
  74. package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
  75. package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
  76. package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
  77. package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
  78. package/dist/frqtl/objects/setup/_worldInit.js +162 -0
  79. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
  80. package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
  81. package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
  82. package/dist/frqtl/objects/setup/setupConeEmitter3D.js +251 -0
  83. package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -0
  84. package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
  85. package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
  86. package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
  87. package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
  88. package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
  89. package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
  90. package/dist/frqtl/objects/setup/setupLightSourceLab.js +280 -0
  91. package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
  92. package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
  93. package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
  94. package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1259 -0
  95. package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
  96. package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
  97. package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
  98. package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
  99. package/dist/frqtl/objects/v1-replacements.js +30 -0
  100. package/dist/frqtl/package-core.js +23 -0
  101. package/dist/frqtl/parts/CameraControls.js +643 -0
  102. package/dist/frqtl/parts/CameraPath.js +575 -0
  103. package/dist/frqtl/parts/CollisionActions.js +35 -0
  104. package/dist/frqtl/parts/CollisionTypes.js +31 -0
  105. package/dist/frqtl/parts/DetectorWall.js +496 -0
  106. package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
  107. package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
  108. package/dist/frqtl/parts/FrqtlVision.js +818 -0
  109. package/dist/frqtl/parts/GraphingDetectorWall.js +490 -0
  110. package/dist/frqtl/parts/LargeObject.js +406 -0
  111. package/dist/frqtl/parts/LightColor.js +255 -0
  112. package/dist/frqtl/parts/LiveChart.js +285 -0
  113. package/dist/frqtl/parts/LogicGate.js +720 -0
  114. package/dist/frqtl/parts/OvenBox.js +612 -0
  115. package/dist/frqtl/parts/PointSource.js +763 -0
  116. package/dist/frqtl/parts/Rectangle.js +200 -0
  117. package/dist/frqtl/parts/SlitLightSource.js +234 -0
  118. package/dist/frqtl/parts/SlitScreen.js +628 -0
  119. package/dist/frqtl/parts/SphereEmitter.js +946 -0
  120. package/dist/frqtl/parts/Trace.js +508 -0
  121. package/dist/frqtl/parts/Tunnel.js +1057 -0
  122. package/dist/frqtl/parts/Wall.js +1211 -0
  123. package/dist/frqtl/parts/index.js +33 -0
  124. package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
  125. package/dist/frqtl/scenes/absorbing-wall.js +32 -0
  126. package/dist/frqtl/scenes/atomic-model.js +88 -0
  127. package/dist/frqtl/scenes/big-bang.js +46 -0
  128. package/dist/frqtl/scenes/blackbody.js +81 -0
  129. package/dist/frqtl/scenes/circuit-sat.js +127 -0
  130. package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
  131. package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
  132. package/dist/frqtl/scenes/counting-wall.js +38 -0
  133. package/dist/frqtl/scenes/double-slit.js +31 -0
  134. package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
  135. package/dist/frqtl/scenes/dse-one-slit.js +114 -0
  136. package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
  137. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +244 -0
  138. package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
  139. package/dist/frqtl/scenes/dse-two-slits.js +132 -0
  140. package/dist/frqtl/scenes/empty.js +14 -0
  141. package/dist/frqtl/scenes/laser-buildup.js +49 -0
  142. package/dist/frqtl/scenes/mirror-3d.js +56 -0
  143. package/dist/frqtl/scenes/mirror-route.js +140 -0
  144. package/dist/frqtl/scenes/mirror.js +38 -0
  145. package/dist/frqtl/scenes/omni-3d.js +27 -0
  146. package/dist/frqtl/scenes/omni-source.js +22 -0
  147. package/dist/frqtl/scenes/self-interference.js +39 -0
  148. package/dist/frqtl/scenes/smoking-gun.js +122 -0
  149. package/dist/frqtl/scenes/straight-walk.js +22 -0
  150. package/dist/frqtl/scenes/sun.js +39 -0
  151. package/dist/frqtl/scenes/two-source.js +37 -0
  152. package/dist/frqtl/scenes/wave.js +26 -0
  153. package/dist/shim/bundle/frqnb.js +112 -0
  154. package/dist/shim/bundle/index.js +121 -0
  155. package/dist/shim/bundle/indexHtml.js +107 -0
  156. package/dist/shim/bundle/manifest.js +95 -0
  157. package/dist/shim/bundle/zip.js +243 -0
  158. package/dist/shim/bus.js +67 -0
  159. package/dist/shim/controls/index.js +627 -0
  160. package/dist/shim/controls/registry.js +274 -0
  161. package/dist/shim/engine/artifact-shell.js +212 -0
  162. package/dist/shim/engine/boot-phases.js +44 -0
  163. package/dist/shim/engine/engine-stack-boot.js +59 -0
  164. package/dist/shim/engine/router.js +692 -0
  165. package/dist/shim/experiment-frame/index.js +296 -0
  166. package/dist/shim/expr/ast-schema.js +369 -0
  167. package/dist/shim/field-descriptor/index.js +326 -0
  168. package/dist/shim/frame-data/index.js +224 -0
  169. package/dist/shim/index.js +67 -0
  170. package/dist/shim/notebook/canvas-guard.js +82 -0
  171. package/dist/shim/notebook/catalog.js +35 -0
  172. package/dist/shim/notebook/format.js +736 -0
  173. package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
  174. package/dist/shim/notebook/kernel.js +672 -0
  175. package/dist/shim/notebook/knob.js +151 -0
  176. package/dist/shim/notebook/module-resolver.js +260 -0
  177. package/dist/shim/notebook/notebook-knobs.js +65 -0
  178. package/dist/shim/notebook/produced-bindings.js +67 -0
  179. package/dist/shim/notebook/realm-registry.js +121 -0
  180. package/dist/shim/notebook/realm-transport.js +73 -0
  181. package/dist/shim/notebook/setup-builder.js +510 -0
  182. package/dist/shim/notebook/store.js +159 -0
  183. package/dist/shim/notebook/vendor-importmap.js +134 -0
  184. package/dist/shim/notebook/view-builder.js +189 -0
  185. package/dist/shim/notebook/view-ctx.js +100 -0
  186. package/dist/shim/notebook/view-handle.js +202 -0
  187. package/dist/shim/notebook/warmup.js +102 -0
  188. package/dist/shim/package.json +4 -0
  189. package/dist/shim/page-params/index.js +214 -0
  190. package/dist/shim/project/identity.js +112 -0
  191. package/dist/shim/project/index.js +19 -0
  192. package/dist/shim/project/link.js +402 -0
  193. package/dist/shim/project/node-specifier-hook.mjs +35 -0
  194. package/dist/shim/project/register-node.mjs +17 -0
  195. package/dist/shim/project/specifiers.js +106 -0
  196. package/dist/shim/registry/copy-gate.js +110 -0
  197. package/dist/shim/registry/exhibits.js +455 -0
  198. package/dist/shim/registry/experiments.js +215 -0
  199. package/dist/shim/registry/index.js +19 -0
  200. package/dist/shim/registry/scene-source.js +157 -0
  201. package/dist/shim/registry/walkthrough.js +807 -0
  202. package/dist/shim/render/capture-envelope.js +279 -0
  203. package/dist/shim/render/heat-colormap.js +92 -0
  204. package/dist/shim/render/render-stages.js +855 -0
  205. package/dist/shim/render/sun-look.js +33 -0
  206. package/dist/shim/render-state/apply.js +262 -0
  207. package/dist/shim/render-state/index.js +74 -0
  208. package/dist/shim/rpc/index.js +207 -0
  209. package/dist/shim/runtime/index.js +1023 -0
  210. package/dist/shim/storage/boot.js +69 -0
  211. package/dist/shim/storage/index.js +169 -0
  212. package/dist/shim/storage/local-storage-provider.js +230 -0
  213. package/dist/shim/storage/make-namespace-store.js +132 -0
  214. package/dist/shim/storage/modules.js +249 -0
  215. package/dist/shim/storage/projects.js +150 -0
  216. package/llms.txt +12 -0
  217. package/package.json +62 -4
@@ -0,0 +1,251 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * setupConeEmitter3D — the smallest GYM-visible 3D surface for the CEO's
5
+ * concentric-ring cone-vs-cylinder construction (canonical-bench S2/S3,
6
+ * `docs/internal/physics-lab/canonical-bench/bench.cjs`), driven by ONE live SPREAD knob.
7
+ *
8
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
9
+ *
10
+ * @param {Object} universe
11
+ * The engine universe instance (JS or WASM compat shim), not yet init'd.
12
+ * Obtained from `window.EGPTUniverse` via FrqtlApp.
13
+ *
14
+ * @param {Object} [options]
15
+ * @param {number} [options.fundamentalWaveLength=64] - Desired wavelength of
16
+ * emitted quanta (px) — same `wavelengthScale` derivation every other
17
+ * setup factory in this directory uses (`setupParticleWalk3D.js`,
18
+ * `setupSphereEmitter3D.js`).
19
+ * @param {number} [options.spread=0.6] - v_radial MAGNITUDE (px/tick) each ring
20
+ * leaf's (vx,vy) is scaled to, pointing along its own outward radial unit
21
+ * vector. `spread=0` ⇒ every leaf's (vx,vy)=(0,0) — the LITERAL S2
22
+ * construction (collimated cylinder/laser). `spread>0` ⇒ the S3 construction
23
+ * (diverging cone/flashlight); the emergent half-angle is `atan(spread/vz)`
24
+ * (bench S3 law, measured within ~0.3° across 5 vz values).
25
+ * @param {number} [options.vz=1] - Constant forward velocity (px/tick) every
26
+ * ring leaf is stamped with, along the engine's z axis (bench's S2/S3 axis
27
+ * convention — the CEO's literal "assuming z towards viewer" framing).
28
+ * @param {number} [options.ringCount=4] - Concentric rings per emission burst,
29
+ * radii evenly spaced from `ringRadius/ringCount` to `ringRadius`.
30
+ * @param {number} [options.ringRadius=40] - Outer ring radius (px).
31
+ * @param {number} [options.leavesPerRing=20] - Leaves placed per ring (evenly
32
+ * spaced in theta).
33
+ * @param {number} [options.emitEveryTicks=6] - Ticks between successive ring
34
+ * bursts (sustained emission — a host tick-fn, NOT a native emit-spec; see
35
+ * the Layer 2 note below). A shorter period gives a denser, more continuous
36
+ * beam at higher per-tick leaf-creation cost.
37
+ *
38
+ * @returns {{
39
+ * coneEmitterControl: {
40
+ * setSpread(v), getSpread(): number,
41
+ * setVz(v), getVz(): number,
42
+ * setActive(bool),
43
+ * setRingCount(v), setRingRadius(v),
44
+ * controls: Array<{id,label,min,max,step,value,fmt,onChange}>
45
+ * },
46
+ * largeObjects: []
47
+ * }}
48
+ * `coneEmitterControl` is a live sugar handle (the canonical knob pattern —
49
+ * the blackbody scene's temperature knob → `oven.setTemperature`, `g3-lo-collide`'s `ctl`): a page
50
+ * wires a slider's `input` event straight to `setSpread`/`setVz`, live, no
51
+ * reload. `coneEmitterControl.controls` is the SAME standardized
52
+ * `runner.experimentControls`-compatible array `SphereEmitter.controls`
53
+ * already established — declared ONCE, consumed by both the page's own
54
+ * sliders and the IDE GYM control strip.
55
+ *
56
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
57
+ *
58
+ * Zero new physics (canonical-bench SU1) — this is a pure composition of two
59
+ * already-goldened engine behaviors: M5 (velocity is conserved absent
60
+ * collision/attraction — a free leaf drifts in a dead-straight line) and S1
61
+ * (x, y, and z axes run the IDENTICAL update — 2D and 3D are the same physics
62
+ * with more axes). Every emitted leaf is placed on a ring in the x,y plane via
63
+ * `makeLeafFrame` + `injectFrame`, then stamped via `setFrameProperty` with a
64
+ * constant forward `vz` and a radial (vx,vy) scaled by the live `spread` knob
65
+ * — the SAME `leafV`/`ringLeaves` construction verified as canonical-bench
66
+ * `S2`/`S3` (re-applying vx/vy/vz AFTER inject is the documented safe idiom,
67
+ * `feedback_pkgnode_verification_script_gotchas.md` — omitting it silently
68
+ * no-ops the velocity write on some engine builds).
69
+ *
70
+ * `spread=0` reproduces S2 exactly: radial extent stays bounded near the
71
+ * ring's own starting radius while z travels the full 1px/tick speed limit
72
+ * (bench M1) — `atan(radial/z-travel)` SHRINKS toward 0 as z grows, i.e. a
73
+ * COLLIMATED CYLINDER (laser), not a cone. `spread>0` reproduces S3: radial
74
+ * extent now grows in lockstep with z-travel, and the half-angle converges to
75
+ * `atan(spread/vz)` — a genuine diverging CONE (flashlight). No engine change
76
+ * is implicated anywhere in this file — it is purely which velocity a
77
+ * scene-builder hands each leaf (see
78
+ * `.claude/agent-memory/frqtl-experiment-coder/reference_concentric_ring_cone_vs_cylinder_law.md`).
79
+ *
80
+ * Sustained emission is a HOST TICK-FN pushed to
81
+ * `universe.experimentTickFunctions` — the SAME framework hook
82
+ * Blackbody's thermostat already uses
83
+ * (architect guardrail R1: no new engine hook, no native emit-spec). Because
84
+ * each successive ring burst is younger (has traveled less z), a running scene
85
+ * shows a series of receding ring-shells at increasing z-depth — a solid-
86
+ * looking collimated tube (spread=0) or an opening flashlight cone (spread>0).
87
+ *
88
+ * Fast-path regime (task-specified): no `withInterQuantumCollisions`, no
89
+ * `brownianMotion`, no top-down auto-spawn ladder — `activeAxes`
90
+ * `{x:true,y:true,z:true}` only. Leaves drift forever once emitted (M5); there
91
+ * is no sink here, so `quantum_limit`-style population bounding is left to the
92
+ * page/GYM run duration, matching `particle-walk-3d`'s own precedent (no sink,
93
+ * `wrap:false`-equivalent — leaves simply exit `universe_rect` and stop
94
+ * appearing, per `reference_population_bounded_by_universe_edge_not_only_sink.md`).
95
+ */
96
+
97
+ import { assertSetupOptions as _assertSetupOptions, requireWorldRect as _requireWorldRect } from './_setupOptions.js';
98
+
99
+ // The ONE unknown-option-key validator (objects/setup/_setupOptions.js). Resolved
100
+ // LAZILY in the browser: the game layer defines window.assertSetupOptions before any
101
+ // page script runs, but reading it at eval time would bake in a load-order dependency.
102
+
103
+ /**
104
+ * OPTIONS — this factory's own option vocabulary: EXACTLY the keys it reads.
105
+ * Exported as `setupConeEmitter3D.OPTIONS` so a composing caller consumes it instead of
106
+ * retyping it. `setup-factory-option-vocabulary.test.mjs` re-derives the
107
+ * read-set by static trace and fails if this list drifts from the code.
108
+ */
109
+ var OPTIONS = [
110
+ 'emitEveryTicks', 'fundamentalWaveLength', 'leavesPerRing', 'ringCount',
111
+ 'ringRadius', 'spread', 'vz',
112
+ ];
113
+
114
+ function setupConeEmitter3D(universe, options) {
115
+ options = _assertSetupOptions('setupConeEmitter3D', options, OPTIONS);
116
+
117
+ // The world rect the co-init call wrote (EGPTUniverse.create({rect})) — the ONE size.
118
+ var canvas_rect = universe.universe_rect;
119
+ _requireWorldRect('setupConeEmitter3D universe.universe_rect', canvas_rect);
120
+
121
+ var desiredWaveLength = options.fundamentalWaveLength !== undefined
122
+ ? options.fundamentalWaveLength : 64;
123
+ var waveLengthMultiplier = Math.max(1, Math.floor(desiredWaveLength / 4));
124
+
125
+ // ── Universe init: 3D, fast-path (no collisions/brownian/top-down frame-gen) ──
126
+ universe.init(0, {
127
+ wavelengthScale: waveLengthMultiplier,
128
+ brownianMotion: false,
129
+ activeAxes: { x: true, y: true, z: true }
130
+ });
131
+
132
+ var cx0 = Math.floor(canvas_rect.w / 2);
133
+ var cy0 = Math.floor(canvas_rect.h / 2);
134
+ var cz0 = 0;
135
+
136
+ // ── Live-mutable emission state (closed over by the tick-fn AND the
137
+ // returned control handle — the canonical live-knob pattern:
138
+ // the blackbody scene's knob calls OvenBox.setTemperature, which sets the
139
+ // target the thermostat tick-fn reads on its NEXT evaluation, never the engine
140
+ // directly). Values here are read fresh on every emitted ring, so a
141
+ // slider drag changes the NEXT burst's leaves — already-emitted
142
+ // leaves keep drifting on whatever velocity they were stamped with
143
+ // (M5: velocity is conserved — there is no retroactive rewrite). ──
144
+ var state = {
145
+ spread: options.spread !== undefined ? options.spread : 0.6,
146
+ vz: options.vz !== undefined ? options.vz : 1,
147
+ ringCount: options.ringCount !== undefined ? options.ringCount : 4,
148
+ ringRadius: options.ringRadius !== undefined ? options.ringRadius : 40,
149
+ leavesPerRing: options.leavesPerRing !== undefined ? options.leavesPerRing : 20,
150
+ active: true
151
+ };
152
+ var emitEveryTicks = options.emitEveryTicks !== undefined ? options.emitEveryTicks : 6;
153
+
154
+ // Mirrors bench.cjs's leafV()/ringLeaves() exactly (canonical-bench S2/S3) —
155
+ // re-applying vx/vy/vz AFTER injectFrame is the documented safe idiom.
156
+ function emitOneRing(radiusPx) {
157
+ var N = state.leavesPerRing;
158
+ for (var i = 0; i < N; i++) {
159
+ // HALF-STEP ANGULAR OFFSET (i + 0.5), not i: keep EVERY leaf off the
160
+ // exact cardinal axes. At the bare theta=2πi/N sampling, i=0 lands at
161
+ // theta=0 (vx=spread, vy=EXACTLY 0) and i=N/4,N/2,3N/4 land with a
162
+ // ~1e-16 float residual on their off-axis component — and the engine's
163
+ // move_frame classifies the propagation axis by a BIT-EXACT vx!=0.0 /
164
+ // vy!=0.0 test, so those degenerate leaves get NO transverse wave on the
165
+ // (bit-)zero axis and stay plane-confined, projecting as sharp bright
166
+ // axis-aligned LINES (CEO-flagged, 2026-07-16). The +0.5 offset gives
167
+ // every leaf a meaningfully-nonzero (vx,vy) pair so it fans into the cone
168
+ // VOLUME instead of a plane. |v_radial|=spread is unchanged by a pure
169
+ // angular rotation, so the S3 half-angle law atan(spread/vz) is preserved.
170
+ // Pure SUGAR fix (canonical-bench SU1) — zero engine change; the raw-engine
171
+ // S2/S3 goldens in bench.cjs keep their own 2πi/N sampling untouched.
172
+ var theta = (2 * Math.PI * (i + 0.5)) / N;
173
+ var lx = cx0 + radiusPx * Math.cos(theta);
174
+ var ly = cy0 + radiusPx * Math.sin(theta);
175
+ var vx = state.spread * Math.cos(theta);
176
+ var vy = state.spread * Math.sin(theta);
177
+ var h = universe.makeLeafFrame(lx, ly, 0, null, vx, vy, false, 0, null);
178
+ universe.injectFrame(h);
179
+ var q = universe.getFrameQuantumId(h);
180
+ universe.setFrameProperty(q, 'z', cz0);
181
+ universe.setFrameProperty(q, 'vz', state.vz);
182
+ universe.setFrameProperty(q, 'vx', vx);
183
+ universe.setFrameProperty(q, 'vy', vy);
184
+ }
185
+ }
186
+
187
+ function emitBurst() {
188
+ for (var r = 1; r <= state.ringCount; r++) {
189
+ emitOneRing(state.ringRadius * r / state.ringCount);
190
+ }
191
+ }
192
+
193
+ var _tick = 0;
194
+ var _tickFn = function () {
195
+ if (state.active && (_tick % emitEveryTicks === 0)) { emitBurst(); }
196
+ _tick++;
197
+ };
198
+ universe.experimentTickFunctions.push(_tickFn);
199
+
200
+ // ── Live sugar control handle (the canonical knob pattern) ──────────────
201
+ var control = {
202
+ setSpread: function (v) { state.spread = v; },
203
+ getSpread: function () { return state.spread; },
204
+ setVz: function (v) { state.vz = v; },
205
+ getVz: function () { return state.vz; },
206
+ setRingCount: function (v) { state.ringCount = Math.max(1, Math.round(v)); },
207
+ getRingCount: function () { return state.ringCount; },
208
+ setRingRadius: function (v) { state.ringRadius = v; },
209
+ getRingRadius: function () { return state.ringRadius; },
210
+ setActive: function (v) { state.active = !!v; },
211
+ dispose: function () {
212
+ var i = universe.experimentTickFunctions.indexOf(_tickFn);
213
+ if (i !== -1) { universe.experimentTickFunctions.splice(i, 1); }
214
+ }
215
+ };
216
+
217
+ // Standardized live knobs (runner.experimentControls-compatible — the SAME
218
+ // shape SphereEmitter.controls / g3-lo-collide's CONTROLS array use).
219
+ control.controls = [
220
+ {
221
+ id: 'spread', label: 'spread', min: 0, max: 2, step: 0.05, value: state.spread,
222
+ fmt: function (v) { return v.toFixed(2); },
223
+ onChange: function (v) { control.setSpread(v); }
224
+ },
225
+ {
226
+ id: 'vz', label: 'vz (forward)', min: 0.1, max: 4, step: 0.1, value: state.vz,
227
+ fmt: function (v) { return v.toFixed(1); },
228
+ onChange: function (v) { control.setVz(v); }
229
+ },
230
+ {
231
+ id: 'ringRadius', label: 'ring radius', min: 10, max: 120, step: 5, value: state.ringRadius,
232
+ fmt: function (v) { return String(v); },
233
+ onChange: function (v) { control.setRingRadius(v); }
234
+ },
235
+ {
236
+ id: 'ringCount', label: 'rings', min: 1, max: 10, step: 1, value: state.ringCount,
237
+ fmt: function (v) { return String(v); },
238
+ onChange: function (v) { control.setRingCount(v); }
239
+ }
240
+ ];
241
+
242
+ return {
243
+ coneEmitterControl: control,
244
+ largeObjects: []
245
+ };
246
+ }
247
+
248
+ setupConeEmitter3D.OPTIONS = OPTIONS;
249
+
250
+ export { setupConeEmitter3D };
251
+
@@ -0,0 +1,327 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * setupDoubleSlit — Engine-agnostic double-slit experiment factory.
5
+ * Creates the complete DSE setup: slit screen, light source, detector wall.
6
+ *
7
+ * Engine-agnostic: works with both the JS reference engine and the Rust/WASM engine.
8
+ */
9
+
10
+ import { assertSetupOptions as _assertSetupOptions, requireWorldRect as _requireWorldRect } from './_setupOptions.js';
11
+ import { SlitScreen } from '../../parts/SlitScreen.js';
12
+ import { SlitLightSource } from '../../parts/SlitLightSource.js';
13
+ import { DetectorWall } from '../../parts/DetectorWall.js';
14
+ import { LargeObject } from '../../parts/LargeObject.js';
15
+ import { CollisionTypes } from '../../parts/CollisionTypes.js';
16
+
17
+ // The ONE unknown-option-key validator (objects/setup/_setupOptions.js). Resolved
18
+ // LAZILY in the browser: the game layer defines window.assertSetupOptions before any
19
+ // page script runs, but reading it at eval time would bake in a load-order dependency.
20
+
21
+ // ── Imports ─────────────────────────────────────────────────────────
22
+
23
+ // ── Setup Factory ───────────────────────────────────────────────────
24
+
25
+ /**
26
+ * Create a double-slit experiment.
27
+ *
28
+ * Assembles the complete DSE: a radial light source ({@link SlitLightSource}),
29
+ * a two-slit barrier ({@link SlitScreen}), and a counting detector wall
30
+ * ({@link DetectorWall}). There is no wave equation — each leaf Frame makes a
31
+ * local 1px/tick choice. The interference pattern at the detector wall is
32
+ * emergent from where the two particle streams overlap after passing through
33
+ * the slits.
34
+ *
35
+ * ## Physics you can drive
36
+ *
37
+ * **`options.wavelength`** is the single geometry master knob (default 64).
38
+ * It simultaneously scales every physical dimension of the experiment:
39
+ *
40
+ * | Derived quantity | Formula |
41
+ * |---|---|
42
+ * | Slit opening height | `floor(wavelength * 0.8)` px |
43
+ * | Inter-slit gap height | `floor(wavelength * 0.8)` px |
44
+ * | Slit screen left edge | `wavelength * 1.5` px from canvas left |
45
+ * | Detector wall x | `screenLeft + screenWidth + 3 * wavelength` px |
46
+ * | Universe `wavelengthScale` (derived) | `floor(wavelength / 16)` |
47
+ *
48
+ * Raising `wavelength` enlarges slits, widens the gap, moves the detector
49
+ * further right, and increases the universe oscillation period. The canonical
50
+ * operating point for a 600×600 canvas is `wavelength = 64`: at this value
51
+ * the complete geometry (source → screen → detector) fits within the canvas,
52
+ * both slits receive balanced illumination (slit1/slit2 hit ratio ≈ 1.0), and
53
+ * a clear multi-band interference pattern emerges on the detector histogram.
54
+ *
55
+ * Below 64: geometry shrinks; the slits become too small for balanced
56
+ * illumination and the pattern is suppressed. Above 64 on a 600×600 canvas:
57
+ * geometry exceeds the canvas and the detector wall falls off-screen.
58
+ *
59
+ * Observed sweep (engine = js, seed 42, tick 400):
60
+ *
61
+ * | `wavelength` | `wavelengthScale` | Active quanta | Slit1/Slit2 ratio | Field |
62
+ * |---|---|---|---|---|
63
+ * | 16 | 1 | ~556 | 0.04 | pattern compressed into left sixth of canvas |
64
+ * | 32 | 2 | ~2032 | 0.44 | moderate spread, unbalanced slit hits |
65
+ * | **64** (default) | **4** | ~3601 | **0.98** | full canvas, clear interference bands |
66
+ * | 128 | 8 | ~6029 | — | geometry exceeds canvas; detector not reached at tick 400 |
67
+ *
68
+ * Figure + screenshots: `docs/internal/physics-lab/double-slit-wavelength.md`.
69
+ *
70
+ * **Disambiguate from wave-interference `wavelengthScale`:** the DSE
71
+ * `wavelength` scales PHYSICAL GEOMETRY (moves walls). The wave-interference
72
+ * `wavelengthScale` in `setupWaveInterference` only scales the per-Frame
73
+ * oscillation period without moving anything. In the DSE, `wavelengthScale`
74
+ * is a derived quantity — it is set internally as `floor(wavelength / 16)` and
75
+ * is not a free input unless you pass `options.wavelengthScale` explicitly.
76
+ *
77
+ * **`options.detectAtSlits`** controls the measurement mode at the slit openings:
78
+ * - `0` — no detection (slits are transparent pass-through zones)
79
+ * - `1` (default) — detect + scatter (`RANDOM_XY` collision type) — the act of
80
+ * detection disturbs the particle's trajectory (canonical "which-slit" measurement)
81
+ * - `2` — detect + color only (no trajectory disturbance); used for visualization
82
+ *
83
+ * @param {Object} universe - The engine universe instance (JS or WASM), not yet init'd.
84
+ * @param {Object} [options] - Configuration overrides.
85
+ * @param {number} [options.wavelength=64] - Geometry master knob: scales slit size,
86
+ * inter-slit gap, wall positions, detector distance, and derived
87
+ * `wavelengthScale`. See "Physics you can drive" above.
88
+ * @param {number} [options.fundamentalDimension=2] - Fundamental (leaf) dimension layer.
89
+ * @param {number} [options.detectAtSlits=1] - Slit detection mode: 0=none,
90
+ * 1=detect+scatter (RANDOM_XY), 2=detect+color only. Mode 1 is the canonical
91
+ * "which-slit" measurement.
92
+ * @param {boolean} [options.absorbAtSlitWall=true] - If true, wall segments absorb
93
+ * particles on contact (no reflection). If false, walls block (reflect) particles.
94
+ * @param {number} [options.wavelengthScale] - Override the derived universe
95
+ * `wavelengthScale` (normally `floor(wavelength/16)`). Use only when you want
96
+ * to decouple the oscillation scale from the geometry scale.
97
+ * @param {number} [options.angleIncrement=10] - Degrees between emission angles
98
+ * from the light source. Smaller = more rays = smoother illumination cone;
99
+ * larger = coarser, more visible discrete rays.
100
+ * @param {number} [options.depth] - WS-DSE-3D: total z-extent for a 3D run.
101
+ * OMITTED (default) → today's 2D behavior, byte-identical — no `activeAxes`
102
+ * key touched, every wall/screen/detector rect stays the legacy 4-element form,
103
+ * the light source's inner PointSource gets no z-draw at all. When provided:
104
+ * `universe.init()` gains `activeAxes:{x:true,y:true,z:true}` (mirrors
105
+ * `setupWaveInterference`'s `options.depth`, WS-DSE-3D Bite B0); `SlitScreen`
106
+ * becomes a FLOOR-TO-CEILING 3D wall (CEO geometry amendment, 2026-07-12):
107
+ * `wall1`/`wall2`/`wall3` span the full z-depth `[-depth/2,+depth/2]`, and
108
+ * each slit becomes a CUBIC HOLE (bounded in BOTH y and z, see
109
+ * `options.holeDepth`) tiled from a solid near/far z-side pair around the
110
+ * aperture — NOT a full-depth-in-z slot (supersedes the earlier Bite B1
111
+ * construction). The start-wall and detector wall get the SAME full-z-depth
112
+ * treatment (else z-spread quanta fly around/past them); `SlitLightSource`
113
+ * threads `zMin=-depth/2, zDepth=depth` into its inner PointSource — the
114
+ * IDENTICAL shared z-spread mechanism Bite B0 built for
115
+ * `setupWaveInterference` (`emitterProgram.js`'s opt-in `em.zMin`/`em.zDepth`),
116
+ * reused not forked. PHYSICS EXPECTATION (restated, CEO amendment): the
117
+ * detector pattern is genuinely 2D — y-interference fringes × a z
118
+ * aperture-diffraction envelope (concentrated near the hole's z-band, NOT
119
+ * uniform across all z-bands — the old "every z-band identical" self-check
120
+ * is superseded).
121
+ * @param {number} [options.holeDepth] - 3D only: z-extent of each cubic hole
122
+ * (centered on z=0). Default: the EFFECTIVE slit height (square-ish aperture
123
+ * at the default wavelength — see `SlitScreen`'s docstring for the exact
124
+ * value). Ignored when `options.depth` is omitted.
125
+ * @param {'current'|'noreset'|'symmetric'|'combined'|'bounce'|'ejectY'|'ejectYReflect'} [options.slitScatter='current']
126
+ * EXPERIMENT LOOP (2026-07-12, CEO physics briefing): selects the slit
127
+ * liner's edge-diffraction scatter behavior. `'current'` (default, OMIT to
128
+ * keep it) is today's exact compiled `RANDOM_XY` — byte-identical, the
129
+ * CEO-ratified 2D golden (seed42/wl64/detect1/t400 -> 103/63/40) depends on
130
+ * it. See `SlitScreen`'s `_buildLinerVelocityOps` docstring for
131
+ * `noreset`/`symmetric`/`combined`/`bounce`, and `_buildEjectYOps`'s
132
+ * docstring for `ejectY`/`ejectYReflect` (MATRIX EXTENSION, 2026-07-12 — the
133
+ * standard block+eject idiom other LOs use, Y-into-the-cavity). Works
134
+ * identically on the 2D and 3D (`options.depth`) construction paths.
135
+ * @param {number} [options.gapHeight] - DSE LAB (2026-07-13): inter-slit gap
136
+ * override — the "slit gap"/"slit-tunnel width" (the interference formula's
137
+ * `d`, read back via `interferenceParams.d`). OMITTED (default) -> SlitScreen's
138
+ * own default `floor(wavelength*0.8)`, byte-identical to every existing caller.
139
+ * REINIT-class knob: a live UI change must restart the experiment with a new
140
+ * value (no live LO rect-surgery path in v1).
141
+ * @returns {Object} `{ slitScreen, slitLightSource, detectorWall, startWall,
142
+ * interferenceParams: { d, L, wavelength, depth? } }` where `d` = inter-slit gap
143
+ * height, `L` = distance from slit screen face to detector wall, and `depth`
144
+ * is present ONLY when `options.depth` was set (3D run).
145
+ *
146
+ * @example
147
+ * // Two-cell notebook pattern (declarative-canvas model):
148
+ * // module cell:
149
+ * export function setupDS(frqtl, universe, opts) {
150
+ * return setupDoubleSlit(universe, {
151
+ * wavelength: opts.wavelength ?? 64,
152
+ * detectAtSlits: opts.detectAtSlits ?? 1,
153
+ * canvasRect: universe.universe_rect
154
+ * });
155
+ * }
156
+ * // canvas cell: setup="<module>#setupDS" opts='{"wavelength":64}' stage="600x600" seed="42"
157
+ */
158
+ /**
159
+ * OPTIONS — this factory's own option vocabulary: EXACTLY the keys it reads.
160
+ * Exported as `setupDoubleSlit.OPTIONS` so a composing caller consumes it instead of
161
+ * retyping it. `setup-factory-option-vocabulary.test.mjs` re-derives the
162
+ * read-set by static trace and fails if this list drifts from the code.
163
+ */
164
+ var OPTIONS = [
165
+ 'absorbAtSlitWall', 'angleIncrement', 'depth',
166
+ 'detectAtSlits', 'fundamentalDimension', 'gapHeight', 'holeDepth',
167
+ 'slitScatter', 'wavelength', 'wavelengthScale',
168
+ ];
169
+
170
+ function setupDoubleSlit(universe, options) {
171
+ options = _assertSetupOptions('setupDoubleSlit', options, OPTIONS);
172
+
173
+ var wavelength = options.wavelength !== undefined ? options.wavelength : 64;
174
+ var fundamentalDimension = options.fundamentalDimension !== undefined ? options.fundamentalDimension : 2;
175
+ var detectAtSlits = options.detectAtSlits !== undefined ? options.detectAtSlits : 1;
176
+ var absorbAtSlitWall = options.absorbAtSlitWall !== undefined ? options.absorbAtSlitWall : true;
177
+ var canvasRect = universe.universe_rect;
178
+ // The world height, through the ONE world-rect normalizer (throws on a missing or
179
+ // non-positive dimension — a zero-high screen would count nothing, silently).
180
+ var canvasH = _requireWorldRect('setupDoubleSlit universe.universe_rect', canvasRect).h;
181
+ var angleIncrement = options.angleIncrement !== undefined ? options.angleIncrement : 10;
182
+ // WS-DSE-3D: opt-in 3D depth. undefined/null → 2D, byte-identical.
183
+ var depth = (options.depth !== undefined && options.depth !== null) ? options.depth : null;
184
+ // WS-DSE-3D CEO geometry amendment: cubic-hole z-extent. undefined -> SlitScreen
185
+ // applies its own default (the effective slit_height). 2D-inert.
186
+ var holeDepth = (options.holeDepth !== undefined && options.holeDepth !== null) ? options.holeDepth : undefined;
187
+ // EXPERIMENT LOOP (2026-07-12): liner edge-diffraction scatter variant.
188
+ // 'current' (default/omitted) -> today's exact RANDOM_XY, byte-identical —
189
+ // the CEO-ratified 2D golden depends on this. Works identically in 2D and 3D.
190
+ var slitScatter = options.slitScatter !== undefined ? options.slitScatter : 'current';
191
+ // DSE LAB (2026-07-13): inter-slit gap override ("slit gap" / "slit-tunnel
192
+ // width" — the interference formula's `d`). undefined (default) -> SlitScreen's
193
+ // own default (floor(wavelength*0.8)), byte-identical to every existing caller.
194
+ // REINIT-class: a live change restarts the experiment with a new value; there
195
+ // is no live LO rect-surgery path (v1).
196
+ var gapHeight = (options.gapHeight !== undefined && options.gapHeight !== null) ? options.gapHeight : undefined;
197
+
198
+ // Wavelength constant: default to floor(wavelength / 16) matching original
199
+ var wavelengthScale = options.wavelengthScale !== undefined
200
+ ? options.wavelengthScale
201
+ : Math.floor(wavelength / 16);
202
+
203
+ // Set wavelength constant on universe
204
+ universe.wavelengthScale = wavelengthScale;
205
+
206
+ // Init universe: ground-up mode, brownianMotion:false (the wave-walk). activeAxes is ONLY
207
+ // attached when a 3D depth was requested — omission keeps the exact init()
208
+ // call every existing 2D caller (incl. the settlement CLI/golden) gets.
209
+ var initOpts = {
210
+ wavelengthScale: wavelengthScale,
211
+ brownianMotion: false,
212
+ };
213
+ if (depth !== null) {
214
+ initOpts.activeAxes = { x: true, y: true, z: true };
215
+ }
216
+ universe.init(fundamentalDimension, initOpts);
217
+
218
+ // ── Slit Screen ──────────────────────────────────────────────────
219
+ var slitScreenLeft = wavelength * 1.5;
220
+ var slitScreen = new SlitScreen(
221
+ universe,
222
+ slitScreenLeft,
223
+ wavelength,
224
+ canvasH,
225
+ wavelength,
226
+ absorbAtSlitWall,
227
+ detectAtSlits,
228
+ undefined, // slitHeight override (none)
229
+ gapHeight, // DSE LAB: inter-slit gap override (undefined -> SlitScreen default)
230
+ depth, // WS-DSE-3D: null (default) -> legacy 4-elem rects
231
+ holeDepth, // WS-DSE-3D CEO geometry amendment: cubic-hole z-extent (2D-inert)
232
+ slitScatter // EXPERIMENT LOOP: 'current' (default) -> byte-identical liner behavior
233
+ );
234
+
235
+ // ── Start Wall (absorb wall at left edge) ───────────────────────
236
+ // WS-DSE-3D Bite B1: full-z-depth 6-elem rect when depth is set (Tube.js
237
+ // precedent) — without it, z-spread quanta fly around a paper-thin (z=0,d=w)
238
+ // start wall. depth===null (default) -> legacy 4-elem rect, byte-identical.
239
+ var startWallRect = (depth !== null)
240
+ ? [0, 0, -depth / 2, 10, canvasH, depth]
241
+ : [0, 0, 10, canvasH];
242
+ var startWall = new LargeObject(
243
+ universe,
244
+ startWallRect,
245
+ CollisionTypes.ABSORB,
246
+ // RECOLOR removed: deleted in the engine-no-color cut (I1). Stale dead reference.
247
+ // The ABSORB collision type already absorbs frames; no action needed here.
248
+ [],
249
+ 'start_wall',
250
+ 'closest',
251
+ null,
252
+ 1,
253
+ [120, 120, 120]
254
+ );
255
+ // WS-DSE-3D fix-loop item 1 (render): the Tube.js precedent — a 6-element
256
+ // rect gives the ENGINE a real z-slab, but _buildLoMesh (three-provider.js)
257
+ // only builds a 3D BoxGeometry when the LO ALSO carries the render-only
258
+ // `shape='box'` + `depth` hint (see SlitScreen.js for the identical fix).
259
+ if (startWall.rect.d !== undefined) {
260
+ startWall.shape = 'box';
261
+ startWall.depth = startWall.rect.d;
262
+ }
263
+
264
+ // ── Light Source ─────────────────────────────────────────────────
265
+ var slitLightSource = new SlitLightSource(
266
+ universe,
267
+ slitScreen,
268
+ angleIncrement,
269
+ undefined, // quantum_limit (default)
270
+ undefined, // delay (default)
271
+ { depth: depth } // WS-DSE-3D Bite B1: null (default) -> no z-draw at all
272
+ );
273
+
274
+ // ── Detector Wall ────────────────────────────────────────────────
275
+ var detectorWallX = slitScreen.wall1.rect.left + slitScreen.wall1.rect.w + 3 * wavelength;
276
+ var detectorWall = new DetectorWall(
277
+ universe,
278
+ canvasRect,
279
+ detectorWallX,
280
+ undefined, undefined, undefined, undefined, undefined, undefined,
281
+ { depth: depth } // WS-DSE-3D Bite B1: null (default) -> legacy 4-elem rect
282
+ );
283
+
284
+ // ── Tick function ────────────────────────────────────────────────
285
+ var tickFunction = slitLightSource.getTickFunction();
286
+ universe.experimentTickFunctions.push(tickFunction);
287
+
288
+ // ── Interference parameters (for chart/analysis) ─────────────────
289
+ var d_distance_between_slits = slitScreen.slit_wall_gap_height;
290
+ var L_distance_slits_to_screen = detectorWall.x - (slitScreen.wall1.rect.left + slitScreen.wall1.rect.w);
291
+
292
+ // ── Collect all large objects for rendering ───────────────────────
293
+ var largeObjects = [];
294
+ // Slit screen walls + slit liners
295
+ for (var wi = 0; wi < slitScreen.walls.length; wi++) {
296
+ largeObjects.push(slitScreen.walls[wi]);
297
+ }
298
+ largeObjects.push(startWall);
299
+ largeObjects.push(detectorWall.detector_wall);
300
+
301
+ // WS-DSE-3D Bite B1 (ratified §2.1): depth key present ONLY on a 3D run —
302
+ // a 2D interferenceParams object has the EXACT SAME key set as pre-B1.
303
+ var interferenceParams = {
304
+ d: d_distance_between_slits,
305
+ L: L_distance_slits_to_screen,
306
+ wavelength: wavelength
307
+ };
308
+ if (depth !== null) {
309
+ interferenceParams.depth = depth;
310
+ }
311
+
312
+ return {
313
+ slitScreen: slitScreen,
314
+ slitLightSource: slitLightSource,
315
+ detectorWall: detectorWall,
316
+ startWall: startWall,
317
+ interferenceParams: interferenceParams,
318
+ largeObjects: largeObjects
319
+ };
320
+ }
321
+
322
+ // ── Export ───────────────────────────────────────────────────────────
323
+
324
+ setupDoubleSlit.OPTIONS = OPTIONS;
325
+
326
+ export { setupDoubleSlit };
327
+