@tscircuit/schematic-trace-solver 0.0.107 → 0.0.109

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 (33) hide show
  1. package/dist/index.d.ts +31 -3
  2. package/dist/index.js +784 -247
  3. package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +3 -1
  4. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +58 -27
  5. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateEndpointCollisionDetours.ts +1 -10
  6. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateInternalSegmentCollisionDetours.ts +94 -0
  7. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/pathOps.ts +10 -0
  8. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts +5 -1
  9. package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +18 -3
  10. package/lib/solvers/UnroutedTraceRecoverySolver/UnroutedTraceRecoverySolver.ts +592 -0
  11. package/package.json +1 -1
  12. package/site/bug-reports/bug-report-20260724T175257Z.page.tsx +4 -0
  13. package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +5 -9
  14. package/tests/bug-reports/bug-report-20260708T095725Z/__snapshots__/bug-report-20260708T095725Z.snap.svg +5 -5
  15. package/tests/bug-reports/bug-report-20260708T095725Z/bug-report-20260708T095725Z.test.ts +15 -0
  16. package/tests/bug-reports/bug-report-20260724T175257Z/__snapshots__/bug-report-20260724T175257Z.snap.svg +58 -0
  17. package/tests/bug-reports/bug-report-20260724T175257Z/bug-report-20260724T175257Z.json +138 -0
  18. package/tests/bug-reports/bug-report-20260724T175257Z/bug-report-20260724T175257Z.test.ts +12 -0
  19. package/tests/examples/__snapshots__/example04.snap.svg +7 -9
  20. package/tests/examples/__snapshots__/example08.snap.svg +16 -20
  21. package/tests/examples/__snapshots__/example13.snap.svg +2 -4
  22. package/tests/examples/__snapshots__/example32.snap.svg +64 -66
  23. package/tests/examples/__snapshots__/example46.snap.svg +1 -1
  24. package/tests/repros/__snapshots__/repro-netlabel-collision-687.snap.svg +81 -0
  25. package/tests/repros/__snapshots__/repro-netlabel-overlap-trace.snap.svg +16 -18
  26. package/tests/repros/__snapshots__/repro-tps61222-trace-intersection.snap.svg +63 -220
  27. package/tests/repros/assets/repro-netlabel-collision-687.input.json +212 -0
  28. package/tests/repros/repro-netlabel-collision-687.test.ts +59 -0
  29. package/tests/repros/repro-tps61222-trace-intersection.test.ts +27 -0
  30. package/tests/solvers/SchematicTraceSingleLineSolver2/generate-internal-segment-collision-detours.test.ts +49 -0
  31. package/tests/solvers/UnroutedTraceRecoverySolver/paired-junction-recovery.test.ts +49 -0
  32. package/tests/solvers/UnroutedTraceRecoverySolver/skip-ground.test.ts +30 -0
  33. 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)
@@ -7,16 +7,18 @@ import { getDimsForOrientation } from "lib/solvers/NetLabelPlacementSolver/Singl
7
7
  import { visualizeInputProblem } from "lib/solvers/SchematicTracePipelineSolver/visualizeInputProblem"
8
8
  import type { InputChip, InputProblem } from "lib/types/InputProblem"
9
9
  import type { FacingDirection } from "lib/utils/dir"
10
- import type { RectPadding } from "lib/utils/textBoxBounds"
10
+ import { getTextBoxBounds, type RectPadding } from "lib/utils/textBoxBounds"
11
11
  import { getPinDirection } from "../SchematicTraceSingleLineSolver/getPinDirection"
12
12
  import { calculateDirectShortPath } from "./calculateDirectShortPath"
13
13
  import {
14
14
  findFirstCollision,
15
15
  isHorizontal,
16
16
  isVertical,
17
+ segmentIntersectsRect,
17
18
  segmentOverlapsRectBoundary,
18
19
  } from "./collisions"
19
20
  import { generateEndpointCollisionDetours } from "./generateEndpointCollisionDetours"
