@tscircuit/schematic-trace-solver 0.0.98 → 0.0.100

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 (26) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.js +335 -225
  3. package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +19 -9
  4. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +43 -1
  5. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateEndpointCollisionDetours.ts +84 -0
  6. package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/mid.ts +13 -3
  7. package/package.json +1 -1
  8. package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +18 -18
  9. package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +7 -7
  10. package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +5 -7
  11. package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +6 -12
  12. package/tests/examples/__snapshots__/example09.snap.svg +9 -13
  13. package/tests/examples/__snapshots__/example21.snap.svg +30 -32
  14. package/tests/examples/__snapshots__/example29.snap.svg +13 -19
  15. package/tests/repros/__snapshots__/repro129-host-custom-symbol-passives.snap.svg +60 -0
  16. package/tests/repros/__snapshots__/repro47-endpoint-obstacle-detour.snap.svg +65 -0
  17. package/tests/repros/__snapshots__/repro5-escape-padded-text-obstacles.snap.svg +74 -0
  18. package/tests/repros/assets/repro129-host-custom-symbol-passives.input.json +94 -0
  19. package/tests/repros/assets/repro47-endpoint-obstacle-detour.input.json +133 -0
  20. package/tests/repros/assets/repro5-escape-padded-text-obstacles.input.json +202 -0
  21. package/tests/repros/repro129-host-custom-symbol-passives.test.ts +16 -0
  22. package/tests/repros/repro47-endpoint-obstacle-detour.test.ts +19 -0
  23. package/tests/repros/repro5-escape-padded-text-obstacles.test.ts +17 -0
  24. package/tests/solvers/MspConnectionPairSolver/msp-connection-pair-solver-direct-connection-distance.test.ts +46 -0
  25. package/tests/solvers/SchematicTraceSingleLineSolver2/candidate-mids-from-set.test.ts +39 -0
  26. package/tests/solvers/SchematicTraceSingleLineSolver2/generate-endpoint-collision-detours.test.ts +38 -0
@@ -1,3 +1,6 @@
1
+ import { distance } from "@tscircuit/math-utils"
2
+ import type { ConnectivityMap } from "connectivity-map"
3
+ import type { GraphicsObject } from "graphics-debug"
1
4
  import { BaseSolver } from "lib/solvers/BaseSolver/BaseSolver"
