From 6030046a07c83b7b19f4765228f830bdc4b5c479 Mon Sep 17 00:00:00 2001 From: baeltaezaer Date: Sun, 5 Jul 2026 14:38:24 -0600 Subject: [PATCH 1/2] fix(schematic-viewer): highlight nets on hover --- lib/components/SchematicViewer.tsx | 111 +++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index 865a195..cd2e5b2 100644 --- a/lib/components/SchematicViewer.tsx +++ b/lib/components/SchematicViewer.tsx @@ -66,6 +66,21 @@ interface Props { onSchematicSheetChange?: (schematicSheetId: string) => void } +const escapeCssAttributeValue = (value: string) => + value.replace(/\\/g, "\\\\").replace(/"/g, '\\"') + +const buildTraceHoverStyles = (traceConnectivityKeys: string[]) => + traceConnectivityKeys + .map((key) => { + const escapedKey = escapeCssAttributeValue(key) + const hoveredTraceSelector = `:is(g.trace[data-subcircuit-connectivity-map-key="${escapedKey}"], g.trace-overlays[data-subcircuit-connectivity-map-key="${escapedKey}"])` + return [ + `svg:has(${hoveredTraceSelector}:hover) ${hoveredTraceSelector} { filter: invert(1); }`, + `svg:has(${hoveredTraceSelector}:hover) g.trace-overlays[data-subcircuit-connectivity-map-key="${escapedKey}"] .trace-crossing-outline { opacity: 0; }`, + ].join("\n") + }) + .join("\n") + export const SchematicViewer = ({ circuitJson, containerStyle, @@ -222,14 +237,19 @@ export const SchematicViewer = ({ const [isHoveringClickablePort, setIsHoveringClickablePort] = useState(false) const hoveringPortsRef = useRef>(new Set()) + const [hoveredPortsVersion, setHoveredPortsVersion] = useState(0) const handlePortHoverChange = useCallback( (portId: string, isHovering: boolean) => { + const hadPort = hoveringPortsRef.current.has(portId) if (isHovering) { hoveringPortsRef.current.add(portId) } else { hoveringPortsRef.current.delete(portId) } + if (hadPort !== hoveringPortsRef.current.has(portId)) { + setHoveredPortsVersion((version) => version + 1) + } setIsHoveringClickablePort(hoveringPortsRef.current.size > 0) }, [], @@ -281,6 +301,96 @@ export const SchematicViewer = ({ } }, [circuitJsonKey, circuitJson, showSchematicPorts, activeSheetId]) + const sourcePortConnectivityMap = useMemo(() => { + try { + const map = new Map>() + const sourceTraces = su(circuitJson).source_trace?.list() ?? [] + + for (const sourceTrace of sourceTraces) { + const connectivityKey = (sourceTrace as any) + .subcircuit_connectivity_map_key as string | undefined + if (!connectivityKey) continue + + for (const sourcePortId of sourceTrace.connected_source_port_ids ?? + []) { + const existingKeys = map.get(sourcePortId) + if (existingKeys) { + existingKeys.add(connectivityKey) + } else { + map.set(sourcePortId, new Set([connectivityKey])) + } + } + } + + return map + } catch (err) { + console.error("Failed to derive source port connectivity map", err) + return new Map>() + } + }, [circuitJsonKey, circuitJson]) + + const traceConnectivityKeys = useMemo(() => { + try { + const keys = new Set() + const sourceTraces = su(circuitJson).source_trace?.list() ?? [] + + for (const sourceTrace of sourceTraces) { + const connectivityKey = (sourceTrace as any) + .subcircuit_connectivity_map_key as string | undefined + if (connectivityKey) { + keys.add(connectivityKey) + } + } + + return [...keys] + } catch (err) { + console.error("Failed to derive trace connectivity keys", err) + return [] + } + }, [circuitJsonKey, circuitJson]) + + const traceHoverStyles = useMemo( + () => buildTraceHoverStyles(traceConnectivityKeys), + [traceConnectivityKeys], + ) + + const hoveredPortConnectivityKeys = useMemo(() => { + const keys = new Set() + for (const portId of hoveringPortsRef.current) { + const portKeys = sourcePortConnectivityMap.get(portId) + if (!portKeys) continue + for (const key of portKeys) { + keys.add(key) + } + } + return [...keys] + }, [hoveredPortsVersion, sourcePortConnectivityMap]) + + useEffect(() => { + const svgDiv = svgDivRef.current + if (!svgDiv) return + + const activeTraceElements = svgDiv.querySelectorAll( + '[data-net-hover-active="true"]', + ) + for (const element of Array.from(activeTraceElements)) { + element.removeAttribute("data-net-hover-active") + } + + if (hoveredPortConnectivityKeys.length === 0) { + return + } + + for (const connectivityKey of hoveredPortConnectivityKeys) { + const escapedKey = escapeCssAttributeValue(connectivityKey) + const selector = `:is(g.trace[data-subcircuit-connectivity-map-key="${escapedKey}"], g.trace-overlays[data-subcircuit-connectivity-map-key="${escapedKey}"])` + const matchingElements = svgDiv.querySelectorAll(selector) + for (const element of Array.from(matchingElements)) { + element.setAttribute("data-net-hover-active", "true") + } + } + }, [hoveredPortConnectivityKeys, circuitJsonKey]) + const handleTouchStart = (e: React.TouchEvent) => { const touch = e.touches[0] touchStartRef.current = { @@ -476,6 +586,7 @@ export const SchematicViewer = ({ return ( + {traceHoverStyles && } {onSchematicComponentClicked && ( } {onSchematicComponentClicked && (