@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/README.md +23 -2
- package/lib/add-via-layer-metadata.ts +48 -0
- package/lib/complete-original-endpoints.ts +30 -2
- package/lib/fanout-solver.ts +721 -33
- 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 +681 -0
- package/lib/match-component-dogbone-via-sites.ts +584 -0
- package/lib/prepare-buses.ts +26 -0
- package/lib/route-bus.ts +872 -112
- package/lib/route-via-minimal-winding.ts +390 -74
- package/lib/types.ts +39 -7
- package/lib/validate-fanout-solution.ts +62 -6
- package/lib/validate-routed-copper-drc.ts +47 -8
- package/package.json +1 -1
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:
|
|
211
|
-
fanoutTraces:
|
|
212
|
-
completionTraces:
|
|
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:
|
|
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?:
|
|
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 =
|
|
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:
|
|
418
|
+
outputSrj: SimpleRouteJsonWithFanoutPlanes
|
|
387
419
|
}
|
|
@@ -288,7 +288,17 @@ function validatePlanStructure(params: {
|
|
|
288
288
|
plan,
|
|
289
289
|
)
|
|
290
290
|
}
|
|
291
|
-
|
|
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 (
|
|
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 (
|
|
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(
|
|
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({
|
|
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,
|
|
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 {
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
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,
|