21
+ import { generateInternalSegmentCollisionDetours } from "./generateInternalSegmentCollisionDetours"
20
22
  import {
21
23
  type Axis,
22
24
  aabbFromPoints,
@@ -24,7 +26,12 @@ import {
24
26
  midBetweenPointAndRect,
25
27
  } from "./mid"
26
28
  import { pathKey, shiftSegmentOrth } from "./pathOps"
27
- import { getObstacleRects, type ObstacleRect } from "./rect"
29
+ import {
30
+ getObstacleRects,
31
+ isTextBoxObstacle,
32
+ type ObstacleRect,
33
+ type TextBoxObstacleRect,
34
+ } from "./rect"
28
35
 
29
36
  type PathKey = string
30
37
 
@@ -58,8 +65,8 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
58
65
  chipMap: Record<string, InputChip>
59
66
 
60
67
  obstacles: ObstacleRect[]
61
- textObstacles: Set<ObstacleRect>
62
- endpointTextObstacles: Set<ObstacleRect>
68
+ textObstacles: Set<TextBoxObstacleRect>
69
+ endpointTextObstacles: Set<TextBoxObstacleRect>
63
70
  aabb: { minX: number; maxX: number; minY: number; maxY: number }
64
71
 
65
72
  baseElbow: Point[]
@@ -95,18 +102,17 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
95
102
  this.obstacles = getObstacleRects(this.inputProblem, {
96
103
  textBoxPadding: this.getTextBoxPaddingForConnectionPair(),
97
104
  })
98
- this.textObstacles = new Set(
99
- this.obstacles.filter((r) => r.kind === "text_box"),
100
- )
105
+ this.textObstacles = new Set(this.obstacles.filter(isTextBoxObstacle))
101
106
  const endpointChipIds = new Set(this.pins.map((pin) => pin.chipId))
102
107
  this.endpointTextObstacles = new Set(
103
108
  endpointChipIds.size > 1
104
- ? this.obstacles.filter(
105
- (r) =>
106
- r.kind === "text_box" &&
107
- r.textBox.chipId !== undefined &&
108
- endpointChipIds.has(r.textBox.chipId),
109
- )
109
+ ? this.obstacles
110
+ .filter(isTextBoxObstacle)
111
+ .filter(
112
+ (obstacle) =>
113
+ obstacle.textBox.chipId !== undefined &&
114
+ endpointChipIds.has(obstacle.textBox.chipId),
115
+ )
110
116
  : [],
111
117
  )
112
118
 
@@ -356,7 +362,12 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
356
362
  )
357
363
  ) {
358
364
  for (const textObstacle of this.endpointTextObstacles) {
359
- excludedRects.add(textObstacle)
365
+ // Outside-pin-band routing may cross label padding around endpoint
366
+ // text, but the actual text bounds remain a hard obstacle.
367
+ const textBounds = getTextBoxBounds(textObstacle.textBox)
368
+ if (!segmentIntersectsRect(segmentStart, segmentEnd, textBounds)) {
369
+ excludedRects.add(textObstacle)
370
+ }
360
371
  }
361
372
  }
362
373
 
@@ -500,18 +511,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
500
511
  pinBandPenalty: number
501
512
  }> = []
502
513
 
503
- const addShiftedCandidate = (
504
- candidateSegIndex: number,
505
- candidateAxis: Axis,
506
- coord: number,
507
- ) => {
508
- const newPath = shiftSegmentOrth(
509
- path,
510
- candidateSegIndex,
511
- candidateAxis,
512
- coord,
513
- )
514
- if (!newPath) return
514
+ const addPathCandidate = (newPath: Point[]) => {
515
515
  const key = pathKey(newPath)
516
516
  if (this.visited.has(key)) return
517
517
  this.visited.add(key)
@@ -525,11 +525,42 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
525
525
  })
526
526
  }
527
527
 
