@tscircuit/fanout-solver 0.0.18 → 0.0.20

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
@@ -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,
@@ -34,6 +38,7 @@ export interface RouteBusParams {
34
38
  viaHoleDiameter: number
35
39
  clearance: number
36
40
  compactBusTracks: boolean
41
+ allowSameNetMerges?: boolean
37
42
  staticClearanceCache?: RouteBusStaticClearanceCache
38
43
  blockingBusCounts?: Map<string, number>
39
44
  }
@@ -662,13 +667,29 @@ function segmentIsClearOfObstacles(params: {
662
667
  segment: RoutedSegment
663
668
  plan: FanoutRoutePlan
664
669
  segmentIndex: number
670
+ srj: SimpleRouteJson
671
+ allowSameNetMerges: boolean
665
672
  obstacles: Obstacle[]
666
673
  clearance: number
667
674
  }): boolean {
668
- const { segment, plan, segmentIndex, obstacles, clearance } = params
675
+ const {
676
+ segment,
677
+ plan,
678
+ segmentIndex,
679
+ srj,
680
+ allowSameNetMerges,
681
+ obstacles,
682
+ clearance,
683
+ } = params
669
684
  for (const obstacle of obstacles) {
670
685
  if (!obstacle.layers.includes(segment.layer)) continue
671
686
  if (obstacle.connectedTo.includes(plan.connectionName)) continue
687
+ if (
688
+ allowSameNetMerges &&
689
+ obstacleSharesElectricalNet(srj, obstacle, plan.connectionName)
690
+ ) {
691
+ continue
692
+ }
672
693
  if (
673
694
  segmentIndex === 0 &&
674
695
  obstacle === plan.sourceObstacle &&
@@ -691,8 +712,9 @@ function planIsStaticallyClear(params: {
691
712
  srj: SimpleRouteJson
692
713
  sharedBoundary: Bounds
693
714
  clearance: number
715
+ allowSameNetMerges: boolean
694
716
  }): boolean {
695
- const { plan, srj, sharedBoundary, clearance } = params
717
+ const { plan, srj, sharedBoundary, clearance, allowSameNetMerges } = params
696
718
  const routableBounds = getRoutableBounds(srj.bounds, sharedBoundary)
697
719
  if (
698
720
  !pointIsInsideBounds(plan.exitPoint, routableBounds) ||
@@ -710,6 +732,8 @@ function planIsStaticallyClear(params: {
710
732
  segment: plan.segments[index]!,
711
733
  plan,
712
734
  segmentIndex: index,
735
+ srj,
736
+ allowSameNetMerges,
713
737
  obstacles: srj.obstacles,
714
738
  clearance,
715
739
  })
@@ -724,6 +748,12 @@ function planIsStaticallyClear(params: {
724
748
  ) {
725
749
  continue
726
750
  }
751
+ if (
752
+ allowSameNetMerges &&
753
+ obstacleSharesElectricalNet(srj, obstacle, plan.connectionName)
754
+ ) {
755
+ continue
756
+ }
727
757
  if (
728
758
  distancePointToObstacle(plan.via.center, obstacle) <
729
759
  plan.via.diameter / 2 + clearance - 1e-9
@@ -739,11 +769,30 @@ function planIsStaticallyClear(params: {
739
769
  function planIsClearOfPlans(params: {
740
770
  plan: FanoutRoutePlan
741
771
  otherPlans: FanoutRoutePlan[]
772
+ srj: SimpleRouteJson
773
+ allowSameNetMerges: boolean
742
774
  clearance: number
743
775
  blockingBusCounts?: Map<string, number>
744
776
  }): boolean {
745
- const { plan, otherPlans, clearance, blockingBusCounts } = params
777
+ const {
778
+ plan,
779
+ otherPlans,
780
+ srj,
781
+ allowSameNetMerges,
782
+ clearance,
783
+ blockingBusCounts,
784
+ } = params
746
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
+ }
747
796
  const plansShareSourcePort =
748
797
  (plan.sourcePoint.pcb_port_id &&
749
798
  plan.sourcePoint.pcb_port_id === otherPlan.sourcePoint.pcb_port_id) ||
@@ -817,6 +866,7 @@ function planIsClear(params: {
817
866
  srj: SimpleRouteJson
818
867
  sharedBoundary: Bounds
819
868
  clearance: number
869
+ allowSameNetMerges: boolean
820
870
  }): boolean {
821
871
  const {
822
872
  plan,
@@ -827,6 +877,7 @@ function planIsClear(params: {
827
877
  srj,
828
878
  sharedBoundary,
829
879
  clearance,
880
+ allowSameNetMerges,
830
881
  } = params
831
882
  let staticallyClear = staticClearanceCache?.get(cacheKey)
832
883
  if (staticallyClear === undefined) {
@@ -835,6 +886,7 @@ function planIsClear(params: {
835
886
  srj,
836
887
  sharedBoundary,
837
888
  clearance,
889
+ allowSameNetMerges,
838
890
  })
839
891
  staticClearanceCache?.set(cacheKey, staticallyClear)
840
892
  }
@@ -843,12 +895,61 @@ function planIsClear(params: {
843
895
  planIsClearOfPlans({
844
896
  plan,
845
897
  otherPlans,
898
+ srj,
899
+ allowSameNetMerges,
846
900
  clearance,
847
901
  blockingBusCounts,
848
902
  })
849
903
  )
850
904
  }
851
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
+
852
953
  function routePlaneTerminatedBus(
853
954
  params: RouteBusParams,
854
955
  ): FanoutRoutePlan[] | null {
@@ -864,6 +965,7 @@ function routePlaneTerminatedBus(
864
965
  clearance,
865
966
  staticClearanceCache,
866
967
  blockingBusCounts,
968
+ allowSameNetMerges = false,
867
969
  } = params
868
970
  const sourceObstacle = bus.connections[0]?.sourceObstacle
869
971
  if (!sourceObstacle || bus.termination.type !== "plane") return null
@@ -914,6 +1016,7 @@ function routePlaneTerminatedBus(
914
1016
  srj,
915
1017
  sharedBoundary: bus.sharedBoundary,
916
1018
  clearance,
1019
+ allowSameNetMerges,
917
1020
  })
918
1021
  ) {
919
1022
  orderIsClear = false
@@ -945,6 +1048,7 @@ export function routeBusAlternatives(
945
1048
  compactBusTracks,
946
1049
  staticClearanceCache,
947
1050
  blockingBusCounts,
1051
+ allowSameNetMerges = false,
948
1052
  } = params
949
1053
  if (!Number.isInteger(maxAlternatives) || maxAlternatives < 1) {
950
1054
  throw new Error(
@@ -1055,6 +1159,7 @@ export function routeBusAlternatives(
1055
1159
  srj,
1056
1160
  sharedBoundary: bus.sharedBoundary,
1057
1161
  clearance,
1162
+ allowSameNetMerges,
1058
1163
  })
1059
1164
  ) {
1060
1165
  continue
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 {