diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index 791abf9..f4d4844 100644 --- a/lib/components/SchematicViewer.tsx +++ b/lib/components/SchematicViewer.tsx @@ -6,6 +6,7 @@ import { su } from "@tscircuit/soup-util" import { useChangeSchematicComponentLocationsInSvg } from "lib/hooks/useChangeSchematicComponentLocationsInSvg" import { useChangeSchematicTracesForMovedComponents } from "lib/hooks/useChangeSchematicTracesForMovedComponents" import { useSchematicGroupsOverlay } from "lib/hooks/useSchematicGroupsOverlay" +import { useSchematicTraceHoverHighlight } from "lib/hooks/useSchematicTraceHoverHighlight" import { enableDebug } from "lib/utils/debug" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { @@ -277,7 +278,7 @@ export const SchematicViewer = ({ const containerBackgroundColor = useMemo(() => { const match = svgString.match( - /]*style="[^"]*background-color:\s*([^;\"]+)/i, + /]*style="[^"]*background-color:\s*([^;\"]+ )/i, ) return match?.[1] ?? "transparent" }, [svgString]) @@ -338,6 +339,13 @@ export const SchematicViewer = ({ editEvents: editEventsWithUnappliedEditEvents, }) + // Highlight all traces in the same net on hover + useSchematicTraceHoverHighlight({ + svgDivRef, + circuitJson, + circuitJsonKey, + }) + // Add group overlays when enabled useSchematicGroupsOverlay({ svgDivRef, diff --git a/lib/hooks/useSchematicTraceHoverHighlight.ts b/lib/hooks/useSchematicTraceHoverHighlight.ts new file mode 100644 index 0000000..1d4d3d9 --- /dev/null +++ b/lib/hooks/useSchematicTraceHoverHighlight.ts @@ -0,0 +1,145 @@ +import { useEffect } from "react" +import { su } from "@tscircuit/soup-util" +import type { CircuitJson } from "circuit-json" + +/** + * Highlights all schematic traces belonging to the same electrical net + * whenever the user hovers over any trace segment. + * + * Strategy: + * 1. Build a net-group map from circuitJson: + * schematic_trace_id → Set (all traces on same net) + * using union-find over shared source-port IDs across source_traces. + * 2. Attach mouseover/mouseout listeners to the rendered SVG div. + * 3. On hover, look up the hovered trace’s group and paint all group members + * with HOVER_COLOR; restore original strokes on mouseout. + */ +export const useSchematicTraceHoverHighlight = ({ + svgDivRef, + circuitJson, + circuitJsonKey, +}: { + svgDivRef: React.RefObject + circuitJson: CircuitJson + circuitJsonKey: string +}) => { + useEffect(() => { + const svgEl = svgDivRef.current + if (!svgEl) return + + // --- Build net-group map --- + const traceNetGroupMap = new Map>() + + try { + const schematicTraces = su(circuitJson).schematic_trace?.list() ?? [] + const sourceTraces = su(circuitJson).source_trace?.list() ?? [] + + // Union-Find helpers to merge source_traces that share a port + const parent = new Map() + const getRoot = (id: string): string => { + if (!parent.has(id)) return id + const root = getRoot(parent.get(id)!) + parent.set(id, root) + return root + } + const union = (a: string, b: string) => { + const ra = getRoot(a) + const rb = getRoot(b) + if (ra !== rb) parent.set(ra, rb) + } + + // Group source_traces that share a connected port (same net) + const portToSourceTraces = new Map() + for (const st of sourceTraces) { + for (const portId of (st as any).connected_source_port_ids ?? []) { + const arr = portToSourceTraces.get(portId) ?? [] + arr.push((st as any).source_trace_id) + portToSourceTraces.set(portId, arr) + } + } + for (const [, traceIds] of portToSourceTraces) { + for (let i = 1; i < traceIds.length; i++) { + union(traceIds[0], traceIds[i]) + } + } + + // Map source_trace root → set of schematic_trace_ids + const netGroups = new Map>() + for (const st of schematicTraces) { + const sourceTraceId = (st as any).source_trace_id + if (!sourceTraceId) continue + const root = getRoot(sourceTraceId) + const group = netGroups.get(root) ?? new Set() + group.add((st as any).schematic_trace_id) + netGroups.set(root, group) + } + + // Build reverse lookup: schematic_trace_id → its net group + for (const group of netGroups.values()) { + for (const id of group) { + traceNetGroupMap.set(id, group) + } + } + } catch (_) { + // Parsing failed — hover will still work for individual traces + } + + // --- DOM interaction --- + const HOVER_COLOR = "#f5a623" + const savedStrokes = new Map() + let activeGroup: Set | null = null + + const applyHighlight = (group: Set) => { + if (activeGroup === group) return + clearHighlight() + activeGroup = group + for (const traceId of group) { + const paths = svgEl.querySelectorAll( + `[data-schematic-trace-id="${traceId}"] path,` + + `[data-schematic-trace-id="${traceId}"] line`, + ) + for (const path of Array.from(paths)) { + const el = path as Element + if (el.getAttribute("class")?.includes("invisible")) continue + const orig = el.getAttribute("stroke") ?? "" + savedStrokes.set(el, orig) + el.setAttribute("stroke", HOVER_COLOR) + } + } + } + + const clearHighlight = () => { + for (const [el, color] of savedStrokes) { + el.setAttribute("stroke", color) + } + savedStrokes.clear() + activeGroup = null + } + + const handleMouseOver = (e: MouseEvent) => { + const target = e.target as Element + const traceEl = target.closest("[data-schematic-trace-id]") + if (!traceEl) return + const traceId = traceEl.getAttribute("data-schematic-trace-id") + if (!traceId) return + // Fall back to a single-trace group if net mapping is unavailable + const group = traceNetGroupMap.get(traceId) ?? new Set([traceId]) + applyHighlight(group) + } + + const handleMouseOut = (e: MouseEvent) => { + const related = e.relatedTarget as Element | null + if (related?.closest("[data-schematic-trace-id]")) return + clearHighlight() + } + + svgEl.addEventListener("mouseover", handleMouseOver) + svgEl.addEventListener("mouseout", handleMouseOut) + + return () => { + svgEl.removeEventListener("mouseover", handleMouseOver) + svgEl.removeEventListener("mouseout", handleMouseOut) + clearHighlight() + } + }, [circuitJsonKey, svgDivRef]) +}