@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/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.segments.some(
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
- for (let index = 0; index < plan.segments.length; index++) {
944
+ const segments = getPlanSegments(plan)
945
+ for (let index = 0; index < segments.length; index++) {
730
946
  if (
731
947
  !segmentIsClearOfObstacles({
732
- segment: plan.segments[index]!,
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
- if (plan.via) {
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(plan.via.center, obstacle) <
759
- plan.via.diameter / 2 + clearance - 1e-9
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
- for (const segment of plan.segments) {
810
- for (const otherSegment of otherPlan.segments) {
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
- if (
817
- otherPlan.via?.spanLayers.includes(segment.layer) &&
818
- distancePointToSegment(
819
- otherPlan.via.center,
820
- segment.start,
821
- segment.end,
822
- ) <
823
- otherPlan.via.diameter / 2 + segment.width / 2 + clearance - 1e-9
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
- if (plan.via) {
830
- for (const otherSegment of otherPlan.segments) {
1045
+ for (const planVia of planVias) {
1046
+ for (const otherSegment of otherSegments) {
831
1047
  if (
832
- plan.via.spanLayers.includes(otherSegment.layer) &&
1048
+ planVia.spanLayers.includes(otherSegment.layer) &&
833
1049
  distancePointToSegment(
834
- plan.via.center,
1050
+ planVia.center,
835
1051
  otherSegment.start,
836
1052
  otherSegment.end,
837
1053
  ) <
838
- plan.via.diameter / 2 + otherSegment.width / 2 + clearance - 1e-9
1054
+ planVia.diameter / 2 + otherSegment.width / 2 + clearance - 1e-9
839
1055
  ) {
840
1056
  recordBlocker()
841
1057
  return false
842
1058
  }
843
1059
  }
844
- if (
845
- otherPlan.via &&
846
- plan.via.spanLayers.some((layer) =>
847
- otherPlan.via!.spanLayers.includes(layer),
848
- ) &&
849
- distance(plan.via.center, otherPlan.via.center) <
850
- (plan.via.diameter + otherPlan.via.diameter) / 2 + clearance - 1e-9
851
- ) {
852
- recordBlocker()
853
- return false
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
- for (const viaHandedness of viaHandednesses) {
985
- for (const connectionOrder of getConnectionOrders(bus)) {
986
- const candidatePlans: FanoutRoutePlan[] = []
987
- let orderIsClear = true
988
- for (const preparedConnection of connectionOrder) {
989
- const sourceTrack = getPerpendicularAxis(
990
- preparedConnection.sourcePoint,
991
- bus.direction,
992
- )
993
- const plan = buildPlan({
994
- preparedConnection,
995
- bus,
996
- targetLayer,
997
- track: sourceTrack,
998
- exitAxis: getExitAxis(bus),
999
- layerNames,
1000
- traceWidth,
1001
- viaDiameter,
1002
- viaHoleDiameter,
1003
- viaHandedness,
1004
- interstitialEscape: !pairChannelFitsVia,
1005
- spreadLaneIndex: 0,
1006
- clearance,
1007
- terminateAtVia: true,
1008
- })
1009
- if (
1010
- !planIsClear({
1011
- plan,
1012
- otherPlans: [...acceptedPlans, ...candidatePlans],
1013
- staticClearanceCache,
1014
- blockingBusCounts,
1015
- cacheKey: `plane:${bus.busId}:${targetLayer}:${preparedConnection.connectionIndex}:${viaHandedness}`,
1016
- srj,
1017
- sharedBoundary: bus.sharedBoundary,
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
- allowSameNetMerges,
1234
+ terminateAtVia: true,
1020
1235
  })
1021
- ) {
1022
- orderIsClear = false
1023
- break
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
- candidatePlans.push(plan)
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[]