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 │
└──────────────────────────┬──────────────────────────────────┘
@ -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 `<wokwi-led>`
- React wrapper for `<wokwi-led>`
- Props: color, value, x, y
- **[ArduinoUno.tsx](frontend/src/components/components-wokwi/ArduinoUno.tsx)**
- Wrapper React para `<wokwi-arduino-uno>`
- Control de LED interno (pin 13)
- React wrapper for `<wokwi-arduino-uno>`
- Internal LED control (pin 13)
- **[Resistor.tsx](frontend/src/components/components-wokwi/Resistor.tsx)**
- Wrapper React para `<wokwi-resistor>`
- React wrapper for `<wokwi-resistor>`
- Props: value (ohms)
- **[Pushbutton.tsx](frontend/src/components/components-wokwi/Pushbutton.tsx)**
- Wrapper React para `<wokwi-pushbutton>`
- React wrapper for `<wokwi-pushbutton>`
- 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)

View File

@ -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:

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;
}
.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;

View File

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

View File

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

View File

@ -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) => {
))}
</div>
<div className="palette-help">
<p>💡 Drag components to the canvas</p>
<p>📍 Click a component to assign pin</p>
<p>🗑 Press Delete to remove selected</p>
<p>Drag components to the canvas</p>
<p>Click a component to assign pin</p>
<p>Press Delete to remove selected</p>
</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 {
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<PinOverlayProps> = ({
componentId,
componentX,
@ -52,12 +50,13 @@ export const PinOverlay: React.FC<PinOverlayProps> = ({
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 (
<div

View File

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

View File

@ -2,6 +2,7 @@ import { useSimulatorStore, ARDUINO_POSITION } from '../../store/useSimulatorSto
import React, { useEffect, useState, useRef } from 'react';
import { ArduinoUno } from '../components-wokwi/ArduinoUno';
import { ComponentPickerModal } from '../ComponentPickerModal';
import { ComponentPropertyDialog } from './ComponentPropertyDialog';
import { DynamicComponent, createComponentFromMetadata } from '../DynamicComponent';
import { ComponentRegistry } from '../../services/ComponentRegistry';
import { PinSelector } from './PinSelector';
@ -39,13 +40,19 @@ export const SimulatorCanvas = () => {
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<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
const [draggedComponentId, setDraggedComponentId] = useState<string | null>(null);
const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 });
// Pin visualization
const [hoveredComponentId, setHoveredComponentId] = useState<string | null>(null);
// Canvas ref for coordinate calculations
const canvasRef = useRef<HTMLDivElement>(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 (
<React.Fragment key={component.id}>
@ -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 (
<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 */}
<ComponentPickerModal
isOpen={showComponentPicker}

View File

@ -146,15 +146,15 @@ export class ComponentRegistry {
*/
static getCategoryDisplayName(category: ComponentCategory): string {
const displayNames: Record<ComponentCategory, string> = {
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;
}