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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/CLAUDE.md +87 -0
  2. package/README.md +84 -2
  3. package/dist/dist-manifest.json +218 -0
  4. package/dist/eyes/index.js +109 -0
  5. package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
  6. package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
  7. package/dist/fat/pkg-node/package.json +12 -0
  8. package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
  9. package/dist/frqtl/compat/boot-node.js +34 -0
  10. package/dist/frqtl/compat/engine.js +127 -0
  11. package/dist/frqtl/compat/package-root.js +24 -0
  12. package/dist/frqtl/compat/providers/wasm-engine.js +2501 -0
  13. package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
  14. package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
  15. package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
  16. package/dist/frqtl/compat/renderer/Filters.js +163 -0
  17. package/dist/frqtl/compat/renderer/RenderFamily.js +85 -0
  18. package/dist/frqtl/compat/renderer/RenderMesh.js +100 -0
  19. package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
  20. package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
  21. package/dist/frqtl/compat/renderer/captureStill-common.js +259 -0
  22. package/dist/frqtl/compat/renderer/display-slot.js +96 -0
  23. package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
  24. package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
  25. package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
  26. package/dist/frqtl/compat/renderer/providers/index.js +22 -0
  27. package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
  28. package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
  29. package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
  30. package/dist/frqtl/compat/renderer/providers/three-provider.js +2474 -0
  31. package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
  32. package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
  33. package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
  34. package/dist/frqtl/compat/vendor.js +196 -0
  35. package/dist/frqtl/node-entry.js +17 -0
  36. package/dist/frqtl/objects/FrqtlApp.js +2922 -0
  37. package/dist/frqtl/objects/ObjectFactory.js +297 -0
  38. package/dist/frqtl/objects/RecipeContext.js +322 -0
  39. package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
  40. package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
  41. package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
  42. package/dist/frqtl/objects/instruments/capture-host.js +453 -0
  43. package/dist/frqtl/objects/instruments/capture.js +711 -0
  44. package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -0
  45. package/dist/frqtl/objects/kinds-entry.js +14 -0
  46. package/dist/frqtl/objects/kinds.js +190 -0
  47. package/dist/frqtl/objects/objects/Box.js +273 -0
  48. package/dist/frqtl/objects/objects/Bullet.js +155 -0
  49. package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
  50. package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
  51. package/dist/frqtl/objects/objects/DirectedTunnel.js +412 -0
  52. package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
  53. package/dist/frqtl/objects/objects/LightSource.js +354 -0
  54. package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
  55. package/dist/frqtl/objects/objects/Tube.js +471 -0
  56. package/dist/frqtl/objects/objects/absorbRadiation.js +310 -0
  57. package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
  58. package/dist/frqtl/objects/objects/controlAction.js +89 -0
  59. package/dist/frqtl/objects/objects/controlBind.js +278 -0
  60. package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
  61. package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
  62. package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
  63. package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
  64. package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
  65. package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
  66. package/dist/frqtl/objects/objects/makeControlsForObject.js +824 -0
  67. package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
  68. package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
  69. package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
  70. package/dist/frqtl/objects/objects/tagger.js +228 -0
  71. package/dist/frqtl/objects/schema/engine-scopes.js +469 -0
  72. package/dist/frqtl/objects/schema/index.js +256 -0
  73. package/dist/frqtl/objects/schema/scopes.js +457 -0
  74. package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
  75. package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
  76. package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
  77. package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
  78. package/dist/frqtl/objects/setup/_worldInit.js +162 -0
  79. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
  80. package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
  81. package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
  82. package/dist/frqtl/objects/setup/setupConeEmitter3D.js +251 -0
  83. package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -0
  84. package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
  85. package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
  86. package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
  87. package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
  88. package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
  89. package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
  90. package/dist/frqtl/objects/setup/setupLightSourceLab.js +280 -0
  91. package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
  92. package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
  93. package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
  94. package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1259 -0
  95. package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
  96. package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
  97. package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
  98. package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
  99. package/dist/frqtl/objects/v1-replacements.js +30 -0
  100. package/dist/frqtl/package-core.js +23 -0
  101. package/dist/frqtl/parts/CameraControls.js +643 -0
  102. package/dist/frqtl/parts/CameraPath.js +575 -0
  103. package/dist/frqtl/parts/CollisionActions.js +35 -0
  104. package/dist/frqtl/parts/CollisionTypes.js +31 -0
  105. package/dist/frqtl/parts/DetectorWall.js +496 -0
  106. package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
  107. package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
  108. package/dist/frqtl/parts/FrqtlVision.js +818 -0
  109. package/dist/frqtl/parts/GraphingDetectorWall.js +490 -0
  110. package/dist/frqtl/parts/LargeObject.js +406 -0
  111. package/dist/frqtl/parts/LightColor.js +255 -0
  112. package/dist/frqtl/parts/LiveChart.js +285 -0
  113. package/dist/frqtl/parts/LogicGate.js +720 -0
  114. package/dist/frqtl/parts/OvenBox.js +612 -0
  115. package/dist/frqtl/parts/PointSource.js +763 -0
  116. package/dist/frqtl/parts/Rectangle.js +200 -0
  117. package/dist/frqtl/parts/SlitLightSource.js +234 -0
  118. package/dist/frqtl/parts/SlitScreen.js +628 -0
  119. package/dist/frqtl/parts/SphereEmitter.js +946 -0
  120. package/dist/frqtl/parts/Trace.js +508 -0
  121. package/dist/frqtl/parts/Tunnel.js +1057 -0
  122. package/dist/frqtl/parts/Wall.js +1211 -0
  123. package/dist/frqtl/parts/index.js +33 -0
  124. package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
  125. package/dist/frqtl/scenes/absorbing-wall.js +32 -0
  126. package/dist/frqtl/scenes/atomic-model.js +88 -0
  127. package/dist/frqtl/scenes/big-bang.js +46 -0
  128. package/dist/frqtl/scenes/blackbody.js +81 -0
  129. package/dist/frqtl/scenes/circuit-sat.js +127 -0
  130. package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
  131. package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
  132. package/dist/frqtl/scenes/counting-wall.js +38 -0
  133. package/dist/frqtl/scenes/double-slit.js +31 -0
  134. package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
  135. package/dist/frqtl/scenes/dse-one-slit.js +114 -0
  136. package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
  137. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +244 -0
  138. package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
  139. package/dist/frqtl/scenes/dse-two-slits.js +132 -0
  140. package/dist/frqtl/scenes/empty.js +14 -0
  141. package/dist/frqtl/scenes/laser-buildup.js +49 -0
  142. package/dist/frqtl/scenes/mirror-3d.js +56 -0
  143. package/dist/frqtl/scenes/mirror-route.js +140 -0
  144. package/dist/frqtl/scenes/mirror.js +38 -0
  145. package/dist/frqtl/scenes/omni-3d.js +27 -0
  146. package/dist/frqtl/scenes/omni-source.js +22 -0
  147. package/dist/frqtl/scenes/self-interference.js +39 -0
  148. package/dist/frqtl/scenes/smoking-gun.js +122 -0
  149. package/dist/frqtl/scenes/straight-walk.js +22 -0
  150. package/dist/frqtl/scenes/sun.js +39 -0
  151. package/dist/frqtl/scenes/two-source.js +37 -0
  152. package/dist/frqtl/scenes/wave.js +26 -0
  153. package/dist/shim/bundle/frqnb.js +112 -0
  154. package/dist/shim/bundle/index.js +121 -0
  155. package/dist/shim/bundle/indexHtml.js +107 -0
  156. package/dist/shim/bundle/manifest.js +95 -0
  157. package/dist/shim/bundle/zip.js +243 -0
  158. package/dist/shim/bus.js +67 -0
  159. package/dist/shim/controls/index.js +627 -0
  160. package/dist/shim/controls/registry.js +274 -0
  161. package/dist/shim/engine/artifact-shell.js +212 -0
  162. package/dist/shim/engine/boot-phases.js +44 -0
  163. package/dist/shim/engine/engine-stack-boot.js +59 -0
  164. package/dist/shim/engine/router.js +692 -0
  165. package/dist/shim/experiment-frame/index.js +296 -0
  166. package/dist/shim/expr/ast-schema.js +369 -0
  167. package/dist/shim/field-descriptor/index.js +326 -0
  168. package/dist/shim/frame-data/index.js +224 -0
  169. package/dist/shim/index.js +67 -0
  170. package/dist/shim/notebook/canvas-guard.js +82 -0
  171. package/dist/shim/notebook/catalog.js +35 -0
  172. package/dist/shim/notebook/format.js +736 -0
  173. package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
  174. package/dist/shim/notebook/kernel.js +672 -0
  175. package/dist/shim/notebook/knob.js +151 -0
  176. package/dist/shim/notebook/module-resolver.js +260 -0
  177. package/dist/shim/notebook/notebook-knobs.js +65 -0
  178. package/dist/shim/notebook/produced-bindings.js +67 -0
  179. package/dist/shim/notebook/realm-registry.js +121 -0
  180. package/dist/shim/notebook/realm-transport.js +73 -0
  181. package/dist/shim/notebook/setup-builder.js +510 -0
  182. package/dist/shim/notebook/store.js +159 -0
  183. package/dist/shim/notebook/vendor-importmap.js +134 -0
  184. package/dist/shim/notebook/view-builder.js +189 -0
  185. package/dist/shim/notebook/view-ctx.js +100 -0
  186. package/dist/shim/notebook/view-handle.js +202 -0
  187. package/dist/shim/notebook/warmup.js +102 -0
  188. package/dist/shim/package.json +4 -0
  189. package/dist/shim/page-params/index.js +214 -0
  190. package/dist/shim/project/identity.js +112 -0
  191. package/dist/shim/project/index.js +19 -0
  192. package/dist/shim/project/link.js +402 -0
  193. package/dist/shim/project/node-specifier-hook.mjs +35 -0
  194. package/dist/shim/project/register-node.mjs +17 -0
  195. package/dist/shim/project/specifiers.js +106 -0
  196. package/dist/shim/registry/copy-gate.js +110 -0
  197. package/dist/shim/registry/exhibits.js +455 -0
  198. package/dist/shim/registry/experiments.js +215 -0
  199. package/dist/shim/registry/index.js +19 -0
  200. package/dist/shim/registry/scene-source.js +157 -0
  201. package/dist/shim/registry/walkthrough.js +807 -0
  202. package/dist/shim/render/capture-envelope.js +279 -0
  203. package/dist/shim/render/heat-colormap.js +92 -0
  204. package/dist/shim/render/render-stages.js +855 -0
  205. package/dist/shim/render/sun-look.js +33 -0
  206. package/dist/shim/render-state/apply.js +262 -0
  207. package/dist/shim/render-state/index.js +74 -0
  208. package/dist/shim/rpc/index.js +207 -0
  209. package/dist/shim/runtime/index.js +1023 -0
  210. package/dist/shim/storage/boot.js +69 -0
  211. package/dist/shim/storage/index.js +169 -0
  212. package/dist/shim/storage/local-storage-provider.js +230 -0
  213. package/dist/shim/storage/make-namespace-store.js +132 -0
  214. package/dist/shim/storage/modules.js +249 -0
  215. package/dist/shim/storage/projects.js +150 -0
  216. package/llms.txt +12 -0
  217. package/package.json +62 -4
