@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.
@@ -0,0 +1,934 @@
1
+ import type {
2
+ Obstacle,
3
+ SimpleRouteJson,
4
+ SimplifiedPcbTrace,
5
+ } from "@tscircuit/capacity-autorouter"
6
+ import {
7
+ distance,
8
+ distancePointToObstacle,
9
+ distancePointToSegment,
10
+ distanceSegmentToObstacle,
11
+ segmentsAreClear,
12
+ } from "./geometry"
13
+ import { getLayerSpan } from "./layer-names"
14
+ import type {
15
+ FanoutDirection,
16
+ FanoutRoutePlan,
17
+ Point2D,
18
+ PreparedBus,
19
+ PreparedConnection,
20
+ RoutedSegment,
21
+ } from "./types"
22
+
23
+ interface RouteBusParams {
24
+ srj: SimpleRouteJson
25
+ bus: PreparedBus
26
+ targetLayer: string
27
+ acceptedPlans: FanoutRoutePlan[]
28
+ layerNames: string[]
29
+ traceWidth: number
30
+ viaDiameter: number
31
+ viaHoleDiameter: number
32
+ clearance: number
33
+ compactBusTracks: boolean
34
+ }
35
+
36
+ interface TrackCandidate {
37
+ value: number
38
+ kind: "corridor" | "gap" | "margin" | "preferred"
39
+ }
40
+
41
+ type ViaHandedness = -1 | 0 | 1
42
+
43
+ function isHorizontal(direction: FanoutDirection): boolean {
44
+ return direction === "left" || direction === "right"
45
+ }
46
+
47
+ function directionSign(direction: FanoutDirection): number {
48
+ return direction === "right" || direction === "up" ? 1 : -1
49
+ }
50
+
51
+ function getAxis(point: Point2D, direction: FanoutDirection): number {
52
+ return isHorizontal(direction) ? point.x : point.y
53
+ }
54
+
55
+ function getPerpendicularAxis(
56
+ point: Point2D,
57
+ direction: FanoutDirection,
58
+ ): number {
59
+ return isHorizontal(direction) ? point.y : point.x
60
+ }
61
+
62
+ function makePoint(
63
+ axis: number,
64
+ perpendicularAxis: number,
65
+ direction: FanoutDirection,
66
+ ): Point2D {
67
+ return isHorizontal(direction)
68
+ ? { x: axis, y: perpendicularAxis }
69
+ : { x: perpendicularAxis, y: axis }
70
+ }
71
+
72
+ function getExitAxis(bus: PreparedBus): number {
73
+ switch (bus.direction) {
74
+ case "right":
75
+ return bus.sharedBoundary.maxX
76
+ case "left":
77
+ return bus.sharedBoundary.minX
78
+ case "up":
79
+ return bus.sharedBoundary.maxY
80
+ case "down":
81
+ return bus.sharedBoundary.minY
82
+ }
83
+ }
84
+
85
+ function getDirectionalPitch(bus: PreparedBus): number {
86
+ return isHorizontal(bus.direction) ? bus.pitchX : bus.pitchY
87
+ }
88
+
89
+ function getPerpendicularPitch(bus: PreparedBus): number {
90
+ return isHorizontal(bus.direction) ? bus.pitchY : bus.pitchX
91
+ }
92
+
93
+ function getDepthInRows(bus: PreparedBus): number {
94
+ const directionalCoordinates = (
95
+ isHorizontal(bus.direction) ? bus.xCoordinates : bus.yCoordinates
96
+ ).toSorted((a, b) => a - b)
97
+ const averageDirectionalSource =
98
+ bus.connections.reduce(
99
+ (sum, candidate) => sum + getAxis(candidate.sourcePoint, bus.direction),
100
+ 0,
101
+ ) / bus.connections.length
102
+ const outwardCoordinate =
103
+ directionSign(bus.direction) > 0
104
+ ? directionalCoordinates.at(-1)!
105
+ : directionalCoordinates[0]!
106
+
107
+ return (
108
+ Math.abs(outwardCoordinate - averageDirectionalSource) /
109
+ getDirectionalPitch(bus)
110
+ )
111
+ }
112
+
113
+ function busIsOnOutwardComponentEdge(bus: PreparedBus): boolean {
114
+ const directionalCoordinates = isHorizontal(bus.direction)
115
+ ? bus.xCoordinates
116
+ : bus.yCoordinates
117
+ const averageDirectionalSource =
118
+ bus.connections.reduce(
119
+ (sum, connection) => sum + getAxis(connection.sourcePoint, bus.direction),
120
+ 0,
121
+ ) / bus.connections.length
122
+ const outwardCoordinate =
123
+ directionSign(bus.direction) > 0
124
+ ? Math.max(...directionalCoordinates)
125
+ : Math.min(...directionalCoordinates)
126
+ return Math.abs(averageDirectionalSource - outwardCoordinate) < 1e-6
127
+ }
128
+
129
+ function getConnectionRank(
130
+ bus: PreparedBus,
131
+ connection: PreparedConnection,
132
+ ): number {
133
+ const connectionRank = [...bus.connections]
134
+ .sort(
135
+ (a, b) =>
136
+ getPerpendicularAxis(a.sourcePoint, bus.direction) -
137
+ getPerpendicularAxis(b.sourcePoint, bus.direction),
138
+ )
139
+ .findIndex(
140
+ (candidate) => candidate.connectionIndex === connection.connectionIndex,
141
+ )
142
+ if (connectionRank < 0) {
143
+ throw new Error(
144
+ `FanoutSolver: connection "${connection.connection.name}" is missing from bus "${bus.busId}"`,
145
+ )
146
+ }
147
+ return connectionRank
148
+ }
149
+
150
+ function pointIsInsideBounds(
151
+ point: Point2D,
152
+ bounds: SimpleRouteJson["bounds"],
153
+ ): boolean {
154
+ return (
155
+ point.x >= bounds.minX - 1e-6 &&
156
+ point.x <= bounds.maxX + 1e-6 &&
157
+ point.y >= bounds.minY - 1e-6 &&
158
+ point.y <= bounds.maxY + 1e-6
159
+ )
160
+ }
161
+
162
+ function getTracksInSpan(
163
+ minimum: number,
164
+ maximum: number,
165
+ traceWidth: number,
166
+ clearance: number,
167
+ kind: TrackCandidate["kind"],
168
+ ): TrackCandidate[] {
169
+ const freeWidth = maximum - minimum
170
+ const trackCount = Math.floor(
171
+ (freeWidth - clearance) / (traceWidth + clearance) + 1e-9,
172
+ )
173
+ if (trackCount < 1) return []
174
+ const usedWidth = trackCount * traceWidth + (trackCount - 1) * clearance
175
+ const firstTrack = minimum + (freeWidth - usedWidth) / 2 + traceWidth / 2
176
+ return Array.from({ length: trackCount }, (_, index) => ({
177
+ value: firstTrack + index * (traceWidth + clearance),
178
+ kind,
179
+ }))
180
+ }
181
+
182
+ function getTrackCandidates(params: {
183
+ bus: PreparedBus
184
+ connection: PreparedConnection
185
+ preferredTrack: number
186
+ traceWidth: number
187
+ clearance: number
188
+ }): TrackCandidate[] {
189
+ const { bus, connection, preferredTrack, traceWidth, clearance } = params
190
+ const coordinates = (
191
+ isHorizontal(bus.direction) ? bus.yCoordinates : bus.xCoordinates
192
+ ).toSorted((a, b) => a - b)
193
+ const obstacleHalfSize = Math.max(
194
+ ...bus.componentObstacles.map((obstacle) =>
195
+ isHorizontal(bus.direction) ? obstacle.height / 2 : obstacle.width / 2,
196
+ ),
197
+ )
198
+ const boundaryMinimum = isHorizontal(bus.direction)
199
+ ? bus.sharedBoundary.minY
200
+ : bus.sharedBoundary.minX
201
+ const boundaryMaximum = isHorizontal(bus.direction)
202
+ ? bus.sharedBoundary.maxY
203
+ : bus.sharedBoundary.maxX
204
+ const maximumJog = boundaryMaximum - boundaryMinimum
205
+ const ladderMinimum = boundaryMinimum
206
+ const ladderMaximum = boundaryMaximum
207
+ const tracks: TrackCandidate[] = [
208
+ { value: preferredTrack, kind: "preferred" },
209
+ ...getTracksInSpan(
210
+ ladderMinimum,
211
+ coordinates[0]! - obstacleHalfSize,
212
+ traceWidth,
213
+ clearance,
214
+ "margin",
215
+ ),
216
+ ]
217
+ for (let index = 0; index < coordinates.length; index++) {
218
+ tracks.push({ value: coordinates[index]!, kind: "corridor" })
219
+ if (index < coordinates.length - 1) {
220
+ tracks.push(
221
+ ...getTracksInSpan(
222
+ coordinates[index]! + obstacleHalfSize,
223
+ coordinates[index + 1]! - obstacleHalfSize,
224
+ traceWidth,
225
+ clearance,
226
+ "gap",
227
+ ),
228
+ )
229
+ }
230
+ }
231
+ tracks.push(
232
+ ...getTracksInSpan(
233
+ coordinates.at(-1)! + obstacleHalfSize,
234
+ ladderMaximum,
235
+ traceWidth,
236
+ clearance,
237
+ "margin",
238
+ ),
239
+ )
240
+
241
+ const sourceTrack = getPerpendicularAxis(
242
+ connection.sourcePoint,
243
+ bus.direction,
244
+ )
245
+ const componentCenter = (coordinates[0]! + coordinates.at(-1)!) / 2
246
+ return tracks
247
+ .filter((track) => Math.abs(track.value - sourceTrack) <= maximumJog + 1e-9)
248
+ .filter(
249
+ (track, index, candidates) =>
250
+ candidates.findIndex(
251
+ (candidate) => Math.abs(candidate.value - track.value) < 1e-9,
252
+ ) === index,
253
+ )
254
+ .sort(
255
+ (a, b) =>
256
+ Math.abs(a.value - preferredTrack) -
257
+ Math.abs(b.value - preferredTrack) -
258
+ (Math.abs(a.value - componentCenter) -
259
+ Math.abs(b.value - componentCenter)) *
260
+ 1e-3,
261
+ )
262
+ }
263
+
264
+ function getPreferredTrack(params: {
265
+ bus: PreparedBus
266
+ connection: PreparedConnection
267
+ targetUsesVia: boolean
268
+ interstitialEscape: boolean
269
+ compactBusTracks: boolean
270
+ traceWidth: number
271
+ viaDiameter: number
272
+ clearance: number
273
+ }): number {
274
+ const {
275
+ bus,
276
+ connection,
277
+ targetUsesVia,
278
+ interstitialEscape,
279
+ compactBusTracks,
280
+ traceWidth,
281
+ viaDiameter,
282
+ clearance,
283
+ } = params
284
+ const perpendicularCoordinates = (
285
+ isHorizontal(bus.direction) ? bus.yCoordinates : bus.xCoordinates
286
+ ).toSorted((a, b) => a - b)
287
+ const sourceTrack = getPerpendicularAxis(
288
+ connection.sourcePoint,
289
+ bus.direction,
290
+ )
291
+ if (compactBusTracks) {
292
+ const connectionRank = getConnectionRank(bus, connection)
293
+ const componentCenter =
294
+ (perpendicularCoordinates[0]! + perpendicularCoordinates.at(-1)!) / 2
295
+ return (
296
+ componentCenter +
297
+ (connectionRank - (bus.connections.length - 1) / 2) *
298
+ (traceWidth + clearance)
299
+ )
300
+ }
301
+ if (!targetUsesVia) return sourceTrack
302
+ if (!interstitialEscape) return sourceTrack
303
+
304
+ const depthInRows = getDepthInRows(bus)
305
+
306
+ const trackPitch = traceWidth + clearance
307
+ const halfConnectionCount = Math.ceil(bus.connections.length / 2)
308
+ const sideBandWidth = (halfConnectionCount - 1) * trackPitch
309
+ const bandSeparation = viaDiameter / 2 + traceWidth / 2 + clearance + 1e-3
310
+ const depthIndex = Math.round(depthInRows) + 1
311
+ const nearOffset =
312
+ depthIndex * bandSeparation + (depthIndex - 1) * sideBandWidth
313
+ const connectionRank = getConnectionRank(bus, connection)
314
+ const componentMinimum = perpendicularCoordinates[0]!
315
+ const componentMaximum = perpendicularCoordinates.at(-1)!
316
+ const requestedTrack =
317
+ connectionRank < halfConnectionCount
318
+ ? componentMinimum -
319
+ nearOffset -
320
+ (halfConnectionCount - connectionRank - 1) * trackPitch
321
+ : componentMaximum +
322
+ nearOffset +
323
+ (connectionRank - halfConnectionCount) * trackPitch
324
+ const boundaryMinimum = isHorizontal(bus.direction)
325
+ ? bus.sharedBoundary.minY
326
+ : bus.sharedBoundary.minX
327
+ const boundaryMaximum = isHorizontal(bus.direction)
328
+ ? bus.sharedBoundary.maxY
329
+ : bus.sharedBoundary.maxX
330
+
331
+ return Math.max(
332
+ boundaryMinimum + traceWidth / 2,
333
+ Math.min(boundaryMaximum - traceWidth / 2, requestedTrack),
334
+ )
335
+ }
336
+
337
+ function getConnectionOrders(bus: PreparedBus): PreparedConnection[][] {
338
+ const sign = directionSign(bus.direction)
339
+ const outwardFirst = [...bus.connections].sort((a, b) => {
340
+ const directionalDifference =
341
+ sign *
342
+ (getAxis(b.sourcePoint, bus.direction) -
343
+ getAxis(a.sourcePoint, bus.direction))
344
+ if (Math.abs(directionalDifference) > 1e-6) {
345
+ return directionalDifference
346
+ }
347
+ return (
348
+ getPerpendicularAxis(a.sourcePoint, bus.direction) -
349
+ getPerpendicularAxis(b.sourcePoint, bus.direction)
350
+ )
351
+ })
352
+ const perpendicularFirst = [...bus.connections].sort(
353
+ (a, b) =>
354
+ getPerpendicularAxis(a.sourcePoint, bus.direction) -
355
+ getPerpendicularAxis(b.sourcePoint, bus.direction) ||
356
+ sign *
357
+ (getAxis(b.sourcePoint, bus.direction) -
358
+ getAxis(a.sourcePoint, bus.direction)),
359
+ )
360
+ const orders = [
361
+ outwardFirst,
362
+ [...outwardFirst].reverse(),
363
+ perpendicularFirst,
364
+ [...perpendicularFirst].reverse(),
365
+ ]
366
+ for (let offset = 1; offset < Math.min(outwardFirst.length, 8); offset++) {
367
+ orders.push([
368
+ ...outwardFirst.slice(offset),
369
+ ...outwardFirst.slice(0, offset),
370
+ ])
371
+ }
372
+ return orders
373
+ }
374
+
375
+ function appendSegment(
376
+ segments: RoutedSegment[],
377
+ start: Point2D,
378
+ end: Point2D,
379
+ width: number,
380
+ layer: string,
381
+ ): void {
382
+ if (distance(start, end) < 1e-9) return
383
+ segments.push({ start, end, width, layer })
384
+ }
385
+
386
+ function chamferOrthogonalPolyline(
387
+ points: Point2D[],
388
+ requestedChamfer: number,
389
+ ): Point2D[] {
390
+ if (points.length < 3) return points
391
+ const chamfered: Point2D[] = [points[0]!]
392
+
393
+ for (let index = 1; index < points.length - 1; index++) {
394
+ const previous = points[index - 1]!
395
+ const corner = points[index]!
396
+ const next = points[index + 1]!
397
+ const incomingLength = distance(previous, corner)
398
+ const outgoingLength = distance(corner, next)
399
+ if (incomingLength < 1e-9 || outgoingLength < 1e-9) continue
400
+ const incomingUnit = {
401
+ x: (corner.x - previous.x) / incomingLength,
402
+ y: (corner.y - previous.y) / incomingLength,
403
+ }
404
+ const outgoingUnit = {
405
+ x: (next.x - corner.x) / outgoingLength,
406
+ y: (next.y - corner.y) / outgoingLength,
407
+ }
408
+ const dot =
409
+ incomingUnit.x * outgoingUnit.x + incomingUnit.y * outgoingUnit.y
410
+ if (Math.abs(dot) > 1e-6) {
411
+ chamfered.push(corner)
412
+ continue
413
+ }
414
+
415
+ const chamfer = Math.min(
416
+ requestedChamfer,
417
+ incomingLength / 2,
418
+ outgoingLength / 2,
419
+ )
420
+ chamfered.push({
421
+ x: corner.x - incomingUnit.x * chamfer,
422
+ y: corner.y - incomingUnit.y * chamfer,
423
+ })
424
+ chamfered.push({
425
+ x: corner.x + outgoingUnit.x * chamfer,
426
+ y: corner.y + outgoingUnit.y * chamfer,
427
+ })
428
+ }
429
+
430
+ chamfered.push(points.at(-1)!)
431
+ return chamfered
432
+ }
433
+
434
+ function buildPlan(params: {
435
+ preparedConnection: PreparedConnection
436
+ bus: PreparedBus
437
+ targetLayer: string
438
+ track: number
439
+ exitAxis: number
440
+ layerNames: string[]
441
+ traceWidth: number
442
+ viaDiameter: number
443
+ viaHoleDiameter: number
444
+ viaHandedness: ViaHandedness
445
+ interstitialEscape: boolean
446
+ spreadLaneIndex: number
447
+ clearance: number
448
+ terminateAtVia: boolean
449
+ }): FanoutRoutePlan {
450
+ const {
451
+ preparedConnection,
452
+ bus,
453
+ targetLayer,
454
+ track,
455
+ exitAxis,
456
+ layerNames,
457
+ traceWidth,
458
+ viaDiameter,
459
+ viaHoleDiameter,
460
+ viaHandedness,
461
+ interstitialEscape,
462
+ spreadLaneIndex,
463
+ clearance,
464
+ terminateAtVia,
465
+ } = params
466
+ const sourcePoint = {
467
+ x: preparedConnection.sourcePoint.x,
468
+ y: preparedConnection.sourcePoint.y,
469
+ }
470
+ const sign = directionSign(bus.direction)
471
+ const directionalPitch = getDirectionalPitch(bus)
472
+ const perpendicularPitch = getPerpendicularPitch(bus)
473
+ const targetUsesVia = targetLayer !== preparedConnection.sourceLayer
474
+ const directionalPadSize = isHorizontal(bus.direction)
475
+ ? preparedConnection.sourceObstacle.width
476
+ : preparedConnection.sourceObstacle.height
477
+ const initialEscapeDistance =
478
+ targetUsesVia && !busIsOnOutwardComponentEdge(bus)
479
+ ? directionalPadSize >= directionalPitch
480
+ ? directionalPadSize / 2 + viaDiameter / 2 + clearance + 1e-3
481
+ : directionalPitch * 0.5
482
+ : !targetUsesVia
483
+ ? directionalPadSize / 2 + traceWidth / 2 + clearance + 1e-3
484
+ : Math.max(
485
+ directionalPitch * 0.5,
486
+ directionalPadSize / 2 +
487
+ (targetUsesVia ? viaDiameter : traceWidth) / 2 +
488
+ clearance +
489
+ 1e-3,
490
+ )
491
+ const viaAxis =
492
+ getAxis(sourcePoint, bus.direction) + sign * initialEscapeDistance
493
+ const sourcePerpendicularAxis = getPerpendicularAxis(
494
+ sourcePoint,
495
+ bus.direction,
496
+ )
497
+ const viaPerpendicularAxis =
498
+ sourcePerpendicularAxis + viaHandedness * perpendicularPitch * 0.5
499
+ const viaPoint = makePoint(viaAxis, viaPerpendicularAxis, bus.direction)
500
+ const spreadLaneDistance =
501
+ viaDiameter / 2 +
502
+ traceWidth / 2 +
503
+ clearance +
504
+ 1e-3 +
505
+ spreadLaneIndex * (traceWidth + clearance)
506
+ const useNestedSpread =
507
+ interstitialEscape &&
508
+ directionalPitch >= spreadLaneDistance + viaDiameter / 2 + clearance
509
+ const spreadPoint = useNestedSpread
510
+ ? makePoint(
511
+ viaAxis + sign * spreadLaneDistance,
512
+ viaPerpendicularAxis,
513
+ bus.direction,
514
+ )
515
+ : viaPoint
516
+ const targetLayerDoglegAxis = useNestedSpread
517
+ ? getAxis(spreadPoint, bus.direction)
518
+ : viaAxis + sign * Math.abs(track - viaPerpendicularAxis)
519
+ const doglegPoint = makePoint(targetLayerDoglegAxis, track, bus.direction)
520
+ const exitPoint = terminateAtVia
521
+ ? viaPoint
522
+ : makePoint(exitAxis, track, bus.direction)
523
+ const segments: RoutedSegment[] = []
524
+ const route: SimplifiedPcbTrace["route"] = []
525
+
526
+ route.push({
527
+ route_type: "wire",
528
+ x: sourcePoint.x,
529
+ y: sourcePoint.y,
530
+ width: traceWidth,
531
+ layer: preparedConnection.sourceLayer,
532
+ start_pcb_port_id: preparedConnection.sourcePoint.pcb_port_id,
533
+ })
534
+ appendSegment(
535
+ segments,
536
+ sourcePoint,
537
+ viaPoint,
538
+ traceWidth,
539
+ preparedConnection.sourceLayer,
540
+ )
541
+ route.push({
542
+ route_type: "wire",
543
+ x: viaPoint.x,
544
+ y: viaPoint.y,
545
+ width: traceWidth,
546
+ layer: preparedConnection.sourceLayer,
547
+ })
548
+
549
+ let via: FanoutRoutePlan["via"]
550
+ if (targetLayer !== preparedConnection.sourceLayer) {
551
+ const spanLayers = getLayerSpan(
552
+ preparedConnection.sourceLayer,
553
+ targetLayer,
554
+ layerNames,
555
+ )
556
+ via = {
557
+ center: viaPoint,
558
+ diameter: viaDiameter,
559
+ holeDiameter: viaHoleDiameter,
560
+ fromLayer: preparedConnection.sourceLayer,
561
+ toLayer: targetLayer,
562
+ spanLayers,
563
+ }
564
+ route.push({
565
+ route_type: "via",
566
+ x: viaPoint.x,
567
+ y: viaPoint.y,
568
+ from_layer: preparedConnection.sourceLayer,
569
+ to_layer: targetLayer,
570
+ via_diameter: viaDiameter,
571
+ via_hole_diameter: viaHoleDiameter,
572
+ })
573
+ route.push({
574
+ route_type: "wire",
575
+ x: viaPoint.x,
576
+ y: viaPoint.y,
577
+ width: traceWidth,
578
+ layer: targetLayer,
579
+ })
580
+ }
581
+
582
+ const targetLayerPoints = terminateAtVia
583
+ ? [viaPoint]
584
+ : useNestedSpread
585
+ ? chamferOrthogonalPolyline(
586
+ [viaPoint, spreadPoint, doglegPoint, exitPoint],
587
+ Math.max(traceWidth + clearance, traceWidth * 2),
588
+ )
589
+ : [viaPoint, doglegPoint, exitPoint]
590
+ for (let index = 1; index < targetLayerPoints.length; index++) {
591
+ const previousPoint = targetLayerPoints[index - 1]!
592
+ const nextPoint = targetLayerPoints[index]!
593
+ appendSegment(segments, previousPoint, nextPoint, traceWidth, targetLayer)
594
+ route.push({
595
+ route_type: "wire",
596
+ x: nextPoint.x,
597
+ y: nextPoint.y,
598
+ width: traceWidth,
599
+ layer: targetLayer,
600
+ })
601
+ }
602
+
603
+ return {
604
+ busId: bus.busId,
605
+ connectionName: preparedConnection.connection.name,
606
+ connectionIndex: preparedConnection.connectionIndex,
607
+ sourcePointIndex: preparedConnection.sourcePointIndex,
608
+ sourcePoint: preparedConnection.sourcePoint,
609
+ sourceObstacle: preparedConnection.sourceObstacle,
610
+ sourceLayer: preparedConnection.sourceLayer,
611
+ targetLayer,
612
+ termination: bus.termination,
613
+ direction: bus.direction,
614
+ exitPoint,
615
+ trace: {
616
+ type: "pcb_trace",
617
+ pcb_trace_id: `fanout:${preparedConnection.connection.name}`,
618
+ connection_name: preparedConnection.connection.name,
619
+ connectsTo: [
620
+ preparedConnection.connection.name,
621
+ ...(preparedConnection.sourcePoint.pointId
622
+ ? [preparedConnection.sourcePoint.pointId]
623
+ : []),
624
+ ...(preparedConnection.sourcePoint.pcb_port_id
625
+ ? [preparedConnection.sourcePoint.pcb_port_id]
626
+ : []),
627
+ ],
628
+ route,
629
+ },
630
+ segments,
631
+ via,
632
+ length: segments.reduce(
633
+ (total, segment) => total + distance(segment.start, segment.end),
634
+ 0,
635
+ ),
636
+ }
637
+ }
638
+
639
+ function segmentIsClearOfObstacles(params: {
640
+ segment: RoutedSegment
641
+ plan: FanoutRoutePlan
642
+ segmentIndex: number
643
+ obstacles: Obstacle[]
644
+ clearance: number
645
+ }): boolean {
646
+ const { segment, plan, segmentIndex, obstacles, clearance } = params
647
+ for (const obstacle of obstacles) {
648
+ if (!obstacle.layers.includes(segment.layer)) continue
649
+ if (obstacle.connectedTo.includes(plan.connectionName)) continue
650
+ if (
651
+ segmentIndex === 0 &&
652
+ obstacle === plan.sourceObstacle &&
653
+ segment.layer === plan.sourceLayer
654
+ ) {
655
+ continue
656
+ }
657
+ if (
658
+ distanceSegmentToObstacle(segment, obstacle) <
659
+ segment.width / 2 + clearance - 1e-9
660
+ ) {
661
+ return false
662
+ }
663
+ }
664
+ return true
665
+ }
666
+
667
+ function planIsClear(params: {
668
+ plan: FanoutRoutePlan
669
+ otherPlans: FanoutRoutePlan[]
670
+ srj: SimpleRouteJson
671
+ clearance: number
672
+ }): boolean {
673
+ const { plan, otherPlans, srj, clearance } = params
674
+ if (
675
+ !pointIsInsideBounds(plan.exitPoint, srj.bounds) ||
676
+ plan.segments.some(
677
+ (segment) =>
678
+ !pointIsInsideBounds(segment.start, srj.bounds) ||
679
+ !pointIsInsideBounds(segment.end, srj.bounds),
680
+ )
681
+ ) {
682
+ return false
683
+ }
684
+ for (let index = 0; index < plan.segments.length; index++) {
685
+ if (
686
+ !segmentIsClearOfObstacles({
687
+ segment: plan.segments[index]!,
688
+ plan,
689
+ segmentIndex: index,
690
+ obstacles: srj.obstacles,
691
+ clearance,
692
+ })
693
+ ) {
694
+ return false
695
+ }
696
+ }
697
+ if (plan.via) {
698
+ for (const obstacle of srj.obstacles) {
699
+ if (
700
+ !obstacle.layers.some((layer) => plan.via!.spanLayers.includes(layer))
701
+ ) {
702
+ continue
703
+ }
704
+ if (
705
+ distancePointToObstacle(plan.via.center, obstacle) <
706
+ plan.via.diameter / 2 + clearance - 1e-9
707
+ ) {
708
+ return false
709
+ }
710
+ }
711
+ }
712
+
713
+ for (const otherPlan of otherPlans) {
714
+ for (const segment of plan.segments) {
715
+ for (const otherSegment of otherPlan.segments) {
716
+ if (!segmentsAreClear(segment, otherSegment, clearance)) return false
717
+ }
718
+ if (
719
+ otherPlan.via?.spanLayers.includes(segment.layer) &&
720
+ distancePointToSegment(
721
+ otherPlan.via.center,
722
+ segment.start,
723
+ segment.end,
724
+ ) <
725
+ otherPlan.via.diameter / 2 + segment.width / 2 + clearance - 1e-9
726
+ ) {
727
+ return false
728
+ }
729
+ }
730
+ if (plan.via) {
731
+ for (const otherSegment of otherPlan.segments) {
732
+ if (
733
+ plan.via.spanLayers.includes(otherSegment.layer) &&
734
+ distancePointToSegment(
735
+ plan.via.center,
736
+ otherSegment.start,
737
+ otherSegment.end,
738
+ ) <
739
+ plan.via.diameter / 2 + otherSegment.width / 2 + clearance - 1e-9
740
+ ) {
741
+ return false
742
+ }
743
+ }
744
+ if (
745
+ otherPlan.via &&
746
+ plan.via.spanLayers.some((layer) =>
747
+ otherPlan.via!.spanLayers.includes(layer),
748
+ ) &&
749
+ distance(plan.via.center, otherPlan.via.center) <
750
+ (plan.via.diameter + otherPlan.via.diameter) / 2 + clearance - 1e-9
751
+ ) {
752
+ return false
753
+ }
754
+ }
755
+ }
756
+ return true
757
+ }
758
+
759
+ function routePlaneTerminatedBus(
760
+ params: RouteBusParams,
761
+ ): FanoutRoutePlan[] | null {
762
+ const {
763
+ srj,
764
+ bus,
765
+ targetLayer,
766
+ acceptedPlans,
767
+ layerNames,
768
+ traceWidth,
769
+ viaDiameter,
770
+ viaHoleDiameter,
771
+ clearance,
772
+ } = params
773
+ const sourceObstacle = bus.connections[0]?.sourceObstacle
774
+ if (!sourceObstacle || bus.termination.type !== "plane") return null
775
+ const sourceLayer = bus.connections[0]!.sourceLayer
776
+ if (targetLayer === sourceLayer) return null
777
+ const directionalPadSize = isHorizontal(bus.direction)
778
+ ? sourceObstacle.width
779
+ : sourceObstacle.height
780
+ const pairChannelFitsVia =
781
+ getDirectionalPitch(bus) / 2 - directionalPadSize / 2 >=
782
+ viaDiameter / 2 + clearance - 1e-9
783
+ const viaHandednesses: readonly ViaHandedness[] = pairChannelFitsVia
784
+ ? [0]
785
+ : [1, -1]
786
+
787
+ for (const viaHandedness of viaHandednesses) {
788
+ for (const connectionOrder of getConnectionOrders(bus)) {
789
+ const candidatePlans: FanoutRoutePlan[] = []
790
+ let orderIsClear = true
791
+ for (const preparedConnection of connectionOrder) {
792
+ const sourceTrack = getPerpendicularAxis(
793
+ preparedConnection.sourcePoint,
794
+ bus.direction,
795
+ )
796
+ const plan = buildPlan({
797
+ preparedConnection,
798
+ bus,
799
+ targetLayer,
800
+ track: sourceTrack,
801
+ exitAxis: getExitAxis(bus),
802
+ layerNames,
803
+ traceWidth,
804
+ viaDiameter,
805
+ viaHoleDiameter,
806
+ viaHandedness,
807
+ interstitialEscape: !pairChannelFitsVia,
808
+ spreadLaneIndex: 0,
809
+ clearance,
810
+ terminateAtVia: true,
811
+ })
812
+ if (
813
+ !planIsClear({
814
+ plan,
815
+ otherPlans: [...acceptedPlans, ...candidatePlans],
816
+ srj,
817
+ clearance,
818
+ })
819
+ ) {
820
+ orderIsClear = false
821
+ break
822
+ }
823
+ candidatePlans.push(plan)
824
+ }
825
+ if (orderIsClear) return candidatePlans
826
+ }
827
+ }
828
+
829
+ return null
830
+ }
831
+
832
+ export function routeBus(params: RouteBusParams): FanoutRoutePlan[] | null {
833
+ const {
834
+ srj,
835
+ bus,
836
+ targetLayer,
837
+ acceptedPlans,
838
+ layerNames,
839
+ traceWidth,
840
+ viaDiameter,
841
+ viaHoleDiameter,
842
+ clearance,
843
+ compactBusTracks,
844
+ } = params
845
+ if (bus.termination.type === "plane") {
846
+ return routePlaneTerminatedBus(params)
847
+ }
848
+ const exitAxis = getExitAxis(bus)
849
+ const sourceObstacle = bus.connections[0]?.sourceObstacle
850
+ if (!sourceObstacle) return []
851
+ const directionalPadSize = isHorizontal(bus.direction)
852
+ ? sourceObstacle.width
853
+ : sourceObstacle.height
854
+ const sourceLayer = bus.connections[0]!.sourceLayer
855
+ const targetUsesVia = targetLayer !== sourceLayer
856
+ const outwardEdgeBus = busIsOnOutwardComponentEdge(bus)
857
+ const pairChannelFitsVia =
858
+ getDirectionalPitch(bus) / 2 - directionalPadSize / 2 >=
859
+ viaDiameter / 2 + clearance - 1e-9
860
+ const interstitialEscape =
861
+ targetUsesVia && !outwardEdgeBus && !pairChannelFitsVia
862
+ const viaHandednesses: readonly ViaHandedness[] = targetUsesVia
863
+ ? pairChannelFitsVia || outwardEdgeBus
864
+ ? [0]
865
+ : [1, -1]
866
+ : [0]
867
+
868
+ for (const viaHandedness of viaHandednesses) {
869
+ for (const connectionOrder of getConnectionOrders(bus)) {
870
+ const candidatePlans: FanoutRoutePlan[] = []
871
+ let orderIsClear = true
872
+ for (const preparedConnection of connectionOrder) {
873
+ let acceptedPlan: FanoutRoutePlan | null = null
874
+ for (const track of getTrackCandidates({
875
+ bus,
876
+ connection: preparedConnection,
877
+ preferredTrack: getPreferredTrack({
878
+ bus,
879
+ connection: preparedConnection,
880
+ targetUsesVia,
881
+ interstitialEscape,
882
+ compactBusTracks,
883
+ traceWidth,
884
+ viaDiameter,
885
+ clearance,
886
+ }),
887
+ traceWidth,
888
+ clearance,
889
+ })) {
890
+ const plan = buildPlan({
891
+ preparedConnection,
892
+ bus,
893
+ targetLayer,
894
+ track: track.value,
895
+ exitAxis,
896
+ layerNames,
897
+ traceWidth,
898
+ viaDiameter,
899
+ viaHoleDiameter,
900
+ viaHandedness,
901
+ interstitialEscape,
902
+ spreadLaneIndex: Math.min(
903
+ getConnectionRank(bus, preparedConnection),
904
+ bus.connections.length -
905
+ getConnectionRank(bus, preparedConnection) -
906
+ 1,
907
+ ),
908
+ clearance,
909
+ terminateAtVia: false,
910
+ })
911
+ if (
912
+ planIsClear({
913
+ plan,
914
+ otherPlans: [...acceptedPlans, ...candidatePlans],
915
+ srj,
916
+ clearance,
917
+ })
918
+ ) {
919
+ acceptedPlan = plan
920
+ break
921
+ }
922
+ }
923
+ if (!acceptedPlan) {
924
+ orderIsClear = false
925
+ break
926
+ }
927
+ candidatePlans.push(acceptedPlan)
928
+ }
929
+ if (orderIsClear) return candidatePlans
930
+ }
931
+ }
932
+
933
+ return null
934
+ }