Skip to content
Draft
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
74 changes: 74 additions & 0 deletions examples/example35-inline-net-label-hover.fixture.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import type { CircuitJson } from "circuit-json"
import { SchematicViewer } from "lib/components/SchematicViewer"

const circuitJson: CircuitJson = [
{
type: "source_trace",
source_trace_id: "source_trace_signal_a",
connected_source_port_ids: [],
connected_source_net_ids: [],
subcircuit_connectivity_map_key: "signal_a",
},
{
type: "source_trace",
source_trace_id: "source_trace_signal_b",
connected_source_port_ids: [],
connected_source_net_ids: [],
subcircuit_connectivity_map_key: "signal_b",
},
{
type: "schematic_trace",
schematic_trace_id: "schematic_trace_signal_a",
source_trace_id: "source_trace_signal_a",
subcircuit_connectivity_map_key: "signal_a",
junctions: [],
edges: [{ from: { x: -4, y: 1 }, to: { x: 4, y: 1 } }],
},
{
type: "schematic_trace",
schematic_trace_id: "schematic_trace_signal_b",
source_trace_id: "source_trace_signal_b",
subcircuit_connectivity_map_key: "signal_b",
junctions: [],
edges: [{ from: { x: -4, y: -1 }, to: { x: 4, y: -1 } }],
},
{
type: "schematic_text",
schematic_text_id: "schematic_text_signal_a",
source_trace_id: "source_trace_signal_a",
text: "SIGNAL_A",
position: { x: 0, y: 1.25 },
anchor: "bottom_center",
rotation: 0,
font_size: 0.25,
color: "rgb(132, 0, 0)",
},
{
type: "schematic_text",
schematic_text_id: "schematic_text_signal_b",
source_trace_id: "source_trace_signal_b",
text: "SIGNAL_B",
position: { x: 0, y: -0.75 },
anchor: "bottom_center",
rotation: 0,
font_size: 0.25,
color: "rgb(132, 0, 0)",
},
{
type: "schematic_text",
schematic_text_id: "schematic_text_instruction",
text: "Hover either inline label",
position: { x: 0, y: 2.2 },
anchor: "center",
rotation: 0,
font_size: 0.22,
color: "#006464",
},
]

