UNPKG

circuit-bricks

Version:

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

942 lines (935 loc) 28.9 kB
"use client"; import { getComponentSchema } from "./registry-L10aw9oV.mjs"; import { isBrowser, safeQuerySelector } from "./ssrUtils-Ca211nnK.mjs"; import React, { forwardRef, useCallback, useEffect, useRef, useState } from "react"; import { jsx, jsxs } from "react/jsx-runtime"; //#region src/core/Port.tsx const Port = ({ componentId, port, selected = false, onClick }) => { const handleClick = (event) => { event.stopPropagation(); onClick?.(port.id, event); }; return /* @__PURE__ */ jsx( "circle", // Prevent triggering component click { cx: port.x, cy: port.y, r: 5, fill: selected ? "#4f8ef7" : "#555", stroke: "#000", strokeWidth: 1, "data-component-id": componentId, "data-port-id": port.id, "data-port-type": port.type, vectorEffect: "non-scaling-stroke", onClick: handleClick, style: { cursor: "pointer" } } ); }; var Port_default = Port; //#endregion //#region src/core/BaseComponent.tsx const BaseComponent = ({ schema, component, onClick, onMouseDown, onPortClick, selected = false }) => { const { id, position, size, rotation = 0 } = component; const width = size?.width || schema.defaultWidth; const height = size?.height || schema.defaultHeight; const transform = `translate(${position.x}, ${position.y})${rotation ? ` rotate(${rotation} ${width / 2} ${height / 2})` : ""}`; const outlineStyle = selected ? { stroke: "#4f8ef7", strokeWidth: 2, strokeDasharray: "none", fill: "none", pointerEvents: "none", vectorEffect: "non-scaling-stroke" } : void 0; return /* @__PURE__ */ jsxs("g", { transform, onClick, onMouseDown, "data-component-id": id, "data-component-type": component.type, children: [ /* @__PURE__ */ jsx("svg", { width, height, viewBox: `0 0 ${schema.defaultWidth} ${schema.defaultHeight}`, preserveAspectRatio: "none", xmlns: "http://www.w3.org/2000/svg", children: schema.svgPath.startsWith("<svg") ? /* @__PURE__ */ jsx("g", { dangerouslySetInnerHTML: { __html: schema.svgPath } }) : /* @__PURE__ */ jsx("path", { d: schema.svgPath, fill: "none", stroke: "white", strokeWidth: 2, vectorEffect: "non-scaling-stroke" }) }), schema.ports.map((port) => /* @__PURE__ */ jsx(Port_default, { componentId: id, port, selected: false, onClick: (portId, event) => onPortClick?.(id, portId, event) }, port.id)), selected && /* @__PURE__ */ jsx("rect", { x: 0, y: 0, width, height, ...outlineStyle }) ] }); }; var BaseComponent_default = BaseComponent; //#endregion //#region src/core/Brick.tsx const Brick = ({ component, onClick, onMouseDown, onPortClick, selected = false }) => { const schema = getComponentSchema(component.type); if (!schema) { console.warn(`No schema found for component type: ${component.type}`); return /* @__PURE__ */ jsxs("g", { transform: `translate(${component.position.x}, ${component.position.y})`, onClick, onMouseDown, "data-component-id": component.id, "data-component-type": "unknown", children: [/* @__PURE__ */ jsx("rect", { width: 100, height: 50, fill: "red", stroke: "white", strokeWidth: 1.5, strokeDasharray: "5,5", vectorEffect: "non-scaling-stroke" }), /* @__PURE__ */ jsxs("text", { x: 50, y: 25, textAnchor: "middle", dominantBaseline: "middle", fill: "white", fontFamily: "sans-serif", fontSize: "12px", children: ["Unknown: ", component.type] })] }); } return /* @__PURE__ */ jsx(BaseComponent_default, { schema, component, onClick, onMouseDown, onPortClick, selected }); }; var Brick_default = Brick; //#endregion //#region src/utils/getPortPosition.ts /** * Retrieves the absolute position of a port in the SVG coordinate system, * accounting for any viewport transformations (pan/zoom). * * @param componentId - The ID of the component * @param portId - The ID of the port on the component * @returns The position of the port or null if not found */ function getPortPosition(componentId, portId) { if (!isBrowser()) return null; const portElement = safeQuerySelector(`[data-component-id="${componentId}"][data-port-id="${portId}"]`); if (!portElement) { console.warn(`Port not found: componentId=${componentId}, portId=${portId}`); return null; } let currentEl = portElement; let svgRoot = null; while (currentEl) { if (currentEl instanceof SVGSVGElement) { svgRoot = currentEl; break; } currentEl = currentEl.parentElement; } if (!svgRoot) { console.warn("SVG root not found for port"); return null; } const portRect = portElement.getBoundingClientRect(); const svgRect = svgRoot.getBoundingClientRect(); const clientX = portRect.left + portRect.width / 2; const clientY = portRect.top + portRect.height / 2; const svgPoint = svgRoot.createSVGPoint(); svgPoint.x = clientX; svgPoint.y = clientY; const ctm = svgRoot.getScreenCTM(); if (!ctm) { console.warn("Could not get screen CTM for SVG"); return null; } const inverseCTM = ctm.inverse(); const transformedPoint = svgPoint.matrixTransform(inverseCTM); return { x: transformedPoint.x, y: transformedPoint.y }; } /** * Hook version of getPortPosition for React components * Will be implemented in a separate usePortPosition.ts file */ //#endregion //#region src/core/WirePath.tsx const WirePath = ({ wire, components, selected = false, onClick }) => { const [fromPos, setFromPos] = useState({ x: 0, y: 0, found: false }); const [toPos, setToPos] = useState({ x: 0, y: 0, found: false }); const pathRef = useRef(null); const updatePortPositions = () => { try { const fromPosition = getPortPosition(wire.from.componentId, wire.from.portId); if (fromPosition) setFromPos({ ...fromPosition, found: true }); } catch (error) { console.warn(`Could not find source port for wire ${wire.id}:`, error); } try { const toPosition = getPortPosition(wire.to.componentId, wire.to.portId); if (toPosition) setToPos({ ...toPosition, found: true }); } catch (error) { console.warn(`Could not find destination port for wire ${wire.id}:`, error); } }; useEffect(() => { updatePortPositions(); const observer = new MutationObserver((mutations) => { let shouldUpdate = false; mutations.forEach((mutation) => { if (mutation.type === "attributes" && (mutation.attributeName === "transform" || mutation.attributeName === "x" || mutation.attributeName === "y")) shouldUpdate = true; }); if (shouldUpdate) updatePortPositions(); }); const fromComponent = document.querySelector(`[data-component-id="${wire.from.componentId}"]`); const toComponent = document.querySelector(`[data-component-id="${wire.to.componentId}"]`); if (fromComponent) observer.observe(fromComponent, { attributes: true, attributeFilter: [ "transform", "x", "y" ] }); if (toComponent) observer.observe(toComponent, { attributes: true, attributeFilter: [ "transform", "x", "y" ] }); const resizeObserver = new ResizeObserver(() => { updatePortPositions(); }); if (pathRef.current) { const svgElement = pathRef.current.closest("svg"); if (svgElement) resizeObserver.observe(svgElement); } return () => { observer.disconnect(); resizeObserver.disconnect(); }; }, [wire, components]); useEffect(() => { updatePortPositions(); }, [components.length]); if (!fromPos.found || !toPos.found) return null; const generatePath = (from, to) => { const dx = to.x - from.x; const dy = to.y - from.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < 20) return `M ${from.x} ${from.y} L ${to.x} ${to.y}`; const controlPointLength = Math.min(distance / 3, 60); if (Math.abs(dx) > Math.abs(dy)) { const cp1x = from.x + controlPointLength; const cp1y = from.y; const cp2x = to.x - controlPointLength; const cp2y = to.y; return `M ${from.x} ${from.y} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${to.x} ${to.y}`; } else { const cp1x = from.x; const cp1y = from.y + controlPointLength; const cp2x = to.x; const cp2y = to.y - controlPointLength; return `M ${from.x} ${from.y} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${to.x} ${to.y}`; } }; const wireStyle = { stroke: selected ? "#4f8ef7" : wire.style?.color || "white", strokeWidth: selected ? 3 : wire.style?.strokeWidth || 2, strokeDasharray: wire.style?.dashArray || "none", fill: "none", vectorEffect: "non-scaling-stroke" }; return /* @__PURE__ */ jsx( "path", // Reduced for smoother curves { ref: pathRef, d: generatePath(fromPos, toPos), className: `wire ${selected ? "selected" : ""}`, ...wireStyle, onClick, "data-wire-id": wire.id, "data-from-component": wire.from.componentId, "data-from-port": wire.from.portId, "data-to-component": wire.to.componentId, "data-to-port": wire.to.portId, style: { cursor: "pointer" } } ); }; var WirePath_default = WirePath; //#endregion //#region src/core/CircuitCanvas.tsx const CircuitCanvas = forwardRef(({ components, wires, width = "100%", height = "100%", onComponentClick, onWireClick, onCanvasClick, onComponentDrag, onWireDrawStart, onWireDrawEnd, onWireDrawCancel, wireDrawing = { isDrawing: false, fromComponentId: null, fromPortId: null }, selectedComponentIds = [], selectedWireIds = [], showGrid = true, gridSize = 20, snapToGrid = false, onComponentDrop, initialZoom = 1, minZoom = .25, maxZoom = 3 }, ref) => { const svgRef = useRef(null); const actualRef = ref || svgRef; const [dragState, setDragState] = useState({ isDragging: false, componentId: null, startPos: null, currentPos: null }); const [currentMousePos, setCurrentMousePos] = useState(null); const [sourcePortPos, setSourcePortPos] = useState(null); const [viewportTransform, setViewportTransform] = useState({ x: 0, y: 0, scale: initialZoom }); const [isPanning, setIsPanning] = useState(false); const [panStartPos, setPanStartPos] = useState(null); const isMouseDownRef = useRef(false); useEffect(() => { if (!svgRef.current) return; const viewportChangeEvent = new CustomEvent("viewport-change", { detail: viewportTransform, bubbles: true }); svgRef.current.dispatchEvent(viewportChangeEvent); }, [viewportTransform]); useEffect(() => { if (wireDrawing.isDrawing && wireDrawing.fromComponentId && wireDrawing.fromPortId) { const timer = setTimeout(() => { const portPos = getPortPosition(wireDrawing.fromComponentId, wireDrawing.fromPortId); if (portPos) setSourcePortPos(portPos); }, 0); return () => clearTimeout(timer); } else setSourcePortPos(null); }, [wireDrawing]); const screenToSvgCoordinates = useCallback((clientX, clientY) => { if (!svgRef.current) return { x: 0, y: 0 }; const svgRect = svgRef.current.getBoundingClientRect(); const x = (clientX - svgRect.left - viewportTransform.x) / viewportTransform.scale; const y = (clientY - svgRect.top - viewportTransform.y) / viewportTransform.scale; if (snapToGrid) return { x: Math.round(x / gridSize) * gridSize, y: Math.round(y / gridSize) * gridSize }; return { x, y }; }, [ gridSize, snapToGrid, viewportTransform ]); const getSvgCoordinates = useCallback((e) => { return screenToSvgCoordinates(e.clientX, e.clientY); }, [screenToSvgCoordinates]); const handleCanvasClick = (e) => { if (e.target === e.currentTarget) { if (!isPanning) { onCanvasClick?.(e); if (wireDrawing.isDrawing) onWireDrawCancel?.(); } } }; const handleComponentClick = (id, event) => { if (!dragState.isDragging && !isPanning) onComponentClick?.(id, event); }; const handleWireClick = (id, event) => { if (!isPanning) onWireClick?.(id, event); }; const handleMouseDown = (componentId, event) => { const startPos = getSvgCoordinates(event); setDragState({ isDragging: true, componentId, startPos, currentPos: startPos }); if (!selectedComponentIds.includes(componentId) && onComponentClick) onComponentClick(componentId, event); event.stopPropagation(); }; const handleCanvasMouseDown = (event) => { if (event.target === event.currentTarget && (event.button === 1 || event.button === 0 && event.altKey)) { setIsPanning(true); setPanStartPos({ x: event.clientX, y: event.clientY }); isMouseDownRef.current = true; event.preventDefault(); } }; const handleMouseMove = (event) => { if (isPanning && panStartPos) { const dx = event.clientX - panStartPos.x; const dy = event.clientY - panStartPos.y; setViewportTransform((prev) => ({ ...prev, x: prev.x + dx, y: prev.y + dy })); setPanStartPos({ x: event.clientX, y: event.clientY }); return; } const currentPos = getSvgCoordinates(event); setCurrentMousePos(currentPos); if (dragState.isDragging && dragState.componentId) { setDragState({ ...dragState, currentPos }); const component = components.find((c) => c.id === dragState.componentId); if (component && dragState.startPos && currentPos) { const dx = currentPos.x - dragState.startPos.x; const dy = currentPos.y - dragState.startPos.y; const newPosition = { x: component.position.x + dx, y: component.position.y + dy }; if (snapToGrid) { newPosition.x = Math.round(newPosition.x / gridSize) * gridSize; newPosition.y = Math.round(newPosition.y / gridSize) * gridSize; } onComponentDrag?.(dragState.componentId, newPosition); } } }; const handleMouseUp = (event) => { if (dragState.isDragging) setDragState({ isDragging: false, componentId: null, startPos: null, currentPos: null }); if (isPanning) { setIsPanning(false); setPanStartPos(null); } isMouseDownRef.current = false; }; const handleMouseLeave = () => { if (isPanning) { setIsPanning(false); setPanStartPos(null); } isMouseDownRef.current = false; }; const handleWheel = (event) => { event.preventDefault(); if (event.ctrlKey || event.metaKey) { const delta = event.deltaY > 0 ? -.1 : .1; const newScale = Math.max(minZoom, Math.min(maxZoom, viewportTransform.scale + delta)); const svgRect = svgRef.current?.getBoundingClientRect(); if (!svgRect) return; const mouseX = event.clientX - svgRect.left; const mouseY = event.clientY - svgRect.top; setViewportTransform((prev) => { const worldX = (mouseX - prev.x) / prev.scale; const worldY = (mouseY - prev.y) / prev.scale; const newX = mouseX - worldX * newScale; const newY = mouseY - worldY * newScale; return { x: newX, y: newY, scale: newScale }; }); } else if (event.shiftKey) setViewportTransform((prev) => ({ ...prev, x: prev.x - event.deltaY })); else setViewportTransform((prev) => ({ ...prev, y: prev.y - event.deltaY })); }; const handlePortClick = (componentId, portId, event) => { if (isPanning) return; event.stopPropagation(); if (!wireDrawing.isDrawing) onWireDrawStart?.(componentId, portId); else if (wireDrawing.fromComponentId && wireDrawing.fromPortId) onWireDrawEnd?.(componentId, portId); }; const generateWirePath = (from, to) => { const dx = to.x - from.x; const dy = to.y - from.y; const distance = Math.sqrt(dx * dx + dy * dy); if (distance < 20) return `M ${from.x} ${from.y} L ${to.x} ${to.y}`; const controlPointLength = Math.min(distance / 3, 60); if (Math.abs(dx) > Math.abs(dy)) { const cp1x = from.x + controlPointLength; const cp1y = from.y; const cp2x = to.x - controlPointLength; const cp2y = to.y; return `M ${from.x} ${from.y} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${to.x} ${to.y}`; } else { const cp1x = from.x; const cp1y = from.y + controlPointLength; const cp2x = to.x; const cp2y = to.y - controlPointLength; return `M ${from.x} ${from.y} C ${cp1x} ${cp1y}, ${cp2x} ${cp2y}, ${to.x} ${to.y}`; } }; const handleDragOver = (event) => { event.preventDefault(); event.dataTransfer.dropEffect = "copy"; }; const handleDrop = (event) => { event.preventDefault(); const componentType = event.dataTransfer.getData("application/circuit-component"); if (!componentType || !onComponentDrop) return; const position = getSvgCoordinates(event); onComponentDrop(componentType, position); }; useEffect(() => { const handleKeyDown = (e) => { if (!svgRef.current) return; if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return; if (e.code === "Space" && !isPanning && document.activeElement === document.body) e.preventDefault(); if (e.key === "0" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); setViewportTransform({ x: 0, y: 0, scale: 1 }); } if ((e.key === "+" || e.key === "=") && (e.ctrlKey || e.metaKey)) { e.preventDefault(); setViewportTransform((prev) => ({ ...prev, scale: Math.min(maxZoom, prev.scale + .1) })); } if (e.key === "-" && (e.ctrlKey || e.metaKey)) { e.preventDefault(); setViewportTransform((prev) => ({ ...prev, scale: Math.max(minZoom, prev.scale - .1) })); } if (e.altKey && [ "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight" ].includes(e.key)) { e.preventDefault(); const panAmount = e.shiftKey ? 50 : 20; setViewportTransform((prev) => { let dx = 0, dy = 0; if (e.key === "ArrowLeft") dx = panAmount; if (e.key === "ArrowRight") dx = -panAmount; if (e.key === "ArrowUp") dy = panAmount; if (e.key === "ArrowDown") dy = -panAmount; return { ...prev, x: prev.x + dx, y: prev.y + dy }; }); } }; window.addEventListener("keydown", handleKeyDown); return () => { window.removeEventListener("keydown", handleKeyDown); }; }, [ isPanning, maxZoom, minZoom ]); const [showHelp, setShowHelp] = useState(true); useEffect(() => { if (showHelp) { const timer = setTimeout(() => { setShowHelp(false); }, 15e3); return () => clearTimeout(timer); } }, [showHelp]); const getCursorStyle = () => { if (isPanning) return "grabbing"; if (isMouseDownRef.current) return "grabbing"; if (wireDrawing.isDrawing) return "crosshair"; return "default"; }; const getGridSize = () => { const scaledGridSize = gridSize * (1 + (1 - Math.min(viewportTransform.scale, 1)) * 2); return scaledGridSize; }; return /* @__PURE__ */ jsxs( "svg", // Reduced for smoother curves // Prevent page scroll // Faster panning with Shift { ref: actualRef, width, height, style: { backgroundColor: "#111111", userSelect: "none", cursor: getCursorStyle(), overflow: "hidden" }, onClick: handleCanvasClick, onMouseDown: handleCanvasMouseDown, onMouseMove: handleMouseMove, onMouseUp: handleMouseUp, onMouseLeave: handleMouseLeave, onWheel: handleWheel, onDragOver: handleDragOver, onDrop: handleDrop, "data-circuit-canvas": true, children: [ /* @__PURE__ */ jsxs("defs", { children: [ /* @__PURE__ */ jsxs("pattern", { id: "grid", width: getGridSize(), height: getGridSize(), patternUnits: "userSpaceOnUse", patternTransform: `translate(${viewportTransform.x % getGridSize()},${viewportTransform.y % getGridSize()}) scale(${viewportTransform.scale})`, children: [/* @__PURE__ */ jsx("rect", { width: getGridSize(), height: getGridSize(), fill: "#111111" }), /* @__PURE__ */ jsx("circle", { cx: getGridSize(), cy: getGridSize(), r: "0.5", fill: "#222222" })] }), /* @__PURE__ */ jsxs("pattern", { id: "dots", width: getGridSize(), height: getGridSize(), patternUnits: "userSpaceOnUse", patternTransform: `translate(${viewportTransform.x % getGridSize()},${viewportTransform.y % getGridSize()}) scale(${viewportTransform.scale})`, children: [/* @__PURE__ */ jsx("rect", { width: getGridSize(), height: getGridSize(), fill: "#111111" }), /* @__PURE__ */ jsx("circle", { cx: getGridSize() / 2, cy: getGridSize() / 2, r: "0.7", fill: "#222222" })] }), /* @__PURE__ */ jsxs("pattern", { id: "infinite-grid", width: getGridSize() * 5, height: getGridSize() * 5, patternUnits: "userSpaceOnUse", patternTransform: `translate(${viewportTransform.x % (getGridSize() * 5)},${viewportTransform.y % (getGridSize() * 5)}) scale(${viewportTransform.scale})`, children: [ /* @__PURE__ */ jsx("rect", { width: getGridSize() * 5, height: getGridSize() * 5, fill: "#111111" }), /* @__PURE__ */ jsx("path", { 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}`, stroke: "#222222", strokeWidth: "0.3" }), /* @__PURE__ */ jsx("path", { 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}`, stroke: "#333333", strokeWidth: "0.5" }) ] }) ] }), showGrid && /* @__PURE__ */ jsx("rect", { width: "100%", height: "100%", fill: "url(#infinite-grid)" }), /* @__PURE__ */ jsxs("g", { transform: `translate(${viewportTransform.x}, ${viewportTransform.y}) scale(${viewportTransform.scale})`, children: [/* @__PURE__ */ jsxs("g", { className: "circuit-wires", children: [wires.map((wire) => /* @__PURE__ */ jsx(WirePath_default, { wire, components, selected: selectedWireIds.includes(wire.id), onClick: (e) => handleWireClick(wire.id, e) }, wire.id)), wireDrawing.isDrawing && sourcePortPos && currentMousePos && /* @__PURE__ */ jsx("path", { d: generateWirePath(sourcePortPos, currentMousePos), stroke: "#4f8ef7", strokeWidth: 2, strokeDasharray: "5,5", fill: "none", className: "wire-preview", style: { pointerEvents: "visible" } })] }), /* @__PURE__ */ jsx("g", { className: "circuit-components", children: components.map((component) => /* @__PURE__ */ jsx(Brick_default, { component, selected: selectedComponentIds.includes(component.id), onClick: (e) => handleComponentClick(component.id, e), onMouseDown: (e) => handleMouseDown(component.id, e), onPortClick: handlePortClick }, component.id)) })] }), /* @__PURE__ */ jsx("text", { x: "10", y: "20", fill: "#666", fontSize: "12", pointerEvents: "none", children: `Position: (${Math.round(viewportTransform.x)}, ${Math.round(viewportTransform.y)}) | Zoom: ${viewportTransform.scale.toFixed(2)}` }), /* @__PURE__ */ jsxs("g", { className: "circuit-minimap", transform: `translate(10, ${typeof height === "number" ? height - 120 : "calc(100% - 120px)"})`, children: [ /* @__PURE__ */ jsx("rect", { x: "0", y: "0", width: "110", height: "110", fill: "#222", fillOpacity: "0.7", stroke: "#444", strokeWidth: "1", rx: "4" }), /* @__PURE__ */ jsx("rect", { x: 5, y: 5, width: 100, height: 100, fill: "none", stroke: "#333", strokeWidth: "1" }), /* @__PURE__ */ jsxs("g", { transform: "scale(0.1) translate(50, 50)", children: [components.map((component) => /* @__PURE__ */ jsx("circle", { cx: component.position.x, cy: component.position.y, r: 15, fill: selectedComponentIds.includes(component.id) ? "#4f8ef7" : "#666" }, component.id)), /* @__PURE__ */ jsx("rect", { x: -viewportTransform.x / viewportTransform.scale, y: -viewportTransform.y / viewportTransform.scale, width: typeof width === "number" ? width / viewportTransform.scale : 1e3 / viewportTransform.scale, height: typeof height === "number" ? height / viewportTransform.scale : 800 / viewportTransform.scale, fill: "none", stroke: "#4f8ef7", strokeWidth: "10", strokeDasharray: "20,10" })] }), /* @__PURE__ */ jsx("rect", { x: "85", y: "0", width: "25", height: "25", fill: "#333", rx: "4", style: { cursor: "pointer" }, onClick: () => { const minimapEl = document.querySelector(".circuit-minimap"); if (minimapEl) minimapEl.setAttribute("style", "display: none"); } }), /* @__PURE__ */ jsx("text", { x: "93", y: "17", fill: "#fff", fontSize: "16", style: { cursor: "pointer" }, children: "×" }) ] }), showHelp && /* @__PURE__ */ jsxs("g", { transform: `translate(${typeof width === "number" ? width / 2 - 150 : "50%"}, 30)`, children: [ /* @__PURE__ */ jsx("rect", { x: "-10", y: "-10", width: "320", height: "85", rx: "8", fill: "rgba(0, 0, 0, 0.7)", stroke: "#4f8ef7", strokeWidth: "1" }), /* @__PURE__ */ jsx("text", { x: "0", y: "15", fill: "#fff", fontSize: "14", fontWeight: "bold", children: "Infinite Canvas Controls:" }), /* @__PURE__ */ jsx("text", { x: "0", y: "35", fill: "#eee", fontSize: "12", children: "• Alt + drag/middle mouse: Pan canvas" }), /* @__PURE__ */ jsx("text", { x: "0", y: "50", fill: "#eee", fontSize: "12", children: "• Ctrl/⌘ + scroll: Zoom in/out" }), /* @__PURE__ */ jsx("text", { x: "0", y: "65", fill: "#eee", fontSize: "12", children: "• Ctrl/⌘ + 0: Reset view" }), /* @__PURE__ */ jsx("rect", { x: "290", y: "-10", width: "20", height: "20", rx: "4", fill: "rgba(100, 100, 100, 0.5)", cursor: "pointer", onClick: () => setShowHelp(false) }), /* @__PURE__ */ jsx("text", { x: "296", y: "5", fill: "#fff", fontSize: "16", cursor: "pointer", onClick: () => setShowHelp(false), children: "×" }) ] }), /* @__PURE__ */ jsxs("g", { transform: "translate(10, 10)", style: { opacity: .7 }, onMouseEnter: (e) => e.currentTarget.style.opacity = "1", onMouseLeave: (e) => e.currentTarget.style.opacity = "0.7", children: [ /* @__PURE__ */ jsx("rect", { x: "0", y: "0", width: "80", height: "22", rx: "4", fill: "#222", fillOpacity: "0.8" }), /* @__PURE__ */ jsxs("g", { transform: "translate(10, 11)", cursor: "pointer", onClick: () => setViewportTransform((prev) => ({ ...prev, scale: Math.min(maxZoom, prev.scale + .1) })), children: [/* @__PURE__ */ jsx("circle", { r: "8", fill: "#333" }), /* @__PURE__ */ jsx("path", { d: "M-4,0 H4 M0,-4 V4", stroke: "#aaa", strokeWidth: "1.5" })] }), /* @__PURE__ */ jsxs("g", { transform: "translate(40, 11)", cursor: "pointer", onClick: () => setViewportTransform((prev) => ({ ...prev, scale: Math.max(minZoom, prev.scale - .1) })), children: [/* @__PURE__ */ jsx("circle", { r: "8", fill: "#333" }), /* @__PURE__ */ jsx("path", { d: "M-4,0 H4", stroke: "#aaa", strokeWidth: "1.5" })] }), /* @__PURE__ */ jsxs("g", { transform: "translate(70, 11)", cursor: "pointer", onClick: () => setViewportTransform({ x: 0, y: 0, scale: 1 }), children: [/* @__PURE__ */ jsx("circle", { r: "8", fill: "#333" }), /* @__PURE__ */ jsx("path", { d: "M-3,-3 L3,3 M-3,3 L3,-3", stroke: "#aaa", strokeWidth: "1.5" })] }) ] }) ] } ); }); CircuitCanvas.displayName = "CircuitCanvas"; var CircuitCanvas_default = CircuitCanvas; //#endregion export { BaseComponent_default, Brick_default, CircuitCanvas, CircuitCanvas_default, Port_default, WirePath_default, getPortPosition }; //# sourceMappingURL=CircuitCanvas-DnwkfblW.mjs.map