feat: Enhance board positioning by integrating boardPosition state in export/import functions and updating component dragging logic

pull/10/head
David Montero Crespo 2026-03-07 19:29:23 -03:00
parent 5f2c176648
commit f2b275c03d
4 changed files with 85 additions and 37 deletions

View File

@ -97,9 +97,9 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
const handleExport = async () => { const handleExport = async () => {
try { 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'; 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) { } catch (err) {
setMessage({ type: 'error', text: 'Export failed.' }); setMessage({ type: 'error', text: 'Export failed.' });
} }
@ -113,9 +113,10 @@ export const EditorToolbar = ({ consoleOpen, setConsoleOpen, compileLogs: _compi
try { try {
const result = await importFromWokwiZip(file); const result = await importFromWokwiZip(file);
const { loadFiles } = useEditorStore.getState(); const { loadFiles } = useEditorStore.getState();
const { setComponents, setWires, setBoardType, stopSimulation } = useSimulatorStore.getState(); const { setComponents, setWires, setBoardType, setBoardPosition, stopSimulation } = useSimulatorStore.getState();
stopSimulation(); stopSimulation();
if (result.boardType) setBoardType(result.boardType); if (result.boardType) setBoardType(result.boardType);
setBoardPosition(result.boardPosition);
setComponents(result.components); setComponents(result.components);
setWires(result.wires); setWires(result.wires);
if (result.files.length > 0) loadFiles(result.files); if (result.files.length > 0) loadFiles(result.files);

View File

@ -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 type { BoardType } from '../../store/useSimulatorStore';
import React, { useEffect, useState, useRef, useCallback } from 'react'; import React, { useEffect, useState, useRef, useCallback } from 'react';
import { ArduinoUno } from '../components-wokwi/ArduinoUno'; import { ArduinoUno } from '../components-wokwi/ArduinoUno';
@ -19,6 +19,8 @@ export const SimulatorCanvas = () => {
const { const {
boardType, boardType,
setBoardType, setBoardType,
boardPosition,
setBoardPosition,
components, components,
running, running,
pinManager, pinManager,
@ -289,11 +291,18 @@ export const SimulatorCanvas = () => {
// Handle component dragging // Handle component dragging
if (draggedComponentId) { if (draggedComponentId) {
const world = toWorld(e.clientX, e.clientY); const world = toWorld(e.clientX, e.clientY);
if (draggedComponentId === '__board__') {
setBoardPosition({
x: Math.max(0, world.x - dragOffset.x),
y: Math.max(0, world.y - dragOffset.y),
});
} else {
updateComponent(draggedComponentId, { updateComponent(draggedComponentId, {
x: Math.max(0, world.x - dragOffset.x), x: Math.max(0, world.x - dragOffset.x),
y: Math.max(0, world.y - dragOffset.y), y: Math.max(0, world.y - dragOffset.y),
} as any); } as any);
} }
}
// Handle wire creation preview // Handle wire creation preview
if (wireInProgress) { if (wireInProgress) {
@ -317,7 +326,7 @@ export const SimulatorCanvas = () => {
Math.pow(e.clientY - clickStartPos.y, 2) 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); const component = components.find((c) => c.id === draggedComponentId);
if (component) { if (component) {
setPropertyDialogComponentId(draggedComponentId); setPropertyDialogComponentId(draggedComponentId);
@ -574,23 +583,47 @@ export const SimulatorCanvas = () => {
{/* Board visual — switches based on selected board type */} {/* Board visual — switches based on selected board type */}
{boardType === 'arduino-uno' ? ( {boardType === 'arduino-uno' ? (
<ArduinoUno <ArduinoUno
x={ARDUINO_POSITION.x} x={boardPosition.x}
y={ARDUINO_POSITION.y} y={boardPosition.y}
led13={Boolean(components.find((c) => c.id === 'led-builtin')?.properties.state)} led13={Boolean(components.find((c) => c.id === 'led-builtin')?.properties.state)}
/> />
) : ( ) : (
<NanoRP2040 <NanoRP2040
x={ARDUINO_POSITION.x} x={boardPosition.x}
y={ARDUINO_POSITION.y} y={boardPosition.y}
ledBuiltIn={Boolean(components.find((c) => c.id === 'led-builtin')?.properties.state)} ledBuiltIn={Boolean(components.find((c) => c.id === 'led-builtin')?.properties.state)}
/> />
)} )}
{/* Board interaction overlay for dragging */}
{!running && (
<div
style={{
position: 'absolute',
left: boardPosition.x,
top: boardPosition.y,
width: boardType === 'arduino-uno' ? 360 : 280,
height: boardType === 'arduino-uno' ? 250 : 180,
cursor: 'move',
zIndex: 1,
}}
onMouseDown={(e) => {
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 */} {/* Board pin overlay */}
<PinOverlay <PinOverlay
componentId={boardType === 'arduino-uno' ? 'arduino-uno' : 'nano-rp2040'} componentId={boardType === 'arduino-uno' ? 'arduino-uno' : 'nano-rp2040'}
componentX={ARDUINO_POSITION.x} componentX={boardPosition.x}
componentY={ARDUINO_POSITION.y} componentY={boardPosition.y}
onPinClick={handlePinClick} onPinClick={handlePinClick}
showPins={true} showPins={true}
wrapperOffsetX={0} wrapperOffsetX={0}

View File

@ -19,8 +19,10 @@ export const BOARD_LABELS: Record<BoardType, string> = {
'raspberry-pi-pico': 'Raspberry Pi Pico', 'raspberry-pi-pico': 'Raspberry Pi Pico',
}; };
// Fixed position for the Arduino board (not in components array) // Default position for the Arduino board
export const ARDUINO_POSITION = { x: 50, y: 50 }; 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 { interface Component {
id: string; id: string;
@ -35,6 +37,10 @@ interface SimulatorState {
boardType: BoardType; boardType: BoardType;
setBoardType: (type: BoardType) => void; setBoardType: (type: BoardType) => void;
// Board position (mutable — allows dragging)
boardPosition: { x: number; y: number };
setBoardPosition: (pos: { x: number; y: number }) => void;
// Simulation state // Simulation state
simulator: AVRSimulator | RP2040Simulator | null; simulator: AVRSimulator | RP2040Simulator | null;
pinManager: PinManager; pinManager: PinManager;
@ -102,6 +108,7 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
return { return {
boardType: 'arduino-uno' as BoardType, boardType: 'arduino-uno' as BoardType,
boardPosition: { ...DEFAULT_BOARD_POSITION },
simulator: null, simulator: null,
pinManager, pinManager,
running: false, running: false,
@ -168,6 +175,10 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
serialBaudRate: 0, serialBaudRate: 0,
serialMonitorOpen: false, serialMonitorOpen: false,
setBoardPosition: (pos) => {
set({ boardPosition: pos });
},
setBoardType: (type: BoardType) => { setBoardType: (type: BoardType) => {
const { running } = get(); const { running } = get();
if (running) { if (running) {
@ -426,9 +437,10 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
updateWirePositions: (componentId) => { updateWirePositions: (componentId) => {
set((state) => { set((state) => {
const component = state.components.find((c) => c.id === componentId); const component = state.components.find((c) => c.id === componentId);
// For fixed components like Arduino, use ARDUINO_POSITION // For the board, use boardPosition from state
const compX = component ? component.x : ARDUINO_POSITION.x; const bp = state.boardPosition;
const compY = component ? component.y : ARDUINO_POSITION.y; const compX = component ? component.x : bp.x;
const compY = component ? component.y : bp.y;
const updatedWires = state.wires.map((wire) => { const updatedWires = state.wires.map((wire) => {
const updated = { ...wire }; const updated = { ...wire };
@ -471,8 +483,9 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
const updatedWires = state.wires.map((wire) => { const updatedWires = state.wires.map((wire) => {
const updated = { ...wire }; const updated = { ...wire };
const startComp = state.components.find((c) => c.id === wire.start.componentId); const startComp = state.components.find((c) => c.id === wire.start.componentId);
const startX = startComp ? startComp.x : ARDUINO_POSITION.x; const bp = state.boardPosition;
const startY = startComp ? startComp.y : ARDUINO_POSITION.y; const startX = startComp ? startComp.x : bp.x;
const startY = startComp ? startComp.y : bp.y;
const startPos = calculatePinPosition( const startPos = calculatePinPosition(
wire.start.componentId, wire.start.componentId,
@ -486,8 +499,8 @@ export const useSimulatorStore = create<SimulatorState>((set, get) => {
// Resolve end component position // Resolve end component position
const endComp = state.components.find((c) => c.id === wire.end.componentId); const endComp = state.components.find((c) => c.id === wire.end.componentId);
const endX = endComp ? endComp.x : ARDUINO_POSITION.x; const endX = endComp ? endComp.x : bp.x;
const endY = endComp ? endComp.y : ARDUINO_POSITION.y; const endY = endComp ? endComp.y : bp.y;
const endPos = calculatePinPosition( const endPos = calculatePinPosition(
wire.end.componentId, wire.end.componentId,

View File

@ -14,7 +14,6 @@
import JSZip from 'jszip'; import JSZip from 'jszip';
import type { Wire } from '../types/wire'; import type { Wire } from '../types/wire';
import { ARDUINO_POSITION } from '../store/useSimulatorStore';
// ── Type definitions ────────────────────────────────────────────────────────── // ── Type definitions ──────────────────────────────────────────────────────────
@ -45,6 +44,7 @@ export interface VelxioComponent {
export interface ImportResult { export interface ImportResult {
boardType: 'arduino-uno' | 'raspberry-pi-pico'; boardType: 'arduino-uno' | 'raspberry-pi-pico';
boardPosition: { x: number; y: number };
components: VelxioComponent[]; components: VelxioComponent[];
wires: Wire[]; wires: Wire[];
files: Array<{ name: string; content: string }>; files: Array<{ name: string; content: string }>;
@ -118,6 +118,7 @@ export async function exportToWokwiZip(
wires: Wire[], wires: Wire[],
boardType: string, boardType: string,
projectName: string, projectName: string,
boardPosition: { x: number; y: number } = { x: 50, y: 50 },
): Promise<void> { ): Promise<void> {
const zip = new JSZip(); const zip = new JSZip();
@ -125,14 +126,14 @@ export async function exportToWokwiZip(
const boardId = BOARD_TO_WOKWI_ID[boardType] ?? 'uno'; const boardId = BOARD_TO_WOKWI_ID[boardType] ?? 'uno';
// Build parts — board first, then user components // 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[] = [ const parts: WokwiPart[] = [
{ type: boardWokwiType, id: boardId, top: 0, left: 0, attrs: {} }, { type: boardWokwiType, id: boardId, top: 0, left: 0, attrs: {} },
...components.map((c) => ({ ...components.map((c) => ({
type: metadataIdToWokwiType(c.metadataId), type: metadataIdToWokwiType(c.metadataId),
id: c.id, id: c.id,
top: Math.round(c.y - ARDUINO_POSITION.y), top: Math.round(c.y - boardPosition.y),
left: Math.round(c.x - ARDUINO_POSITION.x), left: Math.round(c.x - boardPosition.x),
attrs: c.properties as Record<string, unknown>, attrs: c.properties as Record<string, unknown>,
})), })),
]; ];
@ -192,20 +193,20 @@ export async function importFromWokwiZip(file: File): Promise<ImportResult> {
const boardType = boardPart ? WOKWI_TYPE_TO_BOARD[boardPart.type] : 'arduino-uno'; const boardType = boardPart ? WOKWI_TYPE_TO_BOARD[boardPart.type] : 'arduino-uno';
const boardId = boardPart?.id ?? 'uno'; const boardId = boardPart?.id ?? 'uno';
// Calculate offset: Wokwi board position → Velxio ARDUINO_POSITION // Board position from diagram (use directly as Velxio board position)
const wokwiBoardX = boardPart?.left ?? 0; const boardPosition = {
const wokwiBoardY = boardPart?.top ?? 0; x: boardPart?.left ?? 50,
const offsetX = ARDUINO_POSITION.x - wokwiBoardX; y: boardPart?.top ?? 50,
const offsetY = ARDUINO_POSITION.y - wokwiBoardY; };
// 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 const components: VelxioComponent[] = diagram.parts
.filter((p) => !WOKWI_TYPE_TO_BOARD[p.type]) .filter((p) => !WOKWI_TYPE_TO_BOARD[p.type])
.map((p) => ({ .map((p) => ({
id: p.id, id: p.id,
metadataId: wokwiTypeToMetadataId(p.type), metadataId: wokwiTypeToMetadataId(p.type),
x: p.left + offsetX, x: p.left,
y: p.top + offsetY, y: p.top,
properties: { ...p.attrs }, properties: { ...p.attrs },
})); }));
@ -257,5 +258,5 @@ export async function importFromWokwiZip(file: File): Promise<ImportResult> {
return a.name.localeCompare(b.name); return a.name.localeCompare(b.name);
}); });
return { boardType, components, wires, files }; return { boardType, boardPosition, components, wires, files };
} }