diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index ab4fd20..57d921f 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 { useHighlightConnectedTracesOnHover } from "lib/hooks/useHighlightConnectedTracesOnHover" +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,10 @@ export const SchematicViewer = ({ editEvents: editEventsWithUnappliedEditEvents, }) + useHighlightConnectedTracesOnHover({ + svgDivRef, + }) + // Add group overlays when enabled useSchematicGroupsOverlay({ svgDivRef, @@ -398,12 +403,14 @@ export const SchematicViewer = ({ {onSchematicComponentClicked && ( )} {onSchematicPortClicked && ( )}
{ + if (!(target instanceof Element)) return null + + return target.closest( + `${TRACE_SELECTOR}[data-schematic-trace-id]`, + ) +} + +export const useHighlightConnectedTracesOnHover = ({ + svgDivRef, +}: { + svgDivRef: RefObject +}) => { + useEffect(() => { + const svg = svgDivRef.current + if (!svg) return + + const style = document.createElement("style") + style.id = STYLE_ID + style.textContent = ` + .${HOVER_CLASS} { + filter: invert(1); + } + + .${HOVER_CLASS} .trace-crossing-outline { + opacity: 0; + } + ` + svg.appendChild(style) + + const clearHighlights = () => { + for (const element of Array.from( + svg.querySelectorAll(`.${HOVER_CLASS}`), + )) { + element.classList.remove(HOVER_CLASS) + } + } + + const highlightMatchingTraces = (traceElement: SVGGElement) => { + clearHighlights() + + const netKey = traceElement.getAttribute(NET_KEY_ATTR) + if (!netKey) { + traceElement.classList.add(HOVER_CLASS) + return + } + + for (const matchingTrace of Array.from( + svg.querySelectorAll( + `${TRACE_SELECTOR}[${NET_KEY_ATTR}="${CSS.escape(netKey)}"]`, + ), + )) { + matchingTrace.classList.add(HOVER_CLASS) + } + } + + const handlePointerOver = (event: PointerEvent) => { + const traceElement = getSchematicTraceElement(event.target) + if (!traceElement) return + highlightMatchingTraces(traceElement) + } + + const handlePointerOut = (event: PointerEvent) => { + const traceElement = getSchematicTraceElement(event.target) + if (!traceElement) return + + const relatedTarget = event.relatedTarget + if ( + relatedTarget instanceof Node && + traceElement.contains(relatedTarget) + ) { + return + } + + clearHighlights() + } + + svg.addEventListener("pointerover", handlePointerOver) + svg.addEventListener("pointerout", handlePointerOut) + + return () => { + svg.removeEventListener("pointerover", handlePointerOver) + svg.removeEventListener("pointerout", handlePointerOut) + clearHighlights() + svg.querySelector(`#${STYLE_ID}`)?.remove() + } + }, [svgDivRef]) +}