@tscircuit/schematic-trace-solver 0.0.144 → 0.0.146
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 +4 -0
- package/dist/index.js +448 -61
- package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +30 -8
- package/lib/solvers/InlineNetLabelSolver/pushAnchoredNetLabelsAwayFromInlineLabels.ts +530 -0
- package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +14 -5
- package/lib/solvers/MspConnectionPairSolver/isLabeledPeripheralConnection.ts +46 -10
- package/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts +7 -0
- package/package.json +1 -1
- package/tests/assets/inline-net-label-anchored-label-clearance.json +56 -0
- package/tests/repros/__snapshots__/repro-tida010076-page02.snap.svg +10 -30
- package/tests/repros/assets/repro-tida010076-page02.input.json +2 -0
- package/tests/repros/repro-tida010076-page02.test.ts +31 -0
- package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label-anchored-label-clearance.snap.svg +54 -0
- package/tests/solvers/InlineNetLabelSolver/inline-net-label-anchored-label-clearance.test.ts +28 -0
- package/tests/solvers/InlineNetLabelSolver/push-anchored-net-labels-away.test.ts +227 -0
- package/tests/solvers/MspConnectionPairSolver/msp-connection-pair-solver-direct-connection-distance.test.ts +80 -0
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
getAxisAlignedSegments,
|
|
19
19
|
} from "./getAxisAlignedSegments"
|
|
20
20
|
import { alignPortOnlyInlineNetLabelStubs } from "./alignPortOnlyInlineNetLabelStubs"
|
|
21
|
+
import { pushAnchoredNetLabelsAwayFromInlineLabels } from "./pushAnchoredNetLabelsAwayFromInlineLabels"
|
|
21
22
|
|
|
22
23
|
export const DEFAULT_INLINE_NET_LABEL_HEIGHT = 0.18
|
|
23
24
|
|
|
@@ -111,6 +112,11 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
111
112
|
|
|
112
113
|
private tracesByPinPairKey: Map<string, SolvedTracePath[]>
|
|
113
114
|
private hasAlignedPortOnlyStubs = false
|
|
115
|
+
private postProcessedOutput?: {
|
|
116
|
+
traces: SolvedTracePath[]
|
|
117
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
118
|
+
inlineNetLabelPlacements: InlineNetLabelPlacement[]
|
|
119
|
+
}
|
|
114
120
|
|
|
115
121
|
constructor(input: InlineNetLabelSolverInput) {
|
|
116
122
|
super()
|
|
@@ -191,6 +197,11 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
191
197
|
return
|
|
192
198
|
}
|
|
193
199
|
|
|
200
|
+
if (!this.postProcessedOutput) {
|
|
201
|
+
this.postProcessedOutput = this.buildPostProcessedOutput()
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
194
205
|
this.solved = true
|
|
195
206
|
this.stats.inlineNetLabelCount = this.inlineNetLabelPlacements.length
|
|
196
207
|
}
|
|
@@ -584,20 +595,31 @@ export class InlineNetLabelSolver extends BaseSolver {
|
|
|
584
595
|
)
|
|
585
596
|
}
|
|
586
597
|
|
|
587
|
-
|
|
598
|
+
private buildPostProcessedOutput() {
|
|
588
599
|
const superseded = this.getSupersededNetLabelKeys()
|
|
600
|
+
const retainedNetLabelPlacements = this.inputNetLabelPlacements.filter(
|
|
601
|
+
(placement) => !superseded.has(placement.globalConnNetId),
|
|
602
|
+
)
|
|
603
|
+
const outputTraces = this.getOutputTraces(superseded)
|
|
604
|
+
const pushed = pushAnchoredNetLabelsAwayFromInlineLabels({
|
|
605
|
+
inputProblem: this.inputProblem,
|
|
606
|
+
traces: outputTraces,
|
|
607
|
+
netLabelPlacements: retainedNetLabelPlacements,
|
|
608
|
+
inlineNetLabelPlacements: this.inlineNetLabelPlacements,
|
|
609
|
+
})
|
|
610
|
+
this.stats.pushedAnchoredNetLabelCount = pushed.movedLabelCount
|
|
589
611
|
return {
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
// actual net trace, but discard that now-orphaned label connector.
|
|
593
|
-
traces: this.getOutputTraces(superseded),
|
|
594
|
-
netLabelPlacements: this.inputNetLabelPlacements.filter(
|
|
595
|
-
(placement) => !superseded.has(placement.globalConnNetId),
|
|
596
|
-
),
|
|
612
|
+
traces: pushed.traces,
|
|
613
|
+
netLabelPlacements: pushed.netLabelPlacements,
|
|
597
614
|
inlineNetLabelPlacements: this.inlineNetLabelPlacements,
|
|
598
615
|
}
|
|
599
616
|
}
|
|
600
617
|
|
|
618
|
+
getOutput() {
|
|
619
|
+
if (this.postProcessedOutput) return this.postProcessedOutput
|
|
620
|
+
return this.buildPostProcessedOutput()
|
|
621
|
+
}
|
|
622
|
+
|
|
601
623
|
override visualize(): GraphicsObject {
|
|
602
624
|
// Mirrors the previous pipeline stage's visualization so that a problem
|
|
603
625
|
// with no inline labels renders identically, then layers the inline labels
|
|
@@ -0,0 +1,530 @@
|
|
|
1
|
+
import type { Bounds, Point } from "@tscircuit/math-utils"
|
|
2
|
+
import {
|
|
3
|
+
getPinMap,
|
|
4
|
+
getTracePins,
|
|
5
|
+
} from "lib/solvers/AvailableNetOrientationSolver/traces"
|
|
6
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
7
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
8
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
9
|
+
import { dir, type FacingDirection } from "lib/utils/dir"
|
|
10
|
+
import { boundsOverlap, getTextBoxBounds } from "lib/utils/textBoxBounds"
|
|
11
|
+
import type { InlineNetLabelPlacement } from "./InlineNetLabelSolver"
|
|
12
|
+
|
|
13
|
+
const LABEL_CLEARANCE = 0.05
|
|
14
|
+
const POINT_EPSILON = 1e-6
|
|
15
|
+
const CONTIGUOUS_LABEL_GAP = 0.01
|
|
16
|
+
const MAX_OUTWARD_DISTANCE = 5
|
|
17
|
+
|
|
18
|
+
const getBounds = (placement: {
|
|
19
|
+
center: Point
|
|
20
|
+
width: number
|
|
21
|
+
height: number
|
|
22
|
+
axis?: "x" | "y"
|
|
23
|
+
}): Bounds => {
|
|
24
|
+
const renderedWidth =
|
|
25
|
+
placement.axis === "y" ? placement.height : placement.width
|
|
26
|
+
const renderedHeight =
|
|
27
|
+
placement.axis === "y" ? placement.width : placement.height
|
|
28
|
+
return {
|
|
29
|
+
minX: placement.center.x - renderedWidth / 2,
|
|
30
|
+
maxX: placement.center.x + renderedWidth / 2,
|
|
31
|
+
minY: placement.center.y - renderedHeight / 2,
|
|
32
|
+
maxY: placement.center.y + renderedHeight / 2,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const pointsEqual = (a: Point, b: Point) =>
|
|
37
|
+
Math.abs(a.x - b.x) <= POINT_EPSILON && Math.abs(a.y - b.y) <= POINT_EPSILON
|
|
38
|
+
|
|
39
|
+
const pathIntersectsBounds = (path: Point[], bounds: Bounds) => {
|
|
40
|
+
for (let index = 0; index < path.length - 1; index++) {
|
|
41
|
+
const start = path[index]!
|
|
42
|
+
const end = path[index + 1]!
|
|
43
|
+
const segmentBounds: Bounds = {
|
|
44
|
+
minX: Math.min(start.x, end.x),
|
|
45
|
+
maxX: Math.max(start.x, end.x),
|
|
46
|
+
minY: Math.min(start.y, end.y),
|
|
47
|
+
maxY: Math.max(start.y, end.y),
|
|
48
|
+
}
|
|
49
|
+
if (boundsOverlap(segmentBounds, bounds)) return true
|
|
50
|
+
}
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isPointOnPath = (point: Point, path: Point[]) => {
|
|
55
|
+
for (let index = 0; index < path.length - 1; index++) {
|
|
56
|
+
const start = path[index]!
|
|
57
|
+
const end = path[index + 1]!
|
|
58
|
+
const minX = Math.min(start.x, end.x) - POINT_EPSILON
|
|
59
|
+
const maxX = Math.max(start.x, end.x) + POINT_EPSILON
|
|
60
|
+
const minY = Math.min(start.y, end.y) - POINT_EPSILON
|
|
61
|
+
const maxY = Math.max(start.y, end.y) + POINT_EPSILON
|
|
62
|
+
const isHorizontal = Math.abs(start.y - end.y) <= POINT_EPSILON
|
|
63
|
+
const isVertical = Math.abs(start.x - end.x) <= POINT_EPSILON
|
|
64
|
+
if (
|
|
65
|
+
((isHorizontal && Math.abs(point.y - start.y) <= POINT_EPSILON) ||
|
|
66
|
+
(isVertical && Math.abs(point.x - start.x) <= POINT_EPSILON)) &&
|
|
67
|
+
point.x >= minX &&
|
|
68
|
+
point.x <= maxX &&
|
|
69
|
+
point.y >= minY &&
|
|
70
|
+
point.y <= maxY
|
|
71
|
+
) {
|
|
72
|
+
return true
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return false
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const getRequiredOutwardDistance = (
|
|
79
|
+
label: NetLabelPlacement,
|
|
80
|
+
inlineBounds: Bounds[],
|
|
81
|
+
) => {
|
|
82
|
+
const labelBounds = getBounds(label)
|
|
83
|
+
|
|
84
|
+
if (label.orientation === "x-" || label.orientation === "x+") {
|
|
85
|
+
const nearby = inlineBounds.filter(
|
|
86
|
+
(bounds) =>
|
|
87
|
+
labelBounds.minY < bounds.maxY && labelBounds.maxY > bounds.minY,
|
|
88
|
+
)
|
|
89
|
+
if (nearby.length === 0) return 0
|
|
90
|
+
if (label.orientation === "x-") {
|
|
91
|
+
const targetMaxX = Math.min(...nearby.map((bounds) => bounds.minX))
|
|
92
|
+
return Math.max(0, labelBounds.maxX - targetMaxX + LABEL_CLEARANCE)
|
|
93
|
+
}
|
|
94
|
+
const targetMinX = Math.max(...nearby.map((bounds) => bounds.maxX))
|
|
95
|
+
return Math.max(0, targetMinX - labelBounds.minX + LABEL_CLEARANCE)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const nearby = inlineBounds.filter(
|
|
99
|
+
(bounds) =>
|
|
100
|
+
labelBounds.minX < bounds.maxX && labelBounds.maxX > bounds.minX,
|
|
101
|
+
)
|
|
102
|
+
if (nearby.length === 0) return 0
|
|
103
|
+
if (label.orientation === "y-") {
|
|
104
|
+
const targetMaxY = Math.min(...nearby.map((bounds) => bounds.minY))
|
|
105
|
+
return Math.max(0, labelBounds.maxY - targetMaxY + LABEL_CLEARANCE)
|
|
106
|
+
}
|
|
107
|
+
const targetMinY = Math.max(...nearby.map((bounds) => bounds.maxY))
|
|
108
|
+
return Math.max(0, targetMinY - labelBounds.minY + LABEL_CLEARANCE)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const moveLabel = (
|
|
112
|
+
label: NetLabelPlacement,
|
|
113
|
+
orientation: FacingDirection,
|
|
114
|
+
distance: number,
|
|
115
|
+
): NetLabelPlacement => {
|
|
116
|
+
const direction = dir(orientation)
|
|
117
|
+
return {
|
|
118
|
+
...label,
|
|
119
|
+
anchorPoint: {
|
|
120
|
+
x: label.anchorPoint.x + direction.x * distance,
|
|
121
|
+
y: label.anchorPoint.y + direction.y * distance,
|
|
122
|
+
},
|
|
123
|
+
center: {
|
|
124
|
+
x: label.center.x + direction.x * distance,
|
|
125
|
+
y: label.center.y + direction.y * distance,
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const getDistanceToShoveBoundsPast = (
|
|
131
|
+
obstacleBounds: Bounds,
|
|
132
|
+
movingBounds: Bounds,
|
|
133
|
+
orientation: FacingDirection,
|
|
134
|
+
) => {
|
|
135
|
+
switch (orientation) {
|
|
136
|
+
case "x-":
|
|
137
|
+
return obstacleBounds.maxX - movingBounds.minX + LABEL_CLEARANCE
|
|
138
|
+
case "x+":
|
|
139
|
+
return movingBounds.maxX - obstacleBounds.minX + LABEL_CLEARANCE
|
|
140
|
+
case "y-":
|
|
141
|
+
return obstacleBounds.maxY - movingBounds.minY + LABEL_CLEARANCE
|
|
142
|
+
case "y+":
|
|
143
|
+
return movingBounds.maxY - obstacleBounds.minY + LABEL_CLEARANCE
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const boundsGapOnPerpendicularAxis = (
|
|
148
|
+
a: Bounds,
|
|
149
|
+
b: Bounds,
|
|
150
|
+
orientation: FacingDirection,
|
|
151
|
+
) => {
|
|
152
|
+
if (orientation === "x-" || orientation === "x+") {
|
|
153
|
+
return Math.max(0, a.minY - b.maxY, b.minY - a.maxY)
|
|
154
|
+
}
|
|
155
|
+
return Math.max(0, a.minX - b.maxX, b.minX - a.maxX)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const sharesOwnerChip = (
|
|
159
|
+
label: NetLabelPlacement,
|
|
160
|
+
ownerChipIds: Set<string>,
|
|
161
|
+
chipIdByPinId: Map<string, string>,
|
|
162
|
+
) =>
|
|
163
|
+
label.pinIds.some((pinId) => ownerChipIds.has(chipIdByPinId.get(pinId) ?? ""))
|
|
164
|
+
|
|
165
|
+
const isGeneratedLabelConnector = (trace: SolvedTracePath) =>
|
|
166
|
+
trace.mspPairId.startsWith("available-net-orientation-") ||
|
|
167
|
+
trace.mspPairId.startsWith("inline-net-label-clearance-")
|
|
168
|
+
|
|
169
|
+
const findConnectorTraceIndex = (
|
|
170
|
+
label: NetLabelPlacement,
|
|
171
|
+
traces: SolvedTracePath[],
|
|
172
|
+
) =>
|
|
173
|
+
traces.findIndex((trace) => {
|
|
174
|
+
if (trace.globalConnNetId !== label.globalConnNetId) return false
|
|
175
|
+
if (!isGeneratedLabelConnector(trace)) return false
|
|
176
|
+
const first = trace.tracePath[0]
|
|
177
|
+
const last = trace.tracePath.at(-1)
|
|
178
|
+
return Boolean(
|
|
179
|
+
(first && pointsEqual(first, label.anchorPoint)) ||
|
|
180
|
+
(last && pointsEqual(last, label.anchorPoint)),
|
|
181
|
+
)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
const canAddConnectorAtAnchor = (
|
|
185
|
+
label: NetLabelPlacement,
|
|
186
|
+
traces: SolvedTracePath[],
|
|
187
|
+
pinMap: ReturnType<typeof getPinMap>,
|
|
188
|
+
) => {
|
|
189
|
+
if (
|
|
190
|
+
label.pinIds.some((pinId) => {
|
|
191
|
+
const pin = pinMap[pinId]
|
|
192
|
+
return pin && pointsEqual(pin, label.anchorPoint)
|
|
193
|
+
})
|
|
194
|
+
) {
|
|
195
|
+
return true
|
|
196
|
+
}
|
|
197
|
+
return traces.some(
|
|
198
|
+
(trace) =>
|
|
199
|
+
trace.globalConnNetId === label.globalConnNetId &&
|
|
200
|
+
isPointOnPath(label.anchorPoint, trace.tracePath),
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const moveConnectorEndpoint = (
|
|
205
|
+
trace: SolvedTracePath,
|
|
206
|
+
oldAnchor: Point,
|
|
207
|
+
newAnchor: Point,
|
|
208
|
+
): SolvedTracePath => {
|
|
209
|
+
const tracePath = trace.tracePath.map((point) => ({ ...point }))
|
|
210
|
+
if (pointsEqual(tracePath[0]!, oldAnchor)) tracePath[0] = newAnchor
|
|
211
|
+
if (pointsEqual(tracePath.at(-1)!, oldAnchor)) {
|
|
212
|
+
tracePath[tracePath.length - 1] = newAnchor
|
|
213
|
+
}
|
|
214
|
+
return { ...trace, tracePath }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const createConnectorTrace = ({
|
|
218
|
+
label,
|
|
219
|
+
labelIndex,
|
|
220
|
+
newAnchor,
|
|
221
|
+
pinMap,
|
|
222
|
+
}: {
|
|
223
|
+
label: NetLabelPlacement
|
|
224
|
+
labelIndex: number
|
|
225
|
+
newAnchor: Point
|
|
226
|
+
pinMap: ReturnType<typeof getPinMap>
|
|
227
|
+
}): SolvedTracePath => {
|
|
228
|
+
const mspPairId = `inline-net-label-clearance-${labelIndex}-${label.netId ?? label.globalConnNetId}`
|
|
229
|
+
return {
|
|
230
|
+
mspPairId,
|
|
231
|
+
dcConnNetId: label.dcConnNetId ?? label.globalConnNetId,
|
|
232
|
+
globalConnNetId: label.globalConnNetId,
|
|
233
|
+
userNetId: label.netId,
|
|
234
|
+
pins: getTracePins(label, pinMap),
|
|
235
|
+
tracePath: [label.anchorPoint, newAnchor],
|
|
236
|
+
mspConnectionPairIds: [mspPairId],
|
|
237
|
+
pinIds: label.pinIds,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const getContiguousLabelGroup = ({
|
|
242
|
+
triggerIndex,
|
|
243
|
+
labels,
|
|
244
|
+
chipIdByPinId,
|
|
245
|
+
}: {
|
|
246
|
+
triggerIndex: number
|
|
247
|
+
labels: NetLabelPlacement[]
|
|
248
|
+
chipIdByPinId: Map<string, string>
|
|
249
|
+
}) => {
|
|
250
|
+
const trigger = labels[triggerIndex]!
|
|
251
|
+
const ownerChipIds = new Set(
|
|
252
|
+
trigger.pinIds.flatMap((pinId) => {
|
|
253
|
+
const chipId = chipIdByPinId.get(pinId)
|
|
254
|
+
return chipId ? [chipId] : []
|
|
255
|
+
}),
|
|
256
|
+
)
|
|
257
|
+
const candidates = labels
|
|
258
|
+
.map((label, labelIndex) => ({ label, labelIndex }))
|
|
259
|
+
.filter(
|
|
260
|
+
({ label }) =>
|
|
261
|
+
label.orientation === trigger.orientation &&
|
|
262
|
+
label.mspConnectionPairIds.length === 0 &&
|
|
263
|
+
sharesOwnerChip(label, ownerChipIds, chipIdByPinId),
|
|
264
|
+
)
|
|
265
|
+
const group = new Set([triggerIndex])
|
|
266
|
+
let changed = true
|
|
267
|
+
while (changed) {
|
|
268
|
+
changed = false
|
|
269
|
+
for (const { label, labelIndex } of candidates) {
|
|
270
|
+
if (group.has(labelIndex)) continue
|
|
271
|
+
if (
|
|
272
|
+
[...group].some(
|
|
273
|
+
(memberIndex) =>
|
|
274
|
+
boundsGapOnPerpendicularAxis(
|
|
275
|
+
getBounds(labels[memberIndex]!),
|
|
276
|
+
getBounds(label),
|
|
277
|
+
trigger.orientation,
|
|
278
|
+
) <= CONTIGUOUS_LABEL_GAP,
|
|
279
|
+
)
|
|
280
|
+
) {
|
|
281
|
+
group.add(labelIndex)
|
|
282
|
+
changed = true
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return { group, ownerChipIds }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Pushes conventional endpoint labels past nearby inline label text.
|
|
291
|
+
*
|
|
292
|
+
* Contiguous conventional labels on the same component side move as a group,
|
|
293
|
+
* keeping their connector tips aligned. When that new column encounters
|
|
294
|
+
* another label belonging to the same component, the obstacle is shoved one
|
|
295
|
+
* column farther outward and receives its own short connector. The entire
|
|
296
|
+
* proposal is rejected if a chip, component text, inline label, fixed label,
|
|
297
|
+
* or unrelated trace would still be hit.
|
|
298
|
+
*/
|
|
299
|
+
export const pushAnchoredNetLabelsAwayFromInlineLabels = ({
|
|
300
|
+
inputProblem,
|
|
301
|
+
traces,
|
|
302
|
+
netLabelPlacements,
|
|
303
|
+
inlineNetLabelPlacements,
|
|
304
|
+
}: {
|
|
305
|
+
inputProblem: InputProblem
|
|
306
|
+
traces: SolvedTracePath[]
|
|
307
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
308
|
+
inlineNetLabelPlacements: InlineNetLabelPlacement[]
|
|
309
|
+
}): {
|
|
310
|
+
traces: SolvedTracePath[]
|
|
311
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
312
|
+
movedLabelCount: number
|
|
313
|
+
} => {
|
|
314
|
+
const outputTraces = traces.map((trace) => ({
|
|
315
|
+
...trace,
|
|
316
|
+
tracePath: trace.tracePath.map((point) => ({ ...point })),
|
|
317
|
+
}))
|
|
318
|
+
const outputLabels = netLabelPlacements.map((label) => ({ ...label }))
|
|
319
|
+
const inlineBounds = inlineNetLabelPlacements.map(getBounds)
|
|
320
|
+
const pinMap = getPinMap(inputProblem)
|
|
321
|
+
const chipIdByPinId = new Map<string, string>()
|
|
322
|
+
for (const chip of inputProblem.chips) {
|
|
323
|
+
for (const pin of chip.pins) chipIdByPinId.set(pin.pinId, chip.chipId)
|
|
324
|
+
}
|
|
325
|
+
const movedLabelIndices = new Set<number>()
|
|
326
|
+
|
|
327
|
+
for (
|
|
328
|
+
let triggerIndex = 0;
|
|
329
|
+
triggerIndex < outputLabels.length;
|
|
330
|
+
triggerIndex++
|
|
331
|
+
) {
|
|
332
|
+
const trigger = outputLabels[triggerIndex]!
|
|
333
|
+
const distance = getRequiredOutwardDistance(trigger, inlineBounds)
|
|
334
|
+
if (distance <= POINT_EPSILON || distance > MAX_OUTWARD_DISTANCE) continue
|
|
335
|
+
|
|
336
|
+
const { group, ownerChipIds } = getContiguousLabelGroup({
|
|
337
|
+
triggerIndex,
|
|
338
|
+
labels: outputLabels,
|
|
339
|
+
chipIdByPinId,
|
|
340
|
+
})
|
|
341
|
+
const distances = new Map<number, number>(
|
|
342
|
+
[...group].map((labelIndex) => [labelIndex, distance]),
|
|
343
|
+
)
|
|
344
|
+
|
|
345
|
+
let failed = false
|
|
346
|
+
for (let iteration = 0; iteration < outputLabels.length; iteration++) {
|
|
347
|
+
let adjustedObstacle = false
|
|
348
|
+
for (const [movingIndex, movingDistance] of distances) {
|
|
349
|
+
const movingBounds = getBounds(
|
|
350
|
+
moveLabel(
|
|
351
|
+
outputLabels[movingIndex]!,
|
|
352
|
+
trigger.orientation,
|
|
353
|
+
movingDistance,
|
|
354
|
+
),
|
|
355
|
+
)
|
|
356
|
+
for (
|
|
357
|
+
let obstacleIndex = 0;
|
|
358
|
+
obstacleIndex < outputLabels.length;
|
|
359
|
+
obstacleIndex++
|
|
360
|
+
) {
|
|
361
|
+
if (group.has(obstacleIndex)) continue
|
|
362
|
+
const obstacle = outputLabels[obstacleIndex]!
|
|
363
|
+
const existingObstacleDistance = distances.get(obstacleIndex) ?? 0
|
|
364
|
+
const obstacleBounds = getBounds(
|
|
365
|
+
moveLabel(obstacle, trigger.orientation, existingObstacleDistance),
|
|
366
|
+
)
|
|
367
|
+
if (!boundsOverlap(movingBounds, obstacleBounds)) continue
|
|
368
|
+
if (
|
|
369
|
+
!sharesOwnerChip(obstacle, ownerChipIds, chipIdByPinId) ||
|
|
370
|
+
(findConnectorTraceIndex(obstacle, outputTraces) === -1 &&
|
|
371
|
+
!canAddConnectorAtAnchor(obstacle, outputTraces, pinMap))
|
|
372
|
+
) {
|
|
373
|
+
failed = true
|
|
374
|
+
break
|
|
375
|
+
}
|
|
376
|
+
const shoveDistance = getDistanceToShoveBoundsPast(
|
|
377
|
+
getBounds(obstacle),
|
|
378
|
+
movingBounds,
|
|
379
|
+
trigger.orientation,
|
|
380
|
+
)
|
|
381
|
+
if (
|
|
382
|
+
shoveDistance > MAX_OUTWARD_DISTANCE ||
|
|
383
|
+
shoveDistance <= existingObstacleDistance + POINT_EPSILON
|
|
384
|
+
) {
|
|
385
|
+
failed = true
|
|
386
|
+
break
|
|
387
|
+
}
|
|
388
|
+
distances.set(obstacleIndex, shoveDistance)
|
|
389
|
+
adjustedObstacle = true
|
|
390
|
+
}
|
|
391
|
+
if (failed) break
|
|
392
|
+
}
|
|
393
|
+
if (failed || !adjustedObstacle) break
|
|
394
|
+
}
|
|
395
|
+
if (failed) continue
|
|
396
|
+
|
|
397
|
+
const proposals = new Map<number, NetLabelPlacement>()
|
|
398
|
+
for (const [labelIndex, labelDistance] of distances) {
|
|
399
|
+
proposals.set(
|
|
400
|
+
labelIndex,
|
|
401
|
+
moveLabel(
|
|
402
|
+
outputLabels[labelIndex]!,
|
|
403
|
+
trigger.orientation,
|
|
404
|
+
labelDistance,
|
|
405
|
+
),
|
|
406
|
+
)
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const finalLabelAt = (labelIndex: number) =>
|
|
410
|
+
proposals.get(labelIndex) ?? outputLabels[labelIndex]!
|
|
411
|
+
for (const [labelIndex, movedLabel] of proposals) {
|
|
412
|
+
const movedBounds = getBounds(movedLabel)
|
|
413
|
+
if (inlineBounds.some((bounds) => boundsOverlap(movedBounds, bounds))) {
|
|
414
|
+
failed = true
|
|
415
|
+
break
|
|
416
|
+
}
|
|
417
|
+
if (
|
|
418
|
+
inputProblem.chips.some((chip) =>
|
|
419
|
+
boundsOverlap(movedBounds, {
|
|
420
|
+
minX: chip.center.x - chip.width / 2,
|
|
421
|
+
maxX: chip.center.x + chip.width / 2,
|
|
422
|
+
minY: chip.center.y - chip.height / 2,
|
|
423
|
+
maxY: chip.center.y + chip.height / 2,
|
|
424
|
+
}),
|
|
425
|
+
) ||
|
|
426
|
+
(inputProblem.textBoxes ?? []).some((textBox) =>
|
|
427
|
+
boundsOverlap(movedBounds, getTextBoxBounds(textBox)),
|
|
428
|
+
)
|
|
429
|
+
) {
|
|
430
|
+
failed = true
|
|
431
|
+
break
|
|
432
|
+
}
|
|
433
|
+
if (
|
|
434
|
+
outputLabels.some(
|
|
435
|
+
(_, otherIndex) =>
|
|
436
|
+
otherIndex !== labelIndex &&
|
|
437
|
+
boundsOverlap(movedBounds, getBounds(finalLabelAt(otherIndex))),
|
|
438
|
+
)
|
|
439
|
+
) {
|
|
440
|
+
failed = true
|
|
441
|
+
break
|
|
442
|
+
}
|
|
443
|
+
if (
|
|
444
|
+
outputTraces.some(
|
|
445
|
+
(trace) =>
|
|
446
|
+
trace.globalConnNetId !== movedLabel.globalConnNetId &&
|
|
447
|
+
pathIntersectsBounds(trace.tracePath, movedBounds),
|
|
448
|
+
)
|
|
449
|
+
) {
|
|
450
|
+
failed = true
|
|
451
|
+
break
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (failed) continue
|
|
455
|
+
|
|
456
|
+
const connectorUpdates: Array<{
|
|
457
|
+
labelIndex: number
|
|
458
|
+
connectorIndex: number
|
|
459
|
+
trace: SolvedTracePath
|
|
460
|
+
}> = []
|
|
461
|
+
for (const [labelIndex, movedLabel] of proposals) {
|
|
462
|
+
const label = outputLabels[labelIndex]!
|
|
463
|
+
const connectorIndex = findConnectorTraceIndex(label, outputTraces)
|
|
464
|
+
if (
|
|
465
|
+
connectorIndex === -1 &&
|
|
466
|
+
!canAddConnectorAtAnchor(label, outputTraces, pinMap)
|
|
467
|
+
) {
|
|
468
|
+
failed = true
|
|
469
|
+
break
|
|
470
|
+
}
|
|
471
|
+
const connector =
|
|
472
|
+
connectorIndex === -1
|
|
473
|
+
? createConnectorTrace({
|
|
474
|
+
label,
|
|
475
|
+
labelIndex,
|
|
476
|
+
newAnchor: movedLabel.anchorPoint,
|
|
477
|
+
pinMap,
|
|
478
|
+
})
|
|
479
|
+
: moveConnectorEndpoint(
|
|
480
|
+
outputTraces[connectorIndex]!,
|
|
481
|
+
label.anchorPoint,
|
|
482
|
+
movedLabel.anchorPoint,
|
|
483
|
+
)
|
|
484
|
+
const connectorObstructed =
|
|
485
|
+
inlineBounds.some((bounds) =>
|
|
486
|
+
pathIntersectsBounds(connector.tracePath, bounds),
|
|
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
|
+
}),
|
|
495
|
+
) ||
|
|
496
|
+
(inputProblem.textBoxes ?? []).some((textBox) =>
|
|
497
|
+
pathIntersectsBounds(connector.tracePath, getTextBoxBounds(textBox)),
|
|
498
|
+
) ||
|
|
499
|
+
outputLabels.some(
|
|
500
|
+
(_, otherIndex) =>
|
|
501
|
+
otherIndex !== labelIndex &&
|
|
502
|
+
pathIntersectsBounds(
|
|
503
|
+
connector.tracePath,
|
|
504
|
+
getBounds(finalLabelAt(otherIndex)),
|
|
505
|
+
),
|
|
506
|
+
)
|
|
507
|
+
if (connectorObstructed) {
|
|
508
|
+
failed = true
|
|
509
|
+
break
|
|
510
|
+
}
|
|
511
|
+
connectorUpdates.push({ labelIndex, connectorIndex, trace: connector })
|
|
512
|
+
}
|
|
513
|
+
if (failed) continue
|
|
514
|
+
|
|
515
|
+
for (const [labelIndex, movedLabel] of proposals) {
|
|
516
|
+
outputLabels[labelIndex] = movedLabel
|
|
517
|
+
movedLabelIndices.add(labelIndex)
|
|
518
|
+
}
|
|
519
|
+
for (const update of connectorUpdates) {
|
|
520
|
+
if (update.connectorIndex === -1) outputTraces.push(update.trace)
|
|
521
|
+
else outputTraces[update.connectorIndex] = update.trace
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return {
|
|
526
|
+
traces: outputTraces,
|
|
527
|
+
netLabelPlacements: outputLabels,
|
|
528
|
+
movedLabelCount: movedLabelIndices.size,
|
|
529
|
+
}
|
|
530
|
+
}
|
|
@@ -14,7 +14,7 @@ import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualize
|
|
|
14
14
|
import { doesPairCrossRestrictedCenterLines } from "./doesPairCrossRestrictedCenterLines"
|
|
15
15
|
import { getConnectivityMapsFromInputProblem } from "./getConnectivityMapFromInputProblem"
|
|
16
16
|
import { getOrthogonalMinimumSpanningTree } from "./getMspConnectionPairsFromPins"
|
|
17
|
-
import {
|
|
17
|
+
import { getLabeledConnectionRouteReason } from "./isLabeledPeripheralConnection"
|
|
18
18
|
|
|
19
19
|
export type MspConnectionPairId = string
|
|
20
20
|
export const DEFAULT_MAX_MSP_PAIR_DISTANCE = 1
|
|
@@ -26,6 +26,8 @@ export type MspConnectionPair = {
|
|
|
26
26
|
dcConnNetId: string
|
|
27
27
|
globalConnNetId: string
|
|
28
28
|
userNetId?: string
|
|
29
|
+
/** The trace replaces fallback labels that could not fit between its pins. */
|
|
30
|
+
suppressNetLabel?: boolean
|
|
29
31
|
pins: [InputPin & { chipId: string }, InputPin & { chipId: string }]
|
|
30
32
|
}
|
|
31
33
|
|
|
@@ -121,14 +123,17 @@ export class MspConnectionPairSolver extends BaseSolver {
|
|
|
121
123
|
if (this.directConnectionPinPairKeys.has(pinPairKey)) {
|
|
122
124
|
pairDistance = distance(p1, p2)
|
|
123
125
|
}
|
|
124
|
-
// Labeled one-pin peripherals
|
|
125
|
-
//
|
|
126
|
-
const
|
|
126
|
+
// Labeled one-pin peripherals and opposed pins whose fallback labels
|
|
127
|
+
// cannot fit need a real trace even when they are far apart.
|
|
128
|
+
const labeledConnectionRouteReason = getLabeledConnectionRouteReason({
|
|
127
129
|
inputProblem: this.inputProblem,
|
|
128
130
|
chipMap: this.chipMap,
|
|
129
131
|
pins: [p1, p2],
|
|
130
132
|
})
|
|
131
|
-
if (
|
|
133
|
+
if (
|
|
134
|
+
pairDistance > this.maxMspPairDistance &&
|
|
135
|
+
!labeledConnectionRouteReason
|
|
136
|
+
) {
|
|
132
137
|
// Too far apart; skip creating an MSP pair for this net
|
|
133
138
|
return
|
|
134
139
|
}
|
|
@@ -155,12 +160,16 @@ export class MspConnectionPairSolver extends BaseSolver {
|
|
|
155
160
|
const globalConnNetId = this.globalConnMap.getNetConnectedToId(pin1!)!
|
|
156
161
|
const userNetId =
|
|
157
162
|
this.userNetIdByPinId[pin1!] ?? this.userNetIdByPinId[pin2!]
|
|
163
|
+
const suppressNetLabel =
|
|
164
|
+
pairDistance > this.maxMspPairDistance &&
|
|
165
|
+
labeledConnectionRouteReason === "overlapping-fallback-labels"
|
|
158
166
|
|
|
159
167
|
this.mspConnectionPairs.push({
|
|
160
168
|
mspPairId: `${pin1}-${pin2}`,
|
|
161
169
|
dcConnNetId: dcNetId,
|
|
162
170
|
globalConnNetId,
|
|
163
171
|
userNetId,
|
|
172
|
+
suppressNetLabel,
|
|
164
173
|
pins: [p1, p2],
|
|
165
174
|
})
|
|
166
175
|
|