@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +87 -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 +2501 -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 +85 -0
- package/dist/frqtl/compat/renderer/RenderMesh.js +100 -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 +259 -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 +2474 -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 +2922 -0
- package/dist/frqtl/objects/ObjectFactory.js +297 -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 +711 -0
- package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -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 +412 -0
- package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
- package/dist/frqtl/objects/objects/LightSource.js +354 -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 +310 -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 +824 -0
- package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
- package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
- package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
- package/dist/frqtl/objects/objects/tagger.js +228 -0
- package/dist/frqtl/objects/schema/engine-scopes.js +469 -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 +251 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -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 +280 -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 +1259 -0
- package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
- package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
- package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
- package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
- package/dist/frqtl/objects/v1-replacements.js +30 -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 +35 -0
- package/dist/frqtl/parts/CollisionTypes.js +31 -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 +490 -0
- package/dist/frqtl/parts/LargeObject.js +406 -0
- package/dist/frqtl/parts/LightColor.js +255 -0
- package/dist/frqtl/parts/LiveChart.js +285 -0
- package/dist/frqtl/parts/LogicGate.js +720 -0
- package/dist/frqtl/parts/OvenBox.js +612 -0
- package/dist/frqtl/parts/PointSource.js +763 -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 +244 -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 +212 -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 +692 -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 +326 -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 +672 -0
- package/dist/shim/notebook/knob.js +151 -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 +510 -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 +214 -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 +807 -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 +62 -4
|
@@ -0,0 +1,855 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lib/shim/render/render-stages — THE render-pipeline stage vocabulary contract.
|
|
5
|
+
*
|
|
6
|
+
* Peer of `lib/shim/frame-data` (`RENDER_INTENT_KEYS` — the per-frame render-intent
|
|
7
|
+
* vocabulary) and `lib/shim/expr/ast-schema` (`NODE_KINDS`/`OP_KINDS` — the op-program
|
|
8
|
+
* vocabulary): the shim owns every cross-layer contract vocabulary, and consumers FERRY
|
|
9
|
+
* the exported list rather than re-enumerating it (the schema-mirror rule). Named ES-module
|
|
10
|
+
* exports, imported by the game layer (`FrqtlApp.js`, the capture instrument) and by
|
|
11
|
+
* consumers above the shim — the `heat-colormap.js` precedent, same directory.
|
|
12
|
+
*
|
|
13
|
+
* ── What this contract IS (and deliberately is NOT) ──────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* The render pipeline already EXISTS as an ordered sequence of transforms, and each of
|
|
16
|
+
* its transform-registration surfaces already exists too:
|
|
17
|
+
*
|
|
18
|
+
* - `scene` appearance registers as the per-provider `renderCapabilities()`
|
|
19
|
+
* descriptor knobs (R4/R5 effect layers).
|
|
20
|
+
* - the scene→composite segment registers as the ORDERED `filterSpec` queue
|
|
21
|
+
* (`Filters.js` catalog + `renderer.filter({name, glsl, uniforms})`).
|
|
22
|
+
* - `field` registers as ONE authored canonical module (`FieldBuilder`).
|
|
23
|
+
* - `tick` is the frame-data bubble — the engine-opaque `string_map` render intent.
|
|
24
|
+
*
|
|
25
|
+
* This module adds **ZERO new registration mechanisms**. It contributes exactly three
|
|
26
|
+
* things those mechanisms never had: a NAME for the pipeline point each attaches to, a
|
|
27
|
+
* way to READ the data at a named point (a tap), and PROVENANCE on what a tap returned.
|
|
28
|
+
* If a change to this file starts to look like a `registerStage()` / `onStage(cb)` /
|
|
29
|
+
* transform-plugin registry, that is the failure mode this contract exists to prevent —
|
|
30
|
+
* a new render transform lands as registered data on its stage's EXISTING surface, never
|
|
31
|
+
* as bespoke provider code and never as a new uber-registry
|
|
32
|
+
* (docs/internal/engine/RENDERER_CONTRACT.md §9, the standing veto).
|
|
33
|
+
*
|
|
34
|
+
* ── Layer 1: API contract ───────────────────────────────────────────────────
|
|
35
|
+
*
|
|
36
|
+
* RENDER_STAGES — the ordered, named, COMPLETE canonical stage list.
|
|
37
|
+
* STAGE_FORMATS — stage → payload format tag ('framedata' | 'r8' | 'rgba8').
|
|
38
|
+
* finalStage() — the LAST stage. `runner.captureStill()` derives its stage
|
|
39
|
+
* from this, never from a hardcoded 'composite' literal.
|
|
40
|
+
* upstreamStages(stage) — the ordered stage names strictly BEFORE `stage`.
|
|
41
|
+
* assertStage(stage) — throw-or-return; the message lists the whole table.
|
|
42
|
+
* stageNotTappableError(stage, who) — the DISTINCT error for a stage that is a real
|
|
43
|
+
* member of the table but has no tap executor on the active
|
|
44
|
+
* provider (v1: `scene`). Never conflated with unknown-stage.
|
|
45
|
+
* SCHEMA_VERSION — the `v` stamped on BOTH records (R1).
|
|
46
|
+
* SCENE_KEYS — the RUN-INVARIANT record: the regeneration input.
|
|
47
|
+
* STILL_KEYS — the SERIALIZED still record: coordinates within a scene.
|
|
48
|
+
* STILL_DERIVED_KEYS — in-memory conveniences that are NOT serialized (composedOf).
|
|
49
|
+
* STILL_BAG_KEYS — the full IN-MEMORY still bag = STILL_KEYS + derived.
|
|
50
|
+
* STILL_PROVENANCE_KEYS — the provenance subset, DERIVED (bag keys minus payload keys).
|
|
51
|
+
* RUNTIME_MUTATION_SURFACES — the mutability line, declared: which APIs touch which
|
|
52
|
+
* still fields. The drift gate runs off this.
|
|
53
|
+
* deepPlainCopy(value) — THE plain-data deep-copy authority (payload + scene record).
|
|
54
|
+
* normalizeScene(partial)— ferry a partial scene bag to the full SceneRecord, FAIL-LOUD.
|
|
55
|
+
* canonicalizeScene(rec) — the ONE canonical byte-string a scene identity is taken over.
|
|
56
|
+
* sceneHash(rec) — `Promise<'sha256:<hex>'>`; the content address.
|
|
57
|
+
* normalizeStill(partial)— ferry a partial bag to the full canonical shape, FAIL-LOUD.
|
|
58
|
+
* SCENE_SURFACE_ATTR — the scene-surface marker attribute, RE-EXPORTED from its owner
|
|
59
|
+
* (compat/renderer/renderer-provider-contract.js).
|
|
60
|
+
*
|
|
61
|
+
* ── The stage table ─────────────────────────────────────────────────────────
|
|
62
|
+
*
|
|
63
|
+
* tick TickData.frameData — pure physics data, format 'framedata'. The frame-data
|
|
64
|
+
* bubble IS stage 0 of this pipeline: the engine-boundary law ("a new
|
|
65
|
+
* behavior is op-program DATA, never engine code") and the render-stack law
|
|
66
|
+
* ("a new transform is registered data on its stage's surface") are the same
|
|
67
|
+
* law at two altitudes, and this stage is where they meet.
|
|
68
|
+
* field the `FieldBuilder` heightfield — format 'r8', physics-pixel space,
|
|
69
|
+
* y-down/top-left, at field resolution. Backend-INDEPENDENT by construction
|
|
70
|
+
* (the one field source), so its tap is runner-side and every BYO provider
|
|
71
|
+
* gets it for free.
|
|
72
|
+
* scene the frozen three-layer composite (backdrop → particle → large-object,
|
|
73
|
+
* RENDERER_CONTRACT §8) BEFORE the filter passes — format 'rgba8'. NAMED
|
|
74
|
+
* here because the pipeline genuinely has this point; its tap EXECUTOR is
|
|
75
|
+
* deferred to its first caller (v1 ships no `scene` tap → the distinct
|
|
76
|
+
* `stageNotTappableError`). "Capture without the water shader" composes the
|
|
77
|
+
* existing knobs instead: setFilterActive(name,false) → repaint() → capture.
|
|
78
|
+
* composite after the ordered filter passes — format 'rgba8'. THE final stage, tapped
|
|
79
|
+
* by the provider's OPTIONAL `captureStill()` (RENDERER_CONTRACT §1a.4).
|
|
80
|
+
*
|
|
81
|
+
* ── Provenance is load-bearing, not decoration ───────────────────────────────
|
|
82
|
+
*
|
|
83
|
+
* The tap consumers are VLA training and the JSONL finetuning corpus. A pre-render tap
|
|
84
|
+
* that is INDISTINGUISHABLE from a post-bloom tap is not a missing feature — it is a
|
|
85
|
+
* silently corrupted training corpus that teaches the wrong mapping from code to
|
|
86
|
+
* picture. So every bag states which stage produced it AND what was active:
|
|
87
|
+
*
|
|
88
|
+
* v the schema version (stamped by the normalizer)
|
|
89
|
+
* scene the content hash of the SceneRecord this (stamped by the dispatcher)
|
|
90
|
+
* still is coordinates within
|
|
91
|
+
* stage the stage name from RENDER_STAGES (stamped by the dispatcher)
|
|
92
|
+
* tick the tick the channel derives from (stamped by the dispatcher)
|
|
93
|
+
* format STAGE_FORMATS[stage] (stage implementation)
|
|
94
|
+
* width, height payload buffer dims (stage implementation)
|
|
95
|
+
* cssWidth/cssHeight the canvas CSS size at capture, or null (stage implementation)
|
|
96
|
+
* fieldOpts the FieldBuilder options the field tap (stage implementation)
|
|
97
|
+
* ran with; null on every other stage
|
|
98
|
+
* filtersApplied ORDERED filter names actually run, or [] (the PROVIDER — its own
|
|
99
|
+
* `_filters` list is the one ordered authority; NEVER derived from the
|
|
100
|
+
* runner's unordered `_activeFilters` map, which is also blind to a
|
|
101
|
+
* direct BYO `prov.filter()` call and would therefore lie for exactly
|
|
102
|
+
* the BYO case)
|
|
103
|
+
* renderBaseCanvas the boolean in effect at composite, or null on a stage where
|
|
104
|
+
* nothing composited (the provider)
|
|
105
|
+
* filterTime the `uTime` the filter passes drew with (simulation seconds at the
|
|
106
|
+
* rendered tick, RendererProviderContract.filterTime), or null when no
|
|
107
|
+
* filter ran or the stage composited nothing (the provider)
|
|
108
|
+
* pose `cameraControl.getOrbit()` (the 2D bag is {distance, target}), or null
|
|
109
|
+
* with no camera pose control at all (the dispatcher, via the
|
|
110
|
+
* ONE camera authority — a derived read, no new machinery; without it
|
|
111
|
+
* a 3D composite cannot be co-registered to the field)
|
|
112
|
+
* composedOf upstreamStages(stage) — the ordered upstream stage names, DERIVED
|
|
113
|
+
* from this table. STRUCTURAL: it says what precedes this stage in the
|
|
114
|
+
* canonical pipeline, which is what "the full stack of transforms
|
|
115
|
+
* composed" means at the API level. It is NOT a per-capture run
|
|
116
|
+
* record — `filtersApplied` (ordered, from the provider) and
|
|
117
|
+
* `renderOptions` are the run record.
|
|
118
|
+
* renderOptions the live value of each key the active provider DECLARES via
|
|
119
|
+
* `renderCapabilities()`, driven off that declared list so a newly
|
|
120
|
+
* declared R4/R5 knob appears in provenance with zero capture-code
|
|
121
|
+
* edits. `filter.*` capability keys are deliberately ABSENT here:
|
|
122
|
+
* their authority is `filtersApplied`, and a second filter read would
|
|
123
|
+
* be the two-sources bug. (Stamped by the dispatcher.)
|
|
124
|
+
*
|
|
125
|
+
* The RUN-INVARIANT provenance — `seed`, `sourceRef`, `build`, `physicsW/H`, `projection` —
|
|
126
|
+
* does NOT ride each still. It lives ONCE on the SceneRecord (`SCENE_KEYS`), which the still
|
|
127
|
+
* references by content hash. 3D scenes are high bandwidth; repeating their header on every
|
|
128
|
+
* capture is the cost this split removes.
|
|
129
|
+
*
|
|
130
|
+
* NO provenance field is ever caller-supplied — a caller-supplied provenance field is the
|
|
131
|
+
* hardcoded-backend-label anti-pattern (dep-map rule 4). `normalizeStill`/`normalizeScene`
|
|
132
|
+
* ENFORCE this for the derived fields: a `composedOf` or a `v` that disagrees THROWS.
|
|
133
|
+
*
|
|
134
|
+
* ── Channel semantics (what the numbers MEAN) ─────────────────────────────────
|
|
135
|
+
*
|
|
136
|
+
* framedata — the frame-data bubble's OWN published contract; `FrameDataSchema` is its
|
|
137
|
+
* owner and this contract points AT it and never restates its fields.
|
|
138
|
+
* r8 — gain-scaled, clamped [0,255], optionally blurred soft-splat fullness
|
|
139
|
+
* density: a dimensionless INSTRUMENT RESPONSE, NOT calibrated mass, and
|
|
140
|
+
* comparable only between captures at equal `fieldOpts`.
|
|
141
|
+
* rgba8 — row-major from the TOP-LEFT origin, colour space **sRGB**, alpha
|
|
142
|
+
* **NON-premultiplied**. An encoder cannot guess either, so both are stated:
|
|
143
|
+
* three initialises `alpha:false` (opaque, a=255) and p5's 2D `getImageData`
|
|
144
|
+
* is unpremultiplied by spec.
|
|
145
|
+
*
|
|
146
|
+
* ── Layer 2: physics behaviour ──────────────────────────────────────────────
|
|
147
|
+
*
|
|
148
|
+
* ZERO physics. This is an output-boundary vocabulary: it names points in the render
|
|
149
|
+
* pipeline and describes what came out of them. Nothing here reads, writes, or gates a
|
|
150
|
+
* frame, a dimension, or a tick — a change to this file cannot move a canonical-bench
|
|
151
|
+
* golden, and if one moves, physics leaked into the convenience layer.
|
|
152
|
+
*
|
|
153
|
+
* Usage:
|
|
154
|
+
* import { RENDER_STAGES, finalStage, normalizeStill } from 'lib/shim/render/render-stages.js';
|
|
155
|
+
*/
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* RENDER_STAGES — the ordered, named, COMPLETE canonical render-pipeline stage list.
|
|
161
|
+
*
|
|
162
|
+
* Ordered EARLIEST → LATEST along the per-tick data flow. Consumers (the tap MECHANISM
|
|
163
|
+
* `objects/instruments/capture.js`, the conformance gate, any host UI) FERRY this array;
|
|
164
|
+
* a hand-written stage list anywhere else is the schema-mirror bug this ownership
|
|
165
|
+
* prevents, and a stage added here appears in every consumer with zero consumer edits.
|
|
166
|
+
*
|
|
167
|
+
* To add a stage: add it HERE in pipeline position, add its STAGE_FORMATS entry, and add
|
|
168
|
+
* its tap executor. Nothing else changes — `finalStage()`, `upstreamStages()`,
|
|
169
|
+
* `composedOf`, the conformance table and `runner.captureStill()` all re-derive.
|
|
170
|
+
*
|
|
171
|
+
* @type {string[]}
|
|
172
|
+
*/
|
|
173
|
+
var RENDER_STAGES = ['tick', 'field', 'scene', 'composite'];
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* STAGE_FORMATS — stage name → payload format tag.
|
|
177
|
+
*
|
|
178
|
+
* 'framedata' → `data` is the Array of FrameData objects (pure physics data)
|
|
179
|
+
* 'r8' → `data` is a Uint8Array, one byte per texel (the heightfield)
|
|
180
|
+
* 'rgba8' → `data` is a Uint8ClampedArray, 4 bytes per pixel, ROW-MAJOR from the
|
|
181
|
+
* TOP-LEFT origin (a provider reading a bottom-left WebGL framebuffer
|
|
182
|
+
* flips the rows itself — callers never see backend row order)
|
|
183
|
+
*
|
|
184
|
+
* @type {Object<string,string>}
|
|
185
|
+
*/
|
|
186
|
+
var STAGE_FORMATS = {
|
|
187
|
+
tick: 'framedata',
|
|
188
|
+
field: 'r8',
|
|
189
|
+
scene: 'rgba8',
|
|
190
|
+
composite: 'rgba8'
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* SCHEMA_VERSION — the `v` stamped on BOTH records (R1).
|
|
195
|
+
*
|
|
196
|
+
* The normalizer IS the version authority: a record's `v` is the contract version that
|
|
197
|
+
* normalized it, and a caller-supplied `v` that disagrees THROWS (same discipline as
|
|
198
|
+
* `composedOf`). Bump on ANY semantic change to `SCENE_KEYS`/`STILL_KEYS`/`STAGE_FORMATS`.
|
|
199
|
+
* Corpus tooling refuses to mix versions silently — which is the whole point: without `v`, a
|
|
200
|
+
* regenerated corpus cannot be safely mixed across generations, and the mixing is invisible.
|
|
201
|
+
* @type {number}
|
|
202
|
+
*/
|
|
203
|
+
var SCHEMA_VERSION = 4;
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* SCENE_KEYS — the RUN-INVARIANT record. The regeneration input, hashed into an identity.
|
|
207
|
+
*
|
|
208
|
+
* The video-container shape: ONE scene record (the header) + many light still records (the
|
|
209
|
+
* frames of that video). A 3D corpus stores one scene plus N tiny stills instead of
|
|
210
|
+
* repeating the whole header on every capture.
|
|
211
|
+
*
|
|
212
|
+
* ── The line is drawn by MUTABILITY, not by category (the trap) ──
|
|
213
|
+
* ONLY what CANNOT change within a run is hoisted here. Several fields LOOK scene-level and
|
|
214
|
+
* are not: `filtersApplied` (a mid-run `setFilterActive('water', false)` IS the ring
|
|
215
|
+
* measurement control), `pose` (a camera path changes it every tick), `cssWidth`/`cssHeight`
|
|
216
|
+
* (viewport resize). Hoisting any one of them would mislabel EVERY subsequent still —
|
|
217
|
+
* corpus corruption arriving through the back door of a bandwidth optimisation. The
|
|
218
|
+
* `RUNTIME_MUTATION_SURFACES` table below makes that a CI failure instead of a discovery at
|
|
219
|
+
* training time.
|
|
220
|
+
*
|
|
221
|
+
* v — schema version (on BOTH records: they are encountered separately and must
|
|
222
|
+
* self-describe; 4 bytes is not the bandwidth problem)
|
|
223
|
+
* seed — the RNG seed. Fixed at universe init. Two of the regeneration triple
|
|
224
|
+
* (code, seed, tick) were previously absent — this is one of them.
|
|
225
|
+
* sourceRef — experiment/rung identity, registered ONCE at setup (nullable). A legitimate
|
|
226
|
+
* authority set at attach time, NOT per-capture caller-supplied provenance.
|
|
227
|
+
* `null` is HONEST for an ad-hoc universe — the corpus row then carries the
|
|
228
|
+
* code itself. The two corpus modes live at the harness, not in the record.
|
|
229
|
+
* sourceSha256 — the CODE half of the regeneration triple, by content: the project identity
|
|
230
|
+
* (lib/shim/project/identity.js projectIdentity — lowercase hex sha256 over the booted
|
|
231
|
+
* project's files), registered ONCE by the door that linked the project (nullable: a
|
|
232
|
+
* sketch booted as a FUNCTION has no text to hash, and says so).
|
|
233
|
+
* build — the runtime build pin, DERIVED from the boot authorities, never a literal.
|
|
234
|
+
* The package is pinned by CONTENT (`wasmSha256`), never by the URL that
|
|
235
|
+
* served it: an origin is a deployment fact, not a regeneration input.
|
|
236
|
+
* physicsW/H — universe bounds, init-fixed. (NOTE: the CANVAS may be resized mid-run; the
|
|
237
|
+
* still's own `width`/`height` carry that. These are the physics bounds.)
|
|
238
|
+
* projection — camera intrinsics, provider-stamped. Scene-level because the DISPLAY
|
|
239
|
+
* provider is boot-fixed and OBSERVERS are live (RENDERER_CONTRACT §9): the
|
|
240
|
+
* one display provider owns canvas/camera/projection and swapping it is a
|
|
241
|
+
* reload; observers own no projection, so scene identity never moves mid-run.
|
|
242
|
+
* Without it a 3D pixel→world mapping is UNDERDETERMINED offline.
|
|
243
|
+
*
|
|
244
|
+
* @type {string[]}
|
|
245
|
+
*/
|
|
246
|
+
var SCENE_KEYS = ['v', 'seed', 'sourceRef', 'sourceSha256', 'build', 'physicsW', 'physicsH', 'projection'];
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* SCENE_BOUND_KEYS — the `SCENE_KEYS` members that are physics BOUNDS (positive integers, written
|
|
250
|
+
* by the scene's co-init call). A host that reads them back from a page, and checks a caller's
|
|
251
|
+
* stated ones as an expectation, iterates this list rather than naming the fields itself.
|
|
252
|
+
*
|
|
253
|
+
* @type {string[]}
|
|
254
|
+
*/
|
|
255
|
+
var SCENE_BOUND_KEYS = ['physicsW', 'physicsH'];
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* STILL_KEYS — the SERIALIZED still record: coordinates WITHIN a scene, plus the payload and
|
|
259
|
+
* everything that can vary per capture. `scene` is the content hash of the SceneRecord this
|
|
260
|
+
* still belongs to, so any drift between what a still claims and what the scene was is
|
|
261
|
+
* DETECTABLE (recompute the hash), never silent.
|
|
262
|
+
* @type {string[]}
|
|
263
|
+
*/
|
|
264
|
+
var STILL_KEYS = [
|
|
265
|
+
'v', 'scene', 'stage', 'format', 'tick',
|
|
266
|
+
'data', 'width', 'height', 'cssWidth', 'cssHeight',
|
|
267
|
+
'pose', 'filtersApplied', 'renderBaseCanvas', 'filterTime', 'renderOptions', 'fieldOpts'
|
|
268
|
+
];
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* STILL_DERIVED_KEYS — present on the IN-MEMORY bag as a convenience, deliberately NOT in
|
|
272
|
+
* `STILL_KEYS`: `composedOf` is derived from (`stage`, the `v`-versioned table), so a
|
|
273
|
+
* consumer re-derives it. Serializing a derivable is stored drift.
|
|
274
|
+
* @type {string[]}
|
|
275
|
+
*/
|
|
276
|
+
var STILL_DERIVED_KEYS = ['composedOf'];
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* The full IN-MEMORY still bag shape = serialized keys + derived conveniences.
|
|
280
|
+
* `normalizeStill` guarantees every one of these is PRESENT (a value may legitimately be
|
|
281
|
+
* `null`), so no consumer tests for key existence and no gate re-types the list.
|
|
282
|
+
* @type {string[]}
|
|
283
|
+
*/
|
|
284
|
+
var STILL_BAG_KEYS = STILL_KEYS.concat(STILL_DERIVED_KEYS);
|
|
285
|
+
|
|
286
|
+
/** The payload/geometry keys — everything else on a still is provenance. @type {string[]} */
|
|
287
|
+
var STILL_PAYLOAD_KEYS = ['data', 'width', 'height'];
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The PROVENANCE subset of the still bag — DERIVED from the two lists above rather than
|
|
291
|
+
* hand-listed, so a new still field lands in provenance automatically.
|
|
292
|
+
* @type {string[]}
|
|
293
|
+
*/
|
|
294
|
+
var STILL_PROVENANCE_KEYS = STILL_BAG_KEYS.filter(function (k) {
|
|
295
|
+
return STILL_PAYLOAD_KEYS.indexOf(k) === -1;
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* RUNTIME_MUTATION_SURFACES — THE MUTABILITY LINE, DECLARED.
|
|
300
|
+
*
|
|
301
|
+
* Every API that can change what a capture reports, mapped to the still fields it touches.
|
|
302
|
+
* The conformance gate asserts each listed field is in `STILL_KEYS` and in NO case in
|
|
303
|
+
* `SCENE_KEYS` — so a future attempt to hoist a toggleable field into the scene header
|
|
304
|
+
* fails a test instead of quietly rotting a corpus. Declared HERE (not in the test) so the
|
|
305
|
+
* line is self-documenting at the contract and a new mutation surface is added once.
|
|
306
|
+
* @type {Object<string,string[]>}
|
|
307
|
+
*/
|
|
308
|
+
var RUNTIME_MUTATION_SURFACES = {
|
|
309
|
+
'cameraControl.setOrbit/look/move/setPreset': ['pose'],
|
|
310
|
+
'setFilterActive / provider.filter / removeFilter': ['filtersApplied', 'filterTime'],
|
|
311
|
+
'setRenderOption': ['renderOptions', 'renderBaseCanvas'],
|
|
312
|
+
'canvas resize (setSize)': ['width', 'height', 'cssWidth', 'cssHeight'],
|
|
313
|
+
'FieldBuilder options at the field tap': ['fieldOpts']
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
/** The rgba8 payload constructor name, for the format check below. */
|
|
317
|
+
function _isStageKnown(stage) { return RENDER_STAGES.indexOf(stage) !== -1; }
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* finalStage() — the LAST stage of the canonical pipeline.
|
|
321
|
+
*
|
|
322
|
+
* `runner.captureStill()` (the convenience) resolves its stage THROUGH this function, so
|
|
323
|
+
* "capture the fully composed stack" follows the table by construction: if the pipeline
|
|
324
|
+
* ever grows a later stage, the convenience follows it with no edit. A hardcoded
|
|
325
|
+
* `'composite'` literal in a convenience would be truth-in-labeling drift at the API
|
|
326
|
+
* level — the API would claim "final" while naming a stage that is no longer last.
|
|
327
|
+
*
|
|
328
|
+
* @returns {string}
|
|
329
|
+
*/
|
|
330
|
+
function finalStage() { return RENDER_STAGES[RENDER_STAGES.length - 1]; }
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* upstreamStages(stage) — the ordered stage names strictly BEFORE `stage` in the table.
|
|
334
|
+
* This is the derivation behind every bag's `composedOf`. Structural (pipeline position),
|
|
335
|
+
* not a per-capture run record — see the module docstring.
|
|
336
|
+
*
|
|
337
|
+
* @param {string} stage
|
|
338
|
+
* @returns {string[]} a fresh array (never the shared table)
|
|
339
|
+
* @throws {Error} on an unknown stage
|
|
340
|
+
*/
|
|
341
|
+
function upstreamStages(stage) {
|
|
342
|
+
assertStage(stage);
|
|
343
|
+
return RENDER_STAGES.slice(0, RENDER_STAGES.indexOf(stage));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* assertStage(stage) — throw-or-return. The message ENUMERATES the whole table, so a
|
|
348
|
+
* typo'd stage name names its own fix (the `getRenderOption` fail-loud precedent).
|
|
349
|
+
*
|
|
350
|
+
* @param {string} stage
|
|
351
|
+
* @returns {string} the same stage, when valid
|
|
352
|
+
* @throws {Error} when `stage` is absent or not a member of RENDER_STAGES
|
|
353
|
+
*/
|
|
354
|
+
function assertStage(stage) {
|
|
355
|
+
if (!_isStageKnown(stage)) {
|
|
356
|
+
throw new Error('[RenderStages] unknown stage ' + JSON.stringify(stage) +
|
|
357
|
+
' — the canonical pipeline is [' + RENDER_STAGES.join(', ') + ']. ' +
|
|
358
|
+
'The stage argument is REQUIRED on the tap mechanism (a default is the ' +
|
|
359
|
+
'convenience\'s job, not the mechanism\'s). (FAIL-LOUD.)');
|
|
360
|
+
}
|
|
361
|
+
return stage;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* stageNotTappableError(stage, who) — the DISTINCT error for a stage that IS a real member
|
|
366
|
+
* of the table but has no tap executor available (v1: `scene`; or `composite` on a
|
|
367
|
+
* provider that does not implement the OPTIONAL `captureStill()`).
|
|
368
|
+
*
|
|
369
|
+
* Kept separate from `assertStage`'s unknown-stage throw on purpose: "you named something
|
|
370
|
+
* that does not exist" and "you named a real pipeline point nothing can read yet" are
|
|
371
|
+
* different diagnoses with different fixes, and collapsing them would send a caller
|
|
372
|
+
* hunting for a typo that is not there.
|
|
373
|
+
*
|
|
374
|
+
* @param {string} stage
|
|
375
|
+
* @param {string} who — the party that cannot tap it (e.g. 'the active renderer provider')
|
|
376
|
+
* @returns {Error} (the caller throws it — never a null bag, never a blank still)
|
|
377
|
+
*/
|
|
378
|
+
function stageNotTappableError(stage, who) {
|
|
379
|
+
return new Error('[RenderStages] stage ' + JSON.stringify(stage) + ' is known, but ' +
|
|
380
|
+
'not tappable by ' + (who || 'the active provider') + '. Known stages: [' +
|
|
381
|
+
RENDER_STAGES.join(', ') + ']. A tap executor is added when a real caller for it ' +
|
|
382
|
+
'appears — never a null image or an empty bag in the meantime. (FAIL-LOUD.)');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function _fail(msg) {
|
|
386
|
+
throw new Error('[RenderStages.normalizeStill] ' + msg + ' (FAIL-LOUD.)');
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** THE positive-integer predicate — `_posInt` (still dims) and `assertSceneBound` (scene bounds)
|
|
390
|
+
both decide through it, each with its own prefix and wording. @private */
|
|
391
|
+
function _isPosInt(v) {
|
|
392
|
+
return typeof v === 'number' && isFinite(v) && v > 0 && Math.floor(v) === v;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function _posInt(v, name) {
|
|
396
|
+
if (!_isPosInt(v)) {
|
|
397
|
+
_fail(name + ' must be a positive integer, got ' + String(v));
|
|
398
|
+
}
|
|
399
|
+
return v;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ── `data` IS IMMUTABLE BY CONTRACT — snapshot at capture ─────────────────────────
|
|
403
|
+
//
|
|
404
|
+
// A still that keeps changing is not a still. The `tick` stage's payload arrives as the
|
|
405
|
+
// LIVE `TickData.frameData` the engine just marshalled, and the render contract makes NO
|
|
406
|
+
// promise that the marshalling layer builds fresh arrays/objects per tick — so a record
|
|
407
|
+
// holding that reference is (a) an aliasing hazard the moment any layer reuses a buffer and
|
|
408
|
+
// (b) not serialization-safe, because a live object graph with prototype accessors does not
|
|
409
|
+
// survive JSON. For a training corpus that is the worst class of defect: every record
|
|
410
|
+
// silently drifts toward the latest tick and nothing announces it. So the normalizer
|
|
411
|
+
// SNAPSHOTS.
|
|
412
|
+
//
|
|
413
|
+
// Snapshotting (rather than ratifying a fresh-per-tick guarantee with the engine-boundary
|
|
414
|
+
// owner) is the ruled choice because it buys the SECOND property too: the snapshot is plain
|
|
415
|
+
// data, hence JSON-safe, which is the prerequisite for any on-disk corpus row.
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Every READABLE property name of an object, own and inherited, data and accessor.
|
|
419
|
+
*
|
|
420
|
+
* Why not `Object.keys` / `structuredClone` / a JSON round-trip: all three copy only OWN
|
|
421
|
+
* ENUMERABLE DATA properties, and the engine's `FrameData.rect` is a `Rectangle` whose
|
|
422
|
+
* `x`/`y`/`left`/`top`/`right`/`bottom` are NON-ENUMERABLE PROTOTYPE ACCESSORS over private
|
|
423
|
+
* `_x`/`_y` backing fields. Any of those three would therefore have produced a snapshot
|
|
424
|
+
* whose `rect.x` was silently `undefined` — swapping one silent-data-loss bug for another,
|
|
425
|
+
* and forcing a corpus consumer to read a private field it should never know about.
|
|
426
|
+
*
|
|
427
|
+
* This walk FLATTENS accessors into plain data. It is deliberately GENERIC — it knows no
|
|
428
|
+
* field names, so it cannot drift from `FrameDataSchema` (the frame-data bubble's owner,
|
|
429
|
+
* which this contract points AT and never restates). The backing field AND its accessor both
|
|
430
|
+
* survive (e.g. both `_x` and `x`) because the walk knows no naming convention — a corpus
|
|
431
|
+
* writer that wants only one may project.
|
|
432
|
+
*
|
|
433
|
+
* Functions are split by OWNERSHIP, deliberately: an INHERITED function is a METHOD and is
|
|
434
|
+
* skipped (that is what flattening means), while an OWN function-valued field is a genuine
|
|
435
|
+
* contract violation and is INCLUDED here so the clone can THROW naming its path. Silently
|
|
436
|
+
* dropping it would be the fail-silent behaviour this whole change exists to remove.
|
|
437
|
+
*
|
|
438
|
+
* @param {object} obj
|
|
439
|
+
* @returns {string[]}
|
|
440
|
+
*/
|
|
441
|
+
function _readableKeys(obj) {
|
|
442
|
+
var keys = [];
|
|
443
|
+
var seen = Object.create(null);
|
|
444
|
+
var o = obj;
|
|
445
|
+
var own = true;
|
|
446
|
+
while (o && o !== Object.prototype && o !== Array.prototype) {
|
|
447
|
+
var names = Object.getOwnPropertyNames(o);
|
|
448
|
+
for (var i = 0; i < names.length; i++) {
|
|
449
|
+
var k = names[i];
|
|
450
|
+
if (k === 'constructor' || seen[k]) { continue; }
|
|
451
|
+
var d = Object.getOwnPropertyDescriptor(o, k);
|
|
452
|
+
if (d.get) { seen[k] = true; keys.push(k); }
|
|
453
|
+
else if (own || typeof d.value !== 'function') { seen[k] = true; keys.push(k); }
|
|
454
|
+
}
|
|
455
|
+
o = Object.getPrototypeOf(o);
|
|
456
|
+
own = false;
|
|
457
|
+
}
|
|
458
|
+
return keys;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function _isTypedArray(v) {
|
|
462
|
+
return v && typeof v === 'object' && typeof v.BYTES_PER_ELEMENT === 'number' &&
|
|
463
|
+
typeof v.length === 'number' && typeof v.constructor === 'function';
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Deep PLAIN-DATA clone. Primitives pass through; typed arrays are copied; arrays and
|
|
468
|
+
* objects are rebuilt as plain `Array`/`Object` with accessors flattened. A FUNCTION value
|
|
469
|
+
* THROWS naming its path — a function inside a data payload means the payload is not
|
|
470
|
+
* serialization-safe, which is precisely what this contract requires it to be.
|
|
471
|
+
* @private
|
|
472
|
+
*/
|
|
473
|
+
function _deepPlainClone(v, path) {
|
|
474
|
+
if (v === null || v === undefined) { return null; }
|
|
475
|
+
var t = typeof v;
|
|
476
|
+
if (t === 'number' || t === 'string' || t === 'boolean') { return v; }
|
|
477
|
+
if (t === 'function') {
|
|
478
|
+
_fail('the payload carries a FUNCTION at ' + path + ' — a still record must be plain, ' +
|
|
479
|
+
'serialization-safe data');
|
|
480
|
+
}
|
|
481
|
+
if (t !== 'object') {
|
|
482
|
+
_fail('the payload carries an unsupported ' + t + ' at ' + path);
|
|
483
|
+
}
|
|
484
|
+
if (_isTypedArray(v)) { return new v.constructor(v); }
|
|
485
|
+
if (Array.isArray(v)) {
|
|
486
|
+
var arr = new Array(v.length);
|
|
487
|
+
for (var i = 0; i < v.length; i++) { arr[i] = _deepPlainClone(v[i], path + '[' + i + ']'); }
|
|
488
|
+
return arr;
|
|
489
|
+
}
|
|
490
|
+
var out = {};
|
|
491
|
+
var keys = _readableKeys(v);
|
|
492
|
+
for (var j = 0; j < keys.length; j++) {
|
|
493
|
+
var k = keys[j];
|
|
494
|
+
var val;
|
|
495
|
+
try { val = v[k]; }
|
|
496
|
+
catch (e) { continue; } // a throwing getter is not payload data
|
|
497
|
+
// An OWN function-valued field reaches here (see _readableKeys) and THROWS below;
|
|
498
|
+
// inherited methods were already filtered out as flattening, not as an error.
|
|
499
|
+
out[k] = _deepPlainClone(val, path + '.' + k);
|
|
500
|
+
}
|
|
501
|
+
return out;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* deepPlainCopy(value) — THE plain-data deep-copy authority.
|
|
506
|
+
*
|
|
507
|
+
* Returns a copy that shares NO object identity with `value` (accessors flattened, typed
|
|
508
|
+
* arrays copied, prototypes dropped). Exported because the same guarantee is needed in two
|
|
509
|
+
* places and must not be written twice: the still PAYLOAD (`snapshotPayload`, R8) and the
|
|
510
|
+
* SCENE RECORD handed out by `runner.sceneRecord()`. A record a caller can mutate is
|
|
511
|
+
* internal state on loan, not a record.
|
|
512
|
+
*
|
|
513
|
+
* @param {*} value
|
|
514
|
+
* @returns {*} a copy sharing no object identity with `value`
|
|
515
|
+
*/
|
|
516
|
+
function deepPlainCopy(value) { return _deepPlainClone(value, 'value'); }
|
|
517
|
+
|
|
518
|
+
/**
|
|
519
|
+
* snapshotPayload(data, format) — the immutability guarantee, applied.
|
|
520
|
+
*
|
|
521
|
+
* `'rgba8'`/`'r8'` → a fresh typed-array copy. `'framedata'` → a deep plain-data clone with
|
|
522
|
+
* accessors flattened. Exported so the conformance gate and any future stage executor use
|
|
523
|
+
* the SAME snapshot, never a second one.
|
|
524
|
+
*
|
|
525
|
+
* @param {*} data — the stage implementation's payload
|
|
526
|
+
* @param {string} format — a `STAGE_FORMATS` value
|
|
527
|
+
* @returns {*} a payload that no later tick can change
|
|
528
|
+
*/
|
|
529
|
+
function snapshotPayload(data, format) {
|
|
530
|
+
if (format === 'framedata') { return _deepPlainClone(data, 'data'); }
|
|
531
|
+
if (_isTypedArray(data)) { return new data.constructor(data); }
|
|
532
|
+
if (Array.isArray(data)) { return _deepPlainClone(data, 'data'); }
|
|
533
|
+
_fail('format ' + JSON.stringify(format) + ' expects an indexed buffer payload, got ' + String(data));
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ── The SCENE record: immutable, CONTENT-ADDRESSED ───────────────────────────────
|
|
537
|
+
//
|
|
538
|
+
// `scene = sha256(canonicalize(SceneRecord))`. Canonicalization lives HERE — one hashing
|
|
539
|
+
// authority, or two writers mint two identities for the same scene. Content-addressing buys
|
|
540
|
+
// two properties for free: identical setups SHARE a scene identity across runs, and any
|
|
541
|
+
// drift between what a still claims and what the scene was is DETECTABLE.
|
|
542
|
+
//
|
|
543
|
+
// Scene records are IMMUTABLE. A structural edit (object added/deleted) does not revise a
|
|
544
|
+
// record, it mints a NEW one — under content-addressing "revision bump" and "new record" are
|
|
545
|
+
// the same operation, and a mutable revisioned record would reintroduce exactly the
|
|
546
|
+
// identity-vs-content ambiguity content-addressing removes. A field-level revision scheme
|
|
547
|
+
// would have been catastrophic in the other direction: a per-tick pose change would mint a
|
|
548
|
+
// revision per tick and the normalization would have destroyed itself.
|
|
549
|
+
|
|
550
|
+
function _failScene(msg) {
|
|
551
|
+
throw new Error('[RenderStages.normalizeScene] ' + msg + ' (FAIL-LOUD.)');
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* assertSceneBound(v, name) — THE rule for a physics bound (`physicsW`/`physicsH`): a positive
|
|
556
|
+
* integer. `normalizeScene` validates through it, and a host that accepts a STATED bound as an
|
|
557
|
+
* expectation checks it here before it acquires anything — one rule, never a second copy.
|
|
558
|
+
*
|
|
559
|
+
* @param {*} v
|
|
560
|
+
* @param {string} name — 'physicsW' | 'physicsH'
|
|
561
|
+
* @returns {number} v
|
|
562
|
+
*/
|
|
563
|
+
function assertSceneBound(v, name) {
|
|
564
|
+
if (!_isPosInt(v)) {
|
|
565
|
+
_failScene(name + ' must be a positive integer (a physics bound, init-fixed), got ' + String(v));
|
|
566
|
+
}
|
|
567
|
+
return v;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* assertSourceSha256(v) — THE rule for a scene's code identity: null/undefined (honestly absent) or a
|
|
572
|
+
* project identity, 64 lowercase hex characters (identity.js projectIdentity). Returns null or v.
|
|
573
|
+
* @param {*} v
|
|
574
|
+
* @returns {string|null}
|
|
575
|
+
*/
|
|
576
|
+
function assertSourceSha256(v) {
|
|
577
|
+
if (v === undefined || v === null) { return null; }
|
|
578
|
+
if (typeof v !== 'string' || !/^[0-9a-f]{64}$/.test(v)) {
|
|
579
|
+
_failScene('sourceSha256 must be null or a project identity (64 lowercase hex characters, ' +
|
|
580
|
+
'lib/shim/project/identity.js projectIdentity), got ' + String(v));
|
|
581
|
+
}
|
|
582
|
+
return v;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* normalizeScene(partial) — ferry a partial scene bag to the full canonical SceneRecord.
|
|
587
|
+
*
|
|
588
|
+
* Every `SCENE_KEYS` key is present on the result. `seed` must be a finite number (it is
|
|
589
|
+
* half of the regeneration triple — a null seed silently downgrades "regenerate" to
|
|
590
|
+
* "hope"); `sourceRef` and `build` and `projection` may be `null` (honestly absent).
|
|
591
|
+
*
|
|
592
|
+
* @param {object} partial
|
|
593
|
+
* @returns {object} a record carrying exactly SCENE_KEYS
|
|
594
|
+
*/
|
|
595
|
+
function normalizeScene(partial) {
|
|
596
|
+
if (!partial || typeof partial !== 'object') {
|
|
597
|
+
_failScene('expected a partial scene bag object, got ' + String(partial));
|
|
598
|
+
}
|
|
599
|
+
if (partial.v !== undefined && partial.v !== null && partial.v !== SCHEMA_VERSION) {
|
|
600
|
+
_failScene('v ' + String(partial.v) + ' disagrees with the contract version ' +
|
|
601
|
+
SCHEMA_VERSION + ' — the normalizer is the version authority, `v` is never ' +
|
|
602
|
+
'caller-supplied');
|
|
603
|
+
}
|
|
604
|
+
if (typeof partial.seed !== 'number' || !isFinite(partial.seed)) {
|
|
605
|
+
_failScene('seed must be a finite number — it is half of the regeneration triple ' +
|
|
606
|
+
'(code, seed, tick), and a null seed downgrades "regenerate" to "trust the ' +
|
|
607
|
+
'raster", got ' + String(partial.seed));
|
|
608
|
+
}
|
|
609
|
+
return {
|
|
610
|
+
v: SCHEMA_VERSION,
|
|
611
|
+
seed: partial.seed,
|
|
612
|
+
sourceRef: (partial.sourceRef === undefined || partial.sourceRef === null)
|
|
613
|
+
? null : String(partial.sourceRef),
|
|
614
|
+
sourceSha256: assertSourceSha256(partial.sourceSha256),
|
|
615
|
+
// COPIED, never aliased: a BYO provider is free to return a CACHED projection
|
|
616
|
+
// object from `projection()`, and a caller is free to keep a reference to the
|
|
617
|
+
// `build` bag it passed in. Either would make the record a live view of somebody
|
|
618
|
+
// else's mutable state — the R8 defect class, in the record that the whole
|
|
619
|
+
// regenerate-not-replay claim rests on.
|
|
620
|
+
build: (partial.build && typeof partial.build === 'object')
|
|
621
|
+
? deepPlainCopy(partial.build) : null,
|
|
622
|
+
physicsW: assertSceneBound(partial.physicsW, 'physicsW'),
|
|
623
|
+
physicsH: assertSceneBound(partial.physicsH, 'physicsH'),
|
|
624
|
+
projection: (partial.projection && typeof partial.projection === 'object')
|
|
625
|
+
? deepPlainCopy(partial.projection) : null
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/** Deterministic key-sorted encoding of a plain value. @private */
|
|
630
|
+
function _stableEncode(v) {
|
|
631
|
+
if (v === null || v === undefined) { return 'null'; }
|
|
632
|
+
var t = typeof v;
|
|
633
|
+
if (t === 'number' || t === 'boolean') { return String(v); }
|
|
634
|
+
if (t === 'string') { return JSON.stringify(v); }
|
|
635
|
+
if (Array.isArray(v)) {
|
|
636
|
+
return '[' + v.map(_stableEncode).join(',') + ']';
|
|
637
|
+
}
|
|
638
|
+
var keys = Object.keys(v).sort();
|
|
639
|
+
return '{' + keys.map(function (k) {
|
|
640
|
+
return JSON.stringify(k) + ':' + _stableEncode(v[k]);
|
|
641
|
+
}).join(',') + '}';
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
/**
|
|
645
|
+
* canonicalizeScene(record) — the ONE canonical byte-string a scene identity is taken over.
|
|
646
|
+
*
|
|
647
|
+
* PROJECTS onto `SCENE_KEYS` in table order, encoding nested objects with sorted keys. Two
|
|
648
|
+
* consequences worth stating: (a) key insertion order cannot change an identity, and (b) an
|
|
649
|
+
* `scene` id field carried ALONGSIDE the record (as `getScene()` does for convenience)
|
|
650
|
+
* cannot perturb its own hash, because it is not a `SCENE_KEYS` member and is projected out.
|
|
651
|
+
*
|
|
652
|
+
* @param {object} record — a normalized SceneRecord (or a superset of one)
|
|
653
|
+
* @returns {string}
|
|
654
|
+
*/
|
|
655
|
+
function canonicalizeScene(record) {
|
|
656
|
+
if (!record || typeof record !== 'object') {
|
|
657
|
+
_failScene('canonicalizeScene expects a record object, got ' + String(record));
|
|
658
|
+
}
|
|
659
|
+
var parts = [];
|
|
660
|
+
for (var i = 0; i < SCENE_KEYS.length; i++) {
|
|
661
|
+
var k = SCENE_KEYS[i];
|
|
662
|
+
parts.push(JSON.stringify(k) + ':' + _stableEncode(record[k]));
|
|
663
|
+
}
|
|
664
|
+
return '{' + parts.join(',') + '}';
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* sceneHash(record) → Promise<'sha256:<hex>'> — the content address.
|
|
669
|
+
*
|
|
670
|
+
* ASYNC because the platform's own digest is: `crypto.subtle.digest` is the ONE hashing
|
|
671
|
+
* implementation used here, and it is Promise-based. It is a global in both the browser and
|
|
672
|
+
* modern Node, so this is a single path — not a per-environment fork of a crypto primitive
|
|
673
|
+
* (hand-rolling sha256 in the render layer would be exactly the wrong kind of DRY win).
|
|
674
|
+
*
|
|
675
|
+
* The `sha256:` prefix makes the algorithm self-describing in the record, so a future
|
|
676
|
+
* algorithm change can never be silent.
|
|
677
|
+
*
|
|
678
|
+
* @param {object} record
|
|
679
|
+
* @returns {Promise<string>}
|
|
680
|
+
*/
|
|
681
|
+
function sceneHash(record) {
|
|
682
|
+
var canon = canonicalizeScene(record);
|
|
683
|
+
var subtle = (typeof crypto !== 'undefined') ? crypto.subtle : null;
|
|
684
|
+
if (!subtle || typeof subtle.digest !== 'function') {
|
|
685
|
+
return Promise.reject(new Error('[RenderStages.sceneHash] crypto.subtle.digest is ' +
|
|
686
|
+
'unavailable — the scene identity is a content hash and there is no fallback ' +
|
|
687
|
+
'hashing path by design (two hashing authorities would mint two identities for ' +
|
|
688
|
+
'one scene). (FAIL-LOUD.)'));
|
|
689
|
+
}
|
|
690
|
+
var bytes = new TextEncoder().encode(canon);
|
|
691
|
+
return Promise.resolve(subtle.digest('SHA-256', bytes)).then(function (buf) {
|
|
692
|
+
var view = new Uint8Array(buf);
|
|
693
|
+
var hex = '';
|
|
694
|
+
for (var i = 0; i < view.length; i++) {
|
|
695
|
+
hex += (view[i] < 16 ? '0' : '') + view[i].toString(16);
|
|
696
|
+
}
|
|
697
|
+
return 'sha256:' + hex;
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/**
|
|
702
|
+
* normalizeStill(partial) — ferry a partial tap result to the FULL canonical bag.
|
|
703
|
+
*
|
|
704
|
+
* The owner-exported normalizer of the "owner exports contract → consumers ferry the bag"
|
|
705
|
+
* pattern: the dispatcher merges the stage implementation's render-domain fields with the
|
|
706
|
+
* provenance it alone knows, hands the merged partial here ONCE, and every consumer reads
|
|
707
|
+
* a bag with all STILL_BAG_KEYS present. No consumer re-enumerates the key list.
|
|
708
|
+
*
|
|
709
|
+
* FAIL-LOUD rules (each closes a way a bag could lie):
|
|
710
|
+
* - unknown/absent `stage` → assertStage throws (names the table)
|
|
711
|
+
* - absent or EMPTY `data` → throw. A zero-length payload is the "silent
|
|
712
|
+
* empty bag"/black-still failure the whole
|
|
713
|
+
* capability exists to make impossible.
|
|
714
|
+
* - `format` ≠ STAGE_FORMATS[stage] → throw. The format is a property of the
|
|
715
|
+
* stage, not a caller opinion.
|
|
716
|
+
* - non-positive-integer width/height → throw.
|
|
717
|
+
* - `filtersApplied` not an array → throw (order is contract; a map is not).
|
|
718
|
+
* - a filter ran without a finite → throw. A filtered still without the uTime it
|
|
719
|
+
* `filterTime`, or a `filterTime` drew with is not regenerable; a time with no
|
|
720
|
+
* with no filter filter claims a pass that never ran. Checked
|
|
721
|
+
* HERE because a BYO captureStill() bag never
|
|
722
|
+
* passes the built-in providers' readback helper.
|
|
723
|
+
* - a caller-supplied `composedOf` that → throw. Provenance is stamped by its
|
|
724
|
+
* disagrees with upstreamStages(stage) authority, never supplied by the caller.
|
|
725
|
+
*
|
|
726
|
+
* @param {object} partial
|
|
727
|
+
* @returns {object} a bag carrying every STILL_BAG_KEYS key
|
|
728
|
+
*/
|
|
729
|
+
function normalizeStill(partial) {
|
|
730
|
+
if (!partial || typeof partial !== 'object') { _fail('expected a partial bag object, got ' + String(partial)); }
|
|
731
|
+
var stage = assertStage(partial.stage);
|
|
732
|
+
|
|
733
|
+
var format = STAGE_FORMATS[stage];
|
|
734
|
+
if (partial.format !== undefined && partial.format !== null && partial.format !== format) {
|
|
735
|
+
_fail('format ' + JSON.stringify(partial.format) + ' does not match stage ' +
|
|
736
|
+
JSON.stringify(stage) + '\'s declared format ' + JSON.stringify(format));
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
var data = partial.data;
|
|
740
|
+
if (data === undefined || data === null) {
|
|
741
|
+
_fail('stage ' + JSON.stringify(stage) + ' returned no `data` payload');
|
|
742
|
+
}
|
|
743
|
+
if (typeof data.length !== 'number') {
|
|
744
|
+
_fail('`data` must be an indexed payload (Array / Uint8Array / Uint8ClampedArray), got ' + String(data));
|
|
745
|
+
}
|
|
746
|
+
if (data.length === 0) {
|
|
747
|
+
_fail('stage ' + JSON.stringify(stage) + ' returned an EMPTY `data` payload — never ' +
|
|
748
|
+
'a blank still: a measurement or golden built on an empty capture is fabricated evidence');
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if (partial.v !== undefined && partial.v !== null && partial.v !== SCHEMA_VERSION) {
|
|
752
|
+
_fail('v ' + String(partial.v) + ' disagrees with the contract version ' + SCHEMA_VERSION +
|
|
753
|
+
' — the normalizer is the version authority, `v` is never caller-supplied');
|
|
754
|
+
}
|
|
755
|
+
if (typeof partial.scene !== 'string' || !partial.scene) {
|
|
756
|
+
_fail('scene must be the content hash of the SceneRecord this still belongs to — a ' +
|
|
757
|
+
'still without a scene id is unmoored: it cannot be verified against, or ' +
|
|
758
|
+
'regenerated from, anything. Got ' + String(partial.scene));
|
|
759
|
+
}
|
|
760
|
+
if (typeof partial.tick !== 'number' || !isFinite(partial.tick) || partial.tick < 0) {
|
|
761
|
+
_fail('tick must be a finite non-negative number (the dispatcher stamps it from the ' +
|
|
762
|
+
'live TickData — the tick-equality assertion is what co-registers a multi-channel ' +
|
|
763
|
+
'corpus row), got ' + String(partial.tick));
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
var filters = partial.filtersApplied;
|
|
767
|
+
if (!Array.isArray(filters)) {
|
|
768
|
+
_fail('filtersApplied must be an ARRAY of filter names in the order they ran (the ' +
|
|
769
|
+
'provider\'s ordered _filters list is the one authority), got ' + String(filters));
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
var filterTime = (partial.filterTime === undefined) ? null : partial.filterTime;
|
|
773
|
+
if (filters.length > 0 ? !(typeof filterTime === 'number' && isFinite(filterTime)) : filterTime !== null) {
|
|
774
|
+
_fail('filterTime must be the finite uTime the filter passes drew with when filtersApplied is ' +
|
|
775
|
+
'non-empty, and null when it is empty (filtersApplied ' + JSON.stringify(filters) +
|
|
776
|
+
', filterTime ' + String(filterTime) + ') — the provider reports it ' +
|
|
777
|
+
'(RendererProviderContract.filterTime of the rendered tick)');
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
var composed = upstreamStages(stage);
|
|
781
|
+
if (partial.composedOf !== undefined && partial.composedOf !== null) {
|
|
782
|
+
var supplied = partial.composedOf;
|
|
783
|
+
var same = Array.isArray(supplied) && supplied.length === composed.length &&
|
|
784
|
+
supplied.every(function (s, i) { return s === composed[i]; });
|
|
785
|
+
if (!same) {
|
|
786
|
+
_fail('composedOf is DERIVED from RENDER_STAGES (' + JSON.stringify(composed) +
|
|
787
|
+
') and is never caller-supplied; got ' + JSON.stringify(supplied));
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
return {
|
|
792
|
+
v: SCHEMA_VERSION,
|
|
793
|
+
scene: partial.scene,
|
|
794
|
+
stage: stage,
|
|
795
|
+
tick: partial.tick,
|
|
796
|
+
format: format,
|
|
797
|
+
// IMMUTABLE BY CONTRACT: snapshot at capture. See snapshotPayload's docstring for
|
|
798
|
+
// why a live reference here is the worst defect class a training corpus can carry.
|
|
799
|
+
data: snapshotPayload(data, format),
|
|
800
|
+
width: _posInt(partial.width, 'width'),
|
|
801
|
+
height: _posInt(partial.height, 'height'),
|
|
802
|
+
// CSS size is meaningful only for a stage that read a real canvas; null elsewhere.
|
|
803
|
+
cssWidth: (typeof partial.cssWidth === 'number') ? partial.cssWidth : null,
|
|
804
|
+
cssHeight: (typeof partial.cssHeight === 'number') ? partial.cssHeight : null,
|
|
805
|
+
filtersApplied: filters.slice(),
|
|
806
|
+
// null (not false) on a stage where NOTHING composited — reporting `true` there
|
|
807
|
+
// would be a claim about a compositing pass that never ran.
|
|
808
|
+
renderBaseCanvas: (typeof partial.renderBaseCanvas === 'boolean') ? partial.renderBaseCanvas : null,
|
|
809
|
+
// The uTime the provider's filter passes drew with; null where no filter ran (checked
|
|
810
|
+
// above). A filtered still is regenerable only with it (a filter may animate on uTime).
|
|
811
|
+
filterTime: filterTime,
|
|
812
|
+
pose: (partial.pose !== undefined && partial.pose !== null) ? partial.pose : null,
|
|
813
|
+
// The FieldBuilder options the field tap actually ran with (gain/radius/blur/dims).
|
|
814
|
+
// Two field captures at different gain are otherwise INDISTINGUISHABLE, and the r8
|
|
815
|
+
// channel is a dimensionless instrument response, comparable only at equal options.
|
|
816
|
+
// null on every other stage.
|
|
817
|
+
fieldOpts: (partial.fieldOpts && typeof partial.fieldOpts === 'object')
|
|
818
|
+
? partial.fieldOpts : null,
|
|
819
|
+
composedOf: composed,
|
|
820
|
+
renderOptions: (partial.renderOptions && typeof partial.renderOptions === 'object')
|
|
821
|
+
? partial.renderOptions : {}
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// ── Export ────────────────────────────────────────────────────────────────────
|
|
826
|
+
|
|
827
|
+
export {
|
|
828
|
+
SCHEMA_VERSION,
|
|
829
|
+
RENDER_STAGES,
|
|
830
|
+
STAGE_FORMATS,
|
|
831
|
+
SCENE_KEYS,
|
|
832
|
+
SCENE_BOUND_KEYS,
|
|
833
|
+
STILL_KEYS,
|
|
834
|
+
STILL_DERIVED_KEYS,
|
|
835
|
+
STILL_BAG_KEYS,
|
|
836
|
+
STILL_PAYLOAD_KEYS,
|
|
837
|
+
STILL_PROVENANCE_KEYS,
|
|
838
|
+
RUNTIME_MUTATION_SURFACES,
|
|
839
|
+
finalStage,
|
|
840
|
+
upstreamStages,
|
|
841
|
+
assertStage,
|
|
842
|
+
stageNotTappableError,
|
|
843
|
+
deepPlainCopy,
|
|
844
|
+
snapshotPayload,
|
|
845
|
+
normalizeScene,
|
|
846
|
+
assertSceneBound,
|
|
847
|
+
assertSourceSha256,
|
|
848
|
+
canonicalizeScene,
|
|
849
|
+
sceneHash,
|
|
850
|
+
normalizeStill
|
|
851
|
+
};
|
|
852
|
+
// SCENE_SURFACE_ATTR — the attribute `FrqtlApp.createCanvas` stamps on the scene's drawing surface. OWNED by the
|
|
853
|
+
// RendererProvider contract; re-exported here so consumers above the engine reach it through the shim's render
|
|
854
|
+
// vocabulary — a barrel, never a second literal.
|
|
855
|
+
export { SCENE_SURFACE_ATTR } from '../../frqtl/compat/renderer/renderer-provider-contract.js';
|