diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 07f9626..22afff0 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,12 +1,12 @@ -# Arquitectura del Proyecto - Arduino Emulator +# Project Architecture - Arduino Emulator -## Visión General +## Overview -Este proyecto es un emulador de Arduino que funciona completamente local, utilizando los repositorios oficiales de Wokwi para máxima compatibilidad. +This project is a fully local Arduino emulator using official Wokwi repositories for maximum compatibility. ``` ┌─────────────────────────────────────────────────────────────┐ -│ USUARIO (Navegador) │ +│ USER (Browser) │ │ http://localhost:5173 │ └──────────────────────────┬──────────────────────────────────┘ │ @@ -24,7 +24,7 @@ Este proyecto es un emulador de Arduino que funciona completamente local, utiliz │ ▼ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ Wokwi Components Integration │ │ -│ │ (wokwi-elements + avr8js desde repos locales) │ │ +│ │ (wokwi-elements + avr8js from local repos) │ │ │ └──────────────────────────────────────────────────────┘ │ └──────────────────────────┬──────────────────────────────────┘ │ HTTP (axios) @@ -35,41 +35,41 @@ Este proyecto es un emulador de Arduino que funciona completamente local, utiliz │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ POST /api/compile │ │ -│ │ - Recibe código Arduino (.ino) │ │ -│ │ - Compila con arduino-cli │ │ -│ │ - Retorna archivo .hex │ │ +│ │ - Receives Arduino code (.ino) │ │ +│ │ - Compiles with arduino-cli │ │ +│ │ - Returns .hex file │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Arduino CLI Service │ │ -│ │ (Invoca arduino-cli como subprocess) │ │ +│ │ (Invokes arduino-cli as subprocess) │ │ │ └─────────────────────────────────────────────────────┘ │ └──────────────────────────┬──────────────────────────────────┘ │ ▼ ┌──────────────────────┐ │ arduino-cli │ - │ (Sistema local) │ + │ (Local system) │ └──────────────────────┘ ``` -## Flujo de Datos: Compilación y Simulación +## Data Flow: Compilation and Simulation -### 1. Edición de Código +### 1. Code Editing ``` -Usuario escribe código +User writes code ↓ Monaco Editor ↓ Zustand (useEditorStore) ↓ -Estado: code +State: code ``` -### 2. Compilación +### 2. Compilation ``` -Click en "Compile" +Click "Compile" ↓ EditorToolbar.tsx → compileCode() ↓ @@ -79,31 +79,31 @@ Backend: ArduinoCLIService.compile() ↓ arduino-cli compile --fqbn arduino:avr:uno ↓ -Genera archivo .hex en directorio temporal +Generates .hex file in temp directory ↓ -Backend lee .hex y retorna contenido +Backend reads .hex and returns content ↓ Frontend: useSimulatorStore.setCompiledHex() ``` -### 3. Simulación (Actualmente simplificada) +### 3. Simulation (Currently simplified) ``` -Click en "Run" +Click "Run" ↓ useSimulatorStore.setRunning(true) ↓ -SimulatorCanvas: useEffect detecta running=true +SimulatorCanvas: useEffect detects running=true ↓ -setInterval cada 1000ms +setInterval every 1000ms ↓ updateComponentState('led-builtin', !state) ↓ -wokwi-led component actualiza visualmente +wokwi-led component updates visually ``` -### 4. Simulación Real (Próximamente con avr8js) +### 4. Real Simulation (Coming with avr8js) ``` -Archivo .hex compilado +Compiled .hex file ↓ AVRSimulator.loadHex(hex) ↓ @@ -117,97 +117,97 @@ requestAnimationFrame loop ↓ CPU.tick() × 267,000 cycles/frame ↓ -Escribe en PORTB/PORTC/PORTD +Writes to PORTB/PORTC/PORTD ↓ Write hooks → PinManager.updatePort() ↓ -PinManager notifica callbacks +PinManager notifies callbacks ↓ -Componentes actualizan estado visual +Components update visual state ``` -## Componentes Clave +## Key Components ### Frontend #### 1. Stores (Zustand) - **[useEditorStore.ts](frontend/src/store/useEditorStore.ts)** - - `code`: Código fuente actual - - `theme`: Tema del editor (dark/light) - - `setCode()`: Actualizar código + - `code`: Current source code + - `theme`: Editor theme (dark/light) + - `setCode()`: Update code - **[useSimulatorStore.ts](frontend/src/store/useSimulatorStore.ts)** - - `running`: Estado de simulación - - `compiledHex`: Archivo hex compilado - - `components`: Lista de componentes electrónicos - - `setCompiledHex()`: Guardar hex - - `updateComponentState()`: Actualizar LED/componente + - `running`: Simulation state + - `compiledHex`: Compiled hex file + - `components`: List of electronic components + - `setCompiledHex()`: Save hex + - `updateComponentState()`: Update LED/component -#### 2. Componentes UI +#### 2. UI Components - **[CodeEditor.tsx](frontend/src/components/editor/CodeEditor.tsx)** - - Wrapper de Monaco Editor - - Syntax highlighting C++ - - Auto-completado + - Monaco Editor wrapper + - C++ syntax highlighting + - Auto-completion - **[EditorToolbar.tsx](frontend/src/components/editor/EditorToolbar.tsx)** - - Botones: Compile, Run, Stop - - Manejo de estados de compilación - - Mensajes de error/éxito + - Buttons: Compile, Run, Stop + - Compilation state handling + - Error/success messages - **[SimulatorCanvas.tsx](frontend/src/components/simulator/SimulatorCanvas.tsx)** - - Renderiza Arduino Uno - - Renderiza componentes (LEDs) - - Loop de simulación + - Renders Arduino Uno + - Renders components (LEDs) + - Simulation loop -#### 3. Wokwi Components Wrappers +#### 3. Wokwi Component Wrappers - **[LED.tsx](frontend/src/components/components-wokwi/LED.tsx)** - - Wrapper React para `` + - React wrapper for `` - Props: color, value, x, y - **[ArduinoUno.tsx](frontend/src/components/components-wokwi/ArduinoUno.tsx)** - - Wrapper React para `` - - Control de LED interno (pin 13) + - React wrapper for `` + - Internal LED control (pin 13) - **[Resistor.tsx](frontend/src/components/components-wokwi/Resistor.tsx)** - - Wrapper React para `` + - React wrapper for `` - Props: value (ohms) - **[Pushbutton.tsx](frontend/src/components/components-wokwi/Pushbutton.tsx)** - - Wrapper React para `` + - React wrapper for `` - Events: onPress, onRelease ### Backend #### 1. API Routes - **[compile.py](backend/app/api/routes/compile.py)** - - `POST /api/compile`: Compilar código - - `GET /api/compile/boards`: Listar placas + - `POST /api/compile`: Compile code + - `GET /api/compile/boards`: List boards #### 2. Services - **[arduino_cli.py](backend/app/services/arduino_cli.py)** - - `compile()`: Compilar sketch con arduino-cli - - `list_boards()`: Obtener placas disponibles - - Manejo de directorios temporales + - `compile()`: Compile sketch with arduino-cli + - `list_boards()`: Get available boards + - Temporary directory management -### Wokwi Libraries (Clonadas Localmente) +### Wokwi Libraries (Cloned Locally) #### 1. wokwi-elements -- **Ubicación**: `wokwi-libs/wokwi-elements/` -- **Build**: `dist/esm/` y `dist/cjs/` -- **Componentes**: 50+ elementos electrónicos -- **Tecnología**: Lit (Web Components) +- **Location**: `wokwi-libs/wokwi-elements/` +- **Build**: `dist/esm/` and `dist/cjs/` +- **Components**: 50+ electronic elements +- **Technology**: Lit (Web Components) #### 2. avr8js -- **Ubicación**: `wokwi-libs/avr8js/` -- **Build**: `dist/esm/` y `dist/cjs/` -- **Funcionalidad**: Emulador completo de ATmega328p -- **Soporta**: CPU, Timers, USART, GPIO, ADC, etc. +- **Location**: `wokwi-libs/avr8js/` +- **Build**: `dist/esm/` and `dist/cjs/` +- **Functionality**: Complete ATmega328p emulator +- **Supports**: CPU, Timers, USART, GPIO, ADC, etc. #### 3. rp2040js -- **Ubicación**: `wokwi-libs/rp2040js/` -- **Uso futuro**: Soporte para Raspberry Pi Pico +- **Location**: `wokwi-libs/rp2040js/` +- **Future use**: Raspberry Pi Pico support -## Integración con Vite +## Vite Integration ### Alias Configuration ```typescript @@ -220,98 +220,98 @@ resolve: { } ``` -Esto permite: -- Usar repos locales en lugar de npm -- Actualizar fácilmente con `git pull` -- Modificar código fuente si es necesario +This allows: +- Use local repos instead of npm +- Easy updates with `git pull` +- Modify source code if needed -## Stack Tecnológico +## Technology Stack ### Frontend -| Tecnología | Versión | Propósito | +| Technology | Version | Purpose | |------------|---------|-----------| -| React | 19.2 | Framework UI | +| React | 19.2 | UI framework | | Vite | 7.3 | Build tool & dev server | -| TypeScript | 5.9 | Tipado estático | -| Monaco Editor | 4.7 | Editor de código (VSCode) | +| TypeScript | 5.9 | Static typing | +| Monaco Editor | 4.7 | Code editor (VSCode) | | Zustand | 5.0 | State management | | Axios | 1.13 | HTTP client | -| wokwi-elements | 1.9.2 | Componentes electrónicos | -| avr8js | 0.21.0 | Emulador AVR8 | +| wokwi-elements | 1.9.2 | Electronic components | +| avr8js | 0.21.0 | AVR8 emulator | ### Backend -| Tecnología | Versión | Propósito | +| Technology | Version | Purpose | |------------|---------|-----------| | Python | 3.12+ | Runtime | | FastAPI | 0.115 | Web framework | | Uvicorn | 0.32 | ASGI server | -| SQLAlchemy | 2.0 | ORM (futuro) | -| aiosqlite | 0.20 | DB async (futuro) | +| SQLAlchemy | 2.0 | ORM (future) | +| aiosqlite | 0.20 | Async DB (future) | -### Herramientas Externas -| Herramienta | Propósito | -|-------------|-----------| -| arduino-cli | Compilador de Arduino | -| Git | Control de versiones de Wokwi libs | +### External Tools +| Tool | Purpose | +|------|---------| +| arduino-cli | Arduino compiler | +| Git | Version control for Wokwi libs | -## Ventajas de la Arquitectura +## Architecture Advantages -### ✅ Separación de Responsabilidades -- **Frontend**: UI, UX, visualización -- **Backend**: Compilación, lógica de negocio -- **Wokwi Libs**: Emulación y componentes (mantenido por Wokwi) +### ✅ Separation of Concerns +- **Frontend**: UI, UX, visualization +- **Backend**: Compilation, business logic +- **Wokwi Libs**: Emulation and components (maintained by Wokwi) -### ✅ Compatibilidad con Wokwi -- Repositorios oficiales = misma funcionalidad -- Actualizaciones automáticas con `git pull` -- Nuevos componentes disponibles inmediatamente +### ✅ Wokwi Compatibility +- Official repositories = same functionality +- Automatic updates with `git pull` +- New components available immediately -### ✅ Escalabilidad -- Frontend puede agregar más componentes fácilmente -- Backend puede agregar más endpoints (proyectos, sensores) -- Wokwi libs se actualizan independientemente +### ✅ Scalability +- Frontend can easily add more components +- Backend can add more endpoints (projects, sensors) +- Wokwi libs update independently -### ✅ Desarrollo Local -- No requiere internet para funcionar -- Compilación local con arduino-cli -- Base de datos local (SQLite) +### ✅ Local Development +- No internet required to work +- Local compilation with arduino-cli +- Local database (SQLite) -## Próximas Mejoras +## Upcoming Improvements -### Fase 2: Emulación Real (avr8js) +### Phase 2: Real Emulation (avr8js) ``` -[ ] Implementar AVRSimulator.ts -[ ] Parser de archivos Intel HEX -[ ] PinManager con write hooks -[ ] Integrar CPU execution loop -[ ] Mapear pines Arduino a componentes +[ ] Implement AVRSimulator.ts +[ ] Intel HEX file parser +[ ] PinManager with write hooks +[ ] Integrate CPU execution loop +[ ] Map Arduino pins to components ``` -### Fase 3: Más Componentes +### Phase 3: More Components ``` -[ ] Integrar más wokwi-elements -[ ] Botones, potenciómetros -[ ] Sensores (DHT22, HC-SR04) -[ ] Pantallas (LCD, 7-segment) +[ ] Integrate more wokwi-elements +[ ] Buttons, potentiometers +[ ] Sensors (DHT22, HC-SR04) +[ ] Displays (LCD, 7-segment) ``` -### Fase 4: Persistencia +### Phase 4: Persistence ``` -[ ] Base de datos SQLite -[ ] Modelos SQLAlchemy -[ ] CRUD de proyectos -[ ] Guardar circuitos como JSON +[ ] SQLite database +[ ] SQLAlchemy models +[ ] Project CRUD +[ ] Save circuits as JSON ``` -### Fase 5: Features Avanzadas +### Phase 5: Advanced Features ``` [ ] Serial Monitor -[ ] Wiring visual (drag & drop) -[ ] Múltiples placas (Mega, Nano, ESP32) -[ ] Exportar a Wokwi.com +[ ] Visual wiring (drag & drop) +[ ] Multiple boards (Mega, Nano, ESP32) +[ ] Export to Wokwi.com ``` -## Referencias +## References - [Wokwi Elements Repo](https://github.com/wokwi/wokwi-elements) - [AVR8js Repo](https://github.com/wokwi/avr8js) diff --git a/backend/test_compilation.py b/backend/test_compilation.py index a7f696b..3523cfd 100644 --- a/backend/test_compilation.py +++ b/backend/test_compilation.py @@ -144,7 +144,7 @@ async def test_api_endpoint(): import httpx # Test if server is running - async with httpx.AsyncClient() as client: + async with httpx.AsyncClient(follow_redirects=True) as client: try: response = await client.get("http://localhost:8001/api/compile/boards", timeout=5.0) if response.status_code == 200: diff --git a/doc/img1.png b/doc/img1.png new file mode 100644 index 0000000..84918b5 Binary files /dev/null and b/doc/img1.png differ diff --git a/doc/img2.png b/doc/img2.png new file mode 100644 index 0000000..956663d Binary files /dev/null and b/doc/img2.png differ diff --git a/frontend/src/components/ComponentPickerModal.css b/frontend/src/components/ComponentPickerModal.css index 7479722..d8c7066 100644 --- a/frontend/src/components/ComponentPickerModal.css +++ b/frontend/src/components/ComponentPickerModal.css @@ -93,16 +93,9 @@ align-items: center; } -.search-icon { - position: absolute; - left: 12px; - font-size: 18px; - pointer-events: none; -} - .search-input { width: 100%; - padding: 12px 40px 12px 40px; + padding: 12px 40px 12px 12px; font-size: 16px; border: 2px solid #e0e0e0; border-radius: 8px; diff --git a/frontend/src/components/ComponentPickerModal.tsx b/frontend/src/components/ComponentPickerModal.tsx index 47fdc92..145a8ce 100644 --- a/frontend/src/components/ComponentPickerModal.tsx +++ b/frontend/src/components/ComponentPickerModal.tsx @@ -81,14 +81,13 @@ export const ComponentPickerModal: React.FC = ({

