@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.
- package/dist/index.d.ts +1 -0
- package/dist/index.js +335 -225
- package/lib/solvers/MspConnectionPairSolver/MspConnectionPairSolver.ts +19 -9
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +43 -1
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateEndpointCollisionDetours.ts +84 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/mid.ts +13 -3
- package/package.json +1 -1
- package/tests/bug-reports/bug-report-20260706T220324Z/__snapshots__/bug-report-20260706T220324Z.snap.svg +18 -18
- package/tests/bug-reports/bug-report-20260707T092615Z/__snapshots__/bug-report-20260707T092615Z.snap.svg +7 -7
- package/tests/bug-reports/bug-report-20260707T134549Z/__snapshots__/bug-report-20260707T134549Z.snap.svg +5 -7
- package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +6 -12
- package/tests/examples/__snapshots__/example09.snap.svg +9 -13
- package/tests/examples/__snapshots__/example21.snap.svg +30 -32
- package/tests/examples/__snapshots__/example29.snap.svg +13 -19
- package/tests/repros/__snapshots__/repro129-host-custom-symbol-passives.snap.svg +60 -0
- package/tests/repros/__snapshots__/repro47-endpoint-obstacle-detour.snap.svg +65 -0
- package/tests/repros/__snapshots__/repro5-escape-padded-text-obstacles.snap.svg +74 -0
- package/tests/repros/assets/repro129-host-custom-symbol-passives.input.json +94 -0
- package/tests/repros/assets/repro47-endpoint-obstacle-detour.input.json +133 -0
- package/tests/repros/assets/repro5-escape-padded-text-obstacles.input.json +202 -0
- package/tests/repros/repro129-host-custom-symbol-passives.test.ts +16 -0
- package/tests/repros/repro47-endpoint-obstacle-detour.test.ts +19 -0
- package/tests/repros/repro5-escape-padded-text-obstacles.test.ts +17 -0
- package/tests/solvers/MspConnectionPairSolver/msp-connection-pair-solver-direct-connection-distance.test.ts +46 -0
- package/tests/solvers/SchematicTraceSingleLineSolver2/candidate-mids-from-set.test.ts +39 -0
- 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
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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(
|
|
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 -
|
|
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 -
|
|
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 =
|
|
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
|
}
|