@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.
Files changed (24) hide show
  1. package/dist/index.d.ts +29 -1
  2. package/dist/index.js +510 -39
  3. package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +3 -1
  4. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +18 -3
  5. package/lib/solvers/TraceLabelOverlapAvoidanceSolver/sub-solvers/LabelMergingSolver/groupLabelsByChipAndOrientation.ts +12 -4
  6. package/lib/solvers/UnroutedTraceRecoverySolver/UnroutedTraceRecoverySolver.ts +592 -0
  7. package/package.json +1 -1
  8. package/site/bug-reports/bug-report-20260724T175257Z.page.tsx +4 -0
  9. package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +5 -9
  10. package/tests/bug-reports/bug-report-20260724T175257Z/__snapshots__/bug-report-20260724T175257Z.snap.svg +58 -0
  11. package/tests/bug-reports/bug-report-20260724T175257Z/bug-report-20260724T175257Z.json +138 -0
  12. package/tests/bug-reports/bug-report-20260724T175257Z/bug-report-20260724T175257Z.test.ts +12 -0
  13. package/tests/examples/__snapshots__/example04.snap.svg +7 -9
  14. package/tests/examples/__snapshots__/example08.snap.svg +16 -20
  15. package/tests/examples/__snapshots__/example13.snap.svg +2 -4
  16. package/tests/examples/__snapshots__/example32.snap.svg +64 -66
  17. package/tests/examples/__snapshots__/example46.snap.svg +1 -1
  18. package/tests/repros/__snapshots__/repro-netlabel-collision-687.snap.svg +81 -0
  19. package/tests/repros/__snapshots__/repro-netlabel-overlap-trace.snap.svg +16 -18
  20. package/tests/repros/assets/repro-netlabel-collision-687.input.json +212 -0
  21. package/tests/repros/repro-netlabel-collision-687.test.ts +59 -0
  22. package/tests/solvers/UnroutedTraceRecoverySolver/paired-junction-recovery.test.ts +49 -0
  23. package/tests/solvers/UnroutedTraceRecoverySolver/skip-ground.test.ts +30 -0
  24. 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 = inputProblem.maxMspPairDistance ?? 1
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.longDistancePairSolver?.getOutput().allTracesMerged!,
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.longDistancePairSolver!.getOutput().allTracesMerged.map(
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
- .longDistancePairSolver!.getOutput()
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 An array of Chip objects from the InputProblem. (Currently not directly used for grouping logic, but part of the signature).
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
- // Extract chipId from the first pinId (e.g., "U1.1" -> "U1")
30
- const chipId = label.pinIds[0].split(".")[0]
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
+ }