@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.
Files changed (217) hide show
  1. package/CLAUDE.md +87 -0
  2. package/README.md +84 -2
  3. package/dist/dist-manifest.json +218 -0
  4. package/dist/eyes/index.js +109 -0
  5. package/dist/fat/pkg-node/egpt_wasm.js +1277 -0
  6. package/dist/fat/pkg-node/egpt_wasm_bg.wasm +0 -0
  7. package/dist/fat/pkg-node/package.json +12 -0
  8. package/dist/frqtl/assets/vendor/chroma.mjs +64 -0
  9. package/dist/frqtl/compat/boot-node.js +34 -0
  10. package/dist/frqtl/compat/engine.js +127 -0
  11. package/dist/frqtl/compat/package-root.js +24 -0
  12. package/dist/frqtl/compat/providers/wasm-engine.js +2501 -0
  13. package/dist/frqtl/compat/providers/wasm-load-browser.js +134 -0
  14. package/dist/frqtl/compat/providers/wasm-load-node.js +59 -0
  15. package/dist/frqtl/compat/renderer/FieldBuilder.js +145 -0
  16. package/dist/frqtl/compat/renderer/Filters.js +163 -0
  17. package/dist/frqtl/compat/renderer/RenderFamily.js +85 -0
  18. package/dist/frqtl/compat/renderer/RenderMesh.js +100 -0
  19. package/dist/frqtl/compat/renderer/RenderShape.js +154 -0
  20. package/dist/frqtl/compat/renderer/camera2d-geometry.js +183 -0
  21. package/dist/frqtl/compat/renderer/captureStill-common.js +259 -0
  22. package/dist/frqtl/compat/renderer/display-slot.js +96 -0
  23. package/dist/frqtl/compat/renderer/lo-drawn-center.js +231 -0
  24. package/dist/frqtl/compat/renderer/orbit-bag.js +216 -0
  25. package/dist/frqtl/compat/renderer/providers/godrays-layer.js +213 -0
  26. package/dist/frqtl/compat/renderer/providers/index.js +22 -0
  27. package/dist/frqtl/compat/renderer/providers/p5-provider.js +861 -0
  28. package/dist/frqtl/compat/renderer/providers/three-camera-contract.js +160 -0
  29. package/dist/frqtl/compat/renderer/providers/three-camera-geometry.js +571 -0
  30. package/dist/frqtl/compat/renderer/providers/three-provider.js +2474 -0
  31. package/dist/frqtl/compat/renderer/renderCapabilities-common.js +186 -0
  32. package/dist/frqtl/compat/renderer/renderer-provider-contract.js +238 -0
  33. package/dist/frqtl/compat/renderer/unprojectStill.js +172 -0
  34. package/dist/frqtl/compat/vendor.js +196 -0
  35. package/dist/frqtl/node-entry.js +17 -0
  36. package/dist/frqtl/objects/FrqtlApp.js +2922 -0
  37. package/dist/frqtl/objects/ObjectFactory.js +297 -0
  38. package/dist/frqtl/objects/RecipeContext.js +322 -0
  39. package/dist/frqtl/objects/collectors/DataCollector.js +116 -0
  40. package/dist/frqtl/objects/constants/WallBehaviors.js +178 -0
  41. package/dist/frqtl/objects/geometry/SlabFace.js +258 -0
  42. package/dist/frqtl/objects/instruments/capture-host.js +453 -0
  43. package/dist/frqtl/objects/instruments/capture.js +711 -0
  44. package/dist/frqtl/objects/instruments/hosted-eyes.js +985 -0
  45. package/dist/frqtl/objects/kinds-entry.js +14 -0
  46. package/dist/frqtl/objects/kinds.js +190 -0
  47. package/dist/frqtl/objects/objects/Box.js +273 -0
  48. package/dist/frqtl/objects/objects/Bullet.js +155 -0
  49. package/dist/frqtl/objects/objects/CircuitGate.js +380 -0
  50. package/dist/frqtl/objects/objects/CircuitProbe.js +214 -0
  51. package/dist/frqtl/objects/objects/DirectedTunnel.js +412 -0
  52. package/dist/frqtl/objects/objects/EgptFractal.js +164 -0
  53. package/dist/frqtl/objects/objects/LightSource.js +354 -0
  54. package/dist/frqtl/objects/objects/QuantumEmitter.js +760 -0
  55. package/dist/frqtl/objects/objects/Tube.js +471 -0
  56. package/dist/frqtl/objects/objects/absorbRadiation.js +310 -0
  57. package/dist/frqtl/objects/objects/actionCompiler.js +1747 -0
  58. package/dist/frqtl/objects/objects/controlAction.js +89 -0
  59. package/dist/frqtl/objects/objects/controlBind.js +278 -0
  60. package/dist/frqtl/objects/objects/directedRedirectProgram.js +120 -0
  61. package/dist/frqtl/objects/objects/emitterProgram.js +209 -0
  62. package/dist/frqtl/objects/objects/frqtl-control-slider.js +197 -0
  63. package/dist/frqtl/objects/objects/frqtl-control-sync.js +272 -0
  64. package/dist/frqtl/objects/objects/frqtl-control-widgets.js +219 -0
  65. package/dist/frqtl/objects/objects/frqtl-control-write-queue.js +174 -0
  66. package/dist/frqtl/objects/objects/makeControlsForObject.js +824 -0
  67. package/dist/frqtl/objects/objects/makeControlsForRenderSurface.js +161 -0
  68. package/dist/frqtl/objects/objects/slitLinerScatter.js +263 -0
  69. package/dist/frqtl/objects/objects/sustainedSource.js +299 -0
  70. package/dist/frqtl/objects/objects/tagger.js +228 -0
  71. package/dist/frqtl/objects/schema/engine-scopes.js +469 -0
  72. package/dist/frqtl/objects/schema/index.js +256 -0
  73. package/dist/frqtl/objects/schema/scopes.js +457 -0
  74. package/dist/frqtl/objects/schema/validate-descriptor.js +49 -0
  75. package/dist/frqtl/objects/schema/valueNormalizer.js +366 -0
  76. package/dist/frqtl/objects/setup/_collisionMatrixLanes.js +66 -0
  77. package/dist/frqtl/objects/setup/_setupOptions.js +118 -0
  78. package/dist/frqtl/objects/setup/_worldInit.js +162 -0
  79. package/dist/frqtl/objects/setup/setupBlackbodySubstrate.js +242 -0
  80. package/dist/frqtl/objects/setup/setupCircuitBasic.js +447 -0
  81. package/dist/frqtl/objects/setup/setupCollisionMatrix.js +188 -0
  82. package/dist/frqtl/objects/setup/setupConeEmitter3D.js +251 -0
  83. package/dist/frqtl/objects/setup/setupDoubleSlit.js +327 -0
  84. package/dist/frqtl/objects/setup/setupDoubleSlit_Laser.js +229 -0
  85. package/dist/frqtl/objects/setup/setupEmpty.js +59 -0
  86. package/dist/frqtl/objects/setup/setupFxSmoke.js +209 -0
  87. package/dist/frqtl/objects/setup/setupG1Race.js +80 -0
  88. package/dist/frqtl/objects/setup/setupG2Sweep.js +89 -0
  89. package/dist/frqtl/objects/setup/setupG3Collide.js +177 -0
  90. package/dist/frqtl/objects/setup/setupLightSourceLab.js +280 -0
  91. package/dist/frqtl/objects/setup/setupOscillator.js +68 -0
  92. package/dist/frqtl/objects/setup/setupParticleWalk.js +93 -0
  93. package/dist/frqtl/objects/setup/setupParticleWalk3D.js +133 -0
  94. package/dist/frqtl/objects/setup/setupSphereEmitter3D.js +1259 -0
  95. package/dist/frqtl/objects/setup/setupTraceCircuit.js +239 -0
  96. package/dist/frqtl/objects/setup/setupTunnelLab.js +242 -0
  97. package/dist/frqtl/objects/setup/setupTunnelPointExit.js +306 -0
  98. package/dist/frqtl/objects/setup/setupWaveInterference.js +178 -0
  99. package/dist/frqtl/objects/v1-replacements.js +30 -0
  100. package/dist/frqtl/package-core.js +23 -0
  101. package/dist/frqtl/parts/CameraControls.js +643 -0
  102. package/dist/frqtl/parts/CameraPath.js +575 -0
  103. package/dist/frqtl/parts/CollisionActions.js +35 -0
  104. package/dist/frqtl/parts/CollisionTypes.js +31 -0
  105. package/dist/frqtl/parts/DetectorWall.js +496 -0
  106. package/dist/frqtl/parts/FrqtlControlOverlay.js +227 -0
  107. package/dist/frqtl/parts/FrqtlControlsPanel.js +415 -0
  108. package/dist/frqtl/parts/FrqtlVision.js +818 -0
  109. package/dist/frqtl/parts/GraphingDetectorWall.js +490 -0
  110. package/dist/frqtl/parts/LargeObject.js +406 -0
  111. package/dist/frqtl/parts/LightColor.js +255 -0
  112. package/dist/frqtl/parts/LiveChart.js +285 -0
  113. package/dist/frqtl/parts/LogicGate.js +720 -0
  114. package/dist/frqtl/parts/OvenBox.js +612 -0
  115. package/dist/frqtl/parts/PointSource.js +763 -0
  116. package/dist/frqtl/parts/Rectangle.js +200 -0
  117. package/dist/frqtl/parts/SlitLightSource.js +234 -0
  118. package/dist/frqtl/parts/SlitScreen.js +628 -0
  119. package/dist/frqtl/parts/SphereEmitter.js +946 -0
  120. package/dist/frqtl/parts/Trace.js +508 -0
  121. package/dist/frqtl/parts/Tunnel.js +1057 -0
  122. package/dist/frqtl/parts/Wall.js +1211 -0
  123. package/dist/frqtl/parts/index.js +33 -0
  124. package/dist/frqtl/scenes/absorbing-wall-3d.js +33 -0
  125. package/dist/frqtl/scenes/absorbing-wall.js +32 -0
  126. package/dist/frqtl/scenes/atomic-model.js +88 -0
  127. package/dist/frqtl/scenes/big-bang.js +46 -0
  128. package/dist/frqtl/scenes/blackbody.js +81 -0
  129. package/dist/frqtl/scenes/circuit-sat.js +127 -0
  130. package/dist/frqtl/scenes/collision-matrix-3d.js +107 -0
  131. package/dist/frqtl/scenes/counting-wall-3d.js +36 -0
  132. package/dist/frqtl/scenes/counting-wall.js +38 -0
  133. package/dist/frqtl/scenes/double-slit.js +31 -0
  134. package/dist/frqtl/scenes/dse-one-emitter.js +196 -0
  135. package/dist/frqtl/scenes/dse-one-slit.js +114 -0
  136. package/dist/frqtl/scenes/dse-photoelectric.js +70 -0
  137. package/dist/frqtl/scenes/dse-tunnel-rebuild.js +244 -0
  138. package/dist/frqtl/scenes/dse-two-emitters.js +196 -0
  139. package/dist/frqtl/scenes/dse-two-slits.js +132 -0
  140. package/dist/frqtl/scenes/empty.js +14 -0
  141. package/dist/frqtl/scenes/laser-buildup.js +49 -0
  142. package/dist/frqtl/scenes/mirror-3d.js +56 -0
  143. package/dist/frqtl/scenes/mirror-route.js +140 -0
  144. package/dist/frqtl/scenes/mirror.js +38 -0
  145. package/dist/frqtl/scenes/omni-3d.js +27 -0
  146. package/dist/frqtl/scenes/omni-source.js +22 -0
  147. package/dist/frqtl/scenes/self-interference.js +39 -0
  148. package/dist/frqtl/scenes/smoking-gun.js +122 -0
  149. package/dist/frqtl/scenes/straight-walk.js +22 -0
  150. package/dist/frqtl/scenes/sun.js +39 -0
  151. package/dist/frqtl/scenes/two-source.js +37 -0
  152. package/dist/frqtl/scenes/wave.js +26 -0
  153. package/dist/shim/bundle/frqnb.js +112 -0
  154. package/dist/shim/bundle/index.js +121 -0
  155. package/dist/shim/bundle/indexHtml.js +107 -0
  156. package/dist/shim/bundle/manifest.js +95 -0
  157. package/dist/shim/bundle/zip.js +243 -0
  158. package/dist/shim/bus.js +67 -0
  159. package/dist/shim/controls/index.js +627 -0
  160. package/dist/shim/controls/registry.js +274 -0
  161. package/dist/shim/engine/artifact-shell.js +212 -0
  162. package/dist/shim/engine/boot-phases.js +44 -0
  163. package/dist/shim/engine/engine-stack-boot.js +59 -0
  164. package/dist/shim/engine/router.js +692 -0
  165. package/dist/shim/experiment-frame/index.js +296 -0
  166. package/dist/shim/expr/ast-schema.js +369 -0
  167. package/dist/shim/field-descriptor/index.js +326 -0
  168. package/dist/shim/frame-data/index.js +224 -0
  169. package/dist/shim/index.js +67 -0
  170. package/dist/shim/notebook/canvas-guard.js +82 -0
  171. package/dist/shim/notebook/catalog.js +35 -0
  172. package/dist/shim/notebook/format.js +736 -0
  173. package/dist/shim/notebook/frqtl-setups-importmap.js +81 -0
  174. package/dist/shim/notebook/kernel.js +672 -0
  175. package/dist/shim/notebook/knob.js +151 -0
  176. package/dist/shim/notebook/module-resolver.js +260 -0
  177. package/dist/shim/notebook/notebook-knobs.js +65 -0
  178. package/dist/shim/notebook/produced-bindings.js +67 -0
  179. package/dist/shim/notebook/realm-registry.js +121 -0
  180. package/dist/shim/notebook/realm-transport.js +73 -0
  181. package/dist/shim/notebook/setup-builder.js +510 -0
  182. package/dist/shim/notebook/store.js +159 -0
  183. package/dist/shim/notebook/vendor-importmap.js +134 -0
  184. package/dist/shim/notebook/view-builder.js +189 -0
  185. package/dist/shim/notebook/view-ctx.js +100 -0
  186. package/dist/shim/notebook/view-handle.js +202 -0
  187. package/dist/shim/notebook/warmup.js +102 -0
  188. package/dist/shim/package.json +4 -0
  189. package/dist/shim/page-params/index.js +214 -0
  190. package/dist/shim/project/identity.js +112 -0
  191. package/dist/shim/project/index.js +19 -0
  192. package/dist/shim/project/link.js +402 -0
  193. package/dist/shim/project/node-specifier-hook.mjs +35 -0
  194. package/dist/shim/project/register-node.mjs +17 -0
  195. package/dist/shim/project/specifiers.js +106 -0
  196. package/dist/shim/registry/copy-gate.js +110 -0
  197. package/dist/shim/registry/exhibits.js +455 -0
  198. package/dist/shim/registry/experiments.js +215 -0
  199. package/dist/shim/registry/index.js +19 -0
  200. package/dist/shim/registry/scene-source.js +157 -0
  201. package/dist/shim/registry/walkthrough.js +807 -0
  202. package/dist/shim/render/capture-envelope.js +279 -0
  203. package/dist/shim/render/heat-colormap.js +92 -0
  204. package/dist/shim/render/render-stages.js +855 -0
  205. package/dist/shim/render/sun-look.js +33 -0
  206. package/dist/shim/render-state/apply.js +262 -0
  207. package/dist/shim/render-state/index.js +74 -0
  208. package/dist/shim/rpc/index.js +207 -0
  209. package/dist/shim/runtime/index.js +1023 -0
  210. package/dist/shim/storage/boot.js +69 -0
  211. package/dist/shim/storage/index.js +169 -0
  212. package/dist/shim/storage/local-storage-provider.js +230 -0
  213. package/dist/shim/storage/make-namespace-store.js +132 -0
  214. package/dist/shim/storage/modules.js +249 -0
  215. package/dist/shim/storage/projects.js +150 -0
  216. package/llms.txt +12 -0
  217. package/package.json +62 -4
