Skip to content
Open
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
119 changes: 117 additions & 2 deletions lib/components/SchematicViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ export const SchematicViewer = ({
})
const [isHoveringClickableComponent, setIsHoveringClickableComponent] =
useState(false)
const [hoveredNetId, setHoveredNetId] = useState<string | null>(null)

const hoveringComponentsRef = useRef<Set<string>>(new Set())

const handleComponentHoverChange = useCallback(
Expand Down Expand Up @@ -154,6 +156,34 @@ export const SchematicViewer = ({
}
}, [circuitJsonKey, circuitJson])

const traceIdToNetId = useMemo(() => {
const map = new Map<string, string>()

try {
const soup = su(circuitJson)
const traces = soup.schematic_trace?.list?.() ?? []

for (const trace of traces as any[]) {
const t = trace as any

// net id may be stored as net_id or netId
const netId = (t.net_id ?? t.netId) as string | undefined

// schematic_trace_id is typed; id is a possible runtime alias
const traceId =
(t.schematic_trace_id as string | undefined) ??
(t.id as string | undefined)

if (!traceId || !netId) continue
map.set(traceId, netId)
}
} catch (err) {
console.error("Failed to derive traceIdToNetId map", err)
}

return map
}, [circuitJsonKey, circuitJson])

const handleTouchStart = (e: React.TouchEvent) => {
const touch = e.touches[0]
touchStartRef.current = {
Expand Down Expand Up @@ -207,6 +237,7 @@ export const SchematicViewer = ({
})

const { containerWidth, containerHeight } = useResizeHandling(containerRef)

const svgString = useMemo(() => {
if (!containerWidth || !containerHeight) return ""

Expand All @@ -223,6 +254,33 @@ export const SchematicViewer = ({
})
}, [circuitJsonKey, containerWidth, containerHeight])

const svgWithNetIds = useMemo(() => {
if (!svgString || typeof window === "undefined") return svgString

try {
const doc = new DOMParser().parseFromString(svgString, "image/svg+xml")

// Select all traces
const traces = doc.querySelectorAll<SVGElement>("path.trace")

// Assign each trace a net ID (fallback: use trace id itself)
traces.forEach((el, index) => {
const rawId =
el.getAttribute("data-schematic-trace-id") ||
el.getAttribute("data-schematic-object-id")

const netId =
traceIdToNetId.get(rawId || "") || rawId || `trace-${index}`
el.setAttribute("data-net-id", netId)
})

return new XMLSerializer().serializeToString(doc.documentElement)
} catch (err) {
console.error("Failed to add net ids", err)
return svgString
}
}, [svgString, traceIdToNetId])

const containerBackgroundColor = useMemo(() => {
const match = svgString.match(
/<svg[^>]*style="[^"]*background-color:\s*([^;\"]+)/i,
Expand Down Expand Up @@ -300,6 +358,54 @@ export const SchematicViewer = ({
handleComponentTouchStartRef.current = handleComponentTouchStart
}, [handleComponentTouchStart])

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

const elements = Array.from(
svgDiv.querySelectorAll<SVGElement>("[data-net-id]"),
)

function enter(this: SVGElement) {
const id = this.getAttribute("data-net-id")
if (id) setHoveredNetId(id)
}

function leave(this: SVGElement) {
const id = this.getAttribute("data-net-id")
if (id) setHoveredNetId((prev) => (prev === id ? null : prev))
}

elements.forEach((el) => {
el.addEventListener("mouseenter", enter)
el.addEventListener("mouseleave", leave)
})

return () => {
elements.forEach((el) => {
el.removeEventListener("mouseenter", enter)
el.removeEventListener("mouseleave", leave)
})
}
}, [svgWithNetIds])

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

// Remove old highlights
svgDiv
.querySelectorAll("[data-net-id].trace-hover")
.forEach((el) => el.classList.remove("trace-hover"))

if (!hoveredNetId) return

// Highlight all traces sharing this net-id
svgDiv
.querySelectorAll(`[data-net-id="${hoveredNetId}"]`)
.forEach((el) => el.classList.add("trace-hover"))
}, [hoveredNetId])

const svgDiv = useMemo(
() => (
<div
Expand All @@ -323,11 +429,11 @@ export const SchematicViewer = ({
}
}}
// biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation>
dangerouslySetInnerHTML={{ __html: svgString }}
dangerouslySetInnerHTML={{ __html: svgWithNetIds }}
/>
),
[
svgString,
svgWithNetIds,
isInteractionEnabled,
clickToInteractEnabled,
editModeEnabled,
Expand All @@ -342,6 +448,15 @@ export const SchematicViewer = ({
{`.schematic-component-clickable [data-schematic-component-id]:hover { cursor: pointer !important; }`}
</style>
)}
<style>
{`
.trace-hover {
stroke: #3399ff !important;
stroke-width: 2.5px !important;
}
`}
</style>

<div
ref={containerRef}
style={{
Expand Down