diff --git a/lib/components/primitive-components/Group/Group.ts b/lib/components/primitive-components/Group/Group.ts index f987c75b3..f0a80bfc7 100644 --- a/lib/components/primitive-components/Group/Group.ts +++ b/lib/components/primitive-components/Group/Group.ts @@ -73,6 +73,7 @@ import { Group_doInitialSchematicTraceRender } from "./Group_doInitialSchematicT import { Group_doInitialSimulationSpiceEngineRender } from "./Group_doInitialSimulationSpiceEngineRender" import { Group_doInitialSourceAddConnectivityMapKey } from "./Group_doInitialSourceAddConnectivityMapKey" import { Group_getRoutingPhasePlans } from "./Group_getRoutingPhasePlans" +import { Group_getFanoutPourNetMap } from "./Group_getFanoutPourNetMap" import { cacheLocalAutoroutingPhaseResult, getCachedLocalAutoroutingPhaseResult, @@ -924,14 +925,6 @@ export class Group = typeof groupProps> props.defaultTraceWidth ?? props.nominalTraceWidth ?? minTraceWidth, ) - const { simpleRouteJson: baseSimpleRouteJson } = - getSimpleRouteJsonFromCircuitJson({ - db, - minTraceWidth, - nominalTraceWidth, - subcircuit_id: this.subcircuit_id, - subcircuitComponent: this, - }) const routingPhasePlans = this._getRoutingPhasePlans() const hasPhasedAutorouting = Group_hasPhasedAutorouting(routingPhasePlans) const routingStages = routingPhasePlans.flatMap((routingPhasePlan) => { @@ -950,6 +943,21 @@ export class Group = typeof groupProps> routingPhasePlan, })) }) + const hasFanoutStage = routingStages.some(({ autorouterConfig }) => + ["fanout", "single_layer_fanout"].includes(autorouterConfig.preset ?? ""), + ) + const fanoutPourNetMap = hasFanoutStage + ? Group_getFanoutPourNetMap(this, routingPhasePlans) + : undefined + const { simpleRouteJson: baseSimpleRouteJson } = + getSimpleRouteJsonFromCircuitJson({ + db, + minTraceWidth, + nominalTraceWidth, + subcircuit_id: this.subcircuit_id, + subcircuitComponent: this, + fanoutPourNetMap, + }) const outputTraces: SimplifiedPcbTrace[] = [] const outputJumpers: Array<{ jumper_footprint: string @@ -1174,6 +1182,7 @@ export class Group = typeof groupProps> commonAutorouterOptions, busFanoutDirections: routingPhasePlan.busFanoutDirections, fanoutBoundaryPadding: routingPhasePlan.fanoutBoundaryPadding, + fanoutRoutingLayers: routingPhasePlan.fanoutRoutingLayers, }) } diff --git a/lib/components/primitive-components/Group/GroupRoutingPhasePlan.ts b/lib/components/primitive-components/Group/GroupRoutingPhasePlan.ts index 534103c98..3e08b25bb 100644 --- a/lib/components/primitive-components/Group/GroupRoutingPhasePlan.ts +++ b/lib/components/primitive-components/Group/GroupRoutingPhasePlan.ts @@ -22,6 +22,8 @@ export interface RoutingPhasePlan { connectionSelectors?: string[] busFanoutDirections?: AutoroutingPhaseProps["busFanoutDirections"] fanoutBoundaryPadding?: AutoroutingPhaseProps["fanoutBoundaryPadding"] + fanoutRoutingLayers?: string[] + fanoutPourNetMap?: AutoroutingPhaseProps["fanoutPourNetMap"] drcTolerances?: RoutingPhaseDrcTolerances nets: Net[] traces: Trace[] diff --git a/lib/components/primitive-components/Group/Group_getFanoutPourNetMap.ts b/lib/components/primitive-components/Group/Group_getFanoutPourNetMap.ts new file mode 100644 index 000000000..5aac7a1f4 --- /dev/null +++ b/lib/components/primitive-components/Group/Group_getFanoutPourNetMap.ts @@ -0,0 +1,69 @@ +import type { FanoutPourNetMap } from "@tscircuit/props" +import type { PrimitiveComponent } from "../../base-components/PrimitiveComponent" +import type { CopperPour } from "../CopperPour/CopperPour" +import type { Group } from "./Group" +import type { RoutingPhasePlan } from "./GroupRoutingPhasePlan" +import type { ISubcircuit } from "./Subcircuit/ISubcircuit" + +const addPourNetMap = ( + target: Map>, + pourNetMap: FanoutPourNetMap, +) => { + for (const [layer, netOrNets] of Object.entries(pourNetMap)) { + if (!netOrNets) continue + const nets = Array.isArray(netOrNets) ? netOrNets : [netOrNets] + const layerNets = target.get(layer) ?? new Set() + for (const net of nets) layerNets.add(net) + target.set(layer, layerNets) + } +} + +const toFanoutPourNetMap = ( + netsByLayer: Map>, +): FanoutPourNetMap => + Object.fromEntries( + Array.from(netsByLayer, ([layer, nets]) => [ + layer, + nets.size === 1 ? Array.from(nets)[0]! : Array.from(nets), + ]), + ) + +/** + * Gets the plane intent for fanout routing. An explicit phase map is + * authoritative. When no phase supplies one, copper pours in the current or + * enclosing subcircuits are used. + */ +export const Group_getFanoutPourNetMap = ( + group: Group, + routingPhasePlans: RoutingPhasePlan[], +): FanoutPourNetMap | undefined => { + const explicitMaps = routingPhasePlans + .map((plan) => plan.fanoutPourNetMap) + .filter((map): map is FanoutPourNetMap => map !== undefined) + + const netsByLayer = new Map>() + if (explicitMaps.length > 0) { + for (const explicitMap of explicitMaps) { + addPourNetMap(netsByLayer, explicitMap) + } + return toFanoutPourNetMap(netsByLayer) + } + + let scope: PrimitiveComponent | null = group + while (scope) { + if (scope.isSubcircuit) { + const subcircuit = scope as unknown as ISubcircuit + const copperPours = subcircuit.selectAll("copperpour") + for (const copperPour of copperPours) { + if (copperPour.getSubcircuit() !== subcircuit) continue + const { layer, connectsTo } = copperPour._parsedProps + addPourNetMap(netsByLayer, { + [layer]: connectsTo, + }) + } + } + scope = scope.parent + } + + return netsByLayer.size > 0 ? toFanoutPourNetMap(netsByLayer) : undefined +} diff --git a/lib/components/primitive-components/Group/Group_getRoutingPhasePlans.ts b/lib/components/primitive-components/Group/Group_getRoutingPhasePlans.ts index 466750fea..b13f471db 100644 --- a/lib/components/primitive-components/Group/Group_getRoutingPhasePlans.ts +++ b/lib/components/primitive-components/Group/Group_getRoutingPhasePlans.ts @@ -277,6 +277,10 @@ export function Group_getRoutingPhasePlans( plan.busFanoutDirections = phaseProps?.busFanoutDirections plan.fanoutBoundaryPadding = phaseProps?.fanoutBoundaryPadding ?? groupFanoutBoundaryPadding + plan.fanoutRoutingLayers = phaseProps?.fanoutRoutingLayers?.map((layer) => + typeof layer === "string" ? layer : layer.name, + ) + plan.fanoutPourNetMap = phaseProps?.fanoutPourNetMap plan.drcTolerances = phaseProps ? getDrcTolerancesFromAutoroutingPhaseProps(phaseProps) : undefined diff --git a/lib/utils/autorouting/FanoutAutorouter.ts b/lib/utils/autorouting/FanoutAutorouter.ts index b774ccd76..0ddbe3a68 100644 --- a/lib/utils/autorouting/FanoutAutorouter.ts +++ b/lib/utils/autorouting/FanoutAutorouter.ts @@ -1,6 +1,7 @@ import { FanoutSolver, type FanoutBorderTarget, + type FanoutDirection, type FanoutSolverOptions, } from "@tscircuit/fanout-solver" import type { @@ -25,6 +26,7 @@ export interface FanoutAutorouterOptions { mode: FanoutAutorouterMode busFanoutDirections?: Readonly> fanoutBoundaryPadding?: FanoutBoundaryPadding + fanoutRoutingLayers?: string[] } const getNinePointAnchor = ( @@ -59,6 +61,27 @@ const getFanoutBorderTarget = ( } } +const getPlaneFanoutDirection = ( + anchor: NinePointAnchor, +): FanoutDirection | undefined => { + switch (anchor) { + case "top_left": + case "top_center": + case "top_right": + return "up" + case "center_left": + return "left" + case "center": + return undefined + case "center_right": + return "right" + case "bottom_left": + case "bottom_center": + case "bottom_right": + return "down" + } +} + const createDownstreamSimpleRouteJson = ({ fanoutSimpleRouteJson, sourceComponentIds, @@ -151,6 +174,11 @@ export class FanoutAutorouter implements GenericLocalAutorouter { | undefined { if (!this.options.busFanoutDirections) return undefined const knownBusIds = new Set(this.input.buses?.map((bus) => bus.busId) ?? []) + const planeBusIds = new Set( + this.input.buses + ?.filter((bus) => bus.termination?.type === "plane") + .map((bus) => bus.busId) ?? [], + ) const preferences: Record = {} for (const [busId, fanoutDirection] of Object.entries( this.options.busFanoutDirections, @@ -160,17 +188,42 @@ export class FanoutAutorouter implements GenericLocalAutorouter { `Fanout direction references unknown bus "${busId}" in this autorouting phase`, ) } + if (planeBusIds.has(busId)) continue const target = getFanoutBorderTarget(getNinePointAnchor(fanoutDirection)) if (target) preferences[busId] = target } return Object.keys(preferences).length > 0 ? preferences : undefined } + private getPlaneBusDirections(): + | Readonly> + | undefined { + if (!this.options.busFanoutDirections) return undefined + const planeBusIds = new Set( + this.input.buses + ?.filter((bus) => bus.termination?.type === "plane") + .map((bus) => bus.busId) ?? [], + ) + const directions: Record = {} + for (const [busId, fanoutDirection] of Object.entries( + this.options.busFanoutDirections, + )) { + if (!planeBusIds.has(busId)) continue + const direction = getPlaneFanoutDirection( + getNinePointAnchor(fanoutDirection), + ) + if (direction) directions[busId] = direction + } + return Object.keys(directions).length > 0 ? directions : undefined + } + private getFanoutSolverOptions(): FanoutSolverOptions { const commonOptions: FanoutSolverOptions = { borderDistribution: "even", compactBusTracks: true, + busDirections: this.getPlaneBusDirections(), busExitPreferences: this.getBusExitPreferences(), + escapeLayers: this.options.fanoutRoutingLayers, } if (this.options.mode === "single_layer_fanout") { return { diff --git a/lib/utils/autorouting/SimpleRouteJson.ts b/lib/utils/autorouting/SimpleRouteJson.ts index 69c505dd7..de0b8353b 100644 --- a/lib/utils/autorouting/SimpleRouteJson.ts +++ b/lib/utils/autorouting/SimpleRouteJson.ts @@ -104,10 +104,20 @@ export type SimpleRouteDifferentialPair = { } /** A group of connections that an autorouter should keep together. */ +export type SimpleRouteBusTermination = + | { + type: "boundary" + } + | { + type: "plane" + layer: string + } + export type SimpleRouteBus = { busId: string name?: string connectionNames: SrjConnectionName[] + termination?: SimpleRouteBusTermination } export type SimpleRouteJson = Omit< diff --git a/lib/utils/autorouting/getBusesForSimpleRouteJson.ts b/lib/utils/autorouting/getBusesForSimpleRouteJson.ts index 52ddeb686..7bed5551e 100644 --- a/lib/utils/autorouting/getBusesForSimpleRouteJson.ts +++ b/lib/utils/autorouting/getBusesForSimpleRouteJson.ts @@ -1,4 +1,4 @@ -import type { SourcePort, SourceTrace } from "circuit-json" +import type { SourceNet, SourcePort, SourceTrace } from "circuit-json" import type { Bus } from "lib/components/primitive-components/Bus" import type { Port } from "lib/components/primitive-components/Port/Port" import type { @@ -18,8 +18,13 @@ type GetBusesParams = { buses: Bus[] sourceTraces: SourceTrace[] subcircuitId?: SubcircuitId | null + planeTerminatedSourceTraceLayers?: ReadonlyMap } +export type FanoutPourNetMap = Readonly< + Record +> + const getBusSourceTraceSubcircuitConnectivityMapKeyOrThrow = ({ bus, busSourceTraces, @@ -107,14 +112,76 @@ const getBusSrjConnectionNameOrThrow = ({ return matchingSrjConnections[0]!.name } +const normalizeNetName = (netNameOrSelector: string): string => + netNameOrSelector + .trim() + .replace(/^net\./, "") + .replace(/^\./, "") + +export const getPlaneTerminatedSourceTraceLayers = ({ + fanoutPourNetMap, + sourceNets, + sourceTraces, + subcircuitId, +}: { + fanoutPourNetMap?: FanoutPourNetMap + sourceNets: SourceNet[] + sourceTraces: SourceTrace[] + subcircuitId?: SubcircuitId | null +}): Map => { + const traceLayers = new Map() + if (!fanoutPourNetMap) return traceLayers + + const planeLayerBySourceNetId = new Map() + for (const [layer, netOrNets] of Object.entries(fanoutPourNetMap)) { + const netNames = Array.isArray(netOrNets) ? netOrNets : [netOrNets] + for (const netNameOrSelector of netNames) { + const netName = normalizeNetName(netNameOrSelector) + for (const sourceNet of sourceNets) { + if (sourceNet.name !== netName) continue + const previousLayer = planeLayerBySourceNetId.get( + sourceNet.source_net_id, + ) + if (previousLayer && previousLayer !== layer) { + throw new Error( + `Fanout plane net "${netName}" maps to multiple layers ("${previousLayer}" and "${layer}"); use fanoutPourNetMap to select one`, + ) + } + planeLayerBySourceNetId.set(sourceNet.source_net_id, layer) + } + } + } + + for (const sourceTrace of sourceTraces) { + if (subcircuitId && sourceTrace.subcircuit_id !== subcircuitId) continue + if (sourceTrace.connected_source_port_ids.length !== 1) continue + + const mappedLayers = new Set( + (sourceTrace.connected_source_net_ids ?? []) + .map((sourceNetId) => planeLayerBySourceNetId.get(sourceNetId)) + .filter((layer): layer is string => layer !== undefined), + ) + if (mappedLayers.size > 1) { + throw new Error( + `Source trace "${sourceTrace.name ?? sourceTrace.source_trace_id}" connects to fanout planes on multiple layers`, + ) + } + const layer = mappedLayers.values().next().value + if (layer) traceLayers.set(sourceTrace.source_trace_id, layer) + } + + return traceLayers +} + /** Converts bus trace names or port selectors into SRJ constraints. */ export const getBusesForSimpleRouteJson = ({ srjConnections, buses, sourceTraces, subcircuitId, + planeTerminatedSourceTraceLayers, }: GetBusesParams): SimpleRouteBus[] | undefined => { - const srjBuses: SimpleRouteBus[] = [] + const declaredSrjBuses: SimpleRouteBus[] = [] for (const bus of buses) { const busSubcircuitId = bus.getSubcircuit().subcircuit_id if (subcircuitId && busSubcircuitId !== subcircuitId) continue @@ -146,12 +213,39 @@ export const getBusesForSimpleRouteJson = ({ ) } - srjBuses.push({ + declaredSrjBuses.push({ busId: bus.name, name: bus.name, connectionNames, }) } + const planeSrjBuses: SimpleRouteBus[] = [] + for (const [sourceTraceId, layer] of planeTerminatedSourceTraceLayers ?? []) { + const sourceTrace = sourceTraces.find( + (candidate) => candidate.source_trace_id === sourceTraceId, + ) + const srjConnection = srjConnections.find( + (connection) => connection.source_trace_id === sourceTraceId, + ) + if (!sourceTrace || !srjConnection) continue + + const busId = sourceTrace.name ?? sourceTrace.source_trace_id + if ( + [...declaredSrjBuses, ...planeSrjBuses].some((bus) => bus.busId === busId) + ) { + throw new Error( + `Fanout plane trace "${busId}" conflicts with an existing bus name`, + ) + } + planeSrjBuses.push({ + busId, + name: busId, + connectionNames: [srjConnection.name], + termination: { type: "plane", layer }, + }) + } + + const srjBuses = [...planeSrjBuses, ...declaredSrjBuses] return srjBuses.length > 0 ? srjBuses : undefined } diff --git a/lib/utils/autorouting/getSimpleRouteJsonFromCircuitJson.ts b/lib/utils/autorouting/getSimpleRouteJsonFromCircuitJson.ts index b1ed1150c..624b00b85 100644 --- a/lib/utils/autorouting/getSimpleRouteJsonFromCircuitJson.ts +++ b/lib/utils/autorouting/getSimpleRouteJsonFromCircuitJson.ts @@ -15,7 +15,11 @@ import type { SimpleRouteJson, } from "./SimpleRouteJson" import { getDescendantSubcircuitIds } from "./getAncestorSubcircuitIds" -import { getBusesForSimpleRouteJson } from "./getBusesForSimpleRouteJson" +import { + type FanoutPourNetMap, + getBusesForSimpleRouteJson, + getPlaneTerminatedSourceTraceLayers, +} from "./getBusesForSimpleRouteJson" import { getDifferentialPairsForSimpleRouteJson } from "./getDifferentialPairsForSimpleRouteJson" import { getPreservedRoutedSubcircuitTraces } from "./getPreservedRoutedSubcircuitTraces" import { getUnbrokenCopperPourObstacles } from "./getUnbrokenCopperPourObstacles" @@ -38,6 +42,7 @@ export const getSimpleRouteJsonFromCircuitJson = ({ minViaPadDiameter, nominalTraceWidth, subcircuitComponent, + fanoutPourNetMap, ignoreExistingTopLevelPcbRouteState = false, }: { db?: CircuitJsonUtilObjects @@ -54,6 +59,11 @@ export const getSimpleRouteJsonFromCircuitJson = ({ minViaHoleDiameter?: number minViaPadDiameter?: number subcircuitComponent?: Pick + /** + * Copper plane intent used by fanout routing. Source-only traces whose nets + * are mapped here become internal plane-terminated buses in SRJ. + */ + fanoutPourNetMap?: FanoutPourNetMap /** * Excludes existing root-level PCB route state from a fresh routing problem. * Routed child-subcircuit traces and vias remain fixed routing geometry. @@ -341,8 +351,14 @@ export const getSimpleRouteJsonFromCircuitJson = ({ // here so it is preserved as fixed copper instead of re-routed. // For cross-boundary traces, add breakout points as additional // waypoints so the autorouter routes through the boundary. - const directTraceConnections = db.source_trace - .list() + const sourceTraces = db.source_trace.list() + const planeTerminatedSourceTraceLayers = getPlaneTerminatedSourceTraceLayers({ + fanoutPourNetMap, + sourceNets: db.source_net.list(), + sourceTraces, + subcircuitId: subcircuit_id, + }) + const directTraceConnections = sourceTraces .filter( (trace) => !sourceTraceIdsAlreadyPreservedAsSrjTraces.has(trace.source_trace_id), @@ -361,7 +377,15 @@ export const getSimpleRouteJsonFromCircuitJson = ({ } }) - if (connectedPorts.length < 2) return null + const isPlaneTerminatedSourceTrace = planeTerminatedSourceTraceLayers.has( + trace.source_trace_id, + ) + if ( + connectedPorts.length < 2 && + !(isPlaneTerminatedSourceTrace && connectedPorts.length === 1) + ) { + return null + } // TODO handle trace.connected_source_net_ids for (const connectedPort of connectedPorts) { @@ -507,6 +531,9 @@ export const getSimpleRouteJsonFromCircuitJson = ({ ) const sourceTracesEligibleForNetConnections = db.source_trace .list() + .filter( + (trace) => !planeTerminatedSourceTraceLayers.has(trace.source_trace_id), + ) .filter( (trace) => // Existing copper must still contribute endpoint connectivity when it @@ -592,6 +619,8 @@ export const getSimpleRouteJsonFromCircuitJson = ({ width: nominalTraceWidthFromConnectedTraces, pointsToConnect, } + if (pointsToConnect.length === 0) continue + connectionsFromNets.push(connection) for (const sourceNetId of connectedSourceNetIds) { connectionFromNetId.set(sourceNetId, connection) @@ -689,6 +718,7 @@ export const getSimpleRouteJsonFromCircuitJson = ({ buses, sourceTraces: db.source_trace.list(), subcircuitId: subcircuit_id, + planeTerminatedSourceTraceLayers, }) if (subcircuit_id) { diff --git a/lib/utils/autorouting/localAutorouterStrategies.ts b/lib/utils/autorouting/localAutorouterStrategies.ts index fc688cc6e..960877f84 100644 --- a/lib/utils/autorouting/localAutorouterStrategies.ts +++ b/lib/utils/autorouting/localAutorouterStrategies.ts @@ -20,6 +20,7 @@ export interface LocalAutorouterStrategyContext { commonAutorouterOptions: AutorouterOptions busFanoutDirections?: AutoroutingPhaseProps["busFanoutDirections"] fanoutBoundaryPadding?: AutoroutingPhaseProps["fanoutBoundaryPadding"] + fanoutRoutingLayers?: string[] } export interface LocalAutorouterStrategy { @@ -45,11 +46,17 @@ const createFanoutAutorouterStrategy = ( ): LocalAutorouterStrategy => ({ cacheable: false, followUpAutorouter: "default", - create: ({ simpleRouteJson, busFanoutDirections, fanoutBoundaryPadding }) => + create: ({ + simpleRouteJson, + busFanoutDirections, + fanoutBoundaryPadding, + fanoutRoutingLayers, + }) => new FanoutAutorouter(simpleRouteJson, { mode, busFanoutDirections, fanoutBoundaryPadding, + fanoutRoutingLayers, }), }) diff --git a/package.json b/package.json index 055a3a7cc..d9b568351 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.598", + "@tscircuit/props": "^0.0.599", "@tscircuit/schematic-match-adapt": "^0.0.18", "@tscircuit/schematic-trace-solver": "^0.0.116", "@tscircuit/solver-utils": "^0.0.16", diff --git a/tests/features/autorouter-fanout-plane-termination.test.tsx b/tests/features/autorouter-fanout-plane-termination.test.tsx new file mode 100644 index 000000000..2f08efeee --- /dev/null +++ b/tests/features/autorouter-fanout-plane-termination.test.tsx @@ -0,0 +1,187 @@ +import { expect, test } from "bun:test" +import { createAutoroutingPhaseIoStack } from "tests/fixtures/create-autorouting-phase-io-stack" +import { getTestFixture } from "tests/fixtures/get-test-fixture" + +const TestPad = ({ + pinNumber, + pcbX, + pcbY, +}: { + pinNumber: number + pcbX: number + pcbY: number +}) => ( + +) + +test("fanout drops source-only power and ground connections to internal planes", async () => { + const { circuit } = getTestFixture() + const autoroutingPhaseIoStack = createAutoroutingPhaseIoStack(circuit) + const bgaPads = Array.from({ length: 16 }, (_, padIndex) => { + const pinNumber = padIndex + 1 + return ( + + ) + }) + + circuit.add( + + + {bgaPads}} /> + + + + + + + , + ) + + await circuit.renderUntilSettled() + + expect(circuit.db.pcb_autorouting_error.list()).toEqual([]) + expect(circuit.db.pcb_trace_error.list()).toEqual([]) + expect(circuit.db.pcb_pad_trace_clearance_error.list()).toEqual([]) + expect(circuit.db.pcb_via_clearance_error.list()).toEqual([]) + expect(autoroutingPhaseIoStack).toHaveLength(2) + + const fanoutInput = autoroutingPhaseIoStack[0]!.startSimpleRouteJson! + expect(fanoutInput.connections).toHaveLength(4) + expect( + fanoutInput.connections + .slice(0, 2) + .every((connection) => connection.pointsToConnect.length === 1), + ).toBe(true) + expect(fanoutInput.buses).toEqual([ + { + busId: "GND_B2", + name: "GND_B2", + connectionNames: [fanoutInput.connections[0]!.name], + termination: { type: "plane", layer: "inner1" }, + }, + { + busId: "VCC_C3", + name: "VCC_C3", + connectionNames: [fanoutInput.connections[1]!.name], + termination: { type: "plane", layer: "inner2" }, + }, + { + busId: "SIGNAL_BUS", + name: "SIGNAL_BUS", + connectionNames: fanoutInput.connections + .slice(2) + .map((connection) => connection.name), + }, + ]) + expect( + autoroutingPhaseIoStack[0]!.endSimpleRouteJson!.connections, + ).toHaveLength(2) + + const vias = circuit.db.pcb_via.list() + expect(vias.some((via) => via.to_layer === "inner1")).toBe(true) + expect(vias.some((via) => via.to_layer === "inner2")).toBe(true) + + const signalSourceTraceIds = new Set( + ["SIGNAL", "SIGNAL_RETURN"].map( + (name) => circuit.db.source_trace.getWhere({ name })!.source_trace_id, + ), + ) + const signalPcbTraces = circuit.db.pcb_trace + .list() + .filter((trace) => signalSourceTraceIds.has(trace.source_trace_id!)) + const signalLayers = signalPcbTraces.flatMap((trace) => + trace.route.flatMap((routePoint) => { + if (routePoint.route_type === "wire") return [routePoint.layer] + if (routePoint.route_type === "via") { + return [routePoint.from_layer, routePoint.to_layer] + } + return [] + }), + ) + expect(signalLayers).not.toContain("inner1") + expect(signalLayers).not.toContain("inner2") +}) + +test("fanout infers plane nets from copper pours", async () => { + const { circuit } = getTestFixture() + const autoroutingPhaseIoStack = createAutoroutingPhaseIoStack(circuit) + + circuit.add( + + + + + + } + /> + + + , + ) + + await circuit.renderUntilSettled() + + expect(circuit.db.pcb_autorouting_error.list()).toEqual([]) + expect(autoroutingPhaseIoStack[0]!.startSimpleRouteJson!.buses).toEqual([ + { + busId: "GND_A1", + name: "GND_A1", + connectionNames: [ + autoroutingPhaseIoStack[0]!.startSimpleRouteJson!.connections[0]!.name, + ], + termination: { type: "plane", layer: "inner1" }, + }, + ]) + expect( + circuit.db.pcb_via.list().some((via) => via.to_layer === "inner1"), + ).toBe(true) +})