From 1206a34e645376a7def73b92a094dea712e0053e Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Sun, 8 Mar 2026 16:30:49 -0300 Subject: [PATCH] feat: implement auto-pan feature for board visibility after project load and normalize pin names for component connections --- .../components/simulator/SimulatorCanvas.tsx | 31 +++++++++++++ frontend/src/store/useSimulatorStore.ts | 7 +++ frontend/src/utils/wokwiZip.ts | 46 +++++++++++++++---- 3 files changed, 76 insertions(+), 8 deletions(-) diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index c3eb5d4..8b166d4 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -439,6 +439,37 @@ export const SimulatorCanvas = () => { return () => timers.forEach(t => clearTimeout(t)); }, [components, recalculateAllWirePositions]); + // Auto-pan to keep the board visible after a project import/load. + // We track the previous component count and only re-center when the count + // jumps (indicating the user loaded a new circuit, not just added one part). + const prevComponentCountRef = useRef(-1); + useEffect(() => { + const prev = prevComponentCountRef.current; + const curr = components.length; + prevComponentCountRef.current = curr; + + // Only re-center when the component list transitions from empty/different + // project to a populated one (i.e., a load/import event). + const isLoad = curr > 0 && (prev <= 0 || Math.abs(curr - prev) > 2); + if (!isLoad) return; + + const timer = setTimeout(() => { + const canvas = canvasRef.current; + if (!canvas) return; + const rect = canvas.getBoundingClientRect(); + const currentZoom = zoomRef.current; + const newPan = { + x: rect.width / 4 - boardPosition.x * currentZoom, + y: rect.height / 4 - boardPosition.y * currentZoom, + }; + panRef.current = newPan; + setPan(newPan); + }, 150); + + return () => clearTimeout(timer); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [components.length]); + // Render component using dynamic renderer const renderComponent = (component: any) => { const metadata = registry.getById(component.metadataId); diff --git a/frontend/src/store/useSimulatorStore.ts b/frontend/src/store/useSimulatorStore.ts index c8aebba..69b85a3 100644 --- a/frontend/src/store/useSimulatorStore.ts +++ b/frontend/src/store/useSimulatorStore.ts @@ -497,6 +497,11 @@ export const useSimulatorStore = create((set, get) => { ); if (startPos) { updated.start = { ...wire.start, x: startPos.x, y: startPos.y }; + } else { + // Pin name not found in element's pinInfo (e.g. board type mismatch). + // Fall back to the component/board position so the wire renders near + // its endpoint rather than at the canvas origin (0,0). + updated.start = { ...wire.start, x: startX, y: startY }; } // Resolve end component position @@ -512,6 +517,8 @@ export const useSimulatorStore = create((set, get) => { ); if (endPos) { updated.end = { ...wire.end, x: endPos.x, y: endPos.y }; + } else { + updated.end = { ...wire.end, x: endX, y: endY }; } // Auto-generate control points for wires that have none diff --git a/frontend/src/utils/wokwiZip.ts b/frontend/src/utils/wokwiZip.ts index 44913a6..2b744bf 100644 --- a/frontend/src/utils/wokwiZip.ts +++ b/frontend/src/utils/wokwiZip.ts @@ -74,6 +74,22 @@ const BOARD_TO_WOKWI_ID: Record = { 'raspberry-pi-pico': 'pico', }; +// ── Pin name aliases ───────────────────────────────────────────────────────── + +// Maps Wokwi connection "signal" pin names to wokwi-element physical pin names. +// Wokwi boards (e.g. board-ssd1306) use different naming than the bare elements. +const COMPONENT_PIN_ALIASES: Record> = { + 'ssd1306': { + 'SDA': 'DATA', + 'SCL': 'CLK', + 'VCC': 'VIN', + }, +}; + +function normalizePinName(metadataId: string, pinName: string): string { + return COMPONENT_PIN_ALIASES[metadataId]?.[pinName] ?? pinName; +} + // ── Color helpers ───────────────────────────────────────────────────────────── const COLOR_NAME_TO_HEX: Record = { @@ -205,20 +221,27 @@ export async function importFromWokwiZip(file: File): Promise { }; const velxioBoardId = VELXIO_BOARD_ID[boardType] ?? 'arduino-uno'; - // Board position from diagram (use directly as Velxio board position) + // Board position from diagram. Apply a minimum offset so the board is never + // crammed against the canvas top-left corner (Wokwi diagrams often use 0,0). + const MIN_OFFSET = 50; + const rawBoardX = boardPart?.left ?? MIN_OFFSET; + const rawBoardY = boardPart?.top ?? MIN_OFFSET; + const offsetX = rawBoardX < MIN_OFFSET ? MIN_OFFSET - rawBoardX : 0; + const offsetY = rawBoardY < MIN_OFFSET ? MIN_OFFSET - rawBoardY : 0; const boardPosition = { - x: boardPart?.left ?? 50, - y: boardPart?.top ?? 50, + x: rawBoardX + offsetX, + y: rawBoardY + offsetY, }; - // Convert non-board parts to Velxio components (use Wokwi coords directly) + // Convert non-board parts to Velxio components. + // Apply the same offset so components keep their relative position to the board. const components: VelxioComponent[] = diagram.parts .filter((p) => !WOKWI_TYPE_TO_BOARD[p.type]) .map((p) => ({ id: p.id, metadataId: wokwiTypeToMetadataId(p.type), - x: p.left, - y: p.top, + x: p.left + offsetX, + y: p.top + offsetY, properties: { ...p.attrs }, })); @@ -236,10 +259,17 @@ export async function importFromWokwiZip(file: File): Promise { const startId = startCompRaw === boardId ? velxioBoardId : startCompRaw; const endId = endCompRaw === boardId ? velxioBoardId : endCompRaw; + // Normalize pin names: Wokwi uses signal names (SDA, SCL, VCC) while + // wokwi-elements use physical/board pin names (DATA, CLK, VIN). + const startMetadataId = components.find((c) => c.id === startId)?.metadataId ?? ''; + const endMetadataId = components.find((c) => c.id === endId)?.metadataId ?? ''; + const normalizedStartPin = normalizePinName(startMetadataId, startPin); + const normalizedEndPin = normalizePinName(endMetadataId, endPin); + return { id: `wire-${i}-${Date.now()}`, - start: { componentId: startId, pinName: startPin, x: 0, y: 0 }, - end: { componentId: endId, pinName: endPin, x: 0, y: 0 }, + start: { componentId: startId, pinName: normalizedStartPin, x: 0, y: 0 }, + end: { componentId: endId, pinName: normalizedEndPin, x: 0, y: 0 }, controlPoints: [], color: colorToHex(color), signalType: 'digital' as const,