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
42 changes: 42 additions & 0 deletions examples/example21-connected-trace-hover.fixture.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { SchematicViewer } from "lib/components/SchematicViewer"
import { renderToCircuitJson } from "lib/dev/render-to-circuit-json"

const circuitJson = renderToCircuitJson(
<board width="80mm" height="50mm">
<group name="ChainA">
<resistor name="R1" resistance={1000} schX={-20} schY={8} />
<resistor name="R2" resistance={1000} schX={-10} schY={8} />
<resistor name="R3" resistance={1000} schX={0} schY={8} />
<resistor name="R4" resistance={1000} schX={10} schY={8} />
</group>

<group name="ChainB">
<resistor name="R5" resistance={2200} schX={-10} schY={-8} />
<resistor name="R6" resistance={2200} schX={0} schY={-8} />
<resistor name="R7" resistance={2200} schX={10} schY={-8} />
</group>

<trace from=".R1 .pin2" to=".R2 .pin1" />
<trace from=".R2 .pin2" to=".R3 .pin1" />
<trace from=".R3 .pin2" to=".R4 .pin1" />

<trace from=".R5 .pin2" to=".R6 .pin1" />
<trace from=".R6 .pin2" to=".R7 .pin1" />
</board>,
)

export default () => {
return (
<div style={{ position: "relative", height: "100%" }}>
<SchematicViewer
circuitJson={circuitJson}
containerStyle={{
width: "100%",
height: "100%",
backgroundColor: "#f8f9fa",
}}
editingEnabled={false}
/>
</div>
)
}
45 changes: 32 additions & 13 deletions lib/components/SchematicViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
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 { getStoredBoolean, setStoredBoolean } from "lib/hooks/useLocalStorage"
import { useSchematicGroupsOverlay } from "lib/hooks/useSchematicGroupsOverlay"
import { useTraceHoverHighlight } from "lib/hooks/useTraceHoverHighlight"
import { enableDebug } from "lib/utils/debug"
import { useCallback, useEffect, useMemo, useRef, useState } from "react"
import {
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,10 @@ export const SchematicViewer = ({
editEvents: editEventsWithUnappliedEditEvents,
})

useTraceHoverHighlight({
svgDivRef,
})

// Add group overlays when enabled
useSchematicGroupsOverlay({
svgDivRef,
Expand Down Expand Up @@ -398,14 +403,28 @@ export const SchematicViewer = ({
<MouseTracker>
{onSchematicComponentClicked && (
<style>
{`.schematic-component-clickable [data-schematic-component-id]:hover { cursor: pointer !important; }`}
{
".schematic-component-clickable [data-schematic-component-id]:hover { cursor: pointer !important; }"
}
</style>
)}
{onSchematicPortClicked && (
<style>
{`[data-schematic-port-id]:hover { cursor: pointer !important; }`}
{"[data-schematic-port-id]:hover { cursor: pointer !important; }"}
</style>
)}
<style>
{`
[data-circuit-json-type="schematic_trace"][data-net-hovered="true"] {
filter: invert(1) !important;
}

[data-circuit-json-type="schematic_trace"][data-net-hovered="true"] .trace-crossing-outline,
[data-circuit-json-type="schematic_trace"][data-net-hovered="true"] .trace-invisible-hover-outline {
opacity: 0 !important;
}
`}
</style>
<div
ref={containerRef}
style={{
Expand Down
93 changes: 93 additions & 0 deletions lib/hooks/useTraceHoverHighlight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { useEffect, useRef } from "react"

const TRACE_GROUP_SELECTOR = '[data-circuit-json-type="schematic_trace"]'
const CONNECTIVITY_KEY_ATTR = "data-subcircuit-connectivity-map-key"
const HOVERED_ATTR = "data-net-hovered"

const getTraceGroupKey = (element: Element | null) => {
if (!element) return null
return (
element.getAttribute(CONNECTIVITY_KEY_ATTR) ??
element.getAttribute("data-schematic-trace-id")
)
}

export const useTraceHoverHighlight = ({
svgDivRef,
}: {
svgDivRef: React.RefObject<HTMLDivElement | null>
}) => {
const hoveredKeyRef = useRef<string | null>(null)

useEffect(() => {
const svgRoot = svgDivRef.current
if (!svgRoot) return

const clearHoveredTraces = () => {
const hoveredTraces = svgRoot.querySelectorAll(`[${HOVERED_ATTR}="true"]`)
for (const trace of Array.from(hoveredTraces)) {
trace.removeAttribute(HOVERED_ATTR)
}
hoveredKeyRef.current = null
}

const setHoveredTraces = (key: string) => {
if (hoveredKeyRef.current === key) return

clearHoveredTraces()

const escapedKey =
typeof CSS !== "undefined" && typeof CSS.escape === "function"
? CSS.escape(key)
: key.replaceAll('"', '\\"')

const matchingTraceGroups = svgRoot.querySelectorAll(
`${TRACE_GROUP_SELECTOR}[${CONNECTIVITY_KEY_ATTR}="${escapedKey}"]`,
)
for (const traceGroup of Array.from(matchingTraceGroups)) {
traceGroup.setAttribute(HOVERED_ATTR, "true")
}

hoveredKeyRef.current = key
}

const handlePointerOver = (event: Event) => {
const target = event.target as Element | null
const traceGroup = target?.closest(TRACE_GROUP_SELECTOR) ?? null
const traceKey = getTraceGroupKey(traceGroup)
if (!traceKey) return
setHoveredTraces(traceKey)
}

const handlePointerOut = (event: Event) => {
const target = event.target as Element | null
const traceGroup = target?.closest(TRACE_GROUP_SELECTOR) ?? null
const traceKey = getTraceGroupKey(traceGroup)
if (!traceKey) return

const relatedTarget = (event as PointerEvent).relatedTarget
const relatedElement =
relatedTarget instanceof Element ? relatedTarget : null
const relatedTraceGroup =
relatedElement?.closest(TRACE_GROUP_SELECTOR) ?? null
const relatedKey = getTraceGroupKey(relatedTraceGroup)

if (relatedKey && relatedKey === traceKey) {
return
}

if (hoveredKeyRef.current === traceKey) {
clearHoveredTraces()
}
}

svgRoot.addEventListener("pointerover", handlePointerOver)
svgRoot.addEventListener("pointerout", handlePointerOut)

return () => {
svgRoot.removeEventListener("pointerover", handlePointerOver)
svgRoot.removeEventListener("pointerout", handlePointerOut)
clearHoveredTraces()
}
}, [svgDivRef])
}