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/base-components/Renderable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export const orderedRenderPhases = [
"PcbLayout",
"PcbBoardAutoSize",
"PanelLayout",
"PcbAutoplaceBreakoutPoints",
"PcbTraceHintRender",
"PcbManualTraceRender",
"PcbTraceRender",
Expand Down
30 changes: 30 additions & 0 deletions lib/components/primitive-components/Breakout/Breakout.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { breakoutProps } from "@tscircuit/props"
import { BreakoutPointSolver } from "@tscircuit/breakout-point-solver"
import { Group } from "../Group/Group"
import { AutoplacedBreakoutPoint } from "../AutoplacedBreakoutPoint"
import { BreakoutPoint } from "../BreakoutPoint"
import { Trace } from "../Trace/Trace"
import type { Port } from "../Port"
import type { z } from "zod"
import { createBreakoutPointSolverInput } from "./createBreakoutPointSolverInput"

export class Breakout extends Group<typeof breakoutProps> {
constructor(props: z.input<typeof breakoutProps>) {
Expand Down Expand Up @@ -72,6 +74,34 @@ export class Breakout extends Group<typeof breakoutProps> {
}
}

doInitialPcbAutoplaceBreakoutPoints(): void {
if (this.root?.pcbDisabled) return

const solverInput = createBreakoutPointSolverInput(this)
if (!solverInput) return

const solver = new BreakoutPointSolver(solverInput)
solver.solve()
const output = solver.getOutput()

const autoBreakoutPoints = this.children.filter(
(c) => c instanceof AutoplacedBreakoutPoint,
) as AutoplacedBreakoutPoint[]

for (const solvedPoint of output.breakoutPoints) {
const matchingBreakoutPoint = autoBreakoutPoints.find(
(child) =>
child.matchedPort?.source_port_id === solvedPoint.sourcePortId,
)
if (matchingBreakoutPoint) {
matchingBreakoutPoint._setPositionFromLayout({
x: solvedPoint.x,
y: solvedPoint.y,
})
}
}
}

doInitialPcbPrimitiveRender(): void {
super.doInitialPcbPrimitiveRender()
if (this.root?.pcbDisabled) return
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import type {
BreakoutPointSolverInput,
PcbLayer,
} from "@tscircuit/breakout-point-solver"
import {
type CircuitJsonUtilObjects,
findBoundsAndCenter,
} from "@tscircuit/circuit-json-util"
import type { PcbPort } from "circuit-json"
import type { Breakout } from "./Breakout"

const getPortLabel = (db: CircuitJsonUtilObjects, sourcePortId?: string) => {
if (!sourcePortId) return undefined
const sourcePort = db.source_port.get(sourcePortId)
const sourceComponent = sourcePort?.source_component_id
? db.source_component.get(sourcePort.source_component_id)
: undefined
if (!sourcePort) return undefined
return sourceComponent?.name
? `${sourceComponent.name}.${sourcePort.name}`
: sourcePort.name
}

const getPadElement = (db: CircuitJsonUtilObjects, pcbPortId: string) => {
return (
db.pcb_smtpad.getWhere({ pcb_port_id: pcbPortId }) ??
db.pcb_plated_hole.getWhere({ pcb_port_id: pcbPortId })
)
}

const toBreakoutPort = (db: CircuitJsonUtilObjects, pcbPort: PcbPort) => {
const pad = getPadElement(db, pcbPort.pcb_port_id)
const padBounds = pad ? findBoundsAndCenter([pad]) : null

return {
sourcePortId: pcbPort.source_port_id!,
position: { x: pcbPort.x!, y: pcbPort.y! },
...(padBounds ? { width: padBounds.width, height: padBounds.height } : {}),
layer: (pcbPort.layers?.[0] as PcbLayer) ?? "top",
label: getPortLabel(db, pcbPort.source_port_id),
}
}

export const createBreakoutPointSolverInput = (
breakout: Breakout,
): BreakoutPointSolverInput | null => {
if (!breakout.root || !breakout.pcb_group_id) return null

const { db } = breakout.root
const pcbGroup = db.pcb_group.get(breakout.pcb_group_id)
if (!pcbGroup || !pcbGroup.width || !pcbGroup.height) return null

const sourcePortIdToPcbPort = new Map<string, PcbPort>()
for (const pcbPort of db.pcb_port.list()) {
if (!pcbPort.source_port_id) continue
sourcePortIdToPcbPort.set(pcbPort.source_port_id, pcbPort)
}

const boundsMinX = pcbGroup.center.x - pcbGroup.width / 2
const boundsMaxX = pcbGroup.center.x + pcbGroup.width / 2
const boundsMinY = pcbGroup.center.y - pcbGroup.height / 2
const boundsMaxY = pcbGroup.center.y + pcbGroup.height / 2

const traces: BreakoutPointSolverInput["traces"] = []
for (const sourceTrace of db.source_trace.list()) {
const pcbPorts = sourceTrace.connected_source_port_ids
.map((sourcePortId) => sourcePortIdToPcbPort.get(sourcePortId))
.filter((port): port is PcbPort => Boolean(port))

const insidePorts = pcbPorts.filter(
(port) => port.pcb_group_id === breakout.pcb_group_id,
)
// Only include outside ports that are geometrically outside the bounds.
// Components may sit inside the boundary area without belonging to the
// breakout group (different pcb_group_id); passing them to the solver
// as "outside" would produce invalid boundary intersections.
const outsidePorts = pcbPorts.filter(
(port) =>
port.pcb_group_id !== breakout.pcb_group_id &&
!(
port.x! >= boundsMinX &&
port.x! <= boundsMaxX &&
port.y! >= boundsMinY &&
port.y! <= boundsMaxY
),
)

if (insidePorts.length === 0 || outsidePorts.length === 0) continue

traces.push({
sourceTraceId: sourceTrace.source_trace_id,
insidePorts: insidePorts.map((port) => toBreakoutPort(db, port)),
outsidePorts: outsidePorts.map((port) => toBreakoutPort(db, port)),
})
}

if (traces.length === 0) return null

const allPadElements = [...db.pcb_smtpad.list(), ...db.pcb_plated_hole.list()]
const pads: BreakoutPointSolverInput["pads"] = []
for (const pad of allPadElements) {
const padBounds = findBoundsAndCenter([pad])
if (!padBounds.width || !padBounds.height) continue
const pcbPort = (pad as any).pcb_port_id
? db.pcb_port.get((pad as any).pcb_port_id)
: null
pads.push({
center: padBounds.center,
width: padBounds.width,
height: padBounds.height,
layer: ((pad as any).layer as PcbLayer) ?? "top",
sourcePortIds: pcbPort?.source_port_id ? [pcbPort.source_port_id] : [],
label: getPortLabel(db, pcbPort?.source_port_id),
})
}

const components = db.pcb_component
.list()
.filter((component) => component.width && component.height)
.map((component) => ({
center: component.center,
width: component.width,
height: component.height,
ccwRotationDegrees: component.rotation,
layer: component.layer as PcbLayer | undefined,
label: component.pcb_component_id,
}))

const usedBoundaryPoints = db.pcb_breakout_point
.list()
.filter((point) => point.pcb_group_id === breakout.pcb_group_id)
.map((point) => ({ x: point.x, y: point.y }))

return {
bounds: {
minX: boundsMinX,
maxX: boundsMaxX,
minY: boundsMinY,
maxY: boundsMaxY,
},
boundaryPointSpacing: 0.5,
traces,
pads,
components,
usedBoundaryPoints,
}
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@biomejs/biome": "^1.8.3",
"@resvg/resvg-js": "^2.6.2",
"@tscircuit/alphabet": "0.0.25",
"@tscircuit/breakout-point-solver": "github:tscircuit/breakout-point-solver#bac9629",
"@tscircuit/capacity-autorouter": "^0.0.529",
"@tscircuit/checks": "0.0.133",
"@tscircuit/circuit-json-util": "^0.0.94",
Expand Down Expand Up @@ -74,7 +75,7 @@
"debug": "^4.3.6",
"eecircuit-engine": "^1.5.6",
"flatbush": "^4.5.0",
"graphics-debug": "^0.0.89",
"graphics-debug": "^0.0.95",
"howfat": "^0.3.8",
"kicad-to-circuit-json": "^0.0.60",
"kicadts": "^0.0.35",
Expand Down
Loading
Loading