diff --git a/examples/example21-connected-trace-hover.fixture.tsx b/examples/example21-connected-trace-hover.fixture.tsx new file mode 100644 index 0000000..9a6b79f --- /dev/null +++ b/examples/example21-connected-trace-hover.fixture.tsx @@ -0,0 +1,62 @@ +import type { CircuitJson } from "circuit-json" +import { ControlledSchematicViewer } from "lib/components/ControlledSchematicViewer" + +const circuitJson = [ + { + type: "source_port", + source_port_id: "source_port_0", + subcircuit_connectivity_map_key: "hover_net_a", + }, + { + type: "source_port", + source_port_id: "source_port_1", + subcircuit_connectivity_map_key: "hover_net_a", + }, + { + type: "schematic_port", + schematic_port_id: "schematic_port_0", + source_port_id: "source_port_0", + center: { x: 0, y: 0 }, + }, + { + type: "schematic_port", + schematic_port_id: "schematic_port_1", + source_port_id: "source_port_1", + center: { x: 4, y: 0 }, + }, + { + type: "schematic_trace", + schematic_trace_id: "trace_0", + edges: [{ from: { x: 0, y: 0 }, to: { x: 1, y: 0 } }], + junctions: [{ x: 1, y: 0 }], + }, + { + type: "schematic_trace", + schematic_trace_id: "trace_1", + edges: [{ from: { x: 1, y: 0 }, to: { x: 3, y: 0 } }], + junctions: [ + { x: 1, y: 0 }, + { x: 3, y: 0 }, + ], + }, + { + type: "schematic_trace", + schematic_trace_id: "trace_2", + edges: [{ from: { x: 3, y: 0 }, to: { x: 4, y: 0 } }], + junctions: [{ x: 3, y: 0 }], + }, + { + type: "schematic_trace", + schematic_trace_id: "trace_3", + edges: [{ from: { x: 0, y: 2 }, to: { x: 4, y: 2 } }], + junctions: [], + }, +] as CircuitJson + +export default () => ( + +) diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index ab4fd20..bd20322 100644 --- a/lib/components/SchematicViewer.tsx +++ b/lib/components/SchematicViewer.tsx @@ -1,10 +1,12 @@ +import { su } from "@tscircuit/soup-util" +import type { CircuitJson } from "circuit-json" import { - convertCircuitJsonToSchematicSvg, type ColorOverrides, + convertCircuitJsonToSchematicSvg, } from "circuit-to-svg" -import { su } from "@tscircuit/soup-util" import { useChangeSchematicComponentLocationsInSvg } from "lib/hooks/useChangeSchematicComponentLocationsInSvg" import { useChangeSchematicTracesForMovedComponents } from "lib/hooks/useChangeSchematicTracesForMovedComponents" +import { getStoredBoolean, setStoredBoolean } from "lib/hooks/useLocalStorage" import { useSchematicGroupsOverlay } from "lib/hooks/useSchematicGroupsOverlay" import { enableDebug } from "lib/utils/debug" import { useCallback, useEffect, useMemo, useRef, useState } from "react" @@ -16,21 +18,20 @@ import { import { useMouseMatrixTransform } from "use-mouse-matrix-transform" import { useResizeHandling } from "../hooks/use-resize-handling" import { useComponentDragging } from "../hooks/useComponentDragging" +import { useSpiceSimulation } from "../hooks/useSpiceSimulation" import type { ManualEditEvent } from "../types/edit-events" +import { addConnectivityKeysToSchematicTraces } from "../utils/getConnectedSchematicTraceGroups" +import { getSpiceFromCircuitJson } from "../utils/spice-utils" +import { zIndexMap } from "../utils/z-index-map" import { EditIcon } from "./EditIcon" import { GridIcon } from "./GridIcon" -import { ViewMenuIcon } from "./ViewMenuIcon" -import { ViewMenu } from "./ViewMenu" -import type { CircuitJson } from "circuit-json" -import { SpiceSimulationIcon } from "./SpiceSimulationIcon" -import { SpiceSimulationOverlay } from "./SpiceSimulationOverlay" -import { zIndexMap } from "../utils/z-index-map" -import { useSpiceSimulation } from "../hooks/useSpiceSimulation" -import { getSpiceFromCircuitJson } from "../utils/spice-utils" -import { getStoredBoolean, setStoredBoolean } from "lib/hooks/useLocalStorage" import { MouseTracker } from "./MouseTracker" import { SchematicComponentMouseTarget } from "./SchematicComponentMouseTarget" import { SchematicPortMouseTarget } from "./SchematicPortMouseTarget" +import { SpiceSimulationIcon } from "./SpiceSimulationIcon" +import { SpiceSimulationOverlay } from "./SpiceSimulationOverlay" +import { ViewMenu } from "./ViewMenu" +import { ViewMenuIcon } from "./ViewMenuIcon" interface Props { circuitJson: CircuitJson @@ -258,11 +259,16 @@ export const SchematicViewer = ({ enabled: isInteractionEnabled && !showSpiceOverlay, }) + const augmentedCircuitJson = useMemo( + () => addConnectivityKeysToSchematicTraces(circuitJson), + [circuitJsonKey, circuitJson], + ) + const { containerWidth, containerHeight } = useResizeHandling(containerRef) const svgString = useMemo(() => { if (!containerWidth || !containerHeight) return "" - return convertCircuitJsonToSchematicSvg(circuitJson as any, { + return convertCircuitJsonToSchematicSvg(augmentedCircuitJson as any, { width: containerWidth, height: containerHeight || 720, drawPorts: showSchematicPorts, @@ -276,6 +282,7 @@ export const SchematicViewer = ({ }) }, [ circuitJsonKey, + augmentedCircuitJson, containerWidth, containerHeight, showGrid, @@ -398,12 +405,14 @@ export const SchematicViewer = ({ {onSchematicComponentClicked && ( )} {onSchematicPortClicked && ( )}
{ + const x = Number(point.x.toFixed(POINT_PRECISION)) + const y = Number(point.y.toFixed(POINT_PRECISION)) + return `${x},${y}` +} + +const sortUnique = (items: Iterable) => + Array.from(new Set(items)).sort() + +const getTracePointKeys = (trace: SchematicTrace) => { + const pointKeys = new Set() + + for (const edge of trace.edges ?? []) { + if (edge.from) pointKeys.add(toPointKey(edge.from)) + if (edge.to) pointKeys.add(toPointKey(edge.to)) + } + + for (const junction of trace.junctions ?? []) { + pointKeys.add(toPointKey(junction)) + } + + return pointKeys +} + +export const getConnectedSchematicTraceGroups = ( + circuitJson: CircuitJson, +): Record => { + const groupKeysByTraceId = getSchematicTraceGroupKeys(circuitJson) + const traceIdsByGroupKey = new Map() + + for (const [traceId, groupKey] of Object.entries(groupKeysByTraceId)) { + if (!traceIdsByGroupKey.has(groupKey)) { + traceIdsByGroupKey.set(groupKey, []) + } + traceIdsByGroupKey.get(groupKey)?.push(traceId) + } + + const groupsByTraceId: Record = {} + for (const [traceId, groupKey] of Object.entries(groupKeysByTraceId)) { + groupsByTraceId[traceId] = sortUnique( + traceIdsByGroupKey.get(groupKey) ?? [], + ) + } + + return groupsByTraceId +} + +export const getSchematicTraceGroupKeys = ( + circuitJson: CircuitJson, +): Record => { + const schematicTraces = su( + circuitJson, + ).schematic_trace.list() as SchematicTrace[] + const schematicPorts = su(circuitJson).schematic_port.list() + const sourcePorts = su(circuitJson).source_port.list() + + const connectivityKeysByPoint = new Map>() + const sourcePortById = new Map( + sourcePorts.map((sourcePort) => [ + sourcePort.source_port_id as string, + sourcePort as SourcePort, + ]), + ) + + for (const schematicPort of schematicPorts) { + if (!schematicPort.center || !schematicPort.source_port_id) continue + + const sourcePort = sourcePortById.get(schematicPort.source_port_id) + const connectivityKey = sourcePort?.subcircuit_connectivity_map_key + if (!connectivityKey) continue + + const pointKey = toPointKey(schematicPort.center) + if (!connectivityKeysByPoint.has(pointKey)) { + connectivityKeysByPoint.set(pointKey, new Set()) + } + connectivityKeysByPoint.get(pointKey)?.add(connectivityKey) + } + + const traceIdsByPoint = new Map>() + const pointKeysByTraceId = new Map() + const schematicTraceById = new Map() + + for (const trace of schematicTraces) { + const traceId = trace.schematic_trace_id + if (!traceId) continue + + const pointKeys = sortUnique(getTracePointKeys(trace)) + schematicTraceById.set(traceId, trace) + pointKeysByTraceId.set(traceId, pointKeys) + + for (const pointKey of pointKeys) { + if (!traceIdsByPoint.has(pointKey)) { + traceIdsByPoint.set(pointKey, new Set()) + } + traceIdsByPoint.get(pointKey)?.add(traceId) + } + } + + const groupKeysByTraceId: Record = {} + const visitedTraceIds = new Set() + let fallbackGroupIndex = 0 + + for (const trace of schematicTraces) { + const startTraceId = trace.schematic_trace_id + if (!startTraceId || visitedTraceIds.has(startTraceId)) continue + + const queue = [startTraceId] + const connectedTraceIds = new Set() + const componentConnectivityKeys = new Set() + + while (queue.length > 0) { + const currentTraceId = queue.shift() + if (!currentTraceId || visitedTraceIds.has(currentTraceId)) continue + + visitedTraceIds.add(currentTraceId) + connectedTraceIds.add(currentTraceId) + + const currentTrace = schematicTraceById.get(currentTraceId) + if (currentTrace?.subcircuit_connectivity_map_key) { + componentConnectivityKeys.add( + currentTrace.subcircuit_connectivity_map_key, + ) + } + + for (const pointKey of pointKeysByTraceId.get(currentTraceId) ?? []) { + for (const connectivityKey of connectivityKeysByPoint.get(pointKey) ?? + []) { + componentConnectivityKeys.add(connectivityKey) + } + + for (const neighborTraceId of traceIdsByPoint.get(pointKey) ?? []) { + if (!visitedTraceIds.has(neighborTraceId)) { + queue.push(neighborTraceId) + } + } + } + } + + if (componentConnectivityKeys.size > 1) { + console.warn( + "Multiple connectivity keys found for schematic trace group", + sortUnique(componentConnectivityKeys), + sortUnique(connectedTraceIds), + ) + } + + const groupKey = + sortUnique(componentConnectivityKeys)[0] ?? + `${FALLBACK_GROUP_PREFIX}_${fallbackGroupIndex++}` + + for (const traceId of connectedTraceIds) { + groupKeysByTraceId[traceId] = groupKey + } + } + + return groupKeysByTraceId +} + +export const addConnectivityKeysToSchematicTraces = ( + circuitJson: CircuitJson, +): CircuitJson => { + const groupKeysByTraceId = getSchematicTraceGroupKeys(circuitJson) + + return circuitJson.map((entry) => { + if (entry.type !== "schematic_trace" || !entry.schematic_trace_id) { + return entry + } + + const subcircuitConnectivityMapKey = + groupKeysByTraceId[entry.schematic_trace_id] ?? + entry.subcircuit_connectivity_map_key + + if (!subcircuitConnectivityMapKey) { + return entry + } + + return { + ...entry, + subcircuit_connectivity_map_key: subcircuitConnectivityMapKey, + } + }) as CircuitJson +} diff --git a/tests/connected-schematic-trace-groups.test.tsx b/tests/connected-schematic-trace-groups.test.tsx new file mode 100644 index 0000000..242f2cb --- /dev/null +++ b/tests/connected-schematic-trace-groups.test.tsx @@ -0,0 +1,171 @@ +import { describe, expect, test } from "bun:test" +import type { CircuitJson } from "circuit-json" +import { renderToCircuitJson } from "../lib/dev/render-to-circuit-json" +import { + addConnectivityKeysToSchematicTraces, + getConnectedSchematicTraceGroups, + getSchematicTraceGroupKeys, +} from "../lib/utils/getConnectedSchematicTraceGroups" + +describe("getConnectedSchematicTraceGroups", () => { + test("copies source-port connectivity keys onto rendered schematic traces", () => { + const circuitJson = renderToCircuitJson( + + + + + , + ) + + const augmentedCircuitJson = + addConnectivityKeysToSchematicTraces(circuitJson) + const schematicTrace = augmentedCircuitJson.find( + (entry: any) => entry.type === "schematic_trace", + ) as any + const sourceTrace = augmentedCircuitJson.find( + (entry: any) => entry.type === "source_trace", + ) as any + + expect(schematicTrace.subcircuit_connectivity_map_key).toBe( + sourceTrace.subcircuit_connectivity_map_key, + ) + }) + + test("groups chain-connected trace segments under the same source net key", () => { + const circuitJson = [ + { + type: "source_port", + source_port_id: "source_port_0", + subcircuit_connectivity_map_key: "net_a", + }, + { + type: "source_port", + source_port_id: "source_port_1", + subcircuit_connectivity_map_key: "net_a", + }, + { + type: "schematic_port", + schematic_port_id: "schematic_port_0", + source_port_id: "source_port_0", + center: { x: 0, y: 0 }, + }, + { + type: "schematic_port", + schematic_port_id: "schematic_port_1", + source_port_id: "source_port_1", + center: { x: 3, y: 0 }, + }, + { + type: "schematic_trace", + schematic_trace_id: "trace_0", + edges: [{ from: { x: 0, y: 0 }, to: { x: 1, y: 0 } }], + junctions: [{ x: 1, y: 0 }], + }, + { + type: "schematic_trace", + schematic_trace_id: "trace_1", + edges: [{ from: { x: 1, y: 0 }, to: { x: 2, y: 0 } }], + junctions: [ + { x: 1, y: 0 }, + { x: 2, y: 0 }, + ], + }, + { + type: "schematic_trace", + schematic_trace_id: "trace_2", + edges: [{ from: { x: 2, y: 0 }, to: { x: 3, y: 0 } }], + junctions: [{ x: 2, y: 0 }], + }, + ] as CircuitJson + + expect(getSchematicTraceGroupKeys(circuitJson)).toEqual({ + trace_0: "net_a", + trace_1: "net_a", + trace_2: "net_a", + }) + + expect(getConnectedSchematicTraceGroups(circuitJson)).toEqual({ + trace_0: ["trace_0", "trace_1", "trace_2"], + trace_1: ["trace_0", "trace_1", "trace_2"], + trace_2: ["trace_0", "trace_1", "trace_2"], + }) + }) + + test("groups disconnected trace segments that share the same port net key", () => { + const circuitJson = [ + { + type: "source_port", + source_port_id: "source_port_0", + subcircuit_connectivity_map_key: "net_b", + }, + { + type: "source_port", + source_port_id: "source_port_1", + subcircuit_connectivity_map_key: "net_b", + }, + { + type: "schematic_port", + schematic_port_id: "schematic_port_0", + source_port_id: "source_port_0", + center: { x: 0, y: 0 }, + }, + { + type: "schematic_port", + schematic_port_id: "schematic_port_1", + source_port_id: "source_port_1", + center: { x: 10, y: 0 }, + }, + { + type: "schematic_trace", + schematic_trace_id: "trace_0", + edges: [{ from: { x: 0, y: 0 }, to: { x: 1, y: 0 } }], + junctions: [], + }, + { + type: "schematic_trace", + schematic_trace_id: "trace_1", + edges: [{ from: { x: 10, y: 0 }, to: { x: 11, y: 0 } }], + junctions: [], + }, + ] as CircuitJson + + expect(getSchematicTraceGroupKeys(circuitJson)).toEqual({ + trace_0: "net_b", + trace_1: "net_b", + }) + + expect(getConnectedSchematicTraceGroups(circuitJson)).toEqual({ + trace_0: ["trace_0", "trace_1"], + trace_1: ["trace_0", "trace_1"], + }) + }) + + test("falls back to geometric grouping when net metadata is missing", () => { + const circuitJson = [ + { + type: "schematic_trace", + schematic_trace_id: "trace_0", + edges: [{ from: { x: 0, y: 0 }, to: { x: 1, y: 0 } }], + junctions: [{ x: 1, y: 0 }], + }, + { + type: "schematic_trace", + schematic_trace_id: "trace_1", + edges: [{ from: { x: 1, y: 0 }, to: { x: 2, y: 0 } }], + junctions: [{ x: 1, y: 0 }], + }, + { + type: "schematic_trace", + schematic_trace_id: "trace_2", + edges: [{ from: { x: 10, y: 0 }, to: { x: 11, y: 0 } }], + junctions: [], + }, + ] as CircuitJson + + const groupKeys = getSchematicTraceGroupKeys(circuitJson) + + expect(groupKeys.trace_0).toBe(groupKeys.trace_1) + expect(groupKeys.trace_0).toMatch(/^schematic_trace_group_/) + expect(groupKeys.trace_2).not.toBe(groupKeys.trace_0) + }) +})