diff --git a/lib/components/index.ts b/lib/components/index.ts index 25c3e74df..9bee95294 100644 --- a/lib/components/index.ts +++ b/lib/components/index.ts @@ -58,6 +58,7 @@ export { SilkscreenGraphic } from "./primitive-components/SilkscreenGraphic" export { SmtPad } from "./primitive-components/SmtPad" export { Fiducial } from "./primitive-components/Fiducial" export { Trace } from "./primitive-components/Trace/Trace" +export { Bus } from "./primitive-components/Bus" export { DifferentialPair } from "./primitive-components/DifferentialPair" export { PcbTrace } from "./primitive-components/PcbTrace" export { PcbVia } from "./primitive-components/PcbVia" diff --git a/lib/components/primitive-components/Bus.ts b/lib/components/primitive-components/Bus.ts new file mode 100644 index 000000000..d87512e72 --- /dev/null +++ b/lib/components/primitive-components/Bus.ts @@ -0,0 +1,17 @@ +import { busProps } from "@tscircuit/props" +import { + type BaseComponentConfig, + PrimitiveComponent, +} from "../base-components/PrimitiveComponent" + +/** + * Declares a group of connections that an autorouter should keep together. + */ +export class Bus extends PrimitiveComponent { + override get config(): BaseComponentConfig { + return { + componentName: "Bus", + zodProps: busProps, + } + } +} diff --git a/lib/fiber/intrinsic-jsx.ts b/lib/fiber/intrinsic-jsx.ts index 20251a63e..a0c09349c 100644 --- a/lib/fiber/intrinsic-jsx.ts +++ b/lib/fiber/intrinsic-jsx.ts @@ -47,6 +47,7 @@ export interface TscircuitElements { cadassembly: Props.CadAssemblyProps net: Props.NetProps trace: Props.TraceProps + bus: Props.BusProps differentialpair: Props.DifferentialPairProps breakout: Props.BreakoutProps breakoutpoint: Props.BreakoutPointProps diff --git a/lib/utils/autorouting/SimpleRouteJson.ts b/lib/utils/autorouting/SimpleRouteJson.ts index 5bebfb75a..f4a5f79fd 100644 --- a/lib/utils/autorouting/SimpleRouteJson.ts +++ b/lib/utils/autorouting/SimpleRouteJson.ts @@ -98,6 +98,12 @@ export type SimpleRouteDifferentialPair = { lengthTolerance: number } +/** A group of connections that an autorouter should keep together. */ +export type SimpleRouteBus = { + name?: string + connectionNames: SrjConnectionName[] +} + export type SimpleRouteJson = Omit< AutorouterSimpleRouteJson, | "connections" @@ -108,6 +114,7 @@ export type SimpleRouteJson = Omit< | "allowJumpers" | "availableJumperTypes" | "differentialPairs" + | "buses" > & { layerCount: number minTraceWidth: number @@ -143,6 +150,7 @@ export type SimpleRouteJson = Omit< allowJumpers?: boolean availableJumperTypes?: Array<"1206x4" | "0603"> differentialPairs?: SimpleRouteDifferentialPair[] + buses?: SimpleRouteBus[] } // declare module "autorouting-dataset" { diff --git a/lib/utils/autorouting/getBusesForSimpleRouteJson.ts b/lib/utils/autorouting/getBusesForSimpleRouteJson.ts new file mode 100644 index 000000000..4a6fdef9d --- /dev/null +++ b/lib/utils/autorouting/getBusesForSimpleRouteJson.ts @@ -0,0 +1,156 @@ +import type { 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 { + SimpleRouteBus, + SimpleRouteConnection, + SrjConnectionName, +} from "./SimpleRouteJson" + +type SourcePortId = NonNullable +type SubcircuitId = NonNullable +type SubcircuitConnectivityMapKey = NonNullable< + SourceTrace["subcircuit_connectivity_map_key"] +> + +type GetBusesParams = { + srjConnections: SimpleRouteConnection[] + buses: Bus[] + sourceTraces: SourceTrace[] + subcircuitId?: SubcircuitId | null +} + +const getBusSourceTraceSubcircuitConnectivityMapKeyOrThrow = ({ + bus, + busSourceTraces, + traceNameOrPortSelector, +}: { + bus: Bus + busSourceTraces: SourceTrace[] + traceNameOrPortSelector: string +}): SubcircuitConnectivityMapKey => { + const sourceTracesWithMatchingName = busSourceTraces.filter( + (sourceTrace) => sourceTrace.name === traceNameOrPortSelector, + ) + const selectedPort = + sourceTracesWithMatchingName.length === 0 + ? bus.getSubcircuit().selectOne(traceNameOrPortSelector, { + type: "port", + }) + : null + const selectedSourcePortId: SourcePortId | undefined = + selectedPort?.source_port_id ?? undefined + const matchingSourceTraces = selectedSourcePortId + ? busSourceTraces.filter((sourceTrace) => + sourceTrace.connected_source_port_ids.includes(selectedSourcePortId), + ) + : sourceTracesWithMatchingName + + if (matchingSourceTraces.length === 0) { + throw new Error( + `Could not find source trace for trace name or port selector "${traceNameOrPortSelector}" in bus "${bus.name}"`, + ) + } + if (matchingSourceTraces.length > 1) { + throw new Error( + `Trace name or port selector "${traceNameOrPortSelector}" matches multiple source traces in bus "${bus.name}"`, + ) + } + + const sourceTrace = matchingSourceTraces[0] + if (!sourceTrace?.subcircuit_connectivity_map_key) { + throw new Error( + `Source trace for "${traceNameOrPortSelector}" does not have a subcircuit connectivity map key in bus "${bus.name}"`, + ) + } + + return sourceTrace.subcircuit_connectivity_map_key +} + +const getBusSrjConnectionNameOrThrow = ({ + srjConnections, + bus, + busSourceTraces, + traceSubcircuitConnectivityMapKey, + traceNameOrPortSelector, +}: { + srjConnections: SimpleRouteConnection[] + bus: Bus + busSourceTraces: SourceTrace[] + traceSubcircuitConnectivityMapKey: SubcircuitConnectivityMapKey + traceNameOrPortSelector: string +}): SrjConnectionName => { + const sourceTraceIds = busSourceTraces + .filter( + (sourceTrace) => + sourceTrace.subcircuit_connectivity_map_key === + traceSubcircuitConnectivityMapKey, + ) + .map((sourceTrace) => sourceTrace.source_trace_id) + const matchingSrjConnections = srjConnections.filter( + (srjConnection) => + srjConnection.source_trace_id && + sourceTraceIds.includes(srjConnection.source_trace_id), + ) + + if (matchingSrjConnections.length === 0) { + throw new Error( + `Could not find an SRJ connection for "${traceNameOrPortSelector}" in bus "${bus.name}"`, + ) + } + if (matchingSrjConnections.length > 1) { + throw new Error( + `Trace name or port selector "${traceNameOrPortSelector}" matches multiple SRJ connections in bus "${bus.name}"`, + ) + } + + return matchingSrjConnections[0]!.name +} + +/** Converts bus trace names or port selectors into SRJ constraints. */ +export const getBusesForSimpleRouteJson = ({ + srjConnections, + buses, + sourceTraces, + subcircuitId, +}: GetBusesParams): SimpleRouteBus[] | undefined => { + const srjBuses: SimpleRouteBus[] = [] + for (const bus of buses) { + const busSubcircuitId = bus.getSubcircuit().subcircuit_id + if (subcircuitId && busSubcircuitId !== subcircuitId) continue + + const busSourceTraces = sourceTraces.filter( + (sourceTrace) => sourceTrace.subcircuit_id === busSubcircuitId, + ) + const connectionNames = bus._parsedProps.connections.map( + (traceNameOrPortSelector) => { + const traceSubcircuitConnectivityMapKey = + getBusSourceTraceSubcircuitConnectivityMapKeyOrThrow({ + bus, + busSourceTraces, + traceNameOrPortSelector, + }) + return getBusSrjConnectionNameOrThrow({ + srjConnections, + bus, + busSourceTraces, + traceSubcircuitConnectivityMapKey, + traceNameOrPortSelector, + }) + }, + ) + + if (new Set(connectionNames).size !== connectionNames.length) { + throw new Error( + `Bus "${bus.name}" resolves multiple entries to one trace`, + ) + } + + srjBuses.push({ + name: bus.name, + connectionNames, + }) + } + + return srjBuses.length > 0 ? srjBuses : undefined +} diff --git a/lib/utils/autorouting/getSimpleRouteJsonFromCircuitJson.ts b/lib/utils/autorouting/getSimpleRouteJsonFromCircuitJson.ts index 2b1c377ed..fcace0b0e 100644 --- a/lib/utils/autorouting/getSimpleRouteJsonFromCircuitJson.ts +++ b/lib/utils/autorouting/getSimpleRouteJsonFromCircuitJson.ts @@ -5,6 +5,7 @@ import { ConnectivityMap, getFullConnectivityMapFromCircuitJson, } from "circuit-json-to-connectivity-map" +import { Bus } from "lib/components/primitive-components/Bus" import { DifferentialPair } from "lib/components/primitive-components/DifferentialPair" import type { ISubcircuit } from "lib/components/primitive-components/Group/Subcircuit/ISubcircuit" import { getObstaclesFromCircuitJson } from "../obstacles/getObstaclesFromCircuitJson" @@ -14,6 +15,7 @@ import type { SimpleRouteJson, } from "./SimpleRouteJson" import { getDescendantSubcircuitIds } from "./getAncestorSubcircuitIds" +import { getBusesForSimpleRouteJson } from "./getBusesForSimpleRouteJson" import { getDifferentialPairsForSimpleRouteJson } from "./getDifferentialPairsForSimpleRouteJson" import { getPreservedRoutedSubcircuitTraces } from "./getPreservedRoutedSubcircuitTraces" import { getUnbrokenCopperPourObstacles } from "./getUnbrokenCopperPourObstacles" @@ -658,6 +660,8 @@ export const getSimpleRouteJsonFromCircuitJson = ({ const differentialPairs: DifferentialPair[] = subcircuitComponent?.selectAll("differentialpair") ?? [] + const buses: Bus[] = subcircuitComponent?.selectAll("bus") ?? [] + const srjDifferentialPairs: SimpleRouteDifferentialPair[] | undefined = getDifferentialPairsForSimpleRouteJson({ srjConnections: allConns, @@ -666,6 +670,13 @@ export const getSimpleRouteJsonFromCircuitJson = ({ subcircuitId: subcircuit_id, }) + const srjBuses = getBusesForSimpleRouteJson({ + srjConnections: allConns, + buses, + sourceTraces: db.source_trace.list(), + subcircuitId: subcircuit_id, + }) + if (subcircuit_id) { const pointIdToConn = new Map() for (const conn of allConns) { @@ -724,6 +735,7 @@ export const getSimpleRouteJsonFromCircuitJson = ({ obstacles, connections: allConns, differentialPairs: srjDifferentialPairs, + buses: srjBuses, traces: preservedRoutedSubcircuitTraces.length > 0 ? preservedRoutedSubcircuitTraces diff --git a/package.json b/package.json index 74404d347..15392bb21 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "@tscircuit/math-utils": "^0.0.36", "@tscircuit/miniflex": "^0.0.4", "@tscircuit/ngspice-spice-engine": "^0.0.20", - "@tscircuit/props": "^0.0.592", + "@tscircuit/props": "^0.0.593", "@tscircuit/schematic-match-adapt": "^0.0.18", "@tscircuit/schematic-trace-solver": "^0.0.111", "@tscircuit/solver-utils": "^0.0.16", diff --git a/tests/components/primitive-components/bus/__snapshots__/trace-name-pcb.snap.svg b/tests/components/primitive-components/bus/__snapshots__/trace-name-pcb.snap.svg new file mode 100644 index 000000000..047094f40 --- /dev/null +++ b/tests/components/primitive-components/bus/__snapshots__/trace-name-pcb.snap.svg @@ -0,0 +1 @@ +U1U2DATA bus: D0, D1, D2, D3 \ No newline at end of file diff --git a/tests/components/primitive-components/bus/trace-name.test.tsx b/tests/components/primitive-components/bus/trace-name.test.tsx new file mode 100644 index 000000000..87ee4c698 --- /dev/null +++ b/tests/components/primitive-components/bus/trace-name.test.tsx @@ -0,0 +1,52 @@ +import { expect, test } from "bun:test" +import { getSimpleRouteJsonFromCircuitJson } from "lib/utils/autorouting/getSimpleRouteJsonFromCircuitJson" +import { getTestFixture } from "tests/fixtures/get-test-fixture" + +test("registers a bus routing constraint from trace names", (): void => { + const { circuit } = getTestFixture() + + circuit.add( + + + + + + + + + + , + ) + + circuit.render() + + const boardSubcircuit = circuit.firstChild + if (!boardSubcircuit) { + throw new Error("Expected the circuit to contain a board") + } + const circuitJsonWithoutPcbTraces = circuit + .getCircuitJson() + .filter((element) => element.type !== "pcb_trace") + const { simpleRouteJson } = getSimpleRouteJsonFromCircuitJson({ + circuitJson: circuitJsonWithoutPcbTraces, + subcircuitComponent: boardSubcircuit, + }) + const sourceTraceIds = ["D0", "D1", "D2", "D3"].map((name) => { + const sourceTrace = circuit.db.source_trace.getWhere({ name }) + if (!sourceTrace) throw new Error(`Expected the ${name} source trace`) + return sourceTrace.source_trace_id + }) + + expect(simpleRouteJson.buses).toEqual([ + { + name: "DATA", + connectionNames: sourceTraceIds, + }, + ]) + expect(circuit).toMatchPcbSnapshot(import.meta.path) +})