2
5
  import type {
3
6
  InputChip,
@@ -5,17 +8,17 @@ import type {
5
8
  InputProblem,
6
9
  PinId,
7
10
  } from "lib/types/InputProblem"
8
- import { ConnectivityMap } from "connectivity-map"
9
- import { getConnectivityMapsFromInputProblem } from "./getConnectivityMapFromInputProblem"
10
- import { getOrthogonalMinimumSpanningTree } from "./getMspConnectionPairsFromPins"
11
- import { doesPairCrossRestrictedCenterLines } from "./doesPairCrossRestrictedCenterLines"
12
- import type { GraphicsObject } from "graphics-debug"
13
11
  import { getColorFromString } from "lib/utils/getColorFromString"
14
- import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
15
12
  import { arePinsInDifferentSchematicSections } from "../../utils/arePinsInDifferentSchematicSections"
13
+ import { visualizeInputProblem } from "../SchematicTracePipelineSolver/visualizeInputProblem"
14
+ import { doesPairCrossRestrictedCenterLines } from "./doesPairCrossRestrictedCenterLines"
15
+ import { getConnectivityMapsFromInputProblem } from "./getConnectivityMapFromInputProblem"
16
+ import { getOrthogonalMinimumSpanningTree } from "./getMspConnectionPairsFromPins"
16
17
 
17
18
  export type MspConnectionPairId = string
18
19
 
20
+ const getPinPairKey = (pinIds: [PinId, PinId]) => [...pinIds].sort().join("::")
21
+
19
22
  export type MspConnectionPair = {
20
23
  mspPairId: MspConnectionPairId
21
24
  dcConnNetId: string
@@ -36,6 +39,7 @@ export class MspConnectionPairSolver extends BaseSolver {
36
39
 
37
40
  pinMap: Record<string, InputPin & { chipId: string }>
38
41
  userNetIdByPinId: Record<string, string | undefined>
42
+ directConnectionPinPairKeys: Set<string>
39
43
 
40
44
  constructor({ inputProblem }: { inputProblem: InputProblem }) {
41
45
  super()
@@ -62,7 +66,9 @@ export class MspConnectionPairSolver extends BaseSolver {
62
66
 
63
67
  // Build a mapping from PinId to user-provided netId (if any)
64
68
  this.userNetIdByPinId = {}
69
+ this.directConnectionPinPairKeys = new Set()
65
70
  for (const dc of inputProblem.directConnections) {
71
+ this.directConnectionPinPairKeys.add(getPinPairKey(dc.pinIds))
66
72
  if (dc.netId) {
67
73
  const [a, b] = dc.pinIds
68
74
  this.userNetIdByPinId[a] = dc.netId
@@ -105,9 +111,13 @@ export class MspConnectionPairSolver extends BaseSolver {
105
111
  const [pin1, pin2] = directlyConnectedPins
106
112
  const p1 = this.pinMap[pin1!]!
107
113
  const p2 = this.pinMap[pin2!]!
108
- // Enforce max pair distance (use Manhattan to match orthogonal routing metric)
109
- const manhattanDist = Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y)
110
- if (manhattanDist > this.maxMspPairDistance) {
114
+ const pinPairKey = getPinPairKey([pin1!, pin2!])
115
+ // Explicit source traces are classified by straight-line distance when
116
+ // their input is created; named nets retain the orthogonal route metric.
117
+ const pairDistance = this.directConnectionPinPairKeys.has(pinPairKey)
118
+ ? distance(p1, p2)
119
+ : Math.abs(p1.x - p2.x) + Math.abs(p1.y - p2.y)
120
+ if (pairDistance > this.maxMspPairDistance) {
111
121
  // Too far apart; skip creating an MSP pair for this net
112
122
  return
113
123
  }
@@ -16,6 +16,7 @@ import {
16
16
  isVertical,
17
17
  segmentOverlapsRectBoundary,
18
18
  } from "./collisions"
19
+ import { generateEndpointCollisionDetours } from "./generateEndpointCollisionDetours"
19
20
  import {
20
21
  type Axis,
21
22
  aabbFromPoints,
@@ -357,6 +358,35 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
357
358
  const isFirstSegment = segIndex === 0
358
359
  const isLastSegment = segIndex === path.length - 2
359
360
 
361
+ if (path.length === 3 && (isFirstSegment || isLastSegment)) {
362
+ const detours = generateEndpointCollisionDetours({
363
+ path,
364
+ collidingSegmentIndex: segIndex,
365
+ obstacle: rect,
366
+ })
367
+ .filter((detour) => {
368
+ const key = pathKey(detour)
369
+ if (this.visited.has(key)) return false
370
+ this.visited.add(key)
371
+ return true
372
+ })
373
+ .sort(
374
+ (a, b) =>
375
+ this.pathLength(a) - this.pathLength(b) ||
376
+ this.getPinBandPenalty(a) - this.getPinBandPenalty(b),
377
+ )
378
+
379
+ for (const detour of detours) {
380
+ const nextCollisionRects = new Set(collisionRects)
381
+ nextCollisionRects.add(rect)
382
+ this.queue.push({
383
+ path: detour,
384
+ collisionRects: nextCollisionRects,
385
+ })
386
+ }
387
+ return
388
+ }
389
+
360
390
  if (isFirstSegment) {
361
391
  // If first segment collides, move the second segment instead
362
392
  if (path.length < 3) {
@@ -383,6 +413,11 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
383
413
 
384
414
  // Note: PA and PB are already defined above
385
415
  const candidates: number[] = []
416
+ // Fixed MSP pairs may leave the endpoint bounds to complete a required
417
+ // route. Optional long-distance candidates retain their bounded search.
418
+ const candidateMidOptions = {
419
+ allowOpenSideCandidates: this.connectionPair !== undefined,
420
+ }
386
421
 
387
422
  if (collisionRects.size === 0) {
388
423
  // First collision on this search branch: use mid(PA, C) and mid(PB, C)
@@ -395,7 +430,13 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
395
430
  candidates.push(...uniqueCandidates)
396
431
  } else {
397
432
  // Subsequent collisions: mid between C and nearest rect/bounds from the set
398
- const mids = candidateMidsFromSet(axis, rect, collisionRects, this.aabb)
433
+ const mids = candidateMidsFromSet(
434
+ axis,
435
+ rect,
436
+ collisionRects,
437
+ this.aabb,
438
+ candidateMidOptions,
439
+ )
399
440
  candidates.push(...mids)
400
441
  }
401
442
 
@@ -466,6 +507,7 @@ export class SchematicTraceSingleLineSolver2 extends BaseSolver {
466
507
  rect,
467
508
  collisionRects,
468
509
  this.aabb,
510
+ candidateMidOptions,
469
511
  ),
470
512
  )
471
513
  }
@@ -0,0 +1,84 @@
1
+ import type { Point } from "@tscircuit/math-utils"
2
+ import { isHorizontal, isVertical } from "./collisions"
3
+ import { type Axis, midBetweenPointAndRect } from "./mid"
4
+ import type { ObstacleRect } from "./rect"
5
+
6
+ const getSegmentAxis = (start: Point, end: Point): Axis | null => {
7
+ if (isVertical(start, end)) return "x"
8
+ if (isHorizontal(start, end)) return "y"
9
+ return null
10
+ }
11
+
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
+ export const generateEndpointCollisionDetours = ({
23
+ path,
24
+ collidingSegmentIndex,
25
+ obstacle,
26
+ }: {
27
+ path: Point[]
28
+ collidingSegmentIndex: number
29
+ obstacle: ObstacleRect
30
+ }): Point[][] => {
31
+ if (path.length !== 3) return []
32
+
33
+ const lastSegmentIndex = path.length - 2
34
+ if (
35
+ collidingSegmentIndex !== 0 &&
36
+ collidingSegmentIndex !== lastSegmentIndex
37
+ ) {
38
+ return []
39
+ }
40
+
41
+ const shouldReverse = collidingSegmentIndex === lastSegmentIndex
42
+ const orderedPath = shouldReverse ? [...path].reverse() : path
43
+ const [start, corner, end] = orderedPath
44
+ const firstSegmentAxis = getSegmentAxis(start!, corner!)
45
+ const secondSegmentAxis = getSegmentAxis(corner!, end!)
46
+ if (!firstSegmentAxis || !secondSegmentAxis) return []
47
+ if (firstSegmentAxis === secondSegmentAxis) return []
48
+
49
+ const escapeCoordinates = [
50
+ ...midBetweenPointAndRect(secondSegmentAxis, start!, obstacle),
51
+ ...midBetweenPointAndRect(secondSegmentAxis, end!, obstacle),
52
+ ]
53
+ const detourCoordinates = [
54
+ ...midBetweenPointAndRect(firstSegmentAxis, start!, obstacle),
55
+ ...midBetweenPointAndRect(firstSegmentAxis, end!, obstacle),
56
+ ]
57
+
58
+ const detours: Point[][] = []
59
+ for (const escapeCoordinate of [...new Set(escapeCoordinates)]) {
60
+ for (const detourCoordinate of [...new Set(detourCoordinates)]) {
61
+ const orderedDetour =
62
+ firstSegmentAxis === "y"
63
+ ? [
64
+ start!,
65
+ { x: escapeCoordinate, y: start!.y },
66
+ { x: escapeCoordinate, y: detourCoordinate },
67
+ { x: end!.x, y: detourCoordinate },
68
+ end!,
69
+ ]
70
+ : [
71
+ start!,
72
+ { x: start!.x, y: escapeCoordinate },
73
+ { x: detourCoordinate, y: escapeCoordinate },
74
+ { x: detourCoordinate, y: end!.y },
75
+ end!,
76
+ ]
77
+
78
+ const detour = shouldReverse ? orderedDetour.reverse() : orderedDetour
79
+ if (hasOnlyNonzeroOrthogonalSegments(detour)) detours.push(detour)
80
+ }
81
+ }
82
+
83
+ return detours
84
+ }
@@ -2,6 +2,7 @@ import type { Point } from "@tscircuit/math-utils"
2
2
  import type { ObstacleRect } from "./rect"
3
3
 
4
4
  const EPS = 1e-9
5
+ const OBSTACLE_CLEARANCE = 0.2
5
6
 
6
7
  export type Axis = "x" | "y"
7
8
 
@@ -26,7 +27,7 @@ export const midBetweenPointAndRect = (
26
27
  return [(p.x + r.maxX) / 2]
27
28
  }
28
29
  // Point is within rect bounds on this axis - generate candidates on both sides
29
- return [r.minX - 0.2, r.maxX + 0.2]
30
+ return [r.minX - OBSTACLE_CLEARANCE, r.maxX + OBSTACLE_CLEARANCE]
30
31
  } else {
31
32
  if (p.y < r.minY - eps) {
32
33
  return [(p.y + r.minY) / 2]
@@ -35,7 +36,7 @@ export const midBetweenPointAndRect = (
35
36
  return [(p.y + r.maxY) / 2]
36
37
  }
37
38
  // Point is within rect bounds on this axis - generate candidates on both sides
38
- return [r.minY - 0.2, r.maxY + 0.2]
39
+ return [r.minY - OBSTACLE_CLEARANCE, r.maxY + OBSTACLE_CLEARANCE]
39
40
  }
40
41
  }
41
42
 
@@ -44,8 +45,9 @@ export const candidateMidsFromSet = (
44
45
  colliding: ObstacleRect,
45
46
  collisionRects: Set<ObstacleRect>,
46
47
  aabb: { minX: number; maxX: number; minY: number; maxY: number },
47
- eps = EPS,
48
+ opts: { allowOpenSideCandidates?: boolean; eps?: number } = {},
48
49
  ): number[] => {
50
+ const { allowOpenSideCandidates = false, eps = EPS } = opts
49
51
  const setRects = [...collisionRects]
50
52
 
51
53
  if (axis === "x") {
@@ -64,9 +66,13 @@ export const candidateMidsFromSet = (
64
66
  const out: number[] = []
65
67
  if (leftNeighbor !== undefined) {
66
68
  out.push((leftNeighbor + colliding.minX) / 2)
69
+ } else if (allowOpenSideCandidates) {
70
+ out.push(colliding.minX - OBSTACLE_CLEARANCE)
67
71
  }
68
72
  if (rightNeighbor !== undefined) {
69
73
  out.push((colliding.maxX + rightNeighbor) / 2)
74
+ } else if (allowOpenSideCandidates) {
75
+ out.push(colliding.maxX + OBSTACLE_CLEARANCE)
70
76
  }
71
77
  return out
72
78
  } else {
@@ -85,9 +91,13 @@ export const candidateMidsFromSet = (
85
91
  const out: number[] = []
86
92
  if (bottomNeighbor !== undefined) {
87
93
  out.push((bottomNeighbor + colliding.minY) / 2)
94
+ } else if (allowOpenSideCandidates) {
95
+ out.push(colliding.minY - OBSTACLE_CLEARANCE)
88
96
  }
89
97
  if (topNeighbor !== undefined) {
90
98
  out.push((colliding.maxY + topNeighbor) / 2)
99
+ } else if (allowOpenSideCandidates) {
100
+ out.push(colliding.maxY + OBSTACLE_CLEARANCE)
91
101
  }
92
102
  return out
93
103
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tscircuit/schematic-trace-solver",
3
3
  "main": "dist/index.js",
4
- "version": "0.0.98",
4
+ "version": "0.0.100",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "start": "cosmos",