feat: add new components and simulations for logic gates, protocols, and sensors
- Added LogicGateParts.ts for simulating various logic gates (AND, NAND, OR, NOR, XOR, NOT).
- Introduced ProtocolParts.ts for simulating I2C and SPI components including SSD1306 OLED, DS1307 RTC, MPU6050 IMU, DHT22 sensor, HX711 load cell, IR receiver, IR remote, and MicroSD card.
- Implemented BasicParts.ts with a membrane keypad and rotary dialer simulations.
- Enhanced SensorParts.ts with a single NeoPixel and PIR motion sensor.
- Updated index.ts to include new parts for logic gates and protocols.
- Modified vite-env.d.ts to declare new custom elements for the added components.
2026-03-09 02:14:03 +07:00
|
|
|
/**
|
|
|
|
|
* 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
|
|
|
|
|
*/
|
|
|
|
|
|
2026-03-15 02:57:22 +07:00
|
|
|
import { PartSimulationRegistry } from './PartSimulationRegistry';
|
|
|
|
|
import type { PartSimulationLogic } from './PartSimulationRegistry';
|
feat: add new components and simulations for logic gates, protocols, and sensors
- Added LogicGateParts.ts for simulating various logic gates (AND, NAND, OR, NOR, XOR, NOT).
- Introduced ProtocolParts.ts for simulating I2C and SPI components including SSD1306 OLED, DS1307 RTC, MPU6050 IMU, DHT22 sensor, HX711 load cell, IR receiver, IR remote, and MicroSD card.
- Implemented BasicParts.ts with a membrane keypad and rotary dialer simulations.
- Enhanced SensorParts.ts with a single NeoPixel and PIR motion sensor.
- Updated index.ts to include new parts for logic gates and protocols.
- Modified vite-env.d.ts to declare new custom elements for the added components.
2026-03-09 02:14:03 +07:00
|
|
|
|
|
|
|
|
// ─── Helper ───────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function twoInputGate(
|
|
|
|
|
compute: (a: boolean, b: boolean) => boolean,
|
|
|
|
|
): PartSimulationLogic {
|
|
|
|
|
return {
|
|
|
|
|
attachEvents: (element, simulator, getPin) => {
|
|
|
|
|
const pinA = getPin('A');
|
|
|
|
|
const pinB = getPin('B');
|
|
|
|
|
const pinY = getPin('Y');
|
|
|
|
|
|
|
|
|
|
if (pinA === null || pinB === null || pinY === null) return () => {};
|
|
|
|
|
|
|
|
|
|
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();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
update(); // Drive Y immediately with initial LOW state
|
|
|
|
|
|
|
|
|
|
return () => { unsubA(); unsubB(); };
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── 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));
|
|
|
|
|
|
feat: expand SPICE component catalog (fases 9 + 10)
Adds 44 SPICE mappers, 58 custom metadata entries, and 12 visual
Web Components covering logic gates, transistors, op-amps, regulators,
sources, electromechanical parts and integrated-circuit packaging.
Fase 9 — component catalog expansion
------------------------------------
- 7 logic gates (AND/OR/NAND/NOR/XOR/XNOR + NOT) as SPICE B-sources
- 8 multi-input gates (AND/OR/NAND/NOR with 3 and 4 inputs)
- 9 transistors: 5 BJTs (incl. PNP 2N3906/BC557) + 4 MOSFETs (incl.
P-channel IRF9540/FQP27P06). NMOS refactored from Level=3 W=0.1
(hangs ngspice) to Level=1 with sane W/L
- 5 op-amps: LM358, LM741, TL072, LM324 with per-chip saturation
rails + opamp-ideal
- 4 linear regulators (7805, 7812, 7905, LM317) with dropout
- 3 batteries (9V, AA, coin-cell) with realistic ESR
- Signal generator (sine / square / DC)
- 2 Schottky diodes (1N5817, 1N5819) + photodiode (lux-driven
current source)
Fase 10 — electromechanical + ICs
---------------------------------
- Relay (SPDT): coil + L + S-switch with native hysteresis +
flyback diode, inverted-control trick for the NC contact
- Optocouplers 4N25 and PC817 (LED + CCCS with CTR=0.5 / 1.0)
- 7 74HC ICs as DIP-14 packages emitting 4 or 6 B-sources per
component (first mapper pattern emitting multiple device cards)
- 3 flip-flops (D, T, JK) — digital-sim only (edge detection is
not representable in ngspice .op)
- L293D dual H-bridge motor driver
Infrastructure
--------------
- scripts/component-overrides.json gains a _customComponents[] array
that lets new Velxio-only parts survive metadata regeneration
(previously applyOverrides() could only patch wokwi-elements
components that had already been scanned)
- scripts/generate-component-metadata.ts injects custom entries
before the patch loop
- New ComponentCategory values: 'logic', 'analog', 'electromech'
- frontend/src/components/DynamicComponent.tsx PASSIVE tracing
extended from just ['resistor','resistor-us'] to 9 two-terminal
passives with per-part pin name maps
- New CI workflow test-circuit.yml runs the sandbox on push/PR
- frontend-tests.yml regenerates metadata and fails if committed
JSON is stale
- Documented 2 new ngspice gotchas in circuit-emulation-gotchas.md:
unicode in netlist titles silently hangs the parser, and
MOSFET Level=3 + W=0.1m causes .op to hang
- 164/164 sandbox tests passing in ~9 s (was 88 pre-fase-9)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 06:44:18 +07:00
|
|
|
// ─── 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) => {
|
|
|
|
|
const inputPins = inputNames.map(n => getPin(n));
|
|
|
|
|
const pinY = getPin('Y');
|
|
|
|
|
|
|
|
|
|
if (inputPins.some(p => p === null) || pinY === null) return () => {};
|
|
|
|
|
|
|
|
|
|
const states = inputNames.map(() => false);
|
|
|
|
|
const update = () => simulator.setPinState(pinY, compute(states));
|
|
|
|
|
|
|
|
|
|
const unsubs = inputPins.map((p, i) =>
|
|
|
|
|
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) => {
|
|
|
|
|
const clkPin = getPin('CLK');
|
|
|
|
|
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 () => {};
|
|
|
|
|
|
|
|
|
|
let prevClk = false;
|
|
|
|
|
let q = initial;
|
|
|
|
|
const dataStates = dataPins.map(() => false);
|
|
|
|
|
|
|
|
|
|
const emit = () => {
|
|
|
|
|
simulator.setPinState(qPin, q);
|
|
|
|
|
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 unsubData = dataPinIds.map((p, i) =>
|
|
|
|
|
simulator.pinManager.onPinChange(p!, (_: number, s: boolean) => {
|
|
|
|
|
dataStates[i] = s;
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
emit(); // Drive initial Q / Qbar
|
|
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
|
unsubClk();
|
|
|
|
|
unsubData.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));
|
|
|
|
|
|
feat: add new components and simulations for logic gates, protocols, and sensors
- Added LogicGateParts.ts for simulating various logic gates (AND, NAND, OR, NOR, XOR, NOT).
- Introduced ProtocolParts.ts for simulating I2C and SPI components including SSD1306 OLED, DS1307 RTC, MPU6050 IMU, DHT22 sensor, HX711 load cell, IR receiver, IR remote, and MicroSD card.
- Implemented BasicParts.ts with a membrane keypad and rotary dialer simulations.
- Enhanced SensorParts.ts with a single NeoPixel and PIR motion sensor.
- Updated index.ts to include new parts for logic gates and protocols.
- Modified vite-env.d.ts to declare new custom elements for the added components.
2026-03-09 02:14:03 +07:00
|
|
|
// ─── NOT (inverter) ───────────────────────────────────────────────────────────
|
|
|
|
|
PartSimulationRegistry.register('logic-gate-not', {
|
|
|
|
|
attachEvents: (element, simulator, getPin) => {
|
|
|
|
|
const pinA = getPin('A');
|
|
|
|
|
const pinY = getPin('Y');
|
|
|
|
|
|
|
|
|
|
if (pinA === null || pinY === 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;
|
|
|
|
|
},
|
|
|
|
|
});
|