diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index ab4fd20..d7f74b4 100644 --- a/lib/components/SchematicViewer.tsx +++ b/lib/components/SchematicViewer.tsx @@ -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 { useConnectedTracesHoverHighlighting } from "lib/hooks/useConnectedTracesHoverHighlighting" +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" @@ -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 @@ -345,6 +346,12 @@ export const SchematicViewer = ({ editEvents: editEventsWithUnappliedEditEvents, }) + useConnectedTracesHoverHighlighting({ + svgDivRef, + circuitJson, + enabled: !editModeEnabled && !showSpiceOverlay, + }) + // Add group overlays when enabled useSchematicGroupsOverlay({ svgDivRef, diff --git a/lib/hooks/useConnectedTracesHoverHighlighting.ts b/lib/hooks/useConnectedTracesHoverHighlighting.ts new file mode 100644 index 0000000..a0eb712 --- /dev/null +++ b/lib/hooks/useConnectedTracesHoverHighlighting.ts @@ -0,0 +1,106 @@ +import type { CircuitJson } from "circuit-json" +import { useEffect, useRef } from "react" +import { findConnectedTraceIds } from "../utils/trace-connectivity" + +export const useConnectedTracesHoverHighlighting = ({ + svgDivRef, + circuitJson, + enabled = true, +}: { + svgDivRef: React.RefObject + circuitJson: CircuitJson + enabled?: boolean +}) => { + const highlightedIdsRef = useRef>(new Set()) + const leaveTimerRef = useRef | null>(null) + + useEffect(() => { + if (!enabled) return + const container = svgDivRef.current + if (!container) return + + const highlightId = (id: string) => + container + .querySelectorAll(`[data-schematic-trace-id="${id}"]`) + .forEach((el) => el.classList.add("trace-highlighted")) + + const unhighlightId = (id: string) => + container + .querySelectorAll(`[data-schematic-trace-id="${id}"]`) + .forEach((el) => el.classList.remove("trace-highlighted")) + + const clearAll = () => { + highlightedIdsRef.current.forEach(unhighlightId) + highlightedIdsRef.current.clear() + } + + const onEnter = (e: Event) => { + const traceGroup = (e.target as Element).closest( + "[data-schematic-trace-id]", + ) + if (!traceGroup) return + const id = traceGroup.getAttribute("data-schematic-trace-id") + if (!id) return + + if (leaveTimerRef.current) { + clearTimeout(leaveTimerRef.current) + leaveTimerRef.current = null + } + + clearAll() + findConnectedTraceIds(circuitJson, id).forEach((connId) => { + highlightId(connId) + highlightedIdsRef.current.add(connId) + }) + } + + const onLeave = () => { + leaveTimerRef.current = setTimeout(clearAll, 50) + } + + const injectStyles = () => { + if (container.querySelector("style#trace-hover-styles")) return + const style = document.createElement("style") + style.id = "trace-hover-styles" + style.textContent = ` + .trace-highlighted path, + .trace-highlighted circle { + stroke: #ff6b35 !important; + filter: drop-shadow(0 0 3px rgba(255,107,53,0.6)) !important; + transition: stroke 0.15s ease-in-out, filter 0.15s ease-in-out !important; + } + [data-schematic-trace-id] { cursor: pointer; } + ` + container.appendChild(style) + } + + const attach = () => { + injectStyles() + container.querySelectorAll("[data-schematic-trace-id]").forEach((el) => { + el.addEventListener("mouseenter", onEnter) + el.addEventListener("mouseleave", onLeave) + }) + } + + const detach = () => + container.querySelectorAll("[data-schematic-trace-id]").forEach((el) => { + el.removeEventListener("mouseenter", onEnter) + el.removeEventListener("mouseleave", onLeave) + }) + + attach() + + const observer = new MutationObserver(() => { + detach() + attach() + }) + observer.observe(container, { childList: true, subtree: true }) + + return () => { + observer.disconnect() + detach() + if (leaveTimerRef.current) clearTimeout(leaveTimerRef.current) + clearAll() + } + }, [svgDivRef, circuitJson, enabled]) +} diff --git a/lib/utils/trace-connectivity.ts b/lib/utils/trace-connectivity.ts new file mode 100644 index 0000000..6b63b98 --- /dev/null +++ b/lib/utils/trace-connectivity.ts @@ -0,0 +1,48 @@ +import { su } from "@tscircuit/soup-util" +import type { CircuitJson } from "circuit-json" + +export const findConnectedTraceIds = ( + circuitJson: CircuitJson, + hoveredSchematicTraceId: string, +): string[] => { + try { + const soup = su(circuitJson) + const hoveredSchematicTrace = soup.schematic_trace.get( + hoveredSchematicTraceId, + ) + if (!hoveredSchematicTrace?.source_trace_id) + return [hoveredSchematicTraceId] + + const hoveredSourceTrace = soup.source_trace.get( + hoveredSchematicTrace.source_trace_id, + ) + if (!hoveredSourceTrace) return [hoveredSchematicTraceId] + + const connectedNetIds = hoveredSourceTrace.connected_source_net_ids ?? [] + if (connectedNetIds.length === 0) return [hoveredSchematicTraceId] + + const allSourceTraces = soup.source_trace.list() + const connectedSourceTraceIds = new Set() + + for (const sourceTrace of allSourceTraces) { + const netIds = sourceTrace.connected_source_net_ids ?? [] + if (netIds.some((id) => connectedNetIds.includes(id))) { + connectedSourceTraceIds.add(sourceTrace.source_trace_id) + } + } + + const allSchematicTraces = soup.schematic_trace.list() + const result = new Set([hoveredSchematicTraceId]) + for (const st of allSchematicTraces) { + if ( + st.source_trace_id && + connectedSourceTraceIds.has(st.source_trace_id) + ) { + result.add(st.schematic_trace_id) + } + } + return Array.from(result) + } catch { + return [hoveredSchematicTraceId] + } +}