@descix/frqtl-node 0.0.0-stage → 0.6.0-p0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +87 -0
- package/README.md +84 -2
- package/dist/dist-manifest.json +218 -0
- package/dist/eyes/index.js +109 -0
- package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
- package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
- package/dist/fat/pkg-node/package.json +12 -0
- package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
- package/dist/frqtl/compat/boot-node.js +34 -0
- package/dist/frqtl/compat/engine.js +127 -0
- package/dist/frqtl/compat/package-root.js +24 -0
- package/dist/frqtl/compat/providers/wasm-engine.js +2501 -0
- package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
- package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
- package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
- package/dist/frqtl/compat/renderer/Filters.js +163 -0
- package/dist/frqtl/compat/renderer/RenderFamily.js +85 -0
- package/dist/frqtl/compat/renderer/RenderMesh.js +100 -0
- package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
- package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
- package/dist/frqtl/compat/renderer/captureStill-common.js +259 -0
- package/dist/frqtl/compat/renderer/display-slot.js +96 -0
- package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
- package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
- package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
- package/dist/frqtl/compat/renderer/providers/index.js +22 -0
- package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
- package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
- package/dist/frqtl/compat/renderer/providers/three-provider.js +2474 -0
- package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
- package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
- package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
- package/dist/frqtl/compat/vendor.js +196 -0
- package/dist/frqtl/node-entry.js +17 -0
- package/dist/frqtl/objects/FrqtlApp.js +2922 -0
- package/dist/frqtl/objects/ObjectFactory.js +297 -0
- package/dist/frqtl/objects/RecipeContext.js +322 -0
- package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
- package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
- package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
- package/dist/frqtl/objects/instruments/capture-host.js +453 -0
- package/dist/frqtl/objects/instruments/capture.js +711 -0
- package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -0
- package/dist/frqtl/objects/kinds-entry.js +14 -0
- package/dist/frqtl/objects/kinds.js +190 -0
- package/dist/frqtl/objects/objects/Box.js +273 -0
- package/dist/frqtl/objects/objects/Bullet.js +155 -0
- package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
- package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
- package/dist/frqtl/objects/objects/DirectedTunnel.js +412 -0
- package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
- package/dist/frqtl/objects/objects/LightSource.js +354 -0
- package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
- package/dist/frqtl/objects/objects/Tube.js +471 -0
- package/dist/frqtl/objects/objects/absorbRadiation.js +310 -0
- package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
- package/dist/frqtl/objects/objects/controlAction.js +89 -0
- package/dist/frqtl/objects/objects/controlBind.js +278 -0
- package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
- package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
- package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
- package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
- package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
- package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
- package/dist/frqtl/objects/objects/makeControlsForObject.js +824 -0
- package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
- package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
- package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
- package/dist/frqtl/objects/objects/tagger.js +228 -0
- package/dist/frqtl/objects/schema/engine-scopes.js +469 -0
- package/dist/frqtl/objects/schema/index.js +256 -0
- package/dist/frqtl/objects/schema/scopes.js +457 -0
- package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
- package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
- package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
- package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
- package/dist/frqtl/objects/setup/_worldInit.js +162 -0
- package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
- package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
- package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
- package/dist/frqtl/objects/setup/setupConeEmitter3D.js +251 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -0
- package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
- package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
- package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
- package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
- package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
- package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
- package/dist/frqtl/objects/setup/setupLightSourceLab.js +280 -0
- package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
- package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
- package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
- package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1259 -0
- package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
- package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
- package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
- package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
- package/dist/frqtl/objects/v1-replacements.js +30 -0
- package/dist/frqtl/package-core.js +23 -0
- package/dist/frqtl/parts/CameraControls.js +643 -0
- package/dist/frqtl/parts/CameraPath.js +575 -0
- package/dist/frqtl/parts/CollisionActions.js +35 -0
- package/dist/frqtl/parts/CollisionTypes.js +31 -0
- package/dist/frqtl/parts/DetectorWall.js +496 -0
- package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
- package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
- package/dist/frqtl/parts/FrqtlVision.js +818 -0
- package/dist/frqtl/parts/GraphingDetectorWall.js +490 -0
- package/dist/frqtl/parts/LargeObject.js +406 -0
- package/dist/frqtl/parts/LightColor.js +255 -0
- package/dist/frqtl/parts/LiveChart.js +285 -0
- package/dist/frqtl/parts/LogicGate.js +720 -0
- package/dist/frqtl/parts/OvenBox.js +612 -0
- package/dist/frqtl/parts/PointSource.js +763 -0
- package/dist/frqtl/parts/Rectangle.js +200 -0
- package/dist/frqtl/parts/SlitLightSource.js +234 -0
- package/dist/frqtl/parts/SlitScreen.js +628 -0
- package/dist/frqtl/parts/SphereEmitter.js +946 -0
- package/dist/frqtl/parts/Trace.js +508 -0
- package/dist/frqtl/parts/Tunnel.js +1057 -0
- package/dist/frqtl/parts/Wall.js +1211 -0
- package/dist/frqtl/parts/index.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
- package/dist/frqtl/scenes/absorbing-wall.js +32 -0
- package/dist/frqtl/scenes/atomic-model.js +88 -0
- package/dist/frqtl/scenes/big-bang.js +46 -0
- package/dist/frqtl/scenes/blackbody.js +81 -0
- package/dist/frqtl/scenes/circuit-sat.js +127 -0
- package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
- package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
- package/dist/frqtl/scenes/counting-wall.js +38 -0
- package/dist/frqtl/scenes/double-slit.js +31 -0
- package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
- package/dist/frqtl/scenes/dse-one-slit.js +114 -0
- package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
- package/dist/frqtl/scenes/dse-tunnel-rebuild.js +244 -0
- package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
- package/dist/frqtl/scenes/dse-two-slits.js +132 -0
- package/dist/frqtl/scenes/empty.js +14 -0
- package/dist/frqtl/scenes/laser-buildup.js +49 -0
- package/dist/frqtl/scenes/mirror-3d.js +56 -0
- package/dist/frqtl/scenes/mirror-route.js +140 -0
- package/dist/frqtl/scenes/mirror.js +38 -0
- package/dist/frqtl/scenes/omni-3d.js +27 -0
- package/dist/frqtl/scenes/omni-source.js +22 -0
- package/dist/frqtl/scenes/self-interference.js +39 -0
- package/dist/frqtl/scenes/smoking-gun.js +122 -0
- package/dist/frqtl/scenes/straight-walk.js +22 -0
- package/dist/frqtl/scenes/sun.js +39 -0
- package/dist/frqtl/scenes/two-source.js +37 -0
- package/dist/frqtl/scenes/wave.js +26 -0
- package/dist/shim/bundle/frqnb.js +112 -0
- package/dist/shim/bundle/index.js +121 -0
- package/dist/shim/bundle/indexHtml.js +107 -0
- package/dist/shim/bundle/manifest.js +95 -0
- package/dist/shim/bundle/zip.js +243 -0
- package/dist/shim/bus.js +67 -0
- package/dist/shim/controls/index.js +627 -0
- package/dist/shim/controls/registry.js +274 -0
- package/dist/shim/engine/artifact-shell.js +212 -0
- package/dist/shim/engine/boot-phases.js +44 -0
- package/dist/shim/engine/engine-stack-boot.js +59 -0
- package/dist/shim/engine/router.js +692 -0
- package/dist/shim/experiment-frame/index.js +296 -0
- package/dist/shim/expr/ast-schema.js +369 -0
- package/dist/shim/field-descriptor/index.js +326 -0
- package/dist/shim/frame-data/index.js +224 -0
- package/dist/shim/index.js +67 -0
- package/dist/shim/notebook/canvas-guard.js +82 -0
- package/dist/shim/notebook/catalog.js +35 -0
- package/dist/shim/notebook/format.js +736 -0
- package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
- package/dist/shim/notebook/kernel.js +672 -0
- package/dist/shim/notebook/knob.js +151 -0
- package/dist/shim/notebook/module-resolver.js +260 -0
- package/dist/shim/notebook/notebook-knobs.js +65 -0
- package/dist/shim/notebook/produced-bindings.js +67 -0
- package/dist/shim/notebook/realm-registry.js +121 -0
- package/dist/shim/notebook/realm-transport.js +73 -0
- package/dist/shim/notebook/setup-builder.js +510 -0
- package/dist/shim/notebook/store.js +159 -0
- package/dist/shim/notebook/vendor-importmap.js +134 -0
- package/dist/shim/notebook/view-builder.js +189 -0
- package/dist/shim/notebook/view-ctx.js +100 -0
- package/dist/shim/notebook/view-handle.js +202 -0
- package/dist/shim/notebook/warmup.js +102 -0
- package/dist/shim/package.json +4 -0
- package/dist/shim/page-params/index.js +214 -0
- package/dist/shim/project/identity.js +112 -0
- package/dist/shim/project/index.js +19 -0
- package/dist/shim/project/link.js +402 -0
- package/dist/shim/project/node-specifier-hook.mjs +35 -0
- package/dist/shim/project/register-node.mjs +17 -0
- package/dist/shim/project/specifiers.js +106 -0
- package/dist/shim/registry/copy-gate.js +110 -0
- package/dist/shim/registry/exhibits.js +455 -0
- package/dist/shim/registry/experiments.js +215 -0
- package/dist/shim/registry/index.js +19 -0
- package/dist/shim/registry/scene-source.js +157 -0
- package/dist/shim/registry/walkthrough.js +807 -0
- package/dist/shim/render/capture-envelope.js +279 -0
- package/dist/shim/render/heat-colormap.js +92 -0
- package/dist/shim/render/render-stages.js +855 -0
- package/dist/shim/render/sun-look.js +33 -0
- package/dist/shim/render-state/apply.js +262 -0
- package/dist/shim/render-state/index.js +74 -0
- package/dist/shim/rpc/index.js +207 -0
- package/dist/shim/runtime/index.js +1023 -0
- package/dist/shim/storage/boot.js +69 -0
- package/dist/shim/storage/index.js +169 -0
- package/dist/shim/storage/local-storage-provider.js +230 -0
- package/dist/shim/storage/make-namespace-store.js +132 -0
- package/dist/shim/storage/modules.js +249 -0
- package/dist/shim/storage/projects.js +150 -0
- package/llms.txt +12 -0
- package/package.json +62 -4
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
/* Copyright 2023-2026 by Essam Abadir */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GraphingDetectorWall — a Detector Screen that PAINTS ITS OWN HIT GRID onto its
|
|
5
|
+
* own faces (ws-experiments-walkthrough, presentation pass Item 1; CEO ruling
|
|
6
|
+
* "the spatial hitmap IS the chart").
|
|
7
|
+
*
|
|
8
|
+
* var screen = new GraphingDetectorWall(universe, {
|
|
9
|
+
* rect: [236, 0, -77, 20, 600, 154], name: 'screen', binPx: 12
|
|
10
|
+
* });
|
|
11
|
+
*
|
|
12
|
+
* ── The contract in one line ────────────────────────────────────────────────
|
|
13
|
+
*
|
|
14
|
+
* The in-scene instrument is a CHART-FREE, SPATIALLY REGISTERED HEATMAP: a hot
|
|
15
|
+
* cell sits exactly where the counted collisions physically occurred. There is
|
|
16
|
+
* no chart chrome on the wall — no axes, ticks, labels, legends or margins; the
|
|
17
|
+
* bin rectangles fill the face edge-to-edge through ONE owned linear map. THE
|
|
18
|
+
* WALL IS THE AXIS. Chart analysis of the same data (marginals, ledgers,
|
|
19
|
+
* topology scatter) stays fully available SEPARATELY in the DOM panel lane
|
|
20
|
+
* (Chart.js, lib/components/live-chart-wiring) — that lane is untouched.
|
|
21
|
+
*
|
|
22
|
+
* ── Layer 1: API contract ───────────────────────────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* `new GraphingDetectorWall(universe, opts)` — EXTENDS `Wall` (prototype chain),
|
|
25
|
+
* so every Wall option, live method and property accessor applies verbatim.
|
|
26
|
+
* When neither `preset` nor `collisionBehavior` is supplied it defaults to the
|
|
27
|
+
* `detector-screen` preset (destroy + count) — the counting authority. Extra
|
|
28
|
+
* options, all DECLARED descriptor fields (so a codegen Restart round-trip
|
|
29
|
+
* rebuilds the heatmap wall from captured state — the W2 declared-field rule):
|
|
30
|
+
*
|
|
31
|
+
* @param {number} [opts.binPx=12] — face bin size in physics pixels. Bins
|
|
32
|
+
* are anchored at the FACE ORIGIN (its top / near-z corner), so the map runs
|
|
33
|
+
* edge-to-edge with no partial bin hanging off the leading edge.
|
|
34
|
+
* @param {number} [opts.refreshMs=100] — repaint throttle (ms). Accumulation
|
|
35
|
+
* is per-tick and un-throttled; only the repaint is gated.
|
|
36
|
+
* @param {number} [opts.heatBase=0.35] — opacity (0..1) of the ramp's ZERO
|
|
37
|
+
* colour washed under the bins, so an un-hit face still reads as a plate and
|
|
38
|
+
* intensity rises monotonically from empty through every bin.
|
|
39
|
+
*
|
|
40
|
+
* Readouts: `getCollisionHistogram()` / `resetHistogram()` / `getLiveCount()` —
|
|
41
|
+
* inherited from `Wall`, UNCHANGED. `heatCanvas()` returns the ferry canvas.
|
|
42
|
+
*
|
|
43
|
+
* ── ESTIMATION BEFORE RUNNING: when will this plate have counted anything? ───
|
|
44
|
+
*
|
|
45
|
+
* A plate reads `{"total":0}` for as long as nothing has crossed the distance to it, and
|
|
46
|
+
* that is not a fault — it is arithmetic you can do BEFORE the run:
|
|
47
|
+
*
|
|
48
|
+
* ticks ≈ (distance from the source to this plate, px) ÷ the source preset's
|
|
49
|
+
* EFFECTIVE FORWARD SPEED (px/tick)
|
|
50
|
+
*
|
|
51
|
+
* The per-preset speeds are MEASURED and tabulated on the source that emits at you —
|
|
52
|
+
* see `SphereEmitter.js`, "ESTIMATION BEFORE RUNNING" (`particle-walk` 1.00, `laser`
|
|
53
|
+
* ~0.99, `hemisphere` 0.65 in 2D / 0.49 in 3D; the isotropic `spherical`/`drum` presets
|
|
54
|
+
* have no forward axis and expand radially at ~1.00). Worked: a `hemisphere` source 350 px
|
|
55
|
+
* upstream in 3D ⇒ 350 ÷ 0.49 ≈ 700 ticks before the MEAN of the population arrives — so a
|
|
56
|
+
* 300-tick run legitimately counts zero, and the fix is more ticks or less distance, not a
|
|
57
|
+
* different wall.
|
|
58
|
+
*
|
|
59
|
+
* It is a random walk's mean, not a wavefront: the first counts land earlier and the bulk
|
|
60
|
+
* later. `binPx` changes the RESOLUTION of what you see, never when you see it.
|
|
61
|
+
*
|
|
62
|
+
* ── The ONE grid authority, consumed — never re-derived ─────────────────────
|
|
63
|
+
*
|
|
64
|
+
* The painter reads the SAME bag `Wall.gridAdd` accumulates
|
|
65
|
+
* (`Wall.js` — `getCollisionHistogram()` → `{combined, grid, total}`): per tick
|
|
66
|
+
* it calls the DRAIN lane `getCollisionHistogram(tickData)` exactly once (the
|
|
67
|
+
* #61 once-per-tick discipline the wall itself enforces), and paints from the
|
|
68
|
+
* no-arg PURE READ. It never scans frames, never touches the engine RNG, and
|
|
69
|
+
* never keeps a second accumulator.
|
|
70
|
+
*
|
|
71
|
+
* IMPORTANT (FAIL-LOUD by construction): a wall's collided-frame buffer may be
|
|
72
|
+
* drained by exactly ONE consumer per tick. A scene that puts a `LiveChart` on
|
|
73
|
+
* a GraphingDetectorWall would hit the wall's OWN double-consume guard. Choose
|
|
74
|
+
* one instrument per detector: the in-scene heatmap OR the DOM chart.
|
|
75
|
+
*
|
|
76
|
+
* ── Layer 2: physics behaviour ──────────────────────────────────────────────
|
|
77
|
+
*
|
|
78
|
+
* ZERO physics (SU1). This object adds no collision logic: counting + destroy is
|
|
79
|
+
* Wall's already-compiled op-program (canonical bench LO7 MARK+COUNT), and the
|
|
80
|
+
* painter is a pure between-tick READER plus a canvas draw at the output
|
|
81
|
+
* boundary. It cannot move a bench golden.
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
import { Wall as WallCls } from './Wall.js';
|
|
85
|
+
import * as HeatColormap from '../../shim/render/heat-colormap.js';
|
|
86
|
+
import { registerKind, kindOf } from '../objects/kinds.js';
|
|
87
|
+
|
|
88
|
+
'use strict';
|
|
89
|
+
|
|
90
|
+
// ── The ONE linear cell→face map (pure; exported for the conformance test) ──
|
|
91
|
+
|
|
92
|
+
/** Largest heat-canvas edge in device pixels (a texture, not a screenshot). */
|
|
93
|
+
var MAX_CANVAS_PX = 512;
|
|
94
|
+
/** Canvas width used when the wall has NO z-slab (one full-width z bucket). */
|
|
95
|
+
var FLAT_CANVAS_W = 64;
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* faceSpec(rect, binPx) — the wall's BROAD-FACE coordinate system + the heat
|
|
99
|
+
* canvas resolution, derived from the LO rect.
|
|
100
|
+
*
|
|
101
|
+
* The instrument's data axes are the grid's own axes: HORIZONTAL = z,
|
|
102
|
+
* VERTICAL = y (`Wall.gridAdd(grid, y, z)`). For the canonical detector
|
|
103
|
+
* screen (thin along x) those ARE the broad face's two axes, so the map is
|
|
104
|
+
* an exact spatial registration. A wall with NO z-slab has a single
|
|
105
|
+
* z-bucket, so its heatmap is one full-width column per y bin — the honest
|
|
106
|
+
* painting of the only spatial information a 2D scene carries.
|
|
107
|
+
*
|
|
108
|
+
* @param {Array|Object} rect — the LO rect ([l,t,w,h] / [l,t,z,w,h,d] /
|
|
109
|
+
* {left,top,w,h[,z,d]} — the shape a codegen round-trip captures).
|
|
110
|
+
* @param {number} binPx
|
|
111
|
+
* @returns {{top:number,height:number,z0:number,depth:number,binPx:number,
|
|
112
|
+
* canvasW:number,canvasH:number}}
|
|
113
|
+
*/
|
|
114
|
+
function faceSpec(rect, binPx) {
|
|
115
|
+
var top, height, z0 = 0, depth = 0;
|
|
116
|
+
if (Array.isArray(rect)) {
|
|
117
|
+
if (rect.length >= 6) { top = rect[1]; z0 = rect[2]; height = rect[4]; depth = rect[5]; }
|
|
118
|
+
else { top = rect[1]; height = rect[3]; }
|
|
119
|
+
} else if (rect) {
|
|
120
|
+
top = rect.top; height = rect.h;
|
|
121
|
+
if (typeof rect.z === 'number') { z0 = rect.z; }
|
|
122
|
+
if (typeof rect.d === 'number') { depth = rect.d; }
|
|
123
|
+
}
|
|
124
|
+
top = Number(top) || 0;
|
|
125
|
+
height = Math.max(1, Number(height) || 1);
|
|
126
|
+
depth = Math.max(0, Number(depth) || 0);
|
|
127
|
+
var b = (typeof binPx === 'number' && binPx > 0) ? binPx : 12;
|
|
128
|
+
var scale = Math.min(1, MAX_CANVAS_PX / Math.max(height, depth || 1));
|
|
129
|
+
return {
|
|
130
|
+
top: top, height: height, z0: z0, depth: depth, binPx: b,
|
|
131
|
+
canvasW: (depth > 0) ? Math.max(1, Math.round(depth * scale)) : FLAT_CANVAS_W,
|
|
132
|
+
canvasH: Math.max(1, Math.round(height * scale))
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* binStart(v, origin, binPx) — the world coordinate of the bin containing `v`,
|
|
138
|
+
* anchored at the FACE ORIGIN (not at world 0). Anchoring at the face is what
|
|
139
|
+
* keeps the map edge-to-edge: a face whose z-range starts at −77 with 12px bins
|
|
140
|
+
* would otherwise open with a partial bin hanging off the canvas. One helper,
|
|
141
|
+
* used by BOTH the binner and the map, so they can never disagree.
|
|
142
|
+
*/
|
|
143
|
+
function binStart(v, origin, binPx) {
|
|
144
|
+
return origin + Math.floor((Number(v) - origin) / binPx) * binPx;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* heatCellRect(spec, y, z) — THE map. One grid cell (world y, world z) → its
|
|
149
|
+
* rectangle on the heat canvas, in canvas pixels. Pure + exported so the
|
|
150
|
+
* conformance test asserts painted positions against THIS function rather
|
|
151
|
+
* than hand-copied coordinates.
|
|
152
|
+
*
|
|
153
|
+
* @param {object} spec — a faceSpec() result.
|
|
154
|
+
* @param {number} y — world y of the hit (physics px).
|
|
155
|
+
* @param {number} z — world z of the hit (physics px; ignored when depth 0).
|
|
156
|
+
* @returns {{x:number,y:number,w:number,h:number}}
|
|
157
|
+
*/
|
|
158
|
+
function heatCellRect(spec, y, z) {
|
|
159
|
+
var b = spec.binPx;
|
|
160
|
+
var sy = spec.canvasH / spec.height;
|
|
161
|
+
var yb = binStart(y, spec.top, b);
|
|
162
|
+
var out = { x: 0, y: (yb - spec.top) * sy, w: spec.canvasW, h: b * sy };
|
|
163
|
+
if (spec.depth > 0) {
|
|
164
|
+
var sx = spec.canvasW / spec.depth;
|
|
165
|
+
var zb = binStart(z, spec.z0, b);
|
|
166
|
+
out.x = (zb - spec.z0) * sx;
|
|
167
|
+
out.w = b * sx;
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* binGrid(grid, spec) — fold the wall's Y×Z grid into binned cells + the peak
|
|
174
|
+
* count, through the SAME `binStart` the map uses. Pure; exported for the unit test.
|
|
175
|
+
* @param {Map<number,Map<number,number>>} grid
|
|
176
|
+
* @param {object} spec — a faceSpec() result.
|
|
177
|
+
* @returns {{cells:Array<{y:number,z:number,v:number}>, maxV:number}}
|
|
178
|
+
*/
|
|
179
|
+
function binGrid(grid, spec) {
|
|
180
|
+
var cells = [], maxV = 0;
|
|
181
|
+
if (!grid || typeof grid.forEach !== 'function') { return { cells: cells, maxV: maxV }; }
|
|
182
|
+
var b = spec.binPx;
|
|
183
|
+
var acc = new Map();
|
|
184
|
+
grid.forEach(function (byZ, y) {
|
|
185
|
+
if (!byZ || typeof byZ.forEach !== 'function') { return; }
|
|
186
|
+
var yb = binStart(y, spec.top, b);
|
|
187
|
+
byZ.forEach(function (count, z) {
|
|
188
|
+
var zb = (spec.depth > 0) ? binStart(z, spec.z0, b) : spec.z0;
|
|
189
|
+
var key = yb + ',' + zb;
|
|
190
|
+
acc.set(key, (acc.get(key) || 0) + (Number(count) || 0));
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
acc.forEach(function (v, key) {
|
|
194
|
+
var p = key.split(',');
|
|
195
|
+
if (v > maxV) { maxV = v; }
|
|
196
|
+
cells.push({ y: Number(p[0]), z: Number(p[1]), v: v });
|
|
197
|
+
});
|
|
198
|
+
return { cells: cells, maxV: maxV };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* paintHeatmap(ctx, spec, grid, opts) — draw the registered heatmap: a base
|
|
203
|
+
* wash of the wall's own colour, then ONE `fillRect` per non-empty bin at
|
|
204
|
+
* `heatCellRect`'s position, coloured by the shared ramp at count/peak.
|
|
205
|
+
* No axes, no labels, no margins (the wall IS the axis).
|
|
206
|
+
*
|
|
207
|
+
* @param {CanvasRenderingContext2D} ctx
|
|
208
|
+
* @param {object} spec — a faceSpec() result.
|
|
209
|
+
* @param {Map} grid — the Wall's Y×Z grid.
|
|
210
|
+
* @param {object} [opts] — { heatBase:number }
|
|
211
|
+
* @returns {number} the number of bins painted (observability).
|
|
212
|
+
*/
|
|
213
|
+
function paintHeatmap(ctx, spec, grid, opts) {
|
|
214
|
+
opts = opts || {};
|
|
215
|
+
var CM = HeatColormap;
|
|
216
|
+
var heatColor = CM.heatColor;
|
|
217
|
+
// The base wash is the ramp's ZERO colour, so intensity rises MONOTONICALLY
|
|
218
|
+
// from an un-hit face through every bin: a lightly-hit cell can never read
|
|
219
|
+
// darker (or hotter) than empty wall. Painting the wall's own colour here
|
|
220
|
+
// instead breaks that ordering — measured in the realm: a green plate wash
|
|
221
|
+
// sat BETWEEN the ramp's cold and warm ends, so a sparse bin looked like a
|
|
222
|
+
// hole. `heatBase` is the wash's opacity (0 = a fully transparent face).
|
|
223
|
+
var zero = CM.heatRgb(0);
|
|
224
|
+
var baseA = (typeof opts.heatBase === 'number') ? opts.heatBase : 0.35;
|
|
225
|
+
ctx.clearRect(0, 0, spec.canvasW, spec.canvasH);
|
|
226
|
+
ctx.fillStyle = 'rgba(' + zero[0] + ',' + zero[1] + ',' + zero[2] + ',' + baseA + ')';
|
|
227
|
+
ctx.fillRect(0, 0, spec.canvasW, spec.canvasH);
|
|
228
|
+
var binned = binGrid(grid, spec);
|
|
229
|
+
for (var i = 0; i < binned.cells.length; i++) {
|
|
230
|
+
var c = binned.cells[i];
|
|
231
|
+
var r = heatCellRect(spec, c.y, c.z);
|
|
232
|
+
ctx.fillStyle = heatColor(binned.maxV > 0 ? c.v / binned.maxV : 0);
|
|
233
|
+
ctx.fillRect(r.x, r.y, r.w, r.h);
|
|
234
|
+
}
|
|
235
|
+
return binned.cells.length;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── The object ───────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
function _num(v, dflt) { return (typeof v === 'number' && isFinite(v)) ? v : dflt; }
|
|
241
|
+
|
|
242
|
+
function GraphingDetectorWall(universe, opts) {
|
|
243
|
+
opts = opts || {};
|
|
244
|
+
// Default to the Detector Screen preset (destroy + count) — this object IS a
|
|
245
|
+
// counting detector; an explicit preset/collisionBehavior still wins.
|
|
246
|
+
if (opts.preset === undefined && opts.collisionBehavior === undefined) {
|
|
247
|
+
var merged = {}; Object.keys(opts).forEach(function (k) { merged[k] = opts[k]; });
|
|
248
|
+
merged.preset = 'detector-screen';
|
|
249
|
+
opts = merged;
|
|
250
|
+
}
|
|
251
|
+
WallCls.call(this, universe, opts);
|
|
252
|
+
this._schemaKind = 'GraphingDetectorWall';
|
|
253
|
+
|
|
254
|
+
// Declared heatmap fields (own props → codegen captureProps round-trip).
|
|
255
|
+
this.binPx = _num(opts.binPx, 12);
|
|
256
|
+
this.refreshMs = _num(opts.refreshMs, 100);
|
|
257
|
+
this.heatBase = _num(opts.heatBase, 0.35);
|
|
258
|
+
|
|
259
|
+
var counts = Array.isArray(this.hooks) && this.hooks.indexOf('count') !== -1;
|
|
260
|
+
if (!counts) {
|
|
261
|
+
throw new Error('[GraphingDetectorWall] this wall does not COUNT (collisionBehavior "' +
|
|
262
|
+
this.collisionBehavior + '", hooks ' + JSON.stringify(this.hooks || []) + ') — a ' +
|
|
263
|
+
'graphing detector needs the count hook (the "detector-screen" preset supplies it), ' +
|
|
264
|
+
'or it would paint an eternally empty grid. (FAIL-LOUD.)');
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
this._faceSpec = faceSpec(this._trueRect, this.binPx);
|
|
268
|
+
this._canvas = this._makeCanvas(opts.makeCanvas);
|
|
269
|
+
// The ferry: the providers composite this canvas natively (p5 image /
|
|
270
|
+
// three CanvasTexture). No provider capability method, no chartSpec.
|
|
271
|
+
this.lo.liveCanvas = this._canvas;
|
|
272
|
+
this._lastPaintMs = 0;
|
|
273
|
+
this._paintedBins = 0;
|
|
274
|
+
this._drainedTickData = undefined;
|
|
275
|
+
|
|
276
|
+
var self = this;
|
|
277
|
+
this._paintTickFn = function graphingDetectorWallTick(arg) {
|
|
278
|
+
self._tick(arg);
|
|
279
|
+
};
|
|
280
|
+
if (universe && Array.isArray(universe.experimentTickFunctions)) {
|
|
281
|
+
universe.experimentTickFunctions.push(this._paintTickFn);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
GraphingDetectorWall.prototype = Object.create(WallCls ? WallCls.prototype : Object.prototype);
|
|
286
|
+
GraphingDetectorWall.prototype.constructor = GraphingDetectorWall;
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Create the offscreen ferry canvas. A HEADLESS environment (Node tests, the
|
|
290
|
+
* pkg-node settlement CLI) has no `document`: the wall then runs with NO
|
|
291
|
+
* canvas — physics, counting and every readout are unchanged, only the paint
|
|
292
|
+
* is skipped. A test may inject `opts.makeCanvas(w,h)`.
|
|
293
|
+
*/
|
|
294
|
+
GraphingDetectorWall.prototype._makeCanvas = function (makeCanvas) {
|
|
295
|
+
var s = this._faceSpec;
|
|
296
|
+
if (typeof makeCanvas === 'function') { return makeCanvas(s.canvasW, s.canvasH); }
|
|
297
|
+
if (typeof document !== 'undefined' && document.createElement) {
|
|
298
|
+
var cv = document.createElement('canvas');
|
|
299
|
+
cv.width = s.canvasW; cv.height = s.canvasH;
|
|
300
|
+
return cv;
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* setRect(rect) — Wall's one position/extent write, then the FACE follows it: the face
|
|
307
|
+
* spec (the paint's world origin) is re-derived from the moved true slab, and the canvas is
|
|
308
|
+
* re-sized when the extent changed. The grid stays keyed by WORLD (y, z) — where each hit
|
|
309
|
+
* landed — so a moved face paints the hits that fall on it where they fall.
|
|
310
|
+
*/
|
|
311
|
+
GraphingDetectorWall.prototype.setRect = function (rect) {
|
|
312
|
+
WallCls.prototype.setRect.call(this, rect);
|
|
313
|
+
this._faceSpec = faceSpec(this._trueRect, this.binPx);
|
|
314
|
+
var cv = this._canvas;
|
|
315
|
+
if (cv && (cv.width !== this._faceSpec.canvasW || cv.height !== this._faceSpec.canvasH)) {
|
|
316
|
+
cv.width = this._faceSpec.canvasW; cv.height = this._faceSpec.canvasH;
|
|
317
|
+
}
|
|
318
|
+
this.repaintHeatmap();
|
|
319
|
+
return this;
|
|
320
|
+
};
|
|
321
|
+
|
|
322
|
+
/** The ferry canvas (or null in a headless environment). */
|
|
323
|
+
GraphingDetectorWall.prototype.heatCanvas = function () { return this._canvas; };
|
|
324
|
+
|
|
325
|
+
/** Bins painted on the last repaint — observability for the realm/unit checks. */
|
|
326
|
+
GraphingDetectorWall.prototype.paintedBins = function () { return this._paintedBins; };
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* The host tick-fn: drain the collided-frame buffer EXACTLY once per tick
|
|
330
|
+
* (the accumulate lane), then repaint at most every `refreshMs`.
|
|
331
|
+
* `experimentTickFunctions` are called `fn(universe)` BEFORE do_tick, so the
|
|
332
|
+
* drain reads the universe's cached LAST TickData — the documented one-tick
|
|
333
|
+
* lag (the absorb-radiation precedent); a test may call `fn(tickData)`.
|
|
334
|
+
*/
|
|
335
|
+
GraphingDetectorWall.prototype._tick = function (arg) {
|
|
336
|
+
var td = (arg && arg.objects) ? arg
|
|
337
|
+
: (this.universe && this.universe._lastTickData) ? this.universe._lastTickData
|
|
338
|
+
: null;
|
|
339
|
+
// The painter keeps its OWN consumed-tick token: Wall's double-consume guard
|
|
340
|
+
// field is reset by every no-arg pure READ (which the repaint below performs),
|
|
341
|
+
// so it cannot double as the drain ledger.
|
|
342
|
+
if (td && td !== this._drainedTickData) {
|
|
343
|
+
this._drainedTickData = td;
|
|
344
|
+
this.getCollisionHistogram(td);
|
|
345
|
+
}
|
|
346
|
+
var now = (typeof Date !== 'undefined') ? Date.now() : 0;
|
|
347
|
+
if (this.refreshMs > 0 && (now - this._lastPaintMs) < this.refreshMs) { return; }
|
|
348
|
+
this._lastPaintMs = now;
|
|
349
|
+
this.repaintHeatmap();
|
|
350
|
+
};
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Repaint the ferry canvas from the CURRENT grid (the no-arg pure read).
|
|
354
|
+
* Safe to call between ticks; a no-op with no canvas (headless).
|
|
355
|
+
* @returns {number} bins painted.
|
|
356
|
+
*/
|
|
357
|
+
GraphingDetectorWall.prototype.repaintHeatmap = function () {
|
|
358
|
+
if (!this._canvas || typeof this._canvas.getContext !== 'function') { return 0; }
|
|
359
|
+
var ctx = this._canvas.getContext('2d');
|
|
360
|
+
if (!ctx) { return 0; }
|
|
361
|
+
var hist = this.getCollisionHistogram(); // no-arg = pure read, never consumes
|
|
362
|
+
this._paintedBins = paintHeatmap(ctx, this._faceSpec, hist && hist.grid, {
|
|
363
|
+
heatBase: this.heatBase
|
|
364
|
+
});
|
|
365
|
+
return this._paintedBins;
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Full teardown: drop the painter tick-fn (so FrqtlApp.setup() reinit
|
|
370
|
+
* does not STACK painters), then Wall's own dispose. Idempotent.
|
|
371
|
+
*/
|
|
372
|
+
GraphingDetectorWall.prototype.dispose = function () {
|
|
373
|
+
if (this._paintTickFn && this.universe && Array.isArray(this.universe.experimentTickFunctions)) {
|
|
374
|
+
var i = this.universe.experimentTickFunctions.indexOf(this._paintTickFn);
|
|
375
|
+
if (i !== -1) { this.universe.experimentTickFunctions.splice(i, 1); }
|
|
376
|
+
this._paintTickFn = null;
|
|
377
|
+
}
|
|
378
|
+
if (WallCls && WallCls.prototype.dispose) { WallCls.prototype.dispose.call(this); }
|
|
379
|
+
};
|
|
380
|
+
|
|
381
|
+
// Statics — the exported pure map (the assertion surface for the unit test).
|
|
382
|
+
GraphingDetectorWall.faceSpec = faceSpec;
|
|
383
|
+
GraphingDetectorWall.heatCellRect = heatCellRect;
|
|
384
|
+
GraphingDetectorWall.binGrid = binGrid;
|
|
385
|
+
GraphingDetectorWall.paintHeatmap = paintHeatmap;
|
|
386
|
+
|
|
387
|
+
// ── The kind (objects/kinds.js): descriptor, construction defaults, palette, builder ──────────
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* GraphingDetectorWall — a Detector Screen that PAINTS ITS OWN HIT GRID onto
|
|
391
|
+
* its own faces (presentation pass Item 1; CEO ruling "the spatial hitmap IS
|
|
392
|
+
* the chart"). A SUGAR kind that EXTENDS `Wall` (prototype chain): the
|
|
393
|
+
* physics authority — counting, `Wall.gridAdd`, the once-per-tick drain
|
|
394
|
+
* discipline — is Wall's, verbatim and untouched. What it adds is a
|
|
395
|
+
* sugar-side painter that reads the SAME grid and paints pure bin rectangles
|
|
396
|
+
* onto the wall LO's `liveCanvas` ferry through ONE owned linear map, so a
|
|
397
|
+
* hot cell sits exactly where the counted collisions physically occurred.
|
|
398
|
+
* No chart chrome in-scene (the wall IS the axis); chart/statistical analysis
|
|
399
|
+
* of the same data stays available separately in the DOM panel lane.
|
|
400
|
+
*
|
|
401
|
+
* FIELDS ARE FERRIED FROM WallDescriptor (never re-listed — the
|
|
402
|
+
* schema-mirror-drift discipline): this descriptor is Wall's field table plus
|
|
403
|
+
* the three heatmap fields. Every one is a DECLARED field, so a codegen
|
|
404
|
+
* Restart round-trip rebuilds the heatmap wall from captured state.
|
|
405
|
+
*/
|
|
406
|
+
var WallDescriptor = kindOf(WallCls).descriptor;
|
|
407
|
+
var GraphingDetectorWallDescriptor = {
|
|
408
|
+
description: 'A counting detector screen that draws its OWN hit heatmap on its faces: ' +
|
|
409
|
+
'collisions are binned over the wall face (y×z) and painted as colour cells exactly ' +
|
|
410
|
+
'where they landed — the wall is the axis, so there are no chart axes, labels or ' +
|
|
411
|
+
'margins in the scene. Extends Wall (defaults to the detector-screen preset: destroy ' +
|
|
412
|
+
'+ count); zero physics of its own.',
|
|
413
|
+
// Same UX as a Wall — a real draggable/resizable boundary, visible during Run
|
|
414
|
+
// (an instrument you WATCH fill up).
|
|
415
|
+
ux: WallDescriptor.ux,
|
|
416
|
+
fields: WallDescriptor.fields.concat([
|
|
417
|
+
{
|
|
418
|
+
name: 'binPx',
|
|
419
|
+
type: 'number',
|
|
420
|
+
constraint: { min: 1, integer: true },
|
|
421
|
+
engineVisible: false,
|
|
422
|
+
unit: 'px',
|
|
423
|
+
widgetHint: 'slider',
|
|
424
|
+
description: 'Face bin size in physics pixels (default 12) — the heatmap cell pitch. ' +
|
|
425
|
+
'Smaller resolves finer structure but each cell holds fewer hits (noisier); larger ' +
|
|
426
|
+
'smooths. The SAME binning the DOM topology chart applies to the same grid.'
|
|
427
|
+
},
|
|
428
|
+
{
|
|
429
|
+
name: 'refreshMs',
|
|
430
|
+
type: 'number',
|
|
431
|
+
constraint: { min: 0, integer: true },
|
|
432
|
+
engineVisible: false,
|
|
433
|
+
unit: 'ms',
|
|
434
|
+
description: 'Heatmap repaint throttle in milliseconds (default 100). Hit accumulation ' +
|
|
435
|
+
'is per-tick and never throttled — only the repaint is gated, so no counts are lost.'
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: 'heatBase',
|
|
439
|
+
type: 'number',
|
|
440
|
+
constraint: { min: 0, max: 1 },
|
|
441
|
+
engineVisible: false,
|
|
442
|
+
widgetHint: 'slider',
|
|
443
|
+
description: 'Opacity (0..1, default 0.35) of the heat ramp\'s ZERO colour washed ' +
|
|
444
|
+
'UNDER the cells, so an un-hit face still reads as a solid plate and intensity ' +
|
|
445
|
+
'rises monotonically from empty through every bin. Render-only.'
|
|
446
|
+
}
|
|
447
|
+
])
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
//
|
|
451
|
+
// A counting detector screen that paints its OWN registered hit heatmap onto
|
|
452
|
+
// its faces (presentation pass Item 1). It EXTENDS Wall, so this builder is
|
|
453
|
+
// the Wall builder's twin: same opts pass-through, same F1 drop idiom.
|
|
454
|
+
registerKind(GraphingDetectorWall, {
|
|
455
|
+
from: import.meta.url,
|
|
456
|
+
descriptor: GraphingDetectorWallDescriptor,
|
|
457
|
+
defaults: {
|
|
458
|
+
w: 20,
|
|
459
|
+
h: 200,
|
|
460
|
+
binPx: 12,
|
|
461
|
+
refreshMs: 100,
|
|
462
|
+
heatBase: 0.35,
|
|
463
|
+
color: [120, 200, 120]
|
|
464
|
+
},
|
|
465
|
+
palette: { displayName: 'Graphing Detector Wall', category: 'measurement', iconHint: 'detector', rank: 3 },
|
|
466
|
+
build: function (props, ctx, Class) {
|
|
467
|
+
var GDW = Class; // the class that registered (a composed fork's own class)
|
|
468
|
+
|
|
469
|
+
var opts = {};
|
|
470
|
+
for (var k in props) {
|
|
471
|
+
if (!Object.prototype.hasOwnProperty.call(props, k)) { continue; }
|
|
472
|
+
opts[k] = props[k];
|
|
473
|
+
}
|
|
474
|
+
if (opts.rect == null && opts.left == null) {
|
|
475
|
+
var ww = (opts.w != null) ? opts.w : 20;
|
|
476
|
+
var wh = (opts.h != null) ? opts.h : 200;
|
|
477
|
+
opts.left = Math.round(ctx.x - ww / 2);
|
|
478
|
+
opts.top = Math.round(ctx.y - wh / 2);
|
|
479
|
+
opts.w = ww;
|
|
480
|
+
opts.h = wh;
|
|
481
|
+
}
|
|
482
|
+
var gdw = new GDW(ctx.runner.universe, opts);
|
|
483
|
+
gdw._schemaKind = Class.name;
|
|
484
|
+
return gdw;
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
// ── Export ───────────────────────────────────────────────────────────────
|
|
489
|
+
export { GraphingDetectorWall };
|
|
490
|
+
|