@tscircuit/schematic-trace-solver 0.0.159 → 0.0.161

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/index.d.ts +43 -9
  2. package/dist/index.js +807 -115
  3. package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +210 -6
  4. package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +536 -94
  5. package/lib/solvers/InlineNetLabelSolver/pushAnchoredNetLabelsAwayFromInlineLabels.ts +9 -7
  6. package/lib/solvers/InlineNetLabelSolver/pushInlineTerminalLabelsAwayFromAnchoredLabels.ts +295 -0
  7. package/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts +7 -35
  8. package/lib/solvers/SchematicTraceLinesSolver/getTraceConnectedPinComponents.ts +60 -0
  9. package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getFixedLabelCoordinate.ts +44 -13
  10. package/lib/types/InputProblem.ts +5 -4
  11. package/package.json +1 -1
  12. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +1 -1
  13. package/tests/bug-reports/bug-report-20260728T144234Z/__snapshots__/bug-report-20260728T144234Z.snap.svg +1 -1
  14. package/tests/bug-reports/bug-report-20260728T225606Z/__snapshots__/bug-report-20260728T225606Z.snap.svg +79 -79
  15. package/tests/bug-reports/bug-report-20260730T061837Z/__snapshots__/bug-report-20260730T061837Z.snap.svg +85 -85
  16. package/tests/bug-reports/bug-report-20260731T052229Z/__snapshots__/bug-report-20260731T052229Z.snap.svg +1 -1
  17. package/tests/bug-reports/bug-report-20260804T095800Z/__snapshots__/bug-report-20260804T095800Z.snap.svg +163 -163
  18. package/tests/bug-reports/bug-report-20260804T171919Z/__snapshots__/bug-report-20260804T171919Z.snap.svg +547 -547
  19. package/tests/bug-reports/bug-report-20260804T225912Z/__snapshots__/bug-report-20260804T225912Z.snap.svg +13 -13
  20. package/tests/bug-reports/bug-report-20260805T061316Z/__snapshots__/bug-report-20260805T061316Z.snap.svg +267 -267
  21. package/tests/bug-reports/bug-report-20260806T061548Z/__snapshots__/bug-report-20260806T061548Z.snap.svg +267 -267
  22. package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +109 -109
  23. package/tests/bug-reports/bug-report-20260811T075142Z/__snapshots__/bug-report-20260811T075142Z.snap.svg +1 -1
  24. package/tests/bug-reports/bug-report-20260815T073240Z/__snapshots__/bug-report-20260815T073240Z.snap.svg +49 -49
  25. package/tests/bug-reports/bug-report-20260819T091818Z/__snapshots__/bug-report-20260819T091818Z.snap.svg +1 -1
  26. package/tests/bug-reports/bug-report-20260825T045913Z/__snapshots__/bug-report-20260825T045913Z.snap.svg +103 -103
  27. package/tests/bug-reports/bug-report-20260825T103621Z/__snapshots__/bug-report-20260825T103621Z.snap.svg +103 -103
  28. package/tests/bug-reports/bug-report-20260826T072956Z/__snapshots__/bug-report-20260826T072956Z.snap.svg +600 -792
  29. package/tests/bug-reports/bug-report-20260826T072956Z/bug-report-20260826T072956Z.test.ts +66 -1
  30. package/tests/examples/__snapshots__/example51.snap.svg +568 -568
  31. package/tests/fixtures/convertSolverOutputToCircuitJson.test.ts +16 -1
  32. package/tests/fixtures/convertSolverOutputToCircuitJson.ts +23 -12
  33. package/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg +736 -736
  34. package/tests/repros/__snapshots__/board-15984-same-net-junction.snap.svg +79 -79
  35. package/tests/repros/__snapshots__/board1096-usb-label-overlap-iteration.snap.svg +127 -127
  36. package/tests/repros/__snapshots__/repro-bluetooth-controller-ground-decoupling-groups.snap.svg +106 -0
  37. package/tests/repros/__snapshots__/repro-board-589-regulator-section.snap.svg +31 -31
  38. package/tests/repros/__snapshots__/repro-board-648-esp12f-section.snap.svg +121 -121
  39. package/tests/repros/__snapshots__/repro-bq25895-cross-net-trace-overlap.snap.svg +43 -43
  40. package/tests/repros/__snapshots__/repro-core-555-power-cross-net-overlap.snap.svg +19 -19
  41. package/tests/repros/__snapshots__/repro-core-ground-inline-label-fallback.snap.svg +277 -0
  42. package/tests/repros/__snapshots__/repro-core-subcircuit-missing-ground.snap.svg +19 -19
  43. package/tests/repros/__snapshots__/repro-core-tb67s579ftg-inline-label-routing.snap.svg +7 -7
  44. package/tests/repros/__snapshots__/repro-dc-power-isolated-routing.snap.svg +37 -37
  45. package/tests/repros/__snapshots__/repro-hdc2080-ground-label-alignment.snap.svg +137 -0
  46. package/tests/repros/__snapshots__/repro-nrf52810-clock-routing.snap.svg +61 -61
  47. package/tests/repros/__snapshots__/repro-pga300-redundant-ground-traces.snap.svg +115 -115
  48. package/tests/repros/__snapshots__/repro-pmp11282-isolated-dcdc.snap.svg +743 -743
  49. package/tests/repros/__snapshots__/repro-rp2040-usb-cc2-ground-overlap.snap.svg +43 -43
  50. package/tests/repros/__snapshots__/repro-ti-power-output-section.snap.svg +1 -1
  51. package/tests/repros/__snapshots__/repro-tida010076-j4-ground-bus.snap.svg +17 -17
  52. package/tests/repros/__snapshots__/repro-tida010076-page02.snap.svg +1131 -1131
  53. package/tests/repros/__snapshots__/repro-type-c-16pin-self-connection.snap.svg +7 -7
  54. package/tests/repros/__snapshots__/repro-type-c-16pin-to-resistor.snap.svg +7 -7
  55. package/tests/repros/__snapshots__/repro-usb-power-vbus-label-detour.snap.svg +666 -666
  56. package/tests/repros/__snapshots__/repro161-power-section-autolayout.snap.svg +73 -73
  57. package/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg +73 -73
  58. package/tests/repros/assets/repro-bluetooth-controller-ground-decoupling-groups.input.json +214 -0
  59. package/tests/repros/assets/repro-core-ground-inline-label-fallback.input.json +863 -0
  60. package/tests/repros/repro-bluetooth-controller-ground-decoupling-groups.test.ts +18 -0
  61. package/tests/repros/repro-core-ground-inline-label-fallback.test.ts +34 -0
  62. package/tests/repros/repro-hdc2080-ground-label-alignment.test.ts +239 -0
  63. package/tests/repros/repro-tida010076-j4-ground-bus.test.ts +14 -10
  64. package/tests/repros/repro-tida010076-page02.test.ts +2 -2
  65. package/tests/repros/repro-usb-power-vbus-label-detour.test.ts +14 -0
  66. package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label02.snap.svg +1 -1
  67. package/tests/solvers/InlineNetLabelSolver/multi-pin-connected-components.test.ts +252 -0
  68. package/tests/solvers/InlineNetLabelSolver/opposite-side-placement.test.ts +71 -0
  69. package/tests/solvers/InlineNetLabelSolver/push-anchored-net-labels-away.test.ts +8 -2
  70. package/tests/solvers/InlineNetLabelSolver/push-inline-terminal-labels-away.test.ts +91 -0
  71. package/tests/solvers/InlineNetLabelSolver/two-pin-terminal-stubs-atomic.test.ts +7 -1
