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