@tscircuit/fanout-solver 0.0.35 → 0.0.37

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/lib/route-bus.ts CHANGED
@@ -3,6 +3,12 @@ import type {
3
3
  SimpleRouteJson,
4
4
  SimplifiedPcbTrace,
5
5
  } from "@tscircuit/capacity-autorouter"
6
+ import {
7
+ getCornerBandSide,
8
+ getDirectionForExitEdge,
9
+ getExitEdgeForDirection,
10
+ } from "./boundary-exit"
11
+ import { createFanoutOutputIds } from "./fanout-output-ids"
6
12
  import {
7
13
  distance,
8
14
  distancePointToObstacle,
@@ -10,13 +16,13 @@ import {
10
16
  distanceSegmentToObstacle,
11
17
  segmentsAreClear,
12
18
  } from "./geometry"
13
- import { createFanoutOutputIds } from "./fanout-output-ids"
14
19
  import { getAllRoutedTraceCopper } from "./get-routed-trace-copper"
15
20
  import { getLayerSpan } from "./layer-names"
16
21
  import {
17
22
  connectionsShareElectricalNet,
18
23
  obstacleSharesElectricalNet,
19
24
  } from "./net-identity"
25
+ import { routeViaMinimalWinding } from "./route-via-minimal-winding"
20
26
  import type {
21
27
  Bounds,
22
28
  FanoutDirection,
@@ -72,6 +78,189 @@ function getPerpendicularAxis(
72
78
  return isHorizontal(direction) ? point.y : point.x
73
79
  }
74
80
 
81
+ function getCornerSide(bus: PreparedBus): "minimum" | "maximum" | undefined {
82
+ return getCornerBandSide(bus.exitEdge, bus.preferredExit)
83
+ }
84
+
85
+ function getLocalCornerSide(
86
+ bus: PreparedBus,
87
+ ): "minimum" | "maximum" | undefined {
88
+ return getCornerBandSide(
89
+ getExitEdgeForDirection(bus.direction),
90
+ bus.preferredExit,
91
+ )
92
+ }
93
+
94
+ function busUsesCoordinatedWindingChannel(bus: PreparedBus): boolean {
95
+ return Boolean(
96
+ bus.exitEdge &&
97
+ bus.termination.type === "boundary" &&
98
+ bus.connections.length > 0 &&
99
+ bus.connections.every(
100
+ (connection) => connection.hasExplicitLayeredExitTarget === true,
101
+ ),
102
+ )
103
+ }
104
+
105
+ function getStableConnectionIdentity(
106
+ connection: PreparedConnection["connection"],
107
+ ): string {
108
+ if (
109
+ "source_trace_id" in connection &&
110
+ typeof connection.source_trace_id === "string"
111
+ ) {
112
+ return connection.source_trace_id
113
+ }
114
+ return connection.name
115
+ }
116
+
117
+ function getWindingTargetRank(params: {
118
+ bus: PreparedBus
119
+ connection: PreparedConnection
120
+ boundaryDirection: FanoutDirection
121
+ layerNames: readonly string[]
122
+ }): { rank: number; connectionCount: number } {
123
+ const { bus, connection, boundaryDirection, layerNames } = params
124
+ const orderedConnections = bus.connections.toSorted((first, second) => {
125
+ const axisDifference =
126
+ getPerpendicularAxis(
127
+ first.exitTargetPoint ?? first.targetPoint,
128
+ boundaryDirection,
129
+ ) -
130
+ getPerpendicularAxis(
131
+ second.exitTargetPoint ?? second.targetPoint,
132
+ boundaryDirection,
133
+ )
134
+ if (axisDifference !== 0) return axisDifference
135
+
136
+ // Equal target coordinates came from distinct target layers and do not
137
+ // impose a physical order after this bus is collapsed onto one escape
138
+ // layer. Keep that free choice independent of allowed-layer ordering so
139
+ // the local fanout can choose a deterministic, via-minimal permutation.
140
+ const identityDifference = first.connection.name.localeCompare(
141
+ second.connection.name,
142
+ )
143
+ if (identityDifference !== 0) return identityDifference
144
+
145
+ const firstStableId = getStableConnectionIdentity(first.connection)
146
+ const secondStableId = getStableConnectionIdentity(second.connection)
147
+ const stableIdentityDifference = firstStableId.localeCompare(secondStableId)
148
+ if (stableIdentityDifference !== 0) return stableIdentityDifference
149
+
150
+ const firstLayer = first.exitTargetPoint?.layer
151
+ const secondLayer = second.exitTargetPoint?.layer
152
+ return (
153
+ layerNames.indexOf(firstLayer ?? "") -
154
+ layerNames.indexOf(secondLayer ?? "") ||
155
+ first.connectionIndex - second.connectionIndex
156
+ )
157
+ })
158
+ const rank = orderedConnections.findIndex(
159
+ (candidate) => candidate.connectionIndex === connection.connectionIndex,
160
+ )
161
+ if (rank < 0) {
162
+ throw new Error(
163
+ `FanoutSolver: connection "${connection.connection.name}" is missing from winding order`,
164
+ )
165
+ }
166
+ return { rank, connectionCount: orderedConnections.length }
167
+ }
168
+
169
+ function getWindingCrossoverLayer(params: {
170
+ bus: PreparedBus
171
+ escapeLayer: string
172
+ }): string | undefined {
173
+ const { bus, escapeLayer } = params
174
+ return (bus.routableEscapeLayers ?? bus.allowedLayers)?.find(
175
+ (layer) => layer !== escapeLayer,
176
+ )
177
+ }
178
+
179
+ function getCornerTargetTrack(params: {
180
+ bus: PreparedBus
181
+ connection: PreparedConnection
182
+ cornerExitLaneOffset: number
183
+ traceWidth: number
184
+ viaDiameter: number
185
+ clearance: number
186
+ layerNames: readonly string[]
187
+ }): number {
188
+ const {
189
+ bus,
190
+ connection,
191
+ cornerExitLaneOffset,
192
+ traceWidth,
193
+ viaDiameter,
194
+ clearance,
195
+ layerNames,
196
+ } = params
197
+ const side = getCornerSide(bus)
198
+ if (!side || !bus.exitEdge) {
199
+ return getPerpendicularAxis(
200
+ connection.exitTargetPoint ?? connection.targetPoint,
201
+ bus.direction,
202
+ )
203
+ }
204
+ const boundaryDirection = getDirectionForExitEdge(bus.exitEdge)
205
+ const boundaryMinimum = isHorizontal(boundaryDirection)
206
+ ? bus.sharedBoundary.minY
207
+ : bus.sharedBoundary.minX
208
+ const boundaryMaximum = isHorizontal(boundaryDirection)
209
+ ? bus.sharedBoundary.maxY
210
+ : bus.sharedBoundary.maxX
211
+ const pitch = Math.max(traceWidth + clearance, viaDiameter + clearance)
212
+ const baseBandCenter =
213
+ boundaryMinimum +
214
+ (boundaryMaximum - boundaryMinimum) * (side === "minimum" ? 0.25 : 0.75)
215
+ const windingTarget = busUsesCoordinatedWindingChannel(bus)
216
+ ? getWindingTargetRank({
217
+ bus,
218
+ connection,
219
+ boundaryDirection,
220
+ layerNames,
221
+ })
222
+ : undefined
223
+ const bandConnectionCount = Math.max(
224
+ bus.connections.length,
225
+ bus.cornerBandConnectionCount ?? bus.connections.length,
226
+ cornerExitLaneOffset + (windingTarget?.connectionCount ?? 0),
227
+ )
228
+ const firstTrack = baseBandCenter - ((bandConnectionCount - 1) * pitch) / 2
229
+ const rank = windingTarget?.rank ?? getConnectionRank(bus, connection)
230
+ const globalSlot = cornerExitLaneOffset + rank
231
+ const reverseSlotOrder =
232
+ !windingTarget &&
233
+ (side === "maximum") === directionSign(boundaryDirection) > 0
234
+ const orientedSlot = reverseSlotOrder
235
+ ? bandConnectionCount - 1 - globalSlot
236
+ : globalSlot
237
+ return firstTrack + orientedSlot * pitch
238
+ }
239
+
240
+ function getCornerLaneOffsets(
241
+ bus: PreparedBus,
242
+ acceptedPlans: readonly FanoutRoutePlan[],
243
+ ): { exit: number; localChannel: number; boundaryChannel: number } {
244
+ const side = getCornerSide(bus)
245
+ if (!side || !bus.exitEdge) {
246
+ return { exit: 0, localChannel: 0, boundaryChannel: 0 }
247
+ }
248
+ const cornerPlans = acceptedPlans.filter(
249
+ (plan) => plan.exitEdge && plan.cornerBandSide !== undefined,
250
+ )
251
+ const plansOnExitEdge = cornerPlans.filter(
252
+ (plan) => plan.exitEdge === bus.exitEdge && plan.cornerBandSide === side,
253
+ )
254
+ const plansOnLocalEdge = cornerPlans.filter(
255
+ (plan) => plan.direction === bus.direction,
256
+ )
257
+ return {
258
+ exit: plansOnExitEdge.length,
259
+ localChannel: plansOnLocalEdge.length,
260
+ boundaryChannel: plansOnExitEdge.length,
261
+ }
262
+ }
263
+
75
264
  function makePoint(
76
265
  axis: number,
77
266
  perpendicularAxis: number,
@@ -82,8 +271,11 @@ function makePoint(
82
271
  : { x: perpendicularAxis, y: axis }
83
272
  }
84
273
 
85
- function getExitAxis(bus: PreparedBus): number {
86
- switch (bus.direction) {
274
+ function getExitAxis(
275
+ bus: PreparedBus,
276
+ direction: FanoutDirection = bus.direction,
277
+ ): number {
278
+ switch (direction) {
87
279
  case "right":
88
280
  return bus.sharedBoundary.maxX
89
281
  case "left":
@@ -296,10 +488,16 @@ function getPreferredTrack(params: {
296
488
  connection: PreparedConnection
297
489
  traceWidth: number
298
490
  }): number {
299
- const preferredTrack = getPerpendicularAxis(
300
- params.connection.exitTargetPoint ?? params.connection.targetPoint,
301
- params.bus.direction,
302
- )
491
+ const preferredTrack =
492
+ getCornerSide(params.bus) || busUsesCoordinatedWindingChannel(params.bus)
493
+ ? getPerpendicularAxis(
494
+ params.connection.sourcePoint,
495
+ params.bus.direction,
496
+ )
497
+ : getPerpendicularAxis(
498
+ params.connection.exitTargetPoint ?? params.connection.targetPoint,
499
+ params.bus.direction,
500
+ )
303
501
  const boundaryMinimum = isHorizontal(params.bus.direction)
304
502
  ? params.bus.sharedBoundary.minY
305
503
  : params.bus.sharedBoundary.minX
@@ -481,46 +679,31 @@ function chamferOrthogonalPolyline(
481
679
  return chamfered
482
680
  }
483
681
 
484
- function buildPlan(params: {
682
+ function getInitialViaPoint(params: {
485
683
  preparedConnection: PreparedConnection
486
684
  bus: PreparedBus
487
685
  targetLayer: string
488
- track: number
489
- exitAxis: number
490
- layerNames: string[]
491
686
  traceWidth: number
492
687
  viaDiameter: number
493
- viaHoleDiameter: number
494
- viaHandedness: ViaHandedness
495
- interstitialEscape: boolean
496
- spreadLaneIndex: number
497
688
  clearance: number
498
- terminateAtVia: boolean
499
- }): FanoutRoutePlan {
689
+ viaHandedness: ViaHandedness
690
+ }): Point2D {
500
691
  const {
501
692
  preparedConnection,
502
693
  bus,
503
694
  targetLayer,
504
- track,
505
- exitAxis,
506
- layerNames,
507
695
  traceWidth,
508
696
  viaDiameter,
509
- viaHoleDiameter,
510
- viaHandedness,
511
- interstitialEscape,
512
- spreadLaneIndex,
513
697
  clearance,
514
- terminateAtVia,
698
+ viaHandedness,
515
699
  } = params
516
700
  const sourcePoint = {
517
701
  x: preparedConnection.sourcePoint.x,
518
702
  y: preparedConnection.sourcePoint.y,
519
703
  }
520
- const sign = directionSign(bus.direction)
704
+ const targetUsesVia = targetLayer !== preparedConnection.sourceLayer
521
705
  const directionalPitch = getDirectionalPitch(bus)
522
706
  const perpendicularPitch = getPerpendicularPitch(bus)
523
- const targetUsesVia = targetLayer !== preparedConnection.sourceLayer
524
707
  const directionalPadSize = isHorizontal(bus.direction)
525
708
  ? preparedConnection.sourceObstacle.width
526
709
  : preparedConnection.sourceObstacle.height
@@ -539,14 +722,77 @@ function buildPlan(params: {
539
722
  1e-3,
540
723
  )
541
724
  const viaAxis =
542
- getAxis(sourcePoint, bus.direction) + sign * initialEscapeDistance
543
- const sourcePerpendicularAxis = getPerpendicularAxis(
544
- sourcePoint,
545
- bus.direction,
546
- )
725
+ getAxis(sourcePoint, bus.direction) +
726
+ directionSign(bus.direction) * initialEscapeDistance
547
727
  const viaPerpendicularAxis =
548
- sourcePerpendicularAxis + viaHandedness * perpendicularPitch * 0.5
549
- const viaPoint = makePoint(viaAxis, viaPerpendicularAxis, bus.direction)
728
+ getPerpendicularAxis(sourcePoint, bus.direction) +
729
+ viaHandedness * perpendicularPitch * 0.5
730
+ return makePoint(viaAxis, viaPerpendicularAxis, bus.direction)
731
+ }
732
+
733
+ function buildPlan(params: {
734
+ preparedConnection: PreparedConnection
735
+ bus: PreparedBus
736
+ targetLayer: string
737
+ track: number
738
+ exitAxis: number
739
+ layerNames: string[]
740
+ traceWidth: number
741
+ viaDiameter: number
742
+ viaHoleDiameter: number
743
+ viaHandedness: ViaHandedness
744
+ interstitialEscape: boolean
745
+ spreadLaneIndex: number
746
+ cornerExitLaneOffset: number
747
+ cornerLocalChannelLaneOffset: number
748
+ cornerBoundaryChannelLaneOffset: number
749
+ clearance: number
750
+ terminateAtVia: boolean
751
+ }): FanoutRoutePlan {
752
+ const {
753
+ preparedConnection,
754
+ bus,
755
+ targetLayer,
756
+ track,
757
+ exitAxis,
758
+ layerNames,
759
+ traceWidth,
760
+ viaDiameter,
761
+ viaHoleDiameter,
762
+ viaHandedness,
763
+ interstitialEscape,
764
+ spreadLaneIndex,
765
+ cornerExitLaneOffset,
766
+ cornerLocalChannelLaneOffset,
767
+ cornerBoundaryChannelLaneOffset,
768
+ clearance,
769
+ terminateAtVia,
770
+ } = params
771
+ const sourcePoint = {
772
+ x: preparedConnection.sourcePoint.x,
773
+ y: preparedConnection.sourcePoint.y,
774
+ }
775
+ const escapeLayer = targetLayer
776
+ const windingCrossoverLayer = busUsesCoordinatedWindingChannel(bus)
777
+ ? getWindingCrossoverLayer({
778
+ bus,
779
+ escapeLayer,
780
+ })
781
+ : undefined
782
+ const usesLayeredWindingChannel = Boolean(windingCrossoverLayer)
783
+ const sign = directionSign(bus.direction)
784
+ const directionalPitch = getDirectionalPitch(bus)
785
+ const viaPoint = getInitialViaPoint({
786
+ preparedConnection,
787
+ bus,
788
+ targetLayer,
789
+ traceWidth,
790
+ viaDiameter,
791
+ clearance,
792
+ viaHandedness,
793
+ })
794
+ const viaAxis = getAxis(viaPoint, bus.direction)
795
+ const viaPerpendicularAxis = getPerpendicularAxis(viaPoint, bus.direction)
550
796
  const spreadLaneDistance =
551
797
  viaDiameter / 2 +
552
798
  traceWidth / 2 +
@@ -567,9 +813,81 @@ function buildPlan(params: {
567
813
  ? getAxis(spreadPoint, bus.direction)
568
814
  : viaAxis + sign * Math.abs(track - viaPerpendicularAxis)
569
815
  const doglegPoint = makePoint(targetLayerDoglegAxis, track, bus.direction)
816
+ const cornerSide = getCornerSide(bus)
817
+ const boundaryDirection = bus.exitEdge
818
+ ? getDirectionForExitEdge(bus.exitEdge)
819
+ : bus.direction
820
+ const boundarySign = directionSign(boundaryDirection)
821
+ const boundaryExitAxis = getExitAxis(bus, boundaryDirection)
822
+ const boundaryTargetTrack =
823
+ cornerSide && bus.exitEdge
824
+ ? getCornerTargetTrack({
825
+ bus,
826
+ connection: preparedConnection,
827
+ cornerExitLaneOffset,
828
+ traceWidth,
829
+ viaDiameter,
830
+ clearance,
831
+ layerNames,
832
+ })
833
+ : usesLayeredWindingChannel
834
+ ? getPerpendicularAxis(
835
+ preparedConnection.exitTargetPoint ??
836
+ preparedConnection.targetPoint,
837
+ boundaryDirection,
838
+ )
839
+ : track
840
+ const connectionRank = getConnectionRank(bus, preparedConnection)
841
+ const localCornerSide = getLocalCornerSide(bus)
842
+ const localChannelLaneIndex =
843
+ localCornerSide === "maximum"
844
+ ? cornerLocalChannelLaneOffset + connectionRank
845
+ : cornerLocalChannelLaneOffset +
846
+ bus.connections.length -
847
+ 1 -
848
+ connectionRank
849
+ const globalBoundarySlot = cornerBoundaryChannelLaneOffset + connectionRank
850
+ const boundaryBandConnectionCount = Math.max(
851
+ bus.connections.length,
852
+ bus.cornerBandConnectionCount ?? bus.connections.length,
853
+ )
854
+ const boundaryChannelLaneIndex =
855
+ boundarySign > 0
856
+ ? globalBoundarySlot
857
+ : boundaryBandConnectionCount - 1 - globalBoundarySlot
858
+ const channelPitch = usesLayeredWindingChannel
859
+ ? viaDiameter / 2 + traceWidth / 2 + clearance
860
+ : traceWidth + clearance
861
+ const channelInset = (laneIndex: number) =>
862
+ viaDiameter / 2 + traceWidth / 2 + clearance + laneIndex * channelPitch
863
+ const localChannelAxis =
864
+ getExitAxis(bus, bus.direction) - sign * channelInset(localChannelLaneIndex)
865
+ const boundaryChannelAxis =
866
+ boundaryExitAxis - boundarySign * channelInset(boundaryChannelLaneIndex)
867
+ const localChannelSourcePoint = makePoint(
868
+ localChannelAxis,
869
+ track,
870
+ bus.direction,
871
+ )
872
+ const localChannelTargetPoint = makePoint(
873
+ boundaryChannelAxis,
874
+ localChannelAxis,
875
+ boundaryDirection,
876
+ )
877
+ const boundaryChannelTargetPoint = makePoint(
878
+ boundaryChannelAxis,
879
+ boundaryTargetTrack,
880
+ boundaryDirection,
881
+ )
882
+ const windingInputTransitionPoint =
883
+ isHorizontal(bus.direction) !== isHorizontal(boundaryDirection)
884
+ ? localChannelTargetPoint
885
+ : makePoint(boundaryChannelAxis, track, boundaryDirection)
570
886
  const exitPoint = terminateAtVia
571
887
  ? viaPoint
572
- : makePoint(exitAxis, track, bus.direction)
888
+ : cornerSide || usesLayeredWindingChannel
889
+ ? makePoint(boundaryExitAxis, boundaryTargetTrack, boundaryDirection)
890
+ : makePoint(exitAxis, track, bus.direction)
573
891
  const segments: RoutedSegment[] = []
574
892
  const route: SimplifiedPcbTrace["route"] = []
575
893
 
@@ -629,25 +947,110 @@ function buildPlan(params: {
629
947
  })
630
948
  }
631
949
 
632
- const targetLayerPoints = terminateAtVia
633
- ? [viaPoint]
634
- : useNestedSpread
635
- ? chamferOrthogonalPolyline(
636
- [viaPoint, spreadPoint, doglegPoint, exitPoint],
637
- Math.max(traceWidth + clearance, traceWidth * 2),
638
- )
639
- : [viaPoint, doglegPoint, exitPoint]
640
- for (let index = 1; index < targetLayerPoints.length; index++) {
641
- const previousPoint = targetLayerPoints[index - 1]!
642
- const nextPoint = targetLayerPoints[index]!
643
- appendSegment(segments, previousPoint, nextPoint, traceWidth, targetLayer)
644
- route.push({
645
- route_type: "wire",
646
- x: nextPoint.x,
647
- y: nextPoint.y,
648
- width: traceWidth,
649
- layer: targetLayer,
650
- })
950
+ const appendLayerPath = (points: Point2D[], layer: string): void => {
951
+ for (let index = 1; index < points.length; index++) {
952
+ const previousPoint = points[index - 1]!
953
+ const nextPoint = points[index]!
954
+ appendSegment(segments, previousPoint, nextPoint, traceWidth, layer)
955
+ route.push({
956
+ route_type: "wire",
957
+ x: nextPoint.x,
958
+ y: nextPoint.y,
959
+ width: traceWidth,
960
+ layer,
961
+ })
962
+ }
963
+ }
964
+
965
+ const additionalVias: NonNullable<FanoutRoutePlan["additionalVias"]> = []
966
+ if (usesLayeredWindingChannel && windingCrossoverLayer) {
967
+ const escapePoints = chamferOrthogonalPolyline(
968
+ [
969
+ viaPoint,
970
+ ...(useNestedSpread ? [spreadPoint] : []),
971
+ doglegPoint,
972
+ ...(isHorizontal(bus.direction) !== isHorizontal(boundaryDirection)
973
+ ? [localChannelSourcePoint]
974
+ : []),
975
+ windingInputTransitionPoint,
976
+ ],
977
+ Math.max(traceWidth + clearance, traceWidth * 2),
978
+ )
979
+ appendLayerPath(escapePoints, escapeLayer)
980
+
981
+ const appendTransitionVia = (
982
+ center: Point2D,
983
+ fromLayer: string,
984
+ toLayer: string,
985
+ ): void => {
986
+ if (fromLayer === toLayer) return
987
+ additionalVias.push({
988
+ center,
989
+ diameter: viaDiameter,
990
+ holeDiameter: viaHoleDiameter,
991
+ fromLayer,
992
+ toLayer,
993
+ spanLayers: getLayerSpan(fromLayer, toLayer, layerNames),
994
+ })
995
+ route.push({
996
+ route_type: "via",
997
+ x: center.x,
998
+ y: center.y,
999
+ from_layer: fromLayer,
1000
+ to_layer: toLayer,
1001
+ via_diameter: viaDiameter,
1002
+ via_hole_diameter: viaHoleDiameter,
1003
+ })
1004
+ route.push({
1005
+ route_type: "wire",
1006
+ x: center.x,
1007
+ y: center.y,
1008
+ width: traceWidth,
1009
+ layer: toLayer,
1010
+ })
1011
+ }
1012
+
1013
+ appendTransitionVia(
1014
+ windingInputTransitionPoint,
1015
+ escapeLayer,
1016
+ windingCrossoverLayer,
1017
+ )
1018
+ appendLayerPath(
1019
+ [windingInputTransitionPoint, boundaryChannelTargetPoint],
1020
+ windingCrossoverLayer,
1021
+ )
1022
+ appendTransitionVia(
1023
+ boundaryChannelTargetPoint,
1024
+ windingCrossoverLayer,
1025
+ escapeLayer,
1026
+ )
1027
+ appendLayerPath([boundaryChannelTargetPoint, exitPoint], escapeLayer)
1028
+ } else {
1029
+ const targetLayerPoints = terminateAtVia
1030
+ ? [viaPoint]
1031
+ : cornerSide
1032
+ ? chamferOrthogonalPolyline(
1033
+ [
1034
+ viaPoint,
1035
+ ...(useNestedSpread ? [spreadPoint] : []),
1036
+ doglegPoint,
1037
+ localChannelSourcePoint,
1038
+ ...(isHorizontal(bus.direction) !==
1039
+ isHorizontal(boundaryDirection)
1040
+ ? [localChannelTargetPoint]
1041
+ : []),
1042
+ boundaryChannelTargetPoint,
1043
+ exitPoint,
1044
+ ],
1045
+ Math.max(traceWidth + clearance, traceWidth * 2),
1046
+ )
1047
+ : useNestedSpread
1048
+ ? chamferOrthogonalPolyline(
1049
+ [viaPoint, spreadPoint, doglegPoint, exitPoint],
1050
+ Math.max(traceWidth + clearance, traceWidth * 2),
1051
+ )
1052
+ : [viaPoint, doglegPoint, exitPoint]
1053
+ appendLayerPath(targetLayerPoints, escapeLayer)
651
1054
  }
652
1055
 
653
1056
  const outputIds = createFanoutOutputIds({
@@ -663,9 +1066,11 @@ function buildPlan(params: {
663
1066
  sourceObstacle: preparedConnection.sourceObstacle,
664
1067
  sourceLayer: preparedConnection.sourceLayer,
665
1068
  targetPoint: preparedConnection.targetPoint,
666
- targetLayer,
1069
+ targetLayer: escapeLayer,
667
1070
  termination: bus.termination,
668
1071
  direction: bus.direction,
1072
+ ...(bus.exitEdge ? { exitEdge: bus.exitEdge } : {}),
1073
+ ...(cornerSide ? { cornerBandSide: cornerSide } : {}),
669
1074
  exitPoint,
670
1075
  trace: {
671
1076
  type: "pcb_trace",
@@ -684,6 +1089,7 @@ function buildPlan(params: {
684
1089
  },
685
1090
  segments,
686
1091
  via,
1092
+ ...(additionalVias.length > 0 ? { additionalVias } : {}),
687
1093
  length: segments.reduce(
688
1094
  (total, segment) => total + distance(segment.start, segment.end),
689
1095
  0,
@@ -941,9 +1347,11 @@ function getPlanSegments(plan: FanoutRoutePlan): RoutedSegment[] {
941
1347
  }
942
1348
 
943
1349
  function getPlanVias(plan: FanoutRoutePlan) {
944
- return [plan.via, plan.planeEndpointVia].filter(
945
- (via): via is NonNullable<FanoutRoutePlan["via"]> => Boolean(via),
946
- )
1350
+ return [
1351
+ plan.via,
1352
+ ...(plan.additionalVias ?? []),
1353
+ plan.planeEndpointVia,
1354
+ ].filter((via): via is NonNullable<FanoutRoutePlan["via"]> => Boolean(via))
947
1355
  }
948
1356
 
949
1357
  function planIsStaticallyClear(params: {
@@ -1314,6 +1722,9 @@ function routePlaneTerminatedBus(
1314
1722
  viaHandedness,
1315
1723
  interstitialEscape: !pairChannelFitsVia,
1316
1724
  spreadLaneIndex: 0,
1725
+ cornerExitLaneOffset: 0,
1726
+ cornerLocalChannelLaneOffset: 0,
1727
+ cornerBoundaryChannelLaneOffset: 0,
1317
1728
  clearance,
1318
1729
  terminateAtVia: true,
1319
1730
  })
@@ -1415,6 +1826,7 @@ export function routeBusAlternatives(
1415
1826
 
1416
1827
  const alternatives: FanoutRoutePlan[][] = []
1417
1828
  const seenAlternativeKeys = new Set<string>()
1829
+ const cornerLaneOffsets = getCornerLaneOffsets(bus, acceptedPlans)
1418
1830
 
1419
1831
  const addAlternative = (plans: FanoutRoutePlan[]): void => {
1420
1832
  const key = plans
@@ -1428,6 +1840,75 @@ export function routeBusAlternatives(
1428
1840
  alternatives.push(plans)
1429
1841
  }
1430
1842
 
1843
+ if (busUsesCoordinatedWindingChannel(bus) && bus.exitEdge) {
1844
+ const boundaryDirection = getDirectionForExitEdge(bus.exitEdge)
1845
+ const boundaryExitAxis = getExitAxis(bus, boundaryDirection)
1846
+ const cornerSide = getCornerSide(bus)
1847
+ for (const viaHandedness of viaHandednesses) {
1848
+ const terminals = bus.connections.map((preparedConnection) => {
1849
+ const boundaryTrack = cornerSide
1850
+ ? getCornerTargetTrack({
1851
+ bus,
1852
+ connection: preparedConnection,
1853
+ cornerExitLaneOffset: cornerLaneOffsets.exit,
1854
+ traceWidth,
1855
+ viaDiameter,
1856
+ clearance,
1857
+ layerNames,
1858
+ })
1859
+ : getPerpendicularAxis(
1860
+ preparedConnection.exitTargetPoint ??
1861
+ preparedConnection.targetPoint,
1862
+ boundaryDirection,
1863
+ )
1864
+ return {
1865
+ connection: preparedConnection,
1866
+ viaPoint: getInitialViaPoint({
1867
+ preparedConnection,
1868
+ bus,
1869
+ targetLayer,
1870
+ traceWidth,
1871
+ viaDiameter,
1872
+ clearance,
1873
+ viaHandedness,
1874
+ }),
1875
+ exitPoint: makePoint(
1876
+ boundaryExitAxis,
1877
+ boundaryTrack,
1878
+ boundaryDirection,
1879
+ ),
1880
+ }
1881
+ })
1882
+ const viaMinimalPlans = routeViaMinimalWinding({
1883
+ srj,
1884
+ bus,
1885
+ targetLayer,
1886
+ terminals,
1887
+ acceptedPlans,
1888
+ layerNames,
1889
+ traceWidth,
1890
+ viaDiameter,
1891
+ viaHoleDiameter,
1892
+ clearance,
1893
+ allowSameNetMerges,
1894
+ })
1895
+ if (
1896
+ !viaMinimalPlans ||
1897
+ !fanoutPlansAreClear({
1898
+ plans: [...acceptedPlans, ...viaMinimalPlans],
1899
+ srj,
1900
+ sharedBoundary: bus.sharedBoundary,
1901
+ clearance,
1902
+ allowSameNetMerges,
1903
+ })
1904
+ ) {
1905
+ continue
1906
+ }
1907
+ addAlternative(viaMinimalPlans)
1908
+ if (alternatives.length >= maxAlternatives) return alternatives
1909
+ }
1910
+ }
1911
+
1431
1912
  const searchConnectionOrder = (
1432
1913
  connectionOrder: PreparedConnection[],
1433
1914
  viaHandedness: ViaHandedness,
@@ -1501,6 +1982,9 @@ export function routeBusAlternatives(
1501
1982
  connectionRank,
1502
1983
  bus.connections.length - connectionRank - 1,
1503
1984
  ),
1985
+ cornerExitLaneOffset: cornerLaneOffsets.exit,
1986
+ cornerLocalChannelLaneOffset: cornerLaneOffsets.localChannel,
1987
+ cornerBoundaryChannelLaneOffset: cornerLaneOffsets.boundaryChannel,
1504
1988
  clearance,
1505
1989
  terminateAtVia: false,
1506
1990
  })
@@ -1510,7 +1994,7 @@ export function routeBusAlternatives(
1510
1994
  otherPlans: [...acceptedPlans, ...candidatePlans],
1511
1995
  staticClearanceCache,
1512
1996
  blockingBusCounts,
1513
- cacheKey: `boundary:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${viaHandedness}:${trackIndex}`,
1997
+ cacheKey: `boundary:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${viaHandedness}:${trackIndex}:${bus.exitEdge ?? "legacy"}:${cornerLaneOffsets.exit}:${cornerLaneOffsets.localChannel}:${cornerLaneOffsets.boundaryChannel}`,
1514
1998
  srj,
1515
1999
  sharedBoundary: bus.sharedBoundary,
1516
2000
  clearance,