feat: add capacitor and inductor components, update netlist builder to return pinNetMap

This commit is contained in:
David Montero Crespo 2026-04-17 19:27:18 -03:00
parent 64f6f76160
commit 61c1ddfc22
12 changed files with 341 additions and 64 deletions

View File

@ -1,6 +1,6 @@
{
"version": "1.0.0",
"generatedAt": "2026-04-16T04:07:07.823Z",
"generatedAt": "2026-04-17T22:10:37.555Z",
"components": [
{
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\">\n <rect width=\"64\" height=\"64\" fill=\"#e0e0e0\" rx=\"4\"/>\n <text x=\"50%\" y=\"50%\" text-anchor=\"middle\" dy=\".3em\" font-size=\"10\" fill=\"#666\">\n DIODE-1N4007\n </text>\n </svg>",
@ -2853,6 +2853,29 @@
"led"
]
},
{
"id": "capacitor",
"tagName": "wokwi-capacitor",
"name": "Capacitor",
"category": "passive",
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\">\n <rect width=\"64\" height=\"64\" fill=\"#e0e0e0\" rx=\"4\"/>\n <text x=\"50%\" y=\"50%\" text-anchor=\"middle\" dy=\".3em\" font-size=\"10\" fill=\"#666\">\n CAPACITOR\n </text>\n </svg>",
"properties": [
{
"name": "value",
"type": "string",
"defaultValue": "1u",
"control": "text"
}
],
"defaultValues": {
"value": "1u"
},
"pinCount": 0,
"tags": [
"capacitor",
""
]
},
{
"id": "franzininho",
"tagName": "wokwi-franzininho",
@ -2889,6 +2912,29 @@
"franzininho"
]
},
{
"id": "inductor",
"tagName": "wokwi-inductor",
"name": "Inductor",
"category": "passive",
"thumbnail": "<svg width=\"64\" height=\"64\" xmlns=\"http://www.w3.org/2000/svg\">\n <rect width=\"64\" height=\"64\" fill=\"#e0e0e0\" rx=\"4\"/>\n <text x=\"50%\" y=\"50%\" text-anchor=\"middle\" dy=\".3em\" font-size=\"10\" fill=\"#666\">\n INDUCTOR\n </text>\n </svg>",
"properties": [
{
"name": "value",
"type": "string",
"defaultValue": "1m",
"control": "text"
}
],
"defaultValues": {
"value": "1m"
},
"pinCount": 0,
"tags": [
"inductor",
""
]
},
{
"id": "ir-receiver",
"tagName": "wokwi-ir-receiver",

View File

@ -172,7 +172,7 @@ describe('componentToSpice — ngspice accepts every card', () => {
});
extraLoadCards.push(`R_load_${i} n_probe_${i} 0 1Meg`);
}
const netlist = buildNetlist({
const { netlist } = buildNetlist({
components: [
{ id: 'dut', metadataId: id, properties: fx.properties ?? {} },
...Array.from({ length: pins.length - 2 }, (_, i) => ({

View File

@ -10,7 +10,7 @@ import { runNetlist } from '../simulation/spice/SpiceEngine';
describe('Voltmeter (instr-voltmeter)', () => {
it('reads ~3.33V across the midpoint of a 1k+2k divider', { timeout: 30_000 }, async () => {
const netlist = buildNetlist({
const { netlist } = buildNetlist({
components: [
{ id: 'r1', metadataId: 'resistor', properties: { value: '1k' } },
{ id: 'r2', metadataId: 'resistor', properties: { value: '2k' } },
@ -48,7 +48,7 @@ describe('Ammeter (instr-ammeter)', () => {
it('reads ~22.7 mA through a 220 Ω load on 5V', { timeout: 30_000 }, async () => {
// Circuit:
// +5V ── R (220Ω) ── [A+ ammeter A-] ── GND
const netlist = buildNetlist({
const { netlist } = buildNetlist({
components: [
{ id: 'r1', metadataId: 'resistor', properties: { value: '220' } },
{ id: 'am', metadataId: 'instr-ammeter', properties: {} },

View File

@ -49,7 +49,7 @@ describe('parseValueWithUnits', () => {
describe('NetlistBuilder — simple cases', () => {
it('emits a voltage divider and ngspice gets 6V', { timeout: 30_000 }, async () => {
const netlist = buildNetlist({
const { netlist } = buildNetlist({
components: [
{ id: 'r1', metadataId: 'resistor', properties: { value: '1k' } },
{ id: 'r2', metadataId: 'resistor', properties: { value: '2k' } },
@ -86,7 +86,7 @@ describe('NetlistBuilder — simple cases', () => {
});
it('emits an LED + resistor from 5V rail and ngspice solves non-linear', { timeout: 30_000 }, async () => {
const netlist = buildNetlist({
const { netlist } = buildNetlist({
components: [
{ id: 'r1', metadataId: 'resistor', properties: { value: '220' } },
{ id: 'led1', metadataId: 'led', properties: { color: 'red' } },
@ -120,7 +120,7 @@ describe('NetlistBuilder — simple cases', () => {
});
it('NTC divider: T=25°C produces V(a0) ≈ 2.5V', { timeout: 30_000 }, async () => {
const netlist = buildNetlist({
const { netlist } = buildNetlist({
components: [
{ id: 'r1', metadataId: 'resistor', properties: { value: '10k' } },
{ id: 'ntc1', metadataId: 'ntc-temperature-sensor', properties: { temperature: 25 } },
@ -149,7 +149,7 @@ describe('NetlistBuilder — simple cases', () => {
it('adds auto pull-down on floating cap-only node', () => {
// Two caps share a node (net x) that only touches capacitors → DC-floating.
const netlist = buildNetlist({
const { netlist } = buildNetlist({
components: [
{ id: 'c1', metadataId: 'capacitor', properties: { value: '1u' } },
{ id: 'c2', metadataId: 'capacitor', properties: { value: '1u' } },
@ -166,7 +166,7 @@ describe('NetlistBuilder — simple cases', () => {
});
it('PWM pin emits DC-equivalent voltage source', () => {
const netlist = buildNetlist({
const { netlist } = buildNetlist({
components: [],
wires: [
{ id: 'w1', start: { componentId: 'uno', pinName: 'D9' }, end: { componentId: 'uno', pinName: 'GND' } },

View File

@ -55,6 +55,7 @@ void loop() {
category: 'circuits', difficulty: 'beginner',
code: `// RC Low-Pass Filter
// PWM on pin 9 → R=10k → C=10uF → smooth DC on A0
// DC steady-state: V_out ≈ duty × Vcc = 0.5 × 5 = 2.5 V
void setup() { Serial.begin(9600); analogWrite(9, 128); } // 50% duty
void loop() {
float v = analogRead(A0) * 5.0 / 1023.0;
@ -64,11 +65,13 @@ void loop() {
components: [
UNO,
{ type: 'wokwi-resistor', id: 'r1', x: 350, y: 100, properties: { value: '10000' } },
{ type: 'wokwi-capacitor', id: 'c1', x: 420, y: 200, properties: { value: '10u' } },
],
wires: [
w('w1', ['arduino-uno','9'], ['r1','1'], '#00aaff'),
w('w2', ['r1','2'], ['arduino-uno','A0'], '#ffaa00'),
w('w3', ['r1','2'], ['arduino-uno','GND'], '#000000'),
w('w3', ['r1','2'], ['c1','1'], '#ffaa00'),
w('w4', ['c1','2'], ['arduino-uno','GND'], '#000000'),
],
},
@ -290,11 +293,13 @@ void loop() {
components: [
UNO,
{ type: 'wokwi-resistor', id: 'r1', x: 380, y: 100, properties: { value: '10000' } },
{ type: 'wokwi-capacitor', id: 'c1', x: 450, y: 200, properties: { value: '100u' } },
],
wires: [
w('w1', ['arduino-uno','8'], ['r1','1'], '#00aaff'),
w('w2', ['r1','2'], ['arduino-uno','A0'], '#ffaa00'),
w('w3', ['r1','2'], ['arduino-uno','GND'], '#000000'),
w('w2', ['r1','2'], ['c1','1'], '#ffaa00'),
w('w3', ['r1','2'], ['arduino-uno','A0'], '#ffaa00'),
w('w4', ['c1','2'], ['arduino-uno','GND'], '#000000'),
],
},

View File

@ -71,7 +71,7 @@ class CircuitScheduler {
this.pending = null;
this.inFlight = true;
const netlist = buildNetlist(req.input);
const { netlist, pinNetMap } = buildNetlist(req.input);
const t0 = performance.now();
let result: ElectricalSolveResult;
try {
@ -99,6 +99,7 @@ class CircuitScheduler {
error: null,
solveMs: performance.now() - t0,
submittedNetlist: netlist,
pinNetMap,
};
} catch (err) {
result = {
@ -108,6 +109,7 @@ class CircuitScheduler {
error: String(err instanceof Error ? err.message : err),
solveMs: performance.now() - t0,
submittedNetlist: netlist,
pinNetMap,
};
} finally {
this.inFlight = false;
@ -141,6 +143,7 @@ function noopResult(reason: string): ElectricalSolveResult {
error: reason,
solveMs: 0,
submittedNetlist: '',
pinNetMap: new Map(),
};
}

View File

@ -35,7 +35,13 @@ function skipCanonicalization(metadataId: string): boolean {
return metadataId.startsWith('instr-');
}
export function buildNetlist(input: BuildNetlistInput): string {
export interface BuildNetlistResult {
netlist: string;
/** "boardId:pinName" → SPICE net name, from the same UF used to build the netlist. */
pinNetMap: Map<string, string>;
}
export function buildNetlist(input: BuildNetlistInput): BuildNetlistResult {
const { components, wires, boards, analysis, extraCards = [] } = input;
// ── 1. Union-Find over wires ─────────────────────────────────────────────
@ -141,7 +147,23 @@ export function buildNetlist(input: BuildNetlistInput): string {
}
lines.push('.end');
return lines.join('\n');
// ── 9. Build board pin → net map from the same UF ─────────────────────────
// Must use the same `uf` and `netNames` so net names match what ngspice sees.
const pinNetMap = new Map<string, string>();
for (const board of boards) {
// All wire endpoints that belong to this board are already in the UF.
for (const w of wires) {
for (const endpoint of [w.start, w.end]) {
if (endpoint.componentId !== board.id) continue;
const key = pinKey(board.id, endpoint.pinName);
if (!uf.has(key)) continue;
const netName = netNames.get(uf.find(key));
if (netName) pinNetMap.set(key, netName);
}
}
}
return { netlist: lines.join('\n'), pinNetMap };
}
// ── Helpers ────────────────────────────────────────────────────────────────
@ -262,5 +284,75 @@ export function buildWireNetMap(
return result;
}
/**
* Build a map from `"${boardId}:${pinName}"` SPICE net name for every
* board pin that participates in the circuit. Used by the ADC injection
* step in subscribeToStore so it can look up voltages by pin name.
*/
export function buildBoardPinNetMap(
input: Pick<BuildNetlistInput, 'components' | 'wires' | 'boards'>,
): Map<string, string> {
const { wires, boards, components } = input;
const uf = new UnionFind();
const pin = (cId: string, pName: string) => `${cId}:${pName}`;
// Collect board IDs for fast lookup
const boardIds = new Set(boards.map((b) => b.id));
for (const w of wires) {
const a = pin(w.start.componentId, w.start.pinName);
const b = pin(w.end.componentId, w.end.pinName);
uf.add(a);
uf.add(b);
uf.union(a, b);
}
// Canonicalize board ground/vcc pins (from boardPinGroups metadata)
for (const board of boards) {
for (const pName of board.groundPinNames ?? []) {
const k = pin(board.id, pName);
uf.add(k);
uf.setCanonical(k, '0');
}
for (const pName of board.vccPinNames ?? []) {
const k = pin(board.id, pName);
uf.add(k);
uf.setCanonical(k, 'vcc_rail');
}
}
// Canonicalize non-board component GND/VCC pins referenced by wires
for (const comp of components) {
if (comp.metadataId.startsWith('instr-')) continue;
if (boardIds.has(comp.id)) continue; // board handled above
for (const pName of pinsReferencedByWires(comp.id, wires)) {
if (GROUND_PIN_RE.test(pName)) uf.setCanonical(pin(comp.id, pName), '0');
else if (VCC_PIN_RE.test(pName)) uf.setCanonical(pin(comp.id, pName), 'vcc_rail');
}
}
const netNames = assignDeterministicNetNames(uf);
const result = new Map<string, string>();
// For each board, collect ALL pins that appear in wires (via wire endpoints)
// plus the explicit groundPinNames/vccPinNames/pins lists.
for (const board of boards) {
const wireReferencedPins = pinsReferencedByWires(board.id, wires);
const allPins = new Set([
...(board.groundPinNames ?? []),
...(board.vccPinNames ?? []),
...Object.keys(board.pins ?? {}),
...wireReferencedPins, // ← the pins that actually exist in the UF
]);
for (const pName of allPins) {
const k = pin(board.id, pName);
if (uf.has(k)) {
const netName = netNames.get(uf.find(k));
if (netName) result.set(k, netName);
}
}
}
return result;
}
/** Re-export types for callers. */
export type { BuildNetlistInput, ComponentForSpice, BoardForSpice, WireForSpice } from './types';

View File

@ -7,11 +7,13 @@
* Called once at app startup (typically from EditorPage or main.tsx).
* Returns an `unsubscribe()` for cleanup.
*/
import { useSimulatorStore } from '../../store/useSimulatorStore';
import { useSimulatorStore, getBoardSimulator, getBoardPinManager } from '../../store/useSimulatorStore';
import { useElectricalStore } from '../../store/useElectricalStore';
import { buildInputFromStore } from './storeAdapter';
import { setAdcVoltage } from '../parts/partUtils';
import type { PinSourceState } from './types';
import type { BoardKind } from '../../types/board';
import { BOARD_PIN_GROUPS } from './boardPinGroups';
// Which Arduino-style pin name maps to which ADC channel, per board.
// Used to inject SPICE-solved voltages back into the MCU's ADC peripheral.
@ -62,75 +64,149 @@ const ADC_PIN_MAP: Partial<Record<BoardKind, Array<{ pinName: string; channel: n
'aitewinrobot-esp32c3-supermini': adcRange('GPIO', 0, 6),
};
export function wireElectricalSolver(): () => void {
let lastPinStates: Record<string, Record<string, PinSourceState>> = {};
/**
* Convert an ADC pin name + channel to the GPIO pin number that
* `setAdcVoltage()` (partUtils) expects, per board family.
*
* AVR: A014, A115, ... (analog pins start at 14)
* RP2040: GP2626, GP2727, ... (GPIO number directly)
* ESP32: GPIO3232, ... or A0channel-dependent (GPIO number)
*/
function avrPinFromName(_name: string, channel: number): number { return 14 + channel; }
// eslint-disable-next-line @typescript-eslint/no-unused-vars
function gpioPinFromName(name: string, _channel: number): number {
const m = name.match(/(\d+)$/);
return m ? parseInt(m[1], 10) : -1;
}
function collectPinStates(): Record<string, Record<string, PinSourceState>> {
const boards = useSimulatorStore.getState().boards;
const out: Record<string, Record<string, PinSourceState>> = {};
for (const board of boards) {
const simulator = board.simulator as unknown as {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
pinManager?: any;
} | null;
const entries: Record<string, PinSourceState> = {};
// NOTE: Velxio's PinManager tracks state per pin number. Translating
// number → pin name depends on board-specific maps. For Phase 8.3 we
// emit pin states only when `pinManager.getPinState(pin)` is readable;
// boards without that accessor simply contribute no GPIO sources
// (their wires still participate via canonicalized ground/vcc).
if (simulator?.pinManager?.getPinState) {
// Best-effort: we don't yet have a canonical pin-number → pin-name
// map for all boards here. Future work (Phase 8.4) will enrich this.
}
out[board.id] = entries;
}
return out;
const ADC_PIN_TO_GPIO: Partial<Record<BoardKind, (pinName: string, channel: number) => number>> = {
'arduino-uno': avrPinFromName,
'arduino-nano': avrPinFromName,
'arduino-mega': avrPinFromName,
'attiny85': avrPinFromName,
'raspberry-pi-pico': gpioPinFromName,
'pi-pico-w': gpioPinFromName,
'esp32': gpioPinFromName,
'esp32-devkit-c-v4': gpioPinFromName,
'esp32-cam': gpioPinFromName,
'wemos-lolin32-lite': gpioPinFromName,
'esp32-s3': gpioPinFromName,
'xiao-esp32-s3': gpioPinFromName,
'arduino-nano-esp32': avrPinFromName, // uses A0-A7 naming
'esp32-c3': gpioPinFromName,
'xiao-esp32-c3': gpioPinFromName,
'aitewinrobot-esp32c3-supermini': gpioPinFromName,
};
/**
* Convert a board pin name (e.g. "9", "A0", "GP26", "GPIO32") to the
* Arduino-style pin number that PinManager uses internally.
* Returns -1 if the name doesn't map to a GPIO pin.
*/
function pinNameToArduinoPin(pinName: string, boardKind: BoardKind): number {
const group = BOARD_PIN_GROUPS[boardKind] ?? BOARD_PIN_GROUPS.default;
// Skip power/ground pins — they're handled as canonical nets
if (group.gnd.includes(pinName) || group.vcc_pins.includes(pinName)) return -1;
// RP2040: "GP26" → 26
if (pinName.startsWith('GP')) {
const n = parseInt(pinName.slice(2), 10);
return Number.isFinite(n) ? n : -1;
}
// ESP32: "GPIO32" → 32
if (pinName.startsWith('GPIO')) {
const n = parseInt(pinName.slice(4), 10);
return Number.isFinite(n) ? n : -1;
}
// AVR analog: "A0" → 14, "A1" → 15, ...
if (/^A\d+$/.test(pinName)) {
return 14 + parseInt(pinName.slice(1), 10);
}
// Bare numeric: "9" → 9, "13" → 13
if (/^\d+$/.test(pinName)) {
return parseInt(pinName, 10);
}
return -1;
}
/**
* Collect MCU output pin states from PinManager for pins that participate
* in the circuit (i.e., are referenced by wires).
*/
function collectPinStates(
boardId: string,
boardKind: BoardKind,
wires: Array<{ start: { componentId: string; pinName: string }; end: { componentId: string; pinName: string } }>,
): Record<string, PinSourceState> {
const pm = getBoardPinManager(boardId);
if (!pm) return {};
const group = BOARD_PIN_GROUPS[boardKind] ?? BOARD_PIN_GROUPS.default;
const vcc = group.vcc;
const result: Record<string, PinSourceState> = {};
// Gather all pin names wired to this board
const pinNames = new Set<string>();
for (const w of wires) {
if (w.start.componentId === boardId) pinNames.add(w.start.pinName);
if (w.end.componentId === boardId) pinNames.add(w.end.pinName);
}
for (const pinName of pinNames) {
const arduinoPin = pinNameToArduinoPin(pinName, boardKind);
if (arduinoPin < 0) continue;
const pwmDuty = pm.getPwmValue(arduinoPin);
if (pwmDuty > 0) {
result[pinName] = { type: 'pwm', duty: pwmDuty };
} else if (pm.getPinState(arduinoPin)) {
result[pinName] = { type: 'digital', v: vcc };
}
// If pin is LOW or unknown, don't add — treated as input/floating by SPICE
}
return result;
}
export function wireElectricalSolver(): () => void {
function maybeSolve() {
const { mode } = useElectricalStore.getState();
if (mode === 'off') return;
const storeState = useSimulatorStore.getState();
const pinStates = collectPinStates();
lastPinStates = pinStates;
const snap = {
components: storeState.components,
wires: storeState.wires,
boards: storeState.boards.map((b) => ({
id: b.id,
boardKind: b.boardKind,
pinStates: pinStates[b.id] ?? {},
pinStates: collectPinStates(b.id, b.boardKind, storeState.wires),
})),
};
const input = buildInputFromStore(snap);
// pinNetMap is now built inside buildNetlist() from the same UF and
// returned via CircuitScheduler → ElectricalSolveResult → store.
useElectricalStore.getState().triggerSolve(input);
}
function injectVoltagesIntoADC() {
const { nodeVoltages } = useElectricalStore.getState();
const { nodeVoltages, pinNetMap } = useElectricalStore.getState();
const { boards } = useSimulatorStore.getState();
for (const board of boards) {
const map = ADC_PIN_MAP[board.boardKind];
if (!map) continue;
const simulator = board.simulator as unknown as {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getADC?: () => { channelValues: number[] } | null;
} | null;
const adc = simulator?.getADC?.();
if (!adc) continue;
// Try to find a net matching "<boardId>_<pinName>" by string lookup
for (const { pinName, channel } of map) {
const probeKey = `${board.id}:${pinName}`;
// ngspice result keys are net names from NetlistBuilder — we don't
// yet have a direct pin→net lookup here. This is a forward-looking
// scaffold; Phase 8.4 will add the map via storeAdapter.
void probeKey;
// For now: if any net name literally equals the pin label, use it.
const v = nodeVoltages[pinName] ?? nodeVoltages[probeKey] ?? null;
if (v != null) {
adc.channelValues[channel] = Math.max(0, Math.min(board.boardKind.startsWith('esp32') ? 3.3 : 5, v));
}
const adcPins = ADC_PIN_MAP[board.boardKind];
if (!adcPins) continue;
const sim = getBoardSimulator(board.id);
if (!sim) continue;
const vMax = board.boardKind.startsWith('esp32') ? 3.3 : 5.0;
for (const { pinName, channel } of adcPins) {
const netName = pinNetMap.get(`${board.id}:${pinName}`);
if (!netName) continue;
const v = nodeVoltages[netName];
if (v == null) continue;
const clamped = Math.max(0, Math.min(vMax, v));
// setAdcVoltage handles AVR (pin 14-19), RP2040 (GPIO 26-29),
// and ESP32 (bridge shim) transparently.
const gpioPin = ADC_PIN_TO_GPIO[board.boardKind]?.(pinName, channel);
if (gpioPin != null) setAdcVoltage(sim, gpioPin, clamped);
}
}
}
@ -155,10 +231,53 @@ export function wireElectricalSolver(): () => void {
}
});
// Re-inject whenever boards change (e.g. loadHex recreates AVRADC).
// loadHex() creates a fresh AVRADC *before* updating the store, so by the
// time this fires the new ADC already exists and needs the SPICE values.
const unsubBoards = useSimulatorStore.subscribe((state, prev) => {
if (state.boards !== prev.boards) {
const { nodeVoltages } = useElectricalStore.getState();
if (Object.keys(nodeVoltages).length > 0) {
injectVoltagesIntoADC();
}
}
});
// Periodic re-solve while any board is running, so SPICE picks up
// MCU pin-state changes (e.g. analogWrite → PWM → voltage source).
let solveInterval: ReturnType<typeof setInterval> | null = null;
const SOLVE_INTERVAL_MS = 200;
function updateSolveTimer() {
const { mode } = useElectricalStore.getState();
const anyRunning = useSimulatorStore.getState().boards.some((b) => b.running);
if (mode !== 'off' && anyRunning) {
if (!solveInterval) {
solveInterval = setInterval(maybeSolve, SOLVE_INTERVAL_MS);
}
} else if (solveInterval) {
clearInterval(solveInterval);
solveInterval = null;
}
}
const unsubRunning = useSimulatorStore.subscribe((state, prev) => {
const wasRunning = prev.boards.some((b) => b.running);
const nowRunning = state.boards.some((b) => b.running);
if (wasRunning !== nowRunning) updateSolveTimer();
});
const unsubModeTimer = useElectricalStore.subscribe((state, prev) => {
if (state.mode !== prev.mode) updateSolveTimer();
});
return () => {
unsubSim();
unsubMode();
unsubResult();
void lastPinStates;
unsubBoards();
unsubRunning();
unsubModeTimer();
if (solveInterval) clearInterval(solveInterval);
};
}

View File

@ -71,4 +71,9 @@ export interface ElectricalSolveResult {
solveMs: number;
/** The netlist we submitted — useful for debugging in the UI. */
submittedNetlist: string;
/**
* Maps "boardId:pinName" SPICE net name, built from the same Union-Find
* used to generate the netlist. Used by ADC injection to locate voltages.
*/
pinNetMap: Map<string, string>;
}

View File

@ -36,6 +36,8 @@ interface ElectricalState {
engineLoading: boolean;
engineReady: boolean;
submittedNetlist: string;
/** "boardId:pinName" → SPICE net name. Populated after each solve. */
pinNetMap: Map<string, string>;
setMode: (m: ElectricalMode) => Promise<void>;
triggerSolve: (input: BuildNetlistInput) => void;
@ -54,6 +56,7 @@ export const useElectricalStore = create<ElectricalState>((set, get) => {
error: r.error,
lastSolveMs: r.solveMs,
submittedNetlist: r.submittedNetlist,
pinNetMap: r.pinNetMap,
});
});
@ -67,6 +70,7 @@ export const useElectricalStore = create<ElectricalState>((set, get) => {
engineLoading: false,
engineReady: false,
submittedNetlist: '',
pinNetMap: new Map(),
async setMode(m) {
if (!ELECTRICAL_SIM_ENABLED && m !== 'off') {
@ -111,6 +115,7 @@ export const useElectricalStore = create<ElectricalState>((set, get) => {
error: null,
lastSolveMs: 0,
submittedNetlist: '',
pinNetMap: new Map(),
});
},
};

View File

@ -72,6 +72,7 @@ const CATEGORY_MAP: Record<string, ComponentCategory> = {
// Passive Components
'resistor': 'passive',
'capacitor': 'passive',
'inductor': 'passive',
'diode': 'passive',
'analog-multiplexer': 'passive',
'ir-receiver': 'passive',

@ -0,0 +1 @@
Subproject commit 14296fa4b6797ce8ddedc55b47eafa0fe9c2a0e0