From e92b1c301ab4aac10f6ab3cf25df6488396d30d2 Mon Sep 17 00:00:00 2001 From: tttislandyyy Date: Fri, 5 Jun 2026 13:09:36 +0800 Subject: [PATCH 1/5] Fix schematic trace net hover highlighting --- lib/components/SchematicViewer.tsx | 7 + .../useHighlightConnectedTracesOnHover.ts | 208 ++++++++++++++++++ 2 files changed, 215 insertions(+) create mode 100644 lib/hooks/useHighlightConnectedTracesOnHover.ts diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index ab4fd20..01d25be 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 { useHighlightConnectedTracesOnHover } from "lib/hooks/useHighlightConnectedTracesOnHover" import { enableDebug } from "lib/utils/debug" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { @@ -353,6 +354,12 @@ export const SchematicViewer = ({ showGroups: showSchematicGroups && !disableGroups, }) + useHighlightConnectedTracesOnHover({ + svgDivRef, + circuitJson, + circuitJsonKey, + }) + // keep the latest touch handler without re-rendering the svg div const handleComponentTouchStartRef = useRef(handleComponentTouchStart) useEffect(() => { diff --git a/lib/hooks/useHighlightConnectedTracesOnHover.ts b/lib/hooks/useHighlightConnectedTracesOnHover.ts new file mode 100644 index 0000000..7442c88 --- /dev/null +++ b/lib/hooks/useHighlightConnectedTracesOnHover.ts @@ -0,0 +1,208 @@ +import { su } from "@tscircuit/soup-util" +import type { CircuitJson } from "circuit-json" +import { useEffect } from "react" + +const HOVER_STYLE_ID = "schematic-trace-net-hover-style" +const HOVER_ATTR = "data-schematic-trace-net-hover" + +const getConnectedSourceTraceIds = ( + hoveredSourceTraceId: string, + sourceTraceIdToNetIds: Map>, + netIdToSourceTraceIds: Map>, + sourceTraceIdToPortIds: Map>, + portIdToSourceTraceIds: Map>, +) => { + const connectedSourceTraceIds = new Set() + const queue = [hoveredSourceTraceId] + + while (queue.length > 0) { + const sourceTraceId = queue.pop() + if (!sourceTraceId || connectedSourceTraceIds.has(sourceTraceId)) continue + + connectedSourceTraceIds.add(sourceTraceId) + + for (const netId of sourceTraceIdToNetIds.get(sourceTraceId) ?? []) { + for (const nextTraceId of netIdToSourceTraceIds.get(netId) ?? []) { + if (!connectedSourceTraceIds.has(nextTraceId)) { + queue.push(nextTraceId) + } + } + } + + for (const portId of sourceTraceIdToPortIds.get(sourceTraceId) ?? []) { + for (const nextTraceId of portIdToSourceTraceIds.get(portId) ?? []) { + if (!connectedSourceTraceIds.has(nextTraceId)) { + queue.push(nextTraceId) + } + } + } + } + + return connectedSourceTraceIds +} + +export const useHighlightConnectedTracesOnHover = ({ + svgDivRef, + circuitJson, + circuitJsonKey, +}: { + svgDivRef: React.RefObject + circuitJson: CircuitJson + circuitJsonKey: string +}) => { + useEffect(() => { + const svg = svgDivRef.current + if (!svg) return + + const schematicTraceIdToSourceTraceId = new Map() + const sourceTraceIdToSchematicTraceIds = new Map>() + const sourceTraceIdToNetIds = new Map>() + const netIdToSourceTraceIds = new Map>() + const sourceTraceIdToPortIds = new Map>() + const portIdToSourceTraceIds = new Map>() + + for (const sourceTrace of su(circuitJson).source_trace.list() as any[]) { + const sourceTraceId = sourceTrace.source_trace_id as string | undefined + if (!sourceTraceId) continue + + const netIds = new Set( + (sourceTrace.connected_source_net_ids ?? []).filter(Boolean), + ) + sourceTraceIdToNetIds.set(sourceTraceId, netIds) + + for (const netId of netIds) { + const sourceTraceIds = netIdToSourceTraceIds.get(netId) ?? new Set() + sourceTraceIds.add(sourceTraceId) + netIdToSourceTraceIds.set(netId, sourceTraceIds) + } + + const portIds = new Set( + (sourceTrace.connected_source_port_ids ?? []).filter(Boolean), + ) + sourceTraceIdToPortIds.set(sourceTraceId, portIds) + + for (const portId of portIds) { + const sourceTraceIds = portIdToSourceTraceIds.get(portId) ?? new Set() + sourceTraceIds.add(sourceTraceId) + portIdToSourceTraceIds.set(portId, sourceTraceIds) + } + } + + for (const schematicTrace of su( + circuitJson, + ).schematic_trace.list() as any[]) { + const schematicTraceId = schematicTrace.schematic_trace_id as + | string + | undefined + const sourceTraceId = schematicTrace.source_trace_id as string | undefined + + if (!schematicTraceId || !sourceTraceId) continue + + schematicTraceIdToSourceTraceId.set(schematicTraceId, sourceTraceId) + const schematicTraceIds = + sourceTraceIdToSchematicTraceIds.get(sourceTraceId) ?? new Set() + schematicTraceIds.add(schematicTraceId) + sourceTraceIdToSchematicTraceIds.set(sourceTraceId, schematicTraceIds) + } + + const ensureHoverStyle = () => { + if (!svg.querySelector(`style#${HOVER_STYLE_ID}`)) { + const style = document.createElement("style") + style.id = HOVER_STYLE_ID + style.textContent = ` + [data-circuit-json-type="schematic_trace"][${HOVER_ATTR}="true"] { + filter: invert(1); + } + + [data-circuit-json-type="schematic_trace"][${HOVER_ATTR}="true"] .trace-crossing-outline { + opacity: 0; + } + ` + svg.appendChild(style) + } + } + + const clearHighlightedTraces = () => { + for (const trace of Array.from(svg.querySelectorAll(`[${HOVER_ATTR}]`))) { + trace.removeAttribute(HOVER_ATTR) + } + } + + const highlightConnectedTraces = (schematicTraceId: string) => { + clearHighlightedTraces() + + const sourceTraceId = + schematicTraceIdToSourceTraceId.get(schematicTraceId) + if (!sourceTraceId) return + + const connectedSourceTraceIds = getConnectedSourceTraceIds( + sourceTraceId, + sourceTraceIdToNetIds, + netIdToSourceTraceIds, + sourceTraceIdToPortIds, + portIdToSourceTraceIds, + ) + + for (const connectedSourceTraceId of connectedSourceTraceIds) { + for (const connectedSchematicTraceId of sourceTraceIdToSchematicTraceIds.get( + connectedSourceTraceId, + ) ?? []) { + for (const traceElement of Array.from( + svg.querySelectorAll( + `[data-circuit-json-type="schematic_trace"][data-schematic-trace-id="${connectedSchematicTraceId}"]`, + ), + )) { + traceElement.setAttribute(HOVER_ATTR, "true") + } + } + } + } + + const cleanupListeners: Array<() => void> = [] + + const attachTraceHoverListeners = () => { + ensureHoverStyle() + cleanupListeners.splice(0).forEach((cleanup) => cleanup()) + + const traceElements = svg.querySelectorAll( + '[data-circuit-json-type="schematic_trace"][data-schematic-trace-id]', + ) + + for (const traceElement of Array.from(traceElements)) { + const schematicTraceId = traceElement.getAttribute( + "data-schematic-trace-id", + ) + if (!schematicTraceId) continue + + const handlePointerEnter = () => { + highlightConnectedTraces(schematicTraceId) + } + + traceElement.addEventListener("pointerenter", handlePointerEnter) + traceElement.addEventListener("pointerleave", clearHighlightedTraces) + + cleanupListeners.push(() => { + traceElement.removeEventListener("pointerenter", handlePointerEnter) + traceElement.removeEventListener( + "pointerleave", + clearHighlightedTraces, + ) + }) + } + } + + attachTraceHoverListeners() + + const observer = new MutationObserver(attachTraceHoverListeners) + observer.observe(svg, { + childList: true, + subtree: false, + }) + + return () => { + observer.disconnect() + cleanupListeners.splice(0).forEach((cleanup) => cleanup()) + clearHighlightedTraces() + } + }, [svgDivRef, circuitJson, circuitJsonKey]) +} From 02a37205279e6a9e98213928c0c1d0989475bb73 Mon Sep 17 00:00:00 2001 From: tttislandyyy Date: Fri, 5 Jun 2026 20:42:39 +0800 Subject: [PATCH 2/5] Add connected trace hover verification fixture --- ...xample21-connected-trace-hover.fixture.tsx | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 examples/example21-connected-trace-hover.fixture.tsx diff --git a/examples/example21-connected-trace-hover.fixture.tsx b/examples/example21-connected-trace-hover.fixture.tsx new file mode 100644 index 0000000..185c58e --- /dev/null +++ b/examples/example21-connected-trace-hover.fixture.tsx @@ -0,0 +1,44 @@ +import { SchematicViewer } from "lib/components/SchematicViewer" +import { renderToCircuitJson } from "lib/dev/render-to-circuit-json" + +const circuitJson = renderToCircuitJson( + + + + + + + + + + + + + + + + + , +) + +export default () => ( + +) From da7f6c65f56d63088c89b0978a582baf764af202 Mon Sep 17 00:00:00 2001 From: tttislandyyy Date: Fri, 5 Jun 2026 20:48:25 +0800 Subject: [PATCH 3/5] Support solver trace ids in hover highlighting --- .../useHighlightConnectedTracesOnHover.ts | 112 ++++++++++++++++-- 1 file changed, 102 insertions(+), 10 deletions(-) diff --git a/lib/hooks/useHighlightConnectedTracesOnHover.ts b/lib/hooks/useHighlightConnectedTracesOnHover.ts index 7442c88..78b8fac 100644 --- a/lib/hooks/useHighlightConnectedTracesOnHover.ts +++ b/lib/hooks/useHighlightConnectedTracesOnHover.ts @@ -5,6 +5,25 @@ import { useEffect } from "react" const HOVER_STYLE_ID = "schematic-trace-net-hover-style" const HOVER_ATTR = "data-schematic-trace-net-hover" +const addToSetMap = ( + map: Map>, + key: string, + value: string, +) => { + const values = map.get(key) ?? new Set() + values.add(value) + map.set(key, values) +} + +const getSolverSourcePortSelectors = (sourceTraceId: string) => { + if (!sourceTraceId.startsWith("solver_")) return [] + + return sourceTraceId + .replace(/^solver_/, "") + .split("-") + .filter(Boolean) +} + const getConnectedSourceTraceIds = ( hoveredSourceTraceId: string, sourceTraceIdToNetIds: Map>, @@ -60,6 +79,47 @@ export const useHighlightConnectedTracesOnHover = ({ const netIdToSourceTraceIds = new Map>() const sourceTraceIdToPortIds = new Map>() const portIdToSourceTraceIds = new Map>() + const sourcePortSelectorToPortId = new Map() + const sourcePortIdToConnectivityKey = new Map() + + for (const sourceComponent of su( + circuitJson, + ).source_component.list() as any[]) { + const sourceComponentId = sourceComponent.source_component_id as + | string + | undefined + const componentName = sourceComponent.name as string | undefined + if (!sourceComponentId || !componentName) continue + + for (const sourcePort of su(circuitJson).source_port.list({ + source_component_id: sourceComponentId, + }) as any[]) { + const sourcePortId = sourcePort.source_port_id as string | undefined + if (!sourcePortId) continue + + const portSelectors = [ + sourcePort.name, + sourcePort.pin_number, + ...(sourcePort.port_hints ?? []), + ] + .filter(Boolean) + .map(String) + + for (const portSelector of portSelectors) { + sourcePortSelectorToPortId.set( + `${componentName}.${portSelector}`, + sourcePortId, + ) + } + + const connectivityKey = sourcePort.subcircuit_connectivity_map_key as + | string + | undefined + if (connectivityKey) { + sourcePortIdToConnectivityKey.set(sourcePortId, connectivityKey) + } + } + } for (const sourceTrace of su(circuitJson).source_trace.list() as any[]) { const sourceTraceId = sourceTrace.source_trace_id as string | undefined @@ -68,12 +128,15 @@ export const useHighlightConnectedTracesOnHover = ({ const netIds = new Set( (sourceTrace.connected_source_net_ids ?? []).filter(Boolean), ) + if (sourceTrace.subcircuit_connectivity_map_key) { + netIds.add( + `connectivity:${sourceTrace.subcircuit_connectivity_map_key}`, + ) + } sourceTraceIdToNetIds.set(sourceTraceId, netIds) for (const netId of netIds) { - const sourceTraceIds = netIdToSourceTraceIds.get(netId) ?? new Set() - sourceTraceIds.add(sourceTraceId) - netIdToSourceTraceIds.set(netId, sourceTraceIds) + addToSetMap(netIdToSourceTraceIds, netId, sourceTraceId) } const portIds = new Set( @@ -82,9 +145,7 @@ export const useHighlightConnectedTracesOnHover = ({ sourceTraceIdToPortIds.set(sourceTraceId, portIds) for (const portId of portIds) { - const sourceTraceIds = portIdToSourceTraceIds.get(portId) ?? new Set() - sourceTraceIds.add(sourceTraceId) - portIdToSourceTraceIds.set(portId, sourceTraceIds) + addToSetMap(portIdToSourceTraceIds, portId, sourceTraceId) } } @@ -99,10 +160,41 @@ export const useHighlightConnectedTracesOnHover = ({ if (!schematicTraceId || !sourceTraceId) continue schematicTraceIdToSourceTraceId.set(schematicTraceId, sourceTraceId) - const schematicTraceIds = - sourceTraceIdToSchematicTraceIds.get(sourceTraceId) ?? new Set() - schematicTraceIds.add(schematicTraceId) - sourceTraceIdToSchematicTraceIds.set(sourceTraceId, schematicTraceIds) + addToSetMap( + sourceTraceIdToSchematicTraceIds, + sourceTraceId, + schematicTraceId, + ) + + const solverPortIds = getSolverSourcePortSelectors(sourceTraceId) + .map((portSelector) => sourcePortSelectorToPortId.get(portSelector)) + .filter(Boolean) as string[] + + if (solverPortIds.length > 0) { + const netIds = sourceTraceIdToNetIds.get(sourceTraceId) ?? new Set() + const portIds = sourceTraceIdToPortIds.get(sourceTraceId) ?? new Set() + + for (const sourcePortId of solverPortIds) { + portIds.add(sourcePortId) + + const connectivityKey = + sourcePortIdToConnectivityKey.get(sourcePortId) + if (connectivityKey) { + netIds.add(`connectivity:${connectivityKey}`) + } + } + + sourceTraceIdToPortIds.set(sourceTraceId, portIds) + sourceTraceIdToNetIds.set(sourceTraceId, netIds) + + for (const sourcePortId of portIds) { + addToSetMap(portIdToSourceTraceIds, sourcePortId, sourceTraceId) + } + + for (const netId of netIds) { + addToSetMap(netIdToSourceTraceIds, netId, sourceTraceId) + } + } } const ensureHoverStyle = () => { From d77f555f41bc7bdf609829c49c92f4095d1ed3c0 Mon Sep 17 00:00:00 2001 From: tttislandyyy Date: Fri, 5 Jun 2026 21:00:53 +0800 Subject: [PATCH 4/5] Add connected trace hover tests --- .../useHighlightConnectedTracesOnHover.ts | 291 ++++++++++-------- ...ighlight-connected-traces-on-hover.test.ts | 162 ++++++++++ 2 files changed, 318 insertions(+), 135 deletions(-) create mode 100644 tests/use-highlight-connected-traces-on-hover.test.ts diff --git a/lib/hooks/useHighlightConnectedTracesOnHover.ts b/lib/hooks/useHighlightConnectedTracesOnHover.ts index 78b8fac..6d7b5ea 100644 --- a/lib/hooks/useHighlightConnectedTracesOnHover.ts +++ b/lib/hooks/useHighlightConnectedTracesOnHover.ts @@ -60,142 +60,177 @@ const getConnectedSourceTraceIds = ( return connectedSourceTraceIds } -export const useHighlightConnectedTracesOnHover = ({ - svgDivRef, - circuitJson, - circuitJsonKey, -}: { - svgDivRef: React.RefObject - circuitJson: CircuitJson - circuitJsonKey: string -}) => { - useEffect(() => { - const svg = svgDivRef.current - if (!svg) return +export const getConnectedSchematicTraceIdsByHoveredTrace = ( + circuitJson: CircuitJson, +) => { + const schematicTraceIdToSourceTraceId = new Map() + const sourceTraceIdToSchematicTraceIds = new Map>() + const sourceTraceIdToNetIds = new Map>() + const netIdToSourceTraceIds = new Map>() + const sourceTraceIdToPortIds = new Map>() + const portIdToSourceTraceIds = new Map>() + const sourcePortSelectorToPortId = new Map() + const sourcePortIdToConnectivityKey = new Map() + + for (const sourceComponent of su( + circuitJson, + ).source_component.list() as any[]) { + const sourceComponentId = sourceComponent.source_component_id as + | string + | undefined + const componentName = sourceComponent.name as string | undefined + if (!sourceComponentId || !componentName) continue + + for (const sourcePort of su(circuitJson).source_port.list({ + source_component_id: sourceComponentId, + }) as any[]) { + const sourcePortId = sourcePort.source_port_id as string | undefined + if (!sourcePortId) continue + + const portSelectors = [ + sourcePort.name, + sourcePort.pin_number, + ...(sourcePort.port_hints ?? []), + ] + .filter(Boolean) + .map(String) + + for (const portSelector of portSelectors) { + sourcePortSelectorToPortId.set( + `${componentName}.${portSelector}`, + sourcePortId, + ) + } - const schematicTraceIdToSourceTraceId = new Map() - const sourceTraceIdToSchematicTraceIds = new Map>() - const sourceTraceIdToNetIds = new Map>() - const netIdToSourceTraceIds = new Map>() - const sourceTraceIdToPortIds = new Map>() - const portIdToSourceTraceIds = new Map>() - const sourcePortSelectorToPortId = new Map() - const sourcePortIdToConnectivityKey = new Map() - - for (const sourceComponent of su( - circuitJson, - ).source_component.list() as any[]) { - const sourceComponentId = sourceComponent.source_component_id as + const connectivityKey = sourcePort.subcircuit_connectivity_map_key as | string | undefined - const componentName = sourceComponent.name as string | undefined - if (!sourceComponentId || !componentName) continue - - for (const sourcePort of su(circuitJson).source_port.list({ - source_component_id: sourceComponentId, - }) as any[]) { - const sourcePortId = sourcePort.source_port_id as string | undefined - if (!sourcePortId) continue - - const portSelectors = [ - sourcePort.name, - sourcePort.pin_number, - ...(sourcePort.port_hints ?? []), - ] - .filter(Boolean) - .map(String) - - for (const portSelector of portSelectors) { - sourcePortSelectorToPortId.set( - `${componentName}.${portSelector}`, - sourcePortId, - ) - } - - const connectivityKey = sourcePort.subcircuit_connectivity_map_key as - | string - | undefined - if (connectivityKey) { - sourcePortIdToConnectivityKey.set(sourcePortId, connectivityKey) - } + if (connectivityKey) { + sourcePortIdToConnectivityKey.set(sourcePortId, connectivityKey) } } + } - for (const sourceTrace of su(circuitJson).source_trace.list() as any[]) { - const sourceTraceId = sourceTrace.source_trace_id as string | undefined - if (!sourceTraceId) continue + for (const sourceTrace of su(circuitJson).source_trace.list() as any[]) { + const sourceTraceId = sourceTrace.source_trace_id as string | undefined + if (!sourceTraceId) continue - const netIds = new Set( - (sourceTrace.connected_source_net_ids ?? []).filter(Boolean), - ) - if (sourceTrace.subcircuit_connectivity_map_key) { - netIds.add( - `connectivity:${sourceTrace.subcircuit_connectivity_map_key}`, - ) - } - sourceTraceIdToNetIds.set(sourceTraceId, netIds) + const netIds = new Set( + (sourceTrace.connected_source_net_ids ?? []).filter(Boolean), + ) + if (sourceTrace.subcircuit_connectivity_map_key) { + netIds.add(`connectivity:${sourceTrace.subcircuit_connectivity_map_key}`) + } + sourceTraceIdToNetIds.set(sourceTraceId, netIds) - for (const netId of netIds) { - addToSetMap(netIdToSourceTraceIds, netId, sourceTraceId) - } + for (const netId of netIds) { + addToSetMap(netIdToSourceTraceIds, netId, sourceTraceId) + } - const portIds = new Set( - (sourceTrace.connected_source_port_ids ?? []).filter(Boolean), - ) - sourceTraceIdToPortIds.set(sourceTraceId, portIds) + const portIds = new Set( + (sourceTrace.connected_source_port_ids ?? []).filter(Boolean), + ) + sourceTraceIdToPortIds.set(sourceTraceId, portIds) - for (const portId of portIds) { - addToSetMap(portIdToSourceTraceIds, portId, sourceTraceId) - } + for (const portId of portIds) { + addToSetMap(portIdToSourceTraceIds, portId, sourceTraceId) } + } - for (const schematicTrace of su( - circuitJson, - ).schematic_trace.list() as any[]) { - const schematicTraceId = schematicTrace.schematic_trace_id as - | string - | undefined - const sourceTraceId = schematicTrace.source_trace_id as string | undefined + for (const schematicTrace of su( + circuitJson, + ).schematic_trace.list() as any[]) { + const schematicTraceId = schematicTrace.schematic_trace_id as + | string + | undefined + const sourceTraceId = schematicTrace.source_trace_id as string | undefined - if (!schematicTraceId || !sourceTraceId) continue + if (!schematicTraceId || !sourceTraceId) continue - schematicTraceIdToSourceTraceId.set(schematicTraceId, sourceTraceId) - addToSetMap( - sourceTraceIdToSchematicTraceIds, - sourceTraceId, - schematicTraceId, - ) + schematicTraceIdToSourceTraceId.set(schematicTraceId, sourceTraceId) + addToSetMap( + sourceTraceIdToSchematicTraceIds, + sourceTraceId, + schematicTraceId, + ) - const solverPortIds = getSolverSourcePortSelectors(sourceTraceId) - .map((portSelector) => sourcePortSelectorToPortId.get(portSelector)) - .filter(Boolean) as string[] + const solverPortIds = getSolverSourcePortSelectors(sourceTraceId) + .map((portSelector) => sourcePortSelectorToPortId.get(portSelector)) + .filter(Boolean) as string[] - if (solverPortIds.length > 0) { - const netIds = sourceTraceIdToNetIds.get(sourceTraceId) ?? new Set() - const portIds = sourceTraceIdToPortIds.get(sourceTraceId) ?? new Set() + if (solverPortIds.length > 0) { + const netIds = sourceTraceIdToNetIds.get(sourceTraceId) ?? new Set() + const portIds = sourceTraceIdToPortIds.get(sourceTraceId) ?? new Set() - for (const sourcePortId of solverPortIds) { - portIds.add(sourcePortId) + for (const sourcePortId of solverPortIds) { + portIds.add(sourcePortId) - const connectivityKey = - sourcePortIdToConnectivityKey.get(sourcePortId) - if (connectivityKey) { - netIds.add(`connectivity:${connectivityKey}`) - } + const connectivityKey = sourcePortIdToConnectivityKey.get(sourcePortId) + if (connectivityKey) { + netIds.add(`connectivity:${connectivityKey}`) } + } - sourceTraceIdToPortIds.set(sourceTraceId, portIds) - sourceTraceIdToNetIds.set(sourceTraceId, netIds) + sourceTraceIdToPortIds.set(sourceTraceId, portIds) + sourceTraceIdToNetIds.set(sourceTraceId, netIds) - for (const sourcePortId of portIds) { - addToSetMap(portIdToSourceTraceIds, sourcePortId, sourceTraceId) - } + for (const sourcePortId of portIds) { + addToSetMap(portIdToSourceTraceIds, sourcePortId, sourceTraceId) + } - for (const netId of netIds) { - addToSetMap(netIdToSourceTraceIds, netId, sourceTraceId) - } + for (const netId of netIds) { + addToSetMap(netIdToSourceTraceIds, netId, sourceTraceId) } } + } + + const connectedSchematicTraceIdsByTraceId = new Map>() + + for (const [ + schematicTraceId, + sourceTraceId, + ] of schematicTraceIdToSourceTraceId) { + const connectedSchematicTraceIds = new Set() + const connectedSourceTraceIds = getConnectedSourceTraceIds( + sourceTraceId, + sourceTraceIdToNetIds, + netIdToSourceTraceIds, + sourceTraceIdToPortIds, + portIdToSourceTraceIds, + ) + + for (const connectedSourceTraceId of connectedSourceTraceIds) { + for (const connectedSchematicTraceId of sourceTraceIdToSchematicTraceIds.get( + connectedSourceTraceId, + ) ?? []) { + connectedSchematicTraceIds.add(connectedSchematicTraceId) + } + } + + connectedSchematicTraceIdsByTraceId.set( + schematicTraceId, + connectedSchematicTraceIds, + ) + } + + return connectedSchematicTraceIdsByTraceId +} + +export const useHighlightConnectedTracesOnHover = ({ + svgDivRef, + circuitJson, + circuitJsonKey, +}: { + svgDivRef: React.RefObject + circuitJson: CircuitJson + circuitJsonKey: string +}) => { + useEffect(() => { + const svg = svgDivRef.current + if (!svg) return + + const connectedSchematicTraceIdsByTraceId = + getConnectedSchematicTraceIdsByHoveredTrace(circuitJson) const ensureHoverStyle = () => { if (!svg.querySelector(`style#${HOVER_STYLE_ID}`)) { @@ -223,29 +258,15 @@ export const useHighlightConnectedTracesOnHover = ({ const highlightConnectedTraces = (schematicTraceId: string) => { clearHighlightedTraces() - const sourceTraceId = - schematicTraceIdToSourceTraceId.get(schematicTraceId) - if (!sourceTraceId) return - - const connectedSourceTraceIds = getConnectedSourceTraceIds( - sourceTraceId, - sourceTraceIdToNetIds, - netIdToSourceTraceIds, - sourceTraceIdToPortIds, - portIdToSourceTraceIds, - ) - - for (const connectedSourceTraceId of connectedSourceTraceIds) { - for (const connectedSchematicTraceId of sourceTraceIdToSchematicTraceIds.get( - connectedSourceTraceId, - ) ?? []) { - for (const traceElement of Array.from( - svg.querySelectorAll( - `[data-circuit-json-type="schematic_trace"][data-schematic-trace-id="${connectedSchematicTraceId}"]`, - ), - )) { - traceElement.setAttribute(HOVER_ATTR, "true") - } + for (const connectedSchematicTraceId of connectedSchematicTraceIdsByTraceId.get( + schematicTraceId, + ) ?? []) { + for (const traceElement of Array.from( + svg.querySelectorAll( + `[data-circuit-json-type="schematic_trace"][data-schematic-trace-id="${connectedSchematicTraceId}"]`, + ), + )) { + traceElement.setAttribute(HOVER_ATTR, "true") } } } diff --git a/tests/use-highlight-connected-traces-on-hover.test.ts b/tests/use-highlight-connected-traces-on-hover.test.ts new file mode 100644 index 0000000..50f3667 --- /dev/null +++ b/tests/use-highlight-connected-traces-on-hover.test.ts @@ -0,0 +1,162 @@ +import { describe, expect, test } from "bun:test" +import { getConnectedSchematicTraceIdsByHoveredTrace } from "lib/hooks/useHighlightConnectedTracesOnHover" + +const getConnectedTraceIds = ( + circuitJson: any[], + hoveredSchematicTraceId: string, +) => + [ + ...(getConnectedSchematicTraceIdsByHoveredTrace(circuitJson as any).get( + hoveredSchematicTraceId, + ) ?? []), + ].sort() + +describe("getConnectedSchematicTraceIdsByHoveredTrace", () => { + test("groups traces through connected source net ids", () => { + const circuitJson = [ + { + type: "source_trace", + source_trace_id: "source_trace_gnd_a", + connected_source_net_ids: ["source_net_gnd"], + }, + { + type: "source_trace", + source_trace_id: "source_trace_gnd_b", + connected_source_net_ids: ["source_net_gnd"], + }, + { + type: "source_trace", + source_trace_id: "source_trace_sig", + connected_source_net_ids: ["source_net_sig"], + }, + { + type: "schematic_trace", + schematic_trace_id: "schematic_trace_gnd_a", + source_trace_id: "source_trace_gnd_a", + }, + { + type: "schematic_trace", + schematic_trace_id: "schematic_trace_gnd_b", + source_trace_id: "source_trace_gnd_b", + }, + { + type: "schematic_trace", + schematic_trace_id: "schematic_trace_sig", + source_trace_id: "source_trace_sig", + }, + ] + + expect(getConnectedTraceIds(circuitJson, "schematic_trace_gnd_a")).toEqual([ + "schematic_trace_gnd_a", + "schematic_trace_gnd_b", + ]) + }) + + test("falls back to connected source ports when net ids are unavailable", () => { + const circuitJson = [ + { + type: "source_trace", + source_trace_id: "source_trace_a", + connected_source_port_ids: ["source_port_shared"], + }, + { + type: "source_trace", + source_trace_id: "source_trace_b", + connected_source_port_ids: ["source_port_shared"], + }, + { + type: "source_trace", + source_trace_id: "source_trace_c", + connected_source_port_ids: ["source_port_other"], + }, + { + type: "schematic_trace", + schematic_trace_id: "schematic_trace_a", + source_trace_id: "source_trace_a", + }, + { + type: "schematic_trace", + schematic_trace_id: "schematic_trace_b", + source_trace_id: "source_trace_b", + }, + { + type: "schematic_trace", + schematic_trace_id: "schematic_trace_c", + source_trace_id: "source_trace_c", + }, + ] + + expect(getConnectedTraceIds(circuitJson, "schematic_trace_a")).toEqual([ + "schematic_trace_a", + "schematic_trace_b", + ]) + }) + + test("resolves solver trace endpoints through source port connectivity keys", () => { + const circuitJson = [ + { + type: "source_component", + source_component_id: "source_component_r1", + name: "R1", + }, + { + type: "source_component", + source_component_id: "source_component_u1", + name: "U1", + }, + { + type: "source_component", + source_component_id: "source_component_c1", + name: "C1", + }, + { + type: "source_port", + source_port_id: "source_port_r1_1", + source_component_id: "source_component_r1", + pin_number: "1", + subcircuit_connectivity_map_key: "net_gnd", + }, + { + type: "source_port", + source_port_id: "source_port_r1_2", + source_component_id: "source_component_r1", + pin_number: "2", + subcircuit_connectivity_map_key: "net_sig", + }, + { + type: "source_port", + source_port_id: "source_port_u1_4", + source_component_id: "source_component_u1", + pin_number: "4", + subcircuit_connectivity_map_key: "net_gnd", + }, + { + type: "source_port", + source_port_id: "source_port_c1_1", + source_component_id: "source_component_c1", + pin_number: "1", + subcircuit_connectivity_map_key: "net_sig", + }, + { + type: "schematic_trace", + schematic_trace_id: "schematic_trace_gnd_a", + source_trace_id: "solver_R1.1-U1.4", + }, + { + type: "schematic_trace", + schematic_trace_id: "schematic_trace_gnd_b", + source_trace_id: "solver_U1.4-R1.1", + }, + { + type: "schematic_trace", + schematic_trace_id: "schematic_trace_sig", + source_trace_id: "solver_R1.2-C1.1", + }, + ] + + expect(getConnectedTraceIds(circuitJson, "schematic_trace_gnd_a")).toEqual([ + "schematic_trace_gnd_a", + "schematic_trace_gnd_b", + ]) + }) +}) From 1548db9a15f6f10a4f5b65f3edb1b5a8e5062a8e Mon Sep 17 00:00:00 2001 From: tttislandyyy Date: Fri, 5 Jun 2026 21:17:14 +0800 Subject: [PATCH 5/5] Let trace hover receive pointer events --- lib/hooks/useHighlightConnectedTracesOnHover.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/hooks/useHighlightConnectedTracesOnHover.ts b/lib/hooks/useHighlightConnectedTracesOnHover.ts index 6d7b5ea..116244b 100644 --- a/lib/hooks/useHighlightConnectedTracesOnHover.ts +++ b/lib/hooks/useHighlightConnectedTracesOnHover.ts @@ -237,6 +237,10 @@ export const useHighlightConnectedTracesOnHover = ({ const style = document.createElement("style") style.id = HOVER_STYLE_ID style.textContent = ` + .boundary { + pointer-events: none; + } + [data-circuit-json-type="schematic_trace"][${HOVER_ATTR}="true"] { filter: invert(1); }