/** * Dynamic Component Renderer * * Generic component that renders any wokwi-element web component dynamically. * Replaces individual React wrapper components (LED.tsx, Resistor.tsx, etc.) * * Features: * - Creates web component from metadata * - Syncs React props to web component properties * - Extracts pinInfo from DOM for wire connections * - Handles component lifecycle */ import React, { useRef, useEffect, useCallback } from 'react'; import type { ComponentMetadata } from '../types/component-metadata'; import { useSimulatorStore } from '../store/useSimulatorStore'; import { PartSimulationRegistry } from '../simulation/parts'; interface DynamicComponentProps { id: string; metadata: ComponentMetadata; properties: Record; x?: number; y?: number; isSelected?: boolean; onMouseDown?: (e: React.MouseEvent) => void; onDoubleClick?: (e: React.MouseEvent) => void; onMouseEnter?: () => void; onMouseLeave?: () => void; onPinInfoReady?: (pinInfo: any[]) => void; } export const DynamicComponent: React.FC = ({ id, metadata, properties, x = 0, y = 0, isSelected = false, onMouseDown, onDoubleClick, onMouseEnter, onMouseLeave, onPinInfoReady, }) => { const elementRef = useRef(null); const containerRef = useRef(null); const mountedRef = useRef(false); const handleComponentEvent = useSimulatorStore((s) => s.handleComponentEvent); const running = useSimulatorStore((s) => s.running); const simulator = useSimulatorStore((s) => s.simulator); // Check if component is interactive (has simulation logic with attachEvents) const logic = PartSimulationRegistry.get(metadata.id || id.split('-')[0]); const isInteractive = logic?.attachEvents !== undefined; /** * Sync React properties to Web Component */ useEffect(() => { if (!elementRef.current) return; Object.entries(properties).forEach(([key, value]) => { try { (elementRef.current as any)[key] = value; } catch (error) { console.warn(`Failed to set property ${key} on ${metadata.tagName}:`, error); } }); }, [properties, metadata.tagName]); /** * Extract pinInfo from web component after it initializes */ useEffect(() => { if (!elementRef.current || !onPinInfoReady) return; // Wait for web component to fully initialize const checkPinInfo = () => { try { const pinInfo = (elementRef.current as any)?.pinInfo; if (pinInfo && Array.isArray(pinInfo) && pinInfo.length > 0) { onPinInfoReady(pinInfo); return true; } } catch { // Element not ready yet } return false; }; // Try immediately if (checkPinInfo()) return; // Otherwise poll every 100ms for up to 2 seconds const interval = setInterval(() => { if (checkPinInfo()) { clearInterval(interval); } }, 100); const timeout = setTimeout(() => { clearInterval(interval); }, 2000); return () => { clearInterval(interval); clearTimeout(timeout); }; }, [onPinInfoReady]); /** * Handle mouse events */ const handleMouseDown = useCallback( (e: React.MouseEvent) => { if (onMouseDown) { e.stopPropagation(); onMouseDown(e); } }, [onMouseDown] ); const handleDoubleClick = useCallback( (e: React.MouseEvent) => { if (onDoubleClick) { e.stopPropagation(); onDoubleClick(e); } }, [onDoubleClick] ); /** * Mount web component (only once) */ useEffect(() => { if (!containerRef.current) return; // Prevent double-mount in React StrictMode if (mountedRef.current) { return; } const element = document.createElement(metadata.tagName); element.id = id; // Set initial properties Object.entries(properties).forEach(([key, value]) => { try { (element as any)[key] = value; } catch (error) { console.warn(`Failed to set initial property ${key}:`, error); } }); containerRef.current.appendChild(element); elementRef.current = element; mountedRef.current = true; return () => { if (containerRef.current && element.parentNode === containerRef.current) { containerRef.current.removeChild(element); } elementRef.current = null; mountedRef.current = false; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [metadata.tagName, id]); // Only re-create if tagName or id changes /** * Attach component-specific DOM events (like button presses) */ useEffect(() => { const el = elementRef.current; if (!el) return; const onButtonPress = (e: Event) => handleComponentEvent(id, 'button-press', e); const onButtonRelease = (e: Event) => handleComponentEvent(id, 'button-release', e); el.addEventListener('button-press', onButtonPress); el.addEventListener('button-release', onButtonRelease); const logic = PartSimulationRegistry.get(metadata.id || id.split('-')[0]); let cleanupSimulationEvents: (() => void) | undefined; if (logic && logic.attachEvents && simulator) { // Helper to find Arduino pin connected to a component pin const getArduinoPin = (componentPinName: string): number | null => { const wires = useSimulatorStore.getState().wires.filter( w => (w.start.componentId === id && w.start.pinName === componentPinName) || (w.end.componentId === id && w.end.pinName === componentPinName) ); for (const w of wires) { const arduinoEndpoint = w.start.componentId === 'arduino-uno' ? w.start : w.end.componentId === 'arduino-uno' ? w.end : null; if (arduinoEndpoint) { const pin = parseInt(arduinoEndpoint.pinName, 10); if (!isNaN(pin)) return pin; } } return null; }; cleanupSimulationEvents = logic.attachEvents(el, simulator, getArduinoPin); } return () => { if (cleanupSimulationEvents) cleanupSimulationEvents(); el.removeEventListener('button-press', onButtonPress); el.removeEventListener('button-release', onButtonRelease); }; }, [id, handleComponentEvent, metadata.id, simulator]); return (
{/* Container for web component */}
{/* Component label */}
{properties.pin !== undefined ? `Pin ${properties.pin}` : metadata.name}
); }; /** * Helper function to create a component instance from metadata */ export function createComponentFromMetadata( metadata: ComponentMetadata, x: number, y: number ): { id: string; metadataId: string; x: number; y: number; properties: Record; } { return { id: `${metadata.id}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, metadataId: metadata.id, x, y, properties: { ...metadata.defaultValues }, }; }