@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,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,277 @@
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, same object,
7
+ * same DSE-scale defaults.
8
+ *
9
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
10
+ *
11
+ * @param {Object} universe - WASM universe (not yet init'd — this factory calls init()).
12
+ * Same seeding convention as every other setup factory (no `opts.seed` — the
13
+ * seed is fixed by `FrqtlApp` before any factory runs).
14
+ * @param {Object} [opts]
15
+ * @param {number} [opts.bore=8] - Tunnel bore (`LightSource`'s own DSE-scale
16
+ * default). REINIT-CLASS.
17
+ * @param {number} [opts.length=16] - Tunnel length (`LightSource`'s own DSE-scale
18
+ * default). REINIT-CLASS.
19
+ * @param {'particleWalk'|'emitter'} [opts.source='particleWalk'] - `LightSource`'s
20
+ * own I2 default (the PRIMARY reference source). REINIT-CLASS (the two
21
+ * source mechanisms are fundamentally different constructions — see
22
+ * `LightSource.js`'s own docstring).
23
+ * @param {'random'|'positive'|'negative'} [opts.polarization='random'] - passed
24
+ * straight through to `LightSource`. REINIT-CLASS.
25
+ * @param {number} [opts.particleCount=20] - The INITIAL one-shot burst size
26
+ * hand-placed at construction (unchanged meaning/default — `LightSource`'s
27
+ * own construction-time placement, T2/I2 semantics untouched).
28
+ * @param {number} [opts.sourceDensity] - the SUSTAIN fix (without it the
29
+ * LightSource Lab decays to 0 by tick ~2000: particleWalk is a one-shot
30
+ * snapshot that never replenishes).
31
+ * Quanta per RECURRING emission event, for BOTH source modes — mirrors
32
+ * `setupTunnelLab.js`'s own `sourceDensity` exactly ("same treatment,
33
+ * layered"). Defaults to `particleCount` (the recurring bursts match the
34
+ * initial one's size unless told otherwise). `source:'particleWalk'` -> a
35
+ * NEW local host tick-fn (this factory, NOT `LightSource.js` — that class
36
+ * stays a one-shot construction for every OTHER caller, e.g. DSE Lab)
37
+ * re-places `sourceDensity` quanta at the SAME entrance pixel/velocity/
38
+ * charge convention `LightSource.js`'s own particleWalk uses (mirrored
39
+ * here, cited below). `source:'emitter'` -> `emitterSpec.burstSize`
40
+ * (quanta/tick from the underlying `QuantumEmitter`).
41
+ * @param {number} [opts.sourceRate=1] - Ticks between recurring emission
42
+ * events (mirrors `setupTunnelLab.js`'s own `sourceRate` exactly; `1` =
43
+ * every tick, the densest reachable cadence). `source:'particleWalk'` ->
44
+ * the local tick-fn's own cadence gate. `source:'emitter'` ->
45
+ * `emitterSpec.delay = max(0, sourceRate-1)` (`QuantumEmitter`'s own "ticks
46
+ * between bursts," 0-based — this mapping keeps `sourceRate=1` meaning
47
+ * "every tick" for BOTH source modes identically).
48
+ * @param {boolean} [opts.sourceActive=true] - Master on/off. LIVE
49
+ * (`setupResult.sourceState.active`) for `source:'particleWalk'` (gates the
50
+ * local recurring tick-fn). `source:'emitter'` does NOT currently wire this
51
+ * to the underlying `QuantumEmitter` (it auto-fires unconditionally once
52
+ * constructed, matching `Gun.js`'s own breech emitter — no live on/off
53
+ * toggle exists for ANY `Tube`/`DirectedTunnel` emitter today); `setupResult
54
+ * .sourceState.active` is still returned for a future caller that wants it.
55
+ *
56
+ * GUN-CHAMBER PARITY (ground-truthed `Gun.js`'s own breech
57
+ * `QuantumEmitter`, 2026-07-14): `auto_fire:true, quantum_limit:Infinity,
58
+ * delay:0` — an UNLIMITED, every-tick-firing emitter, exactly what
59
+ * `source:'emitter'` now does (`quantumLimit:Infinity` unconditionally,
60
+ * never `particleCount` — the OLD finite cap was the bug this fix removes).
61
+ * @param {string} [opts.wallBehavior='option2'] - `LightSource.tunnel`'s initial
62
+ * behavior (`DirectedTunnel.BEHAVIORS`). LIVE (`setupResult.tunnel.
63
+ * updateBehavior(newBehavior)` — the same stock `DirectedTunnel` accessor
64
+ * `tunnel-lab` uses; REPLICA DISCIPLINE means nothing LightSource-specific is
65
+ * needed here).
66
+ * @param {number} [opts.depth] - 3D toggle. OMITTED
67
+ * (default) -> 2D (byte-identical to every pre-existing caller). A positive
68
+ * value -> real z-containment (`activeAxes:{x,y,z}` on `universe.init()`,
69
+ * `LightSource.tunnel`'s own `depth`/`z=-depth/2` — `LightSource.js` ALREADY
70
+ * z-centres its `particleWalk` quanta whenever `tunnel.depth` is real, N1 fix
71
+ * precedent — zero new logic needed here beyond forwarding). Mirrors
72
+ * `setupTunnelLab.js`'s identical `opts.depth` recipe exactly (tunnel-lab and
73
+ * lightsource-lab STAY layered — same object, same 3D recipe). REINIT-CLASS.
74
+ *
75
+ * @returns {{ lightSource: LightSource, tunnel: DirectedTunnel, largeObjects: LargeObject[] }}
76
+ * `largeObjects` (= `lightSource.largeObjects` = `lightSource.tunnel.largeObjects`)
77
+ * is the canonical key `FrqtlApp.setup()` auto-wires for rendering.
78
+ *
79
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
80
+ *
81
+ * Zero new physics (canonical-bench SU1) — this factory only constructs a
82
+ * STOCK `LightSource` (itself zero new physics over `DirectedTunnel`/`Tube` — see
83
+ * those objects' own docstrings) with DSE-scale defaults, matching
84
+ * the directed-tunnel composition model §2.3's "the Gun Chamber object set, scaled
85
+ * by sugar params."`source:'particleWalk'`'s snapshot burst vs `source:'emitter'`'s
86
+ * continuous stream is the REAL, honest visual contrast between the two source
87
+ * mechanisms this page exists to demonstrate (I2's source-invariance oracle
88
+ * quantifies how close their downstream exit ensembles are — see that script's
89
+ * README section).
90
+ */
91
+
92
+ import { assertSetupOptions as _assertSetupOptions, requireWorldRect as _requireWorldRect } from './_setupOptions.js';
93
+ import { LightSource } from '../objects/LightSource.js';
94
+ import * as SustainedSource from '../objects/sustainedSource.js';
95
+
96
+ // The ONE unknown-option-key validator (objects/setup/_setupOptions.js). Resolved
97
+ // LAZILY in the browser: the game layer defines window.assertSetupOptions before any
98
+ // page script runs, but reading it at eval time would bake in a load-order dependency.
99
+
100
+ // ── Imports ─────────────────────────────────────────────────────────────
101
+
102
+ var FUNDAMENTAL_DIMENSION = 2; // matches setupTunnelLab.js / setupDoubleSlit.js / LightSource.js's own construction path
103
+
104
+ // MATCHED CONTROLS -> MATCHED PHYSICS: the
105
+ // SAME MAX_USEFUL_WAVELENGTH clamp authority setupTunnelLab.js derives its
106
+ // own ceiling from (SustainedSource.capacityAtDimension) — at identical
107
+ // FUNDAMENTAL_DIMENSION=2, this is byte-identical to setupTunnelLab.
108
+ // MAX_USEFUL_WAVELENGTH (16), so a matched-wavelength setting on both labs
109
+ // produces the SAME mass/fullness/color.
110
+ var LIGHTSOURCE_LAB_CAPACITY = SustainedSource.capacityAtDimension(FUNDAMENTAL_DIMENSION);
111
+ /**
112
+ * OPTIONS — this factory's own option vocabulary: EXACTLY the keys it reads.
113
+ * Exported as `setupLightSourceLab.OPTIONS` so a composing caller consumes it instead of
114
+ * retyping it. `setup-factory-option-vocabulary.test.mjs` re-derives the
115
+ * read-set by static trace and fails if this list drifts from the code.
116
+ */
117
+ var OPTIONS = [
118
+ 'bore', 'depth',
119
+ 'length', 'particleCount', 'polarization', 'source',
120
+ 'sourceActive', 'sourceDensity', 'sourceRate', 'wallBehavior',
121
+ 'wavelength',
122
+ ];
123
+
124
+ function setupLightSourceLab(universe, opts) {
125
+ opts = _assertSetupOptions('setupLightSourceLab', opts, OPTIONS);
126
+ // The world rect the co-init call wrote (EGPTUniverse.create({rect})) — the ONE size.
127
+ var canvasRect = universe.universe_rect;
128
+ var world = _requireWorldRect('setupLightSourceLab universe.universe_rect', canvasRect);
129
+ var cw = world.w, ch = world.h;
130
+
131
+ var bore = opts.bore !== undefined ? opts.bore : 8;
132
+ var length = opts.length !== undefined ? opts.length : 16;
133
+ var source = opts.source !== undefined ? opts.source : 'particleWalk';
134
+ var polarization = opts.polarization !== undefined ? opts.polarization : 'random';
135
+ var particleCount = opts.particleCount !== undefined ? opts.particleCount : 20;
136
+ var wallBehavior = opts.wallBehavior !== undefined ? opts.wallBehavior : 'option2';
137
+ var depth = (opts.depth !== undefined && opts.depth !== null) ? opts.depth : null;
138
+ // lightsource-lab has the SAME wavelength control tunnel-lab has, same
139
+ // default 8, same MAX_USEFUL_WAVELENGTH clamp authority: threaded into the
140
+ // INITIAL one-shot burst
141
+ // (`lsOpts.particle.wavelength`, `source:'particleWalk'`, matching
142
+ // `LightSource.js`'s own `options.particle.wavelength` field) AND into
143
+ // the recurring sustain spec below — this is THE field whose omission
144
+ // on this factory's OLD injector produced "different particle
145
+ // colors" (tunnel-lab's mass=2/fullness=0.125/red vs this page's
146
+ // mass=16/fullness=1.0/violet at otherwise-identical settings).
147
+ // lightsource-lab's λ slider governs emitter mode too — `source:
148
+ // 'emitter'` ALSO stamps `wavelength` (via `lsOpts.emitterSpec.
149
+ // wavelength` below, QuantumEmitter's new `opts.wavelength`), and is LIVE
150
+ // via the `sourceState.wavelength` accessor built further down.
151
+ var wavelength = opts.wavelength !== undefined ? opts.wavelength : 8;
152
+
153
+ // SUSTAIN fix:
154
+ // recurring emission params, ONE authority for BOTH source modes —
155
+ // mirrors setupTunnelLab.js's own sourceDensity/sourceRate/sourceActive
156
+ // exactly ("same treatment, layered"). Defaults to particleCount so the
157
+ // recurring bursts match the initial one's size unless told otherwise.
158
+ var sourceDensity = opts.sourceDensity !== undefined ? opts.sourceDensity : particleCount;
159
+ var sourceRate = opts.sourceRate !== undefined ? opts.sourceRate : 1;
160
+ var sourceActive = opts.sourceActive !== undefined ? !!opts.sourceActive : true;
161
+
162
+ var initOpts = {};
163
+ if (depth !== null) { initOpts.activeAxes = { x: true, y: true, z: true }; }
164
+ universe.init(FUNDAMENTAL_DIMENSION, initOpts);
165
+
166
+ var left = Math.round(cw * 0.1);
167
+ var top = Math.round(ch / 2 - bore / 2);
168
+
169
+ var tunnelOpts = {
170
+ left: left, top: top, length: length, width: bore,
171
+ capNegX: true, capPosX: false, // closed breech, open muzzle — Gun-Chamber convention
172
+ behavior: wallBehavior,
173
+ };
174
+ if (depth !== null) { tunnelOpts.depth = depth; tunnelOpts.z = -depth / 2; }
175
+
176
+ var lsOpts = {
177
+ source: source,
178
+ polarization: polarization,
179
+ tunnel: tunnelOpts,
180
+ };
181
+ if (source === 'particleWalk') {
182
+ lsOpts.particleCount = particleCount;
183
+ lsOpts.particle = { wavelength: wavelength };
184
+ // LightSource itself has the sustain capability through the SAME
185
+ // single authority: delegates
186
+ // the recurring re-placement to `LightSource`'s own opt-in
187
+ // `options.sustain` (-> `sustainedSource.js`'s ONE authority) —
188
+ // REPLACES this factory's own previously hand-rolled injector
189
+ // tick-fn (which "mirror[ed] setupTunnelLab.js's own injector idiom
190
+ // byte-for-byte"). `wavelength` here (density/rate/active too) feeds
191
+ // the recurring bursts; the initial one-shot burst above uses the
192
+ // SAME `wavelength` via `lsOpts.particle` — one source of truth,
193
+ // never two independently-typed values.
194
+ lsOpts.sustain = { density: sourceDensity, rate: sourceRate, active: sourceActive, wavelength: wavelength };
195
+ } else {
196
+ // GUN-CHAMBER PARITY (ground-truthed Gun.js's own breech
197
+ // QuantumEmitter, 2026-07-14): auto_fire:true, quantum_limit:
198
+ // Infinity, delay:0 — UNLIMITED. quantumLimit is now ALWAYS
199
+ // Infinity (never particleCount — that finite cap was the O5.2
200
+ // bug: the stream stopped admitting new quanta and the existing
201
+ // population eventually escaped/died with nothing to replace it).
202
+ // burstSize/delay are now driven by sourceDensity/sourceRate (the
203
+ // SAME rate-control mechanism 'particleWalk' mode uses above).
204
+ lsOpts.emitterSpec = {
205
+ burstSize: sourceDensity,
206
+ quantumLimit: Infinity,
207
+ randomDirection: false,
208
+ delay: Math.max(0, sourceRate - 1),
209
+ // lightsource-lab's λ slider governs emitter mode too: threads through
210
+ // LightSource.js's generic `options.emitterSpec` passthrough ->
211
+ // DirectedTunnel's generic `options.emitter` copy -> Tube.js's new
212
+ // `espec.wavelength` read -> QuantumEmitter's new `opts.wavelength`.
213
+ // Matches `lsOpts.particle.wavelength` above — ONE source of truth
214
+ // for BOTH source modes, never two independently-typed values.
215
+ wavelength: wavelength,
216
+ };
217
+ }
218
+
219
+ var lightSource = new LightSource(universe, lsOpts);
220
+
221
+ // sourceState: for 'particleWalk', this IS `lightSource.sustainedSource
222
+ // .sourceState` (the SAME live-mutable normalized spec `sustainedSource
223
+ // .js` returns — a UI slider mutating `sourceState.wavelength`/`density`
224
+ // /`rate`/`active` takes effect on the NEXT recurring burst, no
225
+ // reinit). For 'emitter', a plain info object PLUS a `wavelength`
226
+ // ACCESSOR that delegates to `QuantumEmitter.prototype.
227
+ // setWavelength` — so the page's wavelength slider
228
+ // (`runner.setupResult.sourceState.wavelength = v`, the SAME bare-
229
+ // assignment idiom used for BOTH modes) works identically whether the
230
+ // live source is a sustainedSource or this emitter, with zero page-level
231
+ // branching on source mode. burstSize/delay stay NOT live-wired
232
+ // (unchanged).
233
+ var sourceState;
234
+ if (source === 'particleWalk' && lightSource.sustainedSource) {
235
+ sourceState = lightSource.sustainedSource.sourceState;
236
+ } else {
237
+ sourceState = { density: sourceDensity, rate: sourceRate, active: sourceActive };
238
+ if (source === 'emitter' && lightSource.emitter) {
239
+ Object.defineProperty(sourceState, 'wavelength', {
240
+ get: function () { return lightSource.emitter.wavelength; },
241
+ set: function (v) { lightSource.emitter.setWavelength(v); },
242
+ enumerable: true,
243
+ configurable: true,
244
+ });
245
+ }
246
+ }
247
+
248
+ return {
249
+ lightSource: lightSource,
250
+ tunnel: lightSource.tunnel,
251
+ sourceState: sourceState,
252
+ // FrqtlApp.setup() (FrqtlApp.js:528) auto-wires
253
+ // the render list; today the world's own census carries them by construction.
254
+ largeObjects: lightSource.largeObjects,
255
+ // CANONICAL TEARDOWN (the reinit-
256
+ // compounding fix): delegates to `LightSource.prototype.dispose`
257
+ // — that ONE method already tears down BOTH
258
+ // `this.sustainedSource` (particleWalk+sustain) and `this.emitter`
259
+ // (emitter mode's own QuantumEmitter.dispose) — no bespoke
260
+ // teardownFns array needed here anymore.
261
+ dispose: function () { lightSource.dispose(); },
262
+ };
263
+ }
264
+
265
+ // ONE authority for the achievable wavelength ceiling at this factory's
266
+ // FUNDAMENTAL_DIMENSION — byte-identical formula/value to
267
+ // `setupTunnelLab.MAX_USEFUL_WAVELENGTH` (both derive from
268
+ // `SustainedSource.capacityAtDimension`), so `lightsource-lab/index.html`
269
+ // reads THIS instead of hand-typing "16" — matched controls on both pages.
270
+ setupLightSourceLab.FUNDAMENTAL_DIMENSION = FUNDAMENTAL_DIMENSION;
271
+ setupLightSourceLab.CAPACITY = LIGHTSOURCE_LAB_CAPACITY;
272
+ setupLightSourceLab.MAX_USEFUL_WAVELENGTH = LIGHTSOURCE_LAB_CAPACITY;
273
+
274
+ setupLightSourceLab.OPTIONS = OPTIONS;
275
+
276
+ export { setupLightSourceLab };
277
+
@@ -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
+