@tscircuit/schematic-trace-solver 0.0.177 → 0.0.178
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 +4 -1
- package/dist/index.js +286 -120
- package/lib/solvers/AvailableNetOrientationSolver/AvailableNetOrientationSolver.ts +1 -0
- package/lib/solvers/SchematicTracePipelineSolver/SchematicTracePipelineSolver.ts +6 -1
- package/lib/solvers/TraceCleanupSolver/TraceCleanupSolver.ts +25 -0
- package/lib/solvers/TraceCleanupSolver/rerouteGeneratedNetLabelConnectorCrossings.ts +194 -0
- package/lib/solvers/TraceCleanupSolver/sub-solver/findIntersectionsWithObstacles.ts +19 -5
- package/lib/solvers/TraceCleanupSolver/sub-solver/generateLShapeRerouteCandidates.ts +35 -5
- package/package.json +1 -1
- package/tests/repros/__snapshots__/repro-wireless-mouse-charger-section.snap.svg +2 -2
- package/tests/solvers/TraceCleanupSolver/alignSameNetRails-pipeline-label-connector.test.ts +4 -2
- package/tests/solvers/TraceCleanupSolver/reroute-generated-net-label-connector-crossings.test.ts +160 -0
|
@@ -467,12 +467,17 @@ export class SchematicTracePipelineSolver extends BaseSolver {
|
|
|
467
467
|
allLabelPlacements: collisionOutput.netLabelPlacements,
|
|
468
468
|
mergedLabelNetIdMap: labelMergingOutput.mergedLabelNetIdMap,
|
|
469
469
|
paddingBuffer: 0.1,
|
|
470
|
-
operations: [
|
|
470
|
+
operations: [
|
|
471
|
+
"rerouting_generated_net_label_connector_crossings",
|
|
472
|
+
"aligning_same_net_rails",
|
|
473
|
+
],
|
|
471
474
|
eligibleTraceIds: new Set(
|
|
472
475
|
instance
|
|
473
476
|
.traceCleanupSolver!.getOutput()
|
|
474
477
|
.traces.map((trace) => trace.mspPairId),
|
|
475
478
|
),
|
|
479
|
+
netLabelConnectorTraceIds:
|
|
480
|
+
instance.availableNetOrientationSolver!.netLabelConnectorTraceIds,
|
|
476
481
|
},
|
|
477
482
|
]
|
|
478
483
|
},
|
|
@@ -10,6 +10,7 @@ import { alignSameNetRails } from "./alignSameNetRails"
|
|
|
10
10
|
|
|
11
11
|
export type TraceCleanupOperation =
|
|
12
12
|
| "untangling_traces"
|
|
13
|
+
| "rerouting_generated_net_label_connector_crossings"
|
|
13
14
|
| "minimizing_turns"
|
|
14
15
|
| "balancing_l_shapes"
|
|
15
16
|
| "aligning_same_net_rails"
|
|
@@ -25,10 +26,12 @@ export interface TraceCleanupSolverInput {
|
|
|
25
26
|
paddingBuffer: number
|
|
26
27
|
operations?: readonly TraceCleanupOperation[]
|
|
27
28
|
eligibleTraceIds?: ReadonlySet<string>
|
|
29
|
+
netLabelConnectorTraceIds?: ReadonlySet<string>
|
|
28
30
|
}
|
|
29
31
|
|
|
30
32
|
import { UntangleTraceSubsolver } from "./sub-solver/UntangleTraceSubsolver"
|
|
31
33
|
import { is4PointRectangle } from "./is4PointRectangle"
|
|
34
|
+
import { rerouteGeneratedNetLabelConnectorCrossings } from "./rerouteGeneratedNetLabelConnectorCrossings"
|
|
32
35
|
|
|
33
36
|
/**
|
|
34
37
|
* Represents the different stages or steps within the trace cleanup pipeline.
|
|
@@ -97,6 +100,9 @@ export class TraceCleanupSolver extends BaseSolver {
|
|
|
97
100
|
case "untangling_traces":
|
|
98
101
|
this._runUntangleTracesStep()
|
|
99
102
|
break
|
|
103
|
+
case "rerouting_generated_net_label_connector_crossings":
|
|
104
|
+
this._runGeneratedNetLabelConnectorCrossingRerouteStep()
|
|
105
|
+
break
|
|
100
106
|
case "minimizing_turns":
|
|
101
107
|
this._runMinimizeTurnsStep()
|
|
102
108
|
break
|
|
@@ -123,6 +129,25 @@ export class TraceCleanupSolver extends BaseSolver {
|
|
|
123
129
|
})
|
|
124
130
|
}
|
|
125
131
|
|
|
132
|
+
private _runGeneratedNetLabelConnectorCrossingRerouteStep() {
|
|
133
|
+
const result = rerouteGeneratedNetLabelConnectorCrossings({
|
|
134
|
+
inputProblem: this.input.inputProblem,
|
|
135
|
+
traces: this.outputTraces,
|
|
136
|
+
netLabelPlacements: this.input.allLabelPlacements,
|
|
137
|
+
mergedLabelNetIdMap: this.input.mergedLabelNetIdMap,
|
|
138
|
+
clearance: this.input.paddingBuffer,
|
|
139
|
+
eligibleTraceIds: this.input.eligibleTraceIds,
|
|
140
|
+
connectorTraceIds: this.input.netLabelConnectorTraceIds ?? new Set(),
|
|
141
|
+
})
|
|
142
|
+
this.outputTraces = result.traces
|
|
143
|
+
this.tracesMap = new Map(
|
|
144
|
+
this.outputTraces.map((trace) => [trace.mspPairId, trace]),
|
|
145
|
+
)
|
|
146
|
+
this.stats.reroutedGeneratedConnectorCrossingTraceCount =
|
|
147
|
+
result.reroutedTraceCount
|
|
148
|
+
this._advancePipeline()
|
|
149
|
+
}
|
|
150
|
+
|
|
126
151
|
private _runMinimizeTurnsStep() {
|
|
127
152
|
if (this.traceIdQueue.length === 0) {
|
|
128
153
|
this._advancePipeline()
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import type { Bounds } from "@tscircuit/math-utils"
|
|
2
|
+
import { getPathLength } from "lib/solvers/Example28Solver/geometry"
|
|
3
|
+
import type { NetLabelPlacement } from "lib/solvers/NetLabelPlacementSolver/NetLabelPlacementSolver"
|
|
4
|
+
import { getRectBounds } from "lib/solvers/NetLabelPlacementSolver/SingleNetLabelPlacementSolver/geometry"
|
|
5
|
+
import { tracePathContainsPoint } from "lib/solvers/RailNetLabelCornerPlacementSolver/geometry"
|
|
6
|
+
import type { SolvedTracePath } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceLinesSolver"
|
|
7
|
+
import { isPathCollidingWithObstacles } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/collisions"
|
|
8
|
+
import { getObstacleRects } from "lib/solvers/SchematicTraceLinesSolver/SchematicTraceSingleLineSolver2/rect"
|
|
9
|
+
import type { InputProblem } from "lib/types/InputProblem"
|
|
10
|
+
import { doesPathCoincideWithTraces } from "lib/utils/doesPathCoincideWithTraces"
|
|
11
|
+
import { countTurns } from "./countTurns"
|
|
12
|
+
import { hasCollisionsWithLabels } from "./hasCollisionsWithLabels"
|
|
13
|
+
import { findPerpendicularPathCrossings } from "./sub-solver/findIntersectionsWithObstacles"
|
|
14
|
+
import { generatePerpendicularTraceDetours } from "./sub-solver/generateLShapeRerouteCandidates"
|
|
15
|
+
import { isPathColliding } from "./sub-solver/isPathColliding"
|
|
16
|
+
|
|
17
|
+
const EPS = 1e-6
|
|
18
|
+
|
|
19
|
+
const getConnectorObstacleBounds = (
|
|
20
|
+
connector: SolvedTracePath,
|
|
21
|
+
segmentIndex: number,
|
|
22
|
+
labels: NetLabelPlacement[],
|
|
23
|
+
): Bounds => {
|
|
24
|
+
const start = connector.tracePath[segmentIndex]!
|
|
25
|
+
const end = connector.tracePath[segmentIndex + 1]!
|
|
26
|
+
const bounds = {
|
|
27
|
+
minX: Math.min(start.x, end.x),
|
|
28
|
+
minY: Math.min(start.y, end.y),
|
|
29
|
+
maxX: Math.max(start.x, end.x),
|
|
30
|
+
maxY: Math.max(start.y, end.y),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const label of labels) {
|
|
34
|
+
if (label.globalConnNetId !== connector.globalConnNetId) continue
|
|
35
|
+
if (!label.pinIds.every((pinId) => connector.pinIds.includes(pinId)))
|
|
36
|
+
continue
|
|
37
|
+
if (!tracePathContainsPoint(connector.tracePath, label.anchorPoint))
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
const labelBounds = getRectBounds(label.center, label.width, label.height)
|
|
41
|
+
bounds.minX = Math.min(bounds.minX, labelBounds.minX)
|
|
42
|
+
bounds.minY = Math.min(bounds.minY, labelBounds.minY)
|
|
43
|
+
bounds.maxX = Math.max(bounds.maxX, labelBounds.maxX)
|
|
44
|
+
bounds.maxY = Math.max(bounds.maxY, labelBounds.maxY)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return bounds
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const getEligibleConnectorCrossings = (
|
|
51
|
+
traces: SolvedTracePath[],
|
|
52
|
+
connectorTraceIds: ReadonlySet<string>,
|
|
53
|
+
eligibleTraceIds?: ReadonlySet<string>,
|
|
54
|
+
) => {
|
|
55
|
+
const crossings: Array<{
|
|
56
|
+
traceIndex: number
|
|
57
|
+
connector: SolvedTracePath
|
|
58
|
+
connectorSegmentIndex: number
|
|
59
|
+
traceSegmentIndex: number
|
|
60
|
+
}> = []
|
|
61
|
+
|
|
62
|
+
for (const connector of traces) {
|
|
63
|
+
if (!connectorTraceIds.has(connector.mspPairId)) continue
|
|
64
|
+
|
|
65
|
+
for (let traceIndex = 0; traceIndex < traces.length; traceIndex++) {
|
|
66
|
+
const trace = traces[traceIndex]!
|
|
67
|
+
if (trace.mspPairId === connector.mspPairId) continue
|
|
68
|
+
if (connectorTraceIds.has(trace.mspPairId)) continue
|
|
69
|
+
if (trace.globalConnNetId === connector.globalConnNetId) continue
|
|
70
|
+
if (eligibleTraceIds && !eligibleTraceIds.has(trace.mspPairId)) continue
|
|
71
|
+
|
|
72
|
+
for (const crossing of findPerpendicularPathCrossings(
|
|
73
|
+
trace.tracePath,
|
|
74
|
+
connector.tracePath,
|
|
75
|
+
{ includeTerminalSegments: true },
|
|
76
|
+
)) {
|
|
77
|
+
crossings.push({
|
|
78
|
+
traceIndex,
|
|
79
|
+
connector,
|
|
80
|
+
connectorSegmentIndex: crossing.otherPathSegmentIndex,
|
|
81
|
+
traceSegmentIndex: crossing.pathSegmentIndex,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return crossings
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Generated label connectors are added after the component traces are routed.
|
|
91
|
+
// Reuse the normal perpendicular-detour candidates here, keeping connectors
|
|
92
|
+
// fixed and considering only traces that came from the original routing pass.
|
|
93
|
+
export const rerouteGeneratedNetLabelConnectorCrossings = ({
|
|
94
|
+
inputProblem,
|
|
95
|
+
traces,
|
|
96
|
+
netLabelPlacements,
|
|
97
|
+
mergedLabelNetIdMap,
|
|
98
|
+
clearance,
|
|
99
|
+
eligibleTraceIds,
|
|
100
|
+
connectorTraceIds,
|
|
101
|
+
}: {
|
|
102
|
+
inputProblem: InputProblem
|
|
103
|
+
traces: SolvedTracePath[]
|
|
104
|
+
netLabelPlacements: NetLabelPlacement[]
|
|
105
|
+
mergedLabelNetIdMap: Record<string, Set<string>>
|
|
106
|
+
clearance: number
|
|
107
|
+
eligibleTraceIds?: ReadonlySet<string>
|
|
108
|
+
connectorTraceIds: ReadonlySet<string>
|
|
109
|
+
}) => {
|
|
110
|
+
const outputTraces = [...traces]
|
|
111
|
+
const componentAndTextObstacles = getObstacleRects(inputProblem).filter(
|
|
112
|
+
(obstacle) => obstacle.kind === "chip" || obstacle.kind === "text_box",
|
|
113
|
+
)
|
|
114
|
+
let reroutedTraceCount = 0
|
|
115
|
+
|
|
116
|
+
while (true) {
|
|
117
|
+
const candidates = getEligibleConnectorCrossings(
|
|
118
|
+
outputTraces,
|
|
119
|
+
connectorTraceIds,
|
|
120
|
+
eligibleTraceIds,
|
|
121
|
+
).flatMap((crossing) => {
|
|
122
|
+
const trace = outputTraces[crossing.traceIndex]!
|
|
123
|
+
const obstacleBounds = getConnectorObstacleBounds(
|
|
124
|
+
crossing.connector,
|
|
125
|
+
crossing.connectorSegmentIndex,
|
|
126
|
+
netLabelPlacements,
|
|
127
|
+
)
|
|
128
|
+
const foreignTraces = outputTraces.filter(
|
|
129
|
+
(otherTrace) =>
|
|
130
|
+
otherTrace.mspPairId !== trace.mspPairId &&
|
|
131
|
+
otherTrace.globalConnNetId !== trace.globalConnNetId,
|
|
132
|
+
)
|
|
133
|
+
const foreignLabelBounds = netLabelPlacements
|
|
134
|
+
.filter((label) => {
|
|
135
|
+
const mergedNetIds = mergedLabelNetIdMap[label.globalConnNetId]
|
|
136
|
+
return mergedNetIds
|
|
137
|
+
? !mergedNetIds.has(trace.globalConnNetId)
|
|
138
|
+
: label.globalConnNetId !== trace.globalConnNetId
|
|
139
|
+
})
|
|
140
|
+
.map((label) => getRectBounds(label.center, label.width, label.height))
|
|
141
|
+
|
|
142
|
+
return generatePerpendicularTraceDetours({
|
|
143
|
+
trace,
|
|
144
|
+
segmentIndex: crossing.traceSegmentIndex,
|
|
145
|
+
obstacleStart:
|
|
146
|
+
crossing.connector.tracePath[crossing.connectorSegmentIndex]!,
|
|
147
|
+
obstacleEnd:
|
|
148
|
+
crossing.connector.tracePath[crossing.connectorSegmentIndex + 1]!,
|
|
149
|
+
obstacleBounds,
|
|
150
|
+
chipBounds: [],
|
|
151
|
+
clearance,
|
|
152
|
+
})
|
|
153
|
+
.filter(
|
|
154
|
+
(candidate) =>
|
|
155
|
+
getPathLength(candidate.path) <=
|
|
156
|
+
getPathLength(trace.tracePath) + EPS &&
|
|
157
|
+
countTurns(candidate.path) <= countTurns(trace.tracePath) + 2 &&
|
|
158
|
+
!isPathCollidingWithObstacles(
|
|
159
|
+
candidate.path,
|
|
160
|
+
componentAndTextObstacles,
|
|
161
|
+
) &&
|
|
162
|
+
!hasCollisionsWithLabels(candidate.path, foreignLabelBounds) &&
|
|
163
|
+
!isPathColliding(candidate.path, foreignTraces, trace.mspPairId)
|
|
164
|
+
.isColliding &&
|
|
165
|
+
!doesPathCoincideWithTraces(candidate.path, foreignTraces),
|
|
166
|
+
)
|
|
167
|
+
.map((candidate) => ({
|
|
168
|
+
...candidate,
|
|
169
|
+
traceIndex: crossing.traceIndex,
|
|
170
|
+
}))
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
candidates.sort((first, second) => {
|
|
174
|
+
const lengthDifference =
|
|
175
|
+
getPathLength(first.path) - getPathLength(second.path)
|
|
176
|
+
return Math.abs(lengthDifference) > EPS
|
|
177
|
+
? lengthDifference
|
|
178
|
+
: countTurns(first.path) - countTurns(second.path)
|
|
179
|
+
})
|
|
180
|
+
const bestCandidate = candidates[0]
|
|
181
|
+
if (!bestCandidate) break
|
|
182
|
+
|
|
183
|
+
outputTraces[bestCandidate.traceIndex] = {
|
|
184
|
+
...outputTraces[bestCandidate.traceIndex]!,
|
|
185
|
+
tracePath: bestCandidate.path,
|
|
186
|
+
}
|
|
187
|
+
reroutedTraceCount++
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
traces: outputTraces,
|
|
192
|
+
reroutedTraceCount,
|
|
193
|
+
}
|
|
194
|
+
}
|
|
@@ -9,6 +9,10 @@ export interface PerpendicularPathCrossing {
|
|
|
9
9
|
otherPathSegmentIndex: number
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
interface FindPerpendicularPathCrossingsOptions {
|
|
13
|
+
includeTerminalSegments?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
12
16
|
/**
|
|
13
17
|
* Finds all intersection points between a given line segment (p1-p2) and a list of trace obstacles.
|
|
14
18
|
* It iterates through each segment of every obstacle and checks for intersections with the input segment.
|
|
@@ -48,14 +52,24 @@ const isSamePoint = (first: Point, second: Point) =>
|
|
|
48
52
|
export const findPerpendicularPathCrossings = (
|
|
49
53
|
path: Point[],
|
|
50
54
|
otherPath: Point[],
|
|
55
|
+
options: FindPerpendicularPathCrossingsOptions = {},
|
|
51
56
|
): PerpendicularPathCrossing[] => {
|
|
52
57
|
const crossings: PerpendicularPathCrossing[] = []
|
|
58
|
+
const firstPathSegmentIndex = options.includeTerminalSegments ? 0 : 1
|
|
59
|
+
const lastPathSegmentIndex = options.includeTerminalSegments
|
|
60
|
+
? path.length - 1
|
|
61
|
+
: path.length - 2
|
|
62
|
+
const firstOtherPathSegmentIndex = options.includeTerminalSegments ? 0 : 1
|
|
63
|
+
const lastOtherPathSegmentIndex = options.includeTerminalSegments
|
|
64
|
+
? otherPath.length - 1
|
|
65
|
+
: otherPath.length - 2
|
|
53
66
|
|
|
54
67
|
// Terminal segments connect to pins and are allowed to meet other traces at
|
|
55
|
-
// their endpoints.
|
|
68
|
+
// their endpoints. Callers may include them when looking for strict
|
|
69
|
+
// crossings through a segment's interior after all connector traces exist.
|
|
56
70
|
for (
|
|
57
|
-
let pathSegmentIndex =
|
|
58
|
-
pathSegmentIndex <
|
|
71
|
+
let pathSegmentIndex = firstPathSegmentIndex;
|
|
72
|
+
pathSegmentIndex < lastPathSegmentIndex;
|
|
59
73
|
pathSegmentIndex++
|
|
60
74
|
) {
|
|
61
75
|
const start = path[pathSegmentIndex]!
|
|
@@ -63,8 +77,8 @@ export const findPerpendicularPathCrossings = (
|
|
|
63
77
|
const isVertical = Math.abs(start.x - end.x) < EPS
|
|
64
78
|
|
|
65
79
|
for (
|
|
66
|
-
let otherPathSegmentIndex =
|
|
67
|
-
otherPathSegmentIndex <
|
|
80
|
+
let otherPathSegmentIndex = firstOtherPathSegmentIndex;
|
|
81
|
+
otherPathSegmentIndex < lastOtherPathSegmentIndex;
|
|
68
82
|
otherPathSegmentIndex++
|
|
69
83
|
) {
|
|
70
84
|
const otherStart = otherPath[otherPathSegmentIndex]!
|
|
@@ -113,6 +113,7 @@ export interface PerpendicularTraceDetourInput {
|
|
|
113
113
|
segmentIndex: number
|
|
114
114
|
obstacleStart: Point
|
|
115
115
|
obstacleEnd: Point
|
|
116
|
+
obstacleBounds?: Bounds
|
|
116
117
|
chipBounds: Bounds[]
|
|
117
118
|
clearance: number
|
|
118
119
|
}
|
|
@@ -179,6 +180,7 @@ export const generatePerpendicularTraceDetours = ({
|
|
|
179
180
|
segmentIndex,
|
|
180
181
|
obstacleStart,
|
|
181
182
|
obstacleEnd,
|
|
183
|
+
obstacleBounds,
|
|
182
184
|
chipBounds,
|
|
183
185
|
clearance,
|
|
184
186
|
}: PerpendicularTraceDetourInput): TraceDetourCandidate[] => {
|
|
@@ -187,20 +189,48 @@ export const generatePerpendicularTraceDetours = ({
|
|
|
187
189
|
const end = path[index + 1]!
|
|
188
190
|
const movingAxis: "x" | "y" = Math.abs(start.x - end.x) < EPS ? "y" : "x"
|
|
189
191
|
const detourAxis = movingAxis === "x" ? "y" : "x"
|
|
190
|
-
const
|
|
192
|
+
const bounds = obstacleBounds ?? {
|
|
193
|
+
minX: Math.min(obstacleStart.x, obstacleEnd.x),
|
|
194
|
+
minY: Math.min(obstacleStart.y, obstacleEnd.y),
|
|
195
|
+
maxX: Math.max(obstacleStart.x, obstacleEnd.x),
|
|
196
|
+
maxY: Math.max(obstacleStart.y, obstacleEnd.y),
|
|
197
|
+
}
|
|
198
|
+
const movingLowBound = movingAxis === "x" ? "minX" : "minY"
|
|
199
|
+
const movingHighBound = movingAxis === "x" ? "maxX" : "maxY"
|
|
200
|
+
let gate =
|
|
191
201
|
obstacleStart[movingAxis] +
|
|
192
202
|
Math.sign(start[movingAxis] - obstacleStart[movingAxis]) * clearance
|
|
193
|
-
|
|
203
|
+
if (obstacleBounds) {
|
|
204
|
+
const startSide =
|
|
205
|
+
start[movingAxis] <
|
|
206
|
+
(bounds[movingLowBound] + bounds[movingHighBound]) / 2
|
|
207
|
+
? -1
|
|
208
|
+
: 1
|
|
209
|
+
gate =
|
|
210
|
+
bounds[startSide < 0 ? movingLowBound : movingHighBound] +
|
|
211
|
+
startSide * clearance
|
|
212
|
+
}
|
|
194
213
|
const lowBound = detourAxis === "x" ? "minX" : "minY"
|
|
195
214
|
const highBound = detourAxis === "x" ? "maxX" : "maxY"
|
|
215
|
+
const minDetour = bounds[lowBound] - clearance
|
|
216
|
+
const maxDetour = bounds[highBound] + clearance
|
|
217
|
+
const nextAnchor = obstacleBounds ? path[index + 2] : undefined
|
|
218
|
+
const balancedDetour = nextAnchor
|
|
219
|
+
? nextAnchor[detourAxis] < minDetour
|
|
220
|
+
? (nextAnchor[detourAxis] + minDetour) / 2
|
|
221
|
+
: nextAnchor[detourAxis] > maxDetour
|
|
222
|
+
? (nextAnchor[detourAxis] + maxDetour) / 2
|
|
223
|
+
: undefined
|
|
224
|
+
: undefined
|
|
196
225
|
const detourCoordinates = [
|
|
197
|
-
|
|
198
|
-
|
|
226
|
+
balancedDetour,
|
|
227
|
+
minDetour,
|
|
228
|
+
maxDetour,
|
|
199
229
|
...chipBounds.flatMap((bounds) => [
|
|
200
230
|
bounds[lowBound] - clearance,
|
|
201
231
|
bounds[highBound] + clearance,
|
|
202
232
|
]),
|
|
203
|
-
]
|
|
233
|
+
].filter((coordinate): coordinate is number => coordinate !== undefined)
|
|
204
234
|
|
|
205
235
|
return [...new Set(detourCoordinates)].map((detour) =>
|
|
206
236
|
simplifyPath([
|