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
52 changes: 42 additions & 10 deletions lib/components/normal-components/Board.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { boardProps } from "@tscircuit/props"
import type { AnyCircuitElement, LayerRef, PcbBoard } from "circuit-json"
import { getBoardAvailableLayers } from "lib/utils/getViaSpanLayers"
import { type Matrix, compose, translate } from "transformation-matrix"
import type { z } from "zod"
import { getDescendantSubcircuitIds } from "../../utils/autorouting/getAncestorSubcircuitIds"
import { getBoardCenterFromAnchor } from "../../utils/boards/get-board-center-from-anchor"
import { inflateCircuitJson } from "../../utils/circuit-json/inflate-circuit-json"
Expand All @@ -24,6 +25,7 @@ import { Subcircuit_doInitialRenderIsolatedSubcircuits } from "../primitive-comp
import { Subcircuit_getSubcircuitPropHash } from "../primitive-components/Group/Subcircuit_getSubcircuitPropHash"
import type { BoardI } from "./BoardI"
import { Board_doInitialPcbPlacementDesignRuleChecks } from "./Board_doInitialPcbPlacementDesignRuleChecks"
import { BoardCastellatedHole } from "./board-castellated-hole"

const MIN_EFFECTIVE_BORDER_RADIUS_MM = 0.01
const DEFAULT_VIA_PAD_DIAMETER_OVER_HOLE_DIAMETER_MM = 0.15
Expand Down Expand Up @@ -105,6 +107,19 @@ export class Board
_drcChecksInProgress = false
_connectedSchematicPortPairs = new Set<string>()
_panelPositionOffset: { x: number; y: number } | null = null
readonly _castellatedHoles: BoardCastellatedHole[]

constructor(props: z.input<typeof boardProps>) {
super(props)
this._castellatedHoles = BoardCastellatedHole.fromBoardOutline(
this._parsedProps.outline,
)
for (const castellatedHole of this._castellatedHoles) {
if (castellatedHole.port) this.add(castellatedHole.port)
this.add(castellatedHole)
if (castellatedHole.trace) this.add(castellatedHole.trace)
}
}

get isSubcircuit() {
return true
Expand Down Expand Up @@ -661,23 +676,31 @@ export class Board

if (shouldRunRoutingChecks) {
checksToRun.push(
runAllRoutingChecks(circuitJson) as Promise<AnyCircuitElement[]>,
runAllRoutingChecks(circuitJson).then((results) =>
results.filter(
(result) => !this._isExpectedCastellatedHoleDrcError(result),
),
) as Promise<AnyCircuitElement[]>,
)
}

if (shouldRunPlacementChecks) {
const existingPlacementDiagnostics = db.toArray()
checksToRun.push(
runAllPlacementChecks(circuitJson).then((results) =>
results.filter(
(result) =>
!existingPlacementDiagnostics.some(
(existing) =>
existing.type === result.type &&
"message" in existing &&
existing.message === result.message,
),
),
results
.filter(
(result) => !this._isExpectedCastellatedHoleDrcError(result),
)
.filter(
(result) =>
!existingPlacementDiagnostics.some(
(existing) =>
existing.type === result.type &&
"message" in existing &&
existing.message === result.message,
),
),
) as Promise<AnyCircuitElement[]>,
)
}
Expand Down Expand Up @@ -770,8 +793,17 @@ export class Board
db.pcb_board.update(this.pcb_board_id, {
outline: newOutline,
})
for (const castellatedHole of this._castellatedHoles) {
castellatedHole.syncPositionToBoardOutline()
}
}
}
}
}

