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
155 changes: 155 additions & 0 deletions lib/components/SchematicTraceMouseTarget.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
import { useCallback, useEffect, useRef, useState } from "react"
import { useMouseEventsOverBoundingBox } from "../hooks/useMouseEventsOverBoundingBox"
import type { BoundingBoxBounds } from "./MouseTracker"
import { zIndexMap } from "../utils/z-index-map"

interface RelativeRect {
left: number
top: number
width: number
height: number
}

interface Measurement {
bounds: BoundingBoxBounds
rect: RelativeRect
}

const areMeasurementsEqual = (a: Measurement | null, b: Measurement | null) => {
if (!a && !b) return true
if (!a || !b) return false
return (
Math.abs(a.bounds.minX - b.bounds.minX) < 0.5 &&
Math.abs(a.bounds.maxX - b.bounds.maxX) < 0.5 &&
Math.abs(a.bounds.minY - b.bounds.minY) < 0.5 &&
Math.abs(a.bounds.maxY - b.bounds.maxY) < 0.5 &&
Math.abs(a.rect.left - b.rect.left) < 0.5 &&
Math.abs(a.rect.top - b.rect.top) < 0.5 &&
Math.abs(a.rect.width - b.rect.width) < 0.5 &&
Math.abs(a.rect.height - b.rect.height) < 0.5
)
}

interface Props {
traceId: string
svgDivRef: React.RefObject<HTMLDivElement | null>
containerRef: React.RefObject<HTMLDivElement | null>
onHoverChange?: (traceId: string, isHovering: boolean) => void
circuitJsonKey: string
}

export const SchematicTraceMouseTarget = ({
traceId,
svgDivRef,
containerRef,
onHoverChange,
circuitJsonKey,
}: Props) => {
const [measurement, setMeasurement] = useState<Measurement | null>(null)
const frameRef = useRef<number | null>(null)

const measure = useCallback(() => {
frameRef.current = null
const svgDiv = svgDivRef.current
const container = containerRef.current
if (!svgDiv || !container) {
setMeasurement((prev) => (prev ? null : prev))
return
}
const element = svgDiv.querySelector<SVGGraphicsElement | HTMLElement>(
`[data-schematic-trace-id="${traceId}"]`,
)
if (!element) {
setMeasurement((prev) => (prev ? null : prev))
return
}

const elementRect = element.getBoundingClientRect()
const containerRect = container.getBoundingClientRect()

const nextMeasurement: Measurement = {
bounds: {
minX: elementRect.left,
maxX: elementRect.right,
minY: elementRect.top,
maxY: elementRect.bottom,
},
rect: {
left: elementRect.left - containerRect.left,
top: elementRect.top - containerRect.top,
width: elementRect.width,
height: elementRect.height,
},
}

setMeasurement((prev) =>
areMeasurementsEqual(prev, nextMeasurement) ? prev : nextMeasurement,
)
}, [traceId, containerRef, svgDivRef])

const scheduleMeasure = useCallback(() => {
if (frameRef.current !== null) return
frameRef.current = window.requestAnimationFrame(measure)
}, [measure])

useEffect(() => {
scheduleMeasure()
}, [scheduleMeasure, circuitJsonKey])

useEffect(() => {
scheduleMeasure()
const svgDiv = svgDivRef.current
const container = containerRef.current
if (!svgDiv || !container) return

const resizeObserver =
typeof ResizeObserver !== "undefined"
? new ResizeObserver(() => {
scheduleMeasure()
})
: null
resizeObserver?.observe(container)
resizeObserver?.observe(svgDiv)

const mutationObserver =
typeof MutationObserver !== "undefined"
? new MutationObserver(() => {
scheduleMeasure()
})
: null
mutationObserver?.observe(svgDiv, {
attributes: true,
attributeFilter: ["style", "transform"],
subtree: true,
childList: true,
})

window.addEventListener("scroll", scheduleMeasure, true)
window.addEventListener("resize", scheduleMeasure)

return () => {
resizeObserver?.disconnect()
mutationObserver?.disconnect()
window.removeEventListener("scroll", scheduleMeasure, true)
window.removeEventListener("resize", scheduleMeasure)
if (frameRef.current !== null) {
cancelAnimationFrame(frameRef.current)
frameRef.current = null
}
}
}, [scheduleMeasure, svgDivRef, containerRef])

const bounds = measurement?.bounds ?? null

const { hovering } = useMouseEventsOverBoundingBox({
bounds,
})

useEffect(() => {
if (onHoverChange) {
onHoverChange(traceId, hovering)
}
}, [hovering, traceId, onHoverChange])

return null
}
82 changes: 82 additions & 0 deletions lib/components/SchematicViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { getStoredBoolean, setStoredBoolean } from "lib/hooks/useLocalStorage"
import { MouseTracker } from "./MouseTracker"
import { SchematicComponentMouseTarget } from "./SchematicComponentMouseTarget"
import { SchematicPortMouseTarget } from "./SchematicPortMouseTarget"
import { SchematicTraceMouseTarget } from "./SchematicTraceMouseTarget"

