Skip to content
Merged
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
7 changes: 7 additions & 0 deletions lib/components/base-components/Renderable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ export const orderedRenderPhases = [
"SchematicTraceRender",
"SchematicSheetRender",
"SchematicReplaceNetLabelsWithSymbols",
"SchematicLabelNetsWithConflictingNames",
"PanelBoardLayout",
"ValidatePcbCoordinates",
"PcbComponentRender",
Expand Down Expand Up @@ -107,6 +108,12 @@ const asyncPhaseDependencies: Partial<Record<RenderPhase, RenderPhase[]>> = {
"PcbFootprintStringRender",
"FetchPartFootprint",
],
SchematicLabelNetsWithConflictingNames: [
"RenderIsolatedSubcircuits",
"PcbFootprintStringRender",
"FetchPartFootprint",
"SchematicTraceRender",
],
PcbFootprintLayout: ["PcbFootprintStringRender", "FetchPartFootprint"],
PcbComponentSizeCalculation: [
"PcbFootprintStringRender",
Expand Down
13 changes: 13 additions & 0 deletions lib/components/primitive-components/Group/Group.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { assignSchematicNetLabelSuperscripts } from "lib/utils/schematic/assign-schematic-net-label-superscripts"
import {
type SimpleRouteJson as AutorouterSimpleRouteJson,
type RerouteRectRegion,
Expand Down Expand Up @@ -2726,6 +2727,18 @@ export class Group<Props extends z.ZodType<any, any, any> = typeof groupProps>
Group_doInitialStandaloneSubcircuitPcbDesignRuleChecks(this)
}

doInitialSchematicLabelNetsWithConflictingNames() {
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)
}

updateSchematicLabelNetsWithConflictingNames() {
this.doInitialSchematicLabelNetsWithConflictingNames()
}

doInitialSchematicReplaceNetLabelsWithSymbols() {
if (this.root?.schematicDisabled) return
if (!this.isSubcircuit) return
Expand Down
85 changes: 85 additions & 0 deletions lib/utils/schematic/assign-schematic-net-label-superscripts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { CircuitJsonUtilObjects } from "@tscircuit/circuit-json-util"
import type { SourceNet } from "circuit-json"
import {
type ConnectivityMap,
getFullConnectivityMapFromCircuitJson,
} from "circuit-json-to-connectivity-map"

type NetName = SourceNet["name"]
type SourceNetworkId = keyof ConnectivityMap["netMap"]

/** Assign display suffixes during SchematicLabelNetsWithConflictingNames. */
export function assignSchematicNetLabelSuperscripts(
db: CircuitJsonUtilObjects,
) {
const labels = [
...db.schematic_net_label.list(),
...db.schematic_text.list().filter((text) => text.source_trace_id),
]
if (labels.length === 0) return

// Source connectivity is authoritative; PCB routing must not join source nets.
const connMap = getFullConnectivityMapFromCircuitJson(
db.toArray().filter((element) => element.type.startsWith("source_")),
)
// The full-map API omits standalone nets and standalone internal connections.
connMap.addConnections([
...db.source_net.list().map((net) => [net.source_net_id]),
...db.source_component_internal_connection
.list()
.map((connection) => connection.source_port_ids),
])

const labelsWithNetworks = labels.map((label) => {
const isNetLabel = label.type === "schematic_net_label"
const sourceTraceId =
label.source_trace_id ??
(isNetLabel && label.schematic_trace_id
? db.schematic_trace.get(label.schematic_trace_id)?.source_trace_id
: undefined)
const network =
(isNetLabel
? connMap.getNetConnectedToId(label.source_net_id)
: undefined) ??
(sourceTraceId ? connMap.getNetConnectedToId(sourceTraceId) : undefined)
return { label, network }
})

const networksByName = new Map<NetName, SourceNetworkId[]>()
const addName = (name: NetName, network: SourceNetworkId | undefined) => {
if (!name.trim() || network === undefined) return
const networks = networksByName.get(name) ?? []
if (!networks.includes(network)) networks.push(network)
networksByName.set(name, networks)
}
// Only names displayed on multiple networks need disambiguation.
// Unused source-net declarations must not create a visible conflict.
for (const { label, network } of labelsWithNetworks) {
addName(label.text, network)
}

// Member IDs keep numbering stable when Circuit JSON is reordered.
const firstSourceId = (network: SourceNetworkId) =>
[...connMap.getIdsConnectedToNet(network)].sort()[0]!
for (const networks of networksByName.values()) {
networks.sort((a, b) =>
firstSourceId(a).localeCompare(firstSourceId(b), "en", { numeric: true }),
)
}

for (const { label, network } of labelsWithNetworks) {
const networks = networksByName.get(label.text) ?? []
const display_superscript =
network !== undefined && networks.length > 1
? String(networks.indexOf(network) + 1)
: undefined
if (label.display_superscript === display_superscript) continue
if (label.type === "schematic_net_label") {
db.schematic_net_label.update(label.schematic_net_label_id, {
display_superscript,
})
} else {
db.schematic_text.update(label.schematic_text_id, { display_superscript })
}
}
}
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
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<void>((resolve) => {
finishTraceRender = resolve
})
class AsyncLabels extends Renderable {
doInitialSchematicTraceRender() {
this._queueAsyncEffect("labels", () => traceRenderFinished)
}
}
class SuperscriptRoot extends Renderable {
initialCalls = 0
updateCalls = 0
doInitialSchematicLabelNetsWithConflictingNames() {
this.initialCalls++
}
updateSchematicLabelNetsWithConflictingNames() {
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.SchematicLabelNetsWithConflictingNames.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.SchematicLabelNetsWithConflictingNames.dirty,
).toBe(true)
root.runRenderCycle()
expect(root.initialCalls).toBe(1)
expect(root.updateCalls).toBe(1)
expect(
orderedRenderPhases.indexOf("SchematicLabelNetsWithConflictingNames"),
).toBeGreaterThan(
orderedRenderPhases.indexOf("SchematicReplaceNetLabelsWithSymbols"),
)
})
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,23 @@ it("should keep local nets isolated between adjacent subcircuits", async () => {
</board>,
)

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",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { SourceNet } from "circuit-json"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The folder name schematic-net-superscripts is inconsistent with the naming used throughout the rest of the project. The utility file is named assign-schematic-net-label-superscripts.ts, the render phase is called SchematicNetLabelSuperscripts, and the base-components test file is named schematic-net-label-superscripts-phase.test.ts. The folder should be renamed to schematic-net-label-superscripts to match the established naming convention (kebab-case, consistent with other file/export names in the project).

Spotted by Graphite (based on custom rule: Custom rule)

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

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(
<board width={30} height={20}>
{["A", "B", "C"].map((name, index) => (
<subcircuit key={name} name={name} schX={index * 4}>
<resistor name={`R${index + 1}`} resistance="1k" />
<netlabel
net="GND"
connectsTo={`R${index + 1}.pin1`}
schY={-2}
anchorSide="top"
/>
</subcircuit>
))}
<trace from=".A .R1 .pin1" to=".B .R2 .pin1" />
</board>,
)
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)

// 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("SchematicLabelNetsWithConflictingNames")
circuit.render()
expect(
circuit.db.schematic_net_label
.list()
.filter((label) => label.text === "GND")
.every((label) => label.display_superscript === undefined),
).toBe(true)
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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(
<board width={30} height={20} schMaxTraceDistance={10}>
{["A", "B"].map((name, index) => (
<subcircuit key={name} name={name} schY={index * -4}>
<resistor name={`R${index * 2 + 1}`} resistance="1k" schX={-3} />
<resistor name={`R${index * 2 + 2}`} resistance="2k" schX={3} />
<netlabel net="GND" connectsTo={`R${index * 2 + 1}.pin1`} schX={-5} />
<netlabel net="SIGNAL" connectsTo={`R${index * 2 + 1}.pin2`} inline />
<trace from={`R${index * 2 + 2}.pin1`} to="net.SIGNAL" />
<netlabel
net={`UNIQUE_${name}`}
connectsTo={`R${index * 2 + 2}.pin2`}
schX={5}
/>
</subcircuit>
))}
</board>,
)
let suffixesAtPhaseEnd: Array<string | undefined> = []
circuit.on("renderable:renderLifecycle:anyEvent", (event) => {
if (
event.type ===
"renderable:renderLifecycle:SchematicLabelNetsWithConflictingNames: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")
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)
})
Loading
Loading