_isExpectedCastellatedHoleDrcError(result: AnyCircuitElement): boolean {
return this._castellatedHoles.some((castellatedHole) =>
castellatedHole.isExpectedBoardEdgeDrcError(result),
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ export const Board_doInitialPcbPlacementDesignRuleChecks = (board: Board) => {
const placementCheckResults = await runAllPlacementChecks(
subcircuitCircuitJson,
)
const newPlacementDiagnostics = placementCheckResults.filter(
const relevantPlacementCheckResults = placementCheckResults.filter(
(result) => !board._isExpectedCastellatedHoleDrcError(result),
)
const newPlacementDiagnostics = relevantPlacementCheckResults.filter(
(result) =>
!existingPlacementDiagnostics.some(
(existing) =>
Expand All @@ -55,7 +58,7 @@ export const Board_doInitialPcbPlacementDesignRuleChecks = (board: Board) => {
)

db.insertAll(newPlacementDiagnostics as AnyCircuitElement[])
board._pcbPlacementDrcErrorCount = placementCheckResults.filter(
board._pcbPlacementDrcErrorCount = relevantPlacementCheckResults.filter(
(result) => result.type.endsWith("_error"),
).length
} catch (error) {
Expand Down
168 changes: 168 additions & 0 deletions lib/components/normal-components/board-castellated-hole.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import type { BoardOutlinePoint } from "@tscircuit/props"
import {
type AnyCircuitElement,
type PcbPlatedHoleCircle,
distance,
} from "circuit-json"
import { PlatedHole } from "../primitive-components/PlatedHole"
import { Port } from "../primitive-components/Port"
import { Trace } from "../primitive-components/Trace/Trace"
import type { Board } from "./Board"

const CASTELLATED_HOLE_ENDPOINT_TOLERANCE_MM = 1e-6

const getConnectionTargets = (
connectsTo: BoardOutlinePoint["connectsTo"],
): string[] => {
if (!connectsTo) return []
return Array.isArray(connectsTo) ? connectsTo : [connectsTo]
}

export class BoardCastellatedHole extends PlatedHole {
readonly outlinePointIndex: number
readonly holeDiameter: number
readonly padDiameter: number
readonly port: Port | null
readonly trace: Trace | null

static fromBoardOutline(
outline: BoardOutlinePoint[] | undefined,
): BoardCastellatedHole[] {
return (outline ?? []).flatMap((outlinePoint, outlinePointIndex) =>
outlinePoint.isCastellatedHole
? [new BoardCastellatedHole(outlinePoint, outlinePointIndex)]
: [],
)
}

constructor(outlinePoint: BoardOutlinePoint, outlinePointIndex: number) {
const name = `castellated_hole_${outlinePointIndex + 1}`
const connectionTargets = getConnectionTargets(outlinePoint.connectsTo)
const holeDiameter = distance.parse(outlinePoint.holeDiameter!)
const padDiameter = distance.parse(outlinePoint.padDiameter!)

super({
shape: "circle",
holeDiameter,
outerDiameter: padDiameter,
portHints: [name],
})

this.outlinePointIndex = outlinePointIndex
this.holeDiameter = holeDiameter
this.padDiameter = padDiameter
this.port = connectionTargets.length > 0 ? new Port({ name }) : null
this.trace = this.port
? new Trace({
path: [`port.${name}`, ...connectionTargets],
displayName: `Castellated hole ${outlinePointIndex + 1} connectivity`,
})
: null
}

private _getParentBoard(): Board {
if (this.parent?.componentName !== "Board") {
throw new Error("A board castellated hole must be a direct board child")
}
return this.parent as Board
}

/**
* Returns the castellation center as a point in the right-handed PCB world
* XY frame (+X right, +Y top), in millimeters. The emitted board outline is
* the canonical source, so this point already includes every translation.
*/
private _getPositionFromBoardOutline(): { x: number; y: number } {
const board = this._getParentBoard()
const pcbBoard = board.pcb_board_id
? board.root?.db.pcb_board.get(board.pcb_board_id)
: null
const outlinePoint = pcbBoard?.outline?.[this.outlinePointIndex]
if (!outlinePoint) {
throw new Error(
`Missing emitted board outline point ${this.outlinePointIndex}`,
)
}
return outlinePoint
}

override _getGlobalPcbPositionBeforeLayout(): { x: number; y: number } {
return this._getPositionFromBoardOutline()
}

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

const { db } = this.root!
const position = this._getPositionFromBoardOutline()
const pcbPlatedHole = db.pcb_plated_hole.insert({
shape: "circle",
outer_diameter: this.padDiameter,
hole_diameter: this.holeDiameter,
...position,
layers: this.getAvailablePcbLayers(),
port_hints: this.getNameAndAliases(),
subcircuit_id: this.getSubcircuit()?.subcircuit_id ?? undefined,
} as Omit<PcbPlatedHoleCircle, "type" | "pcb_plated_hole_id">)
this.pcb_plated_hole_id = pcbPlatedHole.pcb_plated_hole_id
}

syncPositionToBoardOutline(): void {
if (!this.pcb_plated_hole_id) return
this._setPositionFromLayout(this._getPositionFromBoardOutline())
}

removePcbPrimitiveRender(): void {
const { db } = this.root!
if (this.pcb_plated_hole_id) {
db.pcb_plated_hole.delete(this.pcb_plated_hole_id)
this.pcb_plated_hole_id = null
}
if (this.matchedPort?.pcb_port_id) {
db.pcb_port.delete(this.matchedPort.pcb_port_id)
this.matchedPort.pcb_port_id = null
}
}

isExpectedBoardEdgeDrcError(result: AnyCircuitElement): boolean {
if (!this.pcb_plated_hole_id) return false

if (result.type === "pcb_placement_error") {
return (
result.pcb_placement_error_id ===
`copper_too_close_to_board_edge_${this.pcb_plated_hole_id}`
)
}

if (
result.type !== "pcb_trace_error" ||
!result.pcb_trace_error_id.startsWith("trace_too_close_to_board_")
) {
return false
}

const segmentIndexMatch = result.pcb_trace_error_id.match(/_segment_(\d+)$/)
if (!segmentIndexMatch) return false

const pcbTrace = this.root?.db.pcb_trace.get(result.pcb_trace_id)
const segmentIndex = Number(segmentIndexMatch[1])
const segmentEndpoints = [
pcbTrace?.route[segmentIndex],
pcbTrace?.route[segmentIndex + 1],
].flatMap((routePoint) =>
routePoint && "x" in routePoint && "y" in routePoint
? [{ x: routePoint.x, y: routePoint.y }]
: [],
)
const platedHole = this.root?.db.pcb_plated_hole.get(
this.pcb_plated_hole_id,
)
if (!platedHole) return false

return segmentEndpoints.some(
(endpoint) =>
Math.hypot(endpoint.x - platedHole.x, endpoint.y - platedHole.y) <=
CASTELLATED_HOLE_ENDPOINT_TOLERANCE_MM,
)
}
}
2 changes: 1 addition & 1 deletion lib/components/primitive-components/Port/Port.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export class Port extends PrimitiveComponent<typeof portProps> {
}

isGroupPort(): boolean {
return this.parent?.componentName === "Group"
return this.parent?.isGroup === true
}

isComponentPort(): boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,37 @@
import type { Port } from "./Port"
import { areAllPcbPrimitivesOverlapping } from "./areAllPcbPrimitivesOverlapping"
import { getCenterOfPcbPrimitives } from "./getCenterOfPcbPrimitives"

export function Port_tryRenderGroupPcbPort(port: Port): boolean {
if (port.root?.pcbDisabled) return false
if (port.pcb_port_id) return true

const { db } = port.root!
const matchedPcbPrimitives = port.matchedComponents.filter(
(component) => component.isPcbPrimitive,
)
const matchedPrimitiveCenter =
matchedPcbPrimitives.length === 1
? matchedPcbPrimitives[0]._getPcbCircuitJsonBounds().center
: matchedPcbPrimitives.length > 1 &&
areAllPcbPrimitivesOverlapping(matchedPcbPrimitives)
? getCenterOfPcbPrimitives(matchedPcbPrimitives)
: null

if (matchedPrimitiveCenter) {
const pcbPort = db.pcb_port.insert({
pcb_component_id: undefined as any,
layers: port.getAvailablePcbLayers(),
subcircuit_id: port.getSubcircuit()?.subcircuit_id ?? undefined,
pcb_group_id: port.getGroup()?.pcb_group_id ?? undefined,
...matchedPrimitiveCenter,
source_port_id: port.source_port_id!,
is_board_pinout: port.parent?.componentName === "Board",
})
port.pcb_port_id = pcbPort.pcb_port_id
return true
}

const connectedPort = port._getConnectedPortsFromConnectsTo()[0]
if (!connectedPort?.pcb_port_id) return false

Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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.635",
"@tscircuit/props": "^0.0.636",
"@tscircuit/schematic-match-adapt": "^0.0.18",
"@tscircuit/schematic-trace-solver": "^0.0.159",
"@tscircuit/solver-utils": "^0.0.16",
Expand Down Expand Up @@ -130,7 +130,7 @@
},
"overrides": {
"@tscircuit/circuit-json-util": "^0.0.106",
"@tscircuit/props": "^0.0.635",
"@tscircuit/props": "^0.0.636",
"circuit-json": "^0.0.476"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading