@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,258 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* captureStill-common.js — the SHARED pixel-readback + bag-assembly owner for every
|
|
5
|
+
* `RendererProvider.captureStill()` implementation (the `composite`-stage tap executor).
|
|
6
|
+
*
|
|
7
|
+
* Peer of `renderCapabilities-common.js` in this directory, and it exists for the same
|
|
8
|
+
* reason: without it, `p5-provider.js` and `three-provider.js` would each carry their own
|
|
9
|
+
* row-order flip, their own bag-shape assembly, and their own "is this buffer actually
|
|
10
|
+
* non-blank?" check — three chances for the two backends to disagree about what a still
|
|
11
|
+
* IS. A capture that silently differs per backend is exactly the corrupted-corpus failure
|
|
12
|
+
* the provenance contract exists to prevent, so the parts that are NOT backend-specific
|
|
13
|
+
* live here once, and each provider contributes only its own irreducible readback call.
|
|
14
|
+
*
|
|
15
|
+
* ── Layer 1: API contract ────────────────────────────────────────────────────
|
|
16
|
+
*
|
|
17
|
+
* Three pure, module-level free functions — no live WebGL/THREE/p5 instance is captured;
|
|
18
|
+
* every backend handle arrives as an ARGUMENT, which is what keeps the readback logic
|
|
19
|
+
* unit-testable in plain Node (a `Uint8Array` and a `{ readPixels }` object are enough).
|
|
20
|
+
*
|
|
21
|
+
* `flipRowsToTopLeft(src, width, height)` → `Uint8ClampedArray`
|
|
22
|
+
* Reverse the ROW order of an RGBA8 buffer. WebGL's `readPixels` origin is BOTTOM-LEFT;
|
|
23
|
+
* the contract's origin is TOP-LEFT (§1a.4), so a GL-based provider flips and a 2D-canvas
|
|
24
|
+
* provider (already top-left) does not. This is the one place the flip is written: an
|
|
25
|
+
* inverted still is invisible to every non-visual check and reads as "the physics is
|
|
26
|
+
* upside down", which is the worst kind of wrong answer — confidently mirrored data.
|
|
27
|
+
*
|
|
28
|
+
* `captureFrom2dCanvas(canvas, meta)` → the provider bag
|
|
29
|
+
* The 2D-canvas path (p5). `getImageData` is already RGBA, row-major, top-left, so no
|
|
30
|
+
* flip. Reads the canvas's BACKING-STORE size (`canvas.width/height`) — never its CSS
|
|
31
|
+
* size — because that is what `uResolution` means (§4).
|
|
32
|
+
*
|
|
33
|
+
* `captureFromGl(gl, width, height, meta)` → the provider bag
|
|
34
|
+
* The WebGL path (three). `readPixels` into a fresh `Uint8Array`, then ONE
|
|
35
|
+
* `flipRowsToTopLeft`. The CALLER must have re-rendered its retained scene in the SAME
|
|
36
|
+
* JS task immediately before calling this (see the readback guarantee below).
|
|
37
|
+
*
|
|
38
|
+
* `meta` (both capture fns) carries the fields only the PROVIDER is the authority for:
|
|
39
|
+
* `{ cssWidth, cssHeight, physicsW, physicsH, filtersApplied, renderBaseCanvas,
|
|
40
|
+
* filterTime, composited }`.
|
|
41
|
+
* `filtersApplied` MUST come from the provider's own ORDERED `_filters` list — never
|
|
42
|
+
* from the runner's unordered `_activeFilters` map, which is also blind to a direct BYO
|
|
43
|
+
* `prov.filter()` call and would therefore lie for exactly the BYO case.
|
|
44
|
+
* `filterTime` is the `uTime` the provider fed its filter passes (the contract's
|
|
45
|
+
* `filterTime` of the rendered tick): a number when `filtersApplied` is non-empty, null
|
|
46
|
+
* when it is empty.
|
|
47
|
+
* `composited` is the TickData the provider last drew (its own retained `_lastTickData`);
|
|
48
|
+
* its `universeData.tick` becomes the bag's `renderedTick` — the provider's REPORT of which
|
|
49
|
+
* tick these pixels are, which the tap mechanism compares against its stamp.
|
|
50
|
+
*
|
|
51
|
+
* The returned bag is the §1a.4 provider shape:
|
|
52
|
+
* `{ pixels, width, height, cssWidth, cssHeight, physicsW, physicsH,
|
|
53
|
+
* filtersApplied, renderBaseCanvas, filterTime, renderedTick }`
|
|
54
|
+
* The RUNNER then normalizes it into the generic tap bag (`lib/shim/render/render-stages`
|
|
55
|
+
* `normalizeStill`, where `pixels` becomes the stage-agnostic `data` payload and the
|
|
56
|
+
* dispatcher adds `stage`/`tick`/`pose`/`composedOf`/`renderOptions`). Providers never
|
|
57
|
+
* import the shim — a BYO renderer's whole capture burden stays ONE method.
|
|
58
|
+
*
|
|
59
|
+
* ── The readback-correctness guarantee (the hard sentence) ────────────────────
|
|
60
|
+
*
|
|
61
|
+
* A returned bag is the FULL composited canvas — all three layers (§8) plus every active
|
|
62
|
+
* filter pass (§4) — as of the last completed `renderTick`/`repaint`. The WebGL trap:
|
|
63
|
+
* `three-provider.js` constructs its `WebGLRenderer` WITHOUT `preserveDrawingBuffer`, so a
|
|
64
|
+
* `readPixels` in a LATER task reads a CLEARED buffer and would return an all-black still.
|
|
65
|
+
* The compliant GL path therefore re-renders its RETAINED scene (the scene graph and the
|
|
66
|
+
* filter passes both persist between ticks) and reads back IN THE SAME JS TASK. Flipping
|
|
67
|
+
* `preserveDrawingBuffer:true` globally is the WRONG fix: it taxes every frame to subsidise
|
|
68
|
+
* occasional captures and leaves the guarantee implicit rather than enforced.
|
|
69
|
+
* `assertNonBlank` below is the belt-and-braces check that this actually happened — a
|
|
70
|
+
* uniformly-zero buffer THROWS rather than being handed back as evidence.
|
|
71
|
+
*
|
|
72
|
+
* ── FAIL-LOUD, never a null image ────────────────────────────────────────────
|
|
73
|
+
*
|
|
74
|
+
* Every function here throws on a bad input (absent canvas, no 2D context, no GL context,
|
|
75
|
+
* a length mismatch, a uniformly-blank readback). A capability that is ABSENT hides an
|
|
76
|
+
* affordance (the `move()` precedent — a host capability-checks and shows no button); a
|
|
77
|
+
* capability that is PRESENT but cannot honor its guarantee THROWS. A black still from a
|
|
78
|
+
* cleared framebuffer is a lie in the product surface, the same violation class as a
|
|
79
|
+
* hardcoded backend label (dep-map rule 4).
|
|
80
|
+
*
|
|
81
|
+
* ── Module format: an ES module, like every peer under lib/frqtl/compat/renderer/ ──
|
|
82
|
+
*
|
|
83
|
+
* ── Layer 2: physics behaviour ───────────────────────────────────────────────
|
|
84
|
+
* ZERO physics. This is the output boundary reading its own output: nothing here touches a
|
|
85
|
+
* frame, a dimension or a tick, so no canonical-bench golden can move because of it.
|
|
86
|
+
*/
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
function _fail(msg) {
|
|
91
|
+
throw new Error('[captureStill] ' + msg + ' (FAIL-LOUD — never a null or blank still.)');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Reverse the ROW order of an RGBA8 buffer (bottom-left origin → top-left origin).
|
|
96
|
+
*
|
|
97
|
+
* @param {Uint8Array|Uint8ClampedArray|Array<number>} src — RGBA8, `width*height*4` long
|
|
98
|
+
* @param {number} width — pixels per row
|
|
99
|
+
* @param {number} height — number of rows
|
|
100
|
+
* @returns {Uint8ClampedArray} a NEW buffer, rows reversed, channels untouched
|
|
101
|
+
* @throws {Error} on a length that is not exactly width*height*4
|
|
102
|
+
*/
|
|
103
|
+
function flipRowsToTopLeft(src, width, height) {
|
|
104
|
+
if (!src || typeof src.length !== 'number') { _fail('flipRowsToTopLeft: src is not an indexed buffer'); }
|
|
105
|
+
var stride = width * 4;
|
|
106
|
+
if (src.length !== stride * height) {
|
|
107
|
+
_fail('flipRowsToTopLeft: buffer length ' + src.length + ' != width*height*4 (' +
|
|
108
|
+
width + '*' + height + '*4 = ' + (stride * height) + ')');
|
|
109
|
+
}
|
|
110
|
+
var out = new Uint8ClampedArray(src.length);
|
|
111
|
+
for (var row = 0; row < height; row++) {
|
|
112
|
+
var from = (height - 1 - row) * stride;
|
|
113
|
+
for (var i = 0; i < stride; i++) { out[row * stride + i] = src[from + i]; }
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Throw if a readback is uniformly one value across every byte.
|
|
120
|
+
*
|
|
121
|
+
* A composited scene with a background and at least one quantum is never uniform, so a
|
|
122
|
+
* uniform buffer means the readback happened against a cleared/unbound framebuffer — the
|
|
123
|
+
* black-still failure mode. This is checked HERE (not in the test) so a broken realm path
|
|
124
|
+
* reports itself instead of quietly feeding a training corpus.
|
|
125
|
+
*
|
|
126
|
+
* @param {Uint8Array|Uint8ClampedArray} buf
|
|
127
|
+
* @param {string} where — the provider/path name for the message
|
|
128
|
+
*/
|
|
129
|
+
function assertNonBlank(buf, where) {
|
|
130
|
+
if (!buf.length) { _fail(where + ': readback returned an EMPTY buffer'); }
|
|
131
|
+
var first = buf[0];
|
|
132
|
+
for (var i = 1; i < buf.length; i++) {
|
|
133
|
+
if (buf[i] !== first) { return; }
|
|
134
|
+
}
|
|
135
|
+
_fail(where + ': readback is UNIFORMLY ' + first + ' across every byte — the buffer was ' +
|
|
136
|
+
'cleared or unbound. A GL provider must re-render its retained scene and read back ' +
|
|
137
|
+
'in the SAME JS task (no preserveDrawingBuffer)');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Validate + copy the provider-authored provenance half of the bag. */
|
|
141
|
+
function _meta(meta, width, height) {
|
|
142
|
+
meta = meta || {};
|
|
143
|
+
if (!Array.isArray(meta.filtersApplied)) {
|
|
144
|
+
_fail('meta.filtersApplied must be the provider\'s ORDERED filter-name array (its own ' +
|
|
145
|
+
'_filters list is the one authority — never the runner\'s unordered _activeFilters map)');
|
|
146
|
+
}
|
|
147
|
+
// physicsW/H is the UNIVERSE-MAPPING half of provenance: it is what lets a consumer turn a
|
|
148
|
+
// pixel back into a physics coordinate. Defaulting it to the buffer dims INVENTED a
|
|
149
|
+
// universe size — silently correct only when the canvas happens to be 1:1 with the
|
|
150
|
+
// universe, and a mislabeled corpus row otherwise. There is no honest fallback, so this
|
|
151
|
+
// throws.
|
|
152
|
+
if (!(typeof meta.physicsW === 'number' && meta.physicsW > 0) ||
|
|
153
|
+
!(typeof meta.physicsH === 'number' && meta.physicsH > 0)) {
|
|
154
|
+
_fail('meta.physicsW/physicsH must be the POSITIVE physics-canvas dimensions this ' +
|
|
155
|
+
'buffer maps from — they are the universe-mapping half of provenance and cannot ' +
|
|
156
|
+
'be guessed from the buffer size (that is only right at 1:1). Got ' +
|
|
157
|
+
String(meta.physicsW) + 'x' + String(meta.physicsH));
|
|
158
|
+
}
|
|
159
|
+
// renderedTick is the TICK half of provenance, reported by the one party that knows which
|
|
160
|
+
// TickData these pixels were drawn from. The tap stamps `tick` off the LOOP, and the loop
|
|
161
|
+
// may have ticked past the last draw (a batched rAF frame renders only its last tick), so
|
|
162
|
+
// without this report nothing could tell a still labelled t from pixels of t+k.
|
|
163
|
+
var composited = meta.composited;
|
|
164
|
+
var renderedTick = composited && composited.universeData && composited.universeData.tick;
|
|
165
|
+
if (!(typeof renderedTick === 'number' && isFinite(renderedTick))) {
|
|
166
|
+
_fail('meta.composited must be the TickData this provider last drew (its retained ' +
|
|
167
|
+
'_lastTickData, carrying universeData.tick) — nothing has been composited, so ' +
|
|
168
|
+
'there is no tick these pixels could be labelled with');
|
|
169
|
+
}
|
|
170
|
+
// filterTime is the CLOCK half of a filtered still's provenance: a filter that animates on
|
|
171
|
+
// `uTime` draws different pixels at a different time, so a filtered still without the time
|
|
172
|
+
// it drew with is not regenerable. A filter ran ⇔ a time was fed.
|
|
173
|
+
var filterTime = (meta.filterTime === undefined) ? null : meta.filterTime;
|
|
174
|
+
var filtered = meta.filtersApplied.length > 0;
|
|
175
|
+
if (filtered ? !(typeof filterTime === 'number' && isFinite(filterTime)) : filterTime !== null) {
|
|
176
|
+
_fail('meta.filterTime must be the finite uTime the filter passes drew with when ' +
|
|
177
|
+
'filtersApplied is non-empty, and null when it is empty (filtersApplied ' +
|
|
178
|
+
JSON.stringify(meta.filtersApplied) + ', filterTime ' + String(filterTime) + ')');
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
renderedTick: renderedTick,
|
|
182
|
+
width: width,
|
|
183
|
+
height: height,
|
|
184
|
+
// cssWidth/Height DO default to the buffer dims: that is the IDENTITY case (a canvas
|
|
185
|
+
// with no CSS scaling), not an invented value — and a headless capture genuinely has
|
|
186
|
+
// no CSS size to report.
|
|
187
|
+
cssWidth: (typeof meta.cssWidth === 'number' && meta.cssWidth > 0) ? meta.cssWidth : width,
|
|
188
|
+
cssHeight: (typeof meta.cssHeight === 'number' && meta.cssHeight > 0) ? meta.cssHeight : height,
|
|
189
|
+
physicsW: meta.physicsW,
|
|
190
|
+
physicsH: meta.physicsH,
|
|
191
|
+
filtersApplied: meta.filtersApplied.slice(),
|
|
192
|
+
renderBaseCanvas: (typeof meta.renderBaseCanvas === 'boolean') ? meta.renderBaseCanvas : null,
|
|
193
|
+
filterTime: filterTime
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Capture from a 2D canvas (the p5 path). `getImageData` is already RGBA, row-major,
|
|
199
|
+
* TOP-LEFT — no flip.
|
|
200
|
+
*
|
|
201
|
+
* @param {HTMLCanvasElement} canvas
|
|
202
|
+
* @param {object} meta — see the module docstring
|
|
203
|
+
* @returns {object} the §1a.4 provider bag
|
|
204
|
+
*/
|
|
205
|
+
function captureFrom2dCanvas(canvas, meta) {
|
|
206
|
+
if (!canvas) {
|
|
207
|
+
_fail('no canvas to read — captureStill() was called before init() created one');
|
|
208
|
+
}
|
|
209
|
+
var ctx = (typeof canvas.getContext === 'function') ? canvas.getContext('2d') : null;
|
|
210
|
+
if (!ctx || typeof ctx.getImageData !== 'function') {
|
|
211
|
+
_fail('the canvas has no readable 2D context (getImageData unavailable)');
|
|
212
|
+
}
|
|
213
|
+
var w = canvas.width, h = canvas.height;
|
|
214
|
+
if (!(w > 0 && h > 0)) { _fail('canvas backing store is ' + w + 'x' + h); }
|
|
215
|
+
var img = ctx.getImageData(0, 0, w, h);
|
|
216
|
+
if (!img || !img.data || img.data.length !== w * h * 4) {
|
|
217
|
+
_fail('getImageData returned ' + (img && img.data ? img.data.length : 'nothing') +
|
|
218
|
+
' bytes, expected ' + (w * h * 4));
|
|
219
|
+
}
|
|
220
|
+
assertNonBlank(img.data, '2d-canvas');
|
|
221
|
+
var bag = _meta(meta, w, h);
|
|
222
|
+
// Copy: the ImageData buffer is owned by the context and may be reused.
|
|
223
|
+
bag.pixels = new Uint8ClampedArray(img.data);
|
|
224
|
+
return bag;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Capture from a WebGL context (the three path). The caller MUST have re-rendered its
|
|
229
|
+
* retained scene in this SAME JS task immediately before calling this.
|
|
230
|
+
*
|
|
231
|
+
* @param {WebGLRenderingContext|WebGL2RenderingContext} gl
|
|
232
|
+
* @param {number} width — framebuffer width in backing-store pixels
|
|
233
|
+
* @param {number} height — framebuffer height in backing-store pixels
|
|
234
|
+
* @param {object} meta — see the module docstring
|
|
235
|
+
* @returns {object} the §1a.4 provider bag (rows already flipped to top-left)
|
|
236
|
+
*/
|
|
237
|
+
function captureFromGl(gl, width, height, meta) {
|
|
238
|
+
if (!gl || typeof gl.readPixels !== 'function') {
|
|
239
|
+
_fail('no WebGL context to read — captureStill() was called before init(), or the ' +
|
|
240
|
+
'context was lost');
|
|
241
|
+
}
|
|
242
|
+
if (!(width > 0 && height > 0)) { _fail('GL framebuffer is ' + width + 'x' + height); }
|
|
243
|
+
var raw = new Uint8Array(width * height * 4);
|
|
244
|
+
gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, raw);
|
|
245
|
+
assertNonBlank(raw, 'webgl');
|
|
246
|
+
var bag = _meta(meta, width, height);
|
|
247
|
+
bag.pixels = flipRowsToTopLeft(raw, width, height);
|
|
248
|
+
return bag;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Export ────────────────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
export {
|
|
254
|
+
flipRowsToTopLeft,
|
|
255
|
+
assertNonBlank,
|
|
256
|
+
captureFrom2dCanvas,
|
|
257
|
+
captureFromGl
|
|
258
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* display-slot.js — THE DISPLAY SLOT: the ONE answer to "which provider owns the display".
|
|
5
|
+
*
|
|
6
|
+
* ── The rule (RENDERER_CONTRACT §9 + §1a.5) ──────────────────────────────────
|
|
7
|
+
*
|
|
8
|
+
* The DISPLAY provider is boot-fixed; OBSERVERS are live.
|
|
9
|
+
*
|
|
10
|
+
* Exactly one `RendererProvider` — the display provider — owns the canvas, the camera and
|
|
11
|
+
* `projection`; swapping it is a reload. N observers (non-interactive consumers of the frame
|
|
12
|
+
* stream) are addable and removable at any time; an observer owns no canvas, no camera and no
|
|
13
|
+
* projection. This module holds the DISPLAY half of that rule and nothing else: there is no
|
|
14
|
+
* observer registry here, and adding one before its first real caller is the speculative
|
|
15
|
+
* registry §10 bans.
|
|
16
|
+
*
|
|
17
|
+
* ── What this slot is, and what it is NOT ───────────────────────────────────
|
|
18
|
+
*
|
|
19
|
+
* It is KNOWLEDGE, not ownership. The provider still creates and owns its own canvas in its
|
|
20
|
+
* own `init()` — the frozen §1 contract is untouched, and the literal canvas-element handoff
|
|
21
|
+
* was VETOED (one canvas owns one rendering context; p5 cannot adopt a foreign element). The
|
|
22
|
+
* slot records WHICH provider that is, so every consumer asks one place instead of reading a
|
|
23
|
+
* bare global that anything could assign.
|
|
24
|
+
*
|
|
25
|
+
* ── Why it lives in the RENDERER layer and not on the capture instrument ────
|
|
26
|
+
*
|
|
27
|
+
* The boot order forces it. The boot module (`experiments/_shared/boot.js`) sets it while the
|
|
28
|
+
* renderer boots, BEFORE the game layer constructs anything, and the three.js path registers
|
|
29
|
+
* ASYNC — later still. A slot that lived in a game-layer file would not exist when its one
|
|
30
|
+
* writer needs it. The RENDERER layer is also where
|
|
31
|
+
* `CANONICAL_STACK_DIAGRAM` already puts the renderer pluggable surface, so this is the
|
|
32
|
+
* authority table's own home rather than a new one. The capture instrument READS the slot
|
|
33
|
+
* (through its `providerGetter` binding); it does not own it — an instrument is per-runner
|
|
34
|
+
* and the display is per-process.
|
|
35
|
+
*
|
|
36
|
+
* ── The name chain (truth in labeling) ──────────────────────────────────────
|
|
37
|
+
*
|
|
38
|
+
* The boot authority's renderer NAME (`bootRealm({ renderer })` / `?renderer=`) heads ONE chain:
|
|
39
|
+
* boot authority → `build.renderer` provenance
|
|
40
|
+
* boot authority → this slot's `name()`
|
|
41
|
+
* The slot FERRIES the boot authority's name; it never re-derives one. A slot that could
|
|
42
|
+
* answer differently from `build.renderer` is the truth-in-labeling failure (dep-map rule 4),
|
|
43
|
+
* so `display-slot.test.mjs` gates `name()` against the booted renderer after a real boot.
|
|
44
|
+
*
|
|
45
|
+
* ── Boot-fixed is a SEMANTIC, not a write-once lock ─────────────────────────
|
|
46
|
+
*
|
|
47
|
+
* `set()` deliberately does NOT throw on a second call. The notebook realm switches a cell
|
|
48
|
+
* between p5 and three by re-invoking the boot module's `selectRenderer(name)` — that IS the
|
|
49
|
+
* reload the rule describes (a new renderer session, a new scene identity), not a hot swap of
|
|
50
|
+
* a live scene. Locking the slot would break the realm while proving nothing: what makes the
|
|
51
|
+
* display boot-fixed is that `projection` and `build.renderer` are SCENE-record fields, so a
|
|
52
|
+
* re-selection mints a new scene rather than silently re-parenting stills.
|
|
53
|
+
*/
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
var _provider = null; // the display RendererProvider instance, or null
|
|
57
|
+
var _name = null; // the boot authority's name it was registered under
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Register the booted display provider. Called by whichever boot ran the selection
|
|
61
|
+
* (the boot module's `selectRenderer`, a test harness declaring a renderer the way a
|
|
62
|
+
* page does).
|
|
63
|
+
*
|
|
64
|
+
* @param {object} provider — the `RendererProvider` instance. The slot validates only
|
|
65
|
+
* that there IS one: provider SHAPE has an authority already —
|
|
66
|
+
* `renderer-provider-contract.js`'s `validateRendererProvider` on the BYO path — and a second
|
|
67
|
+
* shape check here would be a second authority that could disagree with it (and would
|
|
68
|
+
* silently tighten what the shipped providers are allowed to be).
|
|
69
|
+
* @param {string} [name] — the boot authority's renderer name,
|
|
70
|
+
* FERRIED. Never re-derived here.
|
|
71
|
+
* @returns {object} the provider, so a caller can register and assign in one expression.
|
|
72
|
+
*/
|
|
73
|
+
function set(provider, name) {
|
|
74
|
+
if (!provider || typeof provider !== 'object') {
|
|
75
|
+
throw new Error('[FrqtlDisplay.set] the display provider must be a ' +
|
|
76
|
+
'RendererProvider instance. Got: ' + String(provider) + '. (FAIL-LOUD — never ' +
|
|
77
|
+
'a display slot holding nothing while a page believes it has a renderer.)');
|
|
78
|
+
}
|
|
79
|
+
_provider = provider;
|
|
80
|
+
_name = (name === undefined || name === null) ? null : String(name);
|
|
81
|
+
return provider;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** @returns {object|null} the display provider, or null before boot completes. */
|
|
85
|
+
function get() { return _provider; }
|
|
86
|
+
|
|
87
|
+
/** @returns {string|null} the boot authority's name this provider was registered under. */
|
|
88
|
+
function name() { return _name; }
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Empty the slot. For teardown ONLY (a test restoring its host, a realm disposing a
|
|
92
|
+
* session) — never a way to "unset" a display mid-scene.
|
|
93
|
+
*/
|
|
94
|
+
function clear() { _provider = null; _name = null; }
|
|
95
|
+
|
|
96
|
+
export { set, get, name, clear };
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* lo-drawn-center.js — THE authority for where a LargeObject rect is DRAWN: its center point.
|
|
5
|
+
*
|
|
6
|
+
* Peer of `orbit-bag.js`: a shared contract-half with no geometry of its own beyond one rule, so
|
|
7
|
+
* the fact has ONE owner. Its consumers are the three.js provider (it places each LO mesh at this
|
|
8
|
+
* point and paints the shadow-surround from its volume) and `FrqtlApp` (the 3D mouse pick aims
|
|
9
|
+
* at the centre; the aim ray tests the volume). Two
|
|
10
|
+
* derivations of the one fact is the drift this file exists to make impossible — a pick aimed at
|
|
11
|
+
* a different point than the mesh is drawn at selects the wrong object silently.
|
|
12
|
+
*
|
|
13
|
+
* ── The rule ─────────────────────────────────────────────────────────────────
|
|
14
|
+
*
|
|
15
|
+
* An LO rect is corner-mode (`left`/`top` are the near corner, never a center — the LargeObject
|
|
16
|
+
* convention). A 6-element rect (`{left, top, z, w, h, d}`) also carries `z`, the NEAR-face corner
|
|
17
|
+
* on the z axis, so its z-center is `z + d/2`, with `d` defaulting to `w` exactly as the engine's
|
|
18
|
+
* z-capable rectangle does. A legacy 4-element rect has no `z`: it sits at z-center 0.
|
|
19
|
+
*
|
|
20
|
+
* drawnCenter(rect) → [left + w/2, top + h/2, (z present) ? z + (d ?? w)/2 : 0]
|
|
21
|
+
* hasZ(rect) → whether the rect carries z — the ONE z-presence test (consumers never re-derive it)
|
|
22
|
+
*
|
|
23
|
+
* Its INVERSE places the same-sized rect at a centre (a grabbed wall carried by the camera):
|
|
24
|
+
*
|
|
25
|
+
* rectAtCenter(rect, [cx, cy, cz]) → the WRITE-wire array, the rect's own form:
|
|
26
|
+
* z present → [cx − w/2, cy − h/2, cz − D/2, w, h, D] with D = d ?? w STATED (so the written
|
|
27
|
+
* rect reads back through drawnCenter to the centre it was placed at);
|
|
28
|
+
* z absent → [cx − w/2, cy − h/2, w, h], and cz must be 0 — a z-less rect sits at z-centre
|
|
29
|
+
* 0 by the rule above, so any other cz THROWS rather than being dropped.
|
|
30
|
+
*
|
|
31
|
+
* ── The drawn VOLUME and the center-ray hit (STUDIO's aim) ──────────────────────
|
|
32
|
+
*
|
|
33
|
+
* The same rect also owns the VOLUME the aim ray tests and the shadow-surround paints, so the
|
|
34
|
+
* thing you aim at, the thing that lights up and the mesh are one geometry:
|
|
35
|
+
*
|
|
36
|
+
* drawnVolume({rect, orientationDeg?, pitchDeg?}) → a BOX: centre = drawnCenter(rect), half
|
|
37
|
+
* extents [w/2, h/2, D/2] with D = the rect's z-extent (d ?? w when z is present — the same
|
|
38
|
+
* default the centre uses — else 0, a flat legacy rect), turned by the shell's own rotation
|
|
39
|
+
* (yaw about world z, then pitch about the plate's own long axis: Rz(yaw)·Ry(pitch), the
|
|
40
|
+
* renderer's 'ZYX' order).
|
|
41
|
+
* drawnVolume({center, radius}) → a SPHERE (a centre-only target, e.g. an emitter).
|
|
42
|
+
* rayIntersect(origin, dir, volume, margin?) → the ENTRY ray parameter t ≥ 0 (0 when the
|
|
43
|
+
* origin is inside), or null for a miss / a volume wholly behind the origin. `margin`
|
|
44
|
+
* inflates the volume on every side first, so a thin wall is aimable.
|
|
45
|
+
* firstHit(origin, dir, volumes, margin?) → {index, t} | null — THE hover rule: the nearest
|
|
46
|
+
* entry wins, except that a smaller volume whose entry point lies inside the winner's
|
|
47
|
+
* (a volume nested in another along the ray) wins over it.
|
|
48
|
+
* volumeContains(volume, point, margin?) — the containment test firstHit uses.
|
|
49
|
+
*
|
|
50
|
+
* ── Layer 2: physics behaviour ───────────────────────────────────────────────
|
|
51
|
+
* ZERO physics. This reads a rect the engine already published; it moves no frame and no LO.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* hasZ(rect) — whether the rect carries a z (a z-capable / 6-element rect). THE one z-presence test
|
|
57
|
+
* every consumer asks (the carry's flat/z-held rule, a part's 6-on-flat refusal), never re-derived.
|
|
58
|
+
* @param {{z?:number}} rect @returns {boolean}
|
|
59
|
+
*/
|
|
60
|
+
function hasZ(rect) {
|
|
61
|
+
return typeof rect.z === 'number';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {{left:number, top:number, w:number, h:number, z?:number, d?:number}} rect
|
|
66
|
+
* @returns {number[]} `[cx, cy, cz]` — the center the renderer draws the rect's box at.
|
|
67
|
+
*/
|
|
68
|
+
function drawnCenter(rect) {
|
|
69
|
+
var cz = hasZ(rect)
|
|
70
|
+
? rect.z + ((typeof rect.d === 'number') ? rect.d : rect.w) / 2
|
|
71
|
+
: 0;
|
|
72
|
+
return [rect.left + rect.w / 2, rect.top + rect.h / 2, cz];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {{left:number, top:number, w:number, h:number, z?:number, d?:number}} rect — the rect
|
|
77
|
+
* being placed (its w/h and z-extent are kept).
|
|
78
|
+
* @param {number[]} center — `[cx, cy, cz]` (cz may be omitted for a z-less rect).
|
|
79
|
+
* @returns {number[]} the write-wire array `[l, t, z, w, h, d]` (z present) or `[l, t, w, h]`.
|
|
80
|
+
*/
|
|
81
|
+
function rectAtCenter(rect, center) {
|
|
82
|
+
var cz = (typeof center[2] === 'number') ? center[2] : 0;
|
|
83
|
+
if (hasZ(rect)) {
|
|
84
|
+
var D = _zExtent(rect);
|
|
85
|
+
return [center[0] - rect.w / 2, center[1] - rect.h / 2, cz - D / 2, rect.w, rect.h, D];
|
|
86
|
+
}
|
|
87
|
+
if (cz !== 0) {
|
|
88
|
+
throw new RangeError('[LoDrawnCenter.rectAtCenter] a z-less rect sits at z-centre 0 and cannot ' +
|
|
89
|
+
'be placed at z = ' + cz + ' — place it at cz 0, or give the rect an explicit z. (FAIL-LOUD.)');
|
|
90
|
+
}
|
|
91
|
+
return [center[0] - rect.w / 2, center[1] - rect.h / 2, rect.w, rect.h];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
var DEG = Math.PI / 180;
|
|
95
|
+
|
|
96
|
+
/** The rect's z-extent: d ?? w when it carries z (the engine's z-capable rectangle), else 0. */
|
|
97
|
+
function _zExtent(rect) {
|
|
98
|
+
if (!hasZ(rect)) { return 0; }
|
|
99
|
+
return (typeof rect.d === 'number') ? rect.d : rect.w;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @param {{rect:object, orientationDeg?:number, pitchDeg?:number}|{center:number[], radius:number}} src
|
|
104
|
+
* @returns {{kind:'box', center:number[], half:number[], yawDeg:number, pitchDeg:number}
|
|
105
|
+
* |{kind:'sphere', center:number[], radius:number}}
|
|
106
|
+
*/
|
|
107
|
+
function drawnVolume(src) {
|
|
108
|
+
if (src && src.rect) {
|
|
109
|
+
var r = src.rect;
|
|
110
|
+
return {
|
|
111
|
+
kind: 'box',
|
|
112
|
+
center: drawnCenter(r),
|
|
113
|
+
half: [r.w / 2, r.h / 2, _zExtent(r) / 2],
|
|
114
|
+
yawDeg: (typeof src.orientationDeg === 'number') ? src.orientationDeg : 0,
|
|
115
|
+
pitchDeg: (typeof src.pitchDeg === 'number') ? src.pitchDeg : 0
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (src && Array.isArray(src.center) && typeof src.radius === 'number') {
|
|
119
|
+
var c = src.center;
|
|
120
|
+
return { kind: 'sphere', center: [c[0], c[1], (typeof c[2] === 'number') ? c[2] : 0], radius: src.radius };
|
|
121
|
+
}
|
|
122
|
+
throw new TypeError('[LoDrawnCenter.drawnVolume] needs {rect} (an LO) or {center, radius} (a ' +
|
|
123
|
+
'centre-only target); got ' + JSON.stringify(src) + '. (FAIL-LOUD.)');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** A world vector expressed in the box's local frame: Rᵀ·v with R = Rz(yaw)·Ry(pitch). */
|
|
127
|
+
function _toLocal(box, v) {
|
|
128
|
+
var a = box.yawDeg * DEG, b = box.pitchDeg * DEG;
|
|
129
|
+
var ca = Math.cos(a), sa = Math.sin(a), cb = Math.cos(b), sb = Math.sin(b);
|
|
130
|
+
return [
|
|
131
|
+
ca * cb * v[0] + sa * cb * v[1] - sb * v[2],
|
|
132
|
+
-sa * v[0] + ca * v[1],
|
|
133
|
+
ca * sb * v[0] + sa * sb * v[1] + cb * v[2]
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function _sub(a, b) { return [a[0] - b[0], a[1] - b[1], a[2] - b[2]]; }
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* @param {number[]} origin @param {number[]} dir (need not be unit; t is in units of |dir|)
|
|
141
|
+
* @param {object} volume — a drawnVolume result. @param {number} [margin=0] world px.
|
|
142
|
+
* @returns {number|null}
|
|
143
|
+
*/
|
|
144
|
+
function rayIntersect(origin, dir, volume, margin) {
|
|
145
|
+
var m = (typeof margin === 'number') ? margin : 0;
|
|
146
|
+
if (volume.kind === 'sphere') {
|
|
147
|
+
var R = volume.radius + m;
|
|
148
|
+
var oc = _sub(origin, volume.center);
|
|
149
|
+
var A = dir[0] * dir[0] + dir[1] * dir[1] + dir[2] * dir[2];
|
|
150
|
+
var B = 2 * (oc[0] * dir[0] + oc[1] * dir[1] + oc[2] * dir[2]);
|
|
151
|
+
var C = oc[0] * oc[0] + oc[1] * oc[1] + oc[2] * oc[2] - R * R;
|
|
152
|
+
var disc = B * B - 4 * A * C;
|
|
153
|
+
if (!(A > 0) || disc < 0) { return null; }
|
|
154
|
+
var sq = Math.sqrt(disc);
|
|
155
|
+
var t0 = (-B - sq) / (2 * A), t1 = (-B + sq) / (2 * A);
|
|
156
|
+
if (t1 < 0) { return null; }
|
|
157
|
+
return (t0 > 0) ? t0 : 0;
|
|
158
|
+
}
|
|
159
|
+
if (volume.kind === 'box') {
|
|
160
|
+
var o = _toLocal(volume, _sub(origin, volume.center));
|
|
161
|
+
var d = _toLocal(volume, dir);
|
|
162
|
+
var tNear = -Infinity, tFar = Infinity;
|
|
163
|
+
for (var i = 0; i < 3; i++) {
|
|
164
|
+
var h = volume.half[i] + m;
|
|
165
|
+
if (Math.abs(d[i]) < 1e-12) {
|
|
166
|
+
if (o[i] < -h || o[i] > h) { return null; } // parallel to this slab, outside it
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
var ta = (-h - o[i]) / d[i], tb = (h - o[i]) / d[i];
|
|
170
|
+
if (ta > tb) { var tmp = ta; ta = tb; tb = tmp; }
|
|
171
|
+
if (ta > tNear) { tNear = ta; }
|
|
172
|
+
if (tb < tFar) { tFar = tb; }
|
|
173
|
+
if (tNear > tFar) { return null; }
|
|
174
|
+
}
|
|
175
|
+
if (tFar < 0) { return null; }
|
|
176
|
+
return (tNear > 0) ? tNear : 0;
|
|
177
|
+
}
|
|
178
|
+
throw new TypeError('[LoDrawnCenter.rayIntersect] unknown volume kind ' + String(volume && volume.kind) +
|
|
179
|
+
' — volumes come from drawnVolume(). (FAIL-LOUD.)');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Whether `point` lies inside `volume` (inflated by `margin`). */
|
|
183
|
+
function volumeContains(volume, point, margin) {
|
|
184
|
+
var m = (typeof margin === 'number') ? margin : 0;
|
|
185
|
+
var p = _sub(point, volume.center);
|
|
186
|
+
if (volume.kind === 'sphere') {
|
|
187
|
+
var R = volume.radius + m;
|
|
188
|
+
return p[0] * p[0] + p[1] * p[1] + p[2] * p[2] <= R * R + 1e-9;
|
|
189
|
+
}
|
|
190
|
+
var l = _toLocal(volume, p);
|
|
191
|
+
for (var i = 0; i < 3; i++) { if (Math.abs(l[i]) > volume.half[i] + m + 1e-9) { return false; } }
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** The inflated volume's size — the nested-volume tie-break compares it. */
|
|
196
|
+
function _size(volume, m) {
|
|
197
|
+
if (volume.kind === 'sphere') { var R = volume.radius + m; return R * R * R * 4.18879; }
|
|
198
|
+
return 8 * (volume.half[0] + m) * (volume.half[1] + m) * (volume.half[2] + m);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* @param {number[]} origin @param {number[]} dir @param {object[]} volumes @param {number} [margin=0]
|
|
203
|
+
* @returns {{index:number, t:number}|null}
|
|
204
|
+
*/
|
|
205
|
+
function firstHit(origin, dir, volumes, margin) {
|
|
206
|
+
var m = (typeof margin === 'number') ? margin : 0;
|
|
207
|
+
var hits = [];
|
|
208
|
+
for (var i = 0; i < volumes.length; i++) {
|
|
209
|
+
var t = rayIntersect(origin, dir, volumes[i], m);
|
|
210
|
+
if (t !== null) { hits.push({ index: i, t: t }); }
|
|
211
|
+
}
|
|
212
|
+
if (!hits.length) { return null; }
|
|
213
|
+
hits.sort(function (a, b) { return a.t - b.t; });
|
|
214
|
+
var best = hits[0];
|
|
215
|
+
for (var k = 1; k < hits.length; k++) {
|
|
216
|
+
var h = hits[k], vb = volumes[best.index], vh = volumes[h.index];
|
|
217
|
+
var entry = [origin[0] + dir[0] * h.t, origin[1] + dir[1] * h.t, origin[2] + dir[2] * h.t];
|
|
218
|
+
if (volumeContains(vb, entry, m) && _size(vh, m) < _size(vb, m)) { best = h; }
|
|
219
|
+
}
|
|
220
|
+
return best;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export {
|
|
224
|
+
drawnCenter,
|
|
225
|
+
rectAtCenter,
|
|
226
|
+
hasZ,
|
|
227
|
+
drawnVolume,
|
|
228
|
+
rayIntersect,
|
|
229
|
+
volumeContains,
|
|
230
|
+
firstHit
|
|
231
|
+
};
|