From b9f6861a7a0cf33119b14dcfcc1003d4a1bbfd21 Mon Sep 17 00:00:00 2001 From: mohan-bee Date: Thu, 27 Aug 2026 23:50:44 +0530 Subject: [PATCH 1/3] up --- lib/components/SchematicViewer.tsx | 2 +- lib/hooks/useSchematicNetHover.ts | 42 +++++-- .../schematic-inline-net-label-hover.test.tsx | 104 ++++++++++++++++++ 3 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 tests/schematic-inline-net-label-hover.test.tsx diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index e1a92a47..ff290bce 100644 --- a/lib/components/SchematicViewer.tsx +++ b/lib/components/SchematicViewer.tsx @@ -511,7 +511,7 @@ export const SchematicViewer = ({ {netHoverHighlightEnabled && ( )} {searchEnabled && ( diff --git a/lib/hooks/useSchematicNetHover.ts b/lib/hooks/useSchematicNetHover.ts index 7bfb2c7b..53c2266b 100644 --- a/lib/hooks/useSchematicNetHover.ts +++ b/lib/hooks/useSchematicNetHover.ts @@ -1,24 +1,31 @@ import { su } from "@tscircuit/soup-util" -import type { CircuitJson } from "circuit-json" +import type { CircuitJson, SourceTrace } from "circuit-json" import { useEffect } from "react" +type SourceTraceId = SourceTrace["source_trace_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 INLINE_NET_LABEL_SELECTOR = ".sch-inline-net-label[data-source-trace-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) + * - inline labels: .sch-inline-net-label[data-source-trace-id] * * Faded elements get the `sch-net-faded` class (styled by SchematicViewer). */ @@ -37,7 +44,8 @@ export const useSchematicNetHover = ({ const svgDiv = svgDivRef.current if (!enabled || !svgDiv) return - const { componentIdToKeys, netLabelIdToKey } = buildNetRegistry(circuitJson) + const { componentIdToKeys, netLabelIdToKey, sourceTraceIdToKey } = + 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 @@ -81,6 +89,19 @@ export const useSchematicNetHover = ({ } netElements.push({ el, keys }) } + for (const el of Array.from( + svg.querySelectorAll(INLINE_NET_LABEL_SELECTOR), + )) { + const key = sourceTraceIdToKey.get( + el.getAttribute("data-source-trace-id")!, + ) + const keys = new Set() + if (key) { + keys.add(key) + triggerNetKeys.set(el, key) + } + netElements.push({ el, keys }) + } } // Fade everything not on `key` (null clears the fade). @@ -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}, ${INLINE_NET_LABEL_SELECTOR}`, + ) if (!trigger) { highlightNet(null) return @@ -148,9 +171,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>() + 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 ?? "", @@ -175,5 +203,5 @@ function buildNetRegistry(circuitJson: CircuitJson) { netLabelIdToKey.set(label.schematic_net_label_id, key) } - return { componentIdToKeys, netLabelIdToKey } + return { componentIdToKeys, netLabelIdToKey, sourceTraceIdToKey } } diff --git a/tests/schematic-inline-net-label-hover.test.tsx b/tests/schematic-inline-net-label-hover.test.tsx new file mode 100644 index 00000000..b8668082 --- /dev/null +++ b/tests/schematic-inline-net-label-hover.test.tsx @@ -0,0 +1,104 @@ +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", + }, +] + +test("hovering inline net label highlights its source trace net", async () => { + const dom = new JSDOM('
') + 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(null) + useSchematicNetHover({ + svgDivRef, + circuitJson, + circuitJsonKey: "inline-net-label-hover", + enabled: true, + }) + + return ( +
+ + + + + SIGNAL + + +
+ ) + } + + const reactRoot = createRoot(document.getElementById("root")!) + + try { + await act(async () => reactRoot.render()) + + 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", + ) + } finally { + await act(async () => reactRoot.unmount()) + Object.assign(globalThis, { + ...previousGlobals, + IS_REACT_ACT_ENVIRONMENT: false, + }) + dom.window.close() + } +}) From 9f4b3d4eaea67ef1a71424dafde93019073cd657 Mon Sep 17 00:00:00 2001 From: mohan-bee Date: Fri, 28 Aug 2026 00:03:41 +0530 Subject: [PATCH 2/3] up --- lib/components/SchematicViewer.tsx | 2 +- lib/hooks/useSchematicNetHover.ts | 38 ++++++++++++------- .../schematic-inline-net-label-hover.test.tsx | 35 ++++++++++++++++- 3 files changed, 59 insertions(+), 16 deletions(-) diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index ff290bce..9eab44d6 100644 --- a/lib/components/SchematicViewer.tsx +++ b/lib/components/SchematicViewer.tsx @@ -511,7 +511,7 @@ export const SchematicViewer = ({ {netHoverHighlightEnabled && ( )} {searchEnabled && ( diff --git a/lib/hooks/useSchematicNetHover.ts b/lib/hooks/useSchematicNetHover.ts index 53c2266b..0a4e2bbc 100644 --- a/lib/hooks/useSchematicNetHover.ts +++ b/lib/hooks/useSchematicNetHover.ts @@ -1,8 +1,9 @@ import { su } from "@tscircuit/soup-util" -import type { CircuitJson, SourceTrace } 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"] > @@ -13,7 +14,7 @@ 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 INLINE_NET_LABEL_SELECTOR = ".sch-inline-net-label[data-source-trace-id]" +const SCHEMATIC_TEXT_SELECTOR = "[data-schematic-text-id]" /** * Net highlighting on hover, done entirely in JS (the base SVG carries no @@ -25,7 +26,7 @@ const INLINE_NET_LABEL_SELECTOR = ".sch-inline-net-label[data-source-trace-id]" * - 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) - * - inline labels: .sch-inline-net-label[data-source-trace-id] + * - schematic text: [data-schematic-text-id] * * Faded elements get the `sch-net-faded` class (styled by SchematicViewer). */ @@ -44,7 +45,7 @@ export const useSchematicNetHover = ({ const svgDiv = svgDivRef.current if (!enabled || !svgDiv) return - const { componentIdToKeys, netLabelIdToKey, sourceTraceIdToKey } = + const { componentIdToKeys, netLabelIdToKey, schematicTextIdToKey } = buildNetRegistry(circuitJson) // Every net element and the net key(s) it belongs to, plus each hover @@ -90,16 +91,15 @@ export const useSchematicNetHover = ({ netElements.push({ el, keys }) } for (const el of Array.from( - svg.querySelectorAll(INLINE_NET_LABEL_SELECTOR), + svg.querySelectorAll(SCHEMATIC_TEXT_SELECTOR), )) { - const key = sourceTraceIdToKey.get( - el.getAttribute("data-source-trace-id")!, + const key = schematicTextIdToKey.get( + el.getAttribute("data-schematic-text-id")!, ) + if (!key) continue const keys = new Set() - if (key) { - keys.add(key) - triggerNetKeys.set(el, key) - } + keys.add(key) + triggerNetKeys.set(el, key) netElements.push({ el, keys }) } } @@ -120,7 +120,7 @@ export const useSchematicNetHover = ({ return } const trigger = target.closest( - `${TRACE_SELECTOR}, ${NET_LABEL_SELECTOR}, ${INLINE_NET_LABEL_SELECTOR}`, + `${TRACE_SELECTOR}, ${NET_LABEL_SELECTOR}, ${SCHEMATIC_TEXT_SELECTOR}`, ) if (!trigger) { highlightNet(null) @@ -156,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) @@ -191,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). @@ -203,5 +215,5 @@ function buildNetRegistry(circuitJson: CircuitJson) { netLabelIdToKey.set(label.schematic_net_label_id, key) } - return { componentIdToKeys, netLabelIdToKey, sourceTraceIdToKey } + return { componentIdToKeys, netLabelIdToKey, schematicTextIdToKey } } diff --git a/tests/schematic-inline-net-label-hover.test.tsx b/tests/schematic-inline-net-label-hover.test.tsx index b8668082..4bc588c2 100644 --- a/tests/schematic-inline-net-label-hover.test.tsx +++ b/tests/schematic-inline-net-label-hover.test.tsx @@ -20,6 +20,27 @@ const circuitJson: CircuitJson = [ 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 () => { @@ -65,11 +86,18 @@ test("hovering inline net label highlights its source trace net", async () => { /> SIGNAL + + note + ) @@ -93,6 +121,9 @@ test("hovering inline net label highlights its source trace net", async () => { 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, { From 3817c561934388f594bffe4c3507deede605db62 Mon Sep 17 00:00:00 2001 From: mohan-bee Date: Fri, 28 Aug 2026 00:10:22 +0530 Subject: [PATCH 3/3] up --- ...ample35-inline-net-label-hover.fixture.tsx | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 examples/example35-inline-net-label-hover.fixture.tsx diff --git a/examples/example35-inline-net-label-hover.fixture.tsx b/examples/example35-inline-net-label-hover.fixture.tsx new file mode 100644 index 00000000..4256df57 --- /dev/null +++ b/examples/example35-inline-net-label-hover.fixture.tsx @@ -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 () => ( + +)