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
29 changes: 18 additions & 11 deletions lib/components/SchematicViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { su } from "@tscircuit/soup-util"
import type { CircuitJson } from "circuit-json"
import {
convertCircuitJsonToSchematicSvg,
type ColorOverrides,
convertCircuitJsonToSchematicSvg,
} from "circuit-to-svg"
import { su } from "@tscircuit/soup-util"
import { useChangeSchematicComponentLocationsInSvg } from "lib/hooks/useChangeSchematicComponentLocationsInSvg"
import { useChangeSchematicTracesForMovedComponents } from "lib/hooks/useChangeSchematicTracesForMovedComponents"
import { useHighlightSameNetTracesOnHover } from "lib/hooks/useHighlightSameNetTracesOnHover"
import { getStoredBoolean, setStoredBoolean } from "lib/hooks/useLocalStorage"
import { useSchematicGroupsOverlay } from "lib/hooks/useSchematicGroupsOverlay"
import { enableDebug } from "lib/utils/debug"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
Expand All @@ -16,21 +19,19 @@ import {
import { useMouseMatrixTransform } from "use-mouse-matrix-transform"
import { useResizeHandling } from "../hooks/use-resize-handling"
import { useComponentDragging } from "../hooks/useComponentDragging"
import { useSpiceSimulation } from "../hooks/useSpiceSimulation"
import type { ManualEditEvent } from "../types/edit-events"
import { getSpiceFromCircuitJson } from "../utils/spice-utils"
import { zIndexMap } from "../utils/z-index-map"
import { EditIcon } from "./EditIcon"
import { GridIcon } from "./GridIcon"
import { ViewMenuIcon } from "./ViewMenuIcon"
import { ViewMenu } from "./ViewMenu"
import type { CircuitJson } from "circuit-json"
import { SpiceSimulationIcon } from "./SpiceSimulationIcon"
import { SpiceSimulationOverlay } from "./SpiceSimulationOverlay"
import { zIndexMap } from "../utils/z-index-map"
import { useSpiceSimulation } from "../hooks/useSpiceSimulation"
import { getSpiceFromCircuitJson } from "../utils/spice-utils"
import { getStoredBoolean, setStoredBoolean } from "lib/hooks/useLocalStorage"
import { MouseTracker } from "./MouseTracker"
import { SchematicComponentMouseTarget } from "./SchematicComponentMouseTarget"
import { SchematicPortMouseTarget } from "./SchematicPortMouseTarget"
import { SpiceSimulationIcon } from "./SpiceSimulationIcon"
import { SpiceSimulationOverlay } from "./SpiceSimulationOverlay"
import { ViewMenu } from "./ViewMenu"
import { ViewMenuIcon } from "./ViewMenuIcon"

interface Props {
circuitJson: CircuitJson
Expand Down Expand Up @@ -345,6 +346,12 @@ export const SchematicViewer = ({
editEvents: editEventsWithUnappliedEditEvents,
})

useHighlightSameNetTracesOnHover({
svgDivRef,
circuitJson,
svgContentKey: svgString,
})

// Add group overlays when enabled
useSchematicGroupsOverlay({
svgDivRef,
Expand Down
145 changes: 145 additions & 0 deletions lib/hooks/useHighlightSameNetTracesOnHover.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { su } from "@tscircuit/soup-util"
import type { CircuitJson } from "circuit-json"
import { useEffect, useMemo } from "react"

const HOVERED_TRACE_CLASS = "schematic-trace-same-net-hover"
const HOVER_STYLE_ID = "schematic-trace-same-net-hover-style"

export const useHighlightSameNetTracesOnHover = ({
svgDivRef,
circuitJson,
svgContentKey,
}: {
svgDivRef: React.RefObject<HTMLDivElement | null>
circuitJson: CircuitJson
svgContentKey: string
}) => {
const schematicTraceIdsBySourceTraceId = useMemo(() => {
const traceIdsBySourceTraceId = new Map<string, string[]>()

try {
for (const trace of su(circuitJson).schematic_trace.list()) {
if (!trace.source_trace_id || !trace.schematic_trace_id) continue

const traceIds =
traceIdsBySourceTraceId.get(trace.source_trace_id) ?? []
traceIds.push(trace.schematic_trace_id)
traceIdsBySourceTraceId.set(trace.source_trace_id, traceIds)
}
} catch (err) {
console.error("Failed to derive schematic trace hover groups", err)
}

return traceIdsBySourceTraceId
}, [circuitJson])

const sourceTraceIdBySchematicTraceId = useMemo(() => {
const sourceTraceIds = new Map<string, string>()

for (const [
sourceTraceId,
schematicTraceIds,
] of schematicTraceIdsBySourceTraceId) {
for (const schematicTraceId of schematicTraceIds) {
sourceTraceIds.set(schematicTraceId, sourceTraceId)
}
}

return sourceTraceIds
}, [schematicTraceIdsBySourceTraceId])

useEffect(() => {
const svgContainer = svgDivRef.current
const svg = svgContainer?.querySelector("svg")
if (!svgContainer || !svg) return

const traceGroups = Array.from(
svg.querySelectorAll<SVGGElement>(
'[data-circuit-json-type="schematic_trace"][data-schematic-trace-id]',
),
)

const ensureHoverStyle = () => {
if (svg.querySelector(`#${HOVER_STYLE_ID}`)) return

const style = document.createElementNS(
"http://www.w3.org/2000/svg",
"style",
)
style.id = HOVER_STYLE_ID
style.textContent = `
.${HOVERED_TRACE_CLASS} path:not(.trace-invisible-hover-outline):not(.trace-crossing-outline) {
stroke: rgb(255, 140, 0) !important;
filter: drop-shadow(0 0 3px rgba(255, 140, 0, 0.85));
}

.${HOVERED_TRACE_CLASS} .trace-crossing-outline {
opacity: 0;
}
`
svg.appendChild(style)
}

let hoveredSourceTraceId: string | null = null

const clearHoveredTraces = () => {
if (!hoveredSourceTraceId) return
hoveredSourceTraceId = null

for (const traceGroup of traceGroups) {
traceGroup.classList.remove(HOVERED_TRACE_CLASS)
}
}

const setHoveredTrace = (schematicTraceId: string | null) => {
const nextSourceTraceId = schematicTraceId
? (sourceTraceIdBySchematicTraceId.get(schematicTraceId) ?? null)
: null

if (nextSourceTraceId === hoveredSourceTraceId) return

clearHoveredTraces()
hoveredSourceTraceId = nextSourceTraceId
if (!hoveredSourceTraceId) return

ensureHoverStyle()
const sameNetTraceIds =
schematicTraceIdsBySourceTraceId.get(hoveredSourceTraceId) ?? []

for (const traceGroup of traceGroups) {
const traceId = traceGroup.getAttribute("data-schematic-trace-id")
if (traceId && sameNetTraceIds.includes(traceId)) {
traceGroup.classList.add(HOVERED_TRACE_CLASS)
}
}
}

const handlePointerMove = (event: PointerEvent) => {
const target = event.target
if (!(target instanceof Element)) return

const traceGroup = target.closest(
'[data-circuit-json-type="schematic_trace"][data-schematic-trace-id]',
)
setHoveredTrace(
traceGroup?.getAttribute("data-schematic-trace-id") ?? null,
)
}

svg.addEventListener("pointermove", handlePointerMove)
svg.addEventListener("pointerleave", clearHoveredTraces)
svg.addEventListener("pointercancel", clearHoveredTraces)

return () => {
svg.removeEventListener("pointermove", handlePointerMove)
svg.removeEventListener("pointerleave", clearHoveredTraces)
svg.removeEventListener("pointercancel", clearHoveredTraces)
clearHoveredTraces()
}
}, [
schematicTraceIdsBySourceTraceId,
sourceTraceIdBySchematicTraceId,
svgContentKey,
svgDivRef,
])
}
Loading