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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/CLAUDE.md +87 -0
  2. package/LICENSE.md +664 -0
  3. package/README.md +84 -2
  4. package/dist/dist-manifest.json +218 -0
  5. package/dist/eyes/index.js +109 -0
  6. package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
  7. package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
  8. package/dist/fat/pkg-node/package.json +12 -0
  9. package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
  10. package/dist/frqtl/compat/boot-node.js +34 -0
  11. package/dist/frqtl/compat/engine.js +127 -0
  12. package/dist/frqtl/compat/package-root.js +24 -0
  13. package/dist/frqtl/compat/providers/wasm-engine.js +2499 -0
  14. package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
  15. package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
  16. package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
  17. package/dist/frqtl/compat/renderer/Filters.js +163 -0
  18. package/dist/frqtl/compat/renderer/RenderFamily.js +84 -0
  19. package/dist/frqtl/compat/renderer/RenderMesh.js +99 -0
  20. package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
  21. package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
  22. package/dist/frqtl/compat/renderer/captureStill-common.js +258 -0
  23. package/dist/frqtl/compat/renderer/display-slot.js +96 -0
  24. package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
  25. package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
  26. package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
  27. package/dist/frqtl/compat/renderer/providers/index.js +22 -0
  28. package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
  29. package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
  30. package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
  31. package/dist/frqtl/compat/renderer/providers/three-provider.js +2470 -0
  32. package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
  33. package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
  34. package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
  35. package/dist/frqtl/compat/vendor.js +196 -0
  36. package/dist/frqtl/node-entry.js +17 -0
  37. package/dist/frqtl/objects/FrqtlApp.js +2920 -0
  38. package/dist/frqtl/objects/ObjectFactory.js +296 -0
  39. package/dist/frqtl/objects/RecipeContext.js +322 -0
  40. package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
  41. package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
  42. package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
  43. package/dist/frqtl/objects/instruments/capture-host.js +453 -0
  44. package/dist/frqtl/objects/instruments/capture.js +710 -0
  45. package/dist/frqtl/objects/instruments/hosted-eyes.js +984 -0
  46. package/dist/frqtl/objects/kinds-entry.js +14 -0
  47. package/dist/frqtl/objects/kinds.js +190 -0
  48. package/dist/frqtl/objects/objects/Box.js +273 -0
  49. package/dist/frqtl/objects/objects/Bullet.js +155 -0
  50. package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
  51. package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
  52. package/dist/frqtl/objects/objects/DirectedTunnel.js +411 -0
  53. package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
  54. package/dist/frqtl/objects/objects/LightSource.js +351 -0
  55. package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
  56. package/dist/frqtl/objects/objects/Tube.js +471 -0
  57. package/dist/frqtl/objects/objects/absorbRadiation.js +309 -0
  58. package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
  59. package/dist/frqtl/objects/objects/controlAction.js +89 -0
  60. package/dist/frqtl/objects/objects/controlBind.js +278 -0
  61. package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
  62. package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
  63. package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
  64. package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
  65. package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
  66. package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
  67. package/dist/frqtl/objects/objects/makeControlsForObject.js +820 -0
  68. package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
  69. package/dist/frqtl/objects/objects/slitLinerScatter.js +261 -0
  70. package/dist/frqtl/objects/objects/sustainedSource.js +298 -0
  71. package/dist/frqtl/objects/objects/tagger.js +228 -0
  72. package/dist/frqtl/objects/schema/engine-scopes.js +468 -0
  73. package/dist/frqtl/objects/schema/index.js +256 -0
  74. package/dist/frqtl/objects/schema/scopes.js +457 -0
  75. package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
  76. package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
  77. package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
  78. package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
  79. package/dist/frqtl/objects/setup/_worldInit.js +162 -0
  80. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
  81. package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
  82. package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
  83. package/dist/frqtl/objects/setup/setupConeEmitter3D.js +250 -0
  84. package/dist/frqtl/objects/setup/setupDoubleSlit.js +326 -0
  85. package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
  86. package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
  87. package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
  88. package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
  89. package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
  90. package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
  91. package/dist/frqtl/objects/setup/setupLightSourceLab.js +277 -0
  92. package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
  93. package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
  94. package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
  95. package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1258 -0
  96. package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
  97. package/dist/frqtl/objects/setup/setupTunnelLab.js +240 -0
  98. package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
  99. package/dist/frqtl/objects/setup/setupWaveInterference.js +177 -0
  100. package/dist/frqtl/objects/v1-replacements.js +29 -0
  101. package/dist/frqtl/package-core.js +23 -0
  102. package/dist/frqtl/parts/CameraControls.js +643 -0
  103. package/dist/frqtl/parts/CameraPath.js +575 -0
  104. package/dist/frqtl/parts/CollisionActions.js +34 -0
  105. package/dist/frqtl/parts/CollisionTypes.js +30 -0
  106. package/dist/frqtl/parts/DetectorWall.js +496 -0
  107. package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
  108. package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
  109. package/dist/frqtl/parts/FrqtlVision.js +818 -0
  110. package/dist/frqtl/parts/GraphingDetectorWall.js +489 -0
  111. package/dist/frqtl/parts/LargeObject.js +405 -0
  112. package/dist/frqtl/parts/LightColor.js +255 -0
  113. package/dist/frqtl/parts/LiveChart.js +285 -0
  114. package/dist/frqtl/parts/LogicGate.js +717 -0
  115. package/dist/frqtl/parts/OvenBox.js +611 -0
  116. package/dist/frqtl/parts/PointSource.js +762 -0
  117. package/dist/frqtl/parts/Rectangle.js +200 -0
  118. package/dist/frqtl/parts/SlitLightSource.js +234 -0
  119. package/dist/frqtl/parts/SlitScreen.js +628 -0
  120. package/dist/frqtl/parts/SphereEmitter.js +946 -0
  121. package/dist/frqtl/parts/Trace.js +508 -0
  122. package/dist/frqtl/parts/Tunnel.js +1057 -0
  123. package/dist/frqtl/parts/Wall.js +1211 -0
  124. package/dist/frqtl/parts/index.js +33 -0
  125. package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
  126. package/dist/frqtl/scenes/absorbing-wall.js +32 -0
  127. package/dist/frqtl/scenes/atomic-model.js +88 -0
  128. package/dist/frqtl/scenes/big-bang.js +46 -0
  129. package/dist/frqtl/scenes/blackbody.js +81 -0
  130. package/dist/frqtl/scenes/circuit-sat.js +127 -0
  131. package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
  132. package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
  133. package/dist/frqtl/scenes/counting-wall.js +38 -0
  134. package/dist/frqtl/scenes/double-slit.js +31 -0
  135. package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
  136. package/dist/frqtl/scenes/dse-one-slit.js +114 -0
  137. package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
  138. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +243 -0
  139. package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
  140. package/dist/frqtl/scenes/dse-two-slits.js +132 -0
  141. package/dist/frqtl/scenes/empty.js +14 -0
  142. package/dist/frqtl/scenes/laser-buildup.js +49 -0
  143. package/dist/frqtl/scenes/mirror-3d.js +56 -0
  144. package/dist/frqtl/scenes/mirror-route.js +140 -0
  145. package/dist/frqtl/scenes/mirror.js +38 -0
  146. package/dist/frqtl/scenes/omni-3d.js +27 -0
  147. package/dist/frqtl/scenes/omni-source.js +22 -0
  148. package/dist/frqtl/scenes/self-interference.js +39 -0
  149. package/dist/frqtl/scenes/smoking-gun.js +122 -0
  150. package/dist/frqtl/scenes/straight-walk.js +22 -0
  151. package/dist/frqtl/scenes/sun.js +39 -0
  152. package/dist/frqtl/scenes/two-source.js +37 -0
  153. package/dist/frqtl/scenes/wave.js +26 -0
  154. package/dist/shim/bundle/frqnb.js +112 -0
  155. package/dist/shim/bundle/index.js +121 -0
  156. package/dist/shim/bundle/indexHtml.js +107 -0
  157. package/dist/shim/bundle/manifest.js +95 -0
  158. package/dist/shim/bundle/zip.js +243 -0
  159. package/dist/shim/bus.js +67 -0
  160. package/dist/shim/controls/index.js +627 -0
  161. package/dist/shim/controls/registry.js +274 -0
  162. package/dist/shim/engine/artifact-shell.js +211 -0
  163. package/dist/shim/engine/boot-phases.js +44 -0
  164. package/dist/shim/engine/engine-stack-boot.js +59 -0
  165. package/dist/shim/engine/router.js +690 -0
  166. package/dist/shim/experiment-frame/index.js +296 -0
  167. package/dist/shim/expr/ast-schema.js +369 -0
  168. package/dist/shim/field-descriptor/index.js +325 -0
  169. package/dist/shim/frame-data/index.js +224 -0
  170. package/dist/shim/index.js +67 -0
  171. package/dist/shim/notebook/canvas-guard.js +82 -0
  172. package/dist/shim/notebook/catalog.js +35 -0
  173. package/dist/shim/notebook/format.js +736 -0
  174. package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
  175. package/dist/shim/notebook/kernel.js +671 -0
  176. package/dist/shim/notebook/knob.js +150 -0
  177. package/dist/shim/notebook/module-resolver.js +260 -0
  178. package/dist/shim/notebook/notebook-knobs.js +65 -0
  179. package/dist/shim/notebook/produced-bindings.js +67 -0
  180. package/dist/shim/notebook/realm-registry.js +121 -0
  181. package/dist/shim/notebook/realm-transport.js +73 -0
  182. package/dist/shim/notebook/setup-builder.js +507 -0
  183. package/dist/shim/notebook/store.js +159 -0
  184. package/dist/shim/notebook/vendor-importmap.js +134 -0
  185. package/dist/shim/notebook/view-builder.js +189 -0
  186. package/dist/shim/notebook/view-ctx.js +100 -0
  187. package/dist/shim/notebook/view-handle.js +202 -0
  188. package/dist/shim/notebook/warmup.js +102 -0
  189. package/dist/shim/package.json +4 -0
  190. package/dist/shim/page-params/index.js +213 -0
  191. package/dist/shim/project/identity.js +112 -0
  192. package/dist/shim/project/index.js +19 -0
  193. package/dist/shim/project/link.js +402 -0
  194. package/dist/shim/project/node-specifier-hook.mjs +35 -0
  195. package/dist/shim/project/register-node.mjs +17 -0
  196. package/dist/shim/project/specifiers.js +106 -0
  197. package/dist/shim/registry/copy-gate.js +110 -0
  198. package/dist/shim/registry/exhibits.js +455 -0
  199. package/dist/shim/registry/experiments.js +215 -0
  200. package/dist/shim/registry/index.js +19 -0
  201. package/dist/shim/registry/scene-source.js +157 -0
  202. package/dist/shim/registry/walkthrough.js +804 -0
  203. package/dist/shim/render/capture-envelope.js +279 -0
  204. package/dist/shim/render/heat-colormap.js +92 -0
  205. package/dist/shim/render/render-stages.js +855 -0
  206. package/dist/shim/render/sun-look.js +33 -0
  207. package/dist/shim/render-state/apply.js +262 -0
  208. package/dist/shim/render-state/index.js +74 -0
  209. package/dist/shim/rpc/index.js +207 -0
  210. package/dist/shim/runtime/index.js +1023 -0
  211. package/dist/shim/storage/boot.js +69 -0
  212. package/dist/shim/storage/index.js +169 -0
  213. package/dist/shim/storage/local-storage-provider.js +230 -0
  214. package/dist/shim/storage/make-namespace-store.js +132 -0
  215. package/dist/shim/storage/modules.js +249 -0
  216. package/dist/shim/storage/projects.js +150 -0
  217. package/llms.txt +12 -0
  218. package/package.json +63 -4
