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
64 changes: 64 additions & 0 deletions examples/example13-trace-hover-highlighting-demo.fixture.tsx
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>
);
};
9 changes: 9 additions & 0 deletions lib/components/SchematicViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import { useChangeSchematicComponentLocationsInSvg } from "lib/hooks/useChangeSchematicComponentLocationsInSvg"
import { useChangeSchematicTracesForMovedComponents } from "lib/hooks/useChangeSchematicTracesForMovedComponents"
import { useSchematicGroupsOverlay } from "lib/hooks/useSchematicGroupsOverlay"
import { useConnectedTracesHoverHighlighting } from "lib/hooks/useConnectedTracesHoverHighlighting"
import { enableDebug } from "lib/utils/debug"
import { useEffect, useMemo, useRef, useState } from "react"
import {
Expand Down Expand Up @@ -243,6 +244,14 @@ export const SchematicViewer = ({
showGroups: showSchematicGroups,
})

// Add trace hover highlighting
useConnectedTracesHoverHighlighting({
svgDivRef,
circuitJson,
circuitJsonKey,
enabled: true,
})

const svgDiv = useMemo(
() => (
<div
Expand Down
128 changes: 128 additions & 0 deletions lib/hooks/useConnectedTracesHoverHighlighting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { useEffect, useRef } from "react";
import { findConnectedTraceIds } from "../utils/trace-connectivity";
import {
addTraceHighlightingStyles,
removeTraceHighlightingStyles,
} from "../utils/trace-highlighting-styles";

interface useConnectedTracesHoverHighlightingOptions {
svgDivRef: React.RefObject<HTMLDivElement | null>;
circuitJson: any[];
circuitJsonKey?: string;
enabled?: boolean;
}

/**
* Optimized trace highlighting using CSS classes and circuit-to-svg metadata
*/
export const useConnectedTracesHoverHighlighting = ({
svgDivRef,
circuitJson,
circuitJsonKey,
enabled = true,
}: useConnectedTracesHoverHighlightingOptions) => {
const activeNetRef = useRef<string | null>(null);
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);

useEffect(() => {
if (!enabled || !svgDivRef.current || !circuitJson || !circuitJsonKey) {
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, circuitJsonKey, enabled]);

return {
currentHighlightedNet: activeNetRef.current,
};
};
62 changes: 62 additions & 0 deletions lib/utils/trace-connectivity.ts
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];
}
};
58 changes: 58 additions & 0 deletions lib/utils/trace-highlighting-styles.ts
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();
}
};