@tscircuit/fanout-solver 0.0.38 → 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/geometry.ts CHANGED
@@ -98,6 +98,27 @@ export function pointIsInsideObstacle(
98
98
  )
99
99
  }
100
100
 
101
+ export function circleFitsInsideObstacle(params: {
102
+ center: Point2D
103
+ diameter: number
104
+ obstacle: Obstacle
105
+ tolerance?: number
106
+ }): boolean {
107
+ const { center, diameter, obstacle, tolerance = EPSILON } = params
108
+ const radius = diameter / 2
109
+ if (obstacleIsCircular(obstacle)) {
110
+ return (
111
+ distance(center, obstacle.center) + radius <=
112
+ obstacle.width / 2 + tolerance
113
+ )
114
+ }
115
+ const localCenter = toObstacleLocalPoint(center, obstacle)
116
+ return (
117
+ Math.abs(localCenter.x) + radius <= obstacle.width / 2 + tolerance &&
118
+ Math.abs(localCenter.y) + radius <= obstacle.height / 2 + tolerance
119
+ )
120
+ }
121
+
101
122
  export function distancePointToObstacle(
102
123
  point: Point2D,
103
124
  obstacle: Obstacle,
@@ -2,7 +2,7 @@ import type {
2
2
  SimpleRouteJson,
3
3
  SimplifiedPcbTrace,
4
4
  } from "@tscircuit/capacity-autorouter"
5
- import { getCopperLayerNames, getLayerSpan } from "./layer-names"
5
+ import { getCopperLayerNames, getRouteViaSpanLayers } from "./layer-names"
6
6
  import type { Point2D, RoutedSegment } from "./types"
7
7
 
8
8
  export interface RoutedTraceVia {
@@ -21,6 +21,7 @@ export interface RoutedTraceCopper {
21
21
  export function getRoutedTraceCopper(
22
22
  srj: SimpleRouteJson,
23
23
  trace: SimplifiedPcbTrace,
24
+ allowBlindAndBuriedVias = true,
24
25
  ): RoutedTraceCopper {
25
26
  const layerNames = getCopperLayerNames(srj.layerCount)
26
27
  const segments: RoutedSegment[] = []
@@ -39,11 +40,16 @@ export function getRoutedTraceCopper(
39
40
  srj.min_via_pad_diameter ??
40
41
  srj.minViaDiameter ??
41
42
  srj.minTraceWidth,
42
- spanLayers: getLayerSpan(
43
- routePoint.from_layer,
44
- routePoint.to_layer,
43
+ spanLayers: getRouteViaSpanLayers({
44
+ fromLayer: routePoint.from_layer,
45
+ toLayer: routePoint.to_layer,
46
+ layers:
47
+ "layers" in routePoint && Array.isArray(routePoint.layers)
48
+ ? (routePoint.layers as string[])
49
+ : undefined,
45
50
  layerNames,
46
- ),
51
+ allowBlindAndBuriedVias,
52
+ }),
47
53
  })
48
54
  previousWire = undefined
49
55
  continue
@@ -78,6 +84,9 @@ export function getRoutedTraceCopper(
78
84
 
79
85
  export function getAllRoutedTraceCopper(
80
86
  srj: SimpleRouteJson,
87
+ allowBlindAndBuriedVias = true,
81
88
  ): RoutedTraceCopper[] {
82
- return (srj.traces ?? []).map((trace) => getRoutedTraceCopper(srj, trace))
89
+ return (srj.traces ?? []).map((trace) =>
90
+ getRoutedTraceCopper(srj, trace, allowBlindAndBuriedVias),
91
+ )
83
92
  }
package/lib/index.ts CHANGED
@@ -24,10 +24,13 @@ export type {
24
24
  FanoutPlaneConnectivity,
25
25
  FanoutPlaneTermination,
26
26
  FanoutRoutePlan,
27
+ FanoutRoutePoint,
28
+ FanoutSimplifiedPcbTrace,
27
29
  FanoutSolverOptions,
28
30
  FanoutSolverOutput,
29
31
  FanoutValidationIssue,
30
32
  FanoutValidationReport,
33
+ FanoutViaRoutePoint,
31
34
  Point2D,
32
35
  PreparedBus,
33
36
  SimpleRouteJsonWithFanoutPlanes,
@@ -34,6 +34,46 @@ export function getLayerSpan(
34
34
  return layerNames.slice(firstIndex, lastIndex + 1)
35
35
  }
36
36
 
37
+ export function getViaSpanLayers(params: {
38
+ fromLayer: string
39
+ toLayer: string
40
+ layerNames: string[]
41
+ allowBlindAndBuriedVias: boolean
42
+ }): string[] {
43
+ const { fromLayer, toLayer, layerNames, allowBlindAndBuriedVias } = params
44
+ const logicalSpan = getLayerSpan(fromLayer, toLayer, layerNames)
45
+ return allowBlindAndBuriedVias ? logicalSpan : [...layerNames]
46
+ }
47
+
48
+ export function getRouteViaSpanLayers(params: {
49
+ fromLayer: string
50
+ toLayer: string
51
+ layers?: readonly string[]
52
+ layerNames: string[]
53
+ allowBlindAndBuriedVias: boolean
54
+ }): string[] {
55
+ const { fromLayer, toLayer, layers, layerNames, allowBlindAndBuriedVias } =
56
+ params
57
+ const configuredSpan = getViaSpanLayers({
58
+ fromLayer,
59
+ toLayer,
60
+ layerNames,
61
+ allowBlindAndBuriedVias,
62
+ })
63
+ if (!layers) return configuredSpan
64
+ if (
65
+ layers.length === 0 ||
66
+ !layers.includes(fromLayer) ||
67
+ !layers.includes(toLayer) ||
68
+ layers.some((layer) => !layerNames.includes(layer))
69
+ ) {
70
+ throw new Error(
71
+ `FanoutSolver: route via from "${fromLayer}" to "${toLayer}" has an invalid physical layer span`,
72
+ )
73
+ }
74
+ return [...layers]
75
+ }
76
+
37
77
  export function generateLayerAssignments(params: {
38
78
  busIds: string[]
39
79
  layers: string[]
@@ -42,21 +42,46 @@ function rebuildTraceRoute(
42
42
  const lastOriginalWire = plan.trace.route.findLast(
43
43
  (point) => point.route_type === "wire",
44
44
  )
45
+ const getWireMetadata = (
46
+ wire: typeof firstOriginalWire,
47
+ ): Partial<
48
+ Extract<SimplifiedPcbTrace["route"][number], { route_type: "wire" }>
49
+ > => {
50
+ if (wire?.route_type !== "wire") return {}
51
+ const metadata: Partial<
52
+ Extract<SimplifiedPcbTrace["route"][number], { route_type: "wire" }>
53
+ > = { ...wire }
54
+ delete metadata.route_type
55
+ delete metadata.x
56
+ delete metadata.y
57
+ delete metadata.width
58
+ delete metadata.layer
59
+ return metadata
60
+ }
61
+ const vias = getPlanVias(plan)
62
+ const startsWithSourceVia =
63
+ pointsMatch(firstSegment.start, plan.sourcePoint) &&
64
+ firstSegment.layer !== plan.sourceLayer &&
65
+ vias.some(
66
+ (via) =>
67
+ pointsMatch(via.center, firstSegment.start) &&
68
+ via.spanLayers.includes(plan.sourceLayer) &&
69
+ via.spanLayers.includes(firstSegment.layer),
70
+ )
71
+ const initialLayer = startsWithSourceVia
72
+ ? plan.sourceLayer
73
+ : firstSegment.layer
45
74
  const route: SimplifiedPcbTrace["route"] = [
46
75
  {
76
+ ...getWireMetadata(firstOriginalWire),
47
77
  route_type: "wire",
48
78
  ...firstSegment.start,
49
79
  width: firstSegment.width,
50
- layer: firstSegment.layer,
51
- ...(firstOriginalWire?.route_type === "wire" &&
52
- firstOriginalWire.start_pcb_port_id
53
- ? { start_pcb_port_id: firstOriginalWire.start_pcb_port_id }
54
- : {}),
80
+ layer: initialLayer,
55
81
  },
56
82
  ]
57
83
  let currentPoint = firstSegment.start
58
- let currentLayer = firstSegment.layer
59
- const vias = getPlanVias(plan)
84
+ let currentLayer = initialLayer
60
85
 
61
86
  for (const [segmentIndex, segment] of segments.entries()) {
62
87
  if (!pointsMatch(currentPoint, segment.start)) return null
@@ -85,15 +110,13 @@ function rebuildTraceRoute(
85
110
  currentLayer = segment.layer
86
111
  }
87
112
  route.push({
113
+ ...(segmentIndex === segments.length - 1
114
+ ? getWireMetadata(lastOriginalWire)
115
+ : {}),
88
116
  route_type: "wire",
89
117
  ...segment.end,
90
118
  width: segment.width,
91
119
  layer: segment.layer,
92
- ...(segmentIndex === segments.length - 1 &&
93
- lastOriginalWire?.route_type === "wire" &&
94
- lastOriginalWire.end_pcb_port_id
95
- ? { end_pcb_port_id: lastOriginalWire.end_pcb_port_id }
96
- : {}),
97
120
  })
98
121
  currentPoint = segment.end
99
122
  }
@@ -131,6 +154,67 @@ function pointIsOutsideDenseBounds(
131
154
  )
132
155
  }
133
156
 
157
+ function splitSegmentAtDenseBounds(params: {
158
+ segment: RoutedSegment
159
+ bounds: Bounds
160
+ margin: number
161
+ }): RoutedSegment[] {
162
+ const { segment, bounds, margin } = params
163
+ const expandedBounds = {
164
+ minX: bounds.minX - margin,
165
+ maxX: bounds.maxX + margin,
166
+ minY: bounds.minY - margin,
167
+ maxY: bounds.maxY + margin,
168
+ }
169
+ const deltaX = segment.end.x - segment.start.x
170
+ const deltaY = segment.end.y - segment.start.y
171
+ const splitParameters = [0, 1]
172
+ const addSplitParameter = (parameter: number): void => {
173
+ if (parameter <= EPSILON || parameter >= 1 - EPSILON) return
174
+ const point = {
175
+ x: segment.start.x + deltaX * parameter,
176
+ y: segment.start.y + deltaY * parameter,
177
+ }
178
+ if (
179
+ point.x < expandedBounds.minX - EPSILON ||
180
+ point.x > expandedBounds.maxX + EPSILON ||
181
+ point.y < expandedBounds.minY - EPSILON ||
182
+ point.y > expandedBounds.maxY + EPSILON
183
+ ) {
184
+ return
185
+ }
186
+ splitParameters.push(parameter)
187
+ }
188
+ if (Math.abs(deltaX) > EPSILON) {
189
+ addSplitParameter((expandedBounds.minX - segment.start.x) / deltaX)
190
+ addSplitParameter((expandedBounds.maxX - segment.start.x) / deltaX)
191
+ }
192
+ if (Math.abs(deltaY) > EPSILON) {
193
+ addSplitParameter((expandedBounds.minY - segment.start.y) / deltaY)
194
+ addSplitParameter((expandedBounds.maxY - segment.start.y) / deltaY)
195
+ }
196
+ const parameters = splitParameters
197
+ .toSorted((first, second) => first - second)
198
+ .filter(
199
+ (parameter, index, values) =>
200
+ index === 0 || Math.abs(parameter - values[index - 1]!) > EPSILON,
201
+ )
202
+ return parameters.slice(1).map((endParameter, index) => {
203
+ const startParameter = parameters[index]!
204
+ return {
205
+ ...segment,
206
+ start: {
207
+ x: segment.start.x + deltaX * startParameter,
208
+ y: segment.start.y + deltaY * startParameter,
209
+ },
210
+ end: {
211
+ x: segment.start.x + deltaX * endParameter,
212
+ y: segment.start.y + deltaY * endParameter,
213
+ },
214
+ }
215
+ })
216
+ }
217
+
134
218
  function getDenseCopperBounds(bus: PreparedBus): Bounds {
135
219
  return bus.componentObstacles.reduce<Bounds>(
136
220
  (bounds, obstacle) => ({
@@ -364,8 +448,16 @@ function createTunedPlanCandidates(params: {
364
448
  targetAddedLength: number
365
449
  clearance: number
366
450
  sharedBoundary: Bounds
451
+ denseBoundarySplitApplied?: boolean
367
452
  }): FanoutRoutePlan[] {
368
- const { plan, bus, targetAddedLength, clearance, sharedBoundary } = params
453
+ const {
454
+ plan,
455
+ bus,
456
+ targetAddedLength,
457
+ clearance,
458
+ sharedBoundary,
459
+ denseBoundarySplitApplied = false,
460
+ } = params
369
461
  const candidates: FanoutRoutePlan[] = []
370
462
  const denseCopperBounds = getDenseCopperBounds(bus)
371
463
  const denseMargin = plan.segments[0]?.width
@@ -447,7 +539,22 @@ function createTunedPlanCandidates(params: {
447
539
  }
448
540
  }
449
541
  }
450
- return candidates
542
+ if (denseBoundarySplitApplied) return candidates
543
+ const splitSegments = plan.segments.flatMap((segment) =>
544
+ splitSegmentAtDenseBounds({
545
+ segment,
546
+ bounds: denseCopperBounds,
547
+ margin: denseMargin,
548
+ }),
549
+ )
550
+ const splitPlan = createPlanWithSegments(plan, splitSegments)
551
+ if (!splitPlan) return candidates
552
+ const splitCandidates = createTunedPlanCandidates({
553
+ ...params,
554
+ plan: splitPlan,
555
+ denseBoundarySplitApplied: true,
556
+ })
557
+ return [...candidates, ...splitCandidates]
451
558
  }
452
559
 
453
560
  function getBusSkew(plans: readonly FanoutRoutePlan[]): number {
@@ -466,6 +573,7 @@ export function matchBusPlanLengths(params: {
466
573
  inputSrj: SimpleRouteJson
467
574
  sharedBoundary: Bounds
468
575
  clearance: number
576
+ allowBlindAndBuriedVias?: boolean
469
577
  allowSameNetMerges?: boolean
470
578
  }):
471
579
  | { plans: FanoutRoutePlan[]; failedBus?: never }
@@ -475,6 +583,7 @@ export function matchBusPlanLengths(params: {
475
583
  inputSrj,
476
584
  sharedBoundary,
477
585
  clearance,
586
+ allowBlindAndBuriedVias = true,
478
587
  allowSameNetMerges = false,
479
588
  } = params
480
589
  let matchedPlans = [...params.plans]
@@ -547,6 +656,7 @@ export function matchBusPlanLengths(params: {
547
656
  srj: inputSrj,
548
657
  sharedBoundary,
549
658
  clearance,
659
+ allowBlindAndBuriedVias,
550
660
  allowSameNetMerges,
551
661
  })
552
662
  ) {