309 lines
11 KiB
TypeScript
309 lines
11 KiB
TypeScript
/**
|
|
* LogicGateParts.ts — Simulation logic for logic gate components.
|
|
*
|
|
* All gates listen to their input pins via pinManager.onPinChange,
|
|
* compute the boolean output, and drive the Y pin accordingly.
|
|
*
|
|
* 2-input gates: A, B → Y
|
|
* NOT gate: A → Y
|
|
*/
|
|
|
|
import { PartSimulationRegistry } from './PartSimulationRegistry';
|
|
import type { PartSimulationLogic } from './PartSimulationRegistry';
|
|
|
|
// ─── Helper ───────────────────────────────────────────────────────────────────
|
|
|
|
function twoInputGate(compute: (a: boolean, b: boolean) => boolean): PartSimulationLogic {
|
|
return {
|
|
attachEvents: (element, simulator, getPin, _componentId, getPinResolver) => {
|
|
const pinY = getPin('Y');
|
|
if (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 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 state
|
|
|
|
return () => unsubs.forEach((u) => u());
|
|
},
|
|
};
|
|
}
|
|
|
|
// ─── AND ──────────────────────────────────────────────────────────────────────
|
|
PartSimulationRegistry.register(
|
|
'logic-gate-and',
|
|
twoInputGate((a, b) => a && b),
|
|
);
|
|
|
|
// ─── NAND ─────────────────────────────────────────────────────────────────────
|
|
PartSimulationRegistry.register(
|
|
'logic-gate-nand',
|
|
twoInputGate((a, b) => !(a && b)),
|
|
);
|
|
|
|
// ─── OR ───────────────────────────────────────────────────────────────────────
|
|
PartSimulationRegistry.register(
|
|
'logic-gate-or',
|
|
twoInputGate((a, b) => a || b),
|
|
);
|
|
|
|
// ─── NOR ──────────────────────────────────────────────────────────────────────
|
|
PartSimulationRegistry.register(
|
|
'logic-gate-nor',
|
|
twoInputGate((a, b) => !(a || b)),
|
|
);
|
|
|
|
// ─── XOR ──────────────────────────────────────────────────────────────────────
|
|
PartSimulationRegistry.register(
|
|
'logic-gate-xor',
|
|
twoInputGate((a, b) => a !== b),
|
|
);
|
|
|
|
// ─── XNOR ─────────────────────────────────────────────────────────────────────
|
|
PartSimulationRegistry.register(
|
|
'logic-gate-xnor',
|
|
twoInputGate((a, b) => a === b),
|
|
);
|
|
|
|
// ─── Multi-input gates (3 / 4 inputs) ─────────────────────────────────────────
|
|
|
|
function nInputGate(
|
|
inputNames: string[],
|
|
compute: (inputs: boolean[]) => boolean,
|
|
): PartSimulationLogic {
|
|
return {
|
|
attachEvents: (element, simulator, getPin, _componentId, getPinResolver) => {
|
|
const pinY = getPin('Y');
|
|
if (pinY === null) return () => {};
|
|
|
|
const useResolver = typeof getPinResolver === 'function';
|
|
const states = inputNames.map(() => false);
|
|
const update = () => simulator.setPinState(pinY, compute(states));
|
|
const unsubs: Array<() => void> = [];
|
|
|
|
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());
|
|
},
|
|
};
|
|
}
|
|
|
|
const allTrue = (xs: boolean[]) => xs.every(Boolean);
|
|
const anyTrue = (xs: boolean[]) => xs.some(Boolean);
|
|
const notAll = (xs: boolean[]) => !allTrue(xs);
|
|
const notAny = (xs: boolean[]) => !anyTrue(xs);
|
|
|
|
// ─── Flip-flops (edge-triggered, digital-sim only) ────────────────────────────
|
|
// SPICE mode cannot simulate real edge detection at DC; these components are
|
|
// therefore digital-only and do not emit a SPICE mapper.
|
|
//
|
|
// Each FF samples its data inputs on the rising edge of CLK. Q and Qbar are
|
|
// driven synchronously.
|
|
|
|
function edgeTriggeredFF(
|
|
dataPins: string[],
|
|
initial: boolean,
|
|
sample: (state: boolean, inputs: boolean[]) => boolean,
|
|
): PartSimulationLogic {
|
|
return {
|
|
attachEvents: (element, simulator, getPin, _componentId, getPinResolver) => {
|
|
const qPin = getPin('Q');
|
|
const qbarPin = getPin('Qbar');
|
|
if (qPin === null || qbarPin === null) return () => {};
|
|
|
|
const useResolver = typeof getPinResolver === 'function';
|
|
let prevClk = false;
|
|
let q = initial;
|
|
const dataStates = dataPins.map(() => false);
|
|
|
|
const emit = () => {
|
|
simulator.setPinState(qPin, q);
|
|
simulator.setPinState(qbarPin, !q);
|
|
};
|
|
|
|
const unsubs: Array<() => void> = [];
|
|
|
|
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 () => unsubs.forEach((u) => u());
|
|
},
|
|
};
|
|
}
|
|
|
|
// D flip-flop: Q ← D on rising CLK
|
|
PartSimulationRegistry.register(
|
|
'flip-flop-d',
|
|
edgeTriggeredFF(['D'], false, (_q, [d]) => d),
|
|
);
|
|
|
|
// T flip-flop: Q ← Q ⊕ T on rising CLK (toggle when T=1)
|
|
PartSimulationRegistry.register(
|
|
'flip-flop-t',
|
|
edgeTriggeredFF(['T'], false, (q, [t]) => (t ? !q : q)),
|
|
);
|
|
|
|
// JK flip-flop:
|
|
// J=0, K=0 → hold
|
|
// J=1, K=0 → set (Q=1)
|
|
// J=0, K=1 → reset (Q=0)
|
|
// J=1, K=1 → toggle
|
|
PartSimulationRegistry.register(
|
|
'flip-flop-jk',
|
|
edgeTriggeredFF(['J', 'K'], false, (q, [j, k]) => {
|
|
if (j && k) return !q;
|
|
if (j) return true;
|
|
if (k) return false;
|
|
return q;
|
|
}),
|
|
);
|
|
|
|
PartSimulationRegistry.register('logic-gate-and-3', nInputGate(['A', 'B', 'C'], allTrue));
|
|
PartSimulationRegistry.register('logic-gate-or-3', nInputGate(['A', 'B', 'C'], anyTrue));
|
|
PartSimulationRegistry.register('logic-gate-nand-3', nInputGate(['A', 'B', 'C'], notAll));
|
|
PartSimulationRegistry.register('logic-gate-nor-3', nInputGate(['A', 'B', 'C'], notAny));
|
|
PartSimulationRegistry.register('logic-gate-and-4', nInputGate(['A', 'B', 'C', 'D'], allTrue));
|
|
PartSimulationRegistry.register('logic-gate-or-4', nInputGate(['A', 'B', 'C', 'D'], anyTrue));
|
|
PartSimulationRegistry.register('logic-gate-nand-4', nInputGate(['A', 'B', 'C', 'D'], notAll));
|
|
PartSimulationRegistry.register('logic-gate-nor-4', nInputGate(['A', 'B', 'C', 'D'], notAny));
|
|
|
|
// ─── NOT (inverter) ───────────────────────────────────────────────────────────
|
|
PartSimulationRegistry.register('logic-gate-not', {
|
|
attachEvents: (element, simulator, getPin, _componentId, getPinResolver) => {
|
|
const pinY = getPin('Y');
|
|
if (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);
|
|
});
|
|
|
|
simulator.setPinState(pinY, true); // NOT LOW = HIGH (initial LOW input → HIGH output)
|
|
|
|
return unsub;
|
|
},
|
|
});
|