@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,946 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * SphereEmitter — a p5/three-familiar OBJECT-CONSTRUCTOR facing over the
5
+ * existing `setupSphereEmitter3D` setup factory (ws-emitter-wave "Option B",
6
+ * CEO-approved 2026-07-20). It exists so a programmer can write the object idiom
7
+ *
8
+ * var sun = new SphereEmitter([x, y], opts).addTo(universe);
9
+ * sun.setCenter([x2, y2]); // reposition live
10
+ * sun.setWavelength(200); // dial the next leaf's mass
11
+ * sun.dispose();
12
+ *
13
+ * — hold the returned object, and have the emitter emit relative to its OWN
14
+ * position — WITHOUT forking a single line of the factory.
15
+ *
16
+ * ── DRY / PURE DELEGATION SHELL (the whole point) ────────────────────────────
17
+ * `setupSphereEmitter3D(universe, opts)` stays the ONE implementation of the
18
+ * sphere emitter (sampling, positioning, the host emit tick-fn, the live control
19
+ * handle, dispose). `SphereEmitter` adds ZERO sampling / positioning / emit /
20
+ * physics logic — `.addTo(universe)` simply calls the factory with
21
+ * `{ center:[x,y], ...opts }` and then WEARS the factory's returned
22
+ * `sphereEmitterControl` as an object face: every live method (`setCenter`,
23
+ * `setWavelength`, `setEmitEveryTicks`, `setActive`, `setPreset`, `setR`,
24
+ * `dispose`, …) is a straight pass-through to that control. There is NO second
25
+ * emit loop, NO re-implemented sampler, NO engine, NO physics here (canonical-
26
+ * bench SU1 — a delegation shell moves no golden). If you ever find yourself
27
+ * copying logic OUT of `setupSphereEmitter3D` into this file, stop and call the
28
+ * factory instead.
29
+ *
30
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
31
+ *
32
+ * `new SphereEmitter(position?, opts?)`
33
+ * @param {[number,number]|[number,number,number]|Object} [position] — emission
34
+ * SOURCE POSITION in ABSOLUTE canvas px (`[x,y]` or `[x,y,z]`), the center
35
+ * this emitter seeds its leaves around. Forwarded verbatim to the factory's
36
+ * `options.center`. If the FIRST argument is a plain options object (not an
37
+ * array), it is treated as `opts` and the position defaults to the canvas
38
+ * center (byte-identical to a factory call with no `center`) — so
39
+ * `new SphereEmitter()` and `new SphereEmitter({ preset:'laser' })` both work.
40
+ * @param {Object} [opts] — the SAME options `setupSphereEmitter3D` takes
41
+ * (`preset`, `emissionBoxSide`, `fundamentalDimension`, `leafDimension`,
42
+ * `wavelength`, `chargeMode`, `emissionShell`, `innerR`, `burstSize`,
43
+ * `emitEveryTicks`, `is2D`, `mode`, `R`, `renderColor`, …).
44
+ * Stored as-is; the universe is NOT touched until `.addTo()`. An explicit
45
+ * `position` argument WINS over `opts.center` (the position arg IS the
46
+ * source position — that is the object's whole purpose).
47
+ *
48
+ * `.addTo(universe) → this`
49
+ * Calls `setupSphereEmitter3D(universe, { center:position, ...opts })`, stores
50
+ * the returned `sphereEmitterControl`, generically forwards its every method
51
+ * onto this object (pure pass-through), and returns `this`. Now the emitter is
52
+ * LIVE (its host tick-fn is registered on `universe.experimentTickFunctions`)
53
+ * and the caller holds the object. Throws if called twice on one instance, or
54
+ * if `setupSphereEmitter3D` isn't loaded (FAIL-LOUD — never a silent no-op).
55
+ *
56
+ * Delegated live handle methods (identical signatures + behavior to the
57
+ * factory's control, since they ARE the factory's control functions):
58
+ * `setCenter(xOrArray, y?, z?)` — accepts EITHER the p5-familiar array
59
+ * (`setCenter([x,y])` / `[x,y,z]`) OR the factory's scalar form
60
+ * (`setCenter(x, y, z)`); both spread to the ONE `control.setCenter`.
61
+ * `getCenter() → [x,y,z]`, `setWavelength(v)`, `getWavelength()`,
62
+ * `getMaxWavelength()`, `setEmitEveryTicks(v)`, `getEmitEveryTicks()`,
63
+ * `setActive(bool)`, `setPreset(v)`, `getPreset()`, `setR(v)`, `getR()`,
64
+ * `setBurstSize(v)`, `getBurstSize()`, `setMode(v)`, `getMode()`,
65
+ * `setChargeMode(v)`, `getChargeMode()`, `setEmissionShell(v)`,
66
+ * `getEmissionShell()`, `setInnerR(v)`, `getInnerR()`, `setEmissionBoxSide(v)`,
67
+ * `getEmissionBoxSide()`, `setRenderColor(rgb)`, `getRenderColor()`,
68
+ * `setAxisRange(axis,lo,hi)`, `getAxisRange(axis)`, `getIs2D()`,
69
+ * `getLeafDimension()`, `getFundamentalDimension()`, `dispose()`, … — every
70
+ * function the control exposes is auto-forwarded (future-proof: a new control
71
+ * method is a new facade method with zero edits here).
72
+ *
73
+ * Convenience getters:
74
+ * `emitter.center` → `getCenter()` (or the pre-`addTo` staged position).
75
+ * `emitter.active` → the last `setActive` value (true until set false).
76
+ * `emitter.control` → the underlying `sphereEmitterControl` (escape hatch for
77
+ * `control.controls` / anything not surfaced as a method).
78
+ * `emitter.controls` → the standardized `runner.experimentControls`-compatible
79
+ * descriptor array (a straight ref to `control.controls`).
80
+ *
81
+ * `.dispose()` — removes this emitter's host tick-fn (idempotent — the factory's
82
+ * control.dispose splices by indexOf, so a double-dispose is a safe no-op).
83
+ *
84
+ * ── ESTIMATION BEFORE RUNNING: how long until there is something to see? ─────
85
+ *
86
+ * A reader must be able to ESTIMATE settle time BEFORE the first run — the dogfood that
87
+ * produced this section spent its first attempt on `{"total":0}` at 300 ticks with a
88
+ * detector 350 px away, and had no way to know that was simply too early rather than
89
+ * broken. THE HEURISTIC:
90
+ *
91
+ * ticks ≈ FLIGHT DISTANCE (px) ÷ the preset's EFFECTIVE FORWARD SPEED (px/tick)
92
+ *
93
+ * A quantum always MOVES at ~1 px/tick — but only a `particle-walk` beam spends all of
94
+ * that going forward. Every other preset spreads its directions, so the mean progress
95
+ * ALONG THE AXIS YOU CARE ABOUT is a FRACTION of the speed, and that fraction is what the
96
+ * heuristic divides by. **Effective forward speed, per preset** (MEASURED on the WASM
97
+ * engine: one 400-leaf burst at the preset defaults, free flight for 300 ticks, seed 42;
98
+ * the mean displacement per tick along the preset's own forward axis, and the mean radial
99
+ * displacement per tick):
100
+ *
101
+ * | preset | forward axis | forward px/tick | radial px/tick |
102
+ * |-----------------|----------------|-----------------|----------------|
103
+ * | `particle-walk` | +x (2D and 3D) | 1.00 | 1.00 |
104
+ * | `laser` | +x 2D / +z 3D | 0.99 / 0.98 | 1.00 |
105
+ * | `hemisphere` | +x (2D and 3D) | 0.65 / 0.49 | 1.00 |
106
+ * | `spherical` | — (isotropic) | ~0 (no drift) | 1.00 |
107
+ * | `drum` | — (in-plane) | ~0 (no drift) | 1.00 |
108
+ *
109
+ * For the two ISOTROPIC presets there is no forward axis: the front of the emission
110
+ * expands radially at ~1 px/tick, so a detector at distance d first sees arrivals at
111
+ * roughly d ticks whatever direction it is in. For a DIRECTED preset use the forward
112
+ * column — e.g. the walkthrough's `hemisphere` source in 3D with a plate 350 px downrange:
113
+ * 350 ÷ 0.49 ≈ **700 ticks** before the MEAN of the population reaches it. At 300 ticks
114
+ * the mean has covered ~150 px, which is exactly why that run counted zero.
115
+ *
116
+ * Read these as a random walk's MEAN, never a wavefront: the spread grows with distance,
117
+ * so the earliest arrivals land well before the estimate and the bulk lands well after.
118
+ * The estimate is for "roughly when is there something to look at", which is the question
119
+ * a reader actually has. `orientationDeg` rotates the forward axis without changing the
120
+ * fraction (a measure-preserving rotation), and `emitEveryTicks`/`burstSize` change HOW
121
+ * MANY arrive, never how fast they cross.
122
+ *
123
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
124
+ * ZERO physics (canonical-bench SU1). Every emergent behavior — uniform-
125
+ * direction ball-rejection sampling, the ±1px/tick move-gate, the transverse
126
+ * wave-walk, `wavelength=capacity/mass`, per-emitter `leafDimension`, the
127
+ * velocity-aligned surface ring launch, opposite-charge braiding — is REALIZED
128
+ * ENTIRELY by `setupSphereEmitter3D` + the engine it composes. This object is a
129
+ * naming/ergonomics face only; it cannot and does not change any golden. See
130
+ * `setupSphereEmitter3D.js`'s Layer 2 for the physics.
131
+ */
132
+
133
+ import { setupSphereEmitter3D } from '../objects/setup/setupSphereEmitter3D.js';
134
+ import { registerKind } from '../objects/kinds.js';
135
+ import { LargeObject } from './LargeObject.js';
136
+ import { CollisionTypes } from './CollisionTypes.js';
137
+
138
+ /**
139
+ * FACADE_OPTIONS — the option keys THIS OBJECT owns and the factory beneath it does
140
+ * not: they are extracted here, never forwarded (`addTo` strips them by driving off
141
+ * this list, so the strip and the declaration cannot drift).
142
+ *
143
+ * It is exported because a SECOND surface needs it. The canonical sketch idiom shares
144
+ * ONE bag across the world step and the part step —
145
+ * `exp.createCanvas(W, H, setupSphereEmitter3D.worldCfg(SRC)); exp.addObject('SphereEmitter', SRC);`
146
+ * (walkthrough.js cl1d and its 3D twin) — so `worldCfg` is handed a SUPERSET of the
147
+ * factory's own vocabulary and must tolerate exactly these keys. The factory cannot
148
+ * require this file (the arrow runs object → setup; a back-require is a cycle), so it
149
+ * declares `SKETCH_OPTIONS` and `setup-factory-option-vocabulary.test.mjs` §4 asserts
150
+ * that list equals `setupSphereEmitter3D.OPTIONS ∪ SphereEmitter.FACADE_OPTIONS` —
151
+ * the drift is a CI failure rather than a hand-kept mirror.
152
+ */
153
+ var FACADE_OPTIONS = ['emissionCap', 'gateCounter'];
154
+
155
+ /**
156
+ * @param {[number,number]|[number,number,number]|Object} [position]
157
+ * @param {Object} [opts]
158
+ */
159
+ function SphereEmitter(position, opts) {
160
+ this._schemaKind = 'SphereEmitter';
161
+ // First-arg overload: an array is a position; a plain object is opts
162
+ // (position defaults to canvas center → byte-identical factory default).
163
+ if (Array.isArray(position)) {
164
+ this._center = position.slice();
165
+ } else if (position && typeof position === 'object' && opts === undefined) {
166
+ opts = position;
167
+ this._center = null;
168
+ } else {
169
+ this._center = null;
170
+ }
171
+ this._opts = (opts && typeof opts === 'object') ? opts : {};
172
+ this._control = null; // set by addTo — the factory's sphereEmitterControl
173
+ this._setupResult = null; // the factory's full return (top-level dispose lives here)
174
+ this.universe = null;
175
+ this._active = true; // mirror of control state (control exposes no getActive)
176
+
177
+ // ── Declared EMISSION GATE (emissionCap/gateCounter — CEO ruling 05c98e1f,
178
+ // architect circulation-emission-gate-contract §2) ──────────────────────
179
+ // The Blackbody-thermostat pattern as DECLARED DATA: `emissionCap` is a
180
+ // self-throttle target — the emitter deactivates when the live population
181
+ // measured by the counting Wall NAMED by `gateCounter` reaches the cap, and
182
+ // reactivates below it. Compiled at CONSTRUCTION into a host tick-fn (see
183
+ // _installEmissionGate in addTo) so the gate is codegen-round-trippable and
184
+ // Restart-safe (the Restart-safe host-tick-fn rule — the gate is a
185
+ // construction side effect of captured descriptor fields, exactly like the
186
+ // ABSORB radiator, NEVER free recipe JS). Validation is throw-or-apply:
187
+ // a cap without a resolvable counter NAME can never work → THROW here.
188
+ // Zero physics (SU1) — pure emission regulation.
189
+ this._emissionCap = undefined;
190
+ this._gateCounter = undefined;
191
+ this._gateTickFn = null;
192
+ this._runner = null; // threaded by the ObjectFactory builder (by-name resolution seam)
193
+ if (this._opts.emissionCap != null) {
194
+ this._emissionCap = _validateEmissionCap(this._opts.emissionCap);
195
+ if (typeof this._opts.gateCounter !== 'string' || !this._opts.gateCounter) {
196
+ throw new Error('[SphereEmitter] emissionCap requires gateCounter — the NAME of the ' +
197
+ 'counting Wall (collisionBehavior "nothing" + count hook) whose getLiveCount() is ' +
198
+ 'the cap\'s population authority. An unresolved gate silently running uncapped is ' +
199
+ 'the FAIL-LOUD violation this contract closes.');
200
+ }
201
+ }
202
+ if (this._opts.gateCounter != null) {
203
+ if (typeof this._opts.gateCounter !== 'string' || !this._opts.gateCounter) {
204
+ throw new Error('[SphereEmitter] gateCounter must be a non-empty string (the counting Wall\'s name).');
205
+ }
206
+ this._gateCounter = this._opts.gateCounter;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Throw-or-apply validation for emissionCap (schema constraint {min:1, integer:true}).
212
+ * @param {*} v
213
+ * @returns {number}
214
+ */
215
+ function _validateEmissionCap(v) {
216
+ if (typeof v !== 'number' || !isFinite(v) || v < 1 || Math.floor(v) !== v) {
217
+ throw new Error('[SphereEmitter] emissionCap must be an integer >= 1; got ' + JSON.stringify(v) +
218
+ '. (Throw-or-apply — never a silently ignored cap.)');
219
+ }
220
+ return v;
221
+ }
222
+
223
+ /**
224
+ * Register this emitter on `universe` via the ONE factory and wear its
225
+ * control as an object face. Pure delegation — no physics, no second loop.
226
+ * @param {Object} universe - an EGPTUniverse (JS or WASM compat shim).
227
+ * @returns {SphereEmitter} this (for chaining, p5/three-style).
228
+ */
229
+ SphereEmitter.prototype.addTo = function (universe) {
230
+ if (this._control) {
231
+ throw new Error('[SphereEmitter] addTo() called twice — one SphereEmitter maps to one live emitter. Construct a new SphereEmitter for a second source.');
232
+ }
233
+ var factory = setupSphereEmitter3D;
234
+
235
+ // Build the factory options: the SAME opts, with the object's position
236
+ // as `center` (the position arg WINS over any opts.center). The declared
237
+ // emission-gate fields (emissionCap/gateCounter) are FACADE-owned regulation
238
+ // data — extracted in the constructor, never forwarded to the factory (the
239
+ // factory's emit contract is untouched; SU1).
240
+ var factoryOpts = {};
241
+ for (var k in this._opts) {
242
+ if (!Object.prototype.hasOwnProperty.call(this._opts, k)) { continue; }
243
+ if (FACADE_OPTIONS.indexOf(k) !== -1) { continue; }
244
+ factoryOpts[k] = this._opts[k];
245
+ }
246
+ if (Array.isArray(this._center)) { factoryOpts.center = this._center.slice(); }
247
+
248
+ var res = factory(universe, factoryOpts);
249
+ this.universe = universe;
250
+ this._setupResult = res;
251
+ this._control = res.sphereEmitterControl;
252
+
253
+ // Generic pure pass-through: forward EVERY control method onto this
254
+ // object (future-proof — a new control method needs no edit here).
255
+ // Methods ALREADY defined on the prototype (setCenter's array overload,
256
+ // setActive's _active mirror, dispose) are skipped so their adaptations win.
257
+ var control = this._control;
258
+ var self = this;
259
+ Object.keys(control).forEach(function (key) {
260
+ if (typeof control[key] === 'function' && typeof self[key] === 'undefined') {
261
+ self[key] = function () { return control[key].apply(control, arguments); };
262
+ }
263
+ });
264
+
265
+ // Compile the declared emission gate into a host tick-fn (construction-time
266
+ // side effect of the captured emissionCap/gateCounter fields — Restart-safe).
267
+ if (this._emissionCap != null) { this._installEmissionGate(universe); }
268
+ return this;
269
+ };
270
+
271
+ /**
272
+ * ── The compiled EMISSION-GATE host tick-fn (architect §3/§4 firm rulings) ───
273
+ *
274
+ * Layer 1 (API): registered on `universe.experimentTickFunctions` (the Phase-A
275
+ * `fn(universe)` seam — the SAME seam as the emit tick-fn, the blackbody
276
+ * thermostat, and the ABSORB radiator). Each tick it:
277
+ * 1. LAZILY resolves the counting Wall by NAME on the FIRST tick (handles are
278
+ * registration-fresh and never survive codegen — the name is the reference;
279
+ * lazy resolution makes emitter/counter construction order irrelevant).
280
+ * Unresolved after the first tick → THROW (never a silent uncapped run).
281
+ * 2. Reads the live population through `counter.getLiveCount(universe._lastTickData)`
282
+ * — the ONE population authority (loop-polish B). It never re-scans
283
+ * tickData.objects[...] inline and never reads a host activeQuanta list; the
284
+ * count-hook guard is REUSED by calling getLiveCount (which throws on a
285
+ * non-counting wall), never re-implemented. `_lastTickData` is the documented
286
+ * one-tick-lag cache the ABSORB radiator also samples (Phase A runs pre-tick).
287
+ * 3. Self-throttles via the emitter's OWN setActive: active while live < cap.
288
+ * The cap is re-read from `this._emissionCap` each tick, so a LIVE edit
289
+ * (Inspector → applyProp('emissionCap', v)) applies on the next tick; a cap
290
+ * cleared to null idles the gate (no setActive writes — no re-fighting a
291
+ * manual toggle).
292
+ *
293
+ * Layer 2 (physics): ZERO physics (SU1) — pure emission regulation, exactly the
294
+ * blackbody-thermostat pattern. The population dynamics it regulates are the
295
+ * engine's own.
296
+ *
297
+ * @param {Object} universe
298
+ */
299
+ SphereEmitter.prototype._installEmissionGate = function (universe) {
300
+ var self = this;
301
+ var counter = null; // resolved lazily on the first tick, cached after
302
+ var gateFn = function emissionGateTick(u) {
303
+ var cap = self._emissionCap;
304
+ if (cap == null) { return; } // cap cleared live → the gate idles
305
+ if (!counter) {
306
+ var runner = self._runner;
307
+ if (!runner || typeof runner.getObjectByName !== 'function') {
308
+ throw new Error('[SphereEmitter] emissionCap gate cannot resolve gateCounter "' +
309
+ self._gateCounter + '" — no runner with getObjectByName() is attached (construct ' +
310
+ 'via ObjectFactory/addObject, which threads the runner). FAIL-LOUD, never a ' +
311
+ 'silent uncapped run.');
312
+ }
313
+ var target = runner.getObjectByName(self._gateCounter);
314
+ if (!target) {
315
+ throw new Error('[SphereEmitter] emissionCap gate: gateCounter "' + self._gateCounter +
316
+ '" names no live object on the runner — place a counting Wall (collisionBehavior ' +
317
+ '"nothing" + hooks [\'count\']) with that name. FAIL-LOUD, never a silent uncapped run.');
318
+ }
319
+ if (typeof target.getLiveCount !== 'function') {
320
+ throw new Error('[SphereEmitter] emissionCap gate: "' + self._gateCounter +
321
+ '" is not a counting Wall (no getLiveCount). FAIL-LOUD, never a silent uncapped run.');
322
+ }
323
+ counter = target;
324
+ }
325
+ // The count-hook guard is getLiveCount's OWN throw (reused, not re-implemented);
326
+ // _lastTickData is null before the first completed tick → last-sample 0 → active.
327
+ var uni = (u && u.experimentTickFunctions) ? u : self.universe;
328
+ var alive = counter.getLiveCount(uni ? uni._lastTickData : null);
329
+ self.setActive(alive < cap);
330
+ };
331
+ gateFn._emissionGate = true; // countable marker (dispose-parity / no-stack tests)
332
+ universe.experimentTickFunctions.push(gateFn);
333
+ this._gateTickFn = gateFn;
334
+ };
335
+
336
+ function _requireLive(self) {
337
+ if (!self._control) {
338
+ throw new Error('[SphereEmitter] not added yet — call .addTo(universe) before driving the emitter.');
339
+ }
340
+ return self._control;
341
+ }
342
+
343
+ /**
344
+ * Reposition the emission SOURCE CENTER live (affects the NEXT emitted leaf).
345
+ * Accepts the p5-familiar array form `setCenter([x,y])` / `[x,y,z]` OR the
346
+ * factory's scalar form `setCenter(x, y, z)` — both spread to the ONE
347
+ * `control.setCenter`. Pure pass-through (the factory does the work).
348
+ */
349
+ SphereEmitter.prototype.setCenter = function (xOrArray, y, z) {
350
+ var control = _requireLive(this);
351
+ if (Array.isArray(xOrArray)) {
352
+ control.setCenter(xOrArray[0], xOrArray[1], xOrArray.length > 2 ? xOrArray[2] : undefined);
353
+ } else {
354
+ control.setCenter(xOrArray, y, z);
355
+ }
356
+ return this;
357
+ };
358
+
359
+ /**
360
+ * Toggle emission on/off (delegates to control.setActive; mirrors `_active`
361
+ * locally so the `active` getter can read it — the control exposes no getter).
362
+ */
363
+ SphereEmitter.prototype.setActive = function (v) {
364
+ var control = _requireLive(this);
365
+ this._active = !!v;
366
+ control.setActive(this._active);
367
+ return this;
368
+ };
369
+
370
+ /**
371
+ * LIVE emission-cap setter (the applyProp write path). Throw-or-apply: an
372
+ * invalid value THROWS; null/undefined CLEARS the cap (the gate tick-fn idles —
373
+ * it stops writing setActive rather than re-fighting a manual toggle). The gate
374
+ * tick-fn re-reads the cap each tick, so the new threshold applies next tick.
375
+ * NOTE: setting a cap live on an emitter constructed WITHOUT one does not build
376
+ * a gate (the gate is a CONSTRUCTION-time compile of the declared field —
377
+ * reconstruct with emissionCap to gain one); the value still captures for
378
+ * round-trip, and the rebuilt emitter gates.
379
+ * @param {number|null} v
380
+ * @returns {SphereEmitter} this
381
+ */
382
+ SphereEmitter.prototype.setEmissionCap = function (v) {
383
+ this._emissionCap = (v == null) ? undefined : _validateEmissionCap(v);
384
+ return this;
385
+ };
386
+
387
+ /**
388
+ * Remove this emitter's host tick-fns — the compiled emission gate (dispose
389
+ * PARITY with its construction-time push, the anti-stack rule the ABSORB
390
+ * radiator's dispose is the template for: without the splice every reinit
391
+ * STACKS a second gate) and the factory's emit tick-fn (control.dispose).
392
+ * Idempotent (indexOf splice — a second call no-ops). Safe pre-addTo (no-op).
393
+ */
394
+ SphereEmitter.prototype.dispose = function () {
395
+ if (this._gateTickFn && this.universe && Array.isArray(this.universe.experimentTickFunctions)) {
396
+ var i = this.universe.experimentTickFunctions.indexOf(this._gateTickFn);
397
+ if (i !== -1) { this.universe.experimentTickFunctions.splice(i, 1); }
398
+ }
399
+ this._gateTickFn = null;
400
+ if (this._control) { this._control.dispose(); }
401
+ // The marker is this emitter's (attachMarker): it leaves the world with it.
402
+ if (this._marker && typeof this._marker.dispose === 'function') { this._marker.dispose(); }
403
+ };
404
+
405
+ /**
406
+ * attachMarker(lo) — take ownership of the Design-mode MARKER (the NONE-collision circle
407
+ * LargeObject STUDIO draws at the emission centre). Called ONCE, by the ObjectFactory
408
+ * SphereEmitter builder after `addTo`. The emitter stamps the ONE up-link on it
409
+ * (`lo._owner = this` — so a pick or an aim that lands on the disc resolves to THIS
410
+ * emitter) and keeps the one down-link (`_marker`) it needs to move it and dispose it.
411
+ * Zero physics: the marker carries no frames and collides with nothing.
412
+ * @param {LargeObject} lo @returns {SphereEmitter} this
413
+ */
414
+ SphereEmitter.prototype.attachMarker = function (lo) {
415
+ if (!lo || typeof lo !== 'object') {
416
+ throw new TypeError('[SphereEmitter.attachMarker] needs the marker LargeObject. (FAIL-LOUD.)');
417
+ }
418
+ if (this._marker) {
419
+ throw new Error('[SphereEmitter.attachMarker] this emitter already owns a marker — one emitter, ' +
420
+ 'one marker. (FAIL-LOUD.)');
421
+ }
422
+ this._marker = lo;
423
+ lo._owner = this;
424
+ if (!this._control) {
425
+ throw new Error('[SphereEmitter.attachMarker] addTo() first — the marker follows the live control\'s ' +
426
+ 'centre write. (FAIL-LOUD.)');
427
+ }
428
+ this._control.attachMarker(lo); // the follow hangs on the ONE centre write
429
+ return this;
430
+ };
431
+
432
+ // Convenience getters (the object face — no logic, read-through only).
433
+ Object.defineProperty(SphereEmitter.prototype, 'center', {
434
+ get: function () { return this._control ? this._control.getCenter() : (this._center ? this._center.slice() : null); }
435
+ });
436
+ Object.defineProperty(SphereEmitter.prototype, 'active', {
437
+ get: function () { return this._active; }
438
+ });
439
+ Object.defineProperty(SphereEmitter.prototype, 'control', {
440
+ get: function () { return this._control; }
441
+ });
442
+ Object.defineProperty(SphereEmitter.prototype, 'controls', {
443
+ get: function () { return this._control ? this._control.controls : null; }
444
+ });
445
+
446
+ // ── Descriptor-field GETTERS (Bite 4b — captureProps read surface) ───────
447
+ // captureProps(sphereObj, SphereEmitterDescriptor) reads `sphereObj[field.name]`
448
+ // DIRECTLY for every descriptor field (a getter satisfies it). These expose the
449
+ // LIVE resolved value (from the factory's control) so a dragged/edited emitter
450
+ // Saves at its current state — the codegen round-trip authority. Boot-level fields
451
+ // (fundamentalWaveLength) that the control doesn't surface read from the
452
+ // construction opts (they cannot change live). `rect` is a WRITE-ONLY reposition
453
+ // adapter: its getter is deliberately `undefined` so captureProps SKIPS it —
454
+ // `center` is the single captured position authority (do NOT "fix" this to a value).
455
+ function _liveGetter(readFn) {
456
+ return { get: function () { return this._control ? readFn(this._control, this) : undefined; } };
457
+ }
458
+ Object.defineProperty(SphereEmitter.prototype, 'rect', {
459
+ // PR-1 item iii: a REAL `.rect` getter DERIVED from the live emission center (was
460
+ // hardwired undefined). computeEntityBboxForDesign reads this fresh each paint, so a
461
+ // center-only write (carry / drive / programmatic move) no longer leaves a stale Design
462
+ // bbox — rect DERIVES from the one authority (center), never parallel state. D=24 mirrors
463
+ // the ObjectFactory marker diameter + makeControlsForObject's rect-seed. captureProps no
464
+ // longer trips on this: the descriptor's rect field is `writeOnly:true`, so captureProps
465
+ // SKIPS it by the descriptor flag (not by the getter being undefined) — `center` stays the
466
+ // single captured position authority (codegen round-trip unchanged). The write path
467
+ // (rect → setCenter) is still the applyProp adapter (updateObjectProperty).
468
+ get: function () {
469
+ var c = (this._control && typeof this._control.getCenter === 'function')
470
+ ? this._control.getCenter() : null;
471
+ if (!Array.isArray(c) || typeof c[0] !== 'number' || typeof c[1] !== 'number') { return undefined; }
472
+ var D = SphereEmitter.MARKER_D, cz = (typeof c[2] === 'number') ? c[2] : 0;
473
+ return { left: c[0] - D / 2, top: c[1] - D / 2, z: cz - D / 2, w: D, h: D, d: D };
474
+ }
475
+ });
476
+ Object.defineProperty(SphereEmitter.prototype, 'preset', _liveGetter(function (c) { return c.getPreset(); }));
477
+ Object.defineProperty(SphereEmitter.prototype, 'chargeMode', _liveGetter(function (c) { return c.getChargeMode(); }));
478
+ Object.defineProperty(SphereEmitter.prototype, 'burstSize', _liveGetter(function (c) { return c.getBurstSize(); }));
479
+ Object.defineProperty(SphereEmitter.prototype, 'emitEveryTicks', _liveGetter(function (c) { return c.getEmitEveryTicks(); }));
480
+ Object.defineProperty(SphereEmitter.prototype, 'wavelength', _liveGetter(function (c) { return c.getWavelength(); }));
481
+ Object.defineProperty(SphereEmitter.prototype, 'emissionShell', _liveGetter(function (c) { return c.getEmissionShell(); }));
482
+ Object.defineProperty(SphereEmitter.prototype, 'innerR', _liveGetter(function (c) { return c.getInnerR(); }));
483
+ Object.defineProperty(SphereEmitter.prototype, 'renderColor', _liveGetter(function (c) { return c.getRenderColor(); }));
484
+ Object.defineProperty(SphereEmitter.prototype, 'renderShape', _liveGetter(function (c) { return c.getRenderShape(); }));
485
+ Object.defineProperty(SphereEmitter.prototype, 'fundamentalDimension', _liveGetter(function (c) { return c.getFundamentalDimension(); }));
486
+ Object.defineProperty(SphereEmitter.prototype, 'leafDimension', _liveGetter(function (c) { return c.getLeafDimension(); }));
487
+ Object.defineProperty(SphereEmitter.prototype, 'is2D', _liveGetter(function (c) { return c.getIs2D(); }));
488
+ Object.defineProperty(SphereEmitter.prototype, 'orientationDeg', _liveGetter(function (c) { return c.getOrientation(); }));
489
+ Object.defineProperty(SphereEmitter.prototype, 'emissionBox', _liveGetter(function (c) { return c.getEmissionBox(); }));
490
+ // Declared emission-gate fields (captured for codegen round-trip; undefined
491
+ // when uncapped → captureProps SKIPS them → byte-identical legacy scenes).
492
+ Object.defineProperty(SphereEmitter.prototype, 'emissionCap', {
493
+ get: function () { return this._emissionCap; }
494
+ });
495
+ Object.defineProperty(SphereEmitter.prototype, 'gateCounter', {
496
+ get: function () { return this._gateCounter; }
497
+ });
498
+ // Boot-level, not surfaced by the control — read from the construction opts.
499
+ Object.defineProperty(SphereEmitter.prototype, 'fundamentalWaveLength', {
500
+ get: function () { return (this._opts && this._opts.fundamentalWaveLength !== undefined) ? this._opts.fundamentalWaveLength : 64; }
501
+ });
502
+ // ── Live-edit WRITE ROUTER (Bite 4b — FrqtlApp.updateObjectProperty) ─
503
+ /**
504
+ * Route ONE schema-prop write to the matching factory-control setter (the
505
+ * mechanism-A write path — the Inspector / coupled-move calls this via
506
+ * FrqtlApp.updateObjectProperty, never resolveBind). `rect` is the
507
+ * reposition adapter: a rect write maps rect-CENTER → the emission center.
508
+ * Boot-level/derived fields have NO live setter (reload to change) — a write
509
+ * is a deliberate no-op on the running emitter (never a stale-mirror mutation).
510
+ * @param {string} prop — a SphereEmitter descriptor field name
511
+ * @param {*} value — the new value
512
+ * @returns {SphereEmitter} this
513
+ */
514
+ SphereEmitter.prototype.applyProp = function (prop, value) {
515
+ var c = this._control;
516
+ if (!c) { return this; }
517
+ switch (prop) {
518
+ case 'rect':
519
+ // Input adapter: rect-centre → the ONE centre write (a 6-element rect carries z).
520
+ if (Array.isArray(value) && value.length === 6) {
521
+ c.setCenter(value[0] + value[3] / 2, value[1] + value[4] / 2, value[2] + value[5] / 2);
522
+ } else if (Array.isArray(value) && value.length === 4) {
523
+ c.setCenter(value[0] + value[2] / 2, value[1] + value[3] / 2);
524
+ }
525
+ break;
526
+ case 'center':
527
+ if (Array.isArray(value)) { c.setCenter(value[0], value[1], value.length > 2 ? value[2] : undefined); }
528
+ break;
529
+ case 'preset': c.setPreset(value); break;
530
+ case 'chargeMode': c.setChargeMode(value); break;
531
+ case 'burstSize': c.setBurstSize(value); break;
532
+ case 'emitEveryTicks': c.setEmitEveryTicks(value); break;
533
+ case 'wavelength': c.setWavelength(value); break;
534
+ case 'emissionShell': c.setEmissionShell(value); break;
535
+ case 'innerR': c.setInnerR(value); break;
536
+ case 'renderColor': c.setRenderColor(value); break;
537
+ case 'renderShape': c.setRenderShape(value); break;
538
+ // LIVE "Emit Dimension" (Bite 5b A2) — host-side addDimension + closure flip;
539
+ // next emitted leaf is born at the new per-emitter dimension (M5). See
540
+ // setupSphereEmitter3D.setLeafDimension for the ratchet side effect (S1).
541
+ case 'leafDimension': c.setLeafDimension(value); break;
542
+ // B8 pulsar orientation (deg) — LIVE; sweeps a constrained beam, omni invariant.
543
+ case 'orientationDeg': c.setOrientation(value); break;
544
+ // Emission gate: the cap is LIVE (throw-or-apply; the gate tick-fn re-reads
545
+ // it next tick — truth-in-labeling readers see the new value immediately).
546
+ case 'emissionCap': this.setEmissionCap(value); break;
547
+ // gateCounter is boot-level (live:false — the gate resolves it once on the
548
+ // first tick; captured for round-trip). A write is a deliberate no-op.
549
+ case 'gateCounter': break;
550
+ // Boot-level / derived — no live setter (reload to change). No-op.
551
+ case 'fundamentalWaveLength': case 'fundamentalDimension':
552
+ case 'is2D': case 'emissionBox':
553
+ break;
554
+ default: break;
555
+ }
556
+ return this;
557
+ };
558
+
559
+ // ── Export ───────────────────────────────────────────────────────────
560
+ SphereEmitter.FACADE_OPTIONS = FACADE_OPTIONS;
561
+ /** The Design marker's edge, px (a D×D×D cube about the emission centre): the ONE constant the
562
+ * ObjectFactory builder constructs it with and the `rect` getter derives from. */
563
+ SphereEmitter.MARKER_D = 24;
564
+
565
+ /**
566
+ * SphereEmitter.worldCfg(opts) → the world cfg this emitter's physics needs (its wave-walk
567
+ * scale, fundamental dimension, …) for the sketch's co-init call:
568
+ *
569
+ * exp.createCanvas(W, H, SphereEmitter.worldCfg(SRC)); exp.addObject('SphereEmitter', SRC);
570
+ *
571
+ * PURE DELEGATION to the ONE function, setupSphereEmitter3D.worldCfg — byte-identical to calling
572
+ * it (sphere-emitter-facade.test asserts the identity). The setup factory is machinery, not a
573
+ * part, so this static is the published path to it.
574
+ */
575
+ SphereEmitter.worldCfg = function worldCfg(opts) {
576
+ return setupSphereEmitter3D.worldCfg(opts);
577
+ };
578
+
579
+ // ── The kind (objects/kinds.js): descriptor, construction defaults, palette, builder ──────────
580
+
581
+ /**
582
+ * SphereEmitter — the manipulable palette face over the setupSphereEmitter3D
583
+ * setup factory (ws-experiments-walkthrough Bite 4b). A SUGAR kind (peer of
584
+ * QuantumEmitter/PointSource — NOT a new engine kind): the ObjectFactory
585
+ * builder composes a LargeObject-visual marker + the factory's host emit
586
+ * tick-fn (SU1 — the visual LO is NONE-collision, non-leaf, zero physics).
587
+ *
588
+ * This descriptor is the SINGLE field authority: captureProps (codegen),
589
+ * makeControlsForObject (the Inspector), and FrqtlApp.updateObjectProperty
590
+ * (the live-edit write path) ALL drive off it — no hand-list anywhere.
591
+ *
592
+ * Per-field `live` attribute (Bite 4b, consumed by makeControlsForObject):
593
+ * live:true → a LIVE Inspector control; the write goes through
594
+ * updateObjectProperty(sphereObj, prop, v) → the control setter
595
+ * (mechanism A — NOT resolveBind; the SphereEmitter kind is
596
+ * deliberately outside controlBind's bind-executable allowlist).
597
+ * live:false → a read-only DISPLAY row (boot-level / derived fields that
598
+ * need a reload to change — same class as the renderer-swap
599
+ * reload ruling). captureProps STILL captures its VALUE for
600
+ * round-trip; the Inspector just renders it read-only.
601
+ *
602
+ * FIELD SET is COMPLETE w.r.t. every scene-affecting setupSphereEmitter3D opt
603
+ * (a missing opt = lossy codegen). `emissionBoxSide` is subsumed by the
604
+ * captured derived `emissionBox`. There is no init flag: the SKETCH inits the
605
+ * world once (setupSphereEmitter3D.initWorld / the co-init call), so a second emitter on
606
+ * one universe (6A/6B/6C) is simply a second object.
607
+ */
608
+ var SphereEmitterDescriptor = {
609
+ description: 'A spherical/omni "sun" emitter — uniform-direction ball-rejected emission ' +
610
+ '(preset spherical/laser/drum/particle-walk). The manipulable palette face over the ' +
611
+ 'setupSphereEmitter3D setup factory: pure sugar (a NONE-collision visual marker + a host ' +
612
+ 'emit tick-fn), zero physics of its own. Drag the marker to move the emission center live; ' +
613
+ 'edit burst/rate/wavelength/charge/preset/shell live; dimension/is2D are boot-level (reload).',
614
+ // ── STUDIO UX presentation contract (Bite 5b A0) ──────────────────────
615
+ // A sugar object DECLARES its own STUDIO UX (CEO §9 "the design question").
616
+ // The vocab is owned once at lib/shim/field-descriptor (UX_HANDLE_ZONES /
617
+ // UX_DRAGGABLE_MODES / UX_SHELL_RENDER); this block is read via
618
+ // ObjectSchema.ux('SphereEmitter'). resizable:false — a SphereEmitter has
619
+ // TWO radii (inner+outer); a single-box resize gesture is ambiguous, so the
620
+ // CEO ruled it NOT resizable (individually, per object). draggable 2D (a 3D
621
+ // scene never cursor-drags). handles: ['move'] — it only moves, never scales.
622
+ // shellRender:'design' — the marker paints only while authoring (Bite-4b
623
+ // default; the "red light bulb" flips a per-instance shellRender override).
624
+ ux: {
625
+ resizable: false,
626
+ draggable: { '2d': true },
627
+ handles: ['move'],
628
+ shellRender: 'design'
629
+ },
630
+ fields: [
631
+ {
632
+ name: 'rect',
633
+ type: 'rect',
634
+ engineVisible: false,
635
+ live: true,
636
+ // PR-1 item iii: the facade's `.rect` getter now returns a REAL rect DERIVED from
637
+ // the live center (so computeEntityBboxForDesign reads fresh — no stale bbox). To
638
+ // keep `center` the SINGLE captured position authority (codegen round-trip), this
639
+ // field is `writeOnly:true` → captureProps SKIPS it BY THE FLAG (not by the getter
640
+ // being undefined). The WRITE path is unchanged: a rect write maps rect-CENTER →
641
+ // the emission center (control.setCenter, via applyProp).
642
+ writeOnly: true,
643
+ description: 'WRITE-ONLY reposition ADAPTER (drag handle). A rect write — from the visual ' +
644
+ 'marker\'s coupled-move or a rect edit — maps rect-CENTER → the emission center ' +
645
+ '(control.setCenter). Its READ getter DERIVES a fresh rect from `center` (PR-1 item ' +
646
+ 'iii, for the Design bbox); captureProps SKIPS it via writeOnly:true so `center` stays ' +
647
+ 'the single captured position authority. Inspector seeds the vector control from ' +
648
+ '`center` via a FIELD_VALUE_OVERRIDE.'
649
+ },
650
+ {
651
+ name: 'center',
652
+ type: 'object',
653
+ engineVisible: false,
654
+ live: false,
655
+ description: 'Emission SOURCE POSITION in absolute canvas px as [x,y,z] (the captured ' +
656
+ 'position authority — codegen round-trips this, not rect). Edited live via the rect ' +
657
+ 'adapter / the visual marker drag; shown read-only here.'
658
+ },
659
+ {
660
+ name: 'preset',
661
+ type: 'enum',
662
+ constraint: { values: { spherical: 'spherical', hemisphere: 'hemisphere', laser: 'laser', drum: 'drum', 'particle-walk': 'particle-walk' } },
663
+ engineVisible: false,
664
+ live: true,
665
+ description: 'Velocity-range preset: spherical (full ball / the sun), hemisphere (the SAME ' +
666
+ 'ball-rejected sampler with the forward axis halved to [0,R] — 180° forward emission, ' +
667
+ 'aimed by orientationDeg), laser (collimated beam), drum (z=0 disk), particle-walk ' +
668
+ '(deterministic +X drift). LIVE (next burst).'
669
+ },
670
+ {
671
+ name: 'chargeMode',
672
+ type: 'enum',
673
+ constraint: { values: { positive: 'positive', negative: 'negative', random: 'random' } },
674
+ engineVisible: false,
675
+ live: true,
676
+ description: 'Per-emitted-leaf charge: positive (0), negative (1), or random (per-leaf ±, ' +
677
+ 'the braiding that makes self-interference structure). LIVE (affects the next leaf).'
678
+ },
679
+ {
680
+ name: 'burstSize',
681
+ type: 'number',
682
+ constraint: { min: 1, integer: true },
683
+ engineVisible: false,
684
+ live: true,
685
+ description: 'Leaves emitted per burst (flux). LIVE (affects the next burst).'
686
+ },
687
+ {
688
+ name: 'emitEveryTicks',
689
+ type: 'number',
690
+ constraint: { min: 1, integer: true },
691
+ engineVisible: false,
692
+ live: true,
693
+ unit: 'ticks',
694
+ description: 'Ticks between bursts (sustained-emission cadence). LIVE (affects the next burst).'
695
+ },
696
+ {
697
+ // ── Declared EMISSION GATE (CEO ruling 05c98e1f — the Blackbody-thermostat
698
+ // pattern; architect circulation-emission-gate-contract §2 field shapes).
699
+ // Sensor = a placeable counting Wall (name in gateCounter); regulation =
700
+ // emitter-side sugar compiled at CONSTRUCTION into a host tick-fn (the
701
+ // Restart-safe host-tick-fn rule — codegen captures these two fields and
702
+ // the reconstructed emitter's constructor rebuilds the gate).
703
+ name: 'emissionCap',
704
+ type: 'number',
705
+ label: 'Population Limit',
706
+ constraint: { min: 1, integer: true },
707
+ widgetConfig: { max: 12800, step: 1 }, // stable slider range (constraint has no max); 2x the cl1d default 6400
708
+ engineVisible: false,
709
+ live: true,
710
+ description: 'Self-throttle target: the emitter deactivates when the live population ' +
711
+ 'measured by gateCounter reaches this value and reactivates when it drops below. ' +
712
+ 'null/omitted = uncapped (byte-identical to today). LIVE (next tick). Zero physics ' +
713
+ '(SU1) — pure emission regulation, like the blackbody thermostat.'
714
+ },
715
+ {
716
+ name: 'gateCounter',
717
+ type: 'string',
718
+ engineVisible: false,
719
+ live: false,
720
+ visibleWhen: { field: 'emissionCap', present: true },
721
+ description: 'The NAME of the counting Wall (collisionBehavior:"nothing" + count hook) ' +
722
+ 'whose getLiveCount() is this cap\'s population authority. Resolved lazily by name ' +
723
+ 'at tick time (construction-order independent). FAIL-LOUD if absent once ' +
724
+ 'emissionCap is set.'
725
+ },
726
+ {
727
+ name: 'wavelength',
728
+ type: 'number',
729
+ constraint: { min: 1, integer: true },
730
+ engineVisible: false,
731
+ live: true,
732
+ description: 'Per-leaf wavelength arg → mass=max(1,capacity/wavelength), hence the transverse ' +
733
+ 'oscillation period (CLAUDE.md §6 fork). A NO-OP until fundamentalDimension is raised ' +
734
+ '(capacity 1 saturates mass). LIVE (affects the next leaf).'
735
+ },
736
+ {
737
+ name: 'emissionShell',
738
+ type: 'enum',
739
+ constraint: { values: { volume: 'volume', surface: 'surface' } },
740
+ engineVisible: false,
741
+ live: true,
742
+ description: 'Emission start topology: volume (seed within the box) or surface (velocity-' +
743
+ 'aligned ring launch at innerR — empty center, sharp bands). LIVE (affects the next leaf).'
744
+ },
745
+ {
746
+ name: 'innerR',
747
+ type: 'number',
748
+ constraint: { min: 0, integer: true },
749
+ engineVisible: false,
750
+ live: true,
751
+ unit: 'px',
752
+ description: 'Shell radius for emissionShell:surface (0 collapses to the center point; ' +
753
+ 'ignored in volume mode). LIVE (affects the next leaf).'
754
+ },
755
+ {
756
+ name: 'renderColor',
757
+ type: 'rgb',
758
+ engineVisible: false,
759
+ live: true,
760
+ description: 'Per-leaf render color override [r,g,b] (render-intent data, zero physics); ' +
761
+ 'null = colorless (scheme-driven fullness→ROYGBIV). LIVE (affects the next leaf).'
762
+ },
763
+ {
764
+ name: 'renderShape',
765
+ type: 'enum',
766
+ constraint: { values: { ball: 'ball', box: 'box', circle: 'circle', cube: 'cube', sphere: 'sphere' } },
767
+ engineVisible: false,
768
+ live: true,
769
+ description: 'Per-leaf render.shape override (render-intent data, zero physics) — peer of ' +
770
+ 'renderColor. Stamped into string_map[\'render.shape\'] on each emitted leaf via the ' +
771
+ 'provider render-intent MERGE seam. Overrides the render-level "Default Shape" ' +
772
+ '(setRenderOption). null/omitted = fall back to the default. LIVE (affects the next leaf).'
773
+ },
774
+ {
775
+ // EMITTER ORIENTATION (B8 pulsar) — the CURRENT orientation the velocity sampler
776
+ // rotates each sampled (vx,vy) about z by. Spinning a constrained (laser) emitter
777
+ // sweeps the beam (a pulsar); omni ('spherical') is rotationally invariant. Also the
778
+ // LOOK-drive rotation target. SU1: at 0 the rotation is the identity (byte-identical).
779
+ name: 'orientationDeg',
780
+ type: 'number',
781
+ constraint: { min: 0, max: 360 },
782
+ engineVisible: false,
783
+ live: true,
784
+ widgetHint: 'slider',
785
+ description: 'Emitter orientation in degrees (default 0). The velocity sampler rotates ' +
786
+ 'each emitted (vx,vy) by this angle (M5: affects the NEXT leaf), so spinning a ' +
787
+ 'constrained/laser emitter sweeps its beam (pulsar); an omni emitter is unaffected. ' +
788
+ 'LIVE via control.setOrientation; the LOOK-drive rotation target.'
789
+ },
790
+ {
791
+ name: 'fundamentalWaveLength',
792
+ type: 'number',
793
+ constraint: { min: 1, integer: true },
794
+ engineVisible: false,
795
+ live: false,
796
+ description: 'Init-time wavelengthScale seed (waveLengthMultiplier=floor(fwl/4)); boot-level ' +
797
+ '(set at universe.init — reload to change). Captured for round-trip.'
798
+ },
799
+ {
800
+ name: 'fundamentalDimension',
801
+ type: 'number',
802
+ constraint: { min: 0, integer: true },
803
+ engineVisible: false,
804
+ live: false,
805
+ description: 'The universe base dimension this emitter inits at; boot-level (reload to change). ' +
806
+ 'Raises leaf capacity so the wavelength knob gains leverage. Captured for round-trip.'
807
+ },
808
+ {
809
+ name: 'leafDimension',
810
+ type: 'number',
811
+ constraint: { min: 0, integer: true },
812
+ engineVisible: false,
813
+ live: true,
814
+ // Field NAME stays `leafDimension` (codegen/capture key; a rename = round-trip
815
+ // break + a second name = schema mirror — architect Q-A2). The Inspector shows
816
+ // the display LABEL "Emit Dimension"; the write routes through applyProp →
817
+ // control.setLeafDimension (host-side addDimension + closure var flip; the NEXT
818
+ // emitted leaf is born at the new dimension). Theorist CLEARED mid-run addDimension
819
+ // as SAFE (2026-07-22 verdict). SIDE EFFECT (S1): a value BELOW the current
820
+ // fundamental floor self-lowers the floor UNIVERSE-GLOBALLY and is a ONE-WAY
821
+ // RATCHET (no removeDimension) — the Inspector surfaces a hint when crossing below
822
+ // floor. fundamentalDimension stays display-only (universe base = reinit-class).
823
+ label: 'Emit Dimension',
824
+ // R2-2: rendered as a SCROLLER LIST (a <select> showing the current dimension +
825
+ // choices), NOT a slider — via the resolveWidget 'dimension-select' widget. The
826
+ // option list is dynamic runtime state sourced from the emitter (fundamental +
827
+ // current leafDimension), so the descriptor stays the single FIELD authority while
828
+ // the candidate values come from the world (not a schema mirror).
829
+ widgetHint: 'dimension-select',
830
+ description: 'The per-emitter dimension leaves are born at (default = fundamentalDimension). ' +
831
+ 'LIVE (host-side addDimension + closure flip; next emitted leaf). A leaf born ABOVE ' +
832
+ 'the fundamental is move-gated (Invariant 6A) — slower/larger/redder, the per-dimension ' +
833
+ 'observable. A value BELOW the current fundamental self-lowers the floor universe-globally ' +
834
+ '(a one-way ratchet). Captured for round-trip.'
835
+ },
836
+ {
837
+ name: 'is2D',
838
+ type: 'boolean',
839
+ engineVisible: false,
840
+ live: false,
841
+ description: 'Whether the z-axis is inactive (2D disk of directions vs 3D ball); boot-level ' +
842
+ '(set at init via activeAxes — reload to change). Captured for round-trip.'
843
+ },
844
+ {
845
+ name: 'emissionBox',
846
+ type: 'object',
847
+ engineVisible: false,
848
+ live: false,
849
+ description: 'Derived per-axis emission-position box {x,y,z} (subsumes the scalar ' +
850
+ 'emissionBoxSide input). Captured for round-trip; frozen when re-passed on reload.'
851
+ }
852
+ ]
853
+ };
854
+
855
+ //
856
+ // The manipulable palette face over setupSphereEmitter3D (Bite 4b). Mirrors the
857
+ // QuantumEmitter/PointSource pattern: build the emitter, then a NONE-collision
858
+ // circle LargeObject "marker" the emitter OWNS (attachMarker), and return the
859
+ // emitter — the declared object STUDIO picks, drags and carries. SU1:
860
+ // the marker is NONE-collision + not a leaf → pure render/control surface, zero
861
+ // physics. The emitter itself is the SphereEmitter facade (pure delegation over
862
+ // setupSphereEmitter3D). It is a PART: it never inits the world — the SKETCH
863
+ // (the recipe's co-init call / a page's setup factory) did that before any object.
864
+ registerKind(SphereEmitter, {
865
+ from: import.meta.url,
866
+ descriptor: SphereEmitterDescriptor,
867
+ defaults: {
868
+ preset: 'spherical',
869
+ is2D: true
870
+ },
871
+ palette: { displayName: 'Sphere Emitter', category: 'emitter', iconHint: 'emitter', rank: 0 },
872
+ build: function (props, ctx, Class) {
873
+ var SphereEmitterCls = Class; // the class that registered (a composed fork's own class)
874
+
875
+ // Forward the config VERBATIM to the factory (via the facade) — MINUS the declared
876
+ // `name` (the facade's own construction fact). (The write-only `rect` adapter never
877
+ // reaches a builder: create() refuses it as a construction key.) This verbatim forward
878
+ // is the opts-identity guarantee: the emitter sees exactly what the recipe
879
+ // declared (the walkthrough intercept test asserts this tuple-for-tuple).
880
+ var opts = {};
881
+ for (var k in props) {
882
+ if (!Object.prototype.hasOwnProperty.call(props, k)) { continue; }
883
+ if (k === 'name') { continue; }
884
+ opts[k] = props[k];
885
+ }
886
+ // F1 (tester V2 FAIL, 2026-07-23): an interactive palette DROP carries the
887
+ // click-projected world point in ctx.x/ctx.y (buildObject sets fromPalette:true).
888
+ // Thread it into the emitter's `center` (ABSOLUTE canvas px) so the drop lands at
889
+ // the click, mirroring what the LargeObject/QuantumEmitter builders already do —
890
+ // the emitter otherwise self-inits at its default canvas center and the projected
891
+ // point is silently discarded (every drop landed at [300,300,0]). GUARD: only for
892
+ // a real palette drop (fromPalette) AND only when the recipe/opts did NOT already
893
+ // declare placement (center/offset) — so the recipe/rung path (addObject, no
894
+ // fromPalette, relies on the default center) stays BYTE-IDENTICAL and an explicit
895
+ // recipe center/offset still wins (the opts-identity guarantee).
896
+ if (ctx.fromPalette && opts.center == null && opts.offset == null &&
897
+ typeof ctx.x === 'number' && typeof ctx.y === 'number') {
898
+ opts.center = [ctx.x, ctx.y];
899
+ }
900
+ // Construction validates the option bag (its owner's migration messages win) and
901
+ // registers nothing; the marker classes are resolved before addTo registers the
902
+ // emitter, so a missing class never leaves a marker-less emitter in the world.
903
+ var sphere = new SphereEmitterCls(opts);
904
+ sphere._schemaKind = Class.name;
905
+ var LO = LargeObject;
906
+ var CT = CollisionTypes;
907
+ // Thread the runner BEFORE addTo (which compiles the declared emission gate):
908
+ // the gate's lazy by-name gateCounter resolution goes through the runner's
909
+ // canonical resolver (FrqtlApp.getObjectByName) on its first tick.
910
+ sphere._runner = ctx.runner;
911
+ sphere.addTo(ctx.runner.universe);
912
+
913
+ // Visual marker (circle LargeObject, NONE collision) — the drag/inspect handle.
914
+ var center = (typeof sphere.getCenter === 'function') ? sphere.getCenter() : [ctx.x, ctx.y, 0];
915
+ // The marker is z-capable from the start (a D×D×D cube about the centre, d STATED): the
916
+ // emitter re-places it at every centre write (attachMarker), x, y AND z.
917
+ var D = SphereEmitterCls.MARKER_D;
918
+ var cz = (typeof center[2] === 'number') ? center[2] : 0;
919
+ var vRect = [Math.round(center[0] - D / 2), Math.round(center[1] - D / 2), cz - D / 2, D, D, D];
920
+ // The declared NAME is the emitter's own construction fact; its marker wears it.
921
+ sphere.name = props.name || ('sphere emitter ' + (ctx.runner.largeObjects.length));
922
+ var vis = new LO(ctx.runner.universe,
923
+ vRect,
924
+ CT.NONE, [],
925
+ sphere.name,
926
+ 'closest', null, 1,
927
+ [240, 200, 80]);
928
+ vis.drawAsCircle = true;
929
+ // Shell render defaults (Bite 5b R2-3) so the Inspector's Shell section seeds live
930
+ // controls (render-only; no visual change — alpha 255 = opaque, shellRender 'design'
931
+ // = the Bite-4b marker default, shellShape 'circle' = the current drawAsCircle look).
932
+ vis.alpha = 255;
933
+ vis.shellRender = 'design';
934
+ vis.shellShape = 'circle';
935
+ vis._schemaKind = 'LargeObject';
936
+ // STUDIO Design-mode-ONLY marker — the renderer suppresses it during Run and
937
+ // in pure-output/standalone bundles (a render toggle at the output boundary,
938
+ // zero physics). AuthorView flips a Design-mode signal; see the renderer.
939
+ vis._designMarker = true;
940
+ sphere.attachMarker(vis);
941
+ return sphere;
942
+ }
943
+ });
944
+
945
+ export { SphereEmitter };
946
+