feat: update architecture documentation and improve component property dialog

pull/10/head
David Montero Crespo 2026-03-03 20:42:17 -03:00
parent 5490ad42c8
commit 217736c7cd
14 changed files with 564 additions and 180 deletions

View File

@ -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 │ │ http://localhost:5173 │
└──────────────────────────┬──────────────────────────────────┘ └──────────────────────────┬──────────────────────────────────┘
@ -24,7 +24,7 @@ Este proyecto es un emulador de Arduino que funciona completamente local, utiliz
│ ▼ │ │ ▼ │
│ ┌──────────────────────────────────────────────────────┐ │ │ ┌──────────────────────────────────────────────────────┐ │
│ │ Wokwi Components Integration │ │ │ │ Wokwi Components Integration │ │
│ │ (wokwi-elements + avr8js desde repos locales) │ │ │ │ (wokwi-elements + avr8js from local repos) │ │
│ └──────────────────────────────────────────────────────┘ │ │ └──────────────────────────────────────────────────────┘ │
└──────────────────────────┬──────────────────────────────────┘ └──────────────────────────┬──────────────────────────────────┘
│ HTTP (axios) │ HTTP (axios)
@ -35,41 +35,41 @@ Este proyecto es un emulador de Arduino que funciona completamente local, utiliz
│ │ │ │
│ ┌─────────────────────────────────────────────────────┐ │ │ ┌─────────────────────────────────────────────────────┐ │
│ │ POST /api/compile │ │ │ │ POST /api/compile │ │
│ │ - Recibe código Arduino (.ino) │ │ │ │ - Receives Arduino code (.ino) │ │
│ │ - Compila con arduino-cli │ │ │ │ - Compiles with arduino-cli │ │
│ │ - Retorna archivo .hex │ │ │ │ - Returns .hex file │ │
│ └─────────────────────────────────────────────────────┘ │ │ └─────────────────────────────────────────────────────┘ │
│ │ │ │ │ │
│ ▼ │ │ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │ │ ┌─────────────────────────────────────────────────────┐ │
│ │ Arduino CLI Service │ │ │ │ Arduino CLI Service │ │
│ │ (Invoca arduino-cli como subprocess) │ │ │ │ (Invokes arduino-cli as subprocess) │ │
│ └─────────────────────────────────────────────────────┘ │ │ └─────────────────────────────────────────────────────┘ │
└──────────────────────────┬──────────────────────────────────┘ └──────────────────────────┬──────────────────────────────────┘
┌──────────────────────┐ ┌──────────────────────┐
│ arduino-cli │ │ 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 Monaco Editor
Zustand (useEditorStore) Zustand (useEditorStore)
Estado: code State: code
``` ```
### 2. Compilación ### 2. Compilation
``` ```
Click en "Compile" Click "Compile"
EditorToolbar.tsx → compileCode() EditorToolbar.tsx → compileCode()
@ -79,31 +79,31 @@ Backend: ArduinoCLIService.compile()
arduino-cli compile --fqbn arduino:avr:uno 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() Frontend: useSimulatorStore.setCompiledHex()
``` ```
### 3. Simulación (Actualmente simplificada) ### 3. Simulation (Currently simplified)
``` ```
Click en "Run" Click "Run"
useSimulatorStore.setRunning(true) useSimulatorStore.setRunning(true)
SimulatorCanvas: useEffect detecta running=true SimulatorCanvas: useEffect detects running=true
setInterval cada 1000ms setInterval every 1000ms
updateComponentState('led-builtin', !state) 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) AVRSimulator.loadHex(hex)
@ -117,97 +117,97 @@ requestAnimationFrame loop
CPU.tick() × 267,000 cycles/frame CPU.tick() × 267,000 cycles/frame
Escribe en PORTB/PORTC/PORTD Writes to PORTB/PORTC/PORTD
Write hooks → PinManager.updatePort() Write hooks → PinManager.updatePort()
PinManager notifica callbacks PinManager notifies callbacks
Componentes actualizan estado visual Components update visual state
``` ```
## Componentes Clave ## Key Components
### Frontend ### Frontend
#### 1. Stores (Zustand) #### 1. Stores (Zustand)
- **[useEditorStore.ts](frontend/src/store/useEditorStore.ts)** - **[useEditorStore.ts](frontend/src/store/useEditorStore.ts)**
- `code`: Código fuente actual - `code`: Current source code
- `theme`: Tema del editor (dark/light) - `theme`: Editor theme (dark/light)
- `setCode()`: Actualizar código - `setCode()`: Update code
- **[useSimulatorStore.ts](frontend/src/store/useSimulatorStore.ts)** - **[useSimulatorStore.ts](frontend/src/store/useSimulatorStore.ts)**
- `running`: Estado de simulación - `running`: Simulation state
- `compiledHex`: Archivo hex compilado - `compiledHex`: Compiled hex file
- `components`: Lista de componentes electrónicos - `components`: List of electronic components
- `setCompiledHex()`: Guardar hex - `setCompiledHex()`: Save hex
- `updateComponentState()`: Actualizar LED/componente - `updateComponentState()`: Update LED/component
#### 2. Componentes UI #### 2. UI Components
- **[CodeEditor.tsx](frontend/src/components/editor/CodeEditor.tsx)** - **[CodeEditor.tsx](frontend/src/components/editor/CodeEditor.tsx)**
- Wrapper de Monaco Editor - Monaco Editor wrapper
- Syntax highlighting C++ - C++ syntax highlighting
- Auto-completado - Auto-completion
- **[EditorToolbar.tsx](frontend/src/components/editor/EditorToolbar.tsx)** - **[EditorToolbar.tsx](frontend/src/components/editor/EditorToolbar.tsx)**
- Botones: Compile, Run, Stop - Buttons: Compile, Run, Stop
- Manejo de estados de compilación - Compilation state handling
- Mensajes de error/éxito - Error/success messages
- **[SimulatorCanvas.tsx](frontend/src/components/simulator/SimulatorCanvas.tsx)** - **[SimulatorCanvas.tsx](frontend/src/components/simulator/SimulatorCanvas.tsx)**
- Renderiza Arduino Uno - Renders Arduino Uno
- Renderiza componentes (LEDs) - Renders components (LEDs)
- Loop de simulación - Simulation loop
#### 3. Wokwi Components Wrappers #### 3. Wokwi Component Wrappers
- **[LED.tsx](frontend/src/components/components-wokwi/LED.tsx)** - **[LED.tsx](frontend/src/components/components-wokwi/LED.tsx)**
- Wrapper React para `<wokwi-led>` - React wrapper for `<wokwi-led>`
- Props: color, value, x, y - Props: color, value, x, y
- **[ArduinoUno.tsx](frontend/src/components/components-wokwi/ArduinoUno.tsx)** - **[ArduinoUno.tsx](frontend/src/components/components-wokwi/ArduinoUno.tsx)**
- Wrapper React para `<wokwi-arduino-uno>` - React wrapper for `<wokwi-arduino-uno>`
- Control de LED interno (pin 13) - Internal LED control (pin 13)
- **[Resistor.tsx](frontend/src/components/components-wokwi/Resistor.tsx)** - **[Resistor.tsx](frontend/src/components/components-wokwi/Resistor.tsx)**
- Wrapper React para `<wokwi-resistor>` - React wrapper for `<wokwi-resistor>`
- Props: value (ohms) - Props: value (ohms)
- **[Pushbutton.tsx](frontend/src/components/components-wokwi/Pushbutton.tsx)** - **[Pushbutton.tsx](frontend/src/components/components-wokwi/Pushbutton.tsx)**
- Wrapper React para `<wokwi-pushbutton>` - React wrapper for `<wokwi-pushbutton>`
- Events: onPress, onRelease - Events: onPress, onRelease
### Backend ### Backend
#### 1. API Routes #### 1. API Routes
- **[compile.py](backend/app/api/routes/compile.py)** - **[compile.py](backend/app/api/routes/compile.py)**
- `POST /api/compile`: Compilar código - `POST /api/compile`: Compile code
- `GET /api/compile/boards`: Listar placas - `GET /api/compile/boards`: List boards
#### 2. Services #### 2. Services
- **[arduino_cli.py](backend/app/services/arduino_cli.py)** - **[arduino_cli.py](backend/app/services/arduino_cli.py)**
- `compile()`: Compilar sketch con arduino-cli - `compile()`: Compile sketch with arduino-cli
- `list_boards()`: Obtener placas disponibles - `list_boards()`: Get available boards
- Manejo de directorios temporales - Temporary directory management
### Wokwi Libraries (Clonadas Localmente) ### Wokwi Libraries (Cloned Locally)
#### 1. wokwi-elements #### 1. wokwi-elements
- **Ubicación**: `wokwi-libs/wokwi-elements/` - **Location**: `wokwi-libs/wokwi-elements/`
- **Build**: `dist/esm/` y `dist/cjs/` - **Build**: `dist/esm/` and `dist/cjs/`
- **Componentes**: 50+ elementos electrónicos - **Components**: 50+ electronic elements
- **Tecnología**: Lit (Web Components) - **Technology**: Lit (Web Components)
#### 2. avr8js #### 2. avr8js
- **Ubicación**: `wokwi-libs/avr8js/` - **Location**: `wokwi-libs/avr8js/`
- **Build**: `dist/esm/` y `dist/cjs/` - **Build**: `dist/esm/` and `dist/cjs/`
- **Funcionalidad**: Emulador completo de ATmega328p - **Functionality**: Complete ATmega328p emulator
- **Soporta**: CPU, Timers, USART, GPIO, ADC, etc. - **Supports**: CPU, Timers, USART, GPIO, ADC, etc.
#### 3. rp2040js #### 3. rp2040js
- **Ubicación**: `wokwi-libs/rp2040js/` - **Location**: `wokwi-libs/rp2040js/`
- **Uso futuro**: Soporte para Raspberry Pi Pico - **Future use**: Raspberry Pi Pico support
## Integración con Vite ## Vite Integration
### Alias Configuration ### Alias Configuration
```typescript ```typescript
@ -220,98 +220,98 @@ resolve: {
} }
``` ```
Esto permite: This allows:
- Usar repos locales en lugar de npm - Use local repos instead of npm
- Actualizar fácilmente con `git pull` - Easy updates with `git pull`
- Modificar código fuente si es necesario - Modify source code if needed
## Stack Tecnológico ## Technology Stack
### Frontend ### 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 | | Vite | 7.3 | Build tool & dev server |
| TypeScript | 5.9 | Tipado estático | | TypeScript | 5.9 | Static typing |
| Monaco Editor | 4.7 | Editor de código (VSCode) | | Monaco Editor | 4.7 | Code editor (VSCode) |
| Zustand | 5.0 | State management | | Zustand | 5.0 | State management |
| Axios | 1.13 | HTTP client | | Axios | 1.13 | HTTP client |
| wokwi-elements | 1.9.2 | Componentes electrónicos | | wokwi-elements | 1.9.2 | Electronic components |
| avr8js | 0.21.0 | Emulador AVR8 | | avr8js | 0.21.0 | AVR8 emulator |
### Backend ### Backend
| Tecnología | Versión | Propósito | | Technology | Version | Purpose |
|------------|---------|-----------| |------------|---------|-----------|
| Python | 3.12+ | Runtime | | Python | 3.12+ | Runtime |
| FastAPI | 0.115 | Web framework | | FastAPI | 0.115 | Web framework |
| Uvicorn | 0.32 | ASGI server | | Uvicorn | 0.32 | ASGI server |
| SQLAlchemy | 2.0 | ORM (futuro) | | SQLAlchemy | 2.0 | ORM (future) |
| aiosqlite | 0.20 | DB async (futuro) | | aiosqlite | 0.20 | Async DB (future) |
### Herramientas Externas ### External Tools
| Herramienta | Propósito | | Tool | Purpose |
|-------------|-----------| |------|---------|
| arduino-cli | Compilador de Arduino | | arduino-cli | Arduino compiler |
| Git | Control de versiones de Wokwi libs | | Git | Version control for Wokwi libs |
## Ventajas de la Arquitectura ## Architecture Advantages
### ✅ Separación de Responsabilidades ### ✅ Separation of Concerns
- **Frontend**: UI, UX, visualización - **Frontend**: UI, UX, visualization
- **Backend**: Compilación, lógica de negocio - **Backend**: Compilation, business logic
- **Wokwi Libs**: Emulación y componentes (mantenido por Wokwi) - **Wokwi Libs**: Emulation and components (maintained by Wokwi)
### ✅ Compatibilidad con Wokwi ### ✅ Wokwi Compatibility
- Repositorios oficiales = misma funcionalidad - Official repositories = same functionality
- Actualizaciones automáticas con `git pull` - Automatic updates with `git pull`
- Nuevos componentes disponibles inmediatamente - New components available immediately
### ✅ Escalabilidad ### ✅ Scalability
- Frontend puede agregar más componentes fácilmente - Frontend can easily add more components
- Backend puede agregar más endpoints (proyectos, sensores) - Backend can add more endpoints (projects, sensors)
- Wokwi libs se actualizan independientemente - Wokwi libs update independently
### ✅ Desarrollo Local ### ✅ Local Development
- No requiere internet para funcionar - No internet required to work
- Compilación local con arduino-cli - Local compilation with arduino-cli
- Base de datos local (SQLite) - Local database (SQLite)
## Próximas Mejoras ## Upcoming Improvements
### Fase 2: Emulación Real (avr8js) ### Phase 2: Real Emulation (avr8js)
``` ```
[ ] Implementar AVRSimulator.ts [ ] Implement AVRSimulator.ts
[ ] Parser de archivos Intel HEX [ ] Intel HEX file parser
[ ] PinManager con write hooks [ ] PinManager with write hooks
[ ] Integrar CPU execution loop [ ] Integrate CPU execution loop
[ ] Mapear pines Arduino a componentes [ ] Map Arduino pins to components
``` ```
### Fase 3: Más Componentes ### Phase 3: More Components
``` ```
[ ] Integrar más wokwi-elements [ ] Integrate more wokwi-elements
[ ] Botones, potenciómetros [ ] Buttons, potentiometers
[ ] Sensores (DHT22, HC-SR04) [ ] Sensors (DHT22, HC-SR04)
[ ] Pantallas (LCD, 7-segment) [ ] Displays (LCD, 7-segment)
``` ```
### Fase 4: Persistencia ### Phase 4: Persistence
``` ```
[ ] Base de datos SQLite [ ] SQLite database
[ ] Modelos SQLAlchemy [ ] SQLAlchemy models
[ ] CRUD de proyectos [ ] Project CRUD
[ ] Guardar circuitos como JSON [ ] Save circuits as JSON
``` ```
### Fase 5: Features Avanzadas ### Phase 5: Advanced Features
``` ```
[ ] Serial Monitor [ ] Serial Monitor
[ ] Wiring visual (drag & drop) [ ] Visual wiring (drag & drop)
[ ] Múltiples placas (Mega, Nano, ESP32) [ ] Multiple boards (Mega, Nano, ESP32)
[ ] Exportar a Wokwi.com [ ] Export to Wokwi.com
``` ```
## Referencias ## References
- [Wokwi Elements Repo](https://github.com/wokwi/wokwi-elements) - [Wokwi Elements Repo](https://github.com/wokwi/wokwi-elements)
- [AVR8js Repo](https://github.com/wokwi/avr8js) - [AVR8js Repo](https://github.com/wokwi/avr8js)

View File

@ -144,7 +144,7 @@ async def test_api_endpoint():
import httpx import httpx
# Test if server is running # Test if server is running
async with httpx.AsyncClient() as client: async with httpx.AsyncClient(follow_redirects=True) as client:
try: try:
response = await client.get("http://localhost:8001/api/compile/boards", timeout=5.0) response = await client.get("http://localhost:8001/api/compile/boards", timeout=5.0)
if response.status_code == 200: if response.status_code == 200:

BIN
doc/img1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

BIN
doc/img2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

View File

@ -93,16 +93,9 @@
align-items: center; align-items: center;
} }
.search-icon {
position: absolute;
left: 12px;
font-size: 18px;
pointer-events: none;
}
.search-input { .search-input {
width: 100%; width: 100%;
padding: 12px 40px 12px 40px; padding: 12px 40px 12px 12px;
font-size: 16px; font-size: 16px;
border: 2px solid #e0e0e0; border: 2px solid #e0e0e0;
border-radius: 8px; border-radius: 8px;

View File

@ -81,14 +81,13 @@ export const ComponentPickerModal: React.FC<ComponentPickerModalProps> = ({
<div className="modal-header"> <div className="modal-header">
<h2>Add Component</h2> <h2>Add Component</h2>
<button className="close-btn" onClick={onClose} aria-label="Close"> <button className="close-btn" onClick={onClose} aria-label="Close">
X
</button> </button>
</div> </div>
{/* Search Bar */} {/* Search Bar */}
<div className="search-section"> <div className="search-section">
<div className="search-input-wrapper"> <div className="search-input-wrapper">
<span className="search-icon">🔍</span>
<input <input
type="text" type="text"
className="search-input" className="search-input"
@ -103,7 +102,7 @@ export const ComponentPickerModal: React.FC<ComponentPickerModalProps> = ({
onClick={() => setSearchQuery('')} onClick={() => setSearchQuery('')}
aria-label="Clear search" aria-label="Clear search"
> >
X
</button> </button>
)} )}
</div> </div>

View File

@ -174,6 +174,8 @@ export const DynamicComponent: React.FC<DynamicComponentProps> = ({
userSelect: 'none', userSelect: 'none',
zIndex: isSelected ? 1000 : 1, zIndex: isSelected ? 1000 : 1,
pointerEvents: 'auto', pointerEvents: 'auto',
transform: properties.rotation ? `rotate(${properties.rotation}deg)` : undefined,
transformOrigin: 'center center',
}} }}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
onDoubleClick={handleDoubleClick} onDoubleClick={handleDoubleClick}

