Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified bun.lockb
Binary file not shown.
7 changes: 7 additions & 0 deletions lib/components/SchematicViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
import { useMouseMatrixTransform } from "use-mouse-matrix-transform"
import { useResizeHandling } from "../hooks/use-resize-handling"
import { useComponentDragging } from "../hooks/useComponentDragging"
import { useTraceHoverHighlight } from "lib/hooks/useTraceHoverHighlight"
import type { ManualEditEvent } from "../types/edit-events"
import { EditIcon } from "./EditIcon"
import { GridIcon } from "./GridIcon"
Expand Down Expand Up @@ -353,6 +354,8 @@ export const SchematicViewer = ({
showGroups: showSchematicGroups && !disableGroups,
})

useTraceHoverHighlight({ svgDivRef, circuitJson })

// keep the latest touch handler without re-rendering the svg div
const handleComponentTouchStartRef = useRef(handleComponentTouchStart)
useEffect(() => {
Expand Down Expand Up @@ -406,6 +409,10 @@ export const SchematicViewer = ({
{`[data-schematic-port-id]:hover { cursor: pointer !important; }`}
</style>
)}
<style>
{`.trace-highlighted { filter: invert(1); }
.trace-highlighted .trace-crossing-outline { opacity: 0; }`}
</style>
<div
ref={containerRef}
style={{
Expand Down
149 changes: 149 additions & 0 deletions lib/hooks/useTraceHoverHighlight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { useCallback, useEffect, useRef } from "react"
import type { CircuitJson } from "circuit-json"
import { su } from "@tscircuit/soup-util"

type SvgDivRef = React.RefObject<HTMLDivElement | null>

const TRACE_SEL = "g.trace[data-subcircuit-connectivity-map-key]"

function getConnectivityKey(el: Element): string | null {
return el.getAttribute("data-subcircuit-connectivity-map-key")
}

/**
* Hook that highlights all traces sharing the same connectivity key
* when any trace in that net is hovered.
*
* Works by attaching mouseenter/mouseleave handlers to every `<g class="trace">`
* element and toggling a `.trace-highlighted` class on all traces in the same net.
*
* The approach avoids `:has()` CSS selectors (which have inconsistent SVG support)
* and instead uses direct DOM class manipulation on mouse events.
*/
export function useTraceHoverHighlight({
svgDivRef,
circuitJson,
}: {
svgDivRef: SvgDivRef
circuitJson: CircuitJson
}) {
const highlightedKeyRef = useRef<string | null>(null)
const isHoveringRef = useRef(false)

const getNetKeyToIdMap = useCallback(() => {
const map = new Map<string, string[]>()
for (const elm of circuitJson) {
if (elm.type !== "schematic_trace") continue
const key = (elm as any).subcircuit_connectivity_map_key as
| string
| undefined
const id = (elm as any).schematic_trace_id as string | undefined
if (!key || !id) continue
const ids = map.get(key) ?? []
ids.push(id)
map.set(key, ids)
}
return map
}, [circuitJson])

const applyHighlightForTrace = useCallback(
(traceEl: Element) => {
const key = getConnectivityKey(traceEl)
if (!key) return

highlightedKeyRef.current = key

const svgDiv = svgDivRef.current
if (!svgDiv) return

const allTraces = svgDiv.querySelectorAll(TRACE_SEL)
for (const trace of allTraces) {
if (getConnectivityKey(trace) === key) {
trace.classList.add("trace-highlighted")
} else {
trace.classList.remove("trace-highlighted")
}
}

// Also highlight the overlay groups
const allOverlays = svgDiv.querySelectorAll(
"g.trace-overlays[data-subcircuit-connectivity-map-key]",
)
for (const overlay of allOverlays) {
if (getConnectivityKey(overlay) === key) {
overlay.classList.add("trace-highlighted")
} else {
overlay.classList.remove("trace-highlighted")
}
}
},
[svgDivRef],
)

const removeHighlight = useCallback(() => {
highlightedKeyRef.current = null

const svgDiv = svgDivRef.current
if (!svgDiv) return

const highlighted = svgDiv.querySelectorAll(".trace-highlighted")
for (const el of highlighted) {
el.classList.remove("trace-highlighted")
}
}, [svgDivRef])

useEffect(() => {
const svgDiv = svgDivRef.current
if (!svgDiv) return

const handleMouseEnter = (e: Event) => {
if (isHoveringRef.current) return
isHoveringRef.current = true
const target = e.currentTarget as Element
applyHighlightForTrace(target)
}

const handleMouseLeave = (e: Event) => {
const target = e.currentTarget as Element
const relatedTarget = (e as MouseEvent).relatedTarget as Element | null

// Check if the mouse is moving to another trace in the same net
if (
relatedTarget &&
getConnectivityKey(relatedTarget) === getConnectivityKey(target)
) {
return
}

isHoveringRef.current = false
removeHighlight()
}

// Attach listeners to all trace groups
const traces = svgDiv.querySelectorAll(TRACE_SEL)
for (const trace of traces) {
trace.addEventListener("mouseenter", handleMouseEnter)
trace.addEventListener("mouseleave", handleMouseLeave)
}

// Also attach to overlay groups so hovering over crossings/hops also works
const overlays = svgDiv.querySelectorAll(
"g.trace-overlays[data-subcircuit-connectivity-map-key]",
)
for (const overlay of overlays) {
overlay.addEventListener("mouseenter", handleMouseEnter)
overlay.addEventListener("mouseleave", handleMouseLeave)
}

return () => {
for (const trace of traces) {
trace.removeEventListener("mouseenter", handleMouseEnter)
trace.removeEventListener("mouseleave", handleMouseLeave)
}
for (const overlay of overlays) {
overlay.removeEventListener("mouseenter", handleMouseEnter)
overlay.removeEventListener("mouseleave", handleMouseLeave)
}
}
}, [svgDivRef, applyHighlightForTrace, removeHighlight])
}
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
"@biomejs/biome": "^1.9.4",
"@types/bun": "latest",
"@types/debug": "^4.1.12",
"@types/jsdom": "^28.0.3",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"@types/recharts": "^2.0.1",
"@vitejs/plugin-react": "^4.3.4",
"jsdom": "^29.1.1",
"react": "^19.1.0",
"react-cosmos": "^6.2.1",
"react-cosmos-plugin-vite": "^6.2.0",
Expand Down
Loading