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
1 change: 1 addition & 0 deletions examples/example15-analog-simulation-viewer.fixture.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const SwitchCircuitElement = (
<board schMaxTraceDistance={10} routingDisabled>
<voltagesource name="V1" voltage="5V" />
<resistor name="R_base" resistance="10k" schY={2} />
{/* @ts-ignore */}
<switch name="SW1" simSwitchFrequency="1kHz" schX={1.5} schY={2} />
<transistor
name="Q1"
Expand Down
8 changes: 4 additions & 4 deletions lib/components/AnalogSimulationViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,17 @@ export const AnalogSimulationViewer = ({
const simulationExperimentId = useMemo(() => {
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])

// Find simulation graph IDs from circuit JSON
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(() => {
Expand Down
6 changes: 6 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 { 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"
Expand Down Expand Up @@ -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({
Expand Down
121 changes: 121 additions & 0 deletions lib/hooks/useHighlightConnectedTracesOnHover.ts
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement | null>
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])
}
Loading