@tscircuit/schematic-trace-solver 0.0.120 → 0.0.122
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 +10 -3
- package/dist/index.js +350 -78
- package/lib/solvers/RailNetLabelCornerPlacementSolver/RailNetLabelCornerPlacementSolver.ts +223 -21
- package/lib/solvers/RailNetLabelCornerPlacementSolver/types.ts +3 -3
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +34 -9
- package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/evaluateRailGroup.ts +79 -59
- package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getRailAlignmentFallbackCoordinates.ts +96 -0
- package/lib/utils/doesPathCoincideWithTraces.ts +71 -1
- package/package.json +1 -1
- package/site/bug-reports/bug-report-20260804T095800Z.page.tsx +4 -0
- package/tests/bug-reports/bug-report-20260706T213649Z/__snapshots__/bug-report-20260706T213649Z.snap.svg +4 -4
- package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +5 -5
- package/tests/bug-reports/bug-report-20260717T042845Z/__snapshots__/bug-report-20260717T042845Z.snap.svg +12 -12
- package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260728T144234Z/__snapshots__/bug-report-20260728T144234Z.snap.svg +2 -2
- package/tests/bug-reports/bug-report-20260804T095800Z/__snapshots__/bug-report-20260804T095800Z.snap.svg +135 -0
- package/tests/bug-reports/bug-report-20260804T095800Z/bug-report-20260804T095800Z.json +634 -0
- package/tests/bug-reports/bug-report-20260804T095800Z/bug-report-20260804T095800Z.test.ts +12 -0
- package/tests/examples/__snapshots__/example12.snap.svg +2 -2
- package/tests/examples/__snapshots__/example13.snap.svg +4 -4
- package/tests/examples/__snapshots__/example14.snap.svg +36 -36
- package/tests/examples/__snapshots__/example31.snap.svg +6 -6
- package/tests/examples/__snapshots__/example43.snap.svg +2 -2
- package/tests/examples/__snapshots__/example44.snap.svg +2 -2
- package/tests/repros/__snapshots__/board-15984-same-net-junction.snap.svg +2 -2
- package/tests/repros/__snapshots__/bugreport-001-gnd-overlap.snap.svg +26 -26
- package/tests/repros/__snapshots__/netlabel-connector-through-rail-label.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-atmega328p-fault-pullup.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-atmega328p-missing-gnd-netlabel.snap.svg +2 -2
- package/tests/repros/__snapshots__/repro-bq25895-cross-net-trace-overlap.snap.svg +101 -0
- package/tests/repros/__snapshots__/repro-missing-trace-netlabel.snap.svg +18 -18
- package/tests/repros/__snapshots__/repro-netlabel-overlap-trace.snap.svg +3 -3
- package/tests/repros/__snapshots__/repro-nrf52810-clock-routing.snap.svg +82 -0
- package/tests/repros/assets/repro-bq25895-cross-net-trace-overlap.input.json +498 -0
- package/tests/repros/assets/repro-nrf52810-clock-routing.input.json +234 -0
- package/tests/repros/repro-bq25895-cross-net-trace-overlap.test.ts +88 -0
- package/tests/repros/repro-missing-trace-netlabel.test.ts +12 -0
- package/tests/repros/repro-netlabel-overlap-trace.test.ts +38 -0
- package/tests/repros/repro-nrf52810-clock-routing.test.ts +28 -0
- package/tests/solvers/TraceCleanupSolver/alignSameNetRails-trace-clearance.test.ts +68 -0
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import type { GraphicsObject } from "graphics-debug"
|
|
2
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
3
|
import { traceCrossesBoundsInterior } from "lib/solvers/AvailableNetOrientationSolver/geometry"
|
|
3
4
|
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
5
|
+
import { moveAttachedLabelsToReroutedTrace } from "lib/solvers/Example28Solver/labelMovement"
|
|
4
6
|
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
5
7
|
import {
|
|
6
8
|
getCenterFromAnchor,
|
|
7
9
|
getRectBounds,
|
|
8
10
|
} from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
|
|
9
11
|
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
12
|
+
import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
13
|
+
import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
14
|
+
import { simplifyPath } from "lib/solvers/TraceCleanupSolver/simplifyPath"
|
|
10
15
|
import type { InputProblem } from "lib/types/InputProblem"
|
|
16
|
+
import { doesPathOverlapTraceStrokes } from "lib/utils/doesPathCoincideWithTraces"
|
|
11
17
|
import {
|
|
12
18
|
EPS,
|
|
13
19
|
getDistance,
|
|
@@ -95,6 +101,7 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
|
|
|
95
101
|
|
|
96
102
|
getOutput() {
|
|
97
103
|
return {
|
|
104
|
+
traces: this.traces,
|
|
98
105
|
netLabelPlacements: this.outputNetLabelPlacements,
|
|
99
106
|
}
|
|
100
107
|
}
|
|
@@ -163,7 +170,7 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
|
|
|
163
170
|
center,
|
|
164
171
|
width: label.width,
|
|
165
172
|
height: label.height,
|
|
166
|
-
status: this.getCandidateStatus(bounds, labelIndex),
|
|
173
|
+
status: this.getCandidateStatus(bounds, labelIndex, candidate),
|
|
167
174
|
selected: false,
|
|
168
175
|
}
|
|
169
176
|
}
|
|
@@ -171,11 +178,27 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
|
|
|
171
178
|
private getCandidateStatus(
|
|
172
179
|
bounds: Bounds,
|
|
173
180
|
labelIndex: number,
|
|
181
|
+
candidate: TraceCornerCandidate,
|
|
174
182
|
): CornerCandidateStatus {
|
|
175
183
|
if (this.intersectsAnyChip(bounds)) return "chip-collision"
|
|
176
184
|
if (rectIntersectsAnyTextBox(bounds, this.inputProblem))
|
|
177
185
|
return "text-collision"
|
|
178
|
-
|
|
186
|
+
const candidateTraceMap = candidate.reroutedTracePath
|
|
187
|
+
? {
|
|
188
|
+
...this.traceMap,
|
|
189
|
+
[candidate.traceId]: {
|
|
190
|
+
...this.traceMap[candidate.traceId]!,
|
|
191
|
+
tracePath: candidate.reroutedTracePath,
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
: this.traceMap
|
|
195
|
+
if (traceCrossesBoundsInterior(bounds, candidateTraceMap)) {
|
|
196
|
+
return "trace-collision"
|
|
197
|
+
}
|
|
198
|
+
if (
|
|
199
|
+
candidate.reroutedTracePath &&
|
|
200
|
+
!this.isReroutedTraceClear(candidate, labelIndex)
|
|
201
|
+
) {
|
|
179
202
|
return "trace-collision"
|
|
180
203
|
}
|
|
181
204
|
if (this.intersectsAnyOtherNetLabel(bounds, labelIndex)) {
|
|
@@ -190,6 +213,24 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
|
|
|
190
213
|
label: NetLabelPlacement,
|
|
191
214
|
candidate: EvaluatedCornerCandidate,
|
|
192
215
|
) {
|
|
216
|
+
if (candidate.reroutedTracePath) {
|
|
217
|
+
const originalTrace = this.traceMap[candidate.traceId]!
|
|
218
|
+
const reroutedTrace = {
|
|
219
|
+
...originalTrace,
|
|
220
|
+
tracePath: candidate.reroutedTracePath,
|
|
221
|
+
}
|
|
222
|
+
this.traces = this.traces.map((trace) =>
|
|
223
|
+
trace.mspPairId === candidate.traceId ? reroutedTrace : trace,
|
|
224
|
+
)
|
|
225
|
+
this.traceMap[candidate.traceId] = reroutedTrace
|
|
226
|
+
this.outputNetLabelPlacements = moveAttachedLabelsToReroutedTrace({
|
|
227
|
+
trace: originalTrace,
|
|
228
|
+
originalTracePath: originalTrace.tracePath,
|
|
229
|
+
reroutedTracePath: candidate.reroutedTracePath,
|
|
230
|
+
netLabelPlacements: this.outputNetLabelPlacements,
|
|
231
|
+
})
|
|
232
|
+
}
|
|
233
|
+
|
|
193
234
|
this.outputNetLabelPlacements[labelIndex] = {
|
|
194
235
|
...label,
|
|
195
236
|
anchorPoint: candidate.anchorPoint,
|
|
@@ -225,36 +266,197 @@ export class RailNetLabelCornerPlacementSolver extends BaseSolver {
|
|
|
225
266
|
}
|
|
226
267
|
|
|
227
268
|
private getCornerCandidatesForLabel(label: NetLabelPlacement) {
|
|
228
|
-
const
|
|
229
|
-
const
|
|
230
|
-
const
|
|
269
|
+
const anchorAlignedCandidates: TraceCornerCandidate[] = []
|
|
270
|
+
const railAlignedCandidates: TraceCornerCandidate[] = []
|
|
271
|
+
const labelTraces = this.getTraceLinesForLabel(label)
|
|
272
|
+
const allowRailAlignedFallback =
|
|
273
|
+
this.isConfiguredRailLabel(label) &&
|
|
274
|
+
!labelTraces.some((trace) =>
|
|
275
|
+
getTraceCorners(trace.tracePath).some((corner) =>
|
|
276
|
+
this.pointsEqual(corner, label.anchorPoint),
|
|
277
|
+
),
|
|
278
|
+
)
|
|
231
279
|
|
|
232
|
-
for (const trace of
|
|
280
|
+
for (const trace of labelTraces) {
|
|
233
281
|
const path = trace.tracePath
|
|
234
282
|
const pins = [path[0]!, path[path.length - 1]!]
|
|
235
283
|
for (const anchorPoint of getTraceCorners(path)) {
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
// stub off a pin). Non-aligned corners are no better than where the
|
|
245
|
-
// label already is, so they aren't offered as candidates — this keeps
|
|
246
|
-
// already-well-placed labels untouched.
|
|
284
|
+
const cornerIndex = path.indexOf(anchorPoint)
|
|
285
|
+
const anchorAlignedToPin = pins.some(
|
|
286
|
+
(pin) => Math.abs(pin.x - anchorPoint.x) <= EPS,
|
|
287
|
+
)
|
|
288
|
+
const pinAligned =
|
|
289
|
+
anchorAlignedToPin ||
|
|
290
|
+
(allowRailAlignedFallback &&
|
|
291
|
+
this.isPinAlignedCorner(path, cornerIndex))
|
|
247
292
|
if (!pinAligned) continue
|
|
248
|
-
|
|
293
|
+
|
|
294
|
+
const candidates = anchorAlignedToPin
|
|
295
|
+
? anchorAlignedCandidates
|
|
296
|
+
: railAlignedCandidates
|
|
297
|
+
candidates.push(
|
|
298
|
+
{
|
|
299
|
+
anchorPoint,
|
|
300
|
+
traceId: trace.mspPairId,
|
|
301
|
+
distance: getDistance(anchorPoint, label.anchorPoint),
|
|
302
|
+
pinAligned,
|
|
303
|
+
},
|
|
304
|
+
...this.getClearanceShiftedCornerCandidates({
|
|
305
|
+
label,
|
|
306
|
+
trace,
|
|
307
|
+
cornerIndex,
|
|
308
|
+
pinAligned,
|
|
309
|
+
}),
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return [
|
|
315
|
+
...anchorAlignedCandidates.sort((a, b) => a.distance - b.distance),
|
|
316
|
+
...railAlignedCandidates.sort((a, b) => a.distance - b.distance),
|
|
317
|
+
]
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private isConfiguredRailLabel(label: NetLabelPlacement) {
|
|
321
|
+
if (!label.netId) return false
|
|
322
|
+
return this.inputProblem.availableNetLabelOrientations[label.netId]?.some(
|
|
323
|
+
(orientation) => orientation === "y+" || orientation === "y-",
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
private pointsEqual(a: Point, b: Point) {
|
|
328
|
+
return Math.abs(a.x - b.x) <= EPS && Math.abs(a.y - b.y) <= EPS
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private getVerticalSegmentPointIndices(path: Point[], cornerIndex: number) {
|
|
332
|
+
const corner = path[cornerIndex]!
|
|
333
|
+
const previousIsVertical =
|
|
334
|
+
Math.abs(path[cornerIndex - 1]!.x - corner.x) <= EPS
|
|
335
|
+
const nextIsVertical = Math.abs(path[cornerIndex + 1]!.x - corner.x) <= EPS
|
|
336
|
+
if (previousIsVertical === nextIsVertical) return null
|
|
337
|
+
|
|
338
|
+
const indices = previousIsVertical
|
|
339
|
+
? [cornerIndex - 1, cornerIndex]
|
|
340
|
+
: [cornerIndex, cornerIndex + 1]
|
|
341
|
+
return indices
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
private isPinAlignedCorner(path: Point[], cornerIndex: number) {
|
|
345
|
+
const verticalPointIndices = this.getVerticalSegmentPointIndices(
|
|
346
|
+
path,
|
|
347
|
+
cornerIndex,
|
|
348
|
+
)
|
|
349
|
+
if (!verticalPointIndices) return false
|
|
350
|
+
|
|
351
|
+
const [startIndex, endIndex] = verticalPointIndices
|
|
352
|
+
return startIndex! <= 1 || endIndex! >= path.length - 2
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
private getClearanceShiftedCornerCandidates({
|
|
356
|
+
label,
|
|
357
|
+
trace,
|
|
358
|
+
cornerIndex,
|
|
359
|
+
pinAligned,
|
|
360
|
+
}: {
|
|
361
|
+
label: NetLabelPlacement
|
|
362
|
+
trace: SolvedTracePath
|
|
363
|
+
cornerIndex: number
|
|
364
|
+
pinAligned: boolean
|
|
365
|
+
}): TraceCornerCandidate[] {
|
|
366
|
+
if (label.orientation !== "y+" && label.orientation !== "y-") return []
|
|
367
|
+
|
|
368
|
+
const path = trace.tracePath
|
|
369
|
+
const corner = path[cornerIndex]!
|
|
370
|
+
const verticalPointIndices = this.getVerticalSegmentPointIndices(
|
|
371
|
+
path,
|
|
372
|
+
cornerIndex,
|
|
373
|
+
)
|
|
374
|
+
if (
|
|
375
|
+
!verticalPointIndices ||
|
|
376
|
+
verticalPointIndices.includes(0) ||
|
|
377
|
+
verticalPointIndices.includes(path.length - 1)
|
|
378
|
+
) {
|
|
379
|
+
return []
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
const center = getCenterFromAnchor(
|
|
383
|
+
corner,
|
|
384
|
+
label.orientation,
|
|
385
|
+
label.width,
|
|
386
|
+
label.height,
|
|
387
|
+
)
|
|
388
|
+
const labelBounds = getRectBounds(center, label.width, label.height)
|
|
389
|
+
const collidingChipBounds = this.inputProblem.chips
|
|
390
|
+
.map((chip) => getRectBounds(chip.center, chip.width, chip.height))
|
|
391
|
+
.filter((chipBounds) => rectsOverlap(labelBounds, chipBounds))
|
|
392
|
+
const shiftedCoordinates = new Set<number>()
|
|
393
|
+
for (const chipBounds of collidingChipBounds) {
|
|
394
|
+
shiftedCoordinates.add(chipBounds.minX - label.width / 2)
|
|
395
|
+
shiftedCoordinates.add(chipBounds.maxX + label.width / 2)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return [...shiftedCoordinates].flatMap((x) => {
|
|
399
|
+
if (Math.abs(x - corner.x) <= EPS) return []
|
|
400
|
+
const reroutedTracePath = simplifyPath(
|
|
401
|
+
path.map((point, pointIndex) =>
|
|
402
|
+
verticalPointIndices.includes(pointIndex) ? { ...point, x } : point,
|
|
403
|
+
),
|
|
404
|
+
)
|
|
405
|
+
const anchorPoint = { x, y: corner.y }
|
|
406
|
+
const remainsCorner = getTraceCorners(reroutedTracePath).some((point) =>
|
|
407
|
+
this.pointsEqual(point, anchorPoint),
|
|
408
|
+
)
|
|
409
|
+
if (!remainsCorner) return []
|
|
410
|
+
|
|
411
|
+
return [
|
|
412
|
+
{
|
|
249
413
|
anchorPoint,
|
|
250
414
|
traceId: trace.mspPairId,
|
|
251
415
|
distance: getDistance(anchorPoint, label.anchorPoint),
|
|
252
416
|
pinAligned,
|
|
253
|
-
|
|
254
|
-
|
|
417
|
+
reroutedTracePath,
|
|
418
|
+
},
|
|
419
|
+
]
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
private isReroutedTraceClear(
|
|
424
|
+
candidate: TraceCornerCandidate,
|
|
425
|
+
labelIndex: number,
|
|
426
|
+
) {
|
|
427
|
+
const reroutedTracePath = candidate.reroutedTracePath!
|
|
428
|
+
if (
|
|
429
|
+
isPathCollidingWithObstacles(
|
|
430
|
+
reroutedTracePath,
|
|
431
|
+
getObstacleRects(this.inputProblem),
|
|
432
|
+
)
|
|
433
|
+
) {
|
|
434
|
+
return false
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const candidateTrace = this.traceMap[candidate.traceId]!
|
|
438
|
+
const otherNetTraces = this.traces.filter(
|
|
439
|
+
(trace) =>
|
|
440
|
+
trace.mspPairId !== candidate.traceId &&
|
|
441
|
+
trace.globalConnNetId !== candidateTrace.globalConnNetId,
|
|
442
|
+
)
|
|
443
|
+
if (doesPathOverlapTraceStrokes(reroutedTracePath, otherNetTraces)) {
|
|
444
|
+
return false
|
|
255
445
|
}
|
|
256
446
|
|
|
257
|
-
|
|
447
|
+
const reroutedTraceMap = {
|
|
448
|
+
[candidate.traceId]: {
|
|
449
|
+
...candidateTrace,
|
|
450
|
+
tracePath: reroutedTracePath,
|
|
451
|
+
},
|
|
452
|
+
}
|
|
453
|
+
return this.outputNetLabelPlacements.every((label, index) => {
|
|
454
|
+
if (index === labelIndex) return true
|
|
455
|
+
return !traceCrossesBoundsInterior(
|
|
456
|
+
getRectBounds(label.center, label.width, label.height),
|
|
457
|
+
reroutedTraceMap,
|
|
458
|
+
)
|
|
459
|
+
})
|
|
258
460
|
}
|
|
259
461
|
|
|
260
462
|
private getTraceLinesForLabel(label: NetLabelPlacement) {
|
|
@@ -21,11 +21,11 @@ export type TraceCornerCandidate = {
|
|
|
21
21
|
traceId: string
|
|
22
22
|
distance: number
|
|
23
23
|
/**
|
|
24
|
-
* True when the corner
|
|
25
|
-
*
|
|
26
|
-
* a clean stub off that pin rather than floating over a mid-trace bend.
|
|
24
|
+
* True when the corner is on a vertical rail that connects directly to one
|
|
25
|
+
* of the trace's endpoint pins, rather than on an arbitrary middle bend.
|
|
27
26
|
*/
|
|
28
27
|
pinAligned: boolean
|
|
28
|
+
reroutedTracePath?: Point[]
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export type CornerCandidateStatus =
|
|
@@ -22,6 +22,7 @@ import { LongDistancePairSolver } from "../LongDistancePairSolver/LongDistancePa
|
|
|
22
22
|
import { MergedNetLabelObstacleSolver } from "../TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/LabelMergingSolver"
|
|
23
23
|
import { TraceCleanupSolver } from "../TraceCleanupSolver/TraceCleanupSolver"
|
|
24
24
|
import { Example28Solver } from "../Example28Solver/Example28Solver"
|
|
25
|
+
import { moveAttachedLabelsToReroutedTrace } from "../Example28Solver/labelMovement"
|
|
25
26
|
import { AvailableNetOrientationSolver } from "../AvailableNetOrientationSolver/AvailableNetOrientationSolver"
|
|
26
27
|
import { RailNetLabelCornerPlacementSolver } from "../RailNetLabelCornerPlacementSolver/RailNetLabelCornerPlacementSolver"
|
|
27
28
|
import { TraceAnchoredNetLabelOverlapSolver } from "../TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver"
|
|
@@ -325,9 +326,8 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
325
326
|
(instance) => [
|
|
326
327
|
{
|
|
327
328
|
inputProblem: instance.inputProblem,
|
|
328
|
-
traces:
|
|
329
|
-
instance.
|
|
330
|
-
),
|
|
329
|
+
traces:
|
|
330
|
+
instance.railNetLabelCornerPlacementSolver!.getOutput().traces,
|
|
331
331
|
netLabelPlacements:
|
|
332
332
|
instance.railNetLabelCornerPlacementSolver!
|
|
333
333
|
.outputNetLabelPlacements,
|
|
@@ -340,9 +340,8 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
340
340
|
(instance) => [
|
|
341
341
|
{
|
|
342
342
|
inputProblem: instance.inputProblem,
|
|
343
|
-
traces:
|
|
344
|
-
instance.
|
|
345
|
-
),
|
|
343
|
+
traces:
|
|
344
|
+
instance.railNetLabelCornerPlacementSolver!.getOutput().traces,
|
|
346
345
|
netLabelPlacements:
|
|
347
346
|
instance.traceAnchoredNetLabelOverlapSolver!
|
|
348
347
|
.outputNetLabelPlacements,
|
|
@@ -379,14 +378,40 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
379
378
|
"netLabelTraceCollisionSolver",
|
|
380
379
|
NetLabelTraceCollisionSolver,
|
|
381
380
|
(instance) => {
|
|
382
|
-
const
|
|
381
|
+
const previousOutput =
|
|
383
382
|
instance.preAlignmentNetLabelTraceCollisionSolver!.getOutput()
|
|
383
|
+
const alignmentOutput = instance.traceCleanupSolver2!.getOutput()
|
|
384
|
+
const previousTraceMap = new Map(
|
|
385
|
+
previousOutput.traces.map((trace) => [trace.mspPairId, trace]),
|
|
386
|
+
)
|
|
387
|
+
let netLabelPlacements = previousOutput.netLabelPlacements
|
|
388
|
+
|
|
389
|
+
for (const trace of alignmentOutput.traces) {
|
|
390
|
+
const previousTrace = previousTraceMap.get(trace.mspPairId)
|
|
391
|
+
if (!previousTrace || previousTrace.tracePath === trace.tracePath) {
|
|
392
|
+
continue
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const currentLabels = netLabelPlacements
|
|
396
|
+
const movedLabels = moveAttachedLabelsToReroutedTrace({
|
|
397
|
+
trace: previousTrace,
|
|
398
|
+
originalTracePath: previousTrace.tracePath,
|
|
399
|
+
reroutedTracePath: trace.tracePath,
|
|
400
|
+
netLabelPlacements: currentLabels,
|
|
401
|
+
})
|
|
402
|
+
netLabelPlacements = movedLabels.map((movedLabel, labelIndex) => {
|
|
403
|
+
const label = currentLabels[labelIndex]!
|
|
404
|
+
return label.orientation === "y+" || label.orientation === "y-"
|
|
405
|
+
? movedLabel
|
|
406
|
+
: label
|
|
407
|
+
})
|
|
408
|
+
}
|
|
384
409
|
|
|
385
410
|
return [
|
|
386
411
|
{
|
|
387
412
|
inputProblem: instance.inputProblem,
|
|
388
|
-
traces:
|
|
389
|
-
netLabelPlacements
|
|
413
|
+
traces: alignmentOutput.traces,
|
|
414
|
+
netLabelPlacements,
|
|
390
415
|
},
|
|
391
416
|
]
|
|
392
417
|
},
|
|
@@ -3,8 +3,12 @@ import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/Sche
|
|
|
3
3
|
import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
4
4
|
import type { ObstacleRect } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
5
5
|
import { detectTraceLabelOverlap } from "lib/solvers/TraceLabelOverlapAvoidanceSolver/detectTraceLabelOverlap"
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
doesPathCoincideWithTraces,
|
|
8
|
+
doesPathOverlapTraceStrokes,
|
|
9
|
+
} from "lib/utils/doesPathCoincideWithTraces"
|
|
7
10
|
import { getDistinctCoordinates, pointsEqual } from "./geometry"
|
|
11
|
+
import { getRailAlignmentFallbackCoordinates } from "./getRailAlignmentFallbackCoordinates"
|
|
8
12
|
import { moveRailSegments } from "./moveRailSegments"
|
|
9
13
|
import { preservesLabelAnchors } from "./preservesLabelAnchors"
|
|
10
14
|
import {
|
|
@@ -43,7 +47,7 @@ export const evaluateRailGroup = ({
|
|
|
43
47
|
groupTraceIds.has(trace.mspPairId),
|
|
44
48
|
)
|
|
45
49
|
const baseline = getTraceGeometryMetrics(originalGroupTraces, traces)
|
|
46
|
-
const
|
|
50
|
+
const originalCoordinates = getDistinctCoordinates(
|
|
47
51
|
group.map((segment) => segment.coordinate),
|
|
48
52
|
)
|
|
49
53
|
const otherNetTraces = traces.filter(
|
|
@@ -55,70 +59,86 @@ export const evaluateRailGroup = ({
|
|
|
55
59
|
!eligibleTraceIds.has(trace.mspPairId),
|
|
56
60
|
)
|
|
57
61
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
62
|
+
const evaluateCoordinates = (coordinates: number[]) => {
|
|
63
|
+
let best: AlignmentCandidate | null = null
|
|
64
|
+
for (const coordinate of coordinates) {
|
|
65
|
+
const candidateMap = new Map<string, SolvedTracePath>()
|
|
61
66
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
for (const trace of originalGroupTraces) {
|
|
68
|
+
const candidateTrace = moveRailSegments(
|
|
69
|
+
trace,
|
|
70
|
+
group.filter((segment) => segment.traceId === trace.mspPairId),
|
|
71
|
+
coordinate,
|
|
72
|
+
)
|
|
73
|
+
candidateMap.set(trace.mspPairId, candidateTrace)
|
|
74
|
+
}
|
|
70
75
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
76
|
+
const candidateTraces = [...candidateMap.values()]
|
|
77
|
+
const allCandidateTraces = traces.map(
|
|
78
|
+
(trace) => candidateMap.get(trace.mspPairId) ?? trace,
|
|
79
|
+
)
|
|
80
|
+
const candidatesAreClear = candidateTraces.every(
|
|
81
|
+
(candidate) =>
|
|
82
|
+
!isPathCollidingWithObstacles(candidate.tracePath, obstacles) &&
|
|
83
|
+
detectTraceLabelOverlap({
|
|
84
|
+
traces: [candidate],
|
|
85
|
+
netLabels: netLabelPlacements,
|
|
86
|
+
}).length === 0 &&
|
|
87
|
+
!doesPathOverlapTraceStrokes(candidate.tracePath, otherNetTraces) &&
|
|
88
|
+
!doesPathCoincideWithTraces(
|
|
89
|
+
candidate.tracePath,
|
|
90
|
+
immutableSameNetTraces.filter(
|
|
91
|
+
(trace) => trace.mspPairId !== candidate.mspPairId,
|
|
92
|
+
),
|
|
87
93
|
),
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
}
|
|
94
|
+
)
|
|
95
|
+
if (!candidatesAreClear) continue
|
|
96
|
+
if (
|
|
97
|
+
!preservesLabelAnchors(netLabelPlacements, traces, allCandidateTraces)
|
|
98
|
+
) {
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
96
101
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
const metrics = getTraceGeometryMetrics(
|
|
103
|
+
candidateTraces,
|
|
104
|
+
allCandidateTraces,
|
|
105
|
+
)
|
|
106
|
+
if (metrics.otherNetCrossings > baseline.otherNetCrossings) continue
|
|
107
|
+
if (!isReadabilityImprovement(metrics, baseline)) continue
|
|
100
108
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
109
|
+
const score: AlignmentScore = {
|
|
110
|
+
...metrics,
|
|
111
|
+
displacement: group.reduce(
|
|
112
|
+
(sum, segment) => sum + Math.abs(segment.coordinate - coordinate),
|
|
113
|
+
0,
|
|
114
|
+
),
|
|
115
|
+
coordinate,
|
|
116
|
+
}
|
|
117
|
+
const changedTraceIds = candidateTraces
|
|
118
|
+
.filter((candidate) => {
|
|
119
|
+
const original = traces.find(
|
|
120
|
+
(trace) => trace.mspPairId === candidate.mspPairId,
|
|
121
|
+
)!
|
|
122
|
+
return tracePathChanged(original, candidate)
|
|
123
|
+
})
|
|
124
|
+
.map((trace) => trace.mspPairId)
|
|
125
|
+
if (changedTraceIds.length === 0) continue
|
|
126
|
+
|
|
127
|
+
const candidate = { traces: allCandidateTraces, changedTraceIds, score }
|
|
128
|
+
if (!best || scoreIsBetter(candidate.score, best.score)) best = candidate
|
|
108
129
|
}
|
|
109
|
-
const changedTraceIds = candidateTraces
|
|
110
|
-
.filter((candidate) => {
|
|
111
|
-
const original = traces.find(
|
|
112
|
-
(trace) => trace.mspPairId === candidate.mspPairId,
|
|
113
|
-
)!
|
|
114
|
-
return tracePathChanged(original, candidate)
|
|
115
|
-
})
|
|
116
|
-
.map((trace) => trace.mspPairId)
|
|
117
|
-
if (changedTraceIds.length === 0) continue
|
|
118
130
|
|
|
119
|
-
|
|
120
|
-
if (!best || scoreIsBetter(candidate.score, best.score)) best = candidate
|
|
131
|
+
return best
|
|
121
132
|
}
|
|
122
133
|
|
|
123
|
-
|
|
134
|
+
const originalCandidate = evaluateCoordinates(originalCoordinates)
|
|
135
|
+
if (originalCandidate) return originalCandidate
|
|
136
|
+
|
|
137
|
+
return evaluateCoordinates(
|
|
138
|
+
getRailAlignmentFallbackCoordinates({
|
|
139
|
+
group,
|
|
140
|
+
originalCoordinates,
|
|
141
|
+
otherNetTraces,
|
|
142
|
+
}),
|
|
143
|
+
)
|
|
124
144
|
}
|
package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/getRailAlignmentFallbackCoordinates.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { boundsIntersection, type Bounds } from "@tscircuit/math-utils"
|
|
2
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
3
|
+
import {
|
|
4
|
+
SCHEMATIC_TRACE_MIN_CENTERLINE_CLEARANCE,
|
|
5
|
+
SCHEMATIC_TRACE_MIN_VISUAL_CENTERLINE_CLEARANCE,
|
|
6
|
+
} from "lib/utils/doesPathCoincideWithTraces"
|
|
7
|
+
import {
|
|
8
|
+
getDistinctCoordinates,
|
|
9
|
+
getRailOrientation,
|
|
10
|
+
RAIL_ALIGNMENT_EPSILON,
|
|
11
|
+
} from "./geometry"
|
|
12
|
+
import type { RailOrientation, RailSegment } from "./types"
|
|
13
|
+
|
|
14
|
+
const getAlongBounds = (
|
|
15
|
+
orientation: RailOrientation,
|
|
16
|
+
minAlong: number,
|
|
17
|
+
maxAlong: number,
|
|
18
|
+
): Bounds =>
|
|
19
|
+
orientation === "vertical"
|
|
20
|
+
? { minX: 0, maxX: 0, minY: minAlong, maxY: maxAlong }
|
|
21
|
+
: { minX: minAlong, maxX: maxAlong, minY: 0, maxY: 0 }
|
|
22
|
+
|
|
23
|
+
const hasPositiveAlongOverlap = (
|
|
24
|
+
orientation: RailOrientation,
|
|
25
|
+
first: Bounds,
|
|
26
|
+
second: Bounds,
|
|
27
|
+
) => {
|
|
28
|
+
const overlap = boundsIntersection(first, second)
|
|
29
|
+
if (!overlap) return false
|
|
30
|
+
const overlapLength =
|
|
31
|
+
orientation === "vertical"
|
|
32
|
+
? overlap.maxY - overlap.minY
|
|
33
|
+
: overlap.maxX - overlap.minX
|
|
34
|
+
return overlapLength > RAIL_ALIGNMENT_EPSILON
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Derives alternatives only for original alignment coordinates whose rendered
|
|
39
|
+
* stroke touches a parallel segment from another net.
|
|
40
|
+
*/
|
|
41
|
+
export const getRailAlignmentFallbackCoordinates = ({
|
|
42
|
+
group,
|
|
43
|
+
originalCoordinates,
|
|
44
|
+
otherNetTraces,
|
|
45
|
+
}: {
|
|
46
|
+
group: RailSegment[]
|
|
47
|
+
originalCoordinates: number[]
|
|
48
|
+
otherNetTraces: SolvedTracePath[]
|
|
49
|
+
}) => {
|
|
50
|
+
const orientation = group[0]!.orientation
|
|
51
|
+
const groupAlongBounds = group.map((segment) =>
|
|
52
|
+
getAlongBounds(orientation, segment.minAlong, segment.maxAlong),
|
|
53
|
+
)
|
|
54
|
+
const fallbackClearance = SCHEMATIC_TRACE_MIN_VISUAL_CENTERLINE_CLEARANCE
|
|
55
|
+
const coordinates: number[] = []
|
|
56
|
+
|
|
57
|
+
for (const trace of otherNetTraces) {
|
|
58
|
+
for (let index = 0; index < trace.tracePath.length - 1; index++) {
|
|
59
|
+
const start = trace.tracePath[index]!
|
|
60
|
+
const end = trace.tracePath[index + 1]!
|
|
61
|
+
if (getRailOrientation(start, end) !== orientation) continue
|
|
62
|
+
|
|
63
|
+
const minAlong =
|
|
64
|
+
orientation === "vertical"
|
|
65
|
+
? Math.min(start.y, end.y)
|
|
66
|
+
: Math.min(start.x, end.x)
|
|
67
|
+
const maxAlong =
|
|
68
|
+
orientation === "vertical"
|
|
69
|
+
? Math.max(start.y, end.y)
|
|
70
|
+
: Math.max(start.x, end.x)
|
|
71
|
+
const traceAlongBounds = getAlongBounds(orientation, minAlong, maxAlong)
|
|
72
|
+
if (
|
|
73
|
+
!groupAlongBounds.some((bounds) =>
|
|
74
|
+
hasPositiveAlongOverlap(orientation, bounds, traceAlongBounds),
|
|
75
|
+
)
|
|
76
|
+
) {
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const coordinate = orientation === "vertical" ? start.x : start.y
|
|
81
|
+
const touchesOriginalCandidate = originalCoordinates.some(
|
|
82
|
+
(originalCoordinate) =>
|
|
83
|
+
Math.abs(originalCoordinate - coordinate) <=
|
|
84
|
+
SCHEMATIC_TRACE_MIN_CENTERLINE_CLEARANCE,
|
|
85
|
+
)
|
|
86
|
+
if (!touchesOriginalCandidate) continue
|
|
87
|
+
|
|
88
|
+
coordinates.push(
|
|
89
|
+
coordinate - fallbackClearance,
|
|
90
|
+
coordinate + fallbackClearance,
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return getDistinctCoordinates(coordinates)
|
|
96
|
+
}
|