@@ -0,0 +1,239 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * setupTraceCircuit — the smallest complete circuit on this floor: SOURCE → TRACE → GROUND.
5
+ *
6
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
7
+ *
8
+ * Composes THREE already-committed pieces and adds no physics of its own (SU1):
9
+ * 1. a `SphereEmitter` on the `'laser'` preset — the VOLTAGE source, parked just
10
+ * inside the Trace's inlet and aimed down the first leg, so the whole burst is
11
+ * fed into the bore instead of sprayed at the inlet walls;
12
+ * 2. a `Trace` — the WIRE (mirror `Tunnel` runs + directed `Tunnel` corners);
13
+ * 3. that Trace's own `ground:true` terminal — the SINK, which also tallies delivery.
14
+ *
15
+ * @param {Object} universe - a constructed, NOT-yet-init'd EGPTUniverse. This factory
16
+ * IS the sketch: it inits the world FIRST and then constructs parts (world.init()
17
+ * FIRST → CONSTRUCT parts → WIRE → RUN), so part construction order is irrelevant.
18
+ * Runs the FAST PATH
19
+ * (`withInterQuantumCollisions:false`): that is the lossless-waveguide regime the
20
+ * Trace datasheet is quoted in. Turning inter-quantum collisions ON is RESISTANCE and
21
+ * makes this a different, current-limited component.
22
+ * @param {Object} [options]
23
+ * @param {Array<Array<number>>} [options.path] - the Trace route (cardinal waypoints).
24
+ * Default: a straight 400px run across the middle of the canvas.
25
+ * @param {number} [options.bore=12] @param {number} [options.wallThickness=6]
26
+ * @param {number} [options.voltage=6] - emitter burst per tick. LIVE. Measured Ohmic:
27
+ * delivered current is a straight multiple of this with no knee (see Layer 2).
28
+ * @param {boolean} [options.active=true]
29
+ * @param {number} [options.lanes=1] - build N PARALLEL Traces at the published pitch,
30
+ * sharing rails, with only lane 0 energised. This is the miniaturization demo: the
31
+ * dark lanes are the crosstalk measurement.
32
+ *
33
+ * @returns {{ trace, traces, emitterControl, traceControl, largeObjects, onTick, dispose }}
34
+ * `traceControl` is the live sugar handle (the canonical knob pattern):
35
+ * `setVoltage/getVoltage`, `setActive`, `getDelivered`, `getCurrent`,
36
+ * `getConductance`, `getCrosstalk`, `controls`.
37
+ *
38
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
39
+ * Zero new physics. What the two knobs do to the running scene:
40
+ *
41
+ * `voltage` (emitter burst/tick) — the current is LINEAR in it with no ceiling in
42
+ * this regime: the Trace is a lossless waveguide, so whatever the source pushes in
43
+ * arrives. Conductance (delivered/tick ÷ emitted/tick) measured 1.00 ± 0.02 on a
44
+ * straight run against 0.29 for the same source and ground with the Trace REMOVED.
45
+ *
46
+ * `lanes` — stacks independent parallel Traces wall-against-wall at
47
+ * `Trace.lanePitch(bore, wallThickness)`. Only lane 0 is fed; the readout on the dark
48
+ * lanes IS the crosstalk measurement — 0.98 % into the immediately adjacent lane at
49
+ * bore 3 / wall 3 (9px pitch), and EXACTLY zero into every lane beyond it. This is the
50
+ * picture that shows the part shrinks: four 3px-bore lanes inside a 36px band with the
51
+ * energised lane still at G = 0.98.
52
+ *
53
+ * The full datasheet — bore, length, wall-thickness floor, corner throughput — lives in
54
+ * `Trace.js`'s Layer 2. It is quoted there once and not restated here.
55
+ */
56
+
57
+ import { assertSetupOptions as _assertSetupOptions, requireWorldRect as _requireWorldRect } from './_setupOptions.js';
58
+ import { Trace } from '../../parts/Trace.js';
59
+ import { setupSphereEmitter3D } from './setupSphereEmitter3D.js';
60
+
61
+ /** Degrees → the emitter yaw that aims a burst down the Trace's first leg. */
62
+ function _aimDeg(path) {
63
+ var dx = path[1][0] - path[0][0], dy = path[1][1] - path[0][1];
64
+ if (dx > 0) { return 0; }
65
+ if (dx < 0) { return 180; }
66
+ return dy > 0 ? 90 : 270;
67
+ }
68
+ /**
69
+ * OPTIONS — this factory's own option vocabulary: EXACTLY the keys it reads.
70
+ * Exported as `setupTraceCircuit.OPTIONS` so a composing caller consumes it instead of
71
+ * retyping it. `setup-factory-option-vocabulary.test.mjs` re-derives the
72
+ * read-set by static trace and fails if this list drifts from the code.
73
+ */
74
+ var OPTIONS = [
75
+ 'active', 'bore', 'lanes', 'path',
76
+ 'runner', 'voltage', 'wallThickness',
77
+ ];
78
+
79
+ function setupTraceCircuit(universe, options) {
80
+ options = _assertSetupOptions('setupTraceCircuit', options, OPTIONS);
81
+ var TraceCls = Trace;
82
+ var emitterFactory = setupSphereEmitter3D;
83
+
84
+ // The world rect the co-init call wrote (EGPTUniverse.create({rect})) — the ONE size.
85
+ var world = _requireWorldRect('setupTraceCircuit universe.universe_rect', universe.universe_rect);
86
+ var W = world.w, H = world.h;
87
+ var bore = options.bore !== undefined ? options.bore : 12;
88
+ var wallThickness = options.wallThickness !== undefined ? options.wallThickness : 6;
89
+ var voltage = options.voltage !== undefined ? options.voltage : 6;
90
+ var lanes = Math.max(1, options.lanes !== undefined ? options.lanes : 1);
91
+ var active = options.active !== undefined ? !!options.active : true;
92
+
93
+ // Independent Traces stack wall-against-wall. NOT bore+wallThickness: that would
94
+ // put each pair of neighbours' mirrors on the SAME pixels, which measured 31 %
95
+ // crosstalk while the energised lane still read G = 0.98 (a silent failure).
96
+ var pitch = TraceCls.lanePitch(bore, wallThickness);
97
+ var midY = Math.round(H / 2) - Math.round((lanes - 1) * pitch / 2);
98
+ var basePath = options.path || [[40, midY], [Math.max(140, W - 60), midY]];
99
+
100
+ // ══ STEP 1 — THE WORLD ══════════════════════════════════════════════
101
+ // This factory IS the sketch: it inits the world FIRST, before any part.
102
+ // Everything below is part construction, and construction ORDER among parts
103
+ // no longer matters (the old "source first, walls second" idiom existed only
104
+ // because the emitter factory owned init; it does not any more).
105
+ var aim = _aimDeg(basePath);
106
+ var inPort = basePath[0];
107
+ var inset = 4;
108
+ var emitterOpts = {
109
+ preset: 'laser', is2D: true, R: 24,
110
+ center: [
111
+ inPort[0] + (aim === 0 ? inset : aim === 180 ? -inset : 0),
112
+ inPort[1] + (aim === 90 ? inset : aim === 270 ? -inset : 0)
113
+ ],
114
+ orientationDeg: aim,
115
+ emissionBoxSide: 0, fundamentalDimension: 0,
116
+ burstSize: voltage, emitEveryTicks: 1,
117
+ chargeMode: 'random'
118
+ };
119
+ emitterFactory.initWorld(universe, emitterOpts);
120
+
121
+ // ══ STEP 2 — THE PARTS ══════════════════════════════════════════════
122
+ var emitterResult = emitterFactory(universe, emitterOpts);
123
+ var emitterControl = emitterResult.sphereEmitterControl;
124
+ emitterControl.setActive(active);
125
+
126
+ // Lane 0 carries the authored route; extra lanes are that route offset by the
127
+ // pitch, so exactly one wall of separation sits between neighbours (the measured
128
+ // stacking geometry).
129
+ var traces = [];
130
+ for (var i = 0; i < lanes; i++) {
131
+ var lanePath = basePath.map(function (p) { return [p[0], p[1] + i * pitch]; });
132
+ traces.push(new TraceCls(universe, {
133
+ path: lanePath, bore: bore, wallThickness: wallThickness,
134
+ ground: true, name: 'trace' + i,
135
+ color: i === 0 ? [90, 120, 200] : [70, 80, 110]
136
+ }));
137
+ }
138
+ var trace = traces[0];
139
+
140
+ var control = {
141
+ setVoltage: function (v) {
142
+ voltage = Math.max(0, Math.round(v));
143
+ emitterControl.setBurstSize(voltage);
144
+ return voltage;
145
+ },
146
+ getVoltage: function () { return voltage; },
147
+ setActive: function (v) { emitterControl.setActive(!!v); },
148
+ getDelivered: function () { return trace.getDelivered(); },
149
+ getCurrent: function () { return trace.getCurrent(); },
150
+ /** delivered/tick ÷ emitted/tick — 1.0 is a perfect conductor. */
151
+ getConductance: function () { return voltage ? trace.getCurrent() / voltage : 0; },
152
+ /** Fraction of lane 0's current appearing in the dark lanes (0 with lanes=1). */
153
+ getCrosstalk: function () {
154
+ if (traces.length < 2) { return 0; }
155
+ var lead = trace.getCurrent();
156
+ if (!lead) { return 0; }
157
+ var dark = 0;
158
+ for (var k = 1; k < traces.length; k++) { dark += traces[k].getCurrent(); }
159
+ return dark / lead;
160
+ },
161
+ getPitch: function () { return pitch; },
162
+ getLanes: function () { return traces.length; }
163
+ };
164
+
165
+ // The standardized descriptor array — declared ONCE, consumed by both the page's
166
+ // own sliders and any host control strip (the established `controls` idiom).
167
+ control.controls = [
168
+ {
169
+ id: 'voltage', label: 'voltage (burst/tick)',
170
+ min: 0, max: 32, step: 1, value: voltage,
171
+ fmt: function (v) { return String(v) + ' q/tick'; },
172
+ get: function () { return control.getVoltage(); },
173
+ onChange: function (v) { control.setVoltage(v); }
174
+ },
175
+ {
176
+ // REBUILD-class: bore is wall geometry, so the page re-runs setup().
177
+ id: 'bore', label: 'bore (rebuild)',
178
+ min: 1, max: 40, step: 1, value: bore, rebuild: true,
179
+ fmt: function (v) { return String(v) + 'px'; },
180
+ get: function () { return bore; },
181
+ onChange: function () { /* the page owns the rebuild — see `rebuild:true` */ }
182
+ },
183
+ {
184
+ id: 'lanes', label: 'parallel lanes (rebuild)',
185
+ min: 1, max: 8, step: 1, value: lanes, rebuild: true,
186
+ fmt: function (v) { return String(v) + ' @ ' + pitch + 'px pitch'; },
187
+ get: function () { return control.getLanes(); },
188
+ onChange: function () { /* rebuild */ }
189
+ }
190
+ ];
191
+
192
+ var result = {
193
+ trace: trace,
194
+ traces: traces,
195
+ emitterControl: emitterControl,
196
+ traceControl: control,
197
+ // FrqtlApp.setup() auto-wires this for rendering.
198
+ largeObjects: traces.reduce(function (acc, t) { return acc.concat(t.largeObjects); }, []),
199
+ /**
200
+ * The ONE per-tick call — drives every lane's junctions + ground sampling.
201
+ * `attachToRunner` below wires it for you; this stays public for a harness
202
+ * that ticks the universe directly with no runner.
203
+ */
204
+ onTick: function (tickData) {
205
+ for (var k = 0; k < traces.length; k++) { traces[k].onTick(tickData); }
206
+ },
207
+ /**
208
+ * Subscribe every lane to the runner's per-tick fan-out. REQUIRED: nothing in
209
+ * `FrqtlApp` calls a setupResult's `onTick`, so without this the corner
210
+ * junctions never redirect and `getDelivered()`/`getCurrent()` read 0 forever —
211
+ * which looks exactly like "the wire doesn't conduct" (QA hit precisely that).
212
+ * Uses `addTickListener`, the multi-subscriber peer of the page's single `onTick`
213
+ * slot, so it never steals the page's own hook. Idempotent per lane.
214
+ * @param {Object} runner - an FrqtlApp.
215
+ */
216
+ attachToRunner: function (runner) {
217
+ for (var k = 0; k < traces.length; k++) { traces[k].attachToRunner(runner); }
218
+ return result;
219
+ },
220
+ // CANONICAL TEARDOWN — the runner calls this before installing the next
221
+ // setupResult, so a rebuild never stacks emitters or walls. Each Trace's own
222
+ // dispose drops its tick subscription too.
223
+ dispose: function () {
224
+ if (emitterResult && typeof emitterResult.dispose === 'function') { emitterResult.dispose(); }
225
+ traces.forEach(function (t) { t.dispose(); });
226
+ }
227
+ };
228
+
229
+ // Auto-wire when the caller hands us the runner (the page idiom below). Optional:
230
+ // a caller without a runner calls `result.attachToRunner(runner)` after setup, or
231
+ // drives `result.onTick(tickData)` itself.
232
+ if (options.runner) { result.attachToRunner(options.runner); }
233
+ return result;
234
+ }
235
+
236
+ setupTraceCircuit.OPTIONS = OPTIONS;
237
+
238
+ export { setupTraceCircuit };
239
+
@@ -0,0 +1,242 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * setupTunnelLab — DirectedTunnel ISOLATION (I1, the directed-tunnel composition model
5
+ * §3): ONE `DirectedTunnel`, gun-chamber mechanics (fill / escape / dispersion) at a
6
+ * live-tunable scale — the visual half of the scale-sweep measurement
7
+ * (`docs/internal/physics-lab/directed-tunnel-scale-sweep/`), same methodology, same source shape.
8
+ *
9
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
10
+ *
11
+ * @param {Object} universe - WASM universe (not yet init'd — this factory calls init()).
12
+ * NOTE ON DETERMINISTIC SEEDING: same convention as every other setup factory
13
+ * (`setupGunChamber.js`) — `universe` arrives ALREADY
14
+ * CONSTRUCTED with its seed fixed by `FrqtlApp`; there is no `opts.seed` here.
15
+ * @param {Object} [opts]
16
+ * @param {number} [opts.bore=40] - Tunnel bore (interior height/cross-section), pixels.
17
+ * REINIT-CLASS (changing it needs a fresh `runner.setup()` — matches the sweep's own
18
+ * per-cell tunnel construction; the tunnel geometry is not a live-resizable rect).
19
+ * @param {number} [opts.length=240] - Tunnel length (interior width), pixels. REINIT-CLASS.
20
+ * @param {number} [opts.wavelength=64] - Per-frame wavelength override for EVERY hand-
21
+ * placed source quantum (the SAME `makeLeafFrame` per-frame override the sweep uses —
22
+ * `QuantumEmitter` has no wavelength field, which is exactly why this factory hand-
23
+ * places quanta via a local host tick-fn instead of `DirectedTunnel`'s `options.emitter`
24
+ * — see the Layer 2 note below). LIVE (`setupResult.sourceState.wavelength`).
25
+ * **CEILING (CEO drive #2, 2026-07-14 — "the wavelength slider has no effect"
26
+ * root cause):** Engine Invariant #6 is `mass = max(1, capacity/wavelength)` —
27
+ * at this factory's `FUNDAMENTAL_DIMENSION=2`, `capacity = (2^2)^2 = 16`
28
+ * (`setupTunnelLab.CAPACITY`, exported below), so ANY `wavelength >= capacity`
29
+ * (16) saturates `mass` at its floor (1) and therefore ALSO saturates the
30
+ * DERIVED wavelength (`capacity/mass`) at exactly `capacity` (16) — `wavelength:
31
+ * 64`, `128`, or `1000000` are ALL physically IDENTICAL to `wavelength: 16`
32
+ * (same mass, same transverse-oscillation period). This was NOT a collision-
33
+ * reset bug (verified: `mass` survives every wall behavior — option1/option2/
34
+ * composable — unchanged across ticks, pkg-node probe) — it is a genuine
35
+ * ceiling in the formula. Only `wavelength` values in `(0, capacity]` (i.e.
36
+ * `(0, 16]` here) produce DISTINCT mass/oscillation-period values; `tunnel-lab
37
+ * /index.html`'s own wavelength slider is clamped to `setupTunnelLab.
38
+ * MAX_USEFUL_WAVELENGTH` (= `CAPACITY`) for exactly this reason — one
39
+ * authority, no hand-typed "16" duplicate in the page.
40
+ * @param {string} [opts.wallBehavior='option2'] - Initial `DirectedTunnel.behavior`
41
+ * (`DirectedTunnel.BEHAVIORS` — option1 | option2 | any `SlitLinerScatter` composable
42
+ * name). LIVE — `setupResult.tunnel.updateBehavior(newBehavior)`.
43
+ * @param {number} [opts.sourceDensity=5] - Quanta hand-placed per burst. LIVE
44
+ * (`setupResult.sourceState.density`). NOTE: this is the FACTORY's own
45
+ * omitted-opt default (unchanged, for any headless caller) — `tunnel-lab
46
+ * /index.html`'s own UI default is 10 (CEO drive #2, 2026-07-14
47
+ * population-density fix: the page ALWAYS passes an explicit value read
48
+ * from its own `<input>` default, so this factory default is never
49
+ * actually reached by the page — see that file's own docstring for the
50
+ * pkg-node-measured TPS/alive-count calibration against `setupGunChamber
51
+ * .js`'s own 20/tick emission rate).
52
+ * @param {number} [opts.sourceRate=10] - Ticks between bursts (an `everyNTicks`-style
53
+ * cadence, mirroring `emitterProgram.js`'s own live cadence knob). LIVE
54
+ * (`setupResult.sourceState.rate`). NOTE: same as `sourceDensity` above —
55
+ * the factory default (unchanged) vs. the page's own UI default (1) diverge
56
+ * for the same reason.
57
+ * @param {boolean} [opts.sourceActive=true] - Master on/off for the burst source. LIVE
58
+ * (`setupResult.sourceState.active`).
59
+ * @param {number} [opts.depth] - CEO drive #2 (2026-07-14) 3D toggle. OMITTED
60
+ * (default) -> 2D (byte-identical to every pre-existing caller). A positive
61
+ * value -> real z-containment (`activeAxes:{x,y,z}` on `universe.init()`,
62
+ * `DirectedTunnel`'s own `depth`/`z=-depth/2`, hand-placed quanta z-centred)
63
+ * — the standard `options.depth` recipe. REINIT-CLASS
64
+ * (only meaningful at construction, like bore/length).
65
+ * @param {'spray'|'beam'} [opts.velocityProfile='spray'] - ws-dse-3d AC3 (CEO
66
+ * drive #2 round-2 ruling, "tunnel-lab gains the source-profile selection off
67
+ * the shared surface"): the SAME `SustainedSource.SOURCE_SPEC_FIELDS.
68
+ * velocityProfile` axis `LightSource`'s own `particleWalk` burst always uses
69
+ * (`'beam'`, hardcoded there) — exposed here so tunnel-lab can be set to
70
+ * `'beam'` and produce a directly comparable collimated source to
71
+ * lightsource-lab's default particleWalk mode at matched bore/length/
72
+ * wavelength/wallBehavior ("same settings -> same visuals"). Default `'spray'`
73
+ * — byte-identical to every pre-existing caller (this factory's own original
74
+ * hardcoded biased-fan shape, unchanged). LIVE (`setupResult.sourceState.
75
+ * velocityProfile` — `placeSourceQuanta` reads it fresh every burst, no reinit).
76
+ *
77
+ * @returns {{
78
+ * tunnel: DirectedTunnel, sourceState: {density:number, rate:number, wavelength:number,
79
+ * active:boolean}, largeObjects: LargeObject[]
80
+ * }}
81
+ * `largeObjects` (= `tunnel.largeObjects`) is the canonical key `FrqtlApp.setup()`
82
+ * auto-wires for rendering (matches every other setup factory).
83
+ *
84
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
85
+ *
86
+ * **Zero new physics (canonical-bench SU1)** — this factory only WIRES existing
87
+ * primitives: `DirectedTunnel` (itself zero new physics over `Tube`), and a burst source
88
+ * built from the SAME `makeLeafFrame`+`injectFrame`+`universe.random()` primitives the
89
+ * scale-sweep script and every hand-placed-quantum experiment already use. The source is
90
+ * a LOCAL host tick-fn (NOT `QuantumEmitter`) because `QuantumEmitter`'s constructor has
91
+ * no wavelength field — the isolation lab needs the SAME per-frame wavelength override
92
+ * the scale sweep uses so the page visually demonstrates EXACTLY what the sweep
93
+ * measured, not a physically-different (unmodifiable-wavelength) source. Pushed onto
94
+ * `universe.experimentTickFunctions` (Phase A, matching `emitterProgram.js`'s own
95
+ * registration point) — legitimate here because, unlike `DirectedTunnel`'s `option1`
96
+ * redirect (which needs Phase-D `tickData`), injection is a pre-tick primitive with no
97
+ * such timing constraint.
98
+ *
99
+ * **Gun-chamber convention**: `capNegX:true, capPosX:false` — a CLOSED breech (source
100
+ * fires INTO the bore, quanta bounce/fill) and an OPEN muzzle (quanta escape, then
101
+ * disperse freely with no walls) — the CEO's own Gun-Chamber model ("particles ping-pong
102
+ * in a tube, escape the open end, spread comes from dispersion AFTER escape").
103
+ *
104
+ * **Fundamental dimension = 2** (matches `setupDoubleSlit.js`'s default AND the scale-
105
+ * sweep's own convention — see `docs/internal/physics-lab/directed-tunnel-scale-sweep/README.md`'s
106
+ * "level 0 pins wavelength to a no-op" note): a level-0 leaf has `capacity=1`, so
107
+ * `mass=max(1,capacity/wavelength)` is PINNED at 1 for any `wavelength>=2` — the
108
+ * wavelength slider would be a silent no-op at level 0.
109
+ *
110
+ * **Biased entrance (bunched near the TOP wall)** — the SAME shape the scale sweep uses,
111
+ * because a symmetric/spread entrance is already balanced and shows no washout effect at
112
+ * all (the scale sweep's own finding); bunching is what makes
113
+ * "uniformizes vs transmits unmodified" visually OBSERVABLE as the population fills the
114
+ * bore over time.
115
+ */
116
+
117
+ import { assertSetupOptions as _assertSetupOptions, requireWorldRect as _requireWorldRect } from './_setupOptions.js';
118
+ import { DirectedTunnel } from '../objects/DirectedTunnel.js';
119
+ import * as SustainedSource from '../objects/sustainedSource.js';
120
+
121
+ // The ONE unknown-option-key validator (objects/setup/_setupOptions.js). Resolved
122
+ // LAZILY in the browser: the game layer defines window.assertSetupOptions before any
123
+ // page script runs, but reading it at eval time would bake in a load-order dependency.
124
+
125
+ // ── Imports ─────────────────────────────────────────────────────────────
126
+
127
+ var FUNDAMENTAL_DIMENSION = 2; // matches setupDoubleSlit.js + the scale-sweep script
128
+
129
+ // CEO drive #2 (2026-07-14) — the ONE authority for "what's the largest
130
+ // wavelength value that still produces a DISTINCT mass from any larger one"
131
+ // at this factory's FUNDAMENTAL_DIMENSION. Engine Invariant #6:
132
+ // `mass = max(1, capacity/wavelength)` — the floor (mass=1) is reached
133
+ // exactly at `wavelength === capacity`; every wavelength beyond that is
134
+ // indistinguishable. `tunnel-lab/index.html` reads this (never a hand-typed
135
+ // "16" literal) to clamp its wavelength slider's max to the achievable range.
136
+ // ws-dse-3d: derived from `SustainedSource.capacityAtDimension` — the SAME
137
+ // authority `setupLightSourceLab.js` now also derives its own
138
+ // MAX_USEFUL_WAVELENGTH from (AC4: "same MAX_USEFUL_WAVELENGTH clamp
139
+ // authority" for both labs).
140
+ var TUNNEL_LAB_CAPACITY = SustainedSource.capacityAtDimension(FUNDAMENTAL_DIMENSION); // 16 at FUNDAMENTAL_DIMENSION=2
141
+ /**
142
+ * OPTIONS — this factory's own option vocabulary: EXACTLY the keys it reads.
143
+ * Exported as `setupTunnelLab.OPTIONS` so a composing caller consumes it instead of
144
+ * retyping it. `setup-factory-option-vocabulary.test.mjs` re-derives the
145
+ * read-set by static trace and fails if this list drifts from the code.
146
+ */
147
+ var OPTIONS = [
148
+ 'bore', 'depth',
149
+ 'length', 'sourceActive', 'sourceDensity', 'sourceRate',
150
+ 'velocityProfile', 'wallBehavior', 'wavelength',
151
+ ];
152
+
153
+ function setupTunnelLab(universe, opts) {
154
+ opts = _assertSetupOptions('setupTunnelLab', opts, OPTIONS);
155
+ // The world rect the co-init call wrote (EGPTUniverse.create({rect})) — the ONE size.
156
+ var canvasRect = universe.universe_rect;
157
+ var world = _requireWorldRect('setupTunnelLab universe.universe_rect', canvasRect);
158
+ var cw = world.w, ch = world.h;
159
+
160
+ var bore = opts.bore !== undefined ? opts.bore : 40;
161
+ var length = opts.length !== undefined ? opts.length : 240;
162
+ var wavelength = opts.wavelength !== undefined ? opts.wavelength : 64;
163
+ var wallBehavior = opts.wallBehavior !== undefined ? opts.wallBehavior : 'option2';
164
+ var sourceDensity = opts.sourceDensity !== undefined ? opts.sourceDensity : 5;
165
+ var sourceRate = opts.sourceRate !== undefined ? opts.sourceRate : 10;
166
+ var sourceActive = opts.sourceActive !== undefined ? !!opts.sourceActive : true;
167
+ // CEO drive #2 item 1 (2026-07-14) — OPT-IN 3D, OMISSION-preserves-2D
168
+ // (the "before adding a creation-method parameter" discipline): mirrors
169
+ // the standard `options.depth` recipe — omitted/null ->
170
+ // 2D (zContain:false, byte-identical to every existing caller); a
171
+ // positive value -> real z-containment (Tube/DirectedTunnel heritage —
172
+ // `options.depth` already existed on DirectedTunnel, T1; this factory
173
+ // simply now forwards it + arms activeAxes).
174
+ var depth = (opts.depth !== undefined && opts.depth !== null) ? opts.depth : null;
175
+ // ws-dse-3d AC3 — default 'spray' is byte-identical to every pre-existing
176
+ // caller (this factory's own original hardcoded value below).
177
+ var velocityProfile = opts.velocityProfile !== undefined ? opts.velocityProfile : 'spray';
178
+
179
+ var initOpts = {};
180
+ if (depth !== null) { initOpts.activeAxes = { x: true, y: true, z: true }; }
181
+ universe.init(FUNDAMENTAL_DIMENSION, initOpts);
182
+
183
+ var left = Math.round(cw * 0.1);
184
+ var top = Math.round(ch / 2 - bore / 2);
185
+
186
+ var tunnelOpts = {
187
+ left: left, top: top, length: length, width: bore,
188
+ capNegX: true, capPosX: false, // closed breech, open muzzle — Gun-Chamber convention
189
+ behavior: wallBehavior,
190
+ };
191
+ if (depth !== null) { tunnelOpts.depth = depth; tunnelOpts.z = -depth / 2; }
192
+ var tunnel = new DirectedTunnel(universe, tunnelOpts);
193
+
194
+ // ws-dse-3d (CEO ruling 2026-07-14, "one source surface"): the recurring
195
+ // burst injector now routes through `sustainedSource.js`'s ONE
196
+ // `buildSustainedSource` authority — the SAME surface
197
+ // `setupLightSourceLab.js` (via `LightSource`'s own opt-in
198
+ // `options.sustain`) now also consumes. `velocityProfile:'spray'` +
199
+ // `entranceBias:'top'` reproduce this factory's PRE-EXISTING biased-
200
+ // random-fan shape EXACTLY (the "uniformization visible" demo shape —
201
+ // see the module docstring's "Biased entrance" note); `zIsotropy:true`
202
+ // (the default) is the 3D-SYMMETRIC EMISSION fix (ws-dse-3d, theorist §4
203
+ // option (a)) — when `depth` is armed, every burst now ALSO draws a `vz`
204
+ // symmetric to the `vy` spread, instead of pinning z-momentum at zero
205
+ // forever (the tester-measured z/y=0.23 flatness root cause).
206
+ var src = SustainedSource.buildSustainedSource(universe, tunnel.innerRect, true /* capNegX (Gun-Chamber closed breech) */, depth !== null, {
207
+ wavelength: wavelength,
208
+ velocityProfile: velocityProfile,
209
+ entranceBias: 'top',
210
+ zIsotropy: true,
211
+ density: sourceDensity,
212
+ rate: sourceRate,
213
+ active: sourceActive,
214
+ });
215
+ var sourceState = src.sourceState;
216
+
217
+ return {
218
+ tunnel: tunnel,
219
+ sourceState: sourceState,
220
+ // FrqtlApp.setup() (FrqtlApp.js:528) auto-wires
221
+ // the render list; today the world's own census carries them by construction.
222
+ largeObjects: tunnel.largeObjects,
223
+ // CANONICAL TEARDOWN (CEO drive #2, 2026-07-14 — the reinit-
224
+ // compounding fix): `FrqtlApp.prototype.setup()` calls THIS
225
+ // automatically before installing the NEXT setupResult. Delegates to
226
+ // `src.dispose()` (the ONE `sustainedSource.js` teardown mechanism)
227
+ // rather than re-deriving the tag-then-splice idiom a second time.
228
+ dispose: src.dispose,
229
+ };
230
+ }
231
+
232
+ // ONE authority for the achievable wavelength ceiling at this factory's
233
+ // FUNDAMENTAL_DIMENSION (see the TUNNEL_LAB_CAPACITY comment above) — the
234
+ // page reads this instead of hand-typing "16".
235
+ setupTunnelLab.FUNDAMENTAL_DIMENSION = FUNDAMENTAL_DIMENSION;
236
+ setupTunnelLab.CAPACITY = TUNNEL_LAB_CAPACITY;
237
+ setupTunnelLab.MAX_USEFUL_WAVELENGTH = TUNNEL_LAB_CAPACITY;
238
+
239
+ setupTunnelLab.OPTIONS = OPTIONS;
240
+
241
+ export { setupTunnelLab };
242
+