@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,575 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CameraPath.js — a declarative, REPRODUCIBLE camera path.
|
|
5
|
+
*
|
|
6
|
+
* ONE object for both modes. It drives `runner.cameraControl` — the single camera write path —
|
|
7
|
+
* with ABSOLUTE poses derived from waypoints, so the same waypoints and the same tick always
|
|
8
|
+
* produce the same picture. There is **no `if (is3D)` anywhere**: a 3D path declares full
|
|
9
|
+
* orbit-bag waypoints, a 2D path declares `{distance, target}` waypoints, and both go through
|
|
10
|
+
* the identical per-field interpolation. Which fields are legal is read from the handle's own
|
|
11
|
+
* `poseFields` — the consumer asks what axes EXIST, never what mode it is in.
|
|
12
|
+
*
|
|
13
|
+
* ── Layer 1: API contract ────────────────────────────────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* `CameraPath.attach(runner, { waypoints, loop? }) → handle`
|
|
16
|
+
*
|
|
17
|
+
* @param {object} runner — an FrqtlApp. The ONLY couplings: `runner.cameraControl`
|
|
18
|
+
* (the capability-checked camera handle), the per-tick listener fan-out, and
|
|
19
|
+
* `runner.repaint()`. No renderer-sniffing, no engine, no capture knowledge.
|
|
20
|
+
* @param {Array<{atTick:number, pose:object}>} waypoints — at least one. `pose` is a PARTIAL
|
|
21
|
+
* orbit bag with `setOrbit` patch semantics. Sorted by `atTick` at attach.
|
|
22
|
+
* @param {boolean} [loop=false] — wrap the path modulo its duration.
|
|
23
|
+
* @returns {{
|
|
24
|
+
* start():void, stop():void, dispose():void,
|
|
25
|
+
* poseAt(tick):object, // PURE — same waypoints + same tick ⇒ same pose
|
|
26
|
+
* onWaypoint(cb):function, // cb({ tick, index, pose }); returns an unsubscribe
|
|
27
|
+
* running:boolean
|
|
28
|
+
* }}
|
|
29
|
+
*
|
|
30
|
+
* `CameraPath.poseAt(waypoints, tick, loop)` is also exported as a FREE function — pure, no
|
|
31
|
+
* runner, no wall clock, unit-testable in plain Node (the `three-camera-geometry` discipline).
|
|
32
|
+
*
|
|
33
|
+
* ── Why absolute writes, and why not look()/move() ───────────────────────────
|
|
34
|
+
*
|
|
35
|
+
* Playback is a tick-fan-out subscriber calling `cameraControl.setOrbit(poseAt(tick))`:
|
|
36
|
+
* declarative ABSOLUTE writes through the one camera authority. It deliberately does NOT drive
|
|
37
|
+
* the incremental `look()`/`move()` surface — integrating deltas would accumulate drift and
|
|
38
|
+
* make the path unreproducible, which is the exact property this object exists to guarantee.
|
|
39
|
+
* The tick is the ONLY clock: no wall time, no RAF coupling, so a paused-and-stepped run and a
|
|
40
|
+
* live run trace the identical path.
|
|
41
|
+
*
|
|
42
|
+
* ── Interpolation ────────────────────────────────────────────────────────────
|
|
43
|
+
*
|
|
44
|
+
* Per-field over the bag, between the two bracketing waypoints:
|
|
45
|
+
* - `azimuth`/`roll` — SHORTEST-ARC mod 360 (a camera turning 350° the long way round is
|
|
46
|
+
* never what a waypoint pair meant).
|
|
47
|
+
* - every other number (`elevation`, `distance`, …) — scalar lerp.
|
|
48
|
+
* - `target` — componentwise; a segment mixing `target:null` with a concrete target THROWS
|
|
49
|
+
* at attach, because lerping "the scene centre" against a point is undefined. Declare both
|
|
50
|
+
* or neither on adjacent waypoints.
|
|
51
|
+
* A field present in only ONE of the two bracketing waypoints is held constant (no invented
|
|
52
|
+
* midpoint against an unstated value).
|
|
53
|
+
*
|
|
54
|
+
* ── FAIL-LOUD (a declared camera path that cannot fly is not a hidden affordance) ───
|
|
55
|
+
*
|
|
56
|
+
* `attach` THROWS when `runner.cameraControl` is null (a provider with NO pose control at all)
|
|
57
|
+
* or when a waypoint names a field the handle does not declare (e.g. `azimuth` in 2D). This
|
|
58
|
+
* deliberately BREAKS from the `CameraControls` fail-soft precedent, and the distinction is
|
|
59
|
+
* the same one the capture contract draws: a UI widget that hides is an affordance; a declared
|
|
60
|
+
* camera path that silently does not fly, under a golden or a corpus row, is FABRICATED EVIDENCE.
|
|
61
|
+
*
|
|
62
|
+
* ── Capture at a waypoint belongs to NEITHER object ──────────────────────────
|
|
63
|
+
*
|
|
64
|
+
* It is composition by the caller:
|
|
65
|
+
*
|
|
66
|
+
* `drone.onWaypoint(async () => rows.push(await vision.getStill()));`
|
|
67
|
+
*
|
|
68
|
+
* `FrqtlVision` keeps zero camera knowledge (no pose on capture) and `CameraPath` keeps zero
|
|
69
|
+
* capture knowledge — a `capture:true` waypoint flag would be the coupling that makes both
|
|
70
|
+
* objects unpluggable separately. No data coupling is needed because the provenance already
|
|
71
|
+
* closes the loop: every still stamps `pose` from the live camera authority, so a
|
|
72
|
+
* camera path-captured row records the ACTUAL applied pose. The two objects compose through the
|
|
73
|
+
* contract, not through each other. A callback is invoked and NOT awaited — the tick clock is
|
|
74
|
+
* never held hostage by a consumer (a throwing callback is surfaced, never swallowed).
|
|
75
|
+
*
|
|
76
|
+
* ── Repaint ──────────────────────────────────────────────────────────────────
|
|
77
|
+
*
|
|
78
|
+
* A pose write on a PAUSED scene changes nothing visible until something redraws, which reads
|
|
79
|
+
* as "the camera is broken". So every pose write is followed by `runner.repaint()` — the
|
|
80
|
+
* existing one-shot redraw that re-renders the LAST TickData without advancing the clock. The
|
|
81
|
+
* same recipe applies to a manual pan/zoom: `setOrbit(...)` → `repaint()` → capture.
|
|
82
|
+
*
|
|
83
|
+
* ── Layer 2: physics behaviour ───────────────────────────────────────────────
|
|
84
|
+
* ZERO physics. A camera path is an observation instrument: it changes which pixels you are
|
|
85
|
+
* shown, never a frame, a dimension or a tick. No canonical-bench golden can move because of it.
|
|
86
|
+
*
|
|
87
|
+
* Usage:
|
|
88
|
+
* var drone = CameraPath.attach(runner, { waypoints: [
|
|
89
|
+
* { atTick: 0, pose: { distance: 400, target: null } },
|
|
90
|
+
* { atTick: 200, pose: { distance: 60, target: { x: 300, y: 220 } } }
|
|
91
|
+
* ]});
|
|
92
|
+
* drone.onWaypoint(async () => rows.push(await vision.getStill()));
|
|
93
|
+
* drone.start();
|
|
94
|
+
*/
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
// NO hand list of angular axes. Wrap-ness is DECLARED by the camera that owns the axis
|
|
98
|
+
// (`poseFields[].constraint.wrap` — the period of a periodic value space) and FERRIED here.
|
|
99
|
+
// A hand-written ['azimuth','roll'] encoded, in this file, a fact the contract holds
|
|
100
|
+
// elsewhere: a camera declaring a NEW wrapping axis would have silently got long-way-round
|
|
101
|
+
// interpolation until someone remembered to edit this list.
|
|
102
|
+
|
|
103
|
+
function _fail(msg) {
|
|
104
|
+
throw new Error('[CameraPath] ' + msg + ' (FAIL-LOUD.)');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function _finite(v, where) {
|
|
108
|
+
if (typeof v === 'number' && isFinite(v)) { return v; }
|
|
109
|
+
_fail(where + ' must be a finite number, got ' + String(v));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Shortest-arc interpolation on a value space of the given PERIOD (declared, not assumed).
|
|
114
|
+
* @private
|
|
115
|
+
*/
|
|
116
|
+
function _lerpAngle(a, b, t, period) {
|
|
117
|
+
var half = period / 2;
|
|
118
|
+
var d = ((((b - a) % period) + period + half) % period) - half; // → (-half, half]
|
|
119
|
+
return a + d * t;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function _lerp(a, b, t) { return a + (b - a) * t; }
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Componentwise interpolation of a point, over the UNION of both endpoints' numeric
|
|
126
|
+
* components — so {x,y} and {x,y,z} both work with no mode branch and nothing is dropped.
|
|
127
|
+
* A component present in only one endpoint is HELD (the same rule as a pose field).
|
|
128
|
+
* @private
|
|
129
|
+
*/
|
|
130
|
+
function _lerpPoint(a, b, t) {
|
|
131
|
+
var out = {};
|
|
132
|
+
var keys = _poseKeys(a, b);
|
|
133
|
+
for (var i = 0; i < keys.length; i++) {
|
|
134
|
+
var k = keys[i], va = a[k], vb = b[k];
|
|
135
|
+
if (typeof va === 'number' && typeof vb === 'number') { out[k] = _lerp(va, vb, t); }
|
|
136
|
+
else { out[k] = (vb === undefined) ? va : vb; }
|
|
137
|
+
}
|
|
138
|
+
return out;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* poseAt(waypoints, tick, loop) — THE pure path evaluation.
|
|
143
|
+
*
|
|
144
|
+
* Same waypoints + same tick ⇒ same pose, always. No wall clock, no RAF, no runner: this is
|
|
145
|
+
* what makes a camera path-addressed golden reproducible rather than merely repeatable.
|
|
146
|
+
*
|
|
147
|
+
* @param {Array<{atTick:number, pose:object}>} waypoints — assumed sorted (attach sorts)
|
|
148
|
+
* @param {number} tick
|
|
149
|
+
* @param {boolean} [loop=false]
|
|
150
|
+
* @param {Object<string,number>} [wrapByField] — field name → period, FERRIED from the
|
|
151
|
+
* camera's `poseFields[].constraint.wrap`. A field listed here interpolates on the SHORTEST
|
|
152
|
+
* ARC; everything else is a plain lerp. Omitted ⇒ no field wraps (a caller with no camera
|
|
153
|
+
* is not entitled to guess which axes are circular).
|
|
154
|
+
* @returns {object} an orbit-bag PATCH to hand to `setOrbit`
|
|
155
|
+
*/
|
|
156
|
+
function poseAt(waypoints, tick, loop, wrapByField) {
|
|
157
|
+
if (!Array.isArray(waypoints) || !waypoints.length) { _fail('poseAt needs at least one waypoint'); }
|
|
158
|
+
var first = waypoints[0];
|
|
159
|
+
var last = waypoints[waypoints.length - 1];
|
|
160
|
+
var t = _finite(tick, 'tick');
|
|
161
|
+
|
|
162
|
+
if (loop && waypoints.length > 1) {
|
|
163
|
+
var span = last.atTick - first.atTick;
|
|
164
|
+
if (span > 0) { t = first.atTick + (((t - first.atTick) % span) + span) % span; }
|
|
165
|
+
}
|
|
166
|
+
// Before the first / after the last waypoint the path HOLDS that endpoint — it never
|
|
167
|
+
// extrapolates into poses no waypoint declared.
|
|
168
|
+
if (t <= first.atTick) { return _copyPose(first.pose); }
|
|
169
|
+
if (t >= last.atTick) { return _copyPose(last.pose); }
|
|
170
|
+
|
|
171
|
+
var i = 0;
|
|
172
|
+
while (i < waypoints.length - 1 && waypoints[i + 1].atTick <= t) { i++; }
|
|
173
|
+
var a = waypoints[i], b = waypoints[i + 1];
|
|
174
|
+
var span2 = b.atTick - a.atTick;
|
|
175
|
+
var f = (span2 > 0) ? (t - a.atTick) / span2 : 0;
|
|
176
|
+
|
|
177
|
+
var out = {};
|
|
178
|
+
var keys = _poseKeys(a.pose, b.pose);
|
|
179
|
+
for (var k = 0; k < keys.length; k++) {
|
|
180
|
+
var key = keys[k];
|
|
181
|
+
var va = a.pose[key], vb = b.pose[key];
|
|
182
|
+
// A field present in only ONE bracketing waypoint is HELD — never lerped against an
|
|
183
|
+
// unstated value.
|
|
184
|
+
if (va === undefined) { out[key] = _copyValue(vb); continue; }
|
|
185
|
+
if (vb === undefined) { out[key] = _copyValue(va); continue; }
|
|
186
|
+
if (key === 'target') {
|
|
187
|
+
if (va === null && vb === null) { out[key] = null; continue; }
|
|
188
|
+
// The null/concrete mix is rejected at attach; this is the belt-and-braces.
|
|
189
|
+
if (va === null || vb === null) {
|
|
190
|
+
_fail('cannot interpolate `target` between null (scene centre) and a concrete ' +
|
|
191
|
+
'point across the segment at ticks ' + a.atTick + '→' + b.atTick +
|
|
192
|
+
' — declare target on both waypoints or neither');
|
|
193
|
+
}
|
|
194
|
+
// COMPONENTWISE over whatever components the point actually carries — a 2D
|
|
195
|
+
// target is {x,y}, a 3D target is {x,y,z}. Hardcoding x/y here silently DROPPED
|
|
196
|
+
// z on every 3D tracking path (found by the 3D-target gate, not by review).
|
|
197
|
+
out[key] = _lerpPoint(va, vb, f);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (typeof va === 'number' && typeof vb === 'number') {
|
|
201
|
+
var period = wrapByField ? wrapByField[key] : undefined;
|
|
202
|
+
out[key] = (typeof period === 'number' && period > 0)
|
|
203
|
+
? _lerpAngle(va, vb, f, period) : _lerp(va, vb, f);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
out[key] = _copyValue(vb);
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function _poseKeys(a, b) {
|
|
212
|
+
var seen = Object.create(null), out = [];
|
|
213
|
+
[a, b].forEach(function (p) {
|
|
214
|
+
if (!p) { return; }
|
|
215
|
+
Object.keys(p).forEach(function (k) { if (!seen[k]) { seen[k] = 1; out.push(k); } });
|
|
216
|
+
});
|
|
217
|
+
return out;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function _copyValue(v) {
|
|
221
|
+
// A plain-data copy over ALL own keys — never a hand-listed {x,y}, which dropped z from
|
|
222
|
+
// every 3D target it touched.
|
|
223
|
+
if (v && typeof v === 'object') { return JSON.parse(JSON.stringify(v)); }
|
|
224
|
+
return v;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** A pose copy — a handed-out pose never aliases a waypoint the caller still holds. */
|
|
228
|
+
function _copyPose(pose) {
|
|
229
|
+
var out = {};
|
|
230
|
+
if (!pose) { return out; }
|
|
231
|
+
Object.keys(pose).forEach(function (k) { out[k] = _copyValue(pose[k]); });
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* attach(runner, opts) — validate the path against the camera's DECLARED axes, then bind it.
|
|
237
|
+
* @param {object} runner @param {{waypoints:Array, loop?:boolean}} opts
|
|
238
|
+
* @returns {object} the handle (see the module docstring)
|
|
239
|
+
*/
|
|
240
|
+
function attach(runner, opts) {
|
|
241
|
+
if (!runner || typeof runner.addTickListener !== 'function') {
|
|
242
|
+
_fail('attach needs an FrqtlApp with a per-tick listener fan-out');
|
|
243
|
+
}
|
|
244
|
+
var cam = runner.cameraControl;
|
|
245
|
+
if (!cam) {
|
|
246
|
+
_fail('this renderer exposes NO camera pose control (runner.cameraControl is null), so a ' +
|
|
247
|
+
'declared camera path cannot fly. A UI widget may hide on an absent capability; a ' +
|
|
248
|
+
'programmatic instrument must not silently do nothing — a golden or a corpus row ' +
|
|
249
|
+
'built on a camera path that never moved is fabricated evidence');
|
|
250
|
+
}
|
|
251
|
+
opts = opts || {};
|
|
252
|
+
var waypoints = opts.waypoints;
|
|
253
|
+
if (!Array.isArray(waypoints) || !waypoints.length) {
|
|
254
|
+
_fail('attach needs at least one waypoint: [{ atTick, pose }]');
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// The DECLARED axes of THIS camera — ferried from the handle, never a hand-written list.
|
|
258
|
+
// This is the whole reason there is no mode sniffing: 2D declares {distance, target}, 3D
|
|
259
|
+
// declares the full bag, and the SAME validation covers both.
|
|
260
|
+
var declared = {};
|
|
261
|
+
var fields = cam.poseFields || [];
|
|
262
|
+
for (var f = 0; f < fields.length; f++) { declared[fields[f].name] = true; }
|
|
263
|
+
var declaredNames = Object.keys(declared);
|
|
264
|
+
// FERRIED wrap-ness: field name → period, straight off the camera's own descriptors.
|
|
265
|
+
var wrapByField = {};
|
|
266
|
+
for (var w = 0; w < fields.length; w++) {
|
|
267
|
+
var c = fields[w].constraint;
|
|
268
|
+
if (c && typeof c.wrap === 'number' && c.wrap > 0) { wrapByField[fields[w].name] = c.wrap; }
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
var sorted = waypoints.slice().sort(function (a, b) { return a.atTick - b.atTick; });
|
|
272
|
+
for (var i = 0; i < sorted.length; i++) {
|
|
273
|
+
var wp = sorted[i];
|
|
274
|
+
if (!wp || typeof wp !== 'object') { _fail('waypoint ' + i + ' is not an object'); }
|
|
275
|
+
_finite(wp.atTick, 'waypoint ' + i + ' atTick');
|
|
276
|
+
if (!wp.pose || typeof wp.pose !== 'object') { _fail('waypoint ' + i + ' has no pose bag'); }
|
|
277
|
+
var keys = Object.keys(wp.pose);
|
|
278
|
+
if (!keys.length) { _fail('waypoint ' + i + ' has an EMPTY pose bag'); }
|
|
279
|
+
for (var k = 0; k < keys.length; k++) {
|
|
280
|
+
var key = keys[k];
|
|
281
|
+
if (declaredNames.length && !declared[key]) {
|
|
282
|
+
_fail('waypoint ' + i + ' names pose field "' + key + '", which this camera does ' +
|
|
283
|
+
'NOT declare. Declared axes: [' + declaredNames.join(', ') + ']. Read ' +
|
|
284
|
+
'cameraControl.poseFields rather than assuming a mode — a silently skipped ' +
|
|
285
|
+
'axis is a camera path that does not fly');
|
|
286
|
+
}
|
|
287
|
+
var val = wp.pose[key];
|
|
288
|
+
if (typeof val === 'number') { _finite(val, 'waypoint ' + i + ' ' + key); }
|
|
289
|
+
if (key === 'target' && val !== null && val !== undefined) {
|
|
290
|
+
if (typeof val.x !== 'number' || typeof val.y !== 'number') {
|
|
291
|
+
_fail('waypoint ' + i + ' target must be {x,y} (2D) or {x,y,z} (3D), or null');
|
|
292
|
+
}
|
|
293
|
+
// Validate EVERY numeric component the point carries (z included), so a
|
|
294
|
+
// non-finite z fails at attach rather than steering the camera to NaN.
|
|
295
|
+
var comps = Object.keys(val);
|
|
296
|
+
for (var c = 0; c < comps.length; c++) {
|
|
297
|
+
if (typeof val[comps[c]] === 'number') {
|
|
298
|
+
_finite(val[comps[c]], 'waypoint ' + i + ' target.' + comps[c]);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
// Reject the undefined null/concrete target mix AT ATTACH, where the diagnosis is
|
|
304
|
+
// cheap, rather than mid-flight where it is a grenade.
|
|
305
|
+
if (i > 0) {
|
|
306
|
+
var prev = sorted[i - 1].pose;
|
|
307
|
+
var pHas = ('target' in prev), cHas = ('target' in wp.pose);
|
|
308
|
+
if (pHas && cHas && ((prev.target === null) !== (wp.pose.target === null))) {
|
|
309
|
+
_fail('the segment at ticks ' + sorted[i - 1].atTick + '→' + wp.atTick +
|
|
310
|
+
' mixes target:null (the scene centre) with a concrete point — that ' +
|
|
311
|
+
'interpolation is undefined. Declare target on both waypoints or neither');
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
var loop = !!opts.loop;
|
|
317
|
+
// ── THE ANCHOR ──────────────────────────────────────────────────────────
|
|
318
|
+
// `atTick` is PATH-RELATIVE: tick 0 is "when this camera path starts", not universe tick 0.
|
|
319
|
+
// Absolute ticks were unusable in practice — attaching a drone to an already-running
|
|
320
|
+
// page (a probe, a Studio session, any corpus run that boots the scene first) meant
|
|
321
|
+
// every waypoint was ALREADY IN THE PAST, so `poseAt` returned the final pose and every
|
|
322
|
+
// waypoint fired at once against it. That is not a timing race; it is a coordinate-system
|
|
323
|
+
// bug, and it silently produced well-formed rows that all carried the last pose.
|
|
324
|
+
//
|
|
325
|
+
// `anchorTick` pins the origin explicitly for exact reproduction (a corpus row can record
|
|
326
|
+
// it); omitted, the anchor is the universe tick at `start()`.
|
|
327
|
+
var anchorTick = (typeof opts.anchorTick === 'number' && isFinite(opts.anchorTick))
|
|
328
|
+
? opts.anchorTick : null;
|
|
329
|
+
var anchored = null;
|
|
330
|
+
// Safety valve for an async waypoint handler that never settles (see _holdUntilSettled).
|
|
331
|
+
// 0 disables it — accept a permanent freeze rather than a non-waypoint-exact capture.
|
|
332
|
+
var waypointTimeoutMs = (typeof opts.waypointTimeoutMs === 'number')
|
|
333
|
+
? opts.waypointTimeoutMs : 10000;
|
|
334
|
+
// How many suspends THIS drone currently holds, so stop()/dispose() can release exactly
|
|
335
|
+
// its own and never another consumer's.
|
|
336
|
+
var heldCount = 0;
|
|
337
|
+
var listeners = [];
|
|
338
|
+
var unsubscribe = null;
|
|
339
|
+
var lastFiredIndex = -1;
|
|
340
|
+
|
|
341
|
+
var draining = false;
|
|
342
|
+
|
|
343
|
+
function _applyAt(tick) { _applyPose(poseAt(sorted, tick, loop, wrapByField)); }
|
|
344
|
+
|
|
345
|
+
function _applyPose(pose) {
|
|
346
|
+
cam.setOrbit(pose);
|
|
347
|
+
// A pose write on a PAUSED scene is invisible until something redraws — working code
|
|
348
|
+
// that reads as broken. repaint() re-renders the LAST TickData without advancing the
|
|
349
|
+
// tick clock (the clock is the loop's alone).
|
|
350
|
+
if (typeof runner.repaint === 'function') { runner.repaint(); }
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Fire the handlers for every waypoint newly reached, holding the TICK CLOCK until they
|
|
355
|
+
* settle.
|
|
356
|
+
*
|
|
357
|
+
* ── Why the clock is held (this REVERSES the D9b default, deliberately) ──
|
|
358
|
+
* D9b fired handlers and did not await them, on the rule "the tick clock is never held
|
|
359
|
+
* hostage by a consumer". That rule is right for a PASSIVE observer, and wrong here. At
|
|
360
|
+
* unbounded TPS the runner executes a whole batch of ticks inside one rAF frame, so an
|
|
361
|
+
* `await` inside a handler resumes only AFTER the batch — and a capture taken there
|
|
362
|
+
* records the pose and pixels of the LAST tick of the batch, not the waypoint's. The
|
|
363
|
+
* provenance stays truthful (the still stamps what the canvas actually showed), which is
|
|
364
|
+
* precisely what makes the failure so quiet: the row is well-formed and simply is not the
|
|
365
|
+
* waypoint's record. For corpus building that is worthless.
|
|
366
|
+
*
|
|
367
|
+
* A waypoint is not an observation, it is a SYNCHRONISATION POINT — "this instant,
|
|
368
|
+
* specifically" is its entire content. So the narrower correct rule: a waypoint handler
|
|
369
|
+
* MAY hold the clock; it must not be able to hold it forever. Hence the counted
|
|
370
|
+
* suspend/resume pair and the safety valve below.
|
|
371
|
+
*
|
|
372
|
+
* The tick SEQUENCE is unchanged (tick N is tick N), so physics determinism is untouched;
|
|
373
|
+
* only wall-clock pacing changes. And the drone still knows NOTHING about capture — it
|
|
374
|
+
* guarantees the world is frozen at the waypoint and lets the consumer do whatever it
|
|
375
|
+
* likes with that instant.
|
|
376
|
+
*/
|
|
377
|
+
function _notifyWaypoints(tick) {
|
|
378
|
+
for (var i = 0; i < sorted.length; i++) {
|
|
379
|
+
if (sorted[i].atTick <= tick && i > lastFiredIndex) {
|
|
380
|
+
lastFiredIndex = i;
|
|
381
|
+
var evt = { tick: sorted[i].atTick, index: i, pose: _copyPose(sorted[i].pose) };
|
|
382
|
+
var pending = [];
|
|
383
|
+
for (var L = 0; L < listeners.length; L++) {
|
|
384
|
+
// A throwing SYNCHRONOUS handler is surfaced and cannot abort its peers.
|
|
385
|
+
try {
|
|
386
|
+
var r = listeners[L](evt);
|
|
387
|
+
if (r && typeof r.then === 'function') { pending.push(r); }
|
|
388
|
+
} catch (e) { _surface('onWaypoint callback threw', e); }
|
|
389
|
+
}
|
|
390
|
+
if (pending.length) { _holdUntilSettled(pending, evt); }
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function _surface(what, e) {
|
|
396
|
+
if (typeof console !== 'undefined' && console.error) {
|
|
397
|
+
console.error('[CameraPath] ' + what + ':', e);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Hold the tick clock until every pending handler settles.
|
|
403
|
+
*
|
|
404
|
+
* The suspend is raised SYNCHRONOUSLY, inside the tick listener, so the runner's batch
|
|
405
|
+
* loop (which checks the depth per tick) breaks immediately — before another tick can
|
|
406
|
+
* move the world out from under an in-flight capture.
|
|
407
|
+
*
|
|
408
|
+
* The safety valve: a handler that never settles would freeze the sim, and a frozen canvas
|
|
409
|
+
* is ambiguous rather than loud. After `waypointTimeoutMs` the clock is released and the
|
|
410
|
+
* problem is reported. It cannot affect a healthy path (it never fires there), so it buys
|
|
411
|
+
* a loud failure without introducing wall-clock dependence into the deterministic route.
|
|
412
|
+
*/
|
|
413
|
+
function _holdUntilSettled(pending, evt, cont) {
|
|
414
|
+
if (typeof runner.suspendTicks !== 'function') {
|
|
415
|
+
// A runner predating the suspend contract: the capture cannot be made
|
|
416
|
+
// waypoint-exact, and pretending otherwise would fabricate corpus rows.
|
|
417
|
+
_fail('this runner exposes no suspendTicks()/resumeTicks(), so an async ' +
|
|
418
|
+
'onWaypoint handler cannot be guaranteed to observe waypoint ' + evt.index +
|
|
419
|
+
'. Upgrade the runner, or use only synchronous handlers');
|
|
420
|
+
}
|
|
421
|
+
runner.suspendTicks();
|
|
422
|
+
heldCount += 1;
|
|
423
|
+
var released = false;
|
|
424
|
+
var release = function () {
|
|
425
|
+
if (released) { return; }
|
|
426
|
+
released = true;
|
|
427
|
+
heldCount -= 1;
|
|
428
|
+
runner.resumeTicks();
|
|
429
|
+
if (typeof cont === 'function') { cont(); }
|
|
430
|
+
};
|
|
431
|
+
var timer = null;
|
|
432
|
+
if (waypointTimeoutMs > 0 && typeof setTimeout === 'function') {
|
|
433
|
+
timer = setTimeout(function () {
|
|
434
|
+
if (!released) {
|
|
435
|
+
_surface('an onWaypoint handler for waypoint ' + evt.index + ' did not settle ' +
|
|
436
|
+
'within ' + waypointTimeoutMs + 'ms — releasing the tick clock so the sim ' +
|
|
437
|
+
'does not stay frozen. That capture is NOT waypoint-exact',
|
|
438
|
+
new Error('waypoint handler timeout'));
|
|
439
|
+
release();
|
|
440
|
+
}
|
|
441
|
+
}, waypointTimeoutMs);
|
|
442
|
+
}
|
|
443
|
+
var settle = function (results) {
|
|
444
|
+
if (timer !== null && typeof clearTimeout === 'function') { clearTimeout(timer); }
|
|
445
|
+
if (results) {
|
|
446
|
+
for (var i = 0; i < results.length; i++) {
|
|
447
|
+
if (results[i] && results[i].status === 'rejected') {
|
|
448
|
+
_surface('onWaypoint callback rejected', results[i].reason);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
release();
|
|
453
|
+
};
|
|
454
|
+
// allSettled so ONE rejecting handler cannot strand the clock for its peers.
|
|
455
|
+
if (typeof Promise.allSettled === 'function') {
|
|
456
|
+
Promise.allSettled(pending).then(settle, function (e) { _surface('waypoint hold failed', e); release(); });
|
|
457
|
+
} else {
|
|
458
|
+
Promise.all(pending.map(function (p) { return p.then(null, function () {}); }))
|
|
459
|
+
.then(function () { settle(null); }, function () { release(); });
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function _universeTick(tickData) {
|
|
464
|
+
return (tickData && tickData.universeData && typeof tickData.universeData.tick === 'number')
|
|
465
|
+
? tickData.universeData.tick : null;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Drain every waypoint that has become due, ONE AT A TIME, each against ITS OWN pose.
|
|
470
|
+
*
|
|
471
|
+
* Two properties this buys, both load-bearing for corpus rows:
|
|
472
|
+
* - a waypoint handler always observes the pose of THAT waypoint, never a later one. If
|
|
473
|
+
* several waypoints come due in a single tick (a stalled frame, or a path denser than
|
|
474
|
+
* the tick rate), firing them all against the current interpolated pose would stamp
|
|
475
|
+
* them identically — the exact shape of the realm defect.
|
|
476
|
+
* - only ONE waypoint is ever in flight, so the clock hold is unambiguous.
|
|
477
|
+
*/
|
|
478
|
+
function _drainDue(pathTick) {
|
|
479
|
+
if (draining) { return; }
|
|
480
|
+
var next = lastFiredIndex + 1;
|
|
481
|
+
if (next >= sorted.length || sorted[next].atTick > pathTick) { return; }
|
|
482
|
+
draining = true;
|
|
483
|
+
_fireWaypoint(next, pathTick);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function _fireWaypoint(i, pathTick) {
|
|
487
|
+
lastFiredIndex = i;
|
|
488
|
+
// Pin the camera to THIS waypoint's own pose before anyone observes it.
|
|
489
|
+
_applyPose(poseAt(sorted, sorted[i].atTick, loop, wrapByField));
|
|
490
|
+
var evt = { tick: sorted[i].atTick, index: i, pose: _copyPose(sorted[i].pose) };
|
|
491
|
+
var pending = [];
|
|
492
|
+
for (var L = 0; L < listeners.length; L++) {
|
|
493
|
+
try {
|
|
494
|
+
var r = listeners[L](evt);
|
|
495
|
+
if (r && typeof r.then === 'function') { pending.push(r); }
|
|
496
|
+
} catch (e) { _surface('onWaypoint callback threw', e); }
|
|
497
|
+
}
|
|
498
|
+
var cont = function () {
|
|
499
|
+
var nxt = lastFiredIndex + 1;
|
|
500
|
+
if (nxt < sorted.length && sorted[nxt].atTick <= pathTick) { _fireWaypoint(nxt, pathTick); }
|
|
501
|
+
else { draining = false; _applyAt(pathTick); } // back to the interpolated pose
|
|
502
|
+
};
|
|
503
|
+
if (pending.length) { _holdUntilSettled(pending, evt, cont); }
|
|
504
|
+
else { cont(); }
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function _surface(what, e) {
|
|
508
|
+
if (typeof console !== 'undefined' && console.error) {
|
|
509
|
+
console.error('[CameraPath] ' + what + ':', e);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function onTick(tickData) {
|
|
514
|
+
var ut = _universeTick(tickData);
|
|
515
|
+
if (ut === null) { return; }
|
|
516
|
+
if (anchored === null) { anchored = (anchorTick !== null) ? anchorTick : ut; }
|
|
517
|
+
var pathTick = ut - anchored;
|
|
518
|
+
if (!draining) { _applyAt(pathTick); }
|
|
519
|
+
_drainDue(pathTick);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
var handle = {
|
|
523
|
+
get running() { return !!unsubscribe; },
|
|
524
|
+
/** Bind to the per-tick fan-out and apply the pose for the CURRENT tick immediately. */
|
|
525
|
+
start: function () {
|
|
526
|
+
if (unsubscribe) { return; }
|
|
527
|
+
// Anchor FIRST, from the live universe tick, so path tick 0 is "now" even on a
|
|
528
|
+
// page that has already been running for a thousand ticks.
|
|
529
|
+
var td = runner._lastTickData;
|
|
530
|
+
var ut = _universeTick(td);
|
|
531
|
+
if (anchored === null) { anchored = (anchorTick !== null) ? anchorTick : (ut === null ? 0 : ut); }
|
|
532
|
+
unsubscribe = runner.addTickListener(onTick);
|
|
533
|
+
// A paused scene frames itself at once rather than at the next tick.
|
|
534
|
+
if (ut !== null) { onTick(td); }
|
|
535
|
+
},
|
|
536
|
+
/** The universe tick path-tick 0 maps to (null before start()). Record it to reproduce. */
|
|
537
|
+
get anchorTick() { return anchored; },
|
|
538
|
+
stop: function () {
|
|
539
|
+
if (unsubscribe) { unsubscribe(); unsubscribe = null; }
|
|
540
|
+
draining = false;
|
|
541
|
+
lastFiredIndex = -1;
|
|
542
|
+
anchored = null; // a restart re-anchors to the new now
|
|
543
|
+
// Never leave the clock held by a drone that is no longer flying — a stopped
|
|
544
|
+
// drone that froze the sim would be the worst kind of leak (silent and total).
|
|
545
|
+
while (heldCount > 0) { heldCount -= 1; if (typeof runner.resumeTicks === 'function') { runner.resumeTicks(); } }
|
|
546
|
+
},
|
|
547
|
+
dispose: function () { handle.stop(); listeners = []; },
|
|
548
|
+
/** The PURE path evaluation, bound to this drone's own waypoints. */
|
|
549
|
+
poseAt: function (tick) { return poseAt(sorted, tick, loop, wrapByField); },
|
|
550
|
+
/**
|
|
551
|
+
* onWaypoint(cb) — cb({tick, index, pose}) when a waypoint is reached.
|
|
552
|
+
* The natural move is `async () => rows.push(await vision.getStill())`.
|
|
553
|
+
* @returns {Function} unsubscribe
|
|
554
|
+
*/
|
|
555
|
+
onWaypoint: function (cb) {
|
|
556
|
+
if (typeof cb !== 'function') { _fail('onWaypoint needs a function'); }
|
|
557
|
+
listeners.push(cb);
|
|
558
|
+
return function () {
|
|
559
|
+
var i = listeners.indexOf(cb);
|
|
560
|
+
if (i !== -1) { listeners.splice(i, 1); }
|
|
561
|
+
};
|
|
562
|
+
},
|
|
563
|
+
/** The validated, sorted path (a copy — the drone's own list is never handed out). */
|
|
564
|
+
get waypoints() { return sorted.map(function (w) {
|
|
565
|
+
return { atTick: w.atTick, pose: _copyPose(w.pose) }; }); }
|
|
566
|
+
};
|
|
567
|
+
return handle;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
// ── Export ────────────────────────────────────────────────────────────────────
|
|
571
|
+
|
|
572
|
+
var CameraPath = { attach: attach, poseAt: poseAt };
|
|
573
|
+
|
|
574
|
+
export { CameraPath };
|
|
575
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/* Copyright 2023-2025 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CollisionActions — Canonical collision action enum for EGPT FRQTL VM.
|
|
5
|
+
* Values are SCREAMING_SNAKE (the JS engine's native vocabulary).
|
|
6
|
+
*
|
|
7
|
+
* Canonical set — exactly what the JS reference engine's lo.collide() action dispatch has.
|
|
8
|
+
* ACTIVE_ACTIONS = [ABSORB, COUNT, MARK, DETECT, LOG].
|
|
9
|
+
* DO NOT add actions not present in the JS reference collide() without first
|
|
10
|
+
* adding them there and updating the conformance suite (§11.3).
|
|
11
|
+
*
|
|
12
|
+
* Wire-format translation (SNAKE→Pascal for WASM) is handled by the
|
|
13
|
+
* wasm-engine provider. Objects emit raw SCREAMING_SNAKE; providers translate.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
const CollisionActions = {
|
|
18
|
+
COUNT: 'COUNT',
|
|
19
|
+
MARK: 'MARK',
|
|
20
|
+
DETECT: 'DETECT',
|
|
21
|
+
LOG: 'LOG',
|
|
22
|
+
ABSORB: 'ABSORB',
|
|
23
|
+
BOUNCE: 'BOUNCE',
|
|
24
|
+
BLOCK: 'BLOCK',
|
|
25
|
+
// RECOLOR deleted: I1 — the engine has no concept of color. Render color lives in
|
|
26
|
+
// data.render.color (sugar stamps it; engine carries opaque; renderer reads it).
|
|
27
|
+
// The feature returns later via the behavior model (P3 write-data-bubble primitive).
|
|
28
|
+
// KILL/CONTAIN/CONTAIN_X/CONTAIN_Y/FORCE_RENDER were Rust-only divergences — deleted (§3.2).
|
|
29
|
+
// Add new actions to the JS reference collide() FIRST, then here + conformance.
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Export
|
|
33
|
+
|
|
34
|
+
export { CollisionActions };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* Copyright 2023-2025 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CollisionTypes — Canonical collision type enum for EGPT FRQTL VM.
|
|
5
|
+
* Values are SCREAMING_SNAKE (the JS engine's native vocabulary).
|
|
6
|
+
*
|
|
7
|
+
* Canonical set — exactly what the JS reference engine's lo.collide() type switch has.
|
|
8
|
+
* DO NOT add types not present in the JS reference collide() without first
|
|
9
|
+
* adding them there and updating the conformance suite (§11.3).
|
|
10
|
+
*
|
|
11
|
+
* Wire-format translation (SNAKE→Pascal for WASM) is handled by the
|
|
12
|
+
* wasm-engine provider. Objects emit raw SCREAMING_SNAKE; providers translate.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
const CollisionTypes = {
|
|
17
|
+
NONE: 'NONE',
|
|
18
|
+
BOUNCE: 'BOUNCE',
|
|
19
|
+
RANDOM_X: 'RANDOM_X',
|
|
20
|
+
RANDOM_Y: 'RANDOM_Y',
|
|
21
|
+
RANDOM_XY: 'RANDOM_XY',
|
|
22
|
+
MOMENTUM_BASED: 'MOMENTUM_BASED',
|
|
23
|
+
WALL: 'WALL'
|
|
24
|
+
// BounceUp/Down/Left/Right were Rust-only divergences — deleted (§3.1).
|
|
25
|
+
// Add new types to the JS reference collide() FIRST, then here + conformance.
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Export
|
|
29
|
+
|
|
30
|
+
export { CollisionTypes };
|