528
+ const addShiftedCandidate = (
529
+ candidateSegIndex: number,
530
+ candidateAxis: Axis,
531
+ coord: number,
532
+ ) => {
533
+ const newPath = shiftSegmentOrth(
534
+ path,
535
+ candidateSegIndex,
536
+ candidateAxis,
537
+ coord,
538
+ )
539
+ if (newPath) addPathCandidate(newPath)
540
+ }
541
+
542
+ const lastSegIndex = path.length - 2
543
+ if (
544
+ this.connectionPair === undefined &&
545
+ rect.kind === "text_box" &&
546
+ this.endpointTextObstacles.has(rect) &&
547
+ !collisionRects.has(rect) &&
548
+ originalSegIndex > 0 &&
549
+ originalSegIndex < lastSegIndex
550
+ ) {
551
+ for (const detour of generateInternalSegmentCollisionDetours({
552
+ path,
553
+ collidingSegmentIndex: originalSegIndex,
554
+ obstacle: rect,
555
+ })) {
556
+ addPathCandidate(detour)
557
+ }
558
+ }
559
+
528
560
  for (const coord of candidates) {
529
561
  addShiftedCandidate(segIndex, axis, coord)
530
562
  }
531
563
 
532
- const lastSegIndex = path.length - 2
533
564
  const adjacentSegmentIndexes =
534
565
  originalSegIndex === 1
535
566
  ? [2]
@@ -1,6 +1,7 @@
1
1
  import type { Point } from "@tscircuit/math-utils"
2
2
  import { isHorizontal, isVertical } from "./collisions"
3
3
  import { type Axis, midBetweenPointAndRect } from "./mid"
4
+ import { hasOnlyNonzeroOrthogonalSegments } from "./pathOps"
4
5
  import type { ObstacleRect } from "./rect"
5
6
 
