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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/CLAUDE.md +87 -0
  2. package/LICENSE.md +664 -0
  3. package/README.md +84 -2
  4. package/dist/dist-manifest.json +218 -0
  5. package/dist/eyes/index.js +109 -0
  6. package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
  7. package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
  8. package/dist/fat/pkg-node/package.json +12 -0
  9. package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
  10. package/dist/frqtl/compat/boot-node.js +34 -0
  11. package/dist/frqtl/compat/engine.js +127 -0
  12. package/dist/frqtl/compat/package-root.js +24 -0
  13. package/dist/frqtl/compat/providers/wasm-engine.js +2499 -0
  14. package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
  15. package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
  16. package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
  17. package/dist/frqtl/compat/renderer/Filters.js +163 -0
  18. package/dist/frqtl/compat/renderer/RenderFamily.js +84 -0
  19. package/dist/frqtl/compat/renderer/RenderMesh.js +99 -0
  20. package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
  21. package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
  22. package/dist/frqtl/compat/renderer/captureStill-common.js +258 -0
  23. package/dist/frqtl/compat/renderer/display-slot.js +96 -0
  24. package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
  25. package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
  26. package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
  27. package/dist/frqtl/compat/renderer/providers/index.js +22 -0
  28. package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
  29. package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
  30. package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
  31. package/dist/frqtl/compat/renderer/providers/three-provider.js +2470 -0
  32. package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
  33. package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
  34. package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
  35. package/dist/frqtl/compat/vendor.js +196 -0
  36. package/dist/frqtl/node-entry.js +17 -0
  37. package/dist/frqtl/objects/FrqtlApp.js +2920 -0
  38. package/dist/frqtl/objects/ObjectFactory.js +296 -0
  39. package/dist/frqtl/objects/RecipeContext.js +322 -0
  40. package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
  41. package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
  42. package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
  43. package/dist/frqtl/objects/instruments/capture-host.js +453 -0
  44. package/dist/frqtl/objects/instruments/capture.js +710 -0
  45. package/dist/frqtl/objects/instruments/hosted-eyes.js +984 -0
  46. package/dist/frqtl/objects/kinds-entry.js +14 -0
  47. package/dist/frqtl/objects/kinds.js +190 -0
  48. package/dist/frqtl/objects/objects/Box.js +273 -0
  49. package/dist/frqtl/objects/objects/Bullet.js +155 -0
  50. package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
  51. package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
  52. package/dist/frqtl/objects/objects/DirectedTunnel.js +411 -0
  53. package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
  54. package/dist/frqtl/objects/objects/LightSource.js +351 -0
  55. package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
  56. package/dist/frqtl/objects/objects/Tube.js +471 -0
  57. package/dist/frqtl/objects/objects/absorbRadiation.js +309 -0
  58. package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
  59. package/dist/frqtl/objects/objects/controlAction.js +89 -0
  60. package/dist/frqtl/objects/objects/controlBind.js +278 -0
  61. package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
  62. package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
  63. package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
  64. package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
  65. package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
  66. package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
  67. package/dist/frqtl/objects/objects/makeControlsForObject.js +820 -0
  68. package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
  69. package/dist/frqtl/objects/objects/slitLinerScatter.js +261 -0
  70. package/dist/frqtl/objects/objects/sustainedSource.js +298 -0
  71. package/dist/frqtl/objects/objects/tagger.js +228 -0
  72. package/dist/frqtl/objects/schema/engine-scopes.js +468 -0
  73. package/dist/frqtl/objects/schema/index.js +256 -0
  74. package/dist/frqtl/objects/schema/scopes.js +457 -0
  75. package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
  76. package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
  77. package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
  78. package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
  79. package/dist/frqtl/objects/setup/_worldInit.js +162 -0
  80. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
  81. package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
  82. package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
  83. package/dist/frqtl/objects/setup/setupConeEmitter3D.js +250 -0
  84. package/dist/frqtl/objects/setup/setupDoubleSlit.js +326 -0
  85. package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
  86. package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
  87. package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
  88. package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
  89. package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
  90. package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
  91. package/dist/frqtl/objects/setup/setupLightSourceLab.js +277 -0
  92. package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
  93. package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
  94. package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
  95. package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1258 -0
  96. package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
  97. package/dist/frqtl/objects/setup/setupTunnelLab.js +240 -0
  98. package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
  99. package/dist/frqtl/objects/setup/setupWaveInterference.js +177 -0
  100. package/dist/frqtl/objects/v1-replacements.js +29 -0
  101. package/dist/frqtl/package-core.js +23 -0
  102. package/dist/frqtl/parts/CameraControls.js +643 -0
  103. package/dist/frqtl/parts/CameraPath.js +575 -0
  104. package/dist/frqtl/parts/CollisionActions.js +34 -0
  105. package/dist/frqtl/parts/CollisionTypes.js +30 -0
  106. package/dist/frqtl/parts/DetectorWall.js +496 -0
  107. package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
  108. package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
  109. package/dist/frqtl/parts/FrqtlVision.js +818 -0
  110. package/dist/frqtl/parts/GraphingDetectorWall.js +489 -0
  111. package/dist/frqtl/parts/LargeObject.js +405 -0
  112. package/dist/frqtl/parts/LightColor.js +255 -0
  113. package/dist/frqtl/parts/LiveChart.js +285 -0
  114. package/dist/frqtl/parts/LogicGate.js +717 -0
  115. package/dist/frqtl/parts/OvenBox.js +611 -0
  116. package/dist/frqtl/parts/PointSource.js +762 -0
  117. package/dist/frqtl/parts/Rectangle.js +200 -0
  118. package/dist/frqtl/parts/SlitLightSource.js +234 -0
  119. package/dist/frqtl/parts/SlitScreen.js +628 -0
  120. package/dist/frqtl/parts/SphereEmitter.js +946 -0
  121. package/dist/frqtl/parts/Trace.js +508 -0
  122. package/dist/frqtl/parts/Tunnel.js +1057 -0
  123. package/dist/frqtl/parts/Wall.js +1211 -0
  124. package/dist/frqtl/parts/index.js +33 -0
  125. package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
  126. package/dist/frqtl/scenes/absorbing-wall.js +32 -0
  127. package/dist/frqtl/scenes/atomic-model.js +88 -0
  128. package/dist/frqtl/scenes/big-bang.js +46 -0
  129. package/dist/frqtl/scenes/blackbody.js +81 -0
  130. package/dist/frqtl/scenes/circuit-sat.js +127 -0
  131. package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
  132. package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
  133. package/dist/frqtl/scenes/counting-wall.js +38 -0
  134. package/dist/frqtl/scenes/double-slit.js +31 -0
  135. package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
  136. package/dist/frqtl/scenes/dse-one-slit.js +114 -0
  137. package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
  138. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +243 -0
  139. package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
  140. package/dist/frqtl/scenes/dse-two-slits.js +132 -0
  141. package/dist/frqtl/scenes/empty.js +14 -0
  142. package/dist/frqtl/scenes/laser-buildup.js +49 -0
  143. package/dist/frqtl/scenes/mirror-3d.js +56 -0
  144. package/dist/frqtl/scenes/mirror-route.js +140 -0
  145. package/dist/frqtl/scenes/mirror.js +38 -0
  146. package/dist/frqtl/scenes/omni-3d.js +27 -0
  147. package/dist/frqtl/scenes/omni-source.js +22 -0
  148. package/dist/frqtl/scenes/self-interference.js +39 -0
  149. package/dist/frqtl/scenes/smoking-gun.js +122 -0
  150. package/dist/frqtl/scenes/straight-walk.js +22 -0
  151. package/dist/frqtl/scenes/sun.js +39 -0
  152. package/dist/frqtl/scenes/two-source.js +37 -0
  153. package/dist/frqtl/scenes/wave.js +26 -0
  154. package/dist/shim/bundle/frqnb.js +112 -0
  155. package/dist/shim/bundle/index.js +121 -0
  156. package/dist/shim/bundle/indexHtml.js +107 -0
  157. package/dist/shim/bundle/manifest.js +95 -0
  158. package/dist/shim/bundle/zip.js +243 -0
  159. package/dist/shim/bus.js +67 -0
  160. package/dist/shim/controls/index.js +627 -0
  161. package/dist/shim/controls/registry.js +274 -0
  162. package/dist/shim/engine/artifact-shell.js +211 -0
  163. package/dist/shim/engine/boot-phases.js +44 -0
  164. package/dist/shim/engine/engine-stack-boot.js +59 -0
  165. package/dist/shim/engine/router.js +690 -0
  166. package/dist/shim/experiment-frame/index.js +296 -0
  167. package/dist/shim/expr/ast-schema.js +369 -0
  168. package/dist/shim/field-descriptor/index.js +325 -0
  169. package/dist/shim/frame-data/index.js +224 -0
  170. package/dist/shim/index.js +67 -0
  171. package/dist/shim/notebook/canvas-guard.js +82 -0
  172. package/dist/shim/notebook/catalog.js +35 -0
  173. package/dist/shim/notebook/format.js +736 -0
  174. package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
  175. package/dist/shim/notebook/kernel.js +671 -0
  176. package/dist/shim/notebook/knob.js +150 -0
  177. package/dist/shim/notebook/module-resolver.js +260 -0
  178. package/dist/shim/notebook/notebook-knobs.js +65 -0
  179. package/dist/shim/notebook/produced-bindings.js +67 -0
  180. package/dist/shim/notebook/realm-registry.js +121 -0
  181. package/dist/shim/notebook/realm-transport.js +73 -0
  182. package/dist/shim/notebook/setup-builder.js +507 -0
  183. package/dist/shim/notebook/store.js +159 -0
  184. package/dist/shim/notebook/vendor-importmap.js +134 -0
  185. package/dist/shim/notebook/view-builder.js +189 -0
  186. package/dist/shim/notebook/view-ctx.js +100 -0
  187. package/dist/shim/notebook/view-handle.js +202 -0
  188. package/dist/shim/notebook/warmup.js +102 -0
  189. package/dist/shim/package.json +4 -0
  190. package/dist/shim/page-params/index.js +213 -0
  191. package/dist/shim/project/identity.js +112 -0
  192. package/dist/shim/project/index.js +19 -0
  193. package/dist/shim/project/link.js +402 -0
  194. package/dist/shim/project/node-specifier-hook.mjs +35 -0
  195. package/dist/shim/project/register-node.mjs +17 -0
  196. package/dist/shim/project/specifiers.js +106 -0
  197. package/dist/shim/registry/copy-gate.js +110 -0
  198. package/dist/shim/registry/exhibits.js +455 -0
  199. package/dist/shim/registry/experiments.js +215 -0
  200. package/dist/shim/registry/index.js +19 -0
  201. package/dist/shim/registry/scene-source.js +157 -0
  202. package/dist/shim/registry/walkthrough.js +804 -0
  203. package/dist/shim/render/capture-envelope.js +279 -0
  204. package/dist/shim/render/heat-colormap.js +92 -0
  205. package/dist/shim/render/render-stages.js +855 -0
  206. package/dist/shim/render/sun-look.js +33 -0
  207. package/dist/shim/render-state/apply.js +262 -0
  208. package/dist/shim/render-state/index.js +74 -0
  209. package/dist/shim/rpc/index.js +207 -0
  210. package/dist/shim/runtime/index.js +1023 -0
  211. package/dist/shim/storage/boot.js +69 -0
  212. package/dist/shim/storage/index.js +169 -0
  213. package/dist/shim/storage/local-storage-provider.js +230 -0
  214. package/dist/shim/storage/make-namespace-store.js +132 -0
  215. package/dist/shim/storage/modules.js +249 -0
  216. package/dist/shim/storage/projects.js +150 -0
  217. package/llms.txt +12 -0
  218. package/package.json +63 -4
