@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.
- package/dist/index.d.ts +43 -9
- package/dist/index.js +807 -115
- package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +210 -6
- 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/NetLabelPlacementSolver/NetLabelPlacementSolver.ts +7 -35
- package/lib/solvers/SchematicTraceLinesSolver/getTraceConnectedPinComponents.ts +60 -0
- package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getFixedLabelCoordinate.ts +44 -13
- package/lib/types/InputProblem.ts +5 -4
- package/package.json +1 -1
- package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +1 -1
- package/tests/bug-reports/bug-report-20260728T144234Z/__snapshots__/bug-report-20260728T144234Z.snap.svg +1 -1
- package/tests/bug-reports/bug-report-20260728T225606Z/__snapshots__/bug-report-20260728T225606Z.snap.svg +79 -79
- package/tests/bug-reports/bug-report-20260730T061837Z/__snapshots__/bug-report-20260730T061837Z.snap.svg +85 -85
- package/tests/bug-reports/bug-report-20260731T052229Z/__snapshots__/bug-report-20260731T052229Z.snap.svg +1 -1
- package/tests/bug-reports/bug-report-20260804T095800Z/__snapshots__/bug-report-20260804T095800Z.snap.svg +163 -163
- package/tests/bug-reports/bug-report-20260804T171919Z/__snapshots__/bug-report-20260804T171919Z.snap.svg +547 -547
- package/tests/bug-reports/bug-report-20260804T225912Z/__snapshots__/bug-report-20260804T225912Z.snap.svg +13 -13
- package/tests/bug-reports/bug-report-20260805T061316Z/__snapshots__/bug-report-20260805T061316Z.snap.svg +267 -267
- package/tests/bug-reports/bug-report-20260806T061548Z/__snapshots__/bug-report-20260806T061548Z.snap.svg +267 -267
- package/tests/bug-reports/bug-report-20260806T093501Z/__snapshots__/bug-report-20260806T093501Z.snap.svg +109 -109
- package/tests/bug-reports/bug-report-20260811T075142Z/__snapshots__/bug-report-20260811T075142Z.snap.svg +1 -1
- package/tests/bug-reports/bug-report-20260815T073240Z/__snapshots__/bug-report-20260815T073240Z.snap.svg +49 -49
- package/tests/bug-reports/bug-report-20260819T091818Z/__snapshots__/bug-report-20260819T091818Z.snap.svg +1 -1
- package/tests/bug-reports/bug-report-20260825T045913Z/__snapshots__/bug-report-20260825T045913Z.snap.svg +103 -103
- package/tests/bug-reports/bug-report-20260825T103621Z/__snapshots__/bug-report-20260825T103621Z.snap.svg +103 -103
- package/tests/bug-reports/bug-report-20260826T072956Z/__snapshots__/bug-report-20260826T072956Z.snap.svg +600 -792
- package/tests/bug-reports/bug-report-20260826T072956Z/bug-report-20260826T072956Z.test.ts +66 -1
- package/tests/examples/__snapshots__/example51.snap.svg +568 -568
- package/tests/fixtures/convertSolverOutputToCircuitJson.test.ts +16 -1
- package/tests/fixtures/convertSolverOutputToCircuitJson.ts +23 -12
- package/tests/repros/__snapshots__/board-1273-trace-overlap-cycle.snap.svg +736 -736
- package/tests/repros/__snapshots__/board-15984-same-net-junction.snap.svg +79 -79
- package/tests/repros/__snapshots__/board1096-usb-label-overlap-iteration.snap.svg +127 -127
- package/tests/repros/__snapshots__/repro-bluetooth-controller-ground-decoupling-groups.snap.svg +106 -0
- package/tests/repros/__snapshots__/repro-board-589-regulator-section.snap.svg +31 -31
- package/tests/repros/__snapshots__/repro-board-648-esp12f-section.snap.svg +121 -121
- package/tests/repros/__snapshots__/repro-bq25895-cross-net-trace-overlap.snap.svg +43 -43
- package/tests/repros/__snapshots__/repro-core-555-power-cross-net-overlap.snap.svg +19 -19
- package/tests/repros/__snapshots__/repro-core-ground-inline-label-fallback.snap.svg +277 -0
- package/tests/repros/__snapshots__/repro-core-subcircuit-missing-ground.snap.svg +19 -19
- package/tests/repros/__snapshots__/repro-core-tb67s579ftg-inline-label-routing.snap.svg +7 -7
- package/tests/repros/__snapshots__/repro-dc-power-isolated-routing.snap.svg +37 -37
- package/tests/repros/__snapshots__/repro-hdc2080-ground-label-alignment.snap.svg +137 -0
- package/tests/repros/__snapshots__/repro-nrf52810-clock-routing.snap.svg +61 -61
- package/tests/repros/__snapshots__/repro-pga300-redundant-ground-traces.snap.svg +115 -115
- package/tests/repros/__snapshots__/repro-pmp11282-isolated-dcdc.snap.svg +743 -743
- package/tests/repros/__snapshots__/repro-rp2040-usb-cc2-ground-overlap.snap.svg +43 -43
- package/tests/repros/__snapshots__/repro-ti-power-output-section.snap.svg +1 -1
- package/tests/repros/__snapshots__/repro-tida010076-j4-ground-bus.snap.svg +17 -17
- package/tests/repros/__snapshots__/repro-tida010076-page02.snap.svg +1131 -1131
- package/tests/repros/__snapshots__/repro-type-c-16pin-self-connection.snap.svg +7 -7
- package/tests/repros/__snapshots__/repro-type-c-16pin-to-resistor.snap.svg +7 -7
- package/tests/repros/__snapshots__/repro-usb-power-vbus-label-detour.snap.svg +666 -666
- package/tests/repros/__snapshots__/repro161-power-section-autolayout.snap.svg +73 -73
- package/tests/repros/__snapshots__/repro48-555-timer-vcc-rail-label.snap.svg +73 -73
- package/tests/repros/assets/repro-bluetooth-controller-ground-decoupling-groups.input.json +214 -0
- package/tests/repros/assets/repro-core-ground-inline-label-fallback.input.json +863 -0
- package/tests/repros/repro-bluetooth-controller-ground-decoupling-groups.test.ts +18 -0
- package/tests/repros/repro-core-ground-inline-label-fallback.test.ts +34 -0
- package/tests/repros/repro-hdc2080-ground-label-alignment.test.ts +239 -0
- package/tests/repros/repro-tida010076-j4-ground-bus.test.ts +14 -10
- package/tests/repros/repro-tida010076-page02.test.ts +2 -2
- package/tests/repros/repro-usb-power-vbus-label-detour.test.ts +14 -0
- package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label02.snap.svg +1 -1
- 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
|
@@ -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
|
+
}
|
|
@@ -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
|
|
@@ -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
|
|
70
|
-
(
|
|
71
|
-
label
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
99
|
-
// coordinate may propagate through the
|
|
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
|
-
|
|
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
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
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
|
|