1- import type { AnyCircuitElement , PcbSmtPad } from "circuit-json"
1+ import type { AnyCircuitElement , PcbPlatedHole , PcbSmtPad } from "circuit-json"
22import 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"
45import { applyToPoint } from "transformation-matrix"
56
67const 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+
822function 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
1431function 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+
6990export 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
0 commit comments