@@ -0,0 +1,259 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * captureStill-common.js — the SHARED pixel-readback + bag-assembly owner for every
5
+ * `RendererProvider.captureStill()` implementation (the `composite`-stage tap executor,
6
+ * docs/internal/engine/RENDERER_CONTRACT.md §1a.4).
7
+ *
8
+ * Peer of `renderCapabilities-common.js` in this directory, and it exists for the same
9
+ * reason: without it, `p5-provider.js` and `three-provider.js` would each carry their own
10
+ * row-order flip, their own bag-shape assembly, and their own "is this buffer actually
11
+ * non-blank?" check — three chances for the two backends to disagree about what a still
12
+ * IS. A capture that silently differs per backend is exactly the corrupted-corpus failure
13
+ * the provenance contract exists to prevent, so the parts that are NOT backend-specific
14
+ * live here once, and each provider contributes only its own irreducible readback call.
15
+ *
16
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
17
+ *
18
+ * Three pure, module-level free functions — no live WebGL/THREE/p5 instance is captured;
19
+ * every backend handle arrives as an ARGUMENT, which is what keeps the readback logic
20
+ * unit-testable in plain Node (a `Uint8Array` and a `{ readPixels }` object are enough).
21
+ *
22
+ * `flipRowsToTopLeft(src, width, height)` → `Uint8ClampedArray`
23
+ * Reverse the ROW order of an RGBA8 buffer. WebGL's `readPixels` origin is BOTTOM-LEFT;
24
+ * the contract's origin is TOP-LEFT (§1a.4), so a GL-based provider flips and a 2D-canvas
25
+ * provider (already top-left) does not. This is the one place the flip is written: an
26
+ * inverted still is invisible to every non-visual check and reads as "the physics is
27
+ * upside down", which is the worst kind of wrong answer — confidently mirrored data.
28
+ *
29
+ * `captureFrom2dCanvas(canvas, meta)` → the provider bag
30
+ * The 2D-canvas path (p5). `getImageData` is already RGBA, row-major, top-left, so no
31
+ * flip. Reads the canvas's BACKING-STORE size (`canvas.width/height`) — never its CSS
32
+ * size — because that is what `uResolution` means (§4).
33
+ *
34
+ * `captureFromGl(gl, width, height, meta)` → the provider bag
35
+ * The WebGL path (three). `readPixels` into a fresh `Uint8Array`, then ONE
36
+ * `flipRowsToTopLeft`. The CALLER must have re-rendered its retained scene in the SAME
37
+ * JS task immediately before calling this (see the readback guarantee below).
38
+ *
39
+ * `meta` (both capture fns) carries the fields only the PROVIDER is the authority for:
40
+ * `{ cssWidth, cssHeight, physicsW, physicsH, filtersApplied, renderBaseCanvas,
41
+ * filterTime, composited }`.
42
+ * `filtersApplied` MUST come from the provider's own ORDERED `_filters` list — never
43
+ * from the runner's unordered `_activeFilters` map, which is also blind to a direct BYO
44
+ * `prov.filter()` call and would therefore lie for exactly the BYO case.
45
+ * `filterTime` is the `uTime` the provider fed its filter passes (the contract's
46
+ * `filterTime` of the rendered tick): a number when `filtersApplied` is non-empty, null
47
+ * when it is empty.
48
+ * `composited` is the TickData the provider last drew (its own retained `_lastTickData`);
49
+ * its `universeData.tick` becomes the bag's `renderedTick` — the provider's REPORT of which
50
+ * tick these pixels are, which the tap mechanism compares against its stamp.
51
+ *
52
+ * The returned bag is the §1a.4 provider shape:
53
+ * `{ pixels, width, height, cssWidth, cssHeight, physicsW, physicsH,
54
+ * filtersApplied, renderBaseCanvas, filterTime, renderedTick }`
55
+ * The RUNNER then normalizes it into the generic tap bag (`lib/shim/render/render-stages`
56
+ * `normalizeStill`, where `pixels` becomes the stage-agnostic `data` payload and the
57
+ * dispatcher adds `stage`/`tick`/`pose`/`composedOf`/`renderOptions`). Providers never
58
+ * import the shim — a BYO renderer's whole capture burden stays ONE method.
59
+ *
60
+ * ── The readback-correctness guarantee (the hard sentence) ────────────────────
61
+ *
62
+ * A returned bag is the FULL composited canvas — all three layers (§8) plus every active
63
+ * filter pass (§4) — as of the last completed `renderTick`/`repaint`. The WebGL trap:
64
+ * `three-provider.js` constructs its `WebGLRenderer` WITHOUT `preserveDrawingBuffer`, so a
65
+ * `readPixels` in a LATER task reads a CLEARED buffer and would return an all-black still.
66
+ * The compliant GL path therefore re-renders its RETAINED scene (the scene graph and the
67
+ * filter passes both persist between ticks) and reads back IN THE SAME JS TASK. Flipping
68
+ * `preserveDrawingBuffer:true` globally is the WRONG fix: it taxes every frame to subsidise
69
+ * occasional captures and leaves the guarantee implicit rather than enforced.
70
+ * `assertNonBlank` below is the belt-and-braces check that this actually happened — a
71
+ * uniformly-zero buffer THROWS rather than being handed back as evidence.
72
+ *
73
+ * ── FAIL-LOUD, never a null image ────────────────────────────────────────────
74
+ *
75
+ * Every function here throws on a bad input (absent canvas, no 2D context, no GL context,
76
+ * a length mismatch, a uniformly-blank readback). A capability that is ABSENT hides an
77
+ * affordance (the `move()` precedent — a host capability-checks and shows no button); a
78
+ * capability that is PRESENT but cannot honor its guarantee THROWS. A black still from a
79
+ * cleared framebuffer is a lie in the product surface, the same violation class as a
80
+ * hardcoded backend label (dep-map rule 4).
81
+ *
82
+ * ── Module format: an ES module, like every peer under lib/frqtl/compat/renderer/ ──
83
+ *
84
+ * ── Layer 2: physics behaviour ───────────────────────────────────────────────
85
+ * ZERO physics. This is the output boundary reading its own output: nothing here touches a
86
+ * frame, a dimension or a tick, so no canonical-bench golden can move because of it.
87
+ */
88
+
89
+
90
+
91
+ function _fail(msg) {
92
+ throw new Error('[captureStill] ' + msg + ' (FAIL-LOUD — never a null or blank still.)');
93
+ }
94
+
95
+ /**
96
+ * Reverse the ROW order of an RGBA8 buffer (bottom-left origin → top-left origin).
97
+ *
98
+ * @param {Uint8Array|Uint8ClampedArray|Array<number>} src — RGBA8, `width*height*4` long
99
+ * @param {number} width — pixels per row
100
+ * @param {number} height — number of rows
101
+ * @returns {Uint8ClampedArray} a NEW buffer, rows reversed, channels untouched
102
+ * @throws {Error} on a length that is not exactly width*height*4
103
+ */
104
+ function flipRowsToTopLeft(src, width, height) {
105
+ if (!src || typeof src.length !== 'number') { _fail('flipRowsToTopLeft: src is not an indexed buffer'); }
106
+ var stride = width * 4;
107
+ if (src.length !== stride * height) {
108
+ _fail('flipRowsToTopLeft: buffer length ' + src.length + ' != width*height*4 (' +
109
+ width + '*' + height + '*4 = ' + (stride * height) + ')');
110
+ }
111
+ var out = new Uint8ClampedArray(src.length);
112
+ for (var row = 0; row < height; row++) {
113
+ var from = (height - 1 - row) * stride;
114
+ for (var i = 0; i < stride; i++) { out[row * stride + i] = src[from + i]; }
115
+ }
116
+ return out;
117
+ }
118
+
119
+ /**
120
+ * Throw if a readback is uniformly one value across every byte.
121
+ *
122
+ * A composited scene with a background and at least one quantum is never uniform, so a
123
+ * uniform buffer means the readback happened against a cleared/unbound framebuffer — the
124
+ * black-still failure mode. This is checked HERE (not in the test) so a broken realm path
125
+ * reports itself instead of quietly feeding a training corpus.
126
+ *
127
+ * @param {Uint8Array|Uint8ClampedArray} buf
128
+ * @param {string} where — the provider/path name for the message
129
+ */
130
+ function assertNonBlank(buf, where) {
131
+ if (!buf.length) { _fail(where + ': readback returned an EMPTY buffer'); }
132
+ var first = buf[0];
133
+ for (var i = 1; i < buf.length; i++) {
134
+ if (buf[i] !== first) { return; }
135
+ }
136
+ _fail(where + ': readback is UNIFORMLY ' + first + ' across every byte — the buffer was ' +
137
+ 'cleared or unbound. A GL provider must re-render its retained scene and read back ' +
138
+ 'in the SAME JS task (no preserveDrawingBuffer)');
139
+ }
140
+
141
+ /** Validate + copy the provider-authored provenance half of the bag. */
142
+ function _meta(meta, width, height) {
143
+ meta = meta || {};
144
+ if (!Array.isArray(meta.filtersApplied)) {
145
+ _fail('meta.filtersApplied must be the provider\'s ORDERED filter-name array (its own ' +
146
+ '_filters list is the one authority — never the runner\'s unordered _activeFilters map)');
147
+ }
148
+ // physicsW/H is the UNIVERSE-MAPPING half of provenance: it is what lets a consumer turn a
149
+ // pixel back into a physics coordinate. Defaulting it to the buffer dims INVENTED a
150
+ // universe size — silently correct only when the canvas happens to be 1:1 with the
151
+ // universe, and a mislabeled corpus row otherwise. There is no honest fallback, so this
152
+ // throws.
153
+ if (!(typeof meta.physicsW === 'number' && meta.physicsW > 0) ||
154
+ !(typeof meta.physicsH === 'number' && meta.physicsH > 0)) {
155
+ _fail('meta.physicsW/physicsH must be the POSITIVE physics-canvas dimensions this ' +
156
+ 'buffer maps from — they are the universe-mapping half of provenance and cannot ' +
157
+ 'be guessed from the buffer size (that is only right at 1:1). Got ' +
158
+ String(meta.physicsW) + 'x' + String(meta.physicsH));
159
+ }
160
+ // renderedTick is the TICK half of provenance, reported by the one party that knows which
161
+ // TickData these pixels were drawn from. The tap stamps `tick` off the LOOP, and the loop
162
+ // may have ticked past the last draw (a batched rAF frame renders only its last tick), so
163
+ // without this report nothing could tell a still labelled t from pixels of t+k.
164
+ var composited = meta.composited;
165
+ var renderedTick = composited && composited.universeData && composited.universeData.tick;
166
+ if (!(typeof renderedTick === 'number' && isFinite(renderedTick))) {
167
+ _fail('meta.composited must be the TickData this provider last drew (its retained ' +
168
+ '_lastTickData, carrying universeData.tick) — nothing has been composited, so ' +
169
+ 'there is no tick these pixels could be labelled with');
170
+ }
171
+ // filterTime is the CLOCK half of a filtered still's provenance: a filter that animates on
172
+ // `uTime` draws different pixels at a different time, so a filtered still without the time
173
+ // it drew with is not regenerable. A filter ran ⇔ a time was fed.
174
+ var filterTime = (meta.filterTime === undefined) ? null : meta.filterTime;
175
+ var filtered = meta.filtersApplied.length > 0;
176
+ if (filtered ? !(typeof filterTime === 'number' && isFinite(filterTime)) : filterTime !== null) {
177
+ _fail('meta.filterTime must be the finite uTime the filter passes drew with when ' +
178
+ 'filtersApplied is non-empty, and null when it is empty (filtersApplied ' +
179
+ JSON.stringify(meta.filtersApplied) + ', filterTime ' + String(filterTime) + ')');
180
+ }
181
+ return {
182
+ renderedTick: renderedTick,
183
+ width: width,
184
+ height: height,
185
+ // cssWidth/Height DO default to the buffer dims: that is the IDENTITY case (a canvas
186
+ // with no CSS scaling), not an invented value — and a headless capture genuinely has
187
+ // no CSS size to report.
188
+ cssWidth: (typeof meta.cssWidth === 'number' && meta.cssWidth > 0) ? meta.cssWidth : width,
189
+ cssHeight: (typeof meta.cssHeight === 'number' && meta.cssHeight > 0) ? meta.cssHeight : height,
190
+ physicsW: meta.physicsW,
191
+ physicsH: meta.physicsH,
192
+ filtersApplied: meta.filtersApplied.slice(),
193
+ renderBaseCanvas: (typeof meta.renderBaseCanvas === 'boolean') ? meta.renderBaseCanvas : null,
194
+ filterTime: filterTime
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Capture from a 2D canvas (the p5 path). `getImageData` is already RGBA, row-major,
200
+ * TOP-LEFT — no flip.
201
+ *
202
+ * @param {HTMLCanvasElement} canvas
203
+ * @param {object} meta — see the module docstring
204
+ * @returns {object} the §1a.4 provider bag
205
+ */
206
+ function captureFrom2dCanvas(canvas, meta) {
207
+ if (!canvas) {
208
+ _fail('no canvas to read — captureStill() was called before init() created one');
209
+ }
210
+ var ctx = (typeof canvas.getContext === 'function') ? canvas.getContext('2d') : null;
211
+ if (!ctx || typeof ctx.getImageData !== 'function') {
212
+ _fail('the canvas has no readable 2D context (getImageData unavailable)');
213
+ }
214
+ var w = canvas.width, h = canvas.height;
215
+ if (!(w > 0 && h > 0)) { _fail('canvas backing store is ' + w + 'x' + h); }
216
+ var img = ctx.getImageData(0, 0, w, h);
217
+ if (!img || !img.data || img.data.length !== w * h * 4) {
218
+ _fail('getImageData returned ' + (img && img.data ? img.data.length : 'nothing') +
219
+ ' bytes, expected ' + (w * h * 4));
220
+ }
221
+ assertNonBlank(img.data, '2d-canvas');
222
+ var bag = _meta(meta, w, h);
223
+ // Copy: the ImageData buffer is owned by the context and may be reused.
224
+ bag.pixels = new Uint8ClampedArray(img.data);
225
+ return bag;
226
+ }
227
+
228
+ /**
229
+ * Capture from a WebGL context (the three path). The caller MUST have re-rendered its
230
+ * retained scene in this SAME JS task immediately before calling this.
231
+ *
232
+ * @param {WebGLRenderingContext|WebGL2RenderingContext} gl
233
+ * @param {number} width — framebuffer width in backing-store pixels
234
+ * @param {number} height — framebuffer height in backing-store pixels
235
+ * @param {object} meta — see the module docstring
236
+ * @returns {object} the §1a.4 provider bag (rows already flipped to top-left)
237
+ */
238
+ function captureFromGl(gl, width, height, meta) {
239
+ if (!gl || typeof gl.readPixels !== 'function') {
240
+ _fail('no WebGL context to read — captureStill() was called before init(), or the ' +
241
+ 'context was lost');
242
+ }
243
+ if (!(width > 0 && height > 0)) { _fail('GL framebuffer is ' + width + 'x' + height); }
244
+ var raw = new Uint8Array(width * height * 4);
245
+ gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, raw);
246
+ assertNonBlank(raw, 'webgl');
247
+ var bag = _meta(meta, width, height);
248
+ bag.pixels = flipRowsToTopLeft(raw, width, height);
249
+ return bag;
250
+ }
251
+
252
+ // ── Export ────────────────────────────────────────────────────────────────────
253
+
254
+ export {
255
+ flipRowsToTopLeft,
256
+ assertNonBlank,
257
+ captureFrom2dCanvas,
258
+ captureFromGl
259
+ };
@@ -0,0 +1,96 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * display-slot.js — THE DISPLAY SLOT: the ONE answer to "which provider owns the display".
5
+ *
6
+ * ── The ratified rule (RENDERER_CONTRACT §9 + §1a.5, CEO 2026-08-01) ─────────
7
+ *
8
+ * The DISPLAY provider is boot-fixed; OBSERVERS are live.
9
+ *
10
+ * Exactly one `RendererProvider` — the display provider — owns the canvas, the camera and
11
+ * `projection`; swapping it is a reload. N observers (non-interactive consumers of the frame
12
+ * stream) are addable and removable at any time; an observer owns no canvas, no camera and no
13
+ * projection. This module holds the DISPLAY half of that rule and nothing else: there is no
14
+ * observer registry here, and adding one before its first real caller is the speculative
15
+ * registry §10 bans.
16
+ *
17
+ * ── What this slot is, and what it is NOT ───────────────────────────────────
18
+ *
19
+ * It is KNOWLEDGE, not ownership. The provider still creates and owns its own canvas in its
20
+ * own `init()` — the frozen §1 contract is untouched, and the literal canvas-element handoff
21
+ * was VETOED (one canvas owns one rendering context; p5 cannot adopt a foreign element). The
22
+ * slot records WHICH provider that is, so every consumer asks one place instead of reading a
23
+ * bare global that anything could assign.
24
+ *
25
+ * ── Why it lives in the RENDERER layer and not on the capture instrument ────
26
+ *
27
+ * The boot order forces it. The boot module (`experiments/_shared/boot.js`) sets it while the
28
+ * renderer boots, BEFORE the game layer constructs anything, and the three.js path registers
29
+ * ASYNC — later still. A slot that lived in a game-layer file would not exist when its one
30
+ * writer needs it. The RENDERER layer is also where
31
+ * `CANONICAL_STACK_DIAGRAM` already puts the renderer pluggable surface, so this is the
32
+ * authority table's own home rather than a new one. The capture instrument READS the slot
33
+ * (through its `providerGetter` binding); it does not own it — an instrument is per-runner
34
+ * and the display is per-process.
35
+ *
36
+ * ── The name chain (truth in labeling) ──────────────────────────────────────
37
+ *
38
+ * The boot authority's renderer NAME (`bootRealm({ renderer })` / `?renderer=`) heads ONE chain:
39
+ * boot authority → `build.renderer` provenance
40
+ * boot authority → this slot's `name()`
41
+ * The slot FERRIES the boot authority's name; it never re-derives one. A slot that could
42
+ * answer differently from `build.renderer` is the truth-in-labeling failure (dep-map rule 4),
43
+ * so `display-slot.test.mjs` gates `name()` against the booted renderer after a real boot.
44
+ *
45
+ * ── Boot-fixed is a SEMANTIC, not a write-once lock ─────────────────────────
46
+ *
47
+ * `set()` deliberately does NOT throw on a second call. The notebook realm switches a cell
48
+ * between p5 and three by re-invoking the boot module's `selectRenderer(name)` — that IS the
49
+ * reload the rule describes (a new renderer session, a new scene identity), not a hot swap of
50
+ * a live scene. Locking the slot would break the realm while proving nothing: what makes the
51
+ * display boot-fixed is that `projection` and `build.renderer` are SCENE-record fields, so a
52
+ * re-selection mints a new scene rather than silently re-parenting stills.
53
+ */
54
+
55
+
56
+ var _provider = null; // the display RendererProvider instance, or null
57
+ var _name = null; // the boot authority's name it was registered under
58
+
59
+ /**
60
+ * Register the booted display provider. Called by whichever boot ran the selection
61
+ * (the boot module's `selectRenderer`, a test harness declaring a renderer the way a
62
+ * page does).
63
+ *
64
+ * @param {object} provider — the `RendererProvider` instance. The slot validates only
65
+ * that there IS one: provider SHAPE has an authority already —
66
+ * `renderer-provider-contract.js`'s `validateRendererProvider` on the BYO path — and a second
67
+ * shape check here would be a second authority that could disagree with it (and would
68
+ * silently tighten what the shipped providers are allowed to be).
69
+ * @param {string} [name] — the boot authority's renderer name,
70
+ * FERRIED. Never re-derived here.
71
+ * @returns {object} the provider, so a caller can register and assign in one expression.
72
+ */
73
+ function set(provider, name) {
74
+ if (!provider || typeof provider !== 'object') {
75
+ throw new Error('[FrqtlDisplay.set] the display provider must be a ' +
76
+ 'RendererProvider instance. Got: ' + String(provider) + '. (FAIL-LOUD — never ' +
77
+ 'a display slot holding nothing while a page believes it has a renderer.)');
78
+ }
79
+ _provider = provider;
80
+ _name = (name === undefined || name === null) ? null : String(name);
81
+ return provider;
82
+ }
83
+
84
+ /** @returns {object|null} the display provider, or null before boot completes. */
85
+ function get() { return _provider; }
86
+
87
+ /** @returns {string|null} the boot authority's name this provider was registered under. */
88
+ function name() { return _name; }
89
+
90
+ /**
91
+ * Empty the slot. For teardown ONLY (a test restoring its host, a realm disposing a
92
+ * session) — never a way to "unset" a display mid-scene.
93
+ */
94
+ function clear() { _provider = null; _name = null; }
95
+
96
+ export { set, get, name, clear };
@@ -0,0 +1,231 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * lo-drawn-center.js — THE authority for where a LargeObject rect is DRAWN: its center point.
5
+ *
6
+ * Peer of `orbit-bag.js`: a shared contract-half with no geometry of its own beyond one rule, so
7
+ * the fact has ONE owner. Its consumers are the three.js provider (it places each LO mesh at this
8
+ * point and paints the shadow-surround from its volume) and `FrqtlApp` (the 3D mouse pick aims
9
+ * at the centre; the aim ray tests the volume). Two
10
+ * derivations of the one fact is the drift this file exists to make impossible — a pick aimed at
11
+ * a different point than the mesh is drawn at selects the wrong object silently.
12
+ *
13
+ * ── The rule ─────────────────────────────────────────────────────────────────
14
+ *
15
+ * An LO rect is corner-mode (`left`/`top` are the near corner, never a center — the LargeObject
16
+ * convention). A 6-element rect (`{left, top, z, w, h, d}`) also carries `z`, the NEAR-face corner
17
+ * on the z axis, so its z-center is `z + d/2`, with `d` defaulting to `w` exactly as the engine's
18
+ * z-capable rectangle does. A legacy 4-element rect has no `z`: it sits at z-center 0.
19
+ *
20
+ * drawnCenter(rect) → [left + w/2, top + h/2, (z present) ? z + (d ?? w)/2 : 0]
21
+ * hasZ(rect) → whether the rect carries z — the ONE z-presence test (consumers never re-derive it)
22
+ *
23
+ * Its INVERSE places the same-sized rect at a centre (a grabbed wall carried by the camera):
24
+ *
25
+ * rectAtCenter(rect, [cx, cy, cz]) → the WRITE-wire array, the rect's own form:
26
+ * z present → [cx − w/2, cy − h/2, cz − D/2, w, h, D] with D = d ?? w STATED (so the written
27
+ * rect reads back through drawnCenter to the centre it was placed at);
28
+ * z absent → [cx − w/2, cy − h/2, w, h], and cz must be 0 — a z-less rect sits at z-centre
29
+ * 0 by the rule above, so any other cz THROWS rather than being dropped.
30
+ *
31
+ * ── The drawn VOLUME and the center-ray hit (STUDIO's aim) ──────────────────────
32
+ *
33
+ * The same rect also owns the VOLUME the aim ray tests and the shadow-surround paints, so the
34
+ * thing you aim at, the thing that lights up and the mesh are one geometry:
35
+ *
36
+ * drawnVolume({rect, orientationDeg?, pitchDeg?}) → a BOX: centre = drawnCenter(rect), half
37
+ * extents [w/2, h/2, D/2] with D = the rect's z-extent (d ?? w when z is present — the same
38
+ * default the centre uses — else 0, a flat legacy rect), turned by the shell's own rotation
39
+ * (yaw about world z, then pitch about the plate's own long axis: Rz(yaw)·Ry(pitch), the
40
+ * renderer's 'ZYX' order).
41
+ * drawnVolume({center, radius}) → a SPHERE (a centre-only target, e.g. an emitter).
42
+ * rayIntersect(origin, dir, volume, margin?) → the ENTRY ray parameter t ≥ 0 (0 when the
43
+ * origin is inside), or null for a miss / a volume wholly behind the origin. `margin`
44
+ * inflates the volume on every side first, so a thin wall is aimable.
45
+ * firstHit(origin, dir, volumes, margin?) → {index, t} | null — THE hover rule: the nearest
46
+ * entry wins, except that a smaller volume whose entry point lies inside the winner's
47
+ * (a volume nested in another along the ray) wins over it.
48
+ * volumeContains(volume, point, margin?) — the containment test firstHit uses.
49
+ *
50
+ * ── Layer 2: physics behaviour ───────────────────────────────────────────────
51
+ * ZERO physics. This reads a rect the engine already published; it moves no frame and no LO.
52
+ */
53
+
54
+
55
+ /**
56
+ * hasZ(rect) — whether the rect carries a z (a z-capable / 6-element rect). THE one z-presence test
57
+ * every consumer asks (the carry's flat/z-held rule, a part's 6-on-flat refusal), never re-derived.
58
+ * @param {{z?:number}} rect @returns {boolean}
59
+ */
60
+ function hasZ(rect) {
61
+ return typeof rect.z === 'number';
62
+ }
63
+
64
+ /**
65
+ * @param {{left:number, top:number, w:number, h:number, z?:number, d?:number}} rect
66
+ * @returns {number[]} `[cx, cy, cz]` — the center the renderer draws the rect's box at.
67
+ */
68
+ function drawnCenter(rect) {
69
+ var cz = hasZ(rect)
70
+ ? rect.z + ((typeof rect.d === 'number') ? rect.d : rect.w) / 2
71
+ : 0;
72
+ return [rect.left + rect.w / 2, rect.top + rect.h / 2, cz];
73
+ }
74
+
75
+ /**
76
+ * @param {{left:number, top:number, w:number, h:number, z?:number, d?:number}} rect — the rect
77
+ * being placed (its w/h and z-extent are kept).
78
+ * @param {number[]} center — `[cx, cy, cz]` (cz may be omitted for a z-less rect).
79
+ * @returns {number[]} the write-wire array `[l, t, z, w, h, d]` (z present) or `[l, t, w, h]`.
80
+ */
81
+ function rectAtCenter(rect, center) {
82
+ var cz = (typeof center[2] === 'number') ? center[2] : 0;
83
+ if (hasZ(rect)) {
84
+ var D = _zExtent(rect);
85
+ return [center[0] - rect.w / 2, center[1] - rect.h / 2, cz - D / 2, rect.w, rect.h, D];
86
+ }
87
+ if (cz !== 0) {
88
+ throw new RangeError('[LoDrawnCenter.rectAtCenter] a z-less rect sits at z-centre 0 and cannot ' +
89
+ 'be placed at z = ' + cz + ' — place it at cz 0, or give the rect an explicit z. (FAIL-LOUD.)');
90
+ }
91
+ return [center[0] - rect.w / 2, center[1] - rect.h / 2, rect.w, rect.h];
92
+ }
93
+
94
+ var DEG = Math.PI / 180;
95
+
96
+ /** The rect's z-extent: d ?? w when it carries z (the engine's z-capable rectangle), else 0. */
97
+ function _zExtent(rect) {
98
+ if (!hasZ(rect)) { return 0; }
99
+ return (typeof rect.d === 'number') ? rect.d : rect.w;
100
+ }
101
+
102
+ /**
103
+ * @param {{rect:object, orientationDeg?:number, pitchDeg?:number}|{center:number[], radius:number}} src
104
+ * @returns {{kind:'box', center:number[], half:number[], yawDeg:number, pitchDeg:number}
105
+ * |{kind:'sphere', center:number[], radius:number}}
106
+ */
107
+ function drawnVolume(src) {
108
+ if (src && src.rect) {
109
+ var r = src.rect;
110
+ return {
111
+ kind: 'box',
112
+ center: drawnCenter(r),
113
+ half: [r.w / 2, r.h / 2, _zExtent(r) / 2],
114
+ yawDeg: (typeof src.orientationDeg === 'number') ? src.orientationDeg : 0,
115
+ pitchDeg: (typeof src.pitchDeg === 'number') ? src.pitchDeg : 0
116
+ };
117
+ }
118
+ if (src && Array.isArray(src.center) && typeof src.radius === 'number') {
119
+ var c = src.center;
120
+ return { kind: 'sphere', center: [c[0], c[1], (typeof c[2] === 'number') ? c[2] : 0], radius: src.radius };
121
+ }
122
+ throw new TypeError('[LoDrawnCenter.drawnVolume] needs {rect} (an LO) or {center, radius} (a ' +
123
+ 'centre-only target); got ' + JSON.stringify(src) + '. (FAIL-LOUD.)');
124
+ }
125
+
126
+ /** A world vector expressed in the box's local frame: Rᵀ·v with R = Rz(yaw)·Ry(pitch). */
127
+ function _toLocal(box, v) {
128
+ var a = box.yawDeg * DEG, b = box.pitchDeg * DEG;
129
+ var ca = Math.cos(a), sa = Math.sin(a), cb = Math.cos(b), sb = Math.sin(b);
130
+ return [
131
+ ca * cb * v[0] + sa * cb * v[1] - sb * v[2],
132
+ -sa * v[0] + ca * v[1],
133
+ ca * sb * v[0] + sa * sb * v[1] + cb * v[2]
134
+ ];
135
+ }
136
+
137
+ function _sub(a, b) { return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; }
138
+
139
+ /**
140
+ * @param {number[]} origin @param {number[]} dir (need not be unit; t is in units of |dir|)
141
+ * @param {object} volume — a drawnVolume result. @param {number} [margin=0] world px.
142
+ * @returns {number|null}
143
+ */
144
+ function rayIntersect(origin, dir, volume, margin) {
145
+ var m = (typeof margin === 'number') ? margin : 0;
146
+ if (volume.kind === 'sphere') {
147
+ var R = volume.radius + m;
148
+ var oc = _sub(origin, volume.center);
149
+ var A = dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2];
150
+ var B = 2 * (oc[0] * dir[0] + oc[1] * dir[1] + oc[2] * dir[2]);
151
+ var C = oc[0] * oc[0] + oc[1] * oc[1] + oc[2] * oc[2] - R * R;
152
+ var disc = B * B - 4 * A * C;
153
+ if (!(A > 0) || disc < 0) { return null; }
154
+ var sq = Math.sqrt(disc);
155
+ var t0 = (-B - sq) / (2 * A), t1 = (-B + sq) / (2 * A);
156
+ if (t1 < 0) { return null; }
157
+ return (t0 > 0) ? t0 : 0;
158
+ }
159
+ if (volume.kind === 'box') {
160
+ var o = _toLocal(volume, _sub(origin, volume.center));
161
+ var d = _toLocal(volume, dir);
162
+ var tNear = -Infinity, tFar = Infinity;
163
+ for (var i = 0; i < 3; i++) {
164
+ var h = volume.half[i] + m;
165
+ if (Math.abs(d[i]) < 1e-12) {
166
+ if (o[i] < -h || o[i] > h) { return null; } // parallel to this slab, outside it
167
+ continue;
168
+ }
169
+ var ta = (-h - o[i]) / d[i], tb = (h - o[i]) / d[i];
170
+ if (ta > tb) { var tmp = ta; ta = tb; tb = tmp; }
171
+ if (ta > tNear) { tNear = ta; }
172
+ if (tb < tFar) { tFar = tb; }
173
+ if (tNear > tFar) { return null; }
174
+ }
175
+ if (tFar < 0) { return null; }
176
+ return (tNear > 0) ? tNear : 0;
177
+ }
178
+ throw new TypeError('[LoDrawnCenter.rayIntersect] unknown volume kind ' + String(volume && volume.kind) +
179
+ ' — volumes come from drawnVolume(). (FAIL-LOUD.)');
180
+ }
181
+
182
+ /** Whether `point` lies inside `volume` (inflated by `margin`). */
183
+ function volumeContains(volume, point, margin) {
184
+ var m = (typeof margin === 'number') ? margin : 0;
185
+ var p = _sub(point, volume.center);
186
+ if (volume.kind === 'sphere') {
187
+ var R = volume.radius + m;
188
+ return p[0] * p[0] + p[1] * p[1] + p[2] * p[2] <= R * R + 1e-9;
189
+ }
190
+ var l = _toLocal(volume, p);
191
+ for (var i = 0; i < 3; i++) { if (Math.abs(l[i]) > volume.half[i] + m + 1e-9) { return false; } }
192
+ return true;
193
+ }
194
+
195
+ /** The inflated volume's size — the nested-volume tie-break compares it. */
196
+ function _size(volume, m) {
197
+ if (volume.kind === 'sphere') { var R = volume.radius + m; return R * R * R * 4.18879; }
198
+ return 8 * (volume.half[0] + m) * (volume.half[1] + m) * (volume.half[2] + m);
199
+ }
200
+
201
+ /**
202
+ * @param {number[]} origin @param {number[]} dir @param {object[]} volumes @param {number} [margin=0]
203
+ * @returns {{index:number, t:number}|null}
204
+ */
205
+ function firstHit(origin, dir, volumes, margin) {
206
+ var m = (typeof margin === 'number') ? margin : 0;
207
+ var hits = [];
208
+ for (var i = 0; i < volumes.length; i++) {
209
+ var t = rayIntersect(origin, dir, volumes[i], m);
210
+ if (t !== null) { hits.push({ index: i, t: t }); }
211
+ }
212
+ if (!hits.length) { return null; }
213
+ hits.sort(function (a, b) { return a.t - b.t; });
214
+ var best = hits[0];
215
+ for (var k = 1; k < hits.length; k++) {
216
+ var h = hits[k], vb = volumes[best.index], vh = volumes[h.index];
217
+ var entry = [origin[0] + dir[0] * h.t, origin[1] + dir[1] * h.t, origin[2] + dir[2] * h.t];
218
+ if (volumeContains(vb, entry, m) && _size(vh, m) < _size(vb, m)) { best = h; }
219
+ }
220
+ return best;
221
+ }
222
+
223
+ export {
224
+ drawnCenter,
225
+ rectAtCenter,
226
+ hasZ,
227
+ drawnVolume,
228
+ rayIntersect,
229
+ volumeContains,
230
+ firstHit
231
+ };