@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,1747 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* actionCompiler.js — the op-program compiler. TWO entry points, one output.
|
|
5
|
+
*
|
|
6
|
+
* This is the ONE place that lowers a declared collision behaviour into the op-program AST
|
|
7
|
+
* (lib/shim/expr/ast-schema.js). The engine has NO behaviour vocabulary: it evaluates the
|
|
8
|
+
* program's trigger, applies `set` ops, and provides the min/max/floor/random/intersects
|
|
9
|
+
* primitives. Everything a wall "does" is decided here, at compile time.
|
|
10
|
+
*
|
|
11
|
+
* compileWallV2(spec) — the V2 vocabulary (collisionBehavior +
|
|
12
|
+
* bounceDirection + hooks). Walls speak ONLY this.
|
|
13
|
+
* compileActions(actions, …) — the V1 CollisionActions[] + collision_type path,
|
|
14
|
+
* still live for the non-Wall parts (Box, Bullet,
|
|
15
|
+
* Tube, CircuitGate/Probe, SlitScreen's slits).
|
|
16
|
+
*
|
|
17
|
+
* ## Call order (both paths)
|
|
18
|
+
*
|
|
19
|
+
* The compiler runs BEFORE registration, not after: `LargeObject.js::LargeObject` compiles,
|
|
20
|
+
* puts the program INTO the spec, and only then calls `universe.registerLargeObject(spec)`.
|
|
21
|
+
* (The V2 Wall path registers with an empty spec and pushes its program immediately after,
|
|
22
|
+
* via updateObjectProperty(handle,'LargeObject','program', …) — the AST must cross the WASM
|
|
23
|
+
* boundary as a JSON string, so it cannot ride the spec object itself.)
|
|
24
|
+
*
|
|
25
|
+
* ## There is no native dispatch, so nothing may be left uncompiled
|
|
26
|
+
*
|
|
27
|
+
* `universe.rs::process_large_object_collisions` has no action or collision_type dispatch,
|
|
28
|
+
* and `egpt_wasm::LargeObjectJsSpec` has no `actions` field — serde drops the key. An action
|
|
29
|
+
* this compiler does not lower therefore vanishes at the wire with no error: a typo, a LOG,
|
|
30
|
+
* or a BLOCK with no eject face produced a wall that silently did nothing. So `compileActions`
|
|
31
|
+
* THROWS on anything it cannot compile, naming the action and its replacement.
|
|
32
|
+
*
|
|
33
|
+
* ## Action → op-program map
|
|
34
|
+
*
|
|
35
|
+
* | Action | Op-program form | Notes |
|
|
36
|
+
* |--------------|----------------------------------------------------|----------------------------------|
|
|
37
|
+
* | ABSORB | set 0.is_alive = 0 | kills the colliding frame |
|
|
38
|
+
* | COUNT | set 1.count = 1.count + 1 | increments the LO hit counter |
|
|
39
|
+
* | MARK/DETECT | Tagger.tagOps(loName) | the MARK part (tagger.js) |
|
|
40
|
+
* | BLOCK | set 0.left/top = eject expr (named dir OR closest) | mirrored from OvenBox §10.3 |
|
|
41
|
+
* | BOUNCE | eject + velocity flip (named dir OR closest) | same as OvenBox L4 |
|
|
42
|
+
*
|
|
43
|
+
* ## 'closest' eject (Cond-tree)
|
|
44
|
+
*
|
|
45
|
+
* 'closest' picks the nearest face of the LO to the frame's top-left corner:
|
|
46
|
+
* dL = |LO.left - frame.left|, dR = |LO.right - frame.left|
|
|
47
|
+
* dT = |LO.top - frame.top |, dB = |LO.bottom - frame.top |
|
|
48
|
+
* minD = min(dL, dR, dT, dB); priority L > R > T > B on ties (mirrors native _eject).
|
|
49
|
+
*
|
|
50
|
+
* abs(x) is expressed as Fn('max', [x, Bin('-', Lit(0), x)]).
|
|
51
|
+
* The pick is a nested Cond tree:
|
|
52
|
+
* isHorizMin = (min(dL,dR) < min(dT,dB)) → pick left or right face
|
|
53
|
+
* within horiz: isLeft = (dL <= dR) → eject to left or right
|
|
54
|
+
* within vert: isTop = (dT <= dB) → eject to top or bottom
|
|
55
|
+
* Two set ops: one for frame.left (used when horiz wins), one for frame.top (vert wins).
|
|
56
|
+
*
|
|
57
|
+
* ## Actions that are NOT compilable (each throws)
|
|
58
|
+
*
|
|
59
|
+
* | Action | Why, and what to use instead |
|
|
60
|
+
* |---------------------|----------------------------------------------------------------|
|
|
61
|
+
* | LOG | no logging op exists; read TickData.objects[handle].frames |
|
|
62
|
+
* | RECOLOR | the engine owns no colour; use the v2 `recolor` hook (a tag) |
|
|
63
|
+
* | BLOCK/BOUNCE, no eject face | pass `reposition`, or use compileWallV2 |
|
|
64
|
+
* | anything unrecognised | a typo — it would be dropped at the wire |
|
|
65
|
+
*
|
|
66
|
+
* ## No-double-execution guarantee
|
|
67
|
+
*
|
|
68
|
+
* Structural: the op-program is the ONLY execution path. Every action either becomes an op
|
|
69
|
+
* here or throws here, so there is no second place a behaviour could also run from.
|
|
70
|
+
*
|
|
71
|
+
* ## Arrow legality
|
|
72
|
+
*
|
|
73
|
+
* lib/frqtl/objects/ → lib/shim/expr/ is a legal downward arrow.
|
|
74
|
+
* AstSchema is a static import.
|
|
75
|
+
*/
|
|
76
|
+
|
|
77
|
+
import * as AstSchema from '../../../shim/expr/ast-schema.js';
|
|
78
|
+
import * as SlabFace from '../geometry/SlabFace.js';
|
|
79
|
+
import * as Tagger from './tagger.js';
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
// ── Trigger expr builder (G2b) ──────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Build the trigger expression for an op-program (G2b).
|
|
88
|
+
*
|
|
89
|
+
* For overlap-only actions (ABSORB/COUNT/MARK/BLOCK/BOUNCE/RANDOM/MOMENTUM):
|
|
90
|
+
* trigger = Fn('intersects', [Lit(HANDLE_FRAME), Lit(HANDLE_SELF)])
|
|
91
|
+
*
|
|
92
|
+
* For WALL (directional eject, where frames may have slipped past the face):
|
|
93
|
+
* trigger = Cond(Fn('intersects',...), Lit(1), past_edge_cmp)
|
|
94
|
+
* where past_edge_cmp tests whether the frame has passed through the face
|
|
95
|
+
* this tick (replacing is_frame_past_wall in the engine).
|
|
96
|
+
*
|
|
97
|
+
* The past-edge test is derived from `reposition` (the wall exit direction):
|
|
98
|
+
* 'left' → frame_right < lo_left = Cmp('<', Ref(0,'right'), Ref(1,'left'))
|
|
99
|
+
* 'right' → frame_left > lo_right = Cmp('>', Ref(0,'left'), Ref(1,'right'))
|
|
100
|
+
* 'top' → frame_bottom < lo_top = Cmp('<', Ref(0,'bottom'),Ref(1,'top'))
|
|
101
|
+
* 'bottom' → frame_top > lo_bottom = Cmp('>', Ref(0,'top'), Ref(1,'bottom'))
|
|
102
|
+
* (no direction / 'closest') → intersects-only (no past-edge)
|
|
103
|
+
*
|
|
104
|
+
* Returns the trigger ExprAST.
|
|
105
|
+
*
|
|
106
|
+
* @param {string|null} reposition - Eject direction ('left'|'right'|'top'|'bottom'|'closest'|null)
|
|
107
|
+
* @param {boolean} withPastEdge - true for WALL (test past-edge too); false for all others
|
|
108
|
+
* @param {number} HF - HANDLE_FRAME (0)
|
|
109
|
+
* @param {number} HS - HANDLE_SELF (1)
|
|
110
|
+
* @param {Function} Lit
|
|
111
|
+
* @param {Function} Ref
|
|
112
|
+
* @param {Function} Cmp
|
|
113
|
+
* @param {Function} Cond
|
|
114
|
+
* @param {Function} Fn
|
|
115
|
+
* @returns {ExprAST}
|
|
116
|
+
*/
|
|
117
|
+
function _buildTriggerExpr(reposition, withPastEdge, HF, HS, Lit, Ref, Cmp, Cond, Fn) {
|
|
118
|
+
var intersectsExpr = Fn('intersects', [Lit(HF), Lit(HS)]);
|
|
119
|
+
if (!withPastEdge) {
|
|
120
|
+
return intersectsExpr;
|
|
121
|
+
}
|
|
122
|
+
// WALL: trigger = intersects OR past_edge_cmp
|
|
123
|
+
// Expressed as: Cond(intersectsExpr, Lit(1), past_edge_cmp)
|
|
124
|
+
// This is 1 when overlapping, otherwise falls through to past_edge_cmp (1 or 0).
|
|
125
|
+
//
|
|
126
|
+
// Past-edge semantics: the wall EJECTS in the `reposition` direction, so the wall
|
|
127
|
+
// itself is on the OPPOSITE side. A frame that slips THROUGH the wall exits the
|
|
128
|
+
// box on the wall's exterior face. Mirror of G2 is_frame_past_wall() keyed on the
|
|
129
|
+
// LO name prefix (LEFT/RIGHT/TOP/BOTTOM → which exterior face to test):
|
|
130
|
+
//
|
|
131
|
+
// reposition='left' → wall is the RIGHT wall (ejects inward-left).
|
|
132
|
+
// Past: frame slipped right-of-wall → frame.left > wall.right
|
|
133
|
+
// reposition='right' → wall is the LEFT wall (ejects inward-right).
|
|
134
|
+
// Past: frame slipped left-of-wall → frame.right < wall.left
|
|
135
|
+
// reposition='top' → wall is the BOTTOM wall (ejects inward-up).
|
|
136
|
+
// Past: frame slipped below-wall → frame.top > wall.bottom
|
|
137
|
+
// reposition='bottom' → wall is the TOP wall (ejects inward-down).
|
|
138
|
+
// Past: frame slipped above-wall → frame.bottom < wall.top
|
|
139
|
+
var pastEdge;
|
|
140
|
+
if (reposition === 'left') {
|
|
141
|
+
// RIGHT wall — frame slipped to the right of the wall exterior
|
|
142
|
+
pastEdge = Cmp('>', Ref(HF, 'left'), Ref(HS, 'right'));
|
|
143
|
+
} else if (reposition === 'right') {
|
|
144
|
+
// LEFT wall — frame slipped to the left of the wall exterior
|
|
145
|
+
pastEdge = Cmp('<', Ref(HF, 'right'), Ref(HS, 'left'));
|
|
146
|
+
} else if (reposition === 'top') {
|
|
147
|
+
// BOTTOM wall — frame slipped below the wall exterior
|
|
148
|
+
pastEdge = Cmp('>', Ref(HF, 'top'), Ref(HS, 'bottom'));
|
|
149
|
+
} else if (reposition === 'bottom') {
|
|
150
|
+
// TOP wall — frame slipped above the wall exterior
|
|
151
|
+
pastEdge = Cmp('<', Ref(HF, 'bottom'), Ref(HS, 'top'));
|
|
152
|
+
} else {
|
|
153
|
+
// 'closest' or unknown: no directional past-edge test — intersects only.
|
|
154
|
+
return intersectsExpr;
|
|
155
|
+
}
|
|
156
|
+
// trigger = if intersects then 1 else pastEdge
|
|
157
|
+
return Cond(intersectsExpr, Lit(1), pastEdge);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Attach a program-level trigger to an op-program (G2b — program-level trigger design).
|
|
162
|
+
*
|
|
163
|
+
* Sets program.trigger = trigger so the Rust engine evaluates the trigger ONCE before
|
|
164
|
+
* running any ops. If trigger evaluates to 0 (false), ALL ops are skipped.
|
|
165
|
+
*
|
|
166
|
+
* This replaces the old per-op Cond wrapping approach, which caused a critical bug:
|
|
167
|
+
* when the eject op (op 1) ran and updated scope.left, the bounce op's (op 2) trigger
|
|
168
|
+
* re-evaluated Fn('intersects') using the post-eject position, returning false and
|
|
169
|
+
* silently skipping the velocity flip.
|
|
170
|
+
*
|
|
171
|
+
* The ops are NOT wrapped; they remain unconditional expressions. The trigger gates
|
|
172
|
+
* the entire program atomically.
|
|
173
|
+
*
|
|
174
|
+
* @param {{ on: string, ops: Array }} program — the program to annotate in place
|
|
175
|
+
* @param {ExprAST} trigger — the trigger ExprAST (from _buildTriggerExpr)
|
|
176
|
+
* @returns {{ on: string, ops: Array, trigger: ExprAST }} the annotated program
|
|
177
|
+
*/
|
|
178
|
+
function _applyProgramTrigger(program, trigger) {
|
|
179
|
+
program.trigger = trigger;
|
|
180
|
+
return program;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ── X/Y-only intersects trigger — RETIRED (FRQTL-MAINT, #61, 2026-07-01) ────────────
|
|
184
|
+
//
|
|
185
|
+
// A manual x/y-only overlap trigger (`_buildXYIntersectsTrigger`) used to exist here for
|
|
186
|
+
// thin end-cap walls (Tube's LEFT/RIGHT breech/muzzle), working around a real engine
|
|
187
|
+
// wire-contract gap: every LargeObject's z-slab was hardcoded to `[0, own-x-width]`
|
|
188
|
+
// (`egpt_wasm/src/lib.rs` `LargeObjectJsSpec::into_spec()`), so a thin end-cap's z-slab
|
|
189
|
+
// (`[0, wallThickness]`) never caught a z-drifted frame via the native 3D `Fn('intersects')`
|
|
190
|
+
// test. That gap is now CLOSED: the LO wire contract accepts an explicit 6-element
|
|
191
|
+
// `[left, top, z, width, height, depth]` rect (`rect_to_z_capable_rectangle`,
|
|
192
|
+
// `egpt_wasm/src/lib.rs`, landed commit `1c349ab`) — a wall built with a real z/d that
|
|
193
|
+
// spans the tube's z cross-section makes the native 3D `intersects` trigger correct again,
|
|
194
|
+
// so every wall (including end-caps and the new NEAR/FAR faces) now uses the SAME
|
|
195
|
+
// standard per-face-LO construction + trigger as TOP/BOTTOM (see Tube.js). The manual
|
|
196
|
+
// workaround is deleted, not kept alongside (DRY — superseded code is removed).
|
|
197
|
+
|
|
198
|
+
// ── Eject op builders (extracted from OvenBox.makeEjectProgram — shared authority) ─
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Build the L3 eject op (reposition) for a named wall direction.
|
|
202
|
+
* For 'closest', returns TWO ops (for frame.left and frame.top) that implement
|
|
203
|
+
* the nearest-face pick as a nested Cond tree.
|
|
204
|
+
* Returns an array of AST ops, or null when direction is unknown.
|
|
205
|
+
*
|
|
206
|
+
* Eject formulas (bit-identical to native lo._eject, proven EP22-EP25):
|
|
207
|
+
* 'left' → set frame.left = self.left - frame.w - 1
|
|
208
|
+
* 'right' → set frame.left = self.right + 1
|
|
209
|
+
* 'top' → set frame.top = self.top - frame.h - 1
|
|
210
|
+
* 'bottom' → set frame.top = self.bottom + 1
|
|
211
|
+
*
|
|
212
|
+
* 'closest' nearest-face Cond tree:
|
|
213
|
+
* abs(x) = max(x, 0-x)
|
|
214
|
+
* dL = abs(LO.left - frame.left), dR = abs(LO.right - frame.left)
|
|
215
|
+
* dT = abs(LO.top - frame.top), dB = abs(LO.bottom - frame.top)
|
|
216
|
+
* isHoriz = min(dL,dR) < min(dT,dB) → horizontal face is closer than vertical
|
|
217
|
+
* within horiz: isLeft = dL <= dR → eject left or right
|
|
218
|
+
* within vert: isTop = dT <= dB → eject top or bottom
|
|
219
|
+
* Two set ops: frame.left (horiz wins), frame.top (vert wins).
|
|
220
|
+
* Priority matches native (L > R > T > B on ties): '<' prefers horiz, NOT '<='.
|
|
221
|
+
*
|
|
222
|
+
* @param {string} reposition - 'left'|'right'|'top'|'bottom'|'near'|'far'|'closest'
|
|
223
|
+
* @param {number} HANDLE_FRAME
|
|
224
|
+
* @param {number} HANDLE_SELF
|
|
225
|
+
* @param {Function} Lit
|
|
226
|
+
* @param {Function} Ref
|
|
227
|
+
* @param {Function} Bin
|
|
228
|
+
* @param {Function} Cmp
|
|
229
|
+
* @param {Function} Cond
|
|
230
|
+
* @param {Function} Fn
|
|
231
|
+
* @returns {Array<object>|null} Array of ops (1 for named dir, 2 for 'closest'), or null
|
|
232
|
+
*/
|
|
233
|
+
function _buildEjectOps(reposition, HANDLE_FRAME, HANDLE_SELF, Lit, Ref, Bin, Cmp, Cond, Fn) {
|
|
234
|
+
if (reposition === 'left') {
|
|
235
|
+
return [{ set: HANDLE_FRAME + '.left',
|
|
236
|
+
to: Bin('-', Bin('-', Ref(HANDLE_SELF, 'left'), Ref(HANDLE_FRAME, 'w')), Lit(1)) }];
|
|
237
|
+
} else if (reposition === 'right') {
|
|
238
|
+
return [{ set: HANDLE_FRAME + '.left',
|
|
239
|
+
to: Bin('+', Ref(HANDLE_SELF, 'right'), Lit(1)) }];
|
|
240
|
+
} else if (reposition === 'top') {
|
|
241
|
+
return [{ set: HANDLE_FRAME + '.top',
|
|
242
|
+
to: Bin('-', Bin('-', Ref(HANDLE_SELF, 'top'), Ref(HANDLE_FRAME, 'h')), Lit(1)) }];
|
|
243
|
+
} else if (reposition === 'bottom') {
|
|
244
|
+
return [{ set: HANDLE_FRAME + '.top',
|
|
245
|
+
to: Bin('+', Ref(HANDLE_SELF, 'bottom'), Lit(1)) }];
|
|
246
|
+
} else if (reposition === 'near') {
|
|
247
|
+
// FRQTL-MAINT (2026-07-01, #61): the z NEAR|FAR named-direction case — unblocked
|
|
248
|
+
// by the landed 6-element rect ([left,top,z,w,h,d]) wire contract, which makes
|
|
249
|
+
// `self.near`/`self.far` (the LO's OWN z-slab) meaningful for the first time (was
|
|
250
|
+
// always the origin-pinned [0,w] before — see the RETIRED section above). Formula
|
|
251
|
+
// mirrors 'left'/'right'/'top'/'bottom' exactly, one axis over: push the frame just
|
|
252
|
+
// outside this wall's OWN near face (−z side) — used by a wall positioned at the
|
|
253
|
+
// FAR side of the interior, ejecting intruders back toward −z. Reuses the same
|
|
254
|
+
// shape as the 'closest3d' ejectNear3 formula below (self.near - frame.w/2 - 1).
|
|
255
|
+
return [{ set: HANDLE_FRAME + '.z',
|
|
256
|
+
to: Bin('-', Bin('-', Ref(HANDLE_SELF, 'near'), Bin('*', Ref(HANDLE_FRAME, 'w'), Lit(0.5))), Lit(1)) }];
|
|
257
|
+
} else if (reposition === 'far') {
|
|
258
|
+
// Mirror of 'near': push the frame just outside this wall's OWN far face (+z side)
|
|
259
|
+
// — used by a wall positioned at the NEAR side of the interior, ejecting intruders
|
|
260
|
+
// back toward +z. Reuses the 'closest3d' ejectFar3 formula shape.
|
|
261
|
+
return [{ set: HANDLE_FRAME + '.z',
|
|
262
|
+
to: Bin('+', Bin('+', Ref(HANDLE_SELF, 'far'), Bin('*', Ref(HANDLE_FRAME, 'w'), Lit(0.5))), Lit(1)) }];
|
|
263
|
+
} else if (reposition === 'closest') {
|
|
264
|
+
// Helper: abs(expr) = max(expr, 0 - expr)
|
|
265
|
+
function Abs(e) { return Fn('max', [e, Bin('-', Lit(0), e)]); }
|
|
266
|
+
// Distances: |LO.face - frame.corner|
|
|
267
|
+
// Each used twice → no side-effects so safe to duplicate in AST.
|
|
268
|
+
var dL = Abs(Bin('-', Ref(HANDLE_SELF, 'left'), Ref(HANDLE_FRAME, 'left')));
|
|
269
|
+
var dR = Abs(Bin('-', Ref(HANDLE_SELF, 'right'), Ref(HANDLE_FRAME, 'left')));
|
|
270
|
+
var dT = Abs(Bin('-', Ref(HANDLE_SELF, 'top'), Ref(HANDLE_FRAME, 'top')));
|
|
271
|
+
var dB = Abs(Bin('-', Ref(HANDLE_SELF, 'bottom'), Ref(HANDLE_FRAME, 'top')));
|
|
272
|
+
// Duplicate refs for the second copy (AST is immutable, sharing is safe but
|
|
273
|
+
// clarity-best-practice is separate literals for each occurrence).
|
|
274
|
+
var dL2 = Abs(Bin('-', Ref(HANDLE_SELF, 'left'), Ref(HANDLE_FRAME, 'left')));
|
|
275
|
+
var dR2 = Abs(Bin('-', Ref(HANDLE_SELF, 'right'), Ref(HANDLE_FRAME, 'left')));
|
|
276
|
+
var dT2 = Abs(Bin('-', Ref(HANDLE_SELF, 'top'), Ref(HANDLE_FRAME, 'top')));
|
|
277
|
+
var dB2 = Abs(Bin('-', Ref(HANDLE_SELF, 'bottom'), Ref(HANDLE_FRAME, 'top')));
|
|
278
|
+
var dL3 = Abs(Bin('-', Ref(HANDLE_SELF, 'left'), Ref(HANDLE_FRAME, 'left')));
|
|
279
|
+
var dR3 = Abs(Bin('-', Ref(HANDLE_SELF, 'right'), Ref(HANDLE_FRAME, 'left')));
|
|
280
|
+
var dT3 = Abs(Bin('-', Ref(HANDLE_SELF, 'top'), Ref(HANDLE_FRAME, 'top')));
|
|
281
|
+
var dB3 = Abs(Bin('-', Ref(HANDLE_SELF, 'bottom'), Ref(HANDLE_FRAME, 'top')));
|
|
282
|
+
|
|
283
|
+
// Eject positions
|
|
284
|
+
var ejectLeft = Bin('-', Bin('-', Ref(HANDLE_SELF, 'left'), Ref(HANDLE_FRAME, 'w')), Lit(1));
|
|
285
|
+
var ejectRight = Bin('+', Ref(HANDLE_SELF, 'right'), Lit(1));
|
|
286
|
+
var ejectTop = Bin('-', Bin('-', Ref(HANDLE_SELF, 'top'), Ref(HANDLE_FRAME, 'h')), Lit(1));
|
|
287
|
+
var ejectBottom = Bin('+', Ref(HANDLE_SELF, 'bottom'), Lit(1));
|
|
288
|
+
|
|
289
|
+
// isHoriz: min(dL,dR) < min(dT,dB) → horizontal face is strictly closer
|
|
290
|
+
// Priority: horiz < vert → L/R wins on tie with T/B (matches native if-else order L>R>T>B
|
|
291
|
+
// because native sets newLeft first (overrides newTop), then only changes newTop if
|
|
292
|
+
// neither L nor R won; using strict '<' gives horiz priority over vert on exact tie).
|
|
293
|
+
var minHoriz = Fn('min', [dL, dR]);
|
|
294
|
+
var minVert = Fn('min', [dT, dB]);
|
|
295
|
+
var isHoriz = Cmp('<', minHoriz, minVert);
|
|
296
|
+
|
|
297
|
+
// isLeft (given horiz): dL <= dR ↔ NOT dR < dL ↔ Cmp('<',dR,dL)==0
|
|
298
|
+
// Use: Cond(isHoriz, Cond(isLeft, ejectLeft, ejectRight), keep)
|
|
299
|
+
// isLeft = dL <= dR = NOT (dR < dL) = Cmp('<',dR2,dL2) gives 0 if dL<=dR
|
|
300
|
+
// We want isLeft=1 when dL<=dR, so invert: Cond(Cmp('<',dR2,dL2), ejectRight, ejectLeft)
|
|
301
|
+
// (if dR<dL → right is closer → ejectRight; else dL<=dR → left is closer → ejectLeft)
|
|
302
|
+
var horizEject = Cond(Cmp('<', dR2, dL2), ejectRight, ejectLeft);
|
|
303
|
+
|
|
304
|
+
// op1: set frame.left = if isHoriz → horizEject else → frame.left (no-op)
|
|
305
|
+
var leftOp = {
|
|
306
|
+
set: HANDLE_FRAME + '.left',
|
|
307
|
+
to: Cond(isHoriz, horizEject, Ref(HANDLE_FRAME, 'left'))
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// isTop (given vert): dT <= dB → Cond(Cmp('<',dB3,dT3), ejectBottom, ejectTop)
|
|
311
|
+
var vertEject = Cond(Cmp('<', dB3, dT3), ejectBottom, ejectTop);
|
|
312
|
+
|
|
313
|
+
// op2: set frame.top = if isHoriz → frame.top (no-op) else → vertEject
|
|
314
|
+
// NOT isHoriz = Cond(isHoriz2, Lit(0), Lit(1)) = invert
|
|
315
|
+
// But we don't have logical NOT directly. Use Cond(isHoriz, keepTop, vertEject).
|
|
316
|
+
var minHoriz2 = Fn('min', [dL3, dR3]);
|
|
317
|
+
var minVert2 = Fn('min', [dT3, dB3]);
|
|
318
|
+
var isHoriz2 = Cmp('<', minHoriz2, minVert2);
|
|
319
|
+
var topOp = {
|
|
320
|
+
set: HANDLE_FRAME + '.top',
|
|
321
|
+
to: Cond(isHoriz2, Ref(HANDLE_FRAME, 'top'), vertEject)
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
return [leftOp, topOp];
|
|
325
|
+
|
|
326
|
+
} else if (reposition === 'closest3d') {
|
|
327
|
+
// 3D 'closest' — the 6-face generalization (adds near/far z faces). Used by 3D sugar
|
|
328
|
+
// (e.g. the Bullet) once the z substrate is exposed in the op scope bag (z/near/far).
|
|
329
|
+
// 2D-PARITY note: kept SEPARATE from 'closest' (2-face) so existing 2D scenes are
|
|
330
|
+
// byte-identical; 3D objects OPT IN via reposition:'closest3d'. Safe because the
|
|
331
|
+
// collision trigger (intersects) is now 3D — only frames inside the LO's z-slab fire,
|
|
332
|
+
// so a z-eject can never teleport a far-z frame.
|
|
333
|
+
//
|
|
334
|
+
// WASM-ONLY PATH: the canonical Rust/WASM engine exposes z/near/far
|
|
335
|
+
// in the op scope bag; the FROZEN JS reference (its evalExpr) is 2D-only —
|
|
336
|
+
// z/near/far read 0 there — so closest3d is exercised against WASM only. Conformance
|
|
337
|
+
// C19 (lib/frqtl/conformance/collide_scenarios.cjs — a WASM-only oracle) covers it; do NOT
|
|
338
|
+
// add closest3d to the JS-cross-checked suites (action_compiler_scenarios) without first
|
|
339
|
+
// mirroring z/near/far into the JS-ref scope bag (which the frozen-fixture rule forbids).
|
|
340
|
+
// NIT-2 (non-blocking, next substantive edit): factor the four x/y eject-position
|
|
341
|
+
// expressions (ejectLeft/Right/Top/Bottom) shared with the 'closest' branch above into one
|
|
342
|
+
// helper; closest3d then adds only near/far + the 3-axis priority. Deferred to avoid
|
|
343
|
+
// touching the green 'closest' path at closure.
|
|
344
|
+
function Abs3(e) { return Fn('max', [e, Bin('-', Lit(0), e)]); }
|
|
345
|
+
// Distance factories (fresh AST per use; sharing is safe but fresh matches house style).
|
|
346
|
+
// Reference corner: frame.left/top/near vs the LO's six faces.
|
|
347
|
+
function dL() { return Abs3(Bin('-', Ref(HANDLE_SELF, 'left'), Ref(HANDLE_FRAME, 'left'))); }
|
|
348
|
+
function dR() { return Abs3(Bin('-', Ref(HANDLE_SELF, 'right'), Ref(HANDLE_FRAME, 'left'))); }
|
|
349
|
+
function dT() { return Abs3(Bin('-', Ref(HANDLE_SELF, 'top'), Ref(HANDLE_FRAME, 'top'))); }
|
|
350
|
+
function dB() { return Abs3(Bin('-', Ref(HANDLE_SELF, 'bottom'), Ref(HANDLE_FRAME, 'top'))); }
|
|
351
|
+
function dN() { return Abs3(Bin('-', Ref(HANDLE_SELF, 'near'), Ref(HANDLE_FRAME, 'near'))); }
|
|
352
|
+
function dF() { return Abs3(Bin('-', Ref(HANDLE_SELF, 'far'), Ref(HANDLE_FRAME, 'near'))); }
|
|
353
|
+
function minH() { return Fn('min', [dL(), dR()]); }
|
|
354
|
+
function minV() { return Fn('min', [dT(), dB()]); }
|
|
355
|
+
function minZ() { return Fn('min', [dN(), dF()]); }
|
|
356
|
+
|
|
357
|
+
// Eject targets. x/y write the EDGE (left/top — writable, adjusts center); z writes the
|
|
358
|
+
// CENTER (z is the writable center accessor), so the z formulas use w/2 (depth == w).
|
|
359
|
+
var ejectLeft3 = Bin('-', Bin('-', Ref(HANDLE_SELF, 'left'), Ref(HANDLE_FRAME, 'w')), Lit(1));
|
|
360
|
+
var ejectRight3 = Bin('+', Ref(HANDLE_SELF, 'right'), Lit(1));
|
|
361
|
+
var ejectTop3 = Bin('-', Bin('-', Ref(HANDLE_SELF, 'top'), Ref(HANDLE_FRAME, 'h')), Lit(1));
|
|
362
|
+
var ejectBottom3 = Bin('+', Ref(HANDLE_SELF, 'bottom'), Lit(1));
|
|
363
|
+
// near: place frame fully in front of the near face → z = LO.near - frame.w/2 - 1
|
|
364
|
+
var ejectNear3 = Bin('-', Bin('-', Ref(HANDLE_SELF, 'near'), Bin('*', Ref(HANDLE_FRAME, 'w'), Lit(0.5))), Lit(1));
|
|
365
|
+
// far: place frame fully beyond the far face → z = LO.far + frame.w/2 + 1
|
|
366
|
+
var ejectFar3 = Bin('+', Bin('+', Ref(HANDLE_SELF, 'far'), Bin('*', Ref(HANDLE_FRAME, 'w'), Lit(0.5))), Lit(1));
|
|
367
|
+
|
|
368
|
+
// Within-axis pick (closer face wins; '<' only, so test the strict side).
|
|
369
|
+
var horizEject3 = Cond(Cmp('<', dR(), dL()), ejectRight3, ejectLeft3);
|
|
370
|
+
var vertEject3 = Cond(Cmp('<', dB(), dT()), ejectBottom3, ejectTop3);
|
|
371
|
+
var zEject3 = Cond(Cmp('<', dF(), dN()), ejectFar3, ejectNear3);
|
|
372
|
+
|
|
373
|
+
// Axis selection, priority x > y > z (ties to the earlier axis). No '<=' op, so encode
|
|
374
|
+
// "minH wins ties" as: x loses ONLY if minV or minZ is STRICTLY smaller than minH.
|
|
375
|
+
// isH = NOT(minV < minH) AND NOT(minZ < minH)
|
|
376
|
+
// isV = NOT isH AND NOT(minZ < minV)
|
|
377
|
+
// isZ = NOT isH AND (minZ < minV)
|
|
378
|
+
function isH() {
|
|
379
|
+
return Cond(Cmp('<', minV(), minH()), Lit(0),
|
|
380
|
+
Cond(Cmp('<', minZ(), minH()), Lit(0), Lit(1)));
|
|
381
|
+
}
|
|
382
|
+
// "V wins, given not H" = NOT(minZ < minV)
|
|
383
|
+
function vGivenNotH() { return Cond(Cmp('<', minZ(), minV()), Lit(0), Lit(1)); }
|
|
384
|
+
|
|
385
|
+
var leftOp3 = {
|
|
386
|
+
set: HANDLE_FRAME + '.left',
|
|
387
|
+
to: Cond(isH(), horizEject3, Ref(HANDLE_FRAME, 'left'))
|
|
388
|
+
};
|
|
389
|
+
var topOp3 = {
|
|
390
|
+
set: HANDLE_FRAME + '.top',
|
|
391
|
+
to: Cond(Cond(isH(), Lit(0), vGivenNotH()), vertEject3, Ref(HANDLE_FRAME, 'top'))
|
|
392
|
+
};
|
|
393
|
+
var zOp3 = {
|
|
394
|
+
set: HANDLE_FRAME + '.z',
|
|
395
|
+
to: Cond(Cond(isH(), Lit(0), Cond(vGivenNotH(), Lit(0), Lit(1))), zEject3, Ref(HANDLE_FRAME, 'z'))
|
|
396
|
+
};
|
|
397
|
+
return [leftOp3, topOp3, zOp3];
|
|
398
|
+
}
|
|
399
|
+
return null; // unknown direction
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Build the L4 velocity-flip op for a named wall direction.
|
|
404
|
+
* Returns a single AST op object, or null when wallName has no directional prefix.
|
|
405
|
+
*
|
|
406
|
+
* Velocity-flip formula (bit-identical to native BOUNCE):
|
|
407
|
+
* LEFT|RIGHT → set frame.vx = 0 - frame.vx
|
|
408
|
+
* TOP|BOTTOM → set frame.vy = 0 - frame.vy
|
|
409
|
+
* NEAR|FAR → set frame.vz = 0 - frame.vz (FRQTL-MAINT 2026-07-01, #61 — unblocked
|
|
410
|
+
* by the landed z/depth wire contract; mirrors LEFT/RIGHT/TOP/BOTTOM
|
|
411
|
+
* one axis over)
|
|
412
|
+
*
|
|
413
|
+
* @param {string} loName - LO name string (e.g. 'TOP', 'LEFT_T', 'wall1')
|
|
414
|
+
* @param {number} HANDLE_FRAME
|
|
415
|
+
* @param {Function} Lit
|
|
416
|
+
* @param {Function} Ref
|
|
417
|
+
* @param {Function} Bin
|
|
418
|
+
* @returns {object|null}
|
|
419
|
+
*/
|
|
420
|
+
function _buildVelocityFlipOp(loName, HANDLE_FRAME, Lit, Ref, Bin) {
|
|
421
|
+
var wallDir = loName ? loName.split('_')[0] : '';
|
|
422
|
+
if (wallDir === 'LEFT' || wallDir === 'RIGHT') {
|
|
423
|
+
return { set: HANDLE_FRAME + '.vx',
|
|
424
|
+
to: Bin('-', Lit(0), Ref(HANDLE_FRAME, 'vx')) };
|
|
425
|
+
} else if (wallDir === 'TOP' || wallDir === 'BOTTOM') {
|
|
426
|
+
return { set: HANDLE_FRAME + '.vy',
|
|
427
|
+
to: Bin('-', Lit(0), Ref(HANDLE_FRAME, 'vy')) };
|
|
428
|
+
} else if (wallDir === 'NEAR' || wallDir === 'FAR') {
|
|
429
|
+
return { set: HANDLE_FRAME + '.vz',
|
|
430
|
+
to: Bin('-', Lit(0), Ref(HANDLE_FRAME, 'vz')) };
|
|
431
|
+
}
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ── Z-containment workaround — RETIRED (FRQTL-MAINT, #61, 2026-07-01) ───────────────
|
|
436
|
+
//
|
|
437
|
+
// `_buildZContainmentOps` used to build a manual combined vz-reflect/z-clamp program on
|
|
438
|
+
// ONE full-footprint LO (literal JS-computed z bounds, never `self.near`/`self.far`),
|
|
439
|
+
// working around the same origin-pinned z-slab gap described above (every LO's z was
|
|
440
|
+
// hardcoded to 0.0, depth to its own x-width — `egpt_wasm/src/lib.rs`
|
|
441
|
+
// `LargeObjectJsSpec::into_spec()`, pre-commit `1c349ab`). That gap is closed: the wire
|
|
442
|
+
// contract now accepts an explicit 6-element `[left, top, z, width, height, depth]` rect
|
|
443
|
+
// (`rect_to_z_capable_rectangle`), so a real NEAR wall LO and a real FAR wall LO — each
|
|
444
|
+
// positioned at their own z-slab exactly like TOP/BOTTOM are positioned in y — now work
|
|
445
|
+
// through the SAME standard per-face-LO construction (`new LargeObject(...)` auto-compile
|
|
446
|
+
// via `collisionType`+`reposition`) as every other face, using the `'near'`/`'far'` cases
|
|
447
|
+
// added to `_buildEjectOps`/`_buildVelocityFlipOp` above. Tube.js builds these two extra
|
|
448
|
+
// faces directly; the manual workaround is deleted, not kept alongside (DRY).
|
|
449
|
+
|
|
450
|
+
// ── Collision LEXICON v2 builders (Collision Lab, lexicon v2) ────────────────────
|
|
451
|
+
//
|
|
452
|
+
// These are ADDITIVE, pure op-program builders for the v2 wall vocabulary
|
|
453
|
+
// (behavior nothing|block|bounce|absorb|destroy; bounceDirection reflect|random;
|
|
454
|
+
// hooks mark|count). They compose the SAME closed NODE_KINDS the rest of this
|
|
455
|
+
// file uses — ZERO engine change (v2 advisory, all RUN-verified pkg-node).
|
|
456
|
+
// The existing compileActions() path is UNTOUCHED (byte-identity preserved); v2
|
|
457
|
+
// callers (Wall.js via resolveWallSpec) go through compileWallV2() below.
|
|
458
|
+
//
|
|
459
|
+
// AST logic helpers — the closed vocab has '<','>','==' (no '<=','>=') and no
|
|
460
|
+
// boolean AND/OR, so these are the canonical encodings used throughout:
|
|
461
|
+
// LE(a,b) = a<=b = NOT(a>b) = Cond(Cmp('>',a,b), 0, 1)
|
|
462
|
+
// GE(a,b) = a>=b = NOT(a<b) = Cond(Cmp('<',a,b), 0, 1)
|
|
463
|
+
// OR(x,y) (x,y ∈ {0,1}) = Cond(x, 1, y)
|
|
464
|
+
// AND(x,y) = Cond(x, y, 0)
|
|
465
|
+
// NOT(x) = Cond(x, 0, 1)
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* B2 — incidence-aware single-LO Reflect. Replaces the W1
|
|
469
|
+
* name-prefix flip (`_buildVelocityFlipOp` keyed on the LO NAME) with a Cond tree
|
|
470
|
+
* over the frame's PREVIOUS position: the axis whose move carried the frame INTO
|
|
471
|
+
* the wall this tick is the one whose velocity is inverted — the physical
|
|
472
|
+
* axis-of-incidence, on ONE monolithic wall (no per-face LO decomposition needed).
|
|
473
|
+
*
|
|
474
|
+
* prev_x/prev_y are CENTER coords already in the frame scope bag (universe.rs:2262,
|
|
475
|
+
* read today by MOMENTUM_BASED at :670); w/h are the frame extents; left/right/top/
|
|
476
|
+
* bottom are the frame's CURRENT edges; self.left/right/top/bottom are the wall faces.
|
|
477
|
+
*
|
|
478
|
+
* wasOutsideX = the frame's x-extent did NOT overlap the wall's x-span LAST tick
|
|
479
|
+
* (fully left: prev_x + w/2 <= self.left ; fully right: prev_x - w/2 >= self.right).
|
|
480
|
+
* Corner tie-break (both axes newly-overlap in one ±2-stack tick) = DEEPER-PENETRATION
|
|
481
|
+
* axis (larger current overlap), BOTH on exact tie:
|
|
482
|
+
* penX = min(frame.right, self.right) - max(frame.left, self.left)
|
|
483
|
+
* penY = min(frame.bottom, self.bottom) - max(frame.top, self.top)
|
|
484
|
+
* flipX ⇔ wasOutsideX AND (NOT wasOutsideY OR penX >= penY)
|
|
485
|
+
* flipY ⇔ wasOutsideY AND (NOT wasOutsideX OR penY >= penX) (>= gives both-on-tie)
|
|
486
|
+
*
|
|
487
|
+
* Composes with an eject (the caller prepends `_buildEjectOps('closest')`) — Reflect
|
|
488
|
+
* ALWAYS fuses with eject (a flip-without-reposition re-triggers next tick = in-wall
|
|
489
|
+
* jitter; round-1 §3). Returns ONLY the two flip ops (eject is the caller's).
|
|
490
|
+
*
|
|
491
|
+
* @returns {Array<object>} [flipVxOp, flipVyOp]
|
|
492
|
+
*/
|
|
493
|
+
function _buildIncidenceReflectOps(HF, HS, Lit, Ref, Bin, Cmp, Cond, Fn) {
|
|
494
|
+
function NOT(x) { return Cond(x, Lit(0), Lit(1)); }
|
|
495
|
+
function OR(x, y) { return Cond(x, Lit(1), y); }
|
|
496
|
+
function AND(x, y){ return Cond(x, y, Lit(0)); }
|
|
497
|
+
function LE(a, b) { return Cond(Cmp('>', a, b), Lit(0), Lit(1)); }
|
|
498
|
+
function GE(a, b) { return Cond(Cmp('<', a, b), Lit(0), Lit(1)); }
|
|
499
|
+
function halfW() { return Bin('*', Ref(HF, 'w'), Lit(0.5)); }
|
|
500
|
+
function halfH() { return Bin('*', Ref(HF, 'h'), Lit(0.5)); }
|
|
501
|
+
// prev extents (center ± half): fresh AST per use (immutable, no side effects).
|
|
502
|
+
function prevRight() { return Bin('+', Ref(HF, 'prev_x'), halfW()); }
|
|
503
|
+
function prevLeft() { return Bin('-', Ref(HF, 'prev_x'), halfW()); }
|
|
504
|
+
function prevBottom(){ return Bin('+', Ref(HF, 'prev_y'), halfH()); }
|
|
505
|
+
function prevTop() { return Bin('-', Ref(HF, 'prev_y'), halfH()); }
|
|
506
|
+
function wasOutsideX() { return OR(LE(prevRight(), Ref(HS, 'left')), GE(prevLeft(), Ref(HS, 'right'))); }
|
|
507
|
+
function wasOutsideY() { return OR(LE(prevBottom(), Ref(HS, 'top')), GE(prevTop(), Ref(HS, 'bottom'))); }
|
|
508
|
+
// current-tick overlap depth per axis (deeper = larger).
|
|
509
|
+
function penX() {
|
|
510
|
+
return Bin('-', Fn('min', [Ref(HF, 'right'), Ref(HS, 'right')]),
|
|
511
|
+
Fn('max', [Ref(HF, 'left'), Ref(HS, 'left')]));
|
|
512
|
+
}
|
|
513
|
+
function penY() {
|
|
514
|
+
return Bin('-', Fn('min', [Ref(HF, 'bottom'), Ref(HS, 'bottom')]),
|
|
515
|
+
Fn('max', [Ref(HF, 'top'), Ref(HS, 'top')]));
|
|
516
|
+
}
|
|
517
|
+
var flipXcond = AND(wasOutsideX(), OR(NOT(wasOutsideY()), GE(penX(), penY())));
|
|
518
|
+
var flipYcond = AND(wasOutsideY(), OR(NOT(wasOutsideX()), GE(penY(), penX())));
|
|
519
|
+
return [
|
|
520
|
+
{ set: HF + '.vx', to: Cond(flipXcond, Bin('-', Lit(0), Ref(HF, 'vx')), Ref(HF, 'vx')) },
|
|
521
|
+
{ set: HF + '.vy', to: Cond(flipYcond, Bin('-', Lit(0), Ref(HF, 'vy')), Ref(HF, 'vy')) }
|
|
522
|
+
];
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Z1 — 3D incidence-aware Reflect (Q2 PROVEN as the
|
|
527
|
+
* op-data generalization on top of the landed prev_z scope-bag key — C26). The exact
|
|
528
|
+
* 3-axis lift of `_buildIncidenceReflectOps`: adds the z-axis (near/far faces), flipping
|
|
529
|
+
* whichever of the SIX faces' axis carried the frame INTO the wall's z-slab this tick.
|
|
530
|
+
*
|
|
531
|
+
* prev_z is a CENTER coord in the frame scope bag (universe.rs, WASM-only — the frozen
|
|
532
|
+
* JS fixture has NO prev_* family, so this whole builder is WASM-only, declared alongside
|
|
533
|
+
* the closest3d/C19 precedent). Frames are cubes ⇒ the z half-extent is w/2 (depth==w,
|
|
534
|
+
* matching the `near = z − w/2` derivation), exactly as prev_x uses w/2 in 2D.
|
|
535
|
+
*
|
|
536
|
+
* wasOutsideZ = the frame's z-extent did NOT overlap the wall's z-slab LAST tick
|
|
537
|
+
* (fully near: prev_z + w/2 <= self.near ; fully far: prev_z − w/2 >= self.far).
|
|
538
|
+
* penZ = min(frame.far, self.far) − max(frame.near, self.near) (current overlap depth)
|
|
539
|
+
*
|
|
540
|
+
* 3-AXIS deeper-penetration tie-break (generalizes the ratified 2-axis rule): flip axis A
|
|
541
|
+
* iff A was outside AND A's penetration ≥ EVERY OTHER axis that was ALSO outside (an axis
|
|
542
|
+
* that was already inside does not compete). `>=` gives both-on-exact-tie, exactly as the
|
|
543
|
+
* 2D rule does. Composes with `_buildEjectOps('closest3d')` prepended by the caller (a
|
|
544
|
+
* z-flip without a z-eject re-triggers next tick = in-wall jitter, same as the 2D rule).
|
|
545
|
+
*
|
|
546
|
+
* @returns {Array<object>} [flipVxOp, flipVyOp, flipVzOp]
|
|
547
|
+
*/
|
|
548
|
+
function _buildIncidenceReflectOps3D(HF, HS, Lit, Ref, Bin, Cmp, Cond, Fn) {
|
|
549
|
+
function NOT(x) { return Cond(x, Lit(0), Lit(1)); }
|
|
550
|
+
function OR(x, y) { return Cond(x, Lit(1), y); }
|
|
551
|
+
function AND(x, y){ return Cond(x, y, Lit(0)); }
|
|
552
|
+
function LE(a, b) { return Cond(Cmp('>', a, b), Lit(0), Lit(1)); }
|
|
553
|
+
function GE(a, b) { return Cond(Cmp('<', a, b), Lit(0), Lit(1)); }
|
|
554
|
+
function halfW() { return Bin('*', Ref(HF, 'w'), Lit(0.5)); }
|
|
555
|
+
function halfH() { return Bin('*', Ref(HF, 'h'), Lit(0.5)); }
|
|
556
|
+
// z half-extent: frames are cubes, depth == w (the near=z−w/2 derivation), so w/2.
|
|
557
|
+
function halfD() { return Bin('*', Ref(HF, 'w'), Lit(0.5)); }
|
|
558
|
+
function prevRight() { return Bin('+', Ref(HF, 'prev_x'), halfW()); }
|
|
559
|
+
function prevLeft() { return Bin('-', Ref(HF, 'prev_x'), halfW()); }
|
|
560
|
+
function prevBottom(){ return Bin('+', Ref(HF, 'prev_y'), halfH()); }
|
|
561
|
+
function prevTop() { return Bin('-', Ref(HF, 'prev_y'), halfH()); }
|
|
562
|
+
function prevFar() { return Bin('+', Ref(HF, 'prev_z'), halfD()); }
|
|
563
|
+
function prevNear() { return Bin('-', Ref(HF, 'prev_z'), halfD()); }
|
|
564
|
+
function wasOutsideX() { return OR(LE(prevRight(), Ref(HS, 'left')), GE(prevLeft(), Ref(HS, 'right'))); }
|
|
565
|
+
function wasOutsideY() { return OR(LE(prevBottom(), Ref(HS, 'top')), GE(prevTop(), Ref(HS, 'bottom'))); }
|
|
566
|
+
function wasOutsideZ() { return OR(LE(prevFar(), Ref(HS, 'near')), GE(prevNear(), Ref(HS, 'far'))); }
|
|
567
|
+
function penX() {
|
|
568
|
+
return Bin('-', Fn('min', [Ref(HF, 'right'), Ref(HS, 'right')]),
|
|
569
|
+
Fn('max', [Ref(HF, 'left'), Ref(HS, 'left')]));
|
|
570
|
+
}
|
|
571
|
+
function penY() {
|
|
572
|
+
return Bin('-', Fn('min', [Ref(HF, 'bottom'), Ref(HS, 'bottom')]),
|
|
573
|
+
Fn('max', [Ref(HF, 'top'), Ref(HS, 'top')]));
|
|
574
|
+
}
|
|
575
|
+
function penZ() {
|
|
576
|
+
return Bin('-', Fn('min', [Ref(HF, 'far'), Ref(HS, 'far')]),
|
|
577
|
+
Fn('max', [Ref(HF, 'near'), Ref(HS, 'near')]));
|
|
578
|
+
}
|
|
579
|
+
// flipA ⇔ wasOutsideA AND (NOT wasOutsideB OR penA>=penB) AND (NOT wasOutsideC OR penA>=penC)
|
|
580
|
+
var flipXcond = AND(wasOutsideX(),
|
|
581
|
+
AND(OR(NOT(wasOutsideY()), GE(penX(), penY())), OR(NOT(wasOutsideZ()), GE(penX(), penZ()))));
|
|
582
|
+
var flipYcond = AND(wasOutsideY(),
|
|
583
|
+
AND(OR(NOT(wasOutsideX()), GE(penY(), penX())), OR(NOT(wasOutsideZ()), GE(penY(), penZ()))));
|
|
584
|
+
var flipZcond = AND(wasOutsideZ(),
|
|
585
|
+
AND(OR(NOT(wasOutsideX()), GE(penZ(), penX())), OR(NOT(wasOutsideY()), GE(penZ(), penY()))));
|
|
586
|
+
return [
|
|
587
|
+
{ set: HF + '.vx', to: Cond(flipXcond, Bin('-', Lit(0), Ref(HF, 'vx')), Ref(HF, 'vx')) },
|
|
588
|
+
{ set: HF + '.vy', to: Cond(flipYcond, Bin('-', Lit(0), Ref(HF, 'vy')), Ref(HF, 'vy')) },
|
|
589
|
+
{ set: HF + '.vz', to: Cond(flipZcond, Bin('-', Lit(0), Ref(HF, 'vz')), Ref(HF, 'vz')) }
|
|
590
|
+
];
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* R1 — ARBITRARY-ANGLE mirror reflection (P3b PROVEN
|
|
595
|
+
* EXACT). A mirror is a REFLECTION of the velocity about a mirror LINE at angle α
|
|
596
|
+
* (the mirror SURFACE angle from horizontal, = the wall's `orientationDeg`):
|
|
597
|
+
*
|
|
598
|
+
* R(α) = [ cos2α sin2α ] reflected(+x) = (cos2α, sin2α)
|
|
599
|
+
* [ sin2α −cos2α ] ⇒ slope = tan(2α) (standard optics: a mirror
|
|
600
|
+
* surface at α deflects the beam by 2α).
|
|
601
|
+
*
|
|
602
|
+
* This is incidence-INDEPENDENT (a fixed linear map — physically correct for a
|
|
603
|
+
* genuine tilted mirror plane, so NO Cond-tree on the velocity part). Trig is legal
|
|
604
|
+
* HOST-side at compile time (the SAME pattern the constrained-cone endpoints already
|
|
605
|
+
* bake). Two-op sequential form (op2 reads the NEW vx written by op1, exploiting the
|
|
606
|
+
* write-propagation into the scope bag — v2 P3):
|
|
607
|
+
* op1: vx' = c2·vx + s2·vy (reads both OLD values)
|
|
608
|
+
* op2: vy' = (s2/c2)·vx' − (1/c2)·vy_old ≡ s2·vx_old − c2·vy_old (exact)
|
|
609
|
+
* Requires c2 = cos2α ≠ 0 (the α ≡ 45° mod 90° singularity routes to the exact swap
|
|
610
|
+
* builder below). @returns {Array<object>} [vxOp, vyOp]
|
|
611
|
+
*/
|
|
612
|
+
function _buildAngledReflectOps(HF, alphaDeg, Lit, Ref, Bin) {
|
|
613
|
+
var a = alphaDeg * Math.PI / 180;
|
|
614
|
+
var c2 = Math.cos(2 * a), s2 = Math.sin(2 * a);
|
|
615
|
+
return [
|
|
616
|
+
{ set: HF + '.vx', to: Bin('+', Bin('*', Lit(c2), Ref(HF, 'vx')),
|
|
617
|
+
Bin('*', Lit(s2), Ref(HF, 'vy'))) },
|
|
618
|
+
{ set: HF + '.vy', to: Bin('-', Bin('*', Lit(s2 / c2), Ref(HF, 'vx')),
|
|
619
|
+
Bin('*', Lit(1 / c2), Ref(HF, 'vy'))) }
|
|
620
|
+
];
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/**
|
|
624
|
+
* R1 — the α ≡ 45° mod 90° diagonal mirror, EXACT (P1/P2 PROVEN). At these
|
|
625
|
+
* orientations cos2α = 0, so R(α) = [0, s2; s2, 0] with s2 = ±1 — the 2-op form above
|
|
626
|
+
* divides by zero. The exact realization is the 3-op add/subtract SWAP (a naive
|
|
627
|
+
* `vx=vy; vy=vx` is WRONG — the second op reads the already-overwritten vx;
|
|
628
|
+
* v2 P3), then a negate for the s2 = −1 branch:
|
|
629
|
+
* α ≡ 45° mod 180° (s2=+1): pure swap vx' = vy, vy' = vx (+x → +y)
|
|
630
|
+
* α ≡ 135° mod 180° (s2=−1): swap + negate vx' = −vy, vy' = −vx (+x → −y)
|
|
631
|
+
* @returns {Array<object>} [.. 3 swap ops (+2 negate ops when s2<0) ..]
|
|
632
|
+
*/
|
|
633
|
+
function _buildSwapReflectOps(HF, alphaDeg, Lit, Ref, Bin) {
|
|
634
|
+
var s2 = Math.round(Math.sin(2 * alphaDeg * Math.PI / 180)); // +1 or −1
|
|
635
|
+
var ops = [
|
|
636
|
+
{ set: HF + '.vx', to: Bin('+', Ref(HF, 'vx'), Ref(HF, 'vy')) }, // vx ← vx+vy
|
|
637
|
+
{ set: HF + '.vy', to: Bin('-', Ref(HF, 'vx'), Ref(HF, 'vy')) }, // vy ← (vx+vy)−vy = old vx
|
|
638
|
+
{ set: HF + '.vx', to: Bin('-', Ref(HF, 'vx'), Ref(HF, 'vy')) } // vx ← (vx+vy)−old vx = old vy
|
|
639
|
+
];
|
|
640
|
+
if (s2 < 0) {
|
|
641
|
+
ops.push({ set: HF + '.vx', to: Bin('-', Lit(0), Ref(HF, 'vx')) });
|
|
642
|
+
ops.push({ set: HF + '.vy', to: Bin('-', Lit(0), Ref(HF, 'vy')) });
|
|
643
|
+
}
|
|
644
|
+
return ops;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* R1/Z1 — build the ORIENTATION-AWARE reflection ops for a mirror at `orientationDeg`.
|
|
649
|
+
* The ONE dispatch authority (compileWallV2's reflect branch calls this):
|
|
650
|
+
* • orientation ≡ 0 mod 90 (0/90/180/270) → the proven axis-aligned incidence-flip
|
|
651
|
+
* tree (a BOX reflector handling BOTH faces). Orientation 0 is BYTE-IDENTICAL to
|
|
652
|
+
* the pre-rotation program (SU1) — the incidence tree ignores orientation entirely.
|
|
653
|
+
* When `is3D` (a wall with a real z-slab), the 3-axis lift (`_buildIncidenceReflectOps3D`)
|
|
654
|
+
* handles the near/far faces too — 2D≡3D, the same law with a third axis (WASM-only,
|
|
655
|
+
* rides prev_z / C26).
|
|
656
|
+
* • orientation ≡ 45 mod 90 (45/135/225/315) → the exact swap / swap+negate ops.
|
|
657
|
+
* • any other α → the 2-op host-baked-trig fixed-plane reflection.
|
|
658
|
+
*
|
|
659
|
+
* 3D ORIENTATION SCOPE (kept to what the advisory PROVED): a non-cardinal
|
|
660
|
+
* `orientationDeg` is orientation about the Z-AXIS (rotation in the xy-plane) — the 2D
|
|
661
|
+
* swap/angled law LIFTED to a slab, with vz passing straight through (the beam's z-component
|
|
662
|
+
* is untouched). This is the proven, exact case. FULL 3D orientation (a SECOND tilt angle —
|
|
663
|
+
* e.g. an (x,z)-plane periscope that swaps vx↔vz) is a FLAGGED FOLLOW-UP, not implemented
|
|
664
|
+
* here (it needs a second angle param + the (x,z)/(y,z) swap patterns; no math is invented).
|
|
665
|
+
* @returns {Array<object>}
|
|
666
|
+
*/
|
|
667
|
+
function _buildReflectOps(orientationDeg, HF, HS, Lit, Ref, Bin, Cmp, Cond, Fn, is3D) {
|
|
668
|
+
var o = (typeof orientationDeg === 'number' && isFinite(orientationDeg)) ? orientationDeg : 0;
|
|
669
|
+
var onorm = ((o % 360) + 360) % 360;
|
|
670
|
+
var EPS = 1e-9;
|
|
671
|
+
var mod90 = onorm % 90;
|
|
672
|
+
// DRY — the cardinal gate AND the drawn-face reflection base come from the ONE
|
|
673
|
+
// SlabFace authority (circulation §1). SlabFace normalizes orientationDeg the SAME way
|
|
674
|
+
// (`((o%360)+360)%360`) and returns reflectBaseDeg = (90 + onorm) % 360, so this repoint
|
|
675
|
+
// is BYTE-IDENTICAL to the previous inline `alphaScreen = (90 + onorm) % 360` + the
|
|
676
|
+
// inline cardinal check. (w/h are not passed here — the reflection base does not depend
|
|
677
|
+
// on slab aspect; the OBB region is built separately from SlabFace in Wall.compileWallV2.)
|
|
678
|
+
var slab = SlabFace.resolveSlabFace({ orientationDeg: onorm });
|
|
679
|
+
if (slab.isCardinal) {
|
|
680
|
+
// 0/90/180/270 → axis-aligned box: the proven incidence tree (byte-identical at 0).
|
|
681
|
+
// A 3D wall reflects off all six faces (the z-axis lift); a 2D wall stays 2-axis.
|
|
682
|
+
// ORIENTATION-INDEPENDENT → the drawn-face base below does NOT touch this case
|
|
683
|
+
// (bench + cardinal-reflect byte-identity preserved).
|
|
684
|
+
return is3D
|
|
685
|
+
? _buildIncidenceReflectOps3D(HF, HS, Lit, Ref, Bin, Cmp, Cond, Fn)
|
|
686
|
+
: _buildIncidenceReflectOps(HF, HS, Lit, Ref, Bin, Cmp, Cond, Fn);
|
|
687
|
+
}
|
|
688
|
+
// DRAWN-FACE REFLECTION (supersedes the FIX-3
|
|
689
|
+
// screenα=−orientationDeg guess). The mirror's reflecting surface is the LONG face of the
|
|
690
|
+
// slab the renderer actually draws. The Wall mirror is a TALL slab (h > w): its long face
|
|
691
|
+
// is VERTICAL at orientation 0, so a +x beam bounces straight back (−x) — EXACTLY what the
|
|
692
|
+
// cardinal incidence tree already does at orientation 0. p5 draws that slab rotated by
|
|
693
|
+
// +orientationDeg (rect(-w/2,-h/2,w,h) with h>w; local y-axis long face), so the drawn face
|
|
694
|
+
// LINE sits at (90 + orientationDeg) in the engine's (y-DOWN) velocity coordinates. The
|
|
695
|
+
// exact reflection about the drawn face is therefore R(90 + orientationDeg) — reflect about
|
|
696
|
+
// the line the user SEES and calibrates against.
|
|
697
|
+
//
|
|
698
|
+
// Why the −orientationDeg guess was wrong (proven, scratchpad/reflect_matrix.cjs): 90+o and
|
|
699
|
+
// −o are congruent mod 180 ONLY at o ≡ 45 (mod 90), so R(−o) coincided with the drawn face
|
|
700
|
+
// at 45/135/225/315 (and, via the separate incidence tree, at cardinals) but reflected a +x
|
|
701
|
+
// beam to the OPPOSITE horizontal side at every OTHER angle (30/37/60/120/150…). That off-
|
|
702
|
+
// face deflection at non-45 tilt is the "reflection isn't correct all the time" defect seen
|
|
703
|
+
// when ROTATING the mirror. This change is BYTE-IDENTICAL at cardinals (untouched incidence
|
|
704
|
+
// tree) and at 45/135/225/315 (90+o ≡ −o mod 180 ⇒ same swap ops), and corrects every
|
|
705
|
+
// arbitrary angle for the tall mirror. NOTE (flagged, not fixed here): a WIDE mirror slab
|
|
706
|
+
// (w > h, e.g. 1D mirrorB) has a HORIZONTAL long face (base 0, not 90); an aspect-blind law
|
|
707
|
+
// cannot honor both bases — a wide mirror at a non-45 angle needs the aspect passed in, or
|
|
708
|
+
// the experiment retuned to a tall slab.
|
|
709
|
+
var alphaScreen = slab.reflectBaseDeg; // (90 + onorm) % 360 — from the ONE SlabFace authority.
|
|
710
|
+
if (Math.abs(mod90 - 45) < EPS) {
|
|
711
|
+
// 45/135/225/315 → exact swap (cos2α = 0 singularity of the 2-op form). For a 3D
|
|
712
|
+
// wall this is z-axis orientation — an xy-plane swap; vz passes through unchanged.
|
|
713
|
+
return _buildSwapReflectOps(HF, alphaScreen, Lit, Ref, Bin);
|
|
714
|
+
}
|
|
715
|
+
// arbitrary α → fixed tilted-plane reflection, exact velocity map (xy-plane; z-axis
|
|
716
|
+
// orientation for a 3D wall — vz untouched).
|
|
717
|
+
return _buildAngledReflectOps(HF, alphaScreen, Lit, Ref, Bin);
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
/**
|
|
721
|
+
* R1-GUARD — the ONE-SIDED FACE-CROSSING GUARD for a TILTED reflect mirror
|
|
722
|
+
* (red-team §2, MEASURED single-reflection optics; the drawn-face rule).
|
|
723
|
+
*
|
|
724
|
+
* ## Why this exists (the tilted-reflect LEAK-THROUGH the AABB trigger caused)
|
|
725
|
+
*
|
|
726
|
+
* The cardinal incidence tree (`_buildIncidenceReflectOps`) is GUARDED by a `wasOutside`
|
|
727
|
+
* prev-coord Cond, so it flips a leaf's velocity exactly ONCE per AABB entry. The tilted
|
|
728
|
+
* swap/angled maps (`_buildSwapReflectOps`/`_buildAngledReflectOps`) had NO such guard: the
|
|
729
|
+
* program trigger was plain AABB `intersects`, so the fixed-plane reflection — an INVOLUTION
|
|
730
|
+
* R(α)∘R(α)=identity — re-fired on EVERY overlap tick. The velocity OSCILLATED while the leaf
|
|
731
|
+
* sat in the padded collision zone; when its corner crossed the wall midline the `'closest'`
|
|
732
|
+
* eject flipped to the FAR face and pushed it THROUGH. The leak scaled with leaf size (bench
|
|
733
|
+
* Dim 2/3) and worsened at 135° / thin slabs (isolation report: 78/3600 at Dim 2, 135°).
|
|
734
|
+
*
|
|
735
|
+
* ## The guard (fires the reflect ONCE per genuine SURFACE crossing)
|
|
736
|
+
*
|
|
737
|
+
* The mirror's reflecting surfaces are the two long FACES of the drawn slab — the lines at
|
|
738
|
+
* signed normal distance ±hw (half the slab thickness) from the LO center `C=(cx,cy)` along
|
|
739
|
+
* the unit face normal `n`, BOTH taken VERBATIM from the ONE SlabFace authority
|
|
740
|
+
* (`resolveSlabFace(...).obb.normal` / `.obb.hw` — the SAME geometry the reflect law + the
|
|
741
|
+
* engine OBB region share; NEVER a second inline cos/sin). The signed side value is
|
|
742
|
+
*
|
|
743
|
+
* s(P) = n · (P − C) // signed normal distance from the slab mid-line
|
|
744
|
+
*
|
|
745
|
+
* The program fires iff (AABB broad-phase) AND the leaf crossed a SURFACE this tick while
|
|
746
|
+
* APPROACHING it:
|
|
747
|
+
* front: s(prev) < −hw AND s(now) > −hw AND v·n > 0
|
|
748
|
+
* back : s(prev) > +hw AND s(now) < +hw AND v·n < 0
|
|
749
|
+
* Firing at the SURFACE (not the mid-line) means a beam reflects the moment it touches the
|
|
750
|
+
* face the user SEES — and, fused with the same-tick face-normal exit eject below, a
|
|
751
|
+
* reflected leaf is placed back OUTSIDE the slab in the SAME tick, so no post-tick snapshot
|
|
752
|
+
* ever renders a reflected particle INSIDE the slab (reflect
|
|
753
|
+
* must reposition outside the collided edge in the same tick — particles seen rendering
|
|
754
|
+
* inside the LO then walking through were the bug). The approach term (v·n sign) is what
|
|
755
|
+
* kills the rattle MEASURED for a naive two-sided crossing test: after the
|
|
756
|
+
* reflection v·n flips and the eject holds the leaf outside its incident surface, so neither
|
|
757
|
+
* jitter re-crossings nor the reflected step satisfy the guard again. Handling BOTH surfaces
|
|
758
|
+
* makes the wall a scene-agnostic two-faced mirror (a generic Wall never knows the beam's
|
|
759
|
+
* approach side); a ±2-stacked jump that clears BOTH surfaces in one tick still satisfies
|
|
760
|
+
* the entry condition (s(now) is past −hw when it is past +hw too) and is caught. n and hw
|
|
761
|
+
* are pure functions of the wall spec, so a live re-aim re-bakes this via the setV2Prop
|
|
762
|
+
* recompile. Zero RNG draws.
|
|
763
|
+
*
|
|
764
|
+
* ## Z-FACES extension (3D tilted slab — opt-in, `withZFaces`)
|
|
765
|
+
*
|
|
766
|
+
* A 3D tilted slab (z-axis orientationDeg = rotation in the xy-plane; the z-slab is
|
|
767
|
+
* UNROTATED) has two more reflecting surfaces: its near/far z-faces — axis-aligned planes
|
|
768
|
+
* at `self.near`/`self.far`, exactly the planes the cardinal incidence-3D tree already
|
|
769
|
+
* bounces (C27). The xy-only guard let a leaf entering THROUGH a z-face (xy already inside
|
|
770
|
+
* the slab footprint, so no xy surface crossing ever happens) march straight through the
|
|
771
|
+
* mirror with zero response — measured pkg-node: 135°/Dim-2 z-entry beam, velocity never
|
|
772
|
+
* changed, 140 in-slab render snapshots, maxZ 198 past a far face at 60 (the 3D leak class).
|
|
773
|
+
* With `withZFaces` the trigger ALSO fires on a genuine z-PLANE crossing while approaching:
|
|
774
|
+
* zFront: prev_z < near AND z >= near AND vz > 0
|
|
775
|
+
* zBack : prev_z > far AND z <= far AND vz < 0
|
|
776
|
+
* (center-coord form, the exact z-lift of front/back above; `near`/`far` are the LIVE
|
|
777
|
+
* engine-recentered slab z-band — the broad-phase superset rect widens only x/y, so these
|
|
778
|
+
* self refs stay the TRUE z-faces). The NOW side is INCLUSIVE (>= via the canonical
|
|
779
|
+
* NOT(<) Cond encoding) because z steps are INTEGER pixels and the z-faces sit on integer
|
|
780
|
+
* planes — a 1px step lands EXACTLY ON the plane (prev=near−1 → now=near), so a strict
|
|
781
|
+
* two-sided test never sees the crossing (measured: the z-entry beam marched through with
|
|
782
|
+
* the strict form). Inclusive-now still fires exactly once: the next tick's prev is ON the
|
|
783
|
+
* plane, failing the strict prev-side test. Omitted/falsy ⇒ byte-identical 2D trigger (SU1).
|
|
784
|
+
* `withZFaces` ALSO relaxes the xy approach terms to inclusive (v·n >= 0 / <= 0) so a
|
|
785
|
+
* zero-xy-momentum leaf wander-crossing the drawn surface is positionally excluded (see
|
|
786
|
+
* the inline comment at the approach terms — no rattle risk, 2D byte-identical).
|
|
787
|
+
*
|
|
788
|
+
* @param {{x:number,y:number}} n unit DRAWN-face normal (SlabFace obb.normal — baked Lit).
|
|
789
|
+
* @param {number} hw TRUE slab half-thickness (SlabFace obb.hw); 0 when the slab width is
|
|
790
|
+
* unknown (degenerates to the mid-line crossing form — still leak-free, weaker visually).
|
|
791
|
+
* @param {boolean} [withZFaces] opt-in z-face crossing branches (3D tilted slab). Default
|
|
792
|
+
* omitted → byte-identical 2D trigger.
|
|
793
|
+
* @returns {ExprAST} the guarded program trigger.
|
|
794
|
+
*/
|
|
795
|
+
function _buildTiltedReflectGuardTrigger(n, hw, HF, HS, Lit, Ref, Bin, Cmp, Cond, Fn, withZFaces) {
|
|
796
|
+
function AND(x, y) { return Cond(x, y, Lit(0)); }
|
|
797
|
+
function OR(x, y) { return Cond(x, Lit(1), y); }
|
|
798
|
+
// s(P) = n·(P − C) for (xProp,yProp) ∈ {(prev_x,prev_y),(x,y)} — C = LO center (cx,cy).
|
|
799
|
+
function S(xp, yp) {
|
|
800
|
+
return Bin('+', Bin('*', Lit(n.x), Bin('-', Ref(HF, xp), Ref(HS, 'cx'))),
|
|
801
|
+
Bin('*', Lit(n.y), Bin('-', Ref(HF, yp), Ref(HS, 'cy'))));
|
|
802
|
+
}
|
|
803
|
+
var vDotN = Bin('+', Bin('*', Lit(n.x), Ref(HF, 'vx')), Bin('*', Lit(n.y), Ref(HF, 'vy')));
|
|
804
|
+
// Approach terms. 2D: strict (v·n > 0 / < 0) — byte-identical. 3D (`withZFaces`): the
|
|
805
|
+
// INCLUSIVE forms (>= / <= via the canonical NOT-encodings) so a ZERO-xy-momentum leaf
|
|
806
|
+
// (a pure-z beam inside the z-band whose TRANSVERSE WAVE wanders its center across the
|
|
807
|
+
// drawn surface — the wave moves the leaf with v·n exactly 0) still fires: the eject
|
|
808
|
+
// holds it outside the slab body (positional exclusion — what a solid does) while the
|
|
809
|
+
// reflect law maps its (0,0) xy velocity to (0,0), a no-op. No rattle risk: there is no
|
|
810
|
+
// xy velocity to oscillate, and leaves WITH momentum keep the exact 2D semantics (the
|
|
811
|
+
// inclusive form only admits v·n == 0). Measured: without this, ~half a pure-z beam's
|
|
812
|
+
// leaves were swept-culled at their z-plane crossing tick (center outside ±hw for one
|
|
813
|
+
// tick), then wander-entered the slab body and rendered inside it.
|
|
814
|
+
var frontApproach = withZFaces
|
|
815
|
+
? Cond(Cmp('<', vDotN, Lit(0)), Lit(0), Lit(1)) // v·n >= 0
|
|
816
|
+
: Cmp('>', vDotN, Lit(0));
|
|
817
|
+
var backApproach = withZFaces
|
|
818
|
+
? Cond(Cmp('>', vDotN, Lit(0)), Lit(0), Lit(1)) // v·n <= 0
|
|
819
|
+
: Cmp('<', vDotN, Lit(0));
|
|
820
|
+
var frontCross = AND(Cmp('<', S('prev_x', 'prev_y'), Lit(-hw)),
|
|
821
|
+
AND(Cmp('>', S('x', 'y'), Lit(-hw)),
|
|
822
|
+
frontApproach));
|
|
823
|
+
var backCross = AND(Cmp('>', S('prev_x', 'prev_y'), Lit(hw)),
|
|
824
|
+
AND(Cmp('<', S('x', 'y'), Lit(hw)),
|
|
825
|
+
backApproach));
|
|
826
|
+
var crossExpr = OR(frontCross, backCross);
|
|
827
|
+
if (withZFaces) {
|
|
828
|
+
// GE/LE via the canonical NOT-encodings (closed vocab has no '<='/'>='):
|
|
829
|
+
// inclusive NOW side — integer z steps land exactly ON the integer z-plane.
|
|
830
|
+
function GE(a, b) { return Cond(Cmp('<', a, b), Lit(0), Lit(1)); }
|
|
831
|
+
function LE(a, b) { return Cond(Cmp('>', a, b), Lit(0), Lit(1)); }
|
|
832
|
+
var zFront = AND(Cmp('<', Ref(HF, 'prev_z'), Ref(HS, 'near')),
|
|
833
|
+
AND(GE(Ref(HF, 'z'), Ref(HS, 'near')),
|
|
834
|
+
Cmp('>', Ref(HF, 'vz'), Lit(0))));
|
|
835
|
+
var zBack = AND(Cmp('>', Ref(HF, 'prev_z'), Ref(HS, 'far')),
|
|
836
|
+
AND(LE(Ref(HF, 'z'), Ref(HS, 'far')),
|
|
837
|
+
Cmp('<', Ref(HF, 'vz'), Lit(0))));
|
|
838
|
+
crossExpr = OR(crossExpr, OR(zFront, zBack));
|
|
839
|
+
}
|
|
840
|
+
return Cond(Fn('intersects', [Lit(HF), Lit(HS)]),
|
|
841
|
+
crossExpr,
|
|
842
|
+
Lit(0));
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* R1-GUARD eject — the PREV-ANCHORED FACE-NORMAL EXIT (the ONE eject story for a tilted
|
|
847
|
+
* reflect; the same-tick-exteriority rule). Places the leaf's WHOLE BODY just
|
|
848
|
+
* outside the slab on its INCIDENT side, in the SAME tick as the velocity flip:
|
|
849
|
+
*
|
|
850
|
+
* P' = P_prev + (T − s(P_prev)) · n, T = −sign(v·n) · (hw + w/2 + 1)
|
|
851
|
+
*
|
|
852
|
+
* i.e. anchor at the pre-move position (already on the incident side of the surface — the
|
|
853
|
+
* guard requires it) and push along the face normal to clearance |s| = hw + w/2 + 1: half
|
|
854
|
+
* slab thickness + half leaf width + 1px. The incident side is −sign(v·n) because the eject
|
|
855
|
+
* ops run BEFORE the reflect ops, while v is still the INCOMING velocity. This supersedes
|
|
856
|
+
* BOTH earlier stories: the plain prev-restore (§2) only guaranteed the leaf was on
|
|
857
|
+
* the incident side of the MID-LINE — a restored leaf could still sit up to hw INSIDE the
|
|
858
|
+
* slab body and render there while walking out (exactly the defect seen); and the AABB
|
|
859
|
+
* 'closest' eject scattered or far-face-ejected (the original leak). Exit clearance holds for
|
|
860
|
+
* the swap (45/135) AND arbitrary-angle paths identically — the ops are angle-generic data.
|
|
861
|
+
*
|
|
862
|
+
* ## Why there is NO op cross-coupling (no scratch register, no division)
|
|
863
|
+
*
|
|
864
|
+
* Both ops read ONLY `prev_x`/`prev_y`/`vx`/`vy` — refs that NEITHER op writes (and the
|
|
865
|
+
* reflect ops, which run after, touch only vx/vy). So op1 (set x) and op2 (set y) evaluate
|
|
866
|
+
* the IDENTICAL displacement Δ = T − s(P_prev) and land exactly at P_prev + Δ·n; s(P') = T
|
|
867
|
+
* by construction. Compare the withdrawn OBB-normal eject, which computed s from
|
|
868
|
+
* the CURRENT x/y and needed a vz-scratch to survive its own eviction of x — a trick that
|
|
869
|
+
* breaks on a 3D wall where vz is real momentum. prev-anchoring dissolves the problem.
|
|
870
|
+
* WASM-only (prev_* scope family), like the cardinal incidence tree. z passes straight
|
|
871
|
+
* through (rotation is xy-plane; exteriority in xy IS exteriority of the z-extruded slab).
|
|
872
|
+
*
|
|
873
|
+
* ## `anchorPrevSide` (3D tilted variant — opt-in)
|
|
874
|
+
*
|
|
875
|
+
* The incident-side sign has two equivalent derivations WHEN the xy guard branch fired:
|
|
876
|
+
* −sign(v·n) (v incoming — the 2D form) ≡ sign(s(P_prev)) (the side the leaf came from —
|
|
877
|
+
* the guard requires |s(prev)| > hw). The 3D program gates its ops per-op on PREV-ONLY
|
|
878
|
+
* class flags (see `_tilted3DGates`), and in the rare corner where the trigger fired via a
|
|
879
|
+
* z-face crossing while prev was ALSO outside an xy surface, v·n's sign is not tied to the
|
|
880
|
+
* xy incident side — so the 3D variant anchors the side on sign(s(P_prev)) instead:
|
|
881
|
+
* prev-only, always the genuine incident side, byte-identical displacement whenever the xy
|
|
882
|
+
* guard branch fired. Omitted/falsy ⇒ the 2D −sign(v·n) form, byte-identical (SU1).
|
|
883
|
+
*
|
|
884
|
+
* @param {{x:number,y:number}} n unit face normal (SlabFace obb.normal — baked Lit).
|
|
885
|
+
* @param {number} hw TRUE slab half-thickness (SlabFace obb.hw; 0 fallback → exits to
|
|
886
|
+
* 1 + w/2 past the mid-line — still outside the reflecting line by a full margin).
|
|
887
|
+
* @param {boolean} [anchorPrevSide] opt-in prev-side incident anchor (3D tilted variant).
|
|
888
|
+
* @returns {Array<object>} [setXOp, setYOp]
|
|
889
|
+
*/
|
|
890
|
+
function _buildFaceExitEjectOps(n, hw, HF, HS, Lit, Ref, Bin, Cmp, Cond, anchorPrevSide) {
|
|
891
|
+
function S(xp, yp) {
|
|
892
|
+
return Bin('+', Bin('*', Lit(n.x), Bin('-', Ref(HF, xp), Ref(HS, 'cx'))),
|
|
893
|
+
Bin('*', Lit(n.y), Bin('-', Ref(HF, yp), Ref(HS, 'cy'))));
|
|
894
|
+
}
|
|
895
|
+
var vDotN = Bin('+', Bin('*', Lit(n.x), Ref(HF, 'vx')), Bin('*', Lit(n.y), Ref(HF, 'vy')));
|
|
896
|
+
// T = ±(hw + w/2 + 1): the clearance on the INCIDENT side. Side sign: −sign(v·n) (2D
|
|
897
|
+
// form, v is incoming here) or sign(s(P_prev)) (prev-anchored 3D form — see docstring).
|
|
898
|
+
var clearance = Bin('+', Lit(hw + 1), Bin('*', Ref(HF, 'w'), Lit(0.5)));
|
|
899
|
+
var side = anchorPrevSide
|
|
900
|
+
? Cond(Cmp('<', S('prev_x', 'prev_y'), Lit(0)), Lit(-1), Lit(1))
|
|
901
|
+
: Cond(Cmp('<', vDotN, Lit(0)), Lit(1), Lit(-1));
|
|
902
|
+
var T = Bin('*', side, clearance);
|
|
903
|
+
// Δ = T − s(P_prev): identical in both ops (prev refs are never written by any op).
|
|
904
|
+
function delta() { return Bin('-', T, S('prev_x', 'prev_y')); }
|
|
905
|
+
return [
|
|
906
|
+
{ set: HF + '.x', to: Bin('+', Ref(HF, 'prev_x'), Bin('*', delta(), Lit(n.x))) },
|
|
907
|
+
{ set: HF + '.y', to: Bin('+', Ref(HF, 'prev_y'), Bin('*', delta(), Lit(n.y))) }
|
|
908
|
+
];
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* R1-3D — PREV-ONLY response-class gates for the 3D tilted-reflect program (the z-face
|
|
913
|
+
* completion of the guarded tilted reflect; measured 3D z-entry leak class, 2026-07-25).
|
|
914
|
+
*
|
|
915
|
+
* ## Why per-op gating is SAFE here (vs a program-wide trigger)
|
|
916
|
+
*
|
|
917
|
+
* The retired G2b per-op Cond wrapping broke because its gate re-read values the program's
|
|
918
|
+
* OWN ops mutate (the eject moved x/y, so the gate re-evaluated false mid-program — the
|
|
919
|
+
* bounce-skip bug). These gates are immune BY CONSTRUCTION: they read ONLY `prev_x/prev_y/
|
|
920
|
+
* prev_z` (never written by any op) and `self.cx/cy/near/far` (LO geometry, never written
|
|
921
|
+
* by a frame op) — so every op in the sequence evaluates the IDENTICAL gate value no matter
|
|
922
|
+
* what earlier ops wrote. This is the same no-cross-coupling discipline the prev-anchored
|
|
923
|
+
* eject already uses.
|
|
924
|
+
*
|
|
925
|
+
* ## The two classes (a tilted 3D slab has TWO kinds of reflecting surface)
|
|
926
|
+
*
|
|
927
|
+
* xprev() — prev was outside an xy DRAWN surface (|s(prev)| > hw): the xy face response
|
|
928
|
+
* runs (face-exit eject + the 2θ reflect law); z passes through.
|
|
929
|
+
* zclass() — NOT xprev AND prev was outside the z-band (prev_z beyond near/far): the
|
|
930
|
+
* z-face response runs (z exit eject + vz flip — the SAME near/far bounce the
|
|
931
|
+
* cardinal incidence-3D tree gives an axis-aligned slab, C27); xy passes
|
|
932
|
+
* through. Given the guard trigger fired, `zclass` ⟺ the z-branch fired
|
|
933
|
+
* (an xy branch requires |s(prev)| > hw = xprev). xy has priority on the
|
|
934
|
+
* corner case (prev outside BOTH — mirror-face-dominant, the drawn face wins).
|
|
935
|
+
*
|
|
936
|
+
* Layer 2 — physics: ZERO new physics (SU1) — this is op-program DATA selecting which of
|
|
937
|
+
* the two proven bounce laws (2θ drawn-face / z-plane flip) answers a given crossing.
|
|
938
|
+
* @returns {{xprev:Function, zclass:Function}} fresh-AST gate factories.
|
|
939
|
+
*/
|
|
940
|
+
function _tilted3DGates(n, hw, HF, HS, Lit, Ref, Bin, Cmp, Cond) {
|
|
941
|
+
function S_prev() {
|
|
942
|
+
return Bin('+', Bin('*', Lit(n.x), Bin('-', Ref(HF, 'prev_x'), Ref(HS, 'cx'))),
|
|
943
|
+
Bin('*', Lit(n.y), Bin('-', Ref(HF, 'prev_y'), Ref(HS, 'cy'))));
|
|
944
|
+
}
|
|
945
|
+
function xprev() { // |s(prev)| > hw (OR via Cond)
|
|
946
|
+
return Cond(Cmp('<', S_prev(), Lit(-hw)), Lit(1), Cmp('>', S_prev(), Lit(hw)));
|
|
947
|
+
}
|
|
948
|
+
function zOutPrev() {
|
|
949
|
+
return Cond(Cmp('<', Ref(HF, 'prev_z'), Ref(HS, 'near')), Lit(1),
|
|
950
|
+
Cmp('>', Ref(HF, 'prev_z'), Ref(HS, 'far')));
|
|
951
|
+
}
|
|
952
|
+
function zclass() { // NOT xprev AND zOutPrev
|
|
953
|
+
return Cond(xprev(), Lit(0), zOutPrev());
|
|
954
|
+
}
|
|
955
|
+
return { xprev: xprev, zclass: zclass };
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* R1-3D — the z-face EXIT eject for a 3D tilted slab: place the leaf's whole body just
|
|
960
|
+
* outside the z-face it entered through, in the SAME tick as the vz flip (the
|
|
961
|
+
* same-tick-exteriority rule, z-lifted):
|
|
962
|
+
* z' = cz + sign(prev_z − cz) · (hz + w/2 + 1)
|
|
963
|
+
* with cz/hz derived LIVE from `self.near`/`self.far` (the engine-recentered z-band —
|
|
964
|
+
* the broad-phase superset rect widens only x/y, so near/far are the TRUE z-faces) and
|
|
965
|
+
* w/2 the frame's z half-extent (cube convention, same as closest3d). prev-anchored side
|
|
966
|
+
* (prev_z is on the incident side by the guard's z branch), no scratch, no division.
|
|
967
|
+
* @returns {object} the single set-z op (ungated — caller gates on `zclass`).
|
|
968
|
+
*/
|
|
969
|
+
function _buildZFaceExitEjectOp(HF, HS, Lit, Ref, Bin, Cmp, Cond) {
|
|
970
|
+
function cz() { return Bin('*', Bin('+', Ref(HS, 'near'), Ref(HS, 'far')), Lit(0.5)); }
|
|
971
|
+
function hz() { return Bin('*', Bin('-', Ref(HS, 'far'), Ref(HS, 'near')), Lit(0.5)); }
|
|
972
|
+
var clearance = Bin('+', Bin('+', hz(), Bin('*', Ref(HF, 'w'), Lit(0.5))), Lit(1));
|
|
973
|
+
var side = Cond(Cmp('<', Ref(HF, 'prev_z'), cz()), Lit(-1), Lit(1));
|
|
974
|
+
return { set: HF + '.z', to: Bin('+', cz(), Bin('*', side, clearance)) };
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* R1-3D — gate a list of ops on a prev-only class flag: each op's `to` becomes
|
|
979
|
+
* Cond(gate, to, keep-current-value). Gate factories MUST read only never-op-written refs
|
|
980
|
+
* (see `_tilted3DGates` — the G2b immunity argument). Only used for simple scalar props
|
|
981
|
+
* (x/y/z/vx/vy/vz — `op.set` is HF + '.<prop>').
|
|
982
|
+
* @returns {Array<object>} the gated ops (fresh objects; inputs not mutated).
|
|
983
|
+
*/
|
|
984
|
+
function _gateOps(ops, gateFactory, HF, Ref, Cond) {
|
|
985
|
+
return ops.map(function (op) {
|
|
986
|
+
var prop = op.set.slice((HF + '.').length);
|
|
987
|
+
return { set: op.set, to: Cond(gateFactory(), op.to, Ref(HF, prop)) };
|
|
988
|
+
});
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// ── PITCHED (two-axis) mirror builders ───────────────────────────────────────────
|
|
992
|
+
//
|
|
993
|
+
// A PITCHED plate (pitchDeg ≠ 0 about its own long axis) reflects by the TRUE 3-space law
|
|
994
|
+
// R = I − 2nnᵀ about the plate normal n (SlabFace plate3.n — the ONE authority). This
|
|
995
|
+
// supersedes the vz-passes-through z-axis contract FOR PITCHED PLATES ONLY: pitch 0 keeps
|
|
996
|
+
// every existing program byte-identical (the pitched branch is opt-in). A pitched plate has
|
|
997
|
+
// NO engine ObbRegion (z-theta-only contract): the broad phase is the plate's 3D AABB rect
|
|
998
|
+
// and the guard trigger below IS the narrow phase (face-plane crossing + extent).
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* R2AX — the 3-space reflection ops about unit normal n: v' = v − 2(v·n)n, host-baked.
|
|
1002
|
+
*
|
|
1003
|
+
* ## Sequential-write realization (no scratch, no runtime division)
|
|
1004
|
+
* Each op writes ONE velocity component, so later ops see earlier writes. Let D = n·v_old.
|
|
1005
|
+
* After writing component a: n·v_now = D(1−2na²) ⇒ D = k1·(n·v_now), k1 = 1/(1−2na²)
|
|
1006
|
+
* (k1 HOST-baked — the same baked-reciprocal pattern as `_buildAngledReflectOps`). After b:
|
|
1007
|
+
* D = k2·(n·v_now), k2 = 1/(1−2na²−2nb²) = −1/(1−2nc²). So:
|
|
1008
|
+
* op-a: va' = va − 2na·(n·v)
|
|
1009
|
+
* op-b: vb' = vb − (2nb·k1)·(n·v) — n·v read AFTER op-a
|
|
1010
|
+
* op-c: vc' = vc − (2nc·k2)·(n·v) — n·v read AFTER op-a,b
|
|
1011
|
+
* Pivot order (a,b,c) is chosen HOST-side: a = the smallest-|n| component (na² ≤ 1/3 < 1/2
|
|
1012
|
+
* always), c = the remaining component with c² farthest from 1/2 — valid unless TWO
|
|
1013
|
+
* components have square exactly 1/2 (⇒ the third is 0): the DIAGONAL class, realized
|
|
1014
|
+
* exactly by the proven add/sub SWAP pattern instead (v_p' = −σpσq·v_q, v_q' = −σpσq·v_p,
|
|
1015
|
+
* σ = component signs). Zero RNG; exact f64; deterministic across engines (same op order).
|
|
1016
|
+
*
|
|
1017
|
+
* Layer 2 — physics: standard mirror optics in 3-space — the velocity component along the
|
|
1018
|
+
* plate normal inverts, the tangential components are untouched. At pitch 0 this equals the
|
|
1019
|
+
* 2D law with vz through (not used there — routing keeps pitch-0 byte-identical).
|
|
1020
|
+
* @param {{x:number,y:number,z:number}} n unit plate normal (SlabFace plate3.n).
|
|
1021
|
+
* @returns {Array<object>} 3 ops (generic) or 3-5 ops (diagonal swap class).
|
|
1022
|
+
*/
|
|
1023
|
+
function _buildPitchedReflectOps(n, HF, Lit, Ref, Bin) {
|
|
1024
|
+
var comps = [{ k: 'vx', c: n.x }, { k: 'vy', c: n.y }, { k: 'vz', c: n.z }];
|
|
1025
|
+
var EPS = 1e-9;
|
|
1026
|
+
var halfIdx = [];
|
|
1027
|
+
for (var i = 0; i < 3; i++) { if (Math.abs(comps[i].c * comps[i].c - 0.5) < EPS) { halfIdx.push(i); } }
|
|
1028
|
+
function nDotV() {
|
|
1029
|
+
return Bin('+', Bin('+', Bin('*', Lit(n.x), Ref(HF, 'vx')), Bin('*', Lit(n.y), Ref(HF, 'vy'))),
|
|
1030
|
+
Bin('*', Lit(n.z), Ref(HF, 'vz')));
|
|
1031
|
+
}
|
|
1032
|
+
if (halfIdx.length === 2) {
|
|
1033
|
+
// DIAGONAL class: n = (±√½, ±√½) on axes p,q; third 0. Exact swap (+negate when
|
|
1034
|
+
// σpσq > 0) via the proven 3-op add/sub pattern (never `p=q; q=p`).
|
|
1035
|
+
var pk = comps[halfIdx[0]].k, qk = comps[halfIdx[1]].k;
|
|
1036
|
+
var s = Math.sign(comps[halfIdx[0]].c) * Math.sign(comps[halfIdx[1]].c);
|
|
1037
|
+
var ops = [
|
|
1038
|
+
{ set: HF + '.' + pk, to: Bin('+', Ref(HF, pk), Ref(HF, qk)) },
|
|
1039
|
+
{ set: HF + '.' + qk, to: Bin('-', Ref(HF, pk), Ref(HF, qk)) },
|
|
1040
|
+
{ set: HF + '.' + pk, to: Bin('-', Ref(HF, pk), Ref(HF, qk)) }
|
|
1041
|
+
];
|
|
1042
|
+
if (s > 0) {
|
|
1043
|
+
ops.push({ set: HF + '.' + pk, to: Bin('-', Lit(0), Ref(HF, pk)) });
|
|
1044
|
+
ops.push({ set: HF + '.' + qk, to: Bin('-', Lit(0), Ref(HF, qk)) });
|
|
1045
|
+
}
|
|
1046
|
+
return ops;
|
|
1047
|
+
}
|
|
1048
|
+
// Generic elimination order: a = min |n_c|; c = remaining with |c²−1/2| max; b = other.
|
|
1049
|
+
var order = [0, 1, 2].sort(function (x, y) { return Math.abs(comps[x].c) - Math.abs(comps[y].c); });
|
|
1050
|
+
var aI = order[0];
|
|
1051
|
+
var rest = [order[1], order[2]].sort(function (x, y) {
|
|
1052
|
+
return Math.abs(comps[y].c * comps[y].c - 0.5) - Math.abs(comps[x].c * comps[x].c - 0.5);
|
|
1053
|
+
});
|
|
1054
|
+
var cI = rest[0], bI = rest[1];
|
|
1055
|
+
var na = comps[aI].c, nb = comps[bI].c, nc = comps[cI].c;
|
|
1056
|
+
var k1 = 1 / (1 - 2 * na * na);
|
|
1057
|
+
var k2 = 1 / (1 - 2 * na * na - 2 * nb * nb);
|
|
1058
|
+
return [
|
|
1059
|
+
{ set: HF + '.' + comps[aI].k, to: Bin('-', Ref(HF, comps[aI].k), Bin('*', Lit(2 * na), nDotV())) },
|
|
1060
|
+
{ set: HF + '.' + comps[bI].k, to: Bin('-', Ref(HF, comps[bI].k), Bin('*', Lit(2 * nb * k1), nDotV())) },
|
|
1061
|
+
{ set: HF + '.' + comps[cI].k, to: Bin('-', Ref(HF, comps[cI].k), Bin('*', Lit(2 * nc * k2), nDotV())) }
|
|
1062
|
+
];
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* R2AX — the 3-space one-sided guard trigger for a PITCHED plate: the n-dot form of the
|
|
1067
|
+
* measured 2D cure, with the plate EXTENT as the narrow phase (a pitched plate has no
|
|
1068
|
+
* engine region — the AABB broad phase is a superset; this trigger culls it to the plate).
|
|
1069
|
+
* s(P) = n·(P − C), C = (self.cx, self.cy, (self.near+self.far)/2)
|
|
1070
|
+
* front: s(prev) < −hw AND s(now) >= −hw AND v·n >= 0
|
|
1071
|
+
* back : s(prev) > hw AND s(now) <= hw AND v·n <= 0
|
|
1072
|
+
* extent(now): |u·(P−C)| < hh AND |v·(P−C)| < hz
|
|
1073
|
+
* Inclusive now-side + inclusive approach per the 3D lattice/zero-momentum findings (the
|
|
1074
|
+
* pitch=±90 face is z-lattice-aligned; a zero-normal-momentum wander-cross is positionally
|
|
1075
|
+
* excluded). All refs are prev_*, self props, and current pos — the trigger evaluates ONCE
|
|
1076
|
+
* before any op runs, so current-pos reads are safe here.
|
|
1077
|
+
* @returns {ExprAST}
|
|
1078
|
+
*/
|
|
1079
|
+
function _buildPitchedGuardTrigger(plate, HF, HS, Lit, Ref, Bin, Cmp, Cond, Fn) {
|
|
1080
|
+
var n = plate.n, u = plate.u, v = plate.v;
|
|
1081
|
+
var hw = plate.hw, hh = plate.hh, hz = plate.hz;
|
|
1082
|
+
function AND(x, y) { return Cond(x, y, Lit(0)); }
|
|
1083
|
+
function OR(x, y) { return Cond(x, Lit(1), y); }
|
|
1084
|
+
function GE(a, b) { return Cond(Cmp('<', a, b), Lit(0), Lit(1)); }
|
|
1085
|
+
function LE(a, b) { return Cond(Cmp('>', a, b), Lit(0), Lit(1)); }
|
|
1086
|
+
function Abs(e) { return Fn('max', [e, Bin('-', Lit(0), e)]); }
|
|
1087
|
+
function CZ() { return Bin('*', Bin('+', Ref(HS, 'near'), Ref(HS, 'far')), Lit(0.5)); }
|
|
1088
|
+
function DOT(a, xp, yp, zp) {
|
|
1089
|
+
return Bin('+', Bin('+',
|
|
1090
|
+
Bin('*', Lit(a.x), Bin('-', Ref(HF, xp), Ref(HS, 'cx'))),
|
|
1091
|
+
Bin('*', Lit(a.y), Bin('-', Ref(HF, yp), Ref(HS, 'cy')))),
|
|
1092
|
+
Bin('*', Lit(a.z), Bin('-', Ref(HF, zp), CZ())));
|
|
1093
|
+
}
|
|
1094
|
+
var vDotN = Bin('+', Bin('+', Bin('*', Lit(n.x), Ref(HF, 'vx')), Bin('*', Lit(n.y), Ref(HF, 'vy'))),
|
|
1095
|
+
Bin('*', Lit(n.z), Ref(HF, 'vz')));
|
|
1096
|
+
var front = AND(Cmp('<', DOT(n, 'prev_x', 'prev_y', 'prev_z'), Lit(-hw)),
|
|
1097
|
+
AND(GE(DOT(n, 'x', 'y', 'z'), Lit(-hw)),
|
|
1098
|
+
GE(vDotN, Lit(0))));
|
|
1099
|
+
var back = AND(Cmp('>', DOT(n, 'prev_x', 'prev_y', 'prev_z'), Lit(hw)),
|
|
1100
|
+
AND(LE(DOT(n, 'x', 'y', 'z'), Lit(hw)),
|
|
1101
|
+
LE(vDotN, Lit(0))));
|
|
1102
|
+
var extent = AND(Cmp('<', Abs(DOT(u, 'x', 'y', 'z')), Lit(hh)),
|
|
1103
|
+
Cmp('<', Abs(DOT(v, 'x', 'y', 'z')), Lit(hz)));
|
|
1104
|
+
return Cond(Fn('intersects', [Lit(HF), Lit(HS)]),
|
|
1105
|
+
AND(extent, OR(front, back)),
|
|
1106
|
+
Lit(0));
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* R2AX — the prev-anchored 3-space face exit for a PITCHED plate (same-tick exteriority):
|
|
1111
|
+
* P' = P_prev + (T − s(P_prev))·n, T = sign(s(P_prev))·(hw + w/2 + 1)
|
|
1112
|
+
* The exact n-dot lift of `_buildFaceExitEjectOps` with the prev-side anchor. All three ops
|
|
1113
|
+
* read ONLY prev_* refs, self props, and w — refs no op writes — so they compute the same displacement
|
|
1114
|
+
* (no scratch, no division; the established discipline). Ungated: the pitched program has
|
|
1115
|
+
* ONE response class (the plate normal IS the reflection axis — no separate z-face).
|
|
1116
|
+
* @returns {Array<object>} [setXOp, setYOp, setZOp]
|
|
1117
|
+
*/
|
|
1118
|
+
function _buildPitchedExitEjectOps(plate, HF, HS, Lit, Ref, Bin, Cmp, Cond) {
|
|
1119
|
+
var n = plate.n, hw = plate.hw;
|
|
1120
|
+
function CZ() { return Bin('*', Bin('+', Ref(HS, 'near'), Ref(HS, 'far')), Lit(0.5)); }
|
|
1121
|
+
function SPrev() {
|
|
1122
|
+
return Bin('+', Bin('+',
|
|
1123
|
+
Bin('*', Lit(n.x), Bin('-', Ref(HF, 'prev_x'), Ref(HS, 'cx'))),
|
|
1124
|
+
Bin('*', Lit(n.y), Bin('-', Ref(HF, 'prev_y'), Ref(HS, 'cy')))),
|
|
1125
|
+
Bin('*', Lit(n.z), Bin('-', Ref(HF, 'prev_z'), CZ())));
|
|
1126
|
+
}
|
|
1127
|
+
var clearance = Bin('+', Lit(hw + 1), Bin('*', Ref(HF, 'w'), Lit(0.5)));
|
|
1128
|
+
function T() { return Bin('*', Cond(Cmp('<', SPrev(), Lit(0)), Lit(-1), Lit(1)), clearance); }
|
|
1129
|
+
function delta() { return Bin('-', T(), SPrev()); }
|
|
1130
|
+
return [
|
|
1131
|
+
{ set: HF + '.x', to: Bin('+', Ref(HF, 'prev_x'), Bin('*', delta(), Lit(n.x))) },
|
|
1132
|
+
{ set: HF + '.y', to: Bin('+', Ref(HF, 'prev_y'), Bin('*', delta(), Lit(n.y))) },
|
|
1133
|
+
{ set: HF + '.z', to: Bin('+', Ref(HF, 'prev_z'), Bin('*', delta(), Lit(n.z))) }
|
|
1134
|
+
];
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* B3 — omni direction-only Random: redraw the exit direction
|
|
1139
|
+
* uniformly over the square, DIRECTION ONLY, NO decoherence. This is also the
|
|
1140
|
+
* round-1 half-plane fix (the legacy RANDOM_XY drew vx ∈ [0,1) = forward hemisphere).
|
|
1141
|
+
* Uniform-over-square (not angle-uniform — ops cannot loop ⇒ no in-tick rejection);
|
|
1142
|
+
* that is the pure-data reality (angle-uniform would need a host tick-fn). 2 RNG draws.
|
|
1143
|
+
* @returns {Array<object>} [vxOp, vyOp]
|
|
1144
|
+
*/
|
|
1145
|
+
function _buildOmniRandomOps(HF, Lit, Bin, Fn) {
|
|
1146
|
+
return [
|
|
1147
|
+
{ set: HF + '.vx', to: Bin('-', Bin('*', Fn('random', []), Lit(2)), Lit(1)) },
|
|
1148
|
+
{ set: HF + '.vy', to: Bin('-', Bin('*', Fn('random', []), Lit(2)), Lit(1)) }
|
|
1149
|
+
];
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* B3 — angle-CONSTRAINED Random: redraw the exit direction over a
|
|
1154
|
+
* cone bounded by two endpoint direction vectors A=(ax,ay), B=(bx,by), computed
|
|
1155
|
+
* HOST-SIDE from the wall's INITIAL orientation + [minDeg,maxDeg] and BAKED as Lits
|
|
1156
|
+
* (immune to later spin by construction — a re-aim must recompile). Uses the
|
|
1157
|
+
* single-draw scratch-prop trick (proven P3, 60/60 exact on the consistency law):
|
|
1158
|
+
* every Fn('random') is a fresh draw, so store ONE t in a scratch prop (frame.vx),
|
|
1159
|
+
* read it in BOTH lerps. op2 (vy) reads t BEFORE op3 (vx) overwrites it. 1 RNG draw.
|
|
1160
|
+
* vy = ay + (by-ay)*t ; vx = ax + (bx-ax)*t
|
|
1161
|
+
* Chord-lerp is t-uniform, not angle-uniform (bias grows with cone width — negligible
|
|
1162
|
+
* for narrow "laser" cones); constrain sugar to <180° (near-antipodal endpoints pass
|
|
1163
|
+
* near the zero vector). @returns {Array<object>} [storeT, vyOp, vxOp]
|
|
1164
|
+
*/
|
|
1165
|
+
function _buildConstrainedRandomOps(HF, ax, ay, bx, by, Lit, Ref, Bin, Fn) {
|
|
1166
|
+
return [
|
|
1167
|
+
// scratch: store t = random() in frame.vx (overwritten by op3 below).
|
|
1168
|
+
{ set: HF + '.vx', to: Fn('random', []) },
|
|
1169
|
+
// vy = ay + (by-ay)*t (reads t from vx BEFORE it is overwritten)
|
|
1170
|
+
{ set: HF + '.vy', to: Bin('+', Lit(ay), Bin('*', Bin('-', Lit(by), Lit(ay)), Ref(HF, 'vx'))) },
|
|
1171
|
+
// vx = ax + (bx-ax)*t (overwrites the scratch t with the real vx LAST)
|
|
1172
|
+
{ set: HF + '.vx', to: Bin('+', Lit(ax), Bin('*', Bin('-', Lit(bx), Lit(ax)), Ref(HF, 'vx'))) }
|
|
1173
|
+
];
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* B3 — decoherence as an INDEPENDENT toggle: the phase reset
|
|
1178
|
+
* (`local_time=0`, offset-decoherence) + charge redraw (sign-decoherence, bench Q1)
|
|
1179
|
+
* that the legacy RANDOM_XY bundled in — here SEPARABLE, stackable onto any behavior.
|
|
1180
|
+
* Proven separable (P1/P2). 1 RNG draw (charge). @returns {Array<object>}
|
|
1181
|
+
*/
|
|
1182
|
+
function _buildDecoherenceOps(HF, Lit, Cmp, Cond, Fn) {
|
|
1183
|
+
return [
|
|
1184
|
+
{ set: HF + '.local_time', to: Lit(0) },
|
|
1185
|
+
{ set: HF + '.charge', to: Cond(Cmp('<', Fn('random', []), Lit(0.5)), Lit(0), Lit(1)) }
|
|
1186
|
+
];
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// ── ABSORB v2 tally — WITHDRAWN (round-3) ────────────────────────────────────────
|
|
1190
|
+
// The v1 `count`-as-virtual-mass tally op was a duplicative parallel LO-prop surface,
|
|
1191
|
+
// which the mass-surface rule rejects. ABSORB v2 now accumulates into the LO's REAL
|
|
1192
|
+
// backing-frame mass via the existing frame accessors (the "LO extends Frame" composition;
|
|
1193
|
+
// under MANDATORY BACKING the LO's backing frame is created at registration and READ via
|
|
1194
|
+
// getLargeObjectFrameId) — see `absorbRadiation.js`. The compileWallV2 absorb branch
|
|
1195
|
+
// emits ONLY the kill op (+collect_collided); the host tick-fn does the real-mass tally +
|
|
1196
|
+
// radiation. No op-writable LO mass ledger anywhere.
|
|
1197
|
+
|
|
1198
|
+
// ── Main compiler ──────────────────────────────────────────────────────────────
|
|
1199
|
+
|
|
1200
|
+
/**
|
|
1201
|
+
* Compile a CollisionActions array (and optional collisionType) to an op-program
|
|
1202
|
+
* and a reduced actions list.
|
|
1203
|
+
*
|
|
1204
|
+
* G2: collisionType is now COMPILED, not informational. When non-NONE, velocity
|
|
1205
|
+
* ops are appended LAST to the ops array (after all action ops) to preserve the
|
|
1206
|
+
* global RNG draw order:
|
|
1207
|
+
* - action ops (ABSORB, COUNT, MARK, BLOCK, BOUNCE) draw 0 RNG values each
|
|
1208
|
+
* - RANDOM_X draws 1, RANDOM_Y draws 2, RANDOM_XY draws 3 (matching the
|
|
1209
|
+
* native dispatch_collision_type draw order: velocity first, then reset() charge)
|
|
1210
|
+
* - MOMENTUM_BASED and WALL draw 0 RNG values
|
|
1211
|
+
*
|
|
1212
|
+
* ## RNG discipline (from the JS reference's native dispatch, MUST MATCH):
|
|
1213
|
+
*
|
|
1214
|
+
* RANDOM_X: 1 draw — vx = _rng()*2-1, vy = 1 (no charge draw)
|
|
1215
|
+
* RANDOM_Y: 2 draws — vy = _rng()*2-1, then reset() draws _rng() for charge
|
|
1216
|
+
* RANDOM_XY: 3 draws — vx = _rng(), vy = _rng()*2-1, then reset() draws _rng()
|
|
1217
|
+
* MOMENTUM_BASED: 0 draws — restores pre-move position via prev_x/prev_y
|
|
1218
|
+
* WALL: 0 draws — directional eject (same formulas as BLOCK)
|
|
1219
|
+
* BOUNCE: 0 draws — handled by BOUNCE action above; collisionType=BOUNCE
|
|
1220
|
+
* is a no-op here (action already compiled)
|
|
1221
|
+
* NONE: 0 draws — nothing emitted
|
|
1222
|
+
*
|
|
1223
|
+
* ## charge draw (reset())
|
|
1224
|
+
* `_rng() < 0.5 ? Charge.POSITIVE : Charge.NEGATIVE`
|
|
1225
|
+
* AST: Cond(Cmp('<', Fn('random',[]), Lit(0.5)), Lit(0), Lit(1))
|
|
1226
|
+
* (0 = POSITIVE, 1 = NEGATIVE in both JS and Rust)
|
|
1227
|
+
*
|
|
1228
|
+
* ## Sugar migration (G2):
|
|
1229
|
+
* Sugar objects (SlitScreen, Box, CircuitGate, LogicGate) pass their
|
|
1230
|
+
* CollisionType to this function AND pass CollisionTypes.NONE to the LO spec,
|
|
1231
|
+
* so the engine receives no native collision_type and all velocity comes from
|
|
1232
|
+
* the compiled op-program.
|
|
1233
|
+
*
|
|
1234
|
+
* @param {string[]} actions - CollisionActions array (SCREAMING_SNAKE).
|
|
1235
|
+
* @param {string} loName - LO name (used for MARK/DETECT detector_name and velocity-flip direction).
|
|
1236
|
+
* @param {string} reposition - Eject direction: 'left'|'right'|'top'|'bottom'|'closest'.
|
|
1237
|
+
* @param {string} [collisionType] - G2: CollisionType to compile. When non-NONE, velocity
|
|
1238
|
+
* ops are appended last. Pass CollisionTypes.NONE when
|
|
1239
|
+
* collisionType is already handled by an action (e.g. BOUNCE).
|
|
1240
|
+
*
|
|
1241
|
+
* @returns {{ program: {on:'collision', ops:Array, trigger:Object}|null }}
|
|
1242
|
+
* program — the compiled op-program to attach to the LO (null when nothing compiled).
|
|
1243
|
+
*
|
|
1244
|
+
* @throws on ANY action this compiler does not lower to an op. There is nowhere else for an
|
|
1245
|
+
* action to go: the engine has no action dispatch and `LargeObjectJsSpec` has no `actions`
|
|
1246
|
+
* field, so an uncompiled action is silently discarded at the wire. The error names the
|
|
1247
|
+
* action and its V2 replacement.
|
|
1248
|
+
*/
|
|
1249
|
+
/**
|
|
1250
|
+
* FAIL LOUD for an action this compiler cannot lower to an op.
|
|
1251
|
+
*
|
|
1252
|
+
* There is no fallback path to leave it on: the engine has no action dispatch and the
|
|
1253
|
+
* registration spec has no `actions` field, so an uncompiled action is dropped silently at
|
|
1254
|
+
* the WASM boundary and the caller gets a wall that does nothing, with no error anywhere.
|
|
1255
|
+
*
|
|
1256
|
+
* @param {string} action - the offending action, as the caller spelled it.
|
|
1257
|
+
* @param {string} why - the reason AND the replacement to reach for.
|
|
1258
|
+
*/
|
|
1259
|
+
function _uncompilable(action, why) {
|
|
1260
|
+
throw new Error('[actionCompiler] cannot compile CollisionAction ' +
|
|
1261
|
+
JSON.stringify(action) + ': ' + why + '. Nothing is left uncompiled — the engine has ' +
|
|
1262
|
+
'no action dispatch, so an op-program is the only way a behaviour can run.');
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function compileActions(actions, loName, reposition, collisionType) {
|
|
1266
|
+
var ast = AstSchema;
|
|
1267
|
+
var Lit = ast.Lit;
|
|
1268
|
+
var Ref = ast.Ref;
|
|
1269
|
+
var Bin = ast.Bin;
|
|
1270
|
+
var Cmp = ast.Cmp;
|
|
1271
|
+
var Cond = ast.Cond;
|
|
1272
|
+
var Fn = ast.Fn;
|
|
1273
|
+
// NOTE: no local StrLit binding — every string write in this compiler is a TAG, and
|
|
1274
|
+
// the tag-op shape is owned by tagger.js (Tagger.tagOps). Re-binding StrLit here
|
|
1275
|
+
// would invite a fourth hand-spelled `0.data.<key>` op.
|
|
1276
|
+
var HANDLE_FRAME = ast.HANDLE_FRAME; // 0
|
|
1277
|
+
var HANDLE_SELF = ast.HANDLE_SELF; // 1
|
|
1278
|
+
|
|
1279
|
+
var ops = [];
|
|
1280
|
+
// G2b: track whether WALL velocity is being compiled (needs past-edge trigger).
|
|
1281
|
+
var isWall = false;
|
|
1282
|
+
|
|
1283
|
+
// Phase 1 — compile actions (0-draw each; order matches native dispatch_collision_actions).
|
|
1284
|
+
var actionsList = actions || [];
|
|
1285
|
+
for (var i = 0; i < actionsList.length; i++) {
|
|
1286
|
+
var action = actionsList[i];
|
|
1287
|
+
|
|
1288
|
+
if (action === 'ABSORB') {
|
|
1289
|
+
// ABSORB → set 0.is_alive = 0 (kills the colliding frame).
|
|
1290
|
+
// The engine's native ABSORB calls p.kill() which sets is_alive=false.
|
|
1291
|
+
// Op-program: set frame.is_alive = 0 (0 → false via !!value in setFrameProperty).
|
|
1292
|
+
ops.push({ set: HANDLE_FRAME + '.is_alive', to: Lit(0) });
|
|
1293
|
+
|
|
1294
|
+
} else if (action === 'COUNT') {
|
|
1295
|
+
// COUNT → set 1.count = 1.count + 1 (LO hit counter, same as OvenBox counter).
|
|
1296
|
+
// Mirrors the OvenBox oven-counter program (§2.1, slice 3).
|
|
1297
|
+
ops.push({ set: HANDLE_SELF + '.count',
|
|
1298
|
+
to: Bin('+', Ref(HANDLE_SELF, 'count'), Lit(1)) });
|
|
1299
|
+
|
|
1300
|
+
} else if (action === 'MARK' || action === 'DETECT') {
|
|
1301
|
+
// MARK/DETECT → a Tagger stamping the LO name under the canonical provenance
|
|
1302
|
+
// key: `set 0.data.detector_name = StrLit(loName)`. The op SHAPE is owned by
|
|
1303
|
+
// tagger.js (the MARK part) and FERRIED here — this compiler no longer spells
|
|
1304
|
+
// the '0.data.' path or the key name. The op-apply layer (_runOpProgram /
|
|
1305
|
+
// run_op_program_with_frame) intercepts StrLit before calling eval_expr and
|
|
1306
|
+
// routes it to the string_map write path; DetectorWall reads it back through
|
|
1307
|
+
// the SAME part (Tagger.readTag). No in-engine string comparison needed.
|
|
1308
|
+
var markName = loName || '';
|
|
1309
|
+
ops = ops.concat(Tagger.tagOps(markName)); // bare string ⇒ {detector_name: markName}
|
|
1310
|
+
|
|
1311
|
+
} else if (action === 'BLOCK') {
|
|
1312
|
+
// BLOCK → eject to nearest face (supports 'closest' via Cond-tree since StrLit landed).
|
|
1313
|
+
// Native: if pastWall → _eject(force=true); else → rect.eject(overlap).
|
|
1314
|
+
// Op-program: equivalent to _eject (force=true path, §10.3 formula).
|
|
1315
|
+
// 'closest': two Cond set ops (frame.left + frame.top) pick nearest face at runtime.
|
|
1316
|
+
if (!reposition) { _uncompilable(action, 'it needs an eject face: pass a reposition ' +
|
|
1317
|
+
'of left|right|top|bottom|near|far|closest, or build the wall with V2 ' +
|
|
1318
|
+
"compileWallV2({behavior:'block', reposition})"); }
|
|
1319
|
+
var blockEjectOps = _buildEjectOps(reposition, HANDLE_FRAME, HANDLE_SELF, Lit, Ref, Bin, Cmp, Cond, Fn);
|
|
1320
|
+
if (!blockEjectOps) { _uncompilable(action, 'reposition "' + reposition + '" is not an ' +
|
|
1321
|
+
'eject face (left|right|top|bottom|near|far|closest|closest3d)'); }
|
|
1322
|
+
for (var bi = 0; bi < blockEjectOps.length; bi++) ops.push(blockEjectOps[bi]);
|
|
1323
|
+
|
|
1324
|
+
} else if (action === 'BOUNCE') {
|
|
1325
|
+
// BOUNCE action → eject + velocity flip (named dir OR 'closest').
|
|
1326
|
+
// Same as OvenBox L4 (EP27-EP28). 'closest' Cond-tree now supported.
|
|
1327
|
+
if (!reposition) { _uncompilable(action, 'it needs an eject face: pass a reposition ' +
|
|
1328
|
+
'of left|right|top|bottom|near|far|closest, or build the wall with V2 ' +
|
|
1329
|
+
"compileWallV2({behavior:'bounce', bounceDirection:'reflect', reposition})"); }
|
|
1330
|
+
var bounceEjectOps = _buildEjectOps(reposition, HANDLE_FRAME, HANDLE_SELF, Lit, Ref, Bin, Cmp, Cond, Fn);
|
|
1331
|
+
var flipOp2 = _buildVelocityFlipOp(loName || '', HANDLE_FRAME, Lit, Ref, Bin);
|
|
1332
|
+
if (!bounceEjectOps && !flipOp2) { _uncompilable(action, 'neither reposition "' +
|
|
1333
|
+
reposition + '" nor the LO name "' + (loName || '') + '" names a face to bounce off'); }
|
|
1334
|
+
if (bounceEjectOps) {
|
|
1335
|
+
for (var bj = 0; bj < bounceEjectOps.length; bj++) ops.push(bounceEjectOps[bj]);
|
|
1336
|
+
}
|
|
1337
|
+
if (flipOp2) ops.push(flipOp2);
|
|
1338
|
+
|
|
1339
|
+
} else if (action === 'LOG') {
|
|
1340
|
+
_uncompilable(action, 'there is no logging op and no native dispatch to fall back ' +
|
|
1341
|
+
'to; read the collided frames from TickData.objects[handle].frames instead ' +
|
|
1342
|
+
"(compileWallV2({behavior:'nothing', hooks:['count']}) collects them)");
|
|
1343
|
+
|
|
1344
|
+
} else if (action === 'RECOLOR') {
|
|
1345
|
+
_uncompilable(action, 'the engine owns no colour; stamp render.color as a tag ' +
|
|
1346
|
+
"instead — compileWallV2({..., hooks:['recolor'], color:[r,g,b]})");
|
|
1347
|
+
|
|
1348
|
+
} else if (action === 'KILL') {
|
|
1349
|
+
// KILL: was a Rust-only divergence, deleted from canonical action set.
|
|
1350
|
+
// Collapse to ABSORB's op (kill the frame) for backward compat.
|
|
1351
|
+
ops.push({ set: HANDLE_FRAME + '.is_alive', to: Lit(0) });
|
|
1352
|
+
|
|
1353
|
+
} else {
|
|
1354
|
+
_uncompilable(action, 'it is not a CollisionAction this compiler knows ' +
|
|
1355
|
+
'(ABSORB|COUNT|MARK|DETECT|BLOCK|BOUNCE|KILL) — check the spelling, or use ' +
|
|
1356
|
+
'the V2 wall vocabulary via compileWallV2({behavior, bounceDirection, hooks})');
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
// Phase 2 — G2: compile velocity ops from collisionType (APPENDED LAST to preserve
|
|
1361
|
+
// global RNG draw order: all 0-draw ops first, then RNG draws at the end, matching
|
|
1362
|
+
// the native dispatch_collision_type which ran AFTER dispatch_collision_actions).
|
|
1363
|
+
var ct = collisionType || 'NONE';
|
|
1364
|
+
|
|
1365
|
+
if (ct === 'RANDOM_X') {
|
|
1366
|
+
// RANDOM_X: 1 RNG draw — vx = _rng()*2-1, vy = 1 (no charge reset).
|
|
1367
|
+
// Native: vx = _rng()*2-1; vy = 1;
|
|
1368
|
+
ops.push({ set: HANDLE_FRAME + '.vx',
|
|
1369
|
+
to: Bin('-', Bin('*', Fn('random', []), Lit(2)), Lit(1)) });
|
|
1370
|
+
ops.push({ set: HANDLE_FRAME + '.vy',
|
|
1371
|
+
to: Lit(1) });
|
|
1372
|
+
|
|
1373
|
+
} else if (ct === 'RANDOM_Y') {
|
|
1374
|
+
// RANDOM_Y: 2 RNG draws — vy = _rng()*2-1, then reset() draws charge.
|
|
1375
|
+
// Native: vy = _rng()*2-1; vx = 1; reset() → charge draw.
|
|
1376
|
+
// reset() also clears local_time=0 (wave-phase reset — G2 mirrors this with a 0-draw op).
|
|
1377
|
+
ops.push({ set: HANDLE_FRAME + '.vy',
|
|
1378
|
+
to: Bin('-', Bin('*', Fn('random', []), Lit(2)), Lit(1)) });
|
|
1379
|
+
ops.push({ set: HANDLE_FRAME + '.vx',
|
|
1380
|
+
to: Lit(1) });
|
|
1381
|
+
// Reset wave phase (reset() side-effect — no RNG draw).
|
|
1382
|
+
ops.push({ set: HANDLE_FRAME + '.local_time', to: Lit(0) });
|
|
1383
|
+
// charge draw: _rng() < 0.5 ? Charge.POSITIVE : Charge.NEGATIVE (0 or 1)
|
|
1384
|
+
ops.push({ set: HANDLE_FRAME + '.charge',
|
|
1385
|
+
to: Cond(Cmp('<', Fn('random', []), Lit(0.5)), Lit(0), Lit(1)) });
|
|
1386
|
+
|
|
1387
|
+
} else if (ct === 'RANDOM_XY') {
|
|
1388
|
+
// RANDOM_XY: 3 RNG draws — vx = _rng(), vy = _rng()*2-1, then reset() draws charge.
|
|
1389
|
+
// Native: vx = _rng(); vy = _rng()*2-1; reset() → charge.
|
|
1390
|
+
// reset() also clears local_time=0 (wave-phase reset — G2 mirrors this with a 0-draw op).
|
|
1391
|
+
ops.push({ set: HANDLE_FRAME + '.vx',
|
|
1392
|
+
to: Fn('random', []) });
|
|
1393
|
+
ops.push({ set: HANDLE_FRAME + '.vy',
|
|
1394
|
+
to: Bin('-', Bin('*', Fn('random', []), Lit(2)), Lit(1)) });
|
|
1395
|
+
// Reset wave phase (reset() side-effect — no RNG draw).
|
|
1396
|
+
ops.push({ set: HANDLE_FRAME + '.local_time', to: Lit(0) });
|
|
1397
|
+
// charge draw: _rng() < 0.5 ? Charge.POSITIVE : Charge.NEGATIVE (0 or 1)
|
|
1398
|
+
ops.push({ set: HANDLE_FRAME + '.charge',
|
|
1399
|
+
to: Cond(Cmp('<', Fn('random', []), Lit(0.5)), Lit(0), Lit(1)) });
|
|
1400
|
+
|
|
1401
|
+
} else if (ct === 'MOMENTUM_BASED') {
|
|
1402
|
+
// MOMENTUM_BASED: 0 RNG draws — restores pre-move position (move_undo equivalent).
|
|
1403
|
+
// Native: p.move_undo() → p.x = p.prev_x, p.y = p.prev_y.
|
|
1404
|
+
// G2: prev_x/prev_y are exposed in the frame scope bag (frame_to_scope_bag).
|
|
1405
|
+
ops.push({ set: HANDLE_FRAME + '.x',
|
|
1406
|
+
to: Ref(HANDLE_FRAME, 'prev_x') });
|
|
1407
|
+
ops.push({ set: HANDLE_FRAME + '.y',
|
|
1408
|
+
to: Ref(HANDLE_FRAME, 'prev_y') });
|
|
1409
|
+
|
|
1410
|
+
} else if (ct === 'WALL') {
|
|
1411
|
+
// WALL: 0 RNG draws — directional eject only (no velocity flip).
|
|
1412
|
+
// Same eject formulas as BLOCK; the wall direction is the LO name prefix.
|
|
1413
|
+
// Native: _eject(p.rect, reposition, false, pastWall) → positional eject only.
|
|
1414
|
+
// G2b: WALL needs past-edge trigger (frames can slip through).
|
|
1415
|
+
if (reposition) {
|
|
1416
|
+
var wallEjectOps = _buildEjectOps(reposition, HANDLE_FRAME, HANDLE_SELF, Lit, Ref, Bin, Cmp, Cond, Fn);
|
|
1417
|
+
if (wallEjectOps) {
|
|
1418
|
+
for (var wi = 0; wi < wallEjectOps.length; wi++) ops.push(wallEjectOps[wi]);
|
|
1419
|
+
isWall = true; // trigger needs past-edge test
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
} else if (ct === 'BOUNCE') {
|
|
1424
|
+
// BOUNCE collision_type: only compile if no BOUNCE action was already compiled.
|
|
1425
|
+
// Sugar objects (Box) use BOUNCE actions; collision_type=BOUNCE was redundant.
|
|
1426
|
+
// After migration, Box passes NONE here — but if somehow both arrive, skip
|
|
1427
|
+
// to avoid double eject+flip.
|
|
1428
|
+
var hasBounceAction = actionsList.indexOf('BOUNCE') >= 0;
|
|
1429
|
+
if (!hasBounceAction && reposition) {
|
|
1430
|
+
var ctBounceEjectOps = _buildEjectOps(reposition, HANDLE_FRAME, HANDLE_SELF, Lit, Ref, Bin, Cmp, Cond, Fn);
|
|
1431
|
+
var ctFlipOp = _buildVelocityFlipOp(loName || '', HANDLE_FRAME, Lit, Ref, Bin);
|
|
1432
|
+
if (ctBounceEjectOps) {
|
|
1433
|
+
for (var bk = 0; bk < ctBounceEjectOps.length; bk++) ops.push(ctBounceEjectOps[bk]);
|
|
1434
|
+
}
|
|
1435
|
+
if (ctFlipOp) ops.push(ctFlipOp);
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
}
|
|
1439
|
+
// NONE: nothing appended.
|
|
1440
|
+
|
|
1441
|
+
// G2b: attach a program-level trigger so the Rust engine evaluates it ONCE before
|
|
1442
|
+
// running any ops. If trigger == 0 (false), ALL ops are skipped atomically.
|
|
1443
|
+
//
|
|
1444
|
+
// This replaces the old per-op Cond wrapping approach. The old approach re-evaluated
|
|
1445
|
+
// Fn('intersects') for EACH op, but after the eject op (op 1) updated scope.left,
|
|
1446
|
+
// the bounce op's (op 2) intersects check used the post-eject position, returning false
|
|
1447
|
+
// and silently skipping the velocity flip — a critical bug (G2b per-op Cond bug).
|
|
1448
|
+
//
|
|
1449
|
+
// WALL uses the extended trigger (intersects OR past_edge).
|
|
1450
|
+
// All other action types use the overlap-only trigger.
|
|
1451
|
+
var program = ops.length > 0
|
|
1452
|
+
? { on: 'collision', ops: ops }
|
|
1453
|
+
: null;
|
|
1454
|
+
|
|
1455
|
+
if (program !== null) {
|
|
1456
|
+
var trigger = _buildTriggerExpr(
|
|
1457
|
+
reposition || null,
|
|
1458
|
+
isWall,
|
|
1459
|
+
HANDLE_FRAME, HANDLE_SELF,
|
|
1460
|
+
Lit, Ref, Cmp, Cond, Fn
|
|
1461
|
+
);
|
|
1462
|
+
_applyProgramTrigger(program, trigger);
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
return { program: program };
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
/**
|
|
1469
|
+
* Compile the Collision LEXICON v2 wall spec → an op-program (the SINGLE compiler
|
|
1470
|
+
* authority for the v2 vocabulary; the W1 compileActions() path is untouched).
|
|
1471
|
+
*
|
|
1472
|
+
* @param {Object} spec
|
|
1473
|
+
* @param {string} spec.behavior - nothing | block | bounce | absorb | destroy
|
|
1474
|
+
* @param {string} [spec.bounceDirection] - reflect | random (when behavior==bounce)
|
|
1475
|
+
* @param {Array<number>} [spec.angleConstraint] - [minDeg,maxDeg] offsets around the
|
|
1476
|
+
* wall's INITIAL orientation (random only). Omitted/full ⇒ omni.
|
|
1477
|
+
* @param {number} [spec.orientationDeg=0]- the wall's INITIAL orientation (baked; a
|
|
1478
|
+
* later spin must recompile — the cone endpoints are compile-time Lits).
|
|
1479
|
+
* @param {boolean}[spec.decoherence=false]- append the phase/charge reset (random only).
|
|
1480
|
+
* @param {Array<string>}[spec.hooks] - ['mark','count'] (composable with any behavior).
|
|
1481
|
+
* @param {string} [spec.markTag] - the user string MARK stamps (default '').
|
|
1482
|
+
* @param {number} [spec.capacity=64] - ABSORB mass ceiling + 1/capacity radiation.
|
|
1483
|
+
* @param {number} [spec.emissivity] - ABSORB radiating efficiency, a fullness fraction
|
|
1484
|
+
* in (0,1] applied to the emitted leaf. Omitted ⇒ absorbRadiation's default (0.5).
|
|
1485
|
+
* @param {number[]}[spec.color] - the wall's drawn [r,g,b(,a)] — the ONE authority
|
|
1486
|
+
* the `recolor` hook derives its render.color tag from (never a parallel color field).
|
|
1487
|
+
* @param {string} [spec.reposition='closest'] - eject face for bounce.
|
|
1488
|
+
* @returns {{ program:Object|null, collectCollided:boolean,
|
|
1489
|
+
* radiation:{capacity:number, emissivity?:number}|null }}
|
|
1490
|
+
*/
|
|
1491
|
+
// _rgbaCsv([r,g,b(,a)]) → 'r,g,b,a' (the render.color CSV the providers read). Clamps to
|
|
1492
|
+
// ints in [0,255]; default alpha 255. Returns null for a missing/malformed color so the
|
|
1493
|
+
// recolor hook is a no-op rather than stamping garbage. This mirrors the exact 4-element
|
|
1494
|
+
// CSV shape colored emitter frames already ride (makeLeafFrame → rgba.join(',')).
|
|
1495
|
+
function _rgbaCsv(color) {
|
|
1496
|
+
if (!Array.isArray(color) || color.length < 3) { return null; }
|
|
1497
|
+
function _b(n) { n = Math.round(Number(n)); return isNaN(n) ? null : Math.max(0, Math.min(255, n)); }
|
|
1498
|
+
var r = _b(color[0]), g = _b(color[1]), b = _b(color[2]);
|
|
1499
|
+
if (r === null || g === null || b === null) { return null; }
|
|
1500
|
+
var a = (color.length >= 4) ? _b(color[3]) : 255;
|
|
1501
|
+
if (a === null) { a = 255; }
|
|
1502
|
+
return r + ',' + g + ',' + b + ',' + a;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
function compileWallV2(spec) {
|
|
1506
|
+
var ast = AstSchema;
|
|
1507
|
+
// No StrLit binding — the mark/recolor hooks are Taggers (tagger.js owns the op shape).
|
|
1508
|
+
var Lit = ast.Lit, Ref = ast.Ref, Bin = ast.Bin, Cmp = ast.Cmp, Cond = ast.Cond,
|
|
1509
|
+
Fn = ast.Fn, HF = ast.HANDLE_FRAME, HS = ast.HANDLE_SELF;
|
|
1510
|
+
|
|
1511
|
+
spec = spec || {};
|
|
1512
|
+
var behavior = spec.behavior || 'nothing';
|
|
1513
|
+
var reposition = spec.reposition || 'closest';
|
|
1514
|
+
// Z1 — a 3D wall (a real z-slab, is3D) ejects on all SIX faces and reflects on all
|
|
1515
|
+
// three axes. Auto-promote the DEFAULT 'closest' eject to the 6-face 'closest3d' so a
|
|
1516
|
+
// frame entering via the near/far face is z-ejected (else in-wall z jitter); an explicit
|
|
1517
|
+
// reposition is honored verbatim. 2D walls (is3D falsy) are entirely unchanged (SU1).
|
|
1518
|
+
var is3D = (spec.is3D === true);
|
|
1519
|
+
if (is3D && reposition === 'closest') { reposition = 'closest3d'; }
|
|
1520
|
+
var hooks = Array.isArray(spec.hooks) ? spec.hooks : [];
|
|
1521
|
+
var collectCollided = false;
|
|
1522
|
+
var radiation = null;
|
|
1523
|
+
var ops = [];
|
|
1524
|
+
|
|
1525
|
+
// ── Motion / life response (the ONE behavior axis) ───────────────────────
|
|
1526
|
+
// TILTED REFLECT GUARD (§2 + the same-tick-exteriority rule): a
|
|
1527
|
+
// NON-cardinal REFLECT mirror is fired ONCE per genuine drawn-SURFACE crossing (the guard
|
|
1528
|
+
// trigger, applied below) and ejects by the PREV-ANCHORED FACE-NORMAL EXIT — the ONE
|
|
1529
|
+
// coherent eject story that replaces BOTH the interim OBB-normal eject and the
|
|
1530
|
+
// plain prev-restore. Cardinal / random-bounce / non-reflect walls keep the
|
|
1531
|
+
// AABB 'closest' eject + plain-intersects trigger — BYTE-IDENTICAL (SU1). The guard normal
|
|
1532
|
+
// is CONSUMED from the ONE SlabFace authority (obb.normal = the −1-eigenvector the reflect
|
|
1533
|
+
// law flips, engine y-down frame) — never a second inline sin/cos convention. Applies to
|
|
1534
|
+
// BOTH 2D and 3D (a non-cardinal orientation is rotation in the xy-plane; z passes
|
|
1535
|
+
// through — 2D≡3D). slabW/slabH ride along so the resolved obb is fully formed; the guard
|
|
1536
|
+
// itself reads only obb.normal (the slab EXTENT is enforced by the engine's swept-OBB
|
|
1537
|
+
// region, which culls candidates outside the rotated slab before the trigger runs).
|
|
1538
|
+
var _reflectSlab = (behavior === 'bounce' && spec.bounceDirection !== 'random')
|
|
1539
|
+
? SlabFace.resolveSlabFace({ orientationDeg: spec.orientationDeg,
|
|
1540
|
+
w: spec.slabW, h: spec.slabH,
|
|
1541
|
+
pitchDeg: spec.pitchDeg, d: spec.slabD }) : null;
|
|
1542
|
+
// R2AX — a PITCHED plate (two-axis mirror) takes the true
|
|
1543
|
+
// 3-space reflection path (R = I − 2nnᵀ about SlabFace plate3.n + the n-dot guard +
|
|
1544
|
+
// the prev-anchored 3-space exit). pitch 0/absent never sets `pitched` — every
|
|
1545
|
+
// existing program stays byte-identical.
|
|
1546
|
+
var pitchedReflect = !!(_reflectSlab && _reflectSlab.pitched);
|
|
1547
|
+
if (pitchedReflect && !is3D) {
|
|
1548
|
+
throw new Error('[actionCompiler] pitchDeg requires a 3D wall (a real z/depth) — ' +
|
|
1549
|
+
'a 2D wall has no z axis to pitch into.');
|
|
1550
|
+
}
|
|
1551
|
+
var tiltedReflect = !!(_reflectSlab && !_reflectSlab.isCardinal && !pitchedReflect);
|
|
1552
|
+
if (behavior === 'bounce') {
|
|
1553
|
+
if (pitchedReflect) {
|
|
1554
|
+
// ONE response class: face exit along n (prev-anchored, same-tick exteriority)
|
|
1555
|
+
// then the 3-space reflection about n. The guard trigger (below) is the narrow
|
|
1556
|
+
// phase (plane crossing + plate extent); the engine gets NO region (broad phase
|
|
1557
|
+
// = the plate's 3D AABB rect, emitted by the Wall sugar).
|
|
1558
|
+
ops = ops.concat(_buildPitchedExitEjectOps(_reflectSlab.plate3, HF, HS, Lit, Ref, Bin, Cmp, Cond));
|
|
1559
|
+
ops = ops.concat(_buildPitchedReflectOps(_reflectSlab.plate3.n, HF, Lit, Ref, Bin));
|
|
1560
|
+
} else if (tiltedReflect) {
|
|
1561
|
+
// GUARDED tilted reflect: the face-normal EXIT eject FIRST (it reads the INCOMING
|
|
1562
|
+
// velocity to pick the incident side), then the reflection law (position and
|
|
1563
|
+
// velocity are independent writes). The whole program fires only when the guard
|
|
1564
|
+
// trigger (below) confirms a surface crossing, so the involution runs exactly once
|
|
1565
|
+
// per hit AND the leaf's rendered position is outside the slab the SAME tick — no
|
|
1566
|
+
// oscillation, no far-face eject, no leak-through, no in-slab rendering.
|
|
1567
|
+
var _ghw = (_reflectSlab.obb && isFinite(_reflectSlab.obb.hw)) ? _reflectSlab.obb.hw : 0;
|
|
1568
|
+
if (is3D) {
|
|
1569
|
+
// 3D TILTED slab — the z-face COMPLETION (measured 3D z-entry leak class,
|
|
1570
|
+
// 2026-07-25): the slab has TWO kinds of reflecting surface — the rotated xy
|
|
1571
|
+
// DRAWN faces (the 2θ law) and the UNROTATED near/far z-faces (the C27 z-plane
|
|
1572
|
+
// flip). One program handles both: every op is gated on a PREV-ONLY class flag
|
|
1573
|
+
// (`_tilted3DGates` — immune to the retired G2b re-evaluation bug because the
|
|
1574
|
+
// gates read only prev_*/self.* refs no op writes). Order matters only for the
|
|
1575
|
+
// documented sequential-swap reads; the gates are constant across the sequence.
|
|
1576
|
+
// xy class: prev-side-anchored face exit + the swap/angled reflect (vz through)
|
|
1577
|
+
// z class: z-face exit + vz flip (xy untouched)
|
|
1578
|
+
var _g3 = _tilted3DGates(_reflectSlab.obb.normal, _ghw, HF, HS, Lit, Ref, Bin, Cmp, Cond);
|
|
1579
|
+
var _ejXY = _buildFaceExitEjectOps(_reflectSlab.obb.normal, _ghw, HF, HS, Lit, Ref, Bin, Cmp, Cond, true);
|
|
1580
|
+
ops = ops.concat(_gateOps(_ejXY, _g3.xprev, HF, Ref, Cond));
|
|
1581
|
+
ops = ops.concat(_gateOps([_buildZFaceExitEjectOp(HF, HS, Lit, Ref, Bin, Cmp, Cond)], _g3.zclass, HF, Ref, Cond));
|
|
1582
|
+
ops = ops.concat(_gateOps(_buildReflectOps(spec.orientationDeg, HF, HS, Lit, Ref, Bin, Cmp, Cond, Fn, is3D), _g3.xprev, HF, Ref, Cond));
|
|
1583
|
+
ops = ops.concat(_gateOps([{ set: HF + '.vz', to: Bin('-', Lit(0), Ref(HF, 'vz')) }], _g3.zclass, HF, Ref, Cond));
|
|
1584
|
+
} else {
|
|
1585
|
+
ops = ops.concat(_buildFaceExitEjectOps(_reflectSlab.obb.normal, _ghw, HF, HS, Lit, Ref, Bin, Cmp, Cond));
|
|
1586
|
+
ops = ops.concat(_buildReflectOps(spec.orientationDeg, HF, HS, Lit, Ref, Bin, Cmp, Cond, Fn, is3D));
|
|
1587
|
+
}
|
|
1588
|
+
} else {
|
|
1589
|
+
// Cardinal / random-bounce: AABB eject (penetration-stop) THEN the direction op —
|
|
1590
|
+
// BYTE-IDENTICAL to the pre-guard program (SU1).
|
|
1591
|
+
var ejectOps = _buildEjectOps(reposition, HF, HS, Lit, Ref, Bin, Cmp, Cond, Fn) || [];
|
|
1592
|
+
for (var e = 0; e < ejectOps.length; e++) { ops.push(ejectOps[e]); }
|
|
1593
|
+
if (spec.bounceDirection === 'random') {
|
|
1594
|
+
var ac = spec.angleConstraint;
|
|
1595
|
+
if (Array.isArray(ac) && ac.length === 2 && isFinite(ac[0]) && isFinite(ac[1]) &&
|
|
1596
|
+
(Math.abs(ac[1] - ac[0]) < 360)) {
|
|
1597
|
+
// Cone endpoints baked HOST-SIDE from the INITIAL orientation (trig legal here).
|
|
1598
|
+
var o = (typeof spec.orientationDeg === 'number') ? spec.orientationDeg : 0;
|
|
1599
|
+
var a0 = (o + ac[0]) * Math.PI / 180, a1 = (o + ac[1]) * Math.PI / 180;
|
|
1600
|
+
ops = ops.concat(_buildConstrainedRandomOps(
|
|
1601
|
+
HF, Math.cos(a0), Math.sin(a0), Math.cos(a1), Math.sin(a1), Lit, Ref, Bin, Fn));
|
|
1602
|
+
} else {
|
|
1603
|
+
ops = ops.concat(_buildOmniRandomOps(HF, Lit, Bin, Fn));
|
|
1604
|
+
}
|
|
1605
|
+
if (spec.decoherence === true) {
|
|
1606
|
+
ops = ops.concat(_buildDecoherenceOps(HF, Lit, Cmp, Cond, Fn));
|
|
1607
|
+
}
|
|
1608
|
+
} else {
|
|
1609
|
+
// reflect (cardinal): the proven incidence box tree (byte-identical at 0 — SU1);
|
|
1610
|
+
// fused with the AABB eject above. A live orientation edit re-bakes via setV2Prop.
|
|
1611
|
+
ops = ops.concat(_buildReflectOps(spec.orientationDeg, HF, HS, Lit, Ref, Bin, Cmp, Cond, Fn, is3D));
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
} else if (behavior === 'block') {
|
|
1615
|
+
// BLOCK — the particle is STOPPED at the surface. ONE response, composed entirely
|
|
1616
|
+
// from the SHARED eject authority (`_buildEjectOps`): reposition the frame to the
|
|
1617
|
+
// CLOSEST position OUTSIDE the wall, velocity UNTOUCHED. That is literally the
|
|
1618
|
+
// `bounce` op-set minus the reflect ops, and byte-identical to the op-set the
|
|
1619
|
+
// legacy `CollisionActions.BLOCK` compiled — so porting a legacy `block` caller
|
|
1620
|
+
// moves ZERO physics.
|
|
1621
|
+
//
|
|
1622
|
+
// WHY "closest outside" and NOT "revert the last move" (both were options;
|
|
1623
|
+
// measured head-to-head on the canonical WASM engine, wall x∈[200,220]):
|
|
1624
|
+
// - approaching leaf (x=150, vx=+1): the two are equivalent in outcome — the leaf
|
|
1625
|
+
// stops dead at the face and stays (eject parks at x=198.5, revert at x=199) for
|
|
1626
|
+
// every remaining tick, velocity kept at +1. Either satisfies "block".
|
|
1627
|
+
// - leaf whose PREVIOUS position was ALSO inside (spawned/tunnelled in at x=210):
|
|
1628
|
+
// eject-to-closest is outside in ONE tick (0/30 ticks inside); revert-last-move
|
|
1629
|
+
// restores it to a position that is itself inside, so the guard re-fires forever
|
|
1630
|
+
// and the leaf is PINNED INSIDE THE WALL (30/30 ticks inside). Revert has no
|
|
1631
|
+
// exteriority guarantee; eject does, from any state. That decides it.
|
|
1632
|
+
// The composition also inherits `reposition`'s 3D promotion for free: an is3D wall
|
|
1633
|
+
// already auto-promotes 'closest' → 'closest3d' above, so a z-slab block wall ejects
|
|
1634
|
+
// on all six faces (2D≡3D, no block-specific 3D code).
|
|
1635
|
+
//
|
|
1636
|
+
// TILT: like `bounce/random`, a non-cardinal block wall uses the AABB 'closest'
|
|
1637
|
+
// eject (the prev-anchored face-normal exit is a REFLECT-only construction — it
|
|
1638
|
+
// reads the incoming velocity to pick the incident side, which a velocity-preserving
|
|
1639
|
+
// stop has no use for). A tilted block wall therefore stops the particle at the
|
|
1640
|
+
// bounding-box face, not the drawn face.
|
|
1641
|
+
var blockEjectOps = _buildEjectOps(reposition, HF, HS, Lit, Ref, Bin, Cmp, Cond, Fn) || [];
|
|
1642
|
+
for (var bq = 0; bq < blockEjectOps.length; bq++) { ops.push(blockEjectOps[bq]); }
|
|
1643
|
+
} else if (behavior === 'absorb') {
|
|
1644
|
+
// Track A (the mass-surface rule): the collision op just KILLS; the host
|
|
1645
|
+
// radiation tick-fn (absorbRadiation.js) tallies the collected killed masses into the
|
|
1646
|
+
// LO's REAL BACKING-FRAME mass (via the existing frame accessors) and radiates per the
|
|
1647
|
+
// conservation contract. The v1 count-as-virtual-mass tally op is WITHDRAWN (round-3
|
|
1648
|
+
// — it was exactly the duplicative parallel surface the rule rejects).
|
|
1649
|
+
var cap = (typeof spec.capacity === 'number' && spec.capacity > 0) ? spec.capacity : 64;
|
|
1650
|
+
ops.push({ set: HF + '.is_alive', to: Lit(0) });
|
|
1651
|
+
collectCollided = true; // buffer killed frames' masses for the host tally
|
|
1652
|
+
// EMISSIVITY rides the radiation bag beside the clock capacity: it is the wall's
|
|
1653
|
+
// radiating efficiency as a FULLNESS fraction of the emitted leaf, so it sets what the
|
|
1654
|
+
// re-emitted quantum IS (its speed and its wavelength together). Omitted ⇒ the sugar's
|
|
1655
|
+
// own default (absorbRadiation.DEFAULT_EMISSIVITY); this compiler does not mirror the
|
|
1656
|
+
// default, it just declines to state one.
|
|
1657
|
+
radiation = { capacity: cap };
|
|
1658
|
+
if (spec.emissivity !== undefined && spec.emissivity !== null) {
|
|
1659
|
+
radiation.emissivity = spec.emissivity;
|
|
1660
|
+
}
|
|
1661
|
+
} else if (behavior === 'destroy') {
|
|
1662
|
+
ops.push({ set: HF + '.is_alive', to: Lit(0) });
|
|
1663
|
+
}
|
|
1664
|
+
// 'nothing' → no motion/life op (pass-through).
|
|
1665
|
+
|
|
1666
|
+
// ── Hooks (composable with ANY behavior) ─────────────────────────────────
|
|
1667
|
+
if (hooks.indexOf('mark') !== -1) {
|
|
1668
|
+
// The `mark` hook IS a Tagger: the user string under the canonical provenance key.
|
|
1669
|
+
// Shape owned by tagger.js, ferried here (`''` is a legal tag value — a mark hook
|
|
1670
|
+
// with no markTag has always stamped the empty string; the part preserves that).
|
|
1671
|
+
ops = ops.concat(Tagger.tagOps(spec.markTag || ''));
|
|
1672
|
+
}
|
|
1673
|
+
if (hooks.indexOf('count') !== -1) {
|
|
1674
|
+
ops.push({ set: HS + '.count', to: Bin('+', Ref(HS, 'count'), Lit(1)) });
|
|
1675
|
+
collectCollided = true; // buffer frames for the between-tick stats reader
|
|
1676
|
+
}
|
|
1677
|
+
// recolor — a RENDER-INTENT hook (MARK's sibling): stamp the wall's OWN drawn color
|
|
1678
|
+
// into the collided frame's opaque string_map under 'render.color' (CSV 'r,g,b,a').
|
|
1679
|
+
// The engine carries the string_map key VERBATIM and interprets nothing (I1 — the
|
|
1680
|
+
// engine has no color); the renderer providers already read string_map['render.color']
|
|
1681
|
+
// (highest-priority render intent), so a colliding beam RE-COLORS to this wall's color
|
|
1682
|
+
// with ZERO renderer change. The color is derived at compile time from the ONE authority
|
|
1683
|
+
// — the wall's `color` property (spec.color) — never a parallel color field. StrLit is
|
|
1684
|
+
// the same string-map write path MARK uses; strKey = prop.slice(5) keeps the dotted
|
|
1685
|
+
// 'render.color' key intact.
|
|
1686
|
+
if (hooks.indexOf('recolor') !== -1) {
|
|
1687
|
+
var _csv = _rgbaCsv(spec.color);
|
|
1688
|
+
// recolor is MARK's sibling and therefore the SAME part with a different key:
|
|
1689
|
+
// a Tagger whose key ('render.color') happens to be one the renderer reads. The
|
|
1690
|
+
// dotted key survives the tag path whole (strKey = prop.slice(5)).
|
|
1691
|
+
if (_csv) { ops = ops.concat(Tagger.tagOps({ 'render.color': _csv })); }
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
var program = ops.length > 0 ? { on: 'collision', ops: ops } : null;
|
|
1695
|
+
if (program !== null) {
|
|
1696
|
+
if (pitchedReflect) {
|
|
1697
|
+
// R2AX — the pitched guard IS the narrow phase (plane crossing + extent along
|
|
1698
|
+
// the plate's u/v axes; no engine region for a pitched plate).
|
|
1699
|
+
_applyProgramTrigger(program, _buildPitchedGuardTrigger(
|
|
1700
|
+
_reflectSlab.plate3, HF, HS, Lit, Ref, Bin, Cmp, Cond, Fn));
|
|
1701
|
+
} else if (tiltedReflect) {
|
|
1702
|
+
// GUARDED tilted reflect: fire the reflection ONCE per drawn-SURFACE crossing
|
|
1703
|
+
// (surface-crossing + approach guard — §2 upgraded to the ±hw surfaces
|
|
1704
|
+
// per the same-tick-exteriority rule). The crossing NORMAL is `obb.normal`
|
|
1705
|
+
// and the half-thickness `obb.hw`, BOTH from the ONE SlabFace authority: the
|
|
1706
|
+
// normal is the axis R(reflectBaseDeg) actually FLIPS (its −1-eigenvector,
|
|
1707
|
+
// engine y-down frame) — the SAME derivation the reflect law, the renderer's
|
|
1708
|
+
// drawn face, and the engine OBB region share (circulation §1: never a second
|
|
1709
|
+
// inline sin/cos convention; the guard logic is invariant under n → −n because
|
|
1710
|
+
// it handles both surfaces symmetrically). The finite slab EXTENT along the
|
|
1711
|
+
// face line is enforced by the engine's swept-OBB narrow-phase, which culls
|
|
1712
|
+
// candidates outside the rotated slab BEFORE this trigger runs.
|
|
1713
|
+
var _ghw2 = (_reflectSlab.obb && isFinite(_reflectSlab.obb.hw)) ? _reflectSlab.obb.hw : 0;
|
|
1714
|
+
// `withZFaces = is3D`: a 3D tilted slab's trigger ALSO fires on a genuine z-face
|
|
1715
|
+
// crossing (the z-entry leak completion); 2D stays byte-identical (SU1).
|
|
1716
|
+
_applyProgramTrigger(program, _buildTiltedReflectGuardTrigger(
|
|
1717
|
+
_reflectSlab.obb.normal, _ghw2, HF, HS, Lit, Ref, Bin, Cmp, Cond, Fn, is3D));
|
|
1718
|
+
} else {
|
|
1719
|
+
// Overlap-only trigger (frame is inside the wall when any v2 op fires) — SU1.
|
|
1720
|
+
_applyProgramTrigger(program, _buildTriggerExpr(reposition, false, HF, HS, Lit, Ref, Cmp, Cond, Fn));
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
return { program: program, collectCollided: collectCollided, radiation: radiation };
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// ── Export ─────────────────────────────────────────────────────────────────────
|
|
1727
|
+
|
|
1728
|
+
// Collision LEXICON v2 builders (proven; consumed by compileWallV2 and driven RAW by the bench
|
|
1729
|
+
// LO behaviors — the SINGLE authority for these shapes) and the shared eject/flip builders (OvenBox, Tube).
|
|
1730
|
+
export {
|
|
1731
|
+
compileActions,
|
|
1732
|
+
compileWallV2,
|
|
1733
|
+
_buildIncidenceReflectOps as buildIncidenceReflectOps,
|
|
1734
|
+
_buildIncidenceReflectOps3D as buildIncidenceReflectOps3D,
|
|
1735
|
+
_buildAngledReflectOps as buildAngledReflectOps,
|
|
1736
|
+
_buildSwapReflectOps as buildSwapReflectOps,
|
|
1737
|
+
_buildReflectOps as buildReflectOps,
|
|
1738
|
+
_buildTiltedReflectGuardTrigger as buildTiltedReflectGuardTrigger,
|
|
1739
|
+
_buildFaceExitEjectOps as buildFaceExitEjectOps,
|
|
1740
|
+
_buildOmniRandomOps as buildOmniRandomOps,
|
|
1741
|
+
_buildConstrainedRandomOps as buildConstrainedRandomOps,
|
|
1742
|
+
_buildDecoherenceOps as buildDecoherenceOps,
|
|
1743
|
+
_buildTriggerExpr as buildTriggerExpr,
|
|
1744
|
+
_applyProgramTrigger as applyProgramTrigger,
|
|
1745
|
+
_buildEjectOps as buildEjectOps,
|
|
1746
|
+
_buildVelocityFlipOp as buildVelocityFlipOp
|
|
1747
|
+
};
|