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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/CLAUDE.md +87 -0
  2. package/README.md +84 -2
  3. package/dist/dist-manifest.json +218 -0
  4. package/dist/eyes/index.js +109 -0
  5. package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
  6. package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
  7. package/dist/fat/pkg-node/package.json +12 -0
  8. package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
  9. package/dist/frqtl/compat/boot-node.js +34 -0
  10. package/dist/frqtl/compat/engine.js +127 -0
  11. package/dist/frqtl/compat/package-root.js +24 -0
  12. package/dist/frqtl/compat/providers/wasm-engine.js +2501 -0
  13. package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
  14. package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
  15. package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
  16. package/dist/frqtl/compat/renderer/Filters.js +163 -0
  17. package/dist/frqtl/compat/renderer/RenderFamily.js +85 -0
  18. package/dist/frqtl/compat/renderer/RenderMesh.js +100 -0
  19. package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
  20. package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
  21. package/dist/frqtl/compat/renderer/captureStill-common.js +259 -0
  22. package/dist/frqtl/compat/renderer/display-slot.js +96 -0
  23. package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
  24. package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
  25. package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
  26. package/dist/frqtl/compat/renderer/providers/index.js +22 -0
  27. package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
  28. package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
  29. package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
  30. package/dist/frqtl/compat/renderer/providers/three-provider.js +2474 -0
  31. package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
  32. package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
  33. package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
  34. package/dist/frqtl/compat/vendor.js +196 -0
  35. package/dist/frqtl/node-entry.js +17 -0
  36. package/dist/frqtl/objects/FrqtlApp.js +2922 -0
  37. package/dist/frqtl/objects/ObjectFactory.js +297 -0
  38. package/dist/frqtl/objects/RecipeContext.js +322 -0
  39. package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
  40. package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
  41. package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
  42. package/dist/frqtl/objects/instruments/capture-host.js +453 -0
  43. package/dist/frqtl/objects/instruments/capture.js +711 -0
  44. package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -0
  45. package/dist/frqtl/objects/kinds-entry.js +14 -0
  46. package/dist/frqtl/objects/kinds.js +190 -0
  47. package/dist/frqtl/objects/objects/Box.js +273 -0
  48. package/dist/frqtl/objects/objects/Bullet.js +155 -0
  49. package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
  50. package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
  51. package/dist/frqtl/objects/objects/DirectedTunnel.js +412 -0
  52. package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
  53. package/dist/frqtl/objects/objects/LightSource.js +354 -0
  54. package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
  55. package/dist/frqtl/objects/objects/Tube.js +471 -0
  56. package/dist/frqtl/objects/objects/absorbRadiation.js +310 -0
  57. package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
  58. package/dist/frqtl/objects/objects/controlAction.js +89 -0
  59. package/dist/frqtl/objects/objects/controlBind.js +278 -0
  60. package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
  61. package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
  62. package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
  63. package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
  64. package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
  65. package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
  66. package/dist/frqtl/objects/objects/makeControlsForObject.js +824 -0
  67. package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
  68. package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
  69. package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
  70. package/dist/frqtl/objects/objects/tagger.js +228 -0
  71. package/dist/frqtl/objects/schema/engine-scopes.js +469 -0
  72. package/dist/frqtl/objects/schema/index.js +256 -0
  73. package/dist/frqtl/objects/schema/scopes.js +457 -0
  74. package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
  75. package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
  76. package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
  77. package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
  78. package/dist/frqtl/objects/setup/_worldInit.js +162 -0
  79. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
  80. package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
  81. package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
  82. package/dist/frqtl/objects/setup/setupConeEmitter3D.js +251 -0
  83. package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -0
  84. package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
  85. package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
  86. package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
  87. package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
  88. package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
  89. package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
  90. package/dist/frqtl/objects/setup/setupLightSourceLab.js +280 -0
  91. package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
  92. package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
  93. package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
  94. package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1259 -0
  95. package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
  96. package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
  97. package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
  98. package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
  99. package/dist/frqtl/objects/v1-replacements.js +30 -0
  100. package/dist/frqtl/package-core.js +23 -0
  101. package/dist/frqtl/parts/CameraControls.js +643 -0
  102. package/dist/frqtl/parts/CameraPath.js +575 -0
  103. package/dist/frqtl/parts/CollisionActions.js +35 -0
  104. package/dist/frqtl/parts/CollisionTypes.js +31 -0
  105. package/dist/frqtl/parts/DetectorWall.js +496 -0
  106. package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
  107. package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
  108. package/dist/frqtl/parts/FrqtlVision.js +818 -0
  109. package/dist/frqtl/parts/GraphingDetectorWall.js +490 -0
  110. package/dist/frqtl/parts/LargeObject.js +406 -0
  111. package/dist/frqtl/parts/LightColor.js +255 -0
  112. package/dist/frqtl/parts/LiveChart.js +285 -0
  113. package/dist/frqtl/parts/LogicGate.js +720 -0
  114. package/dist/frqtl/parts/OvenBox.js +612 -0
  115. package/dist/frqtl/parts/PointSource.js +763 -0
  116. package/dist/frqtl/parts/Rectangle.js +200 -0
  117. package/dist/frqtl/parts/SlitLightSource.js +234 -0
  118. package/dist/frqtl/parts/SlitScreen.js +628 -0
  119. package/dist/frqtl/parts/SphereEmitter.js +946 -0
  120. package/dist/frqtl/parts/Trace.js +508 -0
  121. package/dist/frqtl/parts/Tunnel.js +1057 -0
  122. package/dist/frqtl/parts/Wall.js +1211 -0
  123. package/dist/frqtl/parts/index.js +33 -0
  124. package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
  125. package/dist/frqtl/scenes/absorbing-wall.js +32 -0
  126. package/dist/frqtl/scenes/atomic-model.js +88 -0
  127. package/dist/frqtl/scenes/big-bang.js +46 -0
  128. package/dist/frqtl/scenes/blackbody.js +81 -0
  129. package/dist/frqtl/scenes/circuit-sat.js +127 -0
  130. package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
  131. package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
  132. package/dist/frqtl/scenes/counting-wall.js +38 -0
  133. package/dist/frqtl/scenes/double-slit.js +31 -0
  134. package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
  135. package/dist/frqtl/scenes/dse-one-slit.js +114 -0
  136. package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
  137. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +244 -0
  138. package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
  139. package/dist/frqtl/scenes/dse-two-slits.js +132 -0
  140. package/dist/frqtl/scenes/empty.js +14 -0
  141. package/dist/frqtl/scenes/laser-buildup.js +49 -0
  142. package/dist/frqtl/scenes/mirror-3d.js +56 -0
  143. package/dist/frqtl/scenes/mirror-route.js +140 -0
  144. package/dist/frqtl/scenes/mirror.js +38 -0
  145. package/dist/frqtl/scenes/omni-3d.js +27 -0
  146. package/dist/frqtl/scenes/omni-source.js +22 -0
  147. package/dist/frqtl/scenes/self-interference.js +39 -0
  148. package/dist/frqtl/scenes/smoking-gun.js +122 -0
  149. package/dist/frqtl/scenes/straight-walk.js +22 -0
  150. package/dist/frqtl/scenes/sun.js +39 -0
  151. package/dist/frqtl/scenes/two-source.js +37 -0
  152. package/dist/frqtl/scenes/wave.js +26 -0
  153. package/dist/shim/bundle/frqnb.js +112 -0
  154. package/dist/shim/bundle/index.js +121 -0
  155. package/dist/shim/bundle/indexHtml.js +107 -0
  156. package/dist/shim/bundle/manifest.js +95 -0
  157. package/dist/shim/bundle/zip.js +243 -0
  158. package/dist/shim/bus.js +67 -0
  159. package/dist/shim/controls/index.js +627 -0
  160. package/dist/shim/controls/registry.js +274 -0
  161. package/dist/shim/engine/artifact-shell.js +212 -0
  162. package/dist/shim/engine/boot-phases.js +44 -0
  163. package/dist/shim/engine/engine-stack-boot.js +59 -0
  164. package/dist/shim/engine/router.js +692 -0
  165. package/dist/shim/experiment-frame/index.js +296 -0
  166. package/dist/shim/expr/ast-schema.js +369 -0
  167. package/dist/shim/field-descriptor/index.js +326 -0
  168. package/dist/shim/frame-data/index.js +224 -0
  169. package/dist/shim/index.js +67 -0
  170. package/dist/shim/notebook/canvas-guard.js +82 -0
  171. package/dist/shim/notebook/catalog.js +35 -0
  172. package/dist/shim/notebook/format.js +736 -0
  173. package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
  174. package/dist/shim/notebook/kernel.js +672 -0
  175. package/dist/shim/notebook/knob.js +151 -0
  176. package/dist/shim/notebook/module-resolver.js +260 -0
  177. package/dist/shim/notebook/notebook-knobs.js +65 -0
  178. package/dist/shim/notebook/produced-bindings.js +67 -0
  179. package/dist/shim/notebook/realm-registry.js +121 -0
  180. package/dist/shim/notebook/realm-transport.js +73 -0
  181. package/dist/shim/notebook/setup-builder.js +510 -0
  182. package/dist/shim/notebook/store.js +159 -0
  183. package/dist/shim/notebook/vendor-importmap.js +134 -0
  184. package/dist/shim/notebook/view-builder.js +189 -0
  185. package/dist/shim/notebook/view-ctx.js +100 -0
  186. package/dist/shim/notebook/view-handle.js +202 -0
  187. package/dist/shim/notebook/warmup.js +102 -0
  188. package/dist/shim/package.json +4 -0
  189. package/dist/shim/page-params/index.js +214 -0
  190. package/dist/shim/project/identity.js +112 -0
  191. package/dist/shim/project/index.js +19 -0
  192. package/dist/shim/project/link.js +402 -0
  193. package/dist/shim/project/node-specifier-hook.mjs +35 -0
  194. package/dist/shim/project/register-node.mjs +17 -0
  195. package/dist/shim/project/specifiers.js +106 -0
  196. package/dist/shim/registry/copy-gate.js +110 -0
  197. package/dist/shim/registry/exhibits.js +455 -0
  198. package/dist/shim/registry/experiments.js +215 -0
  199. package/dist/shim/registry/index.js +19 -0
  200. package/dist/shim/registry/scene-source.js +157 -0
  201. package/dist/shim/registry/walkthrough.js +807 -0
  202. package/dist/shim/render/capture-envelope.js +279 -0
  203. package/dist/shim/render/heat-colormap.js +92 -0
  204. package/dist/shim/render/render-stages.js +855 -0
  205. package/dist/shim/render/sun-look.js +33 -0
  206. package/dist/shim/render-state/apply.js +262 -0
  207. package/dist/shim/render-state/index.js +74 -0
  208. package/dist/shim/rpc/index.js +207 -0
  209. package/dist/shim/runtime/index.js +1023 -0
  210. package/dist/shim/storage/boot.js +69 -0
  211. package/dist/shim/storage/index.js +169 -0
  212. package/dist/shim/storage/local-storage-provider.js +230 -0
  213. package/dist/shim/storage/make-namespace-store.js +132 -0
  214. package/dist/shim/storage/modules.js +249 -0
  215. package/dist/shim/storage/projects.js +150 -0
  216. package/llms.txt +12 -0
  217. package/package.json +62 -4
