@tscircuit/fanout-solver 0.0.37 → 0.0.39

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/types.ts CHANGED
@@ -9,6 +9,24 @@ import type {
9
9
  import type { OriginalEndpointConnectivityReport } from "./validate-original-endpoint-connectivity"
10
10
  import type { RoutedCopperDrcReport } from "./validate-routed-copper-drc"
11
11
 
12
+ type CapacityRoutePoint = SimplifiedPcbTrace["route"][number]
13
+
14
+ export type FanoutViaRoutePoint = Extract<
15
+ CapacityRoutePoint,
16
+ { route_type: "via" }
17
+ > & {
18
+ /** Physical copper layers occupied by the via barrel. */
19
+ layers?: string[]
20
+ }
21
+
22
+ export type FanoutRoutePoint =
23
+ | Exclude<CapacityRoutePoint, { route_type: "via" }>
24
+ | FanoutViaRoutePoint
25
+
26
+ export type FanoutSimplifiedPcbTrace = Omit<SimplifiedPcbTrace, "route"> & {
27
+ route: FanoutRoutePoint[]
28
+ }
29
+
12
30
  export type FanoutDirection = "left" | "right" | "up" | "down"
13
31
 
14
32
  export type FanoutEdge = "left" | "right" | "top" | "bottom"
@@ -171,6 +189,12 @@ export interface FanoutSolverOptions {
171
189
  viaHoleDiameter?: number
172
190
  clearance?: number
173
191
  compactBusTracks?: boolean
192
+ /**
193
+ * Permit vias whose physical barrel only spans their logical route-layer
194
+ * transition. Defaults to true for compatibility. Set false for boards that
195
+ * manufacture every routed via through the complete copper stack.
196
+ */
197
+ allowBlindAndBuriedVias?: boolean
174
198
  /** Allow branches belonging to the same electrical net to share copper. */
175
199
  allowSameNetMerges?: boolean
176
200
  singleLayerPushAndShove?: boolean
@@ -207,9 +231,9 @@ export interface FanoutAttemptSummary {
207
231
  }
208
232
 
209
233
  export interface FanoutSolverOutput {
210
- simpleRouteJson: SimpleRouteJson
211
- fanoutTraces: SimplifiedPcbTrace[]
212
- completionTraces: SimplifiedPcbTrace[]
234
+ simpleRouteJson: SimpleRouteJsonWithFanoutPlanes
235
+ fanoutTraces: FanoutSimplifiedPcbTrace[]
236
+ completionTraces: FanoutSimplifiedPcbTrace[]
213
237
  endpointCompletion?: FanoutEndpointCompletionReport
214
238
  planeTerminations: FanoutPlaneTermination[]
215
239
  busLayerAssignments: Readonly<Record<string, string>>
@@ -250,6 +274,8 @@ export interface FanoutValidationIssue {
250
274
  | "different-net-trace-clearance"
251
275
  | "different-net-trace-via-clearance"
252
276
  | "different-net-via-clearance"
277
+ | "plan-length-mismatch"
278
+ | "bus-length-skew"
253
279
  message: string
254
280
  connectionName?: string
255
281
  otherConnectionName?: string
@@ -291,6 +317,8 @@ export interface PreparedConnection {
291
317
 
292
318
  export interface PreparedBus {
293
319
  busId: string
320
+ /** Maximum permitted routed copper length difference in millimeters. */
321
+ maxLengthSkew?: number
294
322
  direction: FanoutDirection
295
323
  preferredExit?: FanoutBorderTarget
296
324
  /** Explicit final boundary edge. Omitted for legacy direction-based exits. */
@@ -347,13 +375,13 @@ export interface FanoutRoutePlan {
347
375
  /** Lower/left or upper/right band reserved along `exitEdge`. */
348
376
  cornerBandSide?: "minimum" | "maximum"
349
377
  exitPoint: Point2D
350
- trace: SimplifiedPcbTrace
378
+ trace: FanoutSimplifiedPcbTrace
351
379
  segments: RoutedSegment[]
352
380
  via?: RoutedVia
353
381
  /** Additional layer transitions used by an explicit winding channel. */
354
382
  additionalVias?: RoutedVia[]
355
383
  /** Optional capacitor-side dogbone reserved and emitted with a plane escape. */
356
- planeEndpointTrace?: SimplifiedPcbTrace
384
+ planeEndpointTrace?: FanoutSimplifiedPcbTrace
357
385
  planeEndpointSegments?: RoutedSegment[]
358
386
  planeEndpointVia?: RoutedVia
359
387
  length: number
@@ -376,12 +404,16 @@ export interface FanoutPlaneConnectivity {
376
404
  layer: string
377
405
  }
378
406
 
379
- export type SimpleRouteJsonWithFanoutPlanes = SimpleRouteJson & {
407
+ export type SimpleRouteJsonWithFanoutPlanes = Omit<
408
+ SimpleRouteJson,
409
+ "traces"
410
+ > & {
411
+ traces?: FanoutSimplifiedPcbTrace[]
380
412
  fanoutPlaneConnectivity?: FanoutPlaneConnectivity[]
381
413
  }
382
414
 
383
415
  export interface AssignmentAttempt {
384
416
  summary: FanoutAttemptSummary
385
417
  plans: FanoutRoutePlan[]
386
- outputSrj: SimpleRouteJson
418
+ outputSrj: SimpleRouteJsonWithFanoutPlanes
387
419
  }
@@ -288,7 +288,17 @@ function validatePlanStructure(params: {
288
288
  plan,
289
289
  )
290
290
  }
291
- if (plan.segments.length === 0 || plan.length <= EPSILON) {
291
+ const isAllowedViaInPadPlaneTermination =
292
+ (inputSrj as SimpleRouteJson & { allowViaInPad?: boolean })
293
+ .allowViaInPad === true &&
294
+ plan.termination.type === "plane" &&
295
+ plan.via !== undefined &&
296
+ pointsMatch(plan.via.center, plan.sourcePoint) &&
297
+ pointsMatch(plan.exitPoint, plan.sourcePoint)
298
+ if (
299
+ !isAllowedViaInPadPlaneTermination &&
300
+ (plan.segments.length === 0 || plan.length <= EPSILON)
301
+ ) {
292
302
  addIssue(
293
303
  issues,
294
304
  "not-broken-out",
@@ -316,7 +326,10 @@ function validatePlanStructure(params: {
316
326
  plan,
317
327
  )
318
328
  }
319
- if (!pointsMatch(plan.segments[0]!.start, plan.sourcePoint)) {
329
+ if (
330
+ plan.segments[0] &&
331
+ !pointsMatch(plan.segments[0].start, plan.sourcePoint)
332
+ ) {
320
333
  addIssue(
321
334
  issues,
322
335
  "disconnected-trace",
@@ -324,7 +337,10 @@ function validatePlanStructure(params: {
324
337
  plan,
325
338
  )
326
339
  }
327
- if (!pointsMatch(plan.segments.at(-1)!.end, plan.exitPoint)) {
340
+ if (
341
+ plan.segments.at(-1) &&
342
+ !pointsMatch(plan.segments.at(-1)!.end, plan.exitPoint)
343
+ ) {
328
344
  addIssue(
329
345
  issues,
330
346
  "disconnected-trace",
@@ -360,6 +376,19 @@ function validatePlanStructure(params: {
360
376
  }
361
377
  }
362
378
 
379
+ const measuredLength = [
380
+ ...plan.segments,
381
+ ...(plan.planeEndpointSegments ?? []),
382
+ ].reduce((total, segment) => total + distance(segment.start, segment.end), 0)
383
+ if (Math.abs(measuredLength - plan.length) > 1e-6) {
384
+ addIssue(
385
+ issues,
386
+ "plan-length-mismatch",
387
+ `Plan ${plan.connectionName} declares ${plan.length.toFixed(6)}mm but contains ${measuredLength.toFixed(6)}mm of routed copper`,
388
+ plan,
389
+ )
390
+ }
391
+
363
392
  const traceSegments = extractTraceSegments({
364
393
  trace: plan.trace,
365
394
  plan,
@@ -654,9 +683,10 @@ function validateClearances(params: {
654
683
  plans: readonly FanoutRoutePlan[]
655
684
  inputSrj: SimpleRouteJson
656
685
  clearance: number
686
+ allowBlindAndBuriedVias: boolean
657
687
  issues: FanoutValidationIssue[]
658
688
  }): void {
659
- const { plans, inputSrj, clearance, issues } = params
689
+ const { plans, inputSrj, clearance, allowBlindAndBuriedVias, issues } = params
660
690
  for (const plan of plans) {
661
691
  const segments = getPlanSegments(plan)
662
692
  for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex++) {
@@ -718,7 +748,10 @@ function validateClearances(params: {
718
748
  }
719
749
  }
720
750
 
721
- for (const traceCopper of getAllRoutedTraceCopper(inputSrj)) {
751
+ for (const traceCopper of getAllRoutedTraceCopper(
752
+ inputSrj,
753
+ allowBlindAndBuriedVias,
754
+ )) {
722
755
  if (
723
756
  plan.connectionName === traceCopper.connectionName ||
724
757
  connectionsShareElectricalNet(
@@ -904,6 +937,7 @@ export function validateFanoutSolution(params: {
904
937
  preparedBuses: readonly PreparedBus[]
905
938
  sharedBoundary: Bounds
906
939
  clearance: number
940
+ allowBlindAndBuriedVias?: boolean
907
941
  }): FanoutValidationReport {
908
942
  const {
909
943
  inputSrj,
@@ -912,6 +946,7 @@ export function validateFanoutSolution(params: {
912
946
  preparedBuses,
913
947
  sharedBoundary,
914
948
  clearance,
949
+ allowBlindAndBuriedVias = true,
915
950
  } = params
916
951
  const issues: FanoutValidationIssue[] = []
917
952
  const plansByConnection = new Map<string, FanoutRoutePlan[]>()
@@ -921,6 +956,21 @@ export function validateFanoutSolution(params: {
921
956
  connectionPlans.push(plan)
922
957
  plansByConnection.set(plan.connectionName, connectionPlans)
923
958
  }
959
+ for (const bus of preparedBuses) {
960
+ if (bus.maxLengthSkew === undefined) continue
961
+ const busPlans = plans.filter((plan) => plan.busId === bus.busId)
962
+ if (busPlans.length < 2) continue
963
+ const lengths = busPlans.map((plan) => plan.length)
964
+ const skew = Math.max(...lengths) - Math.min(...lengths)
965
+ if (skew > bus.maxLengthSkew + 1e-6) {
966
+ addIssue(
967
+ issues,
968
+ "bus-length-skew",
969
+ `Bus ${bus.busId} has ${skew.toFixed(6)}mm routed-length skew; ${bus.maxLengthSkew.toFixed(6)}mm is allowed`,
970
+ busPlans[0],
971
+ )
972
+ }
973
+ }
924
974
 
925
975
  for (const connection of inputSrj.connections) {
926
976
  const connectionPlans = plansByConnection.get(connection.name) ?? []
@@ -968,7 +1018,13 @@ export function validateFanoutSolution(params: {
968
1018
  sharedBoundary,
969
1019
  issues,
970
1020
  })
971
- validateClearances({ plans, inputSrj, clearance, issues })
1021
+ validateClearances({
1022
+ plans,
1023
+ inputSrj,
1024
+ clearance,
1025
+ allowBlindAndBuriedVias,
1026
+ issues,
1027
+ })
972
1028
 
973
1029
  return {
974
1030
  valid: issues.length === 0,
@@ -3,6 +3,7 @@ import type {
3
3
  SimplifiedPcbTrace,
4
4
  } from "@tscircuit/capacity-autorouter"
5
5
  import {
6
+ circleFitsInsideObstacle,
6
7
  distance,
7
8
  distancePointToObstacle,
8
9
  distancePointToSegment,
@@ -10,7 +11,7 @@ import {
10
11
  pointIsInsideObstacle,
11
12
  segmentsAreClear,
12
13
  } from "./geometry"
13
- import { getCopperLayerNames, getLayerSpan } from "./layer-names"
14
+ import { getCopperLayerNames, getRouteViaSpanLayers } from "./layer-names"
14
15
  import {
15
16
  connectionsShareElectricalNet,
16
17
  obstacleSharesElectricalNet,
@@ -120,9 +121,17 @@ function extractTraceCopper(params: {
120
121
  trace: SimplifiedPcbTrace
121
122
  connectionName: string
122
123
  layerNames: string[]
124
+ allowBlindAndBuriedVias: boolean
123
125
  issues: RoutedCopperDrcIssue[]
124
126
  }): TraceCopper {
125
- const { srj, trace, connectionName, layerNames, issues } = params
127
+ const {
128
+ srj,
129
+ trace,
130
+ connectionName,
131
+ layerNames,
132
+ allowBlindAndBuriedVias,
133
+ issues,
134
+ } = params
126
135
  const segments: RoutedSegment[] = []
127
136
  const vias: RoutedVia[] = []
128
137
  let previousWire:
@@ -134,11 +143,16 @@ function extractTraceCopper(params: {
134
143
 
135
144
  for (const routePoint of trace.route) {
136
145
  if (routePoint.route_type === "via") {
137
- const spanLayers = getLayerSpan(
138
- routePoint.from_layer,
139
- routePoint.to_layer,
146
+ const spanLayers = getRouteViaSpanLayers({
147
+ fromLayer: routePoint.from_layer,
148
+ toLayer: routePoint.to_layer,
149
+ layers:
150
+ "layers" in routePoint && Array.isArray(routePoint.layers)
151
+ ? (routePoint.layers as string[])
152
+ : undefined,
140
153
  layerNames,
141
- )
154
+ allowBlindAndBuriedVias,
155
+ })
142
156
  vias.push({
143
157
  center: { x: routePoint.x, y: routePoint.y },
144
158
  diameter:
@@ -231,8 +245,14 @@ export function validateRoutedCopperDrc(params: {
231
245
  inputSrj: SimpleRouteJson
232
246
  routedSrj: SimpleRouteJson
233
247
  clearance: number
248
+ allowBlindAndBuriedVias?: boolean
234
249
  }): RoutedCopperDrcReport {
235
- const { inputSrj, routedSrj, clearance } = params
250
+ const {
251
+ inputSrj,
252
+ routedSrj,
253
+ clearance,
254
+ allowBlindAndBuriedVias = true,
255
+ } = params
236
256
  const issues: RoutedCopperDrcIssue[] = []
237
257
  const layerNames = getCopperLayerNames(routedSrj.layerCount)
238
258
  const traceCopper: TraceCopper[] = []
@@ -267,6 +287,7 @@ export function validateRoutedCopperDrc(params: {
267
287
  trace,
268
288
  connectionName,
269
289
  layerNames,
290
+ allowBlindAndBuriedVias,
270
291
  issues,
271
292
  }),
272
293
  )
@@ -309,7 +330,25 @@ export function validateRoutedCopperDrc(params: {
309
330
  const coincidentEndpoint = originalAndRoutedEndpoints.find(
310
331
  ({ point }) => distance(via.center, point) <= EPSILON,
311
332
  )
312
- if (coincidentEndpoint) {
333
+ const isAllowedContainedViaInPad =
334
+ (inputSrj as SimpleRouteJson & { allowViaInPad?: boolean })
335
+ .allowViaInPad === true &&
336
+ inputSrj.obstacles.some(
337
+ (obstacle) =>
338
+ obstacle.layers.some((layer) => via.spanLayers.includes(layer)) &&
339
+ obstacleSharesElectricalNet(
340
+ inputSrj,
341
+ obstacle,
342
+ copper.connectionName,
343
+ ) &&
344
+ circleFitsInsideObstacle({
345
+ center: via.center,
346
+ diameter: via.diameter,
347
+ obstacle,
348
+ tolerance: EPSILON,
349
+ }),
350
+ )
351
+ if (coincidentEndpoint && !isAllowedContainedViaInPad) {
313
352
  addIssue(issues, {
314
353
  code: "via-at-endpoint",
315
354
  traceId: copper.trace.pcb_trace_id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tscircuit/fanout-solver",
3
- "version": "0.0.37",
3
+ "version": "0.0.39",
4
4
  "description": "BGA fanout solver with coordinated bus-layer escapes for SimpleRouteJson",
5
5
  "module": "lib/index.ts",
6
6
  "type": "module",