diff --git a/frontend/public/components-metadata.json b/frontend/public/components-metadata.json index 13eb40a6..68c0260b 100644 --- a/frontend/public/components-metadata.json +++ b/frontend/public/components-metadata.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "generatedAt": "2026-03-19T00:06:03.160Z", + "generatedAt": "2026-03-20T17:33:38.631Z", "components": [ { "id": "arduino-mega", diff --git a/frontend/src/components/DynamicComponent.tsx b/frontend/src/components/DynamicComponent.tsx index c6265213..209583a8 100644 --- a/frontend/src/components/DynamicComponent.tsx +++ b/frontend/src/components/DynamicComponent.tsx @@ -197,7 +197,8 @@ export const DynamicComponent: React.FC = ({ if (logic && logic.attachEvents && simulator) { // Helper to find Arduino pin connected to a component pin const getArduinoPin = (componentPinName: string): number | null => { - const wires = useSimulatorStore.getState().wires.filter( + const state = useSimulatorStore.getState(); + const wires = state.wires.filter( w => (w.start.componentId === id && w.start.pinName === componentPinName) || (w.end.componentId === id && w.end.pinName === componentPinName) ); @@ -207,7 +208,11 @@ export const DynamicComponent: React.FC = ({ const boardEndpoint = isBoardComponent(w.start.componentId) ? w.start : isBoardComponent(w.end.componentId) ? w.end : null; if (boardEndpoint) { - const pin = boardPinToNumber(boardEndpoint.componentId, boardEndpoint.pinName); + // Use the board's actual kind for pin mapping (instance ID may differ from kind, + // e.g. board ID 'arduino-uno' after switching to 'raspberry-pi-pico') + const boardKind = state.boards.find((b) => b.id === boardEndpoint.componentId)?.boardKind + ?? boardEndpoint.componentId; + const pin = boardPinToNumber(boardKind, boardEndpoint.pinName); if (pin !== null) return pin; } } diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index bfa9e745..8f19970e 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -454,34 +454,41 @@ export const SimulatorCanvas = () => { // Helper to add subscription const subscribeComponentToPin = (component: any, pin: number, componentPinName?: string) => { + // Components with attachEvents in PartSimulationRegistry manage their own + // visual state (e.g. servo, buzzer). Skip generic digital/PWM updates for them + // to avoid flickering from raw PWM pulses being misinterpreted as on/off state. + const logic = PartSimulationRegistry.get(component.metadataId); + const hasSelfManagedVisuals = !!(logic && logic.attachEvents); + const unsubscribe = pinManager.onPinChange( pin, (_pin, state) => { - // 1. Update React state for standard properties - updateComponentState(component.id, state); + if (!hasSelfManagedVisuals) { + // 1. Update React state for standard properties (LEDs, buttons, etc.) + updateComponentState(component.id, state); + } // 2. Delegate to PartSimulationRegistry for custom visual updates - const logic = PartSimulationRegistry.get(component.metadataId); if (logic && logic.onPinStateChange) { const el = document.getElementById(component.id); if (el) { logic.onPinStateChange(componentPinName || 'A', state, el); } } - - console.log(`Component ${component.id} on pin ${pin}: ${state ? 'HIGH' : 'LOW'}`); } ); unsubscribers.push(unsubscribe); - // PWM subscription: update LED opacity when the pin receives a LEDC duty cycle. - // duty=0 means no PWM / analogWrite(0) — clear the inline style so the - // component keeps its default visibility instead of becoming invisible. - const pwmUnsub = pinManager.onPwmChange(pin, (_p, duty) => { - const el = document.getElementById(component.id); - if (el) el.style.opacity = duty > 0 ? String(duty) : ''; - }); - unsubscribers.push(pwmUnsub); + // PWM subscription: update LED opacity when the pin receives a PWM duty cycle. + // Skip for self-managed components (servo, buzzer) — their duty cycle is a + // control signal, not a brightness value, so setting opacity would cause flicker. + if (!hasSelfManagedVisuals) { + const pwmUnsub = pinManager.onPwmChange(pin, (_p, duty) => { + const el = document.getElementById(component.id); + if (el) el.style.opacity = duty > 0 ? String(duty) : ''; + }); + unsubscribers.push(pwmUnsub); + } }; components.forEach((component) => { diff --git a/frontend/src/simulation/AVRSimulator.ts b/frontend/src/simulation/AVRSimulator.ts index 28b531a4..a09e806d 100644 --- a/frontend/src/simulation/AVRSimulator.ts +++ b/frontend/src/simulation/AVRSimulator.ts @@ -309,6 +309,11 @@ export class AVRSimulator { return this.adc; } + /** Returns the CPU clock frequency in Hz (16 MHz for AVR). */ + getClockHz(): number { + return 16_000_000; + } + /** * Returns the current CPU cycle count. * Used by timing-sensitive peripherals to schedule future pin changes. diff --git a/frontend/src/simulation/PinManager.ts b/frontend/src/simulation/PinManager.ts index 91be1743..3e17022a 100644 --- a/frontend/src/simulation/PinManager.ts +++ b/frontend/src/simulation/PinManager.ts @@ -73,8 +73,6 @@ export class PinManager { if (callbacks) { callbacks.forEach(cb => cb(arduinoPin, newState)); } - - console.log(`Pin ${arduinoPin} (${portName}${bit}): ${oldState ? 'HIGH' : 'LOW'} → ${newState ? 'HIGH' : 'LOW'}`); } } } diff --git a/frontend/src/simulation/RP2040Simulator.ts b/frontend/src/simulation/RP2040Simulator.ts index a7b2a98b..72566c39 100644 --- a/frontend/src/simulation/RP2040Simulator.ts +++ b/frontend/src/simulation/RP2040Simulator.ts @@ -49,6 +49,8 @@ export class RP2040Simulator { private speed = 1.0; private gpioUnsubscribers: Array<() => void> = []; private flashCopy: Uint8Array | null = null; + private totalCycles = 0; + private scheduledPinChanges: Array<{ cycle: number; pin: number; state: boolean }> = []; /** Serial output callback — fires for each byte the Pico sends on UART0 */ public onSerialData: ((char: string) => void) | null = null; @@ -269,7 +271,10 @@ export class RP2040Simulator { const jump: number = clock.nanosToNextAlarm; if (jump <= 0) break; // no pending alarms clock.tick(jump); - cyclesDone += Math.ceil(jump / CYCLE_NANOS); + const jumped = Math.ceil(jump / CYCLE_NANOS); + cyclesDone += jumped; + this.totalCycles += jumped; + this.flushScheduledPinChanges(); } else { break; } @@ -277,6 +282,8 @@ export class RP2040Simulator { const cycles: number = core.executeInstruction(); if (clock) clock.tick(cycles * CYCLE_NANOS); cyclesDone += cycles; + this.totalCycles += cycles; + this.flushScheduledPinChanges(); } } @@ -308,6 +315,8 @@ export class RP2040Simulator { reset(): void { this.stop(); + this.totalCycles = 0; + this.scheduledPinChanges = []; if (this.rp2040 && this.flashCopy) { this.initMCU(this.flashCopy); // Re-register any previously added I2C devices @@ -328,6 +337,34 @@ export class RP2040Simulator { return this.speed; } + /** Returns the CPU clock frequency in Hz. */ + getClockHz(): number { + return F_CPU; + } + + /** Returns total CPU cycles executed since last reset/load. */ + getCurrentCycles(): number { + return this.totalCycles; + } + + /** + * Schedule a GPIO pin state change at a specific future cycle count. + * Enables cycle-accurate protocol simulation (e.g. HC-SR04 echo timing). + */ + schedulePinChange(pin: number, state: boolean, atCycle: number): void { + let i = this.scheduledPinChanges.length; + while (i > 0 && this.scheduledPinChanges[i - 1].cycle > atCycle) i--; + this.scheduledPinChanges.splice(i, 0, { cycle: atCycle, pin, state }); + } + + private flushScheduledPinChanges(): void { + if (this.scheduledPinChanges.length === 0) return; + while (this.scheduledPinChanges.length > 0 && this.scheduledPinChanges[0].cycle <= this.totalCycles) { + const { pin, state } = this.scheduledPinChanges.shift()!; + this.setPinState(pin, state); + } + } + /** * Drive a GPIO pin externally (e.g. from a button or slider). * GPIO n = Arduino D(n) for Raspberry Pi Pico. diff --git a/frontend/src/simulation/parts/ComplexParts.ts b/frontend/src/simulation/parts/ComplexParts.ts index 44e4757a..e0071bea 100644 --- a/frontend/src/simulation/parts/ComplexParts.ts +++ b/frontend/src/simulation/parts/ComplexParts.ts @@ -184,23 +184,29 @@ PartSimulationRegistry.register('photoresistor-sensor', { */ PartSimulationRegistry.register('analog-joystick', { attachEvents: (element, avrSimulator, getArduinoPinHelper, componentId) => { - const pinX = getArduinoPinHelper('VRX') ?? getArduinoPinHelper('XOUT'); - const pinY = getArduinoPinHelper('VRY') ?? getArduinoPinHelper('YOUT'); - const pinSW = getArduinoPinHelper('SW'); + // wokwi-analog-joystick uses VERT/HORZ/SEL pin names + const pinX = getArduinoPinHelper('VERT') ?? getArduinoPinHelper('VRX') ?? getArduinoPinHelper('XOUT'); + const pinY = getArduinoPinHelper('HORZ') ?? getArduinoPinHelper('VRY') ?? getArduinoPinHelper('YOUT'); + const pinSW = getArduinoPinHelper('SEL') ?? getArduinoPinHelper('SW'); const el = element as any; - // Center position is mid-range (~2.5V) - if (pinX !== null) setAdcVoltage(avrSimulator, pinX, 2.5); - if (pinY !== null) setAdcVoltage(avrSimulator, pinY, 2.5); + // RP2040 uses 3.3V reference; AVR uses 5V + const vcc = avrSimulator instanceof RP2040Simulator ? 3.3 : 5.0; + const centerV = vcc / 2; + + // Initialize to center position and button not pressed + if (pinX !== null) setAdcVoltage(avrSimulator, pinX, centerV); + if (pinY !== null) setAdcVoltage(avrSimulator, pinY, centerV); + if (pinSW !== null) avrSimulator.setPinState(pinSW, true); // HIGH = not pressed const onMove = () => { // xValue / yValue are 0-1023 if (pinX !== null) { - const vx = ((el.xValue ?? 512) / 1023.0) * 5.0; + const vx = ((el.xValue ?? 512) / 1023.0) * vcc; setAdcVoltage(avrSimulator, pinX, vx); } if (pinY !== null) { - const vy = ((el.yValue ?? 512) / 1023.0) * 5.0; + const vy = ((el.yValue ?? 512) / 1023.0) * vcc; setAdcVoltage(avrSimulator, pinY, vy); } }; @@ -219,13 +225,13 @@ PartSimulationRegistry.register('analog-joystick', { element.addEventListener('button-press', onPress); element.addEventListener('button-release', onRelease); - // SensorControlPanel: xAxis/yAxis -512..512 → voltage 0–5V (center = 2.5V) + // SensorControlPanel: xAxis/yAxis -512..512 → voltage 0–VCC (center = VCC/2) registerSensorUpdate(componentId, (values) => { if ('xAxis' in values && pinX !== null) { - setAdcVoltage(avrSimulator, pinX, ((values.xAxis as number + 512) / 1023) * 5.0); + setAdcVoltage(avrSimulator, pinX, ((values.xAxis as number + 512) / 1023) * vcc); } if ('yAxis' in values && pinY !== null) { - setAdcVoltage(avrSimulator, pinY, ((values.yAxis as number + 512) / 1023) * 5.0); + setAdcVoltage(avrSimulator, pinY, ((values.yAxis as number + 512) / 1023) * vcc); } }); @@ -242,34 +248,80 @@ PartSimulationRegistry.register('analog-joystick', { // ─── Servo ─────────────────────────────────────────────────────────────────── /** - * Servo motor — reads OCR1A and ICR1 to calculate pulse width and angle. + * Servo motor — measures actual PWM pulse width from pin state changes. * * Standard RC servo protocol: * - 50 Hz signal (20 ms period) - * - Pulse width 1 ms → 0°, 1.5 ms → 90°, 2 ms → 180° + * - Pulse width 544 µs → 0°, 1472 µs → 90°, 2400 µs → 180° + * (Arduino Servo.h uses 544–2400 µs, NOT the generic 1000–2000 µs range) * - * With Timer1, prescaler=8, F_CPU=16MHz: - * - ICR1 = 20000 for 50Hz - * - OCR1A = 1000 → 0°, 1500 → 90°, 2000 → 180° + * Approach: subscribe to the servo's PWM pin state changes, record the CPU + * cycle count at the rising edge, then compute pulse width on the falling edge. + * avr8js re-schedules Timer1 every 8 CPU cycles (prescaler=8), so each HIGH + * and LOW transition fires in a separate count() call with a distinct cpu.cycles + * value → the measurement is cycle-accurate. * - * We poll these registers every animation frame via a requestAnimationFrame loop. + * Fallback: if no wire is connected (pinSIG === null), poll OCR1A/ICR1 registers + * via requestAnimationFrame (less accurate but still functional). */ PartSimulationRegistry.register('servo', { attachEvents: (element, avrSimulator, getArduinoPinHelper) => { const pinSIG = getArduinoPinHelper('PWM') ?? getArduinoPinHelper('SIG') ?? getArduinoPinHelper('1'); const el = element as any; - // OCR1A low byte = 0x88, OCR1A high byte = 0x89 + // Arduino Servo.h actual pulse range (544µs = 0°, 2400µs = 180°) + const MIN_PULSE_US = 544; + const MAX_PULSE_US = 2400; + const CPU_HZ = 16_000_000; + + // ── Primary: cycle-accurate pulse width measurement ──────────────── + if (pinSIG !== null) { + const pinManager = (avrSimulator as any).pinManager as import('../PinManager').PinManager | undefined; + if (pinManager) { + let riseTime = -1; // cpu.cycles at last rising edge + + const unsubscribe = pinManager.onPinChange(pinSIG, (_pin, state) => { + const cpu = (avrSimulator as any).cpu; + if (!cpu) return; + + if (state) { + // Rising edge — record cycle count + riseTime = cpu.cycles; + } else if (riseTime >= 0) { + // Falling edge — compute pulse width in µs + const pulseCycles = cpu.cycles - riseTime; + const pulseUs = (pulseCycles / CPU_HZ) * 1_000_000; + riseTime = -1; + + // Only update if pulse is in valid RC servo range + if (pulseUs >= MIN_PULSE_US && pulseUs <= MAX_PULSE_US) { + const angle = Math.round( + ((pulseUs - MIN_PULSE_US) / (MAX_PULSE_US - MIN_PULSE_US)) * 180 + ); + el.angle = angle; + } + } + }); + + return () => { unsubscribe(); }; + } + } + + // ── Fallback: poll OCR1A/ICR1 registers when no wire is connected ── + // OCR1A low byte = 0x88, high byte = 0x89 // ICR1L = 0x86, ICR1H = 0x87 const OCR1AL = 0x88; const OCR1AH = 0x89; const ICR1L = 0x86; const ICR1H = 0x87; + const SERVO_PERIOD_US = 20000; let rafId: number | null = null; let lastOcr1a = -1; const poll = () => { + if (!avrSimulator.isRunning()) { rafId = requestAnimationFrame(poll); return; } + const cpu = (avrSimulator as any).cpu; if (!cpu) { rafId = requestAnimationFrame(poll); return; } @@ -278,39 +330,19 @@ PartSimulationRegistry.register('servo', { lastOcr1a = ocr1a; const icr1 = cpu.data[ICR1L] | (cpu.data[ICR1H] << 8); - // Calculate pulse width in microseconds - // prescaler 8, F_CPU 16MHz → 1 tick = 0.5µs - // pulse_us = ocr1a * 0.5 - // But also handle prescaler 64 (1 tick = 4µs) and default ICR1 detection let pulseUs: number; if (icr1 > 0) { - // Proportional to ICR1 period (assume 20ms period) - pulseUs = 1000 + (ocr1a / icr1) * 1000; + pulseUs = (ocr1a / icr1) * SERVO_PERIOD_US; } else { - // Fallback: prescaler 8 + // prescaler 8, 16MHz → 0.5µs per tick pulseUs = ocr1a * 0.5; } - // Clamp to 1000-2000µs and map to 0-180° - const clamped = Math.max(1000, Math.min(2000, pulseUs)); - const angle = Math.round(((clamped - 1000) / 1000) * 180); + const clamped = Math.max(MIN_PULSE_US, Math.min(MAX_PULSE_US, pulseUs)); + const angle = Math.round(((clamped - MIN_PULSE_US) / (MAX_PULSE_US - MIN_PULSE_US)) * 180); el.angle = angle; } - // Also support PWM duty cycle approach via PinManager - if (pinSIG !== null) { - const pinManager = (avrSimulator as any).pinManager; - // Only override angle if cpu-based approach doesn't work - // (ICR1 = 0 means Timer1 not configured as servo) - const icr1 = cpu.data[ICR1L] | (cpu.data[ICR1H] << 8); - if (icr1 === 0 && pinManager) { - const dc = pinManager.getPwmValue(pinSIG); - if (dc > 0) { - el.angle = Math.round(dc * 180); - } - } - } - rafId = requestAnimationFrame(poll); }; diff --git a/frontend/src/simulation/parts/ProtocolParts.ts b/frontend/src/simulation/parts/ProtocolParts.ts index d55c84d6..f2ef7c1d 100644 --- a/frontend/src/simulation/parts/ProtocolParts.ts +++ b/frontend/src/simulation/parts/ProtocolParts.ts @@ -415,31 +415,41 @@ function scheduleDHT22Response(simulator: any, pin: number, element: HTMLElement const now = simulator.getCurrentCycles() as number; // Timing constants at 16 MHz (cycles per µs = 16) + // DHT22 starts pulling the line LOW ~20 µs after MCU releases it HIGH. + // The Adafruit DHT library v1.4.7 calls expectPulse(LOW) at ~55 µs (pullTime default), + // so the preamble LOW must already be active by then. Starting at 20 µs (320 cycles) + // guarantees the pin IS LOW when the library checks. + const RESPONSE_START = 320; // 20 µs — DHT22 response start const LOW80 = 1280; // 80 µs LOW preamble const HIGH80 = 1280; // 80 µs HIGH preamble const LOW50 = 800; // 50 µs LOW marker before each bit const HIGH0 = 416; // 26 µs HIGH → bit '0' const HIGH1 = 1120; // 70 µs HIGH → bit '1' - let t = now; + let t = now + RESPONSE_START; - // Preamble: 80 µs LOW then 80 µs HIGH - t += LOW80; simulator.schedulePinChange(pin, false, t); - t += HIGH80; simulator.schedulePinChange(pin, true, t); + // Preamble: 80 µs LOW + simulator.schedulePinChange(pin, false, t); + t += LOW80; + // Preamble: 80 µs HIGH + simulator.schedulePinChange(pin, true, t); + t += HIGH80; - // 40 data bits, MSB first + // 40 data bits, MSB first — schedule LOW then advance, schedule HIGH then advance for (const byte of payload) { for (let b = 7; b >= 0; b--) { const bit = (byte >> b) & 1; - t += LOW50; simulator.schedulePinChange(pin, false, t); + simulator.schedulePinChange(pin, false, t); + t += LOW50; + simulator.schedulePinChange(pin, true, t); t += bit ? HIGH1 : HIGH0; - simulator.schedulePinChange(pin, true, t); } } - // Release line HIGH (it already is, but explicit for clarity) - t += LOW50; simulator.schedulePinChange(pin, false, t); - t += HIGH0; simulator.schedulePinChange(pin, true, t); + // Final release + simulator.schedulePinChange(pin, false, t); + t += LOW50; + simulator.schedulePinChange(pin, true, t); } PartSimulationRegistry.register('dht22', { @@ -449,10 +459,26 @@ PartSimulationRegistry.register('dht22', { if (pin === null) return () => {}; let wasLow = false; + // Prevent DHT22's own scheduled pin changes from re-triggering the response. + // After the MCU releases DATA HIGH and we begin responding, we ignore all + // pin-change callbacks until the full waveform has been emitted. + // DHT22 response is ~5 ms; at 16 MHz that is ~80 000 cycles. We gate for + // 200 000 cycles (~12.5 ms) to give plenty of headroom. + const RESPONSE_GATE_CYCLES = 200_000; + let responseEndCycle = 0; + + const getCycles = (): number => + typeof (simulator as any).getCurrentCycles === 'function' + ? ((simulator as any).getCurrentCycles() as number) + : -1; const unsub = (simulator as any).pinManager.onPinChange( pin, (_: number, state: boolean) => { + // While DHT22 is driving the line, ignore our own scheduled changes. + const now = getCycles(); + if (now >= 0 && now < responseEndCycle) return; + if (!state) { // MCU drove DATA LOW — start signal detected wasLow = true; @@ -461,6 +487,8 @@ PartSimulationRegistry.register('dht22', { if (wasLow) { // MCU released DATA HIGH — begin DHT22 response wasLow = false; + const cur = getCycles(); + responseEndCycle = cur >= 0 ? cur + RESPONSE_GATE_CYCLES : 0; scheduleDHT22Response(simulator, pin, element); } }, diff --git a/frontend/src/simulation/parts/SensorParts.ts b/frontend/src/simulation/parts/SensorParts.ts index da11d746..8330e252 100644 --- a/frontend/src/simulation/parts/SensorParts.ts +++ b/frontend/src/simulation/parts/SensorParts.ts @@ -642,12 +642,15 @@ PartSimulationRegistry.register('hc-sr04', { // - Echo duration = distanceCm / 17150 s × 16 000 000 cycles/s // (17150 cm/s = speed of sound, one-way = round-trip/2) if (typeof simulator.schedulePinChange === 'function') { + const clockHz: number = typeof (simulator as any).getClockHz === 'function' + ? (simulator as any).getClockHz() + : 16_000_000; const now = simulator.getCurrentCycles() as number; - const processingCycles = 9600; // ~600 µs sensor overhead - const echoCycles = Math.round((distanceCm / 17150) * 16_000_000); + const processingCycles = Math.round(600e-6 * clockHz); // 600 µs sensor overhead + const echoCycles = Math.round((distanceCm / 17150) * clockHz); simulator.schedulePinChange(echoPin, true, now + processingCycles); simulator.schedulePinChange(echoPin, false, now + processingCycles + echoCycles); - console.log(`[HC-SR04] Scheduled ECHO (${distanceCm} cm, echo=${(echoCycles/16000).toFixed(1)} µs)`); + console.log(`[HC-SR04] Scheduled ECHO (${distanceCm} cm, echo=${(echoCycles / (clockHz / 1e6)).toFixed(1)} µs)`); } else { // Fallback: best-effort async (works with delay()-based sketches, not pulseIn) const echoMs = Math.max(1, distanceCm / 17.15); diff --git a/frontend/src/utils/boardPinMapping.ts b/frontend/src/utils/boardPinMapping.ts index f5c15fdc..194846a6 100644 --- a/frontend/src/utils/boardPinMapping.ts +++ b/frontend/src/utils/boardPinMapping.ts @@ -166,6 +166,8 @@ export function isBoardComponent(componentId: string): boolean { */ export function boardPinToNumber(boardId: string, pinName: string): number | null { if (boardId === 'arduino-uno' || boardId === 'arduino-nano') { + // Power / GND pins — not real GPIOs, skip silently + if (/^(GND|VCC|VIN|IOREF|AREF|RESET|3\.3V|3V3|5V|3V)/.test(pinName)) return -1; // Try numeric (covers '0' through '13', also legacy examples using just numbers) const num = parseInt(pinName, 10); if (!isNaN(num) && num >= 0 && num <= 21) return num; @@ -190,6 +192,11 @@ export function boardPinToNumber(boardId: string, pinName: string): number | nul } if (boardId === 'nano-rp2040' || boardId === 'raspberry-pi-pico') { + // Power / GND pins — return -1 so callers skip silently + if (pinName.startsWith('GND') || pinName.startsWith('3.3V') || pinName.startsWith('3V3') + || pinName.startsWith('5V') || pinName.startsWith('VBUS') || pinName.startsWith('VSYS')) { + return -1; + } // Try D-prefix map first (D2 → GPIO25 = LED_BUILTIN, etc.) const mapped = NANO_RP2040_PIN_MAP[pinName]; if (mapped !== undefined) return mapped; diff --git a/frontend/src/utils/pinPositionCalculator.ts b/frontend/src/utils/pinPositionCalculator.ts index 4b180b56..04325e64 100644 --- a/frontend/src/utils/pinPositionCalculator.ts +++ b/frontend/src/utils/pinPositionCalculator.ts @@ -42,7 +42,11 @@ export function calculatePinPosition( } // Find the specific pin - const pin = pinInfo.find((p: any) => p.name === pinName); + let pin = pinInfo.find((p: any) => p.name === pinName); + // Fallback: try numbered variant (e.g. GND → GND.1) for pins that have suffix variants + if (!pin && !pinName.includes('.')) { + pin = pinInfo.find((p: any) => p.name === `${pinName}.1`); + } if (!pin) { console.warn(`[pinPositionCalculator] Pin ${pinName} not found on component ${componentId}`); console.warn(`Available pins:`, pinInfo.map((p: any) => p.name));