@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.
- package/CLAUDE.md +87 -0
- package/LICENSE.md +664 -0
- package/README.md +84 -2
- package/dist/dist-manifest.json +218 -0
- package/dist/eyes/index.js +109 -0
- package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
- package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
- package/dist/fat/pkg-node/package.json +12 -0
- package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
- package/dist/frqtl/compat/boot-node.js +34 -0
- package/dist/frqtl/compat/engine.js +127 -0
- package/dist/frqtl/compat/package-root.js +24 -0
- package/dist/frqtl/compat/providers/wasm-engine.js +2499 -0
- package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
- package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
- package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
- package/dist/frqtl/compat/renderer/Filters.js +163 -0
- package/dist/frqtl/compat/renderer/RenderFamily.js +84 -0
- package/dist/frqtl/compat/renderer/RenderMesh.js +99 -0
- package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
- package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
- package/dist/frqtl/compat/renderer/captureStill-common.js +258 -0
- package/dist/frqtl/compat/renderer/display-slot.js +96 -0
- package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
- package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
- package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
- package/dist/frqtl/compat/renderer/providers/index.js +22 -0
- package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
- package/dist/frqtl/compat/renderer/providers/three-provider.js +2470 -0
- package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
- package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
- package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
- package/dist/frqtl/compat/vendor.js +196 -0
- package/dist/frqtl/node-entry.js +17 -0
- package/dist/frqtl/objects/FrqtlApp.js +2920 -0
- package/dist/frqtl/objects/ObjectFactory.js +296 -0
- package/dist/frqtl/objects/RecipeContext.js +322 -0
- package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
- package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
- package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
- package/dist/frqtl/objects/instruments/capture-host.js +453 -0
- package/dist/frqtl/objects/instruments/capture.js +710 -0
- package/dist/frqtl/objects/instruments/hosted-eyes.js +984 -0
- package/dist/frqtl/objects/kinds-entry.js +14 -0
- package/dist/frqtl/objects/kinds.js +190 -0
- package/dist/frqtl/objects/objects/Box.js +273 -0
- package/dist/frqtl/objects/objects/Bullet.js +155 -0
- package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
- package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
- package/dist/frqtl/objects/objects/DirectedTunnel.js +411 -0
- package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
- package/dist/frqtl/objects/objects/LightSource.js +351 -0
- package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
- package/dist/frqtl/objects/objects/Tube.js +471 -0
- package/dist/frqtl/objects/objects/absorbRadiation.js +309 -0
- package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
- package/dist/frqtl/objects/objects/controlAction.js +89 -0
- package/dist/frqtl/objects/objects/controlBind.js +278 -0
- package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
- package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
- package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
- package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
- package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
- package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
- package/dist/frqtl/objects/objects/makeControlsForObject.js +820 -0
- package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
- package/dist/frqtl/objects/objects/slitLinerScatter.js +261 -0
- package/dist/frqtl/objects/objects/sustainedSource.js +298 -0
- package/dist/frqtl/objects/objects/tagger.js +228 -0
- package/dist/frqtl/objects/schema/engine-scopes.js +468 -0
- package/dist/frqtl/objects/schema/index.js +256 -0
- package/dist/frqtl/objects/schema/scopes.js +457 -0
- package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
- package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
- package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
- package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
- package/dist/frqtl/objects/setup/_worldInit.js +162 -0
- package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
- package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
- package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
- package/dist/frqtl/objects/setup/setupConeEmitter3D.js +250 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit.js +326 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
- package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
- package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
- package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
- package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
- package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
- package/dist/frqtl/objects/setup/setupLightSourceLab.js +277 -0
- package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
- package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
- package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
- package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1258 -0
- package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
- package/dist/frqtl/objects/setup/setupTunnelLab.js +240 -0
- package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
- package/dist/frqtl/objects/setup/setupWaveInterference.js +177 -0
- package/dist/frqtl/objects/v1-replacements.js +29 -0
- package/dist/frqtl/package-core.js +23 -0
- package/dist/frqtl/parts/CameraControls.js +643 -0
- package/dist/frqtl/parts/CameraPath.js +575 -0
- package/dist/frqtl/parts/CollisionActions.js +34 -0
- package/dist/frqtl/parts/CollisionTypes.js +30 -0
- package/dist/frqtl/parts/DetectorWall.js +496 -0
- package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
- package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
- package/dist/frqtl/parts/FrqtlVision.js +818 -0
- package/dist/frqtl/parts/GraphingDetectorWall.js +489 -0
- package/dist/frqtl/parts/LargeObject.js +405 -0
- package/dist/frqtl/parts/LightColor.js +255 -0
- package/dist/frqtl/parts/LiveChart.js +285 -0
- package/dist/frqtl/parts/LogicGate.js +717 -0
- package/dist/frqtl/parts/OvenBox.js +611 -0
- package/dist/frqtl/parts/PointSource.js +762 -0
- package/dist/frqtl/parts/Rectangle.js +200 -0
- package/dist/frqtl/parts/SlitLightSource.js +234 -0
- package/dist/frqtl/parts/SlitScreen.js +628 -0
- package/dist/frqtl/parts/SphereEmitter.js +946 -0
- package/dist/frqtl/parts/Trace.js +508 -0
- package/dist/frqtl/parts/Tunnel.js +1057 -0
- package/dist/frqtl/parts/Wall.js +1211 -0
- package/dist/frqtl/parts/index.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall.js +32 -0
- package/dist/frqtl/scenes/atomic-model.js +88 -0
- package/dist/frqtl/scenes/big-bang.js +46 -0
- package/dist/frqtl/scenes/blackbody.js +81 -0
- package/dist/frqtl/scenes/circuit-sat.js +127 -0
- package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
- package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
- package/dist/frqtl/scenes/counting-wall.js +38 -0
- package/dist/frqtl/scenes/double-slit.js +31 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
- package/dist/frqtl/scenes/dse-one-slit.js +114 -0
- package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
- package/dist/frqtl/scenes/dse-tunnel-rebuild.js +243 -0
- package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
- package/dist/frqtl/scenes/dse-two-slits.js +132 -0
- package/dist/frqtl/scenes/empty.js +14 -0
- package/dist/frqtl/scenes/laser-buildup.js +49 -0
- package/dist/frqtl/scenes/mirror-3d.js +56 -0
- package/dist/frqtl/scenes/mirror-route.js +140 -0
- package/dist/frqtl/scenes/mirror.js +38 -0
- package/dist/frqtl/scenes/omni-3d.js +27 -0
- package/dist/frqtl/scenes/omni-source.js +22 -0
- package/dist/frqtl/scenes/self-interference.js +39 -0
- package/dist/frqtl/scenes/smoking-gun.js +122 -0
- package/dist/frqtl/scenes/straight-walk.js +22 -0
- package/dist/frqtl/scenes/sun.js +39 -0
- package/dist/frqtl/scenes/two-source.js +37 -0
- package/dist/frqtl/scenes/wave.js +26 -0
- package/dist/shim/bundle/frqnb.js +112 -0
- package/dist/shim/bundle/index.js +121 -0
- package/dist/shim/bundle/indexHtml.js +107 -0
- package/dist/shim/bundle/manifest.js +95 -0
- package/dist/shim/bundle/zip.js +243 -0
- package/dist/shim/bus.js +67 -0
- package/dist/shim/controls/index.js +627 -0
- package/dist/shim/controls/registry.js +274 -0
- package/dist/shim/engine/artifact-shell.js +211 -0
- package/dist/shim/engine/boot-phases.js +44 -0
- package/dist/shim/engine/engine-stack-boot.js +59 -0
- package/dist/shim/engine/router.js +690 -0
- package/dist/shim/experiment-frame/index.js +296 -0
- package/dist/shim/expr/ast-schema.js +369 -0
- package/dist/shim/field-descriptor/index.js +325 -0
- package/dist/shim/frame-data/index.js +224 -0
- package/dist/shim/index.js +67 -0
- package/dist/shim/notebook/canvas-guard.js +82 -0
- package/dist/shim/notebook/catalog.js +35 -0
- package/dist/shim/notebook/format.js +736 -0
- package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
- package/dist/shim/notebook/kernel.js +671 -0
- package/dist/shim/notebook/knob.js +150 -0
- package/dist/shim/notebook/module-resolver.js +260 -0
- package/dist/shim/notebook/notebook-knobs.js +65 -0
- package/dist/shim/notebook/produced-bindings.js +67 -0
- package/dist/shim/notebook/realm-registry.js +121 -0
- package/dist/shim/notebook/realm-transport.js +73 -0
- package/dist/shim/notebook/setup-builder.js +507 -0
- package/dist/shim/notebook/store.js +159 -0
- package/dist/shim/notebook/vendor-importmap.js +134 -0
- package/dist/shim/notebook/view-builder.js +189 -0
- package/dist/shim/notebook/view-ctx.js +100 -0
- package/dist/shim/notebook/view-handle.js +202 -0
- package/dist/shim/notebook/warmup.js +102 -0
- package/dist/shim/package.json +4 -0
- package/dist/shim/page-params/index.js +213 -0
- package/dist/shim/project/identity.js +112 -0
- package/dist/shim/project/index.js +19 -0
- package/dist/shim/project/link.js +402 -0
- package/dist/shim/project/node-specifier-hook.mjs +35 -0
- package/dist/shim/project/register-node.mjs +17 -0
- package/dist/shim/project/specifiers.js +106 -0
- package/dist/shim/registry/copy-gate.js +110 -0
- package/dist/shim/registry/exhibits.js +455 -0
- package/dist/shim/registry/experiments.js +215 -0
- package/dist/shim/registry/index.js +19 -0
- package/dist/shim/registry/scene-source.js +157 -0
- package/dist/shim/registry/walkthrough.js +804 -0
- package/dist/shim/render/capture-envelope.js +279 -0
- package/dist/shim/render/heat-colormap.js +92 -0
- package/dist/shim/render/render-stages.js +855 -0
- package/dist/shim/render/sun-look.js +33 -0
- package/dist/shim/render-state/apply.js +262 -0
- package/dist/shim/render-state/index.js +74 -0
- package/dist/shim/rpc/index.js +207 -0
- package/dist/shim/runtime/index.js +1023 -0
- package/dist/shim/storage/boot.js +69 -0
- package/dist/shim/storage/index.js +169 -0
- package/dist/shim/storage/local-storage-provider.js +230 -0
- package/dist/shim/storage/make-namespace-store.js +132 -0
- package/dist/shim/storage/modules.js +249 -0
- package/dist/shim/storage/projects.js +150 -0
- package/llms.txt +12 -0
- package/package.json +63 -4
|
@@ -0,0 +1,1057 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tunnel — TWO parallel MIRROR Walls forming a corridor (a tunnel is implemented
|
|
5
|
+
* with 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: 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 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 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:
|
|
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) ──────────────────────────────────────────────
|
|
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.
|
|
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) ──────────────────────────────────────────
|
|
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 user-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).
|
|
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 — 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 — 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 — 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 — 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 a user
|
|
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 a user
|
|
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
|
+
|