From bf4f39f926957eadd3489344a8bdc91e68a1b7cb Mon Sep 17 00:00:00 2001 From: cuongdodhct-a11y Date: Tue, 26 May 2026 10:32:11 +0700 Subject: [PATCH] Highlight connected schematic traces on hover --- lib/components/SchematicViewer.tsx | 119 +++++++++++++++++++++++++---- 1 file changed, 106 insertions(+), 13 deletions(-) diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index ab4fd20..412520c 100644 --- a/lib/components/SchematicViewer.tsx +++ b/lib/components/SchematicViewer.tsx @@ -1,10 +1,12 @@ +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 { enableDebug } from "lib/utils/debug" import { useCallback, useEffect, useMemo, useRef, useState } from "react" @@ -16,21 +18,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 +345,86 @@ export const SchematicViewer = ({ editEvents: editEventsWithUnappliedEditEvents, }) + const sourceTraceIdBySchematicTraceId = useMemo(() => { + const sourceTraceIds = new Map() + + try { + for (const trace of su(circuitJson).schematic_trace.list()) { + if (!trace.schematic_trace_id || !trace.source_trace_id) continue + sourceTraceIds.set(trace.schematic_trace_id, trace.source_trace_id) + } + } catch (err) { + console.error("Failed to derive schematic trace source ids", err) + } + + return sourceTraceIds + }, [circuitJsonKey, circuitJson]) + + useEffect(() => { + const svg = svgDivRef.current + if (!svg) return + + const traceGroups = Array.from( + svg.querySelectorAll( + '[data-circuit-json-type="schematic_trace"][data-schematic-trace-id]', + ), + ) + + const traceGroupsBySourceTraceId = new Map() + + for (const traceGroup of traceGroups) { + const schematicTraceId = traceGroup.dataset.schematicTraceId + if (!schematicTraceId) continue + + const sourceTraceId = + sourceTraceIdBySchematicTraceId.get(schematicTraceId) ?? + schematicTraceId + + traceGroup.dataset.sourceTraceId = sourceTraceId + + const groupsForSourceTrace = + traceGroupsBySourceTraceId.get(sourceTraceId) ?? [] + groupsForSourceTrace.push(traceGroup) + traceGroupsBySourceTraceId.set(sourceTraceId, groupsForSourceTrace) + } + + const setSameNetTraceHover = ( + traceGroup: SVGGElement, + isHovering: boolean, + ) => { + const sourceTraceId = traceGroup.dataset.sourceTraceId + if (!sourceTraceId) return + + for (const connectedTraceGroup of traceGroupsBySourceTraceId.get( + sourceTraceId, + ) ?? []) { + connectedTraceGroup.classList.toggle( + "schematic-trace-same-net-hover", + isHovering, + ) + } + } + + const cleanupCallbacks = traceGroups.map((traceGroup) => { + const handlePointerEnter = () => setSameNetTraceHover(traceGroup, true) + const handlePointerLeave = () => setSameNetTraceHover(traceGroup, false) + + traceGroup.addEventListener("pointerenter", handlePointerEnter) + traceGroup.addEventListener("pointerleave", handlePointerLeave) + + return () => { + traceGroup.removeEventListener("pointerenter", handlePointerEnter) + traceGroup.removeEventListener("pointerleave", handlePointerLeave) + } + }) + + return () => { + for (const cleanupCallback of cleanupCallbacks) { + cleanupCallback() + } + } + }, [svgString, sourceTraceIdBySchematicTraceId]) + // Add group overlays when enabled useSchematicGroupsOverlay({ svgDivRef, @@ -398,14 +478,27 @@ export const SchematicViewer = ({ {onSchematicComponentClicked && ( )} {onSchematicPortClicked && ( )} +