@tscircuit/schematic-trace-solver 0.0.51 → 0.0.52
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 +65 -3
- package/dist/index.js +827 -51
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +14 -0
- package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver.ts +331 -0
- package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/candidates.ts +429 -0
- package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/geometry.ts +250 -0
- package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/types.ts +47 -0
- package/lib/solvers/TraceAnchoredNetLabelOverlapSolver/visualize.ts +125 -0
- package/package.json +1 -1
- package/tests/examples/__snapshots__/example17.snap.svg +2 -2
- package/tests/examples/__snapshots__/example30.snap.svg +59 -59
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import {
|
|
3
|
+
getCenterFromAnchor,
|
|
4
|
+
getDimsForOrientation,
|
|
5
|
+
} from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
|
|
6
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
7
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
8
|
+
import type { FacingDirection } from "lib/utils/dir"
|
|
9
|
+
import {
|
|
10
|
+
EPS,
|
|
11
|
+
getManhattanDistance,
|
|
12
|
+
getPointAtTraceDistance,
|
|
13
|
+
getTraceDistanceCompatibleOrientations,
|
|
14
|
+
getTraceLength,
|
|
15
|
+
getTraceVertexDistances,
|
|
16
|
+
} from "./geometry"
|
|
17
|
+
import type { Bounds, LabelCandidate, TraceLocation } from "./types"
|
|
18
|
+
|
|
19
|
+
const CANDIDATE_STEP = 0.1
|
|
20
|
+
|
|
21
|
+
export const generateCandidatesAlongTrace = (params: {
|
|
22
|
+
inputProblem: InputProblem
|
|
23
|
+
label: NetLabelPlacement
|
|
24
|
+
traceLocation: TraceLocation
|
|
25
|
+
}) => {
|
|
26
|
+
const { inputProblem, label, traceLocation } = params
|
|
27
|
+
const traceLength = getTraceLength(traceLocation.trace)
|
|
28
|
+
const orientationConstraint = getOrientationConstraint(inputProblem, label)
|
|
29
|
+
const candidateDistances = getCandidateDistances(
|
|
30
|
+
traceLocation,
|
|
31
|
+
traceLength,
|
|
32
|
+
getTraceVertexDistances(traceLocation.trace),
|
|
33
|
+
orientationConstraint,
|
|
34
|
+
)
|
|
35
|
+
const netLabelWidth = getNetLabelWidth(inputProblem, label)
|
|
36
|
+
const candidates: LabelCandidate[] = []
|
|
37
|
+
const seenCandidateKeys = new Set<string>()
|
|
38
|
+
|
|
39
|
+
for (const pathDistance of candidateDistances) {
|
|
40
|
+
const point = getPointAtTraceDistance(traceLocation.trace, pathDistance)
|
|
41
|
+
const compatibleOrientations = getTraceDistanceCompatibleOrientations(
|
|
42
|
+
traceLocation.trace,
|
|
43
|
+
pathDistance,
|
|
44
|
+
)
|
|
45
|
+
const orientations = getOrientationsForPoint({
|
|
46
|
+
inputProblem,
|
|
47
|
+
label,
|
|
48
|
+
point,
|
|
49
|
+
compatibleOrientations,
|
|
50
|
+
orientationConstraint,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
for (const orientation of orientations) {
|
|
54
|
+
if (isSamePlacement(label, point, orientation)) continue
|
|
55
|
+
const key = [point.x.toFixed(6), point.y.toFixed(6), orientation].join(
|
|
56
|
+
":",
|
|
57
|
+
)
|
|
58
|
+
if (seenCandidateKeys.has(key)) continue
|
|
59
|
+
seenCandidateKeys.add(key)
|
|
60
|
+
|
|
61
|
+
candidates.push(
|
|
62
|
+
createCandidate({
|
|
63
|
+
point,
|
|
64
|
+
orientation,
|
|
65
|
+
netLabelWidth,
|
|
66
|
+
traceLocation,
|
|
67
|
+
pathDistance,
|
|
68
|
+
label,
|
|
69
|
+
}),
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return candidates.sort(
|
|
75
|
+
(a, b) =>
|
|
76
|
+
a.distanceFromOriginal - b.distanceFromOriginal ||
|
|
77
|
+
a.pathDistance - b.pathDistance,
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const getCandidateDistances = (
|
|
82
|
+
traceLocation: TraceLocation,
|
|
83
|
+
traceLength: number,
|
|
84
|
+
vertexDistances: number[],
|
|
85
|
+
orientationConstraint: FacingDirection[] | null,
|
|
86
|
+
) => {
|
|
87
|
+
const distances = new Set<number>()
|
|
88
|
+
const maxSteps = Math.ceil(traceLength / CANDIDATE_STEP)
|
|
89
|
+
|
|
90
|
+
for (let i = 0; i <= maxSteps; i++) {
|
|
91
|
+
const distance = Math.min(traceLength, i * CANDIDATE_STEP)
|
|
92
|
+
distances.add(roundDistance(distance))
|
|
93
|
+
}
|
|
94
|
+
for (const distance of vertexDistances) {
|
|
95
|
+
distances.add(roundDistance(distance))
|
|
96
|
+
}
|
|
97
|
+
distances.add(roundDistance(traceLocation.distance))
|
|
98
|
+
|
|
99
|
+
return [...distances]
|
|
100
|
+
.filter((distance) => distance >= -EPS && distance <= traceLength + EPS)
|
|
101
|
+
.filter((distance) =>
|
|
102
|
+
isDistanceAllowedByOrientationConstraint(
|
|
103
|
+
traceLocation,
|
|
104
|
+
distance,
|
|
105
|
+
orientationConstraint,
|
|
106
|
+
),
|
|
107
|
+
)
|
|
108
|
+
.sort(
|
|
109
|
+
(a, b) =>
|
|
110
|
+
Math.abs(a - traceLocation.distance) -
|
|
111
|
+
Math.abs(b - traceLocation.distance) || a - b,
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const getOrientationsForPoint = (params: {
|
|
116
|
+
inputProblem: InputProblem
|
|
117
|
+
label: NetLabelPlacement
|
|
118
|
+
point: Point
|
|
119
|
+
compatibleOrientations: Set<FacingDirection>
|
|
120
|
+
orientationConstraint: FacingDirection[] | null
|
|
121
|
+
}) => {
|
|
122
|
+
const {
|
|
123
|
+
inputProblem,
|
|
124
|
+
label,
|
|
125
|
+
point,
|
|
126
|
+
compatibleOrientations,
|
|
127
|
+
orientationConstraint,
|
|
128
|
+
} = params
|
|
129
|
+
|
|
130
|
+
if (orientationConstraint) {
|
|
131
|
+
return orientationConstraint.filter((orientation) =>
|
|
132
|
+
compatibleOrientations.has(orientation),
|
|
133
|
+
)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return getUnconstrainedOrientations({
|
|
137
|
+
label,
|
|
138
|
+
inputProblem,
|
|
139
|
+
point,
|
|
140
|
+
compatibleOrientations,
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const createCandidate = (params: {
|
|
145
|
+
point: Point
|
|
146
|
+
orientation: FacingDirection
|
|
147
|
+
netLabelWidth: number
|
|
148
|
+
traceLocation: TraceLocation
|
|
149
|
+
pathDistance: number
|
|
150
|
+
label: NetLabelPlacement
|
|
151
|
+
}): LabelCandidate => {
|
|
152
|
+
const {
|
|
153
|
+
point,
|
|
154
|
+
orientation,
|
|
155
|
+
netLabelWidth,
|
|
156
|
+
traceLocation,
|
|
157
|
+
pathDistance,
|
|
158
|
+
label,
|
|
159
|
+
} = params
|
|
160
|
+
const { width, height } = getDimsForOrientation({
|
|
161
|
+
orientation,
|
|
162
|
+
netLabelWidth,
|
|
163
|
+
})
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
anchorPoint: point,
|
|
167
|
+
center: getCenterFromAnchor(point, orientation, width, height),
|
|
168
|
+
width,
|
|
169
|
+
height,
|
|
170
|
+
orientation,
|
|
171
|
+
traceId: traceLocation.trace.mspPairId,
|
|
172
|
+
pathDistance,
|
|
173
|
+
distanceFromOriginal: getManhattanDistance(point, label.anchorPoint),
|
|
174
|
+
status: "valid",
|
|
175
|
+
selected: false,
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const getOrientationConstraint = (
|
|
180
|
+
inputProblem: InputProblem,
|
|
181
|
+
label: NetLabelPlacement,
|
|
182
|
+
): FacingDirection[] | null => {
|
|
183
|
+
for (const netId of getOrientationConstraintKeys(inputProblem, label)) {
|
|
184
|
+
if (Object.hasOwn(inputProblem.availableNetLabelOrientations, netId)) {
|
|
185
|
+
return inputProblem.availableNetLabelOrientations[netId] ?? []
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const isDistanceAllowedByOrientationConstraint = (
|
|
193
|
+
traceLocation: TraceLocation,
|
|
194
|
+
distance: number,
|
|
195
|
+
constrainedOrientations: FacingDirection[] | null,
|
|
196
|
+
) => {
|
|
197
|
+
if (!constrainedOrientations) return true
|
|
198
|
+
|
|
199
|
+
const compatibleOrientations = getTraceDistanceCompatibleOrientations(
|
|
200
|
+
traceLocation.trace,
|
|
201
|
+
distance,
|
|
202
|
+
)
|
|
203
|
+
return constrainedOrientations.some((orientation) =>
|
|
204
|
+
compatibleOrientations.has(orientation),
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const getOrientationConstraintKeys = (
|
|
209
|
+
inputProblem: InputProblem,
|
|
210
|
+
label: NetLabelPlacement,
|
|
211
|
+
) =>
|
|
212
|
+
dedupeStrings([
|
|
213
|
+
label.netId,
|
|
214
|
+
label.globalConnNetId,
|
|
215
|
+
...inputProblem.netConnections
|
|
216
|
+
.filter((connection) =>
|
|
217
|
+
label.pinIds.some((pinId) => connection.pinIds.includes(pinId)),
|
|
218
|
+
)
|
|
219
|
+
.map((connection) => connection.netId),
|
|
220
|
+
])
|
|
221
|
+
|
|
222
|
+
const getUnconstrainedOrientations = (params: {
|
|
223
|
+
label: NetLabelPlacement
|
|
224
|
+
inputProblem: InputProblem
|
|
225
|
+
point: Point
|
|
226
|
+
compatibleOrientations: Set<FacingDirection>
|
|
227
|
+
}) => {
|
|
228
|
+
const { label, inputProblem, point, compatibleOrientations } = params
|
|
229
|
+
return dedupeOrientations([
|
|
230
|
+
...getInitialOrientations(label, compatibleOrientations),
|
|
231
|
+
...getOutwardOrientations({
|
|
232
|
+
inputProblem,
|
|
233
|
+
point,
|
|
234
|
+
compatibleOrientations,
|
|
235
|
+
}),
|
|
236
|
+
...ALL_ORIENTATIONS.filter((orientation) =>
|
|
237
|
+
compatibleOrientations.has(orientation),
|
|
238
|
+
),
|
|
239
|
+
])
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const getInitialOrientations = (
|
|
243
|
+
label: NetLabelPlacement,
|
|
244
|
+
compatibleOrientations: Set<FacingDirection>,
|
|
245
|
+
) =>
|
|
246
|
+
[label.orientation, getFlippedOrientation(label.orientation)].filter(
|
|
247
|
+
(orientation) => compatibleOrientations.has(orientation),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
const getOutwardOrientations = (params: {
|
|
251
|
+
inputProblem: InputProblem
|
|
252
|
+
point: Point
|
|
253
|
+
compatibleOrientations: Set<FacingDirection>
|
|
254
|
+
}) => {
|
|
255
|
+
const { inputProblem, point, compatibleOrientations } = params
|
|
256
|
+
const chipBounds = getChipBounds(inputProblem)
|
|
257
|
+
|
|
258
|
+
return dedupeOrientations(
|
|
259
|
+
getOutwardOrientationOptions(point, chipBounds, compatibleOrientations)
|
|
260
|
+
.filter(({ orientation }) => compatibleOrientations.has(orientation))
|
|
261
|
+
.sort((a, b) => b.outwardScore - a.outwardScore)
|
|
262
|
+
.map(({ orientation }) => orientation),
|
|
263
|
+
)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const getOutwardOrientationOptions = (
|
|
267
|
+
point: Point,
|
|
268
|
+
chipBounds: ChipBounds,
|
|
269
|
+
compatibleOrientations: Set<FacingDirection>,
|
|
270
|
+
) => {
|
|
271
|
+
const options: OutwardOrientationOption[] = []
|
|
272
|
+
|
|
273
|
+
if (compatibleOrientations.has("y+") || compatibleOrientations.has("y-")) {
|
|
274
|
+
options.push(
|
|
275
|
+
getOutwardAxisOption({
|
|
276
|
+
value: point.y,
|
|
277
|
+
min: chipBounds.minY,
|
|
278
|
+
max: chipBounds.maxY,
|
|
279
|
+
center: chipBounds.centerY,
|
|
280
|
+
positiveOrientation: "y+",
|
|
281
|
+
negativeOrientation: "y-",
|
|
282
|
+
}),
|
|
283
|
+
)
|
|
284
|
+
}
|
|
285
|
+
if (compatibleOrientations.has("x+") || compatibleOrientations.has("x-")) {
|
|
286
|
+
options.push(
|
|
287
|
+
getOutwardAxisOption({
|
|
288
|
+
value: point.x,
|
|
289
|
+
min: chipBounds.minX,
|
|
290
|
+
max: chipBounds.maxX,
|
|
291
|
+
center: chipBounds.centerX,
|
|
292
|
+
positiveOrientation: "x+",
|
|
293
|
+
negativeOrientation: "x-",
|
|
294
|
+
}),
|
|
295
|
+
)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return options
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
type OutwardOrientationOption = {
|
|
302
|
+
orientation: FacingDirection
|
|
303
|
+
outwardScore: number
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const getOutwardAxisOption = (params: {
|
|
307
|
+
value: number
|
|
308
|
+
min: number
|
|
309
|
+
max: number
|
|
310
|
+
center: number
|
|
311
|
+
positiveOrientation: FacingDirection
|
|
312
|
+
negativeOrientation: FacingDirection
|
|
313
|
+
}): OutwardOrientationOption => {
|
|
314
|
+
const { value, min, max, center, positiveOrientation, negativeOrientation } =
|
|
315
|
+
params
|
|
316
|
+
|
|
317
|
+
if (value > max + EPS) {
|
|
318
|
+
return {
|
|
319
|
+
orientation: positiveOrientation,
|
|
320
|
+
outwardScore: 1 + value - max,
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
if (value < min - EPS) {
|
|
324
|
+
return {
|
|
325
|
+
orientation: negativeOrientation,
|
|
326
|
+
outwardScore: 1 + min - value,
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
orientation: value >= center ? positiveOrientation : negativeOrientation,
|
|
332
|
+
outwardScore: getNormalizedCenterDistance(value, center, max - min),
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const getNormalizedCenterDistance = (
|
|
337
|
+
value: number,
|
|
338
|
+
center: number,
|
|
339
|
+
span: number,
|
|
340
|
+
) => Math.abs(value - center) / Math.max(EPS, span / 2)
|
|
341
|
+
|
|
342
|
+
const ALL_ORIENTATIONS: FacingDirection[] = ["x+", "x-", "y+", "y-"]
|
|
343
|
+
|
|
344
|
+
const getFlippedOrientation = (
|
|
345
|
+
orientation: FacingDirection,
|
|
346
|
+
): FacingDirection => {
|
|
347
|
+
switch (orientation) {
|
|
348
|
+
case "x+":
|
|
349
|
+
return "x-"
|
|
350
|
+
case "x-":
|
|
351
|
+
return "x+"
|
|
352
|
+
case "y+":
|
|
353
|
+
return "y-"
|
|
354
|
+
case "y-":
|
|
355
|
+
return "y+"
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
type ChipBounds = Bounds & {
|
|
360
|
+
centerX: number
|
|
361
|
+
centerY: number
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
const getChipBounds = (inputProblem: InputProblem): ChipBounds => {
|
|
365
|
+
if (inputProblem.chips.length === 0) {
|
|
366
|
+
return {
|
|
367
|
+
minX: 0,
|
|
368
|
+
minY: 0,
|
|
369
|
+
maxX: 0,
|
|
370
|
+
maxY: 0,
|
|
371
|
+
centerX: 0,
|
|
372
|
+
centerY: 0,
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const bounds = inputProblem.chips.reduce<Bounds>(
|
|
377
|
+
(acc, chip) => ({
|
|
378
|
+
minX: Math.min(acc.minX, chip.center.x - chip.width / 2),
|
|
379
|
+
minY: Math.min(acc.minY, chip.center.y - chip.height / 2),
|
|
380
|
+
maxX: Math.max(acc.maxX, chip.center.x + chip.width / 2),
|
|
381
|
+
maxY: Math.max(acc.maxY, chip.center.y + chip.height / 2),
|
|
382
|
+
}),
|
|
383
|
+
{
|
|
384
|
+
minX: Number.POSITIVE_INFINITY,
|
|
385
|
+
minY: Number.POSITIVE_INFINITY,
|
|
386
|
+
maxX: Number.NEGATIVE_INFINITY,
|
|
387
|
+
maxY: Number.NEGATIVE_INFINITY,
|
|
388
|
+
},
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
return {
|
|
392
|
+
...bounds,
|
|
393
|
+
centerX: (bounds.minX + bounds.maxX) / 2,
|
|
394
|
+
centerY: (bounds.minY + bounds.maxY) / 2,
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const getNetLabelWidth = (
|
|
399
|
+
inputProblem: InputProblem,
|
|
400
|
+
label: NetLabelPlacement,
|
|
401
|
+
) => {
|
|
402
|
+
const configuredWidth = inputProblem.netConnections.find(
|
|
403
|
+
(connection) => connection.netId === label.netId,
|
|
404
|
+
)?.netLabelWidth
|
|
405
|
+
if (configuredWidth !== undefined) return configuredWidth
|
|
406
|
+
|
|
407
|
+
return label.orientation === "y+" || label.orientation === "y-"
|
|
408
|
+
? label.height
|
|
409
|
+
: label.width
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const roundDistance = (distance: number) => Number(distance.toFixed(6))
|
|
413
|
+
|
|
414
|
+
const dedupeOrientations = (orientations: FacingDirection[]) => [
|
|
415
|
+
...new Set(orientations),
|
|
416
|
+
]
|
|
417
|
+
|
|
418
|
+
const dedupeStrings = (values: Array<string | undefined>) => [
|
|
419
|
+
...new Set(values.filter((value): value is string => value !== undefined)),
|
|
420
|
+
]
|
|
421
|
+
|
|
422
|
+
const isSamePlacement = (
|
|
423
|
+
label: NetLabelPlacement,
|
|
424
|
+
point: Point,
|
|
425
|
+
orientation: FacingDirection,
|
|
426
|
+
) =>
|
|
427
|
+
Math.abs(point.x - label.anchorPoint.x) <= EPS &&
|
|
428
|
+
Math.abs(point.y - label.anchorPoint.y) <= EPS &&
|
|
429
|
+
orientation === label.orientation
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
|
|
3
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
4
|
+
import type { FacingDirection } from "lib/utils/dir"
|
|
5
|
+
import type { Bounds, TraceLocation } from "./types"
|
|
6
|
+
|
|
7
|
+
export const EPS = 1e-6
|
|
8
|
+
export const TRACE_BOUNDARY_TOLERANCE = 1e-6
|
|
9
|
+
|
|
10
|
+
export const getLabelBounds = (label: {
|
|
11
|
+
center: Point
|
|
12
|
+
width: number
|
|
13
|
+
height: number
|
|
14
|
+
}) => getRectBounds(label.center, label.width, label.height)
|
|
15
|
+
|
|
16
|
+
export const rectsOverlap = (a: Bounds, b: Bounds) =>
|
|
17
|
+
Math.min(a.maxX, b.maxX) - Math.max(a.minX, b.minX) > EPS &&
|
|
18
|
+
Math.min(a.maxY, b.maxY) - Math.max(a.minY, b.minY) > EPS
|
|
19
|
+
|
|
20
|
+
export const traceCrossesBoundsInterior = (
|
|
21
|
+
bounds: Bounds,
|
|
22
|
+
traces: SolvedTracePath[],
|
|
23
|
+
) => {
|
|
24
|
+
for (const trace of traces) {
|
|
25
|
+
const points = trace.tracePath
|
|
26
|
+
for (let i = 0; i < points.length - 1; i++) {
|
|
27
|
+
if (segmentCrossesBoundsInterior(points[i]!, points[i + 1]!, bounds)) {
|
|
28
|
+
return true
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const getTraceLocationsForPoint = (
|
|
37
|
+
point: Point,
|
|
38
|
+
traces: SolvedTracePath[],
|
|
39
|
+
) => {
|
|
40
|
+
const locations: TraceLocation[] = []
|
|
41
|
+
|
|
42
|
+
for (const trace of traces) {
|
|
43
|
+
if (getUniquePinCount(trace.pinIds) < 2) continue
|
|
44
|
+
|
|
45
|
+
let pathDistance = 0
|
|
46
|
+
for (let i = 0; i < trace.tracePath.length - 1; i++) {
|
|
47
|
+
const start = trace.tracePath[i]!
|
|
48
|
+
const end = trace.tracePath[i + 1]!
|
|
49
|
+
const segmentLength = getManhattanDistance(start, end)
|
|
50
|
+
|
|
51
|
+
if (isPointOnSegment(point, start, end)) {
|
|
52
|
+
locations.push({
|
|
53
|
+
trace,
|
|
54
|
+
distance: pathDistance + getManhattanDistance(start, point),
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
pathDistance += segmentLength
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return locations
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const getTraceLength = (trace: SolvedTracePath) => {
|
|
66
|
+
let length = 0
|
|
67
|
+
for (let i = 0; i < trace.tracePath.length - 1; i++) {
|
|
68
|
+
length += getManhattanDistance(trace.tracePath[i]!, trace.tracePath[i + 1]!)
|
|
69
|
+
}
|
|
70
|
+
return length
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export const getPointAtTraceDistance = (
|
|
74
|
+
trace: SolvedTracePath,
|
|
75
|
+
distance: number,
|
|
76
|
+
) => {
|
|
77
|
+
let pathDistance = 0
|
|
78
|
+
|
|
79
|
+
for (let i = 0; i < trace.tracePath.length - 1; i++) {
|
|
80
|
+
const start = trace.tracePath[i]!
|
|
81
|
+
const end = trace.tracePath[i + 1]!
|
|
82
|
+
const segmentLength = getManhattanDistance(start, end)
|
|
83
|
+
const nextDistance = pathDistance + segmentLength
|
|
84
|
+
|
|
85
|
+
if (distance <= nextDistance + EPS) {
|
|
86
|
+
const offset = Math.max(
|
|
87
|
+
0,
|
|
88
|
+
Math.min(segmentLength, distance - pathDistance),
|
|
89
|
+
)
|
|
90
|
+
const direction = getSegmentDirection(start, end)
|
|
91
|
+
return {
|
|
92
|
+
x: start.x + direction.x * offset,
|
|
93
|
+
y: start.y + direction.y * offset,
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
pathDistance = nextDistance
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return trace.tracePath[trace.tracePath.length - 1]!
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export const getTraceVertexDistances = (trace: SolvedTracePath) => {
|
|
104
|
+
const distances: number[] = []
|
|
105
|
+
let pathDistance = 0
|
|
106
|
+
|
|
107
|
+
for (let i = 0; i < trace.tracePath.length; i++) {
|
|
108
|
+
distances.push(pathDistance)
|
|
109
|
+
const nextPoint = trace.tracePath[i + 1]
|
|
110
|
+
if (nextPoint) {
|
|
111
|
+
pathDistance += getManhattanDistance(trace.tracePath[i]!, nextPoint)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return distances
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const getTraceDistanceCompatibleOrientations = (
|
|
119
|
+
trace: SolvedTracePath,
|
|
120
|
+
distance: number,
|
|
121
|
+
) => {
|
|
122
|
+
const orientations = new Set<FacingDirection>()
|
|
123
|
+
let pathDistance = 0
|
|
124
|
+
|
|
125
|
+
for (let i = 0; i < trace.tracePath.length - 1; i++) {
|
|
126
|
+
const start = trace.tracePath[i]!
|
|
127
|
+
const end = trace.tracePath[i + 1]!
|
|
128
|
+
const segmentLength = getManhattanDistance(start, end)
|
|
129
|
+
const nextDistance = pathDistance + segmentLength
|
|
130
|
+
|
|
131
|
+
if (distance >= pathDistance - EPS && distance <= nextDistance + EPS) {
|
|
132
|
+
addSegmentCompatibleOrientations(orientations, trace, i)
|
|
133
|
+
if (Math.abs(distance - pathDistance) <= EPS) {
|
|
134
|
+
addSegmentCompatibleOrientations(orientations, trace, i - 1)
|
|
135
|
+
}
|
|
136
|
+
if (Math.abs(distance - nextDistance) <= EPS) {
|
|
137
|
+
addSegmentCompatibleOrientations(orientations, trace, i + 1)
|
|
138
|
+
}
|
|
139
|
+
return orientations
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
pathDistance = nextDistance
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return orientations
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const getManhattanDistance = (a: Point, b: Point) =>
|
|
149
|
+
Math.abs(a.x - b.x) + Math.abs(a.y - b.y)
|
|
150
|
+
|
|
151
|
+
const addSegmentCompatibleOrientations = (
|
|
152
|
+
orientations: Set<FacingDirection>,
|
|
153
|
+
trace: SolvedTracePath,
|
|
154
|
+
segmentIndex: number,
|
|
155
|
+
) => {
|
|
156
|
+
const start = trace.tracePath[segmentIndex]
|
|
157
|
+
const end = trace.tracePath[segmentIndex + 1]
|
|
158
|
+
if (!start || !end) return
|
|
159
|
+
|
|
160
|
+
if (isHorizontal(start, end)) {
|
|
161
|
+
orientations.add("y+")
|
|
162
|
+
orientations.add("y-")
|
|
163
|
+
} else if (isVertical(start, end)) {
|
|
164
|
+
orientations.add("x+")
|
|
165
|
+
orientations.add("x-")
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const segmentCrossesBoundsInterior = (
|
|
170
|
+
start: Point,
|
|
171
|
+
end: Point,
|
|
172
|
+
bounds: Bounds,
|
|
173
|
+
) => {
|
|
174
|
+
const interior = {
|
|
175
|
+
minX: bounds.minX + TRACE_BOUNDARY_TOLERANCE,
|
|
176
|
+
minY: bounds.minY + TRACE_BOUNDARY_TOLERANCE,
|
|
177
|
+
maxX: bounds.maxX - TRACE_BOUNDARY_TOLERANCE,
|
|
178
|
+
maxY: bounds.maxY - TRACE_BOUNDARY_TOLERANCE,
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (interior.minX >= interior.maxX || interior.minY >= interior.maxY) {
|
|
182
|
+
return false
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (isVertical(start, end)) {
|
|
186
|
+
if (start.x <= interior.minX + EPS || start.x >= interior.maxX - EPS) {
|
|
187
|
+
return false
|
|
188
|
+
}
|
|
189
|
+
return rangesOverlap(
|
|
190
|
+
Math.min(start.y, end.y),
|
|
191
|
+
Math.max(start.y, end.y),
|
|
192
|
+
interior.minY,
|
|
193
|
+
interior.maxY,
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (isHorizontal(start, end)) {
|
|
198
|
+
if (start.y <= interior.minY + EPS || start.y >= interior.maxY - EPS) {
|
|
199
|
+
return false
|
|
200
|
+
}
|
|
201
|
+
return rangesOverlap(
|
|
202
|
+
Math.min(start.x, end.x),
|
|
203
|
+
Math.max(start.x, end.x),
|
|
204
|
+
interior.minX,
|
|
205
|
+
interior.maxX,
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return false
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const isPointOnSegment = (point: Point, start: Point, end: Point) => {
|
|
213
|
+
if (isHorizontal(start, end)) {
|
|
214
|
+
return (
|
|
215
|
+
Math.abs(point.y - start.y) <= EPS &&
|
|
216
|
+
point.x >= Math.min(start.x, end.x) - EPS &&
|
|
217
|
+
point.x <= Math.max(start.x, end.x) + EPS
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (isVertical(start, end)) {
|
|
222
|
+
return (
|
|
223
|
+
Math.abs(point.x - start.x) <= EPS &&
|
|
224
|
+
point.y >= Math.min(start.y, end.y) - EPS &&
|
|
225
|
+
point.y <= Math.max(start.y, end.y) + EPS
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return false
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const getSegmentDirection = (start: Point, end: Point) => ({
|
|
233
|
+
x: isVertical(start, end) ? 0 : Math.sign(end.x - start.x),
|
|
234
|
+
y: isHorizontal(start, end) ? 0 : Math.sign(end.y - start.y),
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
const isHorizontal = (start: Point, end: Point) =>
|
|
238
|
+
Math.abs(start.y - end.y) <= EPS
|
|
239
|
+
|
|
240
|
+
const isVertical = (start: Point, end: Point) =>
|
|
241
|
+
Math.abs(start.x - end.x) <= EPS
|
|
242
|
+
|
|
243
|
+
const rangesOverlap = (
|
|
244
|
+
minA: number,
|
|
245
|
+
maxA: number,
|
|
246
|
+
minB: number,
|
|
247
|
+
maxB: number,
|
|
248
|
+
) => Math.min(maxA, maxB) - Math.max(minA, minB) > EPS
|
|
249
|
+
|
|
250
|
+
const getUniquePinCount = (pinIds: string[]) => new Set(pinIds).size
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { 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 type { FacingDirection } from "lib/utils/dir"
|
|
6
|
+
|
|
7
|
+
export interface TraceAnchoredNetLabelOverlapSolverParams {
|
|
8
|
+
inputProblem: InputProblem
|
|
9
|
+
traces: SolvedTracePath[]
|
|
10
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type Bounds = {
|
|
14
|
+
minX: number
|
|
15
|
+
minY: number
|
|
16
|
+
maxX: number
|
|
17
|
+
maxY: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type LabelOverlap = {
|
|
21
|
+
firstLabelIndex: number
|
|
22
|
+
secondLabelIndex: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type TraceLocation = {
|
|
26
|
+
trace: SolvedTracePath
|
|
27
|
+
distance: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type CandidateStatus =
|
|
31
|
+
| "valid"
|
|
32
|
+
| "chip-collision"
|
|
33
|
+
| "trace-collision"
|
|
34
|
+
| "netlabel-collision"
|
|
35
|
+
|
|
36
|
+
export type LabelCandidate = {
|
|
37
|
+
anchorPoint: Point
|
|
38
|
+
center: Point
|
|
39
|
+
width: number
|
|
40
|
+
height: number
|
|
41
|
+
orientation: FacingDirection
|
|
42
|
+
traceId: string
|
|
43
|
+
pathDistance: number
|
|
44
|
+
distanceFromOriginal: number
|
|
45
|
+
status: CandidateStatus
|
|
46
|
+
selected: boolean
|
|
47
|
+
}
|