@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,1057 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * Tunnel — TWO parallel MIRROR Walls forming a corridor (ws-experiments-walkthrough,
5
+ * CEO ruling 2026-07-26: "we implement a tunnel using two mirrors"). The waveguide /
6
+ * light-pipe primitive: a beam travelling down the bore bounces off the two facing
7
+ * mirror walls (the guarded reflection law), staying collimated inside the channel.
8
+ *
9
+ * var t = new Tunnel(universe, { left: 96, top: 233, length: 64, bore: 31 });
10
+ * t.walls; // [ Wall{bounce}, Wall{bounce} ] — the two mirror surfaces
11
+ * t.innerRect; // [96, 233, 64, 31] — the clear bore (aperture) the beam passes
12
+ *
13
+ * ── PURE COMPOSITION — zero engine, zero new physics (canonical-bench SU1) ─────
14
+ * A Tunnel adds NO collision logic of its own. It CONSTRUCTS two `Wall`s with
15
+ * `collisionBehavior:'bounce'` (the guarded, zero-leak reflection the Collision-Lab
16
+ * mirrors already prove) and places them a `bore` apart. Every collision effect is
17
+ * REALIZED by the two walls' compiled op-programs — the SAME reflect law the bench
18
+ * goldens hold. A Tunnel cannot move a bench golden; it only names + places a pair
19
+ * of the mirror primitive. If building a Tunnel ever seems to need engine or
20
+ * provider code, STOP: it is two `Wall`s + a bore.
21
+ *
22
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
23
+ *
24
+ * `new Tunnel(universe, opts)` — builds + engine-registers TWO mirror `Wall`s.
25
+ * (It does NOT add itself to any runner render list; the caller — the
26
+ * ObjectFactory builder or a setup factory — iterates `tunnel.walls` /
27
+ * `tunnel.largeObjects`; no render-wiring call is needed (each inner LargeObject
28
+ * registered itself with the world when it was constructed), exactly as it
29
+ * would for a bare `Wall`, or `runner.setup()` auto-wires `tunnel.largeObjects`.)
30
+ * @param {Object} universe — an EGPTUniverse (JS or WASM compat shim).
31
+ * @param {Object} opts
32
+ * @param {number} opts.left, opts.top — the bore (aperture) top-left corner, px.
33
+ * @param {number} opts.length — the travel-axis extent of the corridor, px (x
34
+ * extent for a horizontal tunnel, y extent for a vertical one).
35
+ * @param {number} opts.bore — the cross-section gap between the two mirror inner
36
+ * faces, px (the clear aperture the beam passes through).
37
+ * @param {number} [opts.wallThickness=10] — thickness of each mirror wall, px.
38
+ * @param {'x'|'y'} [opts.axis='x'] — corridor travel axis. 'x' → a HORIZONTAL
39
+ * corridor (two wide/flat mirrors above+below the bore; a +x beam reflects its
40
+ * vy). 'y' → a VERTICAL corridor (two tall/thin mirrors left+right of the bore;
41
+ * a +y beam reflects its vx).
42
+ * @param {number} [opts.z], [opts.depth] — 3D passthrough. When `depth` is given
43
+ * each mirror Wall is built as a z-slab (6-element rect) — the is3D reflection
44
+ * comes FREE from the Wall's own depth field (a 3D corridor with real z-walls);
45
+ * omitted → a flat 2D-in-3D corridor, byte-identical to a 2D scene.
46
+ * @param {Array} [opts.color=[90,120,200]] — RGB render color for both mirrors.
47
+ * @param {number} [opts.wallOpacity] — render opacity 0-255 for both mirrors.
48
+ * @param {string} [opts.name='tunnel'] — display/debug identifier prefix
49
+ * (`<name>_A` = the first mirror, `<name>_B` = the second).
50
+ * @param {'mirror'|'directed'} [opts.mode='mirror'] — the corridor's COLLISION MODE.
51
+ * `'mirror'` (DEFAULT) is the pre-existing guarded-bounce waveguide, unchanged and
52
+ * byte-identical. `'directed'` is the DIRECTED TUNNEL (CEO 2026-07-28): a wall hit
53
+ * re-emits the quantum in a random FORWARD direction within a 180° fan about the
54
+ * corridor's own axis — the tunnel behaves as a semi-circular emitter rather than a
55
+ * mirror. See `opts.directedAngle` / `opts.redirectSpread` and the Layer-2 note.
56
+ * CONSTRUCTION-LEVEL, not live: the directed mode needs its walls registered with the
57
+ * engine's `collect_collided` flag, which is a REGISTRATION-time flag with no live
58
+ * toggle (`Wall.setWallBehavior` refuses exactly this switch). Change it by rebuilding.
59
+ * @param {number} [opts.directedAngle] — `'directed'` mode AIM, in DEGREES: the centre
60
+ * of the forward fan. Omitted ⇒ DERIVED from `axis` (0° = +x for an x-axis corridor,
61
+ * 90° = +y for a y-axis one) — i.e. "forward" is the corridor's own travel direction.
62
+ * Supply `180`/`270` to aim a corridor that is fed from the far end. The BODY stays
63
+ * axis-aligned (a Tunnel is two AABB walls); this angle drives the redirect only —
64
+ * the same v1 split `DirectedTunnel.orientation` documents.
65
+ * @param {number} [opts.redirectSpread=90] — `'directed'` mode fan HALF-WIDTH, DEGREES.
66
+ * The default `90` IS the CEO's ask: a full 180° forward fan (`directedAngle ± 90°`).
67
+ * `0` collapses to a perfectly collimated re-emission along `directedAngle`.
68
+ * @param {boolean} [opts.exitCollapse=false] — THE POINT-EXIT GATE (D3). Adds a THIRD
69
+ * LargeObject spanning the last 2 columns of the bore at the corridor's OUTPUT mouth,
70
+ * whose whole behavior is one position write: every crossing quantum is pinned to the
71
+ * bore's mid-line, so the mouth emits as a one-pixel POINT SOURCE. Orthogonal to
72
+ * `mode` (it composes with either), default OFF. Requires the fast path
73
+ * (`withInterQuantumCollisions:false`) — FAIL-LOUD otherwise, see the Layer-2 note.
74
+ * ZERO offset, no jitter: the SphereEmitter has no occupancy mechanism either (a burst
75
+ * with emission box 0 starts every quantum on the SAME pixel and co-location is inert
76
+ * on the fast path), so a literal one-pixel point source is the faithful replication.
77
+ * A transverse random jitter is specifically what an earlier self-interference seed did
78
+ * before it was replaced for BLURRING the bands — do not add one.
79
+ * @param {boolean} [opts.exitPhaseReset=false] — coherence option ON the exit gate: append
80
+ * `set frame.local_time = 0` to the gate's program, re-phase-locking the ensemble at the
81
+ * mouth. Default OFF — this is a deliberate coherence CHOICE, not a ratified behavior;
82
+ * it exists so both regimes are drivable and the boundary is visible. LIVE via
83
+ * `setExitPhaseReset()`. Requires `exitCollapse` (it is an op on that gate).
84
+ * It is PHASE ONLY — never the separable decoherence bundle, whose second op redraws
85
+ * charge. The corridor WALLS keep their no-reset rule regardless.
86
+ * @param {number} [opts.polarSpread=0] — `'directed'` mode polar (out-of-plane) fan
87
+ * half-width, DEGREES. `0` (DEFAULT, including on a 3D corridor) keeps the re-emission
88
+ * in the XY plane — the measured, angle-uniform case. A non-zero value draws the polar
89
+ * angle UNIFORMLY IN PHI, which is polar-uniform and NOT solid-angle-uniform on the
90
+ * hemisphere (it over-weights the pole). Opt in deliberately; do not read it as an
91
+ * isotropic 3D scatter.
92
+ *
93
+ * @property {Array<Wall>} walls — the two mirror `Wall` facades.
94
+ * @property {Array<LargeObject>} largeObjects — the two inner engine LOs (the key
95
+ * `FrqtlApp.setup()` auto-wires for rendering).
96
+ * @property {Array} innerRect — the clear bore rect [left, top, length, bore]
97
+ * (4-el), or [left, top, z, length, bore, depth] (6-el, 3D).
98
+ * @property {Wall} wallA, wallB — the two mirrors individually.
99
+ *
100
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
101
+ * ZERO physics of its own (SU1). The corridor's collimating action IS emergent
102
+ * from the two mirrors: a particle whose transverse walk would carry it into a
103
+ * mirror is reflected back into the bore (vy flipped for a horizontal tunnel, vx
104
+ * for a vertical one) by the compiled bounce op-program — no random scatter, no
105
+ * leak through the wall (the guarded reflect law), so the beam stays collimated
106
+ * down the channel. This is the physical difference the CEO's rebuild targets vs
107
+ * the legacy RANDOM_XY slit liners (which scatter randomly and can leak).
108
+ *
109
+ * `mode:'directed'` swaps that ONE emergent effect for the other one the CEO named:
110
+ * instead of reflecting the incidence axis, each wall hit RE-EMITS the quantum into a
111
+ * uniformly-drawn direction inside the forward 180°, so the corridor reads as a
112
+ * semi-circular (semi-spherical) emitter rather than a mirror. It is still ZERO new
113
+ * physics — it is the SAME two-piece composition `DirectedTunnel.option1` already
114
+ * commits, ported onto the mirror-pair Tunnel rather than re-invented beside it:
115
+ *
116
+ * 1. IN-TICK, per wall — an EJECT-ONLY compiled behavior (`slitLinerScatter`'s
117
+ * `'ejectY'`: a position write, and deliberately NO velocity ops) so the frame
118
+ * leaves the wall the same tick. `'ejectY'` is chosen for exactly the reason
119
+ * DirectedTunnel chose it — it has no velocity ops at all, so the eject owns
120
+ * POSITION and the redirect owns VELOCITY, with zero cross-coupling.
121
+ * 2. BETWEEN TICKS, host — `directedRedirectProgram.buildRedirectApplier` reads the
122
+ * walls' collected frames from `tickData.objects[handle].frames` and draws each
123
+ * exit azimuth uniformly in `directedAngle ± redirectSpread` with REAL trig,
124
+ * preserving the frame's own incoming speed (`speed:null` ⇒ a DIRECTION-only
125
+ * rewrite; speed is always 1px/tick anyway — Invariant 1 / bench M1 — so only
126
+ * the direction ratio `l2_walk_probs` normalizes is being set).
127
+ *
128
+ * WHY THE HOST HALF IS NOT AN OP-PROGRAM — and must not be "simplified" into one.
129
+ * The op vocabulary has no `cos`/`sin` (the FN_NAMES gap) and ops cannot loop, so no
130
+ * in-tick rejection sampling is expressible. The two in-tick shapes that ARE expressible
131
+ * are both measurably wrong for a 180° angle-uniform fan: the legacy RANDOM_XY box draw
132
+ * (`vx=random()`, `vy=random()*2-1`) is 1.83:1 DIAGONAL-WEIGHTED with peaks at ±45°, and
133
+ * the chord-lerp constrained cone is fully DEGENERATE at exactly 180° (its own docstring
134
+ * restricts it to <180°). Reaching for either is the bug, not the shortcut.
135
+ *
136
+ * Latency is not a reason to prefer an in-tick write either: collision detection is
137
+ * itself post-move within Phase C, so an in-engine `set` and a host `setFrameProperty`
138
+ * both affect motion starting the tick AFTER detection (measured in the DirectedTunnel
139
+ * round). NO decoherence is bundled into the scatter — the legacy RANDOM_XY liner reset
140
+ * `local_time=0`, which phase-locks the ensemble and produced a spurious coherent drift;
141
+ * decoherence stays a separate, off-by-default op set.
142
+ *
143
+ * `exitCollapse` (D3) turns the corridor's OUTPUT MOUTH into a point source. The banding in
144
+ * the self-interference exemplar comes from equal-radius quanta sharing a spin phase: births
145
+ * at `local_time=0` are phase-locked, fundamental leaves step P=1 so AGE = RADIUS, and the λ
146
+ * coin adds a transverse kick whose bias flips per spin quadrant — every ray snakes with
147
+ * period λ and the ensemble reads as concentric bands. Collapsing every exiting quantum onto
148
+ * one pixel supplies the "one origin" half of that mechanism.
149
+ *
150
+ * WHAT IT DOES NOT SUPPLY, and why `exitPhaseReset` exists. Quanta reaching the mouth have
151
+ * DIFFERENT transit histories, so their phases are spread — measured on this exact
152
+ * construction (240px corridor): within-same-crossing-tick `local_time` std ≈ 45 ticks. Whether
153
+ * that spread matters is λ-DEPENDENT, because the spin period IS λ_eff: one full transverse
154
+ * cycle takes λ_eff ticks (frame.rs `compute_spin` quadrants the leaf's `local_time % wl`).
155
+ * - λ_eff ≈ 256 (the self-interference golden): a 45-tick spread is ~18% of the 256-tick
156
+ * period — the exit is ~82% phase-aligned FOR FREE, and the banding should substantially
157
+ * survive with the toggle OFF.
158
+ * - λ_eff ≲ 128: the exit is measurably phase-FLAT (8-bin phase CV ≈ 0.09) — the fan reads
159
+ * smooth and un-banded with the toggle OFF, and banded with it ON.
160
+ * That CONTRAST is the experiment. Nothing here is tuned to make both regimes look alike.
161
+ *
162
+ * The gate is only meaningful on the fast path. With `withInterQuantumCollisions:true` the
163
+ * collapse pixel becomes a one-frame-per-tick SERIALIZER: the frustration pass reverts the
164
+ * losers to their pre-move positions (undoing the collapse write) and kills contended frames
165
+ * — measured, 1 of 5 dead within 20 ticks. The constructor refuses that configuration rather
166
+ * than producing a scene that measures contention instead of a point source.
167
+ *
168
+ * Residual differences from a FRESH emission, if the picture is subtly off: the angular
169
+ * distribution is transit-FILTERED (not the emitter's uniform half-disk); charge/mass/λ are
170
+ * preserved from birth so parity needs a matching feeder; and the launch geometry is a TRUE
171
+ * point, where the exemplar launches on a velocity-aligned shell (innerR 16) — so the
172
+ * exemplar has an empty centre and this deliberately will not.
173
+ */
174
+
175
+ import { Wall as _Wall } from './Wall.js';
176
+ import * as _SlitLinerScatter from '../objects/objects/slitLinerScatter.js';
177
+ import * as DirectedRedirectProgram from '../objects/objects/directedRedirectProgram.js';
178
+ import * as _AstSchema from '../../shim/expr/ast-schema.js';
179
+ import { buildTriggerExpr as _buildTriggerExpr, applyProgramTrigger as _applyProgramTrigger } from '../objects/objects/actionCompiler.js';
180
+ import { LargeObject as _LargeObject } from './LargeObject.js';
181
+ import * as _LoDrawnCenter from '../compat/renderer/lo-drawn-center.js';
182
+ import { registerKind } from '../objects/kinds.js';
183
+ import { setupTunnelPointExit } from '../objects/setup/setupTunnelPointExit.js';
184
+
185
+ // SlitLinerScatter + DirectedRedirectProgram are the TWO halves of the directed
186
+ // mode, both already committed for DirectedTunnel's `option1`. They are CONSUMED
187
+ // here, never re-derived — in particular `applyLinerScatter` carries the O4b
188
+ // program-trigger fix (a replacement program pushed without re-attaching its
189
+ // `trigger` silently no-ops), which a hand-rolled program push in this file would
190
+ // have to reproduce and would very likely get wrong.
191
+
192
+ var MODE_MIRROR = 'mirror';
193
+ var MODE_DIRECTED = 'directed';
194
+ var MODES = [MODE_MIRROR, MODE_DIRECTED];
195
+
196
+ // The exit gate's column WIDTH, in px. TWO, not one, and that is load-bearing: a
197
+ // diagonal mover's base-walk and transverse yo-yo can STACK to ±2 on one axis in a
198
+ // single tick (Invariant 1 — the sanctioned exception), so a 1px column is jumpable
199
+ // and the collapse would silently miss those frames. With 2px a frame may instead sit
200
+ // in the gate for two consecutive ticks and take the op twice — a uniform 1-tick
201
+ // re-pin at the same apex, identical for every frame, harmless to the ensemble.
202
+ var EXIT_GATE_WIDTH = 2;
203
+
204
+ // LoDrawnCenter.hasZ — the ONE z-presence test (lo-drawn-center.js).
205
+ function _hasZ(rect) {
206
+ return _LoDrawnCenter.hasZ(rect);
207
+ }
208
+
209
+ /**
210
+ * THE member geometry (one derivation, shared by the constructor and setRect): the two mirror
211
+ * plate rects, the clear bore, the exit-gate rect and the collapse target, all from the
212
+ * corridor's declared fields. 4-element rects for a flat corridor, z-capable 6-element ones
213
+ * when it has a depth.
214
+ * @private
215
+ */
216
+ function _geometry(left, top, length, bore, wallThickness, axis, z, depth) {
217
+ var g = {};
218
+ if (axis === 'x') {
219
+ g.rectA = [left, top - wallThickness, length, wallThickness]; // top mirror
220
+ g.rectB = [left, top + bore, length, wallThickness]; // bottom mirror
221
+ g.innerRect = [left, top, length, bore];
222
+ g.gateRect = [left + length - EXIT_GATE_WIDTH, top, EXIT_GATE_WIDTH, bore];
223
+ g.midCoord = top + bore / 2; // bore mid-line in Y
224
+ } else {
225
+ g.rectA = [left - wallThickness, top, wallThickness, length]; // left mirror
226
+ g.rectB = [left + bore, top, wallThickness, length]; // right mirror
227
+ g.innerRect = [left, top, bore, length];
228
+ g.gateRect = [left, top + length - EXIT_GATE_WIDTH, bore, EXIT_GATE_WIDTH];
229
+ g.midCoord = left + bore / 2; // bore mid-line in X
230
+ }
231
+ if (depth !== null) {
232
+ var zs = function (r) { return [r[0], r[1], z, r[2], r[3], depth]; };
233
+ g.rectA6 = zs(g.rectA); g.rectB6 = zs(g.rectB); g.gateRect6 = zs(g.gateRect);
234
+ g.innerRect = zs(g.innerRect);
235
+ }
236
+ return g;
237
+ }
238
+
239
+ function Tunnel(universe, opts) {
240
+ opts = opts || {};
241
+ this.universe = universe;
242
+ this._schemaKind = 'Tunnel';
243
+
244
+ var left = opts.left, top = opts.top;
245
+ var length = opts.length, bore = opts.bore;
246
+ if (!(length > 0) || !(bore > 0)) {
247
+ throw new Error('[Tunnel] opts.length and opts.bore must both be positive numbers.');
248
+ }
249
+ var wallThickness = (opts.wallThickness !== undefined && opts.wallThickness !== null) ? opts.wallThickness : 10;
250
+ var axis = (opts.axis === 'y') ? 'y' : 'x';
251
+ var hasDepth = (opts.depth !== undefined && opts.depth !== null);
252
+ var z = (opts.z !== undefined && opts.z !== null) ? opts.z : 0;
253
+ var depth = hasDepth ? opts.depth : null;
254
+ var color = opts.color || [90, 120, 200];
255
+ var name = opts.name || 'tunnel';
256
+
257
+ // ── Collision MODE (D2, CEO 2026-07-28) ──────────────────────────────
258
+ // 'mirror' (default) = the pre-existing guarded-bounce waveguide, untouched.
259
+ // 'directed' = eject-only walls + the host 180° forward re-emission.
260
+ // FAIL-LOUD on anything else (never a silent fallback to mirror).
261
+ var mode = (opts.mode !== undefined && opts.mode !== null) ? opts.mode : MODE_MIRROR;
262
+ if (MODES.indexOf(mode) === -1) {
263
+ throw new Error('[Tunnel] unknown mode "' + mode + '". Expected: ' + MODES.join(' | '));
264
+ }
265
+ var directed = (mode === MODE_DIRECTED);
266
+ // AIM: the fan's centre. Omitted ⇒ the corridor's OWN forward travel direction,
267
+ // derived from `axis` — never a hand-typed default per call site.
268
+ var directedAngleDeg = (typeof opts.directedAngle === 'number')
269
+ ? opts.directedAngle : (axis === 'y' ? 90 : 0);
270
+ // FAN HALF-WIDTH: 90° ⇒ the full 180° forward fan (the CEO's ask).
271
+ var redirectSpreadDeg = (typeof opts.redirectSpread === 'number') ? opts.redirectSpread : 90;
272
+ // POLAR half-width: 0 even on a 3D corridor — a non-zero value is polar-uniform,
273
+ // NOT solid-angle-uniform (see the module docstring). Opt in deliberately.
274
+ var polarSpreadDeg = (typeof opts.polarSpread === 'number') ? opts.polarSpread : 0;
275
+
276
+ // ── EXIT-GATE COLLAPSE (D3, CEO 2026-07-29) ──────────────────────────
277
+ // A third LargeObject at the corridor's OUTPUT mouth that pins every crossing
278
+ // quantum to the bore's mid-line, turning the mouth into a one-pixel POINT SOURCE.
279
+ // Orthogonal to `mode` (it composes with either), default OFF.
280
+ var exitCollapse = !!opts.exitCollapse;
281
+ // THE TWO INDEPENDENT EXIT FLAGS (D4). Both are ops on the gate `exitCollapse` creates,
282
+ // so both require it; neither implies the other.
283
+ // exitRecenter — the point-source collapse. DEFAULT ON (preserves D3 behaviour).
284
+ // exitPhaseReset — coherence. DEFAULT OFF (a choice, not a ratified behaviour).
285
+ // Making recentering optional is what SEPARATES "does the point source matter?" from
286
+ // "does coherence matter?" — while it was mandatory the two were confounded.
287
+ var exitRecenter = (opts.exitRecenter !== undefined) ? !!opts.exitRecenter : true;
288
+ var exitPhaseReset = !!opts.exitPhaseReset;
289
+ if ((exitPhaseReset || opts.exitRecenter !== undefined) && !exitCollapse) {
290
+ throw new Error('[Tunnel] exitPhaseReset / exitRecenter require exitCollapse — both are ' +
291
+ 'ops on the exit gate, and without the gate there is nothing to carry them. (FAIL-LOUD.)');
292
+ }
293
+
294
+ // ── REGIME PRECONDITION — fail loud, not a comment ───────────────────
295
+ // Both the directed scatter and the exit collapse are only valid on the fast path
296
+ // (`withInterQuantumCollisions:false`), which is what every one of these experiments
297
+ // already runs. With IQC ON the collapse point becomes a one-frame-per-tick
298
+ // SERIALIZER: the frustration pass REVERTS the losers to their pre-move positions —
299
+ // undoing the collapse write — and kills contended frames (measured: 1 of 5 dead
300
+ // within 20 ticks). The experiment would then be measuring collapse contention, not
301
+ // a point source. Refuse rather than silently produce that.
302
+ if ((directed || exitCollapse) && universe && universe.withInterQuantumCollisions === true) {
303
+ throw new Error('[Tunnel] mode:"directed" / exitCollapse require ' +
304
+ 'withInterQuantumCollisions:false (the fast path). With inter-quantum collisions ON ' +
305
+ 'the collapse point becomes a one-frame-per-tick serializer: the frustration pass ' +
306
+ 'reverts the collapse write and KILLS contended quanta, so the scene measures ' +
307
+ 'contention rather than the point source. (FAIL-LOUD.)');
308
+ }
309
+
310
+ // Stash the authored construction values (the codegen round-trip authority —
311
+ // the descriptor fields read these back verbatim, Box precedent).
312
+ this.left = left; this.top = top; this.length = length; this.bore = bore;
313
+ this.wallThickness = wallThickness; this.axis = axis; this.color = color;
314
+ this.name = name;
315
+ // `mode` is ALWAYS stashed (unlike the optional z/depth pair): it is the CEO-facing
316
+ // knob, so the Inspector must render its selector on every tunnel, which needs a
317
+ // defined value to read. The three directed-only tunables follow the z/depth
318
+ // OPT-IN precedent instead — left undefined in mirror mode so captureProps skips
319
+ // them and a mirror tunnel's codegen gains exactly ONE key, not four.
320
+ this.mode = mode;
321
+ if (directed) {
322
+ this.directedAngle = directedAngleDeg;
323
+ this.redirectSpread = redirectSpreadDeg;
324
+ this.polarSpread = polarSpreadDeg;
325
+ }
326
+ // Same OPT-IN stashing discipline as z/depth: undefined unless armed, so a tunnel
327
+ // without the gate codegens exactly as it did before D3.
328
+ if (exitCollapse) {
329
+ this.exitCollapse = true;
330
+ this.exitRecenter = exitRecenter;
331
+ this.exitPhaseReset = exitPhaseReset;
332
+ }
333
+ // z/depth are DECLARED descriptor fields (TunnelDescriptor) so a 3D corridor
334
+ // round-trips through codegen/Restart. A FLAT (2D) tunnel leaves them
335
+ // `undefined` — captureProps skips undefined, so a 2D codegen stays
336
+ // byte-identical to the pre-3D output (no `"z":0,"depth":null` noise).
337
+ if (hasDepth) { this.z = z; this.depth = depth; }
338
+
339
+ // ── Mirror wall rects (two parallel plates a `bore` apart) ───────────
340
+ // axis 'x': wide/flat mirrors ABOVE and BELOW the bore (reflect vy).
341
+ // axis 'y': tall/thin mirrors LEFT and RIGHT of the bore (reflect vx).
342
+ var geo = _geometry(left, top, length, bore, wallThickness, axis, z, depth);
343
+ var rectA = geo.rectA, rectB = geo.rectB;
344
+
345
+ // ── Build the two Walls — the ONE mirror atom, or the directed pair ──
346
+ // MIRROR: collisionBehavior 'bounce' (guarded reflect law, bounceDirection 'reflect').
347
+ // DIRECTED: 'nothing' (the wall itself neither reflects nor kills — it must contribute
348
+ // ZERO velocity ops so the host redirect is the only velocity write) + the
349
+ // registration-time collect flag the redirect reads its frames from. The eject-only
350
+ // program is pushed just below, once both walls exist.
351
+ var self = this;
352
+ function mkWall(rect, suffix) {
353
+ var wopts = {
354
+ rect: rect,
355
+ collisionBehavior: directed ? 'nothing' : 'bounce',
356
+ name: name + suffix,
357
+ color: color
358
+ };
359
+ // collect_collided has NO live toggle in the substrate, so a directed tunnel must
360
+ // register for it at construction — this is why `mode` is construction-level.
361
+ if (directed) { wopts.collectCollided = true; }
362
+ if (opts.wallOpacity !== undefined) { wopts.alpha = opts.wallOpacity; }
363
+ // 3D passthrough: a real depth makes each mirror a z-slab (is3D reflect FREE
364
+ // from Wall's own depth field). z lands the slab; omitting depth keeps a flat 2D wall.
365
+ if (depth !== null) { wopts.z = z; wopts.depth = depth; }
366
+ return new _Wall(self.universe, wopts);
367
+ }
368
+ this.wallA = mkWall(rectA, '_A');
369
+ this.wallB = mkWall(rectB, '_B');
370
+ this.walls = [this.wallA, this.wallB];
371
+ this.largeObjects = [this.wallA.lo, this.wallB.lo];
372
+ this.innerRect = geo.innerRect;
373
+
374
+ // ── EXIT GATE (D3): the point-source collapse at the corridor's output mouth ──
375
+ // A THIRD LargeObject spanning the last EXIT_GATE_WIDTH columns of the bore,
376
+ // bore-cross-section only, whose whole behavior is one POSITION write. This is
377
+ // in-tick op-program DATA on the eject precedent (`set 0.top`, conformance
378
+ // EP22-EP25) — NOT a host tick-fn: the host slot exists because angle-uniform
379
+ // VELOCITY redraw needs trig the op vocabulary lacks, and a position write with a
380
+ // baked literal needs none of that.
381
+ this.exitGate = null;
382
+ this._exitGateMid = null;
383
+ if (exitCollapse) {
384
+ var ast = _AstSchema;
385
+ var mkTrigger = _buildTriggerExpr;
386
+ var attachTrigger = _applyProgramTrigger;
387
+ // Gate rect + the collapse target, both derived from the corridor's own geometry
388
+ // — compile-time sugar data, the same "baked at construction" pattern as the
389
+ // directed mode's angleRad. The corridor's LONG axis picks which coordinate is
390
+ // pinned: an x corridor collapses Y to the bore mid-line, a y corridor collapses X.
391
+ var gateRect = geo.gateRect, midCoord = geo.midCoord;
392
+ var pinProp = (axis === 'x') ? 'top' : 'left', sizeProp = (axis === 'x') ? 'h' : 'w';
393
+ this._exitGateMid = midCoord;
394
+ this._exitPinProp = pinProp;
395
+ this._exitSizeProp = sizeProp;
396
+ var gateOpts = {
397
+ rect: gateRect,
398
+ // The gate is not a barrier: it neither reflects nor kills. Its ONLY effect is
399
+ // the position write pushed below. `visible:false` because drawing a bar across
400
+ // the mouth would read as an obstruction it is not.
401
+ collisionBehavior: 'nothing',
402
+ collectCollided: true,
403
+ visible: false,
404
+ name: name + '_exitgate',
405
+ color: color
406
+ };
407
+ if (depth !== null) { gateOpts.z = z; gateOpts.depth = depth; }
408
+ var GateWallCls = _Wall;
409
+ this.exitGate = new GateWallCls(this.universe, gateOpts);
410
+
411
+ // Compile + push the gate's program from the TWO independent flags. ONE authority
412
+ // (`_applyExitGateProgram`), shared by construction and by both live setters, so
413
+ // the four flag combinations can never diverge between the two paths.
414
+ this.exitRecenter = exitRecenter;
415
+ this.exitPhaseReset = exitPhaseReset;
416
+ this._applyExitGateProgram();
417
+ this.walls.push(this.exitGate);
418
+ this.largeObjects.push(this.exitGate.lo);
419
+ }
420
+
421
+ // ── DIRECTED mode: the two-piece composition (ported from DirectedTunnel.option1) ──
422
+ this.applyRedirect = null;
423
+ this._unsubRedirect = null;
424
+ if (directed) {
425
+ var SlitLinerScatter = _SlitLinerScatter;
426
+ var buildRedirectApplier = DirectedRedirectProgram.buildRedirectApplier;
427
+ // (1) IN-TICK: the eject-only program, per wall. The eject NORMAL points INTO the
428
+ // bore — wallA sits on the low side of the corridor's cross-axis and ejects toward
429
+ // the high side, wallB the reverse. Derived from `axis`, never hand-typed:
430
+ // axis 'x' → mirrors above/below → wallA ejects 'bottom', wallB ejects 'top'
431
+ // axis 'y' → mirrors left/right → wallA ejects 'right', wallB ejects 'left'
432
+ // ('ejectY' is the behavior KEY — historical, DSE-liner-named; the DIRECTION is
433
+ // this argument. It is the one behavior with NO velocity ops, which is precisely
434
+ // why it is the correct partner for a host-owned velocity write.)
435
+ var intoBoreA = (axis === 'y') ? 'right' : 'bottom';
436
+ var intoBoreB = (axis === 'y') ? 'left' : 'top';
437
+ SlitLinerScatter.applyLinerScatter(this.wallA.lo, universe, intoBoreA,
438
+ { behavior: 'ejectY', resetLocalTime: false });
439
+ SlitLinerScatter.applyLinerScatter(this.wallB.lo, universe, intoBoreB,
440
+ { behavior: 'ejectY', resetLocalTime: false });
441
+ // resetLocalTime stays FALSE: bundling the `local_time=0` phase reset into a
442
+ // scatter phase-locks the ensemble (the measured spurious-coherent-drift ruling).
443
+ // Decoherence is a separable op set and is deliberately not armed here.
444
+
445
+ // (2) BETWEEN TICKS: ONE host applier over BOTH walls. speed:null ⇒ direction-only.
446
+ this.applyRedirect = buildRedirectApplier(universe, {
447
+ wallHandles: [this.wallA.lo.handle, this.wallB.lo.handle],
448
+ angleRad: directedAngleDeg * Math.PI / 180,
449
+ spreadRad: redirectSpreadDeg * Math.PI / 180,
450
+ speed: null,
451
+ polarSpreadRad: polarSpreadDeg * Math.PI / 180
452
+ });
453
+ }
454
+
455
+ // ── THE ONE UP-LINK: the Tunnel composed its plates and its gate (each a Wall facade),
456
+ // so it stamps THEM — never their LOs (each Wall stamps its own LO). A drawn plate LO
457
+ // therefore resolves LO → Wall → Tunnel. Zero behavior change.
458
+ for (var i = 0; i < this.walls.length; i++) {
459
+ this.walls[i]._owner = this;
460
+ }
461
+ }
462
+
463
+ /**
464
+ * `tunnel.rect` — the corridor's ENVELOPE: the axis-aligned box enclosing every member it
465
+ * composed (both plates and the exit gate, which lies inside the bore), in the LO rect
466
+ * shape `{left, top, w, h}` plus `{z, d}` when the corridor has depth. A pure getter over
467
+ * the members' own rects (each Wall's rect is its one position authority), so it can never
468
+ * disagree with what is drawn. The 2D overlay box, the aim surround and the carry read it.
469
+ */
470
+ Object.defineProperty(Tunnel.prototype, 'rect', {
471
+ get: function () {
472
+ var l = Infinity, t = Infinity, r = -Infinity, b = -Infinity;
473
+ var z0 = Infinity, z1 = -Infinity, hasZ = false;
474
+ for (var i = 0; i < this.walls.length; i++) {
475
+ var m = this.walls[i] && this.walls[i].rect;
476
+ if (!m) { continue; }
477
+ l = Math.min(l, m.left); t = Math.min(t, m.top);
478
+ r = Math.max(r, m.left + m.w); b = Math.max(b, m.top + m.h);
479
+ if (_hasZ(m)) {
480
+ hasZ = true;
481
+ var md = (typeof m.d === 'number') ? m.d : m.w;
482
+ z0 = Math.min(z0, m.z); z1 = Math.max(z1, m.z + md);
483
+ }
484
+ }
485
+ if (!(l < r) || !(t < b)) { return undefined; }
486
+ var env = { left: l, top: t, w: r - l, h: b - t };
487
+ if (hasZ) { env.z = z0; env.d = z1 - z0; }
488
+ return env;
489
+ }
490
+ });
491
+
492
+ /**
493
+ * THE ONE exit-gate program authority (D4). Compiles the gate's op-program from the two
494
+ * INDEPENDENT flags and pushes it on the same live path `applyLinerScatter` /
495
+ * `recompileActions` use. Construction and both live setters call THIS — so the four flag
496
+ * combinations can never diverge between the build path and the toggle path.
497
+ *
498
+ * recenter ON → `set frame.top = boreMid - frame.h*0.5` (the point-source collapse)
499
+ * phase ON → `set frame.local_time = 0` (re-phase-lock the ensemble)
500
+ *
501
+ * BOTH OFF ⇒ **no program at all** (`program = null`, nothing pushed to the engine), NOT an
502
+ * empty-ops program. That distinction is deliberate: an empty-ops program is
503
+ * indistinguishable from a broken/mis-compiled gate when someone debugs this later,
504
+ * whereas a null program reads unambiguously as "gate present, disarmed". The gate LO
505
+ * itself STAYS REGISTERED as an inert collector, because `collect_collided` is a
506
+ * registration-time engine flag with no live toggle — dropping the LO would make the two
507
+ * flags un-flippable at runtime, which is exactly the manipulation this lab exists for.
508
+ *
509
+ * FUTURE-PROOFING NOTE (not built): the recenter op pins to a single mid-line. Generalising
510
+ * it to an APERTURE (a band of width A centred on the mid-line, A=1 ⇒ today's point source,
511
+ * A=bore ⇒ no collapse) is a change to THIS function alone — which is why the collapse
512
+ * target is derived here rather than baked at three call sites.
513
+ *
514
+ * @private
515
+ */
516
+ Tunnel.prototype._applyExitGateProgram = function () {
517
+ if (!this.exitGate) { return null; }
518
+ var ast = _AstSchema;
519
+ var HF = ast.HANDLE_FRAME;
520
+ var ops = [];
521
+ if (this.exitRecenter) {
522
+ ops.push({
523
+ set: HF + '.' + this._exitPinProp,
524
+ to: ast.Bin('-', ast.Lit(this._exitGateMid),
525
+ ast.Bin('*', ast.Ref(HF, this._exitSizeProp), ast.Lit(0.5)))
526
+ });
527
+ }
528
+ if (this.exitPhaseReset) {
529
+ // PHASE ONLY — the single op. NEVER `_buildDecoherenceOps`: that bundle is TWO ops
530
+ // and its second redraws CHARGE with an RNG draw, which is a rider nobody asked for
531
+ // AND shifts every downstream seeded stream. The corridor WALLS keep
532
+ // resetLocalTime:false regardless (standing ruling) — this reset lives at the exit
533
+ // gate and nowhere else.
534
+ ops.push({ set: HF + '.local_time', to: ast.Lit(0) });
535
+ }
536
+
537
+ var program = null;
538
+ if (ops.length) {
539
+ program = { on: 'collision', ops: ops };
540
+ // O4b: a program pushed WITHOUT re-attaching its trigger silently no-ops. Build +
541
+ // attach through the SAME authority compileActions itself uses.
542
+ _applyProgramTrigger(program, _buildTriggerExpr(
543
+ this.exitGate.lo.reposition || null, false,
544
+ ast.HANDLE_FRAME, ast.HANDLE_SELF,
545
+ ast.Lit, ast.Ref, ast.Cmp, ast.Cond, ast.Fn
546
+ ));
547
+ }
548
+ this.exitGate.lo.program = program;
549
+ if (this.universe && typeof this.universe.updateObjectProperty === 'function') {
550
+ this.universe.updateObjectProperty(this.exitGate.lo.handle, 'LargeObject', 'program', program);
551
+ }
552
+ return program;
553
+ };
554
+
555
+ /**
556
+ * THE WRITE ROUTER (D5) — the Inspector / any `updateObjectProperty` caller lands here for
557
+ * a Tunnel's SUGAR props, exactly as `SphereEmitter.prototype.applyProp` does for the
558
+ * emitter's. Without it a live edit fell through to the generic mirror-write, which set a
559
+ * DEAD property on this facade and never called the setter: the flag flipped in the
560
+ * Inspector and the gate program never changed — a silent no-op, which is precisely what
561
+ * the accessor's throw-or-apply contract forbids.
562
+ *
563
+ * `LIVE_PROPS` is the ONE routing table (owned by the object, never a second list inside
564
+ * FrqtlApp — the coupling that has bitten the Wall v2 props before). A new live
565
+ * facade prop is one entry here; `tunnel-point-exit.test.mjs` asserts every `live:true`
566
+ * descriptor field is either routed here or is a render prop that fans out to the walls,
567
+ * so forgetting the entry is a CI failure rather than a dead knob.
568
+ *
569
+ * @param {string} prop
570
+ * @param {*} value
571
+ * @returns {boolean} true when THIS router handled the write (the caller must then stop);
572
+ * false to fall through to the generic render-only / group fan-out path (e.g. `color`).
573
+ */
574
+ Tunnel.prototype.applyProp = function (prop, value) {
575
+ var setter = Tunnel.LIVE_PROPS[prop];
576
+ if (!setter) { return false; }
577
+ if (typeof this[setter] !== 'function') {
578
+ throw new Error('[Tunnel] live prop "' + prop + '" is routed to ' + setter +
579
+ '() but that setter does not exist. (FAIL-LOUD.)');
580
+ }
581
+ this[setter](value);
582
+ return true;
583
+ };
584
+
585
+ /** prop → setter. The ONE routing table for a Tunnel's live sugar props. */
586
+ Tunnel.LIVE_PROPS = {
587
+ exitRecenter: 'setExitRecenter',
588
+ exitPhaseReset: 'setExitPhaseReset',
589
+ // Position: the envelope (the 2D drag and the SPACE carry) and the declared bore origin
590
+ // (an Inspector edit) — every one ends in setRect, the Tunnel's ONE position authority.
591
+ rect: 'setRect',
592
+ left: 'setLeft',
593
+ top: 'setTop'
594
+ };
595
+
596
+ /**
597
+ * setRect(rect) — THE Tunnel position/extent write (its envelope, `tunnel.rect`, is what it
598
+ * places). `rect` is the write wire `[l,t,w,h]` or `[l,t,z,w,h,d]` of the ENVELOPE (both plates
599
+ * and the gate); a 6-element write on a flat corridor THROWS (its 3D-ness is fixed at
600
+ * construction, like a Wall's), a 4-element write on a deep corridor keeps z/depth.
601
+ *
602
+ * SAME EXTENT = a translation: the declared bore origin (left/top, and z) is re-derived from
603
+ * the envelope, every member rect is re-derived through the SAME geometry the constructor used
604
+ * and written through Wall.setRect (same size, so no member re-bakes — a directed corridor's
605
+ * liner-eject programs survive untouched), then the exit-gate collapse target is re-derived and
606
+ * the gate program re-pushed through its ONE authority (_applyExitGateProgram) — the mouth
607
+ * collapses to the NEW mid-line.
608
+ *
609
+ * CHANGED EXTENT = a resize: length/bore (and depth) re-derive from the envelope (the wall
610
+ * thickness is kept). A member whose program this Tunnel pushed (a directed corridor wall, the
611
+ * exit gate) cannot be re-baked by its Wall, so the resize is REFUSED — atomically: every
612
+ * member is checked before the first one moves. A plain mirror corridor's plates own their
613
+ * programs and re-bake.
614
+ *
615
+ * Zero physics of its own: placement only, through each member's own setter.
616
+ * @param {number[]} rect @returns {Tunnel} this
617
+ */
618
+ Tunnel.prototype.setRect = function (rect) {
619
+ if (!Array.isArray(rect) || (rect.length !== 4 && rect.length !== 6) ||
620
+ !rect.every(function (v) { return typeof v === 'number' && isFinite(v); })) {
621
+ throw new TypeError('[Tunnel.setRect] rect must be the envelope [l,t,w,h] or [l,t,z,w,h,d] of finite ' +
622
+ 'numbers; got ' + JSON.stringify(rect) + '. (FAIL-LOUD.)');
623
+ }
624
+ var env = this.rect;
625
+ var deep = _hasZ(env);
626
+ if (rect.length === 6 && !deep) {
627
+ throw new Error('[Tunnel.setRect] "' + this.name + '" is a flat corridor (built without a depth); a ' +
628
+ '6-element rect would give it one, but its 3D-ness is compiled into its walls at construction. ' +
629
+ '(FAIL-LOUD.)');
630
+ }
631
+ var L = rect[0], T = rect[1];
632
+ var W = (rect.length === 6) ? rect[3] : rect[2], H = (rect.length === 6) ? rect[4] : rect[3];
633
+ var Z = (rect.length === 6) ? rect[2] : (deep ? env.z : null);
634
+ var D = (rect.length === 6) ? rect[5] : (deep ? env.d : null);
635
+ var wt = this.wallThickness, axis = this.axis;
636
+ var resized = (W !== env.w || H !== env.h || (deep && D !== env.d));
637
+ var length = this.length, bore = this.bore;
638
+ if (resized) {
639
+ length = (axis === 'x') ? W : H;
640
+ bore = ((axis === 'x') ? H : W) - 2 * wt;
641
+ if (!(length > 0) || !(bore > 0)) {
642
+ throw new Error('[Tunnel.setRect] the envelope ' + JSON.stringify(rect) + ' leaves no bore inside ' +
643
+ 'two ' + wt + 'px walls. (FAIL-LOUD.)');
644
+ }
645
+ // Atomic: a member whose program this Tunnel pushed cannot be re-baked by its Wall.
646
+ for (var i = 0; i < this.walls.length; i++) { this.walls[i].assertOwnsProgram(); }
647
+ }
648
+ var left = (axis === 'x') ? L : L + wt;
649
+ var top = (axis === 'x') ? T + wt : T;
650
+ var g = _geometry(left, top, length, bore, wt, axis, deep ? Z : 0, deep ? D : null);
651
+ this.wallA.setRect(deep ? g.rectA6 : g.rectA);
652
+ this.wallB.setRect(deep ? g.rectB6 : g.rectB);
653
+ this.left = left; this.top = top; this.length = length; this.bore = bore;
654
+ if (deep) { this.z = Z; this.depth = D; }
655
+ this.innerRect = g.innerRect;
656
+ if (this.exitGate) {
657
+ this.exitGate.setRect(deep ? g.gateRect6 : g.gateRect);
658
+ this._exitGateMid = g.midCoord;
659
+ this._applyExitGateProgram();
660
+ }
661
+ return this;
662
+ };
663
+
664
+ /** setLeft(x) — move the declared bore origin's x (a translation through setRect). */
665
+ Tunnel.prototype.setLeft = function (x) {
666
+ var env = this.rect;
667
+ return this.setRect([env.left + (x - this.left), env.top, env.w, env.h]);
668
+ };
669
+ /** setTop(y) — move the declared bore origin's y (a translation through setRect). */
670
+ Tunnel.prototype.setTop = function (y) {
671
+ var env = this.rect;
672
+ return this.setRect([env.left, env.top + (y - this.top), env.w, env.h]);
673
+ };
674
+
675
+ /** True when the exit gate currently carries at least one op (i.e. it does something). */
676
+ Tunnel.prototype.isExitGateArmed = function () {
677
+ return !!(this.exitGate && this.exitGate.lo.program &&
678
+ this.exitGate.lo.program.ops && this.exitGate.lo.program.ops.length > 0);
679
+ };
680
+
681
+ /**
682
+ * LIVE toggle for the exit gate's RECENTERING (D4) — the point-source collapse. Default ON;
683
+ * turning it OFF leaves the corridor's exit spatially unchanged, which is what separates
684
+ * "does the point source matter?" from "does coherence matter?" (the two were confounded
685
+ * while recentering was mandatory).
686
+ * @param {boolean} on
687
+ * @returns {boolean} the resulting state (false if this tunnel has no exit gate).
688
+ */
689
+ Tunnel.prototype.setExitRecenter = function (on) {
690
+ if (!this.exitGate) { return false; }
691
+ var want = !!on;
692
+ if (this.exitRecenter === want) { return want; }
693
+ this.exitRecenter = want;
694
+ this._applyExitGateProgram();
695
+ return want;
696
+ };
697
+
698
+ /**
699
+ * LIVE toggle for the exit gate's phase reset (D3 L2) — re-phase-locks the ensemble at the
700
+ * mouth so every quantum leaving shares a spin phase and AGE = RADIUS again. Flipping it
701
+ * mid-run makes the banding appear/disappear, which IS the experiment. No rebuild.
702
+ * @param {boolean} on
703
+ * @returns {boolean} the resulting state (false if this tunnel has no exit gate).
704
+ */
705
+ Tunnel.prototype.setExitPhaseReset = function (on) {
706
+ if (!this.exitGate) { return false; }
707
+ var want = !!on;
708
+ if (this.exitPhaseReset === want) { return want; }
709
+ this.exitPhaseReset = want;
710
+ this._applyExitGateProgram();
711
+ return want;
712
+ };
713
+
714
+ /**
715
+ * The ONE per-tick call a consumer makes, unconditionally, for every tunnel — it
716
+ * SELF-GATES on the mode, so a caller never needs its own `mode === 'directed'`
717
+ * check (forgetting that gate is the exact C1 bug DirectedTunnel's
718
+ * `applyRedirectIfActive` exists to make structurally impossible). A mirror tunnel
719
+ * is a no-op here.
720
+ *
721
+ * The host half CANNOT be a `universe.experimentTickFunctions` push: those run in
722
+ * Phase A, BEFORE this tick's collision + collect, and the redirect needs THIS tick's
723
+ * Phase-D `tickData.objects[handle].frames`. `attachToRunner` below wires it to the
724
+ * runner's per-tick fan-out (the peer of the page's single `onTick` slot); a caller
725
+ * without a runner calls this directly, the `DetectorWall.getCollisionHistogram(tickData)`
726
+ * idiom.
727
+ * @param {Object} tickData - this tick's TickData (from `universe.doTick()`).
728
+ */
729
+ Tunnel.prototype.applyRedirectIfActive = function (tickData) {
730
+ if (this.mode === MODE_DIRECTED && this.applyRedirect) { this.applyRedirect(tickData); }
731
+ };
732
+
733
+ /**
734
+ * Subscribe this tunnel's redirect to a runner's per-tick fan-out
735
+ * (`FrqtlApp.addTickListener` — the multi-subscriber peer of the page's single
736
+ * `onTick` slot, so it never steals it). Called by the ObjectFactory builder, which is
737
+ * what makes the directed mode survive codegen → Restart: the mode is a DECLARED
738
+ * descriptor field on the facade, so the rebuilt object re-wires itself. A raw tick-fn
739
+ * hand-written in a recipe would NOT survive (Restart evals the codegen'd module).
740
+ *
741
+ * Idempotent, and a no-op for a mirror tunnel or a runner without the fan-out.
742
+ * @param {Object} runner - an FrqtlApp (needs `addTickListener`).
743
+ * @returns {Function|null} the unsubscribe fn (also stored for `dispose`).
744
+ */
745
+ Tunnel.prototype.attachToRunner = function (runner) {
746
+ if (this.mode !== MODE_DIRECTED || this._unsubRedirect) { return this._unsubRedirect; }
747
+ if (!runner || typeof runner.addTickListener !== 'function') { return null; }
748
+ var self = this;
749
+ this._unsubRedirect = runner.addTickListener(function (tickData) {
750
+ self.applyRedirectIfActive(tickData);
751
+ });
752
+ return this._unsubRedirect;
753
+ };
754
+
755
+ /**
756
+ * Full teardown: unsubscribe the directed-mode tick listener (so a removed tunnel
757
+ * stops writing velocities), then dispose both mirror Walls (drops their LOs).
758
+ * Delegating through Wall.dispose keeps the ONE teardown authority. Idempotent.
759
+ * FrqtlApp.removeObject / setup-teardown call this via the facade.
760
+ */
761
+ Tunnel.prototype.dispose = function () {
762
+ if (this._disposed) { return; }
763
+ this._disposed = true;
764
+ if (this._unsubRedirect) { this._unsubRedirect(); this._unsubRedirect = null; }
765
+ for (var i = 0; i < this.walls.length; i++) {
766
+ if (this.walls[i] && typeof this.walls[i].dispose === 'function') { this.walls[i].dispose(); }
767
+ }
768
+ };
769
+
770
+ // The canonical, complete list of legal `opts.mode` values — ONE authority for any
771
+ // consumer (the schema enum, a control-panel select) that needs to enumerate them.
772
+ Tunnel.MODES = MODES;
773
+
774
+ /**
775
+ * Tunnel.pointExitSpec(spec) → `{ geom, emitter, tunnel }` — the point-exit (laser build-up) scene's
776
+ * emitter and corridor configs, for a sketch that declares the parts itself:
777
+ *
778
+ * var spec = Tunnel.pointExitSpec({ canvasW: W, canvasH: H, tunnelLength: 240, … });
779
+ * exp.addObject('SphereEmitter', spec.emitter); exp.addObject('Tunnel', spec.tunnel);
780
+ *
781
+ * PURE DELEGATION to the ONE function, setupTunnelPointExit.sceneSpec — byte-identical to calling
782
+ * it (tunnel-point-exit.test asserts the identity). The setup factory is machinery, not a part.
783
+ */
784
+ Tunnel.pointExitSpec = function pointExitSpec(spec) {
785
+ return setupTunnelPointExit.sceneSpec(spec);
786
+ };
787
+
788
+ // ── The kind (objects/kinds.js): descriptor, construction defaults, palette, builder ──────────
789
+
790
+ /**
791
+ * Tunnel — TWO parallel MIRROR walls forming a corridor (a waveguide / light-pipe).
792
+ * Constructor: Tunnel(universe, { left, top, length, bore, wallThickness, axis, color })
793
+ * Group wrapper: the two inner Wall{bounce} sub-objects are
794
+ * stamped with the ONE up-link `_owner` = <this>. Zero physics of its own (SU1) — it only
795
+ * places two of the guarded mirror primitive a `bore` apart.
796
+ */
797
+ var TunnelDescriptor = {
798
+ description: 'A corridor built from TWO parallel MIRROR walls (guarded bounce) ' +
799
+ 'a `bore` apart — a waveguide / light-pipe. A beam travelling down the bore ' +
800
+ 'reflects off the two facing mirrors and stays collimated (no random scatter, ' +
801
+ 'no leak through the walls). The group is the single author-surface entity; ' +
802
+ 'the two mirror Walls it composes carry its `_owner` up-link. Zero physics of its own.',
803
+ // A tunnel has a genuine draggable/resizable extent (like a Wall). 2D drag only.
804
+ ux: {
805
+ resizable: true,
806
+ draggable: { '2d': true },
807
+ shellRender: 'run'
808
+ },
809
+ fields: [
810
+ {
811
+ name: 'rect',
812
+ type: 'rect',
813
+ engineVisible: false,
814
+ live: true,
815
+ writeOnly: true,
816
+ description: 'WRITE-ONLY reposition ADAPTER: the corridor ENVELOPE (both mirrors and the exit ' +
817
+ 'gate). A rect write — the 2D drag, the SPACE carry — moves the whole Tunnel through ' +
818
+ 'Tunnel.setRect (a same-size write is a translation; a resize re-derives length/bore). ' +
819
+ 'Its read getter derives the envelope from the members; captureProps SKIPS it via ' +
820
+ 'writeOnly:true, so left/top/length/bore stay the captured position authority.'
821
+ },
822
+ {
823
+ name: 'left',
824
+ type: 'number',
825
+ constraint: { integer: true },
826
+ engineVisible: true,
827
+ live: true,
828
+ unit: 'px',
829
+ description: 'Bore (aperture) left X position (physics pixels). For an X-axis tunnel this is the corridor mouth.'
830
+ },
831
+ {
832
+ name: 'top',
833
+ type: 'number',
834
+ constraint: { integer: true },
835
+ engineVisible: true,
836
+ live: true,
837
+ unit: 'px',
838
+ description: 'Bore (aperture) top Y position (physics pixels).'
839
+ },
840
+ {
841
+ name: 'length',
842
+ type: 'number',
843
+ constraint: { min: 1, integer: true },
844
+ engineVisible: true,
845
+ unit: 'px',
846
+ description: 'Corridor travel-axis extent (physics pixels) — X extent for an X-axis tunnel, Y for a Y-axis tunnel. The mirrors span this length.'
847
+ },
848
+ {
849
+ name: 'bore',
850
+ type: 'number',
851
+ constraint: { min: 1, integer: true },
852
+ engineVisible: true,
853
+ unit: 'px',
854
+ description: 'Clear aperture — the gap between the two mirror inner faces (physics pixels). The beam passes through the bore; a wider bore lets more through with less collimation.'
855
+ },
856
+ {
857
+ name: 'wallThickness',
858
+ type: 'number',
859
+ constraint: { min: 1, integer: true },
860
+ engineVisible: true,
861
+ unit: 'px',
862
+ description: 'Thickness of each mirror wall in physics pixels (default 10).'
863
+ },
864
+ {
865
+ name: 'axis',
866
+ type: 'enum',
867
+ constraint: { values: { x: 'x', y: 'y' } },
868
+ engineVisible: true,
869
+ description: 'Corridor travel axis: x = horizontal (two wide mirrors above+below the bore; a +x beam reflects its vy); y = vertical (two tall mirrors left+right; a +y beam reflects its vx).'
870
+ },
871
+ {
872
+ name: 'z',
873
+ type: 'number',
874
+ constraint: { integer: true },
875
+ engineVisible: true,
876
+ unit: 'px',
877
+ description: 'Near-face Z of the corridor z-slab (physics pixels). Paired with `depth`: both mirror walls are built as 6-element-rect z-slabs, so the corridor is a real 3D tube whose z-faces also reflect. Inert without `depth` (a flat 2D corridor).'
878
+ },
879
+ {
880
+ name: 'depth',
881
+ type: 'number',
882
+ constraint: { min: 1, integer: true },
883
+ engineVisible: true,
884
+ unit: 'px',
885
+ description: 'Z extent of the corridor z-slab (physics pixels). OMIT for a flat 2D corridor (the mirror walls stay 4-element rects, unconstrained in z). Supplying it makes each mirror a z-slab spanning z .. z+depth — the corridor a real tube in space.'
886
+ },
887
+ {
888
+ name: 'color',
889
+ type: 'rgb',
890
+ engineVisible: false,
891
+ live: true,
892
+ description: 'Render color [r, g, b] for both mirror walls (render-only). A live edit fans ' +
893
+ 'out to both mirrors (the `_owner` fan-out in updateObjectProperty).'
894
+ },
895
+ {
896
+ // D2 (CEO 2026-07-28) — the corridor's collision MODE. Enum values are
897
+ // ferried from Tunnel.MODES (the object's own authority), never re-listed.
898
+ name: 'mode',
899
+ type: 'enum',
900
+ // The vocabulary's OWNER is MODES (Tunnel.MODES) — this descriptor registers from
901
+ // the same module, so it reads the owner instead of copying it.
902
+ constraint: { values: Object.fromEntries(MODES.map(function (m) { return [m, m]; })) },
903
+ engineVisible: true,
904
+ // CONSTRUCTION-level: the directed mode needs its walls registered with the
905
+ // engine's collect_collided flag, which has no live toggle in the substrate
906
+ // (Wall.setWallBehavior refuses exactly this switch). Rebuild to change it.
907
+ live: false,
908
+ description: 'Collision mode. mirror (default) = the guarded-bounce waveguide: a beam ' +
909
+ 'reflects off the two facing mirrors and stays collimated. directed = the tunnel ' +
910
+ 'acts as a semi-circular EMITTER: each wall hit ejects the quantum back into the ' +
911
+ 'bore and re-emits it in a uniformly random FORWARD direction within 180° of the ' +
912
+ 'corridor axis. Construction-level (collection is a registration-time engine flag).'
913
+ },
914
+ {
915
+ name: 'directedAngle',
916
+ type: 'number',
917
+ constraint: { min: 0, max: 360 },
918
+ engineVisible: true,
919
+ live: false,
920
+ unit: 'deg',
921
+ visibleWhen: { field: 'mode', equals: 'directed' },
922
+ description: 'Centre of the forward re-emission fan, in degrees. Omitted = DERIVED from ' +
923
+ '`axis` (0 = +x for an x corridor, 90 = +y for a y one), i.e. the corridor\'s own ' +
924
+ 'travel direction. Set 180/270 for a corridor fed from the far end. The tunnel BODY ' +
925
+ 'stays axis-aligned; this aims the re-emission only.'
926
+ },
927
+ {
928
+ name: 'redirectSpread',
929
+ type: 'number',
930
+ constraint: { min: 0, max: 180 },
931
+ engineVisible: true,
932
+ live: false,
933
+ unit: 'deg',
934
+ visibleWhen: { field: 'mode', equals: 'directed' },
935
+ description: 'Half-width of the forward fan, in degrees (default 90 = a full 180° forward ' +
936
+ 'fan). The exit azimuth is drawn UNIFORMLY in directedAngle ± this. 0 collapses to a ' +
937
+ 'perfectly collimated re-emission along directedAngle.'
938
+ },
939
+ {
940
+ // D3 (CEO 2026-07-29) — the point-exit gate.
941
+ name: 'exitCollapse',
942
+ type: 'boolean',
943
+ engineVisible: true,
944
+ // Construction-level: the gate is a THIRD LargeObject, and its collect flag is
945
+ // a registration-time engine flag. Rebuild to add or remove it.
946
+ live: false,
947
+ description: 'Point-exit gate. Adds a third LargeObject spanning the last 2 columns of ' +
948
+ 'the bore at the output mouth; every crossing quantum is pinned to the bore mid-line, ' +
949
+ 'so the mouth emits as a one-pixel POINT SOURCE (the self-interference geometry). ' +
950
+ 'Composes with either mode. Requires withInterQuantumCollisions:false — FAIL-LOUD ' +
951
+ 'otherwise, because with collisions ON the collapse pixel serializes and kills quanta.'
952
+ },
953
+ {
954
+ // D4 (CEO 2026-07-29) — recentering becomes optional so the two exit effects
955
+ // can be driven independently.
956
+ name: 'exitRecenter',
957
+ type: 'boolean',
958
+ // SUGAR, not an engine-ABI field: the write goes through the facade's own
959
+ // setter (Tunnel.LIVE_PROPS -> applyProp), which recompiles the exit gate's
960
+ // op-program and pushes it — it never reaches setFrameProperty or a LargeObject
961
+ // bool ABI. Declared engineVisible:true it was DEMOTED to a read-only Inspector
962
+ // row by the documented safety rule for engine-visible booleans (whose bind-only
963
+ // write path would send a bare 0/1 where serde wants a JSON bool), so the CEO
964
+ // could see the flag and not flip it. `Wall.collisionBehavior` is the precedent:
965
+ // also op-program-compiled, also live, correctly engineVisible:false.
966
+ engineVisible: false,
967
+ live: true,
968
+ visibleWhen: { field: 'exitCollapse', truthy: true },
969
+ description: 'Pin every quantum crossing the exit gate to the bore mid-line — the ' +
970
+ 'point-source collapse. DEFAULT ON. Turning it OFF leaves the exit spatially ' +
971
+ 'unchanged, which is what separates "does the point source matter?" from "does ' +
972
+ 'coherence matter?" (exitPhaseReset) — while recentering was mandatory the two ' +
973
+ 'were confounded. With BOTH off the gate carries no program at all and stays ' +
974
+ 'registered only as an inert collector. LIVE (next collision).'
975
+ },
976
+ {
977
+ name: 'exitPhaseReset',
978
+ type: 'boolean',
979
+ // SUGAR, not an engine-ABI field: the write goes through the facade's own
980
+ // setter (Tunnel.LIVE_PROPS -> applyProp), which recompiles the exit gate's
981
+ // op-program and pushes it — it never reaches setFrameProperty or a LargeObject
982
+ // bool ABI. Declared engineVisible:true it was DEMOTED to a read-only Inspector
983
+ // row by the documented safety rule for engine-visible booleans (whose bind-only
984
+ // write path would send a bare 0/1 where serde wants a JSON bool), so the CEO
985
+ // could see the flag and not flip it. `Wall.collisionBehavior` is the precedent:
986
+ // also op-program-compiled, also live, correctly engineVisible:false.
987
+ engineVisible: false,
988
+ // LIVE: flipping it recompiles the gate's op-program and pushes it on the same
989
+ // live path recompileActions uses — so the banding can be toggled mid-run.
990
+ live: true,
991
+ visibleWhen: { field: 'exitCollapse', truthy: true },
992
+ description: 'Re-phase-lock the ensemble at the exit gate (appends `set local_time = 0` ' +
993
+ 'to the gate program). Default OFF — a deliberate coherence choice, not a ratified ' +
994
+ 'behavior. Whether banding needs it is λ-dependent: at long effective λ the exit is ' +
995
+ 'already ~90% phase-aligned and bands survive with it OFF; at short λ the exit is ' +
996
+ 'phase-flat and the fan reads smooth OFF, banded ON. PHASE ONLY — never the ' +
997
+ 'decoherence bundle (whose second op redraws charge). LIVE (next collision).'
998
+ },
999
+ {
1000
+ name: 'polarSpread',
1001
+ type: 'number',
1002
+ constraint: { min: 0, max: 90 },
1003
+ engineVisible: true,
1004
+ live: false,
1005
+ unit: 'deg',
1006
+ visibleWhen: { field: 'mode', equals: 'directed' },
1007
+ description: 'Half-width of the OUT-OF-PLANE (polar) fan, in degrees. 0 (default, even on ' +
1008
+ 'a 3D corridor) keeps the re-emission in the XY plane — the angle-uniform case. A ' +
1009
+ 'non-zero value draws the polar angle uniformly in phi, which over-weights the pole: ' +
1010
+ 'it is polar-uniform, NOT solid-angle-uniform on the hemisphere.'
1011
+ }
1012
+ ]
1013
+ };
1014
+
1015
+ // TWO parallel mirror Walls forming a corridor. A palette drop centers the bore
1016
+ // on the click (F1 drop idiom); a recipe supplies explicit left/top. The Tunnel
1017
+ // stamps its own members; create() declares the Tunnel. Zero physics (SU1) — two
1018
+ // Wall{bounce} mirrors + a bore.
1019
+ registerKind(Tunnel, {
1020
+ from: import.meta.url,
1021
+ descriptor: TunnelDescriptor,
1022
+ defaults: {
1023
+ length: 80,
1024
+ bore: 24,
1025
+ wallThickness: 10,
1026
+ axis: 'x',
1027
+ color: [90, 120, 200]
1028
+ },
1029
+ palette: { displayName: 'Mirror Tunnel', category: 'composite', iconHint: 'tunnel', rank: 2 },
1030
+ build: function (props, ctx, Class) {
1031
+ var TunnelCls = Class; // the class that registered (a composed fork's own class)
1032
+ var opts = {};
1033
+ for (var k in props) {
1034
+ if (!Object.prototype.hasOwnProperty.call(props, k)) { continue; }
1035
+ opts[k] = props[k];
1036
+ }
1037
+ var tlen = (opts.length != null) ? opts.length : 80;
1038
+ var tbore = (opts.bore != null) ? opts.bore : 24;
1039
+ // Derive the bore origin from the drop point when the recipe declared no placement.
1040
+ if (opts.left == null) { opts.left = Math.round(ctx.x - tlen / 2); }
1041
+ if (opts.top == null) { opts.top = Math.round(ctx.y - tbore / 2); }
1042
+ opts.length = tlen; opts.bore = tbore;
1043
+ var tunnel = new TunnelCls(ctx.runner.universe, opts);
1044
+ tunnel._schemaKind = Class.name;
1045
+ // mode:'directed' — subscribe the host redirect to the runner's per-tick fan-out.
1046
+ // Self-gating + idempotent, so this is called unconditionally (a mirror tunnel
1047
+ // no-ops). Wiring it HERE, off the DECLARED `mode` field, is what makes the
1048
+ // directed behavior survive codegen → Restart: the rebuilt facade re-subscribes
1049
+ // itself, where a recipe-authored tick-fn would be lost.
1050
+ if (typeof tunnel.attachToRunner === 'function') { tunnel.attachToRunner(ctx.runner); }
1051
+ return tunnel;
1052
+ }
1053
+ });
1054
+
1055
+ // ── Export ───────────────────────────────────────────────────────────
1056
+ export { Tunnel };
1057
+