diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index ab4fd20..eac0d8f 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 { useHighlightSameNetTracesOnHover } from "lib/hooks/useHighlightSameNetTracesOnHover" +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,12 @@ export const SchematicViewer = ({ editEvents: editEventsWithUnappliedEditEvents, }) + useHighlightSameNetTracesOnHover({ + svgDivRef, + circuitJson, + svgContentKey: svgString, + }) + // Add group overlays when enabled useSchematicGroupsOverlay({ svgDivRef, diff --git a/lib/hooks/useHighlightSameNetTracesOnHover.ts b/lib/hooks/useHighlightSameNetTracesOnHover.ts new file mode 100644 index 0000000..64f374c --- /dev/null +++ b/lib/hooks/useHighlightSameNetTracesOnHover.ts @@ -0,0 +1,145 @@ +import { su } from "@tscircuit/soup-util" +import type { CircuitJson } from "circuit-json" +import { useEffect, useMemo } from "react" + +const HOVERED_TRACE_CLASS = "schematic-trace-same-net-hover" +const HOVER_STYLE_ID = "schematic-trace-same-net-hover-style" + +export const useHighlightSameNetTracesOnHover = ({ + svgDivRef, + circuitJson, + svgContentKey, +}: { + svgDivRef: React.RefObject + circuitJson: CircuitJson + svgContentKey: string +}) => { + const schematicTraceIdsBySourceTraceId = useMemo(() => { + const traceIdsBySourceTraceId = new Map() + + try { + for (const trace of su(circuitJson).schematic_trace.list()) { + if (!trace.source_trace_id || !trace.schematic_trace_id) continue + + const traceIds = + traceIdsBySourceTraceId.get(trace.source_trace_id) ?? [] + traceIds.push(trace.schematic_trace_id) + traceIdsBySourceTraceId.set(trace.source_trace_id, traceIds) + } + } catch (err) { + console.error("Failed to derive schematic trace hover groups", err) + } + + return traceIdsBySourceTraceId + }, [circuitJson]) + + const sourceTraceIdBySchematicTraceId = useMemo(() => { + const sourceTraceIds = new Map() + + for (const [ + sourceTraceId, + schematicTraceIds, + ] of schematicTraceIdsBySourceTraceId) { + for (const schematicTraceId of schematicTraceIds) { + sourceTraceIds.set(schematicTraceId, sourceTraceId) + } + } + + return sourceTraceIds + }, [schematicTraceIdsBySourceTraceId]) + + useEffect(() => { + const svgContainer = svgDivRef.current + const svg = svgContainer?.querySelector("svg") + if (!svgContainer || !svg) return + + const traceGroups = Array.from( + svg.querySelectorAll( + '[data-circuit-json-type="schematic_trace"][data-schematic-trace-id]', + ), + ) + + const ensureHoverStyle = () => { + if (svg.querySelector(`#${HOVER_STYLE_ID}`)) return + + const style = document.createElementNS( + "http://www.w3.org/2000/svg", + "style", + ) + style.id = HOVER_STYLE_ID + style.textContent = ` + .${HOVERED_TRACE_CLASS} path:not(.trace-invisible-hover-outline):not(.trace-crossing-outline) { + stroke: rgb(255, 140, 0) !important; + filter: drop-shadow(0 0 3px rgba(255, 140, 0, 0.85)); + } + + .${HOVERED_TRACE_CLASS} .trace-crossing-outline { + opacity: 0; + } + ` + svg.appendChild(style) + } + + let hoveredSourceTraceId: string | null = null + + const clearHoveredTraces = () => { + if (!hoveredSourceTraceId) return + hoveredSourceTraceId = null + + for (const traceGroup of traceGroups) { + traceGroup.classList.remove(HOVERED_TRACE_CLASS) + } + } + + const setHoveredTrace = (schematicTraceId: string | null) => { + const nextSourceTraceId = schematicTraceId + ? (sourceTraceIdBySchematicTraceId.get(schematicTraceId) ?? null) + : null + + if (nextSourceTraceId === hoveredSourceTraceId) return + + clearHoveredTraces() + hoveredSourceTraceId = nextSourceTraceId + if (!hoveredSourceTraceId) return + + ensureHoverStyle() + const sameNetTraceIds = + schematicTraceIdsBySourceTraceId.get(hoveredSourceTraceId) ?? [] + + for (const traceGroup of traceGroups) { + const traceId = traceGroup.getAttribute("data-schematic-trace-id") + if (traceId && sameNetTraceIds.includes(traceId)) { + traceGroup.classList.add(HOVERED_TRACE_CLASS) + } + } + } + + const handlePointerMove = (event: PointerEvent) => { + const target = event.target + if (!(target instanceof Element)) return + + const traceGroup = target.closest( + '[data-circuit-json-type="schematic_trace"][data-schematic-trace-id]', + ) + setHoveredTrace( + traceGroup?.getAttribute("data-schematic-trace-id") ?? null, + ) + } + + svg.addEventListener("pointermove", handlePointerMove) + svg.addEventListener("pointerleave", clearHoveredTraces) + svg.addEventListener("pointercancel", clearHoveredTraces) + + return () => { + svg.removeEventListener("pointermove", handlePointerMove) + svg.removeEventListener("pointerleave", clearHoveredTraces) + svg.removeEventListener("pointercancel", clearHoveredTraces) + clearHoveredTraces() + } + }, [ + schematicTraceIdsBySourceTraceId, + sourceTraceIdBySchematicTraceId, + svgContentKey, + svgDivRef, + ]) +}