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