View File

@ -7,25 +7,25 @@ const COMPONENT_TEMPLATES: ComponentTemplate[] = [
{ {
type: 'led', type: 'led',
label: 'LED', label: 'LED',
icon: '💡', icon: 'L',
defaultProperties: { color: 'red' }, defaultProperties: { color: 'red' },
}, },
{ {
type: 'resistor', type: 'resistor',
label: 'Resistor', label: 'Resistor',
icon: '', icon: 'R',
defaultProperties: { value: 220 }, defaultProperties: { value: 220 },
}, },
{ {
type: 'pushbutton', type: 'pushbutton',
label: 'Button', label: 'Button',
icon: '🔘', icon: 'B',
defaultProperties: { color: 'red' }, defaultProperties: { color: 'red' },
}, },
{ {
type: 'potentiometer', type: 'potentiometer',
label: 'Potentiometer', label: 'Potentiometer',
icon: '🎛️', icon: 'P',
defaultProperties: { value: 50 }, defaultProperties: { value: 50 },
}, },
]; ];
@ -61,9 +61,9 @@ export const ComponentPalette = ({ onDragStart }: ComponentPaletteProps) => {
))} ))}
</div> </div>
<div className="palette-help"> <div className="palette-help">
<p>💡 Drag components to the canvas</p> <p>Drag components to the canvas</p>
<p>📍 Click a component to assign pin</p> <p>Click a component to assign pin</p>
<p>🗑 Press Delete to remove selected</p> <p>Press Delete to remove selected</p>
</div> </div>
</div> </div>
); );

