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
JavaScript
"use client";
import { getComponentSchema } from "./registry-DltAe79Q.mjs";
import { isBrowser, safeQuerySelector } from "./ssrUtils-Bmt4TsFY.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-BDeahvYS.mjs.map