@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,177 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * setupG3Collide — UNIT-TEST experiment G3: "two LO voxel bodies collide head-on".
5
+ *
6
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
7
+ * @param {Object} universe - WASM universe (this experiment is wasmOnly).
8
+ * @param {Object} [opts]
9
+ * @param {number} [opts.level=6] - Dimension of the bodies (2^level px voxel cube; 6 = 64px).
10
+ * @param {number} [opts.massRatio=1] - mass_B / mass_A. 1 = equal (clean velocity swap).
11
+ * @param {number} [opts.density=1] - Fullness of BOTH boxes: mass = capacity × density.
12
+ * 1.0 = full (boxes abut, no interpenetration); <1 = partial (overlap by empty fraction).
13
+ * @param {number} [opts.offset=0] - Transverse B offset in voxel units. 0 = head-on; ~1 = glancing.
14
+ * @returns {{ aHandle, aQid, bHandle, bQid, level, voxel, massRatio, density, offset,
15
+ * capacity, aMass, midY, collideControl }}
16
+ * `collideControl` is a LIVE control handle (Blackbody shape):
17
+ * - `setMassRatio(r)` — live: updates B's mass relative to A; no reload.
18
+ * - `setDensity(d)` — live: updates BOTH bodies' fullness (mass = capacity × d), then B scaled
19
+ * by current massRatio.
20
+ * - `setOffset(o)` — live: repositions B's y centre to midY + round(o × voxel).
21
+ * - `reset()` — re-seats BOTH bodies to launch positions (initial x, vx=±1, B.y = midY
22
+ * + round(offset × voxel)) so density/offset changes restart the collision
23
+ * honestly. Reads current live state (massRatio, density, offset) so the
24
+ * new collision respects the latest knob values.
25
+ * All setters write through the property accessors (setFrameProperty) against the stored
26
+ * quantum ids (aQid, bQid) — NEVER a page reload.
27
+ *
28
+ * ── Layer 2: physics behavior (what this PROVES) ─────────────────────────────
29
+ * Two LEAF FRAMES at the SAME non-fundamental Dimension (= same voxel size, so each
30
+ * stands in for the virtual children filling its voxel) are launched toward each other
31
+ * (A: vx=+1 from the left, B: vx=-1 from the right) with speed_prob=1 so they approach
32
+ * deterministically and the COLLISION is the whole story. When their voxel extents
33
+ * overlap, the engine's voxel_collision_pass exchanges the overlap-volume momentum
34
+ * (conserved): at full overlap with equal mass the result is a perfect velocity SWAP —
35
+ * both cubes reverse and separate (perfectly-elastic head-on bounce). With massRatio≠1
36
+ * the lighter body recoils faster than the heavier one plows forward. Fundamental
37
+ * "light" quanta are NOT involved — this pass acts only on virtual-children bodies, so
38
+ * the no-collision experiments (Particle Walk, Wave Interference) are untouched.
39
+ *
40
+ * Bodies wrap at canvas edges so after a bounce they re-approach and collide again —
41
+ * a continuous ping-pong demo.
42
+ */
43
+
44
+ import { assertSetupOptions as _assertSetupOptions, requireWorldRect as _requireWorldRect } from './_setupOptions.js';
45
+
46
+ // The ONE unknown-option-key validator (objects/setup/_setupOptions.js). Resolved
47
+ // LAZILY in the browser: the game layer defines window.assertSetupOptions before any
48
+ // page script runs, but reading it at eval time would bake in a load-order dependency.
49
+
50
+ /**
51
+ * OPTIONS — this factory's own option vocabulary: EXACTLY the keys it reads.
52
+ * Exported as `setupG3Collide.OPTIONS` so a composing caller consumes it instead of
53
+ * retyping it. `setup-factory-option-vocabulary.test.mjs` re-derives the
54
+ * read-set by static trace and fails if this list drifts from the code.
55
+ */
56
+ var OPTIONS = [
57
+ 'density', 'level', 'massRatio', 'offset',
58
+ ];
59
+
60
+ function setupG3Collide(universe, opts) {
61
+ opts = _assertSetupOptions('setupG3Collide', opts, OPTIONS);
62
+ // The world rect the co-init call wrote (EGPTUniverse.create({rect})) — the ONE size.
63
+ var rect = universe.universe_rect;
64
+ var world = _requireWorldRect('setupG3Collide universe.universe_rect', rect);
65
+ var cw = world.w, ch = world.h;
66
+ var level = (opts.level !== undefined) ? opts.level : 5; // 2^5 = 32px voxel body
67
+ // Initial knob values — held in the closure so live setters compose correctly.
68
+ var massRatio = (opts.massRatio !== undefined) ? opts.massRatio : 1.0;
69
+ // density ∈ (0,1] — fullness of BOTH boxes (mass = capacity·density). 1 = full (abut, no
70
+ // interpenetration); <1 = partial (the boxes interpenetrate by their empty fraction).
71
+ var density = (opts.density !== undefined) ? opts.density : 1.0;
72
+ // transverse offset (in voxel units): B's y relative to A. 0 = head-on; ~1 = glancing.
73
+ var offset = (opts.offset !== undefined) ? opts.offset : 0.0;
74
+
75
+ universe.init(0, { wavelengthScale: 1, brownianMotion: false });
76
+ // Enable the collision passes (default is false) — G3 needs voxel_collision_pass.
77
+ universe.withInterQuantumCollisions = true;
78
+
79
+ // Register the voxel dimension so the bodies are visible cubes/blobs.
80
+ universe.addDimension(level);
81
+ var voxel = Math.pow(2, level);
82
+
83
+ // Canonical launch x-positions (re-used by reset()).
84
+ var midY = Math.floor(ch * 0.5);
85
+ var ax0 = Math.floor(cw * 0.5) - 1.5 * voxel;
86
+ var bx0 = Math.floor(cw * 0.5) + 1.5 * voxel;
87
+
88
+ // Body A — warm, moving +x from the left. wrap=true so after the bounce it wraps and
89
+ // re-approaches (a continuous demo) instead of escaping a non-reflecting boundary.
90
+ var aHandle = universe.makeLeafFrame(ax0, midY, level, null, 1, 0, true, 0, [255, 140, 70]);
91
+ universe.injectFrame(aHandle);
92
+ // makeLeafFrame returns a FrameId; set/getFrameProperty key on the QUANTUM_ID.
93
+ var aQid = universe.getFrameQuantumId(aHandle);
94
+
95
+ // Body B — cool, moving -x from the right, offset transversely for glancing blows.
96
+ var bHandle = universe.makeLeafFrame(bx0, midY + Math.round(offset * voxel), level, null, -1, 0, true, 0, [70, 160, 255]);
97
+ universe.injectFrame(bHandle);
98
+ var bQid = universe.getFrameQuantumId(bHandle);
99
+
100
+ // Density: set BOTH boxes' fullness (mass = capacity·density). The capacity is the full
101
+ // mass makeLeafFrame gave A. Then mass ratio scales B on top (asymmetric collisions).
102
+ // capacity is treated as a constant after init; density and massRatio are live-writable.
103
+ var capacity = universe.getFrameProperty(aQid, 'mass'); // full body → mass == capacity
104
+ var aMass = capacity * density;
105
+ universe.setFrameProperty(aQid, 'mass', aMass);
106
+ universe.setFrameProperty(bQid, 'mass', aMass * massRatio);
107
+
108
+ // ── Live control handle (Blackbody shape) ──────────────────────────────
109
+ // Each setter writes through the property accessors against the stored quantum ids.
110
+ // No page reload — the same shape as the blackbody scene's temperature knob
111
+ // (lib/frqtl/scenes/blackbody.js: slider onChange → oven.setTemperature, live).
112
+ var collideControl = {
113
+ /**
114
+ * Update mass_B / mass_A live.
115
+ * @param {number} r - New mass ratio (mass_B relative to mass_A).
116
+ */
117
+ setMassRatio: function (r) {
118
+ massRatio = r;
119
+ var curA = universe.getFrameProperty(aQid, 'mass');
120
+ universe.setFrameProperty(bQid, 'mass', curA * massRatio);
121
+ },
122
+
123
+ /**
124
+ * Update the fullness (density) of BOTH bodies live.
125
+ * @param {number} d - Density in (0, 1]. mass = capacity × d; B then scaled by massRatio.
126
+ */
127
+ setDensity: function (d) {
128
+ density = d;
129
+ var m = capacity * density;
130
+ universe.setFrameProperty(aQid, 'mass', m);
131
+ universe.setFrameProperty(bQid, 'mass', m * massRatio);
132
+ },
133
+
134
+ /**
135
+ * Reposition B's y-centre live (transverse offset in voxel units).
136
+ * @param {number} o - Offset in voxel units. 0 = head-on; ~1 = glancing.
137
+ */
138
+ setOffset: function (o) {
139
+ offset = o;
140
+ universe.setFrameProperty(bQid, 'y', midY + Math.round(o * voxel));
141
+ },
142
+
143
+ /**
144
+ * Re-seat BOTH bodies to their canonical launch state so the collision restarts
145
+ * honestly with the current knob values. Reads the live massRatio / density / offset
146
+ * closure state so the new run respects the latest slider positions.
147
+ */
148
+ reset: function () {
149
+ // Re-seat positions and velocities via property accessors.
150
+ universe.setFrameProperty(aQid, 'x', ax0);
151
+ universe.setFrameProperty(aQid, 'y', midY);
152
+ universe.setFrameProperty(aQid, 'vx', 1);
153
+ universe.setFrameProperty(bQid, 'x', bx0);
154
+ universe.setFrameProperty(bQid, 'y', midY + Math.round(offset * voxel));
155
+ universe.setFrameProperty(bQid, 'vx', -1);
156
+ // Re-apply current mass state (density × capacity, B scaled by massRatio).
157
+ var m = capacity * density;
158
+ universe.setFrameProperty(aQid, 'mass', m);
159
+ universe.setFrameProperty(bQid, 'mass', m * massRatio);
160
+ },
161
+
162
+ getMassRatio: function () { return massRatio; },
163
+ getDensity: function () { return density; },
164
+ getOffset: function () { return offset; }
165
+ };
166
+
167
+ return { aHandle: aHandle, aQid: aQid, bHandle: bHandle, bQid: bQid,
168
+ level: level, voxel: voxel, massRatio: massRatio,
169
+ density: density, offset: offset,
170
+ capacity: capacity, aMass: aMass, midY: midY,
171
+ collideControl: collideControl };
172
+ }
173
+
174
+ setupG3Collide.OPTIONS = OPTIONS;
175
+
176
+ export { setupG3Collide };
177
+
@@ -0,0 +1,280 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * setupLightSourceLab — LightSource ISOLATION (I2, directed-tunnel composition model
5
+ * §3 row 2): ONE `LightSource`, gun-chamber mechanics (fill / escape / dispersion) at a
6
+ * live-tunable scale — the visual half of the I2 source-invariance measurement
7
+ * (`docs/internal/physics-lab/directed-tunnel-scale-sweep/source-invariance.cjs`), same object,
8
+ * same DSE-scale defaults.
9
+ *
10
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
11
+ *
12
+ * @param {Object} universe - WASM universe (not yet init'd — this factory calls init()).
13
+ * Same seeding convention as every other setup factory (no `opts.seed` — the
14
+ * seed is fixed by `FrqtlApp` before any factory runs).
15
+ * @param {Object} [opts]
16
+ * @param {number} [opts.bore=8] - Tunnel bore (`LightSource`'s own DSE-scale
17
+ * default). REINIT-CLASS.
18
+ * @param {number} [opts.length=16] - Tunnel length (`LightSource`'s own DSE-scale
19
+ * default). REINIT-CLASS.
20
+ * @param {'particleWalk'|'emitter'} [opts.source='particleWalk'] - `LightSource`'s
21
+ * own I2 default (the CEO's PRIMARY reference source). REINIT-CLASS (the two
22
+ * source mechanisms are fundamentally different constructions — see
23
+ * `LightSource.js`'s own docstring).
24
+ * @param {'random'|'positive'|'negative'} [opts.polarization='random'] - passed
25
+ * straight through to `LightSource`. REINIT-CLASS.
26
+ * @param {number} [opts.particleCount=20] - The INITIAL one-shot burst size
27
+ * hand-placed at construction (unchanged meaning/default — `LightSource`'s
28
+ * own construction-time placement, T2/I2 semantics untouched).
29
+ * @param {number} [opts.sourceDensity] - CEO drive #2 (2026-07-14 SUSTAIN fix
30
+ * — orchestrator own-look finding: "LightSource Lab decays to 0 by tick
31
+ * ~2000, particleWalk is a one-shot snapshot that never replenishes").
32
+ * Quanta per RECURRING emission event, for BOTH source modes — mirrors
33
+ * `setupTunnelLab.js`'s own `sourceDensity` exactly ("same treatment,
34
+ * layered"). Defaults to `particleCount` (the recurring bursts match the
35
+ * initial one's size unless told otherwise). `source:'particleWalk'` -> a
36
+ * NEW local host tick-fn (this factory, NOT `LightSource.js` — that class
37
+ * stays a one-shot construction for every OTHER caller, e.g. DSE Lab)
38
+ * re-places `sourceDensity` quanta at the SAME entrance pixel/velocity/
39
+ * charge convention `LightSource.js`'s own particleWalk uses (mirrored
40
+ * here, cited below). `source:'emitter'` -> `emitterSpec.burstSize`
41
+ * (quanta/tick from the underlying `QuantumEmitter`).
42
+ * @param {number} [opts.sourceRate=1] - Ticks between recurring emission
43
+ * events (mirrors `setupTunnelLab.js`'s own `sourceRate` exactly; `1` =
44
+ * every tick, the densest reachable cadence). `source:'particleWalk'` ->
45
+ * the local tick-fn's own cadence gate. `source:'emitter'` ->
46
+ * `emitterSpec.delay = max(0, sourceRate-1)` (`QuantumEmitter`'s own "ticks
47
+ * between bursts," 0-based — this mapping keeps `sourceRate=1` meaning
48
+ * "every tick" for BOTH source modes identically).
49
+ * @param {boolean} [opts.sourceActive=true] - Master on/off. LIVE
50
+ * (`setupResult.sourceState.active`) for `source:'particleWalk'` (gates the
51
+ * local recurring tick-fn). `source:'emitter'` does NOT currently wire this
52
+ * to the underlying `QuantumEmitter` (it auto-fires unconditionally once
53
+ * constructed, matching `Gun.js`'s own breech emitter — no live on/off
54
+ * toggle exists for ANY `Tube`/`DirectedTunnel` emitter today); `setupResult
55
+ * .sourceState.active` is still returned for a future caller that wants it.
56
+ *
57
+ * GUN-CHAMBER PARITY (ground-truthed `Gun.js`'s own breech
58
+ * `QuantumEmitter`, 2026-07-14): `auto_fire:true, quantum_limit:Infinity,
59
+ * delay:0` — an UNLIMITED, every-tick-firing emitter, exactly what
60
+ * `source:'emitter'` now does (`quantumLimit:Infinity` unconditionally,
61
+ * never `particleCount` — the OLD finite cap was the bug this fix removes).
62
+ * @param {string} [opts.wallBehavior='option2'] - `LightSource.tunnel`'s initial
63
+ * behavior (`DirectedTunnel.BEHAVIORS`). LIVE (`setupResult.tunnel.
64
+ * updateBehavior(newBehavior)` — the same stock `DirectedTunnel` accessor
65
+ * `tunnel-lab` uses; REPLICA DISCIPLINE means nothing LightSource-specific is
66
+ * needed here).
67
+ * @param {number} [opts.depth] - CEO drive #2 (2026-07-14) 3D toggle. OMITTED
68
+ * (default) -> 2D (byte-identical to every pre-existing caller). A positive
69
+ * value -> real z-containment (`activeAxes:{x,y,z}` on `universe.init()`,
70
+ * `LightSource.tunnel`'s own `depth`/`z=-depth/2` — `LightSource.js` ALREADY
71
+ * z-centres its `particleWalk` quanta whenever `tunnel.depth` is real, N1 fix
72
+ * precedent — zero new logic needed here beyond forwarding). Mirrors
73
+ * `setupTunnelLab.js`'s identical `opts.depth` recipe exactly (tunnel-lab and
74
+ * lightsource-lab STAY layered — same object, same 3D recipe). REINIT-CLASS.
75
+ *
76
+ * @returns {{ lightSource: LightSource, tunnel: DirectedTunnel, largeObjects: LargeObject[] }}
77
+ * `largeObjects` (= `lightSource.largeObjects` = `lightSource.tunnel.largeObjects`)
78
+ * is the canonical key `FrqtlApp.setup()` auto-wires for rendering.
79
+ *
80
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
81
+ *
82
+ * Zero new physics (canonical-bench SU1) — this factory only constructs a
83
+ * STOCK `LightSource` (itself zero new physics over `DirectedTunnel`/`Tube` — see
84
+ * those objects' own docstrings) with DSE-scale defaults, matching
85
+ * the directed-tunnel composition model §2.3's "the Gun Chamber object set, scaled
86
+ * by sugar params."`source:'particleWalk'`'s snapshot burst vs `source:'emitter'`'s
87
+ * continuous stream is the REAL, honest visual contrast between the two source
88
+ * mechanisms this page exists to demonstrate (I2's source-invariance oracle
89
+ * quantifies how close their downstream exit ensembles are — see that script's
90
+ * README section).
91
+ */
92
+
93
+ import { assertSetupOptions as _assertSetupOptions, requireWorldRect as _requireWorldRect } from './_setupOptions.js';
94
+ import { LightSource } from '../objects/LightSource.js';
95
+ import * as SustainedSource from '../objects/sustainedSource.js';
96
+
97
+ // The ONE unknown-option-key validator (objects/setup/_setupOptions.js). Resolved
98
+ // LAZILY in the browser: the game layer defines window.assertSetupOptions before any
99
+ // page script runs, but reading it at eval time would bake in a load-order dependency.
100
+
101
+ // ── Imports ─────────────────────────────────────────────────────────────
102
+
103
+ var FUNDAMENTAL_DIMENSION = 2; // matches setupTunnelLab.js / setupDoubleSlit.js / LightSource.js's own construction path
104
+
105
+ // ws-dse-3d AC4 (CEO ruling, "MATCHED CONTROLS -> MATCHED PHYSICS"): the
106
+ // SAME MAX_USEFUL_WAVELENGTH clamp authority setupTunnelLab.js derives its
107
+ // own ceiling from (SustainedSource.capacityAtDimension) — at identical
108
+ // FUNDAMENTAL_DIMENSION=2, this is byte-identical to setupTunnelLab.
109
+ // MAX_USEFUL_WAVELENGTH (16), so a matched-wavelength setting on both labs
110
+ // produces the SAME mass/fullness/color.
111
+ var LIGHTSOURCE_LAB_CAPACITY = SustainedSource.capacityAtDimension(FUNDAMENTAL_DIMENSION);
112
+ /**
113
+ * OPTIONS — this factory's own option vocabulary: EXACTLY the keys it reads.
114
+ * Exported as `setupLightSourceLab.OPTIONS` so a composing caller consumes it instead of
115
+ * retyping it. `setup-factory-option-vocabulary.test.mjs` re-derives the
116
+ * read-set by static trace and fails if this list drifts from the code.
117
+ */
118
+ var OPTIONS = [
119
+ 'bore', 'depth',
120
+ 'length', 'particleCount', 'polarization', 'source',
121
+ 'sourceActive', 'sourceDensity', 'sourceRate', 'wallBehavior',
122
+ 'wavelength',
123
+ ];
124
+
125
+ function setupLightSourceLab(universe, opts) {
126
+ opts = _assertSetupOptions('setupLightSourceLab', opts, OPTIONS);
127
+ // The world rect the co-init call wrote (EGPTUniverse.create({rect})) — the ONE size.
128
+ var canvasRect = universe.universe_rect;
129
+ var world = _requireWorldRect('setupLightSourceLab universe.universe_rect', canvasRect);
130
+ var cw = world.w, ch = world.h;
131
+
132
+ var bore = opts.bore !== undefined ? opts.bore : 8;
133
+ var length = opts.length !== undefined ? opts.length : 16;
134
+ var source = opts.source !== undefined ? opts.source : 'particleWalk';
135
+ var polarization = opts.polarization !== undefined ? opts.polarization : 'random';
136
+ var particleCount = opts.particleCount !== undefined ? opts.particleCount : 20;
137
+ var wallBehavior = opts.wallBehavior !== undefined ? opts.wallBehavior : 'option2';
138
+ var depth = (opts.depth !== undefined && opts.depth !== null) ? opts.depth : null;
139
+ // ws-dse-3d AC4 (CEO ruling, "lightsource-lab gains the SAME wavelength
140
+ // control tunnel-lab has, same default 8, same MAX_USEFUL_WAVELENGTH
141
+ // clamp authority"): threaded into the INITIAL one-shot burst
142
+ // (`lsOpts.particle.wavelength`, `source:'particleWalk'`, matching
143
+ // `LightSource.js`'s own `options.particle.wavelength` field) AND into
144
+ // the recurring sustain spec below — this is THE field whose omission
145
+ // on this factory's OLD injector produced the CEO's "different particle
146
+ // colors" report (tunnel-lab's mass=2/fullness=0.125/red vs this page's
147
+ // mass=16/fullness=1.0/violet at otherwise-identical settings).
148
+ // ws-dse-3d AC1 (round 2, CEO ruling: "lightsource-lab's λ slider must
149
+ // govern emitter mode too") CLOSES the prior round's deferral — `source:
150
+ // 'emitter'` now ALSO stamps `wavelength` (via `lsOpts.emitterSpec.
151
+ // wavelength` below, QuantumEmitter's new `opts.wavelength`), and is LIVE
152
+ // via the `sourceState.wavelength` accessor built further down.
153
+ var wavelength = opts.wavelength !== undefined ? opts.wavelength : 8;
154
+
155
+ // CEO drive #2 SUSTAIN fix (2026-07-14, orchestrator own-look finding):
156
+ // recurring emission params, ONE authority for BOTH source modes —
157
+ // mirrors setupTunnelLab.js's own sourceDensity/sourceRate/sourceActive
158
+ // exactly ("same treatment, layered"). Defaults to particleCount so the
159
+ // recurring bursts match the initial one's size unless told otherwise.
160
+ var sourceDensity = opts.sourceDensity !== undefined ? opts.sourceDensity : particleCount;
161
+ var sourceRate = opts.sourceRate !== undefined ? opts.sourceRate : 1;
162
+ var sourceActive = opts.sourceActive !== undefined ? !!opts.sourceActive : true;
163
+
164
+ var initOpts = {};
165
+ if (depth !== null) { initOpts.activeAxes = { x: true, y: true, z: true }; }
166
+ universe.init(FUNDAMENTAL_DIMENSION, initOpts);
167
+
168
+ var left = Math.round(cw * 0.1);
169
+ var top = Math.round(ch / 2 - bore / 2);
170
+
171
+ var tunnelOpts = {
172
+ left: left, top: top, length: length, width: bore,
173
+ capNegX: true, capPosX: false, // closed breech, open muzzle — Gun-Chamber convention
174
+ behavior: wallBehavior,
175
+ };
176
+ if (depth !== null) { tunnelOpts.depth = depth; tunnelOpts.z = -depth / 2; }
177
+
178
+ var lsOpts = {
179
+ source: source,
180
+ polarization: polarization,
181
+ tunnel: tunnelOpts,
182
+ };
183
+ if (source === 'particleWalk') {
184
+ lsOpts.particleCount = particleCount;
185
+ lsOpts.particle = { wavelength: wavelength };
186
+ // ws-dse-3d AC1 (CEO ruling, "LightSource itself should gain the
187
+ // sustain capability through the SAME single authority"): delegates
188
+ // the recurring re-placement to `LightSource`'s own opt-in
189
+ // `options.sustain` (-> `sustainedSource.js`'s ONE authority) —
190
+ // REPLACES this factory's own previously hand-rolled injector
191
+ // tick-fn (which "mirror[ed] setupTunnelLab.js's own injector idiom
192
+ // byte-for-byte"). `wavelength` here (density/rate/active too) feeds
193
+ // the recurring bursts; the initial one-shot burst above uses the
194
+ // SAME `wavelength` via `lsOpts.particle` — one source of truth,
195
+ // never two independently-typed values.
196
+ lsOpts.sustain = { density: sourceDensity, rate: sourceRate, active: sourceActive, wavelength: wavelength };
197
+ } else {
198
+ // GUN-CHAMBER PARITY (ground-truthed Gun.js's own breech
199
+ // QuantumEmitter, 2026-07-14): auto_fire:true, quantum_limit:
200
+ // Infinity, delay:0 — UNLIMITED. quantumLimit is now ALWAYS
201
+ // Infinity (never particleCount — that finite cap was the O5.2
202
+ // bug: the stream stopped admitting new quanta and the existing
203
+ // population eventually escaped/died with nothing to replace it).
204
+ // burstSize/delay are now driven by sourceDensity/sourceRate (the
205
+ // SAME rate-control mechanism 'particleWalk' mode uses above).
206
+ lsOpts.emitterSpec = {
207
+ burstSize: sourceDensity,
208
+ quantumLimit: Infinity,
209
+ randomDirection: false,
210
+ delay: Math.max(0, sourceRate - 1),
211
+ // ws-dse-3d AC1 (CEO drive #2 round-2 ruling, "lightsource-lab's λ
212
+ // slider must govern emitter mode too"): threads through
213
+ // LightSource.js's generic `options.emitterSpec` passthrough ->
214
+ // DirectedTunnel's generic `options.emitter` copy -> Tube.js's new
215
+ // `espec.wavelength` read -> QuantumEmitter's new `opts.wavelength`.
216
+ // Matches `lsOpts.particle.wavelength` above — ONE source of truth
217
+ // for BOTH source modes, never two independently-typed values.
218
+ wavelength: wavelength,
219
+ };
220
+ }
221
+
222
+ var lightSource = new LightSource(universe, lsOpts);
223
+
224
+ // sourceState: for 'particleWalk', this IS `lightSource.sustainedSource
225
+ // .sourceState` (the SAME live-mutable normalized spec `sustainedSource
226
+ // .js` returns — a UI slider mutating `sourceState.wavelength`/`density`
227
+ // /`rate`/`active` takes effect on the NEXT recurring burst, no
228
+ // reinit). For 'emitter', a plain info object PLUS a `wavelength`
229
+ // ACCESSOR (ws-dse-3d AC1) that delegates to `QuantumEmitter.prototype.
230
+ // setWavelength` — so the page's wavelength slider
231
+ // (`runner.setupResult.sourceState.wavelength = v`, the SAME bare-
232
+ // assignment idiom used for BOTH modes) works identically whether the
233
+ // live source is a sustainedSource or this emitter, with zero page-level
234
+ // branching on source mode. burstSize/delay stay NOT live-wired
235
+ // (unchanged, out of THIS bite's scope).
236
+ var sourceState;
237
+ if (source === 'particleWalk' && lightSource.sustainedSource) {
238
+ sourceState = lightSource.sustainedSource.sourceState;
239
+ } else {
240
+ sourceState = { density: sourceDensity, rate: sourceRate, active: sourceActive };
241
+ if (source === 'emitter' && lightSource.emitter) {
242
+ Object.defineProperty(sourceState, 'wavelength', {
243
+ get: function () { return lightSource.emitter.wavelength; },
244
+ set: function (v) { lightSource.emitter.setWavelength(v); },
245
+ enumerable: true,
246
+ configurable: true,
247
+ });
248
+ }
249
+ }
250
+
251
+ return {
252
+ lightSource: lightSource,
253
+ tunnel: lightSource.tunnel,
254
+ sourceState: sourceState,
255
+ // FrqtlApp.setup() (FrqtlApp.js:528) auto-wires
256
+ // the render list; today the world's own census carries them by construction.
257
+ largeObjects: lightSource.largeObjects,
258
+ // CANONICAL TEARDOWN (CEO drive #2, 2026-07-14 — the reinit-
259
+ // compounding fix): delegates to `LightSource.prototype.dispose`
260
+ // (ws-dse-3d addition) — that ONE method already tears down BOTH
261
+ // `this.sustainedSource` (particleWalk+sustain) and `this.emitter`
262
+ // (emitter mode's own QuantumEmitter.dispose) — no bespoke
263
+ // teardownFns array needed here anymore.
264
+ dispose: function () { lightSource.dispose(); },
265
+ };
266
+ }
267
+
268
+ // ONE authority for the achievable wavelength ceiling at this factory's
269
+ // FUNDAMENTAL_DIMENSION — ws-dse-3d AC4: byte-identical formula/value to
270
+ // `setupTunnelLab.MAX_USEFUL_WAVELENGTH` (both derive from
271
+ // `SustainedSource.capacityAtDimension`), so `lightsource-lab/index.html`
272
+ // reads THIS instead of hand-typing "16" — matched controls on both pages.
273
+ setupLightSourceLab.FUNDAMENTAL_DIMENSION = FUNDAMENTAL_DIMENSION;
274
+ setupLightSourceLab.CAPACITY = LIGHTSOURCE_LAB_CAPACITY;
275
+ setupLightSourceLab.MAX_USEFUL_WAVELENGTH = LIGHTSOURCE_LAB_CAPACITY;
276
+
277
+ setupLightSourceLab.OPTIONS = OPTIONS;
278
+
279
+ export { setupLightSourceLab };
280
+
@@ -0,0 +1,68 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * setupOscillator — oscillator (EGPT Fractal) experiment factory.
5
+ *
6
+ * Builds a three-level frame hierarchy: observer > oscillator > quantum.
7
+ * The quantum does a random walk inside the oscillator while compress()
8
+ * pulls it toward the center, creating stable oscillation. The oscillator
9
+ * moves inside the observer, producing an emergent wave pattern.
10
+ *
11
+ * Uses the STANDARD JS-reference interface (Frame.makeLeafFrame, dimension.addFrame,
12
+ * dimension.createFrameFromRect via EgptFractal). The shim's boundary Dimension
13
+ * covers these: JS engine forwards natively; WASM engine logs+null for APIs not
14
+ * yet implemented in Rust and degrades gracefully.
15
+ */
16
+
17
+ import { assertSetupOptions as _assertSetupOptions, requireWorldRect as _requireWorldRect } from './_setupOptions.js';
18
+ import { EgptFractal } from '../objects/EgptFractal.js';
19
+
20
+ // The ONE unknown-option-key validator (objects/setup/_setupOptions.js). Resolved
21
+ // LAZILY in the browser: the game layer defines window.assertSetupOptions before any
22
+ // page script runs, but reading it at eval time would bake in a load-order dependency.
23
+
24
+ // ── Imports ─────────────────────────────────────────────────────────
25
+
26
+ // ── Setup Factory ───────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Create the oscillator (EGPT Fractal) experiment.
30
+ *
31
+ * The canvas bounds come from `universe.universe_rect` — the universe owns its rect.
32
+ *
33
+ * @param {Object} universe - The engine universe instance (JS or WASM boundary object)
34
+ * @param {Object} options - Configuration
35
+ * @returns {Object} { fractal, quantum, oscillator, observer }
36
+ */
37
+ /**
38
+ * OPTIONS — this factory's own option vocabulary: EXACTLY the keys it reads.
39
+ * Exported as `setupOscillator.OPTIONS` so a composing caller consumes it instead of
40
+ * retyping it. `setup-factory-option-vocabulary.test.mjs` re-derives the
41
+ * read-set by static trace and fails if this list drifts from the code.
42
+ */
43
+ var OPTIONS = [];
44
+
45
+ function setupOscillator(universe, options) {
46
+ options = _assertSetupOptions('setupOscillator', options, OPTIONS);
47
+
48
+ // The world rect the co-init call wrote (EGPTUniverse.create({rect})) — the ONE size.
49
+ var canvas_rect = universe.universe_rect;
50
+ _requireWorldRect('setupOscillator universe.universe_rect', canvas_rect);
51
+
52
+ var fractal = new EgptFractal(universe, canvas_rect);
53
+ fractal.setupFractal();
54
+
55
+ return {
56
+ fractal: fractal,
57
+ quantum: fractal.quanta[0],
58
+ oscillator: fractal.oscillator,
59
+ observer: fractal.observer_frame
60
+ };
61
+ }
62
+
63
+ // ── Export ───────────────────────────────────────────────────────────
64
+
65
+ setupOscillator.OPTIONS = OPTIONS;
66
+
67
+ export { setupOscillator };
68
+
@@ -0,0 +1,93 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * setupParticleWalk — Engine-agnostic particle walk experiment factory.
5
+ * Emits particles rightward from canvas center with wave-walk behavior.
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 { QuantumEmitter } from '../objects/QuantumEmitter.js';
12
+
13
+ // The ONE unknown-option-key validator (objects/setup/_setupOptions.js). Resolved
14
+ // LAZILY in the browser: the game layer defines window.assertSetupOptions before any
15
+ // page script runs, but reading it at eval time would bake in a load-order dependency.
16
+
17
+ // ── Imports ─────────────────────────────────────────────────────────
18
+
19
+ // ── Setup Factory ───────────────────────────────────────────────────
20
+
21
+ /**
22
+ * Create a particle walk experiment.
23
+ *
24
+ * @param {Object} universe - The engine universe instance (JS or WASM), not yet init'd
25
+ * @param {Object} [options] - Configuration overrides
26
+ * @param {number} [options.fundamentalDimension=0] - Base dimension
27
+ * @param {number} [options.fundamentalWaveLength=64] - Desired wavelength
28
+ * @param {number} [options.velocity=1] - Emission velocity
29
+ * @returns {Object} { emitter, largeObjects }
30
+ */
31
+ /**
32
+ * OPTIONS — this factory's own option vocabulary: EXACTLY the keys it reads.
33
+ * Exported as `setupParticleWalk.OPTIONS` so a composing caller consumes it instead of
34
+ * retyping it. `setup-factory-option-vocabulary.test.mjs` re-derives the
35
+ * read-set by static trace and fails if this list drifts from the code.
36
+ */
37
+ var OPTIONS = [
38
+ 'fundamentalDimension', 'fundamentalWaveLength', 'velocity',
39
+ ];
40
+
41
+ function setupParticleWalk(universe, options) {
42
+ options = _assertSetupOptions('setupParticleWalk', options, OPTIONS);
43
+
44
+ // The world rect the co-init call wrote (EGPTUniverse.create({rect})) — the ONE size.
45
+ var canvas_rect = universe.universe_rect;
46
+ _requireWorldRect('setupParticleWalk universe.universe_rect', canvas_rect);
47
+
48
+ var fundamentalDimension = options.fundamentalDimension !== undefined ? options.fundamentalDimension : 0;
49
+ var desiredWaveLength = options.fundamentalWaveLength !== undefined ? options.fundamentalWaveLength : 64;
50
+ var velocity = options.velocity !== undefined ? options.velocity : 1;
51
+
52
+ var waveLengthMultiplier = Math.floor(desiredWaveLength / 4);
53
+ waveLengthMultiplier = Math.max(1, waveLengthMultiplier);
54
+
55
+ var vx = velocity;
56
+ var vy = 0;
57
+
58
+ // Init universe: ground-up mode, brownianMotion:false for the wave branch
59
+ universe.init(fundamentalDimension, { wavelengthScale: waveLengthMultiplier, brownianMotion: false });
60
+ universe.minMBFillRate = 0.2;
61
+
62
+ var light_source_center_y = Math.floor(canvas_rect.h / 2);
63
+ var light_source_center_x = Math.floor(canvas_rect.w / 2);
64
+ var light_source_height = 10;
65
+
66
+ // Create emitter at center, firing rightward
67
+ var sourceRect = [light_source_center_x, light_source_center_y, 1, light_source_height];
68
+
69
+ var emitter = new QuantumEmitter(
70
+ universe,
71
+ [sourceRect],
72
+ vx, vy,
73
+ 20, // burst_size
74
+ 1, // charge = NEGATIVE (1)
75
+ true, // auto_fire
76
+ false, // not random direction
77
+ Infinity, // quantum_limit
78
+ 0, // delay
79
+ false // wrap = false (particles die at canvas boundary)
80
+ );
81
+
82
+ return {
83
+ emitter: emitter,
84
+ largeObjects: []
85
+ };
86
+ }
87
+
88
+ // ── Export ───────────────────────────────────────────────────────────
89
+
90
+ setupParticleWalk.OPTIONS = OPTIONS;
91
+
92
+ export { setupParticleWalk };
93
+