@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,717 @@
1
+ /* Copyright 2023-2026 by Essam Abadir */
2
+
3
+ /**
4
+ * LogicGate — THE SWITCHING PART. Two current-sensing input terminals, one
5
+ * powered output terminal, assembled entirely from stock: a `Wall{destroy,count}`
6
+ * ammeter per input, a mirror `Tunnel` output channel, a `SphereEmitter` supply
7
+ * rail, and a `Wall{destroy}` SHUNT VALVE across the outlet.
8
+ *
9
+ * var g = new LogicGate(universe, { logic: 'AND', left: 200, top: 120, bore: 3 });
10
+ * g.inPortA; // [200, 126] — route a Trace here
11
+ * g.inPortB; // [200, 168]
12
+ * g.outPort; // [248, 147] — route the next Trace from here
13
+ * g.onTick(tickData); // once per tick — sense, decide, actuate
14
+ * g.getState(); // { a, b, out, currentA, currentB }
15
+ *
16
+ * ── THE PACKAGED MECHANISM, IN ONE SENTENCE ─────────────────────────────────
17
+ * A `LogicGate` is a HOST TICK-FN that (1) reads each input terminal's collected
18
+ * collision count out of this tick's TickData, (2) thresholds the two windowed
19
+ * currents into booleans with hysteresis, and (3) writes ONE property — the
20
+ * shunt valve's active flag — so the always-on supply either reaches the outlet
21
+ * or is dumped to ground. Sense with a counting Wall; decide between ticks;
22
+ * actuate with `setActive`. That is the documented control loop
23
+ * (`INTERPRETER_ARCHITECTURE.md`), not a new engine concept.
24
+ *
25
+ * ── WHY A POWERED (VALVE) GATE AND NOT A CURRENT-SUMMING ONE ────────────────
26
+ * Both were measured. Currents DO
27
+ * add at a directed junction, so a wired threshold gate is buildable — but its
28
+ * output level is a multiple of its INPUT level, so levels decay down a chain and
29
+ * a two-deep circuit needs re-tuned thresholds at every stage. The valve gate's
30
+ * output is its OWN supply rail, so every gate emits the same HIGH level no matter
31
+ * what its inputs did: levels are RESTORED and gates cascade unchanged. A
32
+ * half-adder is two gates deep, so this is not a preference — it is the reason the
33
+ * part works at all. The merge measurement is kept in the bench README as the
34
+ * fan-in datum, not as a second gate family.
35
+ *
36
+ * ── PURE COMPOSITION — zero engine, zero new physics (canonical-bench SU1) ───
37
+ * A LogicGate adds NO collision logic. Every piece is a part already on the floor:
38
+ * INPUT TERMINAL → `Wall{ collisionBehavior:'destroy', hooks:['count'] }`
39
+ * — the ammeter; identical to a `Trace`'s own ground, and it
40
+ * uses `Trace.GROUND_COUNTS_PER_ARRIVAL` for calibration
41
+ * rather than re-deriving it (ONE authority).
42
+ * OUTPUT CHANNEL → `Tunnel{ mode:'mirror' }` — the same insulation a Trace run
43
+ * is made of, so the gate shrinks on the same rules.
44
+ * SUPPLY RAIL → `SphereEmitter{ preset:'laser' }` — voltage = burst/tick.
45
+ * SHUNT VALVE → `Wall{ collisionBehavior:'destroy' }` + `lo.setActive()`.
46
+ * SUPPLY CAP → `Wall{ collisionBehavior:'destroy' }` closing the inlet.
47
+ * The comparator is arithmetic on collected data BETWEEN ticks. If building a gate
48
+ * ever seems to need engine or provider code, STOP: it is walls, a tunnel and an
49
+ * emitter.
50
+ *
51
+ * ── Layer 1: API contract ────────────────────────────────────────────────────
52
+ *
53
+ * `new LogicGate(universe, opts)` — builds + engine-registers every sub-part. It
54
+ * does NOT add itself to a runner render list; the caller iterates
55
+ * `gate.largeObjects` (or `runner.setup()` auto-wires them).
56
+ *
57
+ * ORDERING PRECONDITION — `universe.init()` MUST already have run (a gate
58
+ * registers LargeObjects in its constructor and the engine throws on
59
+ * registration against a fresh universe). The sketch order is always
60
+ * world.init() FIRST → CONSTRUCT parts → WIRE → RUN.
61
+ *
62
+ * @param {Object} universe — an EGPTUniverse (JS or WASM compat shim).
63
+ * @param {Object} opts
64
+ * @param {string} opts.logic — REQUIRED. A key of `LogicGate.TRUTH`:
65
+ * AND | OR | XOR | NAND | NOR | XNOR. FAIL-LOUD on anything else. The logic is
66
+ * a comparator ROW, not a geometry — every value builds the identical hardware,
67
+ * which is why an inverting gate costs exactly what a non-inverting one costs
68
+ * here (unlike silicon).
69
+ * @param {number} opts.left, opts.top — the gate cell's top-left corner, px.
70
+ * @param {number} [opts.bore=3] — the lane aperture, px. Sets the lane pitch
71
+ * (`bore + 2*wallThickness`) and therefore the whole footprint.
72
+ * @param {number} [opts.wallThickness=6] — lane insulation, px. This is the knob
73
+ * that sets the shrink floor — see Layer 2 and the `Trace` datasheet.
74
+ * @param {number} [opts.length=48] — the output channel's x extent, px. The floor
75
+ * is `LogicGate.MIN_LENGTH` (20 px — below it the supply inlet and the shunt
76
+ * valve would overlap), and 20 px is also the length the shipped minimal cell
77
+ * uses. FAIL-LOUD below the floor.
78
+ * @param {number} [opts.supplyVoltage=6] — the rail: emitter burst per tick, and
79
+ * therefore the gate's HIGH output level in q/tick. LIVE (`setSupplyVoltage`).
80
+ * @param {number} [opts.thresholdHigh] — input current (q/tick) at or above which
81
+ * an input is called HIGH. DEFAULT `0.5 * supplyVoltage` — referenced to the
82
+ * rail so a gate driven by another gate switches at half that gate's own HIGH
83
+ * level, which is what makes a chain work without per-stage tuning.
84
+ * @param {number} [opts.thresholdLow] — current below which a HIGH input reverts
85
+ * to LOW (hysteresis). DEFAULT `0.25 * supplyVoltage`. Must be < thresholdHigh.
86
+ * @param {number} [opts.windowTicks=20] — sliding window for the current average.
87
+ * Sets BOTH the noise (sd ~ sqrt(I/W)) and half the switching delay.
88
+ * @param {'x'} [opts.axis='x'] — flow axis. Only +x is built and measured;
89
+ * FAIL-LOUD on anything else rather than shipping an unmeasured orientation.
90
+ * @param {Array} [opts.color] — RGB for the channel mirrors.
91
+ * @param {string} [opts.name='gate'] — identifier prefix.
92
+ *
93
+ * @property {Array} inPortA, inPortB — `[x,y]` input terminal faces. Route a
94
+ * `Trace` so its `outPort` lands exactly here (gap 0 — the terminal slab starts
95
+ * at the port).
96
+ * @property {Array} outPort — `[x,y]` the powered outlet. Route the next `Trace`'s
97
+ * `inPort` here.
98
+ * @property {Array} footprint — `[left, top, w, h]` the gate cell's full extent.
99
+ * @property {Array<Wall>} terminals — `[ammeterA, ammeterB]`.
100
+ * @property {Wall} valve, supplyCap — the shunt valve and the inlet cap.
101
+ * @property {Tunnel} channel — the output channel.
102
+ * @property {Array<LargeObject>} largeObjects — every inner engine LO (render wiring).
103
+ *
104
+ * Methods:
105
+ * `onTick(tickData)` — THE ONE per-tick call: sense → decide → actuate.
106
+ * `attachToRunner(runner)` — subscribe `onTick` to the runner's tick fan-out.
107
+ * `getState()` → `{ a, b, out, currentA, currentB }` — the two input booleans,
108
+ * the output boolean, and the two windowed currents in q/tick.
109
+ * `getOutputLevel()` → the nominal HIGH level in q/tick (0 when the output is LOW).
110
+ * `setSupplyVoltage(v)` / `getSupplyVoltage()` — LIVE rail knob. Re-derives the
111
+ * two thresholds only if the caller never pinned them explicitly.
112
+ * `setLogic(name)` / `getLogic()` — LIVE comparator row swap (no rebuild: the
113
+ * hardware is identical for every row).
114
+ * `footprintArea()` → px² — the miniaturization figure of merit.
115
+ * `dispose()` — tears down every sub-part (idempotent).
116
+ *
117
+ * ── Layer 2: physics behavior ────────────────────────────────────────────────
118
+ * ZERO physics of its own (SU1). Everything below is the emergent behaviour of the
119
+ * stock parts, MEASURED on this construction (WASM engine, 2D, fundamental dim 0,
120
+ * inter-quantum collisions OFF, 3 seeds; the numbers are quoted from `LogicGate.MEASURED`):
121
+ *
122
+ * THE INPUT IS AN AMMETER AND IT IS LINEAR. A `Wall{destroy,count}` terminal
123
+ * reads I = G·V with G = 1.01 ± 0.01 and no knee from V = 1 to V = 16, and reads
124
+ * EXACTLY 0.00 with no source. That linearity is why a threshold can be quoted
125
+ * as a FRACTION OF THE RAIL and still mean the same thing at every voltage.
126
+ *
127
+ * THE VALVE IS A HARD ZERO. Closed (the destroy slab active) the outlet delivers
128
+ * 0.000 q/tick — not "small", zero: `destroy` is a plain kill (bench LO6) and
129
+ * nothing gets past it. So the ON/OFF ratio is not a finite number, and the LOW
130
+ * output level is not a level at all. All of a gate's noise lives on the INPUT
131
+ * side.
132
+ *
133
+ * THE SWITCHING THRESHOLD AND ITS TOLERANCE. The terminal current is a counting
134
+ * process: the sd of a W-tick sliding mean goes as ~0.75·sqrt(I/W), so at the
135
+ * default rail (6 q/tick) and W = 20 the input reads 6.05 ± 0.42 — a 7.2 sd
136
+ * margin to the 3.0 threshold, and 5.7 sd even at a rail of 4. Shrinking W speeds
137
+ * the gate up and narrows the margin as 1/sqrt(W); the shipped default is the
138
+ * last W at which every rail from 4 up clears 4 sd.
139
+ *
140
+ * …AND THE ASYMMETRY THAT COMES WITH HYSTERESIS. The comparator rises on the
141
+ * FIRST EXCURSION above `thresholdHigh` and is then LATCHED by `thresholdLow`, so
142
+ * the EFFECTIVE switching point sits about half a standard deviation BELOW the
143
+ * nominal threshold: a drive whose terminal MEAN measured 2.86 q/tick — under the
144
+ * 3.0 threshold — read HIGH on every seed, and never chattered (zero output
145
+ * transitions were measured anywhere in the drive sweep). The rule that follows:
146
+ * an input is reliably LOW only while it stays ~4 sd BELOW `thresholdHigh`, which
147
+ * at the shipped defaults means under about a THIRD of the rail, not merely under
148
+ * half of it. The asymmetry is in the safe direction (a marginal input latches
149
+ * rather than oscillating), but it is not what "threshold = half the rail" reads
150
+ * like, so design to the 4 sd rule, not to the nominal number.
151
+ *
152
+ * PROPAGATION DELAY: ~8 TICKS OF GATE, THE REST IS WIRE. Measured end-to-end at
153
+ * 268 ticks from an input source switching on to the output boolean following —
154
+ * of which 260 ticks are the current FLYING the 260 px input trace at 1 px/tick
155
+ * (bench M1) and only ~8 are the comparator crossing its threshold. The output
156
+ * CURRENT then needs the outlet-to-load flight on top (measured 529 ticks total
157
+ * over a further 240 px). So the part's own latency is ~8 ticks and everything
158
+ * else in a circuit's settling time is LAYOUT: place a gate near its source and
159
+ * it responds almost immediately; a chain's settling time is the sum of its
160
+ * wires, not of its gates.
161
+ *
162
+ * LANE CROSSTALK IS A NON-ISSUE, AND HERE IS THE ARITHMETIC. The three lanes sit
163
+ * at `Trace.lanePitch` — wall against wall, never sharing mirror pixels (two
164
+ * coincident mirrors leak badly; that is in the Trace datasheet). At the tightest
165
+ * shipped geometry the published lane-to-lane crosstalk is 0.98 % and it reaches
166
+ * ONLY the immediately adjacent lane, so at a rail of 6 the output lane puts
167
+ * ~0.06 q/tick onto an input terminal — about 25x below `thresholdLow`. Widen the
168
+ * insulation and it measures exactly zero. The output lane is deliberately in the
169
+ * MIDDLE so both inputs see the identical (tiny) coupling.
170
+ *
171
+ * IT SHRINKS, BUT IT HAS ITS OWN FLOOR — LOWER THAN THE WIRE'S. A `Trace` still
172
+ * conducts at bore 1 (G = 0.987 with 10 px of insulation). A GATE at bore 1 does
173
+ * NOT: it measured 0.14 q/tick and failed its truth table outright, because a gate
174
+ * is not just a run — it puts a 4 px inlet cap and a 4 px shunt valve across the
175
+ * bore and parks the supply 8 px inside it, and that machinery does not fit a 1 px
176
+ * aperture. Expecting the wire's floor to be the gate's floor was WRONG; the
177
+ * measured floor is bore 2-3.
178
+ *
179
+ * AND "IT RETURNS THE RIGHT ANSWER" IS NOT THE ACCEPTANCE TEST. bore 2 / wall 3
180
+ * (a 480 px² cell) holds all sixteen truth-table rows — on a conductor leaking
181
+ * ~23 %, delivering only 4.65 q/tick against a 6 q/tick rail. The BOOLEAN is right
182
+ * and the part is still not shippable, because the HIGH level is what the NEXT
183
+ * gate thresholds against and level restoration is the property the cascade rests
184
+ * on. Acceptance is therefore two conditions — table correct AND HIGH ≥ 90 % of
185
+ * the rail — which makes the shipped minimum `LogicGate.MINIMAL`: bore 3 / wall 3
186
+ * / length 20, a 20 × 27 = 540 px² cell delivering 6.04 ± 0.12 q/tick.
187
+ *
188
+ * A too-thin mirror does not degrade gracefully — it goes transparent, so a
189
+ * sub-floor gate reads DEAD (no output on any row), not noisy.
190
+ *
191
+ * QUIESCENT CURRENT. The rail runs whether the output is HIGH or LOW (a LOW
192
+ * output is the supply being dumped into the valve), so a gate always draws
193
+ * `supplyVoltage` q/tick. This is static logic; there is no idle state.
194
+ */
195
+
196
+ import { Tunnel as _Tunnel } from './Tunnel.js';
197
+ import { Wall as _Wall } from './Wall.js';
198
+ import { Trace as _Trace } from './Trace.js';
199
+ import { SphereEmitter as _SphereEmitter } from './SphereEmitter.js';
200
+ import { registerKind } from '../objects/kinds.js';
201
+
202
+
203
+
204
+
205
+ /**
206
+ * The comparator rows. DATA, not geometry: every row builds the identical
207
+ * hardware and differs only in this one function, which is why NAND costs
208
+ * exactly what AND costs on this floor.
209
+ */
210
+ var TRUTH = {
211
+ AND: function (a, b) { return a && b; },
212
+ OR: function (a, b) { return a || b; },
213
+ XOR: function (a, b) { return a !== b; },
214
+ NAND: function (a, b) { return !(a && b); },
215
+ NOR: function (a, b) { return !(a || b); },
216
+ XNOR: function (a, b) { return a === b; }
217
+ };
218
+
219
+ function LogicGate(universe, opts) {
220
+ opts = opts || {};
221
+ this.universe = universe;
222
+ this._schemaKind = 'LogicGate';
223
+
224
+ var TunnelCls = _Tunnel;
225
+ var WallCls = _Wall;
226
+ var TraceCls = _Trace;
227
+ var EmitterCls = _SphereEmitter;
228
+
229
+ var logic = String(opts.logic || '').toUpperCase();
230
+ if (!TRUTH[logic]) {
231
+ throw new Error('[LogicGate] opts.logic must be one of ' + Object.keys(TRUTH).join(', ') +
232
+ ' — got ' + JSON.stringify(opts.logic) + '. (FAIL-LOUD.)');
233
+ }
234
+ var axis = opts.axis || 'x';
235
+ if (axis !== 'x') {
236
+ throw new Error('[LogicGate] only axis "x" (+x flow) is built and MEASURED. A rotated ' +
237
+ 'gate would be an unmeasured part wearing a measured datasheet. Rotate the layout ' +
238
+ 'instead. (FAIL-LOUD.)');
239
+ }
240
+
241
+ var left = opts.left, top = opts.top;
242
+ if (typeof left !== 'number' || typeof top !== 'number') {
243
+ throw new Error('[LogicGate] opts.left and opts.top are required (px). (FAIL-LOUD.)');
244
+ }
245
+ var bore = (opts.bore !== undefined && opts.bore !== null) ? opts.bore : 3;
246
+ var wallThickness = (opts.wallThickness !== undefined && opts.wallThickness !== null)
247
+ ? opts.wallThickness : 6;
248
+ if (!(bore > 0) || !(wallThickness > 0)) {
249
+ throw new Error('[LogicGate] bore and wallThickness must both be positive. (FAIL-LOUD.)');
250
+ }
251
+ var length = (opts.length !== undefined) ? opts.length : 48;
252
+ if (length < LogicGate.MIN_LENGTH) {
253
+ throw new Error('[LogicGate] opts.length ' + length + ' is below the measured minimum ' +
254
+ LogicGate.MIN_LENGTH + 'px (the supply inlet and the shunt valve would overlap). ' +
255
+ '(FAIL-LOUD.)');
256
+ }
257
+
258
+ var supplyVoltage = (opts.supplyVoltage !== undefined) ? opts.supplyVoltage : 6;
259
+ this._pinnedHigh = (opts.thresholdHigh !== undefined);
260
+ this._pinnedLow = (opts.thresholdLow !== undefined);
261
+ var thresholdHigh = this._pinnedHigh ? opts.thresholdHigh : LogicGate.THRESHOLD_HIGH_FRACTION * supplyVoltage;
262
+ var thresholdLow = this._pinnedLow ? opts.thresholdLow : LogicGate.THRESHOLD_LOW_FRACTION * supplyVoltage;
263
+ if (!(thresholdLow < thresholdHigh)) {
264
+ throw new Error('[LogicGate] thresholdLow (' + thresholdLow + ') must be BELOW ' +
265
+ 'thresholdHigh (' + thresholdHigh + ') — the gap IS the hysteresis. (FAIL-LOUD.)');
266
+ }
267
+
268
+ var name = opts.name || 'gate';
269
+ var color = opts.color || [200, 170, 70];
270
+ var pitch = TraceCls.lanePitch(bore, wallThickness);
271
+
272
+ this.logic = logic;
273
+ this.left = left; this.top = top;
274
+ this.bore = bore; this.wallThickness = wallThickness; this.length = length;
275
+ this.pitch = pitch;
276
+ this.supplyVoltage = supplyVoltage;
277
+ this.thresholdHigh = thresholdHigh;
278
+ this.thresholdLow = thresholdLow;
279
+ this.windowTicks = (opts.windowTicks !== undefined) ? opts.windowTicks : 20;
280
+ this.name = name;
281
+ this.color = color;
282
+
283
+ // ── THE THREE LANES ──────────────────────────────────────────────────
284
+ // Input A on top, the powered output in the MIDDLE (equidistant from both
285
+ // inputs, so the two crosstalk paths are identical), input B on the bottom.
286
+ // Lanes sit at Trace's published pitch — wall against wall, never sharing
287
+ // mirror pixels (the coincident-mirror leak in the Trace datasheet).
288
+ var yA = top + pitch / 2;
289
+ var yO = top + pitch * 1.5;
290
+ var yB = top + pitch * 2.5;
291
+ this.inPortA = [left, yA];
292
+ this.inPortB = [left, yB];
293
+ this.outPort = [left + length, yO];
294
+ this.footprint = [left, top, length, pitch * 3];
295
+
296
+ var T = LogicGate.TERMINAL_THICKNESS;
297
+
298
+ // ── INPUT TERMINALS: the ammeters. Identical to a Trace's own ground. ─
299
+ this.terminals = [
300
+ new WallCls(universe, {
301
+ rect: [left, yA - pitch / 2, T, pitch],
302
+ collisionBehavior: 'destroy', hooks: ['count'],
303
+ name: name + '_inA', color: [60, 60, 255]
304
+ }),
305
+ new WallCls(universe, {
306
+ rect: [left, yB - pitch / 2, T, pitch],
307
+ collisionBehavior: 'destroy', hooks: ['count'],
308
+ name: name + '_inB', color: [60, 60, 255]
309
+ })
310
+ ];
311
+
312
+ // ── OUTPUT CHANNEL: a mirror Tunnel — the same insulation as a Trace run.
313
+ this.channel = new TunnelCls(universe, {
314
+ left: left, top: yO - bore / 2, length: length, bore: bore,
315
+ wallThickness: wallThickness, axis: 'x', mode: 'mirror',
316
+ name: name + '_channel', color: color
317
+ });
318
+
319
+ // ── SUPPLY CAP: closes the inlet so the rail cannot spray backwards out
320
+ // of the cell. A destroy slab, not a mirror — a mirror would send the
321
+ // backward fraction forward again and inflate the HIGH level.
322
+ this.supplyCap = new WallCls(universe, {
323
+ rect: [left, yO - pitch / 2, T, pitch],
324
+ collisionBehavior: 'destroy',
325
+ name: name + '_cap', color: [70, 70, 90]
326
+ });
327
+
328
+ // ── SHUNT VALVE: the ONE property this gate ever writes. ──────────────
329
+ this.valve = new WallCls(universe, {
330
+ rect: [left + length - LogicGate.VALVE_INSET, yO - pitch / 2, T, pitch],
331
+ collisionBehavior: 'destroy',
332
+ name: name + '_valve', color: [220, 90, 90]
333
+ });
334
+
335
+ // ── SUPPLY RAIL: voltage = burst per tick, aimed down the channel. ────
336
+ this.supply = new EmitterCls([left + LogicGate.SUPPLY_INSET, yO], {
337
+ preset: 'laser', is2D: true, R: 24, orientationDeg: 0,
338
+ emissionBoxSide: 0, fundamentalDimension: 0,
339
+ burstSize: Math.max(1, supplyVoltage), emitEveryTicks: 1,
340
+ chargeMode: 'random', renderColor: [255, 120, 60]
341
+ }).addTo(universe);
342
+
343
+ // ── Roll up every sub-part for render wiring. ─────────────────────────
344
+ var parts = this.terminals.concat(this.channel.walls, [this.supplyCap, this.valve]);
345
+ this.walls = parts;
346
+ this.largeObjects = [];
347
+ var self = this;
348
+ parts.forEach(function (w) {
349
+ if (w && w.lo) {
350
+ self.largeObjects.push(w.lo);
351
+ }
352
+ });
353
+ // THE ONE UP-LINK: the gate stamps exactly what IT composed — the terminals, the channel
354
+ // (a Tunnel, which stamps its own plates), the cap, the valve and the supply rail.
355
+ this.terminals.concat([this.channel, this.supplyCap, this.valve, this.supply]).forEach(function (p) {
356
+ if (p) { p._owner = self; }
357
+ });
358
+
359
+ // ── Comparator state ─────────────────────────────────────────────────
360
+ this._counts = [[], []]; // raw cumulative ground counts, per terminal
361
+ this._current = [0, 0]; // windowed q/tick, per terminal
362
+ this._states = [false, false];
363
+ this._out = null;
364
+ this._unsub = null;
365
+ this._disposed = false;
366
+ this._applyOutput(TRUTH[logic](false, false));
367
+ }
368
+
369
+ /** Terminal / valve / cap slab thickness, px. */
370
+ LogicGate.TERMINAL_THICKNESS = 4;
371
+ /** Supply emitter inset from the channel inlet, px (clear of the inlet cap). */
372
+ LogicGate.SUPPLY_INSET = 8;
373
+ /** Valve inset from the outlet, px. Shorter ⇒ faster fall, less stored current. */
374
+ LogicGate.VALVE_INSET = 8;
375
+ /** Shortest channel that still fits the supply inlet and the valve, px. */
376
+ LogicGate.MIN_LENGTH = LogicGate.SUPPLY_INSET + LogicGate.VALVE_INSET + LogicGate.TERMINAL_THICKNESS;
377
+ /** Default switching thresholds AS A FRACTION OF THE RAIL (see Layer 2). */
378
+ LogicGate.THRESHOLD_HIGH_FRACTION = 0.5;
379
+ LogicGate.THRESHOLD_LOW_FRACTION = 0.25;
380
+
381
+ LogicGate.TRUTH = TRUTH;
382
+ /** The logic rows this part realizes — the ONE vocabulary (schema ferries it). */
383
+ LogicGate.LOGICS = Object.keys(TRUTH);
384
+
385
+ /**
386
+ * The measured datasheet AS DATA, so a circuit (or a half-adder) can budget
387
+ * levels, margins and settling time without re-running the bench. Every figure
388
+ * is measured (pkg-node WASM, 2D, fundamental dim 0, inter-quantum collisions OFF,
389
+ * 3 seeds), each against a recorded prediction.
390
+ */
391
+ LogicGate.MEASURED = {
392
+ /** Terminal current vs source voltage: I = inputGain * V, no knee, I(0)=0 exactly. */
393
+ inputGain: 1.011, inputGainSpread: 0.024,
394
+ /** HIGH output level as a fraction of the rail, and its seed spread (q/tick at rail 6). */
395
+ outputLevelFraction: 1.001, outputLevelSd: 0.036,
396
+ /** LOW output. Not a level — the valve is a destroy slab. */
397
+ outputLowLevel: 0,
398
+ /** sd of the W-tick windowed terminal current ~ noiseCoefficient * sqrt(I/W). */
399
+ noiseCoefficient: 0.75,
400
+ /** Margin in sd from the rail to the threshold, at windowTicks 20, rail 4 / 6 / 8. */
401
+ thresholdMarginSd: { 4: 5.67, 6: 7.21, 8: 8.87 },
402
+ /**
403
+ * How far BELOW `thresholdHigh` an input must stay to be reliably LOW, in sd.
404
+ * The comparator rises on the FIRST excursion and then latches, so the
405
+ * effective switch point is below nominal — design to this, not to 0.5*rail.
406
+ */
407
+ reliableLowMarginSd: 4,
408
+ /** The part's own latency, ticks. Everything else in a circuit is wire flight. */
409
+ gateLatencyTicks: 8,
410
+ /** Wire flight, px per tick (bench M1) — the other term in any settling budget. */
411
+ flightPxPerTick: 1,
412
+ /** Output level spread across a 2.7x range of INPUT level (level restoration). */
413
+ levelRestorationSd: 0.018,
414
+ /** Output transitions measured across the whole drive sweep. */
415
+ chatterTransitions: 0
416
+ };
417
+
418
+ /**
419
+ * The smallest cell MEASURED to hold the full truth table (both logics, all
420
+ * four input rows, every seed) — the miniaturization acceptance result, and the
421
+ * palette default. Acceptance is TWO conditions — the table correct AND the HIGH
422
+ * level restored to >= 90% of the rail — because a smaller cell (bore 2 / wall 3,
423
+ * 480 px^2) returns every correct boolean while delivering only 77% of the rail,
424
+ * which is out of margin the moment it drives another gate. Below bore 2 the gate
425
+ * fails outright even where a Trace still conducts: the inlet cap, the shunt valve
426
+ * and the supply do not fit a 1px aperture. A too-thin wall does not degrade — the
427
+ * mirrors go transparent and the gate reads DEAD.
428
+ */
429
+ LogicGate.MINIMAL = { bore: 3, wallThickness: 3, length: 20, w: 20, h: 27, area: 540 };
430
+
431
+ /**
432
+ * THE ONE per-tick call — SENSE, DECIDE, ACTUATE.
433
+ * Reads each terminal's collected collisions out of THIS tick's TickData
434
+ * (each TickData must be consumed exactly once — the Wall guard enforces it),
435
+ * windows them into a current, thresholds with hysteresis, and writes the
436
+ * valve's active flag when — and only when — the output boolean changes.
437
+ * @param {Object} tickData — this tick's TickData (from `universe.doTick()`).
438
+ */
439
+ LogicGate.prototype.onTick = function (tickData) {
440
+ var TraceCls = _Trace;
441
+ var perArrival = TraceCls.GROUND_COUNTS_PER_ARRIVAL;
442
+ for (var i = 0; i < 2; i++) {
443
+ var raw = this.terminals[i].getCollisionHistogram(tickData).total;
444
+ var w = this._counts[i];
445
+ w.push(raw);
446
+ if (w.length > this.windowTicks + 1) { w.shift(); }
447
+ var span = w.length - 1;
448
+ this._current[i] = span > 0 ? ((w[span] - w[0]) / perArrival) / span : 0;
449
+ // Hysteresis: the gap between the two thresholds is what stops a gate
450
+ // sitting on its threshold from chattering on shot noise.
451
+ if (this._states[i] && this._current[i] < this.thresholdLow) { this._states[i] = false; }
452
+ else if (!this._states[i] && this._current[i] >= this.thresholdHigh) { this._states[i] = true; }
453
+ }
454
+ this._applyOutput(TRUTH[this.logic](this._states[0], this._states[1]));
455
+ };
456
+
457
+ /**
458
+ * Write the valve. HIGH ⇒ the valve is INACTIVE (the rail reaches the outlet);
459
+ * LOW ⇒ ACTIVE (the rail is dumped into the destroy slab). Idempotent — the
460
+ * engine write happens only on a transition.
461
+ * @private
462
+ */
463
+ LogicGate.prototype._applyOutput = function (out) {
464
+ if (out === this._out) { return; }
465
+ this._out = out;
466
+ this.valve.lo.setActive(!out);
467
+ };
468
+
469
+ /** Subscribe `onTick` to a runner's per-tick fan-out. Idempotent. */
470
+ LogicGate.prototype.attachToRunner = function (runner) {
471
+ if (this._unsub || !runner || typeof runner.addTickListener !== 'function') { return this._unsub || null; }
472
+ var self = this;
473
+ this._unsub = runner.addTickListener(function (td) { self.onTick(td); });
474
+ return this._unsub;
475
+ };
476
+
477
+ /**
478
+ * The gate's live readout.
479
+ * @returns {{a:boolean, b:boolean, out:boolean, currentA:number, currentB:number}}
480
+ */
481
+ LogicGate.prototype.getState = function () {
482
+ return {
483
+ a: this._states[0], b: this._states[1], out: !!this._out,
484
+ currentA: this._current[0], currentB: this._current[1]
485
+ };
486
+ };
487
+
488
+ /** The nominal output level in q/tick: the rail when HIGH, exactly 0 when LOW. */
489
+ LogicGate.prototype.getOutputLevel = function () {
490
+ return this._out ? this.supplyVoltage : 0;
491
+ };
492
+
493
+ /**
494
+ * LIVE rail knob. Re-derives the two thresholds from the new rail UNLESS the
495
+ * caller pinned them at construction — a pinned threshold is an explicit
496
+ * design decision and is never silently moved.
497
+ * @param {number} v — burst per tick.
498
+ */
499
+ LogicGate.prototype.setSupplyVoltage = function (v) {
500
+ this.supplyVoltage = Math.max(0, Math.round(v));
501
+ this.supply.setBurstSize(Math.max(1, this.supplyVoltage));
502
+ this.supply.setActive(this.supplyVoltage > 0);
503
+ if (!this._pinnedHigh) { this.thresholdHigh = LogicGate.THRESHOLD_HIGH_FRACTION * this.supplyVoltage; }
504
+ if (!this._pinnedLow) { this.thresholdLow = LogicGate.THRESHOLD_LOW_FRACTION * this.supplyVoltage; }
505
+ return this.supplyVoltage;
506
+ };
507
+ LogicGate.prototype.getSupplyVoltage = function () { return this.supplyVoltage; };
508
+
509
+ /** LIVE comparator-row swap. The hardware is identical for every row. */
510
+ LogicGate.prototype.setLogic = function (nameIn) {
511
+ var next = String(nameIn || '').toUpperCase();
512
+ if (!TRUTH[next]) {
513
+ throw new Error('[LogicGate.setLogic] unknown logic ' + JSON.stringify(nameIn) +
514
+ ' — expected one of ' + LogicGate.LOGICS.join(', ') + '. (FAIL-LOUD.)');
515
+ }
516
+ this.logic = next;
517
+ this._applyOutput(TRUTH[next](this._states[0], this._states[1]));
518
+ return next;
519
+ };
520
+ LogicGate.prototype.getLogic = function () { return this.logic; };
521
+
522
+ /** The miniaturization figure of merit: the cell's area in px². */
523
+ LogicGate.prototype.footprintArea = function () {
524
+ return this.footprint[2] * this.footprint[3];
525
+ };
526
+
527
+ /** Full teardown; delegates to each sub-part's own dispose. Idempotent. */
528
+ LogicGate.prototype.dispose = function () {
529
+ if (this._disposed) { return; }
530
+ this._disposed = true;
531
+ if (this._unsub) { this._unsub(); this._unsub = null; }
532
+ this.terminals.forEach(function (w) { w.dispose(); });
533
+ this.channel.dispose();
534
+ this.supplyCap.dispose();
535
+ this.valve.dispose();
536
+ if (this.supply && typeof this.supply.dispose === 'function') { this.supply.dispose(); }
537
+ };
538
+
539
+ /**
540
+ * Cell size for a given lane geometry, WITHOUT constructing anything — so a
541
+ * layout tool (or a half-adder) can place gates before it builds them.
542
+ * @param {number} bore @param {number} wallThickness @param {number} [length=48]
543
+ * @returns {{w:number, h:number, pitch:number, area:number}}
544
+ */
545
+ LogicGate.cellSize = function (bore, wallThickness, length) {
546
+ var TraceCls = _Trace;
547
+ var pitch = TraceCls.lanePitch(bore, wallThickness);
548
+ var len = (length !== undefined) ? length : 48;
549
+ return { w: len, h: pitch * 3, pitch: pitch, area: len * pitch * 3 };
550
+ };
551
+
552
+ // ── The kind (objects/kinds.js): descriptor, construction defaults, palette, builder ──────────
553
+
554
+ /**
555
+ * LogicGate — the V2 stock switching part (the successor named in
556
+ * V2_PARTS_CLASSIFICATION.md §4.4 for CircuitGate): two `Wall{destroy,count}`
557
+ * ammeter terminals, a mirror-`Tunnel` output channel carrying a
558
+ * `SphereEmitter` supply rail, and a `Wall{destroy}` shunt valve.
559
+ * Constructor: LogicGate(universe, { logic, left, top, bore, wallThickness,
560
+ * length, supplyVoltage, thresholdHigh, thresholdLow,
561
+ * windowTicks, color, name })
562
+ * Group wrapper: every inner Wall/Tunnel wall and the supply emitter are
563
+ * stamped with the ONE up-link `_owner` = <this>.
564
+ */
565
+ var LogicGateDescriptor = {
566
+ description: 'A powered logic gate assembled from stock: two counting-Wall ammeter ' +
567
+ 'terminals sense the current arriving on two input lanes, a comparator with ' +
568
+ 'hysteresis thresholds them into booleans, and one shunt valve either lets the ' +
569
+ 'gate\'s own supply rail reach the outlet or dumps it. Because the output is the ' +
570
+ 'gate\'s OWN rail, its HIGH level is independent of the input levels, so gates ' +
571
+ 'cascade without per-stage retuning. Zero physics of its own.',
572
+ // A gate cell has a real placed extent, but its internal lane geometry is
573
+ // derived — resizing it by dragging would silently break the lane pitch.
574
+ ux: {
575
+ resizable: false,
576
+ draggable: { '2d': true },
577
+ shellRender: 'run'
578
+ },
579
+ fields: [
580
+ {
581
+ name: 'logic',
582
+ type: 'enum',
583
+ // The vocabulary's OWNER is TRUTH (LogicGate.TRUTH / LogicGate.LOGICS) — this
584
+ // descriptor registers from the same module, so it reads the owner.
585
+ constraint: { values: Object.fromEntries(Object.keys(TRUTH).map(function (k) { return [k, k]; })) },
586
+ engineVisible: false,
587
+ live: true,
588
+ description: 'The comparator row applied to the two sensed inputs. DATA, not geometry: every row builds the identical hardware and differs only in this function, so an inverting gate (NAND/NOR/XNOR) costs exactly what a non-inverting one costs here — unlike silicon.'
589
+ },
590
+ {
591
+ name: 'left',
592
+ type: 'number',
593
+ constraint: { integer: true },
594
+ engineVisible: true,
595
+ unit: 'px',
596
+ description: 'Gate cell left X position (physics pixels). The two input terminal faces and the output-channel inlet all sit on this edge.'
597
+ },
598
+ {
599
+ name: 'top',
600
+ type: 'number',
601
+ constraint: { integer: true },
602
+ engineVisible: true,
603
+ unit: 'px',
604
+ description: 'Gate cell top Y position (physics pixels). Input A rides the first lane, the powered output the middle lane, input B the third.'
605
+ },
606
+ {
607
+ name: 'bore',
608
+ type: 'number',
609
+ constraint: { min: 1, integer: true },
610
+ engineVisible: true,
611
+ unit: 'px',
612
+ description: 'Lane aperture in physics pixels — the same bore the Traces wired to this gate should use. Sets the lane pitch (bore + 2 x wallThickness) and therefore the whole cell height.'
613
+ },
614
+ {
615
+ name: 'wallThickness',
616
+ type: 'number',
617
+ constraint: { min: 1, integer: true },
618
+ engineVisible: true,
619
+ unit: 'px',
620
+ description: 'Lane insulation in physics pixels. This is the knob that sets the shrink floor: too thin and the channel mirrors go transparent, which reads as a dead gate rather than a noisy one.'
621
+ },
622
+ {
623
+ name: 'length',
624
+ type: 'number',
625
+ constraint: { min: 1, integer: true },
626
+ engineVisible: true,
627
+ unit: 'px',
628
+ description: 'Output-channel X extent in physics pixels — the cell width. The measured floor is 20px (below that the supply inlet and the shunt valve overlap).'
629
+ },
630
+ {
631
+ name: 'supplyVoltage',
632
+ type: 'number',
633
+ constraint: { min: 0, integer: true },
634
+ engineVisible: false,
635
+ live: true,
636
+ unit: 'quanta/tick',
637
+ description: 'The supply rail: emitter burst per tick, and therefore the gate\'s HIGH output level in quanta per tick. Raising it raises the output level and (unless the thresholds are pinned) the switching thresholds with it, so the gate stays correctly biased at any rail.'
638
+ },
639
+ {
640
+ name: 'thresholdHigh',
641
+ type: 'number',
642
+ constraint: { min: 0 },
643
+ engineVisible: false,
644
+ live: true,
645
+ unit: 'quanta/tick',
646
+ description: 'Input current at or above which an input is called HIGH. Defaults to half the rail. Because the sensed current is a counting process, an input must sit several standard deviations clear of this value to be reliably read — see the part datasheet for the measured margin.'
647
+ },
648
+ {
649
+ name: 'thresholdLow',
650
+ type: 'number',
651
+ constraint: { min: 0 },
652
+ engineVisible: false,
653
+ live: true,
654
+ unit: 'quanta/tick',
655
+ description: 'Input current below which a HIGH input reverts to LOW. Defaults to a quarter of the rail. The gap up to thresholdHigh IS the hysteresis — it is what stops an input sitting near its threshold from chattering on shot noise.'
656
+ },
657
+ {
658
+ name: 'windowTicks',
659
+ type: 'number',
660
+ constraint: { min: 1, integer: true },
661
+ engineVisible: false,
662
+ live: true,
663
+ unit: 'ticks',
664
+ description: 'Sliding window over which the input current is averaged. It sets BOTH the sense noise (which falls as 1/sqrt(window)) and half the switching delay — a longer window is a steadier, slower gate.'
665
+ },
666
+ {
667
+ name: 'color',
668
+ type: 'rgb',
669
+ engineVisible: false,
670
+ live: true,
671
+ description: 'Render color [r, g, b] for the output-channel mirrors (render-only). A live edit fans out to both mirrors via the `_owner` fan-out in updateObjectProperty.'
672
+ }
673
+ ]
674
+ };
675
+
676
+ registerKind(LogicGate, {
677
+ from: import.meta.url,
678
+ descriptor: LogicGateDescriptor,
679
+ defaults: {
680
+ // A palette drop makes an AND gate on the densest lane geometry that was
681
+ // MEASURED to hold the full truth table (bore 3 / wall 3 / length 20 —
682
+ // a 20x27 px cell). `left`/`top` are intentionally absent: the
683
+ // ObjectFactory builder derives them from the drop point (F1 drop idiom).
684
+ logic: 'AND',
685
+ bore: 3,
686
+ wallThickness: 3,
687
+ length: 20,
688
+ supplyVoltage: 6,
689
+ windowTicks: 20,
690
+ color: [200, 170, 70]
691
+ },
692
+ palette: { displayName: 'Logic Gate', category: 'circuit', iconHint: 'gate', rank: 5 },
693
+ build: function (props, ctx, Class) {
694
+ var GateCls = Class; // the class that registered (a composed fork's own class)
695
+ var opts = Object.assign({}, props);
696
+ var bore = (opts.bore != null) ? opts.bore : 3;
697
+ var wt = (opts.wallThickness != null) ? opts.wallThickness : 3;
698
+ var len = (opts.length != null) ? opts.length : 20;
699
+ // Derive the cell origin from the drop point when the recipe declared no
700
+ // placement (F1 drop idiom — same as Wall/Tunnel). The cell is three lanes
701
+ // tall, so centring means backing off half of 3 x (bore + 2 x wallThickness).
702
+ if (opts.left == null) { opts.left = Math.round(ctx.x - len / 2); }
703
+ if (opts.top == null) { opts.top = Math.round(ctx.y - 1.5 * (bore + 2 * wt)); }
704
+ opts.bore = bore; opts.wallThickness = wt; opts.length = len;
705
+ var gate = new GateCls(ctx.runner.universe, opts);
706
+ gate._schemaKind = Class.name;
707
+ // The comparator is a HOST tick-fn: subscribe it to the runner's per-tick
708
+ // fan-out here, off the DECLARED fields, so the switching survives
709
+ // codegen → Restart (a recipe-authored tick-fn would be lost). Idempotent.
710
+ if (typeof gate.attachToRunner === 'function') { gate.attachToRunner(ctx.runner); }
711
+ return gate;
712
+ }
713
+ });
714
+
715
+ // ── Export ───────────────────────────────────────────────────────────────
716
+ export { LogicGate };
717
+