@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,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,240 @@
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, same methodology,
7
+ * 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 ("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 (the
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] - 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'] - tunnel-lab has the
66
+ * source-profile selection off the shared surface: the SAME `SustainedSource.SOURCE_SPEC_FIELDS.
67
+ * velocityProfile` axis `LightSource`'s own `particleWalk` burst always uses
68
+ * (`'beam'`, hardcoded there) — exposed here so tunnel-lab can be set to
69
+ * `'beam'` and produce a directly comparable collimated source to
70
+ * lightsource-lab's default particleWalk mode at matched bore/length/
71
+ * wavelength/wallBehavior ("same settings -> same visuals"). Default `'spray'`
72
+ * — byte-identical to every pre-existing caller (this factory's own original
73
+ * hardcoded biased-fan shape, unchanged). LIVE (`setupResult.sourceState.
74
+ * velocityProfile` — `placeSourceQuanta` reads it fresh every burst, no reinit).
75
+ *
76
+ * @returns {{
77
+ * tunnel: DirectedTunnel, sourceState: {density:number, rate:number, wavelength:number,
78
+ * active:boolean}, largeObjects: LargeObject[]
79
+ * }}
80
+ * `largeObjects` (= `tunnel.largeObjects`) is the canonical key `FrqtlApp.setup()`
81
+ * auto-wires for rendering (matches every other setup factory).
82
+ *
83
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
84
+ *
85
+ * **Zero new physics (canonical-bench SU1)** — this factory only WIRES existing
86
+ * primitives: `DirectedTunnel` (itself zero new physics over `Tube`), and a burst source
87
+ * built from the SAME `makeLeafFrame`+`injectFrame`+`universe.random()` primitives the
88
+ * scale-sweep script and every hand-placed-quantum experiment already use. The source is
89
+ * a LOCAL host tick-fn (NOT `QuantumEmitter`) because `QuantumEmitter`'s constructor has
90
+ * no wavelength field — the isolation lab needs the SAME per-frame wavelength override
91
+ * the scale sweep uses so the page visually demonstrates EXACTLY what the sweep
92
+ * measured, not a physically-different (unmodifiable-wavelength) source. Pushed onto
93
+ * `universe.experimentTickFunctions` (Phase A, matching `emitterProgram.js`'s own
94
+ * registration point) — legitimate here because, unlike `DirectedTunnel`'s `option1`
95
+ * redirect (which needs Phase-D `tickData`), injection is a pre-tick primitive with no
96
+ * such timing constraint.
97
+ *
98
+ * **Gun-chamber convention**: `capNegX:true, capPosX:false` — a CLOSED breech (source
99
+ * fires INTO the bore, quanta bounce/fill) and an OPEN muzzle (quanta escape, then
100
+ * disperse freely with no walls) — the Gun-Chamber model (particles ping-pong
101
+ * in a tube, escape the open end, spread comes from dispersion AFTER escape).
102
+ *
103
+ * **Fundamental dimension = 2** (matches `setupDoubleSlit.js`'s default AND the scale-
104
+ * sweep's own convention — level 0 pins wavelength to a no-op): a level-0 leaf has `capacity=1`, so
105
+ * `mass=max(1,capacity/wavelength)` is PINNED at 1 for any `wavelength>=2` — the
106
+ * wavelength slider would be a silent no-op at level 0.
107
+ *
108
+ * **Biased entrance (bunched near the TOP wall)** — the SAME shape the scale sweep uses,
109
+ * because a symmetric/spread entrance is already balanced and shows no washout effect at
110
+ * all (the scale sweep's own finding); bunching is what makes
111
+ * "uniformizes vs transmits unmodified" visually OBSERVABLE as the population fills the
112
+ * bore over time.
113
+ */
114
+
115
+ import { assertSetupOptions as _assertSetupOptions, requireWorldRect as _requireWorldRect } from './_setupOptions.js';
116
+ import { DirectedTunnel } from '../objects/DirectedTunnel.js';
117
+ import * as SustainedSource from '../objects/sustainedSource.js';
118
+
119
+ // The ONE unknown-option-key validator (objects/setup/_setupOptions.js). Resolved
120
+ // LAZILY in the browser: the game layer defines window.assertSetupOptions before any
121
+ // page script runs, but reading it at eval time would bake in a load-order dependency.
122
+
123
+ // ── Imports ─────────────────────────────────────────────────────────────
124
+
125
+ var FUNDAMENTAL_DIMENSION = 2; // matches setupDoubleSlit.js + the scale-sweep script
126
+
127
+ // The ONE authority for "what's the largest
128
+ // wavelength value that still produces a DISTINCT mass from any larger one"
129
+ // at this factory's FUNDAMENTAL_DIMENSION. Engine Invariant #6:
130
+ // `mass = max(1, capacity/wavelength)` — the floor (mass=1) is reached
131
+ // exactly at `wavelength === capacity`; every wavelength beyond that is
132
+ // indistinguishable. `tunnel-lab/index.html` reads this (never a hand-typed
133
+ // "16" literal) to clamp its wavelength slider's max to the achievable range.
134
+ // Derived from `SustainedSource.capacityAtDimension` — the SAME
135
+ // authority `setupLightSourceLab.js` now also derives its own
136
+ // MAX_USEFUL_WAVELENGTH from (AC4: "same MAX_USEFUL_WAVELENGTH clamp
137
+ // authority" for both labs).
138
+ var TUNNEL_LAB_CAPACITY = SustainedSource.capacityAtDimension(FUNDAMENTAL_DIMENSION); // 16 at FUNDAMENTAL_DIMENSION=2
139
+ /**
140
+ * OPTIONS — this factory's own option vocabulary: EXACTLY the keys it reads.
141
+ * Exported as `setupTunnelLab.OPTIONS` so a composing caller consumes it instead of
142
+ * retyping it. `setup-factory-option-vocabulary.test.mjs` re-derives the
143
+ * read-set by static trace and fails if this list drifts from the code.
144
+ */
145
+ var OPTIONS = [
146
+ 'bore', 'depth',
147
+ 'length', 'sourceActive', 'sourceDensity', 'sourceRate',
148
+ 'velocityProfile', 'wallBehavior', 'wavelength',
149
+ ];
150
+
151
+ function setupTunnelLab(universe, opts) {
152
+ opts = _assertSetupOptions('setupTunnelLab', opts, OPTIONS);
153
+ // The world rect the co-init call wrote (EGPTUniverse.create({rect})) — the ONE size.
154
+ var canvasRect = universe.universe_rect;
155
+ var world = _requireWorldRect('setupTunnelLab universe.universe_rect', canvasRect);
156
+ var cw = world.w, ch = world.h;
157
+
158
+ var bore = opts.bore !== undefined ? opts.bore : 40;
159
+ var length = opts.length !== undefined ? opts.length : 240;
160
+ var wavelength = opts.wavelength !== undefined ? opts.wavelength : 64;
161
+ var wallBehavior = opts.wallBehavior !== undefined ? opts.wallBehavior : 'option2';
162
+ var sourceDensity = opts.sourceDensity !== undefined ? opts.sourceDensity : 5;
163
+ var sourceRate = opts.sourceRate !== undefined ? opts.sourceRate : 10;
164
+ var sourceActive = opts.sourceActive !== undefined ? !!opts.sourceActive : true;
165
+ // OPT-IN 3D, OMISSION-preserves-2D
166
+ // (the "before adding a creation-method parameter" discipline): mirrors
167
+ // the standard `options.depth` recipe — omitted/null ->
168
+ // 2D (zContain:false, byte-identical to every existing caller); a
169
+ // positive value -> real z-containment (Tube/DirectedTunnel heritage —
170
+ // `options.depth` already existed on DirectedTunnel, T1; this factory
171
+ // simply now forwards it + arms activeAxes).
172
+ var depth = (opts.depth !== undefined && opts.depth !== null) ? opts.depth : null;
173
+ // Default 'spray' is byte-identical to every pre-existing
174
+ // caller (this factory's own original hardcoded value below).
175
+ var velocityProfile = opts.velocityProfile !== undefined ? opts.velocityProfile : 'spray';
176
+
177
+ var initOpts = {};
178
+ if (depth !== null) { initOpts.activeAxes = { x: true, y: true, z: true }; }
179
+ universe.init(FUNDAMENTAL_DIMENSION, initOpts);
180
+
181
+ var left = Math.round(cw * 0.1);
182
+ var top = Math.round(ch / 2 - bore / 2);
183
+
184
+ var tunnelOpts = {
185
+ left: left, top: top, length: length, width: bore,
186
+ capNegX: true, capPosX: false, // closed breech, open muzzle — Gun-Chamber convention
187
+ behavior: wallBehavior,
188
+ };
189
+ if (depth !== null) { tunnelOpts.depth = depth; tunnelOpts.z = -depth / 2; }
190
+ var tunnel = new DirectedTunnel(universe, tunnelOpts);
191
+
192
+ // One source surface: the recurring
193
+ // burst injector now routes through `sustainedSource.js`'s ONE
194
+ // `buildSustainedSource` authority — the SAME surface
195
+ // `setupLightSourceLab.js` (via `LightSource`'s own opt-in
196
+ // `options.sustain`) now also consumes. `velocityProfile:'spray'` +
197
+ // `entranceBias:'top'` reproduce this factory's PRE-EXISTING biased-
198
+ // random-fan shape EXACTLY (the "uniformization visible" demo shape —
199
+ // see the module docstring's "Biased entrance" note); `zIsotropy:true`
200
+ // (the default) is the 3D-SYMMETRIC EMISSION fix (§4
201
+ // option (a)) — when `depth` is armed, every burst now ALSO draws a `vz`
202
+ // symmetric to the `vy` spread, instead of pinning z-momentum at zero
203
+ // forever (the tester-measured z/y=0.23 flatness root cause).
204
+ var src = SustainedSource.buildSustainedSource(universe, tunnel.innerRect, true /* capNegX (Gun-Chamber closed breech) */, depth !== null, {
205
+ wavelength: wavelength,
206
+ velocityProfile: velocityProfile,
207
+ entranceBias: 'top',
208
+ zIsotropy: true,
209
+ density: sourceDensity,
210
+ rate: sourceRate,
211
+ active: sourceActive,
212
+ });
213
+ var sourceState = src.sourceState;
214
+
215
+ return {
216
+ tunnel: tunnel,
217
+ sourceState: sourceState,
218
+ // FrqtlApp.setup() (FrqtlApp.js:528) auto-wires
219
+ // the render list; today the world's own census carries them by construction.
220
+ largeObjects: tunnel.largeObjects,
221
+ // CANONICAL TEARDOWN (the reinit-
222
+ // compounding fix): `FrqtlApp.prototype.setup()` calls THIS
223
+ // automatically before installing the NEXT setupResult. Delegates to
224
+ // `src.dispose()` (the ONE `sustainedSource.js` teardown mechanism)
225
+ // rather than re-deriving the tag-then-splice idiom a second time.
226
+ dispose: src.dispose,
227
+ };
228
+ }
229
+
230
+ // ONE authority for the achievable wavelength ceiling at this factory's
231
+ // FUNDAMENTAL_DIMENSION (see the TUNNEL_LAB_CAPACITY comment above) — the
232
+ // page reads this instead of hand-typing "16".
233
+ setupTunnelLab.FUNDAMENTAL_DIMENSION = FUNDAMENTAL_DIMENSION;
234
+ setupTunnelLab.CAPACITY = TUNNEL_LAB_CAPACITY;
235
+ setupTunnelLab.MAX_USEFUL_WAVELENGTH = TUNNEL_LAB_CAPACITY;
236
+
237
+ setupTunnelLab.OPTIONS = OPTIONS;
238
+
239
+ export { setupTunnelLab };
240
+