/** * PinOverlay Component * * Renders clickable pin indicators over components to enable wire creation. * Shows when hovering over a component or when creating a wire. */ import React, { useEffect, useState } from 'react'; interface PinInfo { name: string; x: number; // CSS pixels y: number; // CSS pixels signals?: Array<{ type: string; signal?: string }>; } interface PinOverlayProps { componentId: string; componentX: number; componentY: number; onPinClick: (componentId: string, pinName: string, x: number, y: number) => void; showPins: boolean; /** Extra offset to compensate for wrapper padding/border. Default: 4 (x), 6 (y) for component wrappers. Pass 0 when the element has no wrapper. */ wrapperOffsetX?: number; wrapperOffsetY?: number; } export const PinOverlay: React.FC = ({ componentId, componentX, componentY, onPinClick, showPins, wrapperOffsetX = 4, wrapperOffsetY = 6, }) => { const [pins, setPins] = useState([]); useEffect(() => { const tryRead = () => { const element = document.getElementById(componentId); if (element && (element as any).pinInfo) { setPins((element as any).pinInfo); return true; } return false; }; if (!tryRead()) { // Retry once after a tick in case the element sets pinInfo asynchronously (e.g. via useEffect) const t = setTimeout(tryRead, 50); return () => clearTimeout(t); } }, [componentId]); if (!showPins || pins.length === 0) { return null; } return (
{pins.map((pin, index) => { // Pin coordinates are already in CSS pixels const pinX = pin.x; const pinY = pin.y; return (
{ e.stopPropagation(); onPinClick(componentId, pin.name, componentX + wrapperOffsetX + pinX, componentY + wrapperOffsetY + pinY); }} style={{ position: 'absolute', left: `${pinX - 4}px`, top: `${pinY - 4}px`, width: '8px', height: '8px', borderRadius: '2px', backgroundColor: 'rgba(0, 200, 255, 0.8)', border: '1.5px solid white', cursor: 'crosshair', pointerEvents: 'all', transition: 'all 0.15s', }} onMouseEnter={(e) => { e.currentTarget.style.backgroundColor = 'rgba(0, 255, 100, 1)'; e.currentTarget.style.transform = 'scale(1.4)'; }} onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = 'rgba(0, 200, 255, 0.8)'; e.currentTarget.style.transform = 'scale(1)'; }} title={pin.name} /> ); })}
); };