Skip to content

Commit 9fe8c8b

Browse files
authored
Render KiCad DSN through-hole pads as plated holes (#489)
* Fix KiCad DSN through-hole pad import * update
1 parent c67bb3d commit 9fe8c8b

9 files changed

Lines changed: 437 additions & 74 deletions

lib/dsn-pcb/dsn-json-to-circuit-json/dsn-component-converters/convert-padstacks-to-smtpads.ts

Lines changed: 136 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,31 @@
1-
import type { AnyCircuitElement, PcbSmtPad } from "circuit-json"
1+
import type { AnyCircuitElement, PcbPlatedHole, PcbSmtPad } from "circuit-json"
22
import Debug from "debug"
3-
import type { DsnPcb } from "lib/dsn-pcb/types"
3+
import type { DsnPcb, Padstack } from "lib/dsn-pcb/types"
4+
import { parsePadstackName } from "lib/utils/get-padstack-name"
45
import { applyToPoint } from "transformation-matrix"
56

67
const debug = Debug("dsn-converter:convertPadstacksToSmtpads")
78

9+
// Layer name sets for front and back copper — covers KiCad (F.Cu/B.Cu) and
10+
// Freerouting/other tools (Top/Bottom)
11+
const FRONT_COPPER = new Set(["F.Cu", "Top", "F_Cu"])
12+
const BACK_COPPER = new Set(["B.Cu", "Bottom", "B_Cu"])
13+
14+
function isThruHolePadstack(padstack: Padstack): boolean {
15+
const layers = padstack.shapes.map((s) => s.layer)
16+
return (
17+
layers.some((l) => FRONT_COPPER.has(l)) &&
18+
layers.some((l) => BACK_COPPER.has(l))
19+
)
20+
}
21+
822
function getLayerFromPadstack(
923
padstack: DsnPcb["library"]["padstacks"][number],
1024
) {
11-
return padstack.shapes[0].layer.includes("B.") ? "bottom" : "top"
25+
return padstack.shapes[0].layer.includes("B.") ||
26+
padstack.shapes[0].layer === "Bottom"
27+
? "bottom"
28+
: "top"
1229
}
1330

1431
function getPolygonPoints(
@@ -66,6 +83,10 @@ function getRectangleDimensionsFromPolygon(coordinates: number[]) {
6683
}
6784
}
6885

86+
function isApproximatelyEqual(a: number, b: number) {
87+
return Math.abs(a - b) < 1e-6
88+
}
89+
6990
export function convertPadstacksToSmtPads(
7091
pcb: DsnPcb,
7192
dsnToCircuitJsonTransform: any,
@@ -85,13 +106,11 @@ export function convertPadstacksToSmtPads(
85106
return
86107
}
87108

88-
// Handle each placement for this component
89109
placementComponent.places.forEach((place) => {
90110
debug("processing place...", { place })
91111
const { x: compX, y: compY, side } = place
92112

93113
image.pins.forEach((pin) => {
94-
// Find the corresponding padstack
95114
const padstack = padstacks.find((p) => p.name === pin.padstack_name)
96115
debug("found padstack", { padstack })
97116

@@ -100,22 +119,118 @@ export function convertPadstacksToSmtPads(
100119
return
101120
}
102121

103-
// Find shape in padstack - try rectangle first, then polygon
104-
const rectShape = padstack.shapes.find(
105-
(shape) => shape.shapeType === "rect",
122+
const { x: circuitX, y: circuitY } = applyToPoint(
123+
dsnToCircuitJsonTransform,
124+
{
125+
x: (compX || 0) + pin.x,
126+
y: (compY || 0) + pin.y,
127+
},
106128
)
107129

130+
const commonIds = {
131+
pcb_component_id: `${componentId}_${place.refdes}`,
132+
pcb_port_id: `pcb_port_${componentId}-Pad${pin.pin_number}_${place.refdes}`,
133+
port_hints: [pin.pin_number.toString()],
134+
}
135+
const pcbPlatedHoleId = `pcb_plated_hole_${componentId}_${place.refdes}_${pin.pin_number}`
136+
const parsedPadstackName = parsePadstackName(padstack.name)
137+
138+
// ── Through-hole detection ──────────────────────────────────────────
139+
if (isThruHolePadstack(padstack)) {
140+
const circleShape = padstack.shapes.find(
141+
(s) => s.shapeType === "circle",
142+
)
143+
const pathShape = padstack.shapes.find((s) => s.shapeType === "path")
144+
145+
if (circleShape && circleShape.shapeType === "circle") {
146+
const outerDiameter = circleShape.diameter / 1000
147+
148+
// Prefer explicit hole from parser, then name convention, then 60% estimate
149+
const holeDiameter =
150+
padstack.hole?.diameter !== undefined
151+
? padstack.hole.diameter / 1000
152+
: parsedPadstackName?.shape === "circle"
153+
? parsedPadstackName.holeDiameter
154+
: outerDiameter * 0.6
155+
156+
const platedHole: PcbPlatedHole = {
157+
type: "pcb_plated_hole",
158+
pcb_plated_hole_id: pcbPlatedHoleId,
159+
...commonIds,
160+
shape: "circle",
161+
x: circuitX,
162+
y: circuitY,
163+
outer_diameter: outerDiameter,
164+
hole_diameter: holeDiameter,
165+
layers: ["top", "bottom"],
166+
}
167+
elements.push(platedHole)
168+
return
169+
}
170+
171+
if (pathShape && pathShape.shapeType === "path") {
172+
const [x1, y1, x2, y2] = pathShape.coordinates
173+
const strokeWidth = pathShape.width / 1000
174+
const endpointDist =
175+
Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) / 1000
176+
const isHorizontal = Math.abs(x2 - x1) >= Math.abs(y2 - y1)
177+
178+
const major = endpointDist + strokeWidth
179+
const minor = strokeWidth
180+
const outerWidth = isHorizontal ? major : minor
181+
const outerHeight = isHorizontal ? minor : major
182+
183+
let holeWidth: number
184+
let holeHeight: number
185+
if (padstack.hole?.width !== undefined) {
186+
holeWidth = padstack.hole.width / 1000
187+
holeHeight = (padstack.hole.height ?? padstack.hole.width) / 1000
188+
} else {
189+
const parsedNameMatchesOuterDimensions =
190+
parsedPadstackName?.shape === "oval" &&
191+
isApproximatelyEqual(parsedPadstackName.width, outerWidth) &&
192+
isApproximatelyEqual(parsedPadstackName.height, outerHeight)
193+
194+
if (
195+
parsedPadstackName?.shape === "oval" &&
196+
!parsedNameMatchesOuterDimensions
197+
) {
198+
holeWidth = parsedPadstackName.width
199+
holeHeight = parsedPadstackName.height
200+
} else {
201+
holeWidth = outerWidth * 0.6
202+
holeHeight = outerHeight * 0.6
203+
}
204+
}
205+
206+
const platedHole: PcbPlatedHole = {
207+
type: "pcb_plated_hole",
208+
pcb_plated_hole_id: pcbPlatedHoleId,
209+
...commonIds,
210+
shape: "oval",
211+
x: circuitX,
212+
y: circuitY,
213+
outer_width: outerWidth,
214+
outer_height: outerHeight,
215+
hole_width: holeWidth,
216+
hole_height: holeHeight,
217+
ccw_rotation: 0,
218+
layers: ["top", "bottom"],
219+
}
220+
elements.push(platedHole)
221+
return
222+
}
223+
}
224+
225+
// ── SMT pad (single-layer or unrecognised padstack) ─────────────────
226+
const rectShape = padstack.shapes.find((s) => s.shapeType === "rect")
108227
const polygonShape = padstack.shapes.find(
109-
(shape) => shape.shapeType === "polygon",
228+
(s) => s.shapeType === "polygon",
110229
)
111-
112230
const circleShape = padstack.shapes.find(
113-
(shape) => shape.shapeType === "circle",
114-
)
115-
116-
const pathShape = padstack.shapes.find(
117-
(shape) => shape.shapeType === "path",
231+
(s) => s.shapeType === "circle",
118232
)
233+
const pathShape = padstack.shapes.find((s) => s.shapeType === "path")
119234

120235
debug("found shapes", {
121236
rectShape,
@@ -128,39 +243,30 @@ export function convertPadstacksToSmtPads(
128243
let height: number
129244

130245
if (rectShape) {
131-
// Handle rectangle shape
132246
const [x1, y1, x2, y2] = rectShape.coordinates
133-
width = Math.abs(x2 - x1) / 1000 // Convert μm to mm
134-
height = Math.abs(y2 - y1) / 1000 // Convert μm to mm
247+
width = Math.abs(x2 - x1) / 1000
248+
height = Math.abs(y2 - y1) / 1000
135249
} else if (polygonShape) {
136-
// Handle polygon shape
137250
const coordinates = polygonShape.coordinates
138251
let minX = Infinity
139252
let maxX = -Infinity
140253
let minY = Infinity
141254
let maxY = -Infinity
142-
143-
// Coordinates are in pairs (x,y), so iterate by 2
144255
for (let i = 0; i < coordinates.length; i += 2) {
145256
const x = coordinates[i]
146257
const y = coordinates[i + 1]
147-
148258
minX = Math.min(minX, x)
149259
maxX = Math.max(maxX, x)
150260
minY = Math.min(minY, y)
151261
maxY = Math.max(maxY, y)
152262
}
153-
154263
width = Math.abs(maxX - minX) / 1000
155264
height = Math.abs(maxY - minY) / 1000
156265
} else if (pathShape) {
157-
// For path shapes (oval/pill pads), width is the path width
158-
// and height is the distance between path endpoints
159266
const [x1, y1, x2, y2] = pathShape.coordinates
160-
width = pathShape.width / 1000 // Convert μm to mm
267+
width = pathShape.width / 1000
161268
height = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2) / 1000
162269
} else if (circleShape) {
163-
// Handle circle shape
164270
const radius = circleShape.diameter / 2 / 1000
165271
width = radius
166272
height = radius
@@ -169,16 +275,6 @@ export function convertPadstacksToSmtPads(
169275
return
170276
}
171277

172-
// Calculate position in circuit space using the transformation matrix
173-
// Convert component position and pin offset to circuit coordinates
174-
const { x: circuitX, y: circuitY } = applyToPoint(
175-
dsnToCircuitJsonTransform,
176-
{
177-
x: (compX || 0) + pin.x,
178-
y: (compY || 0) + pin.y,
179-
},
180-
)
181-
182278
let pcbPad: PcbSmtPad
183279
const rectangleDimensionsFromPolygon = polygonShape
184280
? getRectangleDimensionsFromPolygon(polygonShape.coordinates)
@@ -188,54 +284,40 @@ export function convertPadstacksToSmtPads(
188284

189285
if (polygonShape && !shouldImportPolygonAsRect) {
190286
const layer = getLayerFromPadstack(padstack)
191-
debug("determining layer with padstack shapes", {
192-
shapes: padstack.shapes,
193-
layer,
194-
})
195287
pcbPad = {
196288
type: "pcb_smtpad",
197289
pcb_smtpad_id: `pcb_smtpad_${componentId}_${place.refdes}_${Number(pin.pin_number) - 1}`,
198-
pcb_component_id: `${componentId}_${place.refdes}`,
199-
pcb_port_id: `pcb_port_${componentId}-Pad${pin.pin_number}_${place.refdes}`,
290+
...commonIds,
200291
shape: "polygon",
201292
points: getPolygonPoints(polygonShape.coordinates, {
202293
x: circuitX,
203294
y: circuitY,
204295
}),
205296
layer,
206-
port_hints: [pin.pin_number.toString()],
207297
}
208298
} else if (rectShape || pathShape || shouldImportPolygonAsRect) {
209299
const layer = getLayerFromPadstack(padstack)
210-
debug("determining layer with padstack shapes", {
211-
shapes: padstack.shapes,
212-
layer,
213-
})
214300
pcbPad = {
215301
type: "pcb_smtpad",
216302
pcb_smtpad_id: `pcb_smtpad_${componentId}_${place.refdes}_${Number(pin.pin_number) - 1}`,
217-
pcb_component_id: `${componentId}_${place.refdes}`,
218-
pcb_port_id: `pcb_port_${componentId}-Pad${pin.pin_number}_${place.refdes}`,
303+
...commonIds,
219304
shape: "rect",
220305
x: circuitX,
221306
y: circuitY,
222307
width: rectangleDimensionsFromPolygon?.width ?? width,
223308
height: rectangleDimensionsFromPolygon?.height ?? height,
224309
layer,
225-
port_hints: [pin.pin_number.toString()],
226310
}
227311
} else {
228312
pcbPad = {
229313
type: "pcb_smtpad",
230314
pcb_smtpad_id: `pcb_smtpad_${componentId}_${place.refdes}_${Number(pin.pin_number) - 1}`,
231-
pcb_component_id: `${componentId}_${place.refdes}`,
232-
pcb_port_id: `pcb_port_${componentId}-Pad${pin.pin_number}_${place.refdes}`,
315+
...commonIds,
233316
shape: "circle",
234317
x: circuitX,
235318
y: circuitY,
236319
radius: circleShape!.diameter / 2 / 1000,
237320
layer: side === "front" ? "top" : "bottom",
238-
port_hints: [pin.pin_number.toString()],
239321
}
240322
}
241323

lib/dsn-pcb/dsn-json-to-circuit-json/parse-dsn-to-dsn-json.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -659,6 +659,22 @@ function processPadstack(nodes: ASTNode[]): Padstack {
659659
if (rest[0].type === "Atom" && typeof rest[0].value === "string") {
660660
padstack.attach = rest[0].value
661661
}
662+
} else if (key === "hole") {
663+
if (rest[0]?.type === "Atom") {
664+
if (typeof rest[0].value === "number") {
665+
padstack.hole = { shape: "circle", diameter: rest[0].value }
666+
} else if (
667+
rest[0].value === "oval" &&
668+
rest[1]?.type === "Atom" &&
669+
rest[2]?.type === "Atom"
670+
) {
671+
padstack.hole = {
672+
shape: "oval",
673+
width: rest[1].value as number,
674+
height: rest[2].value as number,
675+
}
676+
}
677+
}
662678
}
663679
}
664680
}

0 commit comments

Comments
 (0)