diff --git a/examples/example19-tscircuit-demo.fixture.tsx b/examples/example19-tscircuit-demo.fixture.tsx new file mode 100644 index 0000000..147d8f4 --- /dev/null +++ b/examples/example19-tscircuit-demo.fixture.tsx @@ -0,0 +1,30 @@ +import type { CircuitJson } from "circuit-json" +import { SchematicViewer } from "lib/index" +import { useEffect, useState } from "react" + +const CIRCUIT_JSON_URL = + "https://registry-api.tscircuit.com/snippets/get?snippet_id=1bb7c947-85fc-4cbb-bbbe-d326676f4042" + +export default () => { + const [circuitJson, setCircuitJson] = useState(null) + + useEffect(() => { + fetch(CIRCUIT_JSON_URL) + .then((r) => r.json()) + .then((data) => setCircuitJson(data.snippet.circuit_json as CircuitJson)) + .catch(console.error) + }, []) + + if (!circuitJson) return
Loading circuit...
+ + return ( +
+ +
+ ) +} diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index 791abf9..4b1428c 100644 --- a/lib/components/SchematicViewer.tsx +++ b/lib/components/SchematicViewer.tsx @@ -5,6 +5,7 @@ import { import { su } from "@tscircuit/soup-util" import { useChangeSchematicComponentLocationsInSvg } from "lib/hooks/useChangeSchematicComponentLocationsInSvg" import { useChangeSchematicTracesForMovedComponents } from "lib/hooks/useChangeSchematicTracesForMovedComponents" +import { useTraceHoverHighlight } from "lib/hooks/useTraceHoverHighlight" import { useSchematicGroupsOverlay } from "lib/hooks/useSchematicGroupsOverlay" import { enableDebug } from "lib/utils/debug" import { useCallback, useEffect, useMemo, useRef, useState } from "react" @@ -338,6 +339,11 @@ export const SchematicViewer = ({ editEvents: editEventsWithUnappliedEditEvents, }) + useTraceHoverHighlight({ + svgDivRef, + circuitJson, + }) + // Add group overlays when enabled useSchematicGroupsOverlay({ svgDivRef, diff --git a/lib/hooks/useTraceHoverHighlight.ts b/lib/hooks/useTraceHoverHighlight.ts new file mode 100644 index 0000000..8ca7974 --- /dev/null +++ b/lib/hooks/useTraceHoverHighlight.ts @@ -0,0 +1,99 @@ +import { useEffect } from "react" +import type { CircuitJson } from "circuit-json" + +const HIGHLIGHT_COLOR = "#60a5fa" + +/** + * This hook highlights traces on hover and all traces connected to the same net. + * Net grouping is derived from the SVG's data-subcircuit-connectivity-map-key attribute. + */ +export const useTraceHoverHighlight = ({ + svgDivRef, + circuitJson, +}: { + svgDivRef: React.RefObject + circuitJson: CircuitJson +}) => { + useEffect(() => { + const svg = svgDivRef.current + if (!svg) return + + // Hover state + const originalStrokes = new Map() + let currentNetKey: string | null = null + + const clearHighlights = () => { + for (const [el, stroke] of originalStrokes) { + el.setAttribute("stroke", stroke) + } + originalStrokes.clear() + currentNetKey = null + } + + const applyHighlights = (netKey: string) => { + // Find all trace groups on the same net + const sameNetTraces = svg.querySelectorAll( + `[data-subcircuit-connectivity-map-key="${netKey}"][data-circuit-json-type="schematic_trace"]`, + ) + for (const traceGroup of Array.from(sameNetTraces)) { + const paths = traceGroup.querySelectorAll("path") + for (const path of Array.from(paths)) { + if (path.getAttribute("class")?.includes("invisible")) continue + originalStrokes.set(path, path.getAttribute("stroke") || "") + path.setAttribute("stroke", HIGHLIGHT_COLOR) + } + } + currentNetKey = netKey + } + + const handlePointerMove = (e: PointerEvent) => { + const target = e.target as Element + if (!target?.closest) return + + const traceGroup = target.closest( + "[data-circuit-json-type='schematic_trace']", + ) + if (!traceGroup) { + if (currentNetKey !== null) clearHighlights() + return + } + + const netKey = traceGroup.getAttribute( + "data-subcircuit-connectivity-map-key", + ) + if (!netKey) { + // No net key — highlight just this single trace + const traceId = traceGroup.getAttribute("data-schematic-trace-id") + if (!traceId || currentNetKey === `single:${traceId}`) return + clearHighlights() + const paths = traceGroup.querySelectorAll("path") + for (const path of Array.from(paths)) { + if (path.getAttribute("class")?.includes("invisible")) continue + originalStrokes.set(path, path.getAttribute("stroke") || "") + path.setAttribute("stroke", HIGHLIGHT_COLOR) + } + currentNetKey = `single:${traceId}` + return + } + + // Already highlighting this net + if (netKey === currentNetKey) return + + clearHighlights() + applyHighlights(netKey) + } + + const handlePointerLeave = () => { + clearHighlights() + } + + svg.addEventListener("pointermove", handlePointerMove) + svg.addEventListener("pointerleave", handlePointerLeave) + + return () => { + clearHighlights() + svg.removeEventListener("pointermove", handlePointerMove) + svg.removeEventListener("pointerleave", handlePointerLeave) + } + }, [svgDivRef, circuitJson]) +}