@tscircuit/schematic-trace-solver 0.0.158 → 0.0.160
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.
- package/dist/index.d.ts +41 -9
- package/dist/index.js +762 -110
- package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +536 -94
- package/lib/solvers/InlineNetLabelSolver/pushAnchoredNetLabelsAwayFromInlineLabels.ts +9 -7
- package/lib/solvers/InlineNetLabelSolver/pushInlineTerminalLabelsAwayFromAnchoredLabels.ts +295 -0
- package/lib/solvers/LongDistancePairSolver/LongDistancePairSolver.ts +15 -8
- package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +5 -0
- package/lib/solvers/MspConnectionPairSolver/getGroundConnectionPolicy.ts +83 -0
- package/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts +7 -35
- package/lib/solvers/SameNetJunctionAlignmentSolver/SameNetJunctionAlignmentSolver.ts +6 -1
- package/lib/solvers/SameNetJunctionAlignmentSolver/placeGroundRailLabelsAtOuterEnd.ts +122 -0
- package/lib/solvers/SchematicTraceLinesSolver/getTraceConnectedPinComponents.ts +60 -0
- package/lib/types/InputProblem.ts +5 -4
- package/package.json +1 -1
- package/tests/bug-reports/bug-report-20260826T072956Z/__snapshots__/bug-report-20260826T072956Z.snap.svg +600 -783
- package/tests/bug-reports/bug-report-20260826T072956Z/bug-report-20260826T072956Z.test.ts +66 -1
- package/tests/fixtures/parallel-ground-rail.ts +53 -0
- package/tests/repros/__snapshots__/repro-mspm0l1306-capacitor-symmetry.snap.svg +34 -16
- package/tests/repros/repro-mspm0l1306-capacitor-symmetry.test.ts +40 -7
- package/tests/repros/repro-usb-power-vbus-label-detour.test.ts +14 -0
- package/tests/solvers/InlineNetLabelSolver/multi-pin-connected-components.test.ts +252 -0
- package/tests/solvers/InlineNetLabelSolver/opposite-side-placement.test.ts +71 -0
- package/tests/solvers/InlineNetLabelSolver/push-anchored-net-labels-away.test.ts +8 -2
- package/tests/solvers/InlineNetLabelSolver/push-inline-terminal-labels-away.test.ts +91 -0
- package/tests/solvers/InlineNetLabelSolver/two-pin-terminal-stubs-atomic.test.ts +7 -1
- package/tests/solvers/MspConnectionPairSolver/local-ground-branches.test.ts +105 -0
- package/tests/solvers/SameNetJunctionAlignmentSolver/ground-rail-label.test.ts +188 -0
|
@@ -485,13 +485,15 @@ export const pushAnchoredNetLabelsAwayFromInlineLabels = ({
|
|
|
485
485
|
inlineBounds.some((bounds) =>
|
|
486
486
|
pathIntersectsBounds(connector.tracePath, bounds),
|
|
487
487
|
) ||
|
|
488
|
-
inputProblem.chips.some(
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
+
}
|
|
@@ -1,23 +1,24 @@
|
|
|
1
|
-
import { getConnectivityMapsFromInputProblem } from "lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem"
|
|
2
1
|
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import type { ConnectivityMap } from "connectivity-map"
|
|
3
|
+
import { getConnectivityMapsFromInputProblem } from "lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem"
|
|
3
4
|
import {
|
|
4
5
|
DEFAULT_MAX_MSP_PAIR_DISTANCE,
|
|
5
6
|
type MspConnectionPair,
|
|
6
7
|
} from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
|
|
7
8
|
import type {
|
|
8
|
-
|
|
9
|
+
InputChip,
|
|
9
10
|
InputPin,
|
|
11
|
+
InputProblem,
|
|
10
12
|
PinId,
|
|
11
|
-
InputChip,
|
|
12
13
|
} from "lib/types/InputProblem"
|
|
13
|
-
import { BaseSolver } from "../BaseSolver/BaseSolver"
|
|
14
|
-
import { SchematicTraceSingleLineSolver2 } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2"
|
|
15
|
-
import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
|
|
16
|
-
import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
17
|
-
import type { ConnectivityMap } from "connectivity-map"
|
|
18
14
|
import { doesTraceOverlapWithExistingTraces } from "lib/utils/does-trace-overlap-with-existing-traces"
|
|
19
15
|
import { arePinsInDifferentSchematicSections } from "../../utils/arePinsInDifferentSchematicSections"
|
|
16
|
+
import { BaseSolver } from "../BaseSolver/BaseSolver"
|
|
17
|
+
import { getGroundConnectionPolicy } from "../MspConnectionPairSolver/getGroundConnectionPolicy"
|
|
20
18
|
import { isLabeledPeripheralConnection } from "../MspConnectionPairSolver/isLabeledPeripheralConnection"
|
|
19
|
+
import type { SolvedTracePath } from "../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
20
|
+
import { SchematicTraceSingleLineSolver2 } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2"
|
|
21
|
+
import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
|
|
21
22
|
|
|
22
23
|
const NEAREST_NEIGHBOR_COUNT = 3
|
|
23
24
|
|
|
@@ -163,6 +164,7 @@ export class LongDistancePairSolver extends BaseSolver {
|
|
|
163
164
|
|
|
164
165
|
const { inputProblem, primaryMspConnectionPairs, alreadySolvedTraces } =
|
|
165
166
|
this.params
|
|
167
|
+
const canRouteGroundPair = getGroundConnectionPolicy(inputProblem)
|
|
166
168
|
|
|
167
169
|
this.inputProblem = inputProblem
|
|
168
170
|
this.allSolvedTraces = [...alreadySolvedTraces]
|
|
@@ -189,6 +191,10 @@ export class LongDistancePairSolver extends BaseSolver {
|
|
|
189
191
|
// new nearest-neighbor candidates.
|
|
190
192
|
this.queuedFailedConnectionPairs = this.params.failedConnectionPairs.filter(
|
|
191
193
|
(connectionPair) =>
|
|
194
|
+
canRouteGroundPair(
|
|
195
|
+
connectionPair.pins[0].pinId,
|
|
196
|
+
connectionPair.pins[1].pinId,
|
|
197
|
+
) &&
|
|
192
198
|
isLabeledPeripheralConnection({
|
|
193
199
|
inputProblem: this.inputProblem,
|
|
194
200
|
chipMap: this.chipMap,
|
|
@@ -219,6 +225,7 @@ export class LongDistancePairSolver extends BaseSolver {
|
|
|
219
225
|
.flatMap((otherPinId) => {
|
|
220
226
|
const targetPin = pinMap.get(otherPinId)
|
|
221
227
|
if (!targetPin) return [] // Gracefully handle missing pins
|
|
228
|
+
if (!canRouteGroundPair(sourcePin.pinId, targetPin.pinId)) return []
|
|
222
229
|
const isNamedTwoPinConnection = inputProblem.netConnections.some(
|
|
223
230
|
(connection) =>
|
|
224
231
|
connection.pinIds.length === 2 &&
|
|
@@ -13,6 +13,7 @@ import { arePinsInDifferentSchematicSections } from "../../utils/arePinsInDiffer
|
|
|
13
13
|
import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
|
|
14
14
|
import { doesPairCrossRestrictedCenterLines } from "./doesPairCrossRestrictedCenterLines"
|
|
15
15
|
import { getConnectivityMapsFromInputProblem } from "./getConnectivityMapFromInputProblem"
|
|
16
|
+
import { getGroundConnectionPolicy } from "./getGroundConnectionPolicy"
|
|
16
17
|
import { getOrthogonalMinimumSpanningTree } from "./getMspConnectionPairsFromPins"
|
|
17
18
|
import { getLabeledConnectionRouteReason } from "./isLabeledPeripheralConnection"
|
|
18
19
|
|
|
@@ -45,11 +46,13 @@ export class MspConnectionPairSolver extends BaseSolver {
|
|
|
45
46
|
pinMap: Record<string, InputPin & { chipId: string }>
|
|
46
47
|
userNetIdByPinId: Record<string, string | undefined>
|
|
47
48
|
directConnectionPinPairKeys: Set<string>
|
|
49
|
+
private canRouteGroundPair: (firstPinId: PinId, secondPinId: PinId) => boolean
|
|
48
50
|
|
|
49
51
|
constructor({ inputProblem }: { inputProblem: InputProblem }) {
|
|
50
52
|
super()
|
|
51
53
|
|
|
52
54
|
this.inputProblem = inputProblem
|
|
55
|
+
this.canRouteGroundPair = getGroundConnectionPolicy(inputProblem)
|
|
53
56
|
this.maxMspPairDistance =
|
|
54
57
|
inputProblem.maxMspPairDistance ?? DEFAULT_MAX_MSP_PAIR_DISTANCE
|
|
55
58
|
|
|
@@ -124,6 +127,7 @@ export class MspConnectionPairSolver extends BaseSolver {
|
|
|
124
127
|
const [pin1, pin2] = directlyConnectedPins
|
|
125
128
|
const p1 = this.pinMap[pin1!]!
|
|
126
129
|
const p2 = this.pinMap[pin2!]!
|
|
130
|
+
if (!this.canRouteGroundPair(pin1!, pin2!)) return
|
|
127
131
|
const pinPairKey = getPinPairKey([pin1!, pin2!])
|
|
128
132
|
// Explicit source traces are classified by straight-line distance when
|
|
129
133
|
// their input is created; named nets retain the orthogonal route metric.
|
|
@@ -195,6 +199,7 @@ export class MspConnectionPairSolver extends BaseSolver {
|
|
|
195
199
|
{
|
|
196
200
|
maxDistance: this.maxMspPairDistance,
|
|
197
201
|
forbidEdge: (a, b) =>
|
|
202
|
+
!this.canRouteGroundPair(a.pinId, b.pinId) ||
|
|
198
203
|
arePinsInDifferentSchematicSections(
|
|
199
204
|
this.inputProblem,
|
|
200
205
|
a as InputPin & { chipId: string },
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ConnectivityMap } from "connectivity-map"
|
|
2
|
+
import type { InputProblem, PinId } from "lib/types/InputProblem"
|
|
3
|
+
import { getPinDirection } from "../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver/getPinDirection"
|
|
4
|
+
import { getConnectivityMapsFromInputProblem } from "./getConnectivityMapFromInputProblem"
|
|
5
|
+
|
|
6
|
+
// Keep nearby ground connections, but do not extend the usual 1 mm local
|
|
7
|
+
// routing range vertically just because a sheet allows long signal traces.
|
|
8
|
+
const MAX_LOCAL_GROUND_BRANCH_OFFSET = 1
|
|
9
|
+
|
|
10
|
+
/** Avoid return wires between staggered, net-only ground-facing branches. */
|
|
11
|
+
export const getGroundConnectionPolicy = (inputProblem: InputProblem) => {
|
|
12
|
+
const { netConnMap } = getConnectivityMapsFromInputProblem(inputProblem)
|
|
13
|
+
const groundNetId = netConnMap.getNetConnectedToId("GND")
|
|
14
|
+
if (!groundNetId) return () => true
|
|
15
|
+
// Net identifiers do not constitute physical edges: two separate direct
|
|
16
|
+
// connections may have the same netId without requesting a wire between them.
|
|
17
|
+
const physicalConnMap = new ConnectivityMap({})
|
|
18
|
+
for (const connection of inputProblem.directConnections) {
|
|
19
|
+
physicalConnMap.addConnections([connection.pinIds])
|
|
20
|
+
}
|
|
21
|
+
const pins = new Map(
|
|
22
|
+
inputProblem.chips.flatMap((chip) =>
|
|
23
|
+
chip.pins.map((pin) => [pin.pinId, { pin, chip }] as const),
|
|
24
|
+
),
|
|
25
|
+
)
|
|
26
|
+
const groundFacingPins = [...pins.values()].filter(
|
|
27
|
+
({ pin, chip }) =>
|
|
28
|
+
chip.pins.length === 2 &&
|
|
29
|
+
netConnMap.getNetConnectedToId(pin.pinId) === groundNetId &&
|
|
30
|
+
(pin._facingDirection ?? getPinDirection(pin, chip)) === "y-",
|
|
31
|
+
)
|
|
32
|
+
// A deliberately wired, level bank of two-pin loads already has its own
|
|
33
|
+
// shared return rail. Do not extend that rail to independent lower branches
|
|
34
|
+
// just to reduce the number of GND symbols. Other ground topologies retain
|
|
35
|
+
// their existing routing behavior.
|
|
36
|
+
const hasExplicitParallelGroundRail = groundFacingPins.some(({ pin: a }) => {
|
|
37
|
+
const group = physicalConnMap.getNetConnectedToId(a.pinId)
|
|
38
|
+
return (
|
|
39
|
+
group !== undefined &&
|
|
40
|
+
groundFacingPins.some(
|
|
41
|
+
({ pin: b }) =>
|
|
42
|
+
a.pinId !== b.pinId &&
|
|
43
|
+
Math.abs(a.y - b.y) < 1e-6 &&
|
|
44
|
+
Math.abs(a.x - b.x) > 1e-6 &&
|
|
45
|
+
physicalConnMap.getNetConnectedToId(b.pinId) === group,
|
|
46
|
+
)
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
if (!hasExplicitParallelGroundRail) return () => true
|
|
50
|
+
const isGroundFacingTerminal = (pinId: PinId) => {
|
|
51
|
+
const entry = pins.get(pinId)
|
|
52
|
+
return (
|
|
53
|
+
entry?.chip.pins.length === 2 &&
|
|
54
|
+
!physicalConnMap.getNetConnectedToId(pinId) &&
|
|
55
|
+
(entry.pin._facingDirection ?? getPinDirection(entry.pin, entry.chip)) ===
|
|
56
|
+
"y-"
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (firstPinId: PinId, secondPinId: PinId): boolean => {
|
|
61
|
+
if (
|
|
62
|
+
!groundNetId ||
|
|
63
|
+
netConnMap.getNetConnectedToId(firstPinId) !== groundNetId ||
|
|
64
|
+
netConnMap.getNetConnectedToId(secondPinId) !== groundNetId
|
|
65
|
+
) {
|
|
66
|
+
return true
|
|
67
|
+
}
|
|
68
|
+
if (
|
|
69
|
+
!isGroundFacingTerminal(firstPinId) &&
|
|
70
|
+
!isGroundFacingTerminal(secondPinId)
|
|
71
|
+
)
|
|
72
|
+
return true
|
|
73
|
+
const first = pins.get(firstPinId)?.pin
|
|
74
|
+
const second = pins.get(secondPinId)?.pin
|
|
75
|
+
// Level ground pins can still form a shared decoupling rail. A staggered
|
|
76
|
+
// terminal would need a return detour; use its local GND label instead.
|
|
77
|
+
return (
|
|
78
|
+
!first ||
|
|
79
|
+
!second ||
|
|
80
|
+
Math.abs(first.y - second.y) <= MAX_LOCAL_GROUND_BRANCH_OFFSET + 1e-6
|
|
81
|
+
)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
@@ -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
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const
|
|
172
|
-
const
|
|
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
|
|
@@ -6,6 +6,7 @@ import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/
|
|
|
6
6
|
import type { InputProblem } from "lib/types/InputProblem"
|
|
7
7
|
import { getColorFromString } from "lib/utils/getColorFromString"
|
|
8
8
|
import { alignSameNetJunctions } from "./alignSameNetJunctions"
|
|
9
|
+
import { placeGroundRailLabelsAtOuterEnd } from "./placeGroundRailLabelsAtOuterEnd"
|
|
9
10
|
|
|
10
11
|
interface SameNetJunctionAlignmentSolverInput {
|
|
11
12
|
inputProblem: InputProblem
|
|
@@ -29,7 +30,11 @@ export class SameNetJunctionAlignmentSolver extends BaseSolver {
|
|
|
29
30
|
override _step() {
|
|
30
31
|
const result = alignSameNetJunctions(this.input)
|
|
31
32
|
this.outputTraces = result.traces
|
|
32
|
-
this.outputNetLabelPlacements =
|
|
33
|
+
this.outputNetLabelPlacements = placeGroundRailLabelsAtOuterEnd({
|
|
34
|
+
inputProblem: this.input.inputProblem,
|
|
35
|
+
traces: result.traces,
|
|
36
|
+
netLabelPlacements: result.netLabelPlacements,
|
|
37
|
+
})
|
|
33
38
|
this.stats.alignedJunctionCount = result.alignedJunctionCount
|
|
34
39
|
this.solved = true
|
|
35
40
|
}
|