@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,186 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * renderCapabilities-common.js — the SHARED universal + filter capability
5
+ * descriptors every `RendererProvider.renderCapabilities()` implementation
6
+ * declares (WS Studio Controls Realignment, Bite S5b — architect's S5a gate
7
+ * "minor DRY recommendation": `p5-provider.js` and `three-provider.js` had
8
+ * byte-identical `_universalRenderCapabilities` bodies + an identical
9
+ * per-Filters-entry toggle loop, hand-copied across two legitimately-
10
+ * independent BYO files — a 4th universal knob would have needed adding in
11
+ * two places. This module is the ONE owner both providers import).
12
+ *
13
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
14
+ *
15
+ * Pure, module-level free functions — no live WebGL/THREE/canvas instance
16
+ * needed (capability DESCRIPTORS are static metadata; live VALUES are read
17
+ * separately via `FrqtlApp.getRenderOption`) — this is what keeps every caller
18
+ * unit-testable in plain Node with no browser.
19
+ *
20
+ * `universalRenderCapabilities(normalizeFieldDescriptor, RenderShapeRef, FrameDataSchemaRef)`
21
+ * → the 3 knobs BOTH built-in providers honor identically (README §3):
22
+ * `shape` (enum, RenderShape.VALID_SHAPES), `colorScheme` (enum,
23
+ * RENDER_INTENT_VALUES['render.colorScheme']), `renderBaseCanvas` (boolean,
24
+ * render-only). Does NOT include filter toggles — see `filterRenderCapabilities`
25
+ * below (three-provider splices its OWN effect-layer knobs BETWEEN the
26
+ * universal 3 and the filter toggles, so the lists are kept separate).
27
+ *
28
+ * `meshRenderCapabilities(normalizeFieldDescriptor)`
29
+ * → the scene external-mesh pair (`mesh`, `meshScale`) a provider with a mesh
30
+ * render path declares (three).
31
+ *
32
+ * `filterRenderCapabilities(normalizeFieldDescriptor, FiltersRef)`
33
+ * → one `filter.<name>` toggle descriptor (boolean, render-only, default false) per
34
+ * `Filters.js` catalog entry — `filter()`/`removeFilter()` are REQUIRED
35
+ * contract methods on every provider (README §1), so every provider
36
+ * declares every filter identically; this is why the loop is shared
37
+ * rather than per-provider.
38
+ *
39
+ * A caller composes its own full list as
40
+ * `universalRenderCapabilities(...).concat(providerOnlyCaps).concat(filterRenderCapabilities(...))`
41
+ * — see `p5-provider.js`'s `_renderCapabilitiesP5` / `three-provider.js`'s
42
+ * `_renderCapabilitiesThree` for the two worked call sites.
43
+ *
44
+ * `knownRenderCapabilityNames(providers)`
45
+ * → the union of names over the renderer registry (`providers/index.js` RENDERER_PROVIDERS,
46
+ * each entry registering its static `renderCapabilities` beside `makeProvider`) —
47
+ * the "known" set saved render rows are partitioned against.
48
+ *
49
+ * ── Module format: an ES module, like every peer under lib/frqtl/compat/renderer/ ──
50
+ *
51
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
52
+ * Zero physics — pure metadata describing render-surface knobs (root
53
+ * CLAUDE.md §Object architecture: render is output-boundary only).
54
+ */
55
+
56
+
57
+
58
+ /** `{value:value}` identity map — the shape `makeControlsForObject.js`'s
59
+ * `_enumHandler` requires for `constraint.values` (`_isIdentityValuesMap`). */
60
+ function _identityMap(values) {
61
+ var m = {};
62
+ for (var i = 0; i < values.length; i++) { m[values[i]] = values[i]; }
63
+ return m;
64
+ }
65
+
66
+ /**
67
+ * The 3 universal knobs both p5 and three honor identically.
68
+ * @param {function} normalizeFieldDescriptor — `lib/shim/field-descriptor`'s
69
+ * `normalizeFieldDescriptor` (dependency-injected, not required here, so
70
+ * this module has no import of its own — mirrors the callers' own
71
+ * already-resolved-ref-passing convention).
72
+ * @param {{VALID_SHAPES:string[]}} RenderShapeRef
73
+ * @param {{RENDER_INTENT_VALUES:object}} FrameDataSchemaRef
74
+ * @returns {object[]} 3 normalized field descriptors.
75
+ */
76
+ function universalRenderCapabilities(normalizeFieldDescriptor, RenderShapeRef, FrameDataSchemaRef) {
77
+ return [
78
+ normalizeFieldDescriptor({
79
+ name: 'shape', type: 'enum', default: 'ball',
80
+ // Bite 5b A3: this is the render-LEVEL DEFAULT shape (per-object sugar like a
81
+ // SphereEmitter's `renderShape` field overrides it per-leaf via render.shape) —
82
+ // labelled "Default Shape" so the scoping reads true. `label` overrides the shown
83
+ // control label only; the capability key stays `shape` (setRenderOption('shape',…)).
84
+ label: 'Default Shape',
85
+ constraint: { values: _identityMap(RenderShapeRef.VALID_SHAPES) },
86
+ description: 'Render-level DEFAULT particle shape (RenderShape.js — the one shape ' +
87
+ 'authority). A per-object sugar override (e.g. a SphereEmitter renderShape) wins ' +
88
+ 'per-leaf via the render.shape key; this is the fallback when none is set.'
89
+ }),
90
+ normalizeFieldDescriptor({
91
+ name: 'colorScheme', type: 'enum', default: 'warmcool',
92
+ constraint: { values: _identityMap(FrameDataSchemaRef.RENDER_INTENT_VALUES['render.colorScheme']) },
93
+ description: 'Per-experiment default fullness-to-color ramp (LightColor.js — the one color authority).'
94
+ }),
95
+ normalizeFieldDescriptor({
96
+ name: 'renderBaseCanvas', type: 'boolean', default: true, engineVisible: false,
97
+ description: 'Whether the particle/base layer composites into the scene before any ' +
98
+ 'post-process pass (README §3). Both p5 and three honor this identically; the write ' +
99
+ 'gate coerces a 0/1 control value to the boolean the providers read.'
100
+ })
101
+ ];
102
+ }
103
+
104
+ /**
105
+ * The scene external-mesh pair, declared by a provider with a mesh render path (three):
106
+ * `mesh` is read one hop away, in `RenderMesh.resolve(frame, renderOpts)` (priority #3,
107
+ * below a frame's `render.mesh` and a large object's `mesh`); `meshScale` scales the clones.
108
+ * @param {function} normalizeFieldDescriptor
109
+ * @returns {object[]} 2 normalized field descriptors.
110
+ */
111
+ function meshRenderCapabilities(normalizeFieldDescriptor) {
112
+ return [
113
+ normalizeFieldDescriptor({
114
+ name: 'mesh', type: 'string', default: null,
115
+ description: 'Scene-default external glTF asset id (URL) every frame is drawn as, in 3D. ' +
116
+ 'OPEN-ID like its per-frame twin render.mesh; unset draws the shape path.'
117
+ }),
118
+ normalizeFieldDescriptor({
119
+ name: 'meshScale', type: 'number', unit: 'multiplier', default: 1,
120
+ constraint: { min: 0.01, max: 100, integer: false },
121
+ description: 'Scene-wide scale multiplier for external-mesh clones (a frame\'s ' +
122
+ 'render.meshScale overrides it per frame).'
123
+ })
124
+ ];
125
+ }
126
+
127
+ /**
128
+ * One `filter.<name>` toggle capability per `Filters.js` catalog entry.
129
+ * @param {function} normalizeFieldDescriptor
130
+ * @param {object} FiltersRef — `Filters.js`'s exported catalog (`{water:{...}, bloom:{...}, ...}`).
131
+ * @returns {object[]} `Object.keys(FiltersRef).length` normalized field descriptors.
132
+ */
133
+ function filterRenderCapabilities(normalizeFieldDescriptor, FiltersRef) {
134
+ var caps = [];
135
+ var filterNames = Object.keys(FiltersRef);
136
+ for (var i = 0; i < filterNames.length; i++) {
137
+ caps.push(normalizeFieldDescriptor({
138
+ name: 'filter.' + filterNames[i], type: 'boolean', default: false, engineVisible: false,
139
+ description: 'Toggle the "' + filterNames[i] + '" post-process filter ' +
140
+ '(Filters.js — the runner routes the write to filter()/removeFilter(); a 0/1 ' +
141
+ 'control value is coerced to the boolean).'
142
+ }));
143
+ }
144
+ return caps;
145
+ }
146
+
147
+ /**
148
+ * The union of capability NAMES every registered renderer declares — the "known" set a
149
+ * saved render row is partitioned against (declared by the target / by another renderer /
150
+ * by none). Read off each registry entry's STATIC `renderCapabilities` (the providers
151
+ * register it beside `makeProvider`), so no provider is constructed and no name is listed
152
+ * by hand.
153
+ * @param {Object<string, {renderCapabilities:function}>} providers — `RENDERER_PROVIDERS`.
154
+ * @returns {string[]} sorted, de-duplicated names.
155
+ * @throws {Error} when the registry is missing or an entry exposes no static renderCapabilities.
156
+ */
157
+ function knownRenderCapabilityNames(providers) {
158
+ if (!providers || typeof providers !== 'object') {
159
+ throw new Error('[RenderCapabilitiesCommon.knownRenderCapabilityNames] requires the renderer ' +
160
+ 'registry (providers/index.js RENDERER_PROVIDERS); got ' + (providers === null ? 'null' : typeof providers) +
161
+ '. (FAIL-LOUD.)');
162
+ }
163
+ var seen = {};
164
+ var ids = Object.keys(providers);
165
+ for (var i = 0; i < ids.length; i++) {
166
+ var entry = providers[ids[i]];
167
+ if (!entry || typeof entry.renderCapabilities !== 'function') {
168
+ throw new Error('[RenderCapabilitiesCommon.knownRenderCapabilityNames] registered renderer "' +
169
+ ids[i] + '" exposes no static renderCapabilities() — every registry entry publishes its ' +
170
+ 'capability descriptors beside makeProvider. (FAIL-LOUD.)');
171
+ }
172
+ var caps = entry.renderCapabilities();
173
+ for (var j = 0; j < caps.length; j++) { seen[caps[j].name] = true; }
174
+ }
175
+ return Object.keys(seen).sort();
176
+ }
177
+
178
+ // ── Export ────────────────────────────────────────────────────────────────────
179
+
180
+ export {
181
+ universalRenderCapabilities,
182
+ filterRenderCapabilities,
183
+ knownRenderCapabilityNames,
184
+ meshRenderCapabilities,
185
+ _identityMap
186
+ };
@@ -0,0 +1,238 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * renderer-provider-contract.js — the CODE OWNER of the `RendererProvider` method lists.
5
+ *
6
+ * `RendererProvider` is duck-typed: a BYO renderer is a plain object from a
7
+ * `makeProvider()` factory, registered through the boot-fixed display slot. Nothing is
8
+ * imported to implement it — which is exactly why the METHOD LISTS need one owner. Before
9
+ * this module they existed only as prose in `docs/internal/engine/RENDERER_CONTRACT.md` §1
10
+ * and as scattered `typeof provider.X === 'function'` checks in the hosts; a BYO author
11
+ * checking their own object had to hand-copy the list, which is the schema-mirror
12
+ * anti-pattern with the doc as the mirror. THIS FILE is now the owner; the doc cites it.
13
+ *
14
+ * The two lists are transcribed from RENDERER_CONTRACT.md §1 / §1a. A method's SEMANTICS
15
+ * stay in that document — this module owns only the NAMES and the required/optional split,
16
+ * because that is the only part a program can check — plus the checkable rules the built-in
17
+ * providers share: `requireProviderSize` (init/setSize take the runner's physics rect, REQUIRED),
18
+ * `requireMountHost` (init's `parentEl` names the element the canvas mounts in, REQUIRED),
19
+ * `requireSceneSurface` (init RETURNS its HTMLCanvasElement), `SCENE_SURFACE_ATTR` (the marker
20
+ * the runner stamps on that canvas) and `filterTime` (the value of the standard `uTime` filter
21
+ * uniform, derived from the tick being rendered).
22
+ *
23
+ * ── THE REQUIRED/OPTIONAL SPLIT IS LOAD-BEARING ─────────────────────────────
24
+ *
25
+ * An absent OPTIONAL capability is NEVER an error (RENDERER_CONTRACT §1a, "absent ⇒
26
+ * hidden, never an error"): a provider that omits `getCameraControl` simply gets no camera
27
+ * strip. Hosts capability-check per method — they never renderer-sniff. So
28
+ * `validateRendererProvider` reports optionals as CAPABILITIES, never as failures, and its
29
+ * `ok` is decided by the REQUIRED nine alone.
30
+ *
31
+ * ── IT IS A VALIDATOR, NOT A GATE ───────────────────────────────────────────
32
+ *
33
+ * `validateRendererProvider` NEVER THROWS — not even on a non-object. It returns a report;
34
+ * the CALLER decides whether a missing method is fatal in its context (a host that boots a
35
+ * provider fails loud; an authoring tool that lints one shows a list). A validator that
36
+ * threw would force every caller to wrap it in try/catch to ask a question.
37
+ *
38
+ * @example
39
+ * import { validateRendererProvider } from '@descix/frqtl-web'; // or '@descix/frqtl-node'
40
+ * const report = validateRendererProvider(myProvider);
41
+ * if (!report.ok) throw new Error('BYO renderer missing: ' + report.missing.join(', '));
42
+ * // report.capabilities → e.g. ['renderCapabilities', 'captureStill']
43
+ */
44
+
45
+
46
+ /**
47
+ * The nine REQUIRED methods. Every renderer — built-in or BYO — must implement
48
+ * exactly these; nothing above the shim calls anything else on a renderer.
49
+ * @type {string[]}
50
+ */
51
+ var RENDERER_PROVIDER_REQUIRED = [
52
+ 'init', // create canvas/context (+ scene/buffers); RETURNS the HTMLCanvasElement
53
+ 'setSize', // resize the drawing surface
54
+ 'start', // may be a NO-OP (the RAF loop is FrqtlApp's)
55
+ 'stop',
56
+ 'destroy', // tear down; release GL resources
57
+ 'renderTick', // draw ONE frame from pure data
58
+ 'filter', // queue/apply a post-process pass
59
+ 'removeFilter', // remove a queued pass by name
60
+ 'raw' // the underlying backend object (escape hatch)
61
+ ];
62
+
63
+ /**
64
+ * The five OPTIONAL capabilities. Absent ⇒ the corresponding host UI is hidden.
65
+ * NEVER an error.
66
+ * @type {string[]}
67
+ */
68
+ var RENDERER_PROVIDER_OPTIONAL = [
69
+ 'renderCapabilities', // FieldDescriptor[] — this provider's own render knobs
70
+ 'getCameraControl', // the orbit/pose sugar handle
71
+ 'screenToWorld', // unproject a canvas point to a world point
72
+ 'captureStill', // the `composite` stage tap
73
+ 'projection' // the scene's camera intrinsics (rides the SceneRecord)
74
+ ];
75
+
76
+ /**
77
+ * Check a candidate against the frozen `RendererProvider` contract.
78
+ *
79
+ * @param {object} provider - the object a `makeProvider()` factory returned.
80
+ * @returns {{ok: boolean, missing: string[], capabilities: string[]}}
81
+ * `ok` — true iff EVERY required name is a function.
82
+ * `missing` — the required names that are not functions (in contract order).
83
+ * `capabilities` — the optional names that ARE functions (in contract order).
84
+ * A non-object argument reports every required name missing and no capabilities —
85
+ * it does not throw.
86
+ */
87
+ function validateRendererProvider(provider) {
88
+ var isObject = (provider !== null && (typeof provider === 'object' || typeof provider === 'function'));
89
+ if (!isObject) {
90
+ return { ok: false, missing: RENDERER_PROVIDER_REQUIRED.slice(), capabilities: [] };
91
+ }
92
+ var missing = RENDERER_PROVIDER_REQUIRED.filter(function (name) {
93
+ return typeof provider[name] !== 'function';
94
+ });
95
+ var capabilities = RENDERER_PROVIDER_OPTIONAL.filter(function (name) {
96
+ return typeof provider[name] === 'function';
97
+ });
98
+ return { ok: missing.length === 0, missing: missing, capabilities: capabilities };
99
+ }
100
+
101
+ function _isPositiveFinite(n) {
102
+ return typeof n === 'number' && isFinite(n) && n > 0;
103
+ }
104
+
105
+ /**
106
+ * The `init({width, height})` / `setSize(width, height)` size rule, owned here so both
107
+ * built-in providers throw the SAME sentence. `width`/`height` are the runner's physics
108
+ * rect (`runner.canvas_rect`), which `FrqtlApp.createCanvas` hands to
109
+ * `provider.init({width, height})`; the scene writes that rect ONCE through
110
+ * `exp.createCanvas(w, h, cfg)` / `exp.createScene(w, h, d, cfg)`. A provider holds no size of
111
+ * its own. The display size is the host element's CSS box, never a provider argument.
112
+ *
113
+ * Unlike `validateRendererProvider`, this IS a gate: it THROWS on a missing or non-positive
114
+ * dimension (a defaulted size would draw a confident picture of the wrong world).
115
+ *
116
+ * @param {string} who - the calling site, e.g. `'p5Provider.init'`.
117
+ * @param {*} width
118
+ * @param {*} height
119
+ * @throws {Error} unless both are positive finite numbers.
120
+ */
121
+ function requireProviderSize(who, width, height) {
122
+ if (_isPositiveFinite(width) && _isPositiveFinite(height)) { return; }
123
+ throw new Error(who + ': width and height must be positive finite numbers (got width=' +
124
+ String(width) + ', height=' + String(height) + '). They are the runner\'s physics rect ' +
125
+ '(runner.canvas_rect), handed over by FrqtlApp.createCanvas as provider.init({width, height}) ' +
126
+ 'and written once by the scene\'s exp.createCanvas(w, h, cfg) / exp.createScene(w, h, d, cfg). ' +
127
+ 'A provider has no default size; the display size is the host element\'s CSS box, never a ' +
128
+ 'provider argument.');
129
+ }
130
+
131
+ /**
132
+ * The `init({parentEl})` mount rule, owned here so both DOM providers throw the SAME sentence.
133
+ * `parentEl` is the runner's `canvasParentId` (`new FrqtlApp({ canvasParentId })` /
134
+ * `new RecipeContext({ canvasParentId })`), handed over by `FrqtlApp.createCanvas` as
135
+ * `provider.init({ parentEl })`. It must be a non-empty string naming an element in `document`;
136
+ * the provider appends its canvas THERE. There is no default mount: a canvas appended to
137
+ * `document.body` is a canvas the layout never placed.
138
+ *
139
+ * Only a provider that MOUNTS calls this — a DOM-free runner (node, the headless renderer)
140
+ * legitimately has no `canvasParentId`, so the runner never demands one.
141
+ *
142
+ * @param {string} who - the calling site, e.g. `'threeProvider.init'`.
143
+ * @param {*} parentEl - the element id from `init(opts).parentEl`.
144
+ * @returns {HTMLElement} the element named by `parentEl`.
145
+ * @throws {Error} unless `parentEl` names an element in the page.
146
+ */
147
+ function requireMountHost(who, parentEl) {
148
+ var named = typeof parentEl === 'string' && parentEl.length > 0;
149
+ var hasDoc = typeof document !== 'undefined' && document !== null &&
150
+ typeof document.getElementById === 'function';
151
+ var host = (named && hasDoc) ? document.getElementById(parentEl) : null;
152
+ if (host) { return host; }
153
+ var got = !named ? (typeof parentEl === 'string' ? '""' : String(parentEl))
154
+ : !hasDoc ? JSON.stringify(parentEl) + ', and there is no document'
155
+ : JSON.stringify(parentEl) + ', which names no element in the page';
156
+ throw new Error(who + ': canvasParentId is required and must name an element in the page (got ' +
157
+ got + '). The runner hands it as init({ parentEl }) from new FrqtlApp({ canvasParentId }) / ' +
158
+ 'new RecipeContext({ canvasParentId }). There is no default mount: a canvas appended to ' +
159
+ 'document.body is a canvas the layout never placed. (FAIL-LOUD.)');
160
+ }
161
+
162
+ /**
163
+ * The attribute that marks a document's scene drawing surface — the ONE DOM-checkable fact
164
+ * about a provider's surface. `FrqtlApp.createCanvas` (the one mount site) stamps it on the
165
+ * canvas `provider.init` returned; anything that must find the scene's pixels (a paint
166
+ * oracle, the eyes) reads `[SCENE_SURFACE_ATTR]` instead of guessing among a document's
167
+ * canvases. Re-exported to consumers above the engine by `lib/shim/render/render-stages.js`.
168
+ * @type {string}
169
+ */
170
+ var SCENE_SURFACE_ATTR = 'data-frqtl-scene-surface';
171
+
172
+ /**
173
+ * The `init` return rule: `provider.init(opts)` RETURNS its drawing surface as an
174
+ * `HTMLCanvasElement` (RENDERER_CONTRACT §1). Duck-typed on the element's own shape (tag
175
+ * CANVAS + `setAttribute` + `getContext`), never `instanceof`, so a canvas made in another
176
+ * realm (an iframe, a notebook realm) is still a canvas.
177
+ *
178
+ * @param {string} who - the calling site, e.g. `'FrqtlApp.createCanvas'`.
179
+ * @param {*} el - what `provider.init` returned.
180
+ * @returns {HTMLCanvasElement} `el`, unchanged.
181
+ * @throws {Error} unless `el` is a canvas element.
182
+ */
183
+ function requireSceneSurface(who, el) {
184
+ var isCanvas = el !== null && typeof el === 'object' &&
185
+ typeof el.tagName === 'string' && el.tagName.toUpperCase() === 'CANVAS' &&
186
+ typeof el.setAttribute === 'function' && typeof el.getContext === 'function';
187
+ if (isCanvas) { return el; }
188
+ var got = (el === null || el === undefined) ? String(el)
189
+ : (typeof el === 'object' && typeof el.tagName === 'string') ? '<' + el.tagName.toLowerCase() + '>'
190
+ : (typeof el === 'object' && el.elt !== undefined) ? 'a p5.Element (return its .elt)'
191
+ : typeof el;
192
+ throw new Error(who + ': provider.init(opts) must RETURN its drawing surface as an HTMLCanvasElement ' +
193
+ '(got ' + got + '). The runner stamps ' + SCENE_SURFACE_ATTR + ' on it so the scene surface ' +
194
+ 'is declared, never guessed among the document\'s canvases (RENDERER_CONTRACT §1, init row; ' +
195
+ 'owner: lib/frqtl/compat/renderer/renderer-provider-contract.js).');
196
+ }
197
+
198
+ /**
199
+ * Ticks per second of filter time. The GYM runs a scene at a 60 TPS cap by default
200
+ * (CompareView's TPS field), so there a filter animated by `uTime` keeps the pace it had
201
+ * when it was fed wall-clock seconds; at any other rate it runs at that rate's pace.
202
+ * @type {number}
203
+ */
204
+ var FILTER_TICKS_PER_SECOND = 60;
205
+
206
+ /**
207
+ * The `uTime` filter uniform: seconds of SIMULATION time at the tick being rendered,
208
+ * `universeData.tick / FILTER_TICKS_PER_SECOND`. Every provider feeds every filter pass this
209
+ * value, so a filter's animation is a function of the tick: a still of one scene at one
210
+ * tick is reproducible, both providers agree, and paused water stops moving. Not wrapped:
211
+ * a wrap would be a visible jump in the animation.
212
+ *
213
+ * @param {object} tickData - the TickData the provider is drawing (carries `universeData.tick`).
214
+ * @returns {number} seconds of simulation time.
215
+ * @throws {Error} when `tickData` carries no finite non-negative tick (nothing to time the
216
+ * filter by: a wall-clock fallback would make the still unreproducible again).
217
+ */
218
+ function filterTime(tickData) {
219
+ var tick = tickData && tickData.universeData && tickData.universeData.tick;
220
+ if (typeof tick === 'number' && isFinite(tick) && tick >= 0) {
221
+ return tick / FILTER_TICKS_PER_SECOND;
222
+ }
223
+ throw new Error('filterTime: the TickData being rendered must carry universeData.tick (got ' +
224
+ String(tick) + '). uTime is simulation time at the rendered tick, never the wall clock ' +
225
+ '(owner: lib/frqtl/compat/renderer/renderer-provider-contract.js). (FAIL-LOUD.)');
226
+ }
227
+
228
+ export {
229
+ RENDERER_PROVIDER_REQUIRED,
230
+ RENDERER_PROVIDER_OPTIONAL,
231
+ SCENE_SURFACE_ATTR,
232
+ validateRendererProvider,
233
+ requireProviderSize,
234
+ requireMountHost,
235
+ requireSceneSurface,
236
+ FILTER_TICKS_PER_SECOND,
237
+ filterTime
238
+ };
@@ -0,0 +1,172 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * unprojectStill.js — THE pixel→universe mapping authority for a CAPTURED still (R5).
5
+ *
6
+ * Peer of `FieldBuilder.js` (the one field source) and `captureStill-common.js` (the one
7
+ * readback): a single canonical module every consumer FERRIES instead of writing its own.
8
+ *
9
+ * ── Why this exists ──────────────────────────────────────────────────────────
10
+ *
11
+ * "Which universe pixel is this image pixel?" is the question every measurement over a
12
+ * capture actually asks — ring radii, fringe spacing, an object's apparent position. An
13
+ * OFFLINE corpus consumer cannot ask the live renderer: there is no `screenToWorld` in a
14
+ * JSONL row. And for a perspective camera the pose bag plus the buffer dims are NOT enough —
15
+ * the mapping is underdetermined without the camera intrinsics. That is precisely why
16
+ * eyeballed measurements over screenshots come out shaky.
17
+ *
18
+ * So the scene record carries `projection` (the intrinsics) and this module composes it with
19
+ * the still's `pose` and dims into an unambiguous answer. It is a CONTRACT FUNCTION, not
20
+ * prose: a consumer's hand-written unproject drifting from the provider's is the
21
+ * schema-mirror bug applied to MATH, and it is silent — it returns plausible wrong numbers.
22
+ *
23
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
24
+ *
25
+ * `UnprojectStill.unprojectStill(scene, still, bufX, bufY, worldZ?) → {x, y} | null`
26
+ *
27
+ * @param {object} scene — a SceneRecord (`projection`, `physicsW`, `physicsH`).
28
+ * @param {object} still — a still record (`width`, `height`, `cssWidth`, `cssHeight`,
29
+ * `pose`). Must be an `rgba8` still: the `framedata` and `r8` channels are not rasters
30
+ * of the composited canvas, so a canvas-pixel unprojection is meaningless for them and
31
+ * THROWS rather than returning a number nobody should trust. (For `r8`, use
32
+ * `fieldTexelToPhysics` below — the field has its own, simpler map.)
33
+ * @param {number} bufX,bufY — coordinates in the still's OWN BUFFER (i.e. an array index
34
+ * pair: `data[(bufY*width + bufX)*4]`). That is what a corpus consumer actually holds —
35
+ * not a CSS coordinate. The conversion to CSS units happens here, once.
36
+ * @param {number} [worldZ=0] — the universe z-plane to intersect (3D only; ignored in 2D).
37
+ * @returns {{x:number,y:number}|null} physics-pixel point, or `null` when the ray cannot
38
+ * resolve (parallel to the plane, or beyond the scene-scale sanity bound).
39
+ *
40
+ * `UnprojectStill.fieldTexelToPhysics(scene, still, texX, texY) → {x, y}`
41
+ * The `r8` channel's own map: field texel → physics pixel, the exact inverse of the scale
42
+ * `FieldBuilder` applied (`fw/canvasW`, `fh/canvasH`). Separate function because the field
43
+ * is built in physics space and never passes through a camera — conflating it with the
44
+ * canvas unprojection is how a radial profile silently acquires a perspective error.
45
+ *
46
+ * ── FAIL-LOUD ────────────────────────────────────────────────────────────────
47
+ *
48
+ * A `null`/unknown `projection` THROWS naming the gap (a BYO provider that declares no
49
+ * intrinsics cannot be unprojected — and guessing `affine2d` under a perspective camera is
50
+ * the exact "silent 2D-affine fallback" the renderer contract already bans). A
51
+ * `kind:'perspective'` still with no `pose` THROWS. A wrong-format still THROWS. None of
52
+ * these returns a plausible wrong number.
53
+ *
54
+ * ── Layer 2: physics behaviour ───────────────────────────────────────────────
55
+ *
56
+ * ZERO physics: a coordinate transform at the output boundary. It reads records; it cannot
57
+ * touch a frame, a dimension or a tick.
58
+ */
59
+
60
+ import * as ThreeCameraGeometry from './providers/three-camera-geometry.js';
61
+ import * as Camera2dGeometry from './camera2d-geometry.js';
62
+
63
+
64
+
65
+ function _fail(msg) {
66
+ throw new Error('[unprojectStill] ' + msg + ' (FAIL-LOUD — never a plausible wrong point.)');
67
+ }
68
+
69
+
70
+ /**
71
+ * The 2D camera map — the SAME authority the live p5 provider draws and picks with. Ferried,
72
+ * never re-derived: a second copy of the pan/zoom terms is the schema-mirror bug applied to
73
+ * MATH, and it fails silently.
74
+ */
75
+
76
+ function _num(v, name) {
77
+ if (typeof v !== 'number' || !isFinite(v)) { _fail(name + ' must be a finite number, got ' + String(v)); }
78
+ return v;
79
+ }
80
+
81
+ function _requireRaster(still) {
82
+ if (!still || typeof still !== 'object') { _fail('expected a still record, got ' + String(still)); }
83
+ if (still.format !== 'rgba8') {
84
+ _fail('this still\'s format is ' + JSON.stringify(still.format) + ', not "rgba8" — the ' +
85
+ 'framedata and r8 channels are not rasters of the composited canvas, so a ' +
86
+ 'canvas-pixel unprojection has no meaning for them (for r8 use fieldTexelToPhysics)');
87
+ }
88
+ return still;
89
+ }
90
+
91
+ /**
92
+ * unprojectStill — see the module docstring.
93
+ * @returns {{x:number,y:number}|null}
94
+ */
95
+ function unprojectStill(scene, still, bufX, bufY, worldZ) {
96
+ if (!scene || typeof scene !== 'object') { _fail('expected a SceneRecord, got ' + String(scene)); }
97
+ _requireRaster(still);
98
+ var proj = scene.projection;
99
+ if (!proj || typeof proj.kind !== 'string') {
100
+ _fail('the SceneRecord carries no `projection` — its provider declared no intrinsics, ' +
101
+ 'so this capture cannot be unprojected. Guessing an affine map under a ' +
102
+ 'perspective camera would compute the WRONG point silently');
103
+ }
104
+ var pw = _num(scene.physicsW, 'scene.physicsW');
105
+ var ph = _num(scene.physicsH, 'scene.physicsH');
106
+ var bw = _num(still.width, 'still.width');
107
+ var bh = _num(still.height, 'still.height');
108
+ _num(bufX, 'bufX'); _num(bufY, 'bufY');
109
+
110
+ if (proj.kind === 'affine2d') {
111
+ // The 2D map composes (buffer → canvas) with the CAMERA's pan/zoom, and it DELEGATES
112
+ // to the same `camera2d-geometry` authority the live provider draws and picks with —
113
+ // so an offline measurement cannot drift from the picture. The still's own `pose`
114
+ // supplies {distance, target}; at the default pose the scale is 1 and this reduces
115
+ // EXACTLY to the previous pure ratio, so unzoomed captures are byte-unchanged.
116
+ //
117
+ // This is load-bearing, not tidiness: without the pose terms a ZOOMED 2D still would
118
+ // unproject as though it showed the whole universe — a silently wrong answer of
119
+ // exactly the kind that made screenshot measurements untrustworthy.
120
+ return Camera2dGeometry.screenToWorld2d(still.pose, pw, ph, bufX, bufY, bw, bh);
121
+ }
122
+
123
+ if (proj.kind === 'perspective') {
124
+ var pose = still.pose;
125
+ if (!pose || typeof pose !== 'object') {
126
+ _fail('a perspective still needs its `pose` provenance to unproject — a 3D capture ' +
127
+ 'with a null pose is not comparable run-to-run and is not acceptance evidence');
128
+ }
129
+ var fovY = _num(proj.fovY, 'projection.fovY');
130
+ // Buffer px → CSS px (the units the pinhole authority takes). cssWidth/Height default
131
+ // to the buffer dims when the capture had no distinct CSS size (headless).
132
+ var cw = (typeof still.cssWidth === 'number' && still.cssWidth > 0) ? still.cssWidth : bw;
133
+ var ch = (typeof still.cssHeight === 'number' && still.cssHeight > 0) ? still.cssHeight : bh;
134
+ var cssX = bufX * (cw / bw);
135
+ var cssY = bufY * (ch / bh);
136
+ // ONE mapping authority: delegate to the SAME analytic pinhole the live provider's
137
+ // screenToWorld uses, with the captured fovY passed as data.
138
+ return ThreeCameraGeometry._screenToWorldThree(
139
+ pose, pw, ph, cssX, cssY, cw, ch, (typeof worldZ === 'number') ? worldZ : 0, fovY);
140
+ }
141
+
142
+ _fail('unknown projection kind ' + JSON.stringify(proj.kind) +
143
+ ' — known kinds: affine2d, perspective');
144
+ }
145
+
146
+ /**
147
+ * fieldTexelToPhysics — the `r8` channel's own map (the exact inverse of FieldBuilder's scale).
148
+ * @param {object} scene @param {object} still @param {number} texX @param {number} texY
149
+ * @returns {{x:number,y:number}}
150
+ */
151
+ function fieldTexelToPhysics(scene, still, texX, texY) {
152
+ if (!scene || typeof scene !== 'object') { _fail('expected a SceneRecord, got ' + String(scene)); }
153
+ if (!still || still.format !== 'r8') {
154
+ _fail('fieldTexelToPhysics expects an "r8" (field) still, got ' +
155
+ JSON.stringify(still && still.format));
156
+ }
157
+ var pw = _num(scene.physicsW, 'scene.physicsW');
158
+ var ph = _num(scene.physicsH, 'scene.physicsH');
159
+ var fw = _num(still.width, 'still.width');
160
+ var fh = _num(still.height, 'still.height');
161
+ _num(texX, 'texX'); _num(texY, 'texY');
162
+ // FieldBuilder maps physics → texel by (fw/canvasW, fh/canvasH); this is that inverse.
163
+ // The field never passes through a camera, which is exactly why it is a separate map.
164
+ return { x: texX * (pw / fw), y: texY * (ph / fh) };
165
+ }
166
+
167
+ // ── Export ────────────────────────────────────────────────────────────────────
168
+
169
+ export {
170
+ unprojectStill,
171
+ fieldTexelToPhysics
172
+ };