View File

@ -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);
}

View File

@ -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<string, any>;
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<ComponentPropertyDialogProps> = ({
componentId,
componentMetadata,
componentProperties,
position,
pinInfo,
onClose,
onRotate,
onDelete,
}) => {
const dialogRef = useRef<HTMLDivElement>(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 (
<div
ref={dialogRef}
className="component-property-dialog"
style={{
left: `${dialogPosition.x}px`,
top: `${dialogPosition.y}px`,
}}
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="component-property-header">
<span className="component-property-title">{componentMetadata.name}</span>
<button
className="property-close-button"
onClick={onClose}
title="Close"
>
×
</button>
</div>
{/* Pin Roles Section */}
{pinInfo.length > 0 && (
<div className="pin-roles-section">
<div className="pin-roles-label">Pin Roles:</div>
{pinInfo.map((pin) => (
<div key={pin.name} className="pin-role-item">
<span className="pin-name"> {pin.name}</span>
{pin.description && (
<span className="pin-description"> ({pin.description})</span>
)}
</div>
))}
</div>
)}
{/* Current Arduino Pin Assignment */}
{componentProperties.pin !== undefined && (
<div className="pin-assignment-section">
<div className="pin-assignment-label">Arduino Pin:</div>
<div className="pin-assignment-value">
{componentProperties.pin >= 14
? `A${componentProperties.pin - 14}`
: `D${componentProperties.pin}`}
</div>
</div>
)}
{/* Action Buttons */}
<div className="property-actions">
<button
className="property-action-button rotate-button"
onClick={() => onRotate(componentId)}
title="Rotate 90°"
>
Rotate
</button>
<button
className="property-action-button delete-button"
onClick={() => {
if (window.confirm(`Delete ${componentMetadata.name}?`)) {
onDelete(componentId);
}
}}
title="Delete component"
>
Delete
</button>
</div>
</div>
);
};

View File

@ -9,8 +9,8 @@ import React, { useEffect, useState } from 'react';
interface PinInfo { interface PinInfo {
name: string; name: string;
x: number; // mm x: number; // CSS pixels
y: number; // mm y: number; // CSS pixels
signals?: Array<{ type: string; signal?: string }>; signals?: Array<{ type: string; signal?: string }>;
} }
@ -22,8 +22,6 @@ interface PinOverlayProps {
showPins: boolean; showPins: boolean;
} }
const MM_TO_PX = 3.7795275591;
export const PinOverlay: React.FC<PinOverlayProps> = ({ export const PinOverlay: React.FC<PinOverlayProps> = ({
componentId, componentId,
componentX, componentX,
@ -52,12 +50,13 @@ export const PinOverlay: React.FC<PinOverlayProps> = ({
left: `${componentX + 6}px`, // +6px for wrapper padding (4px padding + 2px border) left: `${componentX + 6}px`, // +6px for wrapper padding (4px padding + 2px border)
top: `${componentY + 6}px`, top: `${componentY + 6}px`,
pointerEvents: 'none', pointerEvents: 'none',
zIndex: 10, zIndex: 1002, // Above property dialog (1001)
}} }}
> >
{pins.map((pin) => { {pins.map((pin) => {
const pinX = pin.x * MM_TO_PX; // Pin coordinates are already in CSS pixels
const pinY = pin.y * MM_TO_PX; const pinX = pin.x;
const pinY = pin.y;
return ( return (
<div <div

View File

@ -7,20 +7,19 @@
/* Add Component Button */ /* Add Component Button */
.add-component-btn { .add-component-btn {
padding: 8px 16px; padding: 8px 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); background: #007acc;
color: white; color: white;
border: none; border: none;
border-radius: 6px; border-radius: 6px;
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
transition: all 0.2s; transition: all 0.2s;
white-space: nowrap; white-space: nowrap;
} }
.add-component-btn:hover { .add-component-btn:hover {
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); background: #005a9e;
transform: translateY(-1px); transform: translateY(-1px);
} }
@ -84,10 +83,10 @@
padding: 20px; padding: 20px;
position: relative; position: relative;
overflow: auto; overflow: auto;
background-color: #1e1e1e;
background-image: background-image:
linear-gradient(rgba(255, 255, 255, 0.03) 1px, transparent 1px), repeating-linear-gradient(0deg, transparent, transparent 19px, rgba(255, 255, 255, 0.03) 19px, rgba(255, 255, 255, 0.03) 20px),
linear-gradient(90deg, rgba(255, 255, 255, 0.03) 1px, transparent 1px); repeating-linear-gradient(90deg, transparent, transparent 19px, rgba(255, 255, 255, 0.03) 19px, rgba(255, 255, 255, 0.03) 20px);
background-size: 20px 20px;
} }
.arduino-board { .arduino-board {

View File

@ -2,6 +2,7 @@ import { useSimulatorStore, ARDUINO_POSITION } from '../../store/useSimulatorSto
import React, { useEffect, useState, useRef } from 'react'; import React, { useEffect, useState, useRef } from 'react';
import { ArduinoUno } from '../components-wokwi/ArduinoUno'; import { ArduinoUno } from '../components-wokwi/ArduinoUno';
import { ComponentPickerModal } from '../ComponentPickerModal'; import { ComponentPickerModal } from '../ComponentPickerModal';
import { ComponentPropertyDialog } from './ComponentPropertyDialog';
import { DynamicComponent, createComponentFromMetadata } from '../DynamicComponent'; import { DynamicComponent, createComponentFromMetadata } from '../DynamicComponent';
import { ComponentRegistry } from '../../services/ComponentRegistry'; import { ComponentRegistry } from '../../services/ComponentRegistry';
import { PinSelector } from './PinSelector'; import { PinSelector } from './PinSelector';
@ -39,13 +40,19 @@ export const SimulatorCanvas = () => {
const [showPinSelector, setShowPinSelector] = useState(false); const [showPinSelector, setShowPinSelector] = useState(false);
const [pinSelectorPos, setPinSelectorPos] = useState({ x: 0, y: 0 }); const [pinSelectorPos, setPinSelectorPos] = useState({ x: 0, y: 0 });
// Component property dialog
const [showPropertyDialog, setShowPropertyDialog] = useState(false);
const [propertyDialogComponentId, setPropertyDialogComponentId] = useState<string | null>(null);
const [propertyDialogPosition, setPropertyDialogPosition] = useState({ x: 0, y: 0 });
// Click vs drag detection
const [clickStartTime, setClickStartTime] = useState<number>(0);
const [clickStartPos, setClickStartPos] = useState({ x: 0, y: 0 });
// Component dragging state // Component dragging state
const [draggedComponentId, setDraggedComponentId] = useState<string | null>(null); const [draggedComponentId, setDraggedComponentId] = useState<string | null>(null);
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
// Pin visualization
const [hoveredComponentId, setHoveredComponentId] = useState<string | null>(null);
// Canvas ref for coordinate calculations // Canvas ref for coordinate calculations
const canvasRef = useRef<HTMLDivElement>(null); const canvasRef = useRef<HTMLDivElement>(null);
@ -135,15 +142,33 @@ export const SimulatorCanvas = () => {
} as any); } 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 // Component dragging handlers
const handleComponentMouseDown = (componentId: string, e: React.MouseEvent) => { const handleComponentMouseDown = (componentId: string, e: React.MouseEvent) => {
// Don't start dragging if we're clicking on the pin selector // Don't start dragging if we're clicking on the pin selector or property dialog
if (showPinSelector) return; if (showPinSelector || showPropertyDialog) return;
e.stopPropagation(); e.stopPropagation();
const component = components.find((c) => c.id === componentId); const component = components.find((c) => c.id === componentId);
if (!component || !canvasRef.current) return; 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 // Get canvas position to convert viewport coords to canvas coords
const canvasRect = canvasRef.current.getBoundingClientRect(); const canvasRect = canvasRef.current.getBoundingClientRect();
@ -180,8 +205,25 @@ export const SimulatorCanvas = () => {
} }
}; };
const handleCanvasMouseUp = () => { const handleCanvasMouseUp = (e: React.MouseEvent) => {
if (draggedComponentId) { 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 // Recalculate wire positions after moving component
recalculateAllWirePositions(); recalculateAllWirePositions();
setDraggedComponentId(null); setDraggedComponentId(null);
@ -190,6 +232,11 @@ export const SimulatorCanvas = () => {
// Wire creation via pin clicks // Wire creation via pin clicks
const handlePinClick = (componentId: string, pinName: string, x: number, y: number) => { const handlePinClick = (componentId: string, pinName: string, x: number, y: number) => {
// Close property dialog when starting wire creation
if (showPropertyDialog) {
setShowPropertyDialog(false);
}
if (wireInProgress) { if (wireInProgress) {
// Finish wire creation // Finish wire creation
finishWireCreation({ finishWireCreation({
@ -230,8 +277,8 @@ export const SimulatorCanvas = () => {
} }
const isSelected = selectedComponentId === component.id; const isSelected = selectedComponentId === component.id;
const isHovered = hoveredComponentId === component.id; // Always show pins for better UX when creating wires
const showPinsForComponent = isHovered || wireInProgress !== null; const showPinsForComponent = true;
return ( return (
<React.Fragment key={component.id}> <React.Fragment key={component.id}>
@ -248,8 +295,6 @@ export const SimulatorCanvas = () => {
onDoubleClick={(e) => { onDoubleClick={(e) => {
handleComponentDoubleClick(component.id, e); handleComponentDoubleClick(component.id, e);
}} }}
onMouseEnter={() => setHoveredComponentId(component.id)}
onMouseLeave={() => setHoveredComponentId(null)}
/> />
{/* Pin overlay for wire creation */} {/* Pin overlay for wire creation */}
@ -308,7 +353,7 @@ export const SimulatorCanvas = () => {
componentX={ARDUINO_POSITION.x} componentX={ARDUINO_POSITION.x}
componentY={ARDUINO_POSITION.y} componentY={ARDUINO_POSITION.y}
onPinClick={handlePinClick} onPinClick={handlePinClick}
showPins={wireInProgress !== null} showPins={true}
/> />
{/* Components using wokwi-elements */} {/* 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 (
<ComponentPropertyDialog
componentId={propertyDialogComponentId}
componentMetadata={metadata}
componentProperties={component.properties}
position={propertyDialogPosition}
pinInfo={pinInfo || []}
onClose={() => setShowPropertyDialog(false)}
onRotate={handleRotateComponent}
onDelete={(id) => {
removeComponent(id);
setShowPropertyDialog(false);
}}
/>
);
})()}
{/* Component Picker Modal */} {/* Component Picker Modal */}
<ComponentPickerModal <ComponentPickerModal
isOpen={showComponentPicker} isOpen={showComponentPicker}

View File

@ -146,15 +146,15 @@ export class ComponentRegistry {
*/ */
static getCategoryDisplayName(category: ComponentCategory): string { static getCategoryDisplayName(category: ComponentCategory): string {
const displayNames: Record<ComponentCategory, string> = { const displayNames: Record<ComponentCategory, string> = {
boards: '🖥️ Boards', boards: 'Boards',
sensors: '📡 Sensors', sensors: 'Sensors',
displays: '📺 Displays', displays: 'Displays',
input: '🎮 Input', input: 'Input',
output: '💡 Output', output: 'Output',
motors: '⚙️ Motors', motors: 'Motors',
communication: '📶 Communication', communication: 'Communication',
passive: '🔌 Passive', passive: 'Passive',
other: '📦 Other', other: 'Other',
}; };
return displayNames[category] || category; return displayNames[category] || category;
} }