'use client'; import { useState, useCallback, useRef, useMemo, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { ElectronicsContent, ElectronicCircuitState, ElectronicComponent, ElectronicWire, ElectronicComponentType, getTerminals, componentLabel, } from '@/types/electronics'; import { simulateElectronics } from './simulateElectronics'; import { ComponentSVG, COMP_W, COMP_H, getTerminalPositions } from './ComponentSVG'; import { Trash2, RotateCw, Play, Pause, Maximize2, Minimize2, X } from 'lucide-react'; import { Button } from '@/components/ui/button'; interface ElectronicsLabProps { content: ElectronicsContent; onCircuitChange: (state: ElectronicCircuitState) => void; disabled?: boolean; } const CANVAS_W = 700; const CANVAS_H = 500; interface TermPos { id: string; x: number; y: number } let nextCompId = 1; export function ElectronicsLab({ content, onCircuitChange, disabled }: ElectronicsLabProps) { const [components, setComponents] = useState(content.preplacedComponents ?? []); const [wires, setWires] = useState(content.preplacedWires ?? []); const [placing, setPlacing] = useState(null); const [wiringFrom, setWiringFrom] = useState(null); const [mousePos, setMousePos] = useState<{ x: number; y: number } | null>(null); const [dragging, setDragging] = useState<{ id: string; ox: number; oy: number } | null>(null); const [selected, setSelected] = useState(null); const [running, setRunning] = useState(false); const [fullscreen, setFullscreen] = useState(false); const didDrag = useRef(false); const svgRef = useRef(null); // Escape exits fullscreen useEffect(() => { if (!fullscreen) return; const handleKey = (e: KeyboardEvent) => { if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); setFullscreen(false); } }; window.addEventListener('keydown', handleKey, true); return () => window.removeEventListener('keydown', handleKey, true); }, [fullscreen]); const circuit: ElectronicCircuitState = useMemo(() => ({ components, wires }), [components, wires]); const simResult = useMemo(() => { if (!running) return null; return simulateElectronics(circuit); }, [circuit, running]); const sync = useCallback((c: ElectronicComponent[], w: ElectronicWire[]) => { onCircuitChange({ components: c, wires: w }); }, [onCircuitChange]); const getSvgPoint = useCallback((e: React.MouseEvent) => { const svg = svgRef.current; if (!svg) return { x: 0, y: 0 }; const ctm = svg.getScreenCTM(); if (ctm) { return { x: (e.clientX - ctm.e) / ctm.a, y: (e.clientY - ctm.f) / ctm.d, }; } // Fallback const rect = svg.getBoundingClientRect(); return { x: ((e.clientX - rect.left) / rect.width) * CANVAS_W, y: ((e.clientY - rect.top) / rect.height) * CANVAS_H, }; }, []); // Get all terminal world positions const allTerminals = useMemo(() => { const map = new Map(); for (const comp of components) { const terminals = getTerminalPositions(comp.type, comp.x, comp.y, comp.rotation); for (const [name, pos] of Object.entries(terminals)) { map.set(`${comp.id}:${name}`, pos); } } return map; }, [components]); const handleCanvasClick = useCallback((e: React.MouseEvent) => { if (disabled) return; const pos = getSvgPoint(e); if (placing) { const newComp: ElectronicComponent = { id: `comp-${nextCompId++}`, type: placing, x: pos.x - COMP_W / 2, y: pos.y - COMP_H / 2, rotation: 0, value: placing === 'voltage-source' ? 5 : placing === 'resistor' ? 1000 : placing === 'capacitor' ? 100 : undefined, }; const nc = [...components, newComp]; setComponents(nc); sync(nc, wires); setPlacing(null); return; } // Don't deselect if we just finished dragging a component if (!didDrag.current) { setSelected(null); } didDrag.current = false; setWiringFrom(null); }, [disabled, placing, components, wires, getSvgPoint, sync]); const handleTerminalDragStart = useCallback((termId: string, pos: { x: number; y: number }) => { if (disabled) return; setWiringFrom({ id: termId, ...pos }); }, [disabled]); const handleTerminalDragEnd = useCallback((termId: string) => { if (disabled || !wiringFrom) return; if (termId === wiringFrom.id) { setWiringFrom(null); return; } const existing = wires.find((w) => (w.from === wiringFrom.id && w.to === termId) || (w.from === termId && w.to === wiringFrom.id) ); if (!existing) { const newWire: ElectronicWire = { id: `ew-${Date.now()}`, from: wiringFrom.id, to: termId }; const nw = [...wires, newWire]; setWires(nw); sync(components, nw); } setWiringFrom(null); }, [disabled, wiringFrom, wires, components, sync]); const handleMouseMove = useCallback((e: React.MouseEvent) => { const pos = getSvgPoint(e); setMousePos(pos); if (dragging) { didDrag.current = true; const nc = components.map((c) => c.id === dragging.id ? { ...c, x: pos.x - dragging.ox, y: pos.y - dragging.oy } : c ); setComponents(nc); sync(nc, wires); } }, [dragging, components, wires, getSvgPoint, sync]); const handleMouseUp = useCallback(() => { setDragging(null); if (wiringFrom) setWiringFrom(null); }, [wiringFrom]); const handleCompMouseDown = useCallback((id: string, e: React.MouseEvent) => { if (disabled) return; // Mark as interacted so canvas click doesn't deselect didDrag.current = true; setSelected(id); const comp = components.find((c) => c.id === id); if (!comp) return; const pos = getSvgPoint(e); setDragging({ id, ox: pos.x - comp.x, oy: pos.y - comp.y }); }, [disabled, components, getSvgPoint]); const deleteComp = useCallback((id: string) => { if (disabled) return; const nc = components.filter((c) => c.id !== id); const nw = wires.filter((w) => !w.from.startsWith(id) && !w.to.startsWith(id)); setComponents(nc); setWires(nw); sync(nc, nw); if (selected === id) setSelected(null); }, [disabled, components, wires, selected, sync]); const rotateComp = useCallback((id: string) => { if (disabled) return; const nc = components.map((c) => c.id === id ? { ...c, rotation: ((c.rotation + 90) % 360) as 0 | 90 | 180 | 270 } : c ); setComponents(nc); sync(nc, wires); }, [disabled, components, wires, sync]); const changeValue = useCallback((id: string, value: number) => { const nc = components.map((c) => c.id === id ? { ...c, value } : c); setComponents(nc); sync(nc, wires); }, [components, wires, sync]); const clearAll = useCallback(() => { const preplaced = content.preplacedComponents ?? []; const preWires = content.preplacedWires ?? []; setComponents(preplaced); setWires(preWires); setSelected(null); setWiringFrom(null); sync(preplaced, preWires); }, [content, sync]); function renderWire(from: { x: number; y: number }, to: { x: number; y: number }, color: string, key: string, dashed = false) { const dx = Math.max(Math.abs(to.x - from.x) * 0.4, 20); const d = `M${from.x},${from.y} C${from.x + dx},${from.y} ${to.x - dx},${to.y} ${to.x},${to.y}`; return ; } // Color wire by voltage difference function wireColor(wireObj: ElectronicWire): string { if (!simResult?.success) return '#555'; const vFrom = simResult.nodeVoltages.get(wireObj.from) ?? 0; const vTo = simResult.nodeVoltages.get(wireObj.to) ?? 0; const avg = (vFrom + vTo) / 2; if (avg > 3) return '#ef4444'; // high voltage = red if (avg > 0.5) return '#f59e0b'; // medium = amber return '#555'; // low/ground = gray } // Shared palette content const paletteContent = ( <> {content.availableComponents.map((type) => ( ))}
{selected && (
{components.find((c) => c.id === selected)?.type === 'resistor' && (
c.id === selected)?.value ?? 1000} onChange={(e) => changeValue(selected, parseFloat(e.target.value) || 1000)} onKeyDown={(e) => e.stopPropagation()} />
)} {components.find((c) => c.id === selected)?.type === 'voltage-source' && (
c.id === selected)?.value ?? 5} onChange={(e) => changeValue(selected, parseFloat(e.target.value) || 5)} onKeyDown={(e) => e.stopPropagation()} />
)} {components.find((c) => c.id === selected)?.type === 'capacitor' && (
c.id === selected)?.value ?? 100} onChange={(e) => changeValue(selected, parseFloat(e.target.value) || 100)} onKeyDown={(e) => e.stopPropagation()} />
)}
)}
); // Shared SVG canvas const svgCanvas = (fs: boolean) => ( {wires.map((wire) => { const from = allTerminals.get(wire.from); const to = allTerminals.get(wire.to); if (!from || !to) return null; return renderWire(from, to, wireColor(wire), wire.id); })} {wiringFrom && mousePos && renderWire(wiringFrom, mousePos, '#6366f1', 'wire-preview', true)} {placing && mousePos && ( )} {components.map((comp) => ( ))} ); // Shared probe table const probeTable = content.probes.length > 0 && (
{content.probes.map((probe, i) => { // Find meter readings of the matching type const readings: number[] = []; if (simResult?.success) { for (const [, reading] of simResult.meterReadings) { if ((probe.type === 'voltmeter' && reading.unit === 'V') || (probe.type === 'ammeter' && reading.unit === 'mA')) { readings.push(reading.value); } } } const match = readings.find((r) => Math.abs(r - probe.expected) <= probe.tolerance); const hasReading = readings.length > 0; const pass = match !== undefined; const instrumentName = probe.type === 'voltmeter' ? 'Voltímetro' : 'Amperímetro'; return ( ); })}
Medición Esperado Tu resultado
{instrumentName}:{' '} {probe.label} {probe.expected}{probe.unit} {hasReading ? readings.map((r) => r.toFixed(1) + probe.unit).join(', ') : Coloca un {instrumentName.toLowerCase()} } {hasReading && (pass ? '✓' : '✗')}
); // Play/pause + fullscreen toolbar const toolbar = (fs: boolean) => (
{running ? '● Simulación activa' : '○ Pausada'}
); // === FULLSCREEN PORTAL === const fullscreenOverlay = fullscreen && createPortal(
{/* Sidebar palette */}

Componentes

{paletteContent}
{/* Main area */}
{toolbar(true)}
{svgCanvas(true)}
{probeTable} {simResult && !simResult.success && simResult.error && components.length > 0 && (

{simResult.error}

)}
, document.body ); // === INLINE (normal) === return (

Componentes

{paletteContent}
{toolbar(false)}
{svgCanvas(false)}
{probeTable} {simResult && !simResult.success && simResult.error && components.length > 0 && (

{simResult.error}

)}

Arrastra desde un terminal (●) a otro para conectar. Clic derecho para eliminar.

Selecciona un componente para rotarlo o cambiar su valor.

{fullscreenOverlay}
); }