@@ -0,0 +1,116 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * DataCollector — Base class for tick-by-tick metric sampling.
5
+ *
6
+ * Subclasses override collectMetrics() to return experiment-specific
7
+ * key/value pairs. The base class handles sample scheduling, time-series
8
+ * accumulation, and result formatting.
9
+ *
10
+ * Works with both the JS engine (property getter for activeQuanta) and
11
+ * the WASM engine (method call for activeQuanta).
12
+ *
13
+ * An ES module: `export { DataCollector }`.
14
+ */
15
+
16
+ // ── Helpers ──────────────────────────────────────────────────────────
17
+
18
+ /**
19
+ * Get active quanta count from a universe, handling both the JS engine
20
+ * (property getter) and the WASM engine (method call) patterns.
21
+ *
22
+ * @param {Object} universe
23
+ * @returns {number}
24
+ */
25
+ function getActiveQuanta(universe) {
26
+ if (typeof universe.activeQuanta === 'function') {
27
+ return universe.activeQuanta();
28
+ }
29
+ return universe.activeQuanta;
30
+ }
31
+
32
+ // ── DataCollector ────────────────────────────────────────────────────
33
+
34
+ /**
35
+ * Base collector for tick-by-tick metric sampling.
36
+ *
37
+ * @param {Object} [options]
38
+ * @param {number} [options.sampleInterval=100] - Record a sample every N ticks
39
+ * @param {Object} [options.params] - Experiment params to attach to results
40
+ */
41
+ function DataCollector(options) {
42
+ options = options || {};
43
+ this.sampleInterval = options.sampleInterval !== undefined ? options.sampleInterval : 100;
44
+ this.params = options.params || {};
45
+ this.timeSeries = [];
46
+ }
47
+
48
+ /**
49
+ * Called every tick by the runner. Records a sample if
50
+ * tick % sampleInterval === 0.
51
+ *
52
+ * @param {number} tick - Current tick number (1-based)
53
+ * @param {Object} universe - Engine universe instance
54
+ * @param {Object} setupResult - Object returned by the setup factory
55
+ * @returns {Object|null} The sample object, or null if not a sample tick
56
+ */
57
+ DataCollector.prototype.sample = function (tick, universe, setupResult) {
58
+ if (tick % this.sampleInterval !== 0) {
59
+ return null;
60
+ }
61
+
62
+ var metrics = this.collectMetrics(universe, setupResult);
63
+ var sample = { tick: tick };
64
+
65
+ // Merge metrics into sample
66
+ for (var key in metrics) {
67
+ if (metrics.hasOwnProperty(key)) {
68
+ sample[key] = metrics[key];
69
+ }
70
+ }
71
+
72
+ this.timeSeries.push(sample);
73
+ return sample;
74
+ };
75
+
76
+ /**
77
+ * Override in subclasses to return experiment-specific metrics.
78
+ * The base implementation records only activeQuanta.
79
+ *
80
+ * @param {Object} universe - Engine universe instance
81
+ * @param {Object} setupResult - Object returned by the setup factory
82
+ * @returns {Object} Flat object of metric key/value pairs
83
+ */
84
+ DataCollector.prototype.collectMetrics = function (universe, setupResult) {
85
+ return {
86
+ activeQuanta: getActiveQuanta(universe)
87
+ };
88
+ };
89
+
90
+ /**
91
+ * Return all collected data.
92
+ *
93
+ * @returns {{ params: Object, timeSeries: Array }}
94
+ */
95
+ DataCollector.prototype.getResults = function () {
96
+ return {
97
+ params: this.params,
98
+ timeSeries: this.timeSeries.slice()
99
+ };
100
+ };
101
+
102
+ /**
103
+ * Reset state for the next seed run.
104
+ */
105
+ DataCollector.prototype.reset = function () {
106
+ this.timeSeries = [];
107
+ };
108
+
109
+ // ── Static helper (re-exported for subclass convenience) ─────────────
110
+
111
+ DataCollector.getActiveQuanta = getActiveQuanta;
112
+
113
+ // ── Export ───────────────────────────────────────────────────────────
114
+
115
+ export { DataCollector };
116
+
@@ -0,0 +1,178 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * WallBehaviors — the ONE authority for the Collision LEXICON v2: the wall
5
+ * vocabulary (`collisionBehavior` × `bounceDirection` × composable hooks × presets)
6
+ * and the structured resolver that turns a wall spec into a compiled op-program.
7
+ *
8
+ * ── Schema-mirror discipline (W1(ii)/(iv) guard) ─────────────────────────────
9
+ * The hook table FERRIES the frozen `CollisionActions` constant set BY REFERENCE —
10
+ * it NEVER re-types a SCREAMING_SNAKE string. The vocabulary keys ARE the enum: the
11
+ * `Wall` descriptor's enum fields read these maps (never a hand-listed enum),
12
+ * `Wall.js` reads the resolver, and the conformance/compiler tests drive their
13
+ * expectations off these same tables — so a behavior that works through one wall
14
+ * composite but not another is impossible: they all compile through the SAME
15
+ * `actionCompiler` from the SAME owned constants.
16
+ *
17
+ * ── The behavior axis (all compiled to op-programs by actionCompiler.js) ─────
18
+ * nothing — pass through (no ops at all).
19
+ * block — STOP at the surface: eject to the closest position outside, velocity
20
+ * KEPT (it parks at the face and keeps pushing).
21
+ * bounce — TURN AROUND: eject + a direction response (`bounceDirection`:
22
+ * reflect = the incidence flip, random = a scatter redraw).
23
+ * absorb — the RADIATING absorber: kill + tally the mass into the wall's REAL
24
+ * backing frame + re-radiate (conserving).
25
+ * destroy — the plain kill (the mass sink at an experiment boundary).
26
+ * Hooks (`mark`/`count`/`recolor`) compose with ANY behavior.
27
+ */
28
+
29
+ import { CollisionTypes } from '../../parts/CollisionTypes.js';
30
+ import { CollisionActions } from '../../parts/CollisionActions.js';
31
+ import { compileWallV2 } from '../objects/actionCompiler.js';
32
+
33
+
34
+
35
+ // ═══════════════════════════════════════════════════════════════════════
36
+ // Collision LEXICON v2 — the STUDIO/palette surface and the ONLY
37
+ // wall vocabulary. Three ferried vocab tables + a structured resolver that
38
+ // delegates to the ONE compiler authority (compileWallV2 in actionCompiler.js).
39
+ // ═══════════════════════════════════════════════════════════════════════
40
+
41
+ // behavior axis (ONE of): nothing | block | bounce | absorb | destroy. Keys ARE the enum.
42
+ // Ordered by escalating response to a hit: pass through → stop → turn → consume → remove.
43
+ // nothing — pass-through (no ops at all).
44
+ // block — STOP the particle AT the surface: reposition it to the closest position
45
+ // OUTSIDE the wall, velocity UNTOUCHED. Literally `bounce` minus the reflect
46
+ // ops. MEASURED head-to-head against `bounce` on the canonical WASM engine
47
+ // (wall x∈[200,220], leaf launched x=150 vx=+1): both stop penetration at
48
+ // x=199, but `bounce` flips to vx=−1 and has RECEDED to x=158.5 by t=90,
49
+ // while `block` keeps vx=+1 and PARKS at x=198.5 for every remaining tick.
50
+ // Same contact tick (45), opposite outcome — so `bounce` is NOT a stand-in
51
+ // for `block` (the eject-only op-set is the RESISTANCE intuition: a particle
52
+ // cannot move into occupied space, and nothing gives it a reason to turn).
53
+ // bounce — a motion response (see bounceDirection).
54
+ // absorb — kill + tally into the wall's REAL backing frame + radiate (conserving).
55
+ // destroy — plain kill.
56
+ var WALL_BEHAVIORS = { nothing: 'nothing', block: 'block', bounce: 'bounce', absorb: 'absorb', destroy: 'destroy' };
57
+ // bounceDirection (when behavior==bounce): reflect | random.
58
+ var BOUNCE_DIRECTIONS = { reflect: 'reflect', random: 'random' };
59
+ // hooks (composable with ANY behavior) — FERRIED from the frozen CollisionActions set.
60
+ // `mark`/`count` FERRY the frozen engine CollisionActions. `recolor` is a RENDER-INTENT
61
+ // hook (not an engine action — the engine has no color, I1): it stamps the wall's OWN
62
+ // drawn color into the collided frame's opaque string_map['render.color'], which the
63
+ // renderer providers already read. It is MARK's sibling (a string_map write the engine
64
+ // passes through), so it lives in the SAME composable-hooks slot; its value is a plain
65
+ // marker (there is no CollisionActions.RECOLOR to ferry).
66
+ var WALL_HOOKS = { mark: CollisionActions.MARK, count: CollisionActions.COUNT, recolor: 'recolor' };
67
+
68
+ function _valuesMap(obj) { var m = {}; Object.keys(obj).forEach(function (k) { m[k] = k; }); return m; }
69
+
70
+ /**
71
+ * Resolve the v2 nested wall spec to a compiled op-program + flags via the ONE
72
+ * compiler authority (compileWallV2). FAIL-LOUD on an unknown behavior/direction.
73
+ * @param {Object} props — { collisionBehavior, bounceDirection, angleConstraint,
74
+ * decoherence, hooks, markTag, color, capacity, emissivity, reposition, orientationDeg }
75
+ * @returns {{ program, collectCollided, radiation, behavior, hooks }}
76
+ */
77
+ function resolveWallSpec(props) {
78
+ props = props || {};
79
+ var behavior = props.collisionBehavior || 'nothing';
80
+ if (!WALL_BEHAVIORS[behavior]) {
81
+ throw new Error('[WallBehaviors] unknown collisionBehavior "' + behavior + '" — must be one of [' +
82
+ Object.keys(WALL_BEHAVIORS).join(', ') + '].');
83
+ }
84
+ if (behavior === 'bounce' && props.bounceDirection && !BOUNCE_DIRECTIONS[props.bounceDirection]) {
85
+ throw new Error('[WallBehaviors] unknown bounceDirection "' + props.bounceDirection + '" — must be one of [' +
86
+ Object.keys(BOUNCE_DIRECTIONS).join(', ') + '].');
87
+ }
88
+ var hooks = Array.isArray(props.hooks) ? props.hooks : [];
89
+ hooks.forEach(function (hk) {
90
+ if (!WALL_HOOKS[hk]) {
91
+ throw new Error('[WallBehaviors] unknown hook "' + hk + '" — must be one of [' +
92
+ Object.keys(WALL_HOOKS).join(', ') + '].');
93
+ }
94
+ });
95
+ var compiled = compileWallV2({
96
+ behavior: behavior,
97
+ bounceDirection: props.bounceDirection || 'reflect',
98
+ angleConstraint: props.angleConstraint,
99
+ orientationDeg: props.orientationDeg,
100
+ decoherence: props.decoherence,
101
+ hooks: hooks,
102
+ markTag: props.markTag,
103
+ // The wall's OWN drawn color — the ONE authority the `recolor` hook derives its
104
+ // render.color tag from at compile time (ferried, never a parallel color field).
105
+ color: props.color,
106
+ capacity: props.capacity,
107
+ // ABSORB radiating efficiency (fullness fraction of the emitted leaf). Omitted ⇒
108
+ // absorbRadiation's own default — no second copy of the default lives here.
109
+ emissivity: props.emissivity,
110
+ reposition: props.reposition || 'closest',
111
+ // Z1 — a real z-slab wall reflects/ejects on all six faces (3-axis incidence
112
+ // + closest3d eject). Derived from the caller (Wall.js reads it off the rect);
113
+ // omitted/false ⇒ the 2D path (byte-identical, SU1).
114
+ is3D: props.is3D === true,
115
+ // MIRROR rule — the TRUE slab dims (Wall reads them off the rect) so a tilted 2D
116
+ // reflect wall can bake the OBB-NORMAL eject (perpendicular to the drawn face) instead
117
+ // of the beam-scattering AABB 'closest'. Omitted ⇒ the AABB eject (byte-identical, SU1).
118
+ slabW: props.slabW,
119
+ slabH: props.slabH,
120
+ // R2AX (two-axis mirror) — the plate's up/down tilt + z-depth,
121
+ // ferried so a pitched plate compiles the 3-space reflection about SlabFace's
122
+ // plate3.n. Omitted ⇒ byte-identical un-pitched programs (SU1).
123
+ pitchDeg: props.pitchDeg,
124
+ slabD: props.slabD
125
+ });
126
+ return { program: compiled.program, collectCollided: compiled.collectCollided,
127
+ radiation: compiled.radiation, behavior: behavior, hooks: hooks.slice() };
128
+ }
129
+
130
+ // ── Presets (B6) — pure CONSTRUCTION sugar expanding to the v2 descriptor fields.
131
+ // The recipe/codegen round-trips the EXPANDED fields (never the preset name).
132
+ //
133
+ // THE NAMING RULE: a preset's NAME MUST ASSERT ITS BEHAVIOR.
134
+ // `collisionBehavior:'nothing'` (pass-through) belongs ONLY on a preset whose name SAYS
135
+ // it passes through — `transparent-probe` is the one that earns it. A palette entry
136
+ // labelled "Wall" that particles walk through is the same defect class as mistaking
137
+ // `Tunnel{mode:'mirror'}` for a mirror: a name asserting behavior the part does not have.
138
+ // Before `block` existed there was no blocking behavior in the lexicon to point `wall` at,
139
+ // so `wall → nothing` was as honest as the vocabulary allowed — and simultaneously
140
+ // REDUNDANT with `transparent-probe`. `block` removed that reason, so `wall` now blocks.
141
+ // Apply this rule to every preset added here.
142
+ var WALL_PRESETS = {
143
+ 'mirror': { label: 'Mirror', collisionBehavior: 'bounce', bounceDirection: 'reflect' },
144
+ 'brick': { label: 'Brick Wall', collisionBehavior: 'bounce', bounceDirection: 'random', decoherence: false },
145
+ 'detector-screen': { label: 'Detector Screen', collisionBehavior: 'destroy', hooks: ['count'], countFilter: 'any' },
146
+ 'transparent-probe': { label: 'Transparent Probe', collisionBehavior: 'nothing', hooks: ['mark', 'count'] },
147
+ // A "Wall" is SOLID: it stops the particle at the surface, velocity kept (see `block`
148
+ // above). For a wall-shaped instrument that does NOT stop anything, use
149
+ // `transparent-probe` — that is what its name is for.
150
+ 'wall': { label: 'Wall', collisionBehavior: 'block' }
151
+ };
152
+ /** Expand a preset key to its v2 descriptor-field bag (FRESH copy). FAIL-LOUD on unknown. */
153
+ function expandWallPreset(key) {
154
+ var p = WALL_PRESETS[key];
155
+ if (!p) {
156
+ throw new Error('[WallBehaviors] unknown wall preset "' + key + '" — must be one of [' +
157
+ Object.keys(WALL_PRESETS).join(', ') + '].');
158
+ }
159
+ var out = {}; Object.keys(p).forEach(function (k) { if (k !== 'label') { out[k] = Array.isArray(p[k]) ? p[k].slice() : p[k]; } });
160
+ return out;
161
+ }
162
+
163
+ function wallBehaviorV2ValuesMap() { return _valuesMap(WALL_BEHAVIORS); }
164
+ function bounceDirectionValuesMap() { return _valuesMap(BOUNCE_DIRECTIONS); }
165
+ function wallHookValuesMap() { return _valuesMap(WALL_HOOKS); }
166
+
167
+ // Collision LEXICON v2 — the ONE wall vocabulary.
168
+ export {
169
+ WALL_BEHAVIORS,
170
+ BOUNCE_DIRECTIONS,
171
+ WALL_HOOKS,
172
+ wallBehaviorV2ValuesMap,
173
+ bounceDirectionValuesMap,
174
+ wallHookValuesMap,
175
+ resolveWallSpec,
176
+ WALL_PRESETS,
177
+ expandWallPreset
178
+ };
@@ -0,0 +1,258 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * SlabFace.js — the SOLE face / OBB / convention authority for a rotated Wall slab.
5
+ *
6
+ * ## Why this file exists (the DRY crux — Gate-2, circulation §1)
7
+ *
8
+ * A Wall mirror's reflecting FACE is derived from its `orientationDeg` in what USED to be
9
+ * three independent, hand-authored places, each re-writing the SAME "tall slab, y-DOWN
10
+ * screen handedness" convention:
11
+ * 1. the renderer draws the slab as `rect(-w/2,-h/2,w,h)` (h>w) rotated by +orientationDeg;
12
+ * 2. `actionCompiler._buildReflectOps` baked `alphaScreen = (90 + orientationDeg) % 360`
13
+ * (the drawn-tall-face reflection line);
14
+ * 3. the engine swept-OBB narrow-phase needs the slab's rotated collision region.
15
+ *
16
+ * A hand-mirrored sign in ANY of the three re-creates the off-face deflection bug seen
17
+ * when rotating a mirror (the "1A/1D" class). This module is the ONE place the
18
+ * convention is written: `orientationDeg (+ slab aspect, z-slab) → canonical face geometry`.
19
+ * `_buildReflectOps` reads `reflectBaseDeg` from here; `Wall.compileWallV2` reads `obb` from
20
+ * here for the engine `region` field. The renderer consumes the RAW `orientationDeg` (the
21
+ * primary reference this convention is DEFINED to match — no second convention there).
22
+ *
23
+ * ## The convention (single authority — do not re-derive elsewhere)
24
+ *
25
+ * - **Drawn face.** The Wall mirror is a TALL slab (h > w). At orientation 0 its long face
26
+ * is VERTICAL — the line at 90° in the engine's y-DOWN velocity coordinates — so a +x beam
27
+ * bounces straight back (−x), exactly what the cardinal incidence tree does. p5/three draw
28
+ * the slab rotated by +orientationDeg, so the drawn face LINE sits at `(90 + orientationDeg)`.
29
+ * ⇒ `reflectBaseDeg = (90 + orientationDeg) % 360` — reflect about the line the user SEES.
30
+ * - **OBB geometry (convention-FREE, the ONLY thing that crosses the ABI).** The engine
31
+ * deserializes `{ theta, hw, hh, hz }` and does a segment-vs-OBB test in ITS OWN coordinate
32
+ * frame (`region.rs`, `Obb::to_local` rotates world by −theta). The slab's local x-axis
33
+ * (thin, `hw = w/2`) and local y-axis (tall, `hh = h/2`) are world-rotated by +orientationDeg
34
+ * to match the drawn rect. ⇒ `theta = orientationDeg` (RADIANS). There is NO "90+" in the
35
+ * OBB — the reflection base and the OBB angle are DIFFERENT derived views of one input, and
36
+ * the raw `orientationDeg` NEVER crosses to the engine (storing it there = the convention-
37
+ * drift bug the circulation rejects).
38
+ *
39
+ * ## Cardinal gate (one threshold, shared with the reflect-law cardinal branch)
40
+ *
41
+ * `isCardinal` (orientationDeg ≡ 0 mod 90 within EPS) routes BOTH: (a) `_buildReflectOps` to
42
+ * the axis-aligned incidence tree, and (b) the engine to the byte-identical AABB path
43
+ * (`obb === null` ⇒ the sugar emits `region: null`). Aligning the two gates on one authority
44
+ * means "is this cardinal" is answered ONCE. At a cardinal, `obb` is `null` (byte-identical,
45
+ * SU1) — the region is the AABB and nothing rotated is ever sent.
46
+ *
47
+ * ## Layer 1 — API contract
48
+ *
49
+ * `resolveSlabFace({ cx, cy, w, h, orientationDeg, z, d }) → {`
50
+ * `isCardinal: boolean,` // orientationDeg ≡ 0 (mod 90) within EPS
51
+ * `reflectBaseDeg: number,` // (90 + orientationDeg) % 360 — the value _buildReflectOps bakes
52
+ * `obb: {` // convention-RESOLVED geometry (null when isCardinal)
53
+ * `cx, cy, cz,` // center (LO backing-frame center at collide time; JS-side/tests)
54
+ * `hw, hh, hz,` // half-extents (w/2, h/2, d/2) — hz 0 for a 2D slab
55
+ * `theta,` // RADIANS, engine y-DOWN frame — the ONLY angle that crosses the ABI
56
+ * `faceDir: { x, y },` // unit drawn-face LINE dir, ENGINE (y-down) frame = the reflect
57
+ * // law's invariant line (JS-side predicates; not on the ABI)
58
+ * `normal: { x, y }` // faceDir ⟂ (+90) = the axis R(reflectBaseDeg) FLIPS — the
59
+ * // tilted-reflect guard's crossing normal (sign conventional;
60
+ * // the guard is n→−n invariant). Not on the ABI.
61
+ * `} | null`
62
+ * `}`
63
+ * @param {Object} spec
64
+ * @param {number} [spec.cx=0] emission/collision center X (px) — the LO re-centered center.
65
+ * @param {number} [spec.cy=0] center Y (px).
66
+ * @param {number} spec.w slab width (px, physics). Thin axis for a tall mirror.
67
+ * @param {number} spec.h slab height (px, physics). Long face axis for a tall mirror.
68
+ * @param {number} [spec.orientationDeg=0] mirror SURFACE angle (deg). Normalized to [0,360).
69
+ * @param {number} [spec.z=0] slab z-center (px) — 3D wall; 0 for 2D.
70
+ * @param {number} [spec.d=0] slab z-depth (px) — 3D wall; 0/absent ⇒ hz 0 (2D passthrough).
71
+ * @param {number} [spec.pitchDeg=0] up/down tilt about the plate's own long axis (deg,
72
+ * [-90,90]; 3D wall REQUIRED). ≠0 ⇒ the PITCHED result shape: `{ pitched:true, obb:null,
73
+ * plate3:{ n,u,v, hw,hh,hz, aabb } }` — the true 3-space plate basis; NO engine region
74
+ * (z-theta-only contract; the in-op guard is the narrow phase). 0/absent ⇒ byte-identical
75
+ * un-pitched behavior (this whole block is opt-in).
76
+ *
77
+ * ## Layer 2 — physics behavior
78
+ * ZERO physics of its own (SU1). This is a pure geometry derivation consumed by the reflect
79
+ * op-program (velocity RESPONSE) and the engine narrow-phase (collision MEMBERSHIP). Rotating
80
+ * the mirror sweeps the drawn face; `reflectBaseDeg`/`theta` follow it so the bounce happens
81
+ * AT the face the user sees and the collision REGION is that same rotated slab. A moved bench
82
+ * golden would mean physics leaked into this convenience layer — the SU1 bug.
83
+ */
84
+
85
+
86
+
87
+ var EPS = 1e-9;
88
+ var DEG2RAD = Math.PI / 180;
89
+
90
+ /**
91
+ * Resolve a slab's face convention + OBB geometry from ONE input (orientationDeg).
92
+ * See the file header for the full contract. Convention-free geometry only crosses
93
+ * the ABI (theta + half-extents); the raw orientationDeg NEVER does.
94
+ * @param {Object} spec
95
+ * @returns {{ isCardinal: boolean, reflectBaseDeg: number, obb: Object|null }}
96
+ */
97
+ function resolveSlabFace(spec) {
98
+ spec = spec || {};
99
+ var w = +spec.w;
100
+ var h = +spec.h;
101
+ var o = (typeof spec.orientationDeg === 'number' && isFinite(spec.orientationDeg))
102
+ ? spec.orientationDeg : 0;
103
+ // ── PITCH (two-axis mirror) ────────────────────────────────────────────────
104
+ // `pitchDeg` rotates the plate about its OWN in-plane LONG axis (the drawn face
105
+ // line), tipping the face normal out of the xy-plane: positive pitch tips the
106
+ // normal toward +z. The TRUE 3-space plate basis:
107
+ // u = (cos rb, sin rb, 0) — long axis (pitch rotation axis)
108
+ // n = (n2.x·cos p, n2.y·cos p, sin p) — face normal (the reflection axis)
109
+ // v = u × n — depth axis (pitched)
110
+ // pitch 0 (or absent) is BYTE-IDENTICAL to the un-pitched contract (the z-axis
111
+ // orientation law with vz passing through) — the pitched branch below is opt-in.
112
+ // A pitched plate has NO engine ObbRegion (the region contract is z-theta-only):
113
+ // callers emit region null and use the plate's 3D AABB as the broad phase; the
114
+ // in-op guard (actionCompiler) is the narrow phase. Engine two-angle OBB is a
115
+ // flagged Gate-1 follow-up.
116
+ var p = (typeof spec.pitchDeg === 'number' && isFinite(spec.pitchDeg)) ? spec.pitchDeg : 0;
117
+ if (Math.abs(p) > EPS) {
118
+ if (p < -90 || p > 90) {
119
+ throw new Error('[SlabFace] pitchDeg must be within [-90, 90]; got ' + p);
120
+ }
121
+ var dP = (typeof spec.d === 'number' && isFinite(spec.d) && spec.d > 0) ? spec.d : 0;
122
+ if (!(dP > 0)) {
123
+ throw new Error('[SlabFace] a PITCHED plate requires a real z-depth (3D wall) — ' +
124
+ 'pitchDeg ' + p + ' with d=' + spec.d + '. Give the wall a z/depth first.');
125
+ }
126
+ var onormP = ((o % 360) + 360) % 360;
127
+ var rbP = (90 + onormP) % 360;
128
+ var gP = rbP * DEG2RAD;
129
+ var prad = p * DEG2RAD;
130
+ var u3 = { x: Math.cos(gP), y: Math.sin(gP), z: 0 };
131
+ var n2p = { x: -Math.sin(gP), y: Math.cos(gP) };
132
+ var n3 = { x: n2p.x * Math.cos(prad), y: n2p.y * Math.cos(prad), z: Math.sin(prad) };
133
+ var v3 = { // u × n
134
+ x: u3.y * n3.z - u3.z * n3.y,
135
+ y: u3.z * n3.x - u3.x * n3.z,
136
+ z: u3.x * n3.y - u3.y * n3.x
137
+ };
138
+ var hwP = w / 2, hhP = h / 2, hzP = dP / 2;
139
+ var cxP = (typeof spec.cx === 'number' && isFinite(spec.cx)) ? spec.cx : 0;
140
+ var cyP = (typeof spec.cy === 'number' && isFinite(spec.cy)) ? spec.cy : 0;
141
+ var czP = (typeof spec.z === 'number' && isFinite(spec.z)) ? spec.z : 0;
142
+ return {
143
+ isCardinal: false,
144
+ pitched: true,
145
+ reflectBaseDeg: rbP,
146
+ obb: null, // NO engine region for a pitched plate (z-theta-only contract)
147
+ plate3: {
148
+ cx: cxP, cy: cyP, cz: czP,
149
+ n: n3, u: u3, v: v3,
150
+ hw: hwP, hh: hhP, hz: hzP,
151
+ // World-axis-aligned half-extents of the pitched plate (the broad-phase
152
+ // superset box — the ONE place these are derived).
153
+ aabb: {
154
+ hx: Math.abs(n3.x) * hwP + Math.abs(u3.x) * hhP + Math.abs(v3.x) * hzP,
155
+ hy: Math.abs(n3.y) * hwP + Math.abs(u3.y) * hhP + Math.abs(v3.y) * hzP,
156
+ hz: Math.abs(n3.z) * hwP + Math.abs(u3.z) * hhP + Math.abs(v3.z) * hzP
157
+ }
158
+ }
159
+ };
160
+ }
161
+ // Normalize to [0,360) so 315, −45, 675 all collapse to their canonical residue —
162
+ // the SAME normalization _buildReflectOps used (`((o % 360) + 360) % 360`), so the
163
+ // repoint is byte-identical.
164
+ var onorm = ((o % 360) + 360) % 360;
165
+ var mod90 = onorm % 90;
166
+ var isCardinal = (mod90 < EPS) || ((90 - mod90) < EPS);
167
+
168
+ // reflectBaseDeg = the drawn TALL-face line angle in the engine y-DOWN frame.
169
+ // Identical to the value the pre-repoint _buildReflectOps baked as `alphaScreen`.
170
+ var reflectBaseDeg = (90 + onorm) % 360;
171
+
172
+ var obb = null;
173
+ if (!isCardinal) {
174
+ var cx = (typeof spec.cx === 'number' && isFinite(spec.cx)) ? spec.cx : 0;
175
+ var cy = (typeof spec.cy === 'number' && isFinite(spec.cy)) ? spec.cy : 0;
176
+ var cz = (typeof spec.z === 'number' && isFinite(spec.z)) ? spec.z : 0;
177
+ var d = (typeof spec.d === 'number' && isFinite(spec.d) && spec.d > 0) ? spec.d : 0;
178
+ // theta = the slab's OWN rotation (matches the drawn rect rotated +orientationDeg).
179
+ // NOT reflectBaseDeg — the OBB is the geometric slab, the reflect base is the face
180
+ // LINE. Both are pure functions of the same onorm; neither is a second source.
181
+ var theta = onorm * DEG2RAD;
182
+ // Auxiliary face vectors (JS-side predicates / guards / tests — do NOT cross the
183
+ // ABI), in the ENGINE frame (y-DOWN screen coords — the frame frame.x/y/vx/vy and
184
+ // the reflect law's algebra live in):
185
+ // faceDir = (cos rb, sin rb) — the drawn long-face LINE direction. This is BOTH
186
+ // (a) the p5-drawn long axis (local (0,1) rotated +orientationDeg in screen
187
+ // coords: (−sin o, cos o) ≡ (cos(90+o), sin(90+o))), and
188
+ // (b) the +1-eigenvector (invariant line) of the reflect law R(reflectBaseDeg)
189
+ // = [cos2α sin2α; sin2α −cos2α] that `_buildReflectOps` bakes.
190
+ // normal = (−sin rb, cos rb) — faceDir rotated +90 in the same algebra: the
191
+ // −1-eigenvector R actually FLIPS, i.e. the crossing normal the tilted-reflect
192
+ // guard trigger tests against. Its overall SIGN is conventional (the guard is
193
+ // invariant under n → −n: flipping n flips both s(P) and v·n, and the guard
194
+ // only reads their products).
195
+ // One frame, one convention: drawn face ≡ reflect-law invariant line ≡ guard line.
196
+ // (A y-flipped variant here is the exact hand-mirrored-sign bug circulation §1 bans.)
197
+ var g = reflectBaseDeg * DEG2RAD;
198
+ var faceDir = { x: Math.cos(g), y: Math.sin(g) };
199
+ var normal = { x: -faceDir.y, y: faceDir.x };
200
+ obb = {
201
+ cx: cx, cy: cy, cz: cz,
202
+ hw: w / 2, hh: h / 2, hz: d / 2,
203
+ theta: theta,
204
+ faceDir: faceDir,
205
+ normal: normal
206
+ };
207
+ }
208
+
209
+ return { isCardinal: isCardinal, reflectBaseDeg: reflectBaseDeg, obb: obb };
210
+ }
211
+
212
+ /**
213
+ * Extract the CONVENTION-FREE, center-less engine `region` field from a resolved obb.
214
+ * The engine re-centers the LO's rect on its backing frame every tick, so the stored
215
+ * region carries NO center (a stored center would go stale) — only `{ kind, theta, hw,
216
+ * hh, hz }`. Returns `null` when there is no obb (cardinal ⇒ the AABB path). This is the
217
+ * ONE adapter from SlabFace geometry to the `LargeObject.region` schema field / ABI.
218
+ * @param {Object|null} obb — the `.obb` from resolveSlabFace (or null).
219
+ * @returns {{ kind: 'obb', theta: number, hw: number, hh: number, hz: number }|null}
220
+ */
221
+ function regionFromObb(obb) {
222
+ if (!obb) { return null; }
223
+ return { kind: 'obb', theta: obb.theta, hw: obb.hw, hh: obb.hh, hz: obb.hz };
224
+ }
225
+
226
+ /**
227
+ * BROAD-PHASE SUPERSET (the MIRROR rule — the sugar-half
228
+ * broad-phase-rect flag closed here). The engine's spatial index is an AABB broad
229
+ * phase over the LO's registered rect (`octree.rs`); a tilted TALL slab's raw
230
+ * un-rotated rect is NARROWER in x than the rotated slab, so a beam aimed at the
231
+ * face TIP (outside the raw rect, inside the true rotated slab) is never a broad-
232
+ * phase candidate and never collides. FIX (superset discipline): the LO's broad-
233
+ * phase rect for a tilted wall = the ROTATED slab's AXIS-ALIGNED bounding box; the
234
+ * OBB `region` (narrow phase) is UNCHANGED and culls the AABB corners back out. This
235
+ * is the ONE place the rotated-AABB half-extents are derived (Wall never hand-derives
236
+ * them) — the SAME `theta` the region/reflect law already share, so no second
237
+ * convention. Returns half-extents (the LO rect stays center-symmetric about the
238
+ * slab center, which rotation preserves). z is NOT rotated by the xy-plane OBB, so
239
+ * the z half-extent passes through as `hz`.
240
+ * aabbHx = hw·|cos θ| + hh·|sin θ| (x reach of the rotated w×h slab)
241
+ * aabbHy = hw·|sin θ| + hh·|cos θ| (y reach)
242
+ * aabbHz = hz (z-slab depth is orientation-independent)
243
+ * @param {Object|null} obb — the `.obb` from resolveSlabFace (or null → null, the
244
+ * cardinal/AABB path: broad-phase rect stays the raw rect, byte-identical SU1).
245
+ * @returns {{ hx: number, hy: number, hz: number }|null}
246
+ */
247
+ function aabbHalfExtentsFromObb(obb) {
248
+ if (!obb) { return null; }
249
+ var c = Math.abs(Math.cos(obb.theta));
250
+ var s = Math.abs(Math.sin(obb.theta));
251
+ return {
252
+ hx: obb.hw * c + obb.hh * s,
253
+ hy: obb.hw * s + obb.hh * c,
254
+ hz: obb.hz
255
+ };
256
+ }
257
+
258
+ export { resolveSlabFace, regionFromObb, aabbHalfExtentsFromObb };