@tscircuit/schematic-trace-solver 0.0.105 → 0.0.107
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.js +403 -166
- package/lib/solvers/TraceCleanupSolver/minimizeTurnsWithFilteredLabels.ts +78 -27
- package/lib/solvers/TraceCleanupSolver/sameNetRailAlignment/geometry.ts +40 -20
- 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-20260721T221026Z/__snapshots__/bug-report-20260721T221026Z.snap.svg +1 -1
- package/tests/examples/__snapshots__/example13.snap.svg +3 -3
- package/tests/examples/__snapshots__/example29.snap.svg +23 -23
- package/tests/examples/example19.test.ts +4 -0
- package/tests/repros/__snapshots__/repro-example35-minimize-trace-crossing.snap.svg +256 -0
- package/tests/repros/__snapshots__/repro-tps61222-trace-intersection.snap.svg +221 -0
- package/tests/repros/__snapshots__/repro47-endpoint-obstacle-detour.snap.svg +1 -1
- package/tests/repros/assets/repro-example35-minimize-trace-crossing.input.json +1561 -0
- package/tests/repros/assets/repro-tps61222-trace-intersection.input.json +200 -0
- package/tests/repros/repro-example35-minimize-trace-crossing.test.ts +22 -0
- package/tests/repros/repro-tps61222-trace-intersection.test.ts +14 -0
- package/tests/repros/repro47-endpoint-obstacle-detour.test.ts +6 -0
|
@@ -3,13 +3,18 @@ import { minimizeTurns } from "./turnMinimization"
|
|
|
3
3
|
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
4
4
|
import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
5
5
|
import type { NetLabelPlacement } from "../NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
6
|
+
import { countTurns } from "./countTurns"
|
|
7
|
+
import {
|
|
8
|
+
getVisibleTraceLength,
|
|
9
|
+
getVisibleTraceSegmentCount,
|
|
10
|
+
RAIL_ALIGNMENT_EPSILON,
|
|
11
|
+
} from "./sameNetRailAlignment/geometry"
|
|
6
12
|
|
|
7
13
|
/**
|
|
8
|
-
* Minimizes
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* Finally, it uses a turn minimization algorithm to find a new path for the target trace that avoids these combined obstacles.
|
|
14
|
+
* Minimizes turns with a strict pass that treats every other trace as an
|
|
15
|
+
* obstacle and a relaxed pass that permits joining endpoint-sharing same-net
|
|
16
|
+
* branches. Equivalent-turn routes are compared by their rendered same-net
|
|
17
|
+
* complexity so aligned rails win without arbitrarily shifting local routes.
|
|
13
18
|
*/
|
|
14
19
|
export const minimizeTurnsWithFilteredLabels = ({
|
|
15
20
|
targetMspConnectionPairId,
|
|
@@ -33,27 +38,31 @@ export const minimizeTurnsWithFilteredLabels = ({
|
|
|
33
38
|
throw new Error(`Target trace ${targetMspConnectionPairId} not found`)
|
|
34
39
|
}
|
|
35
40
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
(t) =>
|
|
40
|
-
t.mspPairId !== targetMspConnectionPairId &&
|
|
41
|
-
t.globalConnNetId !== targetTrace.globalConnNetId,
|
|
41
|
+
const targetPinIds = new Set(targetTrace.pinIds)
|
|
42
|
+
const otherTraces = traces.filter(
|
|
43
|
+
(trace) => trace.mspPairId !== targetMspConnectionPairId,
|
|
42
44
|
)
|
|
45
|
+
const relaxedObstacleTraces = otherTraces.filter((trace) => {
|
|
46
|
+
const sharesEndpoint = trace.pinIds.some((pinId) => targetPinIds.has(pinId))
|
|
47
|
+
return (
|
|
48
|
+
trace.globalConnNetId !== targetTrace.globalConnNetId || !sharesEndpoint
|
|
49
|
+
)
|
|
50
|
+
})
|
|
43
51
|
|
|
44
52
|
const TRACE_WIDTH = 0.01
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
const getTraceObstacles = (obstacleTraces: SolvedTracePath[]) =>
|
|
54
|
+
obstacleTraces.flatMap((trace, i) =>
|
|
55
|
+
trace.tracePath.slice(0, -1).map((p1, pi) => {
|
|
56
|
+
const p2 = trace.tracePath[pi + 1]!
|
|
57
|
+
return {
|
|
58
|
+
chipId: `trace-obstacle-${i}-${pi}`,
|
|
59
|
+
minX: Math.min(p1.x, p2.x) - TRACE_WIDTH / 2,
|
|
60
|
+
minY: Math.min(p1.y, p2.y) - TRACE_WIDTH / 2,
|
|
61
|
+
maxX: Math.max(p1.x, p2.x) + TRACE_WIDTH / 2,
|
|
62
|
+
maxY: Math.max(p1.y, p2.y) + TRACE_WIDTH / 2,
|
|
63
|
+
}
|
|
64
|
+
}),
|
|
65
|
+
)
|
|
57
66
|
|
|
58
67
|
const staticObstaclesRaw = getObstacleRects(inputProblem)
|
|
59
68
|
const PADDING = 0.01
|
|
@@ -65,8 +74,6 @@ export const minimizeTurnsWithFilteredLabels = ({
|
|
|
65
74
|
maxY: obs.maxY + PADDING,
|
|
66
75
|
}))
|
|
67
76
|
|
|
68
|
-
const combinedObstacles = [...staticObstacles, ...traceObstacles]
|
|
69
|
-
|
|
70
77
|
const originalPath = targetTrace.tracePath
|
|
71
78
|
const filteredLabels = allLabelPlacements.filter((label) => {
|
|
72
79
|
const originalNetIds = mergedLabelNetIdMap[label.globalConnNetId]
|
|
@@ -83,13 +90,57 @@ export const minimizeTurnsWithFilteredLabels = ({
|
|
|
83
90
|
maxY: nl.center.y + nl.height / 2 + paddingBuffer,
|
|
84
91
|
}))
|
|
85
92
|
|
|
86
|
-
const
|
|
93
|
+
const strictPath = minimizeTurns({
|
|
87
94
|
path: originalPath,
|
|
88
|
-
obstacles:
|
|
95
|
+
obstacles: [...staticObstacles, ...getTraceObstacles(otherTraces)],
|
|
89
96
|
labelBounds,
|
|
90
97
|
originalPath: originalPath,
|
|
91
98
|
})
|
|
92
99
|
|
|
100
|
+
const relaxedPath = minimizeTurns({
|
|
101
|
+
path: originalPath,
|
|
102
|
+
obstacles: [
|
|
103
|
+
...staticObstacles,
|
|
104
|
+
...getTraceObstacles(relaxedObstacleTraces),
|
|
105
|
+
],
|
|
106
|
+
labelBounds,
|
|
107
|
+
originalPath: originalPath,
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const sameNetTraces = otherTraces.filter(
|
|
111
|
+
(trace) => trace.globalConnNetId === targetTrace.globalConnNetId,
|
|
112
|
+
)
|
|
113
|
+
const getSameNetReadability = (tracePath: SolvedTracePath["tracePath"]) => {
|
|
114
|
+
const tracesWithCandidate = [
|
|
115
|
+
...sameNetTraces,
|
|
116
|
+
{ ...targetTrace, tracePath },
|
|
117
|
+
]
|
|
118
|
+
return {
|
|
119
|
+
segmentCount: getVisibleTraceSegmentCount(tracesWithCandidate),
|
|
120
|
+
visibleLength: getVisibleTraceLength(tracesWithCandidate),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const strictTurns = countTurns(strictPath)
|
|
125
|
+
const relaxedTurns = countTurns(relaxedPath)
|
|
126
|
+
let newPath = strictPath
|
|
127
|
+
|
|
128
|
+
if (relaxedTurns < strictTurns) {
|
|
129
|
+
newPath = relaxedPath
|
|
130
|
+
} else if (relaxedTurns === strictTurns) {
|
|
131
|
+
const strictReadability = getSameNetReadability(strictPath)
|
|
132
|
+
const relaxedReadability = getSameNetReadability(relaxedPath)
|
|
133
|
+
|
|
134
|
+
if (
|
|
135
|
+
relaxedReadability.segmentCount < strictReadability.segmentCount ||
|
|
136
|
+
(relaxedReadability.segmentCount === strictReadability.segmentCount &&
|
|
137
|
+
relaxedReadability.visibleLength <
|
|
138
|
+
strictReadability.visibleLength - RAIL_ALIGNMENT_EPSILON)
|
|
139
|
+
) {
|
|
140
|
+
newPath = relaxedPath
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
93
144
|
return {
|
|
94
145
|
...targetTrace,
|
|
95
146
|
tracePath: newPath,
|
|
@@ -48,7 +48,7 @@ interface Interval {
|
|
|
48
48
|
max: number
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const
|
|
51
|
+
const getMergedIntervalMetrics = (intervals: Interval[]) => {
|
|
52
52
|
const groups: Interval[][] = []
|
|
53
53
|
for (const interval of intervals) {
|
|
54
54
|
const group = groups.find((items) =>
|
|
@@ -58,28 +58,35 @@ const getMergedIntervalLength = (intervals: Interval[]) => {
|
|
|
58
58
|
else groups.push([interval])
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
return groups.reduce(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
61
|
+
return groups.reduce(
|
|
62
|
+
(total, group) => {
|
|
63
|
+
const sorted = [...group].sort((a, b) => a.min - b.min)
|
|
64
|
+
let groupLength = 0
|
|
65
|
+
let segmentCount = 1
|
|
66
|
+
let currentMin = sorted[0]!.min
|
|
67
|
+
let currentMax = sorted[0]!.max
|
|
68
|
+
|
|
69
|
+
for (const interval of sorted.slice(1)) {
|
|
70
|
+
if (interval.min <= currentMax + RAIL_ALIGNMENT_EPSILON) {
|
|
71
|
+
currentMax = Math.max(currentMax, interval.max)
|
|
72
|
+
} else {
|
|
73
|
+
groupLength += currentMax - currentMin
|
|
74
|
+
segmentCount++
|
|
75
|
+
currentMin = interval.min
|
|
76
|
+
currentMax = interval.max
|
|
77
|
+
}
|
|
74
78
|
}
|
|
75
|
-
}
|
|
76
79
|
|
|
77
|
-
|
|
78
|
-
|
|
80
|
+
return {
|
|
81
|
+
length: total.length + groupLength + currentMax - currentMin,
|
|
82
|
+
segmentCount: total.segmentCount + segmentCount,
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
{ length: 0, segmentCount: 0 },
|
|
86
|
+
)
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
|
|
82
|
-
export const getVisibleTraceLength = (traces: SolvedTracePath[]) => {
|
|
89
|
+
const getVisibleTraceMetrics = (traces: SolvedTracePath[]) => {
|
|
83
90
|
const horizontal: Interval[] = []
|
|
84
91
|
const vertical: Interval[] = []
|
|
85
92
|
|
|
@@ -104,5 +111,18 @@ export const getVisibleTraceLength = (traces: SolvedTracePath[]) => {
|
|
|
104
111
|
}
|
|
105
112
|
}
|
|
106
113
|
|
|
107
|
-
|
|
114
|
+
const horizontalMetrics = getMergedIntervalMetrics(horizontal)
|
|
115
|
+
const verticalMetrics = getMergedIntervalMetrics(vertical)
|
|
116
|
+
return {
|
|
117
|
+
length: horizontalMetrics.length + verticalMetrics.length,
|
|
118
|
+
segmentCount: horizontalMetrics.segmentCount + verticalMetrics.segmentCount,
|
|
119
|
+
}
|
|
108
120
|
}
|
|
121
|
+
|
|
122
|
+
/** Returns rendered length after overlapping collinear runs are merged. */
|
|
123
|
+
export const getVisibleTraceLength = (traces: SolvedTracePath[]) =>
|
|
124
|
+
getVisibleTraceMetrics(traces).length
|
|
125
|
+
|
|
126
|
+
/** Returns the number of distinct collinear runs visible on the schematic. */
|
|
127
|
+
export const getVisibleTraceSegmentCount = (traces: SolvedTracePath[]) =>
|
|
128
|
+
getVisibleTraceMetrics(traces).segmentCount
|
|
@@ -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
|
+
}
|