@tscircuit/schematic-trace-solver 0.0.137 → 0.0.140
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/.github/workflows/bun-pver-release.yml +7 -3
- package/dist/index.d.ts +2 -0
- package/dist/index.js +261 -32
- package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationObstacleIndex.ts +182 -0
- package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +67 -25
- package/lib/solvers/AvailableNetOrientationSolver/constants.ts +2 -0
- package/lib/solvers/Example28Solver/labelMovement.ts +71 -0
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +12 -4
- package/package.json +5 -1
- package/tests/bug-reports/bug-report-20260819T091818Z/__snapshots__/bug-report-20260819T091818Z.snap.svg +237 -0
- package/tests/bug-reports/bug-report-20260819T091818Z/bug-report-20260819T091818Z.json +1052 -0
- package/tests/bug-reports/bug-report-20260819T091818Z/bug-report-20260819T091818Z.test.ts +33 -0
- package/tests/repros/__snapshots__/repro-board-648-esp12f-section.snap.svg +124 -0
- package/tests/repros/assets/repro-board-648-esp12f-section.input.json +537 -0
- package/tests/repros/repro-board-648-esp12f-section.test.ts +13 -0
- package/tests/repros/repro161-power-section-autolayout.test.ts +7 -0
- package/tests/solvers/AvailableNetOrientationSolver/AvailableNetOrientationObstacleIndex.test.ts +122 -0
|
@@ -17,7 +17,12 @@ import type {
|
|
|
17
17
|
} from "lib/types/InputProblem"
|
|
18
18
|
import { dir, type FacingDirection } from "lib/utils/dir"
|
|
19
19
|
import { rectIntersectsAnyTextBox } from "lib/utils/textBoxBounds"
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
EPS,
|
|
22
|
+
LABEL_SEARCH_STEP,
|
|
23
|
+
MAX_RECORDED_CANDIDATES,
|
|
24
|
+
WICK_CLEARANCE,
|
|
25
|
+
} from "./constants"
|
|
21
26
|
import {
|
|
22
27
|
getConnectorTracePath,
|
|
23
28
|
getMaxSearchDistance,
|
|
@@ -27,8 +32,6 @@ import {
|
|
|
27
32
|
rangesOverlap,
|
|
28
33
|
rectsOverlap,
|
|
29
34
|
simplifyOrthogonalPath,
|
|
30
|
-
traceCrossesBoundsInterior,
|
|
31
|
-
tracePathCrossesAnyBounds,
|
|
32
35
|
tracePathIntersectsBounds,
|
|
33
36
|
} from "./geometry"
|
|
34
37
|
import { orderRoutedLabelsBeforeOverlappingPortLabels } from "./orderRoutedLabelsBeforeOverlappingPortLabels"
|
|
@@ -43,6 +46,7 @@ import type {
|
|
|
43
46
|
EvaluatedCandidate,
|
|
44
47
|
} from "./types"
|
|
45
48
|
import { visualizeAvailableNetOrientationSolver } from "./visualize"
|
|
49
|
+
import { AvailableNetOrientationObstacleIndex } from "./AvailableNetOrientationObstacleIndex"
|
|
46
50
|
|
|
47
51
|
const LABEL_TRACE_CLEARANCE = 0.1
|
|
48
52
|
|
|
@@ -64,6 +68,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
64
68
|
private chipObstacleSpatialIndex: ChipObstacleSpatialIndex
|
|
65
69
|
private maxSearchDistance: number
|
|
66
70
|
private pinMap: Record<string, InputPin & { chipId: string }>
|
|
71
|
+
private obstacleIndex: AvailableNetOrientationObstacleIndex
|
|
67
72
|
|
|
68
73
|
constructor(params: AvailableNetOrientationSolverParams) {
|
|
69
74
|
super()
|
|
@@ -79,6 +84,11 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
79
84
|
params.inputProblem._chipObstacleSpatialIndex ??
|
|
80
85
|
new ChipObstacleSpatialIndex(params.inputProblem.chips)
|
|
81
86
|
this.maxSearchDistance = getMaxSearchDistance(params.inputProblem)
|
|
87
|
+
this.obstacleIndex = new AvailableNetOrientationObstacleIndex({
|
|
88
|
+
chipObstacleSpatialIndex: this.chipObstacleSpatialIndex,
|
|
89
|
+
netLabelPlacements: this.outputNetLabelPlacements,
|
|
90
|
+
traces: this.traces,
|
|
91
|
+
})
|
|
82
92
|
this.crowdedPortOnlyLabelIndices = this.getCrowdedPortOnlyLabelIndices()
|
|
83
93
|
this.queuedLabelIndices = this.getProcessableLabelIndices()
|
|
84
94
|
this.setCurrentLabel(this.queuedLabelIndices[0] ?? null)
|
|
@@ -309,6 +319,10 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
309
319
|
...toNetLabelPlacementPatch(candidate),
|
|
310
320
|
}
|
|
311
321
|
this.addConnectorTrace(label, candidate, labelIndex)
|
|
322
|
+
this.obstacleIndex.rebuild({
|
|
323
|
+
netLabelPlacements: this.outputNetLabelPlacements,
|
|
324
|
+
traces: this.traces,
|
|
325
|
+
})
|
|
312
326
|
}
|
|
313
327
|
|
|
314
328
|
private addConnectorTrace(
|
|
@@ -553,7 +567,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
553
567
|
anchorY - label.anchorPoint.y,
|
|
554
568
|
anchorX - label.anchorPoint.x,
|
|
555
569
|
)
|
|
556
|
-
this.
|
|
570
|
+
this.recordCandidateResult(result)
|
|
557
571
|
if (result.status !== "valid") continue
|
|
558
572
|
|
|
559
573
|
result.selected = true
|
|
@@ -605,7 +619,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
605
619
|
labelIndex,
|
|
606
620
|
"rotate",
|
|
607
621
|
)
|
|
608
|
-
this.
|
|
622
|
+
this.recordCandidateResult(result)
|
|
609
623
|
if (result.status === "valid") {
|
|
610
624
|
result.selected = true
|
|
611
625
|
return result
|
|
@@ -637,7 +651,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
637
651
|
labelIndex,
|
|
638
652
|
"trace-anchor",
|
|
639
653
|
)
|
|
640
|
-
this.
|
|
654
|
+
this.recordCandidateResult(result)
|
|
641
655
|
|
|
642
656
|
if (result.status === "valid") {
|
|
643
657
|
result.selected = true
|
|
@@ -682,7 +696,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
682
696
|
labelIndex,
|
|
683
697
|
"outward-trace-anchor",
|
|
684
698
|
)
|
|
685
|
-
this.
|
|
699
|
+
this.recordCandidateResult(preservedColumnResult)
|
|
686
700
|
if (preservedColumnResult.status === "valid") {
|
|
687
701
|
preservedColumnResult.selected = true
|
|
688
702
|
return preservedColumnResult
|
|
@@ -716,7 +730,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
716
730
|
"outward-trace-anchor",
|
|
717
731
|
distance,
|
|
718
732
|
)
|
|
719
|
-
this.
|
|
733
|
+
this.recordCandidateResult(result)
|
|
720
734
|
|
|
721
735
|
if (result.status === "valid") {
|
|
722
736
|
result.selected = true
|
|
@@ -891,7 +905,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
891
905
|
distance,
|
|
892
906
|
outwardDistance,
|
|
893
907
|
)
|
|
894
|
-
this.
|
|
908
|
+
this.recordCandidateResult(result)
|
|
895
909
|
|
|
896
910
|
if (result.status === "valid") {
|
|
897
911
|
result.selected = true
|
|
@@ -926,6 +940,19 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
926
940
|
}
|
|
927
941
|
}
|
|
928
942
|
|
|
943
|
+
private recordCandidateResult(candidate: EvaluatedCandidate) {
|
|
944
|
+
this.stats.candidateEvaluations = (this.stats.candidateEvaluations ?? 0) + 1
|
|
945
|
+
if (this.currentCandidateResults.length < MAX_RECORDED_CANDIDATES) {
|
|
946
|
+
this.currentCandidateResults.push(candidate)
|
|
947
|
+
} else if (candidate.status === "valid") {
|
|
948
|
+
this.currentCandidateResults[MAX_RECORDED_CANDIDATES - 1] = candidate
|
|
949
|
+
}
|
|
950
|
+
this.stats.maxRecordedCandidates = Math.max(
|
|
951
|
+
this.stats.maxRecordedCandidates ?? 0,
|
|
952
|
+
this.currentCandidateResults.length,
|
|
953
|
+
)
|
|
954
|
+
}
|
|
955
|
+
|
|
929
956
|
private getCandidateConnectorTrace(
|
|
930
957
|
label: NetLabelPlacement,
|
|
931
958
|
candidate: Pick<
|
|
@@ -1269,13 +1296,13 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
1269
1296
|
labelIndex,
|
|
1270
1297
|
)
|
|
1271
1298
|
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
return "chip-collision"
|
|
1275
|
-
}
|
|
1299
|
+
if (this.obstacleIndex.doesTracePathCrossChip(connectorTrace)) {
|
|
1300
|
+
return "chip-collision"
|
|
1276
1301
|
}
|
|
1277
1302
|
|
|
1278
|
-
for (
|
|
1303
|
+
for (const i of this.obstacleIndex.getLabelIndicesNearTracePath(
|
|
1304
|
+
connectorTrace,
|
|
1305
|
+
)) {
|
|
1279
1306
|
if (i === labelIndex) continue
|
|
1280
1307
|
if (this.shouldIgnorePendingCrowdedLabel(labelIndex, i)) continue
|
|
1281
1308
|
const otherLabel = this.outputNetLabelPlacements[i]!
|
|
@@ -1345,7 +1372,7 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
1345
1372
|
if (rectIntersectsAnyTextBox(bounds, this.inputProblem)) {
|
|
1346
1373
|
return "text-collision"
|
|
1347
1374
|
}
|
|
1348
|
-
if (
|
|
1375
|
+
if (this.obstacleIndex.doesTraceCrossBoundsInterior(bounds)) {
|
|
1349
1376
|
return "trace-collision"
|
|
1350
1377
|
}
|
|
1351
1378
|
if (this.isTraceTooCloseToLabel(bounds, label)) {
|
|
@@ -1361,14 +1388,10 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
1361
1388
|
if (!this.shouldCheckTraceClearanceForLabel(label)) return false
|
|
1362
1389
|
|
|
1363
1390
|
const clearanceBounds = this.getLabelTraceClearanceBounds(bounds)
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
return false
|
|
1391
|
+
return this.obstacleIndex.doesTraceIntersectBounds({
|
|
1392
|
+
bounds: clearanceBounds,
|
|
1393
|
+
excludedGlobalConnNetId: label.globalConnNetId,
|
|
1394
|
+
})
|
|
1372
1395
|
}
|
|
1373
1396
|
|
|
1374
1397
|
private getLabelTraceClearanceBounds(bounds: Bounds): Bounds {
|
|
@@ -1496,7 +1519,17 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
1496
1519
|
}
|
|
1497
1520
|
|
|
1498
1521
|
private intersectsAnyOtherNetLabel(bounds: Bounds, labelIndex: number) {
|
|
1499
|
-
|
|
1522
|
+
const searchBounds = {
|
|
1523
|
+
minX: bounds.minX - LABEL_SEARCH_STEP,
|
|
1524
|
+
minY: bounds.minY - LABEL_SEARCH_STEP,
|
|
1525
|
+
maxX: bounds.maxX + LABEL_SEARCH_STEP,
|
|
1526
|
+
maxY: bounds.maxY + LABEL_SEARCH_STEP,
|
|
1527
|
+
}
|
|
1528
|
+
const nearbyLabelIndices = this.obstacleIndex
|
|
1529
|
+
.getLabelIndicesInBounds(searchBounds)
|
|
1530
|
+
.sort((a, b) => a - b)
|
|
1531
|
+
|
|
1532
|
+
for (const i of nearbyLabelIndices) {
|
|
1500
1533
|
if (i === labelIndex) continue
|
|
1501
1534
|
if (this.shouldIgnorePendingCrowdedLabel(labelIndex, i)) continue
|
|
1502
1535
|
const label = this.outputNetLabelPlacements[i]!
|
|
@@ -1515,7 +1548,16 @@ export class AvailableNetOrientationSolver extends BaseSolver {
|
|
|
1515
1548
|
}
|
|
1516
1549
|
|
|
1517
1550
|
private sharesChipBoundary(bounds: Bounds) {
|
|
1518
|
-
|
|
1551
|
+
const boundarySearchBounds = {
|
|
1552
|
+
minX: bounds.minX - WICK_CLEARANCE - EPS,
|
|
1553
|
+
minY: bounds.minY - WICK_CLEARANCE - EPS,
|
|
1554
|
+
maxX: bounds.maxX + WICK_CLEARANCE + EPS,
|
|
1555
|
+
maxY: bounds.maxY + WICK_CLEARANCE + EPS,
|
|
1556
|
+
}
|
|
1557
|
+
const nearbyChips =
|
|
1558
|
+
this.chipObstacleSpatialIndex.getChipsInBounds(boundarySearchBounds)
|
|
1559
|
+
|
|
1560
|
+
for (const chip of nearbyChips) {
|
|
1519
1561
|
const chipBounds = chip.bounds
|
|
1520
1562
|
const adjacentToVerticalSide =
|
|
1521
1563
|
Math.abs(bounds.minX - chipBounds.maxX) <= WICK_CLEARANCE + EPS ||
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export const LABEL_SEARCH_STEP = 0.05
|
|
2
|
+
// Keep failed searches from retaining an unbounded visualization history.
|
|
3
|
+
export const MAX_RECORDED_CANDIDATES = 2_000
|
|
2
4
|
export const WICK_CLEARANCE = 0.001
|
|
3
5
|
export const EPS = 1e-9
|
|
4
6
|
export const TRACE_BOUNDARY_TOLERANCE = WICK_CLEARANCE + EPS
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Point } from "@tscircuit/math-utils"
|
|
2
2
|
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
3
3
|
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
4
|
+
import { tracePathContainsPoint } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
|
|
4
5
|
import { getMovedAnchorPointForReroute } from "./getMovedAnchorPointForReroute"
|
|
5
6
|
import { isLabelAttachedToTrace } from "./isLabelAttachedToTrace"
|
|
6
7
|
|
|
@@ -39,3 +40,73 @@ export const moveAttachedLabelsToReroutedTrace = ({
|
|
|
39
40
|
},
|
|
40
41
|
}
|
|
41
42
|
})
|
|
43
|
+
|
|
44
|
+
export const moveNetLabelConnectorsToReroutedTraces = ({
|
|
45
|
+
originalTraces,
|
|
46
|
+
reroutedTraces,
|
|
47
|
+
netLabelPlacements,
|
|
48
|
+
}: {
|
|
49
|
+
originalTraces: SolvedTracePath[]
|
|
50
|
+
reroutedTraces: SolvedTracePath[]
|
|
51
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
52
|
+
}) => {
|
|
53
|
+
const traces = [...reroutedTraces]
|
|
54
|
+
const reroutedTraceMap = new Map(
|
|
55
|
+
reroutedTraces.map((trace) => [trace.mspPairId, trace]),
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
const labels = netLabelPlacements.map((label) => {
|
|
59
|
+
const originalHostTrace = originalTraces.find((trace) => {
|
|
60
|
+
if (!label.mspConnectionPairIds.includes(trace.mspPairId)) return false
|
|
61
|
+
const reroutedTrace = reroutedTraceMap.get(trace.mspPairId)
|
|
62
|
+
if (!reroutedTrace) return false
|
|
63
|
+
return reroutedTrace.tracePath !== trace.tracePath
|
|
64
|
+
})
|
|
65
|
+
if (!originalHostTrace) return label
|
|
66
|
+
|
|
67
|
+
const reroutedHostTrace = reroutedTraceMap.get(originalHostTrace.mspPairId)!
|
|
68
|
+
const connectorIndex = originalTraces.findIndex((trace) => {
|
|
69
|
+
if (trace.mspPairId === originalHostTrace.mspPairId) return false
|
|
70
|
+
if (trace.globalConnNetId !== label.globalConnNetId) return false
|
|
71
|
+
if (!tracePathContainsPoint(trace.tracePath, label.anchorPoint)) {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
return label.pinIds.every((pinId) => trace.pinIds.includes(pinId))
|
|
75
|
+
})
|
|
76
|
+
if (connectorIndex < 0) return label
|
|
77
|
+
|
|
78
|
+
const connector = originalTraces[connectorIndex]!
|
|
79
|
+
const originalJunction = connector.tracePath.find((point) =>
|
|
80
|
+
tracePathContainsPoint(originalHostTrace.tracePath, point),
|
|
81
|
+
)
|
|
82
|
+
if (!originalJunction) return label
|
|
83
|
+
|
|
84
|
+
const reroutedJunction = getMovedAnchorPointForReroute(
|
|
85
|
+
originalJunction,
|
|
86
|
+
originalHostTrace.tracePath,
|
|
87
|
+
reroutedHostTrace.tracePath,
|
|
88
|
+
)
|
|
89
|
+
if (!reroutedJunction) return label
|
|
90
|
+
|
|
91
|
+
const delta = {
|
|
92
|
+
x: reroutedJunction.x - originalJunction.x,
|
|
93
|
+
y: reroutedJunction.y - originalJunction.y,
|
|
94
|
+
}
|
|
95
|
+
const movedConnector = {
|
|
96
|
+
...connector,
|
|
97
|
+
tracePath: connector.tracePath.map((point) => ({
|
|
98
|
+
x: point.x + delta.x,
|
|
99
|
+
y: point.y + delta.y,
|
|
100
|
+
})),
|
|
101
|
+
}
|
|
102
|
+
traces[connectorIndex] = movedConnector
|
|
103
|
+
return moveAttachedLabelsToReroutedTrace({
|
|
104
|
+
trace: connector,
|
|
105
|
+
originalTracePath: connector.tracePath,
|
|
106
|
+
reroutedTracePath: movedConnector.tracePath,
|
|
107
|
+
netLabelPlacements: [label],
|
|
108
|
+
})[0]!
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return { traces, netLabelPlacements: labels }
|
|
112
|
+
}
|
|
@@ -22,7 +22,10 @@ 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 {
|
|
25
|
+
import {
|
|
26
|
+
moveAttachedLabelsToReroutedTrace,
|
|
27
|
+
moveNetLabelConnectorsToReroutedTraces,
|
|
28
|
+
} from "../Example28Solver/labelMovement"
|
|
26
29
|
import { AvailableNetOrientationSolver } from "../AvailableNetOrientationSolver/AvailableNetOrientationSolver"
|
|
27
30
|
import { RailNetLabelCornerPlacementSolver } from "../RailNetLabelCornerPlacementSolver/RailNetLabelCornerPlacementSolver"
|
|
28
31
|
import { TraceAnchoredNetLabelOverlapSolver } from "../TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver"
|
|
@@ -426,12 +429,17 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
426
429
|
const previousOutput =
|
|
427
430
|
instance.preAlignmentNetLabelTraceCollisionSolver!.getOutput()
|
|
428
431
|
const alignmentOutput = instance.traceCleanupSolver2!.getOutput()
|
|
432
|
+
const connectorMovement = moveNetLabelConnectorsToReroutedTraces({
|
|
433
|
+
originalTraces: previousOutput.traces,
|
|
434
|
+
reroutedTraces: alignmentOutput.traces,
|
|
435
|
+
netLabelPlacements: previousOutput.netLabelPlacements,
|
|
436
|
+
})
|
|
429
437
|
const previousTraceMap = new Map(
|
|
430
438
|
previousOutput.traces.map((trace) => [trace.mspPairId, trace]),
|
|
431
439
|
)
|
|
432
|
-
let netLabelPlacements =
|
|
440
|
+
let netLabelPlacements = connectorMovement.netLabelPlacements
|
|
433
441
|
|
|
434
|
-
for (const trace of
|
|
442
|
+
for (const trace of connectorMovement.traces) {
|
|
435
443
|
const previousTrace = previousTraceMap.get(trace.mspPairId)
|
|
436
444
|
if (!previousTrace || previousTrace.tracePath === trace.tracePath) {
|
|
437
445
|
continue
|
|
@@ -455,7 +463,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
455
463
|
return [
|
|
456
464
|
{
|
|
457
465
|
inputProblem: instance.inputProblem,
|
|
458
|
-
traces:
|
|
466
|
+
traces: connectorMovement.traces,
|
|
459
467
|
netLabelPlacements,
|
|
460
468
|
},
|
|
461
469
|
]
|
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tscircuit/schematic-trace-solver",
|
|
3
|
+
"repository": {
|
|
4
|
+
"type": "git",
|
|
5
|
+
"url": "https://github.com/tscircuit/schematic-trace-solver.git"
|
|
6
|
+
},
|
|
3
7
|
"main": "dist/index.js",
|
|
4
|
-
"version": "0.0.
|
|
8
|
+
"version": "0.0.140",
|
|
5
9
|
"type": "module",
|
|
6
10
|
"scripts": {
|
|
7
11
|
"start": "cosmos",
|