interface Props {
circuitJson: CircuitJson
Expand Down Expand Up @@ -164,9 +165,49 @@ export const SchematicViewer = ({
[],
)

const [hoveredNetId, setHoveredNetId] = useState<string | null>(null)
const hoveringTracesRef = useRef<Map<string, string>>(new Map())

const handleTraceHoverChange = useCallback(
(traceId: string, isHovering: boolean) => {
const trace = su(circuitJson).schematic_trace.get(traceId)
const sourceTraceId = trace?.source_trace_id
const sourceTrace = sourceTraceId
? su(circuitJson).source_trace.get(sourceTraceId)
: null
const netId = sourceTrace?.connected_source_net_ids?.[0]

if (isHovering && netId) {
hoveringTracesRef.current.set(traceId, netId)
} else {
hoveringTracesRef.current.delete(traceId)
}

// Update hoveredNetId based on the most recent hover
const activeNetIds = Array.from(hoveringTracesRef.current.values())
setHoveredNetId(
activeNetIds.length > 0 ? activeNetIds[activeNetIds.length - 1] : null,
)
},
[circuitJson],
)

const svgDivRef = useRef<HTMLDivElement>(null)
const touchStartRef = useRef<{ x: number; y: number } | null>(null)

const schematicTraceIds = useMemo(() => {
try {
return (
su(circuitJson)
.schematic_trace?.list()
?.map((trace) => trace.schematic_trace_id as string) ?? []
)
} catch (err) {
console.error("Failed to derive schematic trace ids", err)
return []
}
}, [circuitJsonKey, circuitJson])

const schematicComponentIds = useMemo(() => {
try {
return (
Expand Down Expand Up @@ -406,6 +447,37 @@ export const SchematicViewer = ({
{`[data-schematic-port-id]:hover { cursor: pointer !important; }`}
</style>
)}
<style>
{`
[data-schematic-trace-id] {
transition: stroke 0.2s, stroke-width 0.2s;
}
${
hoveredNetId
? su(circuitJson)
.schematic_trace.list()
.filter((st) => {
const sourceTrace = st.source_trace_id
? su(circuitJson).source_trace.get(st.source_trace_id)
: null
return sourceTrace?.connected_source_net_ids?.includes(
hoveredNetId,
)
})
.map(
(st) => `
[data-schematic-trace-id="${st.schematic_trace_id}"] {
stroke: #3399ff !important;
stroke-width: 0.1 !important;
cursor: pointer;
}
`,
)
.join("\n")
: ""
}
`}
</style>
<div
ref={containerRef}
style={{
Expand Down Expand Up @@ -560,6 +632,16 @@ export const SchematicViewer = ({
}}
/>
))}
{schematicTraceIds.map((traceId) => (
<SchematicTraceMouseTarget
key={traceId}
traceId={traceId}
svgDivRef={svgDivRef}
containerRef={containerRef}
circuitJsonKey={circuitJsonKey}
onHoverChange={handleTraceHoverChange}
/>
))}
{svgDiv}
{showSchematicPorts &&
schematicPortsInfo.map(({ portId, label }) => (
Expand Down
Loading