@tscircuit/schematic-trace-solver 0.0.106 → 0.0.108
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 +2 -2
- package/dist/index.js +470 -213
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/SchematicTraceSingleLineSolver2.ts +58 -27
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateEndpointCollisionDetours.ts +1 -10
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/generateInternalSegmentCollisionDetours.ts +94 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/pathOps.ts +10 -0
- package/lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect.ts +5 -1
- package/lib/solvers/TraceCleanupSolver/sub-solver/UntangleTraceSubsolver.ts +178 -9
- package/lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles.ts +59 -0
- package/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts +64 -1
- package/package.json +1 -1
- package/tests/bug-reports/bug-report-20260707T140410Z/__snapshots__/bug-report-20260707T140410Z.snap.svg +3 -3
- package/tests/bug-reports/bug-report-20260708T095725Z/__snapshots__/bug-report-20260708T095725Z.snap.svg +5 -5
- package/tests/bug-reports/bug-report-20260708T095725Z/bug-report-20260708T095725Z.test.ts +15 -0
- package/tests/bug-reports/bug-report-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +1 -1
- package/tests/repros/__snapshots__/repro-tps61222-trace-intersection.snap.svg +64 -0
- package/tests/repros/__snapshots__/repro47-endpoint-obstacle-detour.snap.svg +1 -1
- package/tests/repros/assets/repro-tps61222-trace-intersection.input.json +200 -0
- package/tests/repros/repro-tps61222-trace-intersection.test.ts +41 -0
- package/tests/repros/repro47-endpoint-obstacle-detour.test.ts +6 -0
- package/tests/solvers/SchematicTraceSingleLineSolver2/generate-internal-segment-collision-detours.test.ts +49 -0
|
@@ -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
|
|
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 {
|
|
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<
|
|
62
|
-
endpointTextObstacles: Set<
|
|
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
|
|
105
|
-
(
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
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
|
|
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 }
|
|
@@ -6,8 +6,14 @@ import { ChipObstacleSpatialIndex } from "lib/data-structures/ChipObstacleSpatia
|
|
|
6
6
|
|
|
7
7
|
import { findAllLShapedTurns, type LShape } from "./findAllLShapedTurns"
|
|
8
8
|
import { getTraceObstacles } from "./getTraceObstacles"
|
|
9
|
-
import {
|
|
10
|
-
|
|
9
|
+
import {
|
|
10
|
+
findIntersectionsWithObstacles,
|
|
11
|
+
findPerpendicularPathCrossings,
|
|
12
|
+
} from "./findIntersectionsWithObstacles"
|
|
13
|
+
import {
|
|
14
|
+
generateLShapeRerouteCandidates,
|
|
15
|
+
generatePerpendicularTraceDetours,
|
|
16
|
+
} from "./generateLShapeRerouteCandidates"
|
|
11
17
|
import { isPathColliding, type CollisionInfo } from "./isPathColliding"
|
|
12
18
|
import {
|
|
13
19
|
generateRectangleCandidates,
|
|
@@ -24,6 +30,19 @@ import { visualizeTightRectangle } from "../visualizeTightRectangle"
|
|
|
24
30
|
import { visualizeCandidates } from "./visualizeCandidates"
|
|
25
31
|
import { mergeGraphicsObjects } from "../mergeGraphicsObjects"
|
|
26
32
|
import { visualizeCollision } from "./visualizeCollision"
|
|
33
|
+
import {
|
|
34
|
+
getPathLength,
|
|
35
|
+
isPathCollidingWithChipInterior,
|
|
36
|
+
} from "../../Example28Solver/geometry"
|
|
37
|
+
import { getObstacleRects } from "../../SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
38
|
+
|
|
39
|
+
interface TraceCrossing {
|
|
40
|
+
trace: SolvedTracePath
|
|
41
|
+
segmentIndex: number
|
|
42
|
+
otherTrace: SolvedTracePath
|
|
43
|
+
otherSegmentIndex: number
|
|
44
|
+
isInitialBundleCrossing: boolean
|
|
45
|
+
}
|
|
27
46
|
|
|
28
47
|
/**
|
|
29
48
|
* Defines the input structure for the UntangleTraceSubsolver.
|
|
@@ -60,6 +79,9 @@ export class UntangleTraceSubsolver extends BaseSolver {
|
|
|
60
79
|
private input: UntangleTraceSubsolverInput
|
|
61
80
|
private chipObstacleSpatialIndex: ChipObstacleSpatialIndex
|
|
62
81
|
private lShapesToProcess: LShape[] = []
|
|
82
|
+
private ignoredCrossings = new Set<string>()
|
|
83
|
+
private reroutedTraceIds = new Set<string>()
|
|
84
|
+
private processingCrossings = true
|
|
63
85
|
private visualizationMode: VisualizationMode = "l_shapes"
|
|
64
86
|
|
|
65
87
|
private currentLShape: LShape | null = null
|
|
@@ -95,13 +117,6 @@ export class UntangleTraceSubsolver extends BaseSolver {
|
|
|
95
117
|
this.input.inputProblem._chipObstacleSpatialIndex =
|
|
96
118
|
this.chipObstacleSpatialIndex
|
|
97
119
|
}
|
|
98
|
-
|
|
99
|
-
for (const trace of this.input.allTraces) {
|
|
100
|
-
const lShapes = findAllLShapedTurns(trace.tracePath)
|
|
101
|
-
this.lShapesToProcess.push(
|
|
102
|
-
...lShapes.map((l) => ({ ...l, traceId: trace.mspPairId as string })),
|
|
103
|
-
)
|
|
104
|
-
}
|
|
105
120
|
}
|
|
106
121
|
|
|
107
122
|
override _step(): void {
|
|
@@ -110,6 +125,21 @@ export class UntangleTraceSubsolver extends BaseSolver {
|
|
|
110
125
|
return
|
|
111
126
|
}
|
|
112
127
|
|
|
128
|
+
if (this.processingCrossings) {
|
|
129
|
+
// The L-shape pass below only reacts when both arms intersect obstacles.
|
|
130
|
+
// Resolve strict crossings on merged-label bundles before entering it.
|
|
131
|
+
const crossing = this._findCrossing()
|
|
132
|
+
if (crossing) {
|
|
133
|
+
if (!this._resolveCrossing(crossing)) {
|
|
134
|
+
this.ignoredCrossings.add(this._crossingKey(crossing))
|
|
135
|
+
}
|
|
136
|
+
return
|
|
137
|
+
}
|
|
138
|
+
this.processingCrossings = false
|
|
139
|
+
this._initializeLShapes()
|
|
140
|
+
return
|
|
141
|
+
}
|
|
142
|
+
|
|
113
143
|
if (this.lShapeJustProcessed) {
|
|
114
144
|
this._resetAfterLShapProcessing()
|
|
115
145
|
return
|
|
@@ -136,6 +166,145 @@ export class UntangleTraceSubsolver extends BaseSolver {
|
|
|
136
166
|
}
|
|
137
167
|
}
|
|
138
168
|
|
|
169
|
+
private _initializeLShapes() {
|
|
170
|
+
for (const trace of this.input.allTraces) {
|
|
171
|
+
this.lShapesToProcess.push(
|
|
172
|
+
...findAllLShapedTurns(trace.tracePath).map((lShape) => ({
|
|
173
|
+
...lShape,
|
|
174
|
+
traceId: trace.mspPairId,
|
|
175
|
+
})),
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private _crossingKey(crossing: TraceCrossing) {
|
|
181
|
+
return `${crossing.trace.mspPairId}:${crossing.segmentIndex}:${crossing.otherTrace.mspPairId}:${crossing.otherSegmentIndex}`
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private _isTraceBundle(first: SolvedTracePath, second: SolvedTracePath) {
|
|
185
|
+
const componentPair = (trace: SolvedTracePath) =>
|
|
186
|
+
trace.pins
|
|
187
|
+
.map((pin) => pin.chipId)
|
|
188
|
+
.sort()
|
|
189
|
+
.join(":")
|
|
190
|
+
if (componentPair(first) !== componentPair(second)) return false
|
|
191
|
+
|
|
192
|
+
return Object.values(this.input.mergedLabelNetIdMap).some(
|
|
193
|
+
(netIds) =>
|
|
194
|
+
netIds.has(first.globalConnNetId) && netIds.has(second.globalConnNetId),
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
private _findCrossing(): TraceCrossing | null {
|
|
199
|
+
const traces = this.input.allTraces
|
|
200
|
+
for (let firstIndex = 0; firstIndex < traces.length; firstIndex++) {
|
|
201
|
+
const trace = traces[firstIndex]!
|
|
202
|
+
for (
|
|
203
|
+
let secondIndex = firstIndex + 1;
|
|
204
|
+
secondIndex < traces.length;
|
|
205
|
+
secondIndex++
|
|
206
|
+
) {
|
|
207
|
+
const otherTrace = traces[secondIndex]!
|
|
208
|
+
if (trace.globalConnNetId === otherTrace.globalConnNetId) continue
|
|
209
|
+
const isInitialBundleCrossing = this._isTraceBundle(trace, otherTrace)
|
|
210
|
+
if (
|
|
211
|
+
!isInitialBundleCrossing &&
|
|
212
|
+
!this.reroutedTraceIds.has(trace.mspPairId) &&
|
|
213
|
+
!this.reroutedTraceIds.has(otherTrace.mspPairId)
|
|
214
|
+
) {
|
|
215
|
+
continue
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const crossings = findPerpendicularPathCrossings(
|
|
219
|
+
trace.tracePath,
|
|
220
|
+
otherTrace.tracePath,
|
|
221
|
+
)
|
|
222
|
+
for (const { pathSegmentIndex, otherPathSegmentIndex } of crossings) {
|
|
223
|
+
const crossing = {
|
|
224
|
+
trace,
|
|
225
|
+
segmentIndex: pathSegmentIndex,
|
|
226
|
+
otherTrace,
|
|
227
|
+
otherSegmentIndex: otherPathSegmentIndex,
|
|
228
|
+
isInitialBundleCrossing,
|
|
229
|
+
}
|
|
230
|
+
if (!this.ignoredCrossings.has(this._crossingKey(crossing))) {
|
|
231
|
+
return crossing
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
return null
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private _resolveCrossing(crossing: TraceCrossing) {
|
|
240
|
+
const chipBounds = this.chipObstacleSpatialIndex.chips.map(
|
|
241
|
+
(chip) => chip.bounds,
|
|
242
|
+
)
|
|
243
|
+
const candidates = [
|
|
244
|
+
...generatePerpendicularTraceDetours({
|
|
245
|
+
trace: crossing.trace,
|
|
246
|
+
segmentIndex: crossing.segmentIndex,
|
|
247
|
+
obstacleStart:
|
|
248
|
+
crossing.otherTrace.tracePath[crossing.otherSegmentIndex]!,
|
|
249
|
+
obstacleEnd:
|
|
250
|
+
crossing.otherTrace.tracePath[crossing.otherSegmentIndex + 1]!,
|
|
251
|
+
chipBounds,
|
|
252
|
+
clearance: this.input.paddingBuffer,
|
|
253
|
+
}),
|
|
254
|
+
...generatePerpendicularTraceDetours({
|
|
255
|
+
trace: crossing.otherTrace,
|
|
256
|
+
segmentIndex: crossing.otherSegmentIndex,
|
|
257
|
+
obstacleStart: crossing.trace.tracePath[crossing.segmentIndex]!,
|
|
258
|
+
obstacleEnd: crossing.trace.tracePath[crossing.segmentIndex + 1]!,
|
|
259
|
+
chipBounds,
|
|
260
|
+
clearance: this.input.paddingBuffer,
|
|
261
|
+
}),
|
|
262
|
+
]
|
|
263
|
+
const chipObstacles = getObstacleRects(this.input.inputProblem).filter(
|
|
264
|
+
(obstacle) => obstacle.kind === "chip",
|
|
265
|
+
)
|
|
266
|
+
// Prefer a globally clear route. If the initial bundle detour transfers
|
|
267
|
+
// the crossing, continue rip-up/reroute from that moved trace until clear.
|
|
268
|
+
const validCandidates = candidates
|
|
269
|
+
.map((candidate) => ({
|
|
270
|
+
...candidate,
|
|
271
|
+
collision: isPathColliding(
|
|
272
|
+
candidate.path,
|
|
273
|
+
this.input.allTraces,
|
|
274
|
+
candidate.traceId,
|
|
275
|
+
),
|
|
276
|
+
}))
|
|
277
|
+
.filter(
|
|
278
|
+
(candidate) =>
|
|
279
|
+
!isPathCollidingWithChipInterior(candidate.path, chipObstacles) &&
|
|
280
|
+
!isPathColliding(
|
|
281
|
+
candidate.path,
|
|
282
|
+
[crossing.trace, crossing.otherTrace],
|
|
283
|
+
candidate.traceId,
|
|
284
|
+
).isColliding &&
|
|
285
|
+
(crossing.isInitialBundleCrossing ||
|
|
286
|
+
!candidate.collision.isColliding),
|
|
287
|
+
)
|
|
288
|
+
.sort(
|
|
289
|
+
(first, second) =>
|
|
290
|
+
Number(first.collision.isColliding) -
|
|
291
|
+
Number(second.collision.isColliding) ||
|
|
292
|
+
getPathLength(first.path) - getPathLength(second.path),
|
|
293
|
+
)
|
|
294
|
+
const bestCandidate = validCandidates[0]
|
|
295
|
+
if (!bestCandidate) return false
|
|
296
|
+
|
|
297
|
+
const traceIndex = this.input.allTraces.findIndex(
|
|
298
|
+
(trace) => trace.mspPairId === bestCandidate.traceId,
|
|
299
|
+
)
|
|
300
|
+
this.input.allTraces[traceIndex] = {
|
|
301
|
+
...this.input.allTraces[traceIndex]!,
|
|
302
|
+
tracePath: bestCandidate.path,
|
|
303
|
+
}
|
|
304
|
+
this.reroutedTraceIds.add(bestCandidate.traceId)
|
|
305
|
+
return true
|
|
306
|
+
}
|
|
307
|
+
|
|
139
308
|
private _resetAfterLShapProcessing() {
|
|
140
309
|
this.lShapeProcessingStep = "idle"
|
|
141
310
|
this.currentLShape = null
|
|
@@ -2,6 +2,13 @@ import type { Point } from "@tscircuit/math-utils"
|
|
|
2
2
|
import { getSegmentIntersection } from "@tscircuit/math-utils/line-intersections"
|
|
3
3
|
import type { TraceObstacle } from "./getTraceObstacles"
|
|
4
4
|
|
|
5
|
+
const EPS = 1e-6
|
|
6
|
+
|
|
7
|
+
export interface PerpendicularPathCrossing {
|
|
8
|
+
pathSegmentIndex: number
|
|
9
|
+
otherPathSegmentIndex: number
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
/**
|
|
6
13
|
* Finds all intersection points between a given line segment (p1-p2) and a list of trace obstacles.
|
|
7
14
|
* It iterates through each segment of every obstacle and checks for intersections with the input segment.
|
|
@@ -34,3 +41,55 @@ export const findIntersectionsWithObstacles = (
|
|
|
34
41
|
|
|
35
42
|
return intersections
|
|
36
43
|
}
|
|
44
|
+
|
|
45
|
+
const isSamePoint = (first: Point, second: Point) =>
|
|
46
|
+
Math.abs(first.x - second.x) < EPS && Math.abs(first.y - second.y) < EPS
|
|
47
|
+
|
|
48
|
+
export const findPerpendicularPathCrossings = (
|
|
49
|
+
path: Point[],
|
|
50
|
+
otherPath: Point[],
|
|
51
|
+
): PerpendicularPathCrossing[] => {
|
|
52
|
+
const crossings: PerpendicularPathCrossing[] = []
|
|
53
|
+
|
|
54
|
+
// Terminal segments connect to pins and are allowed to meet other traces at
|
|
55
|
+
// their endpoints. Only internal, strict crossings need to be untangled.
|
|
56
|
+
for (
|
|
57
|
+
let pathSegmentIndex = 1;
|
|
58
|
+
pathSegmentIndex < path.length - 2;
|
|
59
|
+
pathSegmentIndex++
|
|
60
|
+
) {
|
|
61
|
+
const start = path[pathSegmentIndex]!
|
|
62
|
+
const end = path[pathSegmentIndex + 1]!
|
|
63
|
+
const isVertical = Math.abs(start.x - end.x) < EPS
|
|
64
|
+
|
|
65
|
+
for (
|
|
66
|
+
let otherPathSegmentIndex = 1;
|
|
67
|
+
otherPathSegmentIndex < otherPath.length - 2;
|
|
68
|
+
otherPathSegmentIndex++
|
|
69
|
+
) {
|
|
70
|
+
const otherStart = otherPath[otherPathSegmentIndex]!
|
|
71
|
+
const otherEnd = otherPath[otherPathSegmentIndex + 1]!
|
|
72
|
+
const otherIsVertical = Math.abs(otherStart.x - otherEnd.x) < EPS
|
|
73
|
+
if (isVertical === otherIsVertical) continue
|
|
74
|
+
|
|
75
|
+
const intersection = getSegmentIntersection(
|
|
76
|
+
start,
|
|
77
|
+
end,
|
|
78
|
+
otherStart,
|
|
79
|
+
otherEnd,
|
|
80
|
+
)
|
|
81
|
+
if (
|
|
82
|
+
!intersection ||
|
|
83
|
+
[start, end, otherStart, otherEnd].some((point) =>
|
|
84
|
+
isSamePoint(point, intersection),
|
|
85
|
+
)
|
|
86
|
+
) {
|
|
87
|
+
continue
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
crossings.push({ pathSegmentIndex, otherPathSegmentIndex })
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return crossings
|
|
95
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import type { Point } from "@tscircuit/math-utils"
|
|
1
|
+
import type { Bounds, Point } from "@tscircuit/math-utils"
|
|
2
2
|
import type { LShape } from "./findAllLShapedTurns"
|
|
3
3
|
import type { Rectangle } from "./generateRectangleCandidates"
|
|
4
|
+
import type { SolvedTracePath } from "../../SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
5
|
+
import { simplifyPath } from "../simplifyPath"
|
|
4
6
|
|
|
5
7
|
const EPS = 1e-6
|
|
6
8
|
|
|
@@ -105,3 +107,64 @@ export const generateLShapeRerouteCandidates = ({
|
|
|
105
107
|
|
|
106
108
|
return [[i1_padded, c2, i2_padded]]
|
|
107
109
|
}
|
|
110
|
+
|
|
111
|
+
export interface PerpendicularTraceDetourInput {
|
|
112
|
+
trace: SolvedTracePath
|
|
113
|
+
segmentIndex: number
|
|
114
|
+
obstacleStart: Point
|
|
115
|
+
obstacleEnd: Point
|
|
116
|
+
chipBounds: Bounds[]
|
|
117
|
+
clearance: number
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface TraceDetourCandidate {
|
|
121
|
+
traceId: string
|
|
122
|
+
path: Point[]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export const generatePerpendicularTraceDetours = ({
|
|
126
|
+
trace,
|
|
127
|
+
segmentIndex,
|
|
128
|
+
obstacleStart,
|
|
129
|
+
obstacleEnd,
|
|
130
|
+
chipBounds,
|
|
131
|
+
clearance,
|
|
132
|
+
}: PerpendicularTraceDetourInput): TraceDetourCandidate[] => {
|
|
133
|
+
const buildDetours = (path: Point[], index: number) => {
|
|
134
|
+
const start = path[index]!
|
|
135
|
+
const end = path[index + 1]!
|
|
136
|
+
const movingAxis: "x" | "y" = Math.abs(start.x - end.x) < EPS ? "y" : "x"
|
|
137
|
+
const detourAxis = movingAxis === "x" ? "y" : "x"
|
|
138
|
+
const gate =
|
|
139
|
+
obstacleStart[movingAxis] +
|
|
140
|
+
Math.sign(start[movingAxis] - obstacleStart[movingAxis]) * clearance
|
|
141
|
+
const obstacleRange = [obstacleStart[detourAxis], obstacleEnd[detourAxis]]
|
|
142
|
+
const lowBound = detourAxis === "x" ? "minX" : "minY"
|
|
143
|
+
const highBound = detourAxis === "x" ? "maxX" : "maxY"
|
|
144
|
+
const detourCoordinates = [
|
|
145
|
+
Math.min(...obstacleRange) - clearance,
|
|
146
|
+
Math.max(...obstacleRange) + clearance,
|
|
147
|
+
...chipBounds.flatMap((bounds) => [
|
|
148
|
+
bounds[lowBound] - clearance,
|
|
149
|
+
bounds[highBound] + clearance,
|
|
150
|
+
]),
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
return [...new Set(detourCoordinates)].map((detour) =>
|
|
154
|
+
simplifyPath([
|
|
155
|
+
...path.slice(0, index + 1),
|
|
156
|
+
{ ...start, [movingAxis]: gate },
|
|
157
|
+
{ ...start, [movingAxis]: gate, [detourAxis]: detour },
|
|
158
|
+
{ ...end, [detourAxis]: detour },
|
|
159
|
+
...path.slice(index + 2),
|
|
160
|
+
]),
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const reversedPath = [...trace.tracePath].reverse()
|
|
165
|
+
const reversedIndex = trace.tracePath.length - 2 - segmentIndex
|
|
166
|
+
return [
|
|
167
|
+
...buildDetours(trace.tracePath, segmentIndex),
|
|
168
|
+
...buildDetours(reversedPath, reversedIndex).map((path) => path.reverse()),
|
|
169
|
+
].map((path) => ({ traceId: trace.mspPairId, path }))
|
|
170
|
+
}
|