@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/README.md +14 -0
- package/lib/boundary-exit.ts +52 -0
- package/lib/build-output.ts +32 -10
- package/lib/fanout-exit-position.ts +78 -0
- package/lib/fanout-solver.ts +267 -38
- package/lib/index.ts +16 -13
- package/lib/prepare-buses.ts +261 -36
- package/lib/route-bus.ts +543 -59
- package/lib/route-single-layer-adaptive-exits.ts +2 -2
- package/lib/route-single-layer-push-shove.ts +2 -1
- package/lib/route-via-minimal-winding.ts +898 -0
- package/lib/types.ts +63 -3
- package/lib/validate-fanout-solution.ts +63 -11
- package/package.json +1 -1
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(
|
|
86
|
-
|
|
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 =
|
|
300
|
-
params.
|
|
301
|
-
|
|
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
|
|
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
|
-
|
|
499
|
-
}):
|
|
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
|
-
|
|
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
|
|
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) +
|
|
543
|
-
|
|
544
|
-
sourcePoint,
|
|
545
|
-
bus.direction,
|
|
546
|
-
)
|
|
725
|
+
getAxis(sourcePoint, bus.direction) +
|
|
726
|
+
directionSign(bus.direction) * initialEscapeDistance
|
|
547
727
|
const viaPerpendicularAxis =
|
|
548
|
-
|
|
549
|
-
|
|
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
|
-
:
|
|
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
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
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 [
|
|
945
|
-
|
|
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,
|