From 7e3fc928b6066de9cf529957060d75c5e8799ad1 Mon Sep 17 00:00:00 2001 From: seveibar Date: Tue, 28 Jul 2026 11:37:49 -0700 Subject: [PATCH] feat: add fanout boundary padding props --- lib/common/fanoutBoundaryPadding.ts | 35 +++++++++++++++++++++++++++++ lib/components/autoroutingphase.ts | 10 +++++++++ lib/components/breakout.ts | 14 ++++++++++-- lib/index.ts | 1 + tests/autoroutingphase.test.ts | 28 +++++++++++++++++++++++ tests/breakout.test.ts | 30 +++++++++++++++++++++++++ 6 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 lib/common/fanoutBoundaryPadding.ts diff --git a/lib/common/fanoutBoundaryPadding.ts b/lib/common/fanoutBoundaryPadding.ts new file mode 100644 index 00000000..ac40decc --- /dev/null +++ b/lib/common/fanoutBoundaryPadding.ts @@ -0,0 +1,35 @@ +import { expectTypesMatch } from "lib/typecheck" +import { z } from "zod" +import { distance, type Distance } from "./distance" + +export interface DirectionalFanoutBoundaryPadding { + top?: Distance + right?: Distance + bottom?: Distance + left?: Distance +} + +/** + * Padding between the union of the fanout source pads and the shared boundary + * where fanout traces terminate. Omitted directional values are treated as + * zero. + */ +export type FanoutBoundaryPadding = Distance | DirectionalFanoutBoundaryPadding + +const nonnegativeDistance = distance.refine((value) => value >= 0, { + message: "Fanout boundary padding cannot be negative", +}) + +export const fanoutBoundaryPadding = z.union([ + nonnegativeDistance, + z.object({ + top: nonnegativeDistance.optional(), + right: nonnegativeDistance.optional(), + bottom: nonnegativeDistance.optional(), + left: nonnegativeDistance.optional(), + }), +]) + +expectTypesMatch>( + true, +) diff --git a/lib/components/autoroutingphase.ts b/lib/components/autoroutingphase.ts index 968b6743..c2723b7c 100644 --- a/lib/components/autoroutingphase.ts +++ b/lib/components/autoroutingphase.ts @@ -1,5 +1,9 @@ import { expectTypesMatch } from "lib/typecheck" import { z } from "zod" +import { + type FanoutBoundaryPadding, + fanoutBoundaryPadding, +} from "../common/fanoutBoundaryPadding" import { type NinePointAnchor, ninePointAnchor, @@ -38,6 +42,11 @@ export interface AutoroutingPhaseProps extends RoutingTolerances { * direction unconstrained. */ busFanoutDirections?: Record + /** + * Padding between the union of the fanout source pads and the shared + * boundary where fanout traces terminate. + */ + fanoutBoundaryPadding?: FanoutBoundaryPadding } const busFanoutDirection = z.union([ @@ -65,6 +74,7 @@ export const autoroutingPhaseProps = z connections: z.array(z.string()).optional(), reroute: z.boolean().optional(), busFanoutDirections: z.record(busFanoutDirection).optional(), + fanoutBoundaryPadding: fanoutBoundaryPadding.optional(), }) .superRefine((value, ctx) => { if ( diff --git a/lib/components/breakout.ts b/lib/components/breakout.ts index 30652f57..d41bdba8 100644 --- a/lib/components/breakout.ts +++ b/lib/components/breakout.ts @@ -1,5 +1,8 @@ -import { distance } from "circuit-json" -import type { Distance } from "lib/common/distance" +import { distance, type Distance } from "lib/common/distance" +import { + type FanoutBoundaryPadding, + fanoutBoundaryPadding, +} from "lib/common/fanoutBoundaryPadding" import { expectTypesMatch } from "lib/typecheck" import { z } from "zod" import { @@ -21,6 +24,12 @@ export interface BreakoutProps paddingRight?: Distance paddingTop?: Distance paddingBottom?: Distance + /** + * Padding between the union of the fanout source pads and the shared + * boundary where fanout traces terminate. This is independent of the + * breakout group's layout padding. + */ + fanoutBoundaryPadding?: FanoutBoundaryPadding } export const breakoutProps = subcircuitGroupProps.extend({ @@ -30,6 +39,7 @@ export const breakoutProps = subcircuitGroupProps.extend({ paddingRight: distance.optional(), paddingTop: distance.optional(), paddingBottom: distance.optional(), + fanoutBoundaryPadding: fanoutBoundaryPadding.optional(), }) type InferredBreakoutProps = z.input diff --git a/lib/index.ts b/lib/index.ts index e2dec7f8..d940d52a 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -139,3 +139,4 @@ export * from "./platformConfig" export * from "./projectConfig" export * from "./utility-types/connections-and-selectors" export * from "./common/ninePointAnchor" +export * from "./common/fanoutBoundaryPadding" diff --git a/tests/autoroutingphase.test.ts b/tests/autoroutingphase.test.ts index afa5b980..33f69363 100644 --- a/tests/autoroutingphase.test.ts +++ b/tests/autoroutingphase.test.ts @@ -42,6 +42,34 @@ test("autorouting phase accepts per-bus fanout directions", () => { expect(autoroutingPhaseProps.parse(raw)).toEqual(raw) }) +test("autorouting phase accepts scalar fanout boundary padding", () => { + const raw = { + autorouter: "fanout", + fanoutBoundaryPadding: "0.6mm", + } satisfies AutoroutingPhaseProps + + expect(autoroutingPhaseProps.parse(raw).fanoutBoundaryPadding).toBe(0.6) +}) + +test("autorouting phase accepts directional fanout boundary padding", () => { + const raw = { + autorouter: "fanout", + fanoutBoundaryPadding: { + top: "0.4mm", + right: 0.8, + bottom: "1.2mm", + left: 0.5, + }, + } satisfies AutoroutingPhaseProps + + expect(autoroutingPhaseProps.parse(raw).fanoutBoundaryPadding).toEqual({ + top: 0.4, + right: 0.8, + bottom: 1.2, + left: 0.5, + }) +}) + test("autorouting phase accepts a single connection", () => { const raw: AutoroutingPhaseProps = { phaseIndex: 2, diff --git a/tests/breakout.test.ts b/tests/breakout.test.ts index 925a02eb..4ec25845 100644 --- a/tests/breakout.test.ts +++ b/tests/breakout.test.ts @@ -24,6 +24,36 @@ test("breakout and fanout elements default to the fanout autorouter", () => { ).toBe("single_layer_fanout") }) +test("breakout accepts scalar fanout boundary padding", () => { + const raw = { + fanoutBoundaryPadding: "0.6mm", + } satisfies BreakoutProps + + expect(breakoutProps.parse(raw).fanoutBoundaryPadding).toBe(0.6) +}) + +test("breakout accepts directional fanout boundary padding", () => { + const raw = { + fanoutBoundaryPadding: { + top: "0.4mm", + right: 0.8, + bottom: "1.2mm", + }, + } satisfies BreakoutProps + + expect(breakoutProps.parse(raw).fanoutBoundaryPadding).toEqual({ + top: 0.4, + right: 0.8, + bottom: 1.2, + }) +}) + +test("breakout rejects negative fanout boundary padding", () => { + expect(() => + breakoutProps.parse({ fanoutBoundaryPadding: "-0.1mm" }), + ).toThrow("Fanout boundary padding cannot be negative") +}) + test("should parse breakout point props", () => { const raw: BreakoutPointProps = { pcbX: 5,