diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index ab4fd20..5ef1bed 100644 --- a/lib/components/SchematicViewer.tsx +++ b/lib/components/SchematicViewer.tsx @@ -31,6 +31,7 @@ import { getStoredBoolean, setStoredBoolean } from "lib/hooks/useLocalStorage" import { MouseTracker } from "./MouseTracker" import { SchematicComponentMouseTarget } from "./SchematicComponentMouseTarget" import { SchematicPortMouseTarget } from "./SchematicPortMouseTarget" +import { getSchematicTraceNetKeyMap } from "../utils/get-schematic-trace-net-keys" interface Props { circuitJson: CircuitJson @@ -303,6 +304,71 @@ export const SchematicViewer = ({ } }, [svgString]) + const schematicTraceNetKeyMap = useMemo( + () => getSchematicTraceNetKeyMap(circuitJson), + [circuitJsonKey, circuitJson], + ) + + useEffect(() => { + const svgRoot = svgDivRef.current + if (!svgRoot) return + + const traceElements = Array.from( + svgRoot.querySelectorAll( + '[data-circuit-json-type="schematic_trace"][data-schematic-trace-id]', + ), + ) + + const traceElementsByNetKey = new Map() + + for (const traceElement of traceElements) { + const schematicTraceId = traceElement.dataset.schematicTraceId + if (!schematicTraceId) continue + + const netKey = schematicTraceNetKeyMap.get(schematicTraceId) + if (!netKey) continue + + traceElement.dataset.schematicNetKey = netKey + if (!traceElementsByNetKey.has(netKey)) { + traceElementsByNetKey.set(netKey, []) + } + traceElementsByNetKey.get(netKey)!.push(traceElement) + } + + const cleanupCallbacks: Array<() => void> = [] + + for (const [netKey, sameNetTraceElements] of traceElementsByNetKey) { + const setSameNetHover = (isHovered: boolean) => { + for (const traceElement of sameNetTraceElements) { + traceElement.toggleAttribute( + "data-tscircuit-same-net-hovered", + isHovered, + ) + } + } + + for (const traceElement of sameNetTraceElements) { + const handleMouseEnter = () => setSameNetHover(true) + const handleMouseLeave = () => setSameNetHover(false) + + traceElement.addEventListener("mouseenter", handleMouseEnter) + traceElement.addEventListener("mouseleave", handleMouseLeave) + cleanupCallbacks.push(() => { + traceElement.removeEventListener("mouseenter", handleMouseEnter) + traceElement.removeEventListener("mouseleave", handleMouseLeave) + traceElement.removeAttribute("data-tscircuit-same-net-hovered") + if (traceElement.dataset.schematicNetKey === netKey) { + delete traceElement.dataset.schematicNetKey + } + }) + } + } + + return () => { + for (const cleanup of cleanupCallbacks) cleanup() + } + }, [svgString, schematicTraceNetKeyMap]) + const handleEditEvent = (event: ManualEditEvent) => { setInternalEditEvents((prev) => [...prev, event]) if (onEditEvent) { @@ -406,6 +472,17 @@ export const SchematicViewer = ({ {`[data-schematic-port-id]:hover { cursor: pointer !important; }`} )} +
{ + const sourceTraceNetKeyMap = getSourceTraceNetKeyMap([ + { + type: "source_trace", + source_trace_id: "source_trace_1", + connected_source_port_ids: ["source_port_1", "source_port_2"], + connected_source_net_ids: [], + }, + { + type: "source_trace", + source_trace_id: "source_trace_2", + connected_source_port_ids: ["source_port_2", "source_port_3"], + connected_source_net_ids: [], + }, + { + type: "source_trace", + source_trace_id: "source_trace_3", + connected_source_port_ids: ["source_port_4", "source_port_5"], + connected_source_net_ids: [], + }, + ] as any) + + expect(sourceTraceNetKeyMap.get("source_trace_1")).toBe( + sourceTraceNetKeyMap.get("source_trace_2"), + ) + expect(sourceTraceNetKeyMap.get("source_trace_1")).not.toBe( + sourceTraceNetKeyMap.get("source_trace_3"), + ) +}) + +test("maps schematic trace ids to their source trace net groups", () => { + const schematicTraceNetKeyMap = getSchematicTraceNetKeyMap([ + { + type: "source_trace", + source_trace_id: "source_trace_1", + connected_source_port_ids: ["source_port_1", "source_port_2"], + connected_source_net_ids: [], + }, + { + type: "source_trace", + source_trace_id: "source_trace_2", + connected_source_port_ids: ["source_port_2", "source_port_3"], + connected_source_net_ids: [], + }, + { + type: "schematic_trace", + schematic_trace_id: "schematic_trace_1", + source_trace_id: "source_trace_1", + }, + { + type: "schematic_trace", + schematic_trace_id: "schematic_trace_2", + source_trace_id: "source_trace_2", + }, + ] as any) + + expect(schematicTraceNetKeyMap.get("schematic_trace_1")).toBe( + schematicTraceNetKeyMap.get("schematic_trace_2"), + ) +} diff --git a/lib/utils/get-schematic-trace-net-keys.ts b/lib/utils/get-schematic-trace-net-keys.ts new file mode 100644 index 0000000..e0ff291 --- /dev/null +++ b/lib/utils/get-schematic-trace-net-keys.ts @@ -0,0 +1,92 @@ +import type { CircuitJson } from "circuit-json" + +class DisjointSet { + private parent = new Map() + + add(id: string) { + if (!this.parent.has(id)) this.parent.set(id, id) + } + + find(id: string): string { + this.add(id) + const parent = this.parent.get(id)! + if (parent === id) return id + const root = this.find(parent) + this.parent.set(id, root) + return root + } + + union(a: string, b: string) { + const rootA = this.find(a) + const rootB = this.find(b) + if (rootA !== rootB) this.parent.set(rootB, rootA) + } +} + +const getStringArray = (value: unknown): string[] => + Array.isArray(value) + ? value.filter((item): item is string => typeof item === "string") + : [] + +export const getSourceTraceNetKeyMap = (circuitJson: CircuitJson) => { + const disjointSet = new DisjointSet() + const sourceTraceIds: string[] = [] + + for (const element of circuitJson as Array>) { + if (element.type !== "source_trace") continue + + const sourceTraceId = element.source_trace_id + if (typeof sourceTraceId !== "string") continue + + sourceTraceIds.push(sourceTraceId) + disjointSet.add(sourceTraceId) + + const connectedIds = [ + ...getStringArray(element.connected_source_port_ids), + ...getStringArray(element.connected_source_net_ids), + ] + + const subcircuitConnectivityMapKey = + typeof element.subcircuit_connectivity_map_key === "string" + ? element.subcircuit_connectivity_map_key + : null + + if (subcircuitConnectivityMapKey) { + connectedIds.push(`subcircuit:${subcircuitConnectivityMapKey}`) + } + + for (const connectedId of connectedIds) { + disjointSet.union(sourceTraceId, connectedId) + } + } + + return new Map( + sourceTraceIds.map((sourceTraceId) => [ + sourceTraceId, + disjointSet.find(sourceTraceId), + ]), + ) +} + +export const getSchematicTraceNetKeyMap = (circuitJson: CircuitJson) => { + const sourceTraceNetKeyMap = getSourceTraceNetKeyMap(circuitJson) + const schematicTraceNetKeyMap = new Map() + + for (const element of circuitJson as Array>) { + if (element.type !== "schematic_trace") continue + + const schematicTraceId = element.schematic_trace_id + const sourceTraceId = element.source_trace_id + if ( + typeof schematicTraceId !== "string" || + typeof sourceTraceId !== "string" + ) { + continue + } + + const netKey = sourceTraceNetKeyMap.get(sourceTraceId) + if (netKey) schematicTraceNetKeyMap.set(schematicTraceId, netKey) + } + + return schematicTraceNetKeyMap +}