From dee9c869f3b168665b7f1dfc511576f850da2cae Mon Sep 17 00:00:00 2001 From: bimakw <51526537+bimakw@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:10:34 +0700 Subject: [PATCH] feat: highlight all connected traces on hover When hovering over a trace, all traces connected to the same net (sharing the same source_trace_id) are now highlighted together. This creates a new useHighlightConnectedTracesOnHover hook that: - Listens for mouseover/mouseout events on trace elements - Finds all connected traces via source_trace_id in CircuitJson - Applies highlight color (#ff6b6b) and increased stroke width - Properly handles edge cases like moving between connected traces Fixes tscircuit/tscircuit#1130 --- lib/components/SchematicViewer.tsx | 7 + .../useHighlightConnectedTracesOnHover.ts | 170 ++++++++++++++++++ 2 files changed, 177 insertions(+) create mode 100644 lib/hooks/useHighlightConnectedTracesOnHover.ts diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index 51e782a..bdfecd1 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 { useHighlightConnectedTracesOnHover } from "lib/hooks/useHighlightConnectedTracesOnHover" import { useSchematicGroupsOverlay } from "lib/hooks/useSchematicGroupsOverlay" import { enableDebug } from "lib/utils/debug" import { useCallback, useEffect, useMemo, useRef, useState } from "react" @@ -286,6 +287,12 @@ export const SchematicViewer = ({ editEvents: editEventsWithUnappliedEditEvents, }) + // Highlight connected traces on hover + useHighlightConnectedTracesOnHover({ + svgDivRef, + circuitJson, + }) + // Add group overlays when enabled useSchematicGroupsOverlay({ svgDivRef, diff --git a/lib/hooks/useHighlightConnectedTracesOnHover.ts b/lib/hooks/useHighlightConnectedTracesOnHover.ts new file mode 100644 index 0000000..4c83986 --- /dev/null +++ b/lib/hooks/useHighlightConnectedTracesOnHover.ts @@ -0,0 +1,170 @@ +import { useEffect } from "react" +import { su } from "@tscircuit/soup-util" +import type { CircuitJson } from "circuit-json" + +/** + * This hook highlights all connected traces in the same net when hovering over a trace + */ +export const useHighlightConnectedTracesOnHover = ({ + svgDivRef, + circuitJson, +}: { + svgDivRef: React.RefObject + circuitJson: CircuitJson +}) => { + useEffect(() => { + const svg = svgDivRef.current + if (!svg) return + + // Store original colors for restoration + const originalColors = new Map() + const HIGHLIGHT_COLOR = "#ff6b6b" + + const getConnectedTraceIds = (schematicTraceId: string): string[] => { + // Find the schematic trace + const schematicTrace = su(circuitJson) + .schematic_trace.list() + .find((st) => st.schematic_trace_id === schematicTraceId) + + if (!schematicTrace?.source_trace_id) { + return [schematicTraceId] + } + + // Find all schematic traces with the same source_trace_id (same net) + const connectedTraces = su(circuitJson) + .schematic_trace.list() + .filter((st) => st.source_trace_id === schematicTrace.source_trace_id) + + return connectedTraces.map((st) => st.schematic_trace_id) + } + + const highlightTraces = (traceIds: string[]) => { + for (const traceId of traceIds) { + const traceElements = svg.querySelectorAll( + `[data-schematic-trace-id="${traceId}"] path`, + ) + for (const traceElement of Array.from(traceElements)) { + if (traceElement.getAttribute("class")?.includes("invisible")) + continue + + // Store original color if not already stored + if (!originalColors.has(traceElement)) { + originalColors.set( + traceElement, + traceElement.getAttribute("stroke") || "", + ) + } + + // Apply highlight color + traceElement.setAttribute("stroke", HIGHLIGHT_COLOR) + ;(traceElement as HTMLElement).style.strokeWidth = "7px" + } + } + } + + const resetTraces = (traceIds: string[]) => { + for (const traceId of traceIds) { + const traceElements = svg.querySelectorAll( + `[data-schematic-trace-id="${traceId}"] path`, + ) + for (const traceElement of Array.from(traceElements)) { + if (traceElement.getAttribute("class")?.includes("invisible")) + continue + + // Restore original color + const originalColor = originalColors.get(traceElement) + if (originalColor) { + traceElement.setAttribute("stroke", originalColor) + } + ;(traceElement as HTMLElement).style.strokeWidth = "" + } + } + } + + let currentHighlightedTraces: string[] = [] + + const handleMouseOver = (e: MouseEvent) => { + const target = e.target as Element + const traceGroup = target.closest( + '[data-circuit-json-type="schematic_trace"]', + ) + + if (!traceGroup) return + + const schematicTraceId = traceGroup.getAttribute( + "data-schematic-trace-id", + ) + if (!schematicTraceId) return + + // Reset previous highlights + if (currentHighlightedTraces.length > 0) { + resetTraces(currentHighlightedTraces) + } + + // Get all connected trace IDs and highlight them + currentHighlightedTraces = getConnectedTraceIds(schematicTraceId) + highlightTraces(currentHighlightedTraces) + } + + const handleMouseOut = (e: MouseEvent) => { + const target = e.target as Element + const traceGroup = target.closest( + '[data-circuit-json-type="schematic_trace"]', + ) + + if (!traceGroup) return + + // Check if we're moving to another element within the same trace group + const relatedTarget = e.relatedTarget as Element | null + if (relatedTarget && traceGroup.contains(relatedTarget)) { + return + } + + // Check if we're moving to another connected trace + const relatedTraceGroup = relatedTarget?.closest?.( + '[data-circuit-json-type="schematic_trace"]', + ) + if (relatedTraceGroup) { + const relatedTraceId = relatedTraceGroup.getAttribute( + "data-schematic-trace-id", + ) + if ( + relatedTraceId && + currentHighlightedTraces.includes(relatedTraceId) + ) { + return + } + } + + // Reset all highlighted traces + resetTraces(currentHighlightedTraces) + currentHighlightedTraces = [] + } + + const setupEventListeners = () => { + svg.addEventListener("mouseover", handleMouseOver) + svg.addEventListener("mouseout", handleMouseOut) + } + + // Set up listeners initially + setupEventListeners() + + // Re-setup listeners when SVG content changes + const observer = new MutationObserver(() => { + // Clear stored colors when SVG changes + originalColors.clear() + currentHighlightedTraces = [] + }) + + observer.observe(svg, { + childList: true, + subtree: false, + }) + + return () => { + svg.removeEventListener("mouseover", handleMouseOver) + svg.removeEventListener("mouseout", handleMouseOut) + observer.disconnect() + } + }, [svgDivRef, circuitJson]) +}