@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/README.md +7 -0
- package/lib/add-via-layer-metadata.ts +48 -0
- package/lib/complete-original-endpoints.ts +30 -2
- package/lib/fanout-solver.ts +636 -14
- package/lib/geometry.ts +21 -0
- package/lib/get-routed-trace-copper.ts +15 -6
- package/lib/index.ts +3 -0
- package/lib/layer-names.ts +40 -0
- package/lib/match-bus-lengths.ts +124 -14
- package/lib/match-component-dogbone-via-sites.ts +584 -0
- package/lib/route-bus.ts +872 -112
- package/lib/route-via-minimal-winding.ts +390 -74
- package/lib/types.ts +35 -7
- package/lib/validate-fanout-solution.ts +34 -6
- package/lib/validate-routed-copper-drc.ts +47 -8
- package/package.json +1 -1
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,
|
|
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:
|
|
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) =>
|
|
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,
|
package/lib/layer-names.ts
CHANGED
|
@@ -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[]
|
package/lib/match-bus-lengths.ts
CHANGED
|
@@ -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:
|
|
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 =
|
|
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 {
|
|
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
|
) {
|