@@ -0,0 +1,763 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * PointSource — Engine-agnostic radial emitter.
5
+ * Creates one emitter per emission angle to preserve the deterministic
6
+ * angle pattern. Each angle gets its own emitter with fixed velocity
7
+ * vx=radius*cos(a), vy=radius*sin(a).
8
+ *
9
+ * Engine-agnostic: works with both the JS reference engine and the Rust/WASM engine.
10
+ * Emits raw canonical charge; providers translate to engine wire-format.
11
+ * No engine-sniffing above the shim (WS-A §2 invariant).
12
+ *
13
+ * chargeMode / random_direction (sugar-layer modes, WS emitter-modes-sugar):
14
+ * chargeMode ∈ 'random' | 'positive' | 'negative' | 'both'
15
+ * random_direction ∈ boolean
16
+ *
17
+ * For PointSource, chargeMode operates on GROUPS of per-angle handles.
18
+ * Each (charge, random_direction) combo has its own full set of N per-angle handles.
19
+ * Switching chargeMode registers the needed group(s) lazily and activates/deactivates.
20
+ *
21
+ * 'both' = two per-angle handle groups (POSITIVE + NEGATIVE), both active → 2N handles.
22
+ *
23
+ * Back-compat: legacy `charge` constructor param maps to chargeMode:
24
+ * null → 'random'
25
+ * 'POSITIVE' → 'positive'
26
+ * 'NEGATIVE' → 'negative'
27
+ */
28
+
29
+ import * as EmitterProgram from '../objects/objects/emitterProgram.js';
30
+ import { registerKind } from '../objects/kinds.js';
31
+ import { LargeObject } from './LargeObject.js';
32
+ import { CollisionTypes } from './CollisionTypes.js';
33
+ import * as ValueNormalizer from '../objects/schema/valueNormalizer.js';
34
+ import { declaredCount } from '../objects/ObjectFactory.js';
35
+
36
+ // G-EMIT-2: the emitter is engine-agnostic sugar (emitterProgram.js).
37
+ var buildEmitterTickFn;
38
+ buildEmitterTickFn = EmitterProgram.buildEmitterTickFn;
39
+
40
+ // ── Internal helpers ─────────────────────────────────────────────────
41
+
42
+ /**
43
+ * Map a legacy charge constructor param to a chargeMode string.
44
+ */
45
+ function _chargeToMode(charge) {
46
+ if (charge === null || charge === undefined) return 'random';
47
+ if (typeof charge === 'string') {
48
+ var u = charge.toUpperCase();
49
+ if (u === 'POSITIVE' || charge === 'Positive' || charge === '+') return 'positive';
50
+ if (u === 'NEGATIVE' || charge === 'Negative' || charge === '-') return 'negative';
51
+ }
52
+ if (charge === 0) return 'positive';
53
+ if (charge === 1) return 'negative';
54
+ return 'random';
55
+ }
56
+
57
+ /**
58
+ * Map chargeMode string to canonical charge value for the emitter handle-program.
59
+ * 'both' callers build POSITIVE and NEGATIVE emitters separately.
60
+ */
61
+ function _modeToCharge(mode) {
62
+ if (mode === 'positive') return 'POSITIVE';
63
+ if (mode === 'negative') return 'NEGATIVE';
64
+ return null; // 'random'
65
+ }
66
+
67
+ /**
68
+ * Canonical key for a handle group: identifies the (charge, random_direction) combo.
69
+ */
70
+ function _groupKey(charge, random_direction) {
71
+ return String(charge) + ':' + String(!!random_direction);
72
+ }
73
+
74
+ /**
75
+ * Build the list of emission radii (max → min in steps of 2).
76
+ */
77
+ function _buildRadii(radius, minRadius) {
78
+ var radii = [];
79
+ if (minRadius !== null && minRadius !== undefined) {
80
+ var maxR = Math.max(minRadius, radius);
81
+ for (var r = maxR; r >= minRadius; r -= 2) {
82
+ radii.push(r);
83
+ }
84
+ } else {
85
+ radii.push(radius);
86
+ }
87
+ return radii;
88
+ }
89
+
90
+ // ── PointSource ─────────────────────────────────────────────────────
91
+
92
+ /**
93
+ * Radial emitter that fires particles outward from the surface of a circle.
94
+ *
95
+ * It registers one fixed-direction emitter per emission angle (so the radial
96
+ * pattern is deterministic), each firing 1px/tick leaf Frames. There is no
97
+ * wave equation — interference is emergent from where these particle streams
98
+ * overlap.
99
+ *
100
+ * ## Physics you can drive (and what changes on screen)
101
+ * - **`opts.wavelength`** sets each emitted particle's **mass**
102
+ * (`mass = capacity / wavelength`). Mass feeds the per-Frame oscillation that
103
+ * produces wave behavior. In the wave-interference setup `wavelength = 32`
104
+ * yields leaf mass ≈ 1 — the clean-fringe regime that also matches the WASM
105
+ * engine. This is DISTINCT from the universe `wavelengthScale` (below).
106
+ * - **Fringe spacing** is governed by the universe `wavelengthScale`
107
+ * (`EGPTfrqtl` / `universe.init(...)`), NOT by this constructor:
108
+ * `wavelength = floor((capacity / mass) × WAVELENGTH_CONSTANT)`. OBSERVED
109
+ * (seed 42): raising it 2 → 32 widens the fringes from a near-uniform dense
110
+ * cloud to sparse, widely-separated radial spokes. Figure + data:
111
+ * `docs/internal/records/physics-lab/2026-06-16-wave-interference-wavelengthScale.md`.
112
+ * - **`angle_increment`** sets the number of emission rays = `floor(360 / inc)`
113
+ * (5° → 72 rays). Smaller increment = denser angular coverage = a smoother
114
+ * radial field; larger = visible discrete spokes.
115
+ * - **`charge`** (`null` = random ±) drives the oscillation quadrant per
116
+ * particle; with mixed charge the two streams produce the canonical fringes.
117
+ * - **`quantum_limit`** caps how many quanta each angle emits before stopping
118
+ * (the steady-state density ceiling); `Infinity` = unlimited.
119
+ *
120
+ * @param {Object} universe - The engine universe instance (JS or WASM).
121
+ * @param {number} x - Center X of the point source (physics pixels).
122
+ * @param {number} y - Center Y of the point source (physics pixels).
123
+ * @param {number} diameter - Diameter of the source circle. Emission radius =
124
+ * `diameter / 2`; this radius ALSO sets the emission velocity (see `speed`).
125
+ * @param {number} speed - CURRENTLY UNUSED. It is stored as `this.speed` but
126
+ * emission velocity is derived from the source radius (`diameter / 2`), not
127
+ * this value (`_ensureGroup`: `vx = emitR·cos θ`, `vy = emitR·sin θ`). The
128
+ * wave-interference setup passes `diameter/2` here, which coincidentally
129
+ * equals the radius, masking that it is ignored. Flagged for review — do not
130
+ * rely on this parameter to change speed.
131
+ * @param {*} charge - `0`/`1`/`'POSITIVE'`/`'NEGATIVE'`/`null`(random per
132
+ * particle). Back-compat; maps to `chargeMode`.
133
+ * @param {boolean} [auto_fire=true] - Whether the emitters fire automatically.
134
+ * @param {number} [quantum_limit=Infinity] - Max quanta per angle.
135
+ * @param {number} [delay=0] - Ticks between bursts.
136
+ * @param {number} [angle_increment=5] - Degrees between emission angles →
137
+ * `floor(360/angle_increment)` rays.
138
+ * @param {Object} [opts] - `{ wavelength, color, minRadius, emergentPhysics, zMin, zDepth }`.
139
+ * @param {number} [opts.wavelength] - Per-emitter wavelength → particle mass
140
+ * (see Physics, above). Omitted → engine default leaf mass.
141
+ * @param {number} [opts.zMin] - WS-DSE-3D Bite B0: when provided (with `opts.zDepth`), every
142
+ * emitted leaf's z is drawn UNIFORMLY from `[zMin, zMin+zDepth)` (one extra `random()` draw
143
+ * per leaf, opt-in only — see `emitterProgram.js`'s DRAW-ORDER CONTRACT). Omitted (default)
144
+ * → no z is ever written, byte-identical to pre-3D behavior. The caller (e.g.
145
+ * `setupWaveInterference`'s `depth` option) decides centering — PointSource just forwards.
146
+ * @param {number} [opts.zDepth] - Depth of the uniform z-spread (paired with `opts.zMin`).
147
+ * Ignored if `opts.zMin` is omitted.
148
+ *
149
+ * @deprecated V1 sugar, kept for byte-identical scenes (big-bang). V2 direction: SphereEmitter{ preset:'drum' | 'spherical' } (needs an opt-in deterministic zero-momentum ring mode to reproduce this emission).
150
+ */
151
+ function PointSource(universe, x, y, diameter, speed, charge, auto_fire, quantum_limit, delay, angle_increment, opts) {
152
+ // Defaults
153
+ if (charge === undefined) charge = null;
154
+ auto_fire = (auto_fire !== undefined) ? auto_fire : true;
155
+ quantum_limit = (quantum_limit !== undefined) ? quantum_limit : Infinity;
156
+ delay = (delay !== undefined) ? delay : 0;
157
+ angle_increment = (angle_increment !== undefined) ? angle_increment : 5;
158
+ opts = opts || {};
159
+
160
+ // When emergentPhysics=true (e.g. Big Bang), particles get no initial velocity
161
+ var emergentPhysics = opts.emergentPhysics || false;
162
+ // minRadius: emit at multiple radii from maxRadius down to minRadius (step 2)
163
+ var minRadius = opts.minRadius !== undefined ? opts.minRadius : null;
164
+ // Optional color override for emitted particles (e.g. [0, 255, 0] for green)
165
+ var color = opts.color || null;
166
+ // Optional wavelength for mass computation (affects wave behavior at dim > 0)
167
+ var wavelength = opts.wavelength || null;
168
+ // WS-DSE-3D Bite B0: optional uniform z-spread (opt-in; null = no z ever written).
169
+ var zMin = opts.zMin !== undefined ? opts.zMin : null;
170
+ var zDepth = opts.zDepth !== undefined ? opts.zDepth : null;
171
+
172
+ this.universe = universe;
173
+ this._schemaKind = 'PointSource';
174
+ this.x = x;
175
+ this.y = y;
176
+ this.diameter = diameter;
177
+ this.speed = speed;
178
+ this.angle_increment = angle_increment;
179
+ this.auto_fire = auto_fire;
180
+ // colorOverride: render-only attribute for emitted particle color.
181
+ // Set via updateObjectProperty(lo, 'colorOverride', [r,g,b]) from the IDE.
182
+ this.colorOverride = color || null;
183
+ // renderShape / renderOutline (Bite 5b R3-1): render-only per-emitted-frame overrides,
184
+ // peers of colorOverride (same per-frame reach SphereEmitter3D has). null → no key stamped.
185
+ this.renderShape = null;
186
+ this.renderOutline = null;
187
+
188
+ // Sugar-layer mode state.
189
+ this._chargeMode = _chargeToMode(charge);
190
+ this._random_direction = false; // PointSource default: directed emission
191
+
192
+ // Store construction params needed for lazy handle group registration.
193
+ this._emergentPhysics = emergentPhysics;
194
+ this._color = color;
195
+ this._wavelength = wavelength;
196
+ this._quantum_limit = quantum_limit;
197
+ this._delay = delay;
198
+ this._zMin = zMin;
199
+ this._zDepth = zDepth;
200
+
201
+ // Pre-compute the list of emission radii (stored for lazy re-use by _ensureGroup).
202
+ this._radii = _buildRadii(diameter / 2, minRadius);
203
+
204
+ // Map of groupKey → Array of per-angle handles (one per (radius, angle) pair).
205
+ // Each group is a full complement of N×M handles (N angles × M radii).
206
+ this._handleGroups = {};
207
+
208
+ // Register the initial handle group for the starting mode.
209
+ this._ensureAndActivate(this._chargeMode, this._random_direction);
210
+
211
+ // Back-compat: emitterHandles points to the active group's handles.
212
+ this.emitterHandles = this._activeHandles();
213
+ }
214
+
215
+ /**
216
+ * Register all per-angle handles for a given (charge, random_direction) group
217
+ * if that group does not yet exist. Returns the handles array.
218
+ * @param {null|'POSITIVE'|'NEGATIVE'} charge
219
+ * @param {boolean} random_direction
220
+ * @returns {Array} handles
221
+ */
222
+ PointSource.prototype._ensureGroup = function (charge, random_direction) {
223
+ var key = _groupKey(charge, random_direction);
224
+ if (this._handleGroups[key] !== undefined) return this._handleGroups[key];
225
+
226
+ var handles = [];
227
+ var universe = this.universe;
228
+ var x = this.x;
229
+ var y = this.y;
230
+ var angle_increment = this.angle_increment;
231
+ var numAngles = Math.floor(360 / angle_increment);
232
+ var emergentPhysics = this._emergentPhysics;
233
+ var color = this._color;
234
+ var renderShape = this.renderShape; // R3-1 per-frame render.shape reach
235
+ var renderOutline = this.renderOutline; // R3-1 per-frame render.outline reach
236
+ var wavelength = this._wavelength;
237
+ var quantum_limit = this._quantum_limit;
238
+ var delay = this._delay;
239
+ var radii = this._radii;
240
+ var rd = !!random_direction;
241
+ var auto_fire = this.auto_fire;
242
+ var zMin = this._zMin;
243
+ var zDepth = this._zDepth;
244
+
245
+ for (var ri = 0; ri < radii.length; ri++) {
246
+ var emitR = radii[ri];
247
+ for (var i = 0; i < numAngles; i++) {
248
+ var angleDeg = i * angle_increment;
249
+ var angleRad = angleDeg * Math.PI / 180;
250
+
251
+ var emitX = x + emitR * Math.cos(angleRad);
252
+ var emitY = y + emitR * Math.sin(angleRad);
253
+
254
+ var vx, vy;
255
+ if (emergentPhysics) {
256
+ vx = 0;
257
+ vy = 0;
258
+ } else {
259
+ vx = emitR * Math.cos(angleRad);
260
+ vy = emitR * Math.sin(angleRad);
261
+ }
262
+
263
+ var sourceRect = [Math.round(emitX), Math.round(emitY), 1, 1];
264
+
265
+ // G-EMIT-2: build a per-angle emState + push the shared emitter handle-program;
266
+ // the sugar OWNS the emState (the emState IS the "handle"). No registerEmitter.
267
+ var em = {
268
+ sourceRects: [sourceRect],
269
+ vx: vx,
270
+ vy: vy,
271
+ burstSize: 1,
272
+ charge: charge, // null | 'POSITIVE' | 'NEGATIVE'
273
+ randomDirection: rd,
274
+ quantumLimit: (quantum_limit === Infinity || quantum_limit == null) ? Infinity : quantum_limit,
275
+ remainingQuanta: (quantum_limit === Infinity || quantum_limit == null) ? Infinity : quantum_limit,
276
+ delay: delay || 0,
277
+ remainingDelay: delay || 0,
278
+ active: auto_fire,
279
+ tombstoned: false,
280
+ color: color || null,
281
+ renderShape: renderShape || null, // R3-1 per-frame render.shape reach
282
+ renderOutline: (renderOutline != null) ? renderOutline : null, // R3-1 render.outline reach
283
+ wrap: false,
284
+ wavelength: wavelength || null,
285
+ fundamentalDim: null, // null → engine uses the universe's fundamental-dim as the leaf level
286
+ zMin: zMin, // WS-DSE-3D Bite B0: null (default) → no z draw/write at all
287
+ zDepth: zDepth,
288
+ };
289
+ var tickFn = buildEmitterTickFn(universe, em);
290
+ // Back-reference so dispose() (WS Studio Controls Realignment S4) can
291
+ // find and splice OUT exactly the tick-fn(s) belonging to this
292
+ // PointSource, mirroring QuantumEmitter's own `_emState` convention.
293
+ tickFn._emState = em;
294
+ universe.experimentTickFunctions.push(tickFn);
295
+ handles.push(em);
296
+ }
297
+ }
298
+
299
+ this._handleGroups[key] = handles;
300
+ return handles;
301
+ };
302
+
303
+ /**
304
+ * Ensure the handle groups required by `mode` + `random_direction` are registered,
305
+ * then activate them and deactivate all others.
306
+ * @param {'random'|'positive'|'negative'|'both'} mode
307
+ * @param {boolean} random_direction
308
+ */
309
+ PointSource.prototype._ensureAndActivate = function (mode, random_direction) {
310
+ var rd = !!random_direction;
311
+ var self = this;
312
+
313
+ // Determine which (charge, rd) combos the new mode requires.
314
+ var required = [];
315
+ if (mode === 'both') {
316
+ required.push({ charge: 'POSITIVE', rd: rd });
317
+ required.push({ charge: 'NEGATIVE', rd: rd });
318
+ } else {
319
+ required.push({ charge: _modeToCharge(mode), rd: rd });
320
+ }
321
+
322
+ // Ensure all required groups are registered.
323
+ for (var i = 0; i < required.length; i++) {
324
+ self._ensureGroup(required[i].charge, required[i].rd);
325
+ }
326
+
327
+ // Build a lookup of which group keys should be active.
328
+ var activeKeys = {};
329
+ for (var j = 0; j < required.length; j++) {
330
+ activeKeys[_groupKey(required[j].charge, required[j].rd)] = true;
331
+ }
332
+
333
+ // Activate / deactivate handles in all registered groups.
334
+ var allGroupKeys = Object.keys(self._handleGroups);
335
+ for (var k = 0; k < allGroupKeys.length; k++) {
336
+ var key = allGroupKeys[k];
337
+ var groupHandles = self._handleGroups[key];
338
+ var shouldBeActive = !!activeKeys[key];
339
+ for (var h = 0; h < groupHandles.length; h++) {
340
+ groupHandles[h].active = shouldBeActive; // mutate sugar-owned emState
341
+ }
342
+ }
343
+ };
344
+
345
+ /**
346
+ * Return the flat array of handles for all currently active groups.
347
+ * Used to keep this.emitterHandles in sync (back-compat for repositionToRect).
348
+ * @returns {Array}
349
+ */
350
+ PointSource.prototype._activeHandles = function () {
351
+ var mode = this._chargeMode;
352
+ var rd = this._random_direction;
353
+ var result = [];
354
+
355
+ var activeKeys = {};
356
+ if (mode === 'both') {
357
+ activeKeys[_groupKey('POSITIVE', rd)] = true;
358
+ activeKeys[_groupKey('NEGATIVE', rd)] = true;
359
+ } else {
360
+ activeKeys[_groupKey(_modeToCharge(mode), rd)] = true;
361
+ }
362
+
363
+ var allGroupKeys = Object.keys(this._handleGroups);
364
+ for (var i = 0; i < allGroupKeys.length; i++) {
365
+ if (activeKeys[allGroupKeys[i]]) {
366
+ var groupHandles = this._handleGroups[allGroupKeys[i]];
367
+ for (var j = 0; j < groupHandles.length; j++) {
368
+ result.push(groupHandles[j]);
369
+ }
370
+ }
371
+ }
372
+ return result;
373
+ };
374
+
375
+ /**
376
+ * DSE LAB (2026-07-13): iterate EVERY registered emState (ALL (charge,
377
+ * random_direction) groups — active or not, so a field set here still applies
378
+ * after a LATER `chargeMode` switch activates a currently-inactive group).
379
+ * The clean, public way to mutate a per-emission field (e.g. `em.everyNTicks`,
380
+ * the firing-rate cadence) on every handle this PointSource owns, without a
381
+ * caller reaching into the private `_handleGroups` map directly.
382
+ * @param {function(Object):void} fn - called once per emState.
383
+ */
384
+ PointSource.prototype.forEachHandle = function (fn) {
385
+ var allGroupKeys = Object.keys(this._handleGroups);
386
+ for (var i = 0; i < allGroupKeys.length; i++) {
387
+ var groupHandles = this._handleGroups[allGroupKeys[i]];
388
+ for (var j = 0; j < groupHandles.length; j++) {
389
+ fn(groupHandles[j]);
390
+ }
391
+ }
392
+ };
393
+
394
+ // ── chargeMode getter/setter ──────────────────────────────────────────
395
+
396
+ /**
397
+ * chargeMode getter/setter (sugar-layer, engine-not-touched).
398
+ *
399
+ * Because updateObjectProperty routes engineVisible:false props as `lo[prop] = value`,
400
+ * this setter fires automatically when the IDE calls:
401
+ * runner.updateObjectProperty(pointSource, 'chargeMode', 'both')
402
+ */
403
+ Object.defineProperty(PointSource.prototype, 'chargeMode', {
404
+ get: function () { return this._chargeMode; },
405
+ set: function (mode) {
406
+ var validModes = { random: true, positive: true, negative: true, both: true };
407
+ if (!validModes[mode]) {
408
+ throw new TypeError(
409
+ '[PointSource] Invalid chargeMode "' + mode +
410
+ '". Expected: random | positive | negative | both'
411
+ );
412
+ }
413
+ this._chargeMode = mode;
414
+ this._ensureAndActivate(mode, this._random_direction);
415
+ this.emitterHandles = this._activeHandles();
416
+ },
417
+ enumerable: true,
418
+ configurable: true
419
+ });
420
+
421
+ // ── random_direction getter/setter ───────────────────────────────────
422
+
423
+ /**
424
+ * random_direction getter/setter.
425
+ *
426
+ * Note: PointSource normally uses directed emission (false). Enabling
427
+ * random_direction registers a new group of angle emitters with rd=true.
428
+ */
429
+ Object.defineProperty(PointSource.prototype, 'random_direction', {
430
+ get: function () { return this._random_direction; },
431
+ set: function (rd) {
432
+ var brd = !!rd;
433
+ this._random_direction = brd;
434
+ this._ensureAndActivate(this._chargeMode, brd);
435
+ this.emitterHandles = this._activeHandles();
436
+ },
437
+ enumerable: true,
438
+ configurable: true
439
+ });
440
+
441
+ // ── Public API ────────────────────────────────────────────────────────
442
+
443
+ /**
444
+ * Enable/disable all angle emitters.
445
+ * @param {boolean} active
446
+ */
447
+ PointSource.prototype.setActive = function (active) {
448
+ this.auto_fire = active;
449
+ var self = this;
450
+ if (active) {
451
+ this._ensureAndActivate(this._chargeMode, this._random_direction);
452
+ } else {
453
+ // Deactivate all handles in all groups.
454
+ var allGroupKeys = Object.keys(self._handleGroups);
455
+ for (var i = 0; i < allGroupKeys.length; i++) {
456
+ var groupHandles = self._handleGroups[allGroupKeys[i]];
457
+ for (var j = 0; j < groupHandles.length; j++) {
458
+ groupHandles[j].active = false; // mutate sugar-owned emState
459
+ }
460
+ }
461
+ }
462
+ };
463
+
464
+ /**
465
+ * Reposition the PointSource to a new [l, t, w, h] rect.
466
+ *
467
+ * This is the canonical ONE-PATH reposition for PointSource:
468
+ * 1. Compute new center (cx = l + w/2, cy = t + h/2).
469
+ * 2. Update this.x / this.y (kept in sync so schema inspector reads the live values).
470
+ * 3. Recompute the 1×1 source rect for each handle in ALL registered groups.
471
+ * 4. Call universe.updateEmitterSourceRects for each handle (engine-concurrence path).
472
+ *
473
+ * Called by FrqtlApp.updateObjectProperty when kind='PointSource', prop='rect'.
474
+ * Routes through universe.updateEmitterSourceRects — no second path, no engine-sniffing.
475
+ *
476
+ * For 'both' mode: both the POSITIVE and NEGATIVE groups are repositioned.
477
+ * The N angle emitters per group are recomputed identically (co-located).
478
+ *
479
+ * @param {number} l - New left edge (physics pixels)
480
+ * @param {number} t - New top edge (physics pixels)
481
+ * @param {number} w - New bounding width (physics pixels)
482
+ * @param {number} h - New bounding height (physics pixels)
483
+ * @param {Object} [universe] - Engine universe (defaults to this.universe)
484
+ */
485
+ PointSource.prototype.repositionToRect = function (l, t, w, h, universe) {
486
+ var u = universe || this.universe;
487
+ var cx = l + w / 2;
488
+ var cy = t + h / 2;
489
+ var radius = this.diameter / 2;
490
+ var angle_increment = this.angle_increment;
491
+ var numAngles = Math.floor(360 / angle_increment);
492
+
493
+ // Update stored center so inspector reads current values.
494
+ this.x = cx;
495
+ this.y = cy;
496
+ // The marker this source owns follows its ONE position write (a diameter disc at the centre).
497
+ if (this._marker) {
498
+ this._marker.setRect([Math.round(cx - this.diameter / 2), Math.round(cy - this.diameter / 2),
499
+ this.diameter, this.diameter]);
500
+ }
501
+
502
+ // Build the angle-indexed source rects (same for all groups — co-located).
503
+ var newRects = [];
504
+ for (var i = 0; i < numAngles; i++) {
505
+ var angleDeg = i * angle_increment;
506
+ var angleRad = angleDeg * Math.PI / 180;
507
+ var emitX = Math.round(cx + radius * Math.cos(angleRad));
508
+ var emitY = Math.round(cy + radius * Math.sin(angleRad));
509
+ newRects.push([emitX, emitY, 1, 1]);
510
+ }
511
+
512
+ // Update handles in ALL registered groups (active and inactive alike —
513
+ // inactive groups must stay positioned correctly for when they are re-activated).
514
+ var allGroupKeys = Object.keys(this._handleGroups);
515
+ for (var gi = 0; gi < allGroupKeys.length; gi++) {
516
+ var groupHandles = this._handleGroups[allGroupKeys[gi]];
517
+ for (var hi = 0; hi < groupHandles.length; hi++) {
518
+ // groupHandles is ordered angle0..angleN-1 (same order as newRects).
519
+ var angleIdx = hi % numAngles;
520
+ groupHandles[hi].sourceRects = [newRects[angleIdx]]; // mutate sugar-owned emState
521
+
522
+ }
523
+ }
524
+ };
525
+
526
+ /**
527
+ * Full teardown (WS Studio Controls Realignment S4, design item 1 — the
528
+ * SAME "delete leaves the tick-fn running" defect class the CEO found on
529
+ * QuantumEmitter, closed here too for the sibling composite emitter type).
530
+ * Tombstones + deactivates every emState across ALL registered (charge, rd)
531
+ * groups (active or not — a re-activated inactive group must not resurrect),
532
+ * then physically splices this PointSource's host tick-fn(s) out of
533
+ * `universe.experimentTickFunctions` via the `_emState` back-reference
534
+ * stashed in `_ensureGroup`. Idempotent — a second call is a safe no-op.
535
+ */
536
+ PointSource.prototype.dispose = function () {
537
+ if (this._disposed) { return; }
538
+ this._disposed = true;
539
+
540
+ var mine = [];
541
+ var allGroupKeys = Object.keys(this._handleGroups);
542
+ for (var gi = 0; gi < allGroupKeys.length; gi++) {
543
+ var groupHandles = this._handleGroups[allGroupKeys[gi]];
544
+ for (var hi = 0; hi < groupHandles.length; hi++) {
545
+ groupHandles[hi].tombstoned = true;
546
+ groupHandles[hi].active = false;
547
+ mine.push(groupHandles[hi]);
548
+ }
549
+ }
550
+
551
+ if (this.universe && Array.isArray(this.universe.experimentTickFunctions)) {
552
+ var fns = this.universe.experimentTickFunctions;
553
+ for (var j = fns.length - 1; j >= 0; j--) {
554
+ if (fns[j]._emState && mine.indexOf(fns[j]._emState) !== -1) {
555
+ fns.splice(j, 1);
556
+ }
557
+ }
558
+ }
559
+ // The marker is this emitter's (attachMarker): it leaves the world with it.
560
+ if (this._marker && typeof this._marker.dispose === 'function') { this._marker.dispose(); }
561
+ };
562
+
563
+ /**
564
+ * attachMarker(lo) — take ownership of the Design marker (the NONE-collision circle LargeObject
565
+ * the ObjectFactory builder draws at this emitter). Called ONCE, by that builder. Stamps the ONE
566
+ * up-link (`lo._owner = this`, so a pick on the disc resolves to THIS emitter) and keeps the one
567
+ * down-link (`_marker`) this emitter moves it and disposes it through. Zero physics.
568
+ * @param {LargeObject} lo @returns {PointSource} this
569
+ */
570
+ PointSource.prototype.attachMarker = function (lo) {
571
+ if (!lo || typeof lo !== 'object') {
572
+ throw new TypeError('[PointSource.attachMarker] needs the marker LargeObject. (FAIL-LOUD.)');
573
+ }
574
+ if (this._marker) {
575
+ throw new Error('[PointSource.attachMarker] this emitter already owns a marker. (FAIL-LOUD.)');
576
+ }
577
+ this._marker = lo;
578
+ lo._owner = this;
579
+ return this;
580
+ };
581
+
582
+ // ── The kind (objects/kinds.js): descriptor, construction defaults, builder ──────────
583
+
584
+ /**
585
+ * PointSource — Radial emitter (one engine emitter handle per angle).
586
+ * Constructor: PointSource(universe, x, y, diameter, speed, charge,
587
+ * auto_fire, quantum_limit, delay, angle_increment, opts)
588
+ * opts fields: { emergentPhysics, minRadius, color, wavelength }
589
+ */
590
+ var PointSourceDescriptor = {
591
+ description: 'A radial point emitter that fires particles outward from the surface ' +
592
+ 'of a circle at regular angular intervals. Each angle gets its own engine emitter ' +
593
+ 'handle so the angular pattern is deterministic. Produces wave fronts, interference ' +
594
+ 'patterns, and Big Bang-style expansions depending on configuration.',
595
+ fields: [
596
+ {
597
+ name: 'rect',
598
+ type: 'rect',
599
+ engineVisible: true,
600
+ description: 'Canonical bounding rect ' + ValueNormalizer.rectTupleText(4) + ' — repositions center (cx=left+w/2, cy=top+h/2) and recomputes all angle source rects (one-path via repositionToRect)'
601
+ },
602
+ {
603
+ name: 'x',
604
+ type: 'number',
605
+ constraint: { integer: true },
606
+ engineVisible: true,
607
+ description: 'Center X of the point source (physics pixels)'
608
+ },
609
+ {
610
+ name: 'y',
611
+ type: 'number',
612
+ constraint: { integer: true },
613
+ engineVisible: true,
614
+ description: 'Center Y of the point source (physics pixels)'
615
+ },
616
+ {
617
+ name: 'diameter',
618
+ type: 'number',
619
+ constraint: { min: 1, integer: true },
620
+ engineVisible: true,
621
+ unit: 'px',
622
+ description: 'Diameter of the emission circle (physics pixels)'
623
+ },
624
+ {
625
+ name: 'speed',
626
+ type: 'number',
627
+ constraint: { min: 0, integer: false },
628
+ engineVisible: true,
629
+ unit: 'px/tick',
630
+ description: 'Velocity magnitude for emitted particles (pixels/tick)'
631
+ },
632
+ {
633
+ name: 'auto_fire',
634
+ type: 'boolean',
635
+ engineVisible: true,
636
+ description: 'Whether all angle emitters fire automatically'
637
+ },
638
+ {
639
+ name: 'quantum_limit',
640
+ type: 'number',
641
+ constraint: { min: 1, integer: true },
642
+ engineVisible: true,
643
+ description: 'Max quanta per angle emitter (null = Infinity)'
644
+ },
645
+ {
646
+ name: 'delay',
647
+ type: 'number',
648
+ constraint: { min: 0, integer: true },
649
+ engineVisible: true,
650
+ description: 'Ticks between bursts for each angle emitter'
651
+ },
652
+ {
653
+ name: 'angle_increment',
654
+ type: 'number',
655
+ constraint: { min: 1, max: 360, integer: false },
656
+ engineVisible: true,
657
+ description: 'Degrees between emission angles. Fewer degrees = more angles = denser wave front. ' +
658
+ '5° gives 72 directions; 10° gives 36; 45° gives 8 (coarser).'
659
+ },
660
+ {
661
+ name: 'colorOverride',
662
+ type: 'rgb',
663
+ engineVisible: false,
664
+ description: 'Override render color for emitted particles [r, g, b] (render-only; stored on the emitter wrapper).'
665
+ },
666
+ {
667
+ name: 'chargeMode',
668
+ type: 'enum',
669
+ constraint: { values: { random: 'random', positive: 'positive', negative: 'negative', both: 'both' } },
670
+ engineVisible: false,
671
+ description: 'High-level charge mode (sugar layer). "both" creates two per-angle handle groups ' +
672
+ '(POSITIVE + NEGATIVE), doubling the emission count. Toggles the handle active flag — no engine ABI change.'
673
+ },
674
+ {
675
+ name: 'random_direction',
676
+ type: 'boolean',
677
+ engineVisible: false,
678
+ description: 'Whether emitted particles have randomized direction (sugar layer). ' +
679
+ 'PointSource normally uses directed emission; enabling this registers a new handle group with rd=true.'
680
+ },
681
+ {
682
+ name: 'layer',
683
+ type: 'number',
684
+ constraint: { min: 0, integer: true },
685
+ engineVisible: false,
686
+ description: 'Dimension layer this emitter targets (0 = fundamental; null = default). Stored on the emitter wrapper; no engine ABI change required.'
687
+ }
688
+ ]
689
+ };
690
+
691
+ //
692
+ // Reconstructs both the PointSource emitter AND a visual circle LargeObject
693
+ // handle (so the restored source is visible and hand-draggable, just like a
694
+ // QuantumEmitter). The marker is the source's x/y/diameter, owned by the source.
695
+ registerKind(PointSource, {
696
+ from: import.meta.url,
697
+ descriptor: PointSourceDescriptor,
698
+ defaults: {
699
+ x: 50, y: 100,
700
+ diameter: 16,
701
+ speed: 1,
702
+ chargeMode: 'random',
703
+ auto_fire: true,
704
+ quantum_limit: null,
705
+ delay: 0,
706
+ angle_increment: 10
707
+ },
708
+ build: function (props, ctx, Class) {
709
+ var PS = Class; // the class that registered (a composed fork's own class)
710
+ var LO3 = LargeObject;
711
+ var CT3 = CollisionTypes;
712
+ var psCx = props.x != null ? props.x : Math.round(ctx.x);
713
+ var psCy = props.y != null ? props.y : Math.round(ctx.y);
714
+ var psDiam = props.diameter || 16;
715
+ // Bug #1 fix: pass null as raw charge; set chargeMode via the sugar setter AFTER
716
+ // construction. The schema only captures chargeMode (not raw charge) now.
717
+ var ps = new PS(
718
+ ctx.runner.universe,
719
+ psCx,
720
+ psCy,
721
+ psDiam,
722
+ props.speed != null ? props.speed : 1,
723
+ null, // raw charge — overridden immediately below via chargeMode setter
724
+ props.auto_fire != null ? props.auto_fire : true,
725
+ props.quantum_limit != null ? props.quantum_limit : Infinity,
726
+ props.delay != null ? props.delay : 0,
727
+ props.angle_increment || 10,
728
+ {
729
+ // Re-pass the construction opts captured by serializeDesign so the
730
+ // rebuilt emitter keeps its wavelength (→ correct leaf mass / particle
731
+ // size + color), particle color, and emergent-physics mode.
732
+ wavelength: props._wavelength != null ? props._wavelength : null,
733
+ color: props._optColor || null,
734
+ emergentPhysics: props._emergentPhysics || false
735
+ }
736
+ );
737
+ ps._schemaKind = Class.name;
738
+ // Restore chargeMode via the sugar setter (handles 'random'|'positive'|'negative'|'both').
739
+ if (props.chargeMode) { ps.chargeMode = props.chargeMode; }
740
+ if (props.colorOverride) { ps.colorOverride = props.colorOverride; }
741
+
742
+ // Visual handle: circle LargeObject at the source position (its x/y/diameter).
743
+ var psVLeft = Math.round(psCx - psDiam / 2), psVTop = Math.round(psCy - psDiam / 2);
744
+ var psVW = psDiam, psVH = psDiam;
745
+ var psName = props.name || ('source ' + declaredCount(ctx.runner, 'PointSource'));
746
+ ps.name = psName; // the declared name is the source's; its marker wears it
747
+ var psVis = new LO3(ctx.runner.universe,
748
+ [psVLeft, psVTop, psVW, psVH],
749
+ CT3.NONE, [],
750
+ psName,
751
+ 'closest', null, 1,
752
+ [0, 0, 0]);
753
+ psVis.drawAsCircle = true;
754
+ psVis._schemaKind = 'LargeObject';
755
+ ps.attachMarker(psVis);
756
+ return ps;
757
+ }
758
+ });
759
+
760
+ // ── Export ───────────────────────────────────────────────────────────
761
+
762
+ export { PointSource };
763
+