@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,710 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * capture.js — THE render-pipeline tap MECHANISM, as an INSTRUMENT on the primitive
5
+ * surface (THE_MINIMAL_SIM.md §4; home = the primitive surface).
6
+ *
7
+ * `capture(stage)` is the ONE generic read-at-a-named-stage mechanism; `captureStill()` is
8
+ * its last-stage CONVENIENCE; `sceneRecord()` is the run-invariant half of the normalized
9
+ * format. All dispatch, all throw rules and ALL provenance stamping live HERE and nowhere
10
+ *
11
+ * ── Why this is not on the host ──────────────────────────────────────────────
12
+ *
13
+ * A tap is not an editor concern. This mechanism was resident in `FrqtlApp` (the editor
14
+ * host) purely by history, which made "eyes" a BROWSER privilege: nothing in reading a
15
+ * stage needs a DOM, a page or a canvas — it needs the loop's retained tick, the active
16
+ * provider, the scene identity, and the live render-options bag. Those four are the whole
17
+ * binding, so the instrument constructs in a bare node process with no host at all:
18
+ *
19
+ * FrqtlCapture.create(loop, providerGetter, identity, renderOptionsGetter)
20
+ *
21
+ * @param loop — anything exposing `lastTickData` (the retained TickData).
22
+ * `SimLoop` after the §4 split; the `FrqtlApp` host today.
23
+ * The instrument NEVER advances the clock: it does not call
24
+ * `step()`/`doTick()`, so a tap can never perturb tick order.
25
+ * @param providerGetter — `() => RendererProvider|null`. The ONE renderer authority,
26
+ * read live (a provider that resolves later is picked up on
27
+ * the next capture). `null` is a legitimate state — the
28
+ * `tick` and `field` stages are backend-independent and work
29
+ * with no renderer at all.
30
+ * @param identity — `{ seed, rect, sourceRef? }`. The seed is FERRIED from the
31
+ * party that handed it to the universe (one seed authority,
32
+ * never re-derived here); `rect` is read LIVE so a host that
33
+ * re-rects its canvas cannot leave a stale scene record.
34
+ * @param renderOptionsGetter — `() => renderOptionsBag`. The ONE LIVE bag `setRenderOption`
35
+ * writes, read THROUGH the getter and never copied — a copy
36
+ * here would be a second options authority whose provenance
37
+ * silently stopped tracking the picture.
38
+ *
39
+ * ── One stage the local process genuinely cannot execute ─────────────────────
40
+ *
41
+ * `tick` and `field` are backend-independent, so a bare node process taps them with no
42
+ * renderer at all. `composite` is different in kind: it IS the render stack's output, and
43
+ * re-drawing it with a node raster library would show the agent a different picture than
44
+ * the user sees (V3 — a god-rays shader turns particles into A SUN, and the
45
+ * shader is part of the experiment, not decoration). So `composite` is executed by a
46
+ * CAPTURE HOST — a remote stage executor running the real stack in a managed headless
47
+ * browser — attached via `attachCaptureHost()`. The KIND, its boundary codec and its
48
+ * per-still verification live in the sibling `instruments/capture-host.js`.
49
+ *
50
+ * Two upward VOCABULARY reads (`RenderStages`, `FieldBuilder`) resolve at CALL time through
51
+ * named FAIL-LOUD guards — the same seam every game-layer object uses for a shim/compat
52
+ * contract. They are leaf tables and executors, never machinery, and they are resolved
53
+ * rather than imported so this file adds no arrow.
54
+ *
55
+ * ── Layer 2: physics behaviour ───────────────────────────────────────────────
56
+ * ZERO physics. This is the output boundary reading its own output — no frame, dimension,
57
+ * tick or op-program is touched, so no canonical-bench golden can move because of it.
58
+ *
59
+ * OBSERVATION ONLY. A tap cannot perturb the pipeline. Pipeline CONFIGURATION stays exactly
60
+ * where it already lives — `setFilterActive` / `setRenderOption` —
61
+ * so "capture with the water shader off" composes existing knobs (setFilterActive('water',
62
+ * false) → repaint() → capture → restore) rather than growing a second filter authority on
63
+ * the capture call.
64
+ */
65
+
66
+ import * as RenderStages from '../../../shim/render/render-stages.js';
67
+ import { FieldBuilder } from '../../compat/renderer/FieldBuilder.js';
68
+ import { FrqtlCaptureHost } from './capture-host.js';
69
+ import { engine, engineState } from '../../compat/engine.js';
70
+ import { loadedWasmSha256 } from '../../compat/providers/wasm-load-browser.js';
71
+ import * as FrqtlDisplay from '../../compat/renderer/display-slot.js';
72
+
73
+
74
+ /**
75
+ * The experiment identity the page ALREADY carries, ferried — not a second authority.
76
+ *
77
+ * The experiments registry DEFINES an experiment id as "the directory name under
78
+ * experiments/" (`experimentById`), and a canonical page is served from
79
+ * `…/experiments/<id>/index.html`. So the path segment IS the registry id, and reading it
80
+ * ferries the existing identity rather than minting a parallel one. The registry module
81
+ * is a registry, not a page fact, which is why the id is taken from the URL the registry's
82
+ * own ids are defined by.
83
+ *
84
+ * Fails SOFT to null (no DOM, a notebook realm, an ad-hoc page): `sourceRef: null` is the
85
+ * honest "this scene has no authored identity" value, and the corpus row then carries the
86
+ * code itself. A caller with a better identity overrides via `setSourceRef()`.
87
+ * @private
88
+ */
89
+ function _defaultSourceRef() {
90
+ try {
91
+ if (typeof location === 'undefined' || !location) { return null; }
92
+ var m = String(location.pathname).match(/\/experiments\/([^/]+)\//);
93
+ return (m && m[1]) ? m[1] : null;
94
+ } catch (e) { return null; }
95
+ }
96
+
97
+ /**
98
+ * The RASTER PATH pin — which GL backend this browser actually rasters with.
99
+ *
100
+ * Measured, not assumed: a
101
+ * `composite` still is bit-reproducible run-to-run WITHIN a pinned (machine, driver,
102
+ * browser build) — under software SwiftShader AND under hardware ANGLE/Metal alike —
103
+ * but the software and hardware pictures differ (0.42% of pixels raw, 1.75% with a
104
+ * bloom pass). Sub-perceptual, and real. So the policy is bit-exact within a path and
105
+ * tolerance across paths, which only works if a row SAYS which path drew it: rows from
106
+ * different raster paths must refuse to mix BY IDENTITY, not by luck.
107
+ *
108
+ * It rides `build` because it is exactly what `build` is for — a runtime pin DERIVED
109
+ * from the environment, never a caller-set flag (a hand-set `software:true` would be
110
+ * the truth-in-labeling violation this field exists to prevent). The value is the
111
+ * browser's own `UNMASKED_RENDERER_WEBGL` string, which separates `SwiftShader driver`
112
+ * from `ANGLE Metal Renderer: Apple M4 Pro` exactly.
113
+ *
114
+ * MEMOIZED per process: it cannot change within a run, and creating a GL context per
115
+ * `sceneRecord()` call would be an absurd cost on a memoized-by-design record. `null`
116
+ * where there is no WebGL to ask (node, or a browser withholding the debug extension) —
117
+ * honestly absent, exactly like the three sibling pins.
118
+ * @private
119
+ */
120
+ var _rasterPathMemo; // undefined = not yet read (null is a legitimate answer)
121
+ function _rasterPath() {
122
+ if (_rasterPathMemo !== undefined) { return _rasterPathMemo; }
123
+ _rasterPathMemo = null;
124
+ try {
125
+ if (typeof document !== 'undefined' && document.createElement) {
126
+ var c = document.createElement('canvas');
127
+ var gl = c.getContext('webgl2') || c.getContext('webgl');
128
+ var ext = gl && gl.getExtension('WEBGL_debug_renderer_info');
129
+ if (ext) { _rasterPathMemo = String(gl.getParameter(ext.UNMASKED_RENDERER_WEBGL)); }
130
+ }
131
+ } catch (e) { _rasterPathMemo = null; }
132
+ return _rasterPathMemo;
133
+ }
134
+
135
+ /**
136
+ * The TICK-PROVENANCE GUARD for a raster the provider executes: the stamp is the pixels'
137
+ * tick, or the capture THROWS.
138
+ *
139
+ * `tick` is stamped off the LOOP's retained TickData when the capture is requested; the
140
+ * pixels are whatever the provider LAST composited, read later. The two diverge whenever
141
+ * the clock ticks without a draw — the rAF loop runs a tick batch (listeners included)
142
+ * BEFORE its one render, so a batched frame composites only its last tick, and the first
143
+ * frame's burst composites only the burst's last. A still labelled with a tick its pixels
144
+ * are not is a falsified physics record, so the provider REPORTS the tick it drew
145
+ * (`renderedTick`, §1a.4) and this compares. The dispatcher takes the readback in the
146
+ * stamp's task, so a between-frame caller always agrees; what is left to
147
+ * refuse is a capture from inside a tick batch (or a genuinely asynchronous BYO
148
+ * readback). There is no reconciling branch: the remedy is the caller's clock
149
+ * discipline, named in the message.
150
+ * @private
151
+ */
152
+ function _assertComposited(stage, tick, bag) {
153
+ var rendered = bag.renderedTick;
154
+ if (!(typeof rendered === 'number' && isFinite(rendered))) {
155
+ throw new Error('[FrqtlCapture.capture] the provider\'s captureStill() bag reports ' +
156
+ 'no renderedTick (got ' + String(rendered) + ') — the tick of the TickData its ' +
157
+ 'pixels were drawn from is REQUIRED (RENDERER_CONTRACT §1a.4), because without ' +
158
+ 'it a still stamped tick ' + tick + ' cannot be shown to carry tick ' + tick +
159
+ '\'s picture. (FAIL-LOUD.)');
160
+ }
161
+ if (rendered !== tick) {
162
+ throw new Error('[FrqtlCapture.capture] PROVENANCE MISMATCH at stage \'' + stage +
163
+ '\': the still would be stamped tick ' + tick + ' (the loop\'s retained TickData) ' +
164
+ 'but the provider composited tick ' + rendered + ' — these pixels are not tick ' +
165
+ tick + '\'s. Capture a tick that was RENDERED: drive the clock with runner.step() ' +
166
+ '(it renders, then notifies), or HOLD it from a tick listener — ' +
167
+ 'runner.suspendTicks(), runner.repaint() (forces the draw; an fps cap skips ' +
168
+ 'display frames), capture, runner.resumeTicks() in the capture\'s finally; ' +
169
+ 'outside a listener, runner.repaint() then capture in the same task. ' +
170
+ '(FAIL-LOUD — a still whose label is not its pixels is a falsified record.)');
171
+ }
172
+ }
173
+
174
+ // ── The instrument ────────────────────────────────────────────────────────
175
+
176
+ /**
177
+ * @param {{lastTickData: object}} loop
178
+ * @param {function(): object|null} providerGetter
179
+ * @param {{seed: *, rect: object, sourceRef: (string|null|undefined)}} identity
180
+ * @param {function(): object} renderOptionsGetter
181
+ */
182
+ function CaptureInstrument(loop, providerGetter, identity, renderOptionsGetter) {
183
+ if (!loop || typeof loop !== 'object') {
184
+ throw new Error('[FrqtlCapture.create] argument 1 must be the LOOP — anything ' +
185
+ 'exposing `lastTickData` (SimLoop / the FrqtlApp host). (FAIL-LOUD.)');
186
+ }
187
+ if (typeof providerGetter !== 'function') {
188
+ throw new Error('[FrqtlCapture.create] argument 2 must be a providerGetter ' +
189
+ 'function returning the active RendererProvider or null — the provider is read ' +
190
+ 'LIVE, never captured at bind time. (FAIL-LOUD.)');
191
+ }
192
+ if (!identity || typeof identity !== 'object' || identity.seed === undefined || !identity.rect) {
193
+ throw new Error('[FrqtlCapture.create] argument 3 must be the identity bag ' +
194
+ '{ seed, rect, sourceRef? } — `seed` is FERRIED from the party that handed it to ' +
195
+ 'the universe and `rect` is the physics canvas. (FAIL-LOUD.)');
196
+ }
197
+ if (typeof renderOptionsGetter !== 'function') {
198
+ throw new Error('[FrqtlCapture.create] argument 4 must be a renderOptionsGetter ' +
199
+ 'function returning the ONE LIVE render-options bag — read through the getter, ' +
200
+ 'never copied (a copy is a second options authority). (FAIL-LOUD.)');
201
+ }
202
+ this._loop = loop;
203
+ this._providerGetter = providerGetter;
204
+ this._identity = identity;
205
+ this._renderOptionsGetter = renderOptionsGetter;
206
+ // Experiment/rung identity for a SceneRecord, registered ONCE at setup by the page or
207
+ // harness via setSourceRef(). null is HONEST (an ad-hoc universe); the corpus row then
208
+ // carries the code itself. Never per-capture caller-supplied provenance.
209
+ this._sourceRef = (identity.sourceRef === undefined || identity.sourceRef === null)
210
+ ? _defaultSourceRef() : String(identity.sourceRef);
211
+ // The code identity (setSourceSha256) — null until a door that linked a project registers it.
212
+ this._sourceSha256 = null;
213
+ // Memo for the content-addressed SceneRecord — see sceneRecord().
214
+ this._sceneMemo = null;
215
+ // The attached CAPTURE HOST (the remote stage-executor KIND), or null. See
216
+ // attachCaptureHost() and instruments/capture-host.js.
217
+ this._host = null;
218
+ }
219
+
220
+ /**
221
+ * attachCaptureHost(host) — attach a REMOTE stage executor (the capture-host KIND).
222
+ *
223
+ * The KIND, its boundary codec and its verification live in the sibling
224
+ * `instruments/capture-host.js`; this method is the seam that lets a stage the LOCAL
225
+ * process cannot execute be executed elsewhere without a second dispatcher. The
226
+ * canonical implementation is the hosted headless browser (`instruments/hosted-eyes.js`)
227
+ * that runs the REAL render stack, so `capture('composite')` works from node through the
228
+ * real renderer and its shaders, drawing the scene source plus the renderState it was handed.
229
+ *
230
+ * ATTACHING EXTENDS THIS INSTRUMENT'S IDENTITY (a.3): from here on `_buildIdentity()`
231
+ * and `_projection()` report the HOST'S page-resolved values, so the scene identity this
232
+ * process computes and the one the page stamps are equal by construction rather than by
233
+ * a comparison someone later loosens.
234
+ *
235
+ * FAIL-LOUD when the live provider already executes a stage the host claims: two
236
+ * executors for one stage is two truths about one picture, and the honest moment to say
237
+ * so is attach time, not the first mismatch.
238
+ *
239
+ * @param {object} host — see instruments/capture-host.js for the KIND
240
+ * @returns {object} the same host
241
+ */
242
+ CaptureInstrument.prototype.attachCaptureHost = function (host) {
243
+ var CH = FrqtlCaptureHost;
244
+ CH.assertCaptureHost(host);
245
+ var prov = this._provider();
246
+ if (prov && typeof prov.captureStill === 'function' && host.stages.indexOf('composite') !== -1) {
247
+ throw new Error('[FrqtlCapture.attachCaptureHost] this instrument already has a ' +
248
+ 'display provider that implements captureStill(), so the `composite` stage is ' +
249
+ 'already executed locally. A capture host is for a stage this process CANNOT ' +
250
+ 'execute; attaching one here would mint a second executor — two truths about ' +
251
+ 'one picture. (FAIL-LOUD.)');
252
+ }
253
+ this._host = host;
254
+ // The identity just changed (build/projection now come from the page), so the memo
255
+ // computed under the old identity must not survive — a stale memo here would hand
256
+ // out the pre-attachment hash and every verification would fail for the right
257
+ // reason with the wrong explanation.
258
+ this._sceneMemo = null;
259
+ return host;
260
+ };
261
+
262
+ /**
263
+ * captureHost() — the attached capture host, or null when this instrument executes its
264
+ * stages locally. `FrqtlVision.record` reads it to forward a recording to the process
265
+ * where the world and its clock are (the host's `record` member).
266
+ * @returns {object|null}
267
+ */
268
+ CaptureInstrument.prototype.captureHost = function () {
269
+ return this._host;
270
+ };
271
+
272
+ /** The live provider, or null. @private */
273
+ CaptureInstrument.prototype._provider = function () {
274
+ return this._providerGetter() || null;
275
+ };
276
+
277
+ /**
278
+ * setSourceRef(ref) — register the experiment/rung identity for the SceneRecord.
279
+ *
280
+ * Called ONCE at setup by the page or harness (a legitimate authority set at attach
281
+ * time), NEVER per capture — a per-capture identity would be caller-supplied provenance,
282
+ * the hardcoded-label anti-pattern. Absent ⇒ `sourceRef: null`, which is honest for an
283
+ * ad-hoc universe: such a scene is REPLAY-grade, not REGENERATE-grade, and the corpus row
284
+ * must carry the code itself.
285
+ * @param {string|null} ref
286
+ */
287
+ CaptureInstrument.prototype.setSourceRef = function (ref) {
288
+ this._sourceRef = (ref === undefined || ref === null) ? null : String(ref);
289
+ };
290
+
291
+ /**
292
+ * setSourceSha256(sha) — register the CODE identity for the SceneRecord: the project identity
293
+ * (lib/shim/project/identity.js projectIdentity) of the project a door linked and built. Called
294
+ * ONCE by that door (bootProject, the empty page's applyModuleSource), never per capture; null
295
+ * (the default) is honest for a sketch booted as a function. Validated by the record's owner.
296
+ * @param {string|null} sha
297
+ */
298
+ CaptureInstrument.prototype.setSourceSha256 = function (sha) {
299
+ this._sourceSha256 = RenderStages.assertSourceSha256(sha);
300
+ };
301
+
302
+ /**
303
+ * The runtime BUILD pin, DERIVED from the boot authorities — never a literal.
304
+ *
305
+ * Pixel-exact regeneration pins the builds, not just the code+seed. What is genuinely
306
+ * derivable at runtime today is the boot identity: the engine the boot booted (the engine
307
+ * handle's own `backend`, compat/engine.js), the renderer the boot selected (the display slot,
308
+ * compat/renderer/display-slot.js), and the CONTENT hash of the wasm binary the browser loader
309
+ * instantiated (`loadedWasmSha256()`, compat/providers/wasm-load-browser.js — the `PINNED.json`
310
+ * `wasmSha256` definition). Never the package URL: the serving origin is a deployment fact, so the same
311
+ * bytes from two hosts must regenerate ONE scene identity. Each is `null` when the
312
+ * authority is absent (a headless harness) — honestly absent, never invented; a hand-typed
313
+ * version string here would be the truth-in-labeling violation (dep-map rule 4) that this
314
+ * method exists to avoid.
315
+ * @private
316
+ */
317
+ CaptureInstrument.prototype._buildIdentity = function () {
318
+ // A capture host is attached ⇒ the picture is produced in the PAGE, so the page's
319
+ // build pin is the truthful one (a.3 identity convergence — see attachCaptureHost).
320
+ if (this._host) { return this._host.identity().build || null; }
321
+ // The PAGE's facts: present only in a realm where the BROWSER loader instantiated a binary
322
+ // (its content hash is recorded — null until then). Every other realm — node, a notebook
323
+ // test — carries nulls, exactly as its record always has (identity convergence above
324
+ // extends a node record through a host).
325
+ var sha = loadedWasmSha256();
326
+ var paged = sha !== null;
327
+ return {
328
+ engine: (paged && engineState() === 'booted') ? String(engine().backend) : null,
329
+ renderer: (paged && FrqtlDisplay.name()) ? String(FrqtlDisplay.name()) : null,
330
+ wasmSha256: paged ? sha : null,
331
+ gl: _rasterPath()
332
+ };
333
+ };
334
+
335
+ /**
336
+ * The camera intrinsics for the scene, from the provider's OPTIONAL `projection()`.
337
+ *
338
+ * Without them a 3D pixel→world mapping is UNDERDETERMINED offline: an offline corpus
339
+ * consumer has no live `screenToWorld`, and the pose bag plus buffer dims are not enough
340
+ * for a perspective camera. `null` when the provider does not declare the capability (a
341
+ * BYO renderer) — honest, and `unprojectStill` then FAILS LOUD at use rather than
342
+ * computing a wrong point.
343
+ * @private
344
+ */
345
+ CaptureInstrument.prototype._projection = function () {
346
+ // Hosted: the camera that made the picture is the PAGE's, so its intrinsics are the
347
+ // only ones that describe this still's pixel→world map (a.3).
348
+ if (this._host) {
349
+ var ident = this._host.identity();
350
+ return (ident && ident.projection) ? ident.projection : null;
351
+ }
352
+ var prov = this._provider();
353
+ if (!prov || typeof prov.projection !== 'function') { return null; }
354
+ return prov.projection();
355
+ };
356
+
357
+ /**
358
+ * The ONE camera authority, read (never re-derived) — through the PROVIDER capability,
359
+ * which is where the camera handle actually lives. The host's `runner.cameraControl`
360
+ * memo is the same handle wearing a host-facing convenience; reading it back up through
361
+ * the host would be an upward coupling for no extra truth. `null` when the active
362
+ * provider exposes no camera pose control at all (a BYO renderer): without a pose a 3D
363
+ * composite cannot be co-registered to the field, and for VLA data the pose is
364
+ * load-bearing, so absent-and-honest is the only alternative to wrong.
365
+ * @private
366
+ */
367
+ CaptureInstrument.prototype._pose = function () {
368
+ var prov = this._provider();
369
+ if (!prov || typeof prov.getCameraControl !== 'function') { return null; }
370
+ var cam = prov.getCameraControl(); // null in a provider with no pose control
371
+ return cam ? cam.getOrbit() : null;
372
+ };
373
+
374
+ /**
375
+ * sceneRecord() → Promise<SceneRecord + its own content id>
376
+ *
377
+ * The RUN-INVARIANT half of the normalized format: the regeneration input
378
+ * (`seed`, `sourceRef`, `build`, `physicsW/H`, `projection`), IMMUTABLE and
379
+ * CONTENT-ADDRESSED as `scene = sha256(canonicalize(record))`. A corpus stores ONE of
380
+ * these plus N light stills that reference it by hash, instead of repeating a
381
+ * high-bandwidth header on every capture.
382
+ *
383
+ * SELF-INVALIDATING BY CONSTRUCTION: the record is rebuilt from the live authorities on
384
+ * every call (seven field reads) and re-hashed ONLY when its canonical form actually
385
+ * changes. So there is no `invalidateScene()` to forget to call, and a record is never
386
+ * stale. Scene records being immutable, a change mints a NEW identity rather than
387
+ * revising one — under content-addressing those are the same operation.
388
+ *
389
+ * The returned object carries its own `scene` id alongside the record for convenience;
390
+ * `canonicalizeScene` PROJECTS onto `SCENE_KEYS`, so that id cannot perturb its own hash.
391
+ *
392
+ * @returns {Promise<object>} the record plus `scene`
393
+ * @throws {Error} (as a rejection) if the seed authority is unreachable, or
394
+ * `crypto.subtle` is unavailable — never a silently unidentified scene.
395
+ */
396
+ CaptureInstrument.prototype.sceneRecord = function () {
397
+ var RS = RenderStages;
398
+ var self = this;
399
+ var record;
400
+ try {
401
+ var rect = this._identity.rect;
402
+ record = RS.normalizeScene({
403
+ seed: this._identity.seed,
404
+ sourceRef: this._sourceRef,
405
+ sourceSha256: this._sourceSha256,
406
+ build: this._buildIdentity(),
407
+ physicsW: rect.w,
408
+ physicsH: rect.h,
409
+ projection: this._projection()
410
+ });
411
+ } catch (e) { return Promise.reject(e); }
412
+ var canon = RS.canonicalizeScene(record);
413
+ if (this._sceneMemo && this._sceneMemo.canon === canon) {
414
+ // COPY, never the memo object itself. Handing out `_sceneMemo.value` made every
415
+ // caller share ONE mutable record that IS internal instrument state: a single
416
+ // careless consumer (`scene.build.engine = ...`) corrupted the record for every
417
+ // later caller AND changed its canonical form, so the hash silently stopped
418
+ // verifying against stills already written. That is the R8 aliasing class landing
419
+ // on the one record the regenerate-not-replay claim depends on. The memo stays —
420
+ // it exists to avoid re-hashing, not to share an object.
421
+ return Promise.resolve(RS.deepPlainCopy(this._sceneMemo.value));
422
+ }
423
+ return RS.sceneHash(record).then(function (id) {
424
+ var value = Object.assign({}, record, { scene: id });
425
+ self._sceneMemo = { canon: canon, value: value };
426
+ // COPY ON THE WAY OUT — see the memo-hit path above for why.
427
+ return RS.deepPlainCopy(value);
428
+ });
429
+ };
430
+
431
+ /**
432
+ * The tick index the channel derives from — the co-registration key.
433
+ *
434
+ * Stamped by the INSTRUMENT off the loop's retained TickData because the loop is the only
435
+ * party that knows the tick: a caller capturing several stages between ticks asserts
436
+ * EQUAL `tick` stamps to prove the channels are aligned, and a caller-supplied tick would
437
+ * make that assertion vacuous.
438
+ * @private
439
+ */
440
+ CaptureInstrument.prototype._tickIndex = function (tickData) {
441
+ var t = tickData && tickData.universeData && tickData.universeData.tick;
442
+ if (typeof t === 'number' && isFinite(t)) { return t; }
443
+ throw new Error('[FrqtlCapture.capture] the live TickData carries no ' +
444
+ 'universeData.tick — the tick stamp is what co-registers a multi-channel capture, ' +
445
+ 'so it is never defaulted or omitted. (FAIL-LOUD.)');
446
+ };
447
+
448
+ /**
449
+ * The capability descriptors the ACTIVE PROVIDER declares — the provider is the
450
+ * capability authority (`renderCapabilities()` is its OPTIONAL contract method), and the
451
+ * host's `runner.renderCapabilities()` is the host-facing read of that SAME authority.
452
+ * A provider that predates the contract addition, or no provider at all, yields `[]` —
453
+ * never an error (the `cameraControl` capability-check precedent).
454
+ * @private
455
+ */
456
+ CaptureInstrument.prototype._renderCapabilities = function () {
457
+ var prov = this._provider();
458
+ if (!prov || typeof prov.renderCapabilities !== 'function') { return []; }
459
+ return prov.renderCapabilities();
460
+ };
461
+
462
+ /**
463
+ * The live values of the keys the active provider DECLARES via `renderCapabilities()`.
464
+ *
465
+ * DRIVEN OFF the declared descriptor list, so a newly declared R4/R5 knob shows up in
466
+ * capture provenance with zero edits here — a hand-typed key list would be the
467
+ * schema-mirror bug. The VALUES come from the ONE live options bag, read through the
468
+ * bound getter (never copied). `filter.*` capability keys are deliberately EXCLUDED:
469
+ * their authority is the provider-stamped, ORDERED `filtersApplied`, and reading them
470
+ * here as well would create a second (unordered, BYO-blind) filter truth.
471
+ * @private
472
+ */
473
+ CaptureInstrument.prototype._captureRenderOptions = function () {
474
+ var caps = this._renderCapabilities();
475
+ var live = this._renderOptionsGetter() || {};
476
+ var out = {};
477
+ for (var i = 0; i < caps.length; i++) {
478
+ var name = caps[i].name;
479
+ if (!name || name.indexOf('filter.') === 0) { continue; }
480
+ out[name] = (live[name] !== undefined) ? live[name] : caps[i].default;
481
+ }
482
+ return out;
483
+ };
484
+
485
+ /**
486
+ * capture(stage) — THE generic render-pipeline tap (the mechanism).
487
+ *
488
+ * Reads the data at a NAMED stage of the render pipeline and returns a provenance-
489
+ * stamped bag. `stage` is REQUIRED: a default belongs to a convenience, not to the
490
+ * mechanism (see `captureStill()` below).
491
+ *
492
+ * @param {string} stage — a member of `RenderStages.RENDER_STAGES`
493
+ * (`'tick' | 'field' | 'scene' | 'composite'`).
494
+ * @returns {Promise<object>} a bag carrying every `RenderStages.STILL_BAG_KEYS` key:
495
+ * the `data` payload (Array of FrameData / `Uint8Array` / `Uint8ClampedArray` — the
496
+ * NUMBERS are first class, an image is something a consumer derives) plus the
497
+ * provenance that makes a pre-render tap distinguishable from a post-bloom one
498
+ * (`stage`, `tick`, `format`, `filtersApplied`, `renderBaseCanvas`, `pose`,
499
+ * `composedOf`, `renderOptions`). Async for all stages by contract: a WebGPU
500
+ * backend's only readback is `mapAsync`, so a sync signature would structurally
501
+ * exclude WebGPU BYO renderers forever. The shipped stages resolve immediately.
502
+ *
503
+ * @throws {Error} unknown/absent stage (message lists the whole table); no tick has
504
+ * run yet (never an empty buffer); a stage that is a real pipeline point but has no
505
+ * tap executor (`'scene'` in v1 — a DISTINCT error from unknown-stage); `'composite'`
506
+ * on a provider that does not implement the OPTIONAL `captureStill()`. Absence of a
507
+ * CAPABILITY hides a host affordance (a UI capability-checks and shows no button);
508
+ * a PROGRAMMATIC capture never degrades to `null` — a measurement or a golden built
509
+ * on a silently-blank capture is fabricated evidence.
510
+ */
511
+ CaptureInstrument.prototype.capture = function (stage) {
512
+ // A Promise-returning method must NEVER throw synchronously: a mixed error channel
513
+ // (sync throw for a bad argument, rejection for a bad readback) guarantees an
514
+ // uncaught exception at some call site that only wrote `.catch()`. Every failure
515
+ // below therefore arrives as a REJECTION, with the identical message. This wrapper
516
+ // exists in exactly ONE place — a host that forwards to `capture()` must not add a
517
+ // second try/catch of its own.
518
+ try { return this._captureDispatch(stage); }
519
+ catch (e) { return Promise.reject(e); }
520
+ };
521
+
522
+ /** @private — the dispatch body; see `capture` for the contract. */
523
+ CaptureInstrument.prototype._captureDispatch = function (stage) {
524
+ var RS = RenderStages;
525
+ RS.assertStage(stage);
526
+
527
+ // ── HOSTED: a REMOTE executor owns this stage ────────────────────────
528
+ // Checked before every local precondition, because none of them apply: the world,
529
+ // the clock, the canvas and the camera are all in the other process. The local side
530
+ // computes the scene identity, asks, and VERIFIES — it never stamps. (Deliberately
531
+ // NOT a provider branch: see instruments/capture-host.js for why a provider-shaped
532
+ // adapter would have had to invent the provenance it does not hold.)
533
+ if (this._host && this._host.stages.indexOf(stage) !== -1) {
534
+ var CH = FrqtlCaptureHost;
535
+ var host = this._host;
536
+ return this.sceneRecord().then(function (scene) {
537
+ return Promise.resolve(host.capture(stage)).then(function (still) {
538
+ return CH.verifyStill(scene.scene, stage, still);
539
+ });
540
+ });
541
+ }
542
+
543
+ var td = this._loop.lastTickData;
544
+ if (!td) {
545
+ throw new Error('[FrqtlCapture.capture] no tick has run yet (the loop has ' +
546
+ 'retained no TickData), so there is no channel to read. Run start()/step() ' +
547
+ 'first. (FAIL-LOUD — never an empty buffer.)');
548
+ }
549
+ // Reject a non-tappable stage BEFORE doing any work — the diagnosis should not wait
550
+ // on a hash. ('scene' is a real pipeline point with no executor in v1.)
551
+ var prov = this._provider();
552
+ if (stage === 'scene') {
553
+ throw RS.stageNotTappableError(stage, 'the active renderer provider');
554
+ }
555
+ if (stage === 'composite' && (!prov || typeof prov.captureStill !== 'function')) {
556
+ throw RS.stageNotTappableError(stage, 'the active renderer provider ' +
557
+ '(it does not implement the OPTIONAL captureStill() capability), and no capture ' +
558
+ 'host is attached. In a process with no display renderer — a node rig — the ' +
559
+ 'composed picture is produced by the HOSTED headless stack, which runs the real ' +
560
+ 'renderer and its shaders: openHostedEyes({sceneRecord, sceneSource, pageUrl}) ' +
561
+ 'from @descix/frqtl-node/eyes (lib/frqtl/objects/instruments/hosted-eyes.js)');
562
+ }
563
+
564
+ // ONE TASK: the tick is stamped now; a raster readback runs in THIS task's microtask
565
+ // checkpoint, queued before anything is awaited. Both halves of that are load-bearing:
566
+ // - never AFTER the scene hash: sceneRecord() is a real task boundary whenever it
567
+ // hashes (the first capture of a scene, and after every scene change), and a running
568
+ // loop can tick and redraw inside it — the readback would carry a later tick's
569
+ // pixels for a caller that did everything right;
570
+ // - never INSIDE the synchronous call: the rAF loop notifies tick listeners BEFORE the
571
+ // frame's draw in the same task, so a draw of tick t that follows the request in
572
+ // this task (the frame's own) must be what the readback sees.
573
+ // The raster's LABELS (pose, renderOptions) are read in the readback's own synchronous run,
574
+ // right beside captureStill(): a listener registered after the requester (CameraPath's
575
+ // setOrbit, a knob write) can change them later in this task, and the pixels are drawn
576
+ // under the later values — nothing can draw between these reads and the readback.
577
+ // Selected by FORMAT off the stage table, not by stage name: every rgba8 stage is a
578
+ // provider-executed raster and inherits the readback timing, the label timing and the guard.
579
+ var self = this;
580
+ var tick = this._tickIndex(td);
581
+ var labels = { pose: null, renderOptions: null };
582
+ var readback = null;
583
+ if (RS.STAGE_FORMATS[stage] === 'rgba8') {
584
+ readback = Promise.resolve().then(function () {
585
+ labels.pose = self._pose();
586
+ labels.renderOptions = self._captureRenderOptions();
587
+ return prov.captureStill();
588
+ });
589
+ } else {
590
+ // No readback: the channel IS the retained TickData, so the stamp-time labels are the
591
+ // ones it is read under.
592
+ labels.pose = this._pose();
593
+ labels.renderOptions = this._captureRenderOptions();
594
+ }
595
+ // The still records COORDINATES WITHIN a scene, so it needs the scene's content id.
596
+ // sceneRecord() is memoized on the canonical form, so this is a hash only when the
597
+ // scene actually changed. Joined with the readback so neither rejection goes unhandled.
598
+ return Promise.all([this.sceneRecord(), readback]).then(function (joined) {
599
+ var scene = joined[0];
600
+ var base = {
601
+ scene: scene.scene,
602
+ stage: stage,
603
+ tick: tick,
604
+ pose: labels.pose,
605
+ renderOptions: labels.renderOptions
606
+ };
607
+
608
+ if (stage === 'tick') {
609
+ // Stage 0 — the frame-data bubble itself. `width`/`height` are the physics
610
+ // canvas the rect coordinates live in (a framedata payload is not a raster).
611
+ // The payload SHAPE is the frame-data bubble's own published contract
612
+ // (`FrameDataSchema` is its owner) — pointed AT here, never restated.
613
+ return RS.normalizeStill(Object.assign({}, base, {
614
+ data: td.frameData,
615
+ width: scene.physicsW,
616
+ height: scene.physicsH,
617
+ filtersApplied: [], // pre-render by definition
618
+ renderBaseCanvas: null // nothing composited on this channel
619
+ }));
620
+ }
621
+
622
+ if (stage === 'field') {
623
+ // The render-free numeric channel: the ONE canonical field source over the
624
+ // retained TickData. Instrument-side ON PURPOSE — the field stage is
625
+ // backend-independent by construction, so putting it on the provider contract
626
+ // would force every BYO renderer to re-ship a stage it does not own. Every
627
+ // BYO provider gets this tap for free, and a headless process gets it with no
628
+ // renderer at all.
629
+ var FB = FieldBuilder;
630
+ var fopts = Object.assign({}, FB.FIELD_OPTS, {
631
+ canvasW: scene.physicsW, canvasH: scene.physicsH
632
+ });
633
+ return RS.normalizeStill(Object.assign({}, base, {
634
+ data: FB.buildHeightField(td.frameData, fopts),
635
+ width: fopts.w,
636
+ height: fopts.h,
637
+ // The r8 channel is a dimensionless INSTRUMENT RESPONSE (gain-scaled,
638
+ // clamped, optionally blurred soft-splat fullness density), comparable
639
+ // only at equal options — so the options ride the record. Two field
640
+ // captures at different gain would otherwise be indistinguishable.
641
+ fieldOpts: fopts,
642
+ filtersApplied: [],
643
+ renderBaseCanvas: null
644
+ }));
645
+ }
646
+
647
+ // rgba8 ('composite') — the provider returns the §1a.4 render-domain bag (pixels + the
648
+ // fields it alone is the authority for). The instrument normalizes ONCE through
649
+ // the vocabulary owner: `pixels` becomes the stage-agnostic `data` payload and
650
+ // the dispatcher's provenance is merged in. Providers never import the shim — a
651
+ // BYO renderer's whole capture burden stays ONE method. The bag was read in the
652
+ // stamp's task above.
653
+ var bag = joined[1];
654
+ if (!bag) {
655
+ throw new Error('[FrqtlCapture.capture] the provider\'s captureStill() ' +
656
+ 'resolved with nothing. (FAIL-LOUD — never a null still.)');
657
+ }
658
+ _assertComposited(stage, tick, bag);
659
+ return RS.normalizeStill(Object.assign({}, base, {
660
+ data: bag.pixels,
661
+ width: bag.width,
662
+ height: bag.height,
663
+ cssWidth: bag.cssWidth,
664
+ cssHeight: bag.cssHeight,
665
+ filtersApplied: bag.filtersApplied,
666
+ renderBaseCanvas: bag.renderBaseCanvas,
667
+ filterTime: bag.filterTime
668
+ }));
669
+ });
670
+ };
671
+
672
+ /**
673
+ * captureStill() — the LAST-STAGE CONVENIENCE. Capturing the canvas is capturing the
674
+ * fully composed stack of transforms.
675
+ *
676
+ * PURE DELEGATION: it contains no capture logic, no provider call and no provenance
677
+ * stamping of its own — it resolves the final stage name and calls the mechanism. The
678
+ * `setPreset`-over-`setOrbit` shape (a full framing reset that still goes through the
679
+ * one primitive). A convenience that duplicated its own primitive would be the second
680
+ * path that later disagrees, so the conformance gate asserts `captureStill()` and
681
+ * `capture(<last stage>)` return identically-shaped, identically-provenanced bags.
682
+ *
683
+ * The final stage is DERIVED from the exported table, never a hardcoded 'composite'
684
+ * literal: if the pipeline ever grows a later stage, this convenience follows it by
685
+ * construction (truth-in-labeling at the API level — an API that says "still of the
686
+ * whole stack" must not name a stage that has stopped being last). The returned bag
687
+ * carries FULL provenance, including `composedOf` (the ordered upstream stages) and
688
+ * `renderOptions` — "final" alone would be a label, not a statement of what produced
689
+ * the picture.
690
+ *
691
+ * @returns {Promise<object>} see `capture(stage)`.
692
+ */
693
+ CaptureInstrument.prototype.captureStill = function () {
694
+ return this.capture(RenderStages.finalStage());
695
+ };
696
+
697
+ // ── Export ────────────────────────────────────────────────────────────────
698
+
699
+ /**
700
+ * create(loop, providerGetter, identity, renderOptionsGetter) → the instrument.
701
+ * The ONE construction path (a bare `new CaptureInstrument` is not exported).
702
+ */
703
+ function create(loop, providerGetter, identity, renderOptionsGetter) {
704
+ return new CaptureInstrument(loop, providerGetter, identity, renderOptionsGetter);
705
+ }
706
+
707
+ var FrqtlCapture = { create: create };
708
+
709
+ export { FrqtlCapture };
710
+