@tscircuit/fanout-solver 0.0.21 → 0.0.22
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 +15 -0
- package/lib/build-output.ts +30 -12
- package/lib/complete-original-endpoints.ts +1208 -0
- package/lib/fanout-solver.ts +150 -20
- package/lib/index.ts +4 -0
- package/lib/route-bus.ts +339 -87
- package/lib/route-single-layer-adaptive-exits.ts +1 -0
- package/lib/route-single-layer-push-shove.ts +1 -0
- package/lib/types.ts +50 -0
- package/lib/validate-fanout-solution.ts +152 -79
- package/lib/validate-original-endpoint-connectivity.ts +42 -1
- package/lib/validate-routed-copper-drc.ts +23 -2
- package/package.json +2 -2
package/lib/route-bus.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
PreparedConnection,
|
|
25
25
|
RoutedSegment,
|
|
26
26
|
} from "./types"
|
|
27
|
+
import { segmentIsLegalTerminalBodyEscape } from "./validate-routed-copper-drc"
|
|
27
28
|
|
|
28
29
|
export type RouteBusStaticClearanceCache = Map<string, boolean>
|
|
29
30
|
|
|
@@ -38,6 +39,7 @@ export interface RouteBusParams {
|
|
|
38
39
|
viaHoleDiameter: number
|
|
39
40
|
clearance: number
|
|
40
41
|
compactBusTracks: boolean
|
|
42
|
+
preferOriginalEndpointTracks?: boolean
|
|
41
43
|
allowSameNetMerges?: boolean
|
|
42
44
|
staticClearanceCache?: RouteBusStaticClearanceCache
|
|
43
45
|
blockingBusCounts?: Map<string, number>
|
|
@@ -294,6 +296,7 @@ function getPreferredTrack(params: {
|
|
|
294
296
|
targetUsesVia: boolean
|
|
295
297
|
interstitialEscape: boolean
|
|
296
298
|
compactBusTracks: boolean
|
|
299
|
+
preferOriginalEndpointTracks: boolean
|
|
297
300
|
traceWidth: number
|
|
298
301
|
viaDiameter: number
|
|
299
302
|
clearance: number
|
|
@@ -304,6 +307,7 @@ function getPreferredTrack(params: {
|
|
|
304
307
|
targetUsesVia,
|
|
305
308
|
interstitialEscape,
|
|
306
309
|
compactBusTracks,
|
|
310
|
+
preferOriginalEndpointTracks,
|
|
307
311
|
traceWidth,
|
|
308
312
|
viaDiameter,
|
|
309
313
|
clearance,
|
|
@@ -315,6 +319,9 @@ function getPreferredTrack(params: {
|
|
|
315
319
|
connection.sourcePoint,
|
|
316
320
|
bus.direction,
|
|
317
321
|
)
|
|
322
|
+
if (preferOriginalEndpointTracks) {
|
|
323
|
+
return getPerpendicularAxis(connection.targetPoint, bus.direction)
|
|
324
|
+
}
|
|
318
325
|
if (compactBusTracks) {
|
|
319
326
|
const connectionRank = getConnectionRank(bus, connection)
|
|
320
327
|
const componentCenter =
|
|
@@ -635,6 +642,7 @@ function buildPlan(params: {
|
|
|
635
642
|
sourcePoint: preparedConnection.sourcePoint,
|
|
636
643
|
sourceObstacle: preparedConnection.sourceObstacle,
|
|
637
644
|
sourceLayer: preparedConnection.sourceLayer,
|
|
645
|
+
targetPoint: preparedConnection.targetPoint,
|
|
638
646
|
targetLayer,
|
|
639
647
|
termination: bus.termination,
|
|
640
648
|
direction: bus.direction,
|
|
@@ -663,6 +671,193 @@ function buildPlan(params: {
|
|
|
663
671
|
}
|
|
664
672
|
}
|
|
665
673
|
|
|
674
|
+
function getPointLayer(point: PreparedConnection["targetPoint"]): string {
|
|
675
|
+
const layer = "layer" in point ? point.layer : point.layers[0]
|
|
676
|
+
if (!layer) {
|
|
677
|
+
throw new Error("FanoutSolver: plane endpoint has no copper layer")
|
|
678
|
+
}
|
|
679
|
+
return layer
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function getPlaneEndpointViaCandidates(params: {
|
|
683
|
+
preparedConnection: PreparedConnection
|
|
684
|
+
bus: PreparedBus
|
|
685
|
+
viaDiameter: number
|
|
686
|
+
clearance: number
|
|
687
|
+
}): Point2D[] {
|
|
688
|
+
const { preparedConnection, bus, viaDiameter, clearance } = params
|
|
689
|
+
const { sourcePoint, targetPoint } = preparedConnection
|
|
690
|
+
const nearbyEndpointLimit = Math.max(bus.pitchX, bus.pitchY) * 0.5
|
|
691
|
+
if (
|
|
692
|
+
distance(sourcePoint, targetPoint) <= 1e-6 ||
|
|
693
|
+
distance(sourcePoint, targetPoint) > nearbyEndpointLimit
|
|
694
|
+
) {
|
|
695
|
+
return []
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const preferred = (() => {
|
|
699
|
+
switch (bus.direction) {
|
|
700
|
+
case "left":
|
|
701
|
+
return { x: -1, y: 0 }
|
|
702
|
+
case "right":
|
|
703
|
+
return { x: 1, y: 0 }
|
|
704
|
+
case "up":
|
|
705
|
+
return { x: 0, y: 1 }
|
|
706
|
+
case "down":
|
|
707
|
+
return { x: 0, y: -1 }
|
|
708
|
+
}
|
|
709
|
+
})()
|
|
710
|
+
const diagonal = Math.SQRT1_2
|
|
711
|
+
const directions = [
|
|
712
|
+
preferred,
|
|
713
|
+
...[
|
|
714
|
+
{ x: diagonal, y: diagonal },
|
|
715
|
+
{ x: diagonal, y: -diagonal },
|
|
716
|
+
{ x: -diagonal, y: diagonal },
|
|
717
|
+
{ x: -diagonal, y: -diagonal },
|
|
718
|
+
{ x: 1, y: 0 },
|
|
719
|
+
{ x: -1, y: 0 },
|
|
720
|
+
{ x: 0, y: 1 },
|
|
721
|
+
{ x: 0, y: -1 },
|
|
722
|
+
].toSorted(
|
|
723
|
+
(first, second) =>
|
|
724
|
+
second.x * preferred.x +
|
|
725
|
+
second.y * preferred.y -
|
|
726
|
+
(first.x * preferred.x + first.y * preferred.y),
|
|
727
|
+
),
|
|
728
|
+
]
|
|
729
|
+
const minimumRadius = Math.max(viaDiameter, viaDiameter / 2 + clearance)
|
|
730
|
+
const radii = [
|
|
731
|
+
minimumRadius,
|
|
732
|
+
Math.max(minimumRadius, Math.min(bus.pitchX, bus.pitchY) * 0.5),
|
|
733
|
+
Math.max(minimumRadius, Math.min(bus.pitchX, bus.pitchY) * 0.625),
|
|
734
|
+
Math.max(minimumRadius, Math.min(bus.pitchX, bus.pitchY) * 0.75),
|
|
735
|
+
]
|
|
736
|
+
const candidates: Point2D[] = []
|
|
737
|
+
for (const origin of [sourcePoint, targetPoint]) {
|
|
738
|
+
for (const radius of radii) {
|
|
739
|
+
for (const direction of directions) {
|
|
740
|
+
const candidate = {
|
|
741
|
+
x: origin.x + direction.x * radius,
|
|
742
|
+
y: origin.y + direction.y * radius,
|
|
743
|
+
}
|
|
744
|
+
if (
|
|
745
|
+
distance(candidate, sourcePoint) <= 1e-6 ||
|
|
746
|
+
distance(candidate, targetPoint) <= 1e-6 ||
|
|
747
|
+
candidates.some((existing) => distance(existing, candidate) <= 1e-6)
|
|
748
|
+
) {
|
|
749
|
+
continue
|
|
750
|
+
}
|
|
751
|
+
candidates.push(candidate)
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return candidates
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
function addPlaneEndpointTerminal(params: {
|
|
759
|
+
plan: FanoutRoutePlan
|
|
760
|
+
preparedConnection: PreparedConnection
|
|
761
|
+
planeLayer: string
|
|
762
|
+
viaPoint: Point2D
|
|
763
|
+
layerNames: string[]
|
|
764
|
+
traceWidth: number
|
|
765
|
+
viaDiameter: number
|
|
766
|
+
viaHoleDiameter: number
|
|
767
|
+
}): FanoutRoutePlan {
|
|
768
|
+
const {
|
|
769
|
+
plan,
|
|
770
|
+
preparedConnection,
|
|
771
|
+
planeLayer,
|
|
772
|
+
viaPoint,
|
|
773
|
+
layerNames,
|
|
774
|
+
traceWidth,
|
|
775
|
+
viaDiameter,
|
|
776
|
+
viaHoleDiameter,
|
|
777
|
+
} = params
|
|
778
|
+
const targetPoint = {
|
|
779
|
+
x: preparedConnection.targetPoint.x,
|
|
780
|
+
y: preparedConnection.targetPoint.y,
|
|
781
|
+
}
|
|
782
|
+
const targetEndpointLayer = getPointLayer(preparedConnection.targetPoint)
|
|
783
|
+
const spanLayers = getLayerSpan(planeLayer, targetEndpointLayer, layerNames)
|
|
784
|
+
if (!spanLayers.includes(planeLayer)) {
|
|
785
|
+
throw new Error(
|
|
786
|
+
`FanoutSolver: via for "${preparedConnection.connection.name}" does not cross plane ${planeLayer}`,
|
|
787
|
+
)
|
|
788
|
+
}
|
|
789
|
+
const planeEndpointSegments: RoutedSegment[] = [
|
|
790
|
+
{
|
|
791
|
+
start: viaPoint,
|
|
792
|
+
end: targetPoint,
|
|
793
|
+
width: traceWidth,
|
|
794
|
+
layer: targetEndpointLayer,
|
|
795
|
+
},
|
|
796
|
+
]
|
|
797
|
+
const via = {
|
|
798
|
+
center: viaPoint,
|
|
799
|
+
diameter: viaDiameter,
|
|
800
|
+
holeDiameter: viaHoleDiameter,
|
|
801
|
+
fromLayer: planeLayer,
|
|
802
|
+
toLayer: targetEndpointLayer,
|
|
803
|
+
spanLayers,
|
|
804
|
+
}
|
|
805
|
+
const planeEndpointTrace: SimplifiedPcbTrace = {
|
|
806
|
+
type: "pcb_trace",
|
|
807
|
+
pcb_trace_id: `fanout-plane-endpoint:${preparedConnection.connection.name}`,
|
|
808
|
+
connection_name: preparedConnection.connection.name,
|
|
809
|
+
connectsTo: [
|
|
810
|
+
preparedConnection.connection.name,
|
|
811
|
+
...(preparedConnection.targetPoint.pointId
|
|
812
|
+
? [preparedConnection.targetPoint.pointId]
|
|
813
|
+
: []),
|
|
814
|
+
...(preparedConnection.targetPoint.pcb_port_id
|
|
815
|
+
? [preparedConnection.targetPoint.pcb_port_id]
|
|
816
|
+
: []),
|
|
817
|
+
],
|
|
818
|
+
route: [
|
|
819
|
+
{
|
|
820
|
+
route_type: "wire",
|
|
821
|
+
...viaPoint,
|
|
822
|
+
width: traceWidth,
|
|
823
|
+
layer: planeLayer,
|
|
824
|
+
},
|
|
825
|
+
{
|
|
826
|
+
route_type: "via",
|
|
827
|
+
...viaPoint,
|
|
828
|
+
from_layer: planeLayer,
|
|
829
|
+
to_layer: targetEndpointLayer,
|
|
830
|
+
via_diameter: viaDiameter,
|
|
831
|
+
via_hole_diameter: viaHoleDiameter,
|
|
832
|
+
},
|
|
833
|
+
{
|
|
834
|
+
route_type: "wire",
|
|
835
|
+
...viaPoint,
|
|
836
|
+
width: traceWidth,
|
|
837
|
+
layer: targetEndpointLayer,
|
|
838
|
+
},
|
|
839
|
+
{
|
|
840
|
+
route_type: "wire",
|
|
841
|
+
...targetPoint,
|
|
842
|
+
width: traceWidth,
|
|
843
|
+
layer: targetEndpointLayer,
|
|
844
|
+
},
|
|
845
|
+
],
|
|
846
|
+
}
|
|
847
|
+
return {
|
|
848
|
+
...plan,
|
|
849
|
+
planeEndpointTrace,
|
|
850
|
+
planeEndpointSegments,
|
|
851
|
+
planeEndpointVia: via,
|
|
852
|
+
length:
|
|
853
|
+
plan.length +
|
|
854
|
+
planeEndpointSegments.reduce(
|
|
855
|
+
(total, segment) => total + distance(segment.start, segment.end),
|
|
856
|
+
0,
|
|
857
|
+
),
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
666
861
|
function segmentIsClearOfObstacles(params: {
|
|
667
862
|
segment: RoutedSegment
|
|
668
863
|
plan: FanoutRoutePlan
|
|
@@ -690,6 +885,16 @@ function segmentIsClearOfObstacles(params: {
|
|
|
690
885
|
) {
|
|
691
886
|
continue
|
|
692
887
|
}
|
|
888
|
+
if (
|
|
889
|
+
segmentIsLegalTerminalBodyEscape({
|
|
890
|
+
inputSrj: srj,
|
|
891
|
+
segment,
|
|
892
|
+
bodyObstacle: obstacle,
|
|
893
|
+
connectionName: plan.connectionName,
|
|
894
|
+
})
|
|
895
|
+
) {
|
|
896
|
+
continue
|
|
897
|
+
}
|
|
693
898
|
if (
|
|
694
899
|
segmentIndex === 0 &&
|
|
695
900
|
obstacle === plan.sourceObstacle &&
|
|
@@ -707,6 +912,16 @@ function segmentIsClearOfObstacles(params: {
|
|
|
707
912
|
return true
|
|
708
913
|
}
|
|
709
914
|
|
|
915
|
+
function getPlanSegments(plan: FanoutRoutePlan): RoutedSegment[] {
|
|
916
|
+
return [...plan.segments, ...(plan.planeEndpointSegments ?? [])]
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
function getPlanVias(plan: FanoutRoutePlan) {
|
|
920
|
+
return [plan.via, plan.planeEndpointVia].filter(
|
|
921
|
+
(via): via is NonNullable<FanoutRoutePlan["via"]> => Boolean(via),
|
|
922
|
+
)
|
|
923
|
+
}
|
|
924
|
+
|
|
710
925
|
function planIsStaticallyClear(params: {
|
|
711
926
|
plan: FanoutRoutePlan
|
|
712
927
|
srj: SimpleRouteJson
|
|
@@ -718,7 +933,7 @@ function planIsStaticallyClear(params: {
|
|
|
718
933
|
const routableBounds = getRoutableBounds(srj.bounds, sharedBoundary)
|
|
719
934
|
if (
|
|
720
935
|
!pointIsInsideBounds(plan.exitPoint, routableBounds) ||
|
|
721
|
-
plan.
|
|
936
|
+
getPlanSegments(plan).some(
|
|
722
937
|
(segment) =>
|
|
723
938
|
!pointIsInsideBounds(segment.start, routableBounds) ||
|
|
724
939
|
!pointIsInsideBounds(segment.end, routableBounds),
|
|
@@ -726,12 +941,13 @@ function planIsStaticallyClear(params: {
|
|
|
726
941
|
) {
|
|
727
942
|
return false
|
|
728
943
|
}
|
|
729
|
-
|
|
944
|
+
const segments = getPlanSegments(plan)
|
|
945
|
+
for (let index = 0; index < segments.length; index++) {
|
|
730
946
|
if (
|
|
731
947
|
!segmentIsClearOfObstacles({
|
|
732
|
-
segment:
|
|
948
|
+
segment: segments[index]!,
|
|
733
949
|
plan,
|
|
734
|
-
segmentIndex: index,
|
|
950
|
+
segmentIndex: index < plan.segments.length ? index : -1,
|
|
735
951
|
srj,
|
|
736
952
|
allowSameNetMerges,
|
|
737
953
|
obstacles: srj.obstacles,
|
|
@@ -741,11 +957,9 @@ function planIsStaticallyClear(params: {
|
|
|
741
957
|
return false
|
|
742
958
|
}
|
|
743
959
|
}
|
|
744
|
-
|
|
960
|
+
for (const via of getPlanVias(plan)) {
|
|
745
961
|
for (const obstacle of srj.obstacles) {
|
|
746
|
-
if (
|
|
747
|
-
!obstacle.layers.some((layer) => plan.via!.spanLayers.includes(layer))
|
|
748
|
-
) {
|
|
962
|
+
if (!obstacle.layers.some((layer) => via.spanLayers.includes(layer))) {
|
|
749
963
|
continue
|
|
750
964
|
}
|
|
751
965
|
if (
|
|
@@ -755,8 +969,8 @@ function planIsStaticallyClear(params: {
|
|
|
755
969
|
continue
|
|
756
970
|
}
|
|
757
971
|
if (
|
|
758
|
-
distancePointToObstacle(
|
|
759
|
-
|
|
972
|
+
distancePointToObstacle(via.center, obstacle) <
|
|
973
|
+
via.diameter / 2 + clearance - 1e-9
|
|
760
974
|
) {
|
|
761
975
|
return false
|
|
762
976
|
}
|
|
@@ -806,51 +1020,54 @@ function planIsClearOfPlans(params: {
|
|
|
806
1020
|
(blockingBusCounts.get(otherPlan.busId) ?? 0) + 1,
|
|
807
1021
|
)
|
|
808
1022
|
}
|
|
809
|
-
|
|
810
|
-
|
|
1023
|
+
const planSegments = getPlanSegments(plan)
|
|
1024
|
+
const otherSegments = getPlanSegments(otherPlan)
|
|
1025
|
+
const planVias = getPlanVias(plan)
|
|
1026
|
+
const otherVias = getPlanVias(otherPlan)
|
|
1027
|
+
for (const segment of planSegments) {
|
|
1028
|
+
for (const otherSegment of otherSegments) {
|
|
811
1029
|
if (!segmentsAreClear(segment, otherSegment, clearance)) {
|
|
812
1030
|
recordBlocker()
|
|
813
1031
|
return false
|
|
814
1032
|
}
|
|
815
1033
|
}
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
recordBlocker()
|
|
826
|
-
return false
|
|
1034
|
+
for (const otherVia of otherVias) {
|
|
1035
|
+
if (
|
|
1036
|
+
otherVia.spanLayers.includes(segment.layer) &&
|
|
1037
|
+
distancePointToSegment(otherVia.center, segment.start, segment.end) <
|
|
1038
|
+
otherVia.diameter / 2 + segment.width / 2 + clearance - 1e-9
|
|
1039
|
+
) {
|
|
1040
|
+
recordBlocker()
|
|
1041
|
+
return false
|
|
1042
|
+
}
|
|
827
1043
|
}
|
|
828
1044
|
}
|
|
829
|
-
|
|
830
|
-
for (const otherSegment of
|
|
1045
|
+
for (const planVia of planVias) {
|
|
1046
|
+
for (const otherSegment of otherSegments) {
|
|
831
1047
|
if (
|
|
832
|
-
|
|
1048
|
+
planVia.spanLayers.includes(otherSegment.layer) &&
|
|
833
1049
|
distancePointToSegment(
|
|
834
|
-
|
|
1050
|
+
planVia.center,
|
|
835
1051
|
otherSegment.start,
|
|
836
1052
|
otherSegment.end,
|
|
837
1053
|
) <
|
|
838
|
-
|
|
1054
|
+
planVia.diameter / 2 + otherSegment.width / 2 + clearance - 1e-9
|
|
839
1055
|
) {
|
|
840
1056
|
recordBlocker()
|
|
841
1057
|
return false
|
|
842
1058
|
}
|
|
843
1059
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1060
|
+
for (const otherVia of otherVias) {
|
|
1061
|
+
if (
|
|
1062
|
+
planVia.spanLayers.some((layer) =>
|
|
1063
|
+
otherVia.spanLayers.includes(layer),
|
|
1064
|
+
) &&
|
|
1065
|
+
distance(planVia.center, otherVia.center) <
|
|
1066
|
+
(planVia.diameter + otherVia.diameter) / 2 + clearance - 1e-9
|
|
1067
|
+
) {
|
|
1068
|
+
recordBlocker()
|
|
1069
|
+
return false
|
|
1070
|
+
}
|
|
854
1071
|
}
|
|
855
1072
|
}
|
|
856
1073
|
}
|
|
@@ -971,60 +1188,93 @@ function routePlaneTerminatedBus(
|
|
|
971
1188
|
if (!sourceObstacle || bus.termination.type !== "plane") return null
|
|
972
1189
|
const sourceLayer = bus.connections[0]!.sourceLayer
|
|
973
1190
|
if (targetLayer === sourceLayer) return null
|
|
974
|
-
const directionalPadSize = isHorizontal(bus.direction)
|
|
975
|
-
? sourceObstacle.width
|
|
976
|
-
: sourceObstacle.height
|
|
977
|
-
const pairChannelFitsVia =
|
|
978
|
-
getDirectionalPitch(bus) / 2 - directionalPadSize / 2 >=
|
|
979
|
-
viaDiameter / 2 + clearance - 1e-9
|
|
980
|
-
const viaHandednesses: readonly ViaHandedness[] = pairChannelFitsVia
|
|
981
|
-
? [0]
|
|
982
|
-
: [1, -1]
|
|
983
1191
|
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1192
|
+
const candidateDirections: FanoutDirection[] = [
|
|
1193
|
+
bus.direction,
|
|
1194
|
+
...(["left", "right", "up", "down"] as const).filter(
|
|
1195
|
+
(direction) => direction !== bus.direction,
|
|
1196
|
+
),
|
|
1197
|
+
]
|
|
1198
|
+
for (const direction of candidateDirections) {
|
|
1199
|
+
const directionalBus =
|
|
1200
|
+
direction === bus.direction ? bus : { ...bus, direction }
|
|
1201
|
+
const directionalPadSize = isHorizontal(direction)
|
|
1202
|
+
? sourceObstacle.width
|
|
1203
|
+
: sourceObstacle.height
|
|
1204
|
+
const pairChannelFitsVia =
|
|
1205
|
+
getDirectionalPitch(directionalBus) / 2 - directionalPadSize / 2 >=
|
|
1206
|
+
viaDiameter / 2 + clearance - 1e-9
|
|
1207
|
+
const viaHandednesses: readonly ViaHandedness[] = pairChannelFitsVia
|
|
1208
|
+
? [0]
|
|
1209
|
+
: [1, -1]
|
|
1210
|
+
|
|
1211
|
+
for (const viaHandedness of viaHandednesses) {
|
|
1212
|
+
for (const connectionOrder of getConnectionOrders(directionalBus)) {
|
|
1213
|
+
const candidatePlans: FanoutRoutePlan[] = []
|
|
1214
|
+
let orderIsClear = true
|
|
1215
|
+
for (const preparedConnection of connectionOrder) {
|
|
1216
|
+
const sourceTrack = getPerpendicularAxis(
|
|
1217
|
+
preparedConnection.sourcePoint,
|
|
1218
|
+
direction,
|
|
1219
|
+
)
|
|
1220
|
+
const basePlan = buildPlan({
|
|
1221
|
+
preparedConnection,
|
|
1222
|
+
bus: directionalBus,
|
|
1223
|
+
targetLayer,
|
|
1224
|
+
track: sourceTrack,
|
|
1225
|
+
exitAxis: getExitAxis(directionalBus),
|
|
1226
|
+
layerNames,
|
|
1227
|
+
traceWidth,
|
|
1228
|
+
viaDiameter,
|
|
1229
|
+
viaHoleDiameter,
|
|
1230
|
+
viaHandedness,
|
|
1231
|
+
interstitialEscape: !pairChannelFitsVia,
|
|
1232
|
+
spreadLaneIndex: 0,
|
|
1018
1233
|
clearance,
|
|
1019
|
-
|
|
1234
|
+
terminateAtVia: true,
|
|
1020
1235
|
})
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1236
|
+
const endpointViaCandidates = getPlaneEndpointViaCandidates({
|
|
1237
|
+
preparedConnection,
|
|
1238
|
+
bus: directionalBus,
|
|
1239
|
+
viaDiameter,
|
|
1240
|
+
clearance,
|
|
1241
|
+
})
|
|
1242
|
+
const plansToTry = [
|
|
1243
|
+
...endpointViaCandidates.map((viaPoint) =>
|
|
1244
|
+
addPlaneEndpointTerminal({
|
|
1245
|
+
plan: basePlan,
|
|
1246
|
+
preparedConnection,
|
|
1247
|
+
planeLayer: targetLayer,
|
|
1248
|
+
viaPoint,
|
|
1249
|
+
layerNames,
|
|
1250
|
+
traceWidth,
|
|
1251
|
+
viaDiameter,
|
|
1252
|
+
viaHoleDiameter,
|
|
1253
|
+
}),
|
|
1254
|
+
),
|
|
1255
|
+
basePlan,
|
|
1256
|
+
]
|
|
1257
|
+
const plan = plansToTry.find((candidatePlan, candidateIndex) =>
|
|
1258
|
+
planIsClear({
|
|
1259
|
+
plan: candidatePlan,
|
|
1260
|
+
otherPlans: [...acceptedPlans, ...candidatePlans],
|
|
1261
|
+
staticClearanceCache,
|
|
1262
|
+
blockingBusCounts,
|
|
1263
|
+
cacheKey: `plane:${bus.busId}:${targetLayer}:${direction}:${preparedConnection.connectionIndex}:${viaHandedness}:${candidateIndex}`,
|
|
1264
|
+
srj,
|
|
1265
|
+
sharedBoundary: bus.sharedBoundary,
|
|
1266
|
+
clearance,
|
|
1267
|
+
allowSameNetMerges,
|
|
1268
|
+
}),
|
|
1269
|
+
)
|
|
1270
|
+
if (!plan) {
|
|
1271
|
+
orderIsClear = false
|
|
1272
|
+
break
|
|
1273
|
+
}
|
|
1274
|
+
candidatePlans.push(plan)
|
|
1024
1275
|
}
|
|
1025
|
-
|
|
1276
|
+
if (orderIsClear) return candidatePlans
|
|
1026
1277
|
}
|
|
1027
|
-
if (orderIsClear) return candidatePlans
|
|
1028
1278
|
}
|
|
1029
1279
|
}
|
|
1030
1280
|
|
|
@@ -1046,6 +1296,7 @@ export function routeBusAlternatives(
|
|
|
1046
1296
|
viaHoleDiameter,
|
|
1047
1297
|
clearance,
|
|
1048
1298
|
compactBusTracks,
|
|
1299
|
+
preferOriginalEndpointTracks = false,
|
|
1049
1300
|
staticClearanceCache,
|
|
1050
1301
|
blockingBusCounts,
|
|
1051
1302
|
allowSameNetMerges = false,
|
|
@@ -1117,6 +1368,7 @@ export function routeBusAlternatives(
|
|
|
1117
1368
|
targetUsesVia,
|
|
1118
1369
|
interstitialEscape,
|
|
1119
1370
|
compactBusTracks,
|
|
1371
|
+
preferOriginalEndpointTracks,
|
|
1120
1372
|
traceWidth,
|
|
1121
1373
|
viaDiameter,
|
|
1122
1374
|
clearance,
|
|
@@ -823,6 +823,7 @@ function buildPlan(route: FlowRoute, traceWidth: number): FanoutRoutePlan {
|
|
|
823
823
|
sourcePoint: item.connection.sourcePoint,
|
|
824
824
|
sourceObstacle: item.connection.sourceObstacle,
|
|
825
825
|
sourceLayer: item.connection.sourceLayer,
|
|
826
|
+
targetPoint: item.connection.targetPoint,
|
|
826
827
|
targetLayer: "top",
|
|
827
828
|
termination: item.bus.termination,
|
|
828
829
|
direction: item.bus.direction,
|
|
@@ -756,6 +756,7 @@ function buildPlan(path: RoutedPath): FanoutRoutePlan {
|
|
|
756
756
|
sourcePoint: item.connection.sourcePoint,
|
|
757
757
|
sourceObstacle: item.connection.sourceObstacle,
|
|
758
758
|
sourceLayer: item.connection.sourceLayer,
|
|
759
|
+
targetPoint: item.connection.targetPoint,
|
|
759
760
|
targetLayer: "top",
|
|
760
761
|
termination: item.bus.termination,
|
|
761
762
|
direction: item.direction,
|
package/lib/types.ts
CHANGED
|
@@ -6,6 +6,8 @@ import type {
|
|
|
6
6
|
SimpleRouteJson,
|
|
7
7
|
SimplifiedPcbTrace,
|
|
8
8
|
} from "@tscircuit/capacity-autorouter"
|
|
9
|
+
import type { OriginalEndpointConnectivityReport } from "./validate-original-endpoint-connectivity"
|
|
10
|
+
import type { RoutedCopperDrcReport } from "./validate-routed-copper-drc"
|
|
9
11
|
|
|
10
12
|
export type FanoutDirection = "left" | "right" | "up" | "down"
|
|
11
13
|
|
|
@@ -93,11 +95,20 @@ export interface FanoutSolverOptions {
|
|
|
93
95
|
sharedBoundary?: Bounds
|
|
94
96
|
escapeLayers?: string[]
|
|
95
97
|
maxLayerCombinations?: number
|
|
98
|
+
/** Balance layer congestion by routed connection count instead of bus count. */
|
|
99
|
+
balanceLayerLoadByConnectionCount?: boolean
|
|
96
100
|
traceWidth?: number
|
|
97
101
|
viaDiameter?: number
|
|
98
102
|
viaHoleDiameter?: number
|
|
99
103
|
clearance?: number
|
|
100
104
|
compactBusTracks?: boolean
|
|
105
|
+
/**
|
|
106
|
+
* Prefer the perpendicular coordinate of each original downstream endpoint
|
|
107
|
+
* when assigning boundary tracks. Ordered edge-pad buses can then enter
|
|
108
|
+
* same-layer pads directly instead of requiring a second global router to
|
|
109
|
+
* undo compacted fanout tracks.
|
|
110
|
+
*/
|
|
111
|
+
preferOriginalEndpointTracks?: boolean
|
|
101
112
|
/** Allow branches belonging to the same electrical net to share copper. */
|
|
102
113
|
allowSameNetMerges?: boolean
|
|
103
114
|
singleLayerPushAndShove?: boolean
|
|
@@ -108,6 +119,14 @@ export interface FanoutSolverOptions {
|
|
|
108
119
|
*/
|
|
109
120
|
singleLayerAdaptiveExits?: boolean
|
|
110
121
|
borderDistribution?: FanoutBorderDistribution
|
|
122
|
+
/**
|
|
123
|
+
* After every source pad is escaped, attempt to physically join the fanout
|
|
124
|
+
* copper to each original downstream endpoint. Every added trace is audited
|
|
125
|
+
* with the independent endpoint-connectivity and emitted-copper validators.
|
|
126
|
+
*/
|
|
127
|
+
completeOriginalEndpoints?: boolean
|
|
128
|
+
/** Effort passed to the bounded downstream capacity-router pass. */
|
|
129
|
+
endpointCompletionEffort?: number
|
|
111
130
|
}
|
|
112
131
|
|
|
113
132
|
export interface FanoutAttemptSummary {
|
|
@@ -123,6 +142,8 @@ export interface FanoutAttemptSummary {
|
|
|
123
142
|
export interface FanoutSolverOutput {
|
|
124
143
|
simpleRouteJson: SimpleRouteJson
|
|
125
144
|
fanoutTraces: SimplifiedPcbTrace[]
|
|
145
|
+
completionTraces: SimplifiedPcbTrace[]
|
|
146
|
+
endpointCompletion?: FanoutEndpointCompletionReport
|
|
126
147
|
planeTerminations: FanoutPlaneTermination[]
|
|
127
148
|
busLayerAssignments: Readonly<Record<string, string>>
|
|
128
149
|
busDirections: Readonly<Record<string, FanoutDirection>>
|
|
@@ -130,6 +151,16 @@ export interface FanoutSolverOutput {
|
|
|
130
151
|
validation: FanoutValidationReport
|
|
131
152
|
}
|
|
132
153
|
|
|
154
|
+
export interface FanoutEndpointCompletionReport {
|
|
155
|
+
attemptedLocalConnectionCount: number
|
|
156
|
+
attemptedDownstreamConnectionCount: number
|
|
157
|
+
completionTraceCount: number
|
|
158
|
+
searchPassCount: number
|
|
159
|
+
errors: string[]
|
|
160
|
+
connectivity: OriginalEndpointConnectivityReport
|
|
161
|
+
drc: RoutedCopperDrcReport
|
|
162
|
+
}
|
|
163
|
+
|
|
133
164
|
export interface FanoutValidationIssue {
|
|
134
165
|
code:
|
|
135
166
|
| "missing-plan"
|
|
@@ -227,6 +258,7 @@ export interface FanoutRoutePlan {
|
|
|
227
258
|
sourcePoint: ConnectionPoint
|
|
228
259
|
sourceObstacle: Obstacle
|
|
229
260
|
sourceLayer: string
|
|
261
|
+
targetPoint: ConnectionPoint
|
|
230
262
|
targetLayer: string
|
|
231
263
|
termination: FanoutBusTermination
|
|
232
264
|
direction: FanoutDirection
|
|
@@ -234,6 +266,10 @@ export interface FanoutRoutePlan {
|
|
|
234
266
|
trace: SimplifiedPcbTrace
|
|
235
267
|
segments: RoutedSegment[]
|
|
236
268
|
via?: RoutedVia
|
|
269
|
+
/** Optional capacitor-side dogbone reserved and emitted with a plane escape. */
|
|
270
|
+
planeEndpointTrace?: SimplifiedPcbTrace
|
|
271
|
+
planeEndpointSegments?: RoutedSegment[]
|
|
272
|
+
planeEndpointVia?: RoutedVia
|
|
237
273
|
length: number
|
|
238
274
|
}
|
|
239
275
|
|
|
@@ -244,6 +280,20 @@ export interface FanoutPlaneTermination {
|
|
|
244
280
|
via: RoutedVia
|
|
245
281
|
}
|
|
246
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Declares an ideal copper plane used by emitted fanout traces. This metadata
|
|
285
|
+
* lets independent connectivity validation join same-net vias on the named
|
|
286
|
+
* layer without inventing a long point-to-point trace across the plane.
|
|
287
|
+
*/
|
|
288
|
+
export interface FanoutPlaneConnectivity {
|
|
289
|
+
connectionName: string
|
|
290
|
+
layer: string
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export type SimpleRouteJsonWithFanoutPlanes = SimpleRouteJson & {
|
|
294
|
+
fanoutPlaneConnectivity?: FanoutPlaneConnectivity[]
|
|
295
|
+
}
|
|
296
|
+
|
|
247
297
|
export interface AssignmentAttempt {
|
|
248
298
|
summary: FanoutAttemptSummary
|
|
249
299
|
plans: FanoutRoutePlan[]
|