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 │
|
│ 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)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
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;
|
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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
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
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue