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]) +}