From 7ded1a30b6ec49deb398591862b0b1c090c82b0e Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 4 Sep 2026 22:31:33 -0700 Subject: [PATCH 1/6] Assign schematic net superscripts for disconnected same-name networks --- lib/IsolatedCircuit.ts | 5 + ...assign-schematic-net-label-superscripts.ts | 123 ++++++++++++++++++ package.json | 6 +- .../connected-subcircuits-schematic.snap.svg | 12 ++ ...isconnected-subcircuits-schematic.snap.svg | 26 ++++ .../named-traces-schematic.snap.svg | 12 ++ .../source-connectivity.snap.svg | 33 +++++ .../connected-subcircuits.test.tsx | 48 +++++++ .../disconnected-subcircuits.test.tsx | 58 +++++++++ .../named-traces.test.tsx | 38 ++++++ .../source-connectivity.test.ts | 109 ++++++++++++++++ 11 files changed, 467 insertions(+), 3 deletions(-) create mode 100644 lib/utils/schematic/assign-schematic-net-label-superscripts.ts create mode 100644 tests/features/schematic-net-superscripts/__snapshots__/connected-subcircuits-schematic.snap.svg create mode 100644 tests/features/schematic-net-superscripts/__snapshots__/disconnected-subcircuits-schematic.snap.svg create mode 100644 tests/features/schematic-net-superscripts/__snapshots__/named-traces-schematic.snap.svg create mode 100644 tests/features/schematic-net-superscripts/__snapshots__/source-connectivity.snap.svg create mode 100644 tests/features/schematic-net-superscripts/connected-subcircuits.test.tsx create mode 100644 tests/features/schematic-net-superscripts/disconnected-subcircuits.test.tsx create mode 100644 tests/features/schematic-net-superscripts/named-traces.test.tsx create mode 100644 tests/features/schematic-net-superscripts/source-connectivity.test.ts diff --git a/lib/IsolatedCircuit.ts b/lib/IsolatedCircuit.ts index f7d0a2086..a8977fc4e 100644 --- a/lib/IsolatedCircuit.ts +++ b/lib/IsolatedCircuit.ts @@ -14,6 +14,8 @@ import type { RootCircuitEventName } from "./events" import { createInstanceFromReactElement } from "./fiber/create-instance-from-react-element" import { isAssemblyDeviceContainer } from "./components/base-components/is-assembly-device-container" +import { assignSchematicNetLabelSuperscripts } from "./utils/schematic/assign-schematic-net-label-superscripts" + export class IsolatedCircuit { firstChild: PrimitiveComponent | null = null children: PrimitiveComponent[] @@ -209,6 +211,9 @@ export class IsolatedCircuit { firstChild.runRenderCycle() this._hasUnrenderedUpdatesFromAsyncEffects = false this._hasRenderedAtleastOnce = true + if (!this.schematicDisabled && !this._hasIncompleteAsyncEffects()) { + assignSchematicNetLabelSuperscripts(db) + } } async renderUntilSettled(): Promise { diff --git a/lib/utils/schematic/assign-schematic-net-label-superscripts.ts b/lib/utils/schematic/assign-schematic-net-label-superscripts.ts new file mode 100644 index 000000000..5efed400e --- /dev/null +++ b/lib/utils/schematic/assign-schematic-net-label-superscripts.ts @@ -0,0 +1,123 @@ +import type { CircuitJsonUtilObjects } from "@tscircuit/circuit-json-util" +import type { SourceNet } from "circuit-json" +import { ConnectivityMap } from "circuit-json-to-connectivity-map" + +type NetName = SourceNet["name"] +// ConnectivityMap currently exports network identifiers only through this API. +type SourceNetworkId = Exclude< + ReturnType, + undefined +> + +/** + * Disambiguate names using source electrical connectivity across all subcircuits. + * Run after rendering settles, including after cached subcircuits are inflated. + * Suffixes are derived display metadata; source names and connectivity stay intact. + */ +export function assignSchematicNetLabelSuperscripts( + db: CircuitJsonUtilObjects, +) { + const schematicNetLabels = db.schematic_net_label.list() + const inlineNetLabels = db.schematic_text + .list() + .filter((text) => text.source_trace_id !== undefined) + if (schematicNetLabels.length === 0 && inlineNetLabels.length === 0) return + + const connectivity = new ConnectivityMap({}) + connectivity.addConnections([ + ...db.source_net.list().map((net) => [net.source_net_id]), + ...db.source_trace + .list() + .map((trace) => [ + trace.source_trace_id, + ...trace.connected_source_net_ids, + ...trace.connected_source_port_ids, + ]), + ...db.source_component + .list() + .flatMap( + (component) => component.internally_connected_source_port_ids ?? [], + ), + ...db.source_component_internal_connection + .list() + .map((connection) => connection.source_port_ids), + ]) + + const networksByName = new Map>() + const addName = (name: NetName, network: SourceNetworkId | undefined) => { + if (!name.trim() || network === undefined) return + const networks = networksByName.get(name) ?? new Set() + networks.add(network) + networksByName.set(name, networks) + } + + for (const net of db.source_net.list()) { + addName(net.name, connectivity.getNetConnectedToId(net.source_net_id)) + } + + const netLabels = schematicNetLabels.map((label) => { + const sourceTraceId = + label.source_trace_id ?? + (label.schematic_trace_id + ? db.schematic_trace.get(label.schematic_trace_id)?.source_trace_id + : undefined) + const network = + connectivity.getNetConnectedToId(label.source_net_id) ?? + (sourceTraceId + ? connectivity.getNetConnectedToId(sourceTraceId) + : undefined) + addName(label.text, network) + return { label, network } + }) + const inlineLabels = inlineNetLabels.map((label) => { + const network = connectivity.getNetConnectedToId(label.source_trace_id!) + addName(label.text, network) + return { label, network } + }) + + const superscriptsByName = new Map>() + for (const [name, networks] of networksByName) { + if (networks.size < 2) continue + // Use a network's member IDs, not the connectivity library's generated net + // number, so reordering Circuit JSON does not change the suffix assignment. + const orderedNetworks = [...networks] + .map((network) => ({ + network, + firstSourceId: [ + ...connectivity.getIdsConnectedToNet(network), + ].sort()[0]!, + })) + .sort((a, b) => + a.firstSourceId.localeCompare(b.firstSourceId, "en", { numeric: true }), + ) + superscriptsByName.set( + name, + new Map( + orderedNetworks.map(({ network }, index) => [ + network, + String(index + 1), + ]), + ), + ) + } + for (const { label, network } of netLabels) { + const display_superscript = + network === undefined + ? undefined + : superscriptsByName.get(label.text)?.get(network) + if (label.display_superscript !== display_superscript) { + db.schematic_net_label.update(label.schematic_net_label_id, { + display_superscript, + }) + } + } + for (const { label, network } of inlineLabels) { + const display_superscript = + network === undefined + ? undefined + : superscriptsByName.get(label.text)?.get(network) + if (label.display_superscript !== display_superscript) { + db.schematic_text.update(label.schematic_text_id, { display_superscript }) + } + } +} diff --git a/package.json b/package.json index 2e13d4b95..23f4107cc 100644 --- a/package.json +++ b/package.json @@ -69,12 +69,12 @@ "bun-match-svg": "0.0.12", "calculate-elbow": "^0.0.12", "chokidar-cli": "^3.0.0", - "circuit-json": "^0.0.481", + "circuit-json": "^0.0.484", "circuit-json-to-bpc": "^0.0.13", "circuit-json-to-connectivity-map": "^0.0.30", "circuit-json-to-gltf": "^0.0.118", "circuit-json-to-spice": "^0.0.45", - "circuit-to-svg": "^0.0.412", + "circuit-to-svg": "^0.0.413", "concurrently": "^9.1.2", "connectivity-map": "^1.0.0", "debug": "^4.3.6", @@ -133,6 +133,6 @@ "overrides": { "@tscircuit/circuit-json-util": "^0.0.106", "@tscircuit/props": "^0.0.646", - "circuit-json": "^0.0.481" + "circuit-json": "^0.0.484" } } diff --git a/tests/features/schematic-net-superscripts/__snapshots__/connected-subcircuits-schematic.snap.svg b/tests/features/schematic-net-superscripts/__snapshots__/connected-subcircuits-schematic.snap.svg new file mode 100644 index 000000000..40d271cb4 --- /dev/null +++ b/tests/features/schematic-net-superscripts/__snapshots__/connected-subcircuits-schematic.snap.svg @@ -0,0 +1,12 @@ +-2,-4-2,-3-2,-2-2,-1-2,0-2,1-1,-4-1,-3-1,-2-1,-1-1,0-1,10,-40,-30,-20,-10,00,11,-41,-31,-21,-11,01,12,-42,-32,-22,-12,02,13,-43,-33,-23,-13,03,14,-44,-34,-24,-14,04,15,-45,-35,-25,-15,05,16,-46,-36,-26,-16,06,17,-47,-37,-27,-17,07,18,-48,-38,-28,-18,08,19,-49,-39,-29,-19,09,1R11kΩR21kΩR31kΩGND1GND1GND2 \ No newline at end of file diff --git a/tests/features/schematic-net-superscripts/__snapshots__/disconnected-subcircuits-schematic.snap.svg b/tests/features/schematic-net-superscripts/__snapshots__/disconnected-subcircuits-schematic.snap.svg new file mode 100644 index 000000000..726d7ea9d --- /dev/null +++ b/tests/features/schematic-net-superscripts/__snapshots__/disconnected-subcircuits-schematic.snap.svg @@ -0,0 +1,26 @@ +-6,-6-6,-5-6,-4-6,-3-6,-2-6,-1-6,0-6,1-5,-6-5,-5-5,-4-5,-3-5,-2-5,-1-5,0-5,1-4,-6-4,-5-4,-4-4,-3-4,-2-4,-1-4,0-4,1-3,-6-3,-5-3,-4-3,-3-3,-2-3,-1-3,0-3,1-2,-6-2,-5-2,-4-2,-3-2,-2-2,-1-2,0-2,1-1,-6-1,-5-1,-4-1,-3-1,-2-1,-1-1,0-1,10,-60,-50,-40,-30,-20,-10,00,11,-61,-51,-41,-31,-21,-11,01,12,-62,-52,-42,-32,-22,-12,02,13,-63,-53,-43,-33,-23,-13,03,14,-64,-54,-44,-34,-24,-14,04,15,-65,-55,-45,-35,-25,-15,05,16,-66,-56,-46,-36,-26,-16,06,17,-67,-57,-47,-37,-27,-17,07,1R11kΩR22kΩR31kΩR42kΩGND1UNIQUE_AGND2UNIQUE_BSIGNAL1SIGNAL1SIGNAL2SIGNAL2 \ No newline at end of file diff --git a/tests/features/schematic-net-superscripts/__snapshots__/named-traces-schematic.snap.svg b/tests/features/schematic-net-superscripts/__snapshots__/named-traces-schematic.snap.svg new file mode 100644 index 000000000..d20a73d38 --- /dev/null +++ b/tests/features/schematic-net-superscripts/__snapshots__/named-traces-schematic.snap.svg @@ -0,0 +1,12 @@ +-4,-5-4,-4-4,-3-4,-2-4,-1-4,0-4,1-3,-5-3,-4-3,-3-3,-2-3,-1-3,0-3,1-2,-5-2,-4-2,-3-2,-2-2,-1-2,0-2,1-1,-5-1,-4-1,-3-1,-2-1,-1-1,0-1,10,-50,-40,-30,-20,-10,00,11,-51,-41,-31,-21,-11,01,12,-52,-42,-32,-22,-12,02,13,-53,-43,-33,-23,-13,03,14,-54,-44,-34,-24,-14,04,1R11kΩR22kΩR31kΩR42kΩBUS1BUS2 \ No newline at end of file diff --git a/tests/features/schematic-net-superscripts/__snapshots__/source-connectivity.snap.svg b/tests/features/schematic-net-superscripts/__snapshots__/source-connectivity.snap.svg new file mode 100644 index 000000000..30a89fc9a --- /dev/null +++ b/tests/features/schematic-net-superscripts/__snapshots__/source-connectivity.snap.svg @@ -0,0 +1,33 @@ +GND1GND1GND2GND1GND1GND2GNDnote \ No newline at end of file diff --git a/tests/features/schematic-net-superscripts/connected-subcircuits.test.tsx b/tests/features/schematic-net-superscripts/connected-subcircuits.test.tsx new file mode 100644 index 000000000..f7b138588 --- /dev/null +++ b/tests/features/schematic-net-superscripts/connected-subcircuits.test.tsx @@ -0,0 +1,48 @@ +import type { SourceNet } from "circuit-json" +import { expect, test } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-fixture" + +test("connected same-name nets share one superscript and lose it when all networks join", async () => { + const { circuit } = getTestFixture({ platform: { pcbDisabled: true } }) + circuit.add( + + {["A", "B", "C"].map((name, index) => ( + + + + + ))} + + , + ) + await circuit.renderUntilSettled() + const nets = circuit.db.source_net.list().filter((net) => net.name === "GND") + expect(nets).toHaveLength(3) + const suffixForNet = (netId: SourceNet["source_net_id"]) => + circuit.db.schematic_net_label + .list() + .find((label) => label.source_net_id === netId)?.display_superscript + expect(suffixForNet(nets[0]!.source_net_id)).toBe("1") + expect(suffixForNet(nets[1]!.source_net_id)).toBe("1") + expect(suffixForNet(nets[2]!.source_net_id)).toBe("2") + expect(circuit).toMatchSchematicSnapshot(import.meta.path) + + // Recompute after source connectivity changes, as when a cached subcircuit + // with its own suffixes is inflated into a connected parent circuit. + circuit.db.source_trace.insert({ + connected_source_net_ids: nets.map((net) => net.source_net_id), + connected_source_port_ids: [], + }) + circuit.render() + expect( + circuit.db.schematic_net_label + .list() + .filter((label) => label.text === "GND") + .every((label) => label.display_superscript === undefined), + ).toBe(true) +}) diff --git a/tests/features/schematic-net-superscripts/disconnected-subcircuits.test.tsx b/tests/features/schematic-net-superscripts/disconnected-subcircuits.test.tsx new file mode 100644 index 000000000..0d52c50e3 --- /dev/null +++ b/tests/features/schematic-net-superscripts/disconnected-subcircuits.test.tsx @@ -0,0 +1,58 @@ +import { expect, test } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-fixture" + +test("disconnected same-name nets get stable superscripts on regular and inline labels", async () => { + const { circuit } = getTestFixture({ platform: { pcbDisabled: true } }) + circuit.add( + + {["A", "B"].map((name, index) => ( + + + + + + + + + ))} + , + ) + await circuit.renderUntilSettled() + const groundLabels = circuit.db.schematic_net_label + .list() + .filter((label) => label.text === "GND") + const signalLabels = circuit.db.schematic_text + .list() + .filter((label) => label.text === "SIGNAL") + expect( + new Set(groundLabels.map((label) => label.display_superscript)), + ).toEqual(new Set(["1", "2"])) + expect( + new Set(signalLabels.map((label) => label.display_superscript)), + ).toEqual(new Set(["1", "2"])) + expect( + circuit.db.schematic_net_label + .list() + .filter((label) => label.text.startsWith("UNIQUE_")) + .every((label) => label.display_superscript === undefined), + ).toBe(true) + const before = [...groundLabels, ...signalLabels].map( + (label) => label.display_superscript, + ) + circuit.render() + expect( + [ + ...circuit.db.schematic_net_label + .list() + .filter((label) => label.text === "GND"), + ...circuit.db.schematic_text + .list() + .filter((label) => label.text === "SIGNAL"), + ].map((label) => label.display_superscript), + ).toEqual(before) + expect(circuit).toMatchSchematicSnapshot(import.meta.path) +}) diff --git a/tests/features/schematic-net-superscripts/named-traces.test.tsx b/tests/features/schematic-net-superscripts/named-traces.test.tsx new file mode 100644 index 000000000..f47d78276 --- /dev/null +++ b/tests/features/schematic-net-superscripts/named-traces.test.tsx @@ -0,0 +1,38 @@ +import { expect, test } from "bun:test" +import { getTestFixture } from "tests/fixtures/get-test-fixture" + +test("inline names on disconnected traces are disambiguated without source nets", async () => { + const { circuit } = getTestFixture({ platform: { pcbDisabled: true } }) + circuit.add( + + {["A", "B"].map((name, index) => ( + + + + + + ))} + , + ) + await circuit.renderUntilSettled() + expect(circuit.db.source_net.list()).toHaveLength(0) + const labels = circuit.db.schematic_text + .list() + .filter((text) => text.text === "BUS") + expect(labels).toHaveLength(2) + expect(labels.map((label) => label.display_superscript).sort()).toEqual([ + "1", + "2", + ]) + expect( + circuit.db.schematic_text + .list() + .filter((text) => !text.source_trace_id) + .every((text) => text.display_superscript === undefined), + ).toBe(true) + expect(circuit).toMatchSchematicSnapshot(import.meta.path) +}) diff --git a/tests/features/schematic-net-superscripts/source-connectivity.test.ts b/tests/features/schematic-net-superscripts/source-connectivity.test.ts new file mode 100644 index 000000000..35423d377 --- /dev/null +++ b/tests/features/schematic-net-superscripts/source-connectivity.test.ts @@ -0,0 +1,109 @@ +import type { AnyCircuitElement } from "circuit-json" +import { expect, test } from "bun:test" +import { cju } from "@tscircuit/circuit-json-util" +import { convertCircuitJsonToSchematicSvg } from "circuit-to-svg" +import { assignSchematicNetLabelSuperscripts } from "lib/utils/schematic/assign-schematic-net-label-superscripts" + +test("internal connections, mixed label kinds, and reordered source JSON use the same network numbering", () => { + const circuitJson: AnyCircuitElement[] = [] + for (const [index, id] of ["a", "b", "c"].entries()) { + circuitJson.push({ + type: "source_net", + source_net_id: `net_${id}`, + name: "GND", + member_source_group_ids: [], + subcircuit_id: id, + subcircuit_connectivity_map_key: "same_scoped_key", + }) + circuitJson.push({ + type: "source_trace", + source_trace_id: `trace_${id}`, + connected_source_net_ids: [`net_${id}`], + connected_source_port_ids: [`port_${id}`], + }) + circuitJson.push({ + type: "schematic_net_label", + schematic_net_label_id: `label_${id}`, + source_net_id: `net_${id}`, + text: "GND", + center: { x: index * 2, y: 0 }, + anchor_position: { x: index * 2, y: 0 }, + anchor_side: "left", + }) + circuitJson.push({ + type: "schematic_text", + schematic_text_id: `inline_${id}`, + font_size: 0.18, + rotation: 0, + color: "black", + source_trace_id: `trace_${id}`, + text: "GND", + position: { x: index * 2, y: -1 }, + anchor: "left", + }) + } + circuitJson.push({ + type: "source_component_internal_connection", + source_component_internal_connection_id: "internal_connection", + source_component_id: "chip", + source_port_ids: ["port_a", "port_bridge"], + }) + circuitJson.push({ + type: "source_component", + source_component_id: "chip", + name: "U1", + ftype: "simple_chip", + internally_connected_source_port_ids: [["port_bridge", "port_b"]], + }) + circuitJson.push({ + type: "schematic_text", + schematic_text_id: "ordinary_text", + font_size: 0.18, + rotation: 0, + color: "black", + anchor: "left", + text: "GND", + display_superscript: "note", + position: { x: 0, y: -2 }, + }) + const db = cju(circuitJson) + assignSchematicNetLabelSuperscripts(db) + expect( + db.schematic_net_label.list().map((label) => label.display_superscript), + ).toEqual(["1", "1", "2"]) + expect( + db.schematic_text.list().map((label) => label.display_superscript), + ).toEqual(["1", "1", "2", "note"]) + const reversed = cju(db.toArray().toReversed()) + assignSchematicNetLabelSuperscripts(reversed) + expect( + reversed.schematic_net_label + .list() + .map((label) => label.display_superscript), + ).toEqual(["2", "1", "1"]) + expect( + reversed.schematic_text.get("ordinary_text")!.display_superscript, + ).toBe("note") + expect(convertCircuitJsonToSchematicSvg(db.toArray())).toMatchSvgSnapshot( + import.meta.path, + ) + db.source_trace.insert({ + connected_source_net_ids: ["net_b", "net_c"], + connected_source_port_ids: [], + }) + assignSchematicNetLabelSuperscripts(db) + expect( + db.schematic_net_label + .list() + .every((label) => label.display_superscript === undefined), + ).toBe(true) + expect( + db.schematic_text + .list() + .filter((label) => label.source_trace_id) + .every((label) => label.display_superscript === undefined), + ).toBe(true) + expect(db.schematic_text.get("ordinary_text")!.display_superscript).toBe( + "note", + ) +}) From 60992310acf2c263dc871d7642bc95394ca1938f Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 4 Sep 2026 22:42:03 -0700 Subject: [PATCH 2/6] Assign net superscripts through a dedicated schematic render phase --- lib/IsolatedCircuit.ts | 5 -- lib/components/base-components/Renderable.ts | 7 +++ .../primitive-components/Group/Group.ts | 13 +++++ ...assign-schematic-net-label-superscripts.ts | 3 +- ...matic-net-label-superscripts-phase.test.ts | 53 +++++++++++++++++++ .../connected-subcircuits.test.tsx | 4 +- .../disconnected-subcircuits.test.tsx | 15 ++++++ 7 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 tests/components/base-components/schematic-net-label-superscripts-phase.test.ts diff --git a/lib/IsolatedCircuit.ts b/lib/IsolatedCircuit.ts index a8977fc4e..f7d0a2086 100644 --- a/lib/IsolatedCircuit.ts +++ b/lib/IsolatedCircuit.ts @@ -14,8 +14,6 @@ import type { RootCircuitEventName } from "./events" import { createInstanceFromReactElement } from "./fiber/create-instance-from-react-element" import { isAssemblyDeviceContainer } from "./components/base-components/is-assembly-device-container" -import { assignSchematicNetLabelSuperscripts } from "./utils/schematic/assign-schematic-net-label-superscripts" - export class IsolatedCircuit { firstChild: PrimitiveComponent | null = null children: PrimitiveComponent[] @@ -211,9 +209,6 @@ export class IsolatedCircuit { firstChild.runRenderCycle() this._hasUnrenderedUpdatesFromAsyncEffects = false this._hasRenderedAtleastOnce = true - if (!this.schematicDisabled && !this._hasIncompleteAsyncEffects()) { - assignSchematicNetLabelSuperscripts(db) - } } async renderUntilSettled(): Promise { diff --git a/lib/components/base-components/Renderable.ts b/lib/components/base-components/Renderable.ts index 44152b2e4..ffbfd97a8 100644 --- a/lib/components/base-components/Renderable.ts +++ b/lib/components/base-components/Renderable.ts @@ -48,6 +48,7 @@ export const orderedRenderPhases = [ "SchematicTraceRender", "SchematicSheetRender", "SchematicReplaceNetLabelsWithSymbols", + "SchematicNetLabelSuperscripts", "PanelBoardLayout", "ValidatePcbCoordinates", "PcbComponentRender", @@ -107,6 +108,12 @@ const asyncPhaseDependencies: Partial> = { "PcbFootprintStringRender", "FetchPartFootprint", ], + SchematicNetLabelSuperscripts: [ + "RenderIsolatedSubcircuits", + "PcbFootprintStringRender", + "FetchPartFootprint", + "SchematicTraceRender", + ], PcbFootprintLayout: ["PcbFootprintStringRender", "FetchPartFootprint"], PcbComponentSizeCalculation: [ "PcbFootprintStringRender", diff --git a/lib/components/primitive-components/Group/Group.ts b/lib/components/primitive-components/Group/Group.ts index a75da1a9d..29fd02ad5 100644 --- a/lib/components/primitive-components/Group/Group.ts +++ b/lib/components/primitive-components/Group/Group.ts @@ -1,3 +1,4 @@ +import { assignSchematicNetLabelSuperscripts } from "lib/utils/schematic/assign-schematic-net-label-superscripts" import { type SimpleRouteJson as AutorouterSimpleRouteJson, type RerouteRectRegion, @@ -2726,6 +2727,18 @@ export class Group = typeof groupProps> Group_doInitialStandaloneSubcircuitPcbDesignRuleChecks(this) } + doInitialSchematicNetLabelSuperscripts() { + if (this.root?.schematicDisabled) return + // Number networks once for the whole circuit, after every group's labels + // exist, rather than independently numbering sibling subcircuits. + if (this.getTopLevelRenderable() !== this) return + assignSchematicNetLabelSuperscripts(this.root!.db) + } + + updateSchematicNetLabelSuperscripts() { + this.doInitialSchematicNetLabelSuperscripts() + } + doInitialSchematicReplaceNetLabelsWithSymbols() { if (this.root?.schematicDisabled) return if (!this.isSubcircuit) return diff --git a/lib/utils/schematic/assign-schematic-net-label-superscripts.ts b/lib/utils/schematic/assign-schematic-net-label-superscripts.ts index 5efed400e..06e8cd8e6 100644 --- a/lib/utils/schematic/assign-schematic-net-label-superscripts.ts +++ b/lib/utils/schematic/assign-schematic-net-label-superscripts.ts @@ -11,7 +11,8 @@ type SourceNetworkId = Exclude< /** * Disambiguate names using source electrical connectivity across all subcircuits. - * Run after rendering settles, including after cached subcircuits are inflated. + * Called by the top-level group during SchematicNetLabelSuperscripts, after + * schematic labels exist throughout the circuit. * Suffixes are derived display metadata; source names and connectivity stay intact. */ export function assignSchematicNetLabelSuperscripts( diff --git a/tests/components/base-components/schematic-net-label-superscripts-phase.test.ts b/tests/components/base-components/schematic-net-label-superscripts-phase.test.ts new file mode 100644 index 000000000..bf6b1f73e --- /dev/null +++ b/tests/components/base-components/schematic-net-label-superscripts-phase.test.ts @@ -0,0 +1,53 @@ +import { expect, test } from "bun:test" +import { + Renderable, + orderedRenderPhases, +} from "lib/components/base-components/Renderable" + +test("superscript phase waits for descendant label work and supports dirty updates", async () => { + let finishTraceRender!: () => void + const traceRenderFinished = new Promise((resolve) => { + finishTraceRender = resolve + }) + class AsyncLabels extends Renderable { + doInitialSchematicTraceRender() { + this._queueAsyncEffect("labels", () => traceRenderFinished) + } + } + class SuperscriptRoot extends Renderable { + initialCalls = 0 + updateCalls = 0 + doInitialSchematicNetLabelSuperscripts() { + this.initialCalls++ + } + updateSchematicNetLabelSuperscripts() { + this.updateCalls++ + } + } + const root = new SuperscriptRoot({}) + const child = new AsyncLabels({}) + root.children.push(child) + child.parent = root + root.runRenderCycle() + expect(root.initialCalls).toBe(0) + expect(root.renderPhaseStates.SchematicNetLabelSuperscripts.initialized).toBe( + false, + ) + finishTraceRender() + await traceRenderFinished + await Promise.resolve() + root.runRenderCycle() + expect(root.initialCalls).toBe(1) + root.runRenderCycle() + expect(root.updateCalls).toBe(0) + child._markDirty("SourceTraceRender") + expect(root.renderPhaseStates.SchematicNetLabelSuperscripts.dirty).toBe(true) + root.runRenderCycle() + expect(root.initialCalls).toBe(1) + expect(root.updateCalls).toBe(1) + expect( + orderedRenderPhases.indexOf("SchematicNetLabelSuperscripts"), + ).toBeGreaterThan( + orderedRenderPhases.indexOf("SchematicReplaceNetLabelsWithSymbols"), + ) +}) diff --git a/tests/features/schematic-net-superscripts/connected-subcircuits.test.tsx b/tests/features/schematic-net-superscripts/connected-subcircuits.test.tsx index f7b138588..eaa8e1a4b 100644 --- a/tests/features/schematic-net-superscripts/connected-subcircuits.test.tsx +++ b/tests/features/schematic-net-superscripts/connected-subcircuits.test.tsx @@ -32,12 +32,12 @@ test("connected same-name nets share one superscript and lose it when all networ expect(suffixForNet(nets[2]!.source_net_id)).toBe("2") expect(circuit).toMatchSchematicSnapshot(import.meta.path) - // Recompute after source connectivity changes, as when a cached subcircuit - // with its own suffixes is inflated into a connected parent circuit. + // A direct database edit must dirty the phase that consumes the new data. circuit.db.source_trace.insert({ connected_source_net_ids: nets.map((net) => net.source_net_id), connected_source_port_ids: [], }) + circuit.firstChild!._markDirty("SchematicNetLabelSuperscripts") circuit.render() expect( circuit.db.schematic_net_label diff --git a/tests/features/schematic-net-superscripts/disconnected-subcircuits.test.tsx b/tests/features/schematic-net-superscripts/disconnected-subcircuits.test.tsx index 0d52c50e3..adab14439 100644 --- a/tests/features/schematic-net-superscripts/disconnected-subcircuits.test.tsx +++ b/tests/features/schematic-net-superscripts/disconnected-subcircuits.test.tsx @@ -21,7 +21,22 @@ test("disconnected same-name nets get stable superscripts on regular and inline ))} , ) + let suffixesAtPhaseEnd: Array = [] + circuit.on("renderable:renderLifecycle:anyEvent", (event) => { + if ( + event.type === + "renderable:renderLifecycle:SchematicNetLabelSuperscripts:end" && + event.renderId === circuit.firstChild!._renderId + ) { + suffixesAtPhaseEnd = circuit.db.schematic_net_label + .list() + .filter((label) => label.text === "GND") + .map((label) => label.display_superscript) + } + }) await circuit.renderUntilSettled() + expect(new Set(suffixesAtPhaseEnd)).toEqual(new Set(["1", "2"])) + const groundLabels = circuit.db.schematic_net_label .list() .filter((label) => label.text === "GND") From 56d1c585f12a1d7c0ba7242a2b5fc92fabf304ef Mon Sep 17 00:00:00 2001 From: seveibar Date: Fri, 4 Sep 2026 22:51:49 -0700 Subject: [PATCH 3/6] Update schematic snapshots for disambiguated local net names --- ...al-components-two-chips-schematic.snap.svg | 2 +- .../chip-with-subcircuit-net-label.test.tsx | 18 ++++++- ...bcircuit-circuit-json14-schematic.snap.svg | 52 +++++++++---------- .../subcircuit-circuit-json14.test.tsx | 19 +++++++ .../__snapshots__/index-schematic.snap.svg | 26 +++++----- ...ator-and-sensor-schematic_stacked.snap.svg | 8 +-- ...-sensor-and-mcu-schematic_stacked.snap.svg | 8 +-- ...u-and-regulator-schematic_stacked.snap.svg | 8 +-- ...ted-subcircuits-schematic_stacked.snap.svg | 6 +-- ...out-subcircuits-schematic_stacked.snap.svg | 6 +-- ...es-to-one-sheet-schematic_stacked.snap.svg | 12 ++--- 11 files changed, 100 insertions(+), 65 deletions(-) diff --git a/tests/components/__snapshots__/normal-components-two-chips-schematic.snap.svg b/tests/components/__snapshots__/normal-components-two-chips-schematic.snap.svg index 4849051ef..a21296a15 100644 --- a/tests/components/__snapshots__/normal-components-two-chips-schematic.snap.svg +++ b/tests/components/__snapshots__/normal-components-two-chips-schematic.snap.svg @@ -9,4 +9,4 @@ .port-label { fill: rgb(0, 100, 100); } .component-name { fill: rgb(0, 100, 100); } - 2,-22,-12,02,12,23,-23,-13,03,13,24,-24,-14,04,14,25,-25,-15,05,15,26,-26,-16,06,16,27,-27,-17,07,17,28,-28,-18,08,18,29,-29,-19,09,19,210,-210,-110,010,110,211,-211,-111,011,111,212,-212,-112,012,112,213,-213,-113,013,113,214,-214,-114,014,114,215,-215,-115,015,115,216,-216,-116,016,116,2U_A1IN_A2OUT_A3INTERNAL8GND4567U_B1IN_B2OUT_B3INTERNAL8GND4567GNDGNDEXTERNALINTERNALEXTERNALINTERNAL \ No newline at end of file + 2,-22,-12,02,12,23,-23,-13,03,13,24,-24,-14,04,14,25,-25,-15,05,15,26,-26,-16,06,16,27,-27,-17,07,17,28,-28,-18,08,18,29,-29,-19,09,19,210,-210,-110,010,110,211,-211,-111,011,111,212,-212,-112,012,112,213,-213,-113,013,113,214,-214,-114,014,114,215,-215,-115,015,115,216,-216,-116,016,116,2U_A1IN_A2OUT_A3INTERNAL8GND4567U_B1IN_B2OUT_B3INTERNAL8GND4567GNDGNDEXTERNALINTERNAL1EXTERNALINTERNAL2 \ No newline at end of file diff --git a/tests/components/normal-components/chip-with-subcircuit-net-label.test.tsx b/tests/components/normal-components/chip-with-subcircuit-net-label.test.tsx index c055c7452..78e750f23 100644 --- a/tests/components/normal-components/chip-with-subcircuit-net-label.test.tsx +++ b/tests/components/normal-components/chip-with-subcircuit-net-label.test.tsx @@ -75,7 +75,23 @@ it("should keep local nets isolated between adjacent subcircuits", async () => { , ) - circuit.render() + await circuit.renderUntilSettled() + + const labels = [ + ...circuit.db.schematic_net_label.list(), + ...circuit.db.schematic_text.list(), + ] + expect( + labels + .filter((label) => label.text === "INTERNAL") + .map((label) => label.display_superscript) + .sort(), + ).toEqual(["1", "2"]) + expect( + labels + .filter((label) => label.text === "EXTERNAL" || label.text === "GND") + .every((label) => label.display_superscript === undefined), + ).toBe(true) expect(circuit.getCircuitJson()).toMatchSchematicSnapshot( import.meta.dir + "-two-chips", diff --git a/tests/features/subcircuit-circuit-json/__snapshots__/subcircuit-circuit-json14-schematic.snap.svg b/tests/features/subcircuit-circuit-json/__snapshots__/subcircuit-circuit-json14-schematic.snap.svg index e4711a335..c08363613 100644 --- a/tests/features/subcircuit-circuit-json/__snapshots__/subcircuit-circuit-json14-schematic.snap.svg +++ b/tests/features/subcircuit-circuit-json/__snapshots__/subcircuit-circuit-json14-schematic.snap.svg @@ -1,4 +1,4 @@ - \ No newline at end of file diff --git a/tests/features/subcircuit-circuit-json/subcircuit-circuit-json14.test.tsx b/tests/features/subcircuit-circuit-json/subcircuit-circuit-json14.test.tsx index 3a05bb3a6..a3a2f4ff5 100644 --- a/tests/features/subcircuit-circuit-json/subcircuit-circuit-json14.test.tsx +++ b/tests/features/subcircuit-circuit-json/subcircuit-circuit-json14.test.tsx @@ -70,6 +70,25 @@ test("subcircuit-circuit-json14 - subcircuit name prop being passed to the chip const pcbTrace = circuit.db.pcb_trace.list() expect(pcbTrace).toHaveLength(2) + // Only the exposed I2C nets are joined; local supply nets stay separate. + const labels = circuit.db.schematic_net_label.list() + for (const name of ["GND", "VCC"]) { + expect( + new Set( + labels + .filter((label) => label.text === name) + .map((label) => label.display_superscript), + ), + ).toEqual(new Set(["1", "2"])) + } + expect( + labels + .filter( + (label) => label.text.includes("SDA") || label.text.includes("SCL"), + ) + .every((label) => label.display_superscript === undefined), + ).toBe(true) + expect(circuit).toMatchPcbSnapshot(import.meta.path) expect(circuit).toMatchSchematicSnapshot(import.meta.path) }) diff --git a/tests/projects/rp2040/__snapshots__/index-schematic.snap.svg b/tests/projects/rp2040/__snapshots__/index-schematic.snap.svg index 0b198a853..ddc71e8ab 100644 --- a/tests/projects/rp2040/__snapshots__/index-schematic.snap.svg +++ b/tests/projects/rp2040/__snapshots__/index-schematic.snap.svg @@ -1,4 +1,4 @@ - \ No newline at end of file diff --git a/tests/repros/__snapshots__/repro134-multi-sheet-regulator-and-sensor-schematic_stacked.snap.svg b/tests/repros/__snapshots__/repro134-multi-sheet-regulator-and-sensor-schematic_stacked.snap.svg index aae4b2043..904f68565 100644 --- a/tests/repros/__snapshots__/repro134-multi-sheet-regulator-and-sensor-schematic_stacked.snap.svg +++ b/tests/repros/__snapshots__/repro134-multi-sheet-regulator-and-sensor-schematic_stacked.snap.svg @@ -1,4 +1,4 @@ -Sheet 1