From 5109fcc9b3ba325802445f2314ce056aca8674d2 Mon Sep 17 00:00:00 2001 From: seveibar Date: Tue, 28 Jul 2026 12:37:50 -0700 Subject: [PATCH] feat: apply fanout boundary padding --- .../primitive-components/Group/Group.ts | 1 + .../Group/GroupRoutingPhasePlan.ts | 1 + .../Group/Group_getRoutingPhasePlans.ts | 7 + lib/utils/autorouting/FanoutAutorouter.ts | 26 +- .../autorouting/get-fanout-shared-boundary.ts | 98 +++++++ .../autorouting/localAutorouterStrategies.ts | 4 +- package.json | 2 +- ...6-decoupling-caps-autorouting-srj.snap.svg | 244 +++++++++--------- ...reakout-bga36-decoupling-caps-pcb.snap.svg | 2 +- .../breakout-bga36-decoupling-caps.test.tsx | 60 ++++- ...autorouter-fanout-autorouting-srj.snap.svg | 42 +-- .../autorouter-fanout-pcb.snap.svg | 2 +- tests/features/autorouter-fanout.test.tsx | 32 +++ .../get-fanout-shared-boundary.test.ts | 85 ++++++ 14 files changed, 456 insertions(+), 150 deletions(-) create mode 100644 lib/utils/autorouting/get-fanout-shared-boundary.ts create mode 100644 tests/utils/autorouting/get-fanout-shared-boundary.test.ts diff --git a/lib/components/primitive-components/Group/Group.ts b/lib/components/primitive-components/Group/Group.ts index ad2aacbc9..4e5b3bd5c 100644 --- a/lib/components/primitive-components/Group/Group.ts +++ b/lib/components/primitive-components/Group/Group.ts @@ -1169,6 +1169,7 @@ export class Group = typeof groupProps> simpleRouteJson, commonAutorouterOptions, busFanoutDirections: routingPhasePlan.busFanoutDirections, + fanoutBoundaryPadding: routingPhasePlan.fanoutBoundaryPadding, }) } diff --git a/lib/components/primitive-components/Group/GroupRoutingPhasePlan.ts b/lib/components/primitive-components/Group/GroupRoutingPhasePlan.ts index 6e9fc1c25..534103c98 100644 --- a/lib/components/primitive-components/Group/GroupRoutingPhasePlan.ts +++ b/lib/components/primitive-components/Group/GroupRoutingPhasePlan.ts @@ -21,6 +21,7 @@ export interface RoutingPhasePlan { region?: AutoroutingPhaseProps["region"] connectionSelectors?: string[] busFanoutDirections?: AutoroutingPhaseProps["busFanoutDirections"] + fanoutBoundaryPadding?: AutoroutingPhaseProps["fanoutBoundaryPadding"] drcTolerances?: RoutingPhaseDrcTolerances nets: Net[] traces: Trace[] diff --git a/lib/components/primitive-components/Group/Group_getRoutingPhasePlans.ts b/lib/components/primitive-components/Group/Group_getRoutingPhasePlans.ts index f0cb47ae1..466750fea 100644 --- a/lib/components/primitive-components/Group/Group_getRoutingPhasePlans.ts +++ b/lib/components/primitive-components/Group/Group_getRoutingPhasePlans.ts @@ -190,6 +190,11 @@ export function Group_getRoutingPhasePlans( const plansByPhaseIndex = new Map() const autoroutersByPhaseIndex = getAutoroutersByPhaseIndex(group) const phasePropsByPhaseIndex = getAutoroutingPhasePropsByPhaseIndex(group) + const groupFanoutBoundaryPadding = ( + group._parsedProps as { + fanoutBoundaryPadding?: AutoroutingPhaseProps["fanoutBoundaryPadding"] + } + ).fanoutBoundaryPadding const hasDirectRoutingTargets = traces.length > 0 || nets.length > 0 const hasReroutePhase = Array.from(phasePropsByPhaseIndex.values()).some( (phaseProps) => phaseProps.reroute, @@ -270,6 +275,8 @@ export function Group_getRoutingPhasePlans( ? getConnectionSelectorsFromAutoroutingPhaseProps(phaseProps) : undefined plan.busFanoutDirections = phaseProps?.busFanoutDirections + plan.fanoutBoundaryPadding = + phaseProps?.fanoutBoundaryPadding ?? groupFanoutBoundaryPadding plan.drcTolerances = phaseProps ? getDrcTolerancesFromAutoroutingPhaseProps(phaseProps) : undefined diff --git a/lib/utils/autorouting/FanoutAutorouter.ts b/lib/utils/autorouting/FanoutAutorouter.ts index f19343c53..b774ccd76 100644 --- a/lib/utils/autorouting/FanoutAutorouter.ts +++ b/lib/utils/autorouting/FanoutAutorouter.ts @@ -3,7 +3,11 @@ import { type FanoutBorderTarget, type FanoutSolverOptions, } from "@tscircuit/fanout-solver" -import type { BusFanoutDirection, NinePointAnchor } from "@tscircuit/props" +import type { + BusFanoutDirection, + FanoutBoundaryPadding, + NinePointAnchor, +} from "@tscircuit/props" import { getViaBoardLayers } from "../getViaSpanLayers" import type { AutorouterCompleteEvent, @@ -13,12 +17,14 @@ import type { GenericLocalAutorouter, } from "./GenericLocalAutorouter" import type { SimpleRouteJson, SimplifiedPcbTrace } from "./SimpleRouteJson" +import { getFanoutSharedBoundary } from "./get-fanout-shared-boundary" export type FanoutAutorouterMode = "single_layer_fanout" | "fanout" export interface FanoutAutorouterOptions { mode: FanoutAutorouterMode busFanoutDirections?: Readonly> + fanoutBoundaryPadding?: FanoutBoundaryPadding } const getNinePointAnchor = ( @@ -181,10 +187,24 @@ export class FanoutAutorouter implements GenericLocalAutorouter { fanoutTraces: SimplifiedPcbTrace[] debugGraphics: AutorouterProgressEvent["debugGraphics"] } { - const fanoutSolver = new FanoutSolver( + const fanoutSolverOptions = this.getFanoutSolverOptions() + let fanoutSolver = new FanoutSolver( this.input as unknown as ConstructorParameters[0], - this.getFanoutSolverOptions(), + fanoutSolverOptions, ) + const sharedBoundary = getFanoutSharedBoundary({ + preparedBuses: fanoutSolver.preparedBuses, + padding: this.options.fanoutBoundaryPadding, + }) + if (sharedBoundary) { + fanoutSolver = new FanoutSolver( + this.input as unknown as ConstructorParameters[0], + { + ...fanoutSolverOptions, + sharedBoundary, + }, + ) + } fanoutSolver.solve() if (fanoutSolver.failed) { throw new Error(fanoutSolver.error ?? "Fanout routing failed") diff --git a/lib/utils/autorouting/get-fanout-shared-boundary.ts b/lib/utils/autorouting/get-fanout-shared-boundary.ts new file mode 100644 index 000000000..c3ca8089b --- /dev/null +++ b/lib/utils/autorouting/get-fanout-shared-boundary.ts @@ -0,0 +1,98 @@ +import type { Bounds, FanoutSolver } from "@tscircuit/fanout-solver" +import type { FanoutBoundaryPadding } from "@tscircuit/props" +import { distance } from "circuit-json" + +type PreparedFanoutBus = FanoutSolver["preparedBuses"][number] +type FanoutSourcePadObstacle = PreparedFanoutBus["componentObstacles"][number] + +const getDirectionalPadding = ( + padding: FanoutBoundaryPadding, +): { + top: number + right: number + bottom: number + left: number +} => { + if (typeof padding !== "object") { + const parsedPadding = distance.parse(padding) + return { + top: parsedPadding, + right: parsedPadding, + bottom: parsedPadding, + left: parsedPadding, + } + } + + return { + top: padding.top === undefined ? 0 : distance.parse(padding.top), + right: padding.right === undefined ? 0 : distance.parse(padding.right), + bottom: padding.bottom === undefined ? 0 : distance.parse(padding.bottom), + left: padding.left === undefined ? 0 : distance.parse(padding.left), + } +} + +const getObstacleHalfExtents = ( + obstacle: FanoutSourcePadObstacle, +): { x: number; y: number } => { + const rotationRadians = ((obstacle.ccwRotationDegrees ?? 0) * Math.PI) / 180 + const absoluteCosine = Math.abs(Math.cos(rotationRadians)) + const absoluteSine = Math.abs(Math.sin(rotationRadians)) + + return { + x: (absoluteCosine * obstacle.width + absoluteSine * obstacle.height) / 2, + y: (absoluteSine * obstacle.width + absoluteCosine * obstacle.height) / 2, + } +} + +export const getFanoutSharedBoundary = ({ + preparedBuses, + padding, +}: { + preparedBuses: FanoutSolver["preparedBuses"] + padding?: FanoutBoundaryPadding +}): Bounds | undefined => { + if (padding === undefined) return undefined + + const sourcePadsByComponentId = new Map() + for (const bus of preparedBuses) { + if (sourcePadsByComponentId.has(bus.componentId)) continue + sourcePadsByComponentId.set( + bus.componentId, + bus.componentObstacles.filter( + (obstacle) => obstacle.connectedTo.length > 0, + ), + ) + } + const sourcePadObstacles = [...sourcePadsByComponentId.values()].flat() + if (sourcePadObstacles.length === 0) { + throw new Error( + "Cannot apply fanout boundary padding without source pad obstacles", + ) + } + + const obstacleBounds = sourcePadObstacles.map((obstacle) => { + const halfExtents = getObstacleHalfExtents(obstacle) + return { + minX: obstacle.center.x - halfExtents.x, + maxX: obstacle.center.x + halfExtents.x, + minY: obstacle.center.y - halfExtents.y, + maxY: obstacle.center.y + halfExtents.y, + } + }) + const directionalPadding = getDirectionalPadding(padding) + + return { + minX: + Math.min(...obstacleBounds.map((bounds) => bounds.minX)) - + directionalPadding.left, + maxX: + Math.max(...obstacleBounds.map((bounds) => bounds.maxX)) + + directionalPadding.right, + minY: + Math.min(...obstacleBounds.map((bounds) => bounds.minY)) - + directionalPadding.bottom, + maxY: + Math.max(...obstacleBounds.map((bounds) => bounds.maxY)) + + directionalPadding.top, + } +} diff --git a/lib/utils/autorouting/localAutorouterStrategies.ts b/lib/utils/autorouting/localAutorouterStrategies.ts index 05bd1046f..fc688cc6e 100644 --- a/lib/utils/autorouting/localAutorouterStrategies.ts +++ b/lib/utils/autorouting/localAutorouterStrategies.ts @@ -19,6 +19,7 @@ export interface LocalAutorouterStrategyContext { simpleRouteJson: SimpleRouteJson commonAutorouterOptions: AutorouterOptions busFanoutDirections?: AutoroutingPhaseProps["busFanoutDirections"] + fanoutBoundaryPadding?: AutoroutingPhaseProps["fanoutBoundaryPadding"] } export interface LocalAutorouterStrategy { @@ -44,10 +45,11 @@ const createFanoutAutorouterStrategy = ( ): LocalAutorouterStrategy => ({ cacheable: false, followUpAutorouter: "default", - create: ({ simpleRouteJson, busFanoutDirections }) => + create: ({ simpleRouteJson, busFanoutDirections, fanoutBoundaryPadding }) => new FanoutAutorouter(simpleRouteJson, { mode, busFanoutDirections, + fanoutBoundaryPadding, }), }) diff --git a/package.json b/package.json index 13c8dbc17..d163e4e93 100644 --- a/package.json +++ b/package.json @@ -53,7 +53,7 @@ "@tscircuit/math-utils": "^0.0.36", "@tscircuit/miniflex": "^0.0.4", "@tscircuit/ngspice-spice-engine": "^0.0.20", - "@tscircuit/props": "^0.0.595", + "@tscircuit/props": "^0.0.598", "@tscircuit/schematic-match-adapt": "^0.0.18", "@tscircuit/schematic-trace-solver": "^0.0.111", "@tscircuit/solver-utils": "^0.0.16", diff --git a/tests/breakout/__snapshots__/breakout-bga36-decoupling-caps-autorouting-srj.snap.svg b/tests/breakout/__snapshots__/breakout-bga36-decoupling-caps-autorouting-srj.snap.svg index ee843e314..163ab279e 100644 --- a/tests/breakout/__snapshots__/breakout-bga36-decoupling-caps-autorouting-srj.snap.svg +++ b/tests/breakout/__snapshots__/breakout-bga36-decoupling-caps-autorouting-srj.snap.svg @@ -13,21 +13,23 @@ >FULL ROUTED CIRCUIT: 0 CONNECTIONS, 16 TRACES -