UNPKG

circuit-bricks

Version:

A modular, Lego-style SVG circuit component system for React (ALPHA - Not for production use)

1 lines 60.2 kB
{"version":3,"file":"CircuitCanvas-CHHpu9w5.mjs","names":["Port: React.FC<PortProps>","event: React.MouseEvent","BaseComponent: React.FC<BaseComponentProps>","Port","Brick: React.FC<BrickProps>","BaseComponent","componentId: string","portId: string","currentEl: Element | null","svgRoot: SVGSVGElement | null","WirePath: React.FC<WirePathProps>","from: Point","to: Point","clientX: number","clientY: number","e: React.MouseEvent | React.DragEvent","e: React.MouseEvent","id: string","event: React.MouseEvent","componentId: string","event: React.WheelEvent","portId: string","from: Point","to: Point","event: React.DragEvent","e: KeyboardEvent","WirePath","Brick"],"sources":["../../src/core/Port.tsx","../../src/core/BaseComponent.tsx","../../src/core/Brick.tsx","../../src/utils/getPortPosition.ts","../../src/core/WirePath.tsx","../../src/core/CircuitCanvas.tsx"],"sourcesContent":["/**\n * Port Component\n *\n * Renders a connection port on a circuit component\n */\n\nimport React from 'react';\nimport { PortSchema } from '../schemas/componentSchema';\n\nexport interface PortProps {\n componentId: string;\n port: PortSchema;\n selected?: boolean;\n onClick?: (portId: string, event: React.MouseEvent) => void;\n}\n\nconst Port: React.FC<PortProps> = ({\n componentId,\n port,\n selected = false,\n onClick\n}) => {\n const handleClick = (event: React.MouseEvent) => {\n event.stopPropagation(); // Prevent triggering component click\n onClick?.(port.id, event);\n };\n\n return (\n <circle\n cx={port.x}\n cy={port.y}\n r={5}\n fill={selected ? '#4f8ef7' : '#555'}\n stroke=\"#000\"\n strokeWidth={1}\n data-component-id={componentId}\n data-port-id={port.id}\n data-port-type={port.type}\n vectorEffect=\"non-scaling-stroke\"\n onClick={handleClick}\n style={{ cursor: 'pointer' }}\n />\n );\n};\n\nexport default Port;\n","/**\n * BaseComponent\n *\n * A core component that renders SVG for a circuit component based on its schema.\n */\n\nimport React from 'react';\nimport { ComponentSchema, ComponentInstance } from '../schemas/componentSchema';\nimport Port from './Port';\n\nexport interface BaseComponentProps {\n schema: ComponentSchema;\n component: ComponentInstance;\n onClick?: (event: React.MouseEvent) => void;\n onMouseDown?: (event: React.MouseEvent) => void;\n onPortClick?: (componentId: string, portId: string, event: React.MouseEvent) => void;\n selected?: boolean;\n}\n\nconst BaseComponent: React.FC<BaseComponentProps> = ({\n schema,\n component,\n onClick,\n onMouseDown,\n onPortClick,\n selected = false\n}) => {\n const { id, position, size, rotation = 0 } = component;\n\n // Determine the size to use - from the component instance or defaults\n const width = size?.width || schema.defaultWidth;\n const height = size?.height || schema.defaultHeight;\n\n // Create a transform for position and optional rotation\n const transform = `translate(${position.x}, ${position.y})${\n rotation ? ` rotate(${rotation} ${width / 2} ${height / 2})` : ''\n }`;\n\n // Style for the selection outline\n const outlineStyle = selected\n ? {\n stroke: '#4f8ef7',\n strokeWidth: 2,\n strokeDasharray: 'none',\n fill: 'none',\n pointerEvents: 'none' as const,\n vectorEffect: 'non-scaling-stroke' as const,\n }\n : undefined;\n\n return (\n <g\n transform={transform}\n onClick={onClick}\n onMouseDown={onMouseDown}\n data-component-id={id}\n data-component-type={component.type}\n >\n {/* SVG content from the schema */}\n <svg\n width={width}\n height={height}\n viewBox={`0 0 ${schema.defaultWidth} ${schema.defaultHeight}`}\n preserveAspectRatio=\"none\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n {/* If svgPath is a complete SVG element with tags */}\n {schema.svgPath.startsWith('<svg') ? (\n <g dangerouslySetInnerHTML={{ __html: schema.svgPath }} />\n ) : (\n // Otherwise treat it as a path data string\n <path\n d={schema.svgPath}\n fill=\"none\"\n stroke=\"white\"\n strokeWidth={2}\n vectorEffect=\"non-scaling-stroke\"\n />\n )}\n </svg>\n\n {/* Render ports */}\n {schema.ports.map(port => (\n <Port\n key={port.id}\n componentId={id}\n port={port}\n selected={false}\n onClick={(portId, event) => onPortClick?.(id, portId, event)}\n />\n ))}\n\n {/* Selection outline */}\n {selected && (\n <rect\n x={0}\n y={0}\n width={width}\n height={height}\n {...outlineStyle}\n />\n )}\n </g>\n );\n};\n\nexport default BaseComponent;\n","/**\n * Brick Component\n *\n * A wrapper around BaseComponent that handles schema lookup and error states.\n */\n\nimport React from 'react';\nimport { ComponentInstance } from '../schemas/componentSchema';\nimport { getComponentSchema } from '../registry';\nimport BaseComponent from './BaseComponent';\n\nexport interface BrickProps {\n component: ComponentInstance;\n onClick?: (event: React.MouseEvent) => void;\n onMouseDown?: (event: React.MouseEvent) => void;\n onPortClick?: (componentId: string, portId: string, event: React.MouseEvent) => void;\n selected?: boolean;\n}\n\nconst Brick: React.FC<BrickProps> = ({\n component,\n onClick,\n onMouseDown,\n onPortClick,\n selected = false\n}) => {\n const schema = getComponentSchema(component.type);\n\n // Handle missing schema with a placeholder component\n if (!schema) {\n console.warn(`No schema found for component type: ${component.type}`);\n return (\n <g\n transform={`translate(${component.position.x}, ${component.position.y})`}\n onClick={onClick}\n onMouseDown={onMouseDown}\n data-component-id={component.id}\n data-component-type=\"unknown\"\n >\n <rect\n width={100}\n height={50}\n fill=\"red\"\n stroke=\"white\"\n strokeWidth={1.5}\n strokeDasharray=\"5,5\"\n vectorEffect=\"non-scaling-stroke\"\n />\n <text\n x={50}\n y={25}\n textAnchor=\"middle\"\n dominantBaseline=\"middle\"\n fill=\"white\"\n fontFamily=\"sans-serif\"\n fontSize=\"12px\"\n >\n Unknown: {component.type}\n </text>\n </g>\n );\n }\n\n // Render the component with its schema\n return (\n <BaseComponent\n schema={schema}\n component={component}\n onClick={onClick}\n onMouseDown={onMouseDown}\n onPortClick={onPortClick}\n selected={selected}\n />\n );\n};\n\nexport default Brick;\n","/**\n * Port position utility\n *\n * Provides functions to get the absolute position of ports in the DOM.\n * Compatible with infinite circuit canvas with panning and zooming.\n * Now includes SSR-safe implementations.\n */\n\nimport { Point } from \"../types\";\nimport { isBrowser, safeQuerySelector } from \"./ssrUtils\";\n\n/**\n * Retrieves the absolute position of a port in the SVG coordinate system,\n * accounting for any viewport transformations (pan/zoom).\n *\n * @param componentId - The ID of the component\n * @param portId - The ID of the port on the component\n * @returns The position of the port or null if not found\n */\nexport function getPortPosition(\n componentId: string,\n portId: string,\n): Point | null {\n // SSR-safe check\n if (!isBrowser()) {\n return null;\n }\n\n // Find the port element in the DOM\n const portElement = safeQuerySelector(\n `[data-component-id=\"${componentId}\"][data-port-id=\"${portId}\"]`,\n );\n\n if (!portElement) {\n console.warn(\n `Port not found: componentId=${componentId}, portId=${portId}`,\n );\n return null;\n }\n\n // Find the SVG root to calculate the relative position\n let currentEl: Element | null = portElement;\n let svgRoot: SVGSVGElement | null = null;\n\n while (currentEl) {\n if (currentEl instanceof SVGSVGElement) {\n svgRoot = currentEl;\n break;\n }\n currentEl = currentEl.parentElement;\n }\n\n if (!svgRoot) {\n console.warn(\"SVG root not found for port\");\n return null;\n }\n\n // Get the bounding client rect of the port element and SVG\n const portRect = (portElement as SVGElement).getBoundingClientRect();\n const svgRect = svgRoot.getBoundingClientRect();\n\n // Get the center point of the port in client coordinates\n const clientX = portRect.left + portRect.width / 2;\n const clientY = portRect.top + portRect.height / 2;\n\n // Create an SVG point in client coordinates\n const svgPoint = svgRoot.createSVGPoint();\n svgPoint.x = clientX;\n svgPoint.y = clientY;\n\n // Get the current transformation matrix and its inverse\n // This will automatically factor in any viewport transformations (pan/zoom)\n const ctm = svgRoot.getScreenCTM();\n if (!ctm) {\n console.warn(\"Could not get screen CTM for SVG\");\n return null;\n }\n\n const inverseCTM = ctm.inverse();\n\n // Transform the client coordinates to SVG user space\n const transformedPoint = svgPoint.matrixTransform(inverseCTM);\n\n // Return the point in user space coordinates, which accounts for pan/zoom\n return {\n x: transformedPoint.x,\n y: transformedPoint.y,\n };\n}\n\n/**\n * Hook version of getPortPosition for React components\n * Will be implemented in a separate usePortPosition.ts file\n */\n","/**\n * WirePath Component\n *\n * Renders a wire connection between two component ports.\n */\n\nimport React, { useEffect, useState, useRef } from 'react';\nimport { Wire, ComponentInstance, Point } from '../schemas/componentSchema';\nimport { getPortPosition } from '../utils/getPortPosition';\n\nexport interface WirePathProps {\n wire: Wire;\n components: ComponentInstance[];\n selected?: boolean;\n onClick?: (event: React.MouseEvent) => void;\n}\n\ninterface PortPosition {\n x: number;\n y: number;\n found: boolean;\n}\n\nconst WirePath: React.FC<WirePathProps> = ({\n wire,\n components,\n selected = false,\n onClick\n}) => {\n const [fromPos, setFromPos] = useState<PortPosition>({ x: 0, y: 0, found: false });\n const [toPos, setToPos] = useState<PortPosition>({ x: 0, y: 0, found: false });\n const pathRef = useRef<SVGPathElement>(null);\n\n // Function to update port positions\n const updatePortPositions = () => {\n try {\n const fromPosition = getPortPosition(wire.from.componentId, wire.from.portId);\n if (fromPosition) {\n setFromPos({ ...fromPosition, found: true });\n }\n } catch (error) {\n console.warn(`Could not find source port for wire ${wire.id}:`, error);\n }\n\n try {\n const toPosition = getPortPosition(wire.to.componentId, wire.to.portId);\n if (toPosition) {\n setToPos({ ...toPosition, found: true });\n }\n } catch (error) {\n console.warn(`Could not find destination port for wire ${wire.id}:`, error);\n }\n };\n\n // Setup a MutationObserver to watch for component position changes\n useEffect(() => {\n updatePortPositions();\n\n // Watch for changes in component attributes (like x, y, transform)\n const observer = new MutationObserver((mutations) => {\n let shouldUpdate = false;\n\n mutations.forEach((mutation) => {\n if (mutation.type === 'attributes' &&\n (mutation.attributeName === 'transform' ||\n mutation.attributeName === 'x' ||\n mutation.attributeName === 'y')) {\n shouldUpdate = true;\n }\n });\n\n if (shouldUpdate) {\n updatePortPositions();\n }\n });\n\n // Find all relevant component SVG elements to observe\n const fromComponent = document.querySelector(`[data-component-id=\"${wire.from.componentId}\"]`);\n const toComponent = document.querySelector(`[data-component-id=\"${wire.to.componentId}\"]`);\n\n if (fromComponent) {\n observer.observe(fromComponent, { attributes: true, attributeFilter: ['transform', 'x', 'y'] });\n }\n\n if (toComponent) {\n observer.observe(toComponent, { attributes: true, attributeFilter: ['transform', 'x', 'y'] });\n }\n\n // Also set up a resize observer to update positions when the canvas resizes\n const resizeObserver = new ResizeObserver(() => {\n updatePortPositions();\n });\n\n if (pathRef.current) {\n const svgElement = pathRef.current.closest('svg');\n if (svgElement) {\n resizeObserver.observe(svgElement);\n }\n }\n\n return () => {\n observer.disconnect();\n resizeObserver.disconnect();\n };\n }, [wire, components]);\n\n // Update positions when components array changes (new components added or removed)\n useEffect(() => {\n updatePortPositions();\n }, [components.length]);\n\n // Do not render the wire if either port was not found\n if (!fromPos.found || !toPos.found) {\n return null;\n }\n\n // Calculate a bezier curve path with proper spacing between components\n const generatePath = (from: Point, to: Point): string => {\n // Calculate distance between points\n const dx = to.x - from.x;\n const dy = to.y - from.y;\n const distance = Math.sqrt(dx * dx + dy * dy);\n\n // For very short distances, use a straight line\n if (distance < 20) {\n return `M ${from.x} ${from.y} L ${to.x} ${to.y}`;\n }\n\n // Calculate control points with optimized offset\n const controlPointLength = Math.min(distance / 3, 60); // Reduced for smoother curves\n\n // Determine dominant axis and adjust control points to create natural curves\n if (Math.abs(dx) > Math.abs(dy)) {\n // Horizontal dominant\n const cp1x = from.x + controlPointLength;\n const cp1y = from.y;\n const cp2x = to.x - controlPointLength;\n const cp2y = to.y;\n return `M ${from.x} ${from.y} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${to.x} ${to.y}`;\n } else {\n // Vertical dominant\n const cp1x = from.x;\n const cp1y = from.y + controlPointLength;\n const cp2x = to.x;\n const cp2y = to.y - controlPointLength;\n return `M ${from.x} ${from.y} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${to.x} ${to.y}`;\n }\n };\n\n // Wire style based on selected state and custom style\n const wireStyle = {\n stroke: selected ? '#4f8ef7' : (wire.style?.color || 'white'),\n strokeWidth: selected ? 3 : (wire.style?.strokeWidth || 2),\n strokeDasharray: wire.style?.dashArray || 'none',\n fill: 'none',\n vectorEffect: 'non-scaling-stroke' as const,\n };\n\n return (\n <path\n ref={pathRef}\n d={generatePath(fromPos, toPos)}\n className={`wire ${selected ? 'selected' : ''}`}\n {...wireStyle}\n onClick={onClick}\n data-wire-id={wire.id}\n data-from-component={wire.from.componentId}\n data-from-port={wire.from.portId}\n data-to-component={wire.to.componentId}\n data-to-port={wire.to.portId}\n style={{ cursor: 'pointer' }}\n />\n );\n};\n\nexport default WirePath;\n","/**\n * CircuitCanvas Component\n *\n * The main canvas component that renders circuit components and wires.\n * Supports infinite panning and zooming for a free-flowing design experience.\n *\n * @component\n * @example\n * <CircuitCanvas\n * components={state.components}\n * wires={state.wires}\n * width={800}\n * height={600}\n * onComponentClick={handleComponentClick}\n * showGrid={true}\n * />\n */\n\nimport React, { useRef, useState, useEffect, useCallback, forwardRef } from 'react';\nimport { ComponentInstance, Wire, Point } from '../schemas/componentSchema';\nimport Brick from './Brick';\nimport WirePath from './WirePath';\nimport { getPortPosition } from '../utils/getPortPosition';\n// import { getComponentSchema } from '../registry';\n\nexport interface CircuitCanvasProps {\n /** Array of component instances to render on the canvas */\n components: ComponentInstance[];\n\n /** Array of wires connecting components */\n wires: Wire[];\n\n /** Width of the canvas. Can be a number (pixels) or string (e.g., '100%') */\n width?: number | string;\n\n /** Height of the canvas. Can be a number (pixels) or string (e.g., '100%') */\n height?: number | string;\n\n /** Callback when a component is clicked */\n onComponentClick?: (id: string, event: React.MouseEvent) => void;\n\n /** Callback when a wire is clicked */\n onWireClick?: (id: string, event: React.MouseEvent) => void;\n\n /** Callback when the canvas background is clicked */\n onCanvasClick?: (event: React.MouseEvent) => void;\n\n /** Callback when a component is dragged to a new position */\n onComponentDrag?: (id: string, newPosition: Point) => void;\n\n /** Callback when wire drawing starts from a port */\n onWireDrawStart?: (componentId: string, portId: string) => void;\n\n /** Callback when wire drawing ends on a port. Return true to accept the connection. */\n onWireDrawEnd?: (componentId: string, portId: string) => boolean;\n\n /** Callback when wire drawing is canceled */\n onWireDrawCancel?: () => void;\n\n /** Object representing the current wire drawing state */\n wireDrawing?: {\n isDrawing: boolean;\n fromComponentId: string | null;\n fromPortId: string | null;\n };\n\n /** Array of currently selected component IDs */\n selectedComponentIds?: string[];\n\n /** Array of currently selected wire IDs */\n selectedWireIds?: string[];\n\n /** Whether to show the grid on the canvas */\n showGrid?: boolean;\n\n /** Size of the grid cells in pixels */\n gridSize?: number;\n\n /** Whether to snap components to the grid */\n snapToGrid?: boolean;\n\n /** Callback when a component is dropped onto the canvas */\n onComponentDrop?: (componentType: string, position: Point) => void;\n\n /** Initial zoom level for the canvas (1.0 = 100%) */\n initialZoom?: number;\n\n /** Minimum allowed zoom level */\n minZoom?: number;\n\n /** Maximum allowed zoom level */\n maxZoom?: number;\n}\n\nexport const CircuitCanvas = forwardRef<SVGSVGElement, CircuitCanvasProps>(({\n components,\n wires,\n width = '100%',\n height = '100%',\n onComponentClick,\n onWireClick,\n onCanvasClick,\n onComponentDrag,\n onWireDrawStart,\n onWireDrawEnd,\n onWireDrawCancel,\n wireDrawing = {\n isDrawing: false,\n fromComponentId: null,\n fromPortId: null\n },\n selectedComponentIds = [],\n selectedWireIds = [],\n showGrid = true,\n gridSize = 20,\n snapToGrid = false,\n onComponentDrop,\n initialZoom = 1.0,\n minZoom = 0.25,\n maxZoom = 3.0\n}, ref) => {\n const svgRef = useRef<SVGSVGElement>(null);\n\n // Use forwarded ref or internal ref\n const actualRef = ref || svgRef;\n const [dragState, setDragState] = useState<{\n isDragging: boolean;\n componentId: string | null;\n startPos: Point | null;\n currentPos: Point | null;\n }>({\n isDragging: false,\n componentId: null,\n startPos: null,\n currentPos: null\n });\n\n const [currentMousePos, setCurrentMousePos] = useState<Point | null>(null);\n const [sourcePortPos, setSourcePortPos] = useState<Point | null>(null);\n\n // Viewport state for infinite canvas\n const [viewportTransform, setViewportTransform] = useState({\n x: 0,\n y: 0,\n scale: initialZoom\n });\n\n // For canvas panning\n const [isPanning, setIsPanning] = useState(false);\n const [panStartPos, setPanStartPos] = useState<Point | null>(null);\n\n // Ref to detect if mouse is down (for panning)\n const isMouseDownRef = useRef(false);\n\n // Effect to dispatch viewport-change event when transform changes\n useEffect(() => {\n if (!svgRef.current) return;\n\n // Create a custom event for viewport changes\n const viewportChangeEvent = new CustomEvent('viewport-change', {\n detail: viewportTransform,\n bubbles: true\n });\n\n // Dispatch the event\n svgRef.current.dispatchEvent(viewportChangeEvent);\n }, [viewportTransform]);\n\n // Effect to update the source port position when wire drawing starts\n useEffect(() => {\n if (wireDrawing.isDrawing && wireDrawing.fromComponentId && wireDrawing.fromPortId) {\n // Schedule a brief timeout to allow the DOM to update\n const timer = setTimeout(() => {\n const portPos = getPortPosition(wireDrawing.fromComponentId!, wireDrawing.fromPortId!);\n if (portPos) {\n setSourcePortPos(portPos);\n }\n }, 0);\n\n return () => clearTimeout(timer);\n } else {\n setSourcePortPos(null);\n }\n }, [wireDrawing]);\n\n // Convert screen coordinates to SVG coordinates (accounting for pan and zoom)\n const screenToSvgCoordinates = useCallback((clientX: number, clientY: number): Point => {\n if (!svgRef.current) return { x: 0, y: 0 };\n\n const svgRect = svgRef.current.getBoundingClientRect();\n const x = (clientX - svgRect.left - viewportTransform.x) / viewportTransform.scale;\n const y = (clientY - svgRect.top - viewportTransform.y) / viewportTransform.scale;\n\n // Snap to grid if enabled\n if (snapToGrid) {\n return {\n x: Math.round(x / gridSize) * gridSize,\n y: Math.round(y / gridSize) * gridSize\n };\n }\n\n return { x, y };\n }, [gridSize, snapToGrid, viewportTransform]);\n\n // Get SVG coordinates from mouse event (accounting for pan and zoom)\n const getSvgCoordinates = useCallback((e: React.MouseEvent | React.DragEvent): Point => {\n return screenToSvgCoordinates(e.clientX, e.clientY);\n }, [screenToSvgCoordinates]);\n\n const handleCanvasClick = (e: React.MouseEvent) => {\n // Only trigger if the click is directly on the canvas\n if (e.target === e.currentTarget) {\n // If we weren't panning, trigger the canvas click\n if (!isPanning) {\n onCanvasClick?.(e);\n\n // If we're drawing a wire, cancel it\n if (wireDrawing.isDrawing) {\n onWireDrawCancel?.();\n }\n }\n }\n };\n\n const handleComponentClick = (id: string, event: React.MouseEvent) => {\n // Only handle click if we're not dragging\n if (!dragState.isDragging && !isPanning) {\n onComponentClick?.(id, event);\n }\n };\n\n const handleWireClick = (id: string, event: React.MouseEvent) => {\n if (!isPanning) {\n onWireClick?.(id, event);\n }\n };\n\n const handleMouseDown = (componentId: string, event: React.MouseEvent) => {\n // Start dragging - allow dragging of any clicked component\n const startPos = getSvgCoordinates(event);\n setDragState({\n isDragging: true,\n componentId,\n startPos,\n currentPos: startPos\n });\n\n // Ensure the component is selected\n if (!selectedComponentIds.includes(componentId) && onComponentClick) {\n onComponentClick(componentId, event);\n }\n\n event.stopPropagation();\n };\n\n // Handle mouse down on the canvas for panning\n const handleCanvasMouseDown = (event: React.MouseEvent) => {\n // Only start panning if we click the canvas background and it's a middle mouse button or space + left click\n if (event.target === event.currentTarget && (event.button === 1 || (event.button === 0 && event.altKey))) {\n setIsPanning(true);\n setPanStartPos({ x: event.clientX, y: event.clientY });\n isMouseDownRef.current = true;\n\n // Prevent default to avoid text selection\n event.preventDefault();\n }\n };\n\n const handleMouseMove = (event: React.MouseEvent) => {\n // Handle pan movement\n if (isPanning && panStartPos) {\n const dx = event.clientX - panStartPos.x;\n const dy = event.clientY - panStartPos.y;\n\n setViewportTransform(prev => ({\n ...prev,\n x: prev.x + dx,\n y: prev.y + dy\n }));\n\n setPanStartPos({ x: event.clientX, y: event.clientY });\n return;\n }\n\n // For regular interaction, convert coordinates\n const currentPos = getSvgCoordinates(event);\n setCurrentMousePos(currentPos);\n\n // Update drag position if dragging\n if (dragState.isDragging && dragState.componentId) {\n setDragState({ ...dragState, currentPos });\n\n // Find the component being dragged\n const component = components.find(c => c.id === dragState.componentId);\n if (component && dragState.startPos && currentPos) {\n // Calculate the new position\n const dx = currentPos.x - dragState.startPos.x;\n const dy = currentPos.y - dragState.startPos.y;\n const newPosition = {\n x: component.position.x + dx,\n y: component.position.y + dy\n };\n\n // Snap to grid if needed (now controlled by snapToGrid prop)\n if (snapToGrid) {\n newPosition.x = Math.round(newPosition.x / gridSize) * gridSize;\n newPosition.y = Math.round(newPosition.y / gridSize) * gridSize;\n }\n\n // Call the onComponentDrag handler\n onComponentDrag?.(dragState.componentId, newPosition);\n }\n }\n };\n\n const handleMouseUp = (event: React.MouseEvent) => {\n // End dragging\n if (dragState.isDragging) {\n setDragState({\n isDragging: false,\n componentId: null,\n startPos: null,\n currentPos: null\n });\n }\n\n // End panning\n if (isPanning) {\n setIsPanning(false);\n setPanStartPos(null);\n }\n\n isMouseDownRef.current = false;\n };\n\n // Handle mouse leaving the canvas\n const handleMouseLeave = () => {\n if (isPanning) {\n setIsPanning(false);\n setPanStartPos(null);\n }\n isMouseDownRef.current = false;\n };\n\n // Handle wheel event for zooming\n const handleWheel = (event: React.WheelEvent) => {\n event.preventDefault();\n\n // Only zoom if ctrl key is pressed\n if (event.ctrlKey || event.metaKey) {\n const delta = event.deltaY > 0 ? -0.1 : 0.1;\n const newScale = Math.max(minZoom, Math.min(maxZoom, viewportTransform.scale + delta));\n\n // Get mouse position relative to SVG\n const svgRect = svgRef.current?.getBoundingClientRect();\n if (!svgRect) return;\n\n const mouseX = event.clientX - svgRect.left;\n const mouseY = event.clientY - svgRect.top;\n\n // Calculate new transform origin based on mouse position\n setViewportTransform(prev => {\n // Calculate the point in world coordinates\n const worldX = (mouseX - prev.x) / prev.scale;\n const worldY = (mouseY - prev.y) / prev.scale;\n\n // Calculate new transform (zoom around mouse position)\n const newX = mouseX - worldX * newScale;\n const newY = mouseY - worldY * newScale;\n\n return {\n x: newX,\n y: newY,\n scale: newScale\n };\n });\n } else {\n // Pan horizontally with shift+wheel\n if (event.shiftKey) {\n setViewportTransform(prev => ({\n ...prev,\n x: prev.x - event.deltaY\n }));\n } else {\n // Pan vertically with wheel\n setViewportTransform(prev => ({\n ...prev,\n y: prev.y - event.deltaY\n }));\n }\n }\n };\n\n const handlePortClick = (componentId: string, portId: string, event: React.MouseEvent) => {\n // Ignore port clicks when panning\n if (isPanning) return;\n\n event.stopPropagation();\n\n // If we're not drawing a wire, start drawing\n if (!wireDrawing.isDrawing) {\n onWireDrawStart?.(componentId, portId);\n } else {\n // If we are drawing a wire, end drawing and connect to this port\n if (wireDrawing.fromComponentId && wireDrawing.fromPortId) {\n onWireDrawEnd?.(componentId, portId);\n }\n }\n };\n\n // Generate a bezier curve path between two points - updated to match WirePath\n const generateWirePath = (from: Point, to: Point): string => {\n // Calculate distance between points\n const dx = to.x - from.x;\n const dy = to.y - from.y;\n const distance = Math.sqrt(dx * dx + dy * dy);\n\n // For very short distances, use a straight line\n if (distance < 20) {\n return `M ${from.x} ${from.y} L ${to.x} ${to.y}`;\n }\n\n // Calculate control points with optimized offset\n const controlPointLength = Math.min(distance / 3, 60); // Reduced for smoother curves\n\n // Determine dominant axis and adjust control points to create natural curves\n if (Math.abs(dx) > Math.abs(dy)) {\n // Horizontal dominant\n const cp1x = from.x + controlPointLength;\n const cp1y = from.y;\n const cp2x = to.x - controlPointLength;\n const cp2y = to.y;\n return `M ${from.x} ${from.y} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${to.x} ${to.y}`;\n } else {\n // Vertical dominant\n const cp1x = from.x;\n const cp1y = from.y + controlPointLength;\n const cp2x = to.x;\n const cp2y = to.y - controlPointLength;\n return `M ${from.x} ${from.y} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${to.x} ${to.y}`;\n }\n };\n\n // Handle drag and drop events for component palette\n const handleDragOver = (event: React.DragEvent) => {\n // Prevent default to allow drop\n event.preventDefault();\n // Set the drop effect to copy\n event.dataTransfer.dropEffect = 'copy';\n };\n\n const handleDrop = (event: React.DragEvent) => {\n event.preventDefault();\n\n // Get the component type from the drag data\n const componentType = event.dataTransfer.getData('application/circuit-component');\n if (!componentType || !onComponentDrop) return;\n\n // Get drop position in SVG coordinates (taking into account pan/zoom)\n const position = getSvgCoordinates(event);\n\n // Create a new component at this position\n onComponentDrop(componentType, position);\n };\n\n // Create keyboard shortcut handlers for common operations\n useEffect(() => {\n const handleKeyDown = (e: KeyboardEvent) => {\n // Only process when the canvas is active\n if (!svgRef.current) return;\n\n // Ignore if we're in an input field\n if (e.target instanceof HTMLInputElement ||\n e.target instanceof HTMLTextAreaElement) {\n return;\n }\n\n // Handle space key for panning toggle\n if (e.code === 'Space' && !isPanning && document.activeElement === document.body) {\n e.preventDefault(); // Prevent page scroll\n }\n\n // Reset view with key '0'\n if (e.key === '0' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault();\n setViewportTransform({\n x: 0,\n y: 0,\n scale: 1.0\n });\n }\n\n // Zoom in with + or =\n if ((e.key === '+' || e.key === '=') && (e.ctrlKey || e.metaKey)) {\n e.preventDefault();\n setViewportTransform(prev => ({\n ...prev,\n scale: Math.min(maxZoom, prev.scale + 0.1)\n }));\n }\n\n // Zoom out with -\n if (e.key === '-' && (e.ctrlKey || e.metaKey)) {\n e.preventDefault();\n setViewportTransform(prev => ({\n ...prev,\n scale: Math.max(minZoom, prev.scale - 0.1)\n }));\n }\n\n // Arrow keys for panning when Alt is pressed\n if (e.altKey && ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {\n e.preventDefault();\n const panAmount = e.shiftKey ? 50 : 20; // Faster panning with Shift\n\n setViewportTransform(prev => {\n let dx = 0, dy = 0;\n if (e.key === 'ArrowLeft') dx = panAmount;\n if (e.key === 'ArrowRight') dx = -panAmount;\n if (e.key === 'ArrowUp') dy = panAmount;\n if (e.key === 'ArrowDown') dy = -panAmount;\n\n return {\n ...prev,\n x: prev.x + dx,\n y: prev.y + dy\n };\n });\n }\n };\n\n window.addEventListener('keydown', handleKeyDown);\n return () => {\n window.removeEventListener('keydown', handleKeyDown);\n };\n }, [isPanning, maxZoom, minZoom]);\n\n // State for showing help message\n const [showHelp, setShowHelp] = useState(true);\n\n // Hide help message after 15 seconds\n useEffect(() => {\n if (showHelp) {\n const timer = setTimeout(() => {\n setShowHelp(false);\n }, 15000);\n return () => clearTimeout(timer);\n }\n }, [showHelp]);\n\n // Cursor style based on current interaction mode\n const getCursorStyle = () => {\n if (isPanning) return 'grabbing';\n if (isMouseDownRef.current) return 'grabbing';\n if (wireDrawing.isDrawing) return 'crosshair';\n return 'default';\n };\n\n // Calculate grid pattern sizing based on zoom\n const getGridSize = () => {\n // Adjust grid density based on zoom level for better visualization\n const scaledGridSize = gridSize * (1 + (1 - Math.min(viewportTransform.scale, 1)) * 2);\n return scaledGridSize;\n };\n\n return (\n <svg\n ref={actualRef}\n width={width}\n height={height}\n style={{\n backgroundColor: '#111111',\n userSelect: 'none',\n cursor: getCursorStyle(),\n overflow: 'hidden'\n }}\n onClick={handleCanvasClick}\n onMouseDown={handleCanvasMouseDown}\n onMouseMove={handleMouseMove}\n onMouseUp={handleMouseUp}\n onMouseLeave={handleMouseLeave}\n onWheel={handleWheel}\n onDragOver={handleDragOver}\n onDrop={handleDrop}\n data-circuit-canvas\n >\n {/* Grid pattern */}\n <defs>\n <pattern\n id=\"grid\"\n width={getGridSize()}\n height={getGridSize()}\n patternUnits=\"userSpaceOnUse\"\n patternTransform={`translate(${viewportTransform.x % getGridSize()},${viewportTransform.y % getGridSize()}) scale(${viewportTransform.scale})`}\n >\n <rect width={getGridSize()} height={getGridSize()} fill=\"#111111\" />\n <circle cx={getGridSize()} cy={getGridSize()} r=\"0.5\" fill=\"#222222\" />\n </pattern>\n\n <pattern\n id=\"dots\"\n width={getGridSize()}\n height={getGridSize()}\n patternUnits=\"userSpaceOnUse\"\n patternTransform={`translate(${viewportTransform.x % getGridSize()},${viewportTransform.y % getGridSize()}) scale(${viewportTransform.scale})`}\n >\n <rect width={getGridSize()} height={getGridSize()} fill=\"#111111\" />\n <circle cx={getGridSize()/2} cy={getGridSize()/2} r=\"0.7\" fill=\"#222222\" />\n </pattern>\n\n {/* Pattern for infinite grid that adjusts with pan/zoom */}\n <pattern\n id=\"infinite-grid\"\n width={getGridSize() * 5}\n height={getGridSize() * 5}\n patternUnits=\"userSpaceOnUse\"\n patternTransform={`translate(${viewportTransform.x % (getGridSize() * 5)},${viewportTransform.y % (getGridSize() * 5)}) scale(${viewportTransform.scale})`}\n >\n <rect width={getGridSize() * 5} height={getGridSize() * 5} fill=\"#111111\" />\n <path\n d={`M 0 0 H ${getGridSize() * 5} M 0 ${getGridSize()} H ${getGridSize() * 5} M 0 ${getGridSize() * 2} H ${getGridSize() * 5} M 0 ${getGridSize() * 3} H ${getGridSize() * 5} M 0 ${getGridSize() * 4} H ${getGridSize() * 5} M 0 0 V ${getGridSize() * 5} M ${getGridSize()} 0 V ${getGridSize() * 5} M ${getGridSize() * 2} 0 V ${getGridSize() * 5} M ${getGridSize() * 3} 0 V ${getGridSize() * 5} M ${getGridSize() * 4} 0 V ${getGridSize() * 5}`}\n stroke=\"#222222\"\n strokeWidth=\"0.3\"\n />\n <path\n d={`M 0 0 H ${getGridSize() * 5} M 0 ${getGridSize() * 5} H ${getGridSize() * 5} M 0 0 V ${getGridSize() * 5} M ${getGridSize() * 5} 0 V ${getGridSize() * 5}`}\n stroke=\"#333333\"\n strokeWidth=\"0.5\"\n />\n </pattern>\n </defs>\n {showGrid && <rect width=\"100%\" height=\"100%\" fill=\"url(#infinite-grid)\" />}\n\n {/* Viewport transformation group */}\n <g transform={`translate(${viewportTransform.x}, ${viewportTransform.y}) scale(${viewportTransform.scale})`}>\n {/* Layers in correct order to ensure proper rendering */}\n {/* 1. Wires layer (bottom) */}\n <g className=\"circuit-wires\">\n {wires.map(wire => (\n <WirePath\n key={wire.id}\n wire={wire}\n components={components}\n selected={selectedWireIds.includes(wire.id)}\n onClick={(e) => handleWireClick(wire.id, e)}\n />\n ))}\n\n {/* Wire currently being drawn */}\n {wireDrawing.isDrawing && sourcePortPos && currentMousePos && (\n <path\n d={generateWirePath(sourcePortPos, currentMousePos)}\n stroke=\"#4f8ef7\"\n strokeWidth={2}\n strokeDasharray=\"5,5\"\n fill=\"none\"\n className=\"wire-preview\"\n style={{ pointerEvents: 'visible' }}\n />\n )}\n </g>\n\n {/* 2. Components layer (top) - ensures components are rendered on top of wires */}\n <g className=\"circuit-components\">\n {components.map(component => (\n <Brick\n key={component.id}\n component={component}\n selected={selectedComponentIds.includes(component.id)}\n onClick={(e) => handleComponentClick(component.id, e)}\n onMouseDown={(e) => handleMouseDown(component.id, e)}\n onPortClick={handlePortClick}\n />\n ))}\n </g>\n </g>\n\n {/* Viewport information overlay (optional for debugging) */}\n {process.env.NODE_ENV === 'development' && (\n <text\n x=\"10\"\n y=\"20\"\n fill=\"#666\"\n fontSize=\"12\"\n pointerEvents=\"none\"\n >\n {`Position: (${Math.round(viewportTransform.x)}, ${Math.round(viewportTransform.y)}) | Zoom: ${viewportTransform.scale.toFixed(2)}`}\n </text>\n )}\n\n {/* Minimap for navigation in infinite canvas (bottom-left) */}\n <g className=\"circuit-minimap\" transform={`translate(10, ${typeof height === 'number' ? height - 120 : 'calc(100% - 120px)'})`}>\n <rect\n x=\"0\"\n y=\"0\"\n width=\"110\"\n height=\"110\"\n fill=\"#222\"\n fillOpacity=\"0.7\"\n stroke=\"#444\"\n strokeWidth=\"1\"\n rx=\"4\"\n />\n\n {/* Viewport indicator */}\n <rect\n x={5}\n y={5}\n width={100}\n height={100}\n fill=\"none\"\n stroke=\"#333\"\n strokeWidth=\"1\"\n />\n\n {/* Components representation */}\n <g transform=\"scale(0.1) translate(50, 50)\">\n {components.map(component => (\n <circle\n key={component.id}\n cx={component.position.x}\n cy={component.position.y}\n r={15}\n fill={selectedComponentIds.includes(component.id) ? \"#4f8ef7\" : \"#666\"}\n />\n ))}\n\n {/* Viewport window representation */}\n <rect\n x={-viewportTransform.x / viewportTransform.scale}\n y={-viewportTransform.y / viewportTransform.scale}\n width={typeof width === 'number' ? width / viewportTransform.scale : 1000 / viewportTransform.scale}\n height={typeof height === 'number' ? height / viewportTransform.scale : 800 / viewportTransform.scale}\n fill=\"none\"\n stroke=\"#4f8ef7\"\n strokeWidth=\"10\"\n strokeDasharray=\"20,10\"\n />\n </g>\n\n {/* Toggle button */}\n <rect\n x=\"85\"\n y=\"0\"\n width=\"25\"\n height=\"25\"\n fill=\"#333\"\n rx=\"4\"\n style={{ cursor: 'pointer' }}\n onClick={() => {\n const minimapEl = document.querySelector('.circuit-minimap');\n if (minimapEl) {\n minimapEl.setAttribute('style', 'display: none');\n }\n }}\n />\n <text x=\"93\" y=\"17\" fill=\"#fff\" fontSize=\"16\" style={{ cursor: 'pointer' }}>\n ×\n </text>\n </g>\n\n {/* Help message for infinite canvas */}\n {showHelp && (\n <g transform={`translate(${typeof width === 'number' ? width / 2 - 150 : '50%'}, 30)`}>\n <rect\n x=\"-10\"\n y=\"-10\"\n width=\"320\"\n height=\"85\"\n rx=\"8\"\n fill=\"rgba(0, 0, 0, 0.7)\"\n stroke=\"#4f8ef7\"\n strokeWidth=\"1\"\n />\n <text x=\"0\" y=\"15\" fill=\"#fff\" fontSize=\"14\" fontWeight=\"bold\">\n Infinite Canvas Controls:\n </text>\n <text x=\"0\" y=\"35\" fill=\"#eee\" fontSize=\"12\">\n • Alt + drag/middle mouse: Pan canvas\n </text>\n <text x=\"0\" y=\"50\" fill=\"#eee\" fontSize=\"12\">\n • Ctrl/⌘ + scroll: Zoom in/out\n </text>\n <text x=\"0\" y=\"65\" fill=\"#eee\" fontSize=\"12\">\n • Ctrl/⌘ + 0: Reset view\n </text>\n <rect\n x=\"290\"\n y=\"-10\"\n width=\"20\"\n height=\"20\"\n rx=\"4\"\n fill=\"rgba(100, 100, 100, 0.5)\"\n cursor=\"pointer\"\n onClick={() => setShowHelp(false)}\n />\n <text x=\"296\" y=\"5\" fill=\"#fff\" fontSize=\"16\" cursor=\"pointer\" onClick={() => setShowHelp(false)}>\n ×\n </text>\n </g>\n )}\n\n {/* Mini controls overlay */}\n <g\n transform=\"translate(10, 10)\"\n style={{ opacity: 0.7 }}\n onMouseEnter={(e) => e.currentTarget.style.opacity = '1'}\n onMouseLeave={(e) => e.currentTarget.style.opacity = '0.7'}\n >\n <rect\n x=\"0\"\n y=\"0\"\n width=\"80\"\n height=\"22\"\n rx=\"4\"\n fill=\"#222\"\n fillOpacity=\"0.8\"\n />\n <g\n transform=\"translate(10, 11)\"\n cursor=\"pointer\"\n onClick={() => setViewportTransform(prev => ({...prev, scale: Math.min(maxZoom, prev.scale + 0.1)}))}\n >\n <circle r=\"8\" fill=\"#333\" />\n <path d=\"M-4,0 H4 M0,-4 V4\" stroke=\"#aaa\" strokeWidth=\"1.5\" />\n </g>\n <g\n transform=\"translate(40, 11)\"\n cursor=\"pointer\"\n onClick={() => setViewportTransform(prev => ({...prev, scale: Math.max(minZoom, prev.scale - 0.1)}))}\n >\n <circle r=\"8\" fill=\"#333\" />\n <path d=\"M-4,0 H4\" stroke=\"#aaa\" strokeWidth=\"1.5\" />\n </g>\n <g\n transform=\"translate(70, 11)\"\n cursor=\"pointer\"\n onClick={() => setViewportTransform({x: 0, y: 0, scale: 1})}\n >\n <circle r=\"8\" fill=\"#333\" />\n <path d=\"M-3,-3 L3,3 M-3,3 L3,-3\" stroke=\"#aaa\" strokeWidth=\"1.5\" />\n </g>\n </g>\n </svg>\n );\n});\n\nCircuitCanvas.displayName = 'CircuitCanvas';\n\nexport default CircuitCanvas;\n"],"mappings":"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