@tscircuit/schematic-trace-solver 0.0.145 → 0.0.147
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 +9 -2
- package/dist/index.js +482 -86
- package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +38 -30
- package/lib/solvers/InlineNetLabelSolver/InlineNetLabelSolver.ts +30 -8
- package/lib/solvers/InlineNetLabelSolver/pushAnchoredNetLabelsAwayFromInlineLabels.ts +530 -0
- package/lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver.ts +6 -21
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +7 -23
- package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/candidates.ts +7 -14
- package/lib/types/InputProblem.ts +8 -0
- package/lib/utils/getNetLabelWidthForConnection.ts +63 -0
- package/package.json +1 -1
- package/tests/assets/inline-net-label-anchored-label-clearance.json +56 -0
- package/tests/repros/__snapshots__/repro-ti-power-output-section.snap.svg +80 -0
- package/tests/repros/repro-ti-power-output-section.input.ts +341 -0
- package/tests/repros/repro-ti-power-output-section.test.ts +13 -0
- package/tests/solvers/InlineNetLabelSolver/__snapshots__/fallback-net-label-width.snap.svg +69 -0
- package/tests/solvers/InlineNetLabelSolver/__snapshots__/inline-net-label-anchored-label-clearance.snap.svg +54 -0
- package/tests/solvers/InlineNetLabelSolver/fallback-net-label-width.test.ts +152 -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
|
@@ -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
|
+
}
|
|
@@ -9,6 +9,7 @@ import type { GraphicsObject } from "graphics-debug"
|
|
|
9
9
|
import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
|
|
10
10
|
import { getColorFromString } from "lib/utils/getColorFromString"
|
|
11
11
|
import { getConnectivityMapsFromInputProblem } from "../MspConnectionPairSolver/getConnectivityMapFromInputProblem"
|
|
12
|
+
import { getNetLabelWidthForConnection } from "lib/utils/getNetLabelWidthForConnection"
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* A group of traces that have at least one overlapping segment and
|
|
@@ -261,31 +262,15 @@ export class NetLabelPlacementSolver extends BaseSolver {
|
|
|
261
262
|
private getNetLabelWidthForGroup(
|
|
262
263
|
group: OverlappingSameNetTraceGroup,
|
|
263
264
|
): number | undefined {
|
|
264
|
-
if (group.netId) {
|
|
265
|
-
const ncWidth = this.inputProblem.netConnections.find(
|
|
266
|
-
(nc) => nc.netId === group.netId,
|
|
267
|
-
)?.netLabelWidth
|
|
268
|
-
if (ncWidth !== undefined) return ncWidth
|
|
269
|
-
|
|
270
|
-
const dcWidthByNetId = this.inputProblem.directConnections.find(
|
|
271
|
-
(dc) => dc.netId === group.netId,
|
|
272
|
-
)?.netLabelWidth
|
|
273
|
-
if (dcWidthByNetId !== undefined) return dcWidthByNetId
|
|
274
|
-
}
|
|
275
|
-
|
|
276
265
|
const pinIds = group.overlappingTraces?.pins.map((p) => p.pinId) ?? []
|
|
277
266
|
if (group.portOnlyPinId) {
|
|
278
267
|
pinIds.push(group.portOnlyPinId)
|
|
279
268
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
return this.inputProblem.netConnections.find((nc) =>
|
|
287
|
-
nc.pinIds.some((pid) => pinIds.includes(pid)),
|
|
288
|
-
)?.netLabelWidth
|
|
269
|
+
return getNetLabelWidthForConnection({
|
|
270
|
+
inputProblem: this.inputProblem,
|
|
271
|
+
netId: group.netId,
|
|
272
|
+
pinIds,
|
|
273
|
+
})
|
|
289
274
|
}
|
|
290
275
|
|
|
291
276
|
private getNetLabelHeightForGroup(
|
|
@@ -7,6 +7,7 @@ import { getDimsForOrientation } from "lib/solvers/NetLabelPlacementSolver/Singl
|
|
|
7
7
|
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
8
8
|
import type { InputChip, InputProblem } from "lib/types/InputProblem"
|
|
9
9
|
import type { FacingDirection } from "lib/utils/dir"
|
|
10
|
+
import { getNetLabelWidthForConnection } from "lib/utils/getNetLabelWidthForConnection"
|
|
10
11
|
import { getTextBoxBounds, type RectPadding } from "lib/utils/textBoxBounds"
|
|
11
12
|
import { getPinDirection } from "../SchematicTraceSingleLineSolver/getPinDirection"
|
|
12
13
|
import { calculateDirectShortPath } from "./calculateDirectShortPath"
|
|
@@ -201,7 +202,12 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
201
202
|
const orientations =
|
|
202
203
|
this.inputProblem.availableNetLabelOrientations[netId] ??
|
|
203
204
|
(["x+", "x-", "y+", "y-"] as FacingDirection[])
|
|
204
|
-
const netLabelWidth =
|
|
205
|
+
const netLabelWidth = getNetLabelWidthForConnection({
|
|
206
|
+
inputProblem: this.inputProblem,
|
|
207
|
+
netId,
|
|
208
|
+
pinIds: this.pins.map((pin) => pin.pinId),
|
|
209
|
+
includeFallbackNetLabelWidth: false,
|
|
210
|
+
})
|
|
205
211
|
const netLabelHeight = this.getNetLabelHeightForConnectionPair(netId)
|
|
206
212
|
const padding: Required<RectPadding> = {
|
|
207
213
|
minX: 0,
|
|
@@ -239,28 +245,6 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
|
|
|
239
245
|
return padding
|
|
240
246
|
}
|
|
241
247
|
|
|
242
|
-
private getNetLabelWidthForConnectionPair(netId: string) {
|
|
243
|
-
const ncWidth = this.inputProblem.netConnections.find(
|
|
244
|
-
(nc) => nc.netId === netId,
|
|
245
|
-
)?.netLabelWidth
|
|
246
|
-
if (ncWidth !== undefined) return ncWidth
|
|
247
|
-
|
|
248
|
-
const dcWidthByNetId = this.inputProblem.directConnections.find(
|
|
249
|
-
(dc) => dc.netId === netId,
|
|
250
|
-
)?.netLabelWidth
|
|
251
|
-
if (dcWidthByNetId !== undefined) return dcWidthByNetId
|
|
252
|
-
|
|
253
|
-
const pinIds = this.pins.map((p) => p.pinId)
|
|
254
|
-
const dcWidthByPinId = this.inputProblem.directConnections.find((dc) =>
|
|
255
|
-
dc.pinIds.some((pid) => pinIds.includes(pid)),
|
|
256
|
-
)?.netLabelWidth
|
|
257
|
-
if (dcWidthByPinId !== undefined) return dcWidthByPinId
|
|
258
|
-
|
|
259
|
-
return this.inputProblem.netConnections.find((nc) =>
|
|
260
|
-
nc.pinIds.some((pid) => pinIds.includes(pid)),
|
|
261
|
-
)?.netLabelWidth
|
|
262
|
-
}
|
|
263
|
-
|
|
264
248
|
private getNetLabelHeightForConnectionPair(netId: string) {
|
|
265
249
|
const ncHeight = this.inputProblem.netConnections.find(
|
|
266
250
|
(nc) => nc.netId === netId,
|
|
@@ -8,6 +8,7 @@ import type { InputProblem } from "lib/types/InputProblem"
|
|
|
8
8
|
import { dedupeOrientations } from "lib/utils/dedupeOrientations"
|
|
9
9
|
import type { FacingDirection } from "lib/utils/dir"
|
|
10
10
|
import { getOrientationConstraint } from "lib/utils/getOrientationConstraint"
|
|
11
|
+
import { getNetLabelWidthForConnection } from "lib/utils/getNetLabelWidthForConnection"
|
|
11
12
|
import {
|
|
12
13
|
EPS,
|
|
13
14
|
getManhattanDistance,
|
|
@@ -310,20 +311,12 @@ const getNetLabelWidth = (
|
|
|
310
311
|
inputProblem: InputProblem,
|
|
311
312
|
label: NetLabelPlacement,
|
|
312
313
|
) => {
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
dc.pinIds.some((pid) => label.pinIds.includes(pid)),
|
|
320
|
-
)?.netLabelWidth
|
|
321
|
-
if (dcWidth !== undefined) return dcWidth
|
|
322
|
-
|
|
323
|
-
const ncWidthByPinId = inputProblem.netConnections.find((nc) =>
|
|
324
|
-
nc.pinIds.some((pid) => label.pinIds.includes(pid)),
|
|
325
|
-
)?.netLabelWidth
|
|
326
|
-
if (ncWidthByPinId !== undefined) return ncWidthByPinId
|
|
314
|
+
const configuredWidth = getNetLabelWidthForConnection({
|
|
315
|
+
inputProblem,
|
|
316
|
+
netId: label.netId,
|
|
317
|
+
pinIds: label.pinIds,
|
|
318
|
+
})
|
|
319
|
+
if (configuredWidth !== undefined) return configuredWidth
|
|
327
320
|
|
|
328
321
|
if (label.orientation === "y+" || label.orientation === "y-") {
|
|
329
322
|
return label.height
|
|
@@ -35,6 +35,13 @@ export interface InputDirectConnection {
|
|
|
35
35
|
netId?: string
|
|
36
36
|
netLabelWidth?: number
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Width of the conventional anchored label to use only when an inline label
|
|
40
|
+
* cannot be placed. Unlike `netLabelWidth`, this does not affect whether or
|
|
41
|
+
* how the point-to-point connection is routed.
|
|
42
|
+
*/
|
|
43
|
+
fallbackNetLabelWidth?: number
|
|
44
|
+
|
|
38
45
|
/**
|
|
39
46
|
* When true, this point-to-point connection may be labeled with an "inline
|
|
40
47
|
* net label": the net name is drawn parallel to (and offset from) the routed
|
|
@@ -66,6 +73,7 @@ export interface InputNetConnection {
|
|
|
66
73
|
netId: string
|
|
67
74
|
pinIds: Array<PinId>
|
|
68
75
|
netLabelWidth?: number
|
|
76
|
+
fallbackNetLabelWidth?: number
|
|
69
77
|
netLabelHeight?: number
|
|
70
78
|
|
|
71
79
|
/**
|