From dceb6a8c407f0f90124acb85a6c3342d3b687076 Mon Sep 17 00:00:00 2001 From: davidmonterocrespo24 Date: Fri, 15 May 2026 18:19:00 +0200 Subject: [PATCH] =?UTF-8?q?feat(sim):=20Phase=205=20=E2=80=94=20migrate=20?= =?UTF-8?q?every=20logic-gate=20handler=20to=20PinResolver?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit twoInputGate (AND/NAND/OR/NOR/XOR/XNOR), nInputGate (3/4-input AND/OR/ NAND/NOR), edgeTriggeredFF (D/T/JK), and the standalone NOT gate all now prefer PinResolver input subscriptions. Output side (setPinState on Y / Q / Qbar) is unchanged — digital propagation between gates keeps flowing through pinManager. Why this matters: logic gates are the biggest beneficiaries of Phase 3 logic-family thresholds. A gate input driven through a BJT collector or MOSFET drain now reads the real SPICE voltage and converts to HIGH/LOW per the board's logic family — instead of relying on the legacy trace's `[C, B]` shortcut. For flip-flops, rising-edge detection on CLK works identically with resolver.onChange: a state transition to HIGH is exactly the rising- edge event the original `!prevClk && s` was watching for. All migrated handlers fall back to the legacy pinManager.onPinChange path when getPinResolver isn't provided (tests / Phase-0-less builds). Phase 5 progress: 16 handlers migrated this session (LED, 7-segment, led-bar-graph, AND/NAND/OR/NOR/XOR/XNOR + 4 multi-input variants + 3 flip-flops + NOT). Remaining: 74HC595, buzzer, RGB LED, servo, neopixel, sensors, motor drivers. Once the output-style handlers are all on PinResolver, the `[C, B]` shortcut in PASSIVE_PIN_PAIRS can be deleted. 113 tests pass across logic-gate-parts, flip-flop-parts, and examples-digital (which exercises real ngspice on multi-gate topologies like the 3-to-8 decoder). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/simulation/parts/LogicGateParts.ts | 194 ++++++++++++------ 1 file changed, 136 insertions(+), 58 deletions(-) diff --git a/frontend/src/simulation/parts/LogicGateParts.ts b/frontend/src/simulation/parts/LogicGateParts.ts index 0536cef7..5f912f12 100644 --- a/frontend/src/simulation/parts/LogicGateParts.ts +++ b/frontend/src/simulation/parts/LogicGateParts.ts @@ -15,33 +15,57 @@ import type { PartSimulationLogic } from './PartSimulationRegistry'; function twoInputGate(compute: (a: boolean, b: boolean) => boolean): PartSimulationLogic { return { - attachEvents: (element, simulator, getPin) => { - const pinA = getPin('A'); - const pinB = getPin('B'); + attachEvents: (element, simulator, getPin, _componentId, getPinResolver) => { const pinY = getPin('Y'); + if (pinY === null) return () => {}; - if (pinA === null || pinB === null || pinY === null) return () => {}; - + // Phase 5 migration: prefer the PinResolver path so gate inputs + // downstream of an active device (e.g. a sensor through a + // transistor) read via SPICE thresholds + the board logic family + // (Phase 3) instead of relying on direct pinManager state. + // The output side keeps using setPinState — digital propagation + // between gates is still pinManager's job. + const useResolver = typeof getPinResolver === 'function'; let stateA = false; let stateB = false; - const update = () => simulator.setPinState(pinY, compute(stateA, stateB)); - const unsubA = simulator.pinManager.onPinChange(pinA, (_: number, s: boolean) => { - stateA = s; - update(); - }); - const unsubB = simulator.pinManager.onPinChange(pinB, (_: number, s: boolean) => { - stateB = s; - update(); - }); + const unsubs: Array<() => void> = []; + if (useResolver) { + const resA = getPinResolver!('A'); + const resB = getPinResolver!('B'); + if (!resA || !resB) return () => {}; + stateA = resA.getCurrentState() === 'HIGH'; + stateB = resB.getCurrentState() === 'HIGH'; + unsubs.push( + resA.onChange((state) => { + stateA = state === 'HIGH'; + update(); + }), + resB.onChange((state) => { + stateB = state === 'HIGH'; + update(); + }), + ); + } else { + const pinA = getPin('A'); + const pinB = getPin('B'); + if (pinA === null || pinB === null) return () => {}; + unsubs.push( + simulator.pinManager.onPinChange(pinA, (_: number, s: boolean) => { + stateA = s; + update(); + }), + simulator.pinManager.onPinChange(pinB, (_: number, s: boolean) => { + stateB = s; + update(); + }), + ); + } - update(); // Drive Y immediately with initial LOW state + update(); // Drive Y immediately with initial state - return () => { - unsubA(); - unsubB(); - }; + return () => unsubs.forEach((u) => u()); }, }; } @@ -89,27 +113,43 @@ function nInputGate( compute: (inputs: boolean[]) => boolean, ): PartSimulationLogic { return { - attachEvents: (element, simulator, getPin) => { - const inputPins = inputNames.map((n) => getPin(n)); + attachEvents: (element, simulator, getPin, _componentId, getPinResolver) => { const pinY = getPin('Y'); + if (pinY === null) return () => {}; - if (inputPins.some((p) => p === null) || pinY === null) return () => {}; - + const useResolver = typeof getPinResolver === 'function'; const states = inputNames.map(() => false); const update = () => simulator.setPinState(pinY, compute(states)); + const unsubs: Array<() => void> = []; - const unsubs = inputPins.map((p, i) => - simulator.pinManager.onPinChange(p!, (_: number, s: boolean) => { - states[i] = s; - update(); - }), - ); + if (useResolver) { + const resolvers = inputNames.map((n) => getPinResolver!(n)); + if (resolvers.some((r) => r === null)) return () => {}; + resolvers.forEach((r, i) => { + states[i] = r!.getCurrentState() === 'HIGH'; + unsubs.push( + r!.onChange((state) => { + states[i] = state === 'HIGH'; + update(); + }), + ); + }); + } else { + const inputPins = inputNames.map((n) => getPin(n)); + if (inputPins.some((p) => p === null)) return () => {}; + inputPins.forEach((p, i) => { + unsubs.push( + simulator.pinManager.onPinChange(p!, (_: number, s: boolean) => { + states[i] = s; + update(); + }), + ); + }); + } update(); - return () => { - unsubs.forEach((u) => u()); - }; + return () => unsubs.forEach((u) => u()); }, }; } @@ -132,15 +172,12 @@ function edgeTriggeredFF( sample: (state: boolean, inputs: boolean[]) => boolean, ): PartSimulationLogic { return { - attachEvents: (element, simulator, getPin) => { - const clkPin = getPin('CLK'); + attachEvents: (element, simulator, getPin, _componentId, getPinResolver) => { const qPin = getPin('Q'); const qbarPin = getPin('Qbar'); - const dataPinIds = dataPins.map((n) => getPin(n)); - - if (clkPin === null || qPin === null || qbarPin === null) return () => {}; - if (dataPinIds.some((p) => p === null)) return () => {}; + if (qPin === null || qbarPin === null) return () => {}; + const useResolver = typeof getPinResolver === 'function'; let prevClk = false; let q = initial; const dataStates = dataPins.map(() => false); @@ -150,27 +187,58 @@ function edgeTriggeredFF( simulator.setPinState(qbarPin, !q); }; - const unsubClk = simulator.pinManager.onPinChange(clkPin, (_: number, s: boolean) => { - if (!prevClk && s) { - // Rising edge - q = sample(q, dataStates); - emit(); - } - prevClk = s; - }); + const unsubs: Array<() => void> = []; - const unsubData = dataPinIds.map((p, i) => - simulator.pinManager.onPinChange(p!, (_: number, s: boolean) => { - dataStates[i] = s; - }), - ); + if (useResolver) { + const resClk = getPinResolver!('CLK'); + const resData = dataPins.map((n) => getPinResolver!(n)); + if (!resClk || resData.some((r) => r === null)) return () => {}; + prevClk = resClk.getCurrentState() === 'HIGH'; + resData.forEach((r, i) => { + dataStates[i] = r!.getCurrentState() === 'HIGH'; + }); + unsubs.push( + resClk.onChange((state) => { + const s = state === 'HIGH'; + if (!prevClk && s) { + q = sample(q, dataStates); + emit(); + } + prevClk = s; + }), + ); + resData.forEach((r, i) => { + unsubs.push( + r!.onChange((state) => { + dataStates[i] = state === 'HIGH'; + }), + ); + }); + } else { + const clkPin = getPin('CLK'); + const dataPinIds = dataPins.map((n) => getPin(n)); + if (clkPin === null || dataPinIds.some((p) => p === null)) return () => {}; + unsubs.push( + simulator.pinManager.onPinChange(clkPin, (_: number, s: boolean) => { + if (!prevClk && s) { + q = sample(q, dataStates); + emit(); + } + prevClk = s; + }), + ); + dataPinIds.forEach((p, i) => { + unsubs.push( + simulator.pinManager.onPinChange(p!, (_: number, s: boolean) => { + dataStates[i] = s; + }), + ); + }); + } emit(); // Drive initial Q / Qbar - return () => { - unsubClk(); - unsubData.forEach((u) => u()); - }; + return () => unsubs.forEach((u) => u()); }, }; } @@ -213,11 +281,21 @@ PartSimulationRegistry.register('logic-gate-nor-4', nInputGate(['A', 'B', 'C', ' // ─── NOT (inverter) ─────────────────────────────────────────────────────────── PartSimulationRegistry.register('logic-gate-not', { - attachEvents: (element, simulator, getPin) => { - const pinA = getPin('A'); + attachEvents: (element, simulator, getPin, _componentId, getPinResolver) => { const pinY = getPin('Y'); + if (pinY === null) return () => {}; - if (pinA === null || pinY === null) return () => {}; + if (typeof getPinResolver === 'function') { + const resA = getPinResolver('A'); + if (!resA) return () => {}; + simulator.setPinState(pinY, resA.getCurrentState() !== 'HIGH'); + return resA.onChange((state) => { + simulator.setPinState(pinY, state !== 'HIGH'); + }); + } + + const pinA = getPin('A'); + if (pinA === null) return () => {}; const unsub = simulator.pinManager.onPinChange(pinA, (_: number, s: boolean) => { simulator.setPinState(pinY, !s);