export default () => (
<SchematicViewer
circuitJson={circuitJson}
containerStyle={{ width: "100vw", height: "100vh" }}
/>
)
2 changes: 1 addition & 1 deletion lib/components/SchematicViewer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ export const SchematicViewer = ({
{netHoverHighlightEnabled && (
<style>
{`.sch-net-faded { opacity: 0.35; }
svg :is(g.trace, g.trace-overlays, g[data-schematic-component-id], [data-schematic-net-label-id]) { transition: opacity 0.12s ease-in-out; }`}
svg :is(g.trace, g.trace-overlays, g[data-schematic-component-id], [data-schematic-net-label-id], [data-schematic-text-id]) { transition: opacity 0.12s ease-in-out; }`}
</style>
)}
{searchEnabled && (
Expand Down
54 changes: 47 additions & 7 deletions lib/hooks/useSchematicNetHover.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
import { su } from "@tscircuit/soup-util"
import type { CircuitJson } from "circuit-json"
import type { CircuitJson, SchematicText, SourceTrace } from "circuit-json"
import { useEffect } from "react"

type SourceTraceId = SourceTrace["source_trace_id"]
type SchematicTextId = SchematicText["schematic_text_id"]
type SubcircuitConnectivityMapKey = NonNullable<
SourceTrace["subcircuit_connectivity_map_key"]
>

const FADED_CLASS = "sch-net-faded"

const TRACE_SELECTOR =
"g.trace[data-subcircuit-connectivity-map-key], g.trace-overlays[data-subcircuit-connectivity-map-key]"

const NET_LABEL_SELECTOR = "[data-schematic-net-label-id]"
const SCHEMATIC_TEXT_SELECTOR = "[data-schematic-text-id]"

/**
* Net highlighting on hover, done entirely in JS (the base SVG carries no
* interaction). Hovering a wire or a net label fades every element that is NOT
* part of that net — other nets' traces/labels and chips not connected to the
* net — so the hovered net stands out by contrast rather than by recoloring.
* interaction). Hovering a wire, anchored net label, or inline net label fades
* every element that is NOT part of that net — other nets' traces/labels and
* chips not connected to the net — so the hovered net stands out by contrast.
*
* Identifies elements by the attributes circuit-to-svg emits:
* - traces: g.trace[data-subcircuit-connectivity-map-key] (+ g.trace-overlays)
* - components: g[data-schematic-component-id]
* - net labels: [data-schematic-net-label-id] (per element, no wrapping group)
* - schematic text: [data-schematic-text-id]
*
* Faded elements get the `sch-net-faded` class (styled by SchematicViewer).
*/
Expand All @@ -37,7 +45,8 @@ export const useSchematicNetHover = ({
const svgDiv = svgDivRef.current
if (!enabled || !svgDiv) return

const { componentIdToKeys, netLabelIdToKey } = buildNetRegistry(circuitJson)
const { componentIdToKeys, netLabelIdToKey, schematicTextIdToKey } =
buildNetRegistry(circuitJson)

// Every net element and the net key(s) it belongs to, plus each hover
// trigger's net key. Rebuilt from the SVG whenever it re-renders; the
Expand Down Expand Up @@ -81,6 +90,18 @@ export const useSchematicNetHover = ({
}
netElements.push({ el, keys })
}
for (const el of Array.from(
svg.querySelectorAll(SCHEMATIC_TEXT_SELECTOR),
)) {
const key = schematicTextIdToKey.get(
el.getAttribute("data-schematic-text-id")!,
)
if (!key) continue
const keys = new Set<string>()
keys.add(key)
triggerNetKeys.set(el, key)
netElements.push({ el, keys })
}
}

// Fade everything not on `key` (null clears the fade).
Expand All @@ -98,7 +119,9 @@ export const useSchematicNetHover = ({
highlightNet(null)
return
}
const trigger = target.closest(`${TRACE_SELECTOR}, ${NET_LABEL_SELECTOR}`)
const trigger = target.closest(
`${TRACE_SELECTOR}, ${NET_LABEL_SELECTOR}, ${SCHEMATIC_TEXT_SELECTOR}`,
)
if (!trigger) {
highlightNet(null)
return
Expand Down Expand Up @@ -133,6 +156,7 @@ export const useSchematicNetHover = ({
* elements to nets:
* - componentIdToKeys: which connectivity nets each schematic component touches
* - netLabelIdToKey: a schematic_net_label_id -> its connectivity key
* - schematicTextIdToKey: an inline label's schematic_text_id -> its connectivity key
*/
function buildNetRegistry(circuitJson: CircuitJson) {
const cju = su(circuitJson)
Expand All @@ -148,9 +172,14 @@ function buildNetRegistry(circuitJson: CircuitJson) {
// schematic_component_id -> the connectivity nets its ports belong to (a chip
// sits on several nets). The connectivity key lives on source_trace.
const componentIdToKeys = new Map<string, Set<string>>()
const sourceTraceIdToKey = new Map<
SourceTraceId,
SubcircuitConnectivityMapKey
>()
for (const sourceTrace of cju.source_trace.list()) {
const key = sourceTrace.subcircuit_connectivity_map_key
if (!key) continue
sourceTraceIdToKey.set(sourceTrace.source_trace_id, key)
for (const portId of sourceTrace.connected_source_port_ids ?? []) {
const schCompId = srcCompToSchComp.get(
cju.source_port.get(portId)?.source_component_id ?? "",
Expand All @@ -163,6 +192,17 @@ function buildNetRegistry(circuitJson: CircuitJson) {
}
}

const schematicTextIdToKey = new Map<
SchematicTextId,
SubcircuitConnectivityMapKey
>()
for (const schematicText of cju.schematic_text.list()) {
if (!schematicText.source_trace_id) continue
const key = sourceTraceIdToKey.get(schematicText.source_trace_id)
if (!key) continue
schematicTextIdToKey.set(schematicText.schematic_text_id, key)
}

// schematic_net_label_id -> connectivity key, resolved via its source_net
// (same key the net's traces use). Falls back to source_net_id, which already
// *is* the key for auto-emitted labels on unrouted nets (no source_net).
Expand All @@ -175,5 +215,5 @@ function buildNetRegistry(circuitJson: CircuitJson) {
netLabelIdToKey.set(label.schematic_net_label_id, key)
}

return { componentIdToKeys, netLabelIdToKey }
return { componentIdToKeys, netLabelIdToKey, schematicTextIdToKey }
}
135 changes: 135 additions & 0 deletions tests/schematic-inline-net-label-hover.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { expect, test } from "bun:test"
import type { CircuitJson } from "circuit-json"
import { JSDOM } from "jsdom"
import { act, useRef } from "react"
import { createRoot } from "react-dom/client"
import { useSchematicNetHover } from "../lib/hooks/useSchematicNetHover"

const circuitJson: CircuitJson = [
{
type: "source_trace",
source_trace_id: "source_trace_signal",
connected_source_port_ids: [],
connected_source_net_ids: [],
subcircuit_connectivity_map_key: "signal_net_key",
},
{
type: "source_trace",
source_trace_id: "source_trace_other",
connected_source_port_ids: [],
connected_source_net_ids: [],
subcircuit_connectivity_map_key: "other_net_key",
},
{
type: "schematic_text",
schematic_text_id: "schematic_text_inline_label",
source_trace_id: "source_trace_signal",
text: "SIGNAL",
position: { x: 0, y: 0 },
anchor: "center",
rotation: 0,
font_size: 0.2,
color: "rgb(132, 0, 0)",
},
{
type: "schematic_text",
schematic_text_id: "schematic_text_note",
text: "note",
position: { x: 0, y: 1 },
anchor: "center",
rotation: 0,
font_size: 0.2,
color: "#006464",
},
]

test("hovering inline net label highlights its source trace net", async () => {
const dom = new JSDOM('<div id="root"></div>')
const previousGlobals = {
window: globalThis.window,
document: globalThis.document,
Element: globalThis.Element,
Event: globalThis.Event,
MutationObserver: globalThis.MutationObserver,
}

Object.assign(globalThis, {
window: dom.window,
document: dom.window.document,
Element: dom.window.Element,
Event: dom.window.Event,
MutationObserver: dom.window.MutationObserver,
IS_REACT_ACT_ENVIRONMENT: true,
})

const Harness = () => {
const svgDivRef = useRef<HTMLDivElement>(null)
useSchematicNetHover({
svgDivRef,
circuitJson,
circuitJsonKey: "inline-net-label-hover",
enabled: true,
})

return (
<div ref={svgDivRef}>
<svg>
<g
id="signal-trace"
className="trace"
data-subcircuit-connectivity-map-key="signal_net_key"
/>
<g
id="other-trace"
className="trace"
data-subcircuit-connectivity-map-key="other_net_key"
/>
<text
id="inline-label"
className="sch-text"
data-schematic-text-id="schematic_text_inline_label"
>
SIGNAL
</text>
<text
id="ordinary-note"
className="sch-text"
data-schematic-text-id="schematic_text_note"
>
note
</text>
</svg>
</div>
)
}

const reactRoot = createRoot(document.getElementById("root")!)

try {
await act(async () => reactRoot.render(<Harness />))

document
.getElementById("inline-label")!
.dispatchEvent(new dom.window.MouseEvent("mouseover", { bubbles: true }))

expect(document.getElementById("signal-trace")!.classList).not.toContain(
"sch-net-faded",
)
expect(document.getElementById("inline-label")!.classList).not.toContain(
"sch-net-faded",
)
expect(document.getElementById("other-trace")!.classList).toContain(
"sch-net-faded",
)
expect(document.getElementById("ordinary-note")!.classList).not.toContain(
"sch-net-faded",
)
} finally {
await act(async () => reactRoot.unmount())
Object.assign(globalThis, {
...previousGlobals,
IS_REACT_ACT_ENVIRONMENT: false,
})
dom.window.close()
}
})
Loading