@@ -485,13 +485,15 @@ export const pushAnchoredNetLabelsAwayFromInlineLabels = ({
485
485
  inlineBounds.some((bounds) =>
486
486
  pathIntersectsBounds(connector.tracePath, bounds),
487
487
  ) ||
488
- inputProblem.chips.some((chip) =>
489
- pathIntersectsBounds(connector.tracePath, {
490
- minX: chip.center.x - chip.width / 2,
491
- maxX: chip.center.x + chip.width / 2,
492
- minY: chip.center.y - chip.height / 2,
493
- maxY: chip.center.y + chip.height / 2,
494
- }),
488
+ inputProblem.chips.some(
489
+ (chip) =>
490
+ !ownerChipIds.has(chip.chipId) &&
491
+ pathIntersectsBounds(connector.tracePath, {
492
+ minX: chip.center.x - chip.width / 2,
493
+ maxX: chip.center.x + chip.width / 2,
494
+ minY: chip.center.y - chip.height / 2,
495
+ maxY: chip.center.y + chip.height / 2,
496
+ }),
495
497
  ) ||
496
498
  (inputProblem.textBoxes ?? []).some((textBox) =>
497
499
  pathIntersectsBounds(connector.tracePath, getTextBoxBounds(textBox)),
@@ -0,0 +1,295 @@
1
+ import type { Bounds, Point } from "@tscircuit/math-utils"
2
+ import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
3
+ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
4
+ import type { InputProblem } from "lib/types/InputProblem"
5
+ import { boundsOverlap, getTextBoxBounds } from "lib/utils/textBoxBounds"
6
+ import type { InlineNetLabelPlacement } from "./InlineNetLabelSolver"
7
+
8
+ type StubDirection = "x+" | "x-" | "y+" | "y-"
9
+
10
+ const LABEL_CLEARANCE = 0.05
11
+ const MAX_OUTWARD_DISTANCE = 5
12
+
13
+ const getStubDirection = (path: [Point, Point]): StubDirection => {
14
+ const [start, end] = path
15
+ if (Math.abs(end.x - start.x) >= Math.abs(end.y - start.y)) {
16
+ return end.x >= start.x ? "x+" : "x-"
17
+ }
18
+ return end.y >= start.y ? "y+" : "y-"
19
+ }
20
+
21
+ const getLabelBounds = (placement: {
22
+ center: Point
23
+ width: number
24
+ height: number
25
+ axis?: "x" | "y"
26
+ }): Bounds => {
27
+ const renderedWidth =
28
+ placement.axis === "y" ? placement.height : placement.width
29
+ const renderedHeight =
30
+ placement.axis === "y" ? placement.width : placement.height
31
+ return {
32
+ minX: placement.center.x - renderedWidth / 2,
33
+ maxX: placement.center.x + renderedWidth / 2,
34
+ minY: placement.center.y - renderedHeight / 2,
35
+ maxY: placement.center.y + renderedHeight / 2,
36
+ }
37
+ }
38
+
39
+ const getPathBounds = (path: Point[]): Bounds => ({
40
+ minX: Math.min(...path.map((point) => point.x)),
41
+ maxX: Math.max(...path.map((point) => point.x)),
42
+ minY: Math.min(...path.map((point) => point.y)),
43
+ maxY: Math.max(...path.map((point) => point.y)),
44
+ })
45
+
46
+ const doesPathIntersectBounds = (path: Point[], bounds: Bounds) => {
47
+ for (let index = 0; index < path.length - 1; index++) {
48
+ if (
49
+ boundsOverlap(getPathBounds([path[index]!, path[index + 1]!]), bounds)
50
+ ) {
51
+ return true
52
+ }
53
+ }
54
+ return false
55
+ }
56
+
57
+ const shiftPoint = (
58
+ point: Point,
59
+ direction: StubDirection,
60
+ distance: number,
61
+ ): Point => {
62
+ switch (direction) {
63
+ case "x+":
64
+ return { x: point.x + distance, y: point.y }
65
+ case "x-":
66
+ return { x: point.x - distance, y: point.y }
67
+ case "y+":
68
+ return { x: point.x, y: point.y + distance }
69
+ case "y-":
70
+ return { x: point.x, y: point.y - distance }
71
+ }
72
+ }
73
+
74
+ const shiftTerminalPlacement = (
75
+ placement: InlineNetLabelPlacement,
76
+ direction: StubDirection,
77
+ distance: number,
78
+ ): InlineNetLabelPlacement => {
79
+ const [start, end] = placement.stubTracePath!
80
+ const shiftedEnd = shiftPoint(end, direction, distance)
81
+ return {
82
+ ...placement,
83
+ stubTracePath: [start, shiftedEnd],
84
+ anchorPoint: shiftPoint(placement.anchorPoint, direction, distance),
85
+ center: shiftPoint(placement.center, direction, distance),
86
+ }
87
+ }
88
+
89
+ const getRequiredOutwardDistance = (
90
+ movingBounds: Bounds,
91
+ obstacleBounds: Bounds,
92
+ direction: StubDirection,
93
+ ) => {
94
+ switch (direction) {
95
+ case "x+":
96
+ return obstacleBounds.maxX - movingBounds.minX + LABEL_CLEARANCE
97
+ case "x-":
98
+ return movingBounds.maxX - obstacleBounds.minX + LABEL_CLEARANCE
99
+ case "y+":
100
+ return obstacleBounds.maxY - movingBounds.minY + LABEL_CLEARANCE
101
+ case "y-":
102
+ return movingBounds.maxY - obstacleBounds.minY + LABEL_CLEARANCE
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Moves a row of terminal inline labels farther outward when a retained
108
+ * anchored label occupies their near-pin column. Every terminal on the same
109
+ * component side moves together, preserving the row's alignment. Only the
110
+ * newly added stub segment and shifted text are collision-checked; the
111
+ * original pin-to-label segment was already validated when it was created.
112
+ * All points and bounds are in schematic-world millimetres, with +x right and
113
+ * +y up.
114
+ */
115
+ export const pushInlineTerminalLabelsAwayFromAnchoredLabels = ({
116
+ inputProblem,
117
+ traces,
118
+ anchoredNetLabelPlacements,
119
+ inlineNetLabelPlacements,
120
+ }: {
121
+ inputProblem: InputProblem
122
+ traces: SolvedTracePath[]
123
+ anchoredNetLabelPlacements: NetLabelPlacement[]
124
+ inlineNetLabelPlacements: InlineNetLabelPlacement[]
125
+ }): {
126
+ inlineNetLabelPlacements: InlineNetLabelPlacement[]
127
+ movedGroupCount: number
128
+ } => {
129
+ const chipIdByPinId = new Map<string, string>()
130
+ for (const chip of inputProblem.chips) {
131
+ for (const pin of chip.pins) chipIdByPinId.set(pin.pinId, chip.chipId)
132
+ }
133
+
134
+ const groups = new Map<
135
+ string,
136
+ Array<{
137
+ placementIndex: number
138
+ placement: InlineNetLabelPlacement
139
+ direction: StubDirection
140
+ ownerChipId?: string
141
+ }>
142
+ >()
143
+ for (const [
144
+ placementIndex,
145
+ placement,
146
+ ] of inlineNetLabelPlacements.entries()) {
147
+ if (!placement.stubTracePath || placement.pinIds.length !== 1) continue
148
+ const direction = getStubDirection(placement.stubTracePath)
149
+ const ownerChipId = chipIdByPinId.get(placement.pinIds[0]!)
150
+ const groupKey = `${ownerChipId ?? placement.pinIds[0]}::${direction}`
151
+ const group = groups.get(groupKey) ?? []
152
+ group.push({ placementIndex, placement, direction, ownerChipId })
153
+ groups.set(groupKey, group)
154
+ }
155
+
156
+ const outputPlacements = [...inlineNetLabelPlacements]
157
+ let movedGroupCount = 0
158
+
159
+ for (const group of groups.values()) {
160
+ const groupPlacementIndices = new Set(
161
+ group.map(({ placementIndex }) => placementIndex),
162
+ )
163
+ const fixedInlinePlacements = outputPlacements.filter(
164
+ (_, placementIndex) => !groupPlacementIndices.has(placementIndex),
165
+ )
166
+ const direction = group[0]!.direction
167
+ let distance = 0
168
+
169
+ for (
170
+ let iteration = 0;
171
+ iteration <= anchoredNetLabelPlacements.length;
172
+ iteration++
173
+ ) {
174
+ let requiredAdditionalDistance = 0
175
+ for (const { placement } of group) {
176
+ const shiftedPlacement = shiftTerminalPlacement(
177
+ placement,
178
+ direction,
179
+ distance,
180
+ )
181
+ const shiftedBounds = getLabelBounds(shiftedPlacement)
182
+ for (const anchoredPlacement of anchoredNetLabelPlacements) {
183
+ if (anchoredPlacement.globalConnNetId === placement.globalConnNetId) {
184
+ continue
185
+ }
186
+ const anchoredBounds = getLabelBounds(anchoredPlacement)
187
+ if (!boundsOverlap(shiftedBounds, anchoredBounds)) continue
188
+ requiredAdditionalDistance = Math.max(
189
+ requiredAdditionalDistance,
190
+ getRequiredOutwardDistance(
191
+ shiftedBounds,
192
+ anchoredBounds,
193
+ direction,
194
+ ),
195
+ )
196
+ }
197
+ }
198
+ if (requiredAdditionalDistance <= 0) break
199
+ distance += requiredAdditionalDistance
200
+ }
201
+
202
+ if (distance <= 0 || distance > MAX_OUTWARD_DISTANCE) continue
203
+
204
+ const proposals = group.map(({ placement }) =>
205
+ shiftTerminalPlacement(placement, direction, distance),
206
+ )
207
+ const hasConflict = proposals.some((proposal, proposalIndex) => {
208
+ const originalPlacement = group[proposalIndex]!.placement
209
+ const originalEnd = originalPlacement.stubTracePath![1]
210
+ const shiftedEnd = proposal.stubTracePath![1]
211
+ const addedStubSegment: [Point, Point] = [originalEnd, shiftedEnd]
212
+ const labelBounds = getLabelBounds(proposal)
213
+ const ownerChipId = group[proposalIndex]!.ownerChipId
214
+
215
+ if (
216
+ anchoredNetLabelPlacements.some((anchoredPlacement) => {
217
+ if (anchoredPlacement.globalConnNetId === proposal.globalConnNetId) {
218
+ return false
219
+ }
220
+ const anchoredBounds = getLabelBounds(anchoredPlacement)
221
+ return (
222
+ boundsOverlap(labelBounds, anchoredBounds) ||
223
+ doesPathIntersectBounds(addedStubSegment, anchoredBounds)
224
+ )
225
+ })
226
+ ) {
227
+ return true
228
+ }
229
+
230
+ for (const chip of inputProblem.chips) {
231
+ if (chip.chipId === ownerChipId) continue
232
+ const chipBounds: Bounds = {
233
+ minX: chip.center.x - chip.width / 2,
234
+ maxX: chip.center.x + chip.width / 2,
235
+ minY: chip.center.y - chip.height / 2,
236
+ maxY: chip.center.y + chip.height / 2,
237
+ }
238
+ if (
239
+ boundsOverlap(labelBounds, chipBounds) ||
240
+ doesPathIntersectBounds(addedStubSegment, chipBounds)
241
+ ) {
242
+ return true
243
+ }
244
+ }
245
+
246
+ for (const textBox of inputProblem.textBoxes ?? []) {
247
+ const textBounds = getTextBoxBounds(textBox)
248
+ if (
249
+ boundsOverlap(labelBounds, textBounds) ||
250
+ doesPathIntersectBounds(addedStubSegment, textBounds)
251
+ ) {
252
+ return true
253
+ }
254
+ }
255
+
256
+ for (const trace of traces) {
257
+ if (trace.globalConnNetId === proposal.globalConnNetId) continue
258
+ if (
259
+ doesPathIntersectBounds(trace.tracePath, labelBounds) ||
260
+ doesPathIntersectBounds(
261
+ trace.tracePath,
262
+ getPathBounds(addedStubSegment),
263
+ )
264
+ ) {
265
+ return true
266
+ }
267
+ }
268
+
269
+ for (const fixedPlacement of fixedInlinePlacements) {
270
+ const fixedBounds = getLabelBounds(fixedPlacement)
271
+ if (
272
+ boundsOverlap(labelBounds, fixedBounds) ||
273
+ doesPathIntersectBounds(addedStubSegment, fixedBounds) ||
274
+ (fixedPlacement.stubTracePath &&
275
+ doesPathIntersectBounds(fixedPlacement.stubTracePath, labelBounds))
276
+ ) {
277
+ return true
278
+ }
279
+ }
280
+
281
+ return false
282
+ })
283
+
284
+ if (hasConflict) continue
285
+ for (const [groupIndex, { placementIndex }] of group.entries()) {
286
+ outputPlacements[placementIndex] = proposals[groupIndex]!
287
+ }
288
+ movedGroupCount++
289
+ }
290
+
291
+ return {
292
+ inlineNetLabelPlacements: outputPlacements,
293
+ movedGroupCount,
294
+ }
295
+ }
@@ -10,6 +10,7 @@ import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualize
10
10
  import { getColorFromString } from "lib/utils/getColorFromString"
11
11
  import { getConnectivityMapsFromInputProblem } from "../MspConnectionPairSolver/getConnectivityMapFromInputProblem"
12
12
  import { getNetLabelWidthForConnection } from "lib/utils/getNetLabelWidthForConnection"
13
+ import { getTraceConnectedPinComponents } from "lib/solvers/SchematicTraceLinesSolver/getTraceConnectedPinComponents"
13
14
 
14
15
  /**
15
16
  * A group of traces that have at least one overlapping segment and
@@ -164,41 +165,12 @@ export class NetLabelPlacementSolver extends BaseSolver {
164
165
  ) as string[]
165
166
  const pinsInNet = allIdsInNet.filter((id) => pinIdToPinMap.has(id))
166
167
 
167
- // Build adjacency from solved traces (edges)
168
- const adj: Record<string, Set<string>> = {}
169
- for (const pid of pinsInNet) adj[pid] = new Set()
170
- for (const t of byGlobal[globalConnNetId] ?? []) {
171
- const a = t.pins[0].pinId
172
- const b = t.pins[1].pinId
173
- if (adj[a] && adj[b]) {
174
- adj[a].add(b)
175
- adj[b].add(a)
176
- }
177
- }
178
-
179
- // Find connected components based on trace edges
180
- const visited = new Set<string>()
181
- for (const pid of pinsInNet) {
182
- if (visited.has(pid)) continue
183
- const stack = [pid]
184
- const component = new Set<string>()
185
- visited.add(pid)
186
- while (stack.length > 0) {
187
- const u = stack.pop()!
188
- component.add(u)
189
- for (const v of adj[u] ?? []) {
190
- if (!visited.has(v)) {
191
- visited.add(v)
192
- stack.push(v)
193
- }
194
- }
195
- }
196
-
197
- // Collect traces fully inside this component
198
- const compTraces = (byGlobal[globalConnNetId] ?? []).filter(
199
- (t) =>
200
- component.has(t.pins[0].pinId) && component.has(t.pins[1].pinId),
201
- )
168
+ for (const traceConnectedComponent of getTraceConnectedPinComponents({
169
+ pinIds: pinsInNet,
170
+ traces: byGlobal[globalConnNetId] ?? [],
171
+ })) {
172
+ const component = new Set(traceConnectedComponent.pinIds)
173
+ const compTraces = traceConnectedComponent.traces
202
174
 
203
175
  if (compTraces.length > 0) {
204
176
  // This routed trace exists specifically because two endpoint labels
@@ -0,0 +1,60 @@
1
+ import type { SolvedTracePath } from "./SchematicTraceLinesSolver"
2
+ import type { PinId } from "lib/types/InputProblem"
3
+
4
+ export interface TraceConnectedPinComponent {
5
+ pinIds: PinId[]
6
+ traces: SolvedTracePath[]
7
+ }
8
+
9
+ /**
10
+ * Partitions pins into connected components using solved traces as graph edges.
11
+ * Pins without a solved trace are returned as singleton components.
12
+ */
13
+ export const getTraceConnectedPinComponents = ({
14
+ pinIds,
15
+ traces,
16
+ }: {
17
+ pinIds: PinId[]
18
+ traces: SolvedTracePath[]
19
+ }): TraceConnectedPinComponent[] => {
20
+ const adjacency = new Map<PinId, Set<PinId>>()
21
+ for (const pinId of pinIds) adjacency.set(pinId, new Set())
22
+
23
+ const relevantTraces = traces.filter((trace) => {
24
+ if (trace.pins.length !== 2) return false
25
+ const [firstPin, secondPin] = trace.pins
26
+ return adjacency.has(firstPin.pinId) && adjacency.has(secondPin.pinId)
27
+ })
28
+ for (const trace of relevantTraces) {
29
+ const [firstPin, secondPin] = trace.pins
30
+ adjacency.get(firstPin.pinId)!.add(secondPin.pinId)
31
+ adjacency.get(secondPin.pinId)!.add(firstPin.pinId)
32
+ }
33
+
34
+ const components: TraceConnectedPinComponent[] = []
35
+ const visited = new Set<PinId>()
36
+ for (const pinId of pinIds) {
37
+ if (visited.has(pinId)) continue
38
+ const componentPinIds: PinId[] = []
39
+ const pending = [pinId]
40
+ visited.add(pinId)
41
+ while (pending.length > 0) {
42
+ const currentPinId = pending.pop()!
43
+ componentPinIds.push(currentPinId)
44
+ for (const adjacentPinId of adjacency.get(currentPinId) ?? []) {
45
+ if (visited.has(adjacentPinId)) continue
46
+ visited.add(adjacentPinId)
47
+ pending.push(adjacentPinId)
48
+ }
49
+ }
50
+
51
+ const componentPinIdSet = new Set(componentPinIds)
52
+ components.push({
53
+ pinIds: componentPinIds,
54
+ traces: relevantTraces.filter((trace) =>
55
+ trace.pins.every((pin) => componentPinIdSet.has(pin.pinId)),
56
+ ),
57
+ })
58
+ }
59
+ return components
60
+ }
@@ -53,6 +53,12 @@ export const getFixedLabelCoordinate = (
53
53
  ) => {
54
54
  const traceMap = new Map(traces.map((trace) => [trace.mspPairId, trace]))
55
55
  const groupTraceIds = new Set(group.map((segment) => segment.traceId))
56
+ const groupIsWithinSingleComponent = [...groupTraceIds].every(
57
+ (traceId) =>
58
+ traceMap
59
+ .get(traceId)
60
+ ?.pins.every((pin) => pin.chipId === group[0]!.componentId) === true,
61
+ )
56
62
  const groupHasOnlySingleRailTraces = [...groupTraceIds].every(
57
63
  (traceId) => traceMap.get(traceId)?.tracePath.length === 4,
58
64
  )
@@ -66,16 +72,27 @@ export const getFixedLabelCoordinate = (
66
72
  netLabelPlacements,
67
73
  traces,
68
74
  )
69
- const anchoringLabels = linkedLabels.filter(
70
- (label) =>
71
- label.orientation === group[0]!.componentFacingDirection &&
72
- label.mspConnectionPairIds.some((traceId) => {
73
- const trace = traceMap.get(traceId)
74
- return (
75
- trace && tracePathContainsPoint(trace.tracePath, label.anchorPoint)
76
- )
77
- }),
75
+ const labelsOrientedAlongRail = new Set(
76
+ linkedLabels.filter(
77
+ (label) =>
78
+ (orientation === "vertical" &&
79
+ (label.orientation === "y+" || label.orientation === "y-")) ||
80
+ (orientation === "horizontal" &&
81
+ (label.orientation === "x+" || label.orientation === "x-")),
82
+ ),
78
83
  )
84
+ const anchoringLabels = linkedLabels.filter((label) => {
85
+ if (
86
+ label.orientation !== group[0]!.componentFacingDirection &&
87
+ (!groupIsWithinSingleComponent || !labelsOrientedAlongRail.has(label))
88
+ ) {
89
+ return false
90
+ }
91
+ return label.mspConnectionPairIds.some((traceId) => {
92
+ const trace = traceMap.get(traceId)
93
+ return trace && tracePathContainsPoint(trace.tracePath, label.anchorPoint)
94
+ })
95
+ })
79
96
  const coordinates = getDistinctCoordinates(
80
97
  anchoringLabels.map((label) =>
81
98
  orientation === "vertical" ? label.anchorPoint.x : label.anchorPoint.y,
@@ -95,11 +112,24 @@ export const getFixedLabelCoordinate = (
95
112
  (traceId) => (traceMap.get(traceId)?.tracePath.length ?? 0) > 4,
96
113
  ),
97
114
  )
98
- // A multi-turn label trace is an intentional routed backbone, so its rail
99
- // coordinate may propagate through the local component chain. Labels riding
100
- // a single movable rail remain subject to the per-connection locality gate.
115
+ // A multi-turn label trace is an intentional routed backbone, so its
116
+ // coordinate may propagate through the connected rail group.
101
117
  if (labelIsAnchoredToRoutedBackbone) return coordinate
102
118
 
119
+ // A label at a same-component rail end may set the shared bus coordinate.
120
+ if (anchoringLabels.some((label) => labelsOrientedAlongRail.has(label))) {
121
+ const railGroupSpan =
122
+ Math.max(...group.map((segment) => segment.maxAlong)) -
123
+ Math.min(...group.map((segment) => segment.minAlong))
124
+ const coordinateIsLocalToRailGroup = group.every(
125
+ (segment) =>
126
+ Math.abs(segment.coordinate - coordinate) <=
127
+ railGroupSpan + RAIL_ALIGNMENT_EPSILON,
128
+ )
129
+ if (coordinateIsLocalToRailGroup) return coordinate
130
+ return null
131
+ }
132
+
103
133
  // Rail cleanup is local: a label cannot pull a connection farther than the
104
134
  // span of the pins that connection already joins.
105
135
  const coordinateIsLocalToEveryTrace = group.every((segment) => {
@@ -110,5 +140,6 @@ export const getFixedLabelCoordinate = (
110
140
  distance(trace.pins[0]!, trace.pins[1]!) + RAIL_ALIGNMENT_EPSILON
111
141
  )
112
142
  })
113
- return coordinateIsLocalToEveryTrace ? coordinate : null
143
+ if (coordinateIsLocalToEveryTrace) return coordinate
144
+ return null
114
145
  }
@@ -110,10 +110,11 @@ export interface InputNetConnection {
110
110
  netLabelHeight?: number
111
111
 
112
112
  /**
113
- * When true, a named one- or two-pin net may use inline labels. A single-pin
114
- * net gets an outward stub. A routed two-pin net gets one label along its
115
- * trace; when that route is intentionally skipped, both endpoints get
116
- * outward stubs. Nets with more than two pins retain anchored labels.
113
+ * When true, a named net may use inline labels. Each routed connected
114
+ * component gets a label along its representative trace, while each
115
+ * disconnected endpoint gets an outward inline-label stub. Conversion is
116
+ * atomic for the whole net so inline and anchored representations are not
117
+ * mixed when any label cannot be placed.
117
118
  */
118
119
  allowInlineNetLabel?: boolean
119
120
 
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "https://github.com/tscircuit/schematic-trace-solver.git"
6
6
  },
7
7
  "main": "dist/index.js",
8
- "version": "0.0.159",
8
+ "version": "0.0.161",
9
9
  "type": "module",
10
10
  "scripts": {
11
11
  "start": "cosmos",