845 lines
23 KiB
Markdown
845 lines
23 KiB
Markdown
# Plan: Visual Wiring System for Wokwi Clone (Incremental Implementation)
|
|
|
|
## Vision
|
|
|
|
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.
|
|
|
|
## User Requirements (from Q&A)
|
|
|
|
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)
|
|
|
|
## Architecture Overview
|
|
|
|
### 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
|
|
|
|
### Key Discovery: Pin Information API
|
|
|
|
All wokwi-elements expose `pinInfo: ElementPin[]`:
|
|
|
|
```typescript
|
|
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.)
|
|
}
|
|
```
|
|
|
|
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
|
|
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 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;
|
|
}
|
|
|
|
export type WireSignalType =
|
|
| 'power-vcc' | 'power-gnd' | 'analog' | 'digital'
|
|
| 'pwm' | 'i2c' | 'spi' | 'usart';
|
|
```
|
|
|
|
### 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<Wire>) => 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 (
|
|
<svg
|
|
className="wire-layer"
|
|
style={{
|
|
position: 'absolute',
|
|
top: 0,
|
|
left: 0,
|
|
width: '100%',
|
|
height: '100%',
|
|
pointerEvents: 'none',
|
|
zIndex: 1, // Debajo de componentes
|
|
}}
|
|
>
|
|
{wires.map(wire => (
|
|
<WireRenderer
|
|
key={wire.id}
|
|
wire={wire}
|
|
isSelected={wire.id === selectedWireId}
|
|
/>
|
|
))}
|
|
|
|
{wireInProgress && (
|
|
<WireInProgressRenderer wireInProgress={wireInProgress} />
|
|
)}
|
|
</svg>
|
|
);
|
|
};
|
|
```
|
|
|
|
**Archivo nuevo**: `frontend/src/components/simulator/WireRenderer.tsx`
|
|
|
|
```typescript
|
|
export const WireRenderer: React.FC<{ wire: Wire; isSelected: boolean }> = ({ wire, isSelected }) => {
|
|
const { setSelectedWire } = useSimulatorStore();
|
|
|
|
const path = useMemo(() => generateWirePath(wire), [wire]);
|
|
|
|
return (
|
|
<g className="wire-group">
|
|
{/* Path invisible para click fácil */}
|
|
<path
|
|
d={path}
|
|
stroke="transparent"
|
|
strokeWidth="10"
|
|
fill="none"
|
|
style={{ pointerEvents: 'stroke', cursor: 'pointer' }}
|
|
onClick={() => setSelectedWire(wire.id)}
|
|
/>
|
|
|
|
{/* Path visible */}
|
|
<path
|
|
d={path}
|
|
stroke={wire.isValid ? wire.color : '#ff4444'}
|
|
strokeWidth="2"
|
|
fill="none"
|
|
strokeDasharray={wire.isValid ? undefined : '5,5'}
|
|
/>
|
|
|
|
{/* Endpoints */}
|
|
<circle cx={wire.start.x} cy={wire.start.y} r="3" fill={wire.color} />
|
|
<circle cx={wire.end.x} cy={wire.end.y} r="3" fill={wire.color} />
|
|
</g>
|
|
);
|
|
};
|
|
```
|
|
|
|
### 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:
|
|
<div className="canvas-content" ...>
|
|
{/* Wire layer - z-index: 1 */}
|
|
<WireLayer />
|
|
|
|
{/* Arduino Uno - z-index: 2 */}
|
|
<ArduinoUno ... />
|
|
|
|
{/* Components */}
|
|
<div className="components-area">
|
|
{components.map(renderComponent)}
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
### 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 (
|
|
<div className="pin-overlay" style={{ zIndex: 10 }}>
|
|
{components.map(component => {
|
|
const pins = getAllPinPositions(component.id, component.x, component.y);
|
|
|
|
return pins.map(pin => (
|
|
<div
|
|
key={`${component.id}-${pin.name}`}
|
|
className="pin-marker"
|
|
style={{
|
|
position: 'absolute',
|
|
left: pin.x - 6,
|
|
top: pin.y - 6,
|
|
width: 12,
|
|
height: 12,
|
|
borderRadius: '50%',
|
|
border: '2px solid #00ff00',
|
|
backgroundColor: 'rgba(0, 255, 0, 0.3)',
|
|
pointerEvents: 'all',
|
|
cursor: 'crosshair',
|
|
}}
|
|
onClick={(e) => handlePinClick(component.id, pin.name, e)}
|
|
/>
|
|
));
|
|
})}
|
|
</div>
|
|
);
|
|
};
|
|
```
|
|
|
|
**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 (
|
|
<circle
|
|
cx={x}
|
|
cy={y}
|
|
r="6"
|
|
fill="#8b5cf6" // Morado como en Wokwi
|
|
stroke="white"
|
|
strokeWidth="2"
|
|
style={{ cursor: 'move', pointerEvents: 'all' }}
|
|
onMouseDown={() => 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}`;
|
|
}
|
|
}
|
|
|
|
// 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}`;
|
|
}
|
|
|
|
return path;
|
|
}
|
|
```
|
|
|
|
### Phase 2 Implementation Steps
|
|
|
|
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
|
|
}
|
|
```
|
|
|
|
### 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<string>
|
|
): { 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)
|
|
}
|
|
```
|
|
|
|
**Archivo nuevo**: `frontend/src/utils/componentBounds.ts`
|
|
|
|
```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,
|
|
};
|
|
}
|
|
```
|
|
|
|
### Auto-rerouting
|
|
|
|
Actualizar store:
|
|
|
|
```typescript
|
|
updateComponent: (id, updates) => {
|
|
set((state) => ({
|
|
components: state.components.map(c =>
|
|
c.id === id ? { ...c, ...updates } : c
|
|
),
|
|
}));
|
|
|
|
// Re-calcular posiciones de cables conectados
|
|
get().updateWirePositions(id);
|
|
},
|
|
|
|
updateWirePositions: (componentId: string) => {
|
|
set((state) => {
|
|
const component = state.components.find(c => c.id === componentId);
|
|
if (!component) return state;
|
|
|
|
const updatedWires = state.wires.map(wire => {
|
|
let updated = { ...wire };
|
|
|
|
// 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 };
|
|
}
|
|
|
|
// 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 };
|
|
}
|
|
|
|
// OPCIONAL: Re-calcular path con A* si hay componentes en el camino
|
|
|
|
return updated;
|
|
});
|
|
|
|
return { wires: updatedWires };
|
|
});
|
|
},
|
|
```
|
|
|
|
### Phase 3 Implementation Steps
|
|
|
|
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
|