feat(sim): Phase 5 — migrate every logic-gate handler to PinResolver
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) <noreply@anthropic.com>
This commit is contained in:
parent
29bb8af6f6
commit
dceb6a8c40
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue