import { useSimulatorStore, ARDUINO_POSITION } from '../../store/useSimulatorStore'; import React, { useEffect, useState, useRef } from 'react'; import { ArduinoUno } from '../components-wokwi/ArduinoUno'; import { ComponentPickerModal } from '../ComponentPickerModal'; import { DynamicComponent, createComponentFromMetadata } from '../DynamicComponent'; import { ComponentRegistry } from '../../services/ComponentRegistry'; import { PinSelector } from './PinSelector'; import { WireLayer } from './WireLayer'; import { PinOverlay } from './PinOverlay'; import type { ComponentMetadata } from '../../types/component-metadata'; import './SimulatorCanvas.css'; export const SimulatorCanvas = () => { const { components, running, pinManager, initSimulator, updateComponentState, addComponent, removeComponent, updateComponent, } = useSimulatorStore(); // Wire management from store const startWireCreation = useSimulatorStore((s) => s.startWireCreation); const updateWireInProgress = useSimulatorStore((s) => s.updateWireInProgress); const finishWireCreation = useSimulatorStore((s) => s.finishWireCreation); const cancelWireCreation = useSimulatorStore((s) => s.cancelWireCreation); const wireInProgress = useSimulatorStore((s) => s.wireInProgress); const recalculateAllWirePositions = useSimulatorStore((s) => s.recalculateAllWirePositions); // Component picker modal const [showComponentPicker, setShowComponentPicker] = useState(false); const [registry] = useState(() => ComponentRegistry.getInstance()); // Component selection const [selectedComponentId, setSelectedComponentId] = useState(null); const [showPinSelector, setShowPinSelector] = useState(false); const [pinSelectorPos, setPinSelectorPos] = useState({ x: 0, y: 0 }); // Component dragging state const [draggedComponentId, setDraggedComponentId] = useState(null); const [dragOffset, setDragOffset] = useState({ x: 0, y: 0 }); // Pin visualization const [hoveredComponentId, setHoveredComponentId] = useState(null); // Canvas ref for coordinate calculations const canvasRef = useRef(null); // Initialize simulator on mount useEffect(() => { initSimulator(); }, [initSimulator]); // Recalculate wire positions after web components initialize their pinInfo useEffect(() => { const timer = setTimeout(() => { recalculateAllWirePositions(); }, 500); return () => clearTimeout(timer); }, [recalculateAllWirePositions]); // Connect components to pin manager useEffect(() => { const unsubscribers: (() => void)[] = []; components.forEach((component) => { if (component.properties.pin !== undefined) { const unsubscribe = pinManager.onPinChange( component.properties.pin, (pin, state) => { // Update component state when pin changes updateComponentState(component.id, state); console.log(`Component ${component.id} on pin ${pin}: ${state ? 'HIGH' : 'LOW'}`); } ); unsubscribers.push(unsubscribe); } }); return () => { unsubscribers.forEach(unsub => unsub()); }; }, [components, pinManager, updateComponentState]); // Handle keyboard delete useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if ((e.key === 'Delete' || e.key === 'Backspace') && selectedComponentId) { removeComponent(selectedComponentId); setSelectedComponentId(null); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [selectedComponentId, removeComponent]); // Handle component selection from modal const handleSelectComponent = (metadata: ComponentMetadata) => { // Calculate grid position to avoid overlapping // Use existing components count to determine position const componentsCount = components.length; const gridSize = 250; // Space between components const cols = 3; // Components per row const col = componentsCount % cols; const row = Math.floor(componentsCount / cols); const x = 400 + (col * gridSize); const y = 100 + (row * gridSize); const component = createComponentFromMetadata(metadata, x, y); addComponent(component as any); setShowComponentPicker(false); }; // Component selection (double click to open pin selector) const handleComponentDoubleClick = (componentId: string, event: React.MouseEvent) => { event.stopPropagation(); setSelectedComponentId(componentId); setPinSelectorPos({ x: event.clientX, y: event.clientY }); setShowPinSelector(true); }; // Pin assignment const handlePinSelect = (componentId: string, pin: number) => { updateComponent(componentId, { properties: { ...components.find((c) => c.id === componentId)?.properties, pin, }, } as any); }; // Component dragging handlers const handleComponentMouseDown = (componentId: string, e: React.MouseEvent) => { // Don't start dragging if we're clicking on the pin selector if (showPinSelector) return; e.stopPropagation(); const component = components.find((c) => c.id === componentId); if (!component || !canvasRef.current) return; // Get canvas position to convert viewport coords to canvas coords const canvasRect = canvasRef.current.getBoundingClientRect(); // Calculate offset in canvas coordinate system setDraggedComponentId(componentId); setDragOffset({ x: (e.clientX - canvasRect.left) - component.x, y: (e.clientY - canvasRect.top) - component.y, }); setSelectedComponentId(componentId); }; const handleCanvasMouseMove = (e: React.MouseEvent) => { if (!canvasRef.current) return; // Handle component dragging if (draggedComponentId) { const canvasRect = canvasRef.current.getBoundingClientRect(); const newX = e.clientX - canvasRect.left - dragOffset.x; const newY = e.clientY - canvasRect.top - dragOffset.y; updateComponent(draggedComponentId, { x: Math.max(0, newX), y: Math.max(0, newY), } as any); } // Handle wire creation preview if (wireInProgress && canvasRef.current) { const canvasRect = canvasRef.current.getBoundingClientRect(); const currentX = e.clientX - canvasRect.left; const currentY = e.clientY - canvasRect.top; updateWireInProgress(currentX, currentY); } }; const handleCanvasMouseUp = () => { if (draggedComponentId) { // Recalculate wire positions after moving component recalculateAllWirePositions(); setDraggedComponentId(null); } }; // Wire creation via pin clicks const handlePinClick = (componentId: string, pinName: string, x: number, y: number) => { if (wireInProgress) { // Finish wire creation finishWireCreation({ componentId, pinName, x, y, }); } else { // Start wire creation startWireCreation({ componentId, pinName, x, y, }); } }; // Keyboard handlers for wires useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === 'Escape' && wireInProgress) { cancelWireCreation(); } }; window.addEventListener('keydown', handleKeyDown); return () => window.removeEventListener('keydown', handleKeyDown); }, [wireInProgress, cancelWireCreation]); // Render component using dynamic renderer const renderComponent = (component: any) => { const metadata = registry.getById(component.metadataId); if (!metadata) { console.warn(`Metadata not found for component: ${component.metadataId}`); return null; } const isSelected = selectedComponentId === component.id; const isHovered = hoveredComponentId === component.id; const showPinsForComponent = isHovered || wireInProgress !== null; return ( { handleComponentMouseDown(component.id, e); }} onDoubleClick={(e) => { handleComponentDoubleClick(component.id, e); }} onMouseEnter={() => setHoveredComponentId(component.id)} onMouseLeave={() => setHoveredComponentId(null)} /> {/* Pin overlay for wire creation */} ); }; return (
{/* Main Canvas */}

Arduino Simulator

{running ? 'Running' : 'Stopped'} {components.length} components
setSelectedComponentId(null)} style={{ cursor: wireInProgress ? 'crosshair' : 'default' }} > {/* Wire Layer - Renders below all components */} {/* Arduino Uno Board using wokwi-elements */} c.id === 'led-builtin')?.properties.state || false} /> {/* Arduino pin overlay */} {/* Components using wokwi-elements */}
{components.map(renderComponent)}
{/* Pin Selector Modal */} {showPinSelector && selectedComponentId && ( c.id === selectedComponentId)?.metadataId || 'unknown' } currentPin={ components.find((c) => c.id === selectedComponentId)?.properties.pin as number | undefined } onPinSelect={handlePinSelect} onClose={() => setShowPinSelector(false)} position={pinSelectorPos} /> )} {/* Component Picker Modal */} setShowComponentPicker(false)} onSelectComponent={handleSelectComponent} />
); };