@tscircuit/fanout-solver 0.0.36 → 0.0.38
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 +16 -2
- package/lib/build-output.ts +32 -10
- package/lib/fanout-solver.ts +294 -44
- package/lib/match-bus-lengths.ts +571 -0
- package/lib/prepare-buses.ts +32 -2
- package/lib/route-bus.ts +378 -81
- package/lib/route-via-minimal-winding.ts +898 -0
- package/lib/types.ts +18 -3
- package/lib/validate-fanout-solution.ts +40 -10
- package/package.json +1 -1
package/lib/types.ts
CHANGED
|
@@ -124,9 +124,14 @@ export interface FanoutBusSpec extends SimpleRouteBus {
|
|
|
124
124
|
* connection's electrical endpoint in SimpleRouteJson.
|
|
125
125
|
*
|
|
126
126
|
* A caller coordinating two fanouts can pass the paired fanout's selected
|
|
127
|
-
* exit here so both sides aim toward the same track.
|
|
127
|
+
* exit here so both sides aim toward the same track. When every target also
|
|
128
|
+
* includes a layer, the solver may use the bus's other allowed layer as an
|
|
129
|
+
* internal crossover channel, then return every exit to one common bus layer
|
|
130
|
+
* in the supplied target order.
|
|
128
131
|
*/
|
|
129
|
-
connectionExitTargets?: Readonly<
|
|
132
|
+
connectionExitTargets?: Readonly<
|
|
133
|
+
Record<string, Point2D & { readonly layer?: string }>
|
|
134
|
+
>
|
|
130
135
|
/**
|
|
131
136
|
* Defaults to `{ type: "boundary" }`.
|
|
132
137
|
*
|
|
@@ -245,6 +250,8 @@ export interface FanoutValidationIssue {
|
|
|
245
250
|
| "different-net-trace-clearance"
|
|
246
251
|
| "different-net-trace-via-clearance"
|
|
247
252
|
| "different-net-via-clearance"
|
|
253
|
+
| "plan-length-mismatch"
|
|
254
|
+
| "bus-length-skew"
|
|
248
255
|
message: string
|
|
249
256
|
connectionName?: string
|
|
250
257
|
otherConnectionName?: string
|
|
@@ -279,11 +286,15 @@ export interface PreparedConnection {
|
|
|
279
286
|
sourceObstacle: Obstacle
|
|
280
287
|
targetPoint: ConnectionPoint
|
|
281
288
|
/** Preferred downstream point used to choose the boundary exit track. */
|
|
282
|
-
exitTargetPoint?: Point2D
|
|
289
|
+
exitTargetPoint?: Point2D & { readonly layer?: string }
|
|
290
|
+
/** Whether the caller supplied the layered handoff metadata needed to coordinate winding. */
|
|
291
|
+
hasExplicitLayeredExitTarget?: boolean
|
|
283
292
|
}
|
|
284
293
|
|
|
285
294
|
export interface PreparedBus {
|
|
286
295
|
busId: string
|
|
296
|
+
/** Maximum permitted routed copper length difference in millimeters. */
|
|
297
|
+
maxLengthSkew?: number
|
|
287
298
|
direction: FanoutDirection
|
|
288
299
|
preferredExit?: FanoutBorderTarget
|
|
289
300
|
/** Explicit final boundary edge. Omitted for legacy direction-based exits. */
|
|
@@ -292,6 +303,8 @@ export interface PreparedBus {
|
|
|
292
303
|
cornerBandConnectionCount?: number
|
|
293
304
|
/** Layers to which this bus is allowed to escape. */
|
|
294
305
|
allowedLayers?: readonly string[]
|
|
306
|
+
/** Bus layers that also survive the solver-wide escape-layer restriction. */
|
|
307
|
+
routableEscapeLayers?: readonly string[]
|
|
295
308
|
termination: FanoutBusTermination
|
|
296
309
|
connections: PreparedConnection[]
|
|
297
310
|
componentId: string
|
|
@@ -341,6 +354,8 @@ export interface FanoutRoutePlan {
|
|
|
341
354
|
trace: SimplifiedPcbTrace
|
|
342
355
|
segments: RoutedSegment[]
|
|
343
356
|
via?: RoutedVia
|
|
357
|
+
/** Additional layer transitions used by an explicit winding channel. */
|
|
358
|
+
additionalVias?: RoutedVia[]
|
|
344
359
|
/** Optional capacitor-side dogbone reserved and emitted with a plane escape. */
|
|
345
360
|
planeEndpointTrace?: SimplifiedPcbTrace
|
|
346
361
|
planeEndpointSegments?: RoutedSegment[]
|
|
@@ -44,9 +44,11 @@ function getPlanSegments(plan: FanoutRoutePlan): RoutedSegment[] {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
function getPlanVias(plan: FanoutRoutePlan) {
|
|
47
|
-
return [
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
return [
|
|
48
|
+
plan.via,
|
|
49
|
+
...(plan.additionalVias ?? []),
|
|
50
|
+
plan.planeEndpointVia,
|
|
51
|
+
].filter((via): via is NonNullable<FanoutRoutePlan["via"]> => Boolean(via))
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
function connectionPointsMatch(
|
|
@@ -341,13 +343,13 @@ function validatePlanStructure(params: {
|
|
|
341
343
|
plan,
|
|
342
344
|
)
|
|
343
345
|
}
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
) {
|
|
346
|
+
const transitionVia = getPlanVias(plan).find(
|
|
347
|
+
(via) =>
|
|
348
|
+
pointsMatch(previous.end, via.center) &&
|
|
349
|
+
via.spanLayers.includes(previous.layer) &&
|
|
350
|
+
via.spanLayers.includes(current.layer),
|
|
351
|
+
)
|
|
352
|
+
if (previous.layer !== current.layer && !transitionVia) {
|
|
351
353
|
addIssue(
|
|
352
354
|
issues,
|
|
353
355
|
"disconnected-trace",
|
|
@@ -358,6 +360,19 @@ function validatePlanStructure(params: {
|
|
|
358
360
|
}
|
|
359
361
|
}
|
|
360
362
|
|
|
363
|
+
const measuredLength = [
|
|
364
|
+
...plan.segments,
|
|
365
|
+
...(plan.planeEndpointSegments ?? []),
|
|
366
|
+
].reduce((total, segment) => total + distance(segment.start, segment.end), 0)
|
|
367
|
+
if (Math.abs(measuredLength - plan.length) > 1e-6) {
|
|
368
|
+
addIssue(
|
|
369
|
+
issues,
|
|
370
|
+
"plan-length-mismatch",
|
|
371
|
+
`Plan ${plan.connectionName} declares ${plan.length.toFixed(6)}mm but contains ${measuredLength.toFixed(6)}mm of routed copper`,
|
|
372
|
+
plan,
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
|
|
361
376
|
const traceSegments = extractTraceSegments({
|
|
362
377
|
trace: plan.trace,
|
|
363
378
|
plan,
|
|
@@ -919,6 +934,21 @@ export function validateFanoutSolution(params: {
|
|
|
919
934
|
connectionPlans.push(plan)
|
|
920
935
|
plansByConnection.set(plan.connectionName, connectionPlans)
|
|
921
936
|
}
|
|
937
|
+
for (const bus of preparedBuses) {
|
|
938
|
+
if (bus.maxLengthSkew === undefined) continue
|
|
939
|
+
const busPlans = plans.filter((plan) => plan.busId === bus.busId)
|
|
940
|
+
if (busPlans.length < 2) continue
|
|
941
|
+
const lengths = busPlans.map((plan) => plan.length)
|
|
942
|
+
const skew = Math.max(...lengths) - Math.min(...lengths)
|
|
943
|
+
if (skew > bus.maxLengthSkew + 1e-6) {
|
|
944
|
+
addIssue(
|
|
945
|
+
issues,
|
|
946
|
+
"bus-length-skew",
|
|
947
|
+
`Bus ${bus.busId} has ${skew.toFixed(6)}mm routed-length skew; ${bus.maxLengthSkew.toFixed(6)}mm is allowed`,
|
|
948
|
+
busPlans[0],
|
|
949
|
+
)
|
|
950
|
+
}
|
|
951
|
+
}
|
|
922
952
|
|
|
923
953
|
for (const connection of inputSrj.connections) {
|
|
924
954
|
const connectionPlans = plansByConnection.get(connection.name) ?? []
|