6
7
  const getSegmentAxis = (start: Point, end: Point): Axis | null => {
@@ -9,16 +10,6 @@ const getSegmentAxis = (start: Point, end: Point): Axis | null => {
9
10
  return null
10
11
  }
11
12
 
12
- const hasOnlyNonzeroOrthogonalSegments = (path: Point[]) =>
13
- path.every((point, index) => {
14
- const nextPoint = path[index + 1]
15
- if (!nextPoint) return true
16
- if (!isHorizontal(point, nextPoint) && !isVertical(point, nextPoint)) {
17
- return false
18
- }
19
- return Math.abs(point.x - nextPoint.x) + Math.abs(point.y - nextPoint.y) > 0
20
- })
21
-
22
13
  export const generateEndpointCollisionDetours = ({
23
14
  path,
24
15
  collidingSegmentIndex,
@@ -0,0 +1,94 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import { isHorizontal, isVertical } from "./collisions"
3
+ import { hasOnlyNonzeroOrthogonalSegments } from "./pathOps"
4
+ import type { RectBounds } from "./rect"
5
+
6
+ const EPS = 1e-9
7
+ const DEFAULT_CLEARANCE = 0.2
8
+
9
+ const isStrictlyBetween = (value: number, a: number, b: number) =>
10
+ value > Math.min(a, b) + EPS && value < Math.max(a, b) - EPS
11
+
12
+ export const generateInternalSegmentCollisionDetours = ({
13
+ path,
14
+ collidingSegmentIndex,
15
+ obstacle,
16
+ clearance = DEFAULT_CLEARANCE,
17
+ }: {
18
+ path: Point[]
19
+ collidingSegmentIndex: number
20
+ obstacle: RectBounds
21
+ clearance?: number
22
+ }): Point[][] => {
23
+ if (collidingSegmentIndex <= 0 || collidingSegmentIndex >= path.length - 2) {
24
+ return []
25
+ }
26
+
27
+ const start = path[collidingSegmentIndex]!
28
+ const end = path[collidingSegmentIndex + 1]!
29
+ const detourPoints: Point[][] = []
30
+
31
+ if (isHorizontal(start, end)) {
32
+ const movingRight = start.x < end.x
33
+ const entryX = movingRight
34
+ ? obstacle.minX - clearance
35
+ : obstacle.maxX + clearance
36
+ const exitX = movingRight
37
+ ? obstacle.maxX + clearance
38
+ : obstacle.minX - clearance
39
+
40
+ if (
41
+ !isStrictlyBetween(entryX, start.x, end.x) ||
42
+ !isStrictlyBetween(exitX, start.x, end.x)
43
+ ) {
44
+ return []
45
+ }
46
+
47
+ for (const detourY of [
48
+ obstacle.minY - clearance,
49
+ obstacle.maxY + clearance,
50
+ ]) {
51
+ detourPoints.push([
52
+ { x: entryX, y: start.y },
53
+ { x: entryX, y: detourY },
54
+ { x: exitX, y: detourY },
55
+ { x: exitX, y: end.y },
56
+ ])
57
+ }
58
+ } else if (isVertical(start, end)) {
59
+ const movingUp = start.y < end.y
60
+ const entryY = movingUp
61
+ ? obstacle.minY - clearance
62
+ : obstacle.maxY + clearance
63
+ const exitY = movingUp
64
+ ? obstacle.maxY + clearance
65
+ : obstacle.minY - clearance
66
+
67
+ if (
68
+ !isStrictlyBetween(entryY, start.y, end.y) ||
69
+ !isStrictlyBetween(exitY, start.y, end.y)
70
+ ) {
71
+ return []
72
+ }
73
+
74
+ for (const detourX of [
75
+ obstacle.minX - clearance,
76
+ obstacle.maxX + clearance,
77
+ ]) {
78
+ detourPoints.push([
79
+ { x: start.x, y: entryY },
80
+ { x: detourX, y: entryY },
81
+ { x: detourX, y: exitY },
82
+ { x: end.x, y: exitY },
83
+ ])
84
+ }
85
+ }
86
+
87
+ return detourPoints
88
+ .map((points) => [
89
+ ...path.slice(0, collidingSegmentIndex + 1),
90
+ ...points,
91
+ ...path.slice(collidingSegmentIndex + 1),
92
+ ])
93
+ .filter(hasOnlyNonzeroOrthogonalSegments)
94
+ }
@@ -5,6 +5,16 @@ const EPS = 1e-9
5
5
 
6
6
  export type Axis = "x" | "y"
7
7
 
8
+ export const hasOnlyNonzeroOrthogonalSegments = (path: Point[]) =>
9
+ path.every((point, index) => {
10
+ const nextPoint = path[index + 1]
11
+ if (!nextPoint) return true
12
+ if (!isHorizontal(point, nextPoint) && !isVertical(point, nextPoint)) {
13
+ return false
14
+ }
15
+ return Math.abs(point.x - nextPoint.x) + Math.abs(point.y - nextPoint.y) > 0
16
+ })
17
+
8
18
  export const shiftSegmentOrth = (
9
19
  pts: Point[],
10
20
  segIndex: number,
@@ -1,5 +1,5 @@
1
- import type { InputChip, InputProblem } from "lib/types/InputProblem"
2
1
  import { getInputChipBounds } from "lib/solvers/GuidelinesSolver/getInputChipBounds"
2
+ import type { InputChip, InputProblem } from "lib/types/InputProblem"
3
3
  import { getTextBoxBounds, type RectPadding } from "lib/utils/textBoxBounds"
4
4
 
5
5
  export type RectBounds = {
@@ -21,6 +21,10 @@ export type TextBoxObstacleRect = RectBounds & {
21
21
 
22
22
  export type ObstacleRect = ChipObstacleRect | TextBoxObstacleRect
23
23
 
24
+ export const isTextBoxObstacle = (
25
+ obstacle: ObstacleRect,
26
+ ): obstacle is TextBoxObstacleRect => obstacle.kind === "text_box"
27
+
24
28
  export const chipToRect = (chip: InputChip): ChipObstacleRect => {
25
29
  const b = getInputChipBounds(chip)
26
30
  return { kind: "chip", chipId: chip.chipId, ...b }
@@ -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)