@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +87 -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 +2501 -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 +85 -0
- package/dist/frqtl/compat/renderer/RenderMesh.js +100 -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 +259 -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 +2474 -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 +2922 -0
- package/dist/frqtl/objects/ObjectFactory.js +297 -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 +711 -0
- package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -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 +412 -0
- package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
- package/dist/frqtl/objects/objects/LightSource.js +354 -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 +310 -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 +824 -0
- package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
- package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
- package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
- package/dist/frqtl/objects/objects/tagger.js +228 -0
- package/dist/frqtl/objects/schema/engine-scopes.js +469 -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 +251 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -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 +280 -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 +1259 -0
- package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
- package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
- package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
- package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
- package/dist/frqtl/objects/v1-replacements.js +30 -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 +35 -0
- package/dist/frqtl/parts/CollisionTypes.js +31 -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 +490 -0
- package/dist/frqtl/parts/LargeObject.js +406 -0
- package/dist/frqtl/parts/LightColor.js +255 -0
- package/dist/frqtl/parts/LiveChart.js +285 -0
- package/dist/frqtl/parts/LogicGate.js +720 -0
- package/dist/frqtl/parts/OvenBox.js +612 -0
- package/dist/frqtl/parts/PointSource.js +763 -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 +244 -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 +212 -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 +692 -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 +326 -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 +672 -0
- package/dist/shim/notebook/knob.js +151 -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 +510 -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 +214 -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 +807 -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 +62 -4
|
@@ -0,0 +1,1211 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wall — the first-class, palette-exposed WALL primitive
|
|
5
|
+
* (ws-experiments-walkthrough W1, CEO ruling E-W1a). It stands to the wall family
|
|
6
|
+
* exactly as `SphereEmitter` stands to the emitter family: a THIN, named, UX-
|
|
7
|
+
* declaring FACADE over the ONE engine-facing primitive that already exists — a
|
|
8
|
+
* single `LargeObject` whose collision behavior is the Collision LEXICON v2
|
|
9
|
+
* (`collisionBehavior`: nothing | block | bounce | absorb | destroy, plus
|
|
10
|
+
* `bounceDirection` and the composable `mark`/`count`/`recolor` hooks), compiled to
|
|
11
|
+
* op-programs by the SAME `actionCompiler` every other wall rides.
|
|
12
|
+
*
|
|
13
|
+
* var barrier = new Wall(universe, { rect: [200, 0, 8, 120], collisionBehavior: 'block' });
|
|
14
|
+
* barrier.setV2Prop('collisionBehavior', 'bounce'); // recompile the running program live
|
|
15
|
+
* var screen = new Wall(universe, { rect: [400, 0, 20, 400], preset: 'detector-screen' });
|
|
16
|
+
* screen.getCollisionHistogram(tickData); // y -> count on the counting screen
|
|
17
|
+
*
|
|
18
|
+
* ── PURE FACADE — zero engine, zero new physics (canonical-bench SU1) ─────────
|
|
19
|
+
* A `Wall` adds NO collision logic of its own. The lexicon lives once in
|
|
20
|
+
* `constants/WallBehaviors.js` (ferried from the frozen `CollisionActions`/
|
|
21
|
+
* `CollisionTypes` sets), `resolveWallSpec` → `compileWallV2` is the ONE compiler
|
|
22
|
+
* authority, and the engine interprets the resulting op-program. The engine has NO
|
|
23
|
+
* "wall" concept and needs none — "a new behavior is new op-program DATA, never new
|
|
24
|
+
* engine code." If building a `Wall` ever seems to need engine or provider code,
|
|
25
|
+
* STOP: it is a `LargeObject` + `actionCompiler`.
|
|
26
|
+
*
|
|
27
|
+
* ── The wall family builds ON this atom ──────────────────────────────────────
|
|
28
|
+
* `DetectorWall` (the counting atom `Wall{'destroy', hooks:['count']}`),
|
|
29
|
+
* and `SlitScreen`'s wall segments (`Wall{'block'|'destroy'}`) are composed from
|
|
30
|
+
* this facade. The specialized parts that are NOT plain walls (SlitScreen's
|
|
31
|
+
* MARK/RANDOM_XY slits + liners) stay as-is.
|
|
32
|
+
*
|
|
33
|
+
* ── Layer 1: API contract ────────────────────────────────────────────────────
|
|
34
|
+
*
|
|
35
|
+
* `new Wall(universe, opts)` — builds + engine-registers ONE `LargeObject` (it
|
|
36
|
+
* does NOT add itself to any runner render list; the caller — the ObjectFactory
|
|
37
|
+
* builder or a setup factory — needs NO render-wiring call: constructing the inner
|
|
38
|
+
* LargeObject registers it with the world for physics AND render, exactly
|
|
39
|
+
* as it would for a bare `LargeObject`).
|
|
40
|
+
* @param {Object} universe — an EGPTUniverse (JS or WASM compat shim).
|
|
41
|
+
* @param {Object} opts
|
|
42
|
+
* @param {Array|Object} [opts.rect] — bounding rect [left,top,w,h] or the
|
|
43
|
+
* explicit-z/depth [left,top,z,w,h,d] / {left,top,w,h[,z,d]} (passed verbatim
|
|
44
|
+
* to `LargeObject`, WASM 3D convention). If omitted, built from
|
|
45
|
+
* opts.left/top/w/h (+ optional z/d).
|
|
46
|
+
* @param {string} opts.collisionBehavior — REQUIRED (or supply a `preset`):
|
|
47
|
+
* nothing | block | bounce | absorb | destroy. The ONE owner is `WallBehaviors`.
|
|
48
|
+
* FAIL-LOUD on an unknown value.
|
|
49
|
+
* @param {string} [opts.bounceDirection='reflect'] — reflect | random (behavior=bounce).
|
|
50
|
+
* @param {string[]} [opts.hooks] — composable: mark | count | recolor.
|
|
51
|
+
* @param {string} [opts.preset] — mirror | brick | detector-screen |
|
|
52
|
+
* transparent-probe | wall. Construction sugar expanding to the fields above.
|
|
53
|
+
* @param {string} [opts.name] — display/debug identifier.
|
|
54
|
+
* @param {Array} [opts.color] — RGB [r,g,b] render color.
|
|
55
|
+
* @param {number} [opts.alpha] — render opacity 0-255.
|
|
56
|
+
* @param {boolean}[opts.visible] — drawn or not (render-only).
|
|
57
|
+
* @param {string} [opts.reposition='closest'] — eject direction.
|
|
58
|
+
* @param {number} [opts.layer] — dimension layer to scope to (null = all).
|
|
59
|
+
* @param {string} [opts.shellShape] — render shape hint (ball|box|circle|…).
|
|
60
|
+
* @param {boolean}[opts.collectCollided] — ADVANCED override of the behavior's
|
|
61
|
+
* default collect_collided flag. Omit for normal use (the `count` hook sets it).
|
|
62
|
+
* @param {number} [opts.capacity=64] — ABSORB only: the fill ceiling, and the
|
|
63
|
+
* `1/capacity` per-tick emission probability. How much it holds and how often it
|
|
64
|
+
* gives some back.
|
|
65
|
+
* @param {number} [opts.emissivity=0.5] — ABSORB only: radiating EFFICIENCY, as a
|
|
66
|
+
* FULLNESS fraction in (0,1] of each emitted leaf. This is a colour/temperature
|
|
67
|
+
* knob, not bookkeeping: emissivity fixes what the re-emitted quantum IS, and both
|
|
68
|
+
* its speed and its wavelength fall out of that fullness together — 1.0 emits a full
|
|
69
|
+
* leaf (P_eff 1, light speed, shortest λ), 0.5 a half-full one (P_eff ½, twice the
|
|
70
|
+
* λ), 0.25 a quarter-full one. FAIL-LOUD outside (0,1]: a fullness above 1 would be
|
|
71
|
+
* a move gate above 1. The emitted leaf's SCALE is not a knob — see `emissivity` in
|
|
72
|
+
* the datasheet: the wall re-emits at the layer it soaked from.
|
|
73
|
+
*
|
|
74
|
+
* Live methods:
|
|
75
|
+
* `setV2Prop(prop, value)` — retarget a lexicon field (collisionBehavior /
|
|
76
|
+
* bounceDirection / decoherence / hooks / markTag / countFilter / capacity /
|
|
77
|
+
* orientationDeg / pitchDeg) and RECOMPILE the running op-program.
|
|
78
|
+
* `emissivity` is also accepted and takes the short path: it is a radiator knob,
|
|
79
|
+
* not a collision mechanic, so it retunes the live radiator without a recompile.
|
|
80
|
+
* `getCollisionHistogram(tickData)` — for a COUNTING wall: accumulate + return
|
|
81
|
+
* `{ combined: Map<y,count>, grid: Map<y,Map<z,count>>, total }` from this LO's
|
|
82
|
+
* collided frames (`tickData.objects[handle].frames`). `combined` is the 1D
|
|
83
|
+
* y-profile (the 2D fringe histogram); `grid` is the SAME hits binned over the
|
|
84
|
+
* wall FACE (the 3D topology instrument) — one accumulation, two readings, and
|
|
85
|
+
* `Wall.deriveMarginalY(grid)` reproduces `combined` exactly. (For per-slit
|
|
86
|
+
* provenance, use the `DetectorWall` wrapper.) Throws on a wall with no `count` hook.
|
|
87
|
+
* `resetHistogram()` — clear the accumulator.
|
|
88
|
+
*
|
|
89
|
+
* ── Layer 2: physics behavior ────────────────────────────────────────────────
|
|
90
|
+
* ZERO physics of its own (SU1). Every collision effect — the eject-to-nearest-face
|
|
91
|
+
* (block), the incidence flip (bounce/reflect), the scatter redraw (bounce/random),
|
|
92
|
+
* the kill (destroy), the tally-and-radiate (absorb), the stamp/tally (mark/count) —
|
|
93
|
+
* is REALIZED by the compiled op-program the engine interprets, the SAME one every
|
|
94
|
+
* composite in the wall family produces. A `Wall` cannot move a bench golden; it
|
|
95
|
+
* only names + places the primitive.
|
|
96
|
+
*/
|
|
97
|
+
|
|
98
|
+
import { LargeObject } from './LargeObject.js';
|
|
99
|
+
import * as _WB from '../objects/constants/WallBehaviors.js';
|
|
100
|
+
import { CollisionTypes } from './CollisionTypes.js';
|
|
101
|
+
import * as AbsorbRadiation from '../objects/objects/absorbRadiation.js';
|
|
102
|
+
import * as SlabFace from '../objects/geometry/SlabFace.js';
|
|
103
|
+
import { registerKind } from '../objects/kinds.js';
|
|
104
|
+
import * as ValueNormalizer from '../objects/schema/valueNormalizer.js';
|
|
105
|
+
import { shapeValuesMap } from '../objects/schema/index.js';
|
|
106
|
+
import { wallBehaviorV2ValuesMap as _wallBehaviorV2ValuesMap, bounceDirectionValuesMap as _bounceDirectionValuesMap, wallHookValuesMap as _wallHookValuesMap } from '../objects/constants/WallBehaviors.js';
|
|
107
|
+
|
|
108
|
+
// ── Imports (Node.js) or globals (browser) ──────────────────────────
|
|
109
|
+
var resolveWallSpec, expandWallPreset;
|
|
110
|
+
resolveWallSpec = _WB.resolveWallSpec;
|
|
111
|
+
expandWallPreset = _WB.expandWallPreset;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Resolve the full slab geometry for a v2 wall from its rect + orientation, via the ONE
|
|
115
|
+
* SlabFace authority. Returns BOTH halves of the swept-OBB collision model:
|
|
116
|
+
*
|
|
117
|
+
* - `region` — the CONVENTION-FREE, center-less narrow-phase OBB descriptor
|
|
118
|
+
* ({ kind:'obb', theta, hw, hh, hz }) for a NON-cardinal wall, or null at a cardinal
|
|
119
|
+
* orientation (the byte-identical AABB path). The engine re-centers the LO rect on
|
|
120
|
+
* its backing frame each tick, so the region carries NO center.
|
|
121
|
+
* - `broadRect` — the LO's REGISTERED rect (the AABB broad phase, `octree.rs`). For a
|
|
122
|
+
* NON-cardinal wall this is EXPANDED to the rotated slab's axis-aligned bounding box
|
|
123
|
+
* (SlabFace.aabbHalfExtentsFromObb — the superset-discipline fix so a beam at the
|
|
124
|
+
* face TIP is a broad-phase candidate; the OBB region then culls the AABB corners).
|
|
125
|
+
* The expansion is symmetric about the slab center (rotation preserves the center),
|
|
126
|
+
* so the rect center — where the region resolves — is unchanged. At a cardinal
|
|
127
|
+
* orientation broadRect === the raw rect (byte-identical SU1).
|
|
128
|
+
* - `trueW`/`trueH` — the ORIGINAL (un-expanded) slab dimensions, ferried onto the LO
|
|
129
|
+
* so the renderer draws the THIN rotated mirror face (not the fat AABB). Undefined
|
|
130
|
+
* at a cardinal orientation (the raw rect IS the true slab → no ferry needed).
|
|
131
|
+
*
|
|
132
|
+
* @param {Array} rectArr — the LO rect ([l,t,w,h] or [l,t,z,w,h,d]).
|
|
133
|
+
* @param {number} orientationDeg — the wall's current orientation.
|
|
134
|
+
* @returns {{region:Object|null, broadRect:Array, trueW:(number|undefined), trueH:(number|undefined)}}
|
|
135
|
+
*/
|
|
136
|
+
function _slabGeometryForWall(rectArr, orientationDeg, pitchDeg) {
|
|
137
|
+
if (!SlabFace) { return { region: null, broadRect: rectArr }; } // pre-load safety
|
|
138
|
+
var wh = _rectWH(rectArr);
|
|
139
|
+
var depth = _rectDepth(rectArr);
|
|
140
|
+
var slab = SlabFace.resolveSlabFace({
|
|
141
|
+
w: wh.w, h: wh.h, orientationDeg: orientationDeg, d: depth, pitchDeg: pitchDeg
|
|
142
|
+
});
|
|
143
|
+
// R2AX — a PITCHED plate (two-axis mirror, CEO ruling 2026-07-26) has NO engine
|
|
144
|
+
// region (the ObbRegion contract is z-theta-only): the broad phase is the plate's
|
|
145
|
+
// full 3D AABB (SlabFace plate3.aabb — the one derivation) and the compiled guard
|
|
146
|
+
// trigger is the narrow phase. Engine two-angle OBB = flagged Gate-1 follow-up.
|
|
147
|
+
if (slab.pitched) {
|
|
148
|
+
var p3 = slab.plate3;
|
|
149
|
+
var whP = _rectWH(rectArr);
|
|
150
|
+
var cxP = rectArr[0] + whP.w / 2, cyP = rectArr[1] + whP.h / 2;
|
|
151
|
+
var zP = rectArr[2], dTot = rectArr[5];
|
|
152
|
+
var czP = zP + dTot / 2;
|
|
153
|
+
return {
|
|
154
|
+
region: null,
|
|
155
|
+
broadRect: [cxP - p3.aabb.hx, cyP - p3.aabb.hy, czP - p3.aabb.hz,
|
|
156
|
+
2 * p3.aabb.hx, 2 * p3.aabb.hy, 2 * p3.aabb.hz],
|
|
157
|
+
trueW: whP.w, trueH: whP.h
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
var region = SlabFace.regionFromObb(slab.obb);
|
|
161
|
+
if (!region) {
|
|
162
|
+
// cardinal → no OBB, no expansion (byte-identical AABB path).
|
|
163
|
+
return { region: null, broadRect: rectArr };
|
|
164
|
+
}
|
|
165
|
+
// NON-cardinal → expand the broad-phase rect to the rotated-slab AABB (same center).
|
|
166
|
+
var aabb = SlabFace.aabbHalfExtentsFromObb(slab.obb);
|
|
167
|
+
var is6 = Array.isArray(rectArr) && rectArr.length === 6;
|
|
168
|
+
var left = rectArr[0], top = rectArr[1];
|
|
169
|
+
var cx = left + wh.w / 2, cy = top + wh.h / 2;
|
|
170
|
+
var broadRect;
|
|
171
|
+
if (is6) {
|
|
172
|
+
var z = rectArr[2], d = rectArr[5];
|
|
173
|
+
broadRect = [cx - aabb.hx, cy - aabb.hy, z, 2 * aabb.hx, 2 * aabb.hy, d];
|
|
174
|
+
} else {
|
|
175
|
+
broadRect = [cx - aabb.hx, cy - aabb.hy, 2 * aabb.hx, 2 * aabb.hy];
|
|
176
|
+
}
|
|
177
|
+
return { region: region, broadRect: broadRect, trueW: wh.w, trueH: wh.h };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function _rectFromOpts(opts) {
|
|
181
|
+
// Normalize an OBJECT-bag rect ({left,top,w,h} / {left,top,z,w,h,d} — the shape a
|
|
182
|
+
// codegen round-trip captures from the live LO, restart-bug fix 2026-07-25) to the
|
|
183
|
+
// canonical ARRAY form FIRST, so the 3D checks below (_rectIs3D/_rectDepth are
|
|
184
|
+
// Array-based) see a captured 3D wall as 3D. Mirrors LargeObject's own rectToArray.
|
|
185
|
+
if (opts.rect && !Array.isArray(opts.rect) && opts.rect.left !== undefined && opts.rect.w !== undefined) {
|
|
186
|
+
var ob = opts.rect;
|
|
187
|
+
var arr = (ob.z !== undefined || ob.d !== undefined)
|
|
188
|
+
? [ob.left, ob.top, (ob.z !== undefined ? ob.z : 0), ob.w, ob.h,
|
|
189
|
+
(ob.d !== undefined ? ob.d : ob.w)]
|
|
190
|
+
: [ob.left, ob.top, ob.w, ob.h];
|
|
191
|
+
opts = Object.assign({}, opts, { rect: arr });
|
|
192
|
+
}
|
|
193
|
+
if (opts.rect) {
|
|
194
|
+
// Z1 — promote a 4-element rect to the explicit z/depth 6-element form when the
|
|
195
|
+
// `depth`/`z` 3D sugar is supplied (a wall you place in 2D but give a z-slab).
|
|
196
|
+
// A rect already in 6-element form, or a bare 4-element with no z/depth sugar,
|
|
197
|
+
// passes through verbatim (byte-identical).
|
|
198
|
+
if (Array.isArray(opts.rect) && opts.rect.length === 4 &&
|
|
199
|
+
(opts.depth !== undefined || opts.z !== undefined)) {
|
|
200
|
+
var r = opts.rect;
|
|
201
|
+
return [r[0], r[1], (opts.z !== undefined ? opts.z : 0), r[2], r[3],
|
|
202
|
+
(opts.depth !== undefined ? opts.depth : r[2])];
|
|
203
|
+
}
|
|
204
|
+
return opts.rect;
|
|
205
|
+
}
|
|
206
|
+
if (opts.z !== undefined || opts.d !== undefined || opts.depth !== undefined) {
|
|
207
|
+
return [opts.left, opts.top, (opts.z !== undefined ? opts.z : 0),
|
|
208
|
+
opts.w, opts.h,
|
|
209
|
+
(opts.depth !== undefined ? opts.depth : (opts.d !== undefined ? opts.d : opts.w))];
|
|
210
|
+
}
|
|
211
|
+
return [opts.left, opts.top, opts.w, opts.h];
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Read w/h out of a rect array ([l,t,w,h] or [l,t,z,w,h,d]) for the reflect-prefix. */
|
|
215
|
+
function _rectWH(rectArr) {
|
|
216
|
+
if (Array.isArray(rectArr) && rectArr.length === 6) { return { w: rectArr[3], h: rectArr[4] }; }
|
|
217
|
+
if (Array.isArray(rectArr)) { return { w: rectArr[2], h: rectArr[3] }; }
|
|
218
|
+
return { w: rectArr.w, h: rectArr.h };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** True when a rect is the 6-element z/depth form with a real (>0) z-slab depth. */
|
|
222
|
+
function _rectIs3D(rectArr) {
|
|
223
|
+
return Array.isArray(rectArr) && rectArr.length === 6 && typeof rectArr[5] === 'number' && rectArr[5] > 0;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Read the depth (z-extent) out of a rect array (0 for a 2D 4-element rect). */
|
|
227
|
+
function _rectDepth(rectArr) {
|
|
228
|
+
return (Array.isArray(rectArr) && rectArr.length === 6) ? rectArr[5] : 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Collision LEXICON v2 construction (nothing|block|bounce|absorb|destroy + nested
|
|
233
|
+
* direction/modifiers + composable mark/count hooks). Compiles the ONE program via
|
|
234
|
+
* resolveWallSpec → compileWallV2 (the single compiler authority), registers a bare
|
|
235
|
+
* LO with the registration-time collect flag, pushes the program, and — for ABSORB —
|
|
236
|
+
* attaches a REAL backing frame + registers the radiation host tick-fn (Track A). SU1.
|
|
237
|
+
*/
|
|
238
|
+
function _constructV2(self, universe, opts) {
|
|
239
|
+
var behavior = opts.collisionBehavior;
|
|
240
|
+
self.collisionBehavior = behavior;
|
|
241
|
+
self.bounceDirection = opts.bounceDirection || (behavior === 'bounce' ? 'reflect' : undefined);
|
|
242
|
+
self.angleConstraint = opts.angleConstraint;
|
|
243
|
+
self.decoherence = !!opts.decoherence;
|
|
244
|
+
self.hooks = Array.isArray(opts.hooks) ? opts.hooks.slice() : [];
|
|
245
|
+
self.markTag = opts.markTag;
|
|
246
|
+
self.countFilter = opts.countFilter;
|
|
247
|
+
self.capacity = (typeof opts.capacity === 'number') ? opts.capacity : 64;
|
|
248
|
+
// ABSORB radiating efficiency — a FULLNESS fraction of the emitted leaf. Left undefined
|
|
249
|
+
// when the caller does not state one, so the ONE default lives in absorbRadiation.js.
|
|
250
|
+
self.emissivity = (typeof opts.emissivity === 'number') ? opts.emissivity : undefined;
|
|
251
|
+
self.orientationDeg = (typeof opts.orientationDeg === 'number') ? opts.orientationDeg : 0;
|
|
252
|
+
// R2AX (CEO two-axis ruling 2026-07-26) — up/down tilt about the plate's own long
|
|
253
|
+
// axis. 0/absent ⇒ byte-identical un-pitched wall. Requires a 3D wall (validated
|
|
254
|
+
// below once the rect is resolved; SlabFace + compileWallV2 double-guard).
|
|
255
|
+
self.pitchDeg = (typeof opts.pitchDeg === 'number') ? opts.pitchDeg : 0;
|
|
256
|
+
|
|
257
|
+
var rect = _rectFromOpts(opts);
|
|
258
|
+
var baseName = opts.name || 'wall';
|
|
259
|
+
self.name = baseName;
|
|
260
|
+
// Z1 — a 6-element rect with a real depth is a 3D wall: it reflects on all three
|
|
261
|
+
// axes (the z-incidence lift) and ejects on all six faces. The eject-region promotion
|
|
262
|
+
// to 'closest3d' happens inside compileWallV2 (is3D), so both the compiled program AND
|
|
263
|
+
// the LargeObject reposition agree; we mirror it here for the LO wrapper reposition.
|
|
264
|
+
var is3D = _rectIs3D(rect);
|
|
265
|
+
var reposition = opts.reposition || (is3D ? 'closest3d' : 'closest');
|
|
266
|
+
// observed dimensions: an explicit opts.layer scopes the wall to that layer; unset =
|
|
267
|
+
// ALL dimensions. The former absorb-only `layer=0` workaround (dodging the multi-dim
|
|
268
|
+
// collect_collided wipe) is REMOVED — the engine now clears the collect buffer once per
|
|
269
|
+
// tick and ACCUMULATES across all observed dimensions (K5 fix), so an absorb wall
|
|
270
|
+
// correctly collects colliders wherever the leaves live, not just in a hardcoded dim 0.
|
|
271
|
+
var layer = opts.layer;
|
|
272
|
+
var observedDimensions = (layer !== undefined && layer !== null) ? [layer] : null;
|
|
273
|
+
|
|
274
|
+
var _wh0 = _rectWH(rect);
|
|
275
|
+
var v2 = resolveWallSpec({
|
|
276
|
+
collisionBehavior: behavior, bounceDirection: self.bounceDirection,
|
|
277
|
+
angleConstraint: self.angleConstraint, decoherence: self.decoherence,
|
|
278
|
+
hooks: self.hooks, markTag: self.markTag, capacity: self.capacity,
|
|
279
|
+
emissivity: self.emissivity,
|
|
280
|
+
// the wall's drawn color → the ONE authority the `recolor` hook tags with.
|
|
281
|
+
color: opts.color,
|
|
282
|
+
orientationDeg: self.orientationDeg, reposition: reposition, is3D: is3D,
|
|
283
|
+
slabW: _wh0.w, slabH: _wh0.h,
|
|
284
|
+
// R2AX — pitch + depth ferry (pitch 0 ⇒ byte-identical programs).
|
|
285
|
+
pitchDeg: self.pitchDeg, slabD: _rectDepth(rect)
|
|
286
|
+
});
|
|
287
|
+
// `opts.collectCollided` is a DECLARED Wall option (see this module's docstring) and
|
|
288
|
+
// the legacy path has always honored it as an override of the behavior's default.
|
|
289
|
+
// The v2 path was reading ONLY `v2.collectCollided` (the compiler's derived flag), so
|
|
290
|
+
// the documented override silently did nothing on a `collisionBehavior` wall — the
|
|
291
|
+
// gap a Tunnel in directed mode hits: its walls carry NO behavior that implies
|
|
292
|
+
// collection (they are 'nothing' + an eject-only program), yet the host redirect
|
|
293
|
+
// needs `tickData.objects[handle].frames` to exist. Collection is a REGISTRATION-time
|
|
294
|
+
// engine flag (no live toggle — see setV2Prop's own guard), which is exactly why
|
|
295
|
+
// it must be settable at construction. Omitted ⇒ the compiler's flag ⇒ byte-identical.
|
|
296
|
+
var collectCollided = (opts.collectCollided !== undefined)
|
|
297
|
+
? !!opts.collectCollided : !!v2.collectCollided;
|
|
298
|
+
self._collectCollided = collectCollided;
|
|
299
|
+
self._radiation = v2.radiation;
|
|
300
|
+
|
|
301
|
+
// SWEPT-OBB geometry (circulation §2 + MIRROR broad-phase-superset ruling): resolve
|
|
302
|
+
// BOTH the narrow-phase region AND the broad-phase rect from the ONE SlabFace authority.
|
|
303
|
+
// The JS wrapper LO keeps the TRUE slab rect (render draws the thin rotated face + the
|
|
304
|
+
// renderer/eject stay byte-identical); the ENGINE's registered rect is EXPANDED to the
|
|
305
|
+
// rotated-slab AABB below (broad-phase superset) so a face-tip beam is a candidate — the
|
|
306
|
+
// OBB region then culls the AABB corners back to the drawn face. Cardinal → no expansion.
|
|
307
|
+
self._trueRect = rect;
|
|
308
|
+
var geom = _slabGeometryForWall(rect, self.orientationDeg, self.pitchDeg);
|
|
309
|
+
|
|
310
|
+
// Bare LO (no auto-compiled actions) with the collect flag AT REGISTRATION, then push
|
|
311
|
+
// the v2 program (the same live-push path recompileActions uses). Construct on the TRUE
|
|
312
|
+
// slab rect; the engine broad-phase rect is widened to the AABB after (region push below).
|
|
313
|
+
self.lo = new LargeObject(universe, rect, 'NONE', [], baseName, reposition, observedDimensions, 1, opts.color, collectCollided);
|
|
314
|
+
// THE ONE UP-LINK: a composer stamps `_owner` on what it composed (the Wall composed its LO).
|
|
315
|
+
self.lo._owner = self;
|
|
316
|
+
// Ferry orientationDeg onto the LO wrapper (mirrors how shellShape/color are ferried)
|
|
317
|
+
// so the renderer providers draw the shell rotated (R2).
|
|
318
|
+
// The engine never sees it (LargeObject is a plain render/collide wrapper here).
|
|
319
|
+
self.lo.orientationDeg = self.orientationDeg;
|
|
320
|
+
// R2AX — ferry the pitch for the renderer (drawn face is truth: the provider tips
|
|
321
|
+
// the plate about its long axis to match SlabFace's plate3.n). Render-only mirror
|
|
322
|
+
// of the physics angle, like orientationDeg. Only ferried when nonzero so an
|
|
323
|
+
// un-pitched wall's LO surface stays byte-identical.
|
|
324
|
+
if (self.pitchDeg) { self.lo.pitchDeg = self.pitchDeg; }
|
|
325
|
+
// Z1 — ferry the z-slab depth onto the LO wrapper so the three provider draws the
|
|
326
|
+
// wall as a real BoxGeometry slab (w×h×depth).
|
|
327
|
+
// (The engine already has the z-slab collision region from the 6-element rect; this
|
|
328
|
+
// is the render-only mirror, like orientationDeg.) Only set for a 3D wall so a 2D
|
|
329
|
+
// wall's LO signature/fit stays byte-identical (undefined → flat plane).
|
|
330
|
+
if (is3D) { self.lo.depth = _rectDepth(rect); }
|
|
331
|
+
self.is3D = is3D;
|
|
332
|
+
if (v2.program && universe && typeof universe.updateObjectProperty === 'function') {
|
|
333
|
+
universe.updateObjectProperty(self.lo.handle, 'LargeObject', 'program', v2.program);
|
|
334
|
+
}
|
|
335
|
+
self.lo.program = v2.program || null;
|
|
336
|
+
// The program THIS wall baked — the record the re-bake guard (`_rebake`) checks, so a
|
|
337
|
+
// program another part pushed onto this LO is never silently re-baked over.
|
|
338
|
+
self._program = self.lo.program;
|
|
339
|
+
// SWEPT-OBB narrow-phase region (circulation §2): a NON-cardinal wall gets the rotated
|
|
340
|
+
// collision slab so the engine filters candidates by the SWEPT prev→now segment-vs-OBB
|
|
341
|
+
// test AT its DRAWN face (not the fat AABB entry plane), fixing membership for EVERY
|
|
342
|
+
// consumer (reflect/absorb/count/detect) at once. Cardinal → null = the byte-identical
|
|
343
|
+
// AABB path (SU1). Pushed on the SAME live path as `program`. WASM-only: the JS provider
|
|
344
|
+
// FAILS LOUD on a non-null region (the frozen 2D fixture cannot honor a rotated slab).
|
|
345
|
+
var region = geom.region;
|
|
346
|
+
if (universe && typeof universe.updateObjectProperty === 'function') {
|
|
347
|
+
universe.updateObjectProperty(self.lo.handle, 'LargeObject', 'region', region);
|
|
348
|
+
// BROAD-PHASE SUPERSET: widen ONLY the ENGINE's registered rect to the rotated-slab
|
|
349
|
+
// AABB (same center). The engine re-centers this rect on the backing frame each tick
|
|
350
|
+
// keeping w/h, so the wider AABB persists; the JS wrapper's lo.rect stays the true
|
|
351
|
+
// slab (render/eject-reference untouched). Cardinal → broadRect === rect → no push
|
|
352
|
+
// (byte-identical SU1). WASM-only alongside the region.
|
|
353
|
+
// R2AX FIX (measured, 2026-07-26): the push is gated on the WIDENING itself, never
|
|
354
|
+
// on `region` — a PITCHED plate has region:null BY DESIGN (z-theta-only contract)
|
|
355
|
+
// yet NEEDS its 3D-AABB broad rect; gating on region left the raw thin slab as the
|
|
356
|
+
// broad phase and face-crossings OUTSIDE it were never candidates (the sweep-matrix
|
|
357
|
+
// pass-through class).
|
|
358
|
+
if (geom.broadRect !== rect) {
|
|
359
|
+
universe.updateObjectProperty(self.lo.handle, 'LargeObject', 'rect', geom.broadRect);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
self.lo.region = region;
|
|
363
|
+
if (opts.alpha !== undefined) { self.lo.alpha = opts.alpha; }
|
|
364
|
+
if (opts.visible !== undefined) { self.lo.visible = opts.visible; }
|
|
365
|
+
if (opts.shellShape !== undefined) { self.lo.shellShape = opts.shellShape; }
|
|
366
|
+
|
|
367
|
+
// ABSORB v2 (Track A): back the LO with a REAL leaf frame + register the radiation
|
|
368
|
+
// host tick-fn (the emitter/thermostat seam). The backing frame is ASSUMED to exist
|
|
369
|
+
// (no fallback) per the mandatory-at-registration directive.
|
|
370
|
+
self._radiationTickFn = null; self._backing = null;
|
|
371
|
+
if (behavior === 'absorb' && AbsorbRadiation) {
|
|
372
|
+
var back = AbsorbRadiation.attachAbsorberBackingFrame(universe, self.lo.handle, rect, self.capacity);
|
|
373
|
+
self._backing = back;
|
|
374
|
+
// The absorb CLOCK (`back.capacity`) is what the radiator runs on; `back.level` is
|
|
375
|
+
// only the record of how that ceiling was quantized. The EMISSION level is not a
|
|
376
|
+
// knob — it is read per emission off the layer the mass was absorbed from.
|
|
377
|
+
var tickFn = AbsorbRadiation.buildAbsorbRadiationTickFn({
|
|
378
|
+
universe: universe, loHandle: self.lo.handle, backingQid: back.backingQid,
|
|
379
|
+
capacity: back.capacity, rect: rect,
|
|
380
|
+
emissivity: (v2.radiation && v2.radiation.emissivity)
|
|
381
|
+
});
|
|
382
|
+
self._radiationTickFn = tickFn;
|
|
383
|
+
// The radiator OWNS the live emissivity (including the default when none was
|
|
384
|
+
// stated). Read the facade prop back off it rather than leaving it undefined —
|
|
385
|
+
// captureProps/codegen and the Inspector read `wall.emissivity`, and a facade that
|
|
386
|
+
// says nothing while the radiator says 0.5 is a second, wrong answer.
|
|
387
|
+
self.emissivity = tickFn.emissivity();
|
|
388
|
+
if (universe && Array.isArray(universe.experimentTickFunctions)) {
|
|
389
|
+
universe.experimentTickFunctions.push(tickFn);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
self._combined = new Map();
|
|
394
|
+
self._grid = new Map();
|
|
395
|
+
self._total = 0;
|
|
396
|
+
self._lastTickData = undefined;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
function Wall(universe, opts) {
|
|
400
|
+
opts = opts || {};
|
|
401
|
+
this.universe = universe;
|
|
402
|
+
this._schemaKind = 'Wall';
|
|
403
|
+
|
|
404
|
+
// A `preset` expands to the v2 descriptor fields (construction sugar; codegen
|
|
405
|
+
// round-trips the EXPANDED fields, never the preset name).
|
|
406
|
+
if (opts.preset) {
|
|
407
|
+
var expanded = expandWallPreset(opts.preset);
|
|
408
|
+
var merged = {}; Object.keys(opts).forEach(function (k) { if (k !== 'preset') { merged[k] = opts[k]; } });
|
|
409
|
+
Object.keys(expanded).forEach(function (k) { if (merged[k] === undefined) { merged[k] = expanded[k]; } });
|
|
410
|
+
opts = merged;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ── Collision LEXICON v2 — the ONE input path ────────────────────────────
|
|
414
|
+
// The legacy `wallBehavior` vocabulary (block|reflect|absorb|detect) is DELETED.
|
|
415
|
+
// A caller still speaking it is named its V2 form rather than silently building
|
|
416
|
+
// a different wall.
|
|
417
|
+
if (opts.wallBehavior !== undefined) {
|
|
418
|
+
throw new Error('[Wall] `wallBehavior` is deleted — use `collisionBehavior`: ' +
|
|
419
|
+
'block → collisionBehavior:\'block\'; reflect → collisionBehavior:\'bounce\', ' +
|
|
420
|
+
'bounceDirection:\'reflect\'; absorb (plain kill) → collisionBehavior:\'destroy\'; ' +
|
|
421
|
+
'detect → collisionBehavior:\'destroy\', hooks:[\'count\'] (= preset ' +
|
|
422
|
+
'\'detector-screen\'). Note collisionBehavior:\'absorb\' is the RADIATING ' +
|
|
423
|
+
'absorber, NOT the old plain kill.');
|
|
424
|
+
}
|
|
425
|
+
if (opts.collisionType !== undefined) {
|
|
426
|
+
throw new Error('[Wall] the `collisionType` override is deleted — a wall\'s ' +
|
|
427
|
+
'collision mechanics ARE its compiled op-program (collisionBehavior + ' +
|
|
428
|
+
'bounceDirection + hooks).');
|
|
429
|
+
}
|
|
430
|
+
if (opts.collisionBehavior === undefined) {
|
|
431
|
+
throw new Error('[Wall] `collisionBehavior` is REQUIRED — one of ' +
|
|
432
|
+
'nothing|block|bounce|absorb|destroy (or pass a `preset`).');
|
|
433
|
+
}
|
|
434
|
+
return _constructV2(this, universe, opts);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// ── Descriptor-field read surface (captureProps / Inspector) ─────────────
|
|
438
|
+
// captureProps(wall, WallDescriptor) reads wall[field.name] directly; these
|
|
439
|
+
// getters expose the LIVE inner-LO state so a dragged/edited wall Saves at its
|
|
440
|
+
// current state (the codegen round-trip authority). `rect` is the single
|
|
441
|
+
// captured position authority (the LO's own Rectangle, whose toJSON pins the
|
|
442
|
+
// {left,top,w,h} wire shape).
|
|
443
|
+
Object.defineProperty(Wall.prototype, 'rect', { get: function () { return this.lo ? this.lo.rect : undefined; } });
|
|
444
|
+
Object.defineProperty(Wall.prototype, 'handle', { get: function () { return this.lo ? this.lo.handle : undefined; } });
|
|
445
|
+
Object.defineProperty(Wall.prototype, 'color', { get: function () { return this.lo ? this.lo.color : undefined; } });
|
|
446
|
+
Object.defineProperty(Wall.prototype, 'alpha', { get: function () { return this.lo ? this.lo.alpha : undefined; } });
|
|
447
|
+
Object.defineProperty(Wall.prototype, 'visible', { get: function () { return this.lo ? this.lo.visible : undefined; } });
|
|
448
|
+
Object.defineProperty(Wall.prototype, 'layer', { get: function () { return this.lo ? this.lo.layer : undefined; } });
|
|
449
|
+
Object.defineProperty(Wall.prototype, 'reposition', { get: function () { return this.lo ? this.lo.reposition : undefined; } });
|
|
450
|
+
Object.defineProperty(Wall.prototype, 'shellShape', { get: function () { return this.lo ? this.lo.shellShape : undefined; } });
|
|
451
|
+
// Z1 — z-slab read-out (derived from the LO's Rectangle; 0 for a 2D wall). The rect is
|
|
452
|
+
// the single live position authority; these are inspectable, round-tripped by the sugar.
|
|
453
|
+
Object.defineProperty(Wall.prototype, 'depth', { get: function () { return (this.lo && this.lo.rect && typeof this.lo.rect.d === 'number') ? this.lo.rect.d : 0; } });
|
|
454
|
+
Object.defineProperty(Wall.prototype, 'z', { get: function () { return (this.lo && this.lo.rect && typeof this.lo.rect.z === 'number') ? this.lo.rect.z : 0; } });
|
|
455
|
+
|
|
456
|
+
// A Wall exposes NO getCenter: its position authority is its RECT (the TRUE slab,
|
|
457
|
+
// `_trueRect`), written ONLY through setRect below. A GRABBED wall (STUDIO's spacebar aim,
|
|
458
|
+
// AIM_GRAB_FEEDBACK_CONTRACT §The carry) holds its camera-relative pose: the runner re-places
|
|
459
|
+
// the rect's drawn centre (lo-drawn-center rectAtCenter) through updateObjectProperty(wall,
|
|
460
|
+
// 'rect') → setRect, and carries the heading through setOrientation.
|
|
461
|
+
|
|
462
|
+
/** getOrientation() → the wall's current orientation in degrees (0 default). */
|
|
463
|
+
Wall.prototype.getOrientation = function () {
|
|
464
|
+
return (typeof this.orientationDeg === 'number') ? this.orientationDeg : 0;
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* setOrientation(deg) — live re-aim (B7). Normalizes to [0,360) and routes through
|
|
469
|
+
* the SAME setV2Prop recompile path the Inspector slider uses (explicit re-bake of the
|
|
470
|
+
* directional cone — the theorist-sanctioned "a wall that should re-aim must recompile"
|
|
471
|
+
* path; NOT a per-tick re-capture). SU1 at deg=0. THROWS on a wall-shaped object with no
|
|
472
|
+
* collisionBehavior: the constructor requires one, so such an object was not built by Wall
|
|
473
|
+
* and has no op-program to re-aim — turning it silently would show a turn that never happened.
|
|
474
|
+
* @param {number} deg @returns {Wall} this
|
|
475
|
+
*/
|
|
476
|
+
Wall.prototype.setOrientation = function (deg) {
|
|
477
|
+
if (this.collisionBehavior === undefined) {
|
|
478
|
+
throw new Error('[Wall.setOrientation] this wall has no collisionBehavior, so it has no ' +
|
|
479
|
+
'compiled op-program to re-aim — a Wall is built only through the v2 lexicon ' +
|
|
480
|
+
'(new Wall(universe, { collisionBehavior | preset })); an object without one was not ' +
|
|
481
|
+
'built by Wall. (FAIL-LOUD.)');
|
|
482
|
+
}
|
|
483
|
+
var d = ((deg % 360) + 360) % 360;
|
|
484
|
+
this.setV2Prop('orientationDeg', d);
|
|
485
|
+
return this;
|
|
486
|
+
};
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* setRect(rect) — THE wall position/extent write (the rect is the one position authority).
|
|
490
|
+
* Every rect write reaches it: FrqtlApp.updateObjectProperty routes a Wall facade's `rect`, and
|
|
491
|
+
* its inner LO's, here. `rect` is the write-wire array `[l,t,w,h]` or `[l,t,z,w,h,d]`:
|
|
492
|
+
* - on a z-slab wall (6-element) a 4-element write is PLANAR: z and d are kept;
|
|
493
|
+
* - a 6-element write on a wall built without z THROWS — the wall's 3D-ness (the z-incidence
|
|
494
|
+
* reflect + the closest3d eject compiled into its program) is fixed at construction.
|
|
495
|
+
* It keeps every wall-derived place in step: the true slab (`_trueRect`, which every later
|
|
496
|
+
* setV2Prop re-derives from), the LO wrapper's drawn rect, the ENGINE rect (the broad-phase
|
|
497
|
+
* rect from the ONE SlabFace authority — the rotated-slab AABB for a tilted wall; the engine
|
|
498
|
+
* moves the backing frame with it), and an absorber's re-emission exit. A changed extent
|
|
499
|
+
* re-bakes the program too (the slab dimensions are compiled in). Zero physics: placement only.
|
|
500
|
+
* @param {number[]} rect @returns {Wall} this
|
|
501
|
+
*/
|
|
502
|
+
Wall.prototype.setRect = function (rect) {
|
|
503
|
+
if (!Array.isArray(rect) || (rect.length !== 4 && rect.length !== 6) ||
|
|
504
|
+
!rect.every(function (v) { return typeof v === 'number' && isFinite(v); })) {
|
|
505
|
+
throw new TypeError('[Wall.setRect] rect must be [l,t,w,h] or [l,t,z,w,h,d] of finite ' +
|
|
506
|
+
'numbers; got ' + JSON.stringify(rect) + '. (FAIL-LOUD.)');
|
|
507
|
+
}
|
|
508
|
+
var cur = this._trueRect;
|
|
509
|
+
var cur6 = Array.isArray(cur) && cur.length === 6;
|
|
510
|
+
var next;
|
|
511
|
+
if (rect.length === 6) {
|
|
512
|
+
if (!cur6) {
|
|
513
|
+
throw new Error('[Wall.setRect] "' + this.name + '" was built without a z-slab; a ' +
|
|
514
|
+
'6-element rect would give it one, but a wall\'s 3D-ness is compiled into its ' +
|
|
515
|
+
'op-program at construction. Remove + re-add it with z/depth. (FAIL-LOUD.)');
|
|
516
|
+
}
|
|
517
|
+
next = rect.slice();
|
|
518
|
+
} else {
|
|
519
|
+
next = cur6 ? [rect[0], rect[1], cur[2], rect[2], rect[3], cur[5]] : rect.slice();
|
|
520
|
+
}
|
|
521
|
+
var wh0 = _rectWH(cur), wh1 = _rectWH(next);
|
|
522
|
+
var resized = wh0.w !== wh1.w || wh0.h !== wh1.h || _rectDepth(cur) !== _rectDepth(next);
|
|
523
|
+
if (resized) { this.assertOwnsProgram(); } // atomic: refuse before anything moves
|
|
524
|
+
this._trueRect = next;
|
|
525
|
+
var lr = this.lo.rect;
|
|
526
|
+
lr.w = wh1.w; lr.h = wh1.h;
|
|
527
|
+
if (cur6) { lr.z = next[2]; lr.d = next[5]; }
|
|
528
|
+
lr.left = next[0]; lr.top = next[1];
|
|
529
|
+
if (this.is3D) { this.lo.depth = _rectDepth(next); }
|
|
530
|
+
if (resized) {
|
|
531
|
+
this._rebake(); // the slab dimensions are compiled in: program + region + broad rect
|
|
532
|
+
} else if (this.universe && typeof this.universe.updateObjectProperty === 'function') {
|
|
533
|
+
var geom = _slabGeometryForWall(next, this.orientationDeg, this.pitchDeg);
|
|
534
|
+
this.universe.updateObjectProperty(this.lo.handle, 'LargeObject', 'rect', geom.broadRect);
|
|
535
|
+
}
|
|
536
|
+
if (this._radiationTickFn) { this._radiationTickFn.setRect(next); }
|
|
537
|
+
return this;
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* R2AX — live PITCH (up/down, two-axis mirror; CEO ruling 2026-07-26). Degrees in
|
|
542
|
+
* [-90, 90], rotation about the plate's own long axis; routes through the SAME
|
|
543
|
+
* setV2Prop recompile path as orientation (re-bakes the 3-space reflection about the
|
|
544
|
+
* SlabFace plate normal + the n-dot guard + the 3D-AABB broad rect). Requires a 3D
|
|
545
|
+
* wall (throws otherwise — the FAIL-LOUD accessor discipline).
|
|
546
|
+
* Layer 2 — physics: tips the mirror face out of the xy-plane, so the reflected beam
|
|
547
|
+
* gains/loses a z-component per R = I − 2nnᵀ (pitch 0 restores the in-plane 2θ law
|
|
548
|
+
* with vz passing through). The reflected spray always leaves on the incident side.
|
|
549
|
+
* @param {number} d degrees, [-90, 90].
|
|
550
|
+
* @returns {Wall} this
|
|
551
|
+
*/
|
|
552
|
+
Wall.prototype.setPitch = function (d) {
|
|
553
|
+
this.setV2Prop('pitchDeg', d);
|
|
554
|
+
return this;
|
|
555
|
+
};
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Live-edit a Collision LEXICON v2 field (bounceDirection / decoherence / hooks /
|
|
559
|
+
* markTag / countFilter / capacity / collisionBehavior) — updates the facade prop IN
|
|
560
|
+
* PLACE (so the Inspector's visibleWhen re-read reveals dependent fields immediately)
|
|
561
|
+
* and RECOMPILES the running op-program via resolveWallSpec (the same live-push path).
|
|
562
|
+
*
|
|
563
|
+
* Two registration-time guards (FAIL-LOUD, atomic — throw before any mutation):
|
|
564
|
+
* - collection (collect_collided) is registration-time; refuse a live switch that
|
|
565
|
+
* would NEWLY need collection (adding count/mark, or an absorb behavior) on a wall
|
|
566
|
+
* not registered to collect.
|
|
567
|
+
* - the ABSORB backing frame + radiation tick-fn are attached at construction; refuse
|
|
568
|
+
* a live switch INTO/out of absorb (reconstruct the wall instead).
|
|
569
|
+
*
|
|
570
|
+
* `emissivity` takes the SHORT path below: it is a radiator knob, not a collision
|
|
571
|
+
* mechanic, so it changes nothing in the compiled op-program and recompiling for it would
|
|
572
|
+
* be theatre.
|
|
573
|
+
*/
|
|
574
|
+
Wall.prototype.setV2Prop = function (prop, value) {
|
|
575
|
+
// EMISSIVITY — live radiating efficiency. The running radiator OWNS the value (one
|
|
576
|
+
// live copy); the facade prop is a ferried mirror for the Inspector/codegen read.
|
|
577
|
+
// Validation (fullness fraction in (0,1]) belongs to the owner and throws from there.
|
|
578
|
+
if (prop === 'emissivity') {
|
|
579
|
+
if (this.collisionBehavior !== 'absorb' || !this._radiationTickFn) {
|
|
580
|
+
throw new Error('[Wall.setV2Prop] emissivity is a RADIATOR knob and this wall is not ' +
|
|
581
|
+
'an ABSORB wall (collisionBehavior "' + this.collisionBehavior + '").');
|
|
582
|
+
}
|
|
583
|
+
this.emissivity = this._radiationTickFn.setEmissivity(value);
|
|
584
|
+
return this;
|
|
585
|
+
}
|
|
586
|
+
return this._rebake(prop, value);
|
|
587
|
+
};
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* THE re-bake (one site): re-derive the wall's program, narrow-phase region and engine broad
|
|
591
|
+
* rect from its spec + true slab through the ONE compiler, committing `prop = value` first when
|
|
592
|
+
* a lexicon prop is being edited (setV2Prop) — or with no prop, re-deriving from the current
|
|
593
|
+
* state (setRect's resize: the slab dimensions are compiled in).
|
|
594
|
+
*
|
|
595
|
+
* GUARD (FAIL-LOUD, atomic — before any mutation): a Wall re-bakes only a program IT baked.
|
|
596
|
+
* A composite may push its own program onto this wall's LO (a directed Tunnel's liner-scatter
|
|
597
|
+
* eject, a Tunnel exit gate, an OvenBox counter wall); re-baking would silently replace that
|
|
598
|
+
* part's physics with the wall's own. Such a program must be re-applied by the part that owns it.
|
|
599
|
+
* @param {string} [prop] @param {*} [value] @returns {Wall} this
|
|
600
|
+
*/
|
|
601
|
+
Wall.prototype._rebake = function (prop, value) {
|
|
602
|
+
this.assertOwnsProgram();
|
|
603
|
+
var nextBehavior = (prop === 'collisionBehavior') ? value : this.collisionBehavior;
|
|
604
|
+
var wasAbsorb = (this.collisionBehavior === 'absorb');
|
|
605
|
+
var willAbsorb = (nextBehavior === 'absorb');
|
|
606
|
+
if (wasAbsorb !== willAbsorb) {
|
|
607
|
+
throw new Error('[Wall.setV2Prop] cannot live-switch ' + (willAbsorb ? 'INTO' : 'out of') +
|
|
608
|
+
' the radiating ABSORB behavior — its backing frame + radiation tick-fn are attached ' +
|
|
609
|
+
'at construction. Remove + re-add the wall as absorb.');
|
|
610
|
+
}
|
|
611
|
+
var nextHooks = (prop === 'hooks') ? (Array.isArray(value) ? value.slice() : []) : this.hooks;
|
|
612
|
+
var _trueWH = _rectWH(this._trueRect);
|
|
613
|
+
var probe = resolveWallSpec({
|
|
614
|
+
collisionBehavior: nextBehavior, bounceDirection: (prop === 'bounceDirection') ? value : this.bounceDirection,
|
|
615
|
+
angleConstraint: this.angleConstraint, decoherence: (prop === 'decoherence') ? !!value : this.decoherence,
|
|
616
|
+
hooks: nextHooks, markTag: (prop === 'markTag') ? value : this.markTag,
|
|
617
|
+
capacity: (prop === 'capacity') ? value : this.capacity,
|
|
618
|
+
// B7: a live orientation edit re-bakes the directional cone (explicit recompile —
|
|
619
|
+
// the theorist-sanctioned re-aim path). Use the NEW value for the probe (the
|
|
620
|
+
// generic commit below sets this.orientationDeg AFTER the probe is built).
|
|
621
|
+
orientationDeg: (prop === 'orientationDeg') ? value : this.orientationDeg,
|
|
622
|
+
// R2AX — a live pitch edit re-bakes the 3-space reflection + guard the same way
|
|
623
|
+
// a live orientation edit re-bakes the 2θ law (the ONE recompile path).
|
|
624
|
+
pitchDeg: (prop === 'pitchDeg') ? value : this.pitchDeg,
|
|
625
|
+
slabD: _rectDepth(this._trueRect),
|
|
626
|
+
reposition: this.lo.reposition,
|
|
627
|
+
// Z1 — preserve the wall's 3D-ness across a live recompile (else an orientation
|
|
628
|
+
// edit on a 3D wall would drop the z-incidence reflect + the closest3d eject).
|
|
629
|
+
is3D: this.is3D === true,
|
|
630
|
+
// MIRROR ruling — the TRUE slab dims (from _trueRect) so a live orientation edit
|
|
631
|
+
// re-bakes the OBB-normal eject for the new angle (cardinal → AABB eject, SU1).
|
|
632
|
+
slabW: _trueWH.w, slabH: _trueWH.h
|
|
633
|
+
});
|
|
634
|
+
if (probe.collectCollided && !this._collectCollided) {
|
|
635
|
+
throw new Error('[Wall.setV2Prop] cannot live-enable collection (mark/count) on a wall not ' +
|
|
636
|
+
'registered to collect_collided — construct it with the hook, or remove + re-add it.');
|
|
637
|
+
}
|
|
638
|
+
// Commit the facade prop, then push the recompiled program.
|
|
639
|
+
if (prop === 'hooks') { this.hooks = nextHooks; }
|
|
640
|
+
else if (prop === 'decoherence') { this.decoherence = !!value; }
|
|
641
|
+
else if (prop !== undefined) { this[prop] = value; }
|
|
642
|
+
// Keep the ferried render orientation in sync so the shell rotation (R2) tracks a live edit.
|
|
643
|
+
if (prop === 'orientationDeg' && this.lo) { this.lo.orientationDeg = value; }
|
|
644
|
+
if (prop === 'pitchDeg' && this.lo) { this.lo.pitchDeg = value; }
|
|
645
|
+
if (this.universe && typeof this.universe.updateObjectProperty === 'function') {
|
|
646
|
+
this.universe.updateObjectProperty(this.lo.handle, 'LargeObject', 'program', probe.program || null);
|
|
647
|
+
}
|
|
648
|
+
this.lo.program = probe.program || null;
|
|
649
|
+
// Re-derive + re-push the swept-OBB geometry on a live edit (a live orientation change
|
|
650
|
+
// re-bakes the rotated collision slab + its broad-phase AABB from the SAME SlabFace
|
|
651
|
+
// authority; a change back to a cardinal orientation clears the region to null = the AABB
|
|
652
|
+
// path and restores the true-slab broad rect). Uses the wall's TRUE (un-expanded) rect —
|
|
653
|
+
// the JS wrapper's lo.rect is the true slab, and the engine broad rect is re-widened here.
|
|
654
|
+
// WASM-only (the JS provider FAILS LOUD on a non-null region).
|
|
655
|
+
var geom2 = _slabGeometryForWall(this._trueRect, this.orientationDeg, this.pitchDeg);
|
|
656
|
+
if (this.universe && typeof this.universe.updateObjectProperty === 'function') {
|
|
657
|
+
this.universe.updateObjectProperty(this.lo.handle, 'LargeObject', 'region', geom2.region);
|
|
658
|
+
// Re-widen the ENGINE broad-phase rect to the new orientation's AABB (or restore the
|
|
659
|
+
// true rect at a cardinal). The JS wrapper rect is left as the true slab (render).
|
|
660
|
+
this.universe.updateObjectProperty(this.lo.handle, 'LargeObject', 'rect', geom2.broadRect);
|
|
661
|
+
}
|
|
662
|
+
this.lo.region = geom2.region;
|
|
663
|
+
this._program = this.lo.program;
|
|
664
|
+
return this;
|
|
665
|
+
};
|
|
666
|
+
|
|
667
|
+
/** THROWS when this wall's LO carries a program the wall did not bake (see `_rebake`). The
|
|
668
|
+
* STUDIO grab of a DECLARED wall asks it first: a wall whose program another part replaced
|
|
669
|
+
* must not be re-baked by a carry or a turn, so the grab is refused. (A composite's members
|
|
670
|
+
* are never grabbed on their own — the composite is the declared object.) */
|
|
671
|
+
Wall.prototype.assertOwnsProgram = function () {
|
|
672
|
+
if (this.lo && this.lo.program !== this._program) {
|
|
673
|
+
throw new Error('[Wall] "' + this.name + '": its LargeObject carries an op-program another ' +
|
|
674
|
+
'part pushed (' + (this._owner ? 'owned by ' + (this._owner.name || 'its composite') :
|
|
675
|
+
'not baked by this wall') + '). Re-baking it here would silently replace that part\'s ' +
|
|
676
|
+
'physics — the owning part must re-apply it. (FAIL-LOUD.)');
|
|
677
|
+
}
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* detect-wall counting readout: accumulate this tick's collided frames into a
|
|
682
|
+
* running y→count histogram and return it. Reads THIS LO's collected frames from
|
|
683
|
+
* the marshalled tick output (`tickData.objects[handle].frames`) — the engine
|
|
684
|
+
* buffers them because the LO was registered `collect_collided:true` (the detect
|
|
685
|
+
* behavior). Pure reader; identical on both engines.
|
|
686
|
+
*
|
|
687
|
+
* Guard: THROWS if called on a non-detect wall (no collected frames buffer) so a
|
|
688
|
+
* mis-wired counting screen fails loud rather than silently reporting zero.
|
|
689
|
+
* @param {Object} tickData — the universe.doTick() result.
|
|
690
|
+
* @returns {{ combined: Map<number,number>, total: number }}
|
|
691
|
+
*/
|
|
692
|
+
Wall.prototype.getCollisionHistogram = function (tickData) {
|
|
693
|
+
// A wall counts if it is the legacy `detect` behavior OR a v2 wall carrying the
|
|
694
|
+
// `count` hook (the Counting Wall / Detector Screen).
|
|
695
|
+
var counts = Array.isArray(this.hooks) && this.hooks.indexOf('count') !== -1;
|
|
696
|
+
if (!counts) {
|
|
697
|
+
throw new Error(
|
|
698
|
+
'[Wall.getCollisionHistogram] this wall does not COUNT (collisionBehavior "' +
|
|
699
|
+
this.collisionBehavior + '", hooks ' + JSON.stringify(this.hooks || []) +
|
|
700
|
+
') — only a wall carrying the `count` hook collects collided frames ' +
|
|
701
|
+
'(preset \'detector-screen\' for a counting screen, \'transparent-probe\' ' +
|
|
702
|
+
'for a pass-through population probe).'
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
// #61 double-consume guard (same discipline as DetectorWall): each tick's
|
|
706
|
+
// data must be consumed exactly once or hits double-count silently.
|
|
707
|
+
if (tickData != null && tickData === this._lastTickData) {
|
|
708
|
+
throw new Error(
|
|
709
|
+
'[Wall.getCollisionHistogram] called twice with the SAME TickData object — ' +
|
|
710
|
+
'each tick must be consumed exactly once. Call universe.doTick() again first.'
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
this._lastTickData = tickData;
|
|
714
|
+
|
|
715
|
+
var handle = this.lo.handle;
|
|
716
|
+
var frames = (tickData && tickData.objects && tickData.objects[handle] &&
|
|
717
|
+
Array.isArray(tickData.objects[handle].frames))
|
|
718
|
+
? tickData.objects[handle].frames : [];
|
|
719
|
+
for (var i = 0; i < frames.length; i++) {
|
|
720
|
+
var fd = frames[i];
|
|
721
|
+
var y;
|
|
722
|
+
if (fd.rect && fd.rect.top != null) { y = Math.floor(fd.rect.top); }
|
|
723
|
+
else if (fd.y != null) { y = Math.floor(fd.y); }
|
|
724
|
+
else { y = 0; }
|
|
725
|
+
// z rides FrameData top-level (the x/y-vs-z field split); 0 in a 2D scene
|
|
726
|
+
// (the engine never writes it) — so the grid degenerates to one z-bucket.
|
|
727
|
+
var z = (fd.z != null) ? Math.floor(fd.z) : 0;
|
|
728
|
+
this._combined.set(y, (this._combined.get(y) || 0) + 1);
|
|
729
|
+
Wall.gridAdd(this._grid, y, z);
|
|
730
|
+
this._total++;
|
|
731
|
+
}
|
|
732
|
+
return { combined: this._combined, grid: this._grid, total: this._total };
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* Add one hit at (y,z) to a Y×Z grid (Map<y, Map<z,count>>). Pure mutation
|
|
737
|
+
* helper — the ONE grid-accumulation authority for the wall family (Wall is the
|
|
738
|
+
* atom; DetectorWall delegates here rather than keeping a second copy).
|
|
739
|
+
* @param {Map<number, Map<number,number>>} grid
|
|
740
|
+
* @param {number} y
|
|
741
|
+
* @param {number} z
|
|
742
|
+
*/
|
|
743
|
+
Wall.gridAdd = function (grid, y, z) {
|
|
744
|
+
var byZ = grid.get(y);
|
|
745
|
+
if (!byZ) { byZ = new Map(); grid.set(y, byZ); }
|
|
746
|
+
byZ.set(z, (byZ.get(z) || 0) + 1);
|
|
747
|
+
};
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Derive the Y-marginal (Map<y,count>) from a Y×Z grid — sums every z-bucket for
|
|
751
|
+
* each y. Pure/static; exists so a caller can PROVE the grid and the 1D
|
|
752
|
+
* histogram agree (the same "Y-marginal DERIVED" discipline as DetectorWall,
|
|
753
|
+
* which delegates to this one authority).
|
|
754
|
+
* @param {Map<number, Map<number,number>>} grid
|
|
755
|
+
* @returns {Map<number,number>}
|
|
756
|
+
*/
|
|
757
|
+
Wall.deriveMarginalY = function (grid) {
|
|
758
|
+
var out = new Map();
|
|
759
|
+
grid.forEach(function (byZ, y) {
|
|
760
|
+
var sum = 0;
|
|
761
|
+
byZ.forEach(function (c) { sum += c; });
|
|
762
|
+
out.set(y, sum);
|
|
763
|
+
});
|
|
764
|
+
return out;
|
|
765
|
+
};
|
|
766
|
+
|
|
767
|
+
/**
|
|
768
|
+
* LIVE-POPULATION readout (loop-polish B, CEO 2026-07-25): the number of frames
|
|
769
|
+
* that collided with THIS wall on the sampled tick. For a FULL-AREA background
|
|
770
|
+
* wall (collisionBehavior 'nothing' + the count hook, intersects trigger) every
|
|
771
|
+
* live leaf inside the rect collides every tick, so this per-tick collided-frame
|
|
772
|
+
* count IS the current live population inside the wall — the ONE canonical
|
|
773
|
+
* "how many live" authority (replaces any host-side scan, e.g. an emission-cap
|
|
774
|
+
* thermostat's activeQuanta read).
|
|
775
|
+
*
|
|
776
|
+
* Layer 1 (API): dual-lane, mirroring the LiveChart PROJECTOR_SPEC shape —
|
|
777
|
+
* getLiveCount(tickData) — SAMPLE lane (per tick): read
|
|
778
|
+
* `tickData.objects[handle].frames.length` and remember it. IDEMPOTENT per
|
|
779
|
+
* TickData (a pure sample, not an accumulator) — no double-consume guard
|
|
780
|
+
* needed, so a gate tick-fn and a chart drain may both sample the same tick.
|
|
781
|
+
* getLiveCount() — READ lane: return the last sample (0 before the first tick).
|
|
782
|
+
*
|
|
783
|
+
* Layer 2 (physics): zero physics — the background wall's compiled program is the
|
|
784
|
+
* COUNT increment only (bench LO7); counting never moves, kills, or recolors a
|
|
785
|
+
* frame (SU1).
|
|
786
|
+
*
|
|
787
|
+
* Guard: THROWS on a wall without the count hook / legacy detect behavior (no
|
|
788
|
+
* collected-frames buffer → it would silently report 0 forever).
|
|
789
|
+
* @param {Object} [tickData] — the universe.doTick() result (sample lane).
|
|
790
|
+
* @returns {number}
|
|
791
|
+
*/
|
|
792
|
+
Wall.prototype.getLiveCount = function (tickData) {
|
|
793
|
+
var counts = Array.isArray(this.hooks) && this.hooks.indexOf('count') !== -1;
|
|
794
|
+
if (!counts) {
|
|
795
|
+
throw new Error(
|
|
796
|
+
'[Wall.getLiveCount] this wall does not COUNT (collisionBehavior "' +
|
|
797
|
+
this.collisionBehavior + '", hooks ' + JSON.stringify(this.hooks || []) +
|
|
798
|
+
') — only a wall carrying the `count` hook collects collided frames ' +
|
|
799
|
+
'(preset \'detector-screen\' for a counting screen, \'transparent-probe\' ' +
|
|
800
|
+
'for a pass-through population probe).'
|
|
801
|
+
);
|
|
802
|
+
}
|
|
803
|
+
if (tickData != null) {
|
|
804
|
+
var handle = this.lo.handle;
|
|
805
|
+
var oc = tickData.objects && tickData.objects[handle];
|
|
806
|
+
this._liveCount = (oc && Array.isArray(oc.frames)) ? oc.frames.length : 0;
|
|
807
|
+
}
|
|
808
|
+
return this._liveCount || 0;
|
|
809
|
+
};
|
|
810
|
+
|
|
811
|
+
/**
|
|
812
|
+
* ABSORB v2 readout: the wall's live { stored, absorbedTotal, radiatedTotal } (the
|
|
813
|
+
* REAL backing-frame mass ledger). Throws on a non-absorb wall. Call between ticks.
|
|
814
|
+
*/
|
|
815
|
+
Wall.prototype.getAbsorbStats = function () {
|
|
816
|
+
if (this.collisionBehavior !== 'absorb' || !this._radiationTickFn) {
|
|
817
|
+
throw new Error('[Wall.getAbsorbStats] this wall is not an ABSORB wall (collisionBehavior "' +
|
|
818
|
+
this.collisionBehavior + '").');
|
|
819
|
+
}
|
|
820
|
+
return this._radiationTickFn.stats();
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
/**
|
|
824
|
+
* Full teardown: drop the ABSORB radiation host tick-fn from the universe's tick-fn
|
|
825
|
+
* list (so FrqtlApp.setup() reinit does not STACK radiators) + dispose the LO.
|
|
826
|
+
* Idempotent. FrqtlApp.removeObject / setup-teardown call this via the facade.
|
|
827
|
+
*/
|
|
828
|
+
Wall.prototype.dispose = function () {
|
|
829
|
+
if (this._disposed) { return; }
|
|
830
|
+
this._disposed = true;
|
|
831
|
+
var u = this.universe;
|
|
832
|
+
if (this._radiationTickFn && u && Array.isArray(u.experimentTickFunctions)) {
|
|
833
|
+
var i = u.experimentTickFunctions.indexOf(this._radiationTickFn);
|
|
834
|
+
if (i !== -1) { u.experimentTickFunctions.splice(i, 1); }
|
|
835
|
+
}
|
|
836
|
+
if (this.lo && typeof this.lo.dispose === 'function') { this.lo.dispose(); }
|
|
837
|
+
};
|
|
838
|
+
|
|
839
|
+
/** Clear the accumulated detect histogram (both the y-marginal and the Y×Z grid). */
|
|
840
|
+
Wall.prototype.resetHistogram = function () {
|
|
841
|
+
this._combined = new Map();
|
|
842
|
+
this._grid = new Map();
|
|
843
|
+
this._total = 0;
|
|
844
|
+
// _lastTickData is deliberately NOT reset (the double-consume guard is
|
|
845
|
+
// orthogonal to accumulator state — same discipline as DetectorWall).
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
// ── The kind (objects/kinds.js): descriptor, construction defaults, palette, builder ──────────
|
|
849
|
+
|
|
850
|
+
/**
|
|
851
|
+
* Wall — the first-class WALL primitive (ws-experiments-walkthrough W1, CEO
|
|
852
|
+
* ruling E-W1a). A SUGAR kind (peer of SphereEmitter — NOT a new engine kind):
|
|
853
|
+
* a thin facade over ONE LargeObject whose collision behavior is a friendly
|
|
854
|
+
* Collision LEXICON v2 (`collisionBehavior`: nothing | block | bounce | absorb |
|
|
855
|
+
* destroy, + bounceDirection + hooks) compiled to op-programs
|
|
856
|
+
* by the SAME actionCompiler every wall rides. The ObjectFactory builder
|
|
857
|
+
* constructs the facade (a bare, engine-registered LargeObject inside), honoring
|
|
858
|
+
* a palette drop's click point (ctx.x/ctx.y).
|
|
859
|
+
*
|
|
860
|
+
* The lexicon enum values are FERRIED from constants/WallBehaviors.js
|
|
861
|
+
* (whose tables themselves ferry the frozen CollisionActions/CollisionTypes sets)
|
|
862
|
+
* — NEVER a hand-listed enum here (schema-mirror guard). FrqtlApp
|
|
863
|
+
* .updateObjectProperty routes a lexicon write to Wall.setV2Prop
|
|
864
|
+
* (recompile-and-push, the "flip a wall into a detector mid-run" live path), and
|
|
865
|
+
* routes the engine-visible props (rect/color/layer/…) DOWN to the inner
|
|
866
|
+
* LargeObject (which keeps its own engine push). SU1: zero physics of its own.
|
|
867
|
+
*/
|
|
868
|
+
var WallDescriptor = {
|
|
869
|
+
description: 'A rectangular wall/boundary with a single collision behavior: nothing ' +
|
|
870
|
+
'(pass through), block (stop at the surface), bounce (turn around), absorb (kill + ' +
|
|
871
|
+
'tally + radiate), or destroy (plain kill) — plus the composable mark/count/recolor ' +
|
|
872
|
+
'hooks. The first-class palette face over a LargeObject + actionCompiler op-program ' +
|
|
873
|
+
'— the ONE wall primitive the composites (SlitScreen, DetectorWall) build ' +
|
|
874
|
+
'on. Zero physics of its own.',
|
|
875
|
+
// STUDIO UX presentation contract (A0 vocab, owned at lib/shim/field-descriptor).
|
|
876
|
+
// A wall has a genuine draggable/resizable extent (unlike the two-radii
|
|
877
|
+
// SphereEmitter) → resizable:true. 2D drag only (3D reposition via the camera).
|
|
878
|
+
// shellRender:'run' — a wall is a boundary you SEE during Run (not a Design-only
|
|
879
|
+
// marker like an emitter), so its shell always paints.
|
|
880
|
+
ux: {
|
|
881
|
+
resizable: true,
|
|
882
|
+
draggable: { '2d': true },
|
|
883
|
+
shellRender: 'run'
|
|
884
|
+
},
|
|
885
|
+
fields: [
|
|
886
|
+
{
|
|
887
|
+
name: 'rect',
|
|
888
|
+
type: 'rect',
|
|
889
|
+
engineVisible: true,
|
|
890
|
+
unit: 'px',
|
|
891
|
+
live: true,
|
|
892
|
+
description: 'Bounding rectangle in physics pixels ' + ValueNormalizer.rectTupleText(4) + ', or the ' +
|
|
893
|
+
'explicit z/depth form ' + ValueNormalizer.rectTupleText(6) + ' — WASM 3D. The wall\'s collision ' +
|
|
894
|
+
'search region. Live-editable (drag/resize); routed to the inner LargeObject ' +
|
|
895
|
+
'engine handle.'
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
// ── Rotation (Collision Lab; R1 UX-pass upgrade) — a live orientation the
|
|
899
|
+
// sugar owns. Layer 1: number, degrees, [0,360), live, render+op-owned
|
|
900
|
+
// (engineVisible:false — the engine LO search region stays AXIS-ALIGNED,
|
|
901
|
+
// the honest AABB catch zone; nothing in lo_to_scope_bag — theorist v2 §1b).
|
|
902
|
+
// A live edit EXPLICITLY recompiles (setV2Prop → compileWallV2) AND the
|
|
903
|
+
// renderer draws the shell rotated (no outline of the un-rotated rect).
|
|
904
|
+
//
|
|
905
|
+
// Layer 2 (what the knob DOES): orientationDeg is the mirror SURFACE angle.
|
|
906
|
+
// For a bounce/reflect mirror the compiled reflection law is now
|
|
907
|
+
// ORIENTATION-AWARE (R1, theorist-proven exact):
|
|
908
|
+
// • 0/90/180/270 → the axis-aligned incidence BOX reflector (both faces;
|
|
909
|
+
// byte-identical at 0 — SU1).
|
|
910
|
+
// • 45/135/225/315 → the exact swap / swap+negate (+x → +y / +x → −y).
|
|
911
|
+
// • any α → the fixed tilted-plane reflection R=[cos2α,sin2α;sin2α,−cos2α].
|
|
912
|
+
// STANDARD OPTICS 2θ LAW: a mirror surface at angle θ DEFLECTS the beam by
|
|
913
|
+
// 2θ, so a +x beam exits at angle 2θ (slope tan2θ). Rotate the mirror to 45°
|
|
914
|
+
// and a +x beam exits +y (the CEO routing landmark); 15° → slope tan30°;
|
|
915
|
+
// 67.5° → (−0.7071, 0.7071). For a constrained-random ("laser-gate") wall,
|
|
916
|
+
// orientation instead re-bakes the scatter CONE endpoints
|
|
917
|
+
// (cos/sin(orientation ± angleConstraint)) — the scatter axis follows the spin.
|
|
918
|
+
// Also the LOOK-drive rotation target.
|
|
919
|
+
name: 'orientationDeg',
|
|
920
|
+
type: 'number',
|
|
921
|
+
constraint: { min: 0, max: 360 },
|
|
922
|
+
engineVisible: false,
|
|
923
|
+
live: true,
|
|
924
|
+
widgetHint: 'slider',
|
|
925
|
+
description: 'Wall orientation in degrees (default 0; visible always) = the mirror ' +
|
|
926
|
+
'SURFACE angle. A live edit recompiles the op-program: a bounce/reflect mirror bakes ' +
|
|
927
|
+
'the ORIENTATION-AWARE reflection law (standard optics — a surface at θ deflects a ' +
|
|
928
|
+
'beam by 2θ, so 45° turns a +x beam to +y), and a constrained-random wall re-aims its ' +
|
|
929
|
+
'scatter cone. The renderer draws the shell rotated to this angle. Also the LOOK-drive ' +
|
|
930
|
+
'rotation target.'
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
// ── R2AX — two-axis mirror (CEO ruling 2026-07-26, Living Log ce4fb0c9):
|
|
934
|
+
// Layer 1 (API): up/down tilt in degrees [-90, 90] about the plate's OWN
|
|
935
|
+
// long axis (the drawn face line). 3D walls only (needs a real z/depth —
|
|
936
|
+
// visible in the Inspector only when depth > 0); a 2D wall THROWS. Live:
|
|
937
|
+
// routes through the same setV2Prop recompile path as orientationDeg.
|
|
938
|
+
// Layer 2 (physics): pitch tips the mirror face out of the xy-plane, so a
|
|
939
|
+
// bounce/reflect wall bakes the TRUE 3-space reflection R = I − 2nn^T about
|
|
940
|
+
// the SlabFace plate normal — the reflected beam gains a z-component and
|
|
941
|
+
// ALWAYS leaves on the incident side (verified: the (yaw × pitch) sweep
|
|
942
|
+
// matrix, zero pass-through at every angle pair). pitch 0 restores the
|
|
943
|
+
// in-plane 2θ law byte-identically (vz passes through — the un-pitched
|
|
944
|
+
// contract). A pitched plate uses the AABB broad phase + the compiled
|
|
945
|
+
// n-dot guard as its narrow phase (no engine ObbRegion — z-theta-only;
|
|
946
|
+
// engine two-angle OBB is a flagged Gate-1 follow-up).
|
|
947
|
+
name: 'pitchDeg',
|
|
948
|
+
type: 'number',
|
|
949
|
+
constraint: { min: -90, max: 90 },
|
|
950
|
+
engineVisible: false,
|
|
951
|
+
live: true,
|
|
952
|
+
widgetHint: 'slider',
|
|
953
|
+
visibleWhen: { field: 'depth', truthy: true },
|
|
954
|
+
description: 'Up/down tilt (degrees, -90..90) about the plate\'s own long axis — the ' +
|
|
955
|
+
'second mirror rotation axis (3D walls only). A live edit recompiles the true ' +
|
|
956
|
+
'3-space reflection about the tipped plate normal: the reflected beam gains a ' +
|
|
957
|
+
'z-component and always leaves on the incident side (never through the mirror). ' +
|
|
958
|
+
'0 = the flat in-plane mirror (vz passes through).'
|
|
959
|
+
},
|
|
960
|
+
// ── Z-slab (Z1, CEO UX-pass 2026-07-24) — a wall's 3D depth/placement. A
|
|
961
|
+
// 6-element rect (RECT_TUPLE[6]) gives the wall a real z-slab: the
|
|
962
|
+
// engine's collision region covers the depth, the wall reflects/ejects on all
|
|
963
|
+
// SIX faces (the 3-axis incidence lift, WASM), and the three renderer draws it
|
|
964
|
+
// as a BoxGeometry slab.
|
|
965
|
+
// `depth` and `z` are DERIVED from the rect (the single live position authority —
|
|
966
|
+
// the `rect` field above already accepts the 6-element z/depth form and is the
|
|
967
|
+
// editable authority; these are the inspectable read-out, round-tripped by the
|
|
968
|
+
// depth/z construction sugar). A dedicated depth SLIDER is a flagged follow-on.
|
|
969
|
+
{
|
|
970
|
+
name: 'depth',
|
|
971
|
+
type: 'readonly',
|
|
972
|
+
unit: 'px',
|
|
973
|
+
engineVisible: false,
|
|
974
|
+
description: 'The wall\'s z-slab depth (z-extent) in physics pixels — 0 for a flat 2D ' +
|
|
975
|
+
'wall. Derived from the 6-element rect ' + ValueNormalizer.rectTupleText(6) + '; a real depth makes the ' +
|
|
976
|
+
'wall a 3D slab (six-face reflect/eject, box render). Edit via the rect field.'
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
name: 'z',
|
|
980
|
+
type: 'readonly',
|
|
981
|
+
unit: 'px',
|
|
982
|
+
engineVisible: false,
|
|
983
|
+
description: 'The wall\'s z placement (near-face … far-face CENTER) in physics pixels ' +
|
|
984
|
+
'(0 for a 2D wall). Derived from the 6-element rect. Edit via the rect field.'
|
|
985
|
+
},
|
|
986
|
+
// ── Collision LEXICON v2 (CEO 3e2016aa) — the nested behavior/direction/
|
|
987
|
+
// modifier/hook surface. Enum values are FERRIED from WallBehaviors.js
|
|
988
|
+
// (never hand-listed). `visibleWhen` (evaluated live at Inspector render
|
|
989
|
+
// against the captured props) reveals the dependent fields only when
|
|
990
|
+
// relevant — ONE declarative predicate, one evaluator (makeControlsForObject).
|
|
991
|
+
{
|
|
992
|
+
name: 'collisionBehavior',
|
|
993
|
+
type: 'enum',
|
|
994
|
+
constraint: { values: _wallBehaviorV2ValuesMap ? _wallBehaviorV2ValuesMap() : {} },
|
|
995
|
+
engineVisible: false,
|
|
996
|
+
live: true,
|
|
997
|
+
description: 'The wall\'s collision BEHAVIOR (compiled to an op-program; the engine has ' +
|
|
998
|
+
'no wall dispatch): nothing = pass-through; block = STOP the particle at the surface ' +
|
|
999
|
+
'(reposition to the closest position outside the wall, velocity kept — it parks at the ' +
|
|
1000
|
+
'face instead of turning around); bounce = a motion response (see bounceDirection); ' +
|
|
1001
|
+
'absorb = tally the colliders\' mass into the wall\'s REAL backing ' +
|
|
1002
|
+
'frame + radiate (conserving); destroy = remove the particle. Hooks (mark/count) compose ' +
|
|
1003
|
+
'with any behavior.'
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
name: 'bounceDirection',
|
|
1007
|
+
type: 'enum',
|
|
1008
|
+
constraint: { values: _bounceDirectionValuesMap ? _bounceDirectionValuesMap() : {} },
|
|
1009
|
+
engineVisible: false,
|
|
1010
|
+
live: true,
|
|
1011
|
+
visibleWhen: { field: 'collisionBehavior', equals: 'bounce' },
|
|
1012
|
+
description: 'How a BOUNCE redirects the particle: reflect = invert the velocity on the ' +
|
|
1013
|
+
'axis of incidence (a mirror); random = redraw the exit direction (omni by default, or ' +
|
|
1014
|
+
'constrained to a cone about the wall\'s INITIAL orientation). Only shown for bounce.'
|
|
1015
|
+
},
|
|
1016
|
+
{
|
|
1017
|
+
name: 'decoherence',
|
|
1018
|
+
type: 'boolean',
|
|
1019
|
+
engineVisible: false,
|
|
1020
|
+
live: true,
|
|
1021
|
+
visibleWhen: [{ field: 'collisionBehavior', equals: 'bounce' },
|
|
1022
|
+
{ field: 'bounceDirection', equals: 'random' }],
|
|
1023
|
+
description: 'When a random BOUNCE also resets the wave phase (local_time=0) + re-rolls the ' +
|
|
1024
|
+
'charge sign (an independent decoherence toggle). Off by default (direction-only). ' +
|
|
1025
|
+
'Only shown for a random bounce.'
|
|
1026
|
+
},
|
|
1027
|
+
{
|
|
1028
|
+
name: 'hooks',
|
|
1029
|
+
type: 'enum-set',
|
|
1030
|
+
constraint: { values: _wallHookValuesMap ? _wallHookValuesMap() : {} },
|
|
1031
|
+
engineVisible: false,
|
|
1032
|
+
live: true,
|
|
1033
|
+
description: 'Composable statistics hooks (any behavior): mark = stamp a user string on ' +
|
|
1034
|
+
'each collided frame (engine-transparent); count = tally collisions (filterable by mark). ' +
|
|
1035
|
+
'The counting screen is destroy + count.'
|
|
1036
|
+
},
|
|
1037
|
+
{
|
|
1038
|
+
name: 'markTag',
|
|
1039
|
+
type: 'string',
|
|
1040
|
+
engineVisible: false,
|
|
1041
|
+
visibleWhen: { field: 'hooks', includes: 'mark' },
|
|
1042
|
+
description: 'The user string MARK stamps into each collided frame\'s string_map (only shown ' +
|
|
1043
|
+
'when the mark hook is active).'
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
name: 'countFilter',
|
|
1047
|
+
type: 'string',
|
|
1048
|
+
engineVisible: false,
|
|
1049
|
+
visibleWhen: { field: 'hooks', includes: 'count' },
|
|
1050
|
+
description: 'Restrict COUNT to frames carrying a specific mark ("any" = count all). Applied ' +
|
|
1051
|
+
'by the between-tick stats reader (only shown when the count hook is active).'
|
|
1052
|
+
},
|
|
1053
|
+
{
|
|
1054
|
+
name: 'capacity',
|
|
1055
|
+
type: 'number',
|
|
1056
|
+
constraint: { min: 1, integer: true },
|
|
1057
|
+
engineVisible: false,
|
|
1058
|
+
live: true,
|
|
1059
|
+
visibleWhen: { field: 'collisionBehavior', equals: 'absorb' },
|
|
1060
|
+
description: 'ABSORB mass ceiling (snapped to a leaf level the engine can actually ' +
|
|
1061
|
+
'size — the capacity law runs over the ACTIVE AXES) AND the 1/capacity per-tick ' +
|
|
1062
|
+
'radiation probability. How much it holds and how often it gives some back. ' +
|
|
1063
|
+
'Only shown for absorb.'
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
name: 'emissivity',
|
|
1067
|
+
type: 'number',
|
|
1068
|
+
constraint: { min: 0.01, max: 1 },
|
|
1069
|
+
engineVisible: false,
|
|
1070
|
+
live: true,
|
|
1071
|
+
visibleWhen: { field: 'collisionBehavior', equals: 'absorb' },
|
|
1072
|
+
description: 'ABSORB radiating efficiency — the FULLNESS of each re-emitted leaf, as ' +
|
|
1073
|
+
'a fraction in (0,1] of that layer\'s cell capacity (default 0.5). A colour knob, ' +
|
|
1074
|
+
'not bookkeeping: fullness IS the move gate and sets the wavelength, so 1.0 ' +
|
|
1075
|
+
're-emits at light speed with the shortest λ and 0.5 at half the step rate with ' +
|
|
1076
|
+
'twice the λ. The emitted leaf\'s SCALE is not a knob — the wall re-emits at the ' +
|
|
1077
|
+
'layer it soaked from. Only shown for absorb.'
|
|
1078
|
+
},
|
|
1079
|
+
{
|
|
1080
|
+
name: 'name',
|
|
1081
|
+
type: 'string',
|
|
1082
|
+
engineVisible: false,
|
|
1083
|
+
description: 'Human-readable identifier (display/debug only).'
|
|
1084
|
+
},
|
|
1085
|
+
{
|
|
1086
|
+
name: 'color',
|
|
1087
|
+
type: 'rgb',
|
|
1088
|
+
engineVisible: false,
|
|
1089
|
+
live: true,
|
|
1090
|
+
description: 'Render color [r, g, b] 0-255 (render-only; engine collision unaffected).'
|
|
1091
|
+
},
|
|
1092
|
+
{
|
|
1093
|
+
name: 'alpha',
|
|
1094
|
+
type: 'number',
|
|
1095
|
+
constraint: { min: 0, max: 255, integer: true },
|
|
1096
|
+
engineVisible: false,
|
|
1097
|
+
live: true,
|
|
1098
|
+
widgetHint: 'slider',
|
|
1099
|
+
description: 'Render opacity 0-255 (render-only). 0 = fully transparent, 255 = fully opaque — ' +
|
|
1100
|
+
'a transparent wall still collides, it simply stops obstructing the view.'
|
|
1101
|
+
},
|
|
1102
|
+
{
|
|
1103
|
+
name: 'visible',
|
|
1104
|
+
type: 'boolean',
|
|
1105
|
+
engineVisible: false,
|
|
1106
|
+
live: true,
|
|
1107
|
+
description: 'Whether the wall is drawn (render-only; collision detection stays active).'
|
|
1108
|
+
},
|
|
1109
|
+
{
|
|
1110
|
+
name: 'layer',
|
|
1111
|
+
type: 'number',
|
|
1112
|
+
constraint: { min: 0, integer: true },
|
|
1113
|
+
engineVisible: true,
|
|
1114
|
+
description: 'Dimension layer this wall is scoped to (0 = fundamental; null = all layers).'
|
|
1115
|
+
},
|
|
1116
|
+
{
|
|
1117
|
+
name: 'shellShape',
|
|
1118
|
+
type: 'enum',
|
|
1119
|
+
constraint: { values: shapeValuesMap() },
|
|
1120
|
+
engineVisible: false,
|
|
1121
|
+
live: true,
|
|
1122
|
+
description: 'Render shape hint of the wall\'s visual shell (ball|box|circle|cube|sphere; ' +
|
|
1123
|
+
'render-only). cube/sphere degrade to box/ball in 2D; absent falls back to a rect.'
|
|
1124
|
+
}
|
|
1125
|
+
]
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
//
|
|
1129
|
+
// The first-class wall primitive (W1). Builds a Wall facade over ONE
|
|
1130
|
+
// engine-registered LargeObject (Wall.js resolves the collision lexicon to an
|
|
1131
|
+
// op-program via actionCompiler), adds its inner LO to the render list, and returns
|
|
1132
|
+
// the FACADE (so the Inspector sees kind 'Wall' + the lexicon fields). Honors a
|
|
1133
|
+
// palette DROP's click point (ctx.x/ctx.y) exactly as the LargeObject builder
|
|
1134
|
+
// does: when the recipe/opts do NOT declare placement, center the wall's rect on
|
|
1135
|
+
// the click. A recipe passing an explicit rect / left+top still wins (byte-
|
|
1136
|
+
// identical addObject path).
|
|
1137
|
+
registerKind(Wall, {
|
|
1138
|
+
from: import.meta.url,
|
|
1139
|
+
descriptor: WallDescriptor,
|
|
1140
|
+
defaults: {
|
|
1141
|
+
// The bare palette drop's behavior authority: preset 'wall' -> collisionBehavior
|
|
1142
|
+
// 'block' (WallBehaviors.WALL_PRESETS), making the catalog's "vertical blocking
|
|
1143
|
+
// wall" true. A recipe's own `preset` replaces this one in _mergeProps's
|
|
1144
|
+
// Object.assign; a recipe's explicit `collisionBehavior` wins because Wall.js
|
|
1145
|
+
// preset-expansion fills ONLY undefined fields. Never a concrete
|
|
1146
|
+
// collisionBehavior here — that clobbered presets (K6).
|
|
1147
|
+
preset: 'wall',
|
|
1148
|
+
w: 12,
|
|
1149
|
+
h: 120,
|
|
1150
|
+
color: [150, 150, 160]
|
|
1151
|
+
},
|
|
1152
|
+
palette: { displayName: 'Wall', category: 'boundary', iconHint: 'wall', rank: 1 },
|
|
1153
|
+
build: function (props, ctx, Class) {
|
|
1154
|
+
var WallCls = Class; // the class that registered (a composed fork's own class)
|
|
1155
|
+
|
|
1156
|
+
var opts = {};
|
|
1157
|
+
for (var k in props) {
|
|
1158
|
+
if (!Object.prototype.hasOwnProperty.call(props, k)) { continue; }
|
|
1159
|
+
opts[k] = props[k];
|
|
1160
|
+
}
|
|
1161
|
+
// Derive the rect from the drop point when the recipe declared no placement.
|
|
1162
|
+
if (opts.rect == null && opts.left == null) {
|
|
1163
|
+
var ww = (opts.w != null) ? opts.w : 12;
|
|
1164
|
+
var wh = (opts.h != null) ? opts.h : 120;
|
|
1165
|
+
opts.left = Math.round(ctx.x - ww / 2);
|
|
1166
|
+
opts.top = Math.round(ctx.y - wh / 2);
|
|
1167
|
+
opts.w = ww;
|
|
1168
|
+
opts.h = wh;
|
|
1169
|
+
}
|
|
1170
|
+
// R#101 (2026-09-28) — a palette DROP into a 3D scene is a FULL-SCENE-DEPTH slab
|
|
1171
|
+
// by default. A bare 4-element rect reaches the engine as z=0/depth=w (the
|
|
1172
|
+
// back-compat promotion) — a ~w-thin plate a directed stream walks around in z
|
|
1173
|
+
// (measured: 146/300 crossers in 30 ticks, vs ~0 through a full-depth wall). The
|
|
1174
|
+
// z-band is CONSUMED from the runner's ONE bounds read (`FrqtlApp.worldBounds`:
|
|
1175
|
+
// the TickData boundary_rect ferry once ticked, the init-time initBounds read-back
|
|
1176
|
+
// before) — never hand-typed. An explicit z / d / depth / z-carrying rect wins
|
|
1177
|
+
// untouched, and the recipe path (no fromPalette) stays byte-identical.
|
|
1178
|
+
// 3D-ness is the WORLD's (its resolved axes), never a boot global.
|
|
1179
|
+
var u3 = ctx.runner && ctx.runner.universe;
|
|
1180
|
+
if (ctx.fromPalette && u3 && u3.activeAxes && u3.activeAxes.z &&
|
|
1181
|
+
opts.z === undefined && opts.d === undefined && opts.depth === undefined &&
|
|
1182
|
+
!(opts.rect && (Array.isArray(opts.rect)
|
|
1183
|
+
? opts.rect.length === 6
|
|
1184
|
+
: (opts.rect.z !== undefined || opts.rect.d !== undefined)))) {
|
|
1185
|
+
var wb = ctx.runner && ctx.runner.worldBounds;
|
|
1186
|
+
if (!wb || typeof wb.near !== 'number' || typeof wb.d !== 'number') {
|
|
1187
|
+
throw new Error('[ObjectFactory] create("Wall"): a 3D palette drop derives its ' +
|
|
1188
|
+
'z-slab from the scene bounds (runner.worldBounds — the TickData ' +
|
|
1189
|
+
'boundary_rect ferry / init-time initBounds read-back), but the runner ' +
|
|
1190
|
+
'publishes none. (FAIL-LOUD — a depth-less wall in a 3D scene is the ' +
|
|
1191
|
+
'z-leak this default repairs.)');
|
|
1192
|
+
}
|
|
1193
|
+
opts.z = wb.near;
|
|
1194
|
+
opts.depth = wb.d;
|
|
1195
|
+
}
|
|
1196
|
+
// Palette drops are census-numbered like every other numbered drop ('block N',
|
|
1197
|
+
// 'sphere emitter N', 'livechart N'): repeated bare drops otherwise all read
|
|
1198
|
+
// "wall" in LAYERS and collide in name-based lookups. Recipe path unchanged
|
|
1199
|
+
// (no fromPalette => Wall's own bare default; an explicit name always wins).
|
|
1200
|
+
if (ctx.fromPalette && opts.name == null) {
|
|
1201
|
+
opts.name = 'wall ' + (ctx.runner.largeObjects.length + 1);
|
|
1202
|
+
}
|
|
1203
|
+
var wall = new WallCls(ctx.runner.universe, opts);
|
|
1204
|
+
wall._schemaKind = Class.name;
|
|
1205
|
+
return wall;
|
|
1206
|
+
}
|
|
1207
|
+
});
|
|
1208
|
+
|
|
1209
|
+
// ── Export ───────────────────────────────────────────────────────────
|
|
1210
|
+
export { Wall };
|
|
1211
|
+
|