@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.
- package/CLAUDE.md +87 -0
- package/LICENSE.md +664 -0
- package/README.md +84 -2
- package/dist/dist-manifest.json +218 -0
- package/dist/eyes/index.js +109 -0
- package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
- package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
- package/dist/fat/pkg-node/package.json +12 -0
- package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
- package/dist/frqtl/compat/boot-node.js +34 -0
- package/dist/frqtl/compat/engine.js +127 -0
- package/dist/frqtl/compat/package-root.js +24 -0
- package/dist/frqtl/compat/providers/wasm-engine.js +2499 -0
- package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
- package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
- package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
- package/dist/frqtl/compat/renderer/Filters.js +163 -0
- package/dist/frqtl/compat/renderer/RenderFamily.js +84 -0
- package/dist/frqtl/compat/renderer/RenderMesh.js +99 -0
- package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
- package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
- package/dist/frqtl/compat/renderer/captureStill-common.js +258 -0
- package/dist/frqtl/compat/renderer/display-slot.js +96 -0
- package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
- package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
- package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
- package/dist/frqtl/compat/renderer/providers/index.js +22 -0
- package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
- package/dist/frqtl/compat/renderer/providers/three-provider.js +2470 -0
- package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
- package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
- package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
- package/dist/frqtl/compat/vendor.js +196 -0
- package/dist/frqtl/node-entry.js +17 -0
- package/dist/frqtl/objects/FrqtlApp.js +2920 -0
- package/dist/frqtl/objects/ObjectFactory.js +296 -0
- package/dist/frqtl/objects/RecipeContext.js +322 -0
- package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
- package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
- package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
- package/dist/frqtl/objects/instruments/capture-host.js +453 -0
- package/dist/frqtl/objects/instruments/capture.js +710 -0
- package/dist/frqtl/objects/instruments/hosted-eyes.js +984 -0
- package/dist/frqtl/objects/kinds-entry.js +14 -0
- package/dist/frqtl/objects/kinds.js +190 -0
- package/dist/frqtl/objects/objects/Box.js +273 -0
- package/dist/frqtl/objects/objects/Bullet.js +155 -0
- package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
- package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
- package/dist/frqtl/objects/objects/DirectedTunnel.js +411 -0
- package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
- package/dist/frqtl/objects/objects/LightSource.js +351 -0
- package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
- package/dist/frqtl/objects/objects/Tube.js +471 -0
- package/dist/frqtl/objects/objects/absorbRadiation.js +309 -0
- package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
- package/dist/frqtl/objects/objects/controlAction.js +89 -0
- package/dist/frqtl/objects/objects/controlBind.js +278 -0
- package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
- package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
- package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
- package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
- package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
- package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
- package/dist/frqtl/objects/objects/makeControlsForObject.js +820 -0
- package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
- package/dist/frqtl/objects/objects/slitLinerScatter.js +261 -0
- package/dist/frqtl/objects/objects/sustainedSource.js +298 -0
- package/dist/frqtl/objects/objects/tagger.js +228 -0
- package/dist/frqtl/objects/schema/engine-scopes.js +468 -0
- package/dist/frqtl/objects/schema/index.js +256 -0
- package/dist/frqtl/objects/schema/scopes.js +457 -0
- package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
- package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
- package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
- package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
- package/dist/frqtl/objects/setup/_worldInit.js +162 -0
- package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
- package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
- package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
- package/dist/frqtl/objects/setup/setupConeEmitter3D.js +250 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit.js +326 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
- package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
- package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
- package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
- package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
- package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
- package/dist/frqtl/objects/setup/setupLightSourceLab.js +277 -0
- package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
- package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
- package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
- package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1258 -0
- package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
- package/dist/frqtl/objects/setup/setupTunnelLab.js +240 -0
- package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
- package/dist/frqtl/objects/setup/setupWaveInterference.js +177 -0
- package/dist/frqtl/objects/v1-replacements.js +29 -0
- package/dist/frqtl/package-core.js +23 -0
- package/dist/frqtl/parts/CameraControls.js +643 -0
- package/dist/frqtl/parts/CameraPath.js +575 -0
- package/dist/frqtl/parts/CollisionActions.js +34 -0
- package/dist/frqtl/parts/CollisionTypes.js +30 -0
- package/dist/frqtl/parts/DetectorWall.js +496 -0
- package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
- package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
- package/dist/frqtl/parts/FrqtlVision.js +818 -0
- package/dist/frqtl/parts/GraphingDetectorWall.js +489 -0
- package/dist/frqtl/parts/LargeObject.js +405 -0
- package/dist/frqtl/parts/LightColor.js +255 -0
- package/dist/frqtl/parts/LiveChart.js +285 -0
- package/dist/frqtl/parts/LogicGate.js +717 -0
- package/dist/frqtl/parts/OvenBox.js +611 -0
- package/dist/frqtl/parts/PointSource.js +762 -0
- package/dist/frqtl/parts/Rectangle.js +200 -0
- package/dist/frqtl/parts/SlitLightSource.js +234 -0
- package/dist/frqtl/parts/SlitScreen.js +628 -0
- package/dist/frqtl/parts/SphereEmitter.js +946 -0
- package/dist/frqtl/parts/Trace.js +508 -0
- package/dist/frqtl/parts/Tunnel.js +1057 -0
- package/dist/frqtl/parts/Wall.js +1211 -0
- package/dist/frqtl/parts/index.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall.js +32 -0
- package/dist/frqtl/scenes/atomic-model.js +88 -0
- package/dist/frqtl/scenes/big-bang.js +46 -0
- package/dist/frqtl/scenes/blackbody.js +81 -0
- package/dist/frqtl/scenes/circuit-sat.js +127 -0
- package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
- package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
- package/dist/frqtl/scenes/counting-wall.js +38 -0
- package/dist/frqtl/scenes/double-slit.js +31 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
- package/dist/frqtl/scenes/dse-one-slit.js +114 -0
- package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
- package/dist/frqtl/scenes/dse-tunnel-rebuild.js +243 -0
- package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
- package/dist/frqtl/scenes/dse-two-slits.js +132 -0
- package/dist/frqtl/scenes/empty.js +14 -0
- package/dist/frqtl/scenes/laser-buildup.js +49 -0
- package/dist/frqtl/scenes/mirror-3d.js +56 -0
- package/dist/frqtl/scenes/mirror-route.js +140 -0
- package/dist/frqtl/scenes/mirror.js +38 -0
- package/dist/frqtl/scenes/omni-3d.js +27 -0
- package/dist/frqtl/scenes/omni-source.js +22 -0
- package/dist/frqtl/scenes/self-interference.js +39 -0
- package/dist/frqtl/scenes/smoking-gun.js +122 -0
- package/dist/frqtl/scenes/straight-walk.js +22 -0
- package/dist/frqtl/scenes/sun.js +39 -0
- package/dist/frqtl/scenes/two-source.js +37 -0
- package/dist/frqtl/scenes/wave.js +26 -0
- package/dist/shim/bundle/frqnb.js +112 -0
- package/dist/shim/bundle/index.js +121 -0
- package/dist/shim/bundle/indexHtml.js +107 -0
- package/dist/shim/bundle/manifest.js +95 -0
- package/dist/shim/bundle/zip.js +243 -0
- package/dist/shim/bus.js +67 -0
- package/dist/shim/controls/index.js +627 -0
- package/dist/shim/controls/registry.js +274 -0
- package/dist/shim/engine/artifact-shell.js +211 -0
- package/dist/shim/engine/boot-phases.js +44 -0
- package/dist/shim/engine/engine-stack-boot.js +59 -0
- package/dist/shim/engine/router.js +690 -0
- package/dist/shim/experiment-frame/index.js +296 -0
- package/dist/shim/expr/ast-schema.js +369 -0
- package/dist/shim/field-descriptor/index.js +325 -0
- package/dist/shim/frame-data/index.js +224 -0
- package/dist/shim/index.js +67 -0
- package/dist/shim/notebook/canvas-guard.js +82 -0
- package/dist/shim/notebook/catalog.js +35 -0
- package/dist/shim/notebook/format.js +736 -0
- package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
- package/dist/shim/notebook/kernel.js +671 -0
- package/dist/shim/notebook/knob.js +150 -0
- package/dist/shim/notebook/module-resolver.js +260 -0
- package/dist/shim/notebook/notebook-knobs.js +65 -0
- package/dist/shim/notebook/produced-bindings.js +67 -0
- package/dist/shim/notebook/realm-registry.js +121 -0
- package/dist/shim/notebook/realm-transport.js +73 -0
- package/dist/shim/notebook/setup-builder.js +507 -0
- package/dist/shim/notebook/store.js +159 -0
- package/dist/shim/notebook/vendor-importmap.js +134 -0
- package/dist/shim/notebook/view-builder.js +189 -0
- package/dist/shim/notebook/view-ctx.js +100 -0
- package/dist/shim/notebook/view-handle.js +202 -0
- package/dist/shim/notebook/warmup.js +102 -0
- package/dist/shim/package.json +4 -0
- package/dist/shim/page-params/index.js +213 -0
- package/dist/shim/project/identity.js +112 -0
- package/dist/shim/project/index.js +19 -0
- package/dist/shim/project/link.js +402 -0
- package/dist/shim/project/node-specifier-hook.mjs +35 -0
- package/dist/shim/project/register-node.mjs +17 -0
- package/dist/shim/project/specifiers.js +106 -0
- package/dist/shim/registry/copy-gate.js +110 -0
- package/dist/shim/registry/exhibits.js +455 -0
- package/dist/shim/registry/experiments.js +215 -0
- package/dist/shim/registry/index.js +19 -0
- package/dist/shim/registry/scene-source.js +157 -0
- package/dist/shim/registry/walkthrough.js +804 -0
- package/dist/shim/render/capture-envelope.js +279 -0
- package/dist/shim/render/heat-colormap.js +92 -0
- package/dist/shim/render/render-stages.js +855 -0
- package/dist/shim/render/sun-look.js +33 -0
- package/dist/shim/render-state/apply.js +262 -0
- package/dist/shim/render-state/index.js +74 -0
- package/dist/shim/rpc/index.js +207 -0
- package/dist/shim/runtime/index.js +1023 -0
- package/dist/shim/storage/boot.js +69 -0
- package/dist/shim/storage/index.js +169 -0
- package/dist/shim/storage/local-storage-provider.js +230 -0
- package/dist/shim/storage/make-namespace-store.js +132 -0
- package/dist/shim/storage/modules.js +249 -0
- package/dist/shim/storage/projects.js +150 -0
- package/llms.txt +12 -0
- 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
|
+
|