-
Notifications
You must be signed in to change notification settings - Fork 38
implement highlight for connected trace when hovering over trace with mouse #118
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
JrmyDev
wants to merge
4
commits into
tscircuit:main
Choose a base branch
from
JrmyDev:feature/117-highlight-connected-traces
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
64 changes: 64 additions & 0 deletions
64
examples/example13-trace-hover-highlighting-demo.fixture.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| import { renderToCircuitJson } from "lib/dev/render-to-circuit-json"; | ||
| import { SchematicViewer } from "lib/index"; | ||
|
|
||
| const circuit = ( | ||
| <board width="80mm" height="60mm"> | ||
| {/* Components */} | ||
| <chip name="U1" footprint="dip8" pcbX={10} pcbY={10} /> | ||
| <chip name="U2" footprint="dip8" pcbX={50} pcbY={10} /> | ||
| <resistor name="R1" resistance="1000 ohm" pcbX={30} pcbY={30} /> | ||
| <resistor name="R2" resistance="330 ohm" pcbX={60} pcbY={30} /> | ||
| <capacitor name="C1" capacitance="100 nF" pcbX={20} pcbY={40} /> | ||
| <capacitor name="C2" capacitance="10 uF" pcbX={40} pcbY={40} /> | ||
| <led name="LED1" pcbX={70} pcbY={40} /> | ||
|
|
||
| {/* VCC POWER RAIL: Multiple traces connecting VCC pins and capacitor positive terminals */} | ||
| <trace from={".U1 > .pin8"} to={".U2 > .pin8"} /> | ||
| <trace from={".U1 > .pin8"} to={".C1 > .pin1"} /> | ||
| <trace from={".C1 > .pin1"} to={".C2 > .pin1"} /> | ||
|
|
||
| {/* GROUND RAIL: Multiple traces connecting GND pins and capacitor negative terminals */} | ||
| <trace from={".U1 > .pin4"} to={".U2 > .pin4"} /> | ||
| <trace from={".U1 > .pin4"} to={".C1 > .pin2"} /> | ||
| <trace from={".C1 > .pin2"} to={".C2 > .pin2"} /> | ||
| <trace from={".C2 > .pin2"} to={".R1 > .pin2"} /> | ||
|
|
||
| {/* SIGNAL NET 1: U1 output through R2 to LED */} | ||
| <trace from={".U1 > .pin1"} to={".R2 > .pin1"} /> | ||
| <trace from={".R2 > .pin2"} to={".LED1 > .anode"} /> | ||
|
|
||
| {/* SIGNAL NET 2: U1 to U2 communication */} | ||
| <trace from={".U1 > .pin2"} to={".U2 > .pin1"} /> | ||
|
|
||
| {/* SIGNAL NET 3: U2 output through R1 to ground (current sink) */} | ||
| <trace from={".U2 > .pin7"} to={".R1 > .pin1"} /> | ||
|
|
||
| {/* LED cathode to ground */} | ||
| <trace from={".LED1 > .cathode"} to={".U2 > .pin4"} /> | ||
|
|
||
| {/* Internal chip connections (isolated) */} | ||
| <trace from={".U1 > .pin3"} to={".U1 > .pin6"} /> | ||
| <trace from={".U2 > .pin2"} to={".U2 > .pin3"} /> | ||
| </board> | ||
| ); | ||
|
|
||
| /** | ||
| * Example showcasing the new trace hover highlighting feature | ||
| * | ||
| * Hover over any trace to see all connected traces in the same net highlighted. | ||
| * This helps visualize circuit connectivity and debug routing issues. | ||
| */ | ||
| export default () => { | ||
| const circuitJson = renderToCircuitJson(circuit); | ||
|
|
||
| return ( | ||
| <div style={{ position: "relative", height: "100%" }}> | ||
| <SchematicViewer | ||
| circuitJson={circuitJson} | ||
| containerStyle={{ height: "100%" }} | ||
| debugGrid | ||
| editingEnabled | ||
| /> | ||
| </div> | ||
| ); | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| import { useEffect, useRef } from "react"; | ||
| import { findConnectedTraceIds } from "../utils/trace-connectivity"; | ||
| import { | ||
| addTraceHighlightingStyles, | ||
| removeTraceHighlightingStyles, | ||
| } from "../utils/trace-highlighting-styles"; | ||
|
|
||
| interface useSubTraceHoverOptions { | ||
| svgDivRef: React.RefObject<HTMLDivElement | null>; | ||
| circuitJson: any[]; | ||
| enabled?: boolean; | ||
| } | ||
|
|
||
| /** | ||
| * Optimized trace highlighting using CSS classes and circuit-to-svg metadata | ||
| */ | ||
| export const useSubTraceHover = ({ | ||
| svgDivRef, | ||
| circuitJson, | ||
| enabled = true, | ||
| }: useSubTraceHoverOptions) => { | ||
| const activeNetRef = useRef<string | null>(null); | ||
| const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); | ||
|
|
||
| useEffect(() => { | ||
| if (!enabled || !svgDivRef.current || !circuitJson) { | ||
| return; | ||
| } | ||
|
|
||
| const svgContainer = svgDivRef.current; | ||
|
|
||
| const handleTraceHover = (event: Event) => { | ||
| const target = event.currentTarget as SVGElement; | ||
|
|
||
| const traceId = | ||
| target.getAttribute("data-schematic-trace-id") || | ||
| target.getAttribute("data-circuit-json-type") === "schematic_trace" | ||
| ? target.getAttribute("data-schematic-trace-id") | ||
| : null; | ||
|
|
||
| if (!traceId) return; | ||
|
|
||
| if (timeoutRef.current) { | ||
| clearTimeout(timeoutRef.current); | ||
| } | ||
|
|
||
| const connectedTraces = findConnectedTraceIds(circuitJson, traceId); | ||
| activeNetRef.current = traceId; | ||
|
|
||
| const svgElement = svgContainer.querySelector("svg"); | ||
| if (svgElement) { | ||
| svgElement.querySelectorAll(".trace-highlighted").forEach((el) => { | ||
| el.classList.remove("trace-highlighted"); | ||
| }); | ||
|
|
||
| connectedTraces.forEach((connectedTraceId) => { | ||
| const traceElement = svgElement.querySelector( | ||
| `[data-schematic-trace-id="${connectedTraceId}"]` | ||
| ); | ||
| if (traceElement) { | ||
| traceElement.classList.add("trace-highlighted"); | ||
| } | ||
| }); | ||
| } | ||
| }; | ||
|
|
||
| const handleTraceLeave = () => { | ||
| timeoutRef.current = setTimeout(() => { | ||
| const svgElement = svgContainer.querySelector("svg"); | ||
| if (svgElement) { | ||
| svgElement.querySelectorAll(".trace-highlighted").forEach((el) => { | ||
| el.classList.remove("trace-highlighted"); | ||
| }); | ||
| } | ||
| activeNetRef.current = null; | ||
| }, 50); | ||
| }; | ||
|
|
||
| const observer = new MutationObserver(() => { | ||
| const svgElement = svgContainer.querySelector("svg"); | ||
| if (svgElement) { | ||
| addTraceHighlightingStyles(svgContainer); | ||
|
|
||
| const traceElements = svgElement.querySelectorAll( | ||
| 'g.trace[data-circuit-json-type="schematic_trace"], [data-schematic-trace-id]' | ||
| ); | ||
|
|
||
| if (traceElements.length > 0) { | ||
| traceElements.forEach((el) => { | ||
| el.addEventListener("mouseenter", handleTraceHover); | ||
| el.addEventListener("mouseleave", handleTraceLeave); | ||
| }); | ||
|
|
||
| observer.disconnect(); | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| observer.observe(svgContainer, { childList: true, subtree: true }); | ||
|
|
||
| return () => { | ||
| observer.disconnect(); | ||
|
|
||
| const svgElement = svgContainer.querySelector("svg"); | ||
| if (svgElement) { | ||
| const traceElements = svgElement.querySelectorAll( | ||
| 'g.trace[data-circuit-json-type="schematic_trace"], [data-schematic-trace-id]' | ||
| ); | ||
| traceElements.forEach((el) => { | ||
| el.removeEventListener("mouseenter", handleTraceHover); | ||
| el.removeEventListener("mouseleave", handleTraceLeave); | ||
| }); | ||
| } | ||
|
|
||
| removeTraceHighlightingStyles(svgContainer); | ||
|
|
||
| if (timeoutRef.current) { | ||
| clearTimeout(timeoutRef.current); | ||
| } | ||
| }; | ||
| }, [svgDivRef, circuitJson, enabled]); | ||
JrmyDev marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| return { | ||
| currentHighlightedNet: activeNetRef.current, | ||
| }; | ||
| }; | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| import { su } from "@tscircuit/soup-util"; | ||
|
|
||
| /** | ||
| * Finds all schematic traces that are electrically connected to the given trace | ||
| * @param circuitJson The circuit JSON data | ||
| * @param hoveredSchematicTraceId The ID of the trace being hovered | ||
| * @returns Array of connected trace IDs (including the original trace) | ||
| */ | ||
| export const findConnectedTraceIds = ( | ||
| circuitJson: any[], | ||
| hoveredSchematicTraceId: string | ||
| ): string[] => { | ||
| try { | ||
| const soup = su(circuitJson); | ||
|
|
||
| const schematicTrace = soup.schematic_trace.get(hoveredSchematicTraceId); | ||
| if (!schematicTrace) { | ||
| return [hoveredSchematicTraceId]; | ||
| } | ||
|
|
||
| const allSchematicTraces = soup.schematic_trace.list(); | ||
| const allSourceTraces = soup.source_trace.list(); | ||
|
|
||
| let sourceTrace = soup.source_trace.get(schematicTrace.source_trace_id); | ||
|
|
||
| // Fallback: find by index if ID mismatch | ||
| if (!sourceTrace) { | ||
| const schematicTraceIndex = parseInt( | ||
| hoveredSchematicTraceId.split("_").pop() || "0" | ||
| ); | ||
| if (schematicTraceIndex < allSourceTraces.length) { | ||
| sourceTrace = allSourceTraces[schematicTraceIndex]; | ||
| } | ||
| } | ||
|
|
||
| if (!sourceTrace) { | ||
| return [hoveredSchematicTraceId]; | ||
| } | ||
|
|
||
| const connectedTraceIds = new Set<string>([hoveredSchematicTraceId]); | ||
|
|
||
| // Find traces with same connectivity key | ||
| const connectivityKey = sourceTrace.subcircuit_connectivity_map_key; | ||
| if (connectivityKey) { | ||
| for (const otherSourceTrace of allSourceTraces) { | ||
| if (otherSourceTrace.subcircuit_connectivity_map_key === connectivityKey) { | ||
| const sourceTraceIndex = allSourceTraces.findIndex( | ||
| (st) => st.source_trace_id === otherSourceTrace.source_trace_id | ||
| ); | ||
| if (sourceTraceIndex >= 0 && sourceTraceIndex < allSchematicTraces.length) { | ||
| const mappedSchematicTrace = allSchematicTraces[sourceTraceIndex]; | ||
| connectedTraceIds.add(mappedSchematicTrace.schematic_trace_id); | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return Array.from(connectedTraceIds); | ||
| } catch (error) { | ||
| return [hoveredSchematicTraceId]; | ||
| } | ||
| }; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| /** | ||
| * Optimized CSS-only approach for trace highlighting | ||
| * Instead of manipulating SVG, we inject CSS that works with existing circuit-to-svg structure | ||
| */ | ||
| export const addTraceHighlightingStyles = (svgContainer: HTMLElement): void => { | ||
| // Check if styles already added | ||
| const existingStyle = svgContainer.querySelector( | ||
| "style[data-trace-highlighting]" | ||
| ); | ||
| if (existingStyle) { | ||
| return; | ||
| } | ||
|
|
||
| // Create style element | ||
| const styleElement = document.createElement("style"); | ||
| styleElement.setAttribute("data-trace-highlighting", "true"); | ||
|
|
||
| styleElement.textContent = ` | ||
| /* Match the original .trace:hover behavior exactly */ | ||
|
|
||
| /* Use the same filter effect as the original hover */ | ||
| svg .trace-highlighted { | ||
| filter: invert(1) !important; | ||
| } | ||
|
|
||
| /* Hide crossing outlines on highlighted traces - matches original */ | ||
| svg .trace-highlighted .trace-crossing-outline { | ||
| // opacity: 0 !important; | ||
| } | ||
|
|
||
| /* Ensure pointer cursor for all trace groups */ | ||
| svg g.trace[data-circuit-json-type="schematic_trace"]:hover { | ||
| cursor: pointer !important; | ||
| } | ||
|
|
||
| /* Alternative selector for data-schematic-trace-id elements */ | ||
| svg [data-schematic-trace-id]:hover { | ||
| cursor: pointer !important; | ||
| } | ||
| `; | ||
|
|
||
| // Add to container (not inside SVG) | ||
| svgContainer.appendChild(styleElement); | ||
| }; | ||
|
|
||
| /** | ||
| * Remove trace highlighting styles | ||
| */ | ||
| export const removeTraceHighlightingStyles = ( | ||
| svgContainer: HTMLElement | ||
| ): void => { | ||
| const styleElement = svgContainer.querySelector( | ||
| "style[data-trace-highlighting]" | ||
| ); | ||
| if (styleElement) { | ||
| styleElement.remove(); | ||
| } | ||
| }; |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.