circuit-bricks
Version:
A modular, Lego-style SVG circuit component system for React (ALPHA - Not for production use)
1,761 lines (1,741 loc) • 119 kB
JavaScript
"use strict";
//#region rolldown:runtime
var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
key = keys[i];
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
get: ((k) => from[k]).bind(null, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
});
}
return to;
};
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
value: mod,
enumerable: true
}) : target, mod));
//#endregion
const react = __toESM(require("react"));
const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
//#region src/registry/components/resistor.json
var id$9 = "resistor";
var name$9 = "Resistor";
var category$9 = "passive";
var description$9 = "A passive two-terminal electrical component that implements electrical resistance";
var defaultWidth$9 = 80;
var defaultHeight$9 = 30;
var ports$9 = [{
"id": "left",
"x": 0,
"y": 15,
"type": "inout"
}, {
"id": "right",
"x": 80,
"y": 15,
"type": "inout"
}];
var properties$9 = [{
"key": "resistance",
"label": "Resistance",
"type": "number",
"unit": "Ω",
"default": 1e3,
"min": 0
}, {
"key": "tolerance",
"label": "Tolerance",
"type": "number",
"unit": "%",
"default": 5,
"min": 0,
"max": 20
}];
var svgPath$9 = "M10,15 h10 l5,-10 l10,20 l10,-20 l10,20 l10,-20 l5,10 h10";
var resistor_default = {
id: id$9,
name: name$9,
category: category$9,
description: description$9,
defaultWidth: defaultWidth$9,
defaultHeight: defaultHeight$9,
ports: ports$9,
properties: properties$9,
svgPath: svgPath$9
};
//#endregion
//#region src/registry/components/capacitor.json
var id$8 = "capacitor";
var name$8 = "Capacitor";
var category$8 = "passive";
var description$8 = "A passive two-terminal electrical component that stores electrical energy in an electric field";
var defaultWidth$8 = 60;
var defaultHeight$8 = 40;
var ports$8 = [{
"id": "left",
"x": 0,
"y": 20,
"type": "inout"
}, {
"id": "right",
"x": 60,
"y": 20,
"type": "inout"
}];
var properties$8 = [{
"key": "capacitance",
"label": "Capacitance",
"type": "number",
"unit": "μF",
"default": 10,
"min": 0
}, {
"key": "voltageRating",
"label": "Voltage Rating",
"type": "number",
"unit": "V",
"default": 50,
"min": 0
}];
var svgPath$8 = "M10,20 h15 M35,5 v30 M45,5 v30 M45,20 h15";
var capacitor_default = {
id: id$8,
name: name$8,
category: category$8,
description: description$8,
defaultWidth: defaultWidth$8,
defaultHeight: defaultHeight$8,
ports: ports$8,
properties: properties$8,
svgPath: svgPath$8
};
//#endregion
//#region src/registry/components/battery.json
var id$7 = "battery";
var name$7 = "Battery";
var category$7 = "sources";
var description$7 = "A source of electrical energy that can be used to power circuits";
var defaultWidth$7 = 60;
var defaultHeight$7 = 40;
var ports$7 = [{
"id": "positive",
"x": 60,
"y": 20,
"type": "output"
}, {
"id": "negative",
"x": 0,
"y": 20,
"type": "output"
}];
var properties$7 = [{
"key": "voltage",
"label": "Voltage",
"type": "number",
"unit": "V",
"default": 9,
"min": 0
}, {
"key": "internal_resistance",
"label": "Internal Resistance",
"type": "number",
"unit": "Ω",
"default": .1,
"min": 0
}];
var svgPath$7 = "M10,20 h10 M20,5 v30 M30,10 v20 M40,5 v30 M40,20 h10";
var battery_default = {
id: id$7,
name: name$7,
category: category$7,
description: description$7,
defaultWidth: defaultWidth$7,
defaultHeight: defaultHeight$7,
ports: ports$7,
properties: properties$7,
svgPath: svgPath$7
};
//#endregion
//#region src/registry/components/ground.json
var id$6 = "ground";
var name$6 = "Ground";
var category$6 = "basic";
var description$6 = "A reference point in an electrical circuit from which voltages are measured";
var defaultWidth$6 = 30;
var defaultHeight$6 = 30;
var ports$6 = [{
"id": "terminal",
"x": 15,
"y": 0,
"type": "inout"
}];
var properties$6 = [];
var svgPath$6 = "M15,0 v15 M5,15 h20 M8,20 h14 M11,25 h8";
var ground_default = {
id: id$6,
name: name$6,
category: category$6,
description: description$6,
defaultWidth: defaultWidth$6,
defaultHeight: defaultHeight$6,
ports: ports$6,
properties: properties$6,
svgPath: svgPath$6
};
//#endregion
//#region src/registry/components/switch.json
var id$5 = "switch";
var name$5 = "Switch";
var category$5 = "basic";
var description$5 = "A component that can be open or closed to control current flow";
var defaultWidth$5 = 60;
var defaultHeight$5 = 30;
var ports$5 = [{
"id": "left",
"x": 0,
"y": 15,
"type": "inout"
}, {
"id": "right",
"x": 60,
"y": 15,
"type": "inout"
}];
var properties$5 = [{
"key": "state",
"label": "State",
"type": "select",
"options": [{
"value": "open",
"label": "Open"
}, {
"value": "closed",
"label": "Closed"
}],
"default": "open"
}];
var svgPath$5 = "M10,15 h10 M20,15 l20,-10 M40,15 h10";
var switch_default = {
id: id$5,
name: name$5,
category: category$5,
description: description$5,
defaultWidth: defaultWidth$5,
defaultHeight: defaultHeight$5,
ports: ports$5,
properties: properties$5,
svgPath: svgPath$5
};
//#endregion
//#region src/registry/components/voltage-source.json
var id$4 = "voltage-source";
var name$4 = "Voltage Source";
var category$4 = "sources";
var description$4 = "An ideal voltage source with constant voltage";
var defaultWidth$4 = 50;
var defaultHeight$4 = 50;
var ports$4 = [{
"id": "positive",
"x": 25,
"y": 0,
"type": "output"
}, {
"id": "negative",
"x": 25,
"y": 50,
"type": "output"
}];
var properties$4 = [
{
"key": "voltage",
"label": "Voltage",
"type": "number",
"unit": "V",
"default": 5,
"min": 0
},
{
"key": "frequency",
"label": "Frequency",
"type": "number",
"unit": "Hz",
"default": 0,
"min": 0
},
{
"key": "sourceType",
"label": "Source Type",
"type": "select",
"options": [{
"value": "dc",
"label": "DC"
}, {
"value": "ac",
"label": "AC"
}],
"default": "dc"
}
];
var svgPath$4 = "M25,10 v10 M20,25 h10 M25,30 v10 M25,25 m-15,0 a15,15 0 1,0 30,0 a15,15 0 1,0 -30,0";
var voltage_source_default = {
id: id$4,
name: name$4,
category: category$4,
description: description$4,
defaultWidth: defaultWidth$4,
defaultHeight: defaultHeight$4,
ports: ports$4,
properties: properties$4,
svgPath: svgPath$4
};
//#endregion
//#region src/registry/components/diode.json
var id$3 = "diode";
var name$3 = "Diode";
var category$3 = "semiconductor";
var description$3 = "A semiconductor device that allows current to flow in only one direction";
var defaultWidth$3 = 60;
var defaultHeight$3 = 30;
var ports$3 = [{
"id": "anode",
"x": 0,
"y": 15,
"type": "inout"
}, {
"id": "cathode",
"x": 60,
"y": 15,
"type": "inout"
}];
var properties$3 = [{
"key": "forwardVoltage",
"label": "Forward Voltage",
"type": "number",
"unit": "V",
"default": .7,
"min": 0
}, {
"key": "maxCurrent",
"label": "Max Current",
"type": "number",
"unit": "A",
"default": 1,
"min": 0
}];
var svgPath$3 = "M10,15 h10 M20,5 l0,20 L40,15 L20,5 M40,5 v20 M40,15 h10";
var diode_default = {
id: id$3,
name: name$3,
category: category$3,
description: description$3,
defaultWidth: defaultWidth$3,
defaultHeight: defaultHeight$3,
ports: ports$3,
properties: properties$3,
svgPath: svgPath$3
};
//#endregion
//#region src/registry/components/transistor-npn.json
var id$2 = "transistor-npn";
var name$2 = "NPN Transistor";
var category$2 = "semiconductor";
var description$2 = "An NPN bipolar junction transistor for amplification and switching";
var defaultWidth$2 = 50;
var defaultHeight$2 = 60;
var ports$2 = [
{
"id": "collector",
"x": 25,
"y": 0,
"type": "inout"
},
{
"id": "base",
"x": 0,
"y": 30,
"type": "input"
},
{
"id": "emitter",
"x": 25,
"y": 60,
"type": "inout"
}
];
var properties$2 = [{
"key": "gain",
"label": "Current Gain (β)",
"type": "number",
"default": 100,
"min": 1
}];
var svgPath$2 = "M25,0 v15 M25,45 v15 M0,30 h15 M15,15 v30 M15,30 l20,-10 l0,20 Z";
var transistor_npn_default = {
id: id$2,
name: name$2,
category: category$2,
description: description$2,
defaultWidth: defaultWidth$2,
defaultHeight: defaultHeight$2,
ports: ports$2,
properties: properties$2,
svgPath: svgPath$2
};
//#endregion
//#region src/registry/components/led.json
var id$1 = "led";
var name$1 = "LED";
var category$1 = "semiconductor";
var description$1 = "Light-emitting diode that emits light when current flows through it";
var defaultWidth$1 = 60;
var defaultHeight$1 = 40;
var ports$1 = [{
"id": "anode",
"x": 0,
"y": 20,
"type": "inout"
}, {
"id": "cathode",
"x": 60,
"y": 20,
"type": "inout"
}];
var properties$1 = [
{
"key": "color",
"label": "Color",
"type": "color",
"default": "#ff0000"
},
{
"key": "forwardVoltage",
"label": "Forward Voltage",
"type": "number",
"unit": "V",
"default": 1.8,
"min": 0
},
{
"key": "maxCurrent",
"label": "Max Current",
"type": "number",
"unit": "mA",
"default": 20,
"min": 0
}
];
var svgPath$1 = "M10,20 h10 M20,5 l0,30 L40,20 L20,5 M40,5 v30 M40,20 h10 M25,5 l5,-5 l5,10 l5,-5 M25,10 l5,-5 l5,10 l5,-5";
var led_default = {
id: id$1,
name: name$1,
category: category$1,
description: description$1,
defaultWidth: defaultWidth$1,
defaultHeight: defaultHeight$1,
ports: ports$1,
properties: properties$1,
svgPath: svgPath$1
};
//#endregion
//#region src/registry/components/ic.json
var id = "ic";
var name = "Integrated Circuit";
var category = "advanced";
var description = "A general-purpose integrated circuit with configurable pins";
var defaultWidth = 80;
var defaultHeight = 100;
var ports = [
{
"id": "vcc",
"x": 20,
"y": 0,
"type": "input",
"label": "VCC"
},
{
"id": "gnd",
"x": 60,
"y": 100,
"type": "input",
"label": "GND"
},
{
"id": "in1",
"x": 0,
"y": 20,
"type": "input",
"label": "IN1"
},
{
"id": "in2",
"x": 0,
"y": 40,
"type": "input",
"label": "IN2"
},
{
"id": "in3",
"x": 0,
"y": 60,
"type": "input",
"label": "IN3"
},
{
"id": "in4",
"x": 0,
"y": 80,
"type": "input",
"label": "IN4"
},
{
"id": "out1",
"x": 80,
"y": 20,
"type": "output",
"label": "OUT1"
},
{
"id": "out2",
"x": 80,
"y": 40,
"type": "output",
"label": "OUT2"
},
{
"id": "out3",
"x": 80,
"y": 60,
"type": "output",
"label": "OUT3"
},
{
"id": "out4",
"x": 80,
"y": 80,
"type": "output",
"label": "OUT4"
}
];
var properties = [{
"key": "name",
"label": "IC Name",
"type": "text",
"default": "IC1"
}, {
"key": "type",
"label": "IC Type",
"type": "text",
"default": "Custom IC"
}];
var svgPath = "<svg width='80' height='100'><rect x='10' y='10' width='60' height='80' fill='none' stroke='black' stroke-width='2'/><text x='40' y='50' text-anchor='middle' font-family='monospace' font-size='12'>IC</text><circle cx='20' cy='10' r='3' fill='none' stroke='black'/></svg>";
var ic_default = {
id,
name,
category,
description,
defaultWidth,
defaultHeight,
ports,
properties,
svgPath
};
//#endregion
//#region src/registry/components/index.ts
const registerBuiltInComponents = () => {
registerComponent(resistor_default);
registerComponent(capacitor_default);
registerComponent(ground_default);
registerComponent(switch_default);
registerComponent(battery_default);
registerComponent(voltage_source_default);
registerComponent(diode_default);
registerComponent(transistor_npn_default);
registerComponent(led_default);
registerComponent(ic_default);
};
var components_default = registerBuiltInComponents;
//#endregion
//#region src/registry/index.ts
const componentRegistry = {};
/**
* Register a component schema in the registry
*
* This function adds a new component schema to the registry or updates an existing one.
* Custom components can be registered to extend the library with new circuit elements.
*
* @param {ComponentSchema} schema - The component schema to register
*
* @example
* // Register a custom LED component
* registerComponent({
* id: 'custom-led',
* name: 'Custom LED',
* category: 'output',
* description: 'A light-emitting diode with customizable color',
* defaultWidth: 30,
* defaultHeight: 20,
* ports: [
* { id: 'anode', x: 0, y: 10, type: 'input' },
* { id: 'cathode', x: 30, y: 10, type: 'output' }
* ],
* properties: [
* { id: 'color', name: 'Color', type: 'color', default: '#ff0000' }
* ],
* svgPath: `<circle cx="15" cy="10" r="8" fill="currentColor" />`
* });
*/
function registerComponent(schema) {
if (componentRegistry[schema.id]) console.warn(`Component with ID ${schema.id} already exists in registry. Overwriting.`);
componentRegistry[schema.id] = schema;
}
/**
* Get a component schema from the registry by ID
*
* Retrieves a component's schema definition by its unique identifier.
* Returns undefined if no component with the specified ID exists.
*
* @param {string} id - The unique identifier of the component
* @returns {ComponentSchema | undefined} The component schema or undefined if not found
*
* @example
* const resistorSchema = getComponentSchema('resistor');
* if (resistorSchema) {
* console.log(`Resistor has ${resistorSchema.ports.length} ports`);
* }
*/
function getComponentSchema(id$10) {
return componentRegistry[id$10];
}
/**
* Get all component schemas in the registry
*/
function getAllComponents() {
return Object.values(componentRegistry);
}
/**
* Get component schemas by category
*/
function getComponentsByCategory(category$10) {
return Object.values(componentRegistry).filter((schema) => schema.category === category$10);
}
components_default();
//#endregion
//#region src/core/Port.tsx
const Port = ({ componentId, port, selected = false, onClick }) => {
const handleClick = (event) => {
event.stopPropagation();
onClick?.(port.id, event);
};
return /* @__PURE__ */ (0, react_jsx_runtime.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: id$10, 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__ */ (0, react_jsx_runtime.jsxs)("g", {
transform,
onClick,
onMouseDown,
"data-component-id": id$10,
"data-component-type": component.type,
children: [
/* @__PURE__ */ (0, react_jsx_runtime.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__ */ (0, react_jsx_runtime.jsx)("g", { dangerouslySetInnerHTML: { __html: schema.svgPath } }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
d: schema.svgPath,
fill: "none",
stroke: "black",
strokeWidth: 2,
vectorEffect: "non-scaling-stroke"
})
}),
schema.ports.map((port) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Port_default, {
componentId: id$10,
port,
selected: false,
onClick: (portId, event) => onPortClick?.(id$10, portId, event)
}, port.id)),
selected && /* @__PURE__ */ (0, react_jsx_runtime.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__ */ (0, react_jsx_runtime.jsxs)("g", {
transform: `translate(${component.position.x}, ${component.position.y})`,
onClick,
onMouseDown,
"data-component-id": component.id,
"data-component-type": "unknown",
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
width: 100,
height: 50,
fill: "red",
stroke: "black",
strokeWidth: 1.5,
strokeDasharray: "5,5",
vectorEffect: "non-scaling-stroke"
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("text", {
x: 50,
y: 25,
textAnchor: "middle",
dominantBaseline: "middle",
fill: "white",
fontFamily: "sans-serif",
fontSize: "12px",
children: ["Unknown: ", component.type]
})]
});
}
return /* @__PURE__ */ (0, react_jsx_runtime.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) {
const portElement = document.querySelector(`[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] = (0, react.useState)({
x: 0,
y: 0,
found: false
});
const [toPos, setToPos] = (0, react.useState)({
x: 0,
y: 0,
found: false
});
const pathRef = (0, react.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);
}
};
(0, react.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]);
(0, react.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 || "#333",
strokeWidth: selected ? 3 : wire.style?.strokeWidth || 2,
strokeDasharray: wire.style?.dashArray || "none",
fill: "none",
vectorEffect: "non-scaling-stroke"
};
return /* @__PURE__ */ (0, react_jsx_runtime.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 = ({ 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 }) => {
const svgRef = (0, react.useRef)(null);
const [dragState, setDragState] = (0, react.useState)({
isDragging: false,
componentId: null,
startPos: null,
currentPos: null
});
const [currentMousePos, setCurrentMousePos] = (0, react.useState)(null);
const [sourcePortPos, setSourcePortPos] = (0, react.useState)(null);
const [viewportTransform, setViewportTransform] = (0, react.useState)({
x: 0,
y: 0,
scale: initialZoom
});
const [isPanning, setIsPanning] = (0, react.useState)(false);
const [panStartPos, setPanStartPos] = (0, react.useState)(null);
const isMouseDownRef = (0, react.useRef)(false);
(0, react.useEffect)(() => {
if (!svgRef.current) return;
const viewportChangeEvent = new CustomEvent("viewport-change", {
detail: viewportTransform,
bubbles: true
});
svgRef.current.dispatchEvent(viewportChangeEvent);
}, [viewportTransform]);
(0, react.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 = (0, react.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 = (0, react.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$10, event) => {
if (!dragState.isDragging && !isPanning) onComponentClick?.(id$10, event);
};
const handleWireClick = (id$10, event) => {
if (!isPanning) onWireClick?.(id$10, 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);
};
(0, react.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] = (0, react.useState)(true);
(0, react.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__ */ (0, react_jsx_runtime.jsxs)(
"svg",
// Reduced for smoother curves
// Prevent page scroll
// Faster panning with Shift
{
ref: svgRef,
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__ */ (0, react_jsx_runtime.jsxs)("defs", { children: [
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("pattern", {
id: "grid",
width: getGridSize(),
height: getGridSize(),
patternUnits: "userSpaceOnUse",
patternTransform: `translate(${viewportTransform.x % getGridSize()},${viewportTransform.y % getGridSize()}) scale(${viewportTransform.scale})`,
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
width: getGridSize(),
height: getGridSize(),
fill: "#111111"
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
cx: getGridSize(),
cy: getGridSize(),
r: "0.5",
fill: "#222222"
})]
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("pattern", {
id: "dots",
width: getGridSize(),
height: getGridSize(),
patternUnits: "userSpaceOnUse",
patternTransform: `translate(${viewportTransform.x % getGridSize()},${viewportTransform.y % getGridSize()}) scale(${viewportTransform.scale})`,
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
width: getGridSize(),
height: getGridSize(),
fill: "#111111"
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
cx: getGridSize() / 2,
cy: getGridSize() / 2,
r: "0.7",
fill: "#222222"
})]
}),
/* @__PURE__ */ (0, react_jsx_runtime.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__ */ (0, react_jsx_runtime.jsx)("rect", {
width: getGridSize() * 5,
height: getGridSize() * 5,
fill: "#111111"
}),
/* @__PURE__ */ (0, react_jsx_runtime.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__ */ (0, react_jsx_runtime.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__ */ (0, react_jsx_runtime.jsx)("rect", {
width: "100%",
height: "100%",
fill: "url(#infinite-grid)"
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
transform: `translate(${viewportTransform.x}, ${viewportTransform.y}) scale(${viewportTransform.scale})`,
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
className: "circuit-wires",
children: [wires.map((wire) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(WirePath_default, {
wire,
components,
selected: selectedWireIds.includes(wire.id),
onClick: (e) => handleWireClick(wire.id, e)
}, wire.id)), wireDrawing.isDrawing && sourcePortPos && currentMousePos && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
d: generateWirePath(sourcePortPos, currentMousePos),
stroke: "#4f8ef7",
strokeWidth: 2,
strokeDasharray: "5,5",
fill: "none",
className: "wire-preview",
style: { pointerEvents: "visible" }
})]
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("g", {
className: "circuit-components",
children: components.map((component) => /* @__PURE__ */ (0, react_jsx_runtime.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))
})]
}),
process.env.NODE_ENV === "development" && /* @__PURE__ */ (0, react_jsx_runtime.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__ */ (0, react_jsx_runtime.jsxs)("g", {
className: "circuit-minimap",
transform: `translate(10, ${typeof height === "number" ? height - 120 : "calc(100% - 120px)"})`,
children: [
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
x: "0",
y: "0",
width: "110",
height: "110",
fill: "#222",
fillOpacity: "0.7",
stroke: "#444",
strokeWidth: "1",
rx: "4"
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
x: 5,
y: 5,
width: 100,
height: 100,
fill: "none",
stroke: "#333",
strokeWidth: "1"
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
transform: "scale(0.1) translate(50, 50)",
children: [components.map((component) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
cx: component.position.x,
cy: component.position.y,
r: 15,
fill: selectedComponentIds.includes(component.id) ? "#4f8ef7" : "#666"
}, component.id)), /* @__PURE__ */ (0, react_jsx_runtime.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__ */ (0, react_jsx_runtime.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__ */ (0, react_jsx_runtime.jsx)("text", {
x: "93",
y: "17",
fill: "#fff",
fontSize: "16",
style: { cursor: "pointer" },
children: "×"
})
]
}),
showHelp && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
transform: `translate(${typeof width === "number" ? width / 2 - 150 : "50%"}, 30)`,
children: [
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("rect", {
x: "-10",
y: "-10",
width: "320",
height: "85",
rx: "8",
fill: "rgba(0, 0, 0, 0.7)",
stroke: "#4f8ef7",
strokeWidth: "1"
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
x: "0",
y: "15",
fill: "#fff",
fontSize: "14",
fontWeight: "bold",
children: "Infinite Canvas Controls:"
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
x: "0",
y: "35",
fill: "#eee",
fontSize: "12",
children: "• Alt + drag/middle mouse: Pan canvas"
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
x: "0",
y: "50",
fill: "#eee",
fontSize: "12",
children: "• Ctrl/⌘ + scroll: Zoom in/out"
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("text", {
x: "0",
y: "65",
fill: "#eee",
fontSize: "12",
children: "• Ctrl/⌘ + 0: Reset view"
}),
/* @__PURE__ */ (0, react_jsx_runtime.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__ */ (0, react_jsx_runtime.jsx)("text", {
x: "296",
y: "5",
fill: "#fff",
fontSize: "16",
cursor: "pointer",
onClick: () => setShowHelp(false),
children: "×"
})
]
}),
/* @__PURE__ */ (0, react_jsx_runtime.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__ */ (0, react_jsx_runtime.jsx)("rect", {
x: "0",
y: "0",
width: "80",
height: "22",
rx: "4",
fill: "#222",
fillOpacity: "0.8"
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
transform: "translate(10, 11)",
cursor: "pointer",
onClick: () => setViewportTransform((prev) => ({
...prev,
scale: Math.min(maxZoom, prev.scale + .1)
})),
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
r: "8",
fill: "#333"
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
d: "M-4,0 H4 M0,-4 V4",
stroke: "#aaa",
strokeWidth: "1.5"
})]
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
transform: "translate(40, 11)",
cursor: "pointer",
onClick: () => setViewportTransform((prev) => ({
...prev,
scale: Math.max(minZoom, prev.scale - .1)
})),
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
r: "8",
fill: "#333"
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
d: "M-4,0 H4",
stroke: "#aaa",
strokeWidth: "1.5"
})]
}),
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("g", {
transform: "translate(70, 11)",
cursor: "pointer",
onClick: () => setViewportTransform({
x: 0,
y: 0,
scale: 1
}),
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("circle", {
r: "8",
fill: "#333"
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
d: "M-3,-3 L3,3 M-3,3 L3,-3",
stroke: "#aaa",
strokeWidth: "1.5"
})]
})
]
})
]
}
);
};
var CircuitCanvas_default = CircuitCanvas;
//#endregion
//#region src/hooks/useCircuit.ts
/**
* React hook for managing circuit state
*
* @param initialState - Optional initial state for the circuit
* @returns A tuple containing the circuit state and action functions
*/
function useCircuit(initialState) {
const undoStack = (0, react.useRef)([]);
const redoStack = (0, react.useRef)([]);
const [state, setState] = (0, react.useState)({
components: initialState?.components || [],
wires: initialState?.wires || [],
selectedComponentIds: initialState?.selectedComponentIds || [],
selectedWireIds: initialState?.selectedWireIds || []
});
const saveStateForUndo = (0, react.useCallback)((prevState) => {
undoStack.current.push(JSON.parse(JSON.stringify(prevState)));
redoStack.current = [];
}, []);
const setStateWithHistory = (0, react.useCallback)((updater) => {
setState((prev) => {
saveStateForUndo(prev);
return updater(prev);
});
}, [saveStateForUndo]);
const generateId = (0, react.useCallback)((prefix) => {
return `${prefix}-${Math.random().toString(36).substring(2, 11)}`;
}, []);
const addComponent = (0, react.useCallback)((component) => {
const id$10 = generateId("component");
setStateWithHistory((prev) => ({
...prev,
components: [...prev.components, {
...component,
id: id$10
}]
}));
return id$10;
}, [generateId, setStateWithHistory]);
const updateComponent = (0, react.useCallback)((id$10, updates) => {
setStateWithHistory((prev) => ({
...prev,
components: prev.components.map((c) => c.id === id$10 ? {
...c,
...updates
} : c)
}));
}, [setStateWithHistory]);
const removeComponent = (0, react.useCallback)((id$10) => {
setStateWithHistory((prev) => {
const components = prev.components.filter((c) => c.id !== id$10);
const wires = prev.wires.filter((w) => w.from.componentId !== id$10 && w.to.componentId !== id$10);
const selectedComponentIds = prev.selectedComponentIds.filter((selectedId) => selectedId !== id$10);
return {
...prev,
components,
wires,
selectedComponentIds
};
});
}, [setStateWithHistory]);
const addWire = (0, react.useCallback)((from, to, style) => {
const id$10 = generateId("wire");
setStateWithHistory((prev) => ({
...prev,
wires: [...prev.wires, {
id: id$10,
from,
to,
style
}]
}));
return id$10;
}, [generateId, setStateWithHistory]);
const updateWire = (0, react.useCallback)((id$10, updates) => {
setStateWithHistory((prev) => ({
...prev,
wires: prev.wires.map((w) => w.id === id$10 ? {
...w,
...updates
} : w)
}));
}, [setStateWithHistory]);
const removeWire = (0, react.useCallback)((id$10) => {
setStateWithHistory((prev) => ({
...prev,
wires: prev.wires.filter((w) => w.id !== id$10),
selectedWireIds: prev.selectedWireIds.filter((wireId) => wireId !== id$10)
}));
}, [setStateWithHistory]);
const selectComponent = (0, react.useCallback)((id$10, addToSelection = false) => {
setState((prev) => {
const selectedComponentIds = addToSelection ? [...prev.selectedComponentIds, id$10] : [id$10];
return {
...prev,
selectedComponentIds: [...new Set(selectedComponentIds)]
};
});
}, []);
const selectWire = (0, react.useCallback)((id$10, addToSelection = false) => {
setState((prev) => {
const selectedWireIds = addToSelection ? [...prev.selectedWireIds, id$10] : [id$10];
return {
...prev,
selectedWireIds: [...new Set(selectedWireIds)]
};
});
}, []);
const deselectAll = (0, react.useCallback)(() => {
setState((prev) => ({
...prev,
selectedComponentIds: [],
selectedWireIds: []
}));
}, []);
const getSelectedComponents = (0, react.useCallback)(() => {
return state.components.filter((c) => state.selectedComponentIds.includes(c.id));
}, [state.components, state.selectedComponentIds]);
const getSelectedWires = (0, react.useCallback)(() => {
return state.wires.filter((w) => state.selectedWireIds.includes(w.id));
}, [state.wires, state.selectedWireIds]);
const moveComponent = (0, react.useCallback)((id$10, newPosition) => {
setStateWithHistory((prev) => ({
...prev,
components: prev.components.map((c) => c.id === id$10 ? {
...c,
position: newPosition
} : c)
}));
}, [setStateWithHistory]);
const moveSelectedComponents = (0, react.useCallback)((dx, dy) => {
setStateWithHistory((prev) => ({
...prev,
components: prev.components.map((c) => {
if (prev.selectedComponentIds.includes(c.id)) return {
...c,
position: {
x: c.position.x + dx,
y: c.position.y + dy
}
};
return c;
})
}));
}, [setStateWithHistory]);
const rotateComponent = (0, react.useCallback)((id$10, degrees) => {
setStateWithHistory((prev) => ({
...prev,
components: prev.components.map((c) => {
if (c.id === id$10) {
const currentRotation = c.rotation || 0;
const newRotation = (currentRotation + degrees) % 360;
return {
...c,
rotation: newRotation < 0 ? newRotation + 360 : newRotation
};
}
return c;
})
}));
}, [setStateWithHistory]);
const rotateSelectedComponents = (0, react.useCallback)((degrees) => {
setStateWithHistory((prev) => ({
...prev,
components: prev.components.map((c) => {
if (prev.selectedComponentIds.includes(c.id)) {
const currentRotation = c.rotation || 0;
const newRotation = (currentRotation + degrees) % 360;
return {
...c,
rotation: newRotation < 0 ? newRotation + 360 : newRotation
};
}
return c;
})
}));
}, [setStateWithHistory]);
const undo = (0, react.useCallback)(() => {
if (undoStack.current.length > 0) {
redoStack.current.push(JSON.parse(JSON.stringify(state)));
const prevState = undoStack.current.pop();
setState(prevState);
}
}, [state]);
const redo = (0, react.useCallback)(() => {
if (redoStack.current.length > 0) {
undoStack.current.push(JSON.parse(JSON.string