Add Component

{/* Search Bar */}
- 🔍 = ({ onClick={() => setSearchQuery('')} aria-label="Clear search" > - ✕ + X )}
diff --git a/frontend/src/components/DynamicComponent.tsx b/frontend/src/components/DynamicComponent.tsx index b1fcdf0..c78a80c 100644 --- a/frontend/src/components/DynamicComponent.tsx +++ b/frontend/src/components/DynamicComponent.tsx @@ -174,6 +174,8 @@ export const DynamicComponent: React.FC = ({ userSelect: 'none', zIndex: isSelected ? 1000 : 1, pointerEvents: 'auto', + transform: properties.rotation ? `rotate(${properties.rotation}deg)` : undefined, + transformOrigin: 'center center', }} onMouseDown={handleMouseDown} onDoubleClick={handleDoubleClick} diff --git a/frontend/src/components/simulator/ComponentPalette.tsx b/frontend/src/components/simulator/ComponentPalette.tsx index cf9c221..49c4d42 100644 --- a/frontend/src/components/simulator/ComponentPalette.tsx +++ b/frontend/src/components/simulator/ComponentPalette.tsx @@ -7,25 +7,25 @@ const COMPONENT_TEMPLATES: ComponentTemplate[] = [ { type: 'led', label: 'LED', - icon: '💡', + icon: 'L', defaultProperties: { color: 'red' }, }, { type: 'resistor', label: 'Resistor', - icon: '⚡', + icon: 'R', defaultProperties: { value: 220 }, }, { type: 'pushbutton', label: 'Button', - icon: '🔘', + icon: 'B', defaultProperties: { color: 'red' }, }, { type: 'potentiometer', label: 'Potentiometer', - icon: '🎛️', + icon: 'P', defaultProperties: { value: 50 }, }, ]; @@ -61,9 +61,9 @@ export const ComponentPalette = ({ onDragStart }: ComponentPaletteProps) => { ))}
-

