@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/CLAUDE.md +87 -0
  2. package/README.md +84 -2
  3. package/dist/dist-manifest.json +218 -0
  4. package/dist/eyes/index.js +109 -0
  5. package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
  6. package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
  7. package/dist/fat/pkg-node/package.json +12 -0
  8. package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
  9. package/dist/frqtl/compat/boot-node.js +34 -0
  10. package/dist/frqtl/compat/engine.js +127 -0
  11. package/dist/frqtl/compat/package-root.js +24 -0
  12. package/dist/frqtl/compat/providers/wasm-engine.js +2501 -0
  13. package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
  14. package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
  15. package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
  16. package/dist/frqtl/compat/renderer/Filters.js +163 -0
  17. package/dist/frqtl/compat/renderer/RenderFamily.js +85 -0
  18. package/dist/frqtl/compat/renderer/RenderMesh.js +100 -0
  19. package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
  20. package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
  21. package/dist/frqtl/compat/renderer/captureStill-common.js +259 -0
  22. package/dist/frqtl/compat/renderer/display-slot.js +96 -0
  23. package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
  24. package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
  25. package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
  26. package/dist/frqtl/compat/renderer/providers/index.js +22 -0
  27. package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
  28. package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
  29. package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
  30. package/dist/frqtl/compat/renderer/providers/three-provider.js +2474 -0
  31. package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
  32. package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
  33. package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
  34. package/dist/frqtl/compat/vendor.js +196 -0
  35. package/dist/frqtl/node-entry.js +17 -0
  36. package/dist/frqtl/objects/FrqtlApp.js +2922 -0
  37. package/dist/frqtl/objects/ObjectFactory.js +297 -0
  38. package/dist/frqtl/objects/RecipeContext.js +322 -0
  39. package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
  40. package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
  41. package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
  42. package/dist/frqtl/objects/instruments/capture-host.js +453 -0
  43. package/dist/frqtl/objects/instruments/capture.js +711 -0
  44. package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -0
  45. package/dist/frqtl/objects/kinds-entry.js +14 -0
  46. package/dist/frqtl/objects/kinds.js +190 -0
  47. package/dist/frqtl/objects/objects/Box.js +273 -0
  48. package/dist/frqtl/objects/objects/Bullet.js +155 -0
  49. package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
  50. package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
  51. package/dist/frqtl/objects/objects/DirectedTunnel.js +412 -0
  52. package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
  53. package/dist/frqtl/objects/objects/LightSource.js +354 -0
  54. package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
  55. package/dist/frqtl/objects/objects/Tube.js +471 -0
  56. package/dist/frqtl/objects/objects/absorbRadiation.js +310 -0
  57. package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
  58. package/dist/frqtl/objects/objects/controlAction.js +89 -0
  59. package/dist/frqtl/objects/objects/controlBind.js +278 -0
  60. package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
  61. package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
  62. package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
  63. package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
  64. package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
  65. package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
  66. package/dist/frqtl/objects/objects/makeControlsForObject.js +824 -0
  67. package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
  68. package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
  69. package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
  70. package/dist/frqtl/objects/objects/tagger.js +228 -0
  71. package/dist/frqtl/objects/schema/engine-scopes.js +469 -0
  72. package/dist/frqtl/objects/schema/index.js +256 -0
  73. package/dist/frqtl/objects/schema/scopes.js +457 -0
  74. package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
  75. package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
  76. package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
  77. package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
  78. package/dist/frqtl/objects/setup/_worldInit.js +162 -0
  79. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
  80. package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
  81. package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
  82. package/dist/frqtl/objects/setup/setupConeEmitter3D.js +251 -0
  83. package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -0
  84. package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
  85. package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
  86. package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
  87. package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
  88. package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
  89. package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
  90. package/dist/frqtl/objects/setup/setupLightSourceLab.js +280 -0
  91. package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
  92. package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
  93. package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
  94. package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1259 -0
  95. package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
  96. package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
  97. package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
  98. package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
  99. package/dist/frqtl/objects/v1-replacements.js +30 -0
  100. package/dist/frqtl/package-core.js +23 -0
  101. package/dist/frqtl/parts/CameraControls.js +643 -0
  102. package/dist/frqtl/parts/CameraPath.js +575 -0
  103. package/dist/frqtl/parts/CollisionActions.js +35 -0
  104. package/dist/frqtl/parts/CollisionTypes.js +31 -0
  105. package/dist/frqtl/parts/DetectorWall.js +496 -0
  106. package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
  107. package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
  108. package/dist/frqtl/parts/FrqtlVision.js +818 -0
  109. package/dist/frqtl/parts/GraphingDetectorWall.js +490 -0
  110. package/dist/frqtl/parts/LargeObject.js +406 -0
  111. package/dist/frqtl/parts/LightColor.js +255 -0
  112. package/dist/frqtl/parts/LiveChart.js +285 -0
  113. package/dist/frqtl/parts/LogicGate.js +720 -0
  114. package/dist/frqtl/parts/OvenBox.js +612 -0
  115. package/dist/frqtl/parts/PointSource.js +763 -0
  116. package/dist/frqtl/parts/Rectangle.js +200 -0
  117. package/dist/frqtl/parts/SlitLightSource.js +234 -0
  118. package/dist/frqtl/parts/SlitScreen.js +628 -0
  119. package/dist/frqtl/parts/SphereEmitter.js +946 -0
  120. package/dist/frqtl/parts/Trace.js +508 -0
  121. package/dist/frqtl/parts/Tunnel.js +1057 -0
  122. package/dist/frqtl/parts/Wall.js +1211 -0
  123. package/dist/frqtl/parts/index.js +33 -0
  124. package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
  125. package/dist/frqtl/scenes/absorbing-wall.js +32 -0
  126. package/dist/frqtl/scenes/atomic-model.js +88 -0
  127. package/dist/frqtl/scenes/big-bang.js +46 -0
  128. package/dist/frqtl/scenes/blackbody.js +81 -0
  129. package/dist/frqtl/scenes/circuit-sat.js +127 -0
  130. package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
  131. package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
  132. package/dist/frqtl/scenes/counting-wall.js +38 -0
  133. package/dist/frqtl/scenes/double-slit.js +31 -0
  134. package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
  135. package/dist/frqtl/scenes/dse-one-slit.js +114 -0
  136. package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
  137. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +244 -0
  138. package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
  139. package/dist/frqtl/scenes/dse-two-slits.js +132 -0
  140. package/dist/frqtl/scenes/empty.js +14 -0
  141. package/dist/frqtl/scenes/laser-buildup.js +49 -0
  142. package/dist/frqtl/scenes/mirror-3d.js +56 -0
  143. package/dist/frqtl/scenes/mirror-route.js +140 -0
  144. package/dist/frqtl/scenes/mirror.js +38 -0
  145. package/dist/frqtl/scenes/omni-3d.js +27 -0
  146. package/dist/frqtl/scenes/omni-source.js +22 -0
  147. package/dist/frqtl/scenes/self-interference.js +39 -0
  148. package/dist/frqtl/scenes/smoking-gun.js +122 -0
  149. package/dist/frqtl/scenes/straight-walk.js +22 -0
  150. package/dist/frqtl/scenes/sun.js +39 -0
  151. package/dist/frqtl/scenes/two-source.js +37 -0
  152. package/dist/frqtl/scenes/wave.js +26 -0
  153. package/dist/shim/bundle/frqnb.js +112 -0
  154. package/dist/shim/bundle/index.js +121 -0
  155. package/dist/shim/bundle/indexHtml.js +107 -0
  156. package/dist/shim/bundle/manifest.js +95 -0
  157. package/dist/shim/bundle/zip.js +243 -0
  158. package/dist/shim/bus.js +67 -0
  159. package/dist/shim/controls/index.js +627 -0
  160. package/dist/shim/controls/registry.js +274 -0
  161. package/dist/shim/engine/artifact-shell.js +212 -0
  162. package/dist/shim/engine/boot-phases.js +44 -0
  163. package/dist/shim/engine/engine-stack-boot.js +59 -0
  164. package/dist/shim/engine/router.js +692 -0
  165. package/dist/shim/experiment-frame/index.js +296 -0
  166. package/dist/shim/expr/ast-schema.js +369 -0
  167. package/dist/shim/field-descriptor/index.js +326 -0
  168. package/dist/shim/frame-data/index.js +224 -0
  169. package/dist/shim/index.js +67 -0
  170. package/dist/shim/notebook/canvas-guard.js +82 -0
  171. package/dist/shim/notebook/catalog.js +35 -0
  172. package/dist/shim/notebook/format.js +736 -0
  173. package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
  174. package/dist/shim/notebook/kernel.js +672 -0
  175. package/dist/shim/notebook/knob.js +151 -0
  176. package/dist/shim/notebook/module-resolver.js +260 -0
  177. package/dist/shim/notebook/notebook-knobs.js +65 -0
  178. package/dist/shim/notebook/produced-bindings.js +67 -0
  179. package/dist/shim/notebook/realm-registry.js +121 -0
  180. package/dist/shim/notebook/realm-transport.js +73 -0
  181. package/dist/shim/notebook/setup-builder.js +510 -0
  182. package/dist/shim/notebook/store.js +159 -0
  183. package/dist/shim/notebook/vendor-importmap.js +134 -0
  184. package/dist/shim/notebook/view-builder.js +189 -0
  185. package/dist/shim/notebook/view-ctx.js +100 -0
  186. package/dist/shim/notebook/view-handle.js +202 -0
  187. package/dist/shim/notebook/warmup.js +102 -0
  188. package/dist/shim/package.json +4 -0
  189. package/dist/shim/page-params/index.js +214 -0
  190. package/dist/shim/project/identity.js +112 -0
  191. package/dist/shim/project/index.js +19 -0
  192. package/dist/shim/project/link.js +402 -0
  193. package/dist/shim/project/node-specifier-hook.mjs +35 -0
  194. package/dist/shim/project/register-node.mjs +17 -0
  195. package/dist/shim/project/specifiers.js +106 -0
  196. package/dist/shim/registry/copy-gate.js +110 -0
  197. package/dist/shim/registry/exhibits.js +455 -0
  198. package/dist/shim/registry/experiments.js +215 -0
  199. package/dist/shim/registry/index.js +19 -0
  200. package/dist/shim/registry/scene-source.js +157 -0
  201. package/dist/shim/registry/walkthrough.js +807 -0
  202. package/dist/shim/render/capture-envelope.js +279 -0
  203. package/dist/shim/render/heat-colormap.js +92 -0
  204. package/dist/shim/render/render-stages.js +855 -0
  205. package/dist/shim/render/sun-look.js +33 -0
  206. package/dist/shim/render-state/apply.js +262 -0
  207. package/dist/shim/render-state/index.js +74 -0
  208. package/dist/shim/rpc/index.js +207 -0
  209. package/dist/shim/runtime/index.js +1023 -0
  210. package/dist/shim/storage/boot.js +69 -0
  211. package/dist/shim/storage/index.js +169 -0
  212. package/dist/shim/storage/local-storage-provider.js +230 -0
  213. package/dist/shim/storage/make-namespace-store.js +132 -0
  214. package/dist/shim/storage/modules.js +249 -0
  215. package/dist/shim/storage/projects.js +150 -0
  216. package/llms.txt +12 -0
  217. package/package.json +62 -4
@@ -0,0 +1,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
+