Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/components/primitive-components/Group/Group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1169,6 +1169,7 @@ export class Group<Props extends z.ZodType<any, any, any> = typeof groupProps>
simpleRouteJson,
commonAutorouterOptions,
busFanoutDirections: routingPhasePlan.busFanoutDirections,
fanoutBoundaryPadding: routingPhasePlan.fanoutBoundaryPadding,
})
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface RoutingPhasePlan {
region?: AutoroutingPhaseProps["region"]
connectionSelectors?: string[]
busFanoutDirections?: AutoroutingPhaseProps["busFanoutDirections"]
fanoutBoundaryPadding?: AutoroutingPhaseProps["fanoutBoundaryPadding"]
drcTolerances?: RoutingPhaseDrcTolerances
nets: Net[]
traces: Trace[]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,11 @@ export function Group_getRoutingPhasePlans(
const plansByPhaseIndex = new Map<number | null, RoutingPhasePlan>()
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,
Expand Down Expand Up @@ -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
Expand Down
26 changes: 23 additions & 3 deletions lib/utils/autorouting/FanoutAutorouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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<Record<string, BusFanoutDirection>>
fanoutBoundaryPadding?: FanoutBoundaryPadding
}

const getNinePointAnchor = (
Expand Down Expand Up @@ -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<typeof FanoutSolver>[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<typeof FanoutSolver>[0],
{
...fanoutSolverOptions,
sharedBoundary,
},
)
}
fanoutSolver.solve()
if (fanoutSolver.failed) {
throw new Error(fanoutSolver.error ?? "Fanout routing failed")
Expand Down
98 changes: 98 additions & 0 deletions lib/utils/autorouting/get-fanout-shared-boundary.ts
Original file line number Diff line number Diff line change
@@ -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<string, FanoutSourcePadObstacle[]>()
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,
}
}
4 changes: 3 additions & 1 deletion lib/utils/autorouting/localAutorouterStrategies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export interface LocalAutorouterStrategyContext {
simpleRouteJson: SimpleRouteJson
commonAutorouterOptions: AutorouterOptions
busFanoutDirections?: AutoroutingPhaseProps["busFanoutDirections"]
fanoutBoundaryPadding?: AutoroutingPhaseProps["fanoutBoundaryPadding"]
}

