@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,760 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * QuantumEmitter — Engine-agnostic emitter wrapper.
5
+ * Engine-agnostic: works with both the JS reference engine and the Rust/WASM engine.
6
+ *
7
+ * SUGAR: builds an emState bag and pushes the shared engine-agnostic emit-loop
8
+ * (emitterProgram.buildEmitterTickFn) onto universe.experimentTickFunctions, then mutates the
9
+ * emState directly (active/velocity/burst/source-rects). There is NO provider emitter ABI —
10
+ * the emit-loop is composed entirely from the generic primitives universe.random / makeLeafFrame /
11
+ * injectFrame, identical on both engines.
12
+ *
13
+ * Emits the CANONICAL spec: charge in raw form (0/1/'POSITIVE'/'NEGATIVE'/null).
14
+ * All charge translation (numeric↔string) lives in the shim providers
15
+ * (compat/providers/js-engine.js and compat/providers/wasm-engine.js).
16
+ * No engine-sniffing above the shim (§2 invariant).
17
+ *
18
+ * chargeMode / random_direction (sugar-layer modes, emitter-modes-sugar):
19
+ * chargeMode ∈ 'random' | 'positive' | 'negative' | 'both'
20
+ * random_direction ∈ boolean
21
+ *
22
+ * The sugar layer manages a SET of underlying emitter handle-programs keyed by
23
+ * (charge, random_direction), lazily built via buildEmitterTickFn (a host tick-fn).
24
+ * The chargeMode / random_direction setters toggle the active flag on the right
25
+ * emState handles — the engine itself is NEVER changed.
26
+ *
27
+ * 'both' = two co-located underlying emitters (POSITIVE + NEGATIVE), both active.
28
+ *
29
+ * Back-compat: the legacy `charge` constructor param maps onto chargeMode:
30
+ * null → 'random'
31
+ * 'POSITIVE' → 'positive'
32
+ * 'NEGATIVE' → 'negative'
33
+ * Existing experiments keep working unchanged.
34
+ */
35
+
36
+ import { LargeObject } from '../../parts/LargeObject.js';
37
+ import * as EmitterProgram from './emitterProgram.js';
38
+ import { registerKind } from '../kinds.js';
39
+ import { CollisionTypes } from '../../parts/CollisionTypes.js';
40
+ import * as ValueNormalizer from '../schema/valueNormalizer.js';
41
+ import { declaredCount } from '../ObjectFactory.js';
42
+
43
+ var rectToArray = LargeObject.rectToArray;
44
+ var buildEmitterTickFn = EmitterProgram.buildEmitterTickFn;
45
+
46
+ // ── Internal helpers ─────────────────────────────────────────────────
47
+
48
+ /**
49
+ * Map a legacy charge constructor param to a chargeMode string.
50
+ * @param {*} charge - null | 'POSITIVE' | 'NEGATIVE' | 0 | 1 | 'Positive' | 'Negative'
51
+ * @returns {'random'|'positive'|'negative'}
52
+ */
53
+ function _chargeToMode(charge) {
54
+ if (charge === null || charge === undefined) return 'random';
55
+ if (typeof charge === 'string') {
56
+ var u = charge.toUpperCase();
57
+ if (u === 'POSITIVE' || charge === 'Positive' || charge === '+') return 'positive';
58
+ if (u === 'NEGATIVE' || charge === 'Negative' || charge === '-') return 'negative';
59
+ }
60
+ if (charge === 0) return 'positive';
61
+ if (charge === 1) return 'negative';
62
+ return 'random';
63
+ }
64
+
65
+ /**
66
+ * Map a chargeMode string to the canonical charge value expected by the emitter handle-program.
67
+ * 'both' is never passed directly here — callers handle it by building two emitters.
68
+ * @param {'random'|'positive'|'negative'} mode
69
+ * @returns {null|'POSITIVE'|'NEGATIVE'}
70
+ */
71
+ function _modeToCharge(mode) {
72
+ if (mode === 'positive') return 'POSITIVE';
73
+ if (mode === 'negative') return 'NEGATIVE';
74
+ return null; // 'random'
75
+ }
76
+
77
+ /**
78
+ * Build the canonical key used to look up a handle in _emitterMap.
79
+ * @param {null|'POSITIVE'|'NEGATIVE'} charge
80
+ * @param {boolean} random_direction
81
+ * @returns {string}
82
+ */
83
+ function _handleKey(charge, random_direction) {
84
+ return String(charge) + ':' + String(!!random_direction);
85
+ }
86
+
87
+ // ── QuantumEmitter ──────────────────────────────────────────────────
88
+
89
+ /**
90
+ * Engine-agnostic emitter wrapper with chargeMode/random_direction modes.
91
+ *
92
+ * @param {Object} universe - The engine universe instance (JS or WASM)
93
+ * @param {Array<Array|Object>} source_rects - Emission regions
94
+ * @param {number} vx - Initial X velocity (voltage direction)
95
+ * @param {number} vy - Initial Y velocity
96
+ * @param {number} burst_size - Particles per fire (amperage)
97
+ * @param {*} charge - 0/1/'POSITIVE'/'NEGATIVE'/null(random) [back-compat; maps to chargeMode]
98
+ * @param {boolean} auto_fire - Whether to auto-fire each tick
99
+ * @param {boolean} random_direction - Randomize emission direction
100
+ * @param {number} quantum_limit - Max quanta to emit (Infinity = unlimited)
101
+ * @param {number} delay - Ticks between bursts
102
+ * @param {boolean} wrap - Wrap emitted particles at canvas boundaries.
103
+ * @param {Object} [opts] - the
104
+ * emitter source mode consumes the SAME normalized SOURCE_SPEC bag:
105
+ * `{ wavelength, zMin, zDepth, zIsotropy }` — the SAME trailing-`opts`
106
+ * idiom + the SAME field names/semantics `PointSource`'s own constructor
107
+ * already established for this EXACT emState contract (`emitterProgram.js`
108
+ * already reads `em.wavelength`/`em.zMin`/`em.zDepth`; QuantumEmitter
109
+ * simply never wired them). Every field OMITTED (default) — byte-identical
110
+ * for every existing caller (none pass a 12th arg).
111
+ * @param {number|null} [opts.wavelength=null] - Per-emitted-particle
112
+ * wavelength override (Engine Invariant #6 `mass=max(1,capacity/wavelength)`).
113
+ * `null` (default) = engine default (`mass=capacity`). LIVE via
114
+ * `setWavelength()`.
115
+ * @param {number|null} [opts.zMin=null] - When provided (with `opts.zDepth`),
116
+ * every emitted leaf's `z` is drawn uniformly from `[zMin, zMin+zDepth)`
117
+ * (one extra `random()` draw, opt-in only — see `emitterProgram.js`'s
118
+ * DRAW-ORDER CONTRACT). `null` (default) = no z ever written.
119
+ * @param {number|null} [opts.zDepth=null] - Depth of the uniform z-spread
120
+ * (paired with `opts.zMin`). Ignored if `opts.zMin` is omitted.
121
+ * @param {boolean} [opts.zIsotropy=false] - When BOTH this AND
122
+ * `random_direction` are true: the random-direction draw becomes a full
123
+ * 3D unit-sphere direction (azimuth + polar angle) instead of a 2D-plane
124
+ * circle, and the drawn `vz` is written via `setFrameProperty` — the
125
+ * "velocity z-symmetry consistent with its profile" half of AC1 (a
126
+ * DIRECTED emitter, `random_direction:false`, stays `vz=0` deliberately —
127
+ * collimation is unaffected regardless of this flag, matching
128
+ * `sustainedSource.js`'s own `'beam'` profile). Default `false` — zero
129
+ * extra draws for every existing `random_direction:true` caller.
130
+ *
131
+ * @deprecated V1 sugar, kept for byte-identical scenes (blackbody's OvenBox). V2 direction: SphereEmitter (needs an opt-in fixed-velocity line source that draws nothing for velocity).
132
+ */
133
+ function QuantumEmitter(universe, source_rects, vx, vy, burst_size, charge, auto_fire, random_direction, quantum_limit, delay, wrap, opts) {
134
+ // Defaults
135
+ burst_size = (burst_size !== undefined) ? burst_size : 1;
136
+ if (charge === undefined) charge = null;
137
+ auto_fire = (auto_fire !== undefined) ? auto_fire : true;
138
+ random_direction = (random_direction !== undefined) ? random_direction : false;
139
+ quantum_limit = (quantum_limit !== undefined) ? quantum_limit : Infinity;
140
+ delay = (delay !== undefined) ? delay : 0;
141
+ wrap = (wrap !== undefined) ? wrap : false;
142
+ opts = opts || {};
143
+ var wavelength = opts.wavelength !== undefined ? opts.wavelength : null;
144
+ var zMin = opts.zMin !== undefined ? opts.zMin : null;
145
+ var zDepth = opts.zDepth !== undefined ? opts.zDepth : null;
146
+ var zIsotropy = !!opts.zIsotropy;
147
+
148
+ this.universe = universe;
149
+ this._schemaKind = 'QuantumEmitter';
150
+ this.vx = vx;
151
+ this.vy = vy;
152
+ this.burst_size = burst_size;
153
+ this.auto_fire = auto_fire;
154
+ // colorOverride: render-only attribute for emitted particle color.
155
+ // Set via updateObjectProperty(lo, 'colorOverride', [r,g,b]) from the IDE.
156
+ this.colorOverride = null;
157
+ // renderShape / renderOutline (R3-1): render-only per-emitted-frame overrides,
158
+ // peers of colorOverride — the same per-frame reach SphereEmitter3D has. null default →
159
+ // no key stamped (byte-identical). Threaded through the em spec → emitterProgram → the
160
+ // makeLeafFrame WASM string_map merge stamp (render.shape / render.outline).
161
+ this.renderShape = null;
162
+ this.renderOutline = null;
163
+ // Exposed for reads (round-trip save/restore, inspector) —
164
+ // mutate via setWavelength(), never a bare assignment (mirrors vx/vy's
165
+ // own setVelocity()-only-mutation discipline).
166
+ this.wavelength = wavelength;
167
+
168
+ // Direct properties exposed so serializeDesign/_captureProps can read them for
169
+ // round-trip save/restore. Kept in sync with _specTemplate via setBurstSize etc.
170
+ this.quantum_limit = (quantum_limit === Infinity) ? null : quantum_limit;
171
+ this.delay = delay;
172
+ this.wrap = wrap;
173
+ // source_rects: normalized copy so the schema field is readable by the inspector.
174
+ this.source_rects = (source_rects || []).map(function (r) { return rectToArray(r); });
175
+
176
+ // Sugar-layer mode state (backing stores for the getters/setters below).
177
+ this._chargeMode = _chargeToMode(charge);
178
+ this._random_direction = !!random_direction;
179
+
180
+ // Shared spec template used when lazily registering underlying emitters.
181
+ this._specTemplate = {
182
+ source_rects: this.source_rects,
183
+ vx: vx,
184
+ vy: vy,
185
+ burst_size: burst_size,
186
+ auto_fire: auto_fire,
187
+ quantum_limit: this.quantum_limit,
188
+ delay: delay,
189
+ wrap: wrap,
190
+ wavelength: wavelength,
191
+ zMin: zMin,
192
+ zDepth: zDepth,
193
+ zIsotropy: zIsotropy
194
+ };
195
+
196
+ // Map of handleKey → engine handle (lazily populated).
197
+ this._emitterMap = {};
198
+
199
+ // Register the initial set of engine emitters for the starting mode.
200
+ this._ensureAndActivate(this._chargeMode, this._random_direction);
201
+
202
+ // Back-compat: expose a single .handle for callers that reference it directly.
203
+ // For 'both', this points to the POSITIVE handle (first registered).
204
+ this.handle = this._primaryHandle();
205
+ }
206
+
207
+ /**
208
+ * Register an underlying emitter for a given charge + random_direction combo
209
+ * if it does not yet exist. Returns the handle.
210
+ * @param {null|'POSITIVE'|'NEGATIVE'} charge
211
+ * @param {boolean} random_direction
212
+ * @returns {*} handle
213
+ */
214
+ QuantumEmitter.prototype._ensureHandle = function (charge, random_direction) {
215
+ var key = _handleKey(charge, random_direction);
216
+ if (this._emitterMap[key] !== undefined) return this._emitterMap[key];
217
+
218
+ // G-EMIT-2: build the emitter as an engine-agnostic handle-program. The sugar OWNS the
219
+ // emState; buildEmitterTickFn composes universe.random/makeLeafFrame/injectFrame. No
220
+ // provider registerEmitter — the tick-fn is pushed straight onto experimentTickFunctions.
221
+ var tmpl = this._specTemplate;
222
+ var em = {
223
+ sourceRects: (tmpl.source_rects || []).map(function (r) {
224
+ return Array.isArray(r) ? r : [r.left, r.top, r.w, r.h];
225
+ }),
226
+ vx: tmpl.vx || 0,
227
+ vy: tmpl.vy || 0,
228
+ burstSize: tmpl.burst_size || 1,
229
+ charge: charge, // null | 'POSITIVE' | 'NEGATIVE'
230
+ randomDirection: !!random_direction,
231
+ quantumLimit: (tmpl.quantum_limit !== undefined && tmpl.quantum_limit !== null) ? tmpl.quantum_limit : Infinity,
232
+ remainingQuanta: (tmpl.quantum_limit !== undefined && tmpl.quantum_limit !== null) ? tmpl.quantum_limit : Infinity,
233
+ delay: tmpl.delay || 0,
234
+ remainingDelay: tmpl.delay || 0,
235
+ active: (tmpl.auto_fire !== undefined) ? tmpl.auto_fire : true,
236
+ tombstoned: false,
237
+ color: this.colorOverride || null,
238
+ renderShape: this.renderShape || null, // R3-1 per-frame render.shape reach
239
+ renderOutline: (this.renderOutline != null) ? this.renderOutline : null, // R3-1 per-frame render.outline reach
240
+ wrap: !!tmpl.wrap,
241
+ wavelength: tmpl.wavelength || null,
242
+ fundamentalDim: null, // null → engine uses the universe's fundamental-dim as the leaf level
243
+ // The SAME em.zMin/em.zDepth fields PointSource already
244
+ // wires (emitterProgram.js's existing opt-in z-spread mechanism) +
245
+ // em.zIsotropy (gates the 3D unit-sphere
246
+ // random-direction draw). null/false (default) — byte-identical.
247
+ zMin: (tmpl.zMin !== undefined && tmpl.zMin !== null) ? tmpl.zMin : null,
248
+ zDepth: (tmpl.zDepth !== undefined && tmpl.zDepth !== null) ? tmpl.zDepth : null,
249
+ zIsotropy: !!tmpl.zIsotropy,
250
+ };
251
+ var tickFn = buildEmitterTickFn(this.universe, em);
252
+ // Back-reference so dispose() (Studio Controls Realignment S4) can find
253
+ // and splice OUT exactly the tick-fn(s) belonging to this emitter, without
254
+ // needing a separate id→fn registry. Harmless extra property on a function.
255
+ tickFn._emState = em;
256
+ this.universe.experimentTickFunctions.push(tickFn);
257
+ this._emitterMap[key] = em; // the emState IS the "handle" now
258
+ return em;
259
+ };
260
+
261
+ /**
262
+ * Ensure the handles required by `mode` + `random_direction` are registered,
263
+ * then activate them and deactivate all others.
264
+ * @param {'random'|'positive'|'negative'|'both'} mode
265
+ * @param {boolean} random_direction
266
+ */
267
+ QuantumEmitter.prototype._ensureAndActivate = function (mode, random_direction) {
268
+ var rd = !!random_direction;
269
+ var self = this;
270
+
271
+ // Build the list of (charge, rd) combos the new mode requires.
272
+ var required = [];
273
+ if (mode === 'both') {
274
+ required.push({ charge: 'POSITIVE', rd: rd });
275
+ required.push({ charge: 'NEGATIVE', rd: rd });
276
+ } else {
277
+ required.push({ charge: _modeToCharge(mode), rd: rd });
278
+ }
279
+
280
+ // Ensure all required handles are registered.
281
+ for (var i = 0; i < required.length; i++) {
282
+ self._ensureHandle(required[i].charge, required[i].rd);
283
+ }
284
+
285
+ // Build a lookup of which keys should be active.
286
+ var activeKeys = {};
287
+ for (var j = 0; j < required.length; j++) {
288
+ activeKeys[_handleKey(required[j].charge, required[j].rd)] = true;
289
+ }
290
+
291
+ // Activate / deactivate all known emitters by mutating the sugar-owned emState directly
292
+ // (no provider setEmitterActive — the tick-fn closes over em.active).
293
+ var allKeys = Object.keys(self._emitterMap);
294
+ for (var k = 0; k < allKeys.length; k++) {
295
+ var key = allKeys[k];
296
+ self._emitterMap[key].active = !!activeKeys[key];
297
+ }
298
+ };
299
+
300
+ /**
301
+ * Return the "primary" handle used for back-compat .handle access.
302
+ * For 'both', returns the POSITIVE handle (first registered).
303
+ * @returns {*}
304
+ */
305
+ QuantumEmitter.prototype._primaryHandle = function () {
306
+ var rd = this._random_direction;
307
+ var mode = this._chargeMode;
308
+ var candidateKey;
309
+
310
+ if (mode === 'both' || mode === 'positive') {
311
+ candidateKey = _handleKey('POSITIVE', rd);
312
+ if (this._emitterMap[candidateKey] !== undefined) return this._emitterMap[candidateKey];
313
+ }
314
+ if (mode === 'negative') {
315
+ candidateKey = _handleKey('NEGATIVE', rd);
316
+ if (this._emitterMap[candidateKey] !== undefined) return this._emitterMap[candidateKey];
317
+ }
318
+ // 'random'
319
+ candidateKey = _handleKey(null, rd);
320
+ if (this._emitterMap[candidateKey] !== undefined) return this._emitterMap[candidateKey];
321
+
322
+ // Fallback: first registered handle.
323
+ var allKeys = Object.keys(this._emitterMap);
324
+ return allKeys.length > 0 ? this._emitterMap[allKeys[0]] : null;
325
+ };
326
+
327
+ /**
328
+ * DSE LAB (2026-07-13): iterate EVERY registered emState (ALL (charge,
329
+ * random_direction) groups, active or not) — mirrors `PointSource.prototype.
330
+ * forEachHandle` exactly (SAME need: a caller-supplied field, e.g.
331
+ * `em.everyNTicks`, must apply even after a LATER chargeMode switch activates
332
+ * a currently-inactive group). Lets a control (e.g. the Lab's firing-rate
333
+ * slider) work IDENTICALLY whether the active source is a `PointSource` or a
334
+ * `QuantumEmitter` — one shared method name, no source-type branching at the
335
+ * call site.
336
+ * @param {Function} fn - called once per registered emState.
337
+ */
338
+ QuantumEmitter.prototype.forEachHandle = function (fn) {
339
+ var allKeys = Object.keys(this._emitterMap);
340
+ for (var i = 0; i < allKeys.length; i++) {
341
+ fn(this._emitterMap[allKeys[i]]);
342
+ }
343
+ };
344
+
345
+ /**
346
+ * DSE LAB (2026-07-13): return the flat array of emStates in currently-ACTIVE
347
+ * groups — mirrors `PointSource.prototype._activeHandles` exactly (SAME need:
348
+ * a manual "FIRE" trigger must only fire handles the user has actually
349
+ * activated, not every registered-but-inactive group). Lets a control (e.g.
350
+ * the Lab's FIRE button) work IDENTICALLY whether the active source is a
351
+ * `PointSource` or a `QuantumEmitter`.
352
+ * @returns {Array}
353
+ */
354
+ QuantumEmitter.prototype._activeHandles = function () {
355
+ var allKeys = Object.keys(this._emitterMap);
356
+ var result = [];
357
+ for (var i = 0; i < allKeys.length; i++) {
358
+ var em = this._emitterMap[allKeys[i]];
359
+ if (em.active) { result.push(em); }
360
+ }
361
+ return result;
362
+ };
363
+
364
+ // ── chargeMode getter/setter ──────────────────────────────────────────
365
+
366
+ /**
367
+ * chargeMode getter/setter.
368
+ *
369
+ * Setter is the sugar-layer reconfigure trigger:
370
+ * emitter.chargeMode = 'both' → activates POSITIVE + NEGATIVE underlying emitters
371
+ * emitter.chargeMode = 'random' → activates the null-charge underlying emitter
372
+ * etc.
373
+ *
374
+ * Because updateObjectProperty routes engineVisible:false props as `lo[prop] = value`,
375
+ * this setter fires automatically when the IDE calls:
376
+ * runner.updateObjectProperty(emitter, 'chargeMode', 'both')
377
+ */
378
+ Object.defineProperty(QuantumEmitter.prototype, 'chargeMode', {
379
+ get: function () { return this._chargeMode; },
380
+ set: function (mode) {
381
+ var validModes = { random: true, positive: true, negative: true, both: true };
382
+ if (!validModes[mode]) {
383
+ throw new TypeError(
384
+ '[QuantumEmitter] Invalid chargeMode "' + mode +
385
+ '". Expected: random | positive | negative | both'
386
+ );
387
+ }
388
+ this._chargeMode = mode;
389
+ this._ensureAndActivate(mode, this._random_direction);
390
+ this.handle = this._primaryHandle();
391
+ },
392
+ enumerable: true,
393
+ configurable: true
394
+ });
395
+
396
+ // ── random_direction getter/setter ───────────────────────────────────
397
+
398
+ /**
399
+ * random_direction getter/setter.
400
+ *
401
+ * Switching random_direction mid-run ensures handles for the new
402
+ * (charge, random_direction) combos are registered and activated while
403
+ * old (charge, old_rd) handles are deactivated.
404
+ */
405
+ Object.defineProperty(QuantumEmitter.prototype, 'random_direction', {
406
+ get: function () { return this._random_direction; },
407
+ set: function (rd) {
408
+ var brd = !!rd;
409
+ this._random_direction = brd;
410
+ this._ensureAndActivate(this._chargeMode, brd);
411
+ this.handle = this._primaryHandle();
412
+ },
413
+ enumerable: true,
414
+ configurable: true
415
+ });
416
+
417
+ // ── Public API ────────────────────────────────────────────────────────
418
+
419
+ /**
420
+ * Enable/disable this emitter.
421
+ * Respects chargeMode — deactivating deactivates ALL underlying handles.
422
+ * @param {boolean} active
423
+ */
424
+ QuantumEmitter.prototype.setActive = function (active) {
425
+ this.auto_fire = active;
426
+ var self = this;
427
+ if (active) {
428
+ // Reactivate only the emitters required by the current mode.
429
+ this._ensureAndActivate(this._chargeMode, this._random_direction);
430
+ } else {
431
+ // Deactivate all underlying emitters (mutate emState directly).
432
+ var allKeys = Object.keys(this._emitterMap);
433
+ for (var i = 0; i < allKeys.length; i++) {
434
+ self._emitterMap[allKeys[i]].active = false;
435
+ }
436
+ }
437
+ };
438
+
439
+ /**
440
+ * Change emission velocity (voltage change).
441
+ * Applied to ALL underlying engine handles.
442
+ * @param {number} vx
443
+ * @param {number} vy
444
+ */
445
+ QuantumEmitter.prototype.setVelocity = function (vx, vy) {
446
+ this.vx = vx;
447
+ this.vy = vy;
448
+ this._specTemplate.vx = vx;
449
+ this._specTemplate.vy = vy;
450
+ var allKeys = Object.keys(this._emitterMap);
451
+ for (var i = 0; i < allKeys.length; i++) {
452
+ this._emitterMap[allKeys[i]].vx = vx;
453
+ this._emitterMap[allKeys[i]].vy = vy;
454
+ }
455
+ };
456
+
457
+ /**
458
+ * Change burst size (amperage change).
459
+ * Applied to ALL underlying engine handles.
460
+ * @param {number} size
461
+ */
462
+ QuantumEmitter.prototype.setBurstSize = function (size) {
463
+ this.burst_size = size;
464
+ this._specTemplate.burst_size = size;
465
+ var allKeys = Object.keys(this._emitterMap);
466
+ for (var i = 0; i < allKeys.length; i++) {
467
+ this._emitterMap[allKeys[i]].burstSize = size;
468
+ }
469
+ };
470
+
471
+ /**
472
+ * The emitter source mode consumes the SAME normalized SOURCE_SPEC bag:
473
+ * change per-emitted-particle
474
+ * wavelength (Engine Invariant #6 `mass=max(1,capacity/wavelength)`) LIVE —
475
+ * mirrors `setVelocity`/`setBurstSize`'s own "write every handle" discipline
476
+ * exactly (real state lives in `emState`, mutated ONLY by a dedicated setter
477
+ * — memory `reference_quantumemitter_no_generic_write_path`). A setup
478
+ * factory's `sourceState.wavelength` accessor (see `setupLightSourceLab.js`)
479
+ * wraps this so a page's wavelength slider works identically whether the
480
+ * live source is a `sustainedSource` (particleWalk) or this emitter.
481
+ * @param {number|null} wavelength
482
+ */
483
+ QuantumEmitter.prototype.setWavelength = function (wavelength) {
484
+ this.wavelength = wavelength;
485
+ this._specTemplate.wavelength = wavelength;
486
+ var allKeys = Object.keys(this._emitterMap);
487
+ for (var i = 0; i < allKeys.length; i++) {
488
+ this._emitterMap[allKeys[i]].wavelength = wavelength;
489
+ }
490
+ };
491
+
492
+ /**
493
+ * Reposition the emitter's single canonical source rect (Studio Controls
494
+ * Realignment S4, design §2.2/§2.5.5 — the "composite emitter move"
495
+ * accessor). One path: mutates `source_rects` on the wrapper (inspector/
496
+ * serializeDesign source of truth) AND every underlying emState's
497
+ * `sourceRects` (the sugar's real, engine-facing state — mirrors
498
+ * setVelocity/setBurstSize's own "write every handle" discipline). There is
499
+ * no provider ABI here (G-EMIT-2 retired `updateEmitterSourceRects` — the
500
+ * emitter is a host tick-fn, not an engine object); this IS the engine-facing
501
+ * write.
502
+ *
503
+ * Called by `FrqtlApp.updateObjectProperty` when
504
+ * `kind === 'QuantumEmitter'`/`prop === 'rect'` — mirrors PointSource's own
505
+ * `repositionToRect` dispatch convention.
506
+ *
507
+ * @param {Array} rectArr - [left, top, w, h]
508
+ */
509
+ QuantumEmitter.prototype.setSourceRect = function (rectArr) {
510
+ var r = (rectArr && rectArr.slice) ? rectArr.slice() : rectArr;
511
+ // The marker this emitter owns follows its ONE position write (it is drawn on the source rect).
512
+ if (this._marker) { this._marker.setRect([r[0], r[1], r[2], r[3]]); }
513
+ this.source_rects = [r];
514
+ this._specTemplate.source_rects = this.source_rects;
515
+ var allKeys = Object.keys(this._emitterMap);
516
+ for (var i = 0; i < allKeys.length; i++) {
517
+ this._emitterMap[allKeys[i]].sourceRects = [r.slice()];
518
+ }
519
+ };
520
+
521
+ /**
522
+ * Full teardown (Studio Controls Realignment S4, design item 1 — the
523
+ * "delete leaves the emitter tick-fn running" defect). Stops this
524
+ * emitter for good:
525
+ * 1. Tombstones every underlying emState — `emitterProgram.js`'s
526
+ * `buildEmitterTickFn` checks `em.tombstoned` FIRST and returns `[]`
527
+ * immediately, so emission stops on the very next tick even before
528
+ * step 2 below completes (defense in depth: correct even if the splice
529
+ * somehow misses an entry).
530
+ * 2. Physically removes this emitter's host tick-fn(s) from
531
+ * `universe.experimentTickFunctions` (via the `_emState` back-reference
532
+ * stashed in `_ensureHandle`) so a repeatedly placed-and-deleted emitter
533
+ * does not accumulate dead closures in that array forever.
534
+ * Idempotent — a second call (double-delete / teardown race) is a safe no-op.
535
+ */
536
+ QuantumEmitter.prototype.dispose = function () {
537
+ if (this._disposed) { return; }
538
+ this._disposed = true;
539
+
540
+ var allKeys = Object.keys(this._emitterMap);
541
+ var mine = [];
542
+ for (var i = 0; i < allKeys.length; i++) {
543
+ var em = this._emitterMap[allKeys[i]];
544
+ em.tombstoned = true;
545
+ em.active = false;
546
+ mine.push(em);
547
+ }
548
+
549
+ if (this.universe && Array.isArray(this.universe.experimentTickFunctions)) {
550
+ var fns = this.universe.experimentTickFunctions;
551
+ for (var j = fns.length - 1; j >= 0; j--) {
552
+ if (fns[j]._emState && mine.indexOf(fns[j]._emState) !== -1) {
553
+ fns.splice(j, 1);
554
+ }
555
+ }
556
+ }
557
+ // The marker is this emitter's (attachMarker): it leaves the world with it.
558
+ if (this._marker && typeof this._marker.dispose === 'function') { this._marker.dispose(); }
559
+ };
560
+
561
+ /**
562
+ * attachMarker(lo) — take ownership of the Design marker (the NONE-collision circle LargeObject
563
+ * the ObjectFactory builder draws at this emitter). Called ONCE, by that builder. Stamps the ONE
564
+ * up-link (`lo._owner = this`, so a pick on the disc resolves to THIS emitter) and keeps the one
565
+ * down-link (`_marker`) this emitter moves it and disposes it through. Zero physics.
566
+ * @param {LargeObject} lo @returns {QuantumEmitter} this
567
+ */
568
+ QuantumEmitter.prototype.attachMarker = function (lo) {
569
+ if (!lo || typeof lo !== 'object') {
570
+ throw new TypeError('[QuantumEmitter.attachMarker] needs the marker LargeObject. (FAIL-LOUD.)');
571
+ }
572
+ if (this._marker) {
573
+ throw new Error('[QuantumEmitter.attachMarker] this emitter already owns a marker. (FAIL-LOUD.)');
574
+ }
575
+ this._marker = lo;
576
+ lo._owner = this;
577
+ return this;
578
+ };
579
+
580
+ // ── The kind (objects/kinds.js): descriptor, construction defaults, builder ──────────
581
+
582
+ /**
583
+ * QuantumEmitter — Engine-registered particle emitter.
584
+ * Constructor: QuantumEmitter(universe, source_rects, vx, vy, burst_size,
585
+ * charge, auto_fire, random_direction,
586
+ * quantum_limit, delay, wrap)
587
+ */
588
+ var QuantumEmitterDescriptor = {
589
+ description: 'A particle emitter that fires leaf quanta (particles) from one or more ' +
590
+ 'source rectangles. Supports directed or random emission, charge polarity, ' +
591
+ 'burst-size (amperage), and velocity (voltage direction). ' +
592
+ 'The chargeMode/random_direction sugar layer composes multiple underlying engine ' +
593
+ 'handles to implement "both" charge and random direction without engine changes.',
594
+ fields: [
595
+ {
596
+ name: 'rect',
597
+ type: 'rect',
598
+ engineVisible: true,
599
+ description: 'Canonical bounding rect ' + ValueNormalizer.rectTupleText(4) + ' — repositions the single emission ' +
600
+ 'source rect. WS Studio Controls Realignment S4: routed through ' +
601
+ 'QuantumEmitter.setSourceRect() (mutates emState.sourceRects directly — the ' +
602
+ 'RETIRED provider ABI updateEmitterSourceRects no longer exists post-G-EMIT-2). ' +
603
+ 'The emitter moves its own marker.'
604
+ },
605
+ {
606
+ name: 'source_rects',
607
+ type: 'rect-array',
608
+ engineVisible: true,
609
+ description: 'Emission regions as array of ' + ValueNormalizer.rectTupleText(4) + ' rects'
610
+ },
611
+ {
612
+ name: 'vx',
613
+ type: 'number',
614
+ constraint: { integer: false },
615
+ engineVisible: true,
616
+ unit: 'px/tick',
617
+ description: 'Initial X velocity of emitted particles (pixels/tick). Positive = rightward, negative = leftward.'
618
+ },
619
+ {
620
+ name: 'vy',
621
+ type: 'number',
622
+ constraint: { integer: false },
623
+ engineVisible: true,
624
+ unit: 'px/tick',
625
+ description: 'Initial Y velocity of emitted particles (pixels/tick). Positive = downward (screen coords), negative = upward.'
626
+ },
627
+ {
628
+ name: 'burst_size',
629
+ type: 'number',
630
+ constraint: { min: 1, integer: true },
631
+ engineVisible: true,
632
+ description: 'Number of particles fired per burst (amperage). Higher values produce more particles per tick; lower values reduce flux.'
633
+ },
634
+ {
635
+ name: 'auto_fire',
636
+ type: 'boolean',
637
+ engineVisible: true,
638
+ description: 'Whether the emitter fires automatically every tick. false = must be triggered manually (e.g. by a tick function or the inspector).'
639
+ },
640
+ {
641
+ name: 'quantum_limit',
642
+ type: 'number',
643
+ constraint: { min: 1, integer: true },
644
+ engineVisible: true,
645
+ description: 'Maximum total quanta to emit (null/Infinity = unlimited). The emitter auto-deactivates after reaching this count.'
646
+ },
647
+ {
648
+ name: 'delay',
649
+ type: 'number',
650
+ constraint: { min: 0, integer: true },
651
+ engineVisible: true,
652
+ unit: 'ticks',
653
+ description: 'Ticks between bursts. 0 = fire every tick; N = fire once every N+1 ticks.'
654
+ },
655
+ {
656
+ name: 'wrap',
657
+ type: 'boolean',
658
+ engineVisible: true,
659
+ description: 'Wrap emitted particles at canvas boundaries instead of letting them escape.'
660
+ },
661
+ {
662
+ name: 'colorOverride',
663
+ type: 'rgb',
664
+ engineVisible: false,
665
+ description: 'Override render color for emitted particles [r, g, b] (render-only; stored on the emitter wrapper)'
666
+ },
667
+ {
668
+ name: 'chargeMode',
669
+ type: 'enum',
670
+ constraint: { values: { random: 'random', positive: 'positive', negative: 'negative', both: 'both' } },
671
+ engineVisible: false,
672
+ description: 'Charge mode (user-facing sugar): random = null charge, positive, negative, or both (two co-located emitters). Composes underlying emitter handle-programs by toggling their active flag — no engine ABI change. Use this instead of the raw charge param.'
673
+ },
674
+ {
675
+ name: 'random_direction',
676
+ type: 'boolean',
677
+ engineVisible: false,
678
+ description: 'Randomize emission direction each burst (sugar layer). Registers a new underlying handle with rd=true — no engine ABI change.'
679
+ },
680
+ {
681
+ name: 'layer',
682
+ type: 'number',
683
+ constraint: { min: 0, integer: true },
684
+ engineVisible: false,
685
+ description: 'Dimension layer this emitter targets (0 = fundamental; null = default). Emitted particles are placed in this layer. Stored on the emitter wrapper; no engine ABI change required.'
686
+ }
687
+ ]
688
+ };
689
+
690
+ registerKind(QuantumEmitter, {
691
+ from: import.meta.url,
692
+ descriptor: QuantumEmitterDescriptor,
693
+ defaults: {
694
+ source_rects: [[50, 95, 10, 10]],
695
+ vx: 1, vy: 0,
696
+ burst_size: 1,
697
+ chargeMode: 'random',
698
+ auto_fire: true,
699
+ random_direction: false,
700
+ quantum_limit: null,
701
+ delay: 0,
702
+ wrap: false
703
+ },
704
+ build: function (props, ctx, Class) {
705
+ var QE = Class; // the class that registered (a composed fork's own class)
706
+ var LO2 = LargeObject;
707
+ var CT2 = CollisionTypes;
708
+ var ew = 20, eh = 20;
709
+ var er;
710
+ if (props.rect) {
711
+ er = [props.rect.left || 0, props.rect.top || 0,
712
+ props.rect.w || ew, props.rect.h || eh];
713
+ } else if (props.source_rects && props.source_rects.length) {
714
+ var sr0 = props.source_rects[0];
715
+ er = sr0.slice ? sr0.slice() : sr0;
716
+ } else {
717
+ er = [Math.round(ctx.x - ew / 2), Math.round(ctx.y - eh / 2), ew, eh];
718
+ }
719
+ // Bug #1 fix: pass null as raw charge; set chargeMode via the sugar setter AFTER
720
+ // construction so the correct underlying engine handles are registered.
721
+ // The old props.charge path was broken: the schema only captures chargeMode now.
722
+ var qemitter = new QE(
723
+ ctx.runner.universe,
724
+ [er.slice ? er.slice() : er],
725
+ props.vx != null ? props.vx : 1,
726
+ props.vy != null ? props.vy : 0,
727
+ props.burst_size != null ? props.burst_size : 1,
728
+ null, // raw charge — overridden immediately below via chargeMode setter
729
+ props.auto_fire != null ? props.auto_fire : true,
730
+ props.random_direction != null ? props.random_direction : false,
731
+ props.quantum_limit != null ? props.quantum_limit : Infinity,
732
+ props.delay != null ? props.delay : 0,
733
+ props.wrap || false
734
+ );
735
+ qemitter._schemaKind = Class.name;
736
+ // Restore chargeMode via the sugar setter (handles 'random'|'positive'|'negative'|'both').
737
+ if (props.chargeMode) { qemitter.chargeMode = props.chargeMode; }
738
+ if (props.colorOverride) { qemitter.colorOverride = props.colorOverride; }
739
+
740
+ // Visual handle (circle LargeObject) so hand tool can drag it.
741
+ // layer → observedDimensions: the visual handle tracks the emitter's layer.
742
+ var qvisObsDims = (props.layer != null) ? [props.layer] : null;
743
+ var qName = props.name || ('emitter ' + declaredCount(ctx.runner, 'QuantumEmitter'));
744
+ qemitter.name = qName; // the declared name is the emitter's; its marker wears it
745
+ var qvis = new LO2(ctx.runner.universe,
746
+ [er[0], er[1], er[2] || ew, er[3] || eh],
747
+ CT2.NONE, [],
748
+ qName,
749
+ 'closest', qvisObsDims, 1, props.color || [80, 200, 80]);
750
+ qvis.drawAsCircle = true;
751
+ qvis._schemaKind = 'LargeObject';
752
+ qemitter.attachMarker(qvis);
753
+ return qemitter;
754
+ }
755
+ });
756
+
757
+ // ── Export ───────────────────────────────────────────────────────────
758
+
759
+ export { QuantumEmitter };
760
+