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 0000000..4256df5 --- /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 () => ( + +) diff --git a/lib/components/SchematicViewer.tsx b/lib/components/SchematicViewer.tsx index e1a92a4..9eab44d 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 7bfb2c7..0a4e2bb 100644 --- a/lib/hooks/useSchematicNetHover.ts +++ b/lib/hooks/useSchematicNetHover.ts @@ -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). */ @@ -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 @@ -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() + 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}, ${SCHEMATIC_TEXT_SELECTOR}`, + ) if (!trigger) { highlightNet(null) return @@ -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) @@ -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>() + 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 ?? "", @@ -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). @@ -175,5 +215,5 @@ function buildNetRegistry(circuitJson: CircuitJson) { netLabelIdToKey.set(label.schematic_net_label_id, key) } - return { componentIdToKeys, netLabelIdToKey } + return { componentIdToKeys, netLabelIdToKey, schematicTextIdToKey } } 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 0000000..4bc588c --- /dev/null +++ b/tests/schematic-inline-net-label-hover.test.tsx @@ -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('
') + 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 + + + note + + +
+ ) + } + + 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", + ) + 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() + } +})