From 879decab22eaec7688259b0b5f73e7b6f5ce1c3c Mon Sep 17 00:00:00 2001 From: David Montero Crespo Date: Wed, 4 Mar 2026 18:34:25 -0300 Subject: [PATCH] feat: Implement visual wiring system with wire rendering, colors, and validation --- fancy-splashing-shannon.md | 1125 +++++++++++++++++++++++++----------- 1 file changed, 790 insertions(+), 335 deletions(-) diff --git a/fancy-splashing-shannon.md b/fancy-splashing-shannon.md index cc5ffc5..3887f99 100644 --- a/fancy-splashing-shannon.md +++ b/fancy-splashing-shannon.md @@ -1,389 +1,844 @@ -# Plan: Construcción de Emulador Arduino Local (Wokwi Clone) +# Plan: Visual Wiring System for Wokwi Clone (Incremental Implementation) -## Resumen Ejecutivo +## Vision -Crear una aplicación web local que permita editar, compilar y emular código Arduino con visualización de componentes electrónicos en tiempo real. +Implement a complete visual wiring system similar to Wokwi, with draggable connections, automatic colors based on signal type, electrical connection validation, and intelligent routing that avoids components. -**Arquitectura:** Monolito web (React + Vite) con backend FastAPI para compilación -**Prioridades:** Editor de código + compilación, emulación Arduino Uno, componentes básicos (LED, resistencias) -**Persistencia:** SQLite +## User Requirements (from Q&A) -## Tecnologías Core +1. **Wire Colors**: Automatic based on signal type (Red=VCC, Black=GND, Blue=Analog, etc.) +2. **Connection Validation**: Strict validation - prevents electrically invalid connections +3. **Routing**: Intelligent automatic with A* algorithm to avoid components +4. **Implementation**: Incremental phases (Phase 1: Basic rendering → Phase 2: Editing → Phase 3: Validation) -### Frontend -- **React + Vite + TypeScript** - Framework principal -- **Monaco Editor** (`@monaco-editor/react`) - Editor de código -- **avr8js** - Emulador AVR8 (ATmega328p = Arduino Uno) -- **@wokwi/elements** - Componentes electrónicos web (LEDs, resistencias) -- **Zustand** - State management +## Architecture Overview -### Backend -- **FastAPI + Python** - API REST -- **arduino-cli** - Compilador de Arduino -- **SQLAlchemy + SQLite** - Base de datos +### Current State +- React + TypeScript + Vite + Zustand +- Canvas: Absolute positioning, 20px grid, coordinates in pixels +- Components: wokwi-elements web components (LED, Arduino, Resistor, etc.) +- **NO** wiring infrastructure currently exists -## Estructura del Proyecto +### Key Discovery: Pin Information API -``` -wokwi_clon/ -├── frontend/ # React + Vite -│ ├── src/ -│ │ ├── components/ -│ │ │ ├── editor/ -│ │ │ │ ├── CodeEditor.tsx # Monaco Editor wrapper -│ │ │ │ └── EditorToolbar.tsx # Compile/Run/Stop -│ │ │ ├── simulator/ -│ │ │ │ ├── SimulatorCanvas.tsx # Canvas principal -│ │ │ │ └── ArduinoBoard.tsx # Visualización Arduino Uno -│ │ │ ├── components-wokwi/ -│ │ │ │ ├── LED.tsx # Wrapper para wokwi-led -│ │ │ │ └── Resistor.tsx # Wrapper para wokwi-resistor -│ │ │ └── projects/ -│ │ │ ├── ProjectList.tsx # Lista de proyectos -│ │ │ └── ProjectDialog.tsx # Guardar/Cargar -│ │ ├── simulation/ -│ │ │ ├── AVRSimulator.ts # Core: avr8js wrapper -│ │ │ ├── PinManager.ts # Gestión de pines -│ │ │ └── ComponentRegistry.ts # Registro de componentes -│ │ ├── store/ -│ │ │ ├── useSimulatorStore.ts # Estado de simulación (Zustand) -│ │ │ ├── useEditorStore.ts # Estado del editor -│ │ │ └── useProjectStore.ts # Estado de proyectos -│ │ ├── services/ -│ │ │ ├── api.ts # Cliente API -│ │ │ └── compilation.ts # Servicio de compilación -│ │ └── utils/ -│ │ └── hexParser.ts # Parser de archivos .hex -│ ├── package.json -│ └── vite.config.ts -│ -├── backend/ # Python + FastAPI -│ ├── app/ -│ │ ├── main.py # Entry point -│ │ ├── api/routes/ -│ │ │ ├── compile.py # POST /api/compile -│ │ │ └── projects.py # CRUD proyectos -│ │ ├── services/ -│ │ │ └── arduino_cli.py # Integración arduino-cli -│ │ ├── models/ -│ │ │ └── project.py # Modelo SQLAlchemy -│ │ └── database/ -│ │ └── connection.py # Conexión SQLite -│ └── requirements.txt -│ -└── README.md -``` - -## Flujo de Datos Principal - -### 1. Compilación (Editor → Backend → Hex) -``` -Usuario escribe código en Monaco Editor - ↓ -Click "Compile" - ↓ -POST /api/compile { code: "...", board_fqbn: "arduino:avr:uno" } - ↓ -Backend: arduino-cli compila código a .hex - ↓ -Backend retorna { success: true, hex_content: "..." } - ↓ -Frontend: useSimulatorStore.loadHex(hex) -``` - -### 2. Emulación (Hex → CPU → Pines → Componentes) -``` -AVRSimulator.loadHex(hexString) - ↓ -Parse hex → Uint16Array (program memory) - ↓ -Inicializar CPU (ATmega328p) - ↓ -Click "Run" - ↓ -Execution loop (requestAnimationFrame) - ↓ -CPU ejecuta ~267k cycles/frame @ 60fps - ↓ -CPU escribe en PORTB/PORTC/PORTD - ↓ -Write hooks → PinManager.updatePort() - ↓ -PinManager notifica componentes conectados - ↓ -LED actualiza estado visual (ref.current.value = true/false) -``` - -## Archivos Críticos para Implementar - -### 1. `frontend/src/simulation/AVRSimulator.ts` -**Motor de emulación** -- Integra avr8js (CPU, AVRTimer, AVRUSART) -- Carga archivos .hex a memoria de programa -- Loop de ejecución con requestAnimationFrame -- Write hooks en registros PORT (0x25=PORTB, 0x28=PORTC, 0x2B=PORTD) +All wokwi-elements expose `pinInfo: ElementPin[]`: ```typescript -export class AVRSimulator { - private cpu: CPU | null = null; - private program: Uint16Array | null = null; - - loadHex(hexContent: string) { - const bytes = hexToUint8Array(hexContent); - this.program = new Uint16Array(16384); // 32KB - // Load bytes into program memory... - this.cpu = new CPU(this.program); - this.setupPinHooks(); - } - - start() { - const execute = () => { - for (let i = 0; i < 267000; i++) { - this.cpu.tick(); - } - requestAnimationFrame(execute); - }; - requestAnimationFrame(execute); - } +interface ElementPin { + name: string; // e.g., 'A', 'C', 'GND.1', '13' + x: number; // X coordinate in millimeters (relative to element origin) + y: number; // Y coordinate in millimeters + signals: PinSignalInfo[]; // Signal types (power, analog, i2c, etc.) } ``` -### 2. `frontend/src/components/components-wokwi/LED.tsx` -**Wrapper React para Web Components** -- Importa `@wokwi/elements` -- Usa `useRef` para manipular DOM directamente -- Propiedades via asignación directa (no atributos) +Example - LED pins: +```typescript +get pinInfo(): ElementPin[] { + return [ + { name: 'A', x: 25, y: 42, signals: [], description: 'Anode' }, + { name: 'C', x: 15, y: 42, signals: [], description: 'Cathode' }, + ]; +} +``` + +### Critical Challenge: Coordinate Systems + +There are 3 coordinate systems: + +1. **Element Space (mm)**: `pinInfo` uses millimeters relative to element origin +2. **SVG Viewport**: wokwi-elements use SVG with internal viewBox +3. **Canvas Space (pixels)**: Absolute positioning on canvas + +**Required conversion**: `1mm = 3.7795275591 pixels` (standard 96 DPI) + +--- + +## PHASE 1: Basic Wire Rendering (MVP) + +### Objetivos +- Dibujar cables estáticos en el canvas +- Colores automáticos según tipo de señal +- Paths SVG simples (forma de L) +- Crear cables manualmente via interfaz + +### Data Model + +**Archivo nuevo**: `frontend/src/types/wire.ts` ```typescript -import '@wokwi/elements'; +export interface WireEndpoint { + componentId: string; // ID del componente (e.g., 'led-123', 'arduino-uno') + pinName: string; // Nombre del pin (e.g., 'A', 'GND.1', '13') + x: number; // Posición absoluta en canvas (pixels) + y: number; +} -export const LED = ({ color, value, x, y }) => { - const ledRef = useRef(null); +export interface Wire { + id: string; + start: WireEndpoint; + end: WireEndpoint; + controlPoints: { x: number; y: number; id: string }[]; + color: string; // Calculado automáticamente del tipo de señal + signalType: WireSignalType | null; + isValid: boolean; +} - useEffect(() => { - if (ledRef.current) { - (ledRef.current as any).value = value; - (ledRef.current as any).color = color; - } - }, [value, color]); +export type WireSignalType = + | 'power-vcc' | 'power-gnd' | 'analog' | 'digital' + | 'pwm' | 'i2c' | 'spi' | 'usart'; +``` - return ; +### Zustand Store Extension + +**Modificar**: `frontend/src/store/useSimulatorStore.ts` + +Agregar al `SimulatorState`: +```typescript +interface SimulatorState { + // ... existing properties ... + + wires: Wire[]; + selectedWireId: string | null; + wireInProgress: WireInProgress | null; + + addWire: (wire: Wire) => void; + removeWire: (wireId: string) => void; + updateWire: (wireId: string, updates: Partial) => void; + setSelectedWire: (wireId: string | null) => void; + startWireCreation: (endpoint: WireEndpoint) => void; + finishWireCreation: (endpoint: WireEndpoint) => void; + cancelWireCreation: () => void; + updateWirePositions: (componentId: string) => void; +} +``` + +### Pin Position Calculator (CRÍTICO) + +**Archivo nuevo**: `frontend/src/utils/pinPositionCalculator.ts` + +Esta es la pieza MÁS IMPORTANTE - convierte coordenadas de pines (mm) a coordenadas de canvas (pixels): + +```typescript +const MM_TO_PX = 3.7795275591; // Conversión estándar 96 DPI + +export function calculatePinPosition( + componentId: string, + pinName: string, + componentX: number, // Posición del componente en canvas + componentY: number +): { x: number; y: number } | null { + const element = document.getElementById(componentId); + if (!element) return null; + + const pinInfo = (element as any).pinInfo as ElementPin[]; + const pin = pinInfo.find(p => p.name === pinName); + if (!pin) return null; + + // Convertir mm a pixels y sumar posición del componente + return { + x: componentX + (pin.x * MM_TO_PX), + y: componentY + (pin.y * MM_TO_PX), + }; +} + +export function getAllPinPositions( + componentId: string, + componentX: number, + componentY: number +): Array<{ name: string; x: number; y: number; signals: PinSignalInfo[] }> { + // Retorna todos los pines con posiciones absolutas +} + +export function findClosestPin( + componentId: string, + componentX: number, + componentY: number, + targetX: number, + targetY: number, + maxDistance: number = 20 +): { name: string; x: number; y: number } | null { + // Encuentra el pin más cercano a una posición (para snapping) +} +``` + +### Wire Colors + +**Archivo nuevo**: `frontend/src/utils/wireColors.ts` + +```typescript +export const WIRE_COLORS = { + 'power-vcc': '#ff0000', // Rojo + 'power-gnd': '#000000', // Negro + 'analog': '#4169e1', // Azul + 'digital': '#00ff00', // Verde + 'pwm': '#8b5cf6', // Morado + 'i2c': '#ffd700', // Amarillo + 'spi': '#ff8c00', // Naranja + 'usart': '#00ced1', // Cyan +}; + +export function determineSignalType(signals: PinSignalInfo[]): WireSignalType | null { + // Prioridad: power > protocolos especializados > PWM > analog > digital + if (signals.find(s => s.type === 'power' && s.signal === 'VCC')) return 'power-vcc'; + if (signals.find(s => s.type === 'power' && s.signal === 'GND')) return 'power-gnd'; + if (signals.find(s => s.type === 'i2c')) return 'i2c'; + // ... etc + return 'digital'; +} + +export function getWireColor(signalType: WireSignalType | null): string { + return signalType ? WIRE_COLORS[signalType] : '#00ff00'; +} +``` + +### Wire Path Generation (Phase 1: Simple) + +**Archivo nuevo**: `frontend/src/utils/wirePathGenerator.ts` + +```typescript +export function generateWirePath(wire: Wire): string { + const { start, end, controlPoints } = wire; + + if (controlPoints.length === 0) { + // Phase 1: Forma de L simple + return generateSimplePath(start.x, start.y, end.x, end.y); + } + // Phase 2 y 3 usarán controlPoints +} + +function generateSimplePath(x1: number, y1: number, x2: number, y2: number): string { + const midX = x1 + (x2 - x1) / 2; + + // L-shape: horizontal primero, luego vertical + return `M ${x1} ${y1} L ${midX} ${y1} L ${midX} ${y2} L ${x2} ${y2}`; +} +``` + +### Wire Rendering Components + +**Archivo nuevo**: `frontend/src/components/simulator/WireLayer.tsx` + +```typescript +export const WireLayer: React.FC = () => { + const { wires, wireInProgress, selectedWireId } = useSimulatorStore(); + + return ( + + {wires.map(wire => ( + + ))} + + {wireInProgress && ( + + )} + + ); }; ``` -### 3. `backend/app/services/arduino_cli.py` -**Integración con arduino-cli** -- Compilación asíncrona con asyncio -- Manejo de archivos temporales -- Parse de errores de compilación - -```python -async def compile(code: str, board_fqbn: str = "arduino:avr:uno") -> dict: - with tempfile.TemporaryDirectory() as temp_dir: - sketch_dir = Path(temp_dir) / "sketch" - sketch_dir.mkdir() - (sketch_dir / "sketch.ino").write_text(code) - - process = await asyncio.create_subprocess_exec( - "arduino-cli", "compile", - "--fqbn", board_fqbn, - "--output-dir", str(sketch_dir / "build"), - str(sketch_dir), - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.PIPE - ) - - stdout, stderr = await process.communicate() - - if process.returncode == 0: - hex_file = sketch_dir / "build" / "sketch.ino.hex" - return { - "success": True, - "hex_content": hex_file.read_text() - } - else: - return { - "success": False, - "error": stderr.decode() - } -``` - -### 4. `frontend/src/store/useSimulatorStore.ts` -**Estado global de simulación (Zustand)** -- Simulator instance -- Estado running/stopped -- Lista de componentes -- Actions: loadHex, start, stop, addComponent +**Archivo nuevo**: `frontend/src/components/simulator/WireRenderer.tsx` ```typescript -export const useSimulatorStore = create((set, get) => ({ - simulator: null, - running: false, - components: [], +export const WireRenderer: React.FC<{ wire: Wire; isSelected: boolean }> = ({ wire, isSelected }) => { + const { setSelectedWire } = useSimulatorStore(); - loadHex: (hex: string) => { - const { simulator } = get(); - simulator?.loadHex(hex); - }, + const path = useMemo(() => generateWirePath(wire), [wire]); - startSimulation: () => { - get().simulator?.start(); - set({ running: true }); - }, + return ( + + {/* Path invisible para click fácil */} + setSelectedWire(wire.id)} + /> - addComponent: (component) => { - set(state => ({ - components: [...state.components, component] - })); + {/* Path visible */} + + + {/* Endpoints */} + + + + ); +}; +``` + +### Integration with Canvas + +**Modificar**: `frontend/src/components/simulator/SimulatorCanvas.tsx` + +```typescript +// Agregar import +import { WireLayer } from './WireLayer'; + +// En el JSX, agregar wire layer DEBAJO de componentes: +
+ {/* Wire layer - z-index: 1 */} + + + {/* Arduino Uno - z-index: 2 */} + + + {/* Components */} +
+ {components.map(renderComponent)} +
+
+``` + +### Phase 1 Implementation Steps + +1. ✅ Crear `types/wire.ts` +2. ✅ Extender Zustand store con wire state +3. ✅ Implementar `pinPositionCalculator.ts` +4. ✅ Implementar `wireColors.ts` +5. ✅ Implementar `wirePathGenerator.ts` (simple) +6. ✅ Crear `WireLayer.tsx` +7. ✅ Crear `WireRenderer.tsx` +8. ✅ Integrar `WireLayer` en `SimulatorCanvas.tsx` +9. ✅ Agregar cables de prueba al store +10. ✅ Verificar rendering y colores + +--- + +## PHASE 2: Wire Editing & Interaction + +### Objetivos +- Crear cables haciendo click en pines +- Editar cables con puntos de control violetas +- Arrastrar puntos de control (horizontal ↔ vertical) +- Seleccionar y eliminar cables + +### Wire Creation System + +**Archivo nuevo**: `frontend/src/components/simulator/PinOverlay.tsx` + +Muestra marcadores clickeables en todos los pines cuando se está creando un cable: + +```typescript +export const PinOverlay: React.FC = () => { + const { components, wireInProgress } = useSimulatorStore(); + + if (!wireInProgress) return null; + + return ( +
+ {components.map(component => { + const pins = getAllPinPositions(component.id, component.x, component.y); + + return pins.map(pin => ( +
handlePinClick(component.id, pin.name, e)} + /> + )); + })} +
+ ); +}; +``` + +**Archivo nuevo**: `frontend/src/components/simulator/WireCreationHandler.tsx` + +Hook para manejar la creación de cables: + +```typescript +export const useWireCreation = () => { + const { + components, + wireInProgress, + startWireCreation, + finishWireCreation, + cancelWireCreation, + addWire, + } = useSimulatorStore(); + + const handlePinClick = useCallback(( + componentId: string, + pinName: string, + event: React.MouseEvent + ) => { + event.stopPropagation(); + + const component = components.find(c => c.id === componentId); + if (!component) return; + + const pinPosition = calculatePinPosition( + componentId, pinName, component.x, component.y + ); + + if (!wireInProgress) { + // Iniciar nuevo cable + startWireCreation({ componentId, pinName, ...pinPosition }); + } else { + // Completar cable + const element = document.getElementById(componentId) as any; + const pinInfo = element?.pinInfo?.find((p: any) => p.name === pinName); + + const signalType = determineSignalType(pinInfo?.signals || []); + + const newWire = { + id: uuidv4(), + start: wireInProgress.startEndpoint, + end: { componentId, pinName, ...pinPosition }, + controlPoints: [], + color: getWireColor(signalType), + signalType, + isValid: true, // Phase 3 agregará validación + }; + + addWire(newWire); + cancelWireCreation(); + } + }, [wireInProgress, components]); + + // Mouse move para mostrar preview + const handleMouseMove = useCallback((event: MouseEvent) => { + if (!wireInProgress) return; + + const canvas = document.querySelector('.canvas-content'); + const rect = canvas.getBoundingClientRect(); + updateWireInProgress( + event.clientX - rect.left, + event.clientY - rect.top + ); + }, [wireInProgress]); + + // ESC para cancelar + const handleKeyDown = useCallback((event: KeyboardEvent) => { + if (event.key === 'Escape' && wireInProgress) { + cancelWireCreation(); + } + }, [wireInProgress]); + + return { handlePinClick }; +}; +``` + +### Control Points + +**Archivo nuevo**: `frontend/src/components/simulator/ControlPoint.tsx` + +```typescript +export const ControlPoint: React.FC<{ + x: number; + y: number; + onDrag: (newX: number, newY: number) => void; +}> = ({ x, y, onDrag }) => { + const [isDragging, setIsDragging] = useState(false); + + const handleMouseMove = useCallback((e: MouseEvent) => { + if (!isDragging) return; + + const canvas = document.querySelector('.canvas-content'); + const rect = canvas.getBoundingClientRect(); + + const newX = e.clientX - rect.left; + const newY = e.clientY - rect.top; + + // Snap to grid (20px) + const snappedX = Math.round(newX / 20) * 20; + const snappedY = Math.round(newY / 20) * 20; + + onDrag(snappedX, snappedY); + }, [isDragging, onDrag]); + + return ( + setIsDragging(true)} + /> + ); +}; +``` + +### Multi-segment Path Generation + +Actualizar `wirePathGenerator.ts`: + +```typescript +function generateMultiSegmentPath( + start: { x: number; y: number }, + controlPoints: WireControlPoint[], + end: { x: number; y: number } +): string { + let path = `M ${start.x} ${start.y}`; + + // Agregar control points con restricción ortogonal + for (let i = 0; i < controlPoints.length; i++) { + const cp = controlPoints[i]; + const prev = i === 0 ? start : controlPoints[i - 1]; + + // Forzar segmentos horizontales o verticales + if (Math.abs(cp.x - prev.x) > Math.abs(cp.y - prev.y)) { + path += ` L ${cp.x} ${prev.y} L ${cp.x} ${cp.y}`; + } else { + path += ` L ${prev.x} ${cp.y} L ${cp.x} ${cp.y}`; + } } -})); -``` -### 5. `backend/app/models/project.py` -**Modelo de base de datos** + // Conectar al endpoint + const lastPoint = controlPoints[controlPoints.length - 1]; + if (Math.abs(end.x - lastPoint.x) > Math.abs(end.y - lastPoint.y)) { + path += ` L ${end.x} ${lastPoint.y} L ${end.x} ${end.y}`; + } else { + path += ` L ${lastPoint.x} ${end.y} L ${end.x} ${end.y}`; + } -```python -class Project(Base): - __tablename__ = "projects" - - id = Column(Integer, primary_key=True) - name = Column(String(255), nullable=False) - code = Column(Text, nullable=False) - circuit = Column(JSON) # { components: [...], wires: [...] } - created_at = Column(DateTime, default=datetime.utcnow) - updated_at = Column(DateTime, onupdate=datetime.utcnow) -``` - -**Circuit JSON Structure:** -```json -{ - "components": [ - { "id": "led1", "type": "led", "x": 250, "y": 150, "properties": { "color": "red" } } - ], - "wires": [ - { "from": { "component": "arduino", "pin": 13 }, "to": { "component": "led1", "pin": "A" } } - ] + return path; } ``` -## Dependencias Clave +### Phase 2 Implementation Steps -### Frontend -```json -{ - "@monaco-editor/react": "^4.6.0", - "avr8js": "^0.30.0", - "@wokwi/elements": "^1.9.1", - "zustand": "^4.5.0", - "axios": "^1.7.0", - "react": "^18.3.1", - "vite": "^5.4.0" +1. ✅ Implementar `useWireCreation` hook +2. ✅ Crear `PinOverlay.tsx` +3. ✅ Crear `WireInProgressRenderer.tsx` +4. ✅ Crear `ControlPoint.tsx` +5. ✅ Actualizar `WireRenderer.tsx` para mostrar control points +6. ✅ Implementar selección de cables +7. ✅ Agregar keyboard handlers (ESC, Delete) +8. ✅ Implementar drag de control points con grid snapping +9. ✅ Actualizar `generateMultiSegmentPath()` +10. ✅ Agregar botón para insertar control points en el medio + +--- + +## PHASE 3: Smart Routing & Validation + +### Objetivos +- A* pathfinding para evitar componentes +- Validación estricta de conexiones eléctricas +- Feedback visual para conexiones inválidas +- Auto-rerouting cuando componentes se mueven + +### Connection Validation + +**Archivo nuevo**: `frontend/src/utils/connectionValidator.ts` + +```typescript +export interface ValidationResult { + isValid: boolean; + error?: string; + warning?: string; +} + +export function validateConnection( + startSignals: PinSignalInfo[], + endSignals: PinSignalInfo[] +): ValidationResult { + const startType = determineSignalType(startSignals); + const endType = determineSignalType(endSignals); + + // Regla 1: No conectar VCC a VCC + if (startType === 'power-vcc' && endType === 'power-vcc') { + return { isValid: false, error: 'Cannot connect VCC to VCC directly' }; + } + + // Regla 2: No conectar GND a GND + if (startType === 'power-gnd' && endType === 'power-gnd') { + return { isValid: false, error: 'Cannot connect GND to GND directly' }; + } + + // Regla 3: No hacer cortocircuito VCC-GND + if (startType === 'power-vcc' && endType === 'power-gnd') { + return { isValid: false, error: 'Cannot short VCC to GND' }; + } + + // Regla 4: Compatibilidad de señales + if (!areSignalsCompatible(startType, endType)) { + return { + isValid: false, + error: `Incompatible signal types: ${startType} and ${endType}`, + }; + } + + // Regla 5: Validación de buses (I2C, SPI) + const busError = validateBusProtocols(startSignals, endSignals); + if (busError) { + return { isValid: false, error: busError }; + } + + return { isValid: true }; +} + +function areSignalsCompatible( + type1: WireSignalType, + type2: WireSignalType +): boolean { + // Power puede conectar a digital/analog pero no a otro power + // Digital es universal + // PWM compatible con digital y analog + // Protocolos específicos deben coincidir } ``` -### Backend -```txt -fastapi==0.115.0 -uvicorn[standard]==0.32.0 -sqlalchemy==2.0.36 -aiosqlite==0.20.0 -pydantic==2.9.2 +### A* Pathfinding + +**Archivo nuevo**: `frontend/src/utils/pathfinding.ts` + +```typescript +const GRID_SIZE = 20; // Coincidir con grid del canvas + +export function findWirePath( + startX: number, + startY: number, + endX: number, + endY: number, + components: Component[], + excludeComponents: string[] = [] +): { x: number; y: number }[] { + // Snap to grid + const gridStartX = Math.round(startX / GRID_SIZE); + const gridStartY = Math.round(startY / GRID_SIZE); + const gridEndX = Math.round(endX / GRID_SIZE); + const gridEndY = Math.round(endY / GRID_SIZE); + + // Build obstacle map + const obstacles = buildObstacleMap(components, excludeComponents); + + // Run A* + const path = aStarSearch( + { x: gridStartX, y: gridStartY }, + { x: gridEndX, y: gridEndY }, + obstacles + ); + + // Convert back to pixels + return path.map(p => ({ + x: p.x * GRID_SIZE, + y: p.y * GRID_SIZE, + })); +} + +function aStarSearch( + start: { x: number; y: number }, + end: { x: number; y: number }, + obstacles: Set +): { x: number; y: number }[] { + // Algoritmo A* estándar + // Heurística: Manhattan distance + // Vecinos: 4-directional (arriba, abajo, izq, der) + // Retorna path simplificado (sin puntos colineales) +} ``` -### Herramientas Externas -```bash -# Instalar arduino-cli -# Windows: -choco install arduino-cli +**Archivo nuevo**: `frontend/src/utils/componentBounds.ts` -# Inicializar: -arduino-cli core update-index -arduino-cli core install arduino:avr +```typescript +export function getComponentBoundingBox(componentId: string): BoundingBox | null { + const element = document.getElementById(componentId); + if (!element) return null; + + const rect = element.getBoundingClientRect(); + const canvas = element.closest('.canvas-content'); + const canvasRect = canvas.getBoundingClientRect(); + + return { + x: rect.left - canvasRect.left, + y: rect.top - canvasRect.top, + width: rect.width, + height: rect.height, + }; +} ``` -## Fases de Implementación +### Auto-rerouting -### Fase 1: Foundation (Prioridad Alta) -- [frontend] Inicializar proyecto Vite + React + TypeScript -- [frontend] Integrar Monaco Editor con syntax highlighting C++ -- [frontend] Layout básico (editor izquierda, canvas derecha) -- [backend] Setup FastAPI + endpoint /api/compile -- [backend] Integración arduino-cli para compilación -- **Entregable:** Compilar código y recibir .hex +Actualizar store: -### Fase 2: Emulation Core (Prioridad Alta) -- [frontend] Implementar AVRSimulator.ts con avr8js -- [frontend] Parser de archivos .hex -- [frontend] PinManager para tracking de pines -- [frontend] Botones Run/Stop/Reset -- **Entregable:** Ejecutar Blink example y ver cambios de pin +```typescript +updateComponent: (id, updates) => { + set((state) => ({ + components: state.components.map(c => + c.id === id ? { ...c, ...updates } : c + ), + })); -### Fase 3: Visual Components (Prioridad Alta) -- [frontend] Wrappers React para wokwi-led, wokwi-resistor -- [frontend] SimulatorCanvas con drag & drop -- [frontend] Conectar LEDs a PinManager -- **Entregable:** LED se enciende/apaga con digitalWrite() + // Re-calcular posiciones de cables conectados + get().updateWirePositions(id); +}, -### Fase 4: Project Persistence (Prioridad Media) -- [backend] Setup SQLite con SQLAlchemy -- [backend] CRUD endpoints para proyectos -- [frontend] UI para guardar/cargar proyectos -- **Entregable:** Persistir código + circuito +updateWirePositions: (componentId: string) => { + set((state) => { + const component = state.components.find(c => c.id === componentId); + if (!component) return state; -### Fase 5: Polish (Prioridad Baja) -- Más componentes (botones, potenciómetros) -- Serial monitor -- Control de velocidad -- Ejemplos pre-cargados -- **Entregable:** App completa y pulida + const updatedWires = state.wires.map(wire => { + let updated = { ...wire }; -## Puntos Críticos de Integración + // Actualizar start endpoint + if (wire.start.componentId === componentId) { + const pos = calculatePinPosition( + componentId, wire.start.pinName, component.x, component.y + ); + if (pos) updated.start = { ...wire.start, ...pos }; + } -### Monaco Editor en Vite -- Usar `@monaco-editor/react` (no `monaco-editor` directamente) -- No requiere configuración especial de Vite -- Syntax highlighting C++ funciona con `defaultLanguage="cpp"` + // Actualizar end endpoint + if (wire.end.componentId === componentId) { + const pos = calculatePinPosition( + componentId, wire.end.pinName, component.x, component.y + ); + if (pos) updated.end = { ...wire.end, ...pos }; + } -### Web Components en React -- Web Components requieren manipulación directa del DOM -- Usar `useRef` + `useEffect` para setear propiedades -- Declarar tipos JSX en `vite-env.d.ts` + // OPCIONAL: Re-calcular path con A* si hay componentes en el camino -### avr8js Performance -- Ejecutar en batches (~267k cycles/frame @ 16MHz/60fps) -- Usar `requestAnimationFrame` para smooth simulation -- Evitar re-renders de React (usar refs) + return updated; + }); -### arduino-cli -- Sketch name debe coincidir con directory name -- Output: `.ino.hex` -- Requiere `arduino:avr` core instalado - -## Comandos de Desarrollo - -```bash -# Backend -cd backend -python -m venv venv -venv\Scripts\activate -pip install -r requirements.txt -uvicorn app.main:app --reload --port 8000 - -# Frontend -cd frontend -npm install -npm run dev - -# Acceso -Frontend: http://localhost:5173 -Backend: http://localhost:8000 -API Docs: http://localhost:8000/docs + return { wires: updatedWires }; + }); +}, ``` -## Archivos de Configuración Esenciales +### Phase 3 Implementation Steps -### [frontend/vite.config.ts](frontend/vite.config.ts) -### [frontend/tsconfig.json](frontend/tsconfig.json) -### [frontend/package.json](frontend/package.json) -### [backend/app/main.py](backend/app/main.py) -### [backend/requirements.txt](backend/requirements.txt) +1. ✅ Implementar `connectionValidator.ts` +2. ✅ Integrar validación en wire creation +3. ✅ Mostrar errores de validación (línea roja punteada + tooltip) +4. ✅ Implementar `componentBounds.ts` +5. ✅ Implementar `pathfinding.ts` con A* +6. ✅ Integrar A* en wire creation (hacer toggleable) +7. ✅ Agregar botón "Re-route" para cables existentes +8. ✅ Implementar auto-rerouting cuando componentes se mueven +9. ✅ Agregar tooltip de validación en hover +10. ✅ Panel de settings para toggle smart routing + +--- + +## Critical Files Summary + +### Archivos Nuevos (17 archivos) + +**Tipos & Utilidades**: +1. `frontend/src/types/wire.ts` - Data structures +2. `frontend/src/utils/pinPositionCalculator.ts` - **CRÍTICO** - Conversión coordenadas +3. `frontend/src/utils/componentBounds.ts` - Bounding boxes +4. `frontend/src/utils/wirePathGenerator.ts` - Generación SVG paths +5. `frontend/src/utils/wireColors.ts` - Mapeo de colores +6. `frontend/src/utils/connectionValidator.ts` - Validación eléctrica +7. `frontend/src/utils/pathfinding.ts` - A* algorithm + +**Componentes**: +8. `frontend/src/components/simulator/WireLayer.tsx` - Contenedor SVG +9. `frontend/src/components/simulator/WireRenderer.tsx` - **CRÍTICO** - Render individual +10. `frontend/src/components/simulator/WireInProgressRenderer.tsx` - Preview durante creación +11. `frontend/src/components/simulator/ControlPoint.tsx` - Puntos violetas +12. `frontend/src/components/simulator/PinOverlay.tsx` - Marcadores de pines +13. `frontend/src/components/simulator/WireCreationHandler.tsx` - Hook de creación + +### Archivos a Modificar (2 archivos) + +14. `frontend/src/store/useSimulatorStore.ts` - **CRÍTICO** - Wire state management +15. `frontend/src/components/simulator/SimulatorCanvas.tsx` - Integración WireLayer + +### CSS + +16. `frontend/src/components/simulator/SimulatorCanvas.css` - Wire styles + +--- + +## Performance Optimizations + +1. **React.memo** en WireRenderer con custom comparator +2. **useMemo** para path generation +3. **useCallback** para event handlers +4. **Debounce** (16ms) para control point updates +5. **Virtual rendering** si hay >100 cables + +--- + +## Edge Cases & Error Handling + +1. **Component deletion**: Eliminar cables conectados +2. **Invalid pin references**: Marcar cable como inválido +3. **Overlapping wires**: Offset visual +4. **Moving components**: Auto-update wire positions +5. **Undo/Redo**: Guardar historial de cambios + +--- + +## Testing Strategy + +**Phase 1**: +- Unit tests para `calculatePinPosition()` +- Unit tests para `determineSignalType()` +- Unit tests para `generateSimplePath()` + +**Phase 2**: +- Integration tests para wire creation flow +- Test control point dragging +- Test keyboard shortcuts + +**Phase 3**: +- Unit tests para `validateConnection()` +- Unit tests para A* pathfinding +- Performance tests con 50+ cables + +--- + +## Success Criteria + +**Phase 1 Done When**: +- ✅ Cables se renderizan con colores correctos +- ✅ Posiciones actualizadas cuando componentes se mueven +- ✅ Puede crear cables manualmente via código + +**Phase 2 Done When**: +- ✅ Click pin → drag → click pin crea cable +- ✅ Puntos de control violetas aparecen al seleccionar +- ✅ Drag puntos reshapes cable +- ✅ Delete elimina cable seleccionado + +**Phase 3 Done When**: +- ✅ Conexiones inválidas son bloqueadas +- ✅ A* encuentra paths que evitan componentes +- ✅ Mover componente re-route cables automáticamente +- ✅ Tooltips muestran errores de validación