@tscircuit/fanout-solver 0.0.10
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/LICENSE +21 -0
- package/README.md +366 -0
- package/lib/build-output.ts +140 -0
- package/lib/fanout-solver.ts +617 -0
- package/lib/geometry.ts +162 -0
- package/lib/index.ts +20 -0
- package/lib/layer-colors.ts +21 -0
- package/lib/layer-names.ts +137 -0
- package/lib/prepare-buses.ts +1016 -0
- package/lib/route-bus.ts +934 -0
- package/lib/route-single-layer-adaptive-exits.ts +1179 -0
- package/lib/route-single-layer-push-shove.ts +1233 -0
- package/lib/types.ts +212 -0
- package/package.json +56 -0
|
@@ -0,0 +1,1233 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Obstacle,
|
|
3
|
+
SimpleRouteJson,
|
|
4
|
+
SimplifiedPcbTrace,
|
|
5
|
+
} from "@tscircuit/capacity-autorouter"
|
|
6
|
+
import {
|
|
7
|
+
distance,
|
|
8
|
+
distanceSegmentToObstacle,
|
|
9
|
+
distanceSegmentToSegment,
|
|
10
|
+
} from "./geometry"
|
|
11
|
+
import type {
|
|
12
|
+
FanoutBorderDistribution,
|
|
13
|
+
FanoutCorner,
|
|
14
|
+
FanoutDirection,
|
|
15
|
+
FanoutRoutePlan,
|
|
16
|
+
Point2D,
|
|
17
|
+
PreparedBus,
|
|
18
|
+
PreparedConnection,
|
|
19
|
+
RoutedSegment,
|
|
20
|
+
} from "./types"
|
|
21
|
+
|
|
22
|
+
interface PushShoveParams {
|
|
23
|
+
srj: SimpleRouteJson
|
|
24
|
+
buses: PreparedBus[]
|
|
25
|
+
traceWidth: number
|
|
26
|
+
clearance: number
|
|
27
|
+
borderDistribution: FanoutBorderDistribution
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface RoutingItem {
|
|
31
|
+
bus: PreparedBus
|
|
32
|
+
connection: PreparedConnection
|
|
33
|
+
direction: FanoutDirection
|
|
34
|
+
source: Point2D
|
|
35
|
+
cornerChannelPrefixes?: Point2D[][]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface RoutedPath {
|
|
39
|
+
item: RoutingItem
|
|
40
|
+
points: Point2D[]
|
|
41
|
+
segments: RoutedSegment[]
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface ActiveRoute {
|
|
45
|
+
item: RoutingItem
|
|
46
|
+
points: Point2D[]
|
|
47
|
+
track: number
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isHorizontal(direction: FanoutDirection): boolean {
|
|
51
|
+
return direction === "left" || direction === "right"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function directionSign(direction: FanoutDirection): number {
|
|
55
|
+
return direction === "right" || direction === "up" ? 1 : -1
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function getAxis(point: Point2D, direction: FanoutDirection): number {
|
|
59
|
+
return isHorizontal(direction) ? point.x : point.y
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function getPerpendicularAxis(
|
|
63
|
+
point: Point2D,
|
|
64
|
+
direction: FanoutDirection,
|
|
65
|
+
): number {
|
|
66
|
+
return isHorizontal(direction) ? point.y : point.x
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function makePoint(
|
|
70
|
+
axis: number,
|
|
71
|
+
perpendicularAxis: number,
|
|
72
|
+
direction: FanoutDirection,
|
|
73
|
+
): Point2D {
|
|
74
|
+
return isHorizontal(direction)
|
|
75
|
+
? { x: axis, y: perpendicularAxis }
|
|
76
|
+
: { x: perpendicularAxis, y: axis }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getExitAxis(bus: PreparedBus): number {
|
|
80
|
+
switch (bus.direction) {
|
|
81
|
+
case "right":
|
|
82
|
+
return bus.sharedBoundary.maxX
|
|
83
|
+
case "left":
|
|
84
|
+
return bus.sharedBoundary.minX
|
|
85
|
+
case "up":
|
|
86
|
+
return bus.sharedBoundary.maxY
|
|
87
|
+
case "down":
|
|
88
|
+
return bus.sharedBoundary.minY
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function compressPath(points: Point2D[]): Point2D[] {
|
|
93
|
+
if (points.length < 3) return points
|
|
94
|
+
const compressed = [points[0]!]
|
|
95
|
+
for (let index = 1; index < points.length - 1; index++) {
|
|
96
|
+
const previous = compressed.at(-1)!
|
|
97
|
+
const current = points[index]!
|
|
98
|
+
const next = points[index + 1]!
|
|
99
|
+
const incoming = {
|
|
100
|
+
x: Math.sign(current.x - previous.x),
|
|
101
|
+
y: Math.sign(current.y - previous.y),
|
|
102
|
+
}
|
|
103
|
+
const outgoing = {
|
|
104
|
+
x: Math.sign(next.x - current.x),
|
|
105
|
+
y: Math.sign(next.y - current.y),
|
|
106
|
+
}
|
|
107
|
+
if (incoming.x !== outgoing.x || incoming.y !== outgoing.y) {
|
|
108
|
+
compressed.push(current)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
compressed.push(points.at(-1)!)
|
|
112
|
+
return compressed
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function getPathSegments(
|
|
116
|
+
points: Point2D[],
|
|
117
|
+
traceWidth: number,
|
|
118
|
+
): RoutedSegment[] {
|
|
119
|
+
const segments: RoutedSegment[] = []
|
|
120
|
+
for (let index = 1; index < points.length; index++) {
|
|
121
|
+
if (distance(points[index - 1]!, points[index]!) < 1e-9) continue
|
|
122
|
+
segments.push({
|
|
123
|
+
start: points[index - 1]!,
|
|
124
|
+
end: points[index]!,
|
|
125
|
+
width: traceWidth,
|
|
126
|
+
layer: "top",
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
return segments
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Finds a same-layer 45-degree channel for a pad enclosed by its own package.
|
|
134
|
+
* The diagonal advances equally along the directional and perpendicular axes,
|
|
135
|
+
* clears every other obstacle, and leaves enough runway for a straight segment
|
|
136
|
+
* before any completion dogleg.
|
|
137
|
+
*/
|
|
138
|
+
function getCornerChannelPrefixes(params: {
|
|
139
|
+
srj: SimpleRouteJson
|
|
140
|
+
bus: PreparedBus
|
|
141
|
+
connection: PreparedConnection
|
|
142
|
+
traceWidth: number
|
|
143
|
+
clearance: number
|
|
144
|
+
}): Point2D[][] | null {
|
|
145
|
+
const { srj, bus, connection, traceWidth, clearance } = params
|
|
146
|
+
const direction = bus.direction
|
|
147
|
+
const sign = directionSign(direction)
|
|
148
|
+
const source = {
|
|
149
|
+
x: connection.sourcePoint.x,
|
|
150
|
+
y: connection.sourcePoint.y,
|
|
151
|
+
}
|
|
152
|
+
const sourceAxis = getAxis(source, direction)
|
|
153
|
+
const sourceTrack = getPerpendicularAxis(source, direction)
|
|
154
|
+
const requiredObstacleDistance = traceWidth / 2 + clearance
|
|
155
|
+
const componentExitAxis = (() => {
|
|
156
|
+
switch (direction) {
|
|
157
|
+
case "right":
|
|
158
|
+
return bus.componentBounds.maxX + requiredObstacleDistance
|
|
159
|
+
case "left":
|
|
160
|
+
return bus.componentBounds.minX - requiredObstacleDistance
|
|
161
|
+
case "up":
|
|
162
|
+
return bus.componentBounds.maxY + requiredObstacleDistance
|
|
163
|
+
case "down":
|
|
164
|
+
return bus.componentBounds.minY - requiredObstacleDistance
|
|
165
|
+
}
|
|
166
|
+
})()
|
|
167
|
+
const obstacleDistanceForSegment = (segment: RoutedSegment): number =>
|
|
168
|
+
Math.min(
|
|
169
|
+
...srj.obstacles
|
|
170
|
+
.filter(
|
|
171
|
+
(obstacle) =>
|
|
172
|
+
obstacle !== connection.sourceObstacle &&
|
|
173
|
+
obstacle.layers.includes("top"),
|
|
174
|
+
)
|
|
175
|
+
.map((obstacle) => distanceSegmentToObstacle(segment, obstacle)),
|
|
176
|
+
)
|
|
177
|
+
const directSegment: RoutedSegment = {
|
|
178
|
+
start: source,
|
|
179
|
+
end: makePoint(componentExitAxis, sourceTrack, direction),
|
|
180
|
+
width: traceWidth,
|
|
181
|
+
layer: "top",
|
|
182
|
+
}
|
|
183
|
+
if (
|
|
184
|
+
obstacleDistanceForSegment(directSegment) >=
|
|
185
|
+
requiredObstacleDistance - 1e-9
|
|
186
|
+
) {
|
|
187
|
+
return null
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const perpendicularBounds = isHorizontal(direction)
|
|
191
|
+
? {
|
|
192
|
+
minimum: bus.componentBounds.minY,
|
|
193
|
+
maximum: bus.componentBounds.maxY,
|
|
194
|
+
}
|
|
195
|
+
: {
|
|
196
|
+
minimum: bus.componentBounds.minX,
|
|
197
|
+
maximum: bus.componentBounds.maxX,
|
|
198
|
+
}
|
|
199
|
+
const directionalDistance = sign * (componentExitAxis - sourceAxis)
|
|
200
|
+
const targetTrack = getPerpendicularAxis(connection.targetPoint, direction)
|
|
201
|
+
const lanePitch = traceWidth + clearance
|
|
202
|
+
const exitAxis = getExitAxis(bus)
|
|
203
|
+
const candidates = ([-1, 1] as const)
|
|
204
|
+
.map((perpendicularSign) => {
|
|
205
|
+
const perpendicularExit =
|
|
206
|
+
perpendicularSign < 0
|
|
207
|
+
? perpendicularBounds.minimum - requiredObstacleDistance
|
|
208
|
+
: perpendicularBounds.maximum + requiredObstacleDistance
|
|
209
|
+
const perpendicularDistance =
|
|
210
|
+
perpendicularSign * (perpendicularExit - sourceTrack)
|
|
211
|
+
const diagonalDistance = Math.max(
|
|
212
|
+
directionalDistance,
|
|
213
|
+
perpendicularDistance,
|
|
214
|
+
)
|
|
215
|
+
const diagonalEnd = makePoint(
|
|
216
|
+
sourceAxis + sign * diagonalDistance,
|
|
217
|
+
sourceTrack + perpendicularSign * diagonalDistance,
|
|
218
|
+
direction,
|
|
219
|
+
)
|
|
220
|
+
if (
|
|
221
|
+
sign * (exitAxis - getAxis(diagonalEnd, direction)) <
|
|
222
|
+
lanePitch - 1e-9
|
|
223
|
+
) {
|
|
224
|
+
return null
|
|
225
|
+
}
|
|
226
|
+
const points = [source, diagonalEnd]
|
|
227
|
+
const withinBoundary = points.every(
|
|
228
|
+
(point) =>
|
|
229
|
+
point.x >= bus.sharedBoundary.minX - 1e-9 &&
|
|
230
|
+
point.x <= bus.sharedBoundary.maxX + 1e-9 &&
|
|
231
|
+
point.y >= bus.sharedBoundary.minY - 1e-9 &&
|
|
232
|
+
point.y <= bus.sharedBoundary.maxY + 1e-9,
|
|
233
|
+
)
|
|
234
|
+
if (!withinBoundary) return null
|
|
235
|
+
const obstacleDistance = Math.min(
|
|
236
|
+
...getPathSegments(points, traceWidth).map(obstacleDistanceForSegment),
|
|
237
|
+
)
|
|
238
|
+
if (obstacleDistance < requiredObstacleDistance - 1e-9) return null
|
|
239
|
+
return {
|
|
240
|
+
points,
|
|
241
|
+
obstacleDistance,
|
|
242
|
+
targetDistance: Math.abs(
|
|
243
|
+
getPerpendicularAxis(diagonalEnd, direction) - targetTrack,
|
|
244
|
+
),
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
.filter((candidate) => candidate !== null)
|
|
248
|
+
.toSorted(
|
|
249
|
+
(first, second) =>
|
|
250
|
+
second.obstacleDistance - first.obstacleDistance ||
|
|
251
|
+
first.targetDistance - second.targetDistance,
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
return candidates.length > 0
|
|
255
|
+
? candidates.map((candidate) => candidate.points)
|
|
256
|
+
: null
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function completeCornerChannelRoute(params: {
|
|
260
|
+
item: RoutingItem
|
|
261
|
+
srj: SimpleRouteJson
|
|
262
|
+
acceptedSegments: RoutedSegment[]
|
|
263
|
+
traceWidth: number
|
|
264
|
+
clearance: number
|
|
265
|
+
}): ActiveRoute | null {
|
|
266
|
+
const { item, srj, acceptedSegments, traceWidth, clearance } = params
|
|
267
|
+
const prefixes = item.cornerChannelPrefixes
|
|
268
|
+
if (!prefixes) return null
|
|
269
|
+
const direction = item.direction
|
|
270
|
+
const sign = directionSign(direction)
|
|
271
|
+
const exitAxis = getExitAxis(item.bus)
|
|
272
|
+
const lanePitch = traceWidth + clearance
|
|
273
|
+
const requiredObstacleDistance = traceWidth / 2 + clearance
|
|
274
|
+
const boundaryMinimum = isHorizontal(direction)
|
|
275
|
+
? item.bus.sharedBoundary.minY
|
|
276
|
+
: item.bus.sharedBoundary.minX
|
|
277
|
+
const boundaryMaximum = isHorizontal(direction)
|
|
278
|
+
? item.bus.sharedBoundary.maxY
|
|
279
|
+
: item.bus.sharedBoundary.maxX
|
|
280
|
+
const minimumTrack = boundaryMinimum + traceWidth / 2
|
|
281
|
+
const maximumTrack = boundaryMaximum - traceWidth / 2
|
|
282
|
+
const targetTrack = getPerpendicularAxis(
|
|
283
|
+
item.connection.targetPoint,
|
|
284
|
+
direction,
|
|
285
|
+
)
|
|
286
|
+
const trackStep = lanePitch / 2
|
|
287
|
+
const trackCandidates = new Set<number>([
|
|
288
|
+
Math.max(minimumTrack, Math.min(maximumTrack, targetTrack)),
|
|
289
|
+
])
|
|
290
|
+
for (
|
|
291
|
+
let track = Math.ceil(minimumTrack / trackStep) * trackStep;
|
|
292
|
+
track <= maximumTrack + 1e-9;
|
|
293
|
+
track += trackStep
|
|
294
|
+
) {
|
|
295
|
+
trackCandidates.add(Number(track.toFixed(9)))
|
|
296
|
+
}
|
|
297
|
+
const orderedTracks = [...trackCandidates].toSorted(
|
|
298
|
+
(first, second) =>
|
|
299
|
+
Math.abs(first - targetTrack) - Math.abs(second - targetTrack),
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
for (const prefix of prefixes) {
|
|
303
|
+
const diagonalEnd = prefix.at(-1)!
|
|
304
|
+
const diagonalTrack = getPerpendicularAxis(diagonalEnd, direction)
|
|
305
|
+
for (const track of orderedTracks) {
|
|
306
|
+
const shift = Math.abs(track - diagonalTrack)
|
|
307
|
+
const straightEnd = makePoint(
|
|
308
|
+
getAxis(diagonalEnd, direction) + sign * lanePitch,
|
|
309
|
+
diagonalTrack,
|
|
310
|
+
direction,
|
|
311
|
+
)
|
|
312
|
+
const doglegEnd = makePoint(
|
|
313
|
+
getAxis(straightEnd, direction) + sign * shift,
|
|
314
|
+
track,
|
|
315
|
+
direction,
|
|
316
|
+
)
|
|
317
|
+
if (sign * (exitAxis - getAxis(doglegEnd, direction)) < -1e-9) {
|
|
318
|
+
continue
|
|
319
|
+
}
|
|
320
|
+
const boundaryPoint = makePoint(exitAxis, track, direction)
|
|
321
|
+
const completionPoints = [
|
|
322
|
+
...prefix,
|
|
323
|
+
straightEnd,
|
|
324
|
+
...(shift > 1e-9 ? [doglegEnd] : []),
|
|
325
|
+
]
|
|
326
|
+
if (distance(completionPoints.at(-1)!, boundaryPoint) > 1e-9) {
|
|
327
|
+
completionPoints.push(boundaryPoint)
|
|
328
|
+
}
|
|
329
|
+
const points = compressPath(completionPoints)
|
|
330
|
+
const segments = getPathSegments(points, traceWidth)
|
|
331
|
+
const clearsObstacles = segments.every((segment) =>
|
|
332
|
+
srj.obstacles.every(
|
|
333
|
+
(obstacle) =>
|
|
334
|
+
obstacle === item.connection.sourceObstacle ||
|
|
335
|
+
!obstacle.layers.includes("top") ||
|
|
336
|
+
distanceSegmentToObstacle(segment, obstacle) >=
|
|
337
|
+
requiredObstacleDistance - 1e-9,
|
|
338
|
+
),
|
|
339
|
+
)
|
|
340
|
+
if (!clearsObstacles) continue
|
|
341
|
+
const clearsRoutes = segments.every((segment) =>
|
|
342
|
+
acceptedSegments.every(
|
|
343
|
+
(acceptedSegment) =>
|
|
344
|
+
distanceSegmentToSegment(
|
|
345
|
+
segment.start,
|
|
346
|
+
segment.end,
|
|
347
|
+
acceptedSegment.start,
|
|
348
|
+
acceptedSegment.end,
|
|
349
|
+
) >=
|
|
350
|
+
traceWidth + clearance - 1e-9,
|
|
351
|
+
),
|
|
352
|
+
)
|
|
353
|
+
if (!clearsRoutes) continue
|
|
354
|
+
return { item, points, track }
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return null
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function getMaximumObstacleClearDistributionShift(params: {
|
|
361
|
+
route: ActiveRoute
|
|
362
|
+
direction: FanoutDirection
|
|
363
|
+
exitAxis: number
|
|
364
|
+
signedShift: number
|
|
365
|
+
maximumShift: number
|
|
366
|
+
obstacles: Obstacle[]
|
|
367
|
+
requiredObstacleDistance: number
|
|
368
|
+
}): number {
|
|
369
|
+
const {
|
|
370
|
+
route,
|
|
371
|
+
direction,
|
|
372
|
+
exitAxis,
|
|
373
|
+
signedShift,
|
|
374
|
+
maximumShift,
|
|
375
|
+
obstacles,
|
|
376
|
+
requiredObstacleDistance,
|
|
377
|
+
} = params
|
|
378
|
+
if (maximumShift <= 1e-9 || Math.abs(signedShift) <= 1e-9) return 0
|
|
379
|
+
const perpendicularSign = Math.sign(signedShift)
|
|
380
|
+
const shiftIsClear = (shift: number): boolean => {
|
|
381
|
+
if (shift <= 1e-9) return true
|
|
382
|
+
const segment = {
|
|
383
|
+
start: makePoint(
|
|
384
|
+
exitAxis - directionSign(direction) * shift,
|
|
385
|
+
route.track,
|
|
386
|
+
direction,
|
|
387
|
+
),
|
|
388
|
+
end: makePoint(
|
|
389
|
+
exitAxis,
|
|
390
|
+
route.track + perpendicularSign * shift,
|
|
391
|
+
direction,
|
|
392
|
+
),
|
|
393
|
+
width: 0,
|
|
394
|
+
layer: "top",
|
|
395
|
+
}
|
|
396
|
+
return obstacles.every(
|
|
397
|
+
(obstacle) =>
|
|
398
|
+
obstacle === route.item.connection.sourceObstacle ||
|
|
399
|
+
!obstacle.layers.includes("top") ||
|
|
400
|
+
distanceSegmentToObstacle(segment, obstacle) >=
|
|
401
|
+
requiredObstacleDistance - 1e-9,
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
if (shiftIsClear(maximumShift)) return maximumShift
|
|
405
|
+
|
|
406
|
+
let lowerShift = 0
|
|
407
|
+
let upperShift = maximumShift
|
|
408
|
+
for (let iteration = 0; iteration < 32; iteration++) {
|
|
409
|
+
const candidateShift = (lowerShift + upperShift) / 2
|
|
410
|
+
if (shiftIsClear(candidateShift)) lowerShift = candidateShift
|
|
411
|
+
else upperShift = candidateShift
|
|
412
|
+
}
|
|
413
|
+
return lowerShift
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Finds an ordered subset of candidate tracks with minimum total displacement.
|
|
418
|
+
* Preserving the order is the "shove" invariant: existing routes can move, but
|
|
419
|
+
* they cannot cross when a new pad row joins the channel.
|
|
420
|
+
*/
|
|
421
|
+
function selectOrderedTracks(params: {
|
|
422
|
+
requestedTracks: number[]
|
|
423
|
+
currentTracks: number[]
|
|
424
|
+
candidateTracks: number[]
|
|
425
|
+
maximumShifts: number[]
|
|
426
|
+
}): number[] | null {
|
|
427
|
+
const { requestedTracks, currentTracks, candidateTracks, maximumShifts } =
|
|
428
|
+
params
|
|
429
|
+
const rowCount = requestedTracks.length + 1
|
|
430
|
+
const columnCount = candidateTracks.length + 1
|
|
431
|
+
const costs = Array.from({ length: rowCount }, () =>
|
|
432
|
+
new Float64Array(columnCount).fill(Number.POSITIVE_INFINITY),
|
|
433
|
+
)
|
|
434
|
+
const tookCandidate = Array.from(
|
|
435
|
+
{ length: rowCount },
|
|
436
|
+
() => new Uint8Array(columnCount),
|
|
437
|
+
)
|
|
438
|
+
costs[0]!.fill(0)
|
|
439
|
+
|
|
440
|
+
for (let row = 1; row < rowCount; row++) {
|
|
441
|
+
for (let column = 1; column < columnCount; column++) {
|
|
442
|
+
const skippedCost = costs[row]![column - 1]!
|
|
443
|
+
const requestedShift = Math.abs(
|
|
444
|
+
requestedTracks[row - 1]! - candidateTracks[column - 1]!,
|
|
445
|
+
)
|
|
446
|
+
const currentShift = Math.abs(
|
|
447
|
+
currentTracks[row - 1]! - candidateTracks[column - 1]!,
|
|
448
|
+
)
|
|
449
|
+
const selectedCost =
|
|
450
|
+
currentShift > maximumShifts[row - 1]! + 1e-9
|
|
451
|
+
? Number.POSITIVE_INFINITY
|
|
452
|
+
: costs[row - 1]![column - 1]! + requestedShift
|
|
453
|
+
if (selectedCost < skippedCost) {
|
|
454
|
+
costs[row]![column] = selectedCost
|
|
455
|
+
tookCandidate[row]![column] = 1
|
|
456
|
+
} else {
|
|
457
|
+
costs[row]![column] = skippedCost
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (!Number.isFinite(costs.at(-1)!.at(-1)!)) return null
|
|
463
|
+
const selectedTracks: number[] = []
|
|
464
|
+
let row = requestedTracks.length
|
|
465
|
+
let column = candidateTracks.length
|
|
466
|
+
while (row > 0 && column > 0) {
|
|
467
|
+
if (tookCandidate[row]![column]) {
|
|
468
|
+
selectedTracks.push(candidateTracks[column - 1]!)
|
|
469
|
+
row--
|
|
470
|
+
}
|
|
471
|
+
column--
|
|
472
|
+
}
|
|
473
|
+
if (row > 0) return null
|
|
474
|
+
return selectedTracks.reverse()
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
function getCandidateTracks(params: {
|
|
478
|
+
direction: FanoutDirection
|
|
479
|
+
activeRoutes: ActiveRoute[]
|
|
480
|
+
obstacles: Obstacle[]
|
|
481
|
+
boundaryMinimum: number
|
|
482
|
+
boundaryMaximum: number
|
|
483
|
+
traceWidth: number
|
|
484
|
+
clearance: number
|
|
485
|
+
requestedTracks: number[]
|
|
486
|
+
maximumShifts: number[]
|
|
487
|
+
}): number[] | null {
|
|
488
|
+
const {
|
|
489
|
+
direction,
|
|
490
|
+
activeRoutes,
|
|
491
|
+
obstacles,
|
|
492
|
+
boundaryMinimum,
|
|
493
|
+
boundaryMaximum,
|
|
494
|
+
traceWidth,
|
|
495
|
+
clearance,
|
|
496
|
+
requestedTracks,
|
|
497
|
+
maximumShifts,
|
|
498
|
+
} = params
|
|
499
|
+
const lanePitch = traceWidth + clearance
|
|
500
|
+
const requiredObstacleDistance = traceWidth / 2 + clearance
|
|
501
|
+
let selectedTracks: number[] | null = null
|
|
502
|
+
let selectedCost = Number.POSITIVE_INFINITY
|
|
503
|
+
|
|
504
|
+
for (const phase of Array.from(
|
|
505
|
+
{ length: Math.round(lanePitch / traceWidth) },
|
|
506
|
+
(_, index) => index * traceWidth,
|
|
507
|
+
)) {
|
|
508
|
+
const candidates: number[] = []
|
|
509
|
+
const firstTrack =
|
|
510
|
+
Math.ceil((boundaryMinimum - phase) / lanePitch) * lanePitch + phase
|
|
511
|
+
for (
|
|
512
|
+
let track = firstTrack;
|
|
513
|
+
track <= boundaryMaximum + 1e-9;
|
|
514
|
+
track += lanePitch
|
|
515
|
+
) {
|
|
516
|
+
const roundedTrack = Math.round(track / traceWidth) * traceWidth
|
|
517
|
+
if (
|
|
518
|
+
obstacles.every((obstacle) => {
|
|
519
|
+
const obstacleTrack = getPerpendicularAxis(obstacle.center, direction)
|
|
520
|
+
const obstacleSize = isHorizontal(direction)
|
|
521
|
+
? obstacle.height
|
|
522
|
+
: obstacle.width
|
|
523
|
+
return (
|
|
524
|
+
Math.abs(roundedTrack - obstacleTrack) >=
|
|
525
|
+
obstacleSize / 2 + requiredObstacleDistance - 1e-9
|
|
526
|
+
)
|
|
527
|
+
})
|
|
528
|
+
) {
|
|
529
|
+
candidates.push(roundedTrack)
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const tracks = selectOrderedTracks({
|
|
534
|
+
requestedTracks,
|
|
535
|
+
currentTracks: activeRoutes.map((route) => route.track),
|
|
536
|
+
candidateTracks: candidates,
|
|
537
|
+
maximumShifts,
|
|
538
|
+
})
|
|
539
|
+
if (!tracks) continue
|
|
540
|
+
const cost = tracks.reduce(
|
|
541
|
+
(sum, track, index) => sum + Math.abs(track - requestedTracks[index]!),
|
|
542
|
+
0,
|
|
543
|
+
)
|
|
544
|
+
if (cost < selectedCost) {
|
|
545
|
+
selectedCost = cost
|
|
546
|
+
selectedTracks = tracks
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
return selectedTracks
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
interface FinalTrackTargets {
|
|
553
|
+
byConnectionName: Map<string, number>
|
|
554
|
+
enforcedConnectionNames: Set<string>
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function getCornerSide(
|
|
558
|
+
corner: FanoutCorner,
|
|
559
|
+
direction: FanoutDirection,
|
|
560
|
+
): "minimum" | "maximum" | null {
|
|
561
|
+
switch (direction) {
|
|
562
|
+
case "up":
|
|
563
|
+
if (corner === "top-left") return "minimum"
|
|
564
|
+
if (corner === "top-right") return "maximum"
|
|
565
|
+
return null
|
|
566
|
+
case "down":
|
|
567
|
+
if (corner === "bottom-left") return "minimum"
|
|
568
|
+
if (corner === "bottom-right") return "maximum"
|
|
569
|
+
return null
|
|
570
|
+
case "left":
|
|
571
|
+
if (corner === "bottom-left") return "minimum"
|
|
572
|
+
if (corner === "top-left") return "maximum"
|
|
573
|
+
return null
|
|
574
|
+
case "right":
|
|
575
|
+
if (corner === "bottom-right") return "minimum"
|
|
576
|
+
if (corner === "top-right") return "maximum"
|
|
577
|
+
return null
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function getFinalTrackTargets(params: {
|
|
582
|
+
items: RoutingItem[]
|
|
583
|
+
direction: FanoutDirection
|
|
584
|
+
boundaryMinimum: number
|
|
585
|
+
boundaryMaximum: number
|
|
586
|
+
traceWidth: number
|
|
587
|
+
clearance: number
|
|
588
|
+
borderDistribution: FanoutBorderDistribution
|
|
589
|
+
currentTrackByConnectionName: ReadonlyMap<string, number>
|
|
590
|
+
}): FinalTrackTargets | null {
|
|
591
|
+
const {
|
|
592
|
+
items,
|
|
593
|
+
direction,
|
|
594
|
+
boundaryMinimum,
|
|
595
|
+
boundaryMaximum,
|
|
596
|
+
traceWidth,
|
|
597
|
+
clearance,
|
|
598
|
+
borderDistribution,
|
|
599
|
+
currentTrackByConnectionName,
|
|
600
|
+
} = params
|
|
601
|
+
const getCurrentTrack = (item: RoutingItem) =>
|
|
602
|
+
currentTrackByConnectionName.get(item.connection.connection.name) ??
|
|
603
|
+
getPerpendicularAxis(item.source, direction)
|
|
604
|
+
const orderedItems = [...items].toSorted(
|
|
605
|
+
(first, second) =>
|
|
606
|
+
getCurrentTrack(first) - getCurrentTrack(second) ||
|
|
607
|
+
first.connection.connection.name.localeCompare(
|
|
608
|
+
second.connection.connection.name,
|
|
609
|
+
),
|
|
610
|
+
)
|
|
611
|
+
const lanePitch = traceWidth + clearance
|
|
612
|
+
const minimumLane = boundaryMinimum + traceWidth / 2
|
|
613
|
+
const maximumLane = boundaryMaximum - traceWidth / 2
|
|
614
|
+
const availableSpan = maximumLane - minimumLane
|
|
615
|
+
const requiredSpan = lanePitch * Math.max(orderedItems.length - 1, 0)
|
|
616
|
+
if (availableSpan < requiredSpan - 1e-9) return null
|
|
617
|
+
|
|
618
|
+
const sourceTracks = orderedItems.map(getCurrentTrack)
|
|
619
|
+
let targetTracks = [...sourceTracks]
|
|
620
|
+
let distributedPitch = lanePitch
|
|
621
|
+
const enforcedConnectionNames = new Set<string>()
|
|
622
|
+
if (borderDistribution === "even" && orderedItems.length > 1) {
|
|
623
|
+
const sourceMinimum = sourceTracks[0]!
|
|
624
|
+
const sourceMaximum = sourceTracks.at(-1)!
|
|
625
|
+
const desiredPitch = Math.max(
|
|
626
|
+
lanePitch,
|
|
627
|
+
(sourceMaximum - sourceMinimum) / (orderedItems.length - 1),
|
|
628
|
+
)
|
|
629
|
+
const buildOutwardTracks = (pitch: number): number[] => {
|
|
630
|
+
const tracks = [...sourceTracks]
|
|
631
|
+
const upperMiddle = Math.floor(orderedItems.length / 2)
|
|
632
|
+
const lowerMiddle =
|
|
633
|
+
orderedItems.length % 2 === 0 ? upperMiddle - 1 : upperMiddle
|
|
634
|
+
if (lowerMiddle !== upperMiddle) {
|
|
635
|
+
const middleGap = tracks[upperMiddle]! - tracks[lowerMiddle]!
|
|
636
|
+
if (middleGap < pitch) {
|
|
637
|
+
const shove = (pitch - middleGap) / 2
|
|
638
|
+
tracks[lowerMiddle] = tracks[lowerMiddle]! - shove
|
|
639
|
+
tracks[upperMiddle] = tracks[upperMiddle]! + shove
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
for (let index = lowerMiddle - 1; index >= 0; index--) {
|
|
643
|
+
tracks[index] = Math.min(tracks[index]!, tracks[index + 1]! - pitch)
|
|
644
|
+
}
|
|
645
|
+
for (let index = upperMiddle + 1; index < tracks.length; index++) {
|
|
646
|
+
tracks[index] = Math.max(tracks[index]!, tracks[index - 1]! + pitch)
|
|
647
|
+
}
|
|
648
|
+
return tracks
|
|
649
|
+
}
|
|
650
|
+
const tracksFitBoundary = (tracks: number[]) =>
|
|
651
|
+
tracks[0]! >= minimumLane - 1e-9 && tracks.at(-1)! <= maximumLane + 1e-9
|
|
652
|
+
targetTracks = buildOutwardTracks(desiredPitch)
|
|
653
|
+
if (tracksFitBoundary(targetTracks)) {
|
|
654
|
+
distributedPitch = desiredPitch
|
|
655
|
+
} else {
|
|
656
|
+
let lowerPitch = lanePitch
|
|
657
|
+
let upperPitch = desiredPitch
|
|
658
|
+
targetTracks = buildOutwardTracks(lowerPitch)
|
|
659
|
+
if (!tracksFitBoundary(targetTracks)) return null
|
|
660
|
+
for (let iteration = 0; iteration < 32; iteration++) {
|
|
661
|
+
const candidatePitch = (lowerPitch + upperPitch) / 2
|
|
662
|
+
const candidateTracks = buildOutwardTracks(candidatePitch)
|
|
663
|
+
if (tracksFitBoundary(candidateTracks)) {
|
|
664
|
+
lowerPitch = candidatePitch
|
|
665
|
+
targetTracks = candidateTracks
|
|
666
|
+
} else {
|
|
667
|
+
upperPitch = candidatePitch
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
distributedPitch = lowerPitch
|
|
671
|
+
}
|
|
672
|
+
for (const item of orderedItems) {
|
|
673
|
+
enforcedConnectionNames.add(item.connection.connection.name)
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
const minimumCornerIndexes: number[] = []
|
|
678
|
+
const maximumCornerIndexes: number[] = []
|
|
679
|
+
for (let index = 0; index < orderedItems.length; index++) {
|
|
680
|
+
const item = orderedItems[index]!
|
|
681
|
+
const preferredExit = item.bus.preferredExit
|
|
682
|
+
if (!preferredExit?.includes("-")) continue
|
|
683
|
+
const cornerSide = getCornerSide(preferredExit as FanoutCorner, direction)
|
|
684
|
+
if (!cornerSide) return null
|
|
685
|
+
enforcedConnectionNames.add(item.connection.connection.name)
|
|
686
|
+
if (cornerSide === "minimum") minimumCornerIndexes.push(index)
|
|
687
|
+
else maximumCornerIndexes.push(index)
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (
|
|
691
|
+
minimumCornerIndexes.some(
|
|
692
|
+
(index, expectedIndex) => index !== expectedIndex,
|
|
693
|
+
) ||
|
|
694
|
+
maximumCornerIndexes.some(
|
|
695
|
+
(index, offset) =>
|
|
696
|
+
index !== orderedItems.length - maximumCornerIndexes.length + offset,
|
|
697
|
+
)
|
|
698
|
+
) {
|
|
699
|
+
return null
|
|
700
|
+
}
|
|
701
|
+
const cornerInset = lanePitch * 2
|
|
702
|
+
if (minimumCornerIndexes.length > 0) {
|
|
703
|
+
targetTracks[0] = minimumLane + cornerInset
|
|
704
|
+
for (let index = 1; index < minimumCornerIndexes.length; index++) {
|
|
705
|
+
const preservedPitch = Math.max(
|
|
706
|
+
distributedPitch,
|
|
707
|
+
sourceTracks[index]! - sourceTracks[index - 1]!,
|
|
708
|
+
)
|
|
709
|
+
targetTracks[index] = targetTracks[index - 1]! + preservedPitch
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (maximumCornerIndexes.length > 0) {
|
|
713
|
+
const lastIndex = orderedItems.length - 1
|
|
714
|
+
targetTracks[lastIndex] = maximumLane - cornerInset
|
|
715
|
+
for (
|
|
716
|
+
let index = lastIndex - 1;
|
|
717
|
+
index >= orderedItems.length - maximumCornerIndexes.length;
|
|
718
|
+
index--
|
|
719
|
+
) {
|
|
720
|
+
const preservedPitch = Math.max(
|
|
721
|
+
distributedPitch,
|
|
722
|
+
sourceTracks[index + 1]! - sourceTracks[index]!,
|
|
723
|
+
)
|
|
724
|
+
targetTracks[index] = targetTracks[index + 1]! - preservedPitch
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return {
|
|
729
|
+
byConnectionName: new Map(
|
|
730
|
+
orderedItems.map((item, index) => [
|
|
731
|
+
item.connection.connection.name,
|
|
732
|
+
targetTracks[index]!,
|
|
733
|
+
]),
|
|
734
|
+
),
|
|
735
|
+
enforcedConnectionNames,
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
function buildPlan(path: RoutedPath): FanoutRoutePlan {
|
|
740
|
+
const { item, points, segments } = path
|
|
741
|
+
const route: SimplifiedPcbTrace["route"] = points.map((point, index) => ({
|
|
742
|
+
route_type: "wire",
|
|
743
|
+
x: point.x,
|
|
744
|
+
y: point.y,
|
|
745
|
+
width: segments[0]?.width ?? 0.1,
|
|
746
|
+
layer: "top",
|
|
747
|
+
...(index === 0 && item.connection.sourcePoint.pcb_port_id
|
|
748
|
+
? { start_pcb_port_id: item.connection.sourcePoint.pcb_port_id }
|
|
749
|
+
: {}),
|
|
750
|
+
}))
|
|
751
|
+
return {
|
|
752
|
+
busId: item.bus.busId,
|
|
753
|
+
connectionName: item.connection.connection.name,
|
|
754
|
+
connectionIndex: item.connection.connectionIndex,
|
|
755
|
+
sourcePointIndex: item.connection.sourcePointIndex,
|
|
756
|
+
sourcePoint: item.connection.sourcePoint,
|
|
757
|
+
sourceObstacle: item.connection.sourceObstacle,
|
|
758
|
+
sourceLayer: item.connection.sourceLayer,
|
|
759
|
+
targetLayer: "top",
|
|
760
|
+
termination: item.bus.termination,
|
|
761
|
+
direction: item.direction,
|
|
762
|
+
exitPoint: points.at(-1)!,
|
|
763
|
+
trace: {
|
|
764
|
+
type: "pcb_trace",
|
|
765
|
+
pcb_trace_id: `fanout:${item.connection.connection.name}`,
|
|
766
|
+
connection_name: item.connection.connection.name,
|
|
767
|
+
connectsTo: [
|
|
768
|
+
item.connection.connection.name,
|
|
769
|
+
...(item.connection.sourcePoint.pointId
|
|
770
|
+
? [item.connection.sourcePoint.pointId]
|
|
771
|
+
: []),
|
|
772
|
+
...(item.connection.sourcePoint.pcb_port_id
|
|
773
|
+
? [item.connection.sourcePoint.pcb_port_id]
|
|
774
|
+
: []),
|
|
775
|
+
],
|
|
776
|
+
route,
|
|
777
|
+
},
|
|
778
|
+
segments,
|
|
779
|
+
length: segments.reduce(
|
|
780
|
+
(total, segment) => total + distance(segment.start, segment.end),
|
|
781
|
+
0,
|
|
782
|
+
),
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function routesAreClear(params: {
|
|
787
|
+
paths: RoutedPath[]
|
|
788
|
+
obstacles: Obstacle[]
|
|
789
|
+
traceWidth: number
|
|
790
|
+
clearance: number
|
|
791
|
+
}): boolean {
|
|
792
|
+
const { paths, obstacles, traceWidth, clearance } = params
|
|
793
|
+
const requiredObstacleDistance = traceWidth / 2 + clearance
|
|
794
|
+
const requiredCenterDistance = traceWidth + clearance
|
|
795
|
+
|
|
796
|
+
for (const path of paths) {
|
|
797
|
+
if (
|
|
798
|
+
Math.abs(
|
|
799
|
+
getAxis(path.points.at(-1)!, path.item.direction) -
|
|
800
|
+
getExitAxis(path.item.bus),
|
|
801
|
+
) > 1e-6
|
|
802
|
+
) {
|
|
803
|
+
return false
|
|
804
|
+
}
|
|
805
|
+
for (const segment of path.segments) {
|
|
806
|
+
for (const obstacle of obstacles) {
|
|
807
|
+
if (
|
|
808
|
+
obstacle === path.item.connection.sourceObstacle ||
|
|
809
|
+
!obstacle.layers.includes("top")
|
|
810
|
+
) {
|
|
811
|
+
continue
|
|
812
|
+
}
|
|
813
|
+
if (
|
|
814
|
+
distanceSegmentToObstacle(segment, obstacle) <
|
|
815
|
+
requiredObstacleDistance - 1e-9
|
|
816
|
+
) {
|
|
817
|
+
return false
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
for (let firstIndex = 0; firstIndex < paths.length; firstIndex++) {
|
|
824
|
+
for (
|
|
825
|
+
let secondIndex = firstIndex + 1;
|
|
826
|
+
secondIndex < paths.length;
|
|
827
|
+
secondIndex++
|
|
828
|
+
) {
|
|
829
|
+
for (const firstSegment of paths[firstIndex]!.segments) {
|
|
830
|
+
for (const secondSegment of paths[secondIndex]!.segments) {
|
|
831
|
+
if (
|
|
832
|
+
distanceSegmentToSegment(
|
|
833
|
+
firstSegment.start,
|
|
834
|
+
firstSegment.end,
|
|
835
|
+
secondSegment.start,
|
|
836
|
+
secondSegment.end,
|
|
837
|
+
) <
|
|
838
|
+
requiredCenterDistance - 1e-9
|
|
839
|
+
) {
|
|
840
|
+
return false
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
return true
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
export function routeSingleLayerWithPushAndShove(
|
|
850
|
+
params: PushShoveParams,
|
|
851
|
+
): FanoutRoutePlan[] | null {
|
|
852
|
+
const { srj, buses, traceWidth, clearance, borderDistribution } = params
|
|
853
|
+
const requiredObstacleDistance = traceWidth / 2 + clearance
|
|
854
|
+
const recordsByConnectionName = new Map<string, ActiveRoute>()
|
|
855
|
+
const enforcedFinalTargets = new Map<string, number>()
|
|
856
|
+
const items = buses.flatMap((bus) =>
|
|
857
|
+
bus.connections.map((connection) => {
|
|
858
|
+
const source = {
|
|
859
|
+
x: connection.sourcePoint.x,
|
|
860
|
+
y: connection.sourcePoint.y,
|
|
861
|
+
}
|
|
862
|
+
const cornerChannelPrefixes = getCornerChannelPrefixes({
|
|
863
|
+
srj,
|
|
864
|
+
bus,
|
|
865
|
+
connection,
|
|
866
|
+
traceWidth,
|
|
867
|
+
clearance,
|
|
868
|
+
})
|
|
869
|
+
return {
|
|
870
|
+
bus,
|
|
871
|
+
connection,
|
|
872
|
+
direction: bus.direction,
|
|
873
|
+
source,
|
|
874
|
+
...(cornerChannelPrefixes ? { cornerChannelPrefixes } : {}),
|
|
875
|
+
} satisfies RoutingItem
|
|
876
|
+
}),
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
for (const direction of [
|
|
880
|
+
"left",
|
|
881
|
+
"right",
|
|
882
|
+
"up",
|
|
883
|
+
"down",
|
|
884
|
+
] as const satisfies readonly FanoutDirection[]) {
|
|
885
|
+
const sign = directionSign(direction)
|
|
886
|
+
const directionItems = items.filter(
|
|
887
|
+
(item) => item.direction === direction && !item.cornerChannelPrefixes,
|
|
888
|
+
)
|
|
889
|
+
if (directionItems.length === 0) continue
|
|
890
|
+
const boundaryMinimum = isHorizontal(direction)
|
|
891
|
+
? directionItems[0]!.bus.sharedBoundary.minY
|
|
892
|
+
: directionItems[0]!.bus.sharedBoundary.minX
|
|
893
|
+
const boundaryMaximum = isHorizontal(direction)
|
|
894
|
+
? directionItems[0]!.bus.sharedBoundary.maxY
|
|
895
|
+
: directionItems[0]!.bus.sharedBoundary.maxX
|
|
896
|
+
const exitAxis = getExitAxis(directionItems[0]!.bus)
|
|
897
|
+
const sourcesByEventKey = new Map<string, RoutingItem[]>()
|
|
898
|
+
const obstaclesByEventKey = new Map<string, Obstacle[]>()
|
|
899
|
+
const axisByEventKey = new Map<string, number>()
|
|
900
|
+
|
|
901
|
+
for (const item of directionItems) {
|
|
902
|
+
const axis = getAxis(item.source, direction)
|
|
903
|
+
const key = axis.toFixed(6)
|
|
904
|
+
axisByEventKey.set(key, axis)
|
|
905
|
+
const eventSources = sourcesByEventKey.get(key) ?? []
|
|
906
|
+
eventSources.push(item)
|
|
907
|
+
sourcesByEventKey.set(key, eventSources)
|
|
908
|
+
}
|
|
909
|
+
for (const [eventKey, eventSources] of sourcesByEventKey) {
|
|
910
|
+
const eventAxis = axisByEventKey.get(eventKey)!
|
|
911
|
+
const eventComponentIds = new Set(
|
|
912
|
+
eventSources.map((item) => item.bus.componentId),
|
|
913
|
+
)
|
|
914
|
+
obstaclesByEventKey.set(
|
|
915
|
+
eventKey,
|
|
916
|
+
srj.obstacles.filter(
|
|
917
|
+
(obstacle) =>
|
|
918
|
+
obstacle.layers.includes("top") &&
|
|
919
|
+
!!obstacle.componentId &&
|
|
920
|
+
eventComponentIds.has(obstacle.componentId) &&
|
|
921
|
+
Math.abs(getAxis(obstacle.center, direction) - eventAxis) < 1e-6,
|
|
922
|
+
),
|
|
923
|
+
)
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
const eventAxes = [...axisByEventKey.values()].toSorted(
|
|
927
|
+
(a, b) => sign * (a - b),
|
|
928
|
+
)
|
|
929
|
+
const active = new Map<string, ActiveRoute>()
|
|
930
|
+
for (let eventIndex = 0; eventIndex < eventAxes.length; eventIndex++) {
|
|
931
|
+
const eventAxis = eventAxes[eventIndex]!
|
|
932
|
+
const eventKey = eventAxis.toFixed(6)
|
|
933
|
+
for (const item of sourcesByEventKey.get(eventKey) ?? []) {
|
|
934
|
+
const directionalPadSize = isHorizontal(direction)
|
|
935
|
+
? item.connection.sourceObstacle.width
|
|
936
|
+
: item.connection.sourceObstacle.height
|
|
937
|
+
const sourceEscapeDistance =
|
|
938
|
+
Math.max(
|
|
939
|
+
item.connection.sourceObstacle.width,
|
|
940
|
+
item.connection.sourceObstacle.height,
|
|
941
|
+
) < 1.5
|
|
942
|
+
? directionalPadSize / 2 + requiredObstacleDistance
|
|
943
|
+
: 0
|
|
944
|
+
const sourceEscapePoint = makePoint(
|
|
945
|
+
eventAxis + sign * sourceEscapeDistance,
|
|
946
|
+
getPerpendicularAxis(item.source, direction),
|
|
947
|
+
direction,
|
|
948
|
+
)
|
|
949
|
+
active.set(item.connection.connection.name, {
|
|
950
|
+
item,
|
|
951
|
+
points:
|
|
952
|
+
distance(item.source, sourceEscapePoint) > 1e-9
|
|
953
|
+
? [item.source, sourceEscapePoint]
|
|
954
|
+
: [item.source],
|
|
955
|
+
track: getPerpendicularAxis(item.source, direction),
|
|
956
|
+
})
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
const nextAxis = eventAxes[eventIndex + 1] ?? exitAxis
|
|
960
|
+
if (active.size === 0 || sign * (nextAxis - eventAxis) <= 1e-9) {
|
|
961
|
+
continue
|
|
962
|
+
}
|
|
963
|
+
const orderedRoutes = [...active.values()].toSorted(
|
|
964
|
+
(a, b) =>
|
|
965
|
+
a.track - b.track ||
|
|
966
|
+
a.item.connection.connection.name.localeCompare(
|
|
967
|
+
b.item.connection.connection.name,
|
|
968
|
+
),
|
|
969
|
+
)
|
|
970
|
+
const lookaheadObstacles = eventAxes
|
|
971
|
+
.slice(eventIndex + 1, eventIndex + 3)
|
|
972
|
+
.flatMap((axis) => obstaclesByEventKey.get(axis.toFixed(6)) ?? [])
|
|
973
|
+
const maximumShifts = orderedRoutes.map((route) =>
|
|
974
|
+
Math.abs(nextAxis - getAxis(route.points.at(-1)!, direction)),
|
|
975
|
+
)
|
|
976
|
+
const selectedTracks = getCandidateTracks({
|
|
977
|
+
direction,
|
|
978
|
+
activeRoutes: orderedRoutes,
|
|
979
|
+
obstacles: lookaheadObstacles,
|
|
980
|
+
boundaryMinimum,
|
|
981
|
+
boundaryMaximum,
|
|
982
|
+
traceWidth,
|
|
983
|
+
clearance,
|
|
984
|
+
requestedTracks: orderedRoutes.map((route) => route.track),
|
|
985
|
+
maximumShifts,
|
|
986
|
+
})
|
|
987
|
+
if (!selectedTracks) return null
|
|
988
|
+
|
|
989
|
+
for (let index = 0; index < orderedRoutes.length; index++) {
|
|
990
|
+
const route = orderedRoutes[index]!
|
|
991
|
+
const selectedTrack = selectedTracks[index]!
|
|
992
|
+
const shift = Math.abs(selectedTrack - route.track)
|
|
993
|
+
const routeStartAxis = getAxis(route.points.at(-1)!, direction)
|
|
994
|
+
const diagonalEnd = makePoint(
|
|
995
|
+
routeStartAxis + sign * shift,
|
|
996
|
+
selectedTrack,
|
|
997
|
+
direction,
|
|
998
|
+
)
|
|
999
|
+
if (distance(route.points.at(-1)!, diagonalEnd) > 1e-9) {
|
|
1000
|
+
route.points.push(diagonalEnd)
|
|
1001
|
+
}
|
|
1002
|
+
const nextPoint = makePoint(nextAxis, selectedTrack, direction)
|
|
1003
|
+
if (distance(route.points.at(-1)!, nextPoint) > 1e-9) {
|
|
1004
|
+
route.points.push(nextPoint)
|
|
1005
|
+
}
|
|
1006
|
+
route.track = selectedTrack
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
const finalTrackTargets = getFinalTrackTargets({
|
|
1011
|
+
items: directionItems,
|
|
1012
|
+
direction,
|
|
1013
|
+
boundaryMinimum,
|
|
1014
|
+
boundaryMaximum,
|
|
1015
|
+
traceWidth,
|
|
1016
|
+
clearance,
|
|
1017
|
+
borderDistribution,
|
|
1018
|
+
currentTrackByConnectionName: new Map(
|
|
1019
|
+
[...active.values()].map((route) => [
|
|
1020
|
+
route.item.connection.connection.name,
|
|
1021
|
+
route.track,
|
|
1022
|
+
]),
|
|
1023
|
+
),
|
|
1024
|
+
})
|
|
1025
|
+
if (!finalTrackTargets) return null
|
|
1026
|
+
const orderedActiveRoutes = [...active.values()].toSorted(
|
|
1027
|
+
(first, second) =>
|
|
1028
|
+
first.track - second.track ||
|
|
1029
|
+
first.item.connection.connection.name.localeCompare(
|
|
1030
|
+
second.item.connection.connection.name,
|
|
1031
|
+
),
|
|
1032
|
+
)
|
|
1033
|
+
const distributionStartOffsetByConnectionName = new Map<string, number>()
|
|
1034
|
+
const lanePitch = traceWidth + clearance
|
|
1035
|
+
let nextMinimumStartOffset = lanePitch
|
|
1036
|
+
for (const route of orderedActiveRoutes) {
|
|
1037
|
+
const connectionName = route.item.connection.connection.name
|
|
1038
|
+
if (!finalTrackTargets.enforcedConnectionNames.has(connectionName)) {
|
|
1039
|
+
continue
|
|
1040
|
+
}
|
|
1041
|
+
const targetTrack =
|
|
1042
|
+
finalTrackTargets.byConnectionName.get(connectionName)!
|
|
1043
|
+
const signedShift = targetTrack - route.track
|
|
1044
|
+
if (signedShift >= -1e-9) continue
|
|
1045
|
+
distributionStartOffsetByConnectionName.set(
|
|
1046
|
+
connectionName,
|
|
1047
|
+
nextMinimumStartOffset,
|
|
1048
|
+
)
|
|
1049
|
+
nextMinimumStartOffset += lanePitch * 2
|
|
1050
|
+
}
|
|
1051
|
+
let nextMaximumStartOffset = lanePitch
|
|
1052
|
+
for (let index = orderedActiveRoutes.length - 1; index >= 0; index--) {
|
|
1053
|
+
const route = orderedActiveRoutes[index]!
|
|
1054
|
+
const connectionName = route.item.connection.connection.name
|
|
1055
|
+
if (!finalTrackTargets.enforcedConnectionNames.has(connectionName)) {
|
|
1056
|
+
continue
|
|
1057
|
+
}
|
|
1058
|
+
const targetTrack =
|
|
1059
|
+
finalTrackTargets.byConnectionName.get(connectionName)!
|
|
1060
|
+
const signedShift = targetTrack - route.track
|
|
1061
|
+
if (signedShift <= 1e-9) continue
|
|
1062
|
+
distributionStartOffsetByConnectionName.set(
|
|
1063
|
+
connectionName,
|
|
1064
|
+
nextMaximumStartOffset,
|
|
1065
|
+
)
|
|
1066
|
+
nextMaximumStartOffset += lanePitch * 2
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
for (const route of orderedActiveRoutes) {
|
|
1070
|
+
const connectionName = route.item.connection.connection.name
|
|
1071
|
+
const intendedTargetTrack =
|
|
1072
|
+
finalTrackTargets.byConnectionName.get(connectionName) ?? route.track
|
|
1073
|
+
const startOffset =
|
|
1074
|
+
distributionStartOffsetByConnectionName.get(connectionName) ?? 0
|
|
1075
|
+
const previousPoint = route.points.at(-2)
|
|
1076
|
+
const availableDepth = previousPoint
|
|
1077
|
+
? sign * (exitAxis - getAxis(previousPoint, direction))
|
|
1078
|
+
: 0
|
|
1079
|
+
const intendedSignedShift = intendedTargetTrack - route.track
|
|
1080
|
+
const maximumRunwayShift = Math.min(
|
|
1081
|
+
Math.abs(intendedSignedShift),
|
|
1082
|
+
Math.max(0, availableDepth - startOffset),
|
|
1083
|
+
)
|
|
1084
|
+
const maximumShift = getMaximumObstacleClearDistributionShift({
|
|
1085
|
+
route,
|
|
1086
|
+
direction,
|
|
1087
|
+
exitAxis,
|
|
1088
|
+
signedShift: intendedSignedShift,
|
|
1089
|
+
maximumShift: maximumRunwayShift,
|
|
1090
|
+
obstacles: srj.obstacles,
|
|
1091
|
+
requiredObstacleDistance,
|
|
1092
|
+
})
|
|
1093
|
+
const adjustedShift = Math.sign(intendedSignedShift) * maximumShift
|
|
1094
|
+
finalTrackTargets.byConnectionName.set(
|
|
1095
|
+
connectionName,
|
|
1096
|
+
route.track + adjustedShift,
|
|
1097
|
+
)
|
|
1098
|
+
}
|
|
1099
|
+
for (const connectionName of finalTrackTargets.enforcedConnectionNames) {
|
|
1100
|
+
enforcedFinalTargets.set(
|
|
1101
|
+
connectionName,
|
|
1102
|
+
finalTrackTargets.byConnectionName.get(connectionName)!,
|
|
1103
|
+
)
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
for (const route of orderedActiveRoutes) {
|
|
1107
|
+
const connectionName = route.item.connection.connection.name
|
|
1108
|
+
const targetTrack =
|
|
1109
|
+
finalTrackTargets.byConnectionName.get(connectionName) ?? route.track
|
|
1110
|
+
const shift = Math.abs(targetTrack - route.track)
|
|
1111
|
+
const distributionStartOffset =
|
|
1112
|
+
distributionStartOffsetByConnectionName.get(connectionName) ?? 0
|
|
1113
|
+
const distributionDepth =
|
|
1114
|
+
shift > 1e-9 ? distributionStartOffset + shift : 0
|
|
1115
|
+
const distributionStartAxis = exitAxis - sign * distributionDepth
|
|
1116
|
+
const lastPoint = route.points.at(-1)!
|
|
1117
|
+
if (Math.abs(getAxis(lastPoint, direction) - exitAxis) > 1e-6) {
|
|
1118
|
+
return null
|
|
1119
|
+
}
|
|
1120
|
+
const previousPoint = route.points.at(-2)
|
|
1121
|
+
if (
|
|
1122
|
+
previousPoint &&
|
|
1123
|
+
(Math.abs(
|
|
1124
|
+
getPerpendicularAxis(previousPoint, direction) - route.track,
|
|
1125
|
+
) > 1e-6 ||
|
|
1126
|
+
sign * (distributionStartAxis - getAxis(previousPoint, direction)) <
|
|
1127
|
+
-1e-6)
|
|
1128
|
+
) {
|
|
1129
|
+
return null
|
|
1130
|
+
}
|
|
1131
|
+
const stagingPoint = makePoint(
|
|
1132
|
+
distributionStartAxis,
|
|
1133
|
+
route.track,
|
|
1134
|
+
direction,
|
|
1135
|
+
)
|
|
1136
|
+
if (previousPoint && distance(previousPoint, stagingPoint) < 1e-9) {
|
|
1137
|
+
route.points.pop()
|
|
1138
|
+
} else {
|
|
1139
|
+
route.points[route.points.length - 1] = stagingPoint
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
for (const route of orderedActiveRoutes) {
|
|
1144
|
+
const connectionName = route.item.connection.connection.name
|
|
1145
|
+
const targetTrack =
|
|
1146
|
+
finalTrackTargets.byConnectionName.get(connectionName) ?? route.track
|
|
1147
|
+
const shift = Math.abs(targetTrack - route.track)
|
|
1148
|
+
const distributionStartOffset =
|
|
1149
|
+
distributionStartOffsetByConnectionName.get(connectionName) ?? 0
|
|
1150
|
+
if (shift > 1e-9 && distributionStartOffset > 1e-9) {
|
|
1151
|
+
route.points.push(
|
|
1152
|
+
makePoint(
|
|
1153
|
+
getAxis(route.points.at(-1)!, direction) +
|
|
1154
|
+
sign * distributionStartOffset,
|
|
1155
|
+
route.track,
|
|
1156
|
+
direction,
|
|
1157
|
+
),
|
|
1158
|
+
)
|
|
1159
|
+
}
|
|
1160
|
+
if (shift > 1e-9) {
|
|
1161
|
+
route.points.push(
|
|
1162
|
+
makePoint(
|
|
1163
|
+
getAxis(route.points.at(-1)!, direction) + sign * shift,
|
|
1164
|
+
targetTrack,
|
|
1165
|
+
direction,
|
|
1166
|
+
),
|
|
1167
|
+
)
|
|
1168
|
+
route.track = targetTrack
|
|
1169
|
+
}
|
|
1170
|
+
const boundaryPoint = makePoint(exitAxis, route.track, direction)
|
|
1171
|
+
if (distance(route.points.at(-1)!, boundaryPoint) > 1e-9) {
|
|
1172
|
+
if (
|
|
1173
|
+
sign * (exitAxis - getAxis(route.points.at(-1)!, direction)) <
|
|
1174
|
+
-1e-6
|
|
1175
|
+
) {
|
|
1176
|
+
return null
|
|
1177
|
+
}
|
|
1178
|
+
route.points.push(boundaryPoint)
|
|
1179
|
+
}
|
|
1180
|
+
recordsByConnectionName.set(connectionName, route)
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
const acceptedSegments = [...recordsByConnectionName.values()].flatMap(
|
|
1185
|
+
(route) => getPathSegments(compressPath(route.points), traceWidth),
|
|
1186
|
+
)
|
|
1187
|
+
for (const item of items) {
|
|
1188
|
+
if (!item.cornerChannelPrefixes) continue
|
|
1189
|
+
const route = completeCornerChannelRoute({
|
|
1190
|
+
item,
|
|
1191
|
+
srj,
|
|
1192
|
+
acceptedSegments,
|
|
1193
|
+
traceWidth,
|
|
1194
|
+
clearance,
|
|
1195
|
+
})
|
|
1196
|
+
if (!route) return null
|
|
1197
|
+
recordsByConnectionName.set(item.connection.connection.name, route)
|
|
1198
|
+
acceptedSegments.push(
|
|
1199
|
+
...getPathSegments(compressPath(route.points), traceWidth),
|
|
1200
|
+
)
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
if (recordsByConnectionName.size !== items.length) return null
|
|
1204
|
+
const maximumTargetError = (traceWidth + clearance) / 2 + 1e-6
|
|
1205
|
+
for (const [connectionName, targetTrack] of enforcedFinalTargets) {
|
|
1206
|
+
const route = recordsByConnectionName.get(connectionName)
|
|
1207
|
+
if (!route || Math.abs(route.track - targetTrack) > maximumTargetError) {
|
|
1208
|
+
return null
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
const paths = items.map((item) => {
|
|
1212
|
+
const activeRoute = recordsByConnectionName.get(
|
|
1213
|
+
item.connection.connection.name,
|
|
1214
|
+
)!
|
|
1215
|
+
const points = compressPath(activeRoute.points)
|
|
1216
|
+
return {
|
|
1217
|
+
item,
|
|
1218
|
+
points,
|
|
1219
|
+
segments: getPathSegments(points, traceWidth),
|
|
1220
|
+
} satisfies RoutedPath
|
|
1221
|
+
})
|
|
1222
|
+
if (
|
|
1223
|
+
!routesAreClear({
|
|
1224
|
+
paths,
|
|
1225
|
+
obstacles: srj.obstacles,
|
|
1226
|
+
traceWidth,
|
|
1227
|
+
clearance,
|
|
1228
|
+
})
|
|
1229
|
+
) {
|
|
1230
|
+
return null
|
|
1231
|
+
}
|
|
1232
|
+
return paths.map(buildPlan)
|
|
1233
|
+
}
|