diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index ab4fd20..b54110f 100644 --- a/lib/components/SchematicViewer.tsx +++ b/lib/components/SchematicViewer.tsx @@ -1,10 +1,13 @@ +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 { useHighlightConnectedSchematicTraces } from "lib/hooks/useHighlightConnectedSchematicTraces" +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 +19,19 @@ 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 { 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 @@ -345,6 +346,13 @@ export const SchematicViewer = ({ editEvents: editEventsWithUnappliedEditEvents, }) + useHighlightConnectedSchematicTraces({ + svgDivRef, + circuitJson, + circuitJsonKey, + svgContentKey: svgString, + }) + // Add group overlays when enabled useSchematicGroupsOverlay({ svgDivRef, @@ -398,12 +406,14 @@ export const SchematicViewer = ({ {onSchematicComponentClicked && ( )} {onSchematicPortClicked && ( )}
+ value.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + +export const useHighlightConnectedSchematicTraces = ({ + svgDivRef, + circuitJson, + circuitJsonKey, + svgContentKey, +}: { + svgDivRef: React.RefObject + circuitJson: CircuitJson + circuitJsonKey: string + svgContentKey: string +}) => { + const schematicTraceIdsBySourceTraceId = useMemo(() => { + const map = new Map() + + try { + for (const schematicTrace of su(circuitJson).schematic_trace.list()) { + const sourceTraceId = schematicTrace.source_trace_id + if (!sourceTraceId) continue + + const ids = map.get(sourceTraceId) ?? [] + ids.push(schematicTrace.schematic_trace_id) + map.set(sourceTraceId, ids) + } + } catch (err) { + console.error("Failed to derive schematic trace source map", err) + } + + return map + }, [circuitJsonKey, circuitJson]) + + const sourceTraceIdsByNetId = useMemo(() => { + const map = new Map() + + try { + for (const sourceTrace of su(circuitJson).source_trace.list()) { + for (const sourceNetId of sourceTrace.connected_source_net_ids ?? []) { + const ids = map.get(sourceNetId) ?? [] + ids.push(sourceTrace.source_trace_id) + map.set(sourceNetId, ids) + } + } + } catch (err) { + console.error("Failed to derive source trace net map", err) + } + + return map + }, [circuitJsonKey, circuitJson]) + + const sourceTraceById = useMemo(() => { + const map = new Map() + + try { + for (const sourceTrace of su(circuitJson).source_trace.list()) { + map.set(sourceTrace.source_trace_id, sourceTrace) + } + } catch (err) { + console.error("Failed to derive source trace lookup", err) + } + + return map + }, [circuitJsonKey, circuitJson]) + + const sourceTraceIdBySchematicTraceId = useMemo(() => { + const map = new Map() + + for (const [ + sourceTraceId, + schematicTraceIds, + ] of schematicTraceIdsBySourceTraceId) { + for (const schematicTraceId of schematicTraceIds) { + map.set(schematicTraceId, sourceTraceId) + } + } + + return map + }, [schematicTraceIdsBySourceTraceId]) + + useEffect(() => { + const svgDiv = svgDivRef.current + if (!svgDiv) return + + const styleId = "schematic-trace-net-hover-style" + if (!svgDiv.querySelector(`style#${styleId}`)) { + const style = document.createElement("style") + style.id = styleId + style.textContent = ` + .${HOVERED_NET_CLASS} { + filter: invert(1); + } + + .${HOVERED_NET_CLASS} .trace-crossing-outline { + opacity: 0; + } + ` + svgDiv.appendChild(style) + } + + const clearHighlights = () => { + for (const trace of Array.from( + svgDiv.querySelectorAll(`.${HOVERED_NET_CLASS}`), + )) { + trace.classList.remove(HOVERED_NET_CLASS) + } + } + + const highlightNetForTrace = (schematicTraceId: string) => { + clearHighlights() + + const sourceTraceId = + sourceTraceIdBySchematicTraceId.get(schematicTraceId) + if (!sourceTraceId) return + + const sourceTrace = sourceTraceById.get(sourceTraceId) + const connectedNetIds = sourceTrace?.connected_source_net_ids ?? [] + if (connectedNetIds.length === 0) return + + const relatedSchematicTraceIds = new Set() + for (const sourceNetId of connectedNetIds) { + for (const relatedSourceTraceId of sourceTraceIdsByNetId.get( + sourceNetId, + ) ?? []) { + for (const relatedSchematicTraceId of schematicTraceIdsBySourceTraceId.get( + relatedSourceTraceId, + ) ?? []) { + relatedSchematicTraceIds.add(relatedSchematicTraceId) + } + } + } + + for (const relatedSchematicTraceId of relatedSchematicTraceIds) { + for (const traceElement of Array.from( + svgDiv.querySelectorAll( + `[data-schematic-trace-id="${escapeCssAttributeValue( + relatedSchematicTraceId, + )}"]`, + ), + )) { + traceElement.classList.add(HOVERED_NET_CLASS) + } + } + } + + const removeListeners: Array<() => void> = [] + for (const traceElement of Array.from( + svgDiv.querySelectorAll( + '[data-circuit-json-type="schematic_trace"][data-schematic-trace-id]', + ), + )) { + const schematicTraceId = traceElement.getAttribute( + "data-schematic-trace-id", + ) + if (!schematicTraceId) continue + + const handlePointerEnter = () => highlightNetForTrace(schematicTraceId) + traceElement.addEventListener("pointerenter", handlePointerEnter) + traceElement.addEventListener("pointerleave", clearHighlights) + removeListeners.push(() => { + traceElement.removeEventListener("pointerenter", handlePointerEnter) + traceElement.removeEventListener("pointerleave", clearHighlights) + }) + } + + return () => { + for (const removeListener of removeListeners) { + removeListener() + } + clearHighlights() + } + }, [ + svgDivRef, + svgContentKey, + schematicTraceIdsBySourceTraceId, + sourceTraceById, + sourceTraceIdsByNetId, + sourceTraceIdBySchematicTraceId, + ]) +}