💡 Drag components to the canvas

-

📍 Click a component to assign pin

-

🗑️ Press Delete to remove selected

+

Drag components to the canvas

+

Click a component to assign pin

+

Press Delete to remove selected

); diff --git a/frontend/src/components/simulator/ComponentPropertyDialog.css b/frontend/src/components/simulator/ComponentPropertyDialog.css new file mode 100644 index 0000000..83ff4d0 --- /dev/null +++ b/frontend/src/components/simulator/ComponentPropertyDialog.css @@ -0,0 +1,153 @@ +/** + * Component Property Dialog Styles + */ + +.component-property-dialog { + position: absolute; + background-color: #2d2d2d; + border: 1px solid #555; + border-radius: 8px; + padding: 12px; + min-width: 200px; + max-width: 250px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5); + z-index: 1001; + pointer-events: all; + color: #fff; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif; +} + +.component-property-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 10px; + padding-bottom: 8px; + border-bottom: 1px solid #444; +} + +.component-property-title { + font-size: 14px; + font-weight: 600; + color: #fff; +} + +.property-close-button { + background: transparent; + border: none; + color: #aaa; + cursor: pointer; + font-size: 20px; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 4px; + transition: background-color 0.2s, color 0.2s; +} + +.property-close-button:hover { + background-color: #444; + color: #fff; +} + +.pin-roles-section { + margin-bottom: 12px; +} + +.pin-roles-label { + font-size: 12px; + color: #aaa; + margin-bottom: 6px; + font-weight: 500; +} + +.pin-role-item { + font-size: 12px; + color: #fff; + padding: 4px 8px; + margin: 2px 0; + background-color: #3d3d3d; + border-radius: 4px; + display: flex; + align-items: center; +} + +.pin-name { + color: #00d9ff; + font-weight: 500; +} + +.pin-description { + color: #aaa; + font-size: 11px; + margin-left: 4px; +} + +.pin-assignment-section { + margin-bottom: 12px; + padding: 6px 8px; + background-color: #3d3d3d; + border-radius: 4px; +} + +.pin-assignment-label { + font-size: 11px; + color: #aaa; + margin-bottom: 4px; +} + +.pin-assignment-value { + font-size: 13px; + color: #00d9ff; + font-weight: 600; +} + +.property-actions { + display: flex; + gap: 8px; + margin-top: 8px; +} + +.property-action-button { + flex: 1; + padding: 6px 12px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 12px; + font-weight: 500; + transition: all 0.2s; + display: flex; + align-items: center; + justify-content: center; + gap: 4px; +} + +.rotate-button { + background-color: #007acc; + color: white; +} + +.rotate-button:hover { + background-color: #005a9e; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 122, 204, 0.3); +} + +.delete-button { + background-color: #dc3545; + color: white; +} + +.delete-button:hover { + background-color: #c82333; + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(220, 53, 69, 0.3); +} + +.property-action-button:active { + transform: translateY(0); +} diff --git a/frontend/src/components/simulator/ComponentPropertyDialog.tsx b/frontend/src/components/simulator/ComponentPropertyDialog.tsx new file mode 100644 index 0000000..b567873 --- /dev/null +++ b/frontend/src/components/simulator/ComponentPropertyDialog.tsx @@ -0,0 +1,168 @@ +/** + * Component Property Dialog + * + * Displays component properties and actions when a component is selected. + * Shows pin roles, rotation, and delete options. + */ + +import React, { useEffect, useState, useRef } from 'react'; +import type { ComponentMetadata } from '../../types/component-metadata'; +import './ComponentPropertyDialog.css'; + +interface ComponentPropertyDialogProps { + componentId: string; + componentMetadata: ComponentMetadata; + componentProperties: Record; + position: { x: number; y: number }; + pinInfo: Array<{ name: string; x: number; y: number; signals?: any[]; description?: string }>; + onClose: () => void; + onRotate: (componentId: string) => void; + onDelete: (componentId: string) => void; +} + +export const ComponentPropertyDialog: React.FC = ({ + componentId, + componentMetadata, + componentProperties, + position, + pinInfo, + onClose, + onRotate, + onDelete, +}) => { + const dialogRef = useRef(null); + const [dialogPosition, setDialogPosition] = useState({ x: 0, y: 0 }); + + // Calculate dialog position on mount + useEffect(() => { + if (!dialogRef.current) return; + + const dialogWidth = 220; + const dialogHeight = dialogRef.current.offsetHeight || 200; + const canvasElement = document.querySelector('.canvas-content'); + if (!canvasElement) return; + + const canvasWidth = canvasElement.clientWidth; + const canvasHeight = canvasElement.clientHeight; + + // Try positioning to the right of component + let x = position.x + 150; // Approximate component width + gap + let y = position.y; + + // If off-screen right, position to left + if (x + dialogWidth > canvasWidth) { + x = Math.max(10, position.x - dialogWidth - 10); + } + + // Keep within vertical bounds + if (y + dialogHeight > canvasHeight) { + y = Math.max(10, canvasHeight - dialogHeight - 10); + } + + setDialogPosition({ x, y }); + }, [position]); + + // Close on Escape key + useEffect(() => { + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + } + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [onClose]); + + // Close when clicking outside + useEffect(() => { + const handleClickOutside = (e: MouseEvent) => { + if (dialogRef.current && !dialogRef.current.contains(e.target as Node)) { + onClose(); + } + }; + + // Delay to avoid immediate close from the click that opened the dialog + const timer = setTimeout(() => { + document.addEventListener('mousedown', handleClickOutside); + }, 100); + + return () => { + clearTimeout(timer); + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [onClose]); + + return ( +
e.stopPropagation()} + > + {/* Header */} +
+ {componentMetadata.name} + +
+ + {/* Pin Roles Section */} + {pinInfo.length > 0 && ( +
+
Pin Roles:
+ {pinInfo.map((pin) => ( +
+ • {pin.name} + {pin.description && ( + ({pin.description}) + )} +
+ ))} +
+ )} + + {/* Current Arduino Pin Assignment */} + {componentProperties.pin !== undefined && ( +
+
Arduino Pin:
+
+ {componentProperties.pin >= 14 + ? `A${componentProperties.pin - 14}` + : `D${componentProperties.pin}`} +
+
+ )} + + {/* Action Buttons */} +
+ + +
+
+ ); +}; diff --git a/frontend/src/components/simulator/PinOverlay.tsx b/frontend/src/components/simulator/PinOverlay.tsx index d7dbac5..8587989 100644 --- a/frontend/src/components/simulator/PinOverlay.tsx +++ b/frontend/src/components/simulator/PinOverlay.tsx @@ -9,8 +9,8 @@ import React, { useEffect, useState } from 'react'; interface PinInfo { name: string; - x: number; // mm - y: number; // mm + x: number; // CSS pixels + y: number; // CSS pixels signals?: Array<{ type: string; signal?: string }>; } @@ -22,8 +22,6 @@ interface PinOverlayProps { showPins: boolean; } -const MM_TO_PX = 3.7795275591; - export const PinOverlay: React.FC = ({ componentId, componentX, @@ -52,12 +50,13 @@ export const PinOverlay: React.FC = ({ left: `${componentX + 6}px`, // +6px for wrapper padding (4px padding + 2px border) top: `${componentY + 6}px`, pointerEvents: 'none', - zIndex: 10, + zIndex: 1002, // Above property dialog (1001) }} > {pins.map((pin) => { - const pinX = pin.x * MM_TO_PX; - const pinY = pin.y * MM_TO_PX; + // Pin coordinates are already in CSS pixels + const pinX = pin.x; + const pinY = pin.y; return (
{ const [showPinSelector, setShowPinSelector] = useState(false); const [pinSelectorPos, setPinSelectorPos] = useState({ x: 0, y: 0 }); + // Component property dialog + const [showPropertyDialog, setShowPropertyDialog] = useState(false); + const [propertyDialogComponentId, setPropertyDialogComponentId] = useState(null); + const [propertyDialogPosition, setPropertyDialogPosition] = useState({ x: 0, y: 0 }); + + // Click vs drag detection + const [clickStartTime, setClickStartTime] = useState(0); + const [clickStartPos, setClickStartPos] = useState({ x: 0, y: 0 }); + // Component dragging state const [draggedComponentId, setDraggedComponentId] = useState(null); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); - // Pin visualization - const [hoveredComponentId, setHoveredComponentId] = useState(null); - // Canvas ref for coordinate calculations const canvasRef = useRef(null); @@ -135,15 +142,33 @@ export const SimulatorCanvas = () => { } as any); }; + // Component rotation + const handleRotateComponent = (componentId: string) => { + const component = components.find((c) => c.id === componentId); + if (!component) return; + + const currentRotation = (component.properties.rotation as number) || 0; + updateComponent(componentId, { + properties: { + ...component.properties, + rotation: (currentRotation + 90) % 360, + }, + } as any); + }; + // Component dragging handlers const handleComponentMouseDown = (componentId: string, e: React.MouseEvent) => { - // Don't start dragging if we're clicking on the pin selector - if (showPinSelector) return; + // Don't start dragging if we're clicking on the pin selector or property dialog + if (showPinSelector || showPropertyDialog) return; e.stopPropagation(); const component = components.find((c) => c.id === componentId); if (!component || !canvasRef.current) return; + // Record click start for click vs drag detection + setClickStartTime(Date.now()); + setClickStartPos({ x: e.clientX, y: e.clientY }); + // Get canvas position to convert viewport coords to canvas coords const canvasRect = canvasRef.current.getBoundingClientRect(); @@ -180,8 +205,25 @@ export const SimulatorCanvas = () => { } }; - const handleCanvasMouseUp = () => { + const handleCanvasMouseUp = (e: React.MouseEvent) => { if (draggedComponentId) { + // Check if this was a click or a drag + const timeDiff = Date.now() - clickStartTime; + const posDiff = Math.sqrt( + Math.pow(e.clientX - clickStartPos.x, 2) + + Math.pow(e.clientY - clickStartPos.y, 2) + ); + + // If moved < 5px and time < 300ms, treat as click + if (posDiff < 5 && timeDiff < 300) { + const component = components.find((c) => c.id === draggedComponentId); + if (component) { + setPropertyDialogComponentId(draggedComponentId); + setPropertyDialogPosition({ x: component.x, y: component.y }); + setShowPropertyDialog(true); + } + } + // Recalculate wire positions after moving component recalculateAllWirePositions(); setDraggedComponentId(null); @@ -190,6 +232,11 @@ export const SimulatorCanvas = () => { // Wire creation via pin clicks const handlePinClick = (componentId: string, pinName: string, x: number, y: number) => { + // Close property dialog when starting wire creation + if (showPropertyDialog) { + setShowPropertyDialog(false); + } + if (wireInProgress) { // Finish wire creation finishWireCreation({ @@ -230,8 +277,8 @@ export const SimulatorCanvas = () => { } const isSelected = selectedComponentId === component.id; - const isHovered = hoveredComponentId === component.id; - const showPinsForComponent = isHovered || wireInProgress !== null; + // Always show pins for better UX when creating wires + const showPinsForComponent = true; return ( @@ -248,8 +295,6 @@ export const SimulatorCanvas = () => { onDoubleClick={(e) => { handleComponentDoubleClick(component.id, e); }} - onMouseEnter={() => setHoveredComponentId(component.id)} - onMouseLeave={() => setHoveredComponentId(null)} /> {/* Pin overlay for wire creation */} @@ -308,7 +353,7 @@ export const SimulatorCanvas = () => { componentX={ARDUINO_POSITION.x} componentY={ARDUINO_POSITION.y} onPinClick={handlePinClick} - showPins={wireInProgress !== null} + showPins={true} /> {/* Components using wokwi-elements */} @@ -332,6 +377,32 @@ export const SimulatorCanvas = () => { /> )} + {/* Component Property Dialog */} + {showPropertyDialog && propertyDialogComponentId && (() => { + const component = components.find((c) => c.id === propertyDialogComponentId); + const metadata = component ? registry.getById(component.metadataId) : null; + if (!component || !metadata) return null; + + const element = document.getElementById(propertyDialogComponentId); + const pinInfo = element ? (element as any).pinInfo : []; + + return ( + setShowPropertyDialog(false)} + onRotate={handleRotateComponent} + onDelete={(id) => { + removeComponent(id); + setShowPropertyDialog(false); + }} + /> + ); + })()} + {/* Component Picker Modal */} = { - boards: '🖥️ Boards', - sensors: '📡 Sensors', - displays: '📺 Displays', - input: '🎮 Input', - output: '💡 Output', - motors: '⚙️ Motors', - communication: '📶 Communication', - passive: '🔌 Passive', - other: '📦 Other', + boards: 'Boards', + sensors: 'Sensors', + displays: 'Displays', + input: 'Input', + output: 'Output', + motors: 'Motors', + communication: 'Communication', + passive: 'Passive', + other: 'Other', }; return displayNames[category] || category; }