@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.2

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