diff --git a/frontend/src/components/editor/EditorToolbar.tsx b/frontend/src/components/editor/EditorToolbar.tsx index fdd7880..d67a4e8 100644 --- a/frontend/src/components/editor/EditorToolbar.tsx +++ b/frontend/src/components/editor/EditorToolbar.tsx @@ -97,9 +97,9 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi const handleExport = async () => { try { - const { components, wires } = useSimulatorStore.getState(); + const { components, wires, boardPosition } = useSimulatorStore.getState(); const projectName = files.find((f) => f.name.endsWith('.ino'))?.name.replace('.ino', '') || 'velxio-project'; - await exportToWokwiZip(files, components, wires, boardType, projectName); + await exportToWokwiZip(files, components, wires, boardType, projectName, boardPosition); } catch (err) { setMessage({ type: 'error', text: 'Export failed.' }); } @@ -113,9 +113,10 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi try { const result = await importFromWokwiZip(file); const { loadFiles } = useEditorStore.getState(); - const { setComponents, setWires, setBoardType, stopSimulation } = useSimulatorStore.getState(); + const { setComponents, setWires, setBoardType, setBoardPosition, stopSimulation } = useSimulatorStore.getState(); stopSimulation(); if (result.boardType) setBoardType(result.boardType); + setBoardPosition(result.boardPosition); setComponents(result.components); setWires(result.wires); if (result.files.length > 0) loadFiles(result.files); diff --git a/frontend/src/components/simulator/SimulatorCanvas.tsx b/frontend/src/components/simulator/SimulatorCanvas.tsx index 26c0b62..fb64d6e 100644 --- a/frontend/src/components/simulator/SimulatorCanvas.tsx +++ b/frontend/src/components/simulator/SimulatorCanvas.tsx @@ -1,4 +1,4 @@ -import { useSimulatorStore, ARDUINO_POSITION, BOARD_LABELS } from '../../store/useSimulatorStore'; +import { useSimulatorStore, BOARD_LABELS } from '../../store/useSimulatorStore'; import type { BoardType } from '../../store/useSimulatorStore'; import React, { useEffect, useState, useRef, useCallback } from 'react'; import { ArduinoUno } from '../components-wokwi/ArduinoUno'; @@ -19,6 +19,8 @@ export const SimulatorCanvas = () => { const { boardType, setBoardType, + boardPosition, + setBoardPosition, components, running, pinManager, @@ -289,10 +291,17 @@ export const SimulatorCanvas = () => { // Handle component dragging if (draggedComponentId) { const world = toWorld(e.clientX, e.clientY); - updateComponent(draggedComponentId, { - x: Math.max(0, world.x - dragOffset.x), - y: Math.max(0, world.y - dragOffset.y), - } as any); + if (draggedComponentId === '__board__') { + setBoardPosition({ + x: Math.max(0, world.x - dragOffset.x), + y: Math.max(0, world.y - dragOffset.y), + }); + } else { + updateComponent(draggedComponentId, { + x: Math.max(0, world.x - dragOffset.x), + y: Math.max(0, world.y - dragOffset.y), + } as any); + } } // Handle wire creation preview @@ -317,7 +326,7 @@ export const SimulatorCanvas = () => { Math.pow(e.clientY - clickStartPos.y, 2) ); - if (posDiff < 5 && timeDiff < 300) { + if (posDiff < 5 && timeDiff < 300 && draggedComponentId !== '__board__') { const component = components.find((c) => c.id === draggedComponentId); if (component) { setPropertyDialogComponentId(draggedComponentId); @@ -574,23 +583,47 @@ export const SimulatorCanvas = () => { {/* Board visual — switches based on selected board type */} {boardType === 'arduino-uno' ? ( c.id === 'led-builtin')?.properties.state)} /> ) : ( c.id === 'led-builtin')?.properties.state)} /> )} + {/* Board interaction overlay for dragging */} + {!running && ( +
{ + e.stopPropagation(); + const world = toWorld(e.clientX, e.clientY); + setDraggedComponentId('__board__'); + setDragOffset({ + x: world.x - boardPosition.x, + y: world.y - boardPosition.y, + }); + }} + /> + )} + {/* Board pin overlay */} = { 'raspberry-pi-pico': 'Raspberry Pi Pico', }; -// Fixed position for the Arduino board (not in components array) -export const ARDUINO_POSITION = { x: 50, y: 50 }; +// Default position for the Arduino board +export const DEFAULT_BOARD_POSITION = { x: 50, y: 50 }; +// Keep legacy export alias for any remaining references +export const ARDUINO_POSITION = DEFAULT_BOARD_POSITION; interface Component { id: string; @@ -35,6 +37,10 @@ interface SimulatorState { boardType: BoardType; setBoardType: (type: BoardType) => void; + // Board position (mutable — allows dragging) + boardPosition: { x: number; y: number }; + setBoardPosition: (pos: { x: number; y: number }) => void; + // Simulation state simulator: AVRSimulator | RP2040Simulator | null; pinManager: PinManager; @@ -102,6 +108,7 @@ export const useSimulatorStore = create((set, get) => { return { boardType: 'arduino-uno' as BoardType, + boardPosition: { ...DEFAULT_BOARD_POSITION }, simulator: null, pinManager, running: false, @@ -168,6 +175,10 @@ export const useSimulatorStore = create((set, get) => { serialBaudRate: 0, serialMonitorOpen: false, + setBoardPosition: (pos) => { + set({ boardPosition: pos }); + }, + setBoardType: (type: BoardType) => { const { running } = get(); if (running) { @@ -426,9 +437,10 @@ export const useSimulatorStore = create((set, get) => { updateWirePositions: (componentId) => { set((state) => { const component = state.components.find((c) => c.id === componentId); - // For fixed components like Arduino, use ARDUINO_POSITION - const compX = component ? component.x : ARDUINO_POSITION.x; - const compY = component ? component.y : ARDUINO_POSITION.y; + // For the board, use boardPosition from state + const bp = state.boardPosition; + const compX = component ? component.x : bp.x; + const compY = component ? component.y : bp.y; const updatedWires = state.wires.map((wire) => { const updated = { ...wire }; @@ -471,8 +483,9 @@ export const useSimulatorStore = create((set, get) => { const updatedWires = state.wires.map((wire) => { const updated = { ...wire }; const startComp = state.components.find((c) => c.id === wire.start.componentId); - const startX = startComp ? startComp.x : ARDUINO_POSITION.x; - const startY = startComp ? startComp.y : ARDUINO_POSITION.y; + const bp = state.boardPosition; + const startX = startComp ? startComp.x : bp.x; + const startY = startComp ? startComp.y : bp.y; const startPos = calculatePinPosition( wire.start.componentId, @@ -486,8 +499,8 @@ export const useSimulatorStore = create((set, get) => { // Resolve end component position const endComp = state.components.find((c) => c.id === wire.end.componentId); - const endX = endComp ? endComp.x : ARDUINO_POSITION.x; - const endY = endComp ? endComp.y : ARDUINO_POSITION.y; + const endX = endComp ? endComp.x : bp.x; + const endY = endComp ? endComp.y : bp.y; const endPos = calculatePinPosition( wire.end.componentId, diff --git a/frontend/src/utils/wokwiZip.ts b/frontend/src/utils/wokwiZip.ts index 9bbf1db..1ab5c4e 100644 --- a/frontend/src/utils/wokwiZip.ts +++ b/frontend/src/utils/wokwiZip.ts @@ -14,7 +14,6 @@ import JSZip from 'jszip'; import type { Wire } from '../types/wire'; -import { ARDUINO_POSITION } from '../store/useSimulatorStore'; // ── Type definitions ────────────────────────────────────────────────────────── @@ -45,6 +44,7 @@ export interface VelxioComponent { export interface ImportResult { boardType: 'arduino-uno' | 'raspberry-pi-pico'; + boardPosition: { x: number; y: number }; components: VelxioComponent[]; wires: Wire[]; files: Array<{ name: string; content: string }>; @@ -118,6 +118,7 @@ export async function exportToWokwiZip( wires: Wire[], boardType: string, projectName: string, + boardPosition: { x: number; y: number } = { x: 50, y: 50 }, ): Promise { const zip = new JSZip(); @@ -125,14 +126,14 @@ export async function exportToWokwiZip( const boardId = BOARD_TO_WOKWI_ID[boardType] ?? 'uno'; // Build parts — board first, then user components - // Subtract ARDUINO_POSITION to convert from Velxio coords to Wokwi-relative coords + // Subtract boardPosition so coords are relative to the board const parts: WokwiPart[] = [ { type: boardWokwiType, id: boardId, top: 0, left: 0, attrs: {} }, ...components.map((c) => ({ type: metadataIdToWokwiType(c.metadataId), id: c.id, - top: Math.round(c.y - ARDUINO_POSITION.y), - left: Math.round(c.x - ARDUINO_POSITION.x), + top: Math.round(c.y - boardPosition.y), + left: Math.round(c.x - boardPosition.x), attrs: c.properties as Record, })), ]; @@ -192,20 +193,20 @@ export async function importFromWokwiZip(file: File): Promise { const boardType = boardPart ? WOKWI_TYPE_TO_BOARD[boardPart.type] : 'arduino-uno'; const boardId = boardPart?.id ?? 'uno'; - // Calculate offset: Wokwi board position → Velxio ARDUINO_POSITION - const wokwiBoardX = boardPart?.left ?? 0; - const wokwiBoardY = boardPart?.top ?? 0; - const offsetX = ARDUINO_POSITION.x - wokwiBoardX; - const offsetY = ARDUINO_POSITION.y - wokwiBoardY; + // Board position from diagram (use directly as Velxio board position) + const boardPosition = { + x: boardPart?.left ?? 50, + y: boardPart?.top ?? 50, + }; - // Convert non-board parts to Velxio components (apply offset) + // Convert non-board parts to Velxio components (use Wokwi coords directly) 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 + offsetX, - y: p.top + offsetY, + x: p.left, + y: p.top, properties: { ...p.attrs }, })); @@ -257,5 +258,5 @@ export async function importFromWokwiZip(file: File): Promise { return a.name.localeCompare(b.name); }); - return { boardType, components, wires, files }; + return { boardType, boardPosition, components, wires, files }; }