diff --git a/examples/example21-connected-trace-hover.fixture.tsx b/examples/example21-connected-trace-hover.fixture.tsx
new file mode 100644
index 0000000..79eeba8
--- /dev/null
+++ b/examples/example21-connected-trace-hover.fixture.tsx
@@ -0,0 +1,42 @@
+import { SchematicViewer } from "lib/components/SchematicViewer"
+import { renderToCircuitJson } from "lib/dev/render-to-circuit-json"
+
+const circuitJson = renderToCircuitJson(
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ,
+)
+
+export default () => {
+ return (
+
+
+
+ )
+}
diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx
index ab4fd20..efa309e 100644
--- a/lib/components/SchematicViewer.tsx
+++ b/lib/components/SchematicViewer.tsx
@@ -1,11 +1,14 @@
+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 { useTraceHoverHighlight } from "lib/hooks/useTraceHoverHighlight"
import { enableDebug } from "lib/utils/debug"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import {
@@ -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,
})
+ useTraceHoverHighlight({
+ svgDivRef,
+ })
+
// Add group overlays when enabled
useSchematicGroupsOverlay({
svgDivRef,
@@ -398,14 +403,28 @@ export const SchematicViewer = ({
{onSchematicComponentClicked && (
)}
{onSchematicPortClicked && (
)}
+
{
+ if (!element) return null
+ return (
+ element.getAttribute(CONNECTIVITY_KEY_ATTR) ??
+ element.getAttribute("data-schematic-trace-id")
+ )
+}
+
+export const useTraceHoverHighlight = ({
+ svgDivRef,
+}: {
+ svgDivRef: React.RefObject
+}) => {
+ const hoveredKeyRef = useRef(null)
+
+ useEffect(() => {
+ const svgRoot = svgDivRef.current
+ if (!svgRoot) return
+
+ const clearHoveredTraces = () => {
+ const hoveredTraces = svgRoot.querySelectorAll(`[${HOVERED_ATTR}="true"]`)
+ for (const trace of Array.from(hoveredTraces)) {
+ trace.removeAttribute(HOVERED_ATTR)
+ }
+ hoveredKeyRef.current = null
+ }
+
+ const setHoveredTraces = (key: string) => {
+ if (hoveredKeyRef.current === key) return
+
+ clearHoveredTraces()
+
+ const escapedKey =
+ typeof CSS !== "undefined" && typeof CSS.escape === "function"
+ ? CSS.escape(key)
+ : key.replaceAll('"', '\\"')
+
+ const matchingTraceGroups = svgRoot.querySelectorAll(
+ `${TRACE_GROUP_SELECTOR}[${CONNECTIVITY_KEY_ATTR}="${escapedKey}"]`,
+ )
+ for (const traceGroup of Array.from(matchingTraceGroups)) {
+ traceGroup.setAttribute(HOVERED_ATTR, "true")
+ }
+
+ hoveredKeyRef.current = key
+ }
+
+ const handlePointerOver = (event: Event) => {
+ const target = event.target as Element | null
+ const traceGroup = target?.closest(TRACE_GROUP_SELECTOR) ?? null
+ const traceKey = getTraceGroupKey(traceGroup)
+ if (!traceKey) return
+ setHoveredTraces(traceKey)
+ }
+
+ const handlePointerOut = (event: Event) => {
+ const target = event.target as Element | null
+ const traceGroup = target?.closest(TRACE_GROUP_SELECTOR) ?? null
+ const traceKey = getTraceGroupKey(traceGroup)
+ if (!traceKey) return
+
+ const relatedTarget = (event as PointerEvent).relatedTarget
+ const relatedElement =
+ relatedTarget instanceof Element ? relatedTarget : null
+ const relatedTraceGroup =
+ relatedElement?.closest(TRACE_GROUP_SELECTOR) ?? null
+ const relatedKey = getTraceGroupKey(relatedTraceGroup)
+
+ if (relatedKey && relatedKey === traceKey) {
+ return
+ }
+
+ if (hoveredKeyRef.current === traceKey) {
+ clearHoveredTraces()
+ }
+ }
+
+ svgRoot.addEventListener("pointerover", handlePointerOver)
+ svgRoot.addEventListener("pointerout", handlePointerOut)
+
+ return () => {
+ svgRoot.removeEventListener("pointerover", handlePointerOver)
+ svgRoot.removeEventListener("pointerout", handlePointerOut)
+ clearHoveredTraces()
+ }
+ }, [svgDivRef])
+}