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