From b7c16016401d50f10a1ba4255736f4f7c7626b69 Mon Sep 17 00:00:00 2001 From: FizaSiddique123 Date: Sun, 7 Dec 2025 13:56:02 +0530 Subject: [PATCH 1/2] Fix schematic trace hover highlighting (#1130) --- lib/components/SchematicViewer.tsx | 134 +++++++++++++++++++++++++++-- 1 file changed, 127 insertions(+), 7 deletions(-) diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index 51e782a..84b1a9e 100644 --- a/lib/components/SchematicViewer.tsx +++ b/lib/components/SchematicViewer.tsx @@ -125,6 +125,8 @@ export const SchematicViewer = ({ }) const [isHoveringClickableComponent, setIsHoveringClickableComponent] = useState(false) + const [hoveredNetId, setHoveredNetId] = useState(null) + const hoveringComponentsRef = useRef>(new Set()) const handleComponentHoverChange = useCallback( @@ -146,7 +148,9 @@ export const SchematicViewer = ({ return ( su(circuitJson) .schematic_component?.list() - ?.map((component) => component.schematic_component_id as string) ?? [] + ?.map( + (component) => component.schematic_component_id as string, + ) ?? [] ) } catch (err) { console.error("Failed to derive schematic component ids", err) @@ -154,6 +158,35 @@ export const SchematicViewer = ({ } }, [circuitJsonKey, circuitJson]) + const traceIdToNetId = useMemo(() => { + const map = new Map() + + 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 = { @@ -207,6 +240,7 @@ export const SchematicViewer = ({ }) const { containerWidth, containerHeight } = useResizeHandling(containerRef) + const svgString = useMemo(() => { if (!containerWidth || !containerHeight) return "" @@ -216,13 +250,40 @@ export const SchematicViewer = ({ grid: !debugGrid ? undefined : { - cellSize: 1, - labelCells: true, - }, + cellSize: 1, + labelCells: true, + }, colorOverrides, }) }, [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("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( /]*style="[^"]*background-color:\s*([^;\"]+)/i, @@ -300,6 +361,55 @@ export const SchematicViewer = ({ handleComponentTouchStartRef.current = handleComponentTouchStart }, [handleComponentTouchStart]) + useEffect(() => { + const svgDiv = svgDivRef.current; + if (!svgDiv) return; + + const elements = Array.from( + svgDiv.querySelectorAll("[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( () => (
- dangerouslySetInnerHTML={{ __html: svgString }} + dangerouslySetInnerHTML={{ __html: svgWithNetIds }} /> ), [ - svgString, + svgWithNetIds, isInteractionEnabled, clickToInteractEnabled, editModeEnabled, @@ -342,6 +452,16 @@ export const SchematicViewer = ({ {`.schematic-component-clickable [data-schematic-component-id]:hover { cursor: pointer !important; }`} )} + + +
{typeof window !== "undefined" && - ("ontouchstart" in window || navigator.maxTouchPoints > 0) + ("ontouchstart" in window || navigator.maxTouchPoints > 0) ? "Touch to Interact" : "Click to Interact"}
From 32a58281fd47e8e8b7699350d33c7e68afd57234 Mon Sep 17 00:00:00 2001 From: FizaSiddique123 Date: Sun, 7 Dec 2025 14:01:22 +0530 Subject: [PATCH 2/2] chore: format code --- lib/components/SchematicViewer.tsx | 87 ++++++++++++++---------------- 1 file changed, 41 insertions(+), 46 deletions(-) diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index 84b1a9e..a41af5e 100644 --- a/lib/components/SchematicViewer.tsx +++ b/lib/components/SchematicViewer.tsx @@ -148,9 +148,7 @@ export const SchematicViewer = ({ return ( su(circuitJson) .schematic_component?.list() - ?.map( - (component) => component.schematic_component_id as string, - ) ?? [] + ?.map((component) => component.schematic_component_id as string) ?? [] ) } catch (err) { console.error("Failed to derive schematic component ids", err) @@ -186,7 +184,6 @@ export const SchematicViewer = ({ return map }, [circuitJsonKey, circuitJson]) - const handleTouchStart = (e: React.TouchEvent) => { const touch = e.touches[0] touchStartRef.current = { @@ -250,39 +247,39 @@ export const SchematicViewer = ({ grid: !debugGrid ? undefined : { - cellSize: 1, - labelCells: true, - }, + cellSize: 1, + labelCells: true, + }, colorOverrides, }) }, [circuitJsonKey, containerWidth, containerHeight]) const svgWithNetIds = useMemo(() => { - if (!svgString || typeof window === "undefined") return svgString; + if (!svgString || typeof window === "undefined") return svgString try { - const doc = new DOMParser().parseFromString(svgString, "image/svg+xml"); + const doc = new DOMParser().parseFromString(svgString, "image/svg+xml") // Select all traces - const traces = doc.querySelectorAll("path.trace"); + const traces = doc.querySelectorAll("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"); + el.getAttribute("data-schematic-object-id") - const netId = traceIdToNetId.get(rawId || "") || rawId || `trace-${index}`; - el.setAttribute("data-net-id", netId); - }); + const netId = + traceIdToNetId.get(rawId || "") || rawId || `trace-${index}` + el.setAttribute("data-net-id", netId) + }) - return new XMLSerializer().serializeToString(doc.documentElement); + return new XMLSerializer().serializeToString(doc.documentElement) } catch (err) { - console.error("Failed to add net ids", err); - return svgString; + console.error("Failed to add net ids", err) + return svgString } - }, [svgString, traceIdToNetId]); - + }, [svgString, traceIdToNetId]) const containerBackgroundColor = useMemo(() => { const match = svgString.match( @@ -362,53 +359,52 @@ export const SchematicViewer = ({ }, [handleComponentTouchStart]) useEffect(() => { - const svgDiv = svgDivRef.current; - if (!svgDiv) return; + const svgDiv = svgDivRef.current + if (!svgDiv) return const elements = Array.from( - svgDiv.querySelectorAll("[data-net-id]") - ); + svgDiv.querySelectorAll("[data-net-id]"), + ) function enter(this: SVGElement) { - const id = this.getAttribute("data-net-id"); - if (id) setHoveredNetId(id); + 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)); + 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); - }); + elements.forEach((el) => { + el.addEventListener("mouseenter", enter) + el.addEventListener("mouseleave", leave) + }) return () => { - elements.forEach(el => { - el.removeEventListener("mouseenter", enter); - el.removeEventListener("mouseleave", leave); - }); - }; - }, [svgWithNetIds]); + elements.forEach((el) => { + el.removeEventListener("mouseenter", enter) + el.removeEventListener("mouseleave", leave) + }) + } + }, [svgWithNetIds]) useEffect(() => { - const svgDiv = svgDivRef.current; - if (!svgDiv) return; + const svgDiv = svgDivRef.current + if (!svgDiv) return // Remove old highlights svgDiv .querySelectorAll("[data-net-id].trace-hover") - .forEach(el => el.classList.remove("trace-hover")); + .forEach((el) => el.classList.remove("trace-hover")) - if (!hoveredNetId) return; + if (!hoveredNetId) return // Highlight all traces sharing this net-id svgDiv .querySelectorAll(`[data-net-id="${hoveredNetId}"]`) - .forEach(el => el.classList.add("trace-hover")); - }, [hoveredNetId]); - + .forEach((el) => el.classList.add("trace-hover")) + }, [hoveredNetId]) const svgDiv = useMemo( () => ( @@ -461,7 +457,6 @@ export const SchematicViewer = ({ `} -
{typeof window !== "undefined" && - ("ontouchstart" in window || navigator.maxTouchPoints > 0) + ("ontouchstart" in window || navigator.maxTouchPoints > 0) ? "Touch to Interact" : "Click to Interact"}