@tscircuit/schematic-trace-solver 0.0.137 → 0.0.141
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 +6 -0
- package/dist/index.js +304 -36
- 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/lib/solvers/TraceOverlapShiftSolver/TraceOverlapShiftSolver.ts +60 -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__/board-1273-trace-overlap-cycle.snap.svg +542 -0
- package/tests/repros/__snapshots__/repro-board-648-esp12f-section.snap.svg +124 -0
- package/tests/repros/assets/board-1273-trace-overlap-cycle.input.json +3519 -0
- package/tests/repros/assets/repro-board-648-esp12f-section.input.json +537 -0
- package/tests/repros/board-1273-trace-overlap-cycle.test.ts +30 -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
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { Bounds, Point } from "@tscircuit/math-utils"
|
|
2
|
+
import Flatbush from "flatbush"
|
|
3
|
+
import type { ChipObstacleSpatialIndex } from "lib/data-structures/ChipObstacleSpatialIndex"
|
|
4
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
5
|
+
import { segmentIntersectsRect } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/collisions"
|
|
6
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
7
|
+
import { EPS } from "./constants"
|
|
8
|
+
import { segmentCrossesBoundsInterior } from "./geometry"
|
|
9
|
+
|
|
10
|
+
export type IndexedTraceSegment = {
|
|
11
|
+
trace: SolvedTracePath
|
|
12
|
+
start: Point
|
|
13
|
+
end: Point
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class AvailableNetOrientationObstacleIndex {
|
|
17
|
+
private chipObstacleSpatialIndex: ChipObstacleSpatialIndex
|
|
18
|
+
private labelIndex: Flatbush | null = null
|
|
19
|
+
private traceSegmentIndex: Flatbush | null = null
|
|
20
|
+
private traceSegments: IndexedTraceSegment[] = []
|
|
21
|
+
|
|
22
|
+
constructor(params: {
|
|
23
|
+
chipObstacleSpatialIndex: ChipObstacleSpatialIndex
|
|
24
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
25
|
+
traces: SolvedTracePath[]
|
|
26
|
+
}) {
|
|
27
|
+
this.chipObstacleSpatialIndex = params.chipObstacleSpatialIndex
|
|
28
|
+
this.rebuild(params)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
rebuild(params: {
|
|
32
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
33
|
+
traces: SolvedTracePath[]
|
|
34
|
+
}) {
|
|
35
|
+
this.labelIndex = this.buildLabelIndex(params.netLabelPlacements)
|
|
36
|
+
this.traceSegments = this.getTraceSegments(params.traces)
|
|
37
|
+
this.traceSegmentIndex = this.buildTraceSegmentIndex(this.traceSegments)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
getLabelIndicesInBounds(bounds: Bounds) {
|
|
41
|
+
if (!this.labelIndex) return []
|
|
42
|
+
return this.labelIndex.search(
|
|
43
|
+
bounds.minX,
|
|
44
|
+
bounds.minY,
|
|
45
|
+
bounds.maxX,
|
|
46
|
+
bounds.maxY,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
getTraceSegmentsInBounds(bounds: Bounds) {
|
|
51
|
+
if (!this.traceSegmentIndex) return []
|
|
52
|
+
const searchBounds = getPaddedBounds(bounds, EPS)
|
|
53
|
+
return this.traceSegmentIndex
|
|
54
|
+
.search(
|
|
55
|
+
searchBounds.minX,
|
|
56
|
+
searchBounds.minY,
|
|
57
|
+
searchBounds.maxX,
|
|
58
|
+
searchBounds.maxY,
|
|
59
|
+
)
|
|
60
|
+
.map((segmentIndex) => this.traceSegments[segmentIndex]!)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
getLabelIndicesNearTracePath(tracePath: Point[]) {
|
|
64
|
+
const labelIndices = new Set<number>()
|
|
65
|
+
for (let pointIndex = 0; pointIndex < tracePath.length - 1; pointIndex++) {
|
|
66
|
+
const segmentBounds = getPaddedBounds(
|
|
67
|
+
getSegmentBounds(tracePath[pointIndex]!, tracePath[pointIndex + 1]!),
|
|
68
|
+
EPS,
|
|
69
|
+
)
|
|
70
|
+
for (const labelIndex of this.getLabelIndicesInBounds(segmentBounds)) {
|
|
71
|
+
labelIndices.add(labelIndex)
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return [...labelIndices].sort((a, b) => a - b)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
doesTracePathCrossChip(tracePath: Point[]) {
|
|
78
|
+
for (let pointIndex = 0; pointIndex < tracePath.length - 1; pointIndex++) {
|
|
79
|
+
const start = tracePath[pointIndex]!
|
|
80
|
+
const end = tracePath[pointIndex + 1]!
|
|
81
|
+
const nearbyChips = this.chipObstacleSpatialIndex.getChipsInBounds(
|
|
82
|
+
getPaddedBounds(getSegmentBounds(start, end), EPS),
|
|
83
|
+
)
|
|
84
|
+
for (const chip of nearbyChips) {
|
|
85
|
+
if (segmentCrossesBoundsInterior(start, end, chip.bounds)) return true
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return false
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
doesTraceCrossBoundsInterior(bounds: Bounds) {
|
|
92
|
+
for (const segment of this.getTraceSegmentsInBounds(bounds)) {
|
|
93
|
+
if (segmentCrossesBoundsInterior(segment.start, segment.end, bounds)) {
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return false
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
doesTraceIntersectBounds(params: {
|
|
101
|
+
bounds: Bounds
|
|
102
|
+
excludedGlobalConnNetId: string
|
|
103
|
+
}) {
|
|
104
|
+
for (const segment of this.getTraceSegmentsInBounds(params.bounds)) {
|
|
105
|
+
if (segment.trace.globalConnNetId === params.excludedGlobalConnNetId) {
|
|
106
|
+
continue
|
|
107
|
+
}
|
|
108
|
+
if (segmentIntersectsRect(segment.start, segment.end, params.bounds)) {
|
|
109
|
+
return true
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return false
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
private buildLabelIndex(netLabelPlacements: NetLabelPlacement[]) {
|
|
116
|
+
if (netLabelPlacements.length === 0) return null
|
|
117
|
+
|
|
118
|
+
const labelIndex = new Flatbush(netLabelPlacements.length)
|
|
119
|
+
for (const label of netLabelPlacements) {
|
|
120
|
+
labelIndex.add(
|
|
121
|
+
label.center.x - label.width / 2,
|
|
122
|
+
label.center.y - label.height / 2,
|
|
123
|
+
label.center.x + label.width / 2,
|
|
124
|
+
label.center.y + label.height / 2,
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
labelIndex.finish()
|
|
128
|
+
return labelIndex
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private getTraceSegments(traces: SolvedTracePath[]) {
|
|
132
|
+
const traceSegments: IndexedTraceSegment[] = []
|
|
133
|
+
for (const trace of traces) {
|
|
134
|
+
for (
|
|
135
|
+
let pointIndex = 0;
|
|
136
|
+
pointIndex < trace.tracePath.length - 1;
|
|
137
|
+
pointIndex++
|
|
138
|
+
) {
|
|
139
|
+
traceSegments.push({
|
|
140
|
+
trace,
|
|
141
|
+
start: trace.tracePath[pointIndex]!,
|
|
142
|
+
end: trace.tracePath[pointIndex + 1]!,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return traceSegments
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private buildTraceSegmentIndex(traceSegments: IndexedTraceSegment[]) {
|
|
150
|
+
if (traceSegments.length === 0) return null
|
|
151
|
+
|
|
152
|
+
const traceSegmentIndex = new Flatbush(traceSegments.length)
|
|
153
|
+
for (const segment of traceSegments) {
|
|
154
|
+
traceSegmentIndex.add(
|
|
155
|
+
Math.min(segment.start.x, segment.end.x),
|
|
156
|
+
Math.min(segment.start.y, segment.end.y),
|
|
157
|
+
Math.max(segment.start.x, segment.end.x),
|
|
158
|
+
Math.max(segment.start.y, segment.end.y),
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
traceSegmentIndex.finish()
|
|
162
|
+
return traceSegmentIndex
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function getSegmentBounds(start: Point, end: Point): Bounds {
|
|
167
|
+
return {
|
|
168
|
+
minX: Math.min(start.x, end.x),
|
|
169
|
+
minY: Math.min(start.y, end.y),
|
|
170
|
+
maxX: Math.max(start.x, end.x),
|
|
171
|
+
maxY: Math.max(start.y, end.y),
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function getPaddedBounds(bounds: Bounds, padding: number): Bounds {
|
|
176
|
+
return {
|
|
177
|
+
minX: bounds.minX - padding,
|
|
178
|
+
minY: bounds.minY - padding,
|
|
179
|
+
maxX: bounds.maxX + padding,
|
|
180
|
+
maxY: bounds.maxY + padding,
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -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
|
]
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
import type { MspConnectionPairId } from "../MspConnectionPairSolver/MspConnectionPairSolver"
|
|
12
12
|
|
|
13
13
|
type ConnNetId = string
|
|
14
|
+
type TraceState = Record<MspConnectionPairId, SolvedTracePath>
|
|
15
|
+
|
|
16
|
+
const TRACE_STATE_POSITION_EPSILON = 1e-6
|
|
14
17
|
|
|
15
18
|
/**
|
|
16
19
|
* This solver finds traces that overlap or meet collinearly and aren't
|
|
@@ -44,6 +47,9 @@ export class TraceOverlapShiftSolver extends BaseSolver {
|
|
|
44
47
|
traceNetIslands: Record<ConnNetId, Array<SolvedTracePath>> = {}
|
|
45
48
|
|
|
46
49
|
correctedTraceMap: Record<MspConnectionPairId, SolvedTracePath> = {}
|
|
50
|
+
// Keep only the current and previous layouts to detect a two-state cycle
|
|
51
|
+
// without accumulating routing history for the whole solve.
|
|
52
|
+
recentTraceStates: TraceState[] = []
|
|
47
53
|
|
|
48
54
|
cleanupPhase: "diagonals" | "done" | null = null
|
|
49
55
|
|
|
@@ -64,6 +70,8 @@ export class TraceOverlapShiftSolver extends BaseSolver {
|
|
|
64
70
|
this.correctedTraceMap[mspPairId] = tracePath
|
|
65
71
|
}
|
|
66
72
|
|
|
73
|
+
this.rememberTraceState(this.correctedTraceMap)
|
|
74
|
+
|
|
67
75
|
this.traceNetIslands = this.computeTraceNetIslands()
|
|
68
76
|
}
|
|
69
77
|
|
|
@@ -361,11 +369,19 @@ export class TraceOverlapShiftSolver extends BaseSolver {
|
|
|
361
369
|
|
|
362
370
|
override _step() {
|
|
363
371
|
if (this.activeSubSolver?.solved) {
|
|
364
|
-
|
|
365
|
-
this.
|
|
366
|
-
|
|
367
|
-
this.correctedTraceMap[mspPairId] = newTrace
|
|
372
|
+
const nextTraceState = {
|
|
373
|
+
...this.correctedTraceMap,
|
|
374
|
+
...this.activeSubSolver.correctedTraceMap,
|
|
368
375
|
}
|
|
376
|
+
// Returning to the older retained layout means corrections are bouncing
|
|
377
|
+
// A -> B -> A. Keep B and finish this pass instead of retrying forever.
|
|
378
|
+
if (this.returnsToPreviousTraceState(nextTraceState)) {
|
|
379
|
+
this.activeSubSolver = null
|
|
380
|
+
this.solved = true
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
this.correctedTraceMap = nextTraceState
|
|
384
|
+
this.rememberTraceState(nextTraceState)
|
|
369
385
|
this.activeSubSolver = null
|
|
370
386
|
this.traceNetIslands = this.computeTraceNetIslands()
|
|
371
387
|
}
|
|
@@ -407,6 +423,46 @@ export class TraceOverlapShiftSolver extends BaseSolver {
|
|
|
407
423
|
})
|
|
408
424
|
}
|
|
409
425
|
|
|
426
|
+
private rememberTraceState(traceState: TraceState) {
|
|
427
|
+
this.recentTraceStates.push({ ...traceState })
|
|
428
|
+
if (this.recentTraceStates.length > 2) {
|
|
429
|
+
this.recentTraceStates.shift()
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
private returnsToPreviousTraceState(candidateTraceState: TraceState) {
|
|
434
|
+
if (this.recentTraceStates.length < 2) return false
|
|
435
|
+
|
|
436
|
+
const previousTraceState = this.recentTraceStates[0]!
|
|
437
|
+
const traceIds = Object.keys(candidateTraceState)
|
|
438
|
+
if (traceIds.length !== Object.keys(previousTraceState).length) return false
|
|
439
|
+
|
|
440
|
+
// Corrections create new objects, so compare the trace geometry itself.
|
|
441
|
+
for (const traceId of traceIds) {
|
|
442
|
+
const candidatePath = candidateTraceState[traceId]
|
|
443
|
+
const previousPath = previousTraceState[traceId]
|
|
444
|
+
if (!candidatePath || !previousPath) return false
|
|
445
|
+
if (candidatePath.tracePath.length !== previousPath.tracePath.length) {
|
|
446
|
+
return false
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
for (let i = 0; i < candidatePath.tracePath.length; i++) {
|
|
450
|
+
const candidatePoint = candidatePath.tracePath[i]!
|
|
451
|
+
const previousPoint = previousPath.tracePath[i]!
|
|
452
|
+
if (
|
|
453
|
+
Math.abs(candidatePoint.x - previousPoint.x) >
|
|
454
|
+
TRACE_STATE_POSITION_EPSILON ||
|
|
455
|
+
Math.abs(candidatePoint.y - previousPoint.y) >
|
|
456
|
+
TRACE_STATE_POSITION_EPSILON
|
|
457
|
+
) {
|
|
458
|
+
return false
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return true
|
|
464
|
+
}
|
|
465
|
+
|
|
410
466
|
override visualize() {
|
|
411
467
|
if (this.activeSubSolver) {
|
|
412
468
|
return this.activeSubSolver.visualize()
|
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.141",
|
|
5
9
|
"type": "module",
|
|
6
10
|
"scripts": {
|
|
7
11
|
"start": "cosmos",
|