diff --git a/examples/example15-analog-simulation-viewer.fixture.tsx b/examples/example15-analog-simulation-viewer.fixture.tsx index ede6609..d3b1b9c 100644 --- a/examples/example15-analog-simulation-viewer.fixture.tsx +++ b/examples/example15-analog-simulation-viewer.fixture.tsx @@ -8,6 +8,7 @@ const SwitchCircuitElement = ( + {/* @ts-ignore */} { if (!circuitJson) return null const simulationElement = circuitJson.find( - (el) => el.type === "simulation_experiment", - ) + (el) => el.type === ("simulation_experiment" as any), + ) as any return simulationElement?.simulation_experiment_id || null }, [circuitJson]) @@ -82,8 +82,8 @@ export const AnalogSimulationViewer = ({ const simulationVoltageGraphIds = useMemo(() => { if (!circuitJson) return [] return circuitJson - .filter((el) => el.type === "simulation_transient_voltage_graph") - .map((el) => el.simulation_transient_voltage_graph_id) + .filter((el) => el.type === ("simulation_transient_voltage_graph" as any)) + .map((el: any) => el.simulation_transient_voltage_graph_id) }, [circuitJson]) const simulationCurrentGraphIds = useMemo(() => { diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index 865a195..f709ef3 100644 --- a/lib/components/SchematicViewer.tsx +++ b/lib/components/SchematicViewer.tsx @@ -5,6 +5,7 @@ import { import { su } from "@tscircuit/soup-util" import { useChangeSchematicComponentLocationsInSvg } from "lib/hooks/useChangeSchematicComponentLocationsInSvg" import { useChangeSchematicTracesForMovedComponents } from "lib/hooks/useChangeSchematicTracesForMovedComponents" +import { useHighlightConnectedTracesOnHover } from "lib/hooks/useHighlightConnectedTracesOnHover" import { useSchematicGroupsOverlay } from "lib/hooks/useSchematicGroupsOverlay" import { enableDebug } from "lib/utils/debug" import { useCallback, useEffect, useMemo, useRef, useState } from "react" @@ -424,6 +425,11 @@ export const SchematicViewer = ({ editEvents: editEventsWithUnappliedEditEvents, }) + useHighlightConnectedTracesOnHover({ + svgDivRef, + circuitJson, + }) + // Add group overlays when enabled. The key includes the active sheet so // overlays are recomputed against the freshly-rendered sheet's SVG. useSchematicGroupsOverlay({ diff --git a/lib/hooks/useHighlightConnectedTracesOnHover.ts b/lib/hooks/useHighlightConnectedTracesOnHover.ts new file mode 100644 index 0000000..06fb7a2 --- /dev/null +++ b/lib/hooks/useHighlightConnectedTracesOnHover.ts @@ -0,0 +1,121 @@ +import { useEffect } from "react" +import { su } from "@tscircuit/soup-util" +import type { CircuitJson } from "circuit-json" + +export const useHighlightConnectedTracesOnHover = ({ + svgDivRef, + circuitJson, +}: { + svgDivRef: React.RefObject + circuitJson: CircuitJson +}) => { + useEffect(() => { + const svg = svgDivRef.current + if (!svg) return + + let currentHoveredTraceId: string | null = null + + const handleMouseOver = (e: MouseEvent) => { + const target = e.target as Element + const traceElement = target.closest( + '[data-circuit-json-type="schematic_trace"]', + ) + if (!traceElement) { + if (currentHoveredTraceId) { + clearHover() + } + return + } + + const schematicTraceId = traceElement.getAttribute( + "data-schematic-trace-id", + ) + if (!schematicTraceId || schematicTraceId === currentHoveredTraceId) + return + + clearHover() + currentHoveredTraceId = schematicTraceId + highlightTraces(schematicTraceId) + } + + const handleMouseOut = (e: MouseEvent) => { + // If the mouse is leaving the svg entirely + const relatedTarget = e.relatedTarget as Element + if (!svg.contains(relatedTarget)) { + clearHover() + } + } + + const clearHover = () => { + currentHoveredTraceId = null + const allTraces = svg.querySelectorAll( + '[data-circuit-json-type="schematic_trace"] path', + ) + for (const trace of Array.from(allTraces)) { + trace.removeAttribute("data-is-hovered") + ;(trace as SVGPathElement).style.stroke = "" + } + } + + const highlightTraces = (schematicTraceId: string) => { + const schematicTrace = + su(circuitJson).schematic_trace.get(schematicTraceId) + if (!schematicTrace) return + + const sourceTraceId = schematicTrace.source_trace_id + const sourceTrace = sourceTraceId + ? su(circuitJson).source_trace.get(sourceTraceId) + : null + const sourceNetIds = sourceTrace?.connected_source_net_ids || [] + + let tracesToHighlight: any[] = [] + + if (sourceNetIds.length > 0) { + const sourceTracesOnNet = su(circuitJson) + .source_trace.list() + .filter((t) => + t.connected_source_net_ids?.some((netId: string) => + sourceNetIds.includes(netId), + ), + ) + const sourceTraceIdsOnNet = new Set( + sourceTracesOnNet.map((t) => t.source_trace_id), + ) + tracesToHighlight = su(circuitJson) + .schematic_trace.list() + .filter( + (t) => + t.source_trace_id && sourceTraceIdsOnNet.has(t.source_trace_id), + ) + } else if (sourceTraceId) { + // Fallback: group by source_trace_id if net is missing + tracesToHighlight = su(circuitJson) + .schematic_trace.list() + .filter((t) => t.source_trace_id === sourceTraceId) + } else { + tracesToHighlight = [schematicTrace] + } + + tracesToHighlight.forEach((trace) => { + const traceElements = svg.querySelectorAll( + `[data-schematic-trace-id="${trace.schematic_trace_id}"] path`, + ) + for (const traceElement of Array.from(traceElements)) { + traceElement.setAttribute("data-is-hovered", "true") + // Set a blue hover color, like components + ;(traceElement as SVGPathElement).style.stroke = + "var(--tscircuit-trace-hover-color, #3b82f6)" + } + }) + } + + svg.addEventListener("mouseover", handleMouseOver) + svg.addEventListener("mouseout", handleMouseOut) + + return () => { + svg.removeEventListener("mouseover", handleMouseOver) + svg.removeEventListener("mouseout", handleMouseOut) + clearHover() + } + }, [svgDivRef, circuitJson]) +}