feat: update architecture documentation and improve component property dialog
parent
5490ad42c8
commit
217736c7cd
256
ARCHITECTURE.md
256
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 `<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)
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue