@tscircuit/schematic-trace-solver 0.0.108 → 0.0.110
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 +29 -1
- package/dist/index.js +510 -39
- package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +3 -1
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +18 -3
- package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/groupLabelsByChipAndOrientation.ts +12 -4
- package/lib/solvers/UnroutedTraceRecoverySolver/UnroutedTraceRecoverySolver.ts +592 -0
- package/package.json +1 -1
- package/site/bug-reports/bug-report-20260724T175257Z.page.tsx +4 -0
- package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +5 -9
- package/tests/bug-reports/bug-report-20260724T175257Z/__snapshots__/bug-report-20260724T175257Z.snap.svg +58 -0
- package/tests/bug-reports/bug-report-20260724T175257Z/bug-report-20260724T175257Z.json +138 -0
- package/tests/bug-reports/bug-report-20260724T175257Z/bug-report-20260724T175257Z.test.ts +12 -0
- package/tests/examples/__snapshots__/example04.snap.svg +7 -9
- package/tests/examples/__snapshots__/example08.snap.svg +16 -20
- package/tests/examples/__snapshots__/example13.snap.svg +2 -4
- package/tests/examples/__snapshots__/example32.snap.svg +64 -66
- package/tests/examples/__snapshots__/example46.snap.svg +1 -1
- package/tests/repros/__snapshots__/repro-netlabel-collision-687.snap.svg +81 -0
- package/tests/repros/__snapshots__/repro-netlabel-overlap-trace.snap.svg +16 -18
- package/tests/repros/assets/repro-netlabel-collision-687.input.json +212 -0
- package/tests/repros/repro-netlabel-collision-687.test.ts +59 -0
- package/tests/solvers/UnroutedTraceRecoverySolver/paired-junction-recovery.test.ts +49 -0
- package/tests/solvers/UnroutedTraceRecoverySolver/skip-ground.test.ts +30 -0
- package/tests/solvers/UnroutedTraceRecoverySolver/skip-long-connection.test.ts +24 -0
|
@@ -16,6 +16,7 @@ import { getConnectivityMapsFromInputProblem } from "./getConnectivityMapFromInp
|
|
|
16
16
|
import { getOrthogonalMinimumSpanningTree } from "./getMspConnectionPairsFromPins"
|
|
17
17
|
|
|
18
18
|
export type MspConnectionPairId = string
|
|
19
|
+
export const DEFAULT_MAX_MSP_PAIR_DISTANCE = 1
|
|
19
20
|
|
|
20
21
|
const getPinPairKey = (pinIds: [PinId, PinId]) => [...pinIds].sort().join("::")
|
|
21
22
|
|
|
@@ -45,7 +46,8 @@ export class MspConnectionPairSolver extends BaseSolver {
|
|
|
45
46
|
super()
|
|
46
47
|
|
|
47
48
|
this.inputProblem = inputProblem
|
|
48
|
-
this.maxMspPairDistance =
|
|
49
|
+
this.maxMspPairDistance =
|
|
50
|
+
inputProblem.maxMspPairDistance ?? DEFAULT_MAX_MSP_PAIR_DISTANCE
|
|
49
51
|
|
|
50
52
|
const { directConnMap, netConnMap } =
|
|
51
53
|
getConnectivityMapsFromInputProblem(inputProblem)
|
|
@@ -27,6 +27,7 @@ import { RailNetLabelCornerPlacementSolver } from "../RailNetLabelCornerPlacemen
|
|
|
27
27
|
import { TraceAnchoredNetLabelOverlapSolver } from "../TraceAnchoredNetLabelOverlapSolver/TraceAnchoredNetLabelOverlapSolver"
|
|
28
28
|
import { NetLabelTraceCollisionSolver } from "../NetLabelTraceCollisionSolver/NetLabelTraceCollisionSolver"
|
|
29
29
|
import { NetLabelNetLabelCollisionSolver } from "../NetLabelNetLabelCollisionSolver/NetLabelNetLabelCollisionSolver"
|
|
30
|
+
import { UnroutedTraceRecoverySolver } from "../UnroutedTraceRecoverySolver/UnroutedTraceRecoverySolver"
|
|
30
31
|
|
|
31
32
|
type PipelineStep<T extends new (...args: any[]) => BaseSolver> = {
|
|
32
33
|
solverName: string
|
|
@@ -70,6 +71,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
70
71
|
// guidelinesSolver?: GuidelinesSolver
|
|
71
72
|
schematicTraceLinesSolver?: SchematicTraceLinesSolver
|
|
72
73
|
longDistancePairSolver?: LongDistancePairSolver
|
|
74
|
+
unroutedTraceRecoverySolver?: UnroutedTraceRecoverySolver
|
|
73
75
|
traceOverlapShiftSolver?: TraceOverlapShiftSolver
|
|
74
76
|
netLabelPlacementSolver?: NetLabelPlacementSolver
|
|
75
77
|
labelMergingSolver?: MergedNetLabelObstacleSolver
|
|
@@ -143,6 +145,19 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
143
145
|
onSolved: (schematicTraceLinesSolver) => {},
|
|
144
146
|
},
|
|
145
147
|
),
|
|
148
|
+
definePipelineStep(
|
|
149
|
+
"unroutedTraceRecoverySolver",
|
|
150
|
+
UnroutedTraceRecoverySolver,
|
|
151
|
+
(instance) => [
|
|
152
|
+
{
|
|
153
|
+
inputProblem: instance.inputProblem,
|
|
154
|
+
failedConnectionPairs:
|
|
155
|
+
instance.schematicTraceLinesSolver!.failedConnectionPairs,
|
|
156
|
+
alreadySolvedTraces:
|
|
157
|
+
instance.longDistancePairSolver!.getOutput().allTracesMerged,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
),
|
|
146
161
|
definePipelineStep(
|
|
147
162
|
"traceOverlapShiftSolver",
|
|
148
163
|
TraceOverlapShiftSolver,
|
|
@@ -150,7 +165,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
150
165
|
{
|
|
151
166
|
inputProblem: this.inputProblem,
|
|
152
167
|
inputTracePaths:
|
|
153
|
-
this.
|
|
168
|
+
this.unroutedTraceRecoverySolver?.getOutput().allTracesMerged!,
|
|
154
169
|
globalConnMap: this.mspConnectionPairSolver!.globalConnMap,
|
|
155
170
|
},
|
|
156
171
|
],
|
|
@@ -167,7 +182,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
167
182
|
inputTraceMap:
|
|
168
183
|
this.traceOverlapShiftSolver?.correctedTraceMap ??
|
|
169
184
|
Object.fromEntries(
|
|
170
|
-
this.
|
|
185
|
+
this.unroutedTraceRecoverySolver!.getOutput().allTracesMerged.map(
|
|
171
186
|
(p) => [p.mspPairId, p],
|
|
172
187
|
),
|
|
173
188
|
),
|
|
@@ -187,7 +202,7 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
187
202
|
instance.traceOverlapShiftSolver?.correctedTraceMap ??
|
|
188
203
|
Object.fromEntries(
|
|
189
204
|
instance
|
|
190
|
-
.
|
|
205
|
+
.unroutedTraceRecoverySolver!.getOutput()
|
|
191
206
|
.allTracesMerged.map((p) => [p.mspPairId, p]),
|
|
192
207
|
)
|
|
193
208
|
const traces = Object.values(traceMap)
|
|
@@ -7,7 +7,8 @@ import type { InputProblem } from "lib/types/InputProblem"
|
|
|
7
7
|
* with the same orientation relative to that chip.
|
|
8
8
|
*
|
|
9
9
|
* @param labels An array of NetLabelPlacement objects to be grouped.
|
|
10
|
-
* @param chips
|
|
10
|
+
* @param chips Chips from the input problem, used to resolve opaque pin IDs to
|
|
11
|
+
* their owning component.
|
|
11
12
|
* @returns A record where keys are in the format "chipId-orientation" (e.g., "U1-left")
|
|
12
13
|
* and values are arrays of NetLabelPlacement objects belonging to that group.
|
|
13
14
|
*/
|
|
@@ -19,6 +20,11 @@ export const groupLabelsByChipAndOrientation = ({
|
|
|
19
20
|
chips: InputProblem["chips"]
|
|
20
21
|
}): Record<string, NetLabelPlacement[]> => {
|
|
21
22
|
const groupedLabels: Record<string, NetLabelPlacement[]> = {}
|
|
23
|
+
const chipIdByPinId = new Map(
|
|
24
|
+
chips.flatMap((chip) =>
|
|
25
|
+
chip.pins.map((pin) => [pin.pinId, chip.chipId] as const),
|
|
26
|
+
),
|
|
27
|
+
)
|
|
22
28
|
|
|
23
29
|
for (const label of labels) {
|
|
24
30
|
if (label.pinIds.length === 0) {
|
|
@@ -26,10 +32,12 @@ export const groupLabelsByChipAndOrientation = ({
|
|
|
26
32
|
continue
|
|
27
33
|
}
|
|
28
34
|
|
|
29
|
-
|
|
30
|
-
|
|
35
|
+
const pinId = label.pinIds[0]!
|
|
36
|
+
// Preserve historical group IDs for component-qualified pins while using
|
|
37
|
+
// the input's pin ownership for opaque IDs such as schematic_port_123.
|
|
38
|
+
const legacyChipId = pinId.includes(".") ? pinId.split(".")[0] : undefined
|
|
39
|
+
const chipId = legacyChipId ?? chipIdByPinId.get(pinId)
|
|
31
40
|
if (!chipId) {
|
|
32
|
-
// Should not happen if pinIds are well-formed, but good to guard
|
|
33
41
|
continue
|
|
34
42
|
}
|
|
35
43
|
|
|
@@ -0,0 +1,592 @@
|
|
|
1
|
+
import type { Point } from "@tscircuit/math-utils"
|
|
2
|
+
import { distance, doSegmentsIntersect } from "@tscircuit/math-utils"
|
|
3
|
+
import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
|
|
4
|
+
import { getConnectivityMapsFromInputProblem } from "lib/solvers/MspConnectionPairSolver/getConnectivityMapFromInputProblem"
|
|
5
|
+
import {
|
|
6
|
+
DEFAULT_MAX_MSP_PAIR_DISTANCE,
|
|
7
|
+
type MspConnectionPair,
|
|
8
|
+
} from "lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver"
|
|
9
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
10
|
+
import {
|
|
11
|
+
findFirstCollision,
|
|
12
|
+
isHorizontal,
|
|
13
|
+
isVertical,
|
|
14
|
+
segmentOverlapsRectBoundary,
|
|
15
|
+
} from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
16
|
+
import {
|
|
17
|
+
getObstacleRects,
|
|
18
|
+
type ObstacleRect,
|
|
19
|
+
} from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
20
|
+
import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
|
|
21
|
+
import type { InputProblem, PinId } from "lib/types/InputProblem"
|
|
22
|
+
import type { FacingDirection } from "lib/utils/dir"
|
|
23
|
+
|
|
24
|
+
const ROUTE_CLEARANCE = 0.2
|
|
25
|
+
const COORDINATE_TOLERANCE = 1e-9
|
|
26
|
+
const GROUND_NET_ID = "GND"
|
|
27
|
+
|
|
28
|
+
const pointsAreEqual = (firstPoint: Point, secondPoint: Point): boolean => {
|
|
29
|
+
return (
|
|
30
|
+
Math.abs(firstPoint.x - secondPoint.x) <= COORDINATE_TOLERANCE &&
|
|
31
|
+
Math.abs(firstPoint.y - secondPoint.y) <= COORDINATE_TOLERANCE
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const removeConsecutiveDuplicatePoints = (path: Point[]): Point[] => {
|
|
36
|
+
const filteredPath: Point[] = []
|
|
37
|
+
for (const point of path) {
|
|
38
|
+
const previousPoint = filteredPath.at(-1)
|
|
39
|
+
if (!previousPoint || !pointsAreEqual(previousPoint, point)) {
|
|
40
|
+
filteredPath.push(point)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return filteredPath
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const getPathLength = (path: Point[]): number => {
|
|
47
|
+
let pathLength = 0
|
|
48
|
+
for (let pointIndex = 0; pointIndex < path.length - 1; pointIndex++) {
|
|
49
|
+
const startPoint = path[pointIndex]!
|
|
50
|
+
const endPoint = path[pointIndex + 1]!
|
|
51
|
+
pathLength +=
|
|
52
|
+
Math.abs(endPoint.x - startPoint.x) + Math.abs(endPoint.y - startPoint.y)
|
|
53
|
+
}
|
|
54
|
+
return pathLength
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const getEscapePoint = ({
|
|
58
|
+
pin,
|
|
59
|
+
facingDirection,
|
|
60
|
+
}: {
|
|
61
|
+
pin: Point
|
|
62
|
+
facingDirection: FacingDirection
|
|
63
|
+
}): Point => {
|
|
64
|
+
const escapePoint = { x: pin.x, y: pin.y }
|
|
65
|
+
if (facingDirection === "x+") {
|
|
66
|
+
escapePoint.x += ROUTE_CLEARANCE
|
|
67
|
+
}
|
|
68
|
+
if (facingDirection === "x-") {
|
|
69
|
+
escapePoint.x -= ROUTE_CLEARANCE
|
|
70
|
+
}
|
|
71
|
+
if (facingDirection === "y+") {
|
|
72
|
+
escapePoint.y += ROUTE_CLEARANCE
|
|
73
|
+
}
|
|
74
|
+
if (facingDirection === "y-") {
|
|
75
|
+
escapePoint.y -= ROUTE_CLEARANCE
|
|
76
|
+
}
|
|
77
|
+
return escapePoint
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const getOuterBounds = (obstacles: ObstacleRect[]) => {
|
|
81
|
+
return {
|
|
82
|
+
minX: Math.min(...obstacles.map((obstacle) => obstacle.minX)),
|
|
83
|
+
minY: Math.min(...obstacles.map((obstacle) => obstacle.minY)),
|
|
84
|
+
maxX: Math.max(...obstacles.map((obstacle) => obstacle.maxX)),
|
|
85
|
+
maxY: Math.max(...obstacles.map((obstacle) => obstacle.maxY)),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const getPerimeterCandidates = ({
|
|
90
|
+
connectionPair,
|
|
91
|
+
obstacles,
|
|
92
|
+
}: {
|
|
93
|
+
connectionPair: MspConnectionPair
|
|
94
|
+
obstacles: ObstacleRect[]
|
|
95
|
+
}): Point[][] => {
|
|
96
|
+
const [firstPin, secondPin] = connectionPair.pins
|
|
97
|
+
const firstEscapePoint = getEscapePoint({
|
|
98
|
+
pin: firstPin,
|
|
99
|
+
facingDirection: firstPin._facingDirection!,
|
|
100
|
+
})
|
|
101
|
+
const secondEscapePoint = getEscapePoint({
|
|
102
|
+
pin: secondPin,
|
|
103
|
+
facingDirection: secondPin._facingDirection!,
|
|
104
|
+
})
|
|
105
|
+
const outerBounds = getOuterBounds(obstacles)
|
|
106
|
+
const horizontalChannels = [
|
|
107
|
+
outerBounds.minY - ROUTE_CLEARANCE,
|
|
108
|
+
outerBounds.maxY + ROUTE_CLEARANCE,
|
|
109
|
+
]
|
|
110
|
+
const verticalChannels = [
|
|
111
|
+
outerBounds.minX - ROUTE_CLEARANCE,
|
|
112
|
+
outerBounds.maxX + ROUTE_CLEARANCE,
|
|
113
|
+
]
|
|
114
|
+
const candidates: Point[][] = []
|
|
115
|
+
|
|
116
|
+
for (const channelY of horizontalChannels) {
|
|
117
|
+
candidates.push(
|
|
118
|
+
removeConsecutiveDuplicatePoints([
|
|
119
|
+
firstPin,
|
|
120
|
+
firstEscapePoint,
|
|
121
|
+
{ x: firstEscapePoint.x, y: channelY },
|
|
122
|
+
{ x: secondEscapePoint.x, y: channelY },
|
|
123
|
+
secondEscapePoint,
|
|
124
|
+
secondPin,
|
|
125
|
+
]),
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
for (const channelX of verticalChannels) {
|
|
130
|
+
candidates.push(
|
|
131
|
+
removeConsecutiveDuplicatePoints([
|
|
132
|
+
firstPin,
|
|
133
|
+
firstEscapePoint,
|
|
134
|
+
{ x: channelX, y: firstEscapePoint.y },
|
|
135
|
+
{ x: channelX, y: secondEscapePoint.y },
|
|
136
|
+
secondEscapePoint,
|
|
137
|
+
secondPin,
|
|
138
|
+
]),
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return candidates.sort(
|
|
143
|
+
(firstPath, secondPath) =>
|
|
144
|
+
getPathLength(firstPath) - getPathLength(secondPath),
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const getSegmentMidpoint = (startPoint: Point, endPoint: Point): Point => {
|
|
149
|
+
return {
|
|
150
|
+
x: (startPoint.x + endPoint.x) / 2,
|
|
151
|
+
y: (startPoint.y + endPoint.y) / 2,
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const getJunctionPoints = (sameNetTraces: SolvedTracePath[]): Point[] => {
|
|
156
|
+
const junctionPoints: Point[] = []
|
|
157
|
+
for (const trace of sameNetTraces) {
|
|
158
|
+
for (
|
|
159
|
+
let pointIndex = 0;
|
|
160
|
+
pointIndex < trace.tracePath.length - 1;
|
|
161
|
+
pointIndex++
|
|
162
|
+
) {
|
|
163
|
+
const startPoint = trace.tracePath[pointIndex]!
|
|
164
|
+
const endPoint = trace.tracePath[pointIndex + 1]!
|
|
165
|
+
junctionPoints.push(startPoint)
|
|
166
|
+
junctionPoints.push(getSegmentMidpoint(startPoint, endPoint))
|
|
167
|
+
}
|
|
168
|
+
const lastPoint = trace.tracePath.at(-1)
|
|
169
|
+
if (lastPoint) {
|
|
170
|
+
junctionPoints.push(lastPoint)
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return junctionPoints
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const getUnconnectedPins = ({
|
|
177
|
+
connectionPair,
|
|
178
|
+
sameNetTraces,
|
|
179
|
+
}: {
|
|
180
|
+
connectionPair: MspConnectionPair
|
|
181
|
+
sameNetTraces: SolvedTracePath[]
|
|
182
|
+
}) => {
|
|
183
|
+
const connectedPinIds = new Set(
|
|
184
|
+
sameNetTraces.flatMap((trace) => trace.pinIds),
|
|
185
|
+
)
|
|
186
|
+
return connectionPair.pins.filter((pin) => !connectedPinIds.has(pin.pinId))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const getJunctionCandidates = ({
|
|
190
|
+
connectionPair,
|
|
191
|
+
sameNetTraces,
|
|
192
|
+
obstacles,
|
|
193
|
+
maxConnectionDistance,
|
|
194
|
+
}: {
|
|
195
|
+
connectionPair: MspConnectionPair
|
|
196
|
+
sameNetTraces: SolvedTracePath[]
|
|
197
|
+
obstacles: ObstacleRect[]
|
|
198
|
+
maxConnectionDistance: number
|
|
199
|
+
}): Point[][] => {
|
|
200
|
+
const outerBounds = getOuterBounds(obstacles)
|
|
201
|
+
const horizontalChannels = [
|
|
202
|
+
outerBounds.minY - ROUTE_CLEARANCE,
|
|
203
|
+
outerBounds.maxY + ROUTE_CLEARANCE,
|
|
204
|
+
]
|
|
205
|
+
const verticalChannels = [
|
|
206
|
+
outerBounds.minX - ROUTE_CLEARANCE,
|
|
207
|
+
outerBounds.maxX + ROUTE_CLEARANCE,
|
|
208
|
+
]
|
|
209
|
+
const candidates: Point[][] = []
|
|
210
|
+
const junctionPoints = getJunctionPoints(sameNetTraces)
|
|
211
|
+
const unconnectedPins = getUnconnectedPins({
|
|
212
|
+
connectionPair,
|
|
213
|
+
sameNetTraces,
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
for (const pin of unconnectedPins) {
|
|
217
|
+
const escapePoint = getEscapePoint({
|
|
218
|
+
pin,
|
|
219
|
+
facingDirection: pin._facingDirection!,
|
|
220
|
+
})
|
|
221
|
+
for (const junctionPoint of junctionPoints) {
|
|
222
|
+
if (distance(pin, junctionPoint) > maxConnectionDistance) {
|
|
223
|
+
continue
|
|
224
|
+
}
|
|
225
|
+
candidates.push(
|
|
226
|
+
removeConsecutiveDuplicatePoints([
|
|
227
|
+
pin,
|
|
228
|
+
escapePoint,
|
|
229
|
+
{ x: escapePoint.x, y: junctionPoint.y },
|
|
230
|
+
junctionPoint,
|
|
231
|
+
]),
|
|
232
|
+
)
|
|
233
|
+
candidates.push(
|
|
234
|
+
removeConsecutiveDuplicatePoints([
|
|
235
|
+
pin,
|
|
236
|
+
escapePoint,
|
|
237
|
+
{ x: junctionPoint.x, y: escapePoint.y },
|
|
238
|
+
junctionPoint,
|
|
239
|
+
]),
|
|
240
|
+
)
|
|
241
|
+
for (const channelY of horizontalChannels) {
|
|
242
|
+
candidates.push(
|
|
243
|
+
removeConsecutiveDuplicatePoints([
|
|
244
|
+
pin,
|
|
245
|
+
escapePoint,
|
|
246
|
+
{ x: escapePoint.x, y: channelY },
|
|
247
|
+
{ x: junctionPoint.x, y: channelY },
|
|
248
|
+
junctionPoint,
|
|
249
|
+
]),
|
|
250
|
+
)
|
|
251
|
+
}
|
|
252
|
+
for (const channelX of verticalChannels) {
|
|
253
|
+
candidates.push(
|
|
254
|
+
removeConsecutiveDuplicatePoints([
|
|
255
|
+
pin,
|
|
256
|
+
escapePoint,
|
|
257
|
+
{ x: channelX, y: escapePoint.y },
|
|
258
|
+
{ x: channelX, y: junctionPoint.y },
|
|
259
|
+
junctionPoint,
|
|
260
|
+
]),
|
|
261
|
+
)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return candidates.sort(
|
|
267
|
+
(firstPath, secondPath) =>
|
|
268
|
+
getPathLength(firstPath) - getPathLength(secondPath),
|
|
269
|
+
)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const pathCollidesWithObstacles = ({
|
|
273
|
+
path,
|
|
274
|
+
obstacles,
|
|
275
|
+
connectionPair,
|
|
276
|
+
rejectComponentBoundaryTravel,
|
|
277
|
+
}: {
|
|
278
|
+
path: Point[]
|
|
279
|
+
obstacles: ObstacleRect[]
|
|
280
|
+
connectionPair: MspConnectionPair
|
|
281
|
+
rejectComponentBoundaryTravel: boolean
|
|
282
|
+
}): boolean => {
|
|
283
|
+
const firstPathPoint = path[0]!
|
|
284
|
+
const lastPathPoint = path.at(-1)!
|
|
285
|
+
const firstPathPin = connectionPair.pins.find((pin) =>
|
|
286
|
+
pointsAreEqual(pin, firstPathPoint),
|
|
287
|
+
)
|
|
288
|
+
const lastPathPin = connectionPair.pins.find((pin) =>
|
|
289
|
+
pointsAreEqual(pin, lastPathPoint),
|
|
290
|
+
)
|
|
291
|
+
const pathConnectsPairPins =
|
|
292
|
+
firstPathPin !== undefined && lastPathPin !== undefined
|
|
293
|
+
const firstChipObstacle = obstacles.find(
|
|
294
|
+
(obstacle) =>
|
|
295
|
+
obstacle.kind === "chip" && obstacle.chipId === firstPathPin?.chipId,
|
|
296
|
+
)
|
|
297
|
+
const secondChipObstacle = obstacles.find(
|
|
298
|
+
(obstacle) =>
|
|
299
|
+
obstacle.kind === "chip" && obstacle.chipId === lastPathPin?.chipId,
|
|
300
|
+
)
|
|
301
|
+
if (
|
|
302
|
+
rejectComponentBoundaryTravel &&
|
|
303
|
+
firstChipObstacle &&
|
|
304
|
+
segmentOverlapsRectBoundary(path[0]!, path[1]!, firstChipObstacle)
|
|
305
|
+
) {
|
|
306
|
+
return true
|
|
307
|
+
}
|
|
308
|
+
if (
|
|
309
|
+
rejectComponentBoundaryTravel &&
|
|
310
|
+
secondChipObstacle &&
|
|
311
|
+
segmentOverlapsRectBoundary(
|
|
312
|
+
path[path.length - 2]!,
|
|
313
|
+
path[path.length - 1]!,
|
|
314
|
+
secondChipObstacle,
|
|
315
|
+
)
|
|
316
|
+
) {
|
|
317
|
+
return true
|
|
318
|
+
}
|
|
319
|
+
const collision = findFirstCollision(path, obstacles, {
|
|
320
|
+
excludeRectsForSegment: (segmentIndex) => {
|
|
321
|
+
const excludedObstacles = new Set<ObstacleRect>()
|
|
322
|
+
if (segmentIndex === 0 && firstChipObstacle) {
|
|
323
|
+
excludedObstacles.add(firstChipObstacle)
|
|
324
|
+
}
|
|
325
|
+
if (
|
|
326
|
+
pathConnectsPairPins &&
|
|
327
|
+
segmentIndex === path.length - 2 &&
|
|
328
|
+
secondChipObstacle
|
|
329
|
+
) {
|
|
330
|
+
excludedObstacles.add(secondChipObstacle)
|
|
331
|
+
}
|
|
332
|
+
return excludedObstacles
|
|
333
|
+
},
|
|
334
|
+
})
|
|
335
|
+
return collision !== null
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const hasParallelFailedConnection = ({
|
|
339
|
+
connectionPair,
|
|
340
|
+
failedConnectionPairs,
|
|
341
|
+
}: {
|
|
342
|
+
connectionPair: MspConnectionPair
|
|
343
|
+
failedConnectionPairs: MspConnectionPair[]
|
|
344
|
+
}): boolean => {
|
|
345
|
+
const connectionChipIds = new Set(
|
|
346
|
+
connectionPair.pins.map((pin) => pin.chipId),
|
|
347
|
+
)
|
|
348
|
+
return failedConnectionPairs.some((otherConnectionPair) => {
|
|
349
|
+
if (otherConnectionPair.mspPairId === connectionPair.mspPairId) {
|
|
350
|
+
return false
|
|
351
|
+
}
|
|
352
|
+
return otherConnectionPair.pins.every((pin) =>
|
|
353
|
+
connectionChipIds.has(pin.chipId),
|
|
354
|
+
)
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
const segmentsOverlapBeyondEndpoint = ({
|
|
359
|
+
firstStart,
|
|
360
|
+
firstEnd,
|
|
361
|
+
secondStart,
|
|
362
|
+
secondEnd,
|
|
363
|
+
}: {
|
|
364
|
+
firstStart: Point
|
|
365
|
+
firstEnd: Point
|
|
366
|
+
secondStart: Point
|
|
367
|
+
secondEnd: Point
|
|
368
|
+
}): boolean => {
|
|
369
|
+
if (
|
|
370
|
+
isHorizontal(firstStart, firstEnd) &&
|
|
371
|
+
isHorizontal(secondStart, secondEnd)
|
|
372
|
+
) {
|
|
373
|
+
const overlapLength =
|
|
374
|
+
Math.min(
|
|
375
|
+
Math.max(firstStart.x, firstEnd.x),
|
|
376
|
+
Math.max(secondStart.x, secondEnd.x),
|
|
377
|
+
) -
|
|
378
|
+
Math.max(
|
|
379
|
+
Math.min(firstStart.x, firstEnd.x),
|
|
380
|
+
Math.min(secondStart.x, secondEnd.x),
|
|
381
|
+
)
|
|
382
|
+
return overlapLength > COORDINATE_TOLERANCE
|
|
383
|
+
}
|
|
384
|
+
if (isVertical(firstStart, firstEnd) && isVertical(secondStart, secondEnd)) {
|
|
385
|
+
const overlapLength =
|
|
386
|
+
Math.min(
|
|
387
|
+
Math.max(firstStart.y, firstEnd.y),
|
|
388
|
+
Math.max(secondStart.y, secondEnd.y),
|
|
389
|
+
) -
|
|
390
|
+
Math.max(
|
|
391
|
+
Math.min(firstStart.y, firstEnd.y),
|
|
392
|
+
Math.min(secondStart.y, secondEnd.y),
|
|
393
|
+
)
|
|
394
|
+
return overlapLength > COORDINATE_TOLERANCE
|
|
395
|
+
}
|
|
396
|
+
return false
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const getAllowedJunctionPoints = ({
|
|
400
|
+
connectionPair,
|
|
401
|
+
existingTrace,
|
|
402
|
+
}: {
|
|
403
|
+
connectionPair: MspConnectionPair
|
|
404
|
+
existingTrace: SolvedTracePath
|
|
405
|
+
}): Point[] => {
|
|
406
|
+
const existingPinIds = new Set<PinId>(existingTrace.pinIds)
|
|
407
|
+
return connectionPair.pins.filter((pin) => existingPinIds.has(pin.pinId))
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const pathCrossesExistingTraces = ({
|
|
411
|
+
path,
|
|
412
|
+
connectionPair,
|
|
413
|
+
existingTraces,
|
|
414
|
+
}: {
|
|
415
|
+
path: Point[]
|
|
416
|
+
connectionPair: MspConnectionPair
|
|
417
|
+
existingTraces: SolvedTracePath[]
|
|
418
|
+
}): boolean => {
|
|
419
|
+
for (const existingTrace of existingTraces) {
|
|
420
|
+
if (existingTrace.globalConnNetId === connectionPair.globalConnNetId) {
|
|
421
|
+
continue
|
|
422
|
+
}
|
|
423
|
+
const allowedJunctionPoints = getAllowedJunctionPoints({
|
|
424
|
+
connectionPair,
|
|
425
|
+
existingTrace,
|
|
426
|
+
})
|
|
427
|
+
for (let pathIndex = 0; pathIndex < path.length - 1; pathIndex++) {
|
|
428
|
+
const pathStart = path[pathIndex]!
|
|
429
|
+
const pathEnd = path[pathIndex + 1]!
|
|
430
|
+
for (
|
|
431
|
+
let traceIndex = 0;
|
|
432
|
+
traceIndex < existingTrace.tracePath.length - 1;
|
|
433
|
+
traceIndex++
|
|
434
|
+
) {
|
|
435
|
+
const traceStart = existingTrace.tracePath[traceIndex]!
|
|
436
|
+
const traceEnd = existingTrace.tracePath[traceIndex + 1]!
|
|
437
|
+
if (!doSegmentsIntersect(pathStart, pathEnd, traceStart, traceEnd)) {
|
|
438
|
+
continue
|
|
439
|
+
}
|
|
440
|
+
const intersectionIsAllowedJunction = allowedJunctionPoints.some(
|
|
441
|
+
(junctionPoint) =>
|
|
442
|
+
(pointsAreEqual(pathStart, junctionPoint) ||
|
|
443
|
+
pointsAreEqual(pathEnd, junctionPoint)) &&
|
|
444
|
+
(pointsAreEqual(traceStart, junctionPoint) ||
|
|
445
|
+
pointsAreEqual(traceEnd, junctionPoint)),
|
|
446
|
+
)
|
|
447
|
+
if (
|
|
448
|
+
intersectionIsAllowedJunction &&
|
|
449
|
+
!segmentsOverlapBeyondEndpoint({
|
|
450
|
+
firstStart: pathStart,
|
|
451
|
+
firstEnd: pathEnd,
|
|
452
|
+
secondStart: traceStart,
|
|
453
|
+
secondEnd: traceEnd,
|
|
454
|
+
})
|
|
455
|
+
) {
|
|
456
|
+
continue
|
|
457
|
+
}
|
|
458
|
+
return true
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return false
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export class UnroutedTraceRecoverySolver extends BaseSolver {
|
|
466
|
+
private inputProblem: InputProblem
|
|
467
|
+
private alreadySolvedTraces: SolvedTracePath[]
|
|
468
|
+
private failedConnectionPairs: MspConnectionPair[]
|
|
469
|
+
private queuedConnectionPairs: MspConnectionPair[]
|
|
470
|
+
private maxConnectionDistance: number
|
|
471
|
+
private groundGlobalConnNetId?: string
|
|
472
|
+
public solvedUnroutedTraces: SolvedTracePath[] = []
|
|
473
|
+
|
|
474
|
+
constructor(
|
|
475
|
+
private params: {
|
|
476
|
+
inputProblem: InputProblem
|
|
477
|
+
failedConnectionPairs: MspConnectionPair[]
|
|
478
|
+
alreadySolvedTraces: SolvedTracePath[]
|
|
479
|
+
},
|
|
480
|
+
) {
|
|
481
|
+
super()
|
|
482
|
+
this.inputProblem = params.inputProblem
|
|
483
|
+
this.alreadySolvedTraces = params.alreadySolvedTraces
|
|
484
|
+
this.failedConnectionPairs = params.failedConnectionPairs
|
|
485
|
+
this.queuedConnectionPairs = [...params.failedConnectionPairs]
|
|
486
|
+
this.maxConnectionDistance =
|
|
487
|
+
this.inputProblem.maxMspPairDistance ?? DEFAULT_MAX_MSP_PAIR_DISTANCE
|
|
488
|
+
const { netConnMap } = getConnectivityMapsFromInputProblem(
|
|
489
|
+
this.inputProblem,
|
|
490
|
+
)
|
|
491
|
+
this.groundGlobalConnNetId =
|
|
492
|
+
netConnMap.getNetConnectedToId(GROUND_NET_ID) ?? undefined
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
override getConstructorParams() {
|
|
496
|
+
return this.params
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
override _step() {
|
|
500
|
+
const connectionPair = this.queuedConnectionPairs.shift()
|
|
501
|
+
if (!connectionPair) {
|
|
502
|
+
this.solved = true
|
|
503
|
+
return
|
|
504
|
+
}
|
|
505
|
+
if (connectionPair.globalConnNetId === this.groundGlobalConnNetId) {
|
|
506
|
+
return
|
|
507
|
+
}
|
|
508
|
+
if (
|
|
509
|
+
distance(connectionPair.pins[0], connectionPair.pins[1]) >
|
|
510
|
+
this.maxConnectionDistance
|
|
511
|
+
) {
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const obstacles = getObstacleRects(this.inputProblem)
|
|
516
|
+
const existingTraces = [
|
|
517
|
+
...this.alreadySolvedTraces,
|
|
518
|
+
...this.solvedUnroutedTraces,
|
|
519
|
+
]
|
|
520
|
+
const sameNetTraces = existingTraces.filter(
|
|
521
|
+
(trace) => trace.globalConnNetId === connectionPair.globalConnNetId,
|
|
522
|
+
)
|
|
523
|
+
const junctionCandidates = getJunctionCandidates({
|
|
524
|
+
connectionPair,
|
|
525
|
+
sameNetTraces,
|
|
526
|
+
obstacles,
|
|
527
|
+
maxConnectionDistance: this.maxConnectionDistance,
|
|
528
|
+
})
|
|
529
|
+
const perimeterCandidates = getPerimeterCandidates({
|
|
530
|
+
connectionPair,
|
|
531
|
+
obstacles,
|
|
532
|
+
})
|
|
533
|
+
const candidates = [...junctionCandidates, ...perimeterCandidates]
|
|
534
|
+
const rejectComponentBoundaryTravel = hasParallelFailedConnection({
|
|
535
|
+
connectionPair,
|
|
536
|
+
failedConnectionPairs: this.failedConnectionPairs,
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
for (const tracePath of candidates) {
|
|
540
|
+
if (
|
|
541
|
+
pathCollidesWithObstacles({
|
|
542
|
+
path: tracePath,
|
|
543
|
+
obstacles,
|
|
544
|
+
connectionPair,
|
|
545
|
+
rejectComponentBoundaryTravel,
|
|
546
|
+
})
|
|
547
|
+
) {
|
|
548
|
+
continue
|
|
549
|
+
}
|
|
550
|
+
if (
|
|
551
|
+
pathCrossesExistingTraces({
|
|
552
|
+
path: tracePath,
|
|
553
|
+
connectionPair,
|
|
554
|
+
existingTraces,
|
|
555
|
+
})
|
|
556
|
+
) {
|
|
557
|
+
continue
|
|
558
|
+
}
|
|
559
|
+
this.solvedUnroutedTraces.push({
|
|
560
|
+
...connectionPair,
|
|
561
|
+
tracePath,
|
|
562
|
+
mspConnectionPairIds: [connectionPair.mspPairId],
|
|
563
|
+
pinIds: connectionPair.pins.map((pin) => pin.pinId),
|
|
564
|
+
})
|
|
565
|
+
return
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
getOutput(): {
|
|
570
|
+
newTraces: SolvedTracePath[]
|
|
571
|
+
allTracesMerged: SolvedTracePath[]
|
|
572
|
+
} {
|
|
573
|
+
return {
|
|
574
|
+
newTraces: this.solvedUnroutedTraces,
|
|
575
|
+
allTracesMerged: [
|
|
576
|
+
...this.alreadySolvedTraces,
|
|
577
|
+
...this.solvedUnroutedTraces,
|
|
578
|
+
],
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
override visualize() {
|
|
583
|
+
const graphics = visualizeInputProblem(this.inputProblem)
|
|
584
|
+
for (const trace of this.solvedUnroutedTraces) {
|
|
585
|
+
graphics.lines!.push({
|
|
586
|
+
points: trace.tracePath,
|
|
587
|
+
strokeColor: "blue",
|
|
588
|
+
})
|
|
589
|
+
}
|
|
590
|
+
return graphics
|
|
591
|
+
}
|
|
592
|
+
}
|