@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,89 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * controlAction.js — OBJECTS-SIDE execution for an `action` control descriptor
5
+ * (presentation pass Item 2, E-P2a/E-P2b). The sibling of
6
+ * `controlBind.js`, same home, same FAIL-LOUD style.
7
+ *
8
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
9
+ *
10
+ * `resolveAction(action, ctx)` → `{ run() }`.
11
+ *
12
+ * Arrow rule (mirrors the ratified bind split EXACTLY): `lib/shim/controls`
13
+ * owns the NAME VOCABULARY (`ControlsSchema.ACTIONS` — the data shape);
14
+ * EXECUTION — mapping a name to a live `FrqtlApp` method and calling it
15
+ * — is OBJECTS-SIDE and lives here, beside the panel/overlay/sync sugar that
16
+ * consumes it. Because of this split NO experiment page re-implements pause /
17
+ * resume / restart plumbing: a page DECLARES `{kind:'action', action:'restart'}`
18
+ * and every host (the page's own button, the panel, the canvas overlay) runs the
19
+ * same one implementation.
20
+ *
21
+ * ctx: { runner } — the live FrqtlApp. Required.
22
+ *
23
+ * Every ACTIONS member maps to a method that exists on FrqtlApp:
24
+ *
25
+ * pause → runner.stop() resume → runner.start()
26
+ * toggle → runner.toggle() restart → runner.restart()
27
+ *
28
+ * FAIL-LOUD on an unknown action name, a missing runner, or a runner lacking the
29
+ * mapped method — never a silent no-op (a dead button is exactly the bug class
30
+ * this layer exists to prevent).
31
+ *
32
+ * ── Layer 2: physics behaviour ──────────────────────────────────────────────
33
+ *
34
+ * ZERO physics. These are RUN-LOOP verbs (start/stop the RAF loop, or replay the
35
+ * scene the runner retained — a setup factory or a sketch build, `retainSceneReplay`). None of them touches a frame, a property accessor or
36
+ * the engine RNG, so none can move a canonical-bench golden.
37
+ */
38
+
39
+ import * as _ControlsSchemaNode from '../../../shim/controls/index.js';
40
+
41
+ /**
42
+ * ACTION_METHODS — the ONE action-name → runner-method map. Keyed by the SAME
43
+ * names `ControlsSchema.ACTIONS` blesses; a conformance test iterates ACTIONS and
44
+ * asserts every member has a row here (drift is a test failure, never a runtime
45
+ * surprise).
46
+ */
47
+ var ACTION_METHODS = Object.freeze({
48
+ pause: 'stop',
49
+ resume: 'start',
50
+ toggle: 'toggle',
51
+ restart: 'restart'
52
+ });
53
+
54
+ /**
55
+ * resolveAction(action, ctx) → { run() }.
56
+ *
57
+ * @param {string} action — a member of ControlsSchema.ACTIONS.
58
+ * @param {{runner:object}} ctx
59
+ * @returns {{run: () => *}}
60
+ * @throws {Error} FAIL-LOUD — unknown action, missing ctx.runner, or a runner
61
+ * without the mapped method.
62
+ */
63
+ function resolveAction(action, ctx) {
64
+ var ACTIONS = _ControlsSchemaNode.ACTIONS;
65
+ if (ACTIONS.indexOf(action) === -1) {
66
+ throw new Error('[controlAction] unknown action "' + String(action) + '" — must be one of [' +
67
+ ACTIONS.join(', ') + '] (the closed vocabulary owned by lib/shim/controls). (FAIL-LOUD.)');
68
+ }
69
+ var method = ACTION_METHODS[action];
70
+ var runner = ctx && ctx.runner;
71
+ if (!runner) {
72
+ throw new Error('[controlAction] resolveAction("' + action + '") requires ctx.runner (the live ' +
73
+ 'FrqtlApp). (FAIL-LOUD — never a silently dead button.)');
74
+ }
75
+ if (typeof runner[method] !== 'function') {
76
+ throw new Error('[controlAction] resolveAction("' + action + '"): the runner has no "' + method +
77
+ '" method — the action vocabulary and the runner surface have drifted. (FAIL-LOUD.)');
78
+ }
79
+ return {
80
+ run: function () { return runner[method](); }
81
+ };
82
+ }
83
+
84
+ // ── Export ────────────────────────────────────────────────────────────────────
85
+
86
+ var ControlAction = { resolveAction: resolveAction, ACTION_METHODS: ACTION_METHODS };
87
+
88
+ export { ControlAction };
89
+
@@ -0,0 +1,278 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * controlBind.js — OBJECTS-SIDE execution for a scalar control descriptor's
5
+ * `bind: {object, prop}` (gym-controls-as-sugar, FULL CONVERGENCE build,
6
+ * Stage i item 2).
7
+ *
8
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
9
+ *
10
+ * `resolveBind(bind, ctx, channel)` → `{ set(value), get() }`.
11
+ *
12
+ * Arrow rule (§5 converged): `lib/shim/controls` owns the `bind` DATA SHAPE
13
+ * only (`{object, prop}`, both non-empty strings — `ControlsSchema.BIND_KEYS`).
14
+ * EXECUTION — resolving `bind.object` to a live handle, dispatching by
15
+ * `channel`/object-kind, and routing through the real property accessors — is
16
+ * OBJECTS-SIDE: it lives here, beside the panel/slider/sync sugar that
17
+ * consumes it, never inside the shim.
18
+ *
19
+ * `ctx` shape (all fields optional; the fields actually used depend on
20
+ * `channel` and the resolved object's `kind` — see the per-branch throws
21
+ * below for exactly what is required when):
22
+ * {
23
+ * registry: a makeControlRegistry() instance (lib/shim/controls/registry.js)
24
+ * — resolves `bind.object` → { instance, kind } via
25
+ * `getObject`/`getObjectKind`. Required for every
26
+ * non-render channel.
27
+ * universe: the live EGPTUniverse — `setFrameProperty`/`getFrameProperty`
28
+ * (Frame kind ONLY — see below). Required for the Frame channel.
29
+ * runner: the FrqtlApp. Required for the render channel
30
+ * (`runner.setRenderOption`/`getRenderOption` — the runner is the
31
+ * ONE render-option gate: it validates the key against the
32
+ * provider's declared capabilities, validates the value, and
33
+ * routes a `filter.<name>` toggle to its filter slots), for the LargeObject/backdrop
34
+ * channel (`runner.updateObjectProperty(wrapper, prop, value)` —
35
+ * the SAME uniform schema-driven accessor Studio's own Inspector
36
+ * calls, per root CLAUDE.md §Object architecture: "Writes go
37
+ * through the property accessors ... throw-or-apply, never
38
+ * silent-skip". S1 fix (design §2.3.1 / gap-census #3): this
39
+ * branch used to call `ctx.universe.updateObjectProperty(handle,
40
+ * kind, prop, value)` directly — the Universe-level WASM ABI,
41
+ * which schema-validates then SILENTLY NO-OPS every
42
+ * `engineVisible:false` prop (color/alpha/collision_type/
43
+ * actions/name) because the JS-wrapper mirror those props
44
+ * depend on lives in `FrqtlApp`, one layer above,
45
+ * which this bypassed. Routing through `ctx.runner` fixes it),
46
+ * AND for a `bind.object === 'universe'` bind
47
+ * (`runner.setUniverseProperty`/`getUniverseProperty` — S1 fix,
48
+ * design §2.3.2 / gap-census #10, the 4th resolveBind dispatch
49
+ * branch, see below).
50
+ * readSnapshot: optional `(handle, kind, prop) => *|undefined` — the
51
+ * LargeObject/backdrop READ path. There is no
52
+ * `getObjectProperty` accessor (`updateObjectProperty` is
53
+ * write-only); the only read authority for LO state is the
54
+ * LAST `TickData.objects[handle]` snapshot (§B2.2:
55
+ * `{handle, count, frames}` — `count` is the one scalar
56
+ * prop actually marshalled today). Callers that hold the
57
+ * last TickData (ControlSync, Stage ii) supply this;
58
+ * omitting it makes the LO `get()` a DEFINED SKIP
59
+ * (returns undefined) — never a fabricated read.
60
+ * }
61
+ *
62
+ * Registration precedent (`registry.js`'s own docstring/tests): a Frame-kind
63
+ * bind target registers the bare QUANTUM_ID (a number — what
64
+ * `setFrameProperty`/`getFrameProperty` key on, e.g. `lane.aQid` from
65
+ * `setupCollisionMatrix.js`); a LargeObject-kind bind target registers the
66
+ * live LO WRAPPER instance (has `.handle`, e.g. a `LargeObject`/`Tube`/
67
+ * `Tube` — `updateObjectProperty` and the TickData snapshot both key on
68
+ * `.handle`, not on the registry name).
69
+ *
70
+ * Two-stage validation (never a bind-local property list): Stage 1 is the
71
+ * shim's shape check (`bind.object`/`bind.prop` are non-empty strings) — DONE
72
+ * before this file ever runs. Stage 2 is the REAL accessor's OWN
73
+ * schema-driven prop validation (`setFrameProperty`/`updateObjectProperty`
74
+ * already consult `ObjectSchema.get(kind)` internally and throw on
75
+ * unknown/readonly props) — `resolveBind`'s `set()` gets this for free by
76
+ * calling the real accessor; it never re-implements a parallel prop list.
77
+ *
78
+ * Kind scope: `resolveBind` dispatches on FOUR things, in this order — render
79
+ * channel (no registry involved) → `bind.object === 'universe'` (reserved
80
+ * sentinel, no registry lookup — see the UNIVERSE branch below) → registry-
81
+ * resolved `kind === 'Frame'` → registry-resolved `kind === 'LargeObject' ||
82
+ * kind === 'backdrop'`. `resolveBind` FAILS LOUD for any OTHER registry-
83
+ * resolved kind (e.g. `Dimension`) rather than silently no-op through
84
+ * `updateObjectProperty`'s own WASM-ABI dispatch gap (`wasm-engine.js` — kinds
85
+ * outside `{LargeObject, backdrop}` pass schema validation then silently
86
+ * no-op there, a pre-existing, out-of-scope gap this file refuses to expose).
87
+ *
88
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
89
+ *
90
+ * This file adds ZERO physics — it is pure dispatch/routing glue between a
91
+ * declarative `{object, prop}` pair and the property-accessor substrate that
92
+ * already exists. A bind-driven slider produces the IDENTICAL simulation
93
+ * effect as calling the SAME accessor by hand (proven at the demo level —
94
+ * see the collision-matrix Rule-0 oracle for the concrete bind-only control).
95
+ *
96
+ */
97
+
98
+
99
+ /**
100
+ * resolveBind(bind, ctx, channel) → { set(value), get() }
101
+ *
102
+ * @param {{object:string, prop:string}} bind — already shape-validated by
103
+ * `lib/shim/controls` at descriptor-normalize time (Stage 1).
104
+ * @param {object} ctx — see file docstring.
105
+ * @param {'physics'|'render'} [channel='physics'] — the descriptor's own
106
+ * `channel` (defaults to the same DEFAULT_CHANNEL the shim applies).
107
+ * @returns {{set:(value:*)=>void, get:()=>*}}
108
+ * @throws {Error} FAIL-LOUD — malformed `bind`, an unregistered `bind.object`,
109
+ * a missing required `ctx` field for the resolved channel/kind, or a
110
+ * resolved object kind outside {Frame, LargeObject, backdrop}.
111
+ */
112
+ function resolveBind(bind, ctx, channel) {
113
+ if (!bind || typeof bind !== 'object' || typeof bind.prop !== 'string' || !bind.prop ||
114
+ typeof bind.object !== 'string' || !bind.object) {
115
+ throw new Error('[controlBind] resolveBind requires bind = {object, prop} (both non-empty ' +
116
+ 'strings) — got ' + JSON.stringify(bind) + '. (FAIL-LOUD — this should already have been ' +
117
+ 'caught by lib/shim/controls at normalize time; resolveBind re-checks defensively, never ' +
118
+ 'as the primary gate.)');
119
+ }
120
+ ctx = ctx || {};
121
+ var ch = channel || 'physics';
122
+
123
+ // ── RENDER CHANNEL ────────────────────────────────────────────────────────
124
+ // `bind.object` is carried for shape uniformity only (the shim's BIND_KEYS
125
+ // contract does not vary by channel); the render option/capability NAME is
126
+ // `bind.prop` (e.g. 'colorScheme', 'shape', 'godrays', 'filter.water'). The
127
+ // runner owns everything about the write — declared-key check, value check,
128
+ // and the `filter.<name>` routing — so this branch FERRIES it and adds none.
129
+ if (ch === 'render') {
130
+ return {
131
+ set: function (value) {
132
+ if (!ctx.runner) {
133
+ throw new Error('[controlBind] render-channel bind on prop "' + bind.prop +
134
+ '" requires ctx.runner (an FrqtlApp). (FAIL-LOUD.)');
135
+ }
136
+ ctx.runner.setRenderOption(bind.prop, value);
137
+ },
138
+ get: function () {
139
+ if (!ctx.runner) { return undefined; } // defined skip — no runner, no read authority
140
+ return ctx.runner.getRenderOption(bind.prop);
141
+ }
142
+ };
143
+ }
144
+
145
+ // ── UNIVERSE — a bare property on the live universe singleton (S1 fix, ─────
146
+ // design §2.3.2 / gap-census #10: Wave Interference's primary physics
147
+ // knob, wavelengthScale, is a UNIVERSE-level global, not a Frame/
148
+ // LargeObject prop — the 3-way {render, Frame, LargeObject/backdrop}
149
+ // dispatch below had no branch for it at all.
150
+ //
151
+ // `bind.object === 'universe'` is a RESERVED SENTINEL, checked BEFORE any
152
+ // registry lookup: the universe is a singleton (exactly one per experiment
153
+ // page), reached directly through ctx.runner — it is never named/registered
154
+ // via registry.registerObject (there is nothing to look up a "universe"
155
+ // object BY NAME for). Any OTHER string is resolved via the registry, same
156
+ // as every other kind.
157
+ if (bind.object === 'universe') {
158
+ return {
159
+ set: function (value) {
160
+ if (!ctx.runner || typeof ctx.runner.setUniverseProperty !== 'function') {
161
+ throw new Error('[controlBind] universe bind on "' + bind.prop +
162
+ '" requires ctx.runner (an FrqtlApp with setUniverseProperty). ' +
163
+ '(FAIL-LOUD.)');
164
+ }
165
+ // Validated accessor — throws on an unknown/readonly Universe field
166
+ // (ObjectSchema.get('Universe')) rather than a bare, unvalidated
167
+ // ctx.universe[bind.prop] = value (the bind-local-property-list
168
+ // anti-pattern this fix exists to close off).
169
+ ctx.runner.setUniverseProperty(bind.prop, value);
170
+ },
171
+ // Universe reads are LIVE — same discipline as the Frame branch (no
172
+ // tick/snapshot dependency, unlike the LargeObject/backdrop branch).
173
+ get: function () {
174
+ if (!ctx.runner || typeof ctx.runner.getUniverseProperty !== 'function') {
175
+ throw new Error('[controlBind] universe bind on "' + bind.prop +
176
+ '" requires ctx.runner (an FrqtlApp with getUniverseProperty). ' +
177
+ '(FAIL-LOUD.)');
178
+ }
179
+ return ctx.runner.getUniverseProperty(bind.prop);
180
+ }
181
+ };
182
+ }
183
+
184
+ // ── PHYSICS CHANNEL — resolve bind.object via the registry ──────────────
185
+ if (!ctx.registry || typeof ctx.registry.getObject !== 'function' ||
186
+ typeof ctx.registry.getObjectKind !== 'function') {
187
+ throw new Error('[controlBind] physics-channel bind on "' + bind.object + '.' + bind.prop +
188
+ '" requires ctx.registry (a makeControlRegistry() instance with getObject/getObjectKind). ' +
189
+ '(FAIL-LOUD.)');
190
+ }
191
+ var registered = ctx.registry.getObject(bind.object); // FAIL-LOUD on unknown name (registry's own guard)
192
+ var kind = ctx.registry.getObjectKind(bind.object);
193
+
194
+ if (kind === 'Frame') {
195
+ // Frame-kind registration precedent: the registered instance IS the
196
+ // quantum_id (a number) — what setFrameProperty/getFrameProperty key on.
197
+ var qid = registered;
198
+ return {
199
+ set: function (value) {
200
+ if (!ctx.universe || typeof ctx.universe.setFrameProperty !== 'function') {
201
+ throw new Error('[controlBind] Frame bind on "' + bind.object + '.' + bind.prop +
202
+ '" requires ctx.universe (setFrameProperty). (FAIL-LOUD.)');
203
+ }
204
+ ctx.universe.setFrameProperty(qid, bind.prop, value);
205
+ },
206
+ // Frame reads are LIVE — getFrameProperty has no tick dependency
207
+ // (unlike the LO snapshot path below).
208
+ get: function () {
209
+ if (!ctx.universe || typeof ctx.universe.getFrameProperty !== 'function') {
210
+ throw new Error('[controlBind] Frame bind on "' + bind.object + '.' + bind.prop +
211
+ '" requires ctx.universe (getFrameProperty). (FAIL-LOUD.)');
212
+ }
213
+ return ctx.universe.getFrameProperty(qid, bind.prop);
214
+ }
215
+ };
216
+ }
217
+
218
+ if (kind === 'LargeObject' || kind === 'backdrop') {
219
+ // LargeObject-kind registration precedent: the registered instance is
220
+ // the live wrapper (`.handle` is the engine-side handle
221
+ // TickData.objects[] keys on). `wrapper` is what `set()` below needs
222
+ // (FrqtlApp.updateObjectProperty takes the WRAPPER, not a bare
223
+ // handle — it reads `lo._schemaKind`/`lo.rect`/etc. to decide the
224
+ // engineVisible:false render-only mirror vs the engine-visible
225
+ // universe-delegation path). `handle` (extracted for `get()`'s
226
+ // TickData-snapshot lookup, which DOES key on the bare handle) still
227
+ // accepts a bare-number registration for back-compat with a caller
228
+ // that registered a handle directly instead of a wrapper.
229
+ var wrapper = registered;
230
+ var handle = (registered && typeof registered === 'object' && registered.handle !== undefined)
231
+ ? registered.handle
232
+ : registered;
233
+ return {
234
+ set: function (value) {
235
+ // S1 fix (design §2.3.1 / gap-census #3, HIGH RISK): route through
236
+ // ctx.runner.updateObjectProperty(wrapper, prop, value) — the SAME
237
+ // Runner-level accessor Studio's Inspector already calls
238
+ // (`AuthorView.jsx` commit()) — NEVER ctx.universe.updateObjectProperty
239
+ // directly. The Universe-level WASM ABI schema-validates then
240
+ // SILENTLY NO-OPS every engineVisible:false prop (color/alpha/
241
+ // collision_type/actions/name) because those props are mirrored
242
+ // onto the JS wrapper one layer above (FrqtlApp), which the
243
+ // old universe-direct call bypassed entirely — a bind-only control
244
+ // on those props "succeeded" (no throw) and did nothing.
245
+ if (!ctx.runner || typeof ctx.runner.updateObjectProperty !== 'function') {
246
+ throw new Error('[controlBind] LargeObject bind on "' + bind.object + '.' + bind.prop +
247
+ '" requires ctx.runner (an FrqtlApp with updateObjectProperty). (FAIL-LOUD.)');
248
+ }
249
+ // The SAME canonical accessor op-programs' compiled `program` field is
250
+ // itself installed through (actionCompiler.js) — schema-validated,
251
+ // throw-or-apply, never a parallel prop list. FrqtlApp's own
252
+ // schema lookup throws TypeError on an unknown/readonly prop for
253
+ // wrapper's kind — that throw propagates from here unmodified.
254
+ ctx.runner.updateObjectProperty(wrapper, bind.prop, value);
255
+ },
256
+ // No `getObjectProperty` accessor exists — the sole read authority is the
257
+ // LAST TickData.objects[handle] snapshot, supplied by the caller
258
+ // (ControlSync) via ctx.readSnapshot. Absent → a DEFINED SKIP (undefined),
259
+ // never a fabricated read (same discipline as the render channel).
260
+ get: function () {
261
+ if (typeof ctx.readSnapshot !== 'function') { return undefined; }
262
+ return ctx.readSnapshot(handle, kind, bind.prop);
263
+ }
264
+ };
265
+ }
266
+
267
+ throw new Error('[controlBind] bind.object "' + bind.object + '" resolved to kind "' + kind +
268
+ '" — bind execution only supports {Frame, LargeObject, backdrop} (updateObjectProperty\'s ' +
269
+ 'own WASM-ABI dispatch has no real branch for any other kind — it would validate then ' +
270
+ 'silently no-op, which this file refuses to do). (FAIL-LOUD.)');
271
+ }
272
+
273
+ // ── Export ────────────────────────────────────────────────────────────────────
274
+
275
+ var ControlBind = { resolveBind: resolveBind };
276
+
277
+ export { ControlBind };
278
+
@@ -0,0 +1,120 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * directedRedirectProgram.js — the "option1" host-trig redirect (DIRECTED TUNNEL
5
+ * BUILD, 2026-07-13).
6
+ *
7
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
8
+ *
9
+ * The report's measured verdict (§2, path (c)): the ONLY way to give an in-tunnel
10
+ * collision the emitter's own true circular-isotropic trig ability is to keep
11
+ * EJECT in-engine (as every existing liner already does via BLOCK) and have the
12
+ * HOST tick-loop read `TickData.objects[handle].frames` between ticks and write
13
+ * back `vx`/`vy` via `setFrameProperty` — measured zero (or bounded ≤1px) extra
14
+ * latency vs a native mechanism, because collision detection is itself
15
+ * post-move within Phase C, so both an in-engine `set` and a host `setFrameProperty`
16
+ * can only affect motion starting the tick AFTER detection regardless of mechanism.
17
+ *
18
+ * This is a HOST TICK-FN COMPONENT in the SAME sense as `emitterProgram.js` — an
19
+ * engine-agnostic function composed entirely from generic primitives
20
+ * (`universe.random()`, `universe.setFrameProperty()`), NOT a native op-program (the
21
+ * op-vocabulary's `FN_NAMES` has no `cos`/`sin` — an established FN_NAMES gap,
22
+ * report §2(b)). UNLIKE `emitterProgram.js` (which registers itself onto
23
+ * `universe.experimentTickFunctions` and runs during Phase A, BEFORE that tick's own
24
+ * collision/collect), this component must run AFTER `doTick()` returns — it needs
25
+ * THIS tick's `TickData.objects[handle].frames`, which does not exist until Phase D
26
+ * completes. `buildRedirectApplier` therefore returns a plain function the CALLER
27
+ * (an experiment page's own `onTick`, or a setup factory's return value the caller
28
+ * invokes) calls ONCE per tick with that tick's `tickData` — mirroring how
29
+ * `DetectorWall.prototype.getCollisionHistogram(tickData)` is called.
30
+ *
31
+ * @param {Object} universe - engine universe exposing random()/setFrameProperty().
32
+ * @param {Object} spec
33
+ * @param {Array<number>} spec.wallHandles - LargeObject handles whose collected
34
+ * frames (`tickData.objects[handle].frames`) get redirected each call. Each wall
35
+ * MUST have been constructed with `collectCollided:true` (Tube's own opt-in param)
36
+ * — otherwise `tickData.objects[handle].frames` is simply absent/empty and this
37
+ * applier is a silent (correct) no-op for that handle.
38
+ * @param {number} spec.angleRad - the directed redirect angle, RADIANS (the
39
+ * direction "the tunnel DIRECTS TOWARDS" — honored EXACTLY, never snapped; only
40
+ * the tunnel BODY snaps to an axis-aligned orientation — see DirectedTunnel.js).
41
+ * @param {number} [spec.spreadRad=0] - isotropic-spread sub-option (AZIMUTH,
42
+ * the XY-plane angle): each redirected frame's actual azimuth is
43
+ * `angleRad + (random()*2-1)*spreadRad` — a uniform draw in
44
+ * `[angleRad-spreadRad, angleRad+spreadRad]`. `0` (default) = fully directed,
45
+ * zero spread (every redirected frame gets the EXACT same angle) — byte-identical
46
+ * opt-out for a pure-directed beam.
47
+ * @param {number} [spec.polarSpreadRad=0] - 3D EXTENSION (§4
48
+ * option (c) — "extend the redirect to draw a direction on the unit SPHERE,
49
+ * azimuth + polar"): an independent isotropic-spread draw for the POLAR angle
50
+ * (elevation out of the XY plane), `phi = (random()*2-1)*polarSpreadRad`. `0`
51
+ * (default) = byte-identical to the pre-3D behavior — `phi` stays exactly `0`
52
+ * (`cos(0)=1, sin(0)=0` exactly in IEEE754), so `vx`/`vy` are UNCHANGED and NO
53
+ * `vz` `setFrameProperty` call happens at all (never even the RNG draw — a 2D
54
+ * tunnel's redirect never touches a 3rd axis). Non-zero (a caller with real
55
+ * z-containment armed) makes the redirected direction genuinely 3D — closes the
56
+ * "the laserBeam path only ever writes 2 axes" gap (DirectedTunnel's `option1`
57
+ * previously flattened every redirected quantum into the XY plane regardless of
58
+ * the tunnel's own z-containment).
59
+ * @param {number|null} [spec.speed=null] - redirected speed magnitude. `null`
60
+ * (default) preserves the frame's OWN incoming speed (`sqrt(vx^2+vy^2)`, falling
61
+ * back to 1 if the frame arrived with zero velocity) — a reflection-style
62
+ * redirect that changes DIRECTION only. A finite number forces every redirected
63
+ * frame to the SAME speed (a collimating/laser-style redirect).
64
+ * @returns {Function} `applyRedirect(tickData)` — call once per tick, AFTER
65
+ * `doTick()` returns that tick's `tickData`.
66
+ */
67
+ 'use strict';
68
+
69
+ function buildRedirectApplier(universe, spec) {
70
+ spec = spec || {};
71
+ var wallHandles = spec.wallHandles || [];
72
+ var angleRad = spec.angleRad;
73
+ if (typeof angleRad !== 'number' || !isFinite(angleRad)) {
74
+ throw new Error('[directedRedirectProgram] spec.angleRad must be a finite number (radians).');
75
+ }
76
+ var spreadRad = (spec.spreadRad !== undefined && spec.spreadRad !== null) ? spec.spreadRad : 0;
77
+ var polarSpreadRad = (spec.polarSpreadRad !== undefined && spec.polarSpreadRad !== null) ? spec.polarSpreadRad : 0;
78
+ var fixedSpeed = (spec.speed !== undefined) ? spec.speed : null;
79
+
80
+ return function applyRedirect(tickData) {
81
+ if (!tickData || !tickData.objects) { return; }
82
+ for (var w = 0; w < wallHandles.length; w++) {
83
+ var entry = tickData.objects[wallHandles[w]];
84
+ if (!entry || !Array.isArray(entry.frames)) { continue; }
85
+ var frames = entry.frames;
86
+ for (var i = 0; i < frames.length; i++) {
87
+ var fd = frames[i];
88
+ var theta = angleRad; // azimuth (XY-plane angle)
89
+ if (spreadRad) {
90
+ theta += (universe.random() * 2 - 1) * spreadRad; // draw — host trig, path (c)
91
+ }
92
+ var speed = (fixedSpeed !== null)
93
+ ? fixedSpeed
94
+ : (Math.sqrt(fd.vx * fd.vx + fd.vy * fd.vy) || 1);
95
+ // 3D EXTENSION: phi (polar, elevation out of the XY
96
+ // plane) draws ONLY when polarSpreadRad is non-zero — otherwise
97
+ // phi stays exactly 0, making this byte-identical to the pre-3D
98
+ // 2-axis formula (cos(0)=1 -> vx=cos(theta)*speed,
99
+ // sin(0)=0 -> vz=0, never written).
100
+ var phi = 0;
101
+ if (polarSpreadRad) {
102
+ phi = (universe.random() * 2 - 1) * polarSpreadRad; // unit-sphere draw — host trig, path (c)
103
+ }
104
+ var vx = Math.cos(theta) * Math.cos(phi) * speed;
105
+ var vy = Math.sin(theta) * Math.cos(phi) * speed;
106
+ var vz = Math.sin(phi) * speed;
107
+ // fd.id is ALREADY the quantum_id (Rust FrameData's own `id` field —
108
+ // NOT a raw FrameId handle; unlike a fresh emission from makeLeafFrame,
109
+ // a COLLECTED tickData frame needs no getFrameQuantumId() conversion).
110
+ universe.setFrameProperty(fd.id, 'vx', vx);
111
+ universe.setFrameProperty(fd.id, 'vy', vy);
112
+ if (polarSpreadRad) { universe.setFrameProperty(fd.id, 'vz', vz); }
113
+ }
114
+ }
115
+ };
116
+ }
117
+
118
+
119
+ export { buildRedirectApplier };
120
+