@tscircuit/fanout-solver 0.0.17 → 0.0.19
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 +41 -22
- package/lib/fanout-solver.ts +583 -82
- package/lib/geometry.ts +38 -19
- package/lib/index.ts +5 -0
- package/lib/layer-names.ts +38 -12
- package/lib/net-identity.ts +163 -0
- package/lib/route-bus.ts +305 -71
- package/lib/types.ts +39 -0
- package/lib/validate-fanout-solution.ts +772 -0
- package/package.json +3 -2
package/lib/route-bus.ts
CHANGED
|
@@ -11,6 +11,10 @@ import {
|
|
|
11
11
|
segmentsAreClear,
|
|
12
12
|
} from "./geometry"
|
|
13
13
|
import { getLayerSpan } from "./layer-names"
|
|
14
|
+
import {
|
|
15
|
+
connectionsShareElectricalNet,
|
|
16
|
+
obstacleSharesElectricalNet,
|
|
17
|
+
} from "./net-identity"
|
|
14
18
|
import type {
|
|
15
19
|
Bounds,
|
|
16
20
|
FanoutDirection,
|
|
@@ -21,7 +25,9 @@ import type {
|
|
|
21
25
|
RoutedSegment,
|
|
22
26
|
} from "./types"
|
|
23
27
|
|
|
24
|
-
|
|
28
|
+
export type RouteBusStaticClearanceCache = Map<string, boolean>
|
|
29
|
+
|
|
30
|
+
export interface RouteBusParams {
|
|
25
31
|
srj: SimpleRouteJson
|
|
26
32
|
bus: PreparedBus
|
|
27
33
|
targetLayer: string
|
|
@@ -32,6 +38,9 @@ interface RouteBusParams {
|
|
|
32
38
|
viaHoleDiameter: number
|
|
33
39
|
clearance: number
|
|
34
40
|
compactBusTracks: boolean
|
|
41
|
+
allowSameNetMerges?: boolean
|
|
42
|
+
staticClearanceCache?: RouteBusStaticClearanceCache
|
|
43
|
+
blockingBusCounts?: Map<string, number>
|
|
35
44
|
}
|
|
36
45
|
|
|
37
46
|
interface TrackCandidate {
|
|
@@ -658,13 +667,29 @@ function segmentIsClearOfObstacles(params: {
|
|
|
658
667
|
segment: RoutedSegment
|
|
659
668
|
plan: FanoutRoutePlan
|
|
660
669
|
segmentIndex: number
|
|
670
|
+
srj: SimpleRouteJson
|
|
671
|
+
allowSameNetMerges: boolean
|
|
661
672
|
obstacles: Obstacle[]
|
|
662
673
|
clearance: number
|
|
663
674
|
}): boolean {
|
|
664
|
-
const {
|
|
675
|
+
const {
|
|
676
|
+
segment,
|
|
677
|
+
plan,
|
|
678
|
+
segmentIndex,
|
|
679
|
+
srj,
|
|
680
|
+
allowSameNetMerges,
|
|
681
|
+
obstacles,
|
|
682
|
+
clearance,
|
|
683
|
+
} = params
|
|
665
684
|
for (const obstacle of obstacles) {
|
|
666
685
|
if (!obstacle.layers.includes(segment.layer)) continue
|
|
667
686
|
if (obstacle.connectedTo.includes(plan.connectionName)) continue
|
|
687
|
+
if (
|
|
688
|
+
allowSameNetMerges &&
|
|
689
|
+
obstacleSharesElectricalNet(srj, obstacle, plan.connectionName)
|
|
690
|
+
) {
|
|
691
|
+
continue
|
|
692
|
+
}
|
|
668
693
|
if (
|
|
669
694
|
segmentIndex === 0 &&
|
|
670
695
|
obstacle === plan.sourceObstacle &&
|
|
@@ -682,14 +707,14 @@ function segmentIsClearOfObstacles(params: {
|
|
|
682
707
|
return true
|
|
683
708
|
}
|
|
684
709
|
|
|
685
|
-
function
|
|
710
|
+
function planIsStaticallyClear(params: {
|
|
686
711
|
plan: FanoutRoutePlan
|
|
687
|
-
otherPlans: FanoutRoutePlan[]
|
|
688
712
|
srj: SimpleRouteJson
|
|
689
713
|
sharedBoundary: Bounds
|
|
690
714
|
clearance: number
|
|
715
|
+
allowSameNetMerges: boolean
|
|
691
716
|
}): boolean {
|
|
692
|
-
const { plan,
|
|
717
|
+
const { plan, srj, sharedBoundary, clearance, allowSameNetMerges } = params
|
|
693
718
|
const routableBounds = getRoutableBounds(srj.bounds, sharedBoundary)
|
|
694
719
|
if (
|
|
695
720
|
!pointIsInsideBounds(plan.exitPoint, routableBounds) ||
|
|
@@ -707,6 +732,8 @@ function planIsClear(params: {
|
|
|
707
732
|
segment: plan.segments[index]!,
|
|
708
733
|
plan,
|
|
709
734
|
segmentIndex: index,
|
|
735
|
+
srj,
|
|
736
|
+
allowSameNetMerges,
|
|
710
737
|
obstacles: srj.obstacles,
|
|
711
738
|
clearance,
|
|
712
739
|
})
|
|
@@ -721,6 +748,12 @@ function planIsClear(params: {
|
|
|
721
748
|
) {
|
|
722
749
|
continue
|
|
723
750
|
}
|
|
751
|
+
if (
|
|
752
|
+
allowSameNetMerges &&
|
|
753
|
+
obstacleSharesElectricalNet(srj, obstacle, plan.connectionName)
|
|
754
|
+
) {
|
|
755
|
+
continue
|
|
756
|
+
}
|
|
724
757
|
if (
|
|
725
758
|
distancePointToObstacle(plan.via.center, obstacle) <
|
|
726
759
|
plan.via.diameter / 2 + clearance - 1e-9
|
|
@@ -730,10 +763,55 @@ function planIsClear(params: {
|
|
|
730
763
|
}
|
|
731
764
|
}
|
|
732
765
|
|
|
766
|
+
return true
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function planIsClearOfPlans(params: {
|
|
770
|
+
plan: FanoutRoutePlan
|
|
771
|
+
otherPlans: FanoutRoutePlan[]
|
|
772
|
+
srj: SimpleRouteJson
|
|
773
|
+
allowSameNetMerges: boolean
|
|
774
|
+
clearance: number
|
|
775
|
+
blockingBusCounts?: Map<string, number>
|
|
776
|
+
}): boolean {
|
|
777
|
+
const {
|
|
778
|
+
plan,
|
|
779
|
+
otherPlans,
|
|
780
|
+
srj,
|
|
781
|
+
allowSameNetMerges,
|
|
782
|
+
clearance,
|
|
783
|
+
blockingBusCounts,
|
|
784
|
+
} = params
|
|
733
785
|
for (const otherPlan of otherPlans) {
|
|
786
|
+
if (
|
|
787
|
+
allowSameNetMerges &&
|
|
788
|
+
connectionsShareElectricalNet(
|
|
789
|
+
srj,
|
|
790
|
+
plan.connectionName,
|
|
791
|
+
otherPlan.connectionName,
|
|
792
|
+
)
|
|
793
|
+
) {
|
|
794
|
+
continue
|
|
795
|
+
}
|
|
796
|
+
const plansShareSourcePort =
|
|
797
|
+
(plan.sourcePoint.pcb_port_id &&
|
|
798
|
+
plan.sourcePoint.pcb_port_id === otherPlan.sourcePoint.pcb_port_id) ||
|
|
799
|
+
(plan.sourcePoint.pointId &&
|
|
800
|
+
plan.sourcePoint.pointId === otherPlan.sourcePoint.pointId)
|
|
801
|
+
if (plansShareSourcePort) continue
|
|
802
|
+
const recordBlocker = (): void => {
|
|
803
|
+
if (otherPlan.busId === plan.busId) return
|
|
804
|
+
blockingBusCounts?.set(
|
|
805
|
+
otherPlan.busId,
|
|
806
|
+
(blockingBusCounts.get(otherPlan.busId) ?? 0) + 1,
|
|
807
|
+
)
|
|
808
|
+
}
|
|
734
809
|
for (const segment of plan.segments) {
|
|
735
810
|
for (const otherSegment of otherPlan.segments) {
|
|
736
|
-
if (!segmentsAreClear(segment, otherSegment, clearance))
|
|
811
|
+
if (!segmentsAreClear(segment, otherSegment, clearance)) {
|
|
812
|
+
recordBlocker()
|
|
813
|
+
return false
|
|
814
|
+
}
|
|
737
815
|
}
|
|
738
816
|
if (
|
|
739
817
|
otherPlan.via?.spanLayers.includes(segment.layer) &&
|
|
@@ -744,6 +822,7 @@ function planIsClear(params: {
|
|
|
744
822
|
) <
|
|
745
823
|
otherPlan.via.diameter / 2 + segment.width / 2 + clearance - 1e-9
|
|
746
824
|
) {
|
|
825
|
+
recordBlocker()
|
|
747
826
|
return false
|
|
748
827
|
}
|
|
749
828
|
}
|
|
@@ -758,6 +837,7 @@ function planIsClear(params: {
|
|
|
758
837
|
) <
|
|
759
838
|
plan.via.diameter / 2 + otherSegment.width / 2 + clearance - 1e-9
|
|
760
839
|
) {
|
|
840
|
+
recordBlocker()
|
|
761
841
|
return false
|
|
762
842
|
}
|
|
763
843
|
}
|
|
@@ -769,6 +849,7 @@ function planIsClear(params: {
|
|
|
769
849
|
distance(plan.via.center, otherPlan.via.center) <
|
|
770
850
|
(plan.via.diameter + otherPlan.via.diameter) / 2 + clearance - 1e-9
|
|
771
851
|
) {
|
|
852
|
+
recordBlocker()
|
|
772
853
|
return false
|
|
773
854
|
}
|
|
774
855
|
}
|
|
@@ -776,6 +857,99 @@ function planIsClear(params: {
|
|
|
776
857
|
return true
|
|
777
858
|
}
|
|
778
859
|
|
|
860
|
+
function planIsClear(params: {
|
|
861
|
+
plan: FanoutRoutePlan
|
|
862
|
+
otherPlans: FanoutRoutePlan[]
|
|
863
|
+
staticClearanceCache?: RouteBusStaticClearanceCache
|
|
864
|
+
blockingBusCounts?: Map<string, number>
|
|
865
|
+
cacheKey: string
|
|
866
|
+
srj: SimpleRouteJson
|
|
867
|
+
sharedBoundary: Bounds
|
|
868
|
+
clearance: number
|
|
869
|
+
allowSameNetMerges: boolean
|
|
870
|
+
}): boolean {
|
|
871
|
+
const {
|
|
872
|
+
plan,
|
|
873
|
+
otherPlans,
|
|
874
|
+
staticClearanceCache,
|
|
875
|
+
blockingBusCounts,
|
|
876
|
+
cacheKey,
|
|
877
|
+
srj,
|
|
878
|
+
sharedBoundary,
|
|
879
|
+
clearance,
|
|
880
|
+
allowSameNetMerges,
|
|
881
|
+
} = params
|
|
882
|
+
let staticallyClear = staticClearanceCache?.get(cacheKey)
|
|
883
|
+
if (staticallyClear === undefined) {
|
|
884
|
+
staticallyClear = planIsStaticallyClear({
|
|
885
|
+
plan,
|
|
886
|
+
srj,
|
|
887
|
+
sharedBoundary,
|
|
888
|
+
clearance,
|
|
889
|
+
allowSameNetMerges,
|
|
890
|
+
})
|
|
891
|
+
staticClearanceCache?.set(cacheKey, staticallyClear)
|
|
892
|
+
}
|
|
893
|
+
return (
|
|
894
|
+
staticallyClear &&
|
|
895
|
+
planIsClearOfPlans({
|
|
896
|
+
plan,
|
|
897
|
+
otherPlans,
|
|
898
|
+
srj,
|
|
899
|
+
allowSameNetMerges,
|
|
900
|
+
clearance,
|
|
901
|
+
blockingBusCounts,
|
|
902
|
+
})
|
|
903
|
+
)
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Validate a complete set of fanout plans against the source SRJ and against
|
|
908
|
+
* one another. This is intentionally separate from route search so every
|
|
909
|
+
* route-producing strategy can share the same final safety invariant.
|
|
910
|
+
*/
|
|
911
|
+
export function fanoutPlansAreClear(params: {
|
|
912
|
+
plans: readonly FanoutRoutePlan[]
|
|
913
|
+
srj: SimpleRouteJson
|
|
914
|
+
sharedBoundary: Bounds
|
|
915
|
+
clearance: number
|
|
916
|
+
allowSameNetMerges?: boolean
|
|
917
|
+
}): boolean {
|
|
918
|
+
const {
|
|
919
|
+
plans,
|
|
920
|
+
srj,
|
|
921
|
+
sharedBoundary,
|
|
922
|
+
clearance,
|
|
923
|
+
allowSameNetMerges = false,
|
|
924
|
+
} = params
|
|
925
|
+
for (let index = 0; index < plans.length; index++) {
|
|
926
|
+
const plan = plans[index]!
|
|
927
|
+
if (
|
|
928
|
+
!planIsStaticallyClear({
|
|
929
|
+
plan,
|
|
930
|
+
srj,
|
|
931
|
+
sharedBoundary,
|
|
932
|
+
clearance,
|
|
933
|
+
allowSameNetMerges,
|
|
934
|
+
})
|
|
935
|
+
) {
|
|
936
|
+
return false
|
|
937
|
+
}
|
|
938
|
+
if (
|
|
939
|
+
!planIsClearOfPlans({
|
|
940
|
+
plan,
|
|
941
|
+
otherPlans: plans.filter((_, otherIndex) => otherIndex !== index),
|
|
942
|
+
srj,
|
|
943
|
+
allowSameNetMerges,
|
|
944
|
+
clearance,
|
|
945
|
+
})
|
|
946
|
+
) {
|
|
947
|
+
return false
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
return true
|
|
951
|
+
}
|
|
952
|
+
|
|
779
953
|
function routePlaneTerminatedBus(
|
|
780
954
|
params: RouteBusParams,
|
|
781
955
|
): FanoutRoutePlan[] | null {
|
|
@@ -789,6 +963,9 @@ function routePlaneTerminatedBus(
|
|
|
789
963
|
viaDiameter,
|
|
790
964
|
viaHoleDiameter,
|
|
791
965
|
clearance,
|
|
966
|
+
staticClearanceCache,
|
|
967
|
+
blockingBusCounts,
|
|
968
|
+
allowSameNetMerges = false,
|
|
792
969
|
} = params
|
|
793
970
|
const sourceObstacle = bus.connections[0]?.sourceObstacle
|
|
794
971
|
if (!sourceObstacle || bus.termination.type !== "plane") return null
|
|
@@ -833,9 +1010,13 @@ function routePlaneTerminatedBus(
|
|
|
833
1010
|
!planIsClear({
|
|
834
1011
|
plan,
|
|
835
1012
|
otherPlans: [...acceptedPlans, ...candidatePlans],
|
|
1013
|
+
staticClearanceCache,
|
|
1014
|
+
blockingBusCounts,
|
|
1015
|
+
cacheKey: `plane:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${viaHandedness}`,
|
|
836
1016
|
srj,
|
|
837
1017
|
sharedBoundary: bus.sharedBoundary,
|
|
838
1018
|
clearance,
|
|
1019
|
+
allowSameNetMerges,
|
|
839
1020
|
})
|
|
840
1021
|
) {
|
|
841
1022
|
orderIsClear = false
|
|
@@ -850,7 +1031,10 @@ function routePlaneTerminatedBus(
|
|
|
850
1031
|
return null
|
|
851
1032
|
}
|
|
852
1033
|
|
|
853
|
-
export function
|
|
1034
|
+
export function routeBusAlternatives(
|
|
1035
|
+
params: RouteBusParams,
|
|
1036
|
+
maxAlternatives = 1,
|
|
1037
|
+
): FanoutRoutePlan[][] {
|
|
854
1038
|
const {
|
|
855
1039
|
srj,
|
|
856
1040
|
bus,
|
|
@@ -862,13 +1046,22 @@ export function routeBus(params: RouteBusParams): FanoutRoutePlan[] | null {
|
|
|
862
1046
|
viaHoleDiameter,
|
|
863
1047
|
clearance,
|
|
864
1048
|
compactBusTracks,
|
|
1049
|
+
staticClearanceCache,
|
|
1050
|
+
blockingBusCounts,
|
|
1051
|
+
allowSameNetMerges = false,
|
|
865
1052
|
} = params
|
|
1053
|
+
if (!Number.isInteger(maxAlternatives) || maxAlternatives < 1) {
|
|
1054
|
+
throw new Error(
|
|
1055
|
+
`FanoutSolver: maxAlternatives must be a positive integer, received ${maxAlternatives}`,
|
|
1056
|
+
)
|
|
1057
|
+
}
|
|
866
1058
|
if (bus.termination.type === "plane") {
|
|
867
|
-
|
|
1059
|
+
const plan = routePlaneTerminatedBus(params)
|
|
1060
|
+
return plan ? [plan] : []
|
|
868
1061
|
}
|
|
869
1062
|
const exitAxis = getExitAxis(bus)
|
|
870
1063
|
const sourceObstacle = bus.connections[0]?.sourceObstacle
|
|
871
|
-
if (!sourceObstacle) return []
|
|
1064
|
+
if (!sourceObstacle) return [[]]
|
|
872
1065
|
const directionalPadSize = isHorizontal(bus.direction)
|
|
873
1066
|
? sourceObstacle.width
|
|
874
1067
|
: sourceObstacle.height
|
|
@@ -886,71 +1079,112 @@ export function routeBus(params: RouteBusParams): FanoutRoutePlan[] | null {
|
|
|
886
1079
|
: [1, -1]
|
|
887
1080
|
: [0]
|
|
888
1081
|
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
1082
|
+
const alternatives: FanoutRoutePlan[][] = []
|
|
1083
|
+
const seenAlternativeKeys = new Set<string>()
|
|
1084
|
+
|
|
1085
|
+
const addAlternative = (plans: FanoutRoutePlan[]): void => {
|
|
1086
|
+
const key = plans
|
|
1087
|
+
.map(
|
|
1088
|
+
(plan) =>
|
|
1089
|
+
`${plan.connectionIndex}:${plan.targetLayer}:${plan.exitPoint.x}:${plan.exitPoint.y}:${plan.segments.map((segment) => `${segment.start.x},${segment.start.y},${segment.end.x},${segment.end.y},${segment.layer}`).join(";")}`,
|
|
1090
|
+
)
|
|
1091
|
+
.join("|")
|
|
1092
|
+
if (seenAlternativeKeys.has(key)) return
|
|
1093
|
+
seenAlternativeKeys.add(key)
|
|
1094
|
+
alternatives.push(plans)
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const searchConnectionOrder = (
|
|
1098
|
+
connectionOrder: PreparedConnection[],
|
|
1099
|
+
viaHandedness: ViaHandedness,
|
|
1100
|
+
connectionIndex: number,
|
|
1101
|
+
candidatePlans: FanoutRoutePlan[],
|
|
1102
|
+
): void => {
|
|
1103
|
+
if (alternatives.length >= maxAlternatives) return
|
|
1104
|
+
if (connectionIndex >= connectionOrder.length) {
|
|
1105
|
+
addAlternative(candidatePlans)
|
|
1106
|
+
return
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const preparedConnection = connectionOrder[connectionIndex]!
|
|
1110
|
+
const connectionRank = getConnectionRank(bus, preparedConnection)
|
|
1111
|
+
const trackCandidates = getTrackCandidates({
|
|
1112
|
+
bus,
|
|
1113
|
+
connection: preparedConnection,
|
|
1114
|
+
preferredTrack: getPreferredTrack({
|
|
1115
|
+
bus,
|
|
1116
|
+
connection: preparedConnection,
|
|
1117
|
+
targetUsesVia,
|
|
1118
|
+
interstitialEscape,
|
|
1119
|
+
compactBusTracks,
|
|
1120
|
+
traceWidth,
|
|
1121
|
+
viaDiameter,
|
|
1122
|
+
clearance,
|
|
1123
|
+
}),
|
|
1124
|
+
traceWidth,
|
|
1125
|
+
clearance,
|
|
1126
|
+
})
|
|
1127
|
+
for (
|
|
1128
|
+
let trackIndex = 0;
|
|
1129
|
+
trackIndex < trackCandidates.length;
|
|
1130
|
+
trackIndex++
|
|
1131
|
+
) {
|
|
1132
|
+
const track = trackCandidates[trackIndex]!
|
|
1133
|
+
const plan = buildPlan({
|
|
1134
|
+
preparedConnection,
|
|
1135
|
+
bus,
|
|
1136
|
+
targetLayer,
|
|
1137
|
+
track: track.value,
|
|
1138
|
+
exitAxis,
|
|
1139
|
+
layerNames,
|
|
1140
|
+
traceWidth,
|
|
1141
|
+
viaDiameter,
|
|
1142
|
+
viaHoleDiameter,
|
|
1143
|
+
viaHandedness,
|
|
1144
|
+
interstitialEscape,
|
|
1145
|
+
spreadLaneIndex: Math.min(
|
|
1146
|
+
connectionRank,
|
|
1147
|
+
bus.connections.length - connectionRank - 1,
|
|
1148
|
+
),
|
|
1149
|
+
clearance,
|
|
1150
|
+
terminateAtVia: false,
|
|
1151
|
+
})
|
|
1152
|
+
if (
|
|
1153
|
+
!planIsClear({
|
|
1154
|
+
plan,
|
|
1155
|
+
otherPlans: [...acceptedPlans, ...candidatePlans],
|
|
1156
|
+
staticClearanceCache,
|
|
1157
|
+
blockingBusCounts,
|
|
1158
|
+
cacheKey: `boundary:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${viaHandedness}:${trackIndex}`,
|
|
1159
|
+
srj,
|
|
1160
|
+
sharedBoundary: bus.sharedBoundary,
|
|
909
1161
|
clearance,
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
targetLayer,
|
|
915
|
-
track: track.value,
|
|
916
|
-
exitAxis,
|
|
917
|
-
layerNames,
|
|
918
|
-
traceWidth,
|
|
919
|
-
viaDiameter,
|
|
920
|
-
viaHoleDiameter,
|
|
921
|
-
viaHandedness,
|
|
922
|
-
interstitialEscape,
|
|
923
|
-
spreadLaneIndex: Math.min(
|
|
924
|
-
getConnectionRank(bus, preparedConnection),
|
|
925
|
-
bus.connections.length -
|
|
926
|
-
getConnectionRank(bus, preparedConnection) -
|
|
927
|
-
1,
|
|
928
|
-
),
|
|
929
|
-
clearance,
|
|
930
|
-
terminateAtVia: false,
|
|
931
|
-
})
|
|
932
|
-
if (
|
|
933
|
-
planIsClear({
|
|
934
|
-
plan,
|
|
935
|
-
otherPlans: [...acceptedPlans, ...candidatePlans],
|
|
936
|
-
srj,
|
|
937
|
-
sharedBoundary: bus.sharedBoundary,
|
|
938
|
-
clearance,
|
|
939
|
-
})
|
|
940
|
-
) {
|
|
941
|
-
acceptedPlan = plan
|
|
942
|
-
break
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
if (!acceptedPlan) {
|
|
946
|
-
orderIsClear = false
|
|
947
|
-
break
|
|
948
|
-
}
|
|
949
|
-
candidatePlans.push(acceptedPlan)
|
|
1162
|
+
allowSameNetMerges,
|
|
1163
|
+
})
|
|
1164
|
+
) {
|
|
1165
|
+
continue
|
|
950
1166
|
}
|
|
951
|
-
|
|
1167
|
+
searchConnectionOrder(
|
|
1168
|
+
connectionOrder,
|
|
1169
|
+
viaHandedness,
|
|
1170
|
+
connectionIndex + 1,
|
|
1171
|
+
[...candidatePlans, plan],
|
|
1172
|
+
)
|
|
1173
|
+
if (alternatives.length >= maxAlternatives) return
|
|
1174
|
+
if (maxAlternatives === 1) return
|
|
952
1175
|
}
|
|
953
1176
|
}
|
|
954
1177
|
|
|
955
|
-
|
|
1178
|
+
for (const viaHandedness of viaHandednesses) {
|
|
1179
|
+
for (const connectionOrder of getConnectionOrders(bus)) {
|
|
1180
|
+
searchConnectionOrder(connectionOrder, viaHandedness, 0, [])
|
|
1181
|
+
if (alternatives.length >= maxAlternatives) return alternatives
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
return alternatives
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
export function routeBus(params: RouteBusParams): FanoutRoutePlan[] | null {
|
|
1189
|
+
return routeBusAlternatives(params, 1)[0] ?? null
|
|
956
1190
|
}
|
package/lib/types.ts
CHANGED
|
@@ -98,6 +98,8 @@ export interface FanoutSolverOptions {
|
|
|
98
98
|
viaHoleDiameter?: number
|
|
99
99
|
clearance?: number
|
|
100
100
|
compactBusTracks?: boolean
|
|
101
|
+
/** Allow branches belonging to the same electrical net to share copper. */
|
|
102
|
+
allowSameNetMerges?: boolean
|
|
101
103
|
singleLayerPushAndShove?: boolean
|
|
102
104
|
/**
|
|
103
105
|
* When preferred single-layer exits cannot coexist, allow a global
|
|
@@ -115,6 +117,7 @@ export interface FanoutAttemptSummary {
|
|
|
115
117
|
routedConnectionCount: number
|
|
116
118
|
failedBusIds: string[]
|
|
117
119
|
score: number
|
|
120
|
+
validationIssues?: FanoutValidationIssue[]
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
export interface FanoutSolverOutput {
|
|
@@ -124,6 +127,42 @@ export interface FanoutSolverOutput {
|
|
|
124
127
|
busLayerAssignments: Readonly<Record<string, string>>
|
|
125
128
|
busDirections: Readonly<Record<string, FanoutDirection>>
|
|
126
129
|
attempts: FanoutAttemptSummary[]
|
|
130
|
+
validation: FanoutValidationReport
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface FanoutValidationIssue {
|
|
134
|
+
code:
|
|
135
|
+
| "missing-plan"
|
|
136
|
+
| "duplicate-plan"
|
|
137
|
+
| "unknown-plan"
|
|
138
|
+
| "connection-mismatch"
|
|
139
|
+
| "source-mismatch"
|
|
140
|
+
| "termination-mismatch"
|
|
141
|
+
| "not-broken-out"
|
|
142
|
+
| "outside-routing-bounds"
|
|
143
|
+
| "disconnected-trace"
|
|
144
|
+
| "unsupported-route-point"
|
|
145
|
+
| "trace-plan-mismatch"
|
|
146
|
+
| "output-connection-missing"
|
|
147
|
+
| "output-exit-mismatch"
|
|
148
|
+
| "downstream-endpoint-lost"
|
|
149
|
+
| "plane-connection-retained"
|
|
150
|
+
| "obstacle-clearance"
|
|
151
|
+
| "via-obstacle-clearance"
|
|
152
|
+
| "different-net-trace-clearance"
|
|
153
|
+
| "different-net-trace-via-clearance"
|
|
154
|
+
| "different-net-via-clearance"
|
|
155
|
+
message: string
|
|
156
|
+
connectionName?: string
|
|
157
|
+
otherConnectionName?: string
|
|
158
|
+
busId?: string
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export interface FanoutValidationReport {
|
|
162
|
+
valid: boolean
|
|
163
|
+
checkedConnectionCount: number
|
|
164
|
+
brokenOutConnectionCount: number
|
|
165
|
+
issues: FanoutValidationIssue[]
|
|
127
166
|
}
|
|
128
167
|
|
|
129
168
|
export interface Point2D {
|