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
8 changes: 8 additions & 0 deletions lib/components/SchematicViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { su } from "@tscircuit/soup-util"
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 { useCallback, useEffect, useMemo, useRef, useState } from "react"
import {
Expand Down Expand Up @@ -294,6 +295,13 @@ export const SchematicViewer = ({
showGroups: showSchematicGroups && !disableGroups,
})

// Add trace hover highlighting for electrical connectivity
useConnectedTracesHoverHighlighting({
svgDivRef,
circuitJson,
enabled: !editModeEnabled && !showSpiceOverlay,
})

// keep the latest touch handler without re-rendering the svg div
const handleComponentTouchStartRef = useRef(handleComponentTouchStart)
useEffect(() => {
Expand Down
166 changes: 166 additions & 0 deletions lib/hooks/useConnectedTracesHoverHighlighting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import { useEffect, useRef } from "react"
import type { CircuitJson } from "circuit-json"
import { findConnectedTraceIds } from "../utils/trace-connectivity"

/**
* Hook that adds hover highlighting functionality to schematic traces.
* When hovering over a trace, all electrically connected traces in the same net
* will be highlighted with an orange glow effect.
*/
export const useConnectedTracesHoverHighlighting = ({
svgDivRef,
circuitJson,
enabled = true,
}: {
svgDivRef: React.RefObject<HTMLDivElement | null>
circuitJson: CircuitJson
enabled?: boolean
}) => {
const highlightedTracesRef = useRef<Set<string>>(new Set())
const highlightTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)

useEffect(() => {
if (!enabled) return

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

const handleTraceMouseEnter = (event: Event) => {
const target = event.target as Element
const traceGroup = target.closest("[data-schematic-trace-id]")
if (!traceGroup) return

const traceId = traceGroup.getAttribute("data-schematic-trace-id")
if (!traceId) return

// Clear any pending unhighlight timeout
if (highlightTimeoutRef.current) {
clearTimeout(highlightTimeoutRef.current)
highlightTimeoutRef.current = null
}

const connectedTraces = findConnectedTraceIds(circuitJson, traceId)

// Clear previous highlights
highlightedTracesRef.current.forEach((highlightedTraceId) => {
const traceElement = svgElement.querySelector(
`[data-schematic-trace-id="${highlightedTraceId}"]`,
)
if (traceElement) {
traceElement.classList.remove("trace-highlighted")
}
})

// Highlight all connected traces
const newHighlightedTraces = new Set<string>()
connectedTraces.forEach((connectedTraceId) => {
const traceElement = svgElement.querySelector(
`[data-schematic-trace-id="${connectedTraceId}"]`,
)
if (traceElement) {
traceElement.classList.add("trace-highlighted")
newHighlightedTraces.add(connectedTraceId)
}
})

highlightedTracesRef.current = newHighlightedTraces
}

const handleTraceMouseLeave = (event: Event) => {
// Add small delay to prevent flickering when moving between connected traces
highlightTimeoutRef.current = setTimeout(() => {
highlightedTracesRef.current.forEach((highlightedTraceId) => {
const traceElement = svgElement.querySelector(
`[data-schematic-trace-id="${highlightedTraceId}"]`,
)
if (traceElement) {
traceElement.classList.remove("trace-highlighted")
}
})
highlightedTracesRef.current.clear()
}, 50)
}

const addEventListeners = () => {
const traceElements = svgElement.querySelectorAll(
"[data-schematic-trace-id]",
)

// Inject CSS styles if not already present
if (!svgElement.querySelector("style#trace-highlighting-styles")) {
const style = document.createElement("style")
style.id = "trace-highlighting-styles"
style.textContent = `
.trace-highlighted {
stroke-width: 3 !important;
stroke: #ff6b35 !important;
filter: drop-shadow(0 0 3px rgba(255, 107, 53, 0.6)) !important;
transition: all 0.15s ease-in-out !important;
z-index: 100 !important;
}

[data-schematic-trace-id] {
cursor: pointer;
}

[data-schematic-trace-id]:hover {
opacity: 0.8;
}

[data-schematic-trace-id].trace-highlighted:hover {
opacity: 1;
}
`
svgElement.appendChild(style)
}

traceElements.forEach((traceElement) => {
traceElement.addEventListener("mouseenter", handleTraceMouseEnter)
traceElement.addEventListener("mouseleave", handleTraceMouseLeave)
})
}

const removeEventListeners = () => {
const traceElements = svgElement.querySelectorAll(
"[data-schematic-trace-id]",
)

traceElements.forEach((traceElement) => {
traceElement.removeEventListener("mouseenter", handleTraceMouseEnter)
traceElement.removeEventListener("mouseleave", handleTraceMouseLeave)
})
}

addEventListeners()

const observer = new MutationObserver(() => {
removeEventListeners()
addEventListeners()
})

observer.observe(svgElement, {
childList: true,
subtree: true,
})

return () => {
observer.disconnect()
removeEventListeners()

if (highlightTimeoutRef.current) {
clearTimeout(highlightTimeoutRef.current)
}

// Clear any remaining highlights
highlightedTracesRef.current.forEach((highlightedTraceId) => {
const traceElement = svgElement?.querySelector(
`[data-schematic-trace-id="${highlightedTraceId}"]`,
)
if (traceElement) {
traceElement.classList.remove("trace-highlighted")
}
})
highlightedTracesRef.current.clear()
}
}, [svgDivRef, circuitJson, enabled])
}
82 changes: 82 additions & 0 deletions lib/utils/trace-connectivity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { su } from "@tscircuit/soup-util"
import type { CircuitJson } from "circuit-json"

/**
* Finds all schematic traces electrically connected to the hovered trace.
* Uses source_net as the source of truth for electrical connectivity.
*
* Algorithm:
* 1. Get schematic_trace by ID
* 2. Find its source_trace
* 3. Get connected_source_net_ids from source_trace
* 4. Find ALL source_traces that share ANY of these net IDs
* 5. Map back to schematic_trace_ids
*
* @param circuitJson - Circuit JSON soup data
* @param hoveredSchematicTraceId - The schematic_trace_id being hovered
* @returns Array of all connected schematic_trace_ids (including hovered)
*/
export const findConnectedTraceIds = (
circuitJson: CircuitJson,
hoveredSchematicTraceId: string,
): string[] => {
try {
const soup = su(circuitJson)

// STEP 1: Get the hovered schematic trace
const hoveredSchematicTrace = soup.schematic_trace.get(
hoveredSchematicTraceId,
)
if (!hoveredSchematicTrace) {
return [hoveredSchematicTraceId]
}

// STEP 2: Get the corresponding source_trace
const hoveredSourceTrace = soup.source_trace.get(
hoveredSchematicTrace.source_trace_id,
)
if (!hoveredSourceTrace) {
return [hoveredSchematicTraceId]
}

// STEP 3: Get all net IDs this trace belongs to (THE KEY!)
const connectedNetIds = hoveredSourceTrace.connected_source_net_ids || []
if (connectedNetIds.length === 0) {
return [hoveredSchematicTraceId]
}

// STEP 4: Find ALL source_traces that share ANY of these nets
const allSourceTraces = soup.source_trace.list()
const connectedSourceTraceIds = new Set<string>()

for (const sourceTrace of allSourceTraces) {
const sourceTraceNetIds = sourceTrace.connected_source_net_ids || []

// Check if this source_trace shares any net with our hovered trace
const sharesNet = sourceTraceNetIds.some((netId) =>
connectedNetIds.includes(netId),
)

if (sharesNet) {
connectedSourceTraceIds.add(sourceTrace.source_trace_id)
}
}

// STEP 5: Map source_trace_ids back to schematic_trace_ids
const allSchematicTraces = soup.schematic_trace.list()
const connectedSchematicTraceIds = new Set<string>([
hoveredSchematicTraceId,
])

for (const schematicTrace of allSchematicTraces) {
if (connectedSourceTraceIds.has(schematicTrace.source_trace_id)) {
connectedSchematicTraceIds.add(schematicTrace.schematic_trace_id)
}
}

return Array.from(connectedSchematicTraceIds)
} catch (error) {
console.error("[trace-connectivity] Error finding connected traces:", error)
return [hoveredSchematicTraceId]
}
}