export interface LocalAutorouterStrategy {
Expand All @@ -44,10 +45,11 @@ const createFanoutAutorouterStrategy = (
): LocalAutorouterStrategy => ({
cacheable: false,
followUpAutorouter: "default",
create: ({ simpleRouteJson, busFanoutDirections }) =>
create: ({ simpleRouteJson, busFanoutDirections, fanoutBoundaryPadding }) =>
new FanoutAutorouter(simpleRouteJson, {
mode,
busFanoutDirections,
fanoutBoundaryPadding,
}),
})

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 59 additions & 1 deletion tests/breakout/breakout-bga36-decoupling-caps.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ test("breakout defaults to fanout for a 6x6 BGA surrounded by four 0603 decoupli
minViaHoleDiameter="0.2mm"
minViaPadDiameter="0.5mm"
>
<breakout name="BGA_BREAKOUT" padding="1.2mm">
<breakout name="BGA_BREAKOUT" padding="1.2mm" fanoutBoundaryPadding="1mm">
<chip name="U1" footprint={bgaFootprint} pcbX={0} pcbY={0} />

<DecouplingCap name="C_TOP" pcbX={0} pcbY={4.2} />
Expand Down Expand Up @@ -88,6 +88,64 @@ test("breakout defaults to fanout for a 6x6 BGA surrounded by four 0603 decoupli
expect(breakoutPhases[0]?.endSimpleRouteJson?.traces).toHaveLength(8)
expect(breakoutPhases[1]?.startSimpleRouteJson?.traces).toHaveLength(8)
expect(breakoutPhases[1]?.endSimpleRouteJson?.traces).toHaveLength(16)

const u1SourceComponent = circuit.db.source_component.getWhere({
name: "U1",
})
const u1PcbComponent = circuit.db.pcb_component.getWhere({
source_component_id: u1SourceComponent?.source_component_id,
})
const u1PadObstacles =
breakoutPhases[0]?.startSimpleRouteJson?.obstacles.filter(
(obstacle) =>
obstacle.componentId === u1PcbComponent?.pcb_component_id &&
obstacle.connectedTo.length > 0,
) ?? []
expect(u1PadObstacles.length).toBeGreaterThan(0)

const expectedFanoutBoundary = {
minX:
Math.min(
...u1PadObstacles.map(
(obstacle) => obstacle.center.x - obstacle.width / 2,
),
) - 1,
maxX:
Math.max(
...u1PadObstacles.map(
(obstacle) => obstacle.center.x + obstacle.width / 2,
),
) + 1,
minY:
Math.min(
...u1PadObstacles.map(
(obstacle) => obstacle.center.y - obstacle.height / 2,
),
) - 1,
maxY:
Math.max(
...u1PadObstacles.map(
(obstacle) => obstacle.center.y + obstacle.height / 2,
),
) + 1,
}
for (const fanoutTrace of breakoutPhases[0]?.endSimpleRouteJson?.traces ??
[]) {
const exitPoint = fanoutTrace.route.findLast(
(routePoint) => "x" in routePoint,
)
expect(exitPoint).toBeDefined()
if (!exitPoint || !("x" in exitPoint)) {
throw new Error("Expected the fanout trace to end at a point")
}
expect(
Math.abs(exitPoint.x - expectedFanoutBoundary.minX) < 1e-6 ||
Math.abs(exitPoint.x - expectedFanoutBoundary.maxX) < 1e-6 ||
Math.abs(exitPoint.y - expectedFanoutBoundary.minY) < 1e-6 ||
Math.abs(exitPoint.y - expectedFanoutBoundary.maxY) < 1e-6,
).toBe(true)
}

expect(
breakoutPhases[0]?.endSimpleRouteJson?.traces
?.flatMap((trace) => trace.route)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32 changes: 32 additions & 0 deletions tests/features/autorouter-fanout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ test('autorouter="fanout" escapes an inner BGA bus before board routing', async
<autoroutingphase
autorouter="fanout"
busFanoutDirections={{ DATA: { direction: "center_right" } }}
fanoutBoundaryPadding={{ right: "1.2mm" }}
/>
<chip name="U1" pcbX={-4} footprint={<footprint>{bgaPads}</footprint>} />
<bus
Expand Down Expand Up @@ -121,6 +122,37 @@ test('autorouter="fanout" escapes an inner BGA bus before board routing', async
4,
)
expect(autoroutingPhaseIoStack[1]?.endSimpleRouteJson?.traces).toHaveLength(8)

const u1SourceComponent = circuit.db.source_component.getWhere({
name: "U1",
})
const u1PcbComponent = circuit.db.pcb_component.getWhere({
source_component_id: u1SourceComponent?.source_component_id,
})
const u1PadObstacles =
autoroutingPhaseIoStack[0]?.startSimpleRouteJson?.obstacles.filter(
(obstacle) =>
obstacle.componentId === u1PcbComponent?.pcb_component_id &&
obstacle.connectedTo.length > 0,
) ?? []
const expectedRightBoundary =
Math.max(
...u1PadObstacles.map(
(obstacle) => obstacle.center.x + obstacle.width / 2,
),
) + 1.2
for (const fanoutTrace of autoroutingPhaseIoStack[0]?.endSimpleRouteJson
?.traces ?? []) {
const exitPoint = fanoutTrace.route.findLast(
(routePoint) => "x" in routePoint,
)
expect(exitPoint).toBeDefined()
if (!exitPoint || !("x" in exitPoint)) {
throw new Error("Expected the fanout trace to end at a point")
}
expect(exitPoint.x).toBeCloseTo(expectedRightBoundary)
}

expect(circuit).toMatchPcbSnapshot(import.meta.path)
await expect(autoroutingPhaseIoStack).toMatchAutoroutingPhaseIoStackSnapshot(
import.meta.path,
Expand Down
Loading
Loading