@@ -0,0 +1,575 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * CameraPath.js — a declarative, REPRODUCIBLE camera path.
5
+ *
6
+ * ONE object for both modes. It drives `runner.cameraControl` — the single camera write path —
7
+ * with ABSOLUTE poses derived from waypoints, so the same waypoints and the same tick always
8
+ * produce the same picture. There is **no `if (is3D)` anywhere**: a 3D path declares full
9
+ * orbit-bag waypoints, a 2D path declares `{distance, target}` waypoints, and both go through
10
+ * the identical per-field interpolation. Which fields are legal is read from the handle's own
11
+ * `poseFields` — the consumer asks what axes EXIST, never what mode it is in.
12
+ *
13
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
14
+ *
15
+ * `CameraPath.attach(runner, { waypoints, loop? }) → handle`
16
+ *
17
+ * @param {object} runner — an FrqtlApp. The ONLY couplings: `runner.cameraControl`
18
+ * (the capability-checked camera handle), the per-tick listener fan-out, and
19
+ * `runner.repaint()`. No renderer-sniffing, no engine, no capture knowledge.
20
+ * @param {Array<{atTick:number, pose:object}>} waypoints — at least one. `pose` is a PARTIAL
21
+ * orbit bag with `setOrbit` patch semantics. Sorted by `atTick` at attach.
22
+ * @param {boolean} [loop=false] — wrap the path modulo its duration.
23
+ * @returns {{
24
+ * start():void, stop():void, dispose():void,
25
+ * poseAt(tick):object, // PURE — same waypoints + same tick ⇒ same pose
26
+ * onWaypoint(cb):function, // cb({ tick, index, pose }); returns an unsubscribe
27
+ * running:boolean
28
+ * }}
29
+ *
30
+ * `CameraPath.poseAt(waypoints, tick, loop)` is also exported as a FREE function — pure, no
31
+ * runner, no wall clock, unit-testable in plain Node (the `three-camera-geometry` discipline).
32
+ *
33
+ * ── Why absolute writes, and why not look()/move() ───────────────────────────
34
+ *
35
+ * Playback is a tick-fan-out subscriber calling `cameraControl.setOrbit(poseAt(tick))`:
36
+ * declarative ABSOLUTE writes through the one camera authority. It deliberately does NOT drive
37
+ * the incremental `look()`/`move()` surface — integrating deltas would accumulate drift and
38
+ * make the path unreproducible, which is the exact property this object exists to guarantee.
39
+ * The tick is the ONLY clock: no wall time, no RAF coupling, so a paused-and-stepped run and a
40
+ * live run trace the identical path.
41
+ *
42
+ * ── Interpolation ────────────────────────────────────────────────────────────
43
+ *
44
+ * Per-field over the bag, between the two bracketing waypoints:
45
+ * - `azimuth`/`roll` — SHORTEST-ARC mod 360 (a camera turning 350° the long way round is
46
+ * never what a waypoint pair meant).
47
+ * - every other number (`elevation`, `distance`, …) — scalar lerp.
48
+ * - `target` — componentwise; a segment mixing `target:null` with a concrete target THROWS
49
+ * at attach, because lerping "the scene centre" against a point is undefined. Declare both
50
+ * or neither on adjacent waypoints.
51
+ * A field present in only ONE of the two bracketing waypoints is held constant (no invented
52
+ * midpoint against an unstated value).
53
+ *
54
+ * ── FAIL-LOUD (a declared camera path that cannot fly is not a hidden affordance) ───
55
+ *
56
+ * `attach` THROWS when `runner.cameraControl` is null (a provider with NO pose control at all)
57
+ * or when a waypoint names a field the handle does not declare (e.g. `azimuth` in 2D). This
58
+ * deliberately BREAKS from the `CameraControls` fail-soft precedent, and the distinction is
59
+ * the same one the capture contract draws: a UI widget that hides is an affordance; a declared
60
+ * camera path that silently does not fly, under a golden or a corpus row, is FABRICATED EVIDENCE.
61
+ *
62
+ * ── Capture at a waypoint belongs to NEITHER object ──────────────────────────
63
+ *
64
+ * It is composition by the caller:
65
+ *
66
+ * `drone.onWaypoint(async () => rows.push(await vision.getStill()));`
67
+ *
68
+ * `FrqtlVision` keeps zero camera knowledge (no pose on capture) and `CameraPath` keeps zero
69
+ * capture knowledge — a `capture:true` waypoint flag would be the coupling that makes both
70
+ * objects unpluggable separately. No data coupling is needed because the provenance already
71
+ * closes the loop: every still stamps `pose` from the live camera authority, so a
72
+ * camera path-captured row records the ACTUAL applied pose. The two objects compose through the
73
+ * contract, not through each other. A callback is invoked and NOT awaited — the tick clock is
74
+ * never held hostage by a consumer (a throwing callback is surfaced, never swallowed).
75
+ *
76
+ * ── Repaint ──────────────────────────────────────────────────────────────────
77
+ *
78
+ * A pose write on a PAUSED scene changes nothing visible until something redraws, which reads
79
+ * as "the camera is broken". So every pose write is followed by `runner.repaint()` — the
80
+ * existing one-shot redraw that re-renders the LAST TickData without advancing the clock. The
81
+ * same recipe applies to a manual pan/zoom: `setOrbit(...)` → `repaint()` → capture.
82
+ *
83
+ * ── Layer 2: physics behaviour ───────────────────────────────────────────────
84
+ * ZERO physics. A camera path is an observation instrument: it changes which pixels you are
85
+ * shown, never a frame, a dimension or a tick. No canonical-bench golden can move because of it.
86
+ *
87
+ * Usage:
88
+ * var drone = CameraPath.attach(runner, { waypoints: [
89
+ * { atTick: 0, pose: { distance: 400, target: null } },
90
+ * { atTick: 200, pose: { distance: 60, target: { x: 300, y: 220 } } }
91
+ * ]});
92
+ * drone.onWaypoint(async () => rows.push(await vision.getStill()));
93
+ * drone.start();
94
+ */
95
+
96
+
97
+ // NO hand list of angular axes. Wrap-ness is DECLARED by the camera that owns the axis
98
+ // (`poseFields[].constraint.wrap` — the period of a periodic value space) and FERRIED here.
99
+ // A hand-written ['azimuth','roll'] encoded, in this file, a fact the contract holds
100
+ // elsewhere: a camera declaring a NEW wrapping axis would have silently got long-way-round
101
+ // interpolation until someone remembered to edit this list.
102
+
103
+ function _fail(msg) {
104
+ throw new Error('[CameraPath] ' + msg + ' (FAIL-LOUD.)');
105
+ }
106
+
107
+ function _finite(v, where) {
108
+ if (typeof v === 'number' && isFinite(v)) { return v; }
109
+ _fail(where + ' must be a finite number, got ' + String(v));
110
+ }
111
+
112
+ /**
113
+ * Shortest-arc interpolation on a value space of the given PERIOD (declared, not assumed).
114
+ * @private
115
+ */
116
+ function _lerpAngle(a, b, t, period) {
117
+ var half = period / 2;
118
+ var d = ((((b - a) % period) + period + half) % period) - half; // → (-half, half]
119
+ return a + d * t;
120
+ }
121
+
122
+ function _lerp(a, b, t) { return a + (b - a) * t; }
123
+
124
+ /**
125
+ * Componentwise interpolation of a point, over the UNION of both endpoints' numeric
126
+ * components — so {x,y} and {x,y,z} both work with no mode branch and nothing is dropped.
127
+ * A component present in only one endpoint is HELD (the same rule as a pose field).
128
+ * @private
129
+ */
130
+ function _lerpPoint(a, b, t) {
131
+ var out = {};
132
+ var keys = _poseKeys(a, b);
133
+ for (var i = 0; i < keys.length; i++) {
134
+ var k = keys[i], va = a[k], vb = b[k];
135
+ if (typeof va === 'number' && typeof vb === 'number') { out[k] = _lerp(va, vb, t); }
136
+ else { out[k] = (vb === undefined) ? va : vb; }
137
+ }
138
+ return out;
139
+ }
140
+
141
+ /**
142
+ * poseAt(waypoints, tick, loop) — THE pure path evaluation.
143
+ *
144
+ * Same waypoints + same tick ⇒ same pose, always. No wall clock, no RAF, no runner: this is
145
+ * what makes a camera path-addressed golden reproducible rather than merely repeatable.
146
+ *
147
+ * @param {Array<{atTick:number, pose:object}>} waypoints — assumed sorted (attach sorts)
148
+ * @param {number} tick
149
+ * @param {boolean} [loop=false]
150
+ * @param {Object<string,number>} [wrapByField] — field name → period, FERRIED from the
151
+ * camera's `poseFields[].constraint.wrap`. A field listed here interpolates on the SHORTEST
152
+ * ARC; everything else is a plain lerp. Omitted ⇒ no field wraps (a caller with no camera
153
+ * is not entitled to guess which axes are circular).
154
+ * @returns {object} an orbit-bag PATCH to hand to `setOrbit`
155
+ */
156
+ function poseAt(waypoints, tick, loop, wrapByField) {
157
+ if (!Array.isArray(waypoints) || !waypoints.length) { _fail('poseAt needs at least one waypoint'); }
158
+ var first = waypoints[0];
159
+ var last = waypoints[waypoints.length - 1];
160
+ var t = _finite(tick, 'tick');
161
+
162
+ if (loop && waypoints.length > 1) {
163
+ var span = last.atTick - first.atTick;
164
+ if (span > 0) { t = first.atTick + (((t - first.atTick) % span) + span) % span; }
165
+ }
166
+ // Before the first / after the last waypoint the path HOLDS that endpoint — it never
167
+ // extrapolates into poses no waypoint declared.
168
+ if (t <= first.atTick) { return _copyPose(first.pose); }
169
+ if (t >= last.atTick) { return _copyPose(last.pose); }
170
+
171
+ var i = 0;
172
+ while (i < waypoints.length - 1 && waypoints[i + 1].atTick <= t) { i++; }
173
+ var a = waypoints[i], b = waypoints[i + 1];
174
+ var span2 = b.atTick - a.atTick;
175
+ var f = (span2 > 0) ? (t - a.atTick) / span2 : 0;
176
+
177
+ var out = {};
178
+ var keys = _poseKeys(a.pose, b.pose);
179
+ for (var k = 0; k < keys.length; k++) {
180
+ var key = keys[k];
181
+ var va = a.pose[key], vb = b.pose[key];
182
+ // A field present in only ONE bracketing waypoint is HELD — never lerped against an
183
+ // unstated value.
184
+ if (va === undefined) { out[key] = _copyValue(vb); continue; }
185
+ if (vb === undefined) { out[key] = _copyValue(va); continue; }
186
+ if (key === 'target') {
187
+ if (va === null && vb === null) { out[key] = null; continue; }
188
+ // The null/concrete mix is rejected at attach; this is the belt-and-braces.
189
+ if (va === null || vb === null) {
190
+ _fail('cannot interpolate `target` between null (scene centre) and a concrete ' +
191
+ 'point across the segment at ticks ' + a.atTick + '→' + b.atTick +
192
+ ' — declare target on both waypoints or neither');
193
+ }
194
+ // COMPONENTWISE over whatever components the point actually carries — a 2D
195
+ // target is {x,y}, a 3D target is {x,y,z}. Hardcoding x/y here silently DROPPED
196
+ // z on every 3D tracking path (found by the 3D-target gate, not by review).
197
+ out[key] = _lerpPoint(va, vb, f);
198
+ continue;
199
+ }
200
+ if (typeof va === 'number' && typeof vb === 'number') {
201
+ var period = wrapByField ? wrapByField[key] : undefined;
202
+ out[key] = (typeof period === 'number' && period > 0)
203
+ ? _lerpAngle(va, vb, f, period) : _lerp(va, vb, f);
204
+ continue;
205
+ }
206
+ out[key] = _copyValue(vb);
207
+ }
208
+ return out;
209
+ }
210
+
211
+ function _poseKeys(a, b) {
212
+ var seen = Object.create(null), out = [];
213
+ [a, b].forEach(function (p) {
214
+ if (!p) { return; }
215
+ Object.keys(p).forEach(function (k) { if (!seen[k]) { seen[k] = 1; out.push(k); } });
216
+ });
217
+ return out;
218
+ }
219
+
220
+ function _copyValue(v) {
221
+ // A plain-data copy over ALL own keys — never a hand-listed {x,y}, which dropped z from
222
+ // every 3D target it touched.
223
+ if (v && typeof v === 'object') { return JSON.parse(JSON.stringify(v)); }
224
+ return v;
225
+ }
226
+
227
+ /** A pose copy — a handed-out pose never aliases a waypoint the caller still holds. */
228
+ function _copyPose(pose) {
229
+ var out = {};
230
+ if (!pose) { return out; }
231
+ Object.keys(pose).forEach(function (k) { out[k] = _copyValue(pose[k]); });
232
+ return out;
233
+ }
234
+
235
+ /**
236
+ * attach(runner, opts) — validate the path against the camera's DECLARED axes, then bind it.
237
+ * @param {object} runner @param {{waypoints:Array, loop?:boolean}} opts
238
+ * @returns {object} the handle (see the module docstring)
239
+ */
240
+ function attach(runner, opts) {
241
+ if (!runner || typeof runner.addTickListener !== 'function') {
242
+ _fail('attach needs an FrqtlApp with a per-tick listener fan-out');
243
+ }
244
+ var cam = runner.cameraControl;
245
+ if (!cam) {
246
+ _fail('this renderer exposes NO camera pose control (runner.cameraControl is null), so a ' +
247
+ 'declared camera path cannot fly. A UI widget may hide on an absent capability; a ' +
248
+ 'programmatic instrument must not silently do nothing — a golden or a corpus row ' +
249
+ 'built on a camera path that never moved is fabricated evidence');
250
+ }
251
+ opts = opts || {};
252
+ var waypoints = opts.waypoints;
253
+ if (!Array.isArray(waypoints) || !waypoints.length) {
254
+ _fail('attach needs at least one waypoint: [{ atTick, pose }]');
255
+ }
256
+
257
+ // The DECLARED axes of THIS camera — ferried from the handle, never a hand-written list.
258
+ // This is the whole reason there is no mode sniffing: 2D declares {distance, target}, 3D
259
+ // declares the full bag, and the SAME validation covers both.
260
+ var declared = {};
261
+ var fields = cam.poseFields || [];
262
+ for (var f = 0; f < fields.length; f++) { declared[fields[f].name] = true; }
263
+ var declaredNames = Object.keys(declared);
264
+ // FERRIED wrap-ness: field name → period, straight off the camera's own descriptors.
265
+ var wrapByField = {};
266
+ for (var w = 0; w < fields.length; w++) {
267
+ var c = fields[w].constraint;
268
+ if (c && typeof c.wrap === 'number' && c.wrap > 0) { wrapByField[fields[w].name] = c.wrap; }
269
+ }
270
+
271
+ var sorted = waypoints.slice().sort(function (a, b) { return a.atTick - b.atTick; });
272
+ for (var i = 0; i < sorted.length; i++) {
273
+ var wp = sorted[i];
274
+ if (!wp || typeof wp !== 'object') { _fail('waypoint ' + i + ' is not an object'); }
275
+ _finite(wp.atTick, 'waypoint ' + i + ' atTick');
276
+ if (!wp.pose || typeof wp.pose !== 'object') { _fail('waypoint ' + i + ' has no pose bag'); }
277
+ var keys = Object.keys(wp.pose);
278
+ if (!keys.length) { _fail('waypoint ' + i + ' has an EMPTY pose bag'); }
279
+ for (var k = 0; k < keys.length; k++) {
280
+ var key = keys[k];
281
+ if (declaredNames.length && !declared[key]) {
282
+ _fail('waypoint ' + i + ' names pose field "' + key + '", which this camera does ' +
283
+ 'NOT declare. Declared axes: [' + declaredNames.join(', ') + ']. Read ' +
284
+ 'cameraControl.poseFields rather than assuming a mode — a silently skipped ' +
285
+ 'axis is a camera path that does not fly');
286
+ }
287
+ var val = wp.pose[key];
288
+ if (typeof val === 'number') { _finite(val, 'waypoint ' + i + ' ' + key); }
289
+ if (key === 'target' && val !== null && val !== undefined) {
290
+ if (typeof val.x !== 'number' || typeof val.y !== 'number') {
291
+ _fail('waypoint ' + i + ' target must be {x,y} (2D) or {x,y,z} (3D), or null');
292
+ }
293
+ // Validate EVERY numeric component the point carries (z included), so a
294
+ // non-finite z fails at attach rather than steering the camera to NaN.
295
+ var comps = Object.keys(val);
296
+ for (var c = 0; c < comps.length; c++) {
297
+ if (typeof val[comps[c]] === 'number') {
298
+ _finite(val[comps[c]], 'waypoint ' + i + ' target.' + comps[c]);
299
+ }
300
+ }
301
+ }
302
+ }
303
+ // Reject the undefined null/concrete target mix AT ATTACH, where the diagnosis is
304
+ // cheap, rather than mid-flight where it is a grenade.
305
+ if (i > 0) {
306
+ var prev = sorted[i - 1].pose;
307
+ var pHas = ('target' in prev), cHas = ('target' in wp.pose);
308
+ if (pHas && cHas && ((prev.target === null) !== (wp.pose.target === null))) {
309
+ _fail('the segment at ticks ' + sorted[i - 1].atTick + '→' + wp.atTick +
310
+ ' mixes target:null (the scene centre) with a concrete point — that ' +
311
+ 'interpolation is undefined. Declare target on both waypoints or neither');
312
+ }
313
+ }
314
+ }
315
+
316
+ var loop = !!opts.loop;
317
+ // ── THE ANCHOR ──────────────────────────────────────────────────────────
318
+ // `atTick` is PATH-RELATIVE: tick 0 is "when this camera path starts", not universe tick 0.
319
+ // Absolute ticks were unusable in practice — attaching a drone to an already-running
320
+ // page (a probe, a Studio session, any corpus run that boots the scene first) meant
321
+ // every waypoint was ALREADY IN THE PAST, so `poseAt` returned the final pose and every
322
+ // waypoint fired at once against it. That is not a timing race; it is a coordinate-system
323
+ // bug, and it silently produced well-formed rows that all carried the last pose.
324
+ //
325
+ // `anchorTick` pins the origin explicitly for exact reproduction (a corpus row can record
326
+ // it); omitted, the anchor is the universe tick at `start()`.
327
+ var anchorTick = (typeof opts.anchorTick === 'number' && isFinite(opts.anchorTick))
328
+ ? opts.anchorTick : null;
329
+ var anchored = null;
330
+ // Safety valve for an async waypoint handler that never settles (see _holdUntilSettled).
331
+ // 0 disables it — accept a permanent freeze rather than a non-waypoint-exact capture.
332
+ var waypointTimeoutMs = (typeof opts.waypointTimeoutMs === 'number')
333
+ ? opts.waypointTimeoutMs : 10000;
334
+ // How many suspends THIS drone currently holds, so stop()/dispose() can release exactly
335
+ // its own and never another consumer's.
336
+ var heldCount = 0;
337
+ var listeners = [];
338
+ var unsubscribe = null;
339
+ var lastFiredIndex = -1;
340
+
341
+ var draining = false;
342
+
343
+ function _applyAt(tick) { _applyPose(poseAt(sorted, tick, loop, wrapByField)); }
344
+
345
+ function _applyPose(pose) {
346
+ cam.setOrbit(pose);
347
+ // A pose write on a PAUSED scene is invisible until something redraws — working code
348
+ // that reads as broken. repaint() re-renders the LAST TickData without advancing the
349
+ // tick clock (the clock is the loop's alone).
350
+ if (typeof runner.repaint === 'function') { runner.repaint(); }
351
+ }
352
+
353
+ /**
354
+ * Fire the handlers for every waypoint newly reached, holding the TICK CLOCK until they
355
+ * settle.
356
+ *
357
+ * ── Why the clock is held (this REVERSES the D9b default, deliberately) ──
358
+ * D9b fired handlers and did not await them, on the rule "the tick clock is never held
359
+ * hostage by a consumer". That rule is right for a PASSIVE observer, and wrong here. At
360
+ * unbounded TPS the runner executes a whole batch of ticks inside one rAF frame, so an
361
+ * `await` inside a handler resumes only AFTER the batch — and a capture taken there
362
+ * records the pose and pixels of the LAST tick of the batch, not the waypoint's. The
363
+ * provenance stays truthful (the still stamps what the canvas actually showed), which is
364
+ * precisely what makes the failure so quiet: the row is well-formed and simply is not the
365
+ * waypoint's record. For corpus building that is worthless.
366
+ *
367
+ * A waypoint is not an observation, it is a SYNCHRONISATION POINT — "this instant,
368
+ * specifically" is its entire content. So the narrower correct rule: a waypoint handler
369
+ * MAY hold the clock; it must not be able to hold it forever. Hence the counted
370
+ * suspend/resume pair and the safety valve below.
371
+ *
372
+ * The tick SEQUENCE is unchanged (tick N is tick N), so physics determinism is untouched;
373
+ * only wall-clock pacing changes. And the drone still knows NOTHING about capture — it
374
+ * guarantees the world is frozen at the waypoint and lets the consumer do whatever it
375
+ * likes with that instant.
376
+ */
377
+ function _notifyWaypoints(tick) {
378
+ for (var i = 0; i < sorted.length; i++) {
379
+ if (sorted[i].atTick <= tick && i > lastFiredIndex) {
380
+ lastFiredIndex = i;
381
+ var evt = { tick: sorted[i].atTick, index: i, pose: _copyPose(sorted[i].pose) };
382
+ var pending = [];
383
+ for (var L = 0; L < listeners.length; L++) {
384
+ // A throwing SYNCHRONOUS handler is surfaced and cannot abort its peers.
385
+ try {
386
+ var r = listeners[L](evt);
387
+ if (r && typeof r.then === 'function') { pending.push(r); }
388
+ } catch (e) { _surface('onWaypoint callback threw', e); }
389
+ }
390
+ if (pending.length) { _holdUntilSettled(pending, evt); }
391
+ }
392
+ }
393
+ }
394
+
395
+ function _surface(what, e) {
396
+ if (typeof console !== 'undefined' && console.error) {
397
+ console.error('[CameraPath] ' + what + ':', e);
398
+ }
399
+ }
400
+
401
+ /**
402
+ * Hold the tick clock until every pending handler settles.
403
+ *
404
+ * The suspend is raised SYNCHRONOUSLY, inside the tick listener, so the runner's batch
405
+ * loop (which checks the depth per tick) breaks immediately — before another tick can
406
+ * move the world out from under an in-flight capture.
407
+ *
408
+ * The safety valve: a handler that never settles would freeze the sim, and a frozen canvas
409
+ * is ambiguous rather than loud. After `waypointTimeoutMs` the clock is released and the
410
+ * problem is reported. It cannot affect a healthy path (it never fires there), so it buys
411
+ * a loud failure without introducing wall-clock dependence into the deterministic route.
412
+ */
413
+ function _holdUntilSettled(pending, evt, cont) {
414
+ if (typeof runner.suspendTicks !== 'function') {
415
+ // A runner predating the suspend contract: the capture cannot be made
416
+ // waypoint-exact, and pretending otherwise would fabricate corpus rows.
417
+ _fail('this runner exposes no suspendTicks()/resumeTicks(), so an async ' +
418
+ 'onWaypoint handler cannot be guaranteed to observe waypoint ' + evt.index +
419
+ '. Upgrade the runner, or use only synchronous handlers');
420
+ }
421
+ runner.suspendTicks();
422
+ heldCount += 1;
423
+ var released = false;
424
+ var release = function () {
425
+ if (released) { return; }
426
+ released = true;
427
+ heldCount -= 1;
428
+ runner.resumeTicks();
429
+ if (typeof cont === 'function') { cont(); }
430
+ };
431
+ var timer = null;
432
+ if (waypointTimeoutMs > 0 && typeof setTimeout === 'function') {
433
+ timer = setTimeout(function () {
434
+ if (!released) {
435
+ _surface('an onWaypoint handler for waypoint ' + evt.index + ' did not settle ' +
436
+ 'within ' + waypointTimeoutMs + 'ms — releasing the tick clock so the sim ' +
437
+ 'does not stay frozen. That capture is NOT waypoint-exact',
438
+ new Error('waypoint handler timeout'));
439
+ release();
440
+ }
441
+ }, waypointTimeoutMs);
442
+ }
443
+ var settle = function (results) {
444
+ if (timer !== null && typeof clearTimeout === 'function') { clearTimeout(timer); }
445
+ if (results) {
446
+ for (var i = 0; i < results.length; i++) {
447
+ if (results[i] && results[i].status === 'rejected') {
448
+ _surface('onWaypoint callback rejected', results[i].reason);
449
+ }
450
+ }
451
+ }
452
+ release();
453
+ };
454
+ // allSettled so ONE rejecting handler cannot strand the clock for its peers.
455
+ if (typeof Promise.allSettled === 'function') {
456
+ Promise.allSettled(pending).then(settle, function (e) { _surface('waypoint hold failed', e); release(); });
457
+ } else {
458
+ Promise.all(pending.map(function (p) { return p.then(null, function () {}); }))
459
+ .then(function () { settle(null); }, function () { release(); });
460
+ }
461
+ }
462
+
463
+ function _universeTick(tickData) {
464
+ return (tickData && tickData.universeData && typeof tickData.universeData.tick === 'number')
465
+ ? tickData.universeData.tick : null;
466
+ }
467
+
468
+ /**
469
+ * Drain every waypoint that has become due, ONE AT A TIME, each against ITS OWN pose.
470
+ *
471
+ * Two properties this buys, both load-bearing for corpus rows:
472
+ * - a waypoint handler always observes the pose of THAT waypoint, never a later one. If
473
+ * several waypoints come due in a single tick (a stalled frame, or a path denser than
474
+ * the tick rate), firing them all against the current interpolated pose would stamp
475
+ * them identically — the exact shape of the realm defect.
476
+ * - only ONE waypoint is ever in flight, so the clock hold is unambiguous.
477
+ */
478
+ function _drainDue(pathTick) {
479
+ if (draining) { return; }
480
+ var next = lastFiredIndex + 1;
481
+ if (next >= sorted.length || sorted[next].atTick > pathTick) { return; }
482
+ draining = true;
483
+ _fireWaypoint(next, pathTick);
484
+ }
485
+
486
+ function _fireWaypoint(i, pathTick) {
487
+ lastFiredIndex = i;
488
+ // Pin the camera to THIS waypoint's own pose before anyone observes it.
489
+ _applyPose(poseAt(sorted, sorted[i].atTick, loop, wrapByField));
490
+ var evt = { tick: sorted[i].atTick, index: i, pose: _copyPose(sorted[i].pose) };
491
+ var pending = [];
492
+ for (var L = 0; L < listeners.length; L++) {
493
+ try {
494
+ var r = listeners[L](evt);
495
+ if (r && typeof r.then === 'function') { pending.push(r); }
496
+ } catch (e) { _surface('onWaypoint callback threw', e); }
497
+ }
498
+ var cont = function () {
499
+ var nxt = lastFiredIndex + 1;
500
+ if (nxt < sorted.length && sorted[nxt].atTick <= pathTick) { _fireWaypoint(nxt, pathTick); }
501
+ else { draining = false; _applyAt(pathTick); } // back to the interpolated pose
502
+ };
503
+ if (pending.length) { _holdUntilSettled(pending, evt, cont); }
504
+ else { cont(); }
505
+ }
506
+
507
+ function _surface(what, e) {
508
+ if (typeof console !== 'undefined' && console.error) {
509
+ console.error('[CameraPath] ' + what + ':', e);
510
+ }
511
+ }
512
+
513
+ function onTick(tickData) {
514
+ var ut = _universeTick(tickData);
515
+ if (ut === null) { return; }
516
+ if (anchored === null) { anchored = (anchorTick !== null) ? anchorTick : ut; }
517
+ var pathTick = ut - anchored;
518
+ if (!draining) { _applyAt(pathTick); }
519
+ _drainDue(pathTick);
520
+ }
521
+
522
+ var handle = {
523
+ get running() { return !!unsubscribe; },
524
+ /** Bind to the per-tick fan-out and apply the pose for the CURRENT tick immediately. */
525
+ start: function () {
526
+ if (unsubscribe) { return; }
527
+ // Anchor FIRST, from the live universe tick, so path tick 0 is "now" even on a
528
+ // page that has already been running for a thousand ticks.
529
+ var td = runner._lastTickData;
530
+ var ut = _universeTick(td);
531
+ if (anchored === null) { anchored = (anchorTick !== null) ? anchorTick : (ut === null ? 0 : ut); }
532
+ unsubscribe = runner.addTickListener(onTick);
533
+ // A paused scene frames itself at once rather than at the next tick.
534
+ if (ut !== null) { onTick(td); }
535
+ },
536
+ /** The universe tick path-tick 0 maps to (null before start()). Record it to reproduce. */
537
+ get anchorTick() { return anchored; },
538
+ stop: function () {
539
+ if (unsubscribe) { unsubscribe(); unsubscribe = null; }
540
+ draining = false;
541
+ lastFiredIndex = -1;
542
+ anchored = null; // a restart re-anchors to the new now
543
+ // Never leave the clock held by a drone that is no longer flying — a stopped
544
+ // drone that froze the sim would be the worst kind of leak (silent and total).
545
+ while (heldCount > 0) { heldCount -= 1; if (typeof runner.resumeTicks === 'function') { runner.resumeTicks(); } }
546
+ },
547
+ dispose: function () { handle.stop(); listeners = []; },
548
+ /** The PURE path evaluation, bound to this drone's own waypoints. */
549
+ poseAt: function (tick) { return poseAt(sorted, tick, loop, wrapByField); },
550
+ /**
551
+ * onWaypoint(cb) — cb({tick, index, pose}) when a waypoint is reached.
552
+ * The natural move is `async () => rows.push(await vision.getStill())`.
553
+ * @returns {Function} unsubscribe
554
+ */
555
+ onWaypoint: function (cb) {
556
+ if (typeof cb !== 'function') { _fail('onWaypoint needs a function'); }
557
+ listeners.push(cb);
558
+ return function () {
559
+ var i = listeners.indexOf(cb);
560
+ if (i !== -1) { listeners.splice(i, 1); }
561
+ };
562
+ },
563
+ /** The validated, sorted path (a copy — the drone's own list is never handed out). */
564
+ get waypoints() { return sorted.map(function (w) {
565
+ return { atTick: w.atTick, pose: _copyPose(w.pose) }; }); }
566
+ };
567
+ return handle;
568
+ }
569
+
570
+ // ── Export ────────────────────────────────────────────────────────────────────
571
+
572
+ var CameraPath = { attach: attach, poseAt: poseAt };
573
+
574
+ export { CameraPath };
575
+
@@ -0,0 +1,34 @@
1
+ /* Copyright 2023-2025 by Essam Abadir */
2
+
3
+ /**
4
+ * CollisionActions — Canonical collision action enum for EGPT FRQTL VM.
5
+ * Values are SCREAMING_SNAKE (the JS engine's native vocabulary).
6
+ *
7
+ * Canonical set — exactly what the JS reference engine's lo.collide() action dispatch has.
8
+ * ACTIVE_ACTIONS = [ABSORB, COUNT, MARK, DETECT, LOG].
9
+ * DO NOT add actions not present in the JS reference collide() without first
10
+ * adding them there and updating the conformance suite (§11.3).
11
+ *
12
+ * Wire-format translation (SNAKE→Pascal for WASM) is handled by the
13
+ * wasm-engine provider. Objects emit raw SCREAMING_SNAKE; providers translate.
14
+ */
15
+
16
+
17
+ const CollisionActions = {
18
+ COUNT: 'COUNT',
19
+ MARK: 'MARK',
20
+ DETECT: 'DETECT',
21
+ LOG: 'LOG',
22
+ ABSORB: 'ABSORB',
23
+ BOUNCE: 'BOUNCE',
24
+ BLOCK: 'BLOCK',
25
+ // RECOLOR deleted: I1 — the engine has no concept of color. Render color lives in
26
+ // data.render.color (sugar stamps it; engine carries opaque; renderer reads it).
27
+ // The feature returns later via the behavior model (P3 write-data-bubble primitive).
28
+ // KILL/CONTAIN/CONTAIN_X/CONTAIN_Y/FORCE_RENDER were Rust-only divergences — deleted (§3.2).
29
+ // Add new actions to the JS reference collide() FIRST, then here + conformance.
30
+ };
31
+
32
+ // Export
33
+
34
+ export { CollisionActions };
@@ -0,0 +1,30 @@
1
+ /* Copyright 2023-2025 by Essam Abadir */
2
+
3
+ /**
4
+ * CollisionTypes — Canonical collision type enum for EGPT FRQTL VM.
5
+ * Values are SCREAMING_SNAKE (the JS engine's native vocabulary).
6
+ *
7
+ * Canonical set — exactly what the JS reference engine's lo.collide() type switch has.
8
+ * DO NOT add types not present in the JS reference collide() without first
9
+ * adding them there and updating the conformance suite (§11.3).
10
+ *
11
+ * Wire-format translation (SNAKE→Pascal for WASM) is handled by the
12
+ * wasm-engine provider. Objects emit raw SCREAMING_SNAKE; providers translate.
13
+ */
14
+
15
+
16
+ const CollisionTypes = {
17
+ NONE: 'NONE',
18
+ BOUNCE: 'BOUNCE',
19
+ RANDOM_X: 'RANDOM_X',
20
+ RANDOM_Y: 'RANDOM_Y',
21
+ RANDOM_XY: 'RANDOM_XY',
22
+ MOMENTUM_BASED: 'MOMENTUM_BASED',
23
+ WALL: 'WALL'
24
+ // BounceUp/Down/Left/Right were Rust-only divergences — deleted (§3.1).
25
+ // Add new types to the JS reference collide() FIRST, then here + conformance.
26
+ };
27
+
28
+ // Export
29
+
30
+ export { CollisionTypes };