@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,2499 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * wasm-engine.js — Env-agnostic WASM adapter provider.
5
+ *
6
+ * Design:
7
+ * - Never loads the WASM module itself — the env-specific loader
8
+ * (wasm-load-browser.js / wasm-load-node.js) resolves the WasmUniverse
9
+ * constructor and the boot door (compat/engine.js `bootEngine`) passes it in
10
+ * via makeProvider(WasmUniverse).
11
+ * - Imports its contracts (ObjectSchema, LightColor, Rectangle) statically; no
12
+ * environment branch.
13
+ * - Constructing a universe before the door booted the engine THROWS, naming the door.
14
+ */
15
+
16
+ import { ObjectSchema } from '../../objects/schema/index.js';
17
+ import { LightColor } from '../../parts/LightColor.js';
18
+ import { Rectangle } from '../../parts/Rectangle.js';
19
+
20
+ // ── Charge enum (matches JS engine) ─────────────────────────────────
21
+
22
+ var Charge = Object.freeze({
23
+ POSITIVE: 0,
24
+ NEGATIVE: 1,
25
+ });
26
+
27
+ // ── Data classes (match JS engine shapes) ───────────────────────────
28
+
29
+ function FrameData(tick, id, layer, rect, fullness, isAlive, childCount, mass, collidedId, vx, vy, z, vz, data) {
30
+ this.tick = tick;
31
+ this.id = id;
32
+ this.layer = layer;
33
+ this.rect = rect;
34
+ this.fullness = fullness;
35
+ this.isAlive = isAlive;
36
+ this.childCount = childCount;
37
+ this.mass = mass;
38
+ this.collidedId = collidedId;
39
+ this.vx = vx;
40
+ this.vy = vy;
41
+ // 3D depth: z position + vz velocity, carried alongside the 2D
42
+ // rect (which stays x,y,w,h). For A={x,y} (2D) experiments z stays 0 — the
43
+ // three.js provider populates _posArr z from this; p5 renders the z=0 slice.
44
+ this.z = z != null ? z : 0;
45
+ this.vz = vz != null ? vz : 0;
46
+ // Phase-D COLLECT data bubble.
47
+ // Canonical shape: { detection: { detector_name }|null, string_map: { ... }|null }
48
+ // detection — engine MARK/DETECT writes; string_map — opaque user-space data (render.color,
49
+ // render.outline, …) carried verbatim (I1). The typed render section is RETIRED.
50
+ // Absent → null. Never undefined — mirrors JS reference null normalization.
51
+ // Coercion: fd.data absent (key not in WASM output) → null, not undefined.
52
+ this.data = (data !== undefined && data !== null) ? data : null;
53
+ }
54
+
55
+ function DimensionData(tick, layer, frameCount, boundaryRect) {
56
+ this.tick = tick;
57
+ this.layer = layer;
58
+ this.frameCount = frameCount;
59
+ this.boundaryRect = boundaryRect;
60
+ }
61
+
62
+ function UniverseData(tick, dimensionCount, boundaryRect) {
63
+ this.tick = tick;
64
+ this.dimensionCount = dimensionCount;
65
+ this.boundaryRect = boundaryRect;
66
+ }
67
+
68
+ function TickData(universeData) {
69
+ this.universeData = universeData;
70
+ this.dimensionData = [];
71
+ this.frameData = [];
72
+ }
73
+ TickData.prototype.addDimensionData = function (dd) { this.dimensionData.push(dd); };
74
+ TickData.prototype.addFrameData = function (fd) { this.frameData.push(fd); };
75
+
76
+ // ── Wire-format translation (canonical→WASM ABI) ─────────────────────
77
+ //
78
+ // These functions translate the CANONICAL spec vocabulary (SCREAMING_SNAKE
79
+ // collision types/actions, numeric/string charge, literal 'closest' reposition)
80
+ // into the WASM Rust ABI wire format (PascalCase, Pascal-string charge, null).
81
+ //
82
+ // Relocated from lib/frqtl/objects/constants/CollisionTypes.js and
83
+ // QuantumEmitter.js per §13.3 (no engine-sniffing above the shim).
84
+
85
+ /**
86
+ * FRQTL-MAINT (2026-07-01) — normalize an OBJECT-shaped LO rect ({left,top,w,h}
87
+ * or {left,top,z,w,h,d}) to the wire array form the WASM ABI accepts: legacy
88
+ * 4-element [left,top,w,h] when z/d are absent (back-compat, z=0/d=w applied
89
+ * engine-side), or explicit 6-element [left,top,z,w,h,d] — MIRRORS
90
+ * Rectangle::from_top_left(l,t,z,w,h,d)'s own argument order (egpt_wasm/src/lib.rs
91
+ * rect_to_z_capable_rectangle) — when either is present. Array-shaped rects
92
+ * (already [l,t,w,h] or [l,t,z,w,h,d]) pass through unchanged; the WASM ABI itself
93
+ * validates length (THROW-OR-APPLY).
94
+ * @param {Object} rect - {left, top, w, h, z?, d?}
95
+ * @returns {Array<number>}
96
+ */
97
+ function _rectSpecToArray(rect) {
98
+ if (rect.z !== undefined || rect.d !== undefined) {
99
+ return [
100
+ rect.left, rect.top,
101
+ rect.z !== undefined ? rect.z : 0,
102
+ rect.w, rect.h,
103
+ rect.d !== undefined ? rect.d : rect.w,
104
+ ];
105
+ }
106
+ return [rect.left, rect.top, rect.w, rect.h];
107
+ }
108
+
109
+ /**
110
+ * Map canonical charge to WASM Pascal-string format.
111
+ * Canonical input: 0/1 (numeric), 'POSITIVE'/'NEGATIVE' (SNAKE), or null (random).
112
+ * WASM output: 'Positive', 'Negative', or null.
113
+ */
114
+ function _wasmCharge(charge) {
115
+ if (charge === null || charge === undefined) return null;
116
+ if (typeof charge === 'string') {
117
+ var upper = charge.toUpperCase();
118
+ if (upper === 'POSITIVE' || charge === '+' || charge === 'Positive') return 'Positive';
119
+ if (upper === 'NEGATIVE' || charge === '-' || charge === 'Negative') return 'Negative';
120
+ return charge; // pass unknown strings through
121
+ }
122
+ if (charge === 0) return 'Positive';
123
+ if (charge === 1) return 'Negative';
124
+ return null;
125
+ }
126
+
127
+ // ── EGPTUniverse wrapper ─────────────────────────────────────────────
128
+ //
129
+ // The WasmUniverse constructor is injected at provider-creation time via
130
+ // makeProvider(). This keeps the adapter env-agnostic.
131
+
132
+ /**
133
+ * UNIVERSE_CONFIG — the constructor's contract, exported so no consumer
134
+ * re-enumerates it by hand. Each row: the key, its default, and what the
135
+ * default MEANS. `EGPTUniverse` reads this table for both defaulting and
136
+ * unknown-key rejection, so the table IS the surface (one authority).
137
+ *
138
+ * A row is EITHER `required: true` (no default — `create` throws without it; today only
139
+ * `rect`) OR carries `def`, the published default.
140
+ *
141
+ * @type {Array<{key:string, required?:boolean, def?:*, unit:string, effect:string}>}
142
+ */
143
+ var UNIVERSE_CONFIG = [
144
+ { key: 'rect', required: true, unit: 'Rectangle (px)',
145
+ effect: 'REQUIRED — the ONE world-rect declaration; there is no world without it. init(fundamentalDimension, options) reads it and takes no rect. A leaf that leaves this rect is gone (the boundary is the only engine-side sink).' },
146
+ { key: 'seed', def: 42, unit: 'integer',
147
+ effect: 'THE deterministic RNG stream. Same seed + same ops ⇒ bit-identical run.' },
148
+ { key: 'fps', def: 60, unit: 'frames/second',
149
+ effect: 'the nominal tick rate the engine config records. Does not change physics per tick (1 tick = 1 time quantum, always).' },
150
+ { key: 'iframeIntervalSeconds', def: 1, unit: 'seconds',
151
+ effect: 'how often Phase B (STRUCTURE — build higher dimensions from the fundamental quadtree) runs. 1/60 = every tick (emergent composites appear immediately); 1 = once a second.' },
152
+ { key: 'withInterQuantumCollisions', def: false, unit: 'boolean',
153
+ effect: 'false = leaves pass through each other (the fast path). true = leaf-leaf collision resolution (superlinear in alive-count — this is what makes a medium resistive).' },
154
+ { key: 'withBonding', def: false, unit: 'boolean',
155
+ effect: 'false = no bonding pass. true = collided leaves may bond into composites.' },
156
+ { key: 'frameGenMode', def: 'groundUp', unit: "'groundUp' | 'topDown'",
157
+ effect: "how PARENT frames are generated. 'groundUp' = bottom-up, by actual collisions between the smallest frames (collision-based promotion, wave-walk). 'topDown' = by how many frames an area can parent (density clustering, brownian ON, collisions off). init() takes its own frameGenMode and does NOT inherit this one." },
158
+ { key: 'minMBFillRate', def: 0.1, unit: 'fraction of capacity (0..1)',
159
+ effect: 'the minimum fullness a merged/promoted composite must reach to survive.' },
160
+ { key: 'wavelengthScale', def: 1, unit: 'multiplier',
161
+ effect: 'the GLOBAL multiplier on every frame per-frame (capacity/mass) wavelength: lambda_eff = (capacity/mass) * wavelengthScale. Raising it widens fringes. init() re-reads this.' },
162
+ { key: 'liveQuantaCap', def: 300000, unit: 'leaf quanta | false',
163
+ effect: 'the WASM preview cap: once this many leaf quanta are alive, every further leaf add (makeLeafFrame) is skipped (returns null) until the count falls below it; the debug console is warned once per universe. The default is the preview limit set for this release: a value above it throws. false = uncapped.' },
164
+ ];
165
+
166
+ /**
167
+ * INIT_OPTIONS — `init(fundamentalDimension, options)`'s contract, the peer of UNIVERSE_CONFIG.
168
+ *
169
+ * `create(config)` has rejected unknown keys against UNIVERSE_CONFIG since it landed;
170
+ * `init()` only rejected RETIRED and LEGACY names, so any OTHER unrecognized key was
171
+ * read nowhere and thrown nowhere — accepted into `options` and silently discarded.
172
+ * A typo (`wavelenghtScale`, `frameGenModeX`) ran the scene with DIFFERENT physics
173
+ * than the author wrote, with zero signal. 20+ V1 setup factories call `init()`
174
+ * directly, below the recipe-layer `assertInitCfg` gate, so this was the widest
175
+ * unguarded physics-config surface in the shim.
176
+ *
177
+ * The two surfaces take DIFFERENT vocabularies (they overlap only on frameGenMode and
178
+ * wavelengthScale), so this is a second TABLE, not a second copy of the first — and,
179
+ * like UNIVERSE_CONFIG, the table IS the surface: it is what init() validates against
180
+ * AND the documentation of what init() accepts, so the doc cannot drift from the check.
181
+ * Both surfaces share ONE retirement map (`_RETIRED_CONFIG_KEYS`) via
182
+ * `_assertNoRetiredKeys`, so a retired name keeps its specific migration message at
183
+ * both, and only a genuinely unrecognized name gets the generic unknown-key error.
184
+ *
185
+ * ALL of these are [GLOBAL] — universe-wide, set once, scaling or gating the whole run.
186
+ * [PER-FRAME] state (mass/capacity→wavelength, speed_prob, charge, vx/vy/vz) is NEVER
187
+ * set here.
188
+ *
189
+ * @type {Array<{key:string, def:*, unit:string, effect:string}>}
190
+ */
191
+ var INIT_OPTIONS = [
192
+ { key: 'wavelengthScale', def: 1, unit: 'multiplier',
193
+ effect: "universe-wide multiplier on every frame's (capacity/mass) wavelength ratio: lambda_eff = (capacity/mass) * wavelengthScale. Raising it widens fringes. (The retired spelling 'wavelengthConstant' THROWS — see _RETIRED_CONFIG_KEYS.)" },
194
+ { key: 'frameGenMode', def: 'groundUp', unit: "'groundUp' | 'topDown'",
195
+ effect: "how PARENT frames are generated. 'groundUp' = bottom-up, by actual collisions between the smallest frames (promotion, wave-walk). 'topDown' = by how many frames an area can parent (density clustering, brownian ON, no collisions). init() does NOT inherit create()'s frameGenMode — the create()-time key is a pre-init record only. (The retired bool 'emergentPhysics' THROWS with the VALUE mapping.)" },
196
+ { key: 'brownianMotion', def: undefined, unit: 'boolean',
197
+ effect: "true = brownian random walk · false = wave-walk (particle walk). Rides the CONFIG BAG (brownian_motion) as the caller's REQUEST; the ENGINE resolves it at construction and JS reads the answer back, so nothing is inherited from a previous init. Omitted ⇒ null ⇒ the Rust unwrap_or is the ONE default authority. frameGenMode:'topDown' REQUIRES it: an explicit brownianMotion:false alongside topDown THROWS (the engine coerces it back to true, so accepting the false would be a lie). (The INVERTED alias 'noObserverFrame' is DELETED and THROWS.)" },
198
+ { key: 'noEscape', def: false, unit: 'boolean',
199
+ effect: 'true = children hard-trapped inside the parent rect.' },
200
+ { key: 'activeAxes', def: undefined, unit: '{x,y,z} boolean bag',
201
+ effect: 'the 1D/2D/3D selector — which axes the base-walk + yo-yo coins fire on. Omitted ⇒ the WASM serde default {x:true,y:true,z:false}, which keeps every existing 2D experiment byte-identical. capacity()/wavelength scale as the product over active axes.' },
202
+ { key: 'depth', def: undefined, unit: 'pixels',
203
+ effect: 'explicit world z-extent, DECOUPLED from width. Omitted ⇒ width (a CUBE), so a square 2D scene is byte-identical to the historic coupling. The z-range is CENTERED [-depth/2, depth/2].' },
204
+ { key: 'stepping', def: 'fixed', unit: "'fixed' | 'average'",
205
+ effect: "the wave-walk STEPPING SAMPLER. Both realize the same mean step (1 px/tick along the momentum vector) and differ in the VARIANCE about it. 'fixed' walks a deterministic error accumulator, so along-ray deviation stays bounded below 1 px forever; 'average' draws a per-axis Bernoulli coin, so it random-walks along the ray (Var = (L1/L2 - 1) px^2/tick — zero on-axis, 0.414 at 45 degrees) and the 2nd/3rd-order interference rings wash out OFF-axis while staying crisp on-axis. Axis-aligned and stationary frames are BIT-IDENTICAL under both. SET ONCE at init — there is no runtime setter and no live property (a mid-run flip would splice two samplers into one trajectory)." },
206
+ ];
207
+
208
+ /**
209
+ * frameGenMode — the ONE table mapping the JS vocabulary to the engine wire name.
210
+ *
211
+ * `frameGenMode` is an INDEPENDENT engine setting: it decides how PARENT frames come
212
+ * into being — 'topDown' (by how many frames an area can parent — periodic density
213
+ * clustering) or 'groundUp' (by actual collisions between the smallest frames). It is
214
+ * not part of the walk law; the walk-law consequences (brownian ON, collisions off
215
+ * under topDown) are COERCIONS the engine applies, and they live in `egpt_engine`.
216
+ *
217
+ * Both directions are served from this one object — the wire name goes OUT in the
218
+ * config bag and comes BACK through the `frame_gen_mode()` getter — so there is no
219
+ * second hand-written map to drift.
220
+ */
221
+ var FRAME_GEN_MODES = { groundUp: 'GroundUp', topDown: 'TopDown' };
222
+
223
+ function _frameGenModeNames() {
224
+ return Object.keys(FRAME_GEN_MODES).map(function (k) { return "'" + k + "'"; }).join(' | ');
225
+ }
226
+
227
+ /** THROW-OR-APPLY: an unknown mode names EVERY legal value. @returns {string} the JS name */
228
+ function _assertFrameGenMode(where, value) {
229
+ if (!Object.prototype.hasOwnProperty.call(FRAME_GEN_MODES, value)) {
230
+ throw new Error(where + ': frameGenMode must be ' + _frameGenModeNames() +
231
+ ' (got ' + JSON.stringify(value) + "). 'groundUp' = parents form from actual " +
232
+ "collisions between the smallest frames; 'topDown' = parents form from how many " +
233
+ 'frames an area can parent (density clustering).');
234
+ }
235
+ return value;
236
+ }
237
+
238
+ /** wire name ('GroundUp'|'TopDown') → JS name, derived from the one table. */
239
+ function _frameGenModeFromWire(wire) {
240
+ var names = Object.keys(FRAME_GEN_MODES);
241
+ for (var i = 0; i < names.length; i++) {
242
+ if (FRAME_GEN_MODES[names[i]] === wire) { return names[i]; }
243
+ }
244
+ throw new Error("EGPTUniverse: the engine reported an unknown frame_gen_mode '" + wire +
245
+ "'. Legal wire names: " + names.map(function (n) { return FRAME_GEN_MODES[n]; }).join(', ') + '.');
246
+ }
247
+
248
+ // Retired names → corrective guidance (FAIL LOUD, never a silent alias).
249
+ //
250
+ // ONE map, consulted by BOTH construction surfaces — `create(config)` and
251
+ // `init(fundamentalDimension, options)` — via `_assertNoRetiredKeys()`. A name accepted at one
252
+ // surface and thrown at the other is one name in two migration states; that split is
253
+ // exactly what let `wavelengthConstant` keep flowing into the engine long after
254
+ // `create()` had retired it.
255
+ var _RETIRED_CONFIG_KEYS = {
256
+ iframe_interval_seconds: "renamed to 'iframeIntervalSeconds'",
257
+ universe_rect: "renamed to 'rect'",
258
+ wavelength_constant: "renamed to 'wavelengthScale'",
259
+ wavelengthConstant: "renamed to 'wavelengthScale'",
260
+ // RETIRED because it was INERT, which is the worst kind of accepted key: create()
261
+ // recorded it and nothing ever read it, so `create({lowestDimension: 2})` ran a
262
+ // dimension-0 world and reported success. The base scale layer has one authority and
263
+ // always did — init()'s FIRST ARGUMENT. Both spellings get the same sentence.
264
+ lowest_dimension: "is RETIRED, and it is NOT a rename. The base (fundamental) scale " +
265
+ "layer is the FIRST ARGUMENT of init(fundamentalDimension, options) — pass it there: " +
266
+ "universe.init(2). create() does not carry it.",
267
+ lowestDimension: "is RETIRED, and it was INERT: create() recorded it and init() never " +
268
+ "read it, so create({lowestDimension: 2}) silently ran a dimension-0 world. The " +
269
+ "base (fundamental) scale layer is the FIRST ARGUMENT of " +
270
+ "init(fundamentalDimension, options) — pass it there: universe.init(2).",
271
+ withFrames: "deleted — it was inert (the WASM engine always has frames). Remove it.",
272
+ // DELETED, not renamed. Read the inversion carefully before migrating a saved
273
+ // recipe by hand — getting its SENSE backwards silently swaps the walk law.
274
+ noObserverFrame: "is DELETED — it is NOT a rename. It was the INVERTED alias of " +
275
+ "'brownianMotion': noObserverFrame:TRUE meant brownianMotion:FALSE (the " +
276
+ "wave-walk), and noObserverFrame:false meant nothing at all — it landed on the " +
277
+ "engine default, which is already brownianMotion:false. So OMIT the key: " +
278
+ "dropping it is exactly what every caller of it was asking for. Only if you " +
279
+ "want the random walk instead, pass brownianMotion:true",
280
+ // A BOOLEAN retired in favour of an ENUM. The message must carry the VALUE mapping,
281
+ // not just the new spelling: a hand-migration that keeps the boolean's `true`/`false`
282
+ // (or guesses which string means which) silently swaps how parent frames are
283
+ // generated, and nothing downstream complains.
284
+ emergentPhysics: "is RETIRED — it is now 'frameGenMode', an ENUM, not a boolean. " +
285
+ "Migrate the VALUE, not just the name: emergentPhysics:true becomes " +
286
+ "frameGenMode:'topDown' (parent frames are generated by how many frames an area " +
287
+ "can parent — density clustering), and emergentPhysics:false becomes " +
288
+ "frameGenMode:'groundUp' (parent frames are generated by actual collisions " +
289
+ "between the smallest frames — the default)",
290
+ };
291
+
292
+ /**
293
+ * FAIL LOUD on any retired key, naming the canonical replacement.
294
+ * @param {string} where — the call shape to quote back ('EGPTUniverse.create(config)' | 'EGPTUniverse.init()')
295
+ * @param {Object} bag — the caller's config/options object
296
+ */
297
+ function _assertNoRetiredKeys(where, bag) {
298
+ Object.keys(bag).forEach(function (k) {
299
+ if (Object.prototype.hasOwnProperty.call(_RETIRED_CONFIG_KEYS, k)) {
300
+ throw new Error(where + ": '" + k + "' " + _RETIRED_CONFIG_KEYS[k] + '.');
301
+ }
302
+ });
303
+ }
304
+
305
+ function _configKeyList() {
306
+ return UNIVERSE_CONFIG.map(function (r) { return r.key; }).join(', ');
307
+ }
308
+
309
+ function _initOptionKeyList() {
310
+ return INIT_OPTIONS.map(function (r) { return r.key; }).join(', ');
311
+ }
312
+
313
+ /**
314
+ * FAIL LOUD on any key the surface does not accept, naming every key it does.
315
+ *
316
+ * The SECOND half of the pair `_assertNoRetiredKeys` starts: a retired name keeps its
317
+ * specific migration message (that map is consulted FIRST, at both surfaces), and only
318
+ * a genuinely unrecognized name reaches this generic error. Run this at EVERY
319
+ * config-bag surface — a bag validated at one surface and swallowed at the other is
320
+ * how a typo reaches the engine as different physics.
321
+ *
322
+ * @param {string} where — the call shape to quote back
323
+ * @param {Object} bag — the caller's config/options object
324
+ * @param {Array<{key:string}>} table — the surface's contract table
325
+ * @param {string} keyList — the human-readable list of legal keys
326
+ */
327
+ function _assertNoUnknownKeys(where, bag, table, keyList) {
328
+ Object.keys(bag).forEach(function (k) {
329
+ var known = table.some(function (r) { return r.key === k; });
330
+ if (!known) {
331
+ throw new Error(where + ": unknown key '" + k + "'. Valid keys: " + keyList + '.');
332
+ }
333
+ });
334
+ }
335
+
336
+ // The ONE construction authority's private brand. `EGPTUniverse.create` is the
337
+ // only holder; a `new EGPTUniverse(...)` anywhere else cannot produce it, so the
338
+ // bare constructor FAILS LOUD instead of quietly becoming construction site #2.
339
+ var _BUILD_BRAND = {};
340
+
341
+ /**
342
+ * EGPTUniverse — the WORLD. **Do not `new` it** — build it with the ONE
343
+ * construction authority, `EGPTUniverse.create(config)` (below), then `init()`
344
+ * it (ALWAYS FIRST: every simulation method throws before init).
345
+ *
346
+ * The bare constructor is BRANDED: called any way other than through
347
+ * `EGPTUniverse.create` it throws, naming the authority. There is no positional
348
+ * form, no compat alias, and no second factory.
349
+ *
350
+ * @param {Object} config — the normalized bag `create()` produced.
351
+ * @param {Object} brand — `_BUILD_BRAND`; anything else is a hard error.
352
+ */
353
+ function EGPTUniverse(config, brand) {
354
+ if (brand !== _BUILD_BRAND) {
355
+ throw new Error(
356
+ 'new EGPTUniverse(...) is not the construction path. Build a universe with ' +
357
+ 'the ONE construction authority: EGPTUniverse.create(config) — e.g. ' +
358
+ 'EGPTUniverse.create({ rect: { left: 0, top: 0, w: 600, h: 400 }, seed: 42 }). ' +
359
+ 'Reach it as engine().EGPTUniverse.create (lib/frqtl/compat/engine.js). Keys + defaults: ' +
360
+ 'EGPTUniverse.CONFIG (' + _configKeyList() + ').'
361
+ );
362
+ }
363
+ var cfg = config;
364
+
365
+ // Internal snake_case mirror consumed by init()'s WASM config assembly.
366
+ this._params = {
367
+ fps: cfg.fps,
368
+ iframe_interval_seconds: cfg.iframeIntervalSeconds,
369
+ withInterQuantumCollisions: !!cfg.withInterQuantumCollisions,
370
+ withBonding: !!cfg.withBonding,
371
+ frameGenMode: cfg.frameGenMode,
372
+ minMBFillRate: cfg.minMBFillRate,
373
+ wavelength_scale: cfg.wavelengthScale,
374
+ seed: cfg.seed,
375
+ // Wrapper state, never sent to the engine: the live-quanta admission cap (_admitLeaf).
376
+ liveQuantaCap: cfg.liveQuantaCap,
377
+ };
378
+ // The cap's per-tick admission ledger (_admitLeaf): the alive count snapshotted once per
379
+ // engine tick, the leaves admitted since, and the once-per-universe warn latch.
380
+ this._capTick = null;
381
+ this._capAlive = 0;
382
+ this._capAdmitted = 0;
383
+ this._capWarned = false;
384
+ this._wasm = null;
385
+ this.tick = 0;
386
+ // The world rect — written HERE, once, from create()'s REQUIRED `rect`; read through
387
+ // the read-only `universe_rect` accessor (init() reads it and takes no rect).
388
+ this._universeRect = cfg.rect;
389
+ this._withInterQuantumCollisions = !!cfg.withInterQuantumCollisions;
390
+ // PRE-INIT ONLY: what `universe.brownianMotion` / `universe.wavelengthScale` /
391
+ // `universe.frameGenMode` answer in the window before init() builds an engine to ask
392
+ // (FrqtlApp constructs a universe and hands it to the sketch un-init'd, so this read
393
+ // is real). From init() onward all three getters read the engine and these fields are
394
+ // never consulted again.
395
+ this._brownianMotion = false;
396
+ this._wavelengthScale = cfg.wavelengthScale;
397
+ this._frameGenMode = cfg.frameGenMode;
398
+ this.experimentTickFunctions = [];
399
+ this._loHandles = [];
400
+ /**
401
+ * THE WRAPPER CENSUS — the world's own list of the JS `LargeObject` wrappers
402
+ * constructed on it, maintained beside the engine handle table above.
403
+ *
404
+ * CONSTRUCTED = IN THE WORLD = ON SCREEN. `new LargeObject(u, …)` is the ONE
405
+ * wrapper construction seam and it appends itself here, so a renderer DERIVES its
406
+ * draw list from the world instead of consuming a second, render-only registration
407
+ * list. That second list is a SHADOW CENSUS: its divergence from the engine's
408
+ * registry is unrepresentable in the mental model the primitive table teaches
409
+ * ("LARGE OBJECTS … self-registers"), and when it diverged the object collided and
410
+ * counted correctly while being completely invisible, with no warning — a silently
411
+ * wrong world, which for a stack that learns by LOOKING is poisoned data.
412
+ *
413
+ * The engine census holds physics only (color/alpha/visible/backdrop/image/
414
+ * drawAsCircle/shellRender are render intent, and "engine emits primitives" forbids
415
+ * them ever becoming engine fields), which is exactly why the derivation runs off
416
+ * the WRAPPERS and not off the handle table.
417
+ *
418
+ * Lifetime is the WORLD's: `init()` empties it, because re-initing the world drops
419
+ * every engine registration with it and a surviving wrapper would hold a stale
420
+ * handle. Removal rides `unregisterLargeObject`, so one teardown call leaves BOTH
421
+ * censuses.
422
+ * @type {Array}
423
+ */
424
+ this.largeObjects = [];
425
+ }
426
+
427
+ // The contract, exported so tests/consumers read it instead of re-listing it.
428
+ EGPTUniverse.CONFIG = UNIVERSE_CONFIG;
429
+ // The init() surface's contract, exported for the same reason. Both surfaces now
430
+ // reject unknown keys, so both tables are load-bearing and neither may be re-listed.
431
+ EGPTUniverse.INIT_OPTIONS = INIT_OPTIONS;
432
+
433
+ /**
434
+ * RETIRED_CONFIG_KEYS — the retirement ledger, PUBLISHED for the same reason
435
+ * `CONFIG` is: so no consumer re-enumerates it by hand.
436
+ *
437
+ * The recipe facades (`exp.createCanvas(w, h, cfg)` / `exp.createScene(w, h, d, cfg)`) reject unknown cfg keys, and a saved STUDIO
438
+ * recipe's unknown key is almost always a name retired HERE. Consulting this export
439
+ * lets their error carry the SAME migration sentence this file throws, so the
440
+ * guidance can never drift into a second table.
441
+ *
442
+ * Read it off the universe's own class (`universe.constructor.RETIRED_CONFIG_KEYS`)
443
+ * — the recipe layer must stay engine-agnostic and never import this module.
444
+ *
445
+ * @type {Object<string,string>} key → the sentence that follows "'<key>' "
446
+ */
447
+ EGPTUniverse.RETIRED_CONFIG_KEYS = _RETIRED_CONFIG_KEYS;
448
+
449
+
450
+ // A Rectangle is recognised STRUCTURALLY, not by `instanceof`: the class is loaded
451
+ // once as a browser global and once as a Node module, so identity is not reliable.
452
+ function _isRectangle(v) {
453
+ return !!v && typeof v === 'object' && v.is_centered !== undefined && v._left !== undefined;
454
+ }
455
+
456
+ /**
457
+ * createUniverse — **THE ONE CONSTRUCTION AUTHORITY.** The only place in the
458
+ * tree that builds a universe; every other site (host, setup factory, bench,
459
+ * conformance scenario, test) calls this. Published as `EGPTUniverse.create`,
460
+ * so it is reachable wherever the class is — `engine().EGPTUniverse.create` after the
461
+ * boot door (`compat/engine.js`) — with NO host in the way
462
+ * (the minimal sim's primitive #1).
463
+ *
464
+ * ── Layer 1: API contract ────────────────────────────────────────────────
465
+ *
466
+ * ONE argument: a CONFIG BAG. `rect` is REQUIRED; every other key is optional and every
467
+ * default lives in `UNIVERSE_CONFIG` (exported as `EGPTUniverse.CONFIG`), the single table
468
+ * that drives defaulting, unknown-key rejection AND this docstring — so a caller
469
+ * states the world and ONLY what else it means to change:
470
+ *
471
+ * EGPTUniverse.create({ rect: { left: 0, top: 0, w: 600, h: 400 }, seed: 42 })
472
+ *
473
+ * | key | default | unit |
474
+ * |----------------------------|---------|-------------------------|
475
+ * | rect | REQUIRED| Rectangle (px) |
476
+ * | seed | 42 | integer |
477
+ * | fps | 60 | frames/second |
478
+ * | iframeIntervalSeconds | 1 | seconds |
479
+ * | withInterQuantumCollisions | false | boolean |
480
+ * | withBonding | false | boolean |
481
+ * | frameGenMode | groundUp| 'groundUp' \| 'topDown' |
482
+ * | minMBFillRate | 0.1 | fraction of capacity |
483
+ * | wavelengthScale | 1 | multiplier |
484
+ * | liveQuantaCap | 300000 | leaf quanta \| false |
485
+ *
486
+ * `rect` accepts EITHER a `Rectangle` OR a plain bounds literal
487
+ * (`{left|x, top|y, w, h}`), which is normalised here to a non-centered
488
+ * `Rectangle` — so a headless caller never hand-rolls one. Its `w` and `h` must be
489
+ * positive finite numbers, gated here: the engine itself builds and ticks a 0x0 world.
490
+ *
491
+ * THROW-OR-APPLY: a non-object argument, a retired positional-era key, or any
492
+ * key outside the table is a hard error naming the canonical replacement. There
493
+ * is no positional form, no second factory and no compat alias.
494
+ *
495
+ * ── Layer 2: physics behavior ────────────────────────────────────────────
496
+ *
497
+ * Construction starts NO physics — it only records the world's settings.
498
+ * `seed` fixes the one deterministic stream; `wavelengthScale` scales every
499
+ * frame's transverse oscillation period (Invariant 6B); `iframeIntervalSeconds`
500
+ * sets how often composites can form; `withInterQuantumCollisions` decides
501
+ * whether the medium resists. Physics begins at `doTick()` — and every
502
+ * simulation method throws until `universe.init(fundamentalDimension, options)`.
503
+ *
504
+ * @param {Object} [config] — see the table above.
505
+ * @returns {EGPTUniverse}
506
+ */
507
+ function createUniverse(config) {
508
+ if (!_WasmUniverse) {
509
+ throw new Error('EGPTUniverse.create(config): the engine is not booted — boot it through a door ' +
510
+ 'first: await bootEngine(loader) (lib/frqtl/compat/engine.js), which the boot module ' +
511
+ '(experiments/_shared/boot.js) and the node door call for you. (FAIL-LOUD.)');
512
+ }
513
+ if (config === undefined) { config = {}; }
514
+ if (config === null || typeof config !== 'object' || Array.isArray(config)) {
515
+ throw new Error(
516
+ 'EGPTUniverse.create(config): takes ONE config bag, e.g. ' +
517
+ 'EGPTUniverse.create({ rect: rect, seed: 42 }). The positional argument list ' +
518
+ '(fps, iframeInterval, …) and the two-argument (canvasRect, opts) factory are ' +
519
+ 'both DELETED — name the keys you mean: ' + _configKeyList() + '.'
520
+ );
521
+ }
522
+ _assertNoRetiredKeys('EGPTUniverse.create(config)', config);
523
+ _assertNoUnknownKeys('EGPTUniverse.create(config)', config,
524
+ UNIVERSE_CONFIG, _configKeyList());
525
+
526
+ var cfg = {};
527
+ UNIVERSE_CONFIG.forEach(function (row) {
528
+ cfg[row.key] = (config[row.key] !== undefined && config[row.key] !== null)
529
+ ? config[row.key] : row.def;
530
+ });
531
+
532
+ // frameGenMode is the one ENUM in the table — THROW-OR-APPLY, naming every legal
533
+ // value. (Every other row is a scalar the engine validates for itself.)
534
+ _assertFrameGenMode('EGPTUniverse.create(config)', cfg.frameGenMode);
535
+ _assertLiveQuantaCap(cfg.liveQuantaCap);
536
+
537
+ // rect: REQUIRED — the ONE world-rect declaration (init() takes none). A Rectangle passes
538
+ // through; a plain bounds literal is normalised. Either way the dimensions are gated
539
+ // HERE — this is the authority's own door, and the engine below it accepts a 0x0, a
540
+ // negative or a NaN world without complaint (measured).
541
+ if (cfg.rect === undefined || cfg.rect === null) {
542
+ throw new Error(
543
+ 'EGPTUniverse.create(config): `rect` is REQUIRED — there is no world without it ' +
544
+ '(got ' + String(cfg.rect) + '). ' + _COINIT_SENTENCE + ' Then ' +
545
+ 'universe.init(fundamentalDimension, options) reads it. (FAIL-LOUD.)'
546
+ );
547
+ }
548
+ if (!_isRectangle(cfg.rect)) {
549
+ var r = cfg.rect;
550
+ if (typeof r !== 'object' || Array.isArray(r)) {
551
+ throw new Error(
552
+ 'EGPTUniverse.create(config): `rect` must be a Rectangle or a bounds ' +
553
+ 'literal { left, top, w, h } — got ' + JSON.stringify(r) + '.'
554
+ );
555
+ }
556
+ var RectCtor = Rectangle;
557
+ if (!RectCtor) {
558
+ throw new Error(
559
+ 'EGPTUniverse.create(config): `rect` was given as a bounds literal but the ' +
560
+ 'Rectangle class is not loaded. Load parts/Rectangle.js (the ENGINE ' +
561
+ 'boot phase does this in the browser; the compat shim does it in Node), or pass ' +
562
+ 'a Rectangle instance.'
563
+ );
564
+ }
565
+ cfg.rect = new RectCtor(
566
+ r.left !== undefined ? r.left : (r.x || 0),
567
+ r.top !== undefined ? r.top : (r.y || 0),
568
+ r.w, r.h, null, false
569
+ );
570
+ }
571
+
572
+ _requireWorldDimensions(cfg.rect);
573
+
574
+ return new EGPTUniverse(cfg, _BUILD_BRAND);
575
+ }
576
+
577
+ // The world-dimension rule at the construction authority's door: `w` and `h` positive
578
+ // finite NUMBERS (a numeric string is not one). The same rule the world-rect normalizer
579
+ // (ObjectSchema.worldRect) and the renderer size rule (requireProviderSize) apply — one value
580
+ // table drives all three doors (compat/providers/world-dimension-doors.test.mjs).
581
+ // liveQuantaCap: `false` (uncapped) or a positive integer no larger than the row's published
582
+ // default — the default is this release's published preview limit, so a larger value
583
+ // throws. The ceiling is READ from the row; nothing else states the number.
584
+ function _liveQuantaCapRow() {
585
+ for (var i = 0; i < UNIVERSE_CONFIG.length; i++) {
586
+ if (UNIVERSE_CONFIG[i].key === 'liveQuantaCap') { return UNIVERSE_CONFIG[i]; }
587
+ }
588
+ throw new Error('[wasm-engine] UNIVERSE_CONFIG has no liveQuantaCap row.');
589
+ }
590
+ function _assertLiveQuantaCap(v) {
591
+ var ceiling = _liveQuantaCapRow().def;
592
+ if (v === false) { return; }
593
+ if (typeof v === 'number' && Number.isInteger(v) && v > 0 && v <= ceiling) { return; }
594
+ throw new Error(
595
+ 'EGPTUniverse.create(config): liveQuantaCap must be false (uncapped) or a positive ' +
596
+ 'integer no larger than ' + ceiling + ' (the release ceiling, EGPTUniverse.CONFIG row ' +
597
+ "'liveQuantaCap') — got " + (typeof v === 'string' ? JSON.stringify(v) : String(v)) + '.'
598
+ );
599
+ }
600
+
601
+ function _isPositiveFiniteDimension(v) {
602
+ return typeof v === 'number' && isFinite(v) && v > 0;
603
+ }
604
+ // The co-init sentence both create() door throws quote — one string, so the two cannot drift.
605
+ var _COINIT_SENTENCE = 'The world rect is written ONCE by the co-init call — ' +
606
+ 'EGPTUniverse.create({ rect }) ← new FrqtlApp({ canvasW, canvasH }) / ' +
607
+ 'exp.createCanvas(w, h, cfg). There is no default world size.';
608
+ function _requireWorldDimensions(r) {
609
+ if (_isPositiveFiniteDimension(r.w) && _isPositiveFiniteDimension(r.h)) { return; }
610
+ throw new Error(
611
+ 'EGPTUniverse.create(config): rect w and h must be positive finite numbers (got w=' +
612
+ _describeDimension(r.w) + ', h=' + _describeDimension(r.h) + '). ' + _COINIT_SENTENCE +
613
+ ' (FAIL-LOUD.)'
614
+ );
615
+ }
616
+ function _describeDimension(v) {
617
+ return (typeof v === 'string') ? JSON.stringify(v) + ' (a string)' : String(v);
618
+ }
619
+
620
+ // THE construction authority, reachable wherever the class is (host-free).
621
+ EGPTUniverse.create = createUniverse;
622
+
623
+ // ── Getter / setter pairs ────────────────────────────────────────────
624
+
625
+ // [GLOBAL] brownianMotion — ENGINE TRUTH, ASKED ON EVERY READ. There is no JS copy of the
626
+ // resolved value to go stale: init() ferries the caller's un-coerced REQUEST in the CONFIG
627
+ // BAG (`brownian_motion`), the ENGINE resolves it (TopDown coercion + the `unwrap_or`
628
+ // default), and this getter asks the running universe what it actually HOLDS. So a flip
629
+ // made ANYWHERE — through this setter or straight at the WASM handle — is visible here,
630
+ // and the reported value can never disagree with the walk that is running. A cached
631
+ // read-back could only ever report the last value JS happened to observe.
632
+ //
633
+ // `_brownianMotion` survives for exactly ONE read: the window between create() and init(),
634
+ // when there is no engine to ask. A value parked there is NOT an input to init() — init()
635
+ // resolves the flag from the options bag — so state it in the init options instead.
636
+ //
637
+ // The setter is the LIVE mutation path (the blackbody / atomic-model factories turning
638
+ // brownian on AFTER init) and writes straight through to the engine.
639
+ Object.defineProperty(EGPTUniverse.prototype, 'brownianMotion', {
640
+ get: function () {
641
+ return this._wasm ? this._wasm.brownian_motion() : this._brownianMotion;
642
+ },
643
+ set: function (value) {
644
+ this._brownianMotion = value;
645
+ if (this._wasm) this._wasm.set_brownian_motion(value);
646
+ }
647
+ });
648
+
649
+ Object.defineProperty(EGPTUniverse.prototype, 'withInterQuantumCollisions', {
650
+ get: function () { return this._withInterQuantumCollisions; },
651
+ set: function (value) {
652
+ this._withInterQuantumCollisions = value;
653
+ if (this._wasm) this._wasm.set_with_inter_quantum_collisions(value);
654
+ }
655
+ });
656
+
657
+ Object.defineProperty(EGPTUniverse.prototype, 'activeQuanta', {
658
+ get: function () {
659
+ if (!this._wasm) return 0;
660
+ return this._wasm.active_quanta();
661
+ }
662
+ });
663
+
664
+ // liveQuantaCap — the ACTIVE preview cap (a number, or false when uncapped), READ-ONLY: its
665
+ // one writer is EGPTUniverse.create({ liveQuantaCap }). "Reached" is derivable:
666
+ // activeQuanta >= liveQuantaCap.
667
+ Object.defineProperty(EGPTUniverse.prototype, 'liveQuantaCap', {
668
+ get: function () { return this._params.liveQuantaCap; }
669
+ });
670
+
671
+ // universe_rect — the world rect, READ-ONLY. Its one writer is `EGPTUniverse.create({ rect })`;
672
+ // init() reads it and takes no rect, and no resize path exists. A write THROWS rather than
673
+ // quietly re-pointing the field at a rect the engine never hears.
674
+ Object.defineProperty(EGPTUniverse.prototype, 'universe_rect', {
675
+ get: function () { return this._universeRect; },
676
+ set: function () {
677
+ throw new Error("EGPTUniverse: 'universe_rect' is read-only — its one writer is " +
678
+ 'EGPTUniverse.create({ rect, seed, … }); universe.init(fundamentalDimension, options) ' +
679
+ 'reads it. (FAIL-LOUD: one declaration per fact.)');
680
+ }
681
+ });
682
+
683
+ // initDimension — the dimension this world was INIT'd at: the one layer `init(fundamentalDimension, options)`
684
+ // allocates, and the seed every part's own layer declaration is re-applied from when a scene
685
+ // is rebuilt. A DIFFERENT fact from `fundamentalDimension` below: that one is the LIVE lowest
686
+ // allocated layer and moves when a part adds a lower dimension; this one never moves until the
687
+ // world is re-init'd. A saved scene's world step re-emits THIS value. Throws before init.
688
+ Object.defineProperty(EGPTUniverse.prototype, 'initDimension', {
689
+ get: function () {
690
+ this._ensureInit();
691
+ return this._initDimension;
692
+ },
693
+ set: function () {
694
+ throw new Error("EGPTUniverse: 'initDimension' is read-only — its one writer is " +
695
+ 'universe.init(fundamentalDimension, options).');
696
+ }
697
+ });
698
+
699
+ // fundamentalDimension — [GLOBAL] the world's LIVE base scale layer, ENGINE TRUTH, ASKED
700
+ // ON EVERY READ. It is the layer a leaf is minted at when its caller declares none, so a
701
+ // part that needs "the world's layer" reads it HERE and never keeps its own copy. It moves
702
+ // only through `addDimension` (the engine self-lowers it to the lowest allocated layer),
703
+ // so a write is refused rather than silently ignored. Before `init()` there is no world
704
+ // and no layer to report.
705
+ Object.defineProperty(EGPTUniverse.prototype, 'fundamentalDimension', {
706
+ get: function () {
707
+ this._ensureInit();
708
+ return this._wasm.fundamental_dimension();
709
+ },
710
+ set: function () {
711
+ throw new Error("EGPTUniverse: 'fundamentalDimension' is read-only — the world's base " +
712
+ 'layer is set by init() and lowered only by universe.addDimension(layer).');
713
+ }
714
+ });
715
+
716
+ // [D5] wavelengthScale — [GLOBAL] universe-wide λ multiplier, ENGINE TRUTH, ASKED ON
717
+ // EVERY READ. Live-writable (wave-interference and tunnel-point-exit bind it to a
718
+ // slider): raising it WIDENS fringes. There is no JS copy of the running value to go
719
+ // stale: init() ferries the caller's value in the CONFIG BAG (`wavelength_scale`), the
720
+ // ENGINE holds it, and this getter asks the running universe what it actually HOLDS. So
721
+ // a write made ANYWHERE — through this setter or straight at the WASM handle — is
722
+ // visible here, and the reported multiplier can never disagree with the λ the leaves
723
+ // are walking. A cached read-back could only ever report the last value JS observed.
724
+ //
725
+ // `_wavelengthScale` survives for exactly ONE read: the window between create() and
726
+ // init(), when there is no engine to ask (FrqtlApp constructs a universe and hands it
727
+ // to the sketch un-init'd). A value parked there is NOT an input to init() — init()
728
+ // takes the scale from its options bag — so state it in the init options instead.
729
+ //
730
+ // The setter is the LIVE mutation path (the λ slider) and writes straight through.
731
+ Object.defineProperty(EGPTUniverse.prototype, 'wavelengthScale', {
732
+ get: function () {
733
+ return this._wasm ? this._wasm.wavelength_scale() : this._wavelengthScale;
734
+ },
735
+ set: function (value) {
736
+ this._wavelengthScale = value;
737
+ if (this._wasm) this._wasm.set_wavelength_scale(value);
738
+ }
739
+ });
740
+
741
+ // [GLOBAL] frameGenMode — how PARENT frames are generated: 'groundUp' (bottom-up, by
742
+ // actual collisions between the smallest frames) | 'topDown' (by how many frames an area
743
+ // can parent — density clustering). ENGINE TRUTH, ASKED ON EVERY READ: init() ferries the
744
+ // mode in the CONFIG BAG (`frame_gen_mode`), the ENGINE holds it, and this getter asks the
745
+ // running universe what it actually HOLDS. A JS copy would go stale the moment a re-init
746
+ // changed the mode — which is exactly how STUDIO's codegen came to print a mode the run
747
+ // was not using.
748
+ //
749
+ // `_frameGenMode` survives for exactly ONE read: the window between create() and init(),
750
+ // when there is no engine to ask. It is NOT an input to init() — init() takes its own
751
+ // `frameGenMode` option and defaults to 'groundUp' — so state the mode in the init options.
752
+ //
753
+ // SET ONCE, no runtime setter (same class as `stepping`): the engine has no
754
+ // set_frame_gen_mode ABI, and a mid-run flip would splice two frame-generation regimes
755
+ // into one run. A write THROWS rather than quietly creating an own property that the
756
+ // engine never hears — the silence is the worst outcome.
757
+ Object.defineProperty(EGPTUniverse.prototype, 'frameGenMode', {
758
+ get: function () {
759
+ return this._wasm ? _frameGenModeFromWire(this._wasm.frame_gen_mode()) : this._frameGenMode;
760
+ },
761
+ set: function (value) {
762
+ throw new Error(
763
+ "EGPTUniverse: 'frameGenMode' is SET ONCE, at construction — there is no " +
764
+ 'runtime setter (the engine holds the mode, and a mid-run flip would splice ' +
765
+ 'two frame-generation regimes into one run). Pass it where the world is ' +
766
+ "built: universe.init(dim, { frameGenMode: " + JSON.stringify(value) +
767
+ ' }), or EGPTUniverse.create({ frameGenMode: … }) before init.'
768
+ );
769
+ }
770
+ });
771
+
772
+ // RETIRED spelling. A throwing accessor, not a compat plug — it NEVER works. Without it,
773
+ // `universe.emergentPhysics = true` in old page/recipe/setup code would quietly create an
774
+ // own property, the engine would never hear it, and the run would silently use the other
775
+ // frame-generation regime. (the big-bang scene declares it as `iframeIntervalSeconds` in its world cfg.)
776
+ Object.defineProperty(EGPTUniverse.prototype, 'emergentPhysics', {
777
+ get: function () {
778
+ throw new Error("EGPTUniverse: 'emergentPhysics' " + _RETIRED_CONFIG_KEYS.emergentPhysics + '.');
779
+ },
780
+ set: function () {
781
+ throw new Error("EGPTUniverse: 'emergentPhysics' " + _RETIRED_CONFIG_KEYS.emergentPhysics + '.');
782
+ }
783
+ });
784
+
785
+ // RETIRED spelling. A throwing accessor, not a compat plug — it NEVER works. Without
786
+ // it, `universe.wavelengthConstant = v` in old page/recipe code would quietly create an
787
+ // own property, the WASM would never hear the write, and the run would look fine while
788
+ // λ stayed at its init value. That silence is the worst outcome; this is the loud one.
789
+ Object.defineProperty(EGPTUniverse.prototype, 'wavelengthConstant', {
790
+ get: function () {
791
+ throw new Error("EGPTUniverse: 'wavelengthConstant' " + _RETIRED_CONFIG_KEYS.wavelengthConstant + '.');
792
+ },
793
+ set: function () {
794
+ throw new Error("EGPTUniverse: 'wavelengthConstant' " + _RETIRED_CONFIG_KEYS.wavelengthConstant + '.');
795
+ }
796
+ });
797
+
798
+ // [D7] iframe_interval_seconds getter/setter — mirrors JS EGPTUniverse parity.
799
+ //
800
+ // The JS reference engine has a `set iframe_interval_seconds(value)` setter that
801
+ // updates `_iframe_interval_ticks` immediately. The WASM wrapper lacked an
802
+ // equivalent, so `universe.iframe_interval_seconds = 1/60` (called by
803
+ // the big-bang setup before universe.init()) was a silent plain-property assignment —
804
+ // `_params.iframe_interval_seconds` stayed at 1, and init() passed
805
+ // `iframe_interval_seconds: 1` to the WASM config. Phase B fired every 60 ticks
806
+ // instead of every tick, producing a "flat leaf cloud" for the first second with
807
+ // no emergent composite frames visible.
808
+ //
809
+ // Pre-init: the setter updates _params.iframe_interval_seconds, which init()
810
+ // reads to populate the WASM config (the only path to set the WASM interval).
811
+ // Post-init: the WASM ABI has no set_iframe_interval_seconds method; the value
812
+ // is frozen after init() in Rust. Post-init changes must call init() again.
813
+ Object.defineProperty(EGPTUniverse.prototype, 'iframe_interval_seconds', {
814
+ get: function () { return this._params.iframe_interval_seconds; },
815
+ set: function (value) {
816
+ this._params.iframe_interval_seconds = value;
817
+ // No WASM ABI for post-init update — value is consumed by the next init().
818
+ }
819
+ });
820
+
821
+ // ── Lifecycle ────────────────────────────────────────────────────────
822
+
823
+ EGPTUniverse.prototype._ensureInit = function () {
824
+ if (!this._wasm) {
825
+ throw new Error('EGPTUniverse.init() must be called before any simulation methods.');
826
+ }
827
+ };
828
+
829
+ // The rect argument is DELETED: the world rect has one writer, `create({ rect })`. A
830
+ // rect-shaped first argument (or a third argument) is the old call shape — it THROWS
831
+ // naming the new signature, never a "rect ignored" arm.
832
+ var _INIT_SIGNATURE_SENTENCE =
833
+ 'EGPTUniverse.init(fundamentalDimension, options) takes NO rect — the world rect is ' +
834
+ 'written ONCE, by EGPTUniverse.create({ rect, seed, … }), and init reads it ' +
835
+ '(universe.universe_rect). Call e.g. universe.init(0, { wavelengthScale: 1 }). (FAIL-LOUD.)';
836
+
837
+ EGPTUniverse.prototype.init = function (
838
+ fundamental_dimension_number,
839
+ options
840
+ ) {
841
+ if ((fundamental_dimension_number !== null && typeof fundamental_dimension_number === 'object') ||
842
+ arguments.length > 2) {
843
+ throw new Error('EGPTUniverse.init(): the rect argument is DELETED. ' + _INIT_SIGNATURE_SENTENCE);
844
+ }
845
+ if (fundamental_dimension_number === undefined || fundamental_dimension_number === null) {
846
+ throw new Error('EGPTUniverse.init(): the fundamental dimension is required. ' + _INIT_SIGNATURE_SENTENCE);
847
+ }
848
+ var universe_rect = this.universe_rect;
849
+ options = (options === undefined) ? {} : options;
850
+ // FAIL-LOUD: options must be a plain object and must not carry retired/legacy keys.
851
+ if (options === null || typeof options !== 'object' || Array.isArray(options)) {
852
+ throw new Error("EGPTUniverse.init(): the second argument must be a plain options object, e.g. init(dim, { wavelengthScale: 1, noEscape: false, frameGenMode: 'groundUp' }).");
853
+ }
854
+ // Retired names throw at init() with the SAME message create() gives — one name,
855
+ // one migration state (see _RETIRED_CONFIG_KEYS).
856
+ _assertNoRetiredKeys('EGPTUniverse.init()', options);
857
+ // Legacy keys (key name → corrective guidance). Key strings are assembled so the
858
+ // retired identifiers do not appear verbatim in source (keeps cleanup greps clean).
859
+ var _legacyKeyGuards = {};
860
+ _legacyKeyGuards['is' + 'Greedy'] = "the 'is" + "Greedy' option has been removed (it was inert). Delete it.";
861
+ _legacyKeyGuards['emergent' + 'Dimensions'] = "the 'emergent" + "Dimensions' option is DELETED. " +
862
+ "The frame-generation regime is now the enum frameGenMode: 'topDown' (what a truthy " +
863
+ "emergent" + "Dimensions asked for) | 'groundUp' (the default).";
864
+ Object.keys(_legacyKeyGuards).forEach(function (k) {
865
+ if (k in options) {
866
+ throw new Error("EGPTUniverse.init(): " + _legacyKeyGuards[k]);
867
+ }
868
+ });
869
+ // Anything left that INIT_OPTIONS does not declare is a key init() reads nowhere.
870
+ // It used to be accepted and discarded, so a typo ran the scene with different
871
+ // physics than the author wrote and nothing said so. LAST of the three checks:
872
+ // retired and legacy names have already claimed their specific migration messages
873
+ // above, so only a genuinely unrecognized name reaches this generic one.
874
+ _assertNoUnknownKeys('EGPTUniverse.init()', options,
875
+ INIT_OPTIONS, _initOptionKeyList());
876
+
877
+ // ── Init options ─────────────────────────────────────────────────────────
878
+ // WHAT each option means, its default, and its physics effect live in the
879
+ // INIT_OPTIONS table at the top of this file — the SAME table the unknown-key
880
+ // check above validates against, so the documentation and the check cannot
881
+ // drift apart. Restating them here would be that drift.
882
+ var wavelengthScale = options.wavelengthScale;
883
+ var stepping = options.stepping; // 'fixed' | 'average' | undefined
884
+ var noEscape = options.noEscape;
885
+ var frameGenModeOpt = options.frameGenMode; // 'groundUp' | 'topDown' | undefined (new canonical)
886
+ var brownianMotionOpt = options.brownianMotion; // bool | undefined (new canonical)
887
+
888
+ // frameGenMode — THROW-OR-APPLY on the enum; omitted ⇒ 'groundUp'. init() deliberately
889
+ // does NOT inherit the create()-time mode: it never did, and every caller that means
890
+ // topDown states it here (the create()-time key is a pre-init record only).
891
+ var frameGenMode = (frameGenModeOpt === undefined || frameGenModeOpt === null)
892
+ ? 'groundUp'
893
+ : _assertFrameGenMode('EGPTUniverse.init()', frameGenModeOpt);
894
+ var isTopDown = (frameGenMode === 'topDown');
895
+
896
+ // activeAxes — the active spatial axes for this run (1D / 2D / 3D).
897
+ //
898
+ // Layer 1 (API): a plain { x, y, z } boolean bag (camelCase option, matching the
899
+ // other init options) forwarded into the WASM UniverseConfig (serde field
900
+ // `active_axes`, default {x:true,y:true,z:false}). OMITTED from the config when not
901
+ // declared, so every existing 2D experiment's config is byte-for-byte unchanged
902
+ // (the engine applies its serde default). This is the experiment-facing knob that
903
+ // fulfills Thread B's coordination request R3.
904
+ // Layer 2 (physics): selects which axes the base-walk + yo-yo coins fire on, i.e.
905
+ // the dimensionality of the emergent motion. {x} → a 1D oscillator/walk (y,z frozen);
906
+ // {x,y,z} → a 3D isotropic emission (sphere, not cube — see the §6.10 isotropy
907
+ // oracle). capacity()/wavelength scale as the product over active axes. This is the
908
+ // browser-facing entry point for the 1D/3D discrete walk landed in the Rust engine;
909
+ // 2D experiments never set it.
910
+ // stepping — FAIL LOUD on anything that is not one of the two legal samplers. The engine
911
+ // would also reject it (serde names both variants in its unknown-variant error), but
912
+ // catching it here names the option the caller actually typed.
913
+ if (stepping !== undefined && stepping !== null &&
914
+ stepping !== 'fixed' && stepping !== 'average') {
915
+ throw new Error("EGPTUniverse.init(): stepping must be 'fixed' or 'average' (got " +
916
+ JSON.stringify(stepping) + "). 'fixed' = deterministic accumulator walk (default, " +
917
+ "bounded along-ray error); 'average' = per-axis Bernoulli walk (the historic sampler).");
918
+ }
919
+
920
+ var activeAxes = options.activeAxes;
921
+ if (activeAxes !== undefined && activeAxes !== null &&
922
+ (typeof activeAxes !== 'object' || Array.isArray(activeAxes) ||
923
+ typeof activeAxes.x !== 'boolean' || typeof activeAxes.y !== 'boolean' || typeof activeAxes.z !== 'boolean')) {
924
+ throw new Error("EGPTUniverse.init(): activeAxes must be a plain object { x:boolean, y:boolean, z:boolean }, e.g. { x:true, y:false, z:false } for a 1D walk.");
925
+ }
926
+
927
+ var frame_gen_mode = FRAME_GEN_MODES[frameGenMode];
928
+ var withCollisions = this._params.withInterQuantumCollisions;
929
+ if (isTopDown) {
930
+ withCollisions = false;
931
+ }
932
+
933
+ // ── brownianMotion — the caller's REQUEST rides the CONFIG BAG; the ENGINE resolves ──
934
+ //
935
+ // This value is a CONSTRUCTION-TIME config field (`brownian_motion` below), exactly
936
+ // like frame_gen_mode and wavelength_scale — NOT a post-construction replay. It used
937
+ // to be pushed after `new _WasmUniverse(config)` via `set_brownian_motion()` guarded
938
+ // by a truthy check on the JS mirror, which made the mirror a SECOND source of truth
939
+ // and produced three defects: (1) the mirror survived a re-init that said nothing
940
+ // about brownian, so a stale `true` was replayed into a fresh universe; (2) the
941
+ // TopDown coercion wrote the mirror, so a topDown run contaminated a later groundUp
942
+ // run on the same object — a real physics divergence in an ordinary preset switch;
943
+ // (3) an explicit `false` under topDown was accepted, reported back, and then
944
+ // discarded by the engine's own coercion.
945
+ //
946
+ // What JS computes here is ONLY the un-coerced REQUEST (`bool | undefined`).
947
+ // The TopDown⇒true / GroundUp⇒unwrap_or(false) COERCION belongs to `egpt_engine`
948
+ // `EGPTUniverse::new` and is NOT restated here: a JS copy of that match is a schema
949
+ // mirror across two languages, and this file used to carry one. `undefined` ferries
950
+ // as `null` ⇒ Rust `None`, so the DEFAULT also has exactly one authority (the Rust
951
+ // `unwrap_or`), never a JS duplicate. The resolved value is read back below through
952
+ // the `brownian_motion()` WASM getter — the running universe answers for itself.
953
+ //
954
+ // The throw below is NOT part of that resolution: it is INPUT VALIDATION on a
955
+ // self-contradictory request. The engine coerces TopDown to true unconditionally, so
956
+ // an explicit `brownianMotion:false` alongside it could only ever be discarded —
957
+ // fail loud instead of accepting a value that cannot be honoured.
958
+ if (isTopDown &&
959
+ brownianMotionOpt !== undefined && brownianMotionOpt !== null && !brownianMotionOpt) {
960
+ throw new Error(
961
+ "EGPTUniverse.init(): frameGenMode:'topDown' requires brownian motion, so an " +
962
+ "explicit brownianMotion:false contradicts it — top-down frame generation IS the " +
963
+ "brownian regime and the engine coerces brownian_motion = true " +
964
+ "unconditionally, so the false would be silently discarded. Choose one: " +
965
+ "drop brownianMotion (topDown supplies it), or use frameGenMode:'groundUp' " +
966
+ "if you want the wave-walk. brownianMotion:true with topDown is redundant " +
967
+ "but consistent, and is allowed."
968
+ );
969
+ }
970
+ var brownianRequest; // bool | undefined — the REQUEST, never the resolution
971
+ if (brownianMotionOpt !== undefined && brownianMotionOpt !== null) {
972
+ brownianRequest = !!brownianMotionOpt;
973
+ }
974
+
975
+ var wlScale = (wavelengthScale !== undefined && wavelengthScale !== null) ? wavelengthScale : 1;
976
+ var config = {
977
+ fps: this._params.fps,
978
+ iframe_interval_seconds: this._params.iframe_interval_seconds,
979
+ with_inter_quantum_collisions: withCollisions,
980
+ with_bonding: this._params.withBonding,
981
+ frame_gen_mode: frame_gen_mode,
982
+ brownian_motion: brownianRequest === undefined ? null : brownianRequest,
983
+ min_fill_rate: this._params.minMBFillRate,
984
+ universe_rect: [universe_rect.left, universe_rect.top, universe_rect.w, universe_rect.h],
985
+ wavelength_scale: wlScale,
986
+ fundamental_dimension: fundamental_dimension_number,
987
+ seed: this._params.seed,
988
+ };
989
+ // Only attach active_axes when the setup declared it — omission lets the WASM
990
+ // serde default ({x,y} 2D) apply, keeping every existing experiment's config
991
+ // (and thus its golden recording) byte-identical.
992
+ //
993
+ // NOTE: "omission ⇒ byte-identical" is a property of
994
+ // active_axes' default, NOT a general rule for omitted keys. `stepping` below defaults
995
+ // to 'fixed', which is a DELIBERATE behavior change for every caller that omits it —
996
+ // the walk sampler moved. Axis-aligned and stationary scenes are still bit-identical
997
+ // (the two samplers coincide there); off-axis scenes are expected to move, and that
998
+ // movement is the point of the change, not a regression.
999
+ if (activeAxes !== undefined && activeAxes !== null) {
1000
+ config.active_axes = { x: activeAxes.x, y: activeAxes.y, z: activeAxes.z };
1001
+ }
1002
+ // Only attach stepping when declared — omission lets the engine's serde default
1003
+ // (Fixed) apply, so there is ONE authority for the default (the Rust enum) instead of
1004
+ // a JS-side copy that could drift from it.
1005
+ if (stepping !== undefined && stepping !== null) {
1006
+ config.stepping = stepping;
1007
+ }
1008
+ // READ-BACK of the RESOLVED axes (what this run actually uses, including the
1009
+ // engine's serde default when the sketch omitted them). Truth-in-labeling: any
1010
+ // consumer that must re-declare this world (STUDIO codegen re-emitting the
1011
+ // sketch's world step) DERIVES it from here rather than guessing.
1012
+ this.activeAxes = config.active_axes
1013
+ ? { x: config.active_axes.x, y: config.active_axes.y, z: config.active_axes.z }
1014
+ : { x: true, y: true, z: false };
1015
+ // depth — explicit world z-extent (DECOUPLED from width). Only attach when the setup
1016
+ // declared it; omission lets the WASM serde default (None → depth = width, a CUBE) apply,
1017
+ // keeping every existing (square) scene's config byte-identical. A non-square 3D scene
1018
+ // (e.g. 800×400) sets depth explicitly so z-near/z-depth are no longer tied to width.
1019
+ var depth = (options.depth !== undefined && options.depth !== null) ? options.depth
1020
+ : (universe_rect && universe_rect.d !== undefined && universe_rect.d !== null &&
1021
+ universe_rect.d !== universe_rect.w ? universe_rect.d : undefined);
1022
+ if (depth !== undefined) {
1023
+ if (typeof depth !== 'number' || !(depth > 0)) {
1024
+ throw new Error('EGPTUniverse.init(): depth must be a positive number (the world z-extent).');
1025
+ }
1026
+ config.depth = depth;
1027
+ }
1028
+ // READ-BACK of the RESOLVED world bounds (truth-in-labeling, exactly as `activeAxes`
1029
+ // above): the explicit `depth` when one was attached, else the engine's documented
1030
+ // square→cube default (the OPTIONS `depth` row: omitted ⇒ depth = width; z-range
1031
+ // CENTERED [-depth/2, depth/2]). Built THROUGH `_wrapBoundaryRect` — THE ONE
1032
+ // engine-bounds → JS bag normalizer — from the same centre+extent 6-tuple the
1033
+ // engine's own `boundary_rect` emits, so the pre-tick bag and the post-tick ferry
1034
+ // share one shape by construction (init-bounds-conformance.test.mjs binds keys AND
1035
+ // values to the first tick's `universeData.boundaryRect`). Pre-tick consumers read
1036
+ // this via FrqtlApp.worldBounds (e.g. the palette Wall drop's full-scene-depth
1037
+ // default, R#101); once a tick has run, the TickData ferry is the authority and
1038
+ // wins (it tracks live resizes; this is the INIT-time declaration).
1039
+ var resolvedDepth = (depth !== undefined) ? depth : universe_rect.w;
1040
+ this.initBounds = _wrapBoundaryRect([
1041
+ universe_rect.left + universe_rect.w / 2,
1042
+ universe_rect.top + universe_rect.h / 2,
1043
+ 0,
1044
+ universe_rect.w,
1045
+ universe_rect.h,
1046
+ resolvedDepth
1047
+ ]);
1048
+
1049
+ // The WORLD is being (re)built: every prior engine registration goes with it, so
1050
+ // the wrapper census empties too. A wrapper that survived a re-init would hold a
1051
+ // handle into a universe that no longer exists — the render list and the physics
1052
+ // registry disagreeing, which is the whole failure this census removes.
1053
+ this.largeObjects.length = 0;
1054
+ this._loHandles.length = 0;
1055
+
1056
+ // _WasmUniverse is captured in the closure created by makeProvider()
1057
+ this._wasm = new _WasmUniverse(config);
1058
+ // The INIT dimension of THIS world (re-init overwrites it): the input a consumer that
1059
+ // re-declares the world re-emits. Read through `initDimension`, never the live
1060
+ // `fundamentalDimension`, which a lower addDimension ratchets down.
1061
+ this._initDimension = fundamental_dimension_number;
1062
+ // NO wavelength-scale read-back here either. The universe that just consumed the
1063
+ // config bag IS the answer, and `universe.wavelengthScale` asks it on every read —
1064
+ // so there is nothing to copy out at init time and nothing that could go stale
1065
+ // afterwards. (An assignment `this._wavelengthScale = wlScale` lived here; it was a
1066
+ // better-SOURCED cache, not engine truth, and it went stale the moment anything wrote
1067
+ // the scale past the JS property.)
1068
+ // NO brownian read-back here. The universe that just consumed the config bag IS the
1069
+ // answer, and `universe.brownianMotion` asks it on every read — so there is nothing to
1070
+ // copy out at init time and nothing that could go stale afterwards. (A read-back
1071
+ // assignment lived here once; it was a better-SOURCED cache, not engine truth.)
1072
+ // Register this universe as the active one so BoundaryFrame.makeLeafFrame
1073
+ // can forward through its makeLeafFrame door without holding an explicit reference.
1074
+ _activeUniverse = this;
1075
+ // A fresh engine starts its tick count over: drop the cap's snapshot so the next add
1076
+ // re-reads the alive count of THIS engine.
1077
+ this._capTick = null;
1078
+
1079
+ if (this._withInterQuantumCollisions) this._wasm.set_with_inter_quantum_collisions(this._withInterQuantumCollisions);
1080
+ // When noEscape is set, hard-trap children inside parent rects.
1081
+ if (noEscape) this._wasm.set_no_escape(true);
1082
+ };
1083
+
1084
+ // ── Simulation API ───────────────────────────────────────────────────
1085
+
1086
+ EGPTUniverse.prototype.addDimension = function (layer) {
1087
+ this._ensureInit();
1088
+ this._wasm.add_dimension(layer);
1089
+ return _makeBoundaryDimension(layer, this._wasm);
1090
+ };
1091
+
1092
+ EGPTUniverse.prototype.registerLargeObject = function (spec) {
1093
+ this._ensureInit();
1094
+ var normalized = Object.assign({}, spec);
1095
+ // No collision_type / actions / reposition translation: the ABI has no such fields.
1096
+ // Every behaviour — the eject face included — is already lowered into `program` by
1097
+ // actionCompiler.js before this call.
1098
+ // Rect shape normalize (existing behavior preserved; FRQTL-MAINT 2026-07-01
1099
+ // extends the object form to carry z/d when present — see _rectSpecToArray).
1100
+ if (spec.rect && !Array.isArray(spec.rect) && spec.rect.left !== undefined) {
1101
+ normalized.rect = _rectSpecToArray(spec.rect);
1102
+ }
1103
+ // Extract program BEFORE passing to register_large_object.
1104
+ // serde_wasm_bindgen v0.6 cannot deserialize internally-tagged enums
1105
+ // (#[serde(tag="kind")]) from JsValue via the custom Deserializer — it can't
1106
+ // look up the "kind" discriminant from a JS object map during tagged-enum dispatch.
1107
+ // Safe path: strip program from the spec, register WITHOUT program, then
1108
+ // attach via set_large_object_program_json (which uses serde_json on a JSON string,
1109
+ // which correctly handles internally-tagged enums). Confirmed working for all
1110
+ // existing variants (Lit, Ref, Bin, Cmp, Cond, Fn) and required for StrLit.
1111
+ var programPayload = normalized.program;
1112
+ delete normalized.program;
1113
+
1114
+ var handle = this._wasm.register_large_object(normalized);
1115
+ this._loHandles.push(handle);
1116
+
1117
+ if (programPayload != null && typeof this._wasm.set_large_object_program_json === 'function') {
1118
+ this._wasm.set_large_object_program_json(handle, JSON.stringify(programPayload));
1119
+ }
1120
+
1121
+ return handle;
1122
+ };
1123
+
1124
+ EGPTUniverse.prototype.doTick = function () {
1125
+ this._ensureInit();
1126
+ // [D6] Run legacy experimentTickFunctions for backwards compat.
1127
+ // Node copy checked return values and injected if array; browser copy ignored return.
1128
+ // Use node's more-complete path so legacy game objects work.
1129
+ for (var i = 0; i < this.experimentTickFunctions.length; i++) {
1130
+ var fn = this.experimentTickFunctions[i];
1131
+ var result = fn(this);
1132
+ if (Array.isArray(result) && result.length > 0) {
1133
+ // The returned-array injection path is DELETED: its wire format carried no
1134
+ // layer, so every frame it made was minted at the fundamental. Injection now
1135
+ // routes by the frame's own layer and cannot infer one from this shape.
1136
+ throw new TypeError(
1137
+ '[doTick] experimentTickFunctions[' + i + '] returned an array of ' +
1138
+ result.length + ' frames. Returning frames to be injected is no longer ' +
1139
+ 'supported — it could not express a frame\'s layer. Mint and inject ' +
1140
+ 'explicitly instead: universe.makeLeafFrame(left, top, level, ...) then ' +
1141
+ 'universe.injectFrame(handle), and return nothing.');
1142
+ }
1143
+ }
1144
+ var rawTick = this._wasm.do_tick();
1145
+ this.tick = Number(this._wasm.tick());
1146
+ var wrapped = this._wrapTickData(rawTick);
1147
+ // Cache the wrapped tick output so POST-tick host consumers (e.g. the ABSORB radiation
1148
+ // tick-fn, which tallies THIS/last tick's collided KILLED frames) can read
1149
+ // TickData.objects[handle].frames. experimentTickFunctions are called with fn(universe)
1150
+ // BEFORE do_tick (Phase-A: emitters inject), so a post-tick consumer registered there
1151
+ // must read the universe's cached last tickData (a one-tick lag; mass stays conserved).
1152
+ // Without this the radiation fn read `universe.objects` (undefined) → silent zero (K5).
1153
+ this._lastTickData = wrapped;
1154
+ return wrapped;
1155
+ };
1156
+
1157
+ /**
1158
+ * G-EMIT generic primitive: advance the engine's deterministic Mulberry32 RNG
1159
+ * and return the next value in [0,1).
1160
+ *
1161
+ * Host tick-fns (e.g. the emitter tick-fn from emitterProgram.buildEmitterTickFn) call this
1162
+ * to draw x/y/angle/charge from the same single deterministic stream that
1163
+ * physics uses. Must be called BEFORE do_tick() (i.e. from experimentTickFunctions).
1164
+ *
1165
+ * @returns {number} Next f64 in [0,1) from the Mulberry32 stream.
1166
+ */
1167
+ EGPTUniverse.prototype.random = function () {
1168
+ this._ensureInit();
1169
+ return this._wasm.random();
1170
+ };
1171
+
1172
+ /**
1173
+ * Render-intent MERGE stamp (provider-side output-boundary marshalling).
1174
+ *
1175
+ * READ the frame's CURRENT engine-opaque data bag → MERGE the given render-intent
1176
+ * string_map keys into its `string_map` (preserving `detection` + any pre-existing
1177
+ * keys) → WRITE the whole bag back. This is a marshalling fix at the OUTPUT
1178
+ * BOUNDARY (zero physics, no engine change): a render-intent stamp NEVER clobbers
1179
+ * other string_map data, and the provider is correct on its OWN — it does not
1180
+ * depend on the engine's `set_frame_property('data')` happening to merge string_map
1181
+ * (Q-A3). Additional render-intent keys (a future `render.shape`,
1182
+ * `render.outline`, …) drop in by extending `keys` — one seam, no color special-case.
1183
+ *
1184
+ * NOTE (empirical finding, pkg-node dump): the engine ALREADY merges
1185
+ * string_map keys and carries `render.color` verbatim across ticks (a live-set
1186
+ * color persists) — this get→merge→set does not FIX a persistence bug (there is
1187
+ * none on the emitter path); it DECOUPLES the provider from the engine's merge
1188
+ * behavior and makes the stamp point trivially multi-key for A3.
1189
+ *
1190
+ * @param {Object} wasm — the WASM universe (this._wasm at makeLeafFrame; `w` at createFrameFromRect)
1191
+ * @param {number} qid — frame quantum_id (get_frame_quantum_id of the SoA handle)
1192
+ * @param {Object} keys — { 'render.color': 'r,g,b,a', ... } string_map keys to merge
1193
+ */
1194
+ function _stampRenderIntentMerge(wasm, qid, keys) {
1195
+ if (!wasm || !qid || qid <= 0 || !keys) { return; }
1196
+ if (typeof wasm.set_frame_property !== 'function') { return; }
1197
+ var bag = null;
1198
+ if (typeof wasm.get_frame_property === 'function') {
1199
+ try {
1200
+ var cur = wasm.get_frame_property(qid, 'data');
1201
+ if (typeof cur === 'string' && cur.length) { bag = JSON.parse(cur); }
1202
+ else if (cur && typeof cur === 'object') { bag = cur; }
1203
+ } catch (_) { bag = null; }
1204
+ }
1205
+ if (!bag || typeof bag !== 'object') { bag = {}; }
1206
+ var sm = (bag.string_map && typeof bag.string_map === 'object') ? bag.string_map : {};
1207
+ for (var k in keys) {
1208
+ if (Object.prototype.hasOwnProperty.call(keys, k) && keys[k] != null) { sm[k] = String(keys[k]); }
1209
+ }
1210
+ bag.string_map = sm;
1211
+ try { wasm.set_frame_property(qid, 'data', JSON.stringify(bag)); } catch (_) {}
1212
+ }
1213
+
1214
+ /**
1215
+ * G-EMIT-2 generic primitive: create a leaf frame and return its handle. Engine-agnostic
1216
+ * surface (mirrors the JS reference's universe.makeLeafFrame) so the emitter is shared sugar
1217
+ * (emitterProgram.js). chargeInt: 0=Positive, 1=Negative. Optional color [r,g,b,a] is stamped
1218
+ * via the data bubble AFTER creation (the make_leaf_frame ABI has no color arg) — 0 RNG draws,
1219
+ * so the golden draw order (make_leaf_frame's ctor-mirror +1) is preserved. The color rides the
1220
+ * generic string_map render-intent MERGE (_stampRenderIntentMerge) so it never clobbers other
1221
+ * bubble keys and additional render-intent keys drop in at the SAME seam.
1222
+ *
1223
+ * WASM-ONLY render-intent positionals (R3-1, DECLARED per the JS-ref-is-a-live-oracle
1224
+ * rule): `color` (9th), `shape` (10th), `outline` (11th) are OUTPUT-BOUNDARY render marshalling —
1225
+ * they are stamped onto the engine-opaque string_map here, WASM-side. The frozen JS reference
1226
+ * engine's makeLeafFrame has ONLY the `color` positional (color_override); it
1227
+ * ignores the shape/outline args (extra JS args = no-op) BY DESIGN — the JS reference is a frozen
1228
+ * visual fixture (never edited; CLAUDE.md §Engine discipline) and the realm is WASM. This is a
1229
+ * DOCUMENTED no-op on the JS twin, NOT a silent drop — covered by the render-intent-merge test
1230
+ * (WASM stamp asserted) + the emitterProgram threading note. Adding a 4th key is one more `if` here.
1231
+ * THE ONE LEAF DOOR, AND THE PREVIEW CAP: every leaf a host births comes through here (the
1232
+ * static Frame.makeLeafFrame routes through this method), so the `liveQuantaCap` admission
1233
+ * check runs FIRST — a refused leaf costs no frame-store slot and no RNG draw — and the add
1234
+ * returns `null` ("no frame"; handle 0 is a real frame). injectFrame(null) and
1235
+ * getFrameQuantumId(null) absorb it; setLargeObjectFrame(h, null) throws.
1236
+ * @returns {number|null} frame handle (SoA FrameId), or null when the cap refused the add
1237
+ */
1238
+ EGPTUniverse.prototype.makeLeafFrame = function (left, top, level, wavelength, vx, vy, wrap, chargeInt, color, shape, outline) {
1239
+ this._ensureInit();
1240
+ if (!this._admitLeaf()) { return null; }
1241
+ // LEVEL (leaf scale) defaults to the universe's fundamental dimension: a fundamental leaf
1242
+ // at dimension N has level N (Rust make_leaf_frame: width=2^N, capacity=width², wavelength
1243
+ // derived). The emitter passes null; only an explicit override passes a number. Mirrors the
1244
+ // JS reference makeLeafFrame.
1245
+ // DYNAMIC DIMENSIONS: the LIVE fundamental self-lowers when a lower dimension is
1246
+ // allocated (add_dimension), so it is read through the one `fundamentalDimension`
1247
+ // getter on every mint, never snapshotted.
1248
+ var lvl = (level != null) ? level : this.fundamentalDimension;
1249
+ // I2 BUGFIX (discovered by the LightSource source-invariance oracle):
1250
+ // the Rust wasm binding's `charge: i32` contract is "0=Positive, 1=Negative,
1251
+ // ANY OTHER VALUE (canonically -1)=random draw inside make_leaf_frame"
1252
+ // (egpt_wasm/src/lib.rs make_leaf_frame doc comment) — but this wrapper
1253
+ // passed `chargeInt` straight through UNTRANSLATED, unlike `wavelength`
1254
+ // right above (which already does `!= null ? wavelength : 0`). A caller
1255
+ // passing `null`/`undefined` for "please draw a random charge" (the
1256
+ // intuitive JS convention this codebase uses everywhere else — see `level`
1257
+ // just above) got silently coerced to `0` at the wasm-bindgen i32 ABI
1258
+ // boundary (JS `null`/`undefined` -> `+0` under numeric coercion), which
1259
+ // the Rust match reads as `0 => Some(Charge::Positive)` — an EXPLICIT
1260
+ // override to ALWAYS Positive, never a random draw. Measured: 100/100
1261
+ // particleWalk quanta got charge=POSITIVE under `polarization:'random'`
1262
+ // before this fix (LightSource.js, andI1's setupTunnelLab.js, both affected
1263
+ // — neither engine-visible before this oracle surfaced it, since every
1264
+ // OTHER makeLeafFrame caller in the tree passes an explicit 0/1). The JS
1265
+ // reference engine's own `_jsCharge` helper (js-engine.js) already treats
1266
+ // null/undefined as "random" correctly — this was a WASM-only parity bug.
1267
+ // Fix: translate null/undefined to -1 (the documented random-draw sentinel)
1268
+ // here, matching the wavelength line's own null-handling convention exactly.
1269
+ var chargeArg = (chargeInt === null || chargeInt === undefined) ? -1 : chargeInt;
1270
+ var handle = this._wasm.make_leaf_frame(
1271
+ left, top, lvl, (wavelength != null ? wavelength : 0), vx, vy, !!wrap, chargeArg);
1272
+ // Render-intent stamp: color AND/OR shape ride the SAME provider-side
1273
+ // MERGE seam (get→merge→set) so multiple keys coexist and neither clobbers the
1274
+ // engine-opaque bubble. Both omitted ⇒ 0 draws, no stamp → byte-identical.
1275
+ var _intent = null;
1276
+ if (color) {
1277
+ _intent = _intent || {};
1278
+ _intent['render.color'] = [color[0], color[1], color[2], color[3] !== undefined ? color[3] : 255].join(',');
1279
+ }
1280
+ if (shape != null && shape !== '') {
1281
+ _intent = _intent || {};
1282
+ _intent['render.shape'] = String(shape);
1283
+ }
1284
+ if (outline != null && outline !== '') {
1285
+ // render.outline is the ENUM 'true'|'false' (frame-data RENDER_INTENT_VALUES). Coerce a
1286
+ // boolean/'true' to 'true', anything else to 'false' — a null/undefined arg stamps nothing.
1287
+ _intent = _intent || {};
1288
+ _intent['render.outline'] = (outline === true || outline === 'true') ? 'true' : 'false';
1289
+ }
1290
+ if (_intent && typeof this._wasm.get_frame_quantum_id === 'function') {
1291
+ var qid = this._wasm.get_frame_quantum_id(handle);
1292
+ if (qid > 0) { _stampRenderIntentMerge(this._wasm, qid, _intent); }
1293
+ }
1294
+ return handle;
1295
+ };
1296
+
1297
+ /**
1298
+ * The `liveQuantaCap` admission check — private to makeLeafFrame. The alive count is an
1299
+ * O(capacity) engine scan, so it is read ONCE per engine tick (keyed by `this.tick`, which
1300
+ * doTick updates after the engine steps: the first add of a tick's Phase A sees the count at
1301
+ * the previous tick's end); every leaf admitted since is counted here, so births never
1302
+ * overshoot the cap. Deaths inside a tick show up at the next snapshot.
1303
+ * @returns {boolean} true = make the leaf; false = refuse it (the cap is reached)
1304
+ */
1305
+ EGPTUniverse.prototype._admitLeaf = function () {
1306
+ var cap = this._params.liveQuantaCap;
1307
+ if (cap === false) { return true; }
1308
+ if (this._capTick !== this.tick) {
1309
+ var alive = this._wasm.active_quanta();
1310
+ if (typeof alive !== 'number' || !isFinite(alive)) {
1311
+ throw new Error('[EGPTUniverse] liveQuantaCap cannot read the alive count: ' +
1312
+ 'active_quanta() returned ' + String(alive) + ', not a finite number.');
1313
+ }
1314
+ this._capTick = this.tick;
1315
+ this._capAlive = alive;
1316
+ this._capAdmitted = 0;
1317
+ }
1318
+ if (this._capAlive + this._capAdmitted >= cap) {
1319
+ if (!this._capWarned) {
1320
+ this._capWarned = true;
1321
+ var ceiling = _liveQuantaCapRow().def.toLocaleString('en-US');
1322
+ console.warn('[EGPTUniverse] liveQuantaCap: ' + cap.toLocaleString('en-US') +
1323
+ ' leaf quanta are alive — further leaf frames are skipped until the count ' +
1324
+ 'falls below it. This version of the engine is a limited preview release ' +
1325
+ 'capped at ' + ceiling + ' live quanta.');
1326
+ }
1327
+ return false;
1328
+ }
1329
+ this._capAdmitted++;
1330
+ return true;
1331
+ };
1332
+
1333
+ /**
1334
+ * THE CAPACITY LAW, PUBLISHED — the cell capacity of a leaf at `level`, read off the
1335
+ * engine that owns it (`ActiveAxes::level_capacity`). Sugar that must size a frame by
1336
+ * level CONSUMES this; it never re-derives `4^level`, which is the A={x,y} answer only
1337
+ * and over-states a leaf's room on every other axis set. Capacity is the denominator of
1338
+ * both the move gate (fullness) and the wavelength, so a hand mirror mis-scales speed and
1339
+ * λ together — see `absorbRadiation.js`, the live consumer.
1340
+ *
1341
+ * @param {number} level - leaf/dimension scale level (edge = 2^level).
1342
+ * @returns {number} cells the frame owns, over the universe's ACTIVE axes.
1343
+ */
1344
+ EGPTUniverse.prototype.levelCapacity = function (level) {
1345
+ this._ensureInit();
1346
+ if (typeof this._wasm.level_capacity !== 'function') {
1347
+ throw new Error('[EGPTUniverse.levelCapacity] the engine does not publish the ' +
1348
+ 'capacity law (level_capacity). Rebuild the engine (make wasm / make wasm-node).');
1349
+ }
1350
+ return this._wasm.level_capacity(level);
1351
+ };
1352
+
1353
+ /**
1354
+ * G-EMIT generic primitive: inject an already-created frame handle into the
1355
+ * engine's injection buffer for consumption in Phase A of the next do_tick().
1356
+ *
1357
+ * Companion to make_leaf_frame(): the host emitter tick-fn calls make_leaf_frame()
1358
+ * to create a frame (with correct wavelength/charge), then calls injectFrame()
1359
+ * to enqueue it.
1360
+ *
1361
+ * @param {number|null} frameHandle - SoA FrameId returned by makeLeafFrame(); `null` (an add
1362
+ * the liveQuantaCap refused) is a no-op — it never reaches the engine's injection buffer.
1363
+ */
1364
+ EGPTUniverse.prototype.injectFrame = function (frameHandle) {
1365
+ this._ensureInit();
1366
+ if (frameHandle === null) { return; }
1367
+ this._wasm.inject_frame(frameHandle);
1368
+ };
1369
+
1370
+ /**
1371
+ * Resolve a makeLeafFrame handle (SoA FrameId) to its QUANTUM_ID — the id that
1372
+ * setFrameProperty/getFrameProperty and tickData.frameData[i].id key on. These two
1373
+ * id spaces differ; mutating a per-frame property with the raw makeLeafFrame handle
1374
+ * silently targets the wrong frame. Use this to bridge them (see the color path above,
1375
+ * which does the same conversion internally). A `null` handle (an add the liveQuantaCap
1376
+ * refused) answers 0, the ABI's own "no frame" id — never reaching the ABI, where null
1377
+ * coerces to the REAL frame 0.
1378
+ */
1379
+ EGPTUniverse.prototype.getFrameQuantumId = function (frameHandle) {
1380
+ this._ensureInit();
1381
+ if (frameHandle === null) { return 0; }
1382
+ return (typeof this._wasm.get_frame_quantum_id === 'function')
1383
+ ? this._wasm.get_frame_quantum_id(frameHandle)
1384
+ : frameHandle;
1385
+ };
1386
+
1387
+ /**
1388
+ * MANDATORY BACKING — RE-ATTACH a LargeObject to a (moving)
1389
+ * backing Frame (a make_leaf_frame handle / FrameId): Bullet-style move-by-frame. Each tick
1390
+ * the engine re-centers the LO's rect on that frame's position (keeping the LO's own w/h/d).
1391
+ * Every LO is ALREADY frame-backed at registration; this RE-POINTS it (e.g. Gun / setupG2Sweep
1392
+ * binding the LO to a moving body leaf). There is NO null/detach path — pass a real FrameId.
1393
+ * Mass is not transferred on re-attach (default no-transfer). This is the "LO extends Frame"
1394
+ * composition.
1395
+ */
1396
+ EGPTUniverse.prototype.setLargeObjectFrame = function (handle, frameHandle) {
1397
+ this._ensureInit();
1398
+ if (frameHandle === null) {
1399
+ throw new Error('[EGPTUniverse.setLargeObjectFrame] the backing frame is null — the ' +
1400
+ 'leaf add was refused by liveQuantaCap (' + this._params.liveQuantaCap + ' live ' +
1401
+ 'quanta). A LargeObject needs a real backing frame.');
1402
+ }
1403
+ this._wasm.set_large_object_property(
1404
+ handle, 'frame_id', JSON.stringify(frameHandle));
1405
+ };
1406
+
1407
+ /**
1408
+ * MANDATORY BACKING — read the SoA FrameId of the LO's backing frame (created at
1409
+ * registration). Sugar (e.g. absorbRadiation.js) READS the ledger frame here instead of
1410
+ * creating one. Returns -1 for an out-of-range handle.
1411
+ */
1412
+ EGPTUniverse.prototype.getLargeObjectFrameId = function (handle) {
1413
+ this._ensureInit();
1414
+ return this._wasm.get_large_object_frame_id(handle);
1415
+ };
1416
+
1417
+ // ── The engine-visible ROUTING TABLE (the ONE statement of what this provider
1418
+ // can push to WASM) ────────────────────────────────────────────────────────
1419
+ //
1420
+ // updateObjectProperty's dispatch AND its unroutable-property error both read
1421
+ // THIS table — never a second hand-written key list. A prop that is
1422
+ // engineVisible:true in the schema but absent here has NO WASM ABI, which is
1423
+ // exactly the condition that used to fall off the end of the old `else if`
1424
+ // chain and return silently (a scene that renders confidently while the engine
1425
+ // never received the edit — the DetectorWall case).
1426
+ //
1427
+ // Keyed by schema KIND. A kind absent from this table has no engine-side
1428
+ // dispatch at all in this provider.
1429
+ var _ENGINE_PROP_ROUTES = {
1430
+ LargeObject: {
1431
+ // G4b: rect/layer/count route through the ONE generic
1432
+ // set_large_object_property accessor (collapsed the typed setters).
1433
+ rect: function (u, handle, value) {
1434
+ // FRQTL-MAINT (2026-07-01): value may be [l,t,w,h], [l,t,z,w,h,d], or the
1435
+ // object form {left,top,w,h,z?,d?} — normalize to the wire array.
1436
+ var rectArr = Array.isArray(value) ? value : _rectSpecToArray(value);
1437
+ u._wasm.set_large_object_property(handle, 'rect', JSON.stringify(rectArr));
1438
+ },
1439
+ layer: function (u, handle, value) {
1440
+ // layer n → observed_dimensions=[n]; null → []
1441
+ u._wasm.set_large_object_property(handle, 'layer',
1442
+ JSON.stringify((value !== null && value !== undefined) ? value : null));
1443
+ },
1444
+ count: function (u, handle, value) {
1445
+ // writable integer counter (JSON int — no BigInt; the accessor reads i64 from JSON)
1446
+ u._wasm.set_large_object_property(handle, 'count', JSON.stringify(Math.trunc(value)));
1447
+ },
1448
+ program: function (u, handle, value) {
1449
+ // §10.3 L3: attach or replace the op-program on an existing LO.
1450
+ // Used to add eject op-programs to walls after registerLargeObject.
1451
+ // value is the program object { on, ops } or null to clear.
1452
+ if (typeof u._wasm.set_large_object_program_json === 'function') {
1453
+ var progJson = (value !== null && value !== undefined)
1454
+ ? JSON.stringify(value)
1455
+ : 'null';
1456
+ u._wasm.set_large_object_program_json(handle, progJson);
1457
+ }
1458
+ // No-op fallback if WASM binary predates this ABI (older builds).
1459
+ },
1460
+ region: function (u, handle, value) {
1461
+ // SWEPT-OBB narrow-phase region (circulation §2): retarget the rotated collision
1462
+ // slab on a running LO (Wall registration + live orientation edit). value is the
1463
+ // convention-free { kind:'obb', theta, hw, hh, hz } from SlabFace, or null to
1464
+ // clear back to the byte-identical AABB path. The ABI validates the shape
1465
+ // (throw-or-apply; a bad kind/extent throws) — never a silent skip. The string
1466
+ // 'null' clears (matches egpt_wasm set_large_object_property 'region' handling).
1467
+ var regionJson = (value !== null && value !== undefined)
1468
+ ? JSON.stringify(value)
1469
+ : 'null';
1470
+ u._wasm.set_large_object_property(handle, 'region', regionJson);
1471
+ }
1472
+ }
1473
+ };
1474
+ // 'backdrop' shares LargeObject's dispatch (same wrapper class, same ABI).
1475
+ _ENGINE_PROP_ROUTES.backdrop = _ENGINE_PROP_ROUTES.LargeObject;
1476
+
1477
+ // ── Props DELIBERATELY applied at the JS layer, not by any WASM ABI ──────────
1478
+ //
1479
+ // These reach this method and are correctly a no-op HERE because
1480
+ // FrqtlApp.updateObjectProperty has already mirrored them onto the JS wrapper,
1481
+ // which is the whole mechanism:
1482
+ //
1483
+ // reposition engineVisible:true, but there is no standalone WASM ABI —
1484
+ // it is read at LO registration time, never during collide()
1485
+ // from Rust. The wrapper mirror IS the applied state.
1486
+ //
1487
+ // This is NOT a mirror of the schema's field list — it is this provider's own
1488
+ // statement about which engine-visible props have no ABI yet.
1489
+ var _JS_APPLIED_PROPS = { reposition: true };
1490
+
1491
+ /**
1492
+ * Uniform schema-driven mutation entry point — ENGINE-VISIBLE props only (Phase 4b).
1493
+ *
1494
+ * The WASM universe does not hold references to JS-side LargeObject wrappers, so
1495
+ * render-only props (engineVisible:false) CANNOT be routed here. The caller
1496
+ * (FrqtlApp.updateObjectProperty) handles render-only props by mutating the
1497
+ * JS wrapper directly before reaching this method.
1498
+ *
1499
+ * This method:
1500
+ * 1. Validates the prop against the schema → FAIL LOUD on unknown/readonly/wrong-kind.
1501
+ * 2. For engineVisible:false props that reach here (a caller that bypassed FrqtlApp):
1502
+ * no-op (the JS wrapper is already updated).
1503
+ * 3. For engineVisible:true props: delegates to the appropriate WASM ABI.
1504
+ *
1505
+ * @param {number} handle - Opaque handle (registerLargeObject)
1506
+ * @param {string} kind - Object kind matching ObjectSchema registry
1507
+ * @param {string} prop - Field name in the schema descriptor
1508
+ * @param {*} value - New value
1509
+ * @throws {TypeError} - If prop is not in the schema for kind, or is readonly
1510
+ */
1511
+ EGPTUniverse.prototype.updateObjectProperty = function (handle, kind, prop, value) {
1512
+ this._ensureInit();
1513
+
1514
+ var schema = ObjectSchema; // the canonical descriptor table (objects/schema), a static import
1515
+
1516
+ // ── Schema validation ─────────────────────────────────────────────────
1517
+ var fieldDef = null;
1518
+ var descriptor = schema.get(kind);
1519
+ if (!descriptor) {
1520
+ throw new TypeError(
1521
+ '[updateObjectProperty] Unknown object kind "' + kind + '"'
1522
+ );
1523
+ }
1524
+ for (var i = 0; i < descriptor.fields.length; i++) {
1525
+ if (descriptor.fields[i].name === prop) {
1526
+ fieldDef = descriptor.fields[i];
1527
+ break;
1528
+ }
1529
+ }
1530
+ if (!fieldDef) {
1531
+ throw new TypeError(
1532
+ '[updateObjectProperty] Property "' + prop + '" is not in the schema for kind "' + kind + '"'
1533
+ );
1534
+ }
1535
+ if (fieldDef.type === 'readonly') {
1536
+ throw new TypeError(
1537
+ '[updateObjectProperty] Property "' + prop + '" is readonly for kind "' + kind + '"'
1538
+ );
1539
+ }
1540
+ // Render-only props: handled by FrqtlApp before we get here.
1541
+ // If they somehow reach this path, no-op — the JS wrapper is already mutated.
1542
+ if (!fieldDef.engineVisible) {
1543
+ return;
1544
+ }
1545
+
1546
+ // ── Engine-visible dispatch → WASM ABI ───────────────────────────────
1547
+ // ONE table (_ENGINE_PROP_ROUTES) drives both the call and the error below.
1548
+ var routes = _ENGINE_PROP_ROUTES[kind];
1549
+ if (routes && Object.prototype.hasOwnProperty.call(routes, prop)) {
1550
+ routes[prop](this, handle, value);
1551
+ return;
1552
+ }
1553
+
1554
+ // Applied at the JS layer by FrqtlApp's wrapper mirror — a no-op here is
1555
+ // the CORRECT outcome, not a swallow. See _JS_APPLIED_PROPS.
1556
+ if (Object.prototype.hasOwnProperty.call(_JS_APPLIED_PROPS, prop)) {
1557
+ return;
1558
+ }
1559
+
1560
+ // ── FAIL LOUD: engine-visible, but nothing routes it ─────────────────
1561
+ // Reaching here means the schema declares this prop engineVisible:true for
1562
+ // this kind while NO WASM ABI receives it. The old code returned silently,
1563
+ // so the Inspector showed the edit and the running scene ignored it
1564
+ // (DetectorWall's wallX/wallY/wallHeight/wallWidth/wallCollisionType, the
1565
+ // double-slit detector — a scene that renders confidently and is not the
1566
+ // scene it claims). If the write genuinely belongs to the JS layer, the
1567
+ // object's own setter must perform it BEFORE delegating here, the way
1568
+ // Wall/Tunnel/SphereEmitter/QuantumEmitter-rect already return early in
1569
+ // FrqtlApp.updateObjectProperty.
1570
+ //
1571
+ // Both lists below are DERIVED: the routable set from the dispatch table
1572
+ // above, the declared set from the descriptor this method already resolved.
1573
+ // Neither is hand-enumerated here.
1574
+ var routable = routes ? Object.keys(routes) : [];
1575
+ var declared = [];
1576
+ var d2 = schema.get(kind);
1577
+ for (var j = 0; j < d2.fields.length; j++) {
1578
+ if (d2.fields[j].engineVisible && d2.fields[j].type !== 'readonly') {
1579
+ declared.push(d2.fields[j].name);
1580
+ }
1581
+ }
1582
+ throw new TypeError(
1583
+ '[updateObjectProperty] Property "' + prop + '" is engine-visible for kind "' +
1584
+ kind + '" but no WASM ABI routes it — the write would be silently discarded. ' +
1585
+ (routable.length
1586
+ ? 'This provider routes for "' + kind + '": ' + routable.join(', ') + '.'
1587
+ : 'This provider has NO engine dispatch for kind "' + kind + '" at all.') +
1588
+ ' The schema declares these engine-visible fields for "' + kind + '": ' +
1589
+ (declared.length ? declared.join(', ') : '(none)') + '. ' +
1590
+ 'Either add the ABI, or apply the write at the JS layer (the object\'s own ' +
1591
+ 'setter) and return before delegating to universe.updateObjectProperty.'
1592
+ );
1593
+ };
1594
+
1595
+ EGPTUniverse.prototype.setLargeObjectCollisionType = function (handle, collisionType) {
1596
+ // G2: native collision_type is fully deleted from the Rust engine.
1597
+ // All velocity behaviors (RANDOM_X/Y/XY, MOMENTUM_BASED, WALL, BOUNCE) are compiled
1598
+ // into op-programs by actionCompiler at registration time. Calling this method at
1599
+ // runtime is a no-op — the op-program is the only velocity mechanism.
1600
+ // set_large_object_collision_type / set_large_object_collision_type_str are DELETED
1601
+ // WASM ABI methods. Calling them would cause a WASM trap. Warn instead of crash.
1602
+ if (typeof console !== 'undefined') {
1603
+ console.warn('[wasm-engine] setLargeObjectCollisionType(' + handle + ', ' +
1604
+ JSON.stringify(collisionType) + ') called after G2 deletion — no-op.' +
1605
+ ' Compile velocity behavior into an op-program at registration time instead.');
1606
+ }
1607
+ };
1608
+
1609
+ EGPTUniverse.prototype.setLargeObjectActive = function (handle, active) {
1610
+ this._ensureInit();
1611
+ // G4b: generic LO accessor (was set_large_object_active).
1612
+ this._wasm.set_large_object_property(handle, 'active', JSON.stringify(!!active));
1613
+ };
1614
+
1615
+ /**
1616
+ * Update the rect of a large object by handle (IDE direct-manipulation API).
1617
+ * Delegates to the WASM ABI when the binary exposes update_large_object_rect;
1618
+ * silently no-ops if the binary is older (render rect is still moved by
1619
+ * FrqtlApp.moveLargeObject — visually correct for the next frame).
1620
+ *
1621
+ * @param {number} handle
1622
+ * @param {Array|Object} rect - [left, top, w, h], [left, top, z, w, h, d]
1623
+ * (FRQTL-MAINT 2026-07-01: explicit z/depth, mirrors Rectangle::from_top_left's
1624
+ * own argument order), or the object form {left, top, w, h, z?, d?}.
1625
+ */
1626
+ EGPTUniverse.prototype.updateLargeObjectRect = function (handle, rect) {
1627
+ this._ensureInit();
1628
+ var rectArr = Array.isArray(rect) ? rect : _rectSpecToArray(rect);
1629
+ if (typeof this._wasm.set_large_object_property === 'function') {
1630
+ // G4b: generic LO accessor (was update_large_object_rect).
1631
+ this._wasm.set_large_object_property(handle, 'rect', JSON.stringify(rectArr));
1632
+ }
1633
+ // No-op fallback: FrqtlApp.moveLargeObject already updated lo.rect
1634
+ // so the renderer sees the new position. Physics rect update deferred until
1635
+ // the WASM binary gains the ABI endpoint.
1636
+ };
1637
+
1638
+
1639
+ EGPTUniverse.prototype.queryRect = function (left, top, w, h) {
1640
+ this._ensureInit();
1641
+ return this._wasm.query_rect(left, top, w, h);
1642
+ };
1643
+
1644
+ /**
1645
+ * SLICE 3 — set the writable integer counter for a registered LargeObject.
1646
+ *
1647
+ * B2.1 (Gate-2 binding): count is read from
1648
+ * TickData.objects[handle].count (the ONE read authority). The pull
1649
+ * getLargeObjectCount is DELETED — count has a single marshalling path.
1650
+ *
1651
+ * @param {number} handle - Opaque handle from registerLargeObject
1652
+ * @param {number} value - New count value (integer)
1653
+ */
1654
+ EGPTUniverse.prototype.setLargeObjectCount = function (handle, value) {
1655
+ this._ensureInit();
1656
+ // G4b: generic LO accessor (was set_large_object_count). JSON int — the accessor reads i64.
1657
+ this._wasm.set_large_object_property(handle, 'count', JSON.stringify(Math.trunc(value)));
1658
+ };
1659
+
1660
+ /**
1661
+ * Query frames overlapping a rectangular region (SLICE 2 — EXTENSIBLE read path).
1662
+ *
1663
+ * Returns an array of canonical FrameData objects — the SAME shape as
1664
+ * doTick() already emits (marshalled via _wrapTickData's FrameData path).
1665
+ * NOT a parallel DTO.
1666
+ *
1667
+ * The WASM ABI returns a JSON-encoded FrameData array. We deserialize and
1668
+ * pass each frame through the same FrameData constructor used by _wrapTickData
1669
+ * so callers get an identical object shape.
1670
+ *
1671
+ * @param {number} left - Left edge (physics pixels)
1672
+ * @param {number} top - Top edge (physics pixels)
1673
+ * @param {number} w - Width (physics pixels)
1674
+ * @param {number} h - Height (physics pixels)
1675
+ * @returns {FrameData[]}
1676
+ */
1677
+ EGPTUniverse.prototype.queryRectFrames = function (left, top, w, h) {
1678
+ this._ensureInit();
1679
+ var json = this._wasm.query_rect_frames(left, top, w, h);
1680
+ if (!json) return [];
1681
+ var rawFrames;
1682
+ try {
1683
+ rawFrames = JSON.parse(json);
1684
+ } catch (_e) {
1685
+ return [];
1686
+ }
1687
+ if (!Array.isArray(rawFrames) || rawFrames.length === 0) return [];
1688
+
1689
+ // Marshal each raw frame through the canonical _wrapRawFrame() — the SAME
1690
+ // path _wrapTickData uses for do_tick() output. No parallel DTO: a single
1691
+ // marshaller means z/vz and the string_map Map→obj normalization
1692
+ // are inherited here for free instead of being hand-mirrored.
1693
+ var result = [];
1694
+ for (var j = 0; j < rawFrames.length; j++) {
1695
+ result.push(_wrapRawFrame(rawFrames[j], this.tick));
1696
+ }
1697
+ return result;
1698
+ };
1699
+
1700
+ /**
1701
+ * Query frames inside an ORIENTED bounding box (the exposed community narrow-phase
1702
+ * primitive; circulation §4). A between-ticks READ — a snapshot has a fixed position,
1703
+ * so this is a point-in-OBB test (no sweep). Standard-math geometry in (explicit center
1704
+ * + theta radians + half-extents in the ENGINE frame — the FRQTL "tall-mirror" convention
1705
+ * is one JS adapter, SlabFace, ABOVE this convention-free primitive); returns canonical
1706
+ * FrameData[] via the SAME marshaller as queryRectFrames (no parallel DTO). DRY: the same
1707
+ * region.rs evaluator the LO narrow-phase filter uses.
1708
+ *
1709
+ * WASM-ONLY: the frozen 2D JS reference has no OBB primitive (the fixture
1710
+ * is never edited); the JS provider FAILS LOUD instead of silently returning [].
1711
+ *
1712
+ * @param {number} cx center X (physics px)
1713
+ * @param {number} cy center Y (physics px)
1714
+ * @param {number} cz center Z (physics px; 0 for 2D)
1715
+ * @param {number} theta rotation about z (RADIANS, engine frame)
1716
+ * @param {number} hw half-extent along local x (px, > 0)
1717
+ * @param {number} hh half-extent along local y (px, > 0)
1718
+ * @param {number} hz half-extent along local z (px; large value = 2D z-passthrough)
1719
+ * @returns {FrameData[]}
1720
+ */
1721
+ EGPTUniverse.prototype.queryObbFrames = function (cx, cy, cz, theta, hw, hh, hz) {
1722
+ this._ensureInit();
1723
+ if (typeof this._wasm.query_obb_frames !== 'function') {
1724
+ throw new Error(
1725
+ '[queryObbFrames] the active WASM binary predates the swept-OBB ABI ' +
1726
+ '(query_obb_frames). Rebuild the engine (make wasm / make wasm-node).'
1727
+ );
1728
+ }
1729
+ var json = this._wasm.query_obb_frames(cx, cy, cz, theta, hw, hh, hz);
1730
+ if (!json) return [];
1731
+ var rawFrames;
1732
+ try {
1733
+ rawFrames = JSON.parse(json);
1734
+ } catch (_e) {
1735
+ return [];
1736
+ }
1737
+ if (!Array.isArray(rawFrames) || rawFrames.length === 0) return [];
1738
+ var out = [];
1739
+ for (var k = 0; k < rawFrames.length; k++) {
1740
+ out.push(_wrapRawFrame(rawFrames[k], this.tick));
1741
+ }
1742
+ return out;
1743
+ };
1744
+
1745
+ /**
1746
+ * Enqueue a one-time batch of property mutations to apply at the top of the
1747
+ * next tick (Phase A, before move) — the EXTENSIBLE deferred-write path.
1748
+ *
1749
+ * FAIL-LOUD: throws on malformed entry, unknown/forbidden/readonly prop.
1750
+ * Does NOT silently drop mutations (stricter than inject_frames).
1751
+ *
1752
+ * Batch entries: { id: quantum_id, prop: string, value: * }
1753
+ * id — frame.id (= quantum_id, FrameData.id from doTick())
1754
+ * prop — writable Frame property name (§2.3 Frame property table)
1755
+ * value — new value (type follows §2.3 table)
1756
+ *
1757
+ * @param {Array<{id:number, prop:string, value:*}>} batch
1758
+ * @throws {TypeError} — malformed entry, forbidden/readonly/unknown prop
1759
+ */
1760
+ EGPTUniverse.prototype.injectMutations = function (batch) {
1761
+ this._ensureInit();
1762
+
1763
+ if (!Array.isArray(batch)) {
1764
+ throw new TypeError('[injectMutations] batch must be an array, got: ' + typeof batch);
1765
+ }
1766
+
1767
+ // Schema-driven pre-validation — fail before any enqueue. The Frame scope row
1768
+ // (objects/schema/scopes.js) is a static import, so it is always present.
1769
+ var descriptor = ObjectSchema.get('Frame');
1770
+
1771
+ for (var i = 0; i < batch.length; i++) {
1772
+ var entry = batch[i];
1773
+ if (entry === null || typeof entry !== 'object' ||
1774
+ typeof entry.id !== 'number' ||
1775
+ typeof entry.prop !== 'string' ||
1776
+ !('value' in entry)) {
1777
+ throw new TypeError(
1778
+ '[injectMutations] Malformed entry at index ' + i +
1779
+ ': expected { id: number, prop: string, value: * }, got ' +
1780
+ JSON.stringify(entry)
1781
+ );
1782
+ }
1783
+ // Schema guard — validates classification before the WASM boundary.
1784
+ var fieldDef = null;
1785
+ for (var fi = 0; fi < descriptor.fields.length; fi++) {
1786
+ if (descriptor.fields[fi].name === entry.prop) { fieldDef = descriptor.fields[fi]; break; }
1787
+ }
1788
+ if (!fieldDef) {
1789
+ throw new TypeError(
1790
+ '[injectMutations] Unknown property "' + entry.prop +
1791
+ '" for kind "Frame" (entry index ' + i + '). ' +
1792
+ 'Valid properties: ' + descriptor.fields.map(function(f){return f.name;}).join(', ')
1793
+ );
1794
+ }
1795
+ if (fieldDef.type === 'forbidden') {
1796
+ throw new TypeError(
1797
+ '[injectMutations] Property "' + entry.prop +
1798
+ '" is engine-owned topology — forbidden (entry index ' + i + '). ' +
1799
+ 'Set position (x/y) and let the engine reconcile topology next tick.'
1800
+ );
1801
+ }
1802
+ if (fieldDef.type === 'readonly') {
1803
+ throw new TypeError(
1804
+ '[injectMutations] Property "' + entry.prop +
1805
+ '" is read-only (entry index ' + i + '). ' +
1806
+ 'Use a writable property to change it indirectly.'
1807
+ );
1808
+ }
1809
+ }
1810
+
1811
+ // Pack the batch as JSON for the WASM ABI.
1812
+ // wire format: [{ id, prop, value }, ...]
1813
+ // value is JSON-serialized to match set_frame_property's json_value ABI.
1814
+ var wasmBatch = [];
1815
+ for (var j = 0; j < batch.length; j++) {
1816
+ var e = batch[j];
1817
+ var wasmValue = e.value;
1818
+ // Wire-format translation for charge (SCREAMING_SNAKE → integer 0/1).
1819
+ if (e.prop === 'charge') {
1820
+ if (wasmValue === 'POSITIVE' || wasmValue === 'Positive') wasmValue = 0;
1821
+ else if (wasmValue === 'NEGATIVE' || wasmValue === 'Negative') wasmValue = 1;
1822
+ } else if (e.prop === 'data') {
1823
+ // data bubble: already an object; pass through (Rust will receive it as JSON).
1824
+ wasmValue = (wasmValue !== null && wasmValue !== undefined) ? wasmValue : null;
1825
+ }
1826
+ wasmBatch.push({ id: e.id, prop: e.prop, value: wasmValue });
1827
+ }
1828
+ // Delegate to WASM ABI.
1829
+ this._wasm.inject_mutations(JSON.stringify(wasmBatch));
1830
+ };
1831
+
1832
+ // [D4] Methods present in node copy only; absent in browser copy — included here.
1833
+
1834
+ EGPTUniverse.prototype.killFrame = function (id) {
1835
+ this._ensureInit();
1836
+ this._wasm.kill_frame(id);
1837
+ };
1838
+
1839
+ EGPTUniverse.prototype.getFramePosition = function (id) {
1840
+ this._ensureInit();
1841
+ var pos = this._wasm.get_frame_position(id);
1842
+ if (pos.length === 2) {
1843
+ return { x: pos[0], y: pos[1] };
1844
+ }
1845
+ return null;
1846
+ };
1847
+
1848
+ /**
1849
+ * Get a property of a frame by ID and property name.
1850
+ *
1851
+ * Schema-driven: reads the Frame descriptor from ObjectSchema to validate prop names.
1852
+ * Throws TypeError for unknown properties.
1853
+ *
1854
+ * Returns the live frame property value at call time (reflects last-tick state).
1855
+ *
1856
+ * @param {number} id — frame ID (quantum_id / FrameData.id)
1857
+ * @param {string} prop — property name (from §2.3 Frame property table)
1858
+ * @returns {*} — property value, or null if frame not found
1859
+ * @throws {TypeError} — unknown property name
1860
+ */
1861
+ EGPTUniverse.prototype.getFrameProperty = function (id, prop) {
1862
+ this._ensureInit();
1863
+
1864
+ // Schema validation — driven off the canonical FrameDescriptor table.
1865
+ // The Frame scope row (objects/schema/scopes.js) — a static import, so it is always present.
1866
+ var descriptor = ObjectSchema.get('Frame');
1867
+ var fieldDef = null;
1868
+ for (var i = 0; i < descriptor.fields.length; i++) {
1869
+ if (descriptor.fields[i].name === prop) { fieldDef = descriptor.fields[i]; break; }
1870
+ }
1871
+ if (!fieldDef) {
1872
+ throw new TypeError(
1873
+ '[getFrameProperty] Unknown property "' + prop + '" for kind "Frame". ' +
1874
+ 'Valid properties: ' + descriptor.fields.map(function(f){return f.name;}).join(', ')
1875
+ );
1876
+ }
1877
+
1878
+ // Delegate to the WASM ABI.
1879
+ var result = this._wasm.get_frame_property(id, prop);
1880
+ // get_frame_property returns: null string for not-found, JSON-encoded value otherwise.
1881
+ if (result === null || result === undefined) return null;
1882
+ if (typeof result === 'string') {
1883
+ // WASM returns JSON-encoded for complex values; primitives are passed through.
1884
+ try { return JSON.parse(result); } catch (_) { return result; }
1885
+ }
1886
+ return result;
1887
+ };
1888
+
1889
+ /**
1890
+ * Set a property of a frame by ID and property name.
1891
+ *
1892
+ * THROW-OR-APPLY: every writable prop reaches the engine or throws.
1893
+ * NO silent no-op — Frames have no JS-wrapper upstream; engine owns them.
1894
+ *
1895
+ * Forbidden topology props (parent, children): FAIL-LOUD TypeError always.
1896
+ * Derived read-only props: FAIL-LOUD TypeError always.
1897
+ * Unknown props: FAIL-LOUD TypeError always.
1898
+ *
1899
+ * @param {number} id — frame ID (quantum_id / FrameData.id)
1900
+ * @param {string} prop — property name (from §2.3 Frame property table)
1901
+ * @param {*} value — new value
1902
+ * @throws {TypeError} — forbidden / readonly / unknown property
1903
+ */
1904
+ EGPTUniverse.prototype.setFrameProperty = function (id, prop, value) {
1905
+ this._ensureInit();
1906
+
1907
+ // Schema validation — driven off the canonical FrameDescriptor table.
1908
+ // The Frame scope row (objects/schema/scopes.js) — a static import, so it is always present.
1909
+ var descriptor = ObjectSchema.get('Frame');
1910
+ var fieldDef = null;
1911
+ for (var i = 0; i < descriptor.fields.length; i++) {
1912
+ if (descriptor.fields[i].name === prop) { fieldDef = descriptor.fields[i]; break; }
1913
+ }
1914
+ if (!fieldDef) {
1915
+ throw new TypeError(
1916
+ '[setFrameProperty] Unknown property "' + prop + '" for kind "Frame". ' +
1917
+ 'Valid properties: ' + descriptor.fields.map(function(f){return f.name;}).join(', ')
1918
+ );
1919
+ }
1920
+ if (fieldDef.type === 'forbidden') {
1921
+ throw new TypeError(
1922
+ '[setFrameProperty] Property "' + prop + '" is engine-owned topology — ' +
1923
+ 'structure is engine-derived. Set position and let the next tick reconcile.'
1924
+ );
1925
+ }
1926
+ if (fieldDef.type === 'readonly') {
1927
+ throw new TypeError(
1928
+ '[setFrameProperty] Property "' + prop + '" is read-only (derived: ' + prop + ').'
1929
+ );
1930
+ }
1931
+
1932
+ // Wire-format translation: charge canonical SCREAMING_SNAKE → Rust enum integer.
1933
+ // data bubble: JSON-encode for WASM ABI.
1934
+ var wasmValue = value;
1935
+ if (prop === 'charge') {
1936
+ if (value === 'POSITIVE' || value === 'Positive') {
1937
+ wasmValue = 0;
1938
+ } else if (value === 'NEGATIVE' || value === 'Negative') {
1939
+ wasmValue = 1;
1940
+ } else if (typeof value !== 'number') {
1941
+ throw new TypeError('[setFrameProperty] Invalid charge value: ' + JSON.stringify(value) +
1942
+ '. Use "POSITIVE" or "NEGATIVE".');
1943
+ }
1944
+ }
1945
+ // NOTE: 'data' is NOT pre-stringified here. Every value is JSON-encoded exactly ONCE below.
1946
+ // Pre-stringifying the bubble double-encodes it to a JSON string, so Rust's value.as_object()
1947
+ // sees a string and silently drops the write — a latent bug masked by a guarded FP10 assert.
1948
+
1949
+ // Delegate to WASM ABI.
1950
+ // set_frame_property(id, prop, json_value) — all values go as JSON strings for uniformity.
1951
+ var jsonVal = (wasmValue === null || wasmValue === undefined)
1952
+ ? 'null'
1953
+ : JSON.stringify(wasmValue);
1954
+ this._wasm.set_frame_property(id, prop, jsonVal);
1955
+ };
1956
+
1957
+ // ── Internal helpers ─────────────────────────────────────────────────
1958
+
1959
+
1960
+ // THE ONE raw-frame → canonical FrameData wrapper. Rust reports flat x/y/z (the CENTRE on all three
1961
+ // axes) + w/h, wrapped here as a centred Rectangle, and a data bubble whose string_map arrives as a
1962
+ // JS Map under serde_wasm_bindgen v0.6; normalize it to a plain object. Used by _wrapTickData for
1963
+ // BOTH frames[] and objects[].frames, and by queryRectFrames/queryObbFrames — no parallel DTO.
1964
+ function _wrapRawFrame(fd, defaultTick) {
1965
+ var rect = new Rectangle(
1966
+ fd.x != null ? fd.x : 0, fd.y != null ? fd.y : 0,
1967
+ fd.w != null ? fd.w : 1, fd.h != null ? fd.h : 1,
1968
+ null, true
1969
+ );
1970
+ var wasmData = (fd.data !== undefined && fd.data !== null) ? fd.data : null;
1971
+ if (wasmData && wasmData.string_map instanceof Map) {
1972
+ var smObj = {};
1973
+ wasmData.string_map.forEach(function (v, k) { smObj[k] = v; });
1974
+ wasmData = Object.assign({}, wasmData, { string_map: smObj });
1975
+ }
1976
+ return new FrameData(
1977
+ fd.tick != null ? fd.tick : defaultTick,
1978
+ fd.id != null ? fd.id : 0,
1979
+ fd.layer != null ? fd.layer : 0,
1980
+ rect,
1981
+ fd.fullness != null ? fd.fullness : 0,
1982
+ fd.is_alive !== false,
1983
+ fd.child_count != null ? fd.child_count : 0,
1984
+ fd.mass != null ? fd.mass : 1,
1985
+ fd.collided_id != null ? fd.collided_id : -1,
1986
+ fd.vx != null ? fd.vx : 0,
1987
+ fd.vy != null ? fd.vy : 0,
1988
+ fd.z != null ? fd.z : 0,
1989
+ fd.vz != null ? fd.vz : 0,
1990
+ wasmData
1991
+ );
1992
+ }
1993
+
1994
+ // THE ONE engine-bounds → JS bag normalizer, for BOTH UniverseData and DimensionData.
1995
+ //
1996
+ // The ENGINE owns the world's bounds and emits them as centre + extent on all three axes
1997
+ // (`tick_data.rs::UniverseData::boundary_rect` / `DimensionData::boundary_rect`, a 6-tuple
1998
+ // `(x,y,z,w,h,d)`). This reads that tuple rather than re-deriving from the JS-side
1999
+ // `universe_rect`, which is the CONFIG rect: it carries no z, and `UniverseConfig::depth`
2000
+ // decouples the world's depth from its width, so the shim cannot compute the z extent —
2001
+ // it has to be TOLD it. Frames already report `z`; without this a consumer receives depth
2002
+ // coordinates with no frame of reference for them.
2003
+ //
2004
+ // The six derived edges are the same ones the engine's own `Rectangle` publishes, so a
2005
+ // consumer reading `left`/`top`/`right`/`bottom` sees exactly what it always did and gains
2006
+ // `z`/`d`/`near`/`far` alongside.
2007
+ function _wrapBoundaryRect(t) {
2008
+ if (!Array.isArray(t) || t.length !== 6) {
2009
+ throw new Error(
2010
+ 'wasm-engine: TickData boundary_rect must be the engine\'s 6-tuple ' +
2011
+ '(x, y, z, w, h, d) — got ' + JSON.stringify(t) + '. The world bounds are owned ' +
2012
+ 'by the engine (tick_data.rs UniverseData/DimensionData::boundary_rect); the shim ' +
2013
+ 'ferries them and never re-derives them from the JS-side config rect.'
2014
+ );
2015
+ }
2016
+ var x = t[0], y = t[1], z = t[2], w = t[3], h = t[4], d = t[5];
2017
+ return {
2018
+ x: x, y: y, z: z, w: w, h: h, d: d,
2019
+ left: x - w / 2, right: x + w / 2,
2020
+ top: y - h / 2, bottom: y + h / 2,
2021
+ near: z - d / 2, far: z + d / 2,
2022
+ r: w / 2
2023
+ };
2024
+ }
2025
+
2026
+ EGPTUniverse.prototype._wrapTickData = function (raw) {
2027
+ var ud = new UniverseData(
2028
+ raw.universe != null ? raw.universe.tick : this.tick,
2029
+ raw.universe != null ? raw.universe.dimension_count : 0,
2030
+ _wrapBoundaryRect(raw.universe && raw.universe.boundary_rect)
2031
+ );
2032
+ var td = new TickData(ud);
2033
+
2034
+ if (raw.dimensions) {
2035
+ for (var i = 0; i < raw.dimensions.length; i++) {
2036
+ var dd = raw.dimensions[i];
2037
+ td.addDimensionData(new DimensionData(
2038
+ dd.tick != null ? dd.tick : this.tick,
2039
+ dd.layer != null ? dd.layer : 0,
2040
+ dd.frame_count != null ? dd.frame_count : 0,
2041
+ _wrapBoundaryRect(dd.boundary_rect)
2042
+ ));
2043
+ }
2044
+ }
2045
+
2046
+ if (raw.frames) {
2047
+ for (var j = 0; j < raw.frames.length; j++) {
2048
+ td.addFrameData(_wrapRawFrame(raw.frames[j], this.tick));
2049
+ }
2050
+ }
2051
+
2052
+ // SLICE 3 — B2: populate objects[] from raw.objects (populated by Rust Phase D COLLECT).
2053
+ // Handle order: 0, 1, 2, ... matches LargeObjectSpec Vec index (same as JS).
2054
+ // B2.1 — ONE read authority: user space reads count from tickData.objects[handle].count.
2055
+ // get_large_object_count() (WASM pull) is DELETED (Dispatch C2, 2026-06-21) — this
2056
+ // section is the sole marshalling path for count (§10.8 B2.1).
2057
+ // B2.2 — shape { handle, count } is the canonical key list owned by ObjectCollectedState.
2058
+ if (raw.objects && Array.isArray(raw.objects) && raw.objects.length > 0) {
2059
+ if (!td.objects) { td.objects = []; }
2060
+ var _wrapTick = this.tick;
2061
+ for (var k = 0; k < raw.objects.length; k++) {
2062
+ var oc = raw.objects[k];
2063
+ // B2.2 — keys { handle, count, frames }. `frames` = the LO's per-tick collected
2064
+ // frames (TickData.objects[handle].frames), marshalled through the ONE wrapper —
2065
+ // the same FrameData shape as TickData.frames. Replaces the retired getCollidedFrames
2066
+ // query: the engine emits the collected frames, it does not answer per-LO queries.
2067
+ td.objects.push({
2068
+ handle: oc.handle != null ? oc.handle : k,
2069
+ count: oc.count != null ? Number(oc.count) : 0,
2070
+ frames: Array.isArray(oc.frames)
2071
+ ? oc.frames.map(function (f) { return _wrapRawFrame(f, _wrapTick); })
2072
+ : []
2073
+ });
2074
+ }
2075
+ }
2076
+
2077
+ return td;
2078
+ };
2079
+
2080
+ // ── SLICE 3 B1: CRUD Delete on WASM side ──────────────────────────────────
2081
+
2082
+ /**
2083
+ * Unregister a large object by handle (SLICE 3, B1).
2084
+ *
2085
+ * Gate-2 binding (§10.8 B1):
2086
+ * B1.1 — FAIL-LOUD on unknown handle: throws (propagates Rust Err → JsValue).
2087
+ * B1.2 — Reuses the ONE integer handle table; handle slot is tombstoned.
2088
+ * B1.3 — Must be called between ticks.
2089
+ *
2090
+ * Mirror: Rust universe.rs unregister_large_object / JS reference unregisterLargeObject.
2091
+ *
2092
+ * @param {number} handle — opaque handle from registerLargeObject()
2093
+ * @throws {Error|TypeError} — if handle is out of range or already tombstoned
2094
+ */
2095
+ EGPTUniverse.prototype.unregisterLargeObject = function (handle) {
2096
+ this._ensureInit();
2097
+ this._wasm.unregister_large_object(handle);
2098
+ // ONE teardown call leaves BOTH censuses: the engine's registry above and the JS
2099
+ // wrapper census the renderer derives its draw list from. A wrapper that outlived
2100
+ // its engine registration would draw a body that no longer collides.
2101
+ for (var i = this.largeObjects.length - 1; i >= 0; i--) {
2102
+ if (this.largeObjects[i] && this.largeObjects[i].handle === handle) {
2103
+ this.largeObjects.splice(i, 1);
2104
+ }
2105
+ }
2106
+ var h = this._loHandles.indexOf(handle);
2107
+ if (h !== -1) { this._loHandles.splice(h, 1); }
2108
+ };
2109
+
2110
+ /**
2111
+ * The LIVE engine registrations on this world that carry NO JS wrapper — i.e. the
2112
+ * objects a renderer deriving its draw list from `largeObjects` cannot see.
2113
+ *
2114
+ * ── Why this is the divergence oracle, and why `tickData.objects[]` is not ──
2115
+ * The collected `objects[]` array keeps a slot for an UNREGISTERED object (the handle
2116
+ * space never compacts), so it cannot tell a live object from a torn-down one and
2117
+ * would flag every disposed wall forever. This world owns BOTH ledgers — the handle
2118
+ * table it hands out and the wrapper census constructed against it — so it is the one
2119
+ * place that can answer honestly.
2120
+ *
2121
+ * The raw `registerLargeObject(spec)` path is legitimate for a headless conformance
2122
+ * harness (there is no renderer to be absent from). In a RENDERED world it is exactly
2123
+ * the silent-invisible case: a body that collides and counts correctly and never
2124
+ * appears in a picture. The host asserts on this before it draws.
2125
+ *
2126
+ * @returns {number[]} live handles with no wrapper, in registration order.
2127
+ */
2128
+ EGPTUniverse.prototype.unwrappedLargeObjectHandles = function () {
2129
+ var wrapped = Object.create(null), i;
2130
+ for (i = 0; i < this.largeObjects.length; i++) {
2131
+ if (this.largeObjects[i]) { wrapped[this.largeObjects[i].handle] = true; }
2132
+ }
2133
+ var out = [];
2134
+ for (i = 0; i < this._loHandles.length; i++) {
2135
+ if (!wrapped[this._loHandles[i]]) { out.push(this._loHandles[i]); }
2136
+ }
2137
+ return out;
2138
+ };
2139
+
2140
+
2141
+ // ── Boundary Frame (standard interface coverage) ───────────────────
2142
+ //
2143
+ // The standard JS-reference interface exposes Frame.makeLeafFrame and instance
2144
+ // methods (insert, addPoints, etc.) that experiment code calls directly.
2145
+ //
2146
+ // On the WASM path these are NOT implemented in Rust. The boundary Frame surfaces
2147
+ // every JS-reference Frame method: static makeLeafFrame forwards or logs+null;
2148
+ // instance methods (insert, addPoints, etc.) do the same. This lets experiments use
2149
+ // the standard interface on both engines — WASM degrades gracefully (log+null) rather
2150
+ // than crashing with "undefined is not a function".
2151
+
2152
+ function BoundaryFrame() {
2153
+ // Boundary instance — represents a Frame-shaped null returned for WASM-missing constructors.
2154
+ this.is_alive = false;
2155
+ this.is_leaf = false;
2156
+ this.children = [];
2157
+ this.mass = 0;
2158
+ this.layer = 0;
2159
+ this.rect = null;
2160
+ this.name = null;
2161
+ this._isBoundaryNull = true; // sentinel: callers can detect the shim null
2162
+ }
2163
+
2164
+ BoundaryFrame.prototype.insert = function (quantum) {
2165
+ if (_activeUniverse && this._handle !== undefined && quantum && quantum._handle !== undefined) {
2166
+ return _activeUniverse._wasm.insert_into_frame(this._handle, quantum._handle);
2167
+ }
2168
+ console.log('[shim] Frame.insert: missing handle(s) or no active WASM universe — returning null');
2169
+ return null;
2170
+ };
2171
+ BoundaryFrame.prototype.addPoints = function (quanta) {
2172
+ console.log('[shim] Frame.addPoints not implemented in WASM engine — returning null');
2173
+ return null;
2174
+ };
2175
+ BoundaryFrame.prototype.move = function (t, boundary_rect) {
2176
+ console.log('[shim] Frame.move not implemented in WASM engine — returning null');
2177
+ return null;
2178
+ };
2179
+ BoundaryFrame.prototype.compress = function () {
2180
+ console.log('[shim] Frame.compress not implemented in WASM engine — returning null');
2181
+ return null;
2182
+ };
2183
+ BoundaryFrame.prototype.reinit = function () {
2184
+ console.log('[shim] Frame.reinit not implemented in WASM engine — returning null');
2185
+ return null;
2186
+ };
2187
+ BoundaryFrame.prototype.kill = function () {
2188
+ console.log('[shim] Frame.kill not implemented in WASM engine — returning null');
2189
+ return null;
2190
+ };
2191
+ BoundaryFrame.prototype.reset = function () {
2192
+ console.log('[shim] Frame.reset not implemented in WASM engine — returning null');
2193
+ return null;
2194
+ };
2195
+ BoundaryFrame.prototype.toFrameData = function () {
2196
+ console.log('[shim] Frame.toFrameData not implemented in WASM engine — returning null');
2197
+ return null;
2198
+ };
2199
+ BoundaryFrame.prototype.checkCollision = function () {
2200
+ console.log('[shim] Frame.checkCollision not implemented in WASM engine — returning null');
2201
+ return null;
2202
+ };
2203
+ BoundaryFrame.prototype.collide = function () {
2204
+ console.log('[shim] Frame.collide not implemented in WASM engine — returning null');
2205
+ return null;
2206
+ };
2207
+ BoundaryFrame.prototype.merge = function () {
2208
+ console.log('[shim] Frame.merge not implemented in WASM engine — returning null');
2209
+ return null;
2210
+ };
2211
+ BoundaryFrame.prototype.bond = function () {
2212
+ console.log('[shim] Frame.bond not implemented in WASM engine — returning null');
2213
+ return null;
2214
+ };
2215
+ BoundaryFrame.prototype.bounce = function () {
2216
+ console.log('[shim] Frame.bounce not implemented in WASM engine — returning null');
2217
+ return null;
2218
+ };
2219
+ BoundaryFrame.prototype.boundary_check = function () {
2220
+ console.log('[shim] Frame.boundary_check not implemented in WASM engine — returning null');
2221
+ return null;
2222
+ };
2223
+ BoundaryFrame.prototype.setWavelength = function () {
2224
+ console.log('[shim] Frame.setWavelength not implemented in WASM engine — returning null');
2225
+ return null;
2226
+ };
2227
+
2228
+ /**
2229
+ * Static makeLeafFrame — matches JS-reference Frame.makeLeafFrame signature.
2230
+ * Routes through the active universe's makeLeafFrame (THE one leaf door, which owns the
2231
+ * charge/wavelength translation and the liveQuantaCap admission). Returns the BoundaryFrame
2232
+ * null marker when no universe is initialised yet OR when the cap refused the add.
2233
+ *
2234
+ * charge: 0=Positive, 1=Negative, null/undefined=random.
2235
+ * wavelength: 0 or negative means "none" on the Rust side (mass = capacity).
2236
+ * `boundary_rect` and `color_override` are NOT forwarded: the instance door's 9th positional
2237
+ * is `color`, so the positional list must never be passed through as-is.
2238
+ *
2239
+ * The JS reference took a trailing frame-generation-mode flag here; it was never read by
2240
+ * this body (the mode is universe state the engine already holds) and is DROPPED. Extra
2241
+ * arguments from an old call site are ignored by JS, so no caller changes.
2242
+ */
2243
+ BoundaryFrame.makeLeafFrame = function (left, top, level, wavelength, vx, vy, wrap, charge, boundary_rect, color_override) {
2244
+ if (!_activeUniverse) {
2245
+ console.log('[shim] Frame.makeLeafFrame: no active WASM universe — returning null');
2246
+ var f0 = new BoundaryFrame();
2247
+ f0.layer = level || 0;
2248
+ return f0;
2249
+ }
2250
+ var frameId = _activeUniverse.makeLeafFrame(left, top, level, wavelength, vx, vy, wrap, charge);
2251
+ if (frameId === null) {
2252
+ var fc = new BoundaryFrame();
2253
+ fc.layer = level || 0;
2254
+ return fc;
2255
+ }
2256
+ var f = new BoundaryFrame();
2257
+ f._handle = frameId;
2258
+ f.layer = level || 0;
2259
+ f.is_leaf = true;
2260
+ f.is_alive = true;
2261
+ f._isBoundaryNull = false;
2262
+ return f;
2263
+ };
2264
+
2265
+ // ── Boundary Dimension (standard interface coverage) ─────────────────
2266
+ //
2267
+ // JS-reference Dimension exposes mb_w, mb_h, addFrame, createFrameFromRect,
2268
+ // createParentFrame, handleStructurePromotion, frames,
2269
+ // layer, isHidden, and more. The boundary Dimension surfaces every method:
2270
+ // for properties computable locally (mb_w, mb_h) values are derived from layer;
2271
+ // for methods not implemented in WASM: log+null; the WASM doTick() still works.
2272
+
2273
+ function _makeBoundaryDimension(layer, wasm) {
2274
+ var mb_w = Math.pow(2, layer);
2275
+ var mb_h = mb_w;
2276
+ var _isHidden = false;
2277
+
2278
+ var dim = {
2279
+ layer: layer,
2280
+ mb_w: mb_w,
2281
+ mb_h: mb_h,
2282
+ frames: [], // not populated by WASM (WASM owns its own frame list)
2283
+
2284
+ // addFrame — forwards to Rust add_frame_to_dimension when a handle is present
2285
+ addFrame: function (mb) {
2286
+ var w = wasm || (_activeUniverse && _activeUniverse._wasm);
2287
+ if (w && mb && mb._handle !== undefined) {
2288
+ w.add_frame_to_dimension(layer, mb._handle);
2289
+ return;
2290
+ }
2291
+ console.log('[shim] Dimension.addFrame: missing handle or no active WASM universe — returning null');
2292
+ return null;
2293
+ },
2294
+
2295
+ // createFrameFromRect — forwards to Rust create_frame_from_rect
2296
+ // Returns a BoundaryFrame with _handle set, or a null-sentinel on failure.
2297
+ createFrameFromRect: function (rect, childFrames, vx, vy, addToDimension, is_stationary, string_map) {
2298
+ var w = wasm || (_activeUniverse && _activeUniverse._wasm);
2299
+ if (w && rect) {
2300
+ var childHandles = (childFrames || []).map(function (c) {
2301
+ return (c && c._handle !== undefined) ? c._handle : 0;
2302
+ });
2303
+ var rectArr = [rect.left || 0, rect.top || 0, rect.w || mb_w, rect.h || mb_h];
2304
+ // usize::MAX (returned on failure) is representable as a JS number
2305
+ // only approximately; use a large-enough threshold instead.
2306
+ var INVALID = 4294967295; // 0xFFFF_FFFF — frame counts will never reach this
2307
+ var frameId = w.create_frame_from_rect(
2308
+ layer, rectArr, childHandles,
2309
+ vx || 0, vy || 0,
2310
+ addToDimension !== false, !!is_stationary
2311
+ );
2312
+ if (frameId >= INVALID) {
2313
+ console.log('[shim] Dimension.createFrameFromRect: Rust returned INVALID — returning sentinel');
2314
+ return new BoundaryFrame();
2315
+ }
2316
+ var f = new BoundaryFrame();
2317
+ f._handle = frameId;
2318
+ f.layer = layer;
2319
+ f.is_alive = true;
2320
+ f._isBoundaryNull = false;
2321
+ // Opaque user-space render hints ride the generic string_map (e.g.
2322
+ // {'render.outline':'true'}). Written via the generic 'data' arm (Rust
2323
+ // set_frame_property → string_map insert); the engine carries it, the renderer
2324
+ // deserializes. set_frame_property resolves by quantum_id (find_by_quantum_id),
2325
+ // but create_frame_from_rect returns the SoA HANDLE — convert via
2326
+ // get_frame_quantum_id first (same as the emitter color stamp).
2327
+ if (string_map && typeof w.get_frame_quantum_id === 'function') {
2328
+ var fqid = w.get_frame_quantum_id(frameId);
2329
+ // MERGE (get→merge→set) via the SAME render-intent seam (Q-A3 (a):
2330
+ // this stamp previously replace-wrote {string_map}).
2331
+ if (fqid > 0) { _stampRenderIntentMerge(w, fqid, string_map); }
2332
+ }
2333
+ return f;
2334
+ }
2335
+ // G6: createFrameFromRect IS implemented on WASM (forwards to Rust
2336
+ // create_frame_from_rect, above) — composite experiments (oscillator, atomic-model)
2337
+ // are realm-verified rendering on ?engine=wasm. This branch is only the
2338
+ // no-active-WASM-universe fallback.
2339
+ console.log('[shim] Dimension.createFrameFromRect: no active WASM universe — returning sentinel');
2340
+ // Return a BoundaryFrame sentinel (not JS null) so callers can safely set
2341
+ // properties (e.g. frame.name = '...') without crashing.
2342
+ return new BoundaryFrame();
2343
+ },
2344
+
2345
+ // createParentFrame — forwards to Rust create_parent_frame
2346
+ createParentFrame: function (childA, childB) {
2347
+ var w = wasm || (_activeUniverse && _activeUniverse._wasm);
2348
+ if (w && childA && childA._handle !== undefined && childB && childB._handle !== undefined) {
2349
+ var INVALID = 4294967295;
2350
+ var frameId = w.create_parent_frame(layer, childA._handle, childB._handle);
2351
+ if (frameId >= INVALID) {
2352
+ console.log('[shim] Dimension.createParentFrame: Rust returned INVALID — returning sentinel');
2353
+ return new BoundaryFrame();
2354
+ }
2355
+ var f = new BoundaryFrame();
2356
+ f._handle = frameId;
2357
+ f.layer = layer;
2358
+ f.is_alive = true;
2359
+ f._isBoundaryNull = false;
2360
+ return f;
2361
+ }
2362
+ console.log('[shim] Dimension.createParentFrame: missing handle(s) or no active WASM universe — returning null');
2363
+ return new BoundaryFrame();
2364
+ },
2365
+
2366
+ // handleStructurePromotion — not in WASM; log+null
2367
+ handleStructurePromotion: function (frameA, frameB) {
2368
+ console.log('[shim] Dimension.handleStructurePromotion not implemented in WASM engine — returning null');
2369
+ return null;
2370
+ },
2371
+
2372
+ // generateFrames — not in WASM; log+null
2373
+ generateFrames: function (qtree) {
2374
+ console.log('[shim] Dimension.generateFrames not implemented in WASM engine — returning null');
2375
+ return null;
2376
+ },
2377
+
2378
+ // addPointsFromQuadTree — not in WASM; log+null
2379
+ addPointsFromQuadTree: function (qtree) {
2380
+ console.log('[shim] Dimension.addPointsFromQuadTree not implemented in WASM engine — returning null');
2381
+ return null;
2382
+ },
2383
+
2384
+ // releasePoints — not in WASM; log+null
2385
+ releasePoints: function (quanta) {
2386
+ console.log('[shim] Dimension.releasePoints not implemented in WASM engine — returning null');
2387
+ return null;
2388
+ },
2389
+
2390
+ // reset — not in WASM; log+null
2391
+ reset: function () {
2392
+ console.log('[shim] Dimension.reset not implemented in WASM engine — returning null');
2393
+ return null;
2394
+ },
2395
+
2396
+ // update — not in WASM (driven internally by doTick); log+null
2397
+ update: function (t, large_object_list) {
2398
+ console.log('[shim] Dimension.update not implemented in WASM engine — returning null');
2399
+ return null;
2400
+ },
2401
+
2402
+ // collectFrameData — not in WASM (WASM produces its own TickData); log+null
2403
+ collectFrameData: function (t) {
2404
+ console.log('[shim] Dimension.collectFrameData not implemented in WASM engine — returning null');
2405
+ return null;
2406
+ },
2407
+
2408
+ // getMassForColor / getWavelengthForColor — not in WASM; log+null
2409
+ getMassForColor: function (colorName) {
2410
+ console.log('[shim] Dimension.getMassForColor not implemented in WASM engine — returning null');
2411
+ return null;
2412
+ },
2413
+ getWavelengthForColor: function (colorName) {
2414
+ console.log('[shim] Dimension.getWavelengthForColor not implemented in WASM engine — returning null');
2415
+ return null;
2416
+ },
2417
+
2418
+ // alignMBtoDimGrid — not in WASM; log+null
2419
+ alignMBtoDimGrid: function (mb_rect) {
2420
+ console.log('[shim] Dimension.alignMBtoDimGrid not implemented in WASM engine — returning null');
2421
+ return null;
2422
+ },
2423
+
2424
+ // getQtreeFromFrames — not in WASM; log+null
2425
+ getQtreeFromFrames: function () {
2426
+ console.log('[shim] Dimension.getQtreeFromFrames not implemented in WASM engine — returning null');
2427
+ return null;
2428
+ }
2429
+ };
2430
+
2431
+ // showFrameOutlinesOnly RETIRED — outline is user-space data on the frame's string_map
2432
+ // ('render.outline'); the dimension has no render concept. isHidden remains settable.
2433
+ Object.defineProperty(dim, 'isHidden', {
2434
+ get: function () { return _isHidden; },
2435
+ set: function (v) { _isHidden = v; },
2436
+ enumerable: true, configurable: true
2437
+ });
2438
+
2439
+ return dim;
2440
+ }
2441
+
2442
+ // ── makeProvider factory ─────────────────────────────────────────────
2443
+ //
2444
+ // The env shim calls makeProvider(WasmUniverseCtor) after resolving the WASM
2445
+ // module. The returned provider object is the EngineHandle exposed to callers.
2446
+ //
2447
+ // _WasmUniverse is a file-scoped variable shared by the EGPTUniverse methods
2448
+ // above (via closure). makeProvider sets it exactly once.
2449
+
2450
+ var _WasmUniverse = null;
2451
+ // Set during EGPTUniverse.prototype.init() so that BoundaryFrame static/instance
2452
+ // methods can forward to the live universe (and its WasmUniverse) without holding an
2453
+ // explicit ref.
2454
+ var _activeUniverse = null;
2455
+
2456
+ function makeProvider(WasmUniverseCtor) {
2457
+ if (!WasmUniverseCtor) {
2458
+ throw new Error('[wasm-engine] WasmUniverse constructor is required.');
2459
+ }
2460
+ _WasmUniverse = WasmUniverseCtor;
2461
+
2462
+ return {
2463
+ backend: 'wasm',
2464
+ Rectangle: Rectangle,
2465
+ EGPTUniverse: EGPTUniverse,
2466
+ Frame: BoundaryFrame,
2467
+ TickData: TickData,
2468
+ FrameData: FrameData,
2469
+ DimensionData: DimensionData,
2470
+ UniverseData: UniverseData,
2471
+ LightColor: LightColor,
2472
+ Charge: Charge,
2473
+ // DELETED, FAIL LOUD: the two-argument `(canvasRect, opts)` provider factory
2474
+ // was a SECOND construction convention with its own defaults (it silently
2475
+ // defaulted seed to 0 where the config bag defaults 42). One authority now.
2476
+ createUniverse: function () {
2477
+ throw new Error(
2478
+ 'handle.createUniverse(canvasRect, opts) is DELETED — it was a second ' +
2479
+ 'construction convention with divergent defaults (seed 0, not 42). Use the ONE ' +
2480
+ 'construction authority: handle.EGPTUniverse.create({ rect: canvasRect, seed: … }). ' +
2481
+ '`rect` accepts the same { left, top, w, h } literal.'
2482
+ );
2483
+ },
2484
+ };
2485
+ }
2486
+
2487
+ // _stampRenderIntentMerge is exported for the merge-semantics unit test (mock-wasm, sweep-safe —
2488
+ // the real WASM color-persistence is verified via pkg-node, like all engine behavior).
2489
+ export {
2490
+ makeProvider,
2491
+ EGPTUniverse,
2492
+ TickData,
2493
+ FrameData,
2494
+ DimensionData,
2495
+ UniverseData,
2496
+ LightColor,
2497
+ Charge,
2498
+ _stampRenderIntentMerge
2499
+ };