pcb-scene3d-viewer 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/AGENTS.md +67 -0
  2. package/COMMERCIAL-LICENSE.md +15 -0
  3. package/CONTRIBUTING.md +14 -0
  4. package/LICENSE +19 -0
  5. package/LICENSES/AGPL-3.0-or-later.txt +235 -0
  6. package/LICENSES/CC-BY-SA-4.0.txt +170 -0
  7. package/LICENSES/LGPL-2.1-or-later.txt +176 -0
  8. package/LICENSES/LicenseRef-PolyForm-Noncommercial-1.0.0.txt +131 -0
  9. package/NOTICE.md +36 -0
  10. package/README.md +128 -0
  11. package/REUSE.toml +16 -0
  12. package/docs/api.md +148 -0
  13. package/docs/circuitjson.md +190 -0
  14. package/docs/model-format.md +117 -0
  15. package/docs/testing.md +23 -0
  16. package/package.json +65 -0
  17. package/spec/library-scope.md +36 -0
  18. package/src/PcbModelArchiveExporter.mjs +320 -0
  19. package/src/PcbScene3dArcUtils.mjs +27 -0
  20. package/src/PcbScene3dBoardAssemblyPlacement.mjs +36 -0
  21. package/src/PcbScene3dBoardAssemblyPresentation.mjs +859 -0
  22. package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +537 -0
  23. package/src/PcbScene3dBoardMaterialPalette.mjs +40 -0
  24. package/src/PcbScene3dBoardShapeFactory.mjs +895 -0
  25. package/src/PcbScene3dBoardSolderMaskFactory.mjs +613 -0
  26. package/src/PcbScene3dCameraRig.mjs +168 -0
  27. package/src/PcbScene3dCircuitJsonAdapter.mjs +545 -0
  28. package/src/PcbScene3dController.mjs +956 -0
  29. package/src/PcbScene3dCopperDetailFilter.mjs +490 -0
  30. package/src/PcbScene3dCopperFactory.mjs +559 -0
  31. package/src/PcbScene3dCopperTextFactory.mjs +534 -0
  32. package/src/PcbScene3dCutoutGeometryFilter.mjs +873 -0
  33. package/src/PcbScene3dDetailCoordinateNormalizer.mjs +65 -0
  34. package/src/PcbScene3dDrillCutoutFilter.mjs +224 -0
  35. package/src/PcbScene3dDrillPathFactory.mjs +362 -0
  36. package/src/PcbScene3dDrillVoidFactory.mjs +268 -0
  37. package/src/PcbScene3dExternalModelLoadOrder.mjs +54 -0
  38. package/src/PcbScene3dExternalModels.mjs +968 -0
  39. package/src/PcbScene3dFallbackVisibility.mjs +82 -0
  40. package/src/PcbScene3dInteractionHints.mjs +56 -0
  41. package/src/PcbScene3dMountRig.mjs +53 -0
  42. package/src/PcbScene3dOutlineBuilder.mjs +210 -0
  43. package/src/PcbScene3dPadFactory.mjs +553 -0
  44. package/src/PcbScene3dPresetState.mjs +48 -0
  45. package/src/PcbScene3dRenderGroupVisibility.mjs +134 -0
  46. package/src/PcbScene3dRuntime.mjs +996 -0
  47. package/src/PcbScene3dRuntimeBoardMeshes.mjs +99 -0
  48. package/src/PcbScene3dSelectionStyler.mjs +252 -0
  49. package/src/PcbScene3dShapePathFactory.mjs +220 -0
  50. package/src/PcbScene3dShellRenderer.mjs +131 -0
  51. package/src/PcbScene3dSilkscreenFactory.mjs +854 -0
  52. package/src/PcbScene3dSilkscreenStrokeWidthResolver.mjs +81 -0
  53. package/src/PcbScene3dStepLoader.mjs +611 -0
  54. package/src/PcbScene3dStrokeFont.mjs +671 -0
  55. package/src/PcbScene3dStrokeGeometryBuilder.mjs +322 -0
  56. package/src/PcbScene3dText.mjs +99 -0
  57. package/src/PcbScene3dTrueTypeTextFactory.mjs +885 -0
  58. package/src/PcbScene3dViaFactory.mjs +176 -0
  59. package/src/PcbScene3dViewCompensation.mjs +109 -0
  60. package/src/PcbScene3dViewScale.mjs +24 -0
  61. package/src/PcbScene3dViewportResize.mjs +35 -0
  62. package/src/PcbScene3dWorkerClient.mjs +123 -0
  63. package/src/index.mjs +1 -0
  64. package/src/scene3d.mjs +44 -0
  65. package/src/styles/scene3d.css +295 -0
@@ -0,0 +1,545 @@
1
+ import {
2
+ CircuitJsonDocument,
3
+ CircuitJsonIndexer,
4
+ CircuitJsonUnits
5
+ } from 'circuitjson-toolkit'
6
+
7
+ const TOP_LAYER_ID = 1
8
+ const BOTTOM_LAYER_ID = 32
9
+ const DEFAULT_BOARD_WIDTH_MM = 25.4
10
+ const DEFAULT_BOARD_HEIGHT_MM = 25.4
11
+ const DEFAULT_BOARD_THICKNESS_MM = 1.6
12
+ const DEFAULT_COMPONENT_HEIGHT_MIL = 60
13
+ const RECTANGULAR_PAD_SHAPE = 2
14
+ const CIRCULAR_PAD_SHAPE = 1
15
+
16
+ /**
17
+ * Converts serialized CircuitJSON element arrays into the viewer render model.
18
+ */
19
+ export class PcbScene3dCircuitJsonAdapter {
20
+ /**
21
+ * Returns true when a value is a serialized CircuitJSON model.
22
+ * @param {unknown} value Candidate model.
23
+ * @returns {boolean}
24
+ */
25
+ static isCircuitJsonModel(value) {
26
+ return CircuitJsonDocument.isModel(value)
27
+ }
28
+
29
+ /**
30
+ * Returns true when a model should bypass format-specific scene builders.
31
+ * @param {unknown} value Candidate model.
32
+ * @returns {boolean}
33
+ */
34
+ static isDirectCircuitJsonModel(value) {
35
+ if (!PcbScene3dCircuitJsonAdapter.isCircuitJsonModel(value)) {
36
+ return false
37
+ }
38
+
39
+ if (String(value?.sourceFormat || '') === 'circuitjson') {
40
+ return true
41
+ }
42
+
43
+ return !PcbScene3dCircuitJsonAdapter.#hasCompatibilityModel(value)
44
+ }
45
+
46
+ /**
47
+ * Builds the internal render model used by the Three.js runtime.
48
+ * @param {object[]} circuitJson Serialized CircuitJSON model.
49
+ * @returns {object}
50
+ */
51
+ static build(circuitJson) {
52
+ CircuitJsonDocument.assertModel(circuitJson)
53
+ const index = CircuitJsonIndexer.index(circuitJson)
54
+ const board = PcbScene3dCircuitJsonAdapter.#buildBoard(index)
55
+ const detail = PcbScene3dCircuitJsonAdapter.#buildDetail(index)
56
+ const components = PcbScene3dCircuitJsonAdapter.#buildComponents(
57
+ index,
58
+ board
59
+ )
60
+
61
+ return {
62
+ sourceFormat: 'circuitjson',
63
+ coordinateSystem: 'circuitjson-mm',
64
+ board,
65
+ components,
66
+ externalPlacements: [],
67
+ boardAssemblyModel: null,
68
+ detail
69
+ }
70
+ }
71
+
72
+ /**
73
+ * Builds the render-model board from a `pcb_board` element.
74
+ * @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
75
+ * @returns {object}
76
+ */
77
+ static #buildBoard(index) {
78
+ const boardElement = index.elementsByType.get('pcb_board')?.[0] || {}
79
+ const widthMil = CircuitJsonUnits.mmToMil(
80
+ boardElement.width,
81
+ DEFAULT_BOARD_WIDTH_MM
82
+ )
83
+ const heightMil = CircuitJsonUnits.mmToMil(
84
+ boardElement.height,
85
+ DEFAULT_BOARD_HEIGHT_MM
86
+ )
87
+ const thicknessMil = CircuitJsonUnits.mmToMil(
88
+ boardElement.thickness,
89
+ DEFAULT_BOARD_THICKNESS_MM
90
+ )
91
+ const center = CircuitJsonUnits.pointMmToMil(boardElement.center || {})
92
+ const minX = center.x - widthMil / 2
93
+ const minY = center.y - heightMil / 2
94
+ const segments = PcbScene3dCircuitJsonAdapter.#buildBoardSegments(
95
+ boardElement,
96
+ {
97
+ minX,
98
+ minY,
99
+ widthMil,
100
+ heightMil
101
+ }
102
+ )
103
+
104
+ return {
105
+ widthMil,
106
+ heightMil,
107
+ thicknessMil,
108
+ minX,
109
+ minY,
110
+ centerX: center.x,
111
+ centerY: center.y,
112
+ segments,
113
+ surfaceColor: null,
114
+ edgeColor: null
115
+ }
116
+ }
117
+
118
+ /**
119
+ * Returns true for parser-produced arrays that already carry a legacy
120
+ * renderer model and must keep using their format-specific scene builder.
121
+ * @param {unknown} value Candidate model.
122
+ * @returns {boolean}
123
+ */
124
+ static #hasCompatibilityModel(value) {
125
+ return Boolean(value?.pcb || value?.schematic || value?.bom)
126
+ }
127
+
128
+ /**
129
+ * Builds closed board line segments from outline points or rectangle size.
130
+ * @param {object} boardElement CircuitJSON board element.
131
+ * @param {{ minX: number, minY: number, widthMil: number, heightMil: number }} fallback Fallback rectangle.
132
+ * @returns {object[]}
133
+ */
134
+ static #buildBoardSegments(boardElement, fallback) {
135
+ const outlinePoints = Array.isArray(boardElement?.outline)
136
+ ? boardElement.outline
137
+ .map((point) => CircuitJsonUnits.pointMmToMil(point))
138
+ .filter((point) => Number.isFinite(point.x + point.y))
139
+ : []
140
+ const points =
141
+ outlinePoints.length >= 3
142
+ ? outlinePoints
143
+ : [
144
+ { x: fallback.minX, y: fallback.minY },
145
+ {
146
+ x: fallback.minX + fallback.widthMil,
147
+ y: fallback.minY
148
+ },
149
+ {
150
+ x: fallback.minX + fallback.widthMil,
151
+ y: fallback.minY + fallback.heightMil
152
+ },
153
+ {
154
+ x: fallback.minX,
155
+ y: fallback.minY + fallback.heightMil
156
+ }
157
+ ]
158
+
159
+ return points.map((point, index) => {
160
+ const next = points[(index + 1) % points.length]
161
+ return {
162
+ type: 'line',
163
+ x1: point.x,
164
+ y1: point.y,
165
+ x2: next.x,
166
+ y2: next.y
167
+ }
168
+ })
169
+ }
170
+
171
+ /**
172
+ * Builds component fallback bodies from `pcb_component` elements.
173
+ * @param {{ elementsByType: Map<string, object[]>, sourceComponentById: Map<string, object> }} index CircuitJSON index.
174
+ * @param {{ centerX: number, centerY: number, thicknessMil: number }} board Render board.
175
+ * @returns {object[]}
176
+ */
177
+ static #buildComponents(index, board) {
178
+ return (index.elementsByType.get('pcb_component') || []).map(
179
+ (component, componentIndex) => {
180
+ const sourceComponent = index.sourceComponentById.get(
181
+ String(component?.source_component_id || '')
182
+ )
183
+ const center = CircuitJsonUnits.pointMmToMil(
184
+ component?.center || {
185
+ x: component?.x,
186
+ y: component?.y
187
+ }
188
+ )
189
+ const width = CircuitJsonUnits.mmToMil(component?.width, 2)
190
+ const depth = CircuitJsonUnits.mmToMil(component?.height, 1.2)
191
+ const height = CircuitJsonUnits.mmToMil(
192
+ component?.component_height,
193
+ DEFAULT_COMPONENT_HEIGHT_MIL / 39.37007874015748
194
+ )
195
+ const designator =
196
+ PcbScene3dCircuitJsonAdapter.#componentDesignator(
197
+ component,
198
+ sourceComponent,
199
+ componentIndex
200
+ )
201
+ const mountSide = PcbScene3dCircuitJsonAdapter.#layerSide(
202
+ component?.layer
203
+ )
204
+
205
+ return {
206
+ designator,
207
+ mountSide,
208
+ rotationDeg: Number(component?.rotation || 0),
209
+ positionMil: {
210
+ x: center.x - board.centerX,
211
+ y: center.y - board.centerY,
212
+ z: board.thicknessMil / 2 + height / 2
213
+ },
214
+ boardPositionMil: {
215
+ x: center.x,
216
+ y: center.y,
217
+ z: 0
218
+ },
219
+ pattern: String(
220
+ sourceComponent?.ftype ||
221
+ sourceComponent?.name ||
222
+ component?.pcb_component_id ||
223
+ 'CircuitJSON component'
224
+ ),
225
+ source: 'circuitjson',
226
+ body: {
227
+ family: 'chip',
228
+ sizeMil: {
229
+ width: Math.max(width, 20),
230
+ depth: Math.max(depth, 20),
231
+ height: Math.max(height, 10)
232
+ }
233
+ },
234
+ externalModel: null
235
+ }
236
+ }
237
+ )
238
+ }
239
+
240
+ /**
241
+ * Resolves a display designator for one CircuitJSON component.
242
+ * @param {object} component PCB component element.
243
+ * @param {object | undefined} sourceComponent Source component element.
244
+ * @param {number} componentIndex Component index.
245
+ * @returns {string}
246
+ */
247
+ static #componentDesignator(component, sourceComponent, componentIndex) {
248
+ return String(
249
+ sourceComponent?.name ||
250
+ component?.name ||
251
+ component?.source_component_id ||
252
+ component?.pcb_component_id ||
253
+ `C${componentIndex + 1}`
254
+ )
255
+ }
256
+
257
+ /**
258
+ * Builds all core board detail primitives.
259
+ * @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
260
+ * @returns {object}
261
+ */
262
+ static #buildDetail(index) {
263
+ return {
264
+ pads: [
265
+ ...PcbScene3dCircuitJsonAdapter.#buildSmtPads(index),
266
+ ...PcbScene3dCircuitJsonAdapter.#buildPlatedHoles(index),
267
+ ...PcbScene3dCircuitJsonAdapter.#buildNonPlatedHoles(index)
268
+ ],
269
+ tracks: PcbScene3dCircuitJsonAdapter.#buildTracks(index),
270
+ arcs: [],
271
+ fills: [],
272
+ vias: PcbScene3dCircuitJsonAdapter.#buildVias(index),
273
+ polygons: [],
274
+ copperTexts: [],
275
+ embeddedFonts: [],
276
+ silkscreen: PcbScene3dCircuitJsonAdapter.#buildSilkscreen(index)
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Builds SMT pad detail primitives.
282
+ * @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
283
+ * @returns {object[]}
284
+ */
285
+ static #buildSmtPads(index) {
286
+ return (index.elementsByType.get('pcb_smtpad') || []).map((pad) => {
287
+ const side = PcbScene3dCircuitJsonAdapter.#layerSide(pad?.layer)
288
+ const size = PcbScene3dCircuitJsonAdapter.#padSize(pad)
289
+ const isBottom = side === 'bottom'
290
+ const padDetail = {
291
+ x: CircuitJsonUnits.mmToMil(pad?.x, 0),
292
+ y: CircuitJsonUnits.mmToMil(pad?.y, 0),
293
+ rotation: Number(pad?.ccw_rotation || 0),
294
+ shapeTop: isBottom
295
+ ? 0
296
+ : PcbScene3dCircuitJsonAdapter.#padShape(pad),
297
+ shapeMid: 0,
298
+ shapeBottom: isBottom
299
+ ? PcbScene3dCircuitJsonAdapter.#padShape(pad)
300
+ : 0,
301
+ sizeTopX: isBottom ? 0 : size.width,
302
+ sizeTopY: isBottom ? 0 : size.height,
303
+ sizeMidX: 0,
304
+ sizeMidY: 0,
305
+ sizeBottomX: isBottom ? size.width : 0,
306
+ sizeBottomY: isBottom ? size.height : 0,
307
+ holeDiameter: 0,
308
+ hasTopSolderMaskOpening: !isBottom,
309
+ hasBottomSolderMaskOpening: isBottom
310
+ }
311
+ return padDetail
312
+ })
313
+ }
314
+
315
+ /**
316
+ * Builds plated-through-hole pad detail primitives.
317
+ * @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
318
+ * @returns {object[]}
319
+ */
320
+ static #buildPlatedHoles(index) {
321
+ return (index.elementsByType.get('pcb_plated_hole') || []).map(
322
+ (hole) => {
323
+ const size =
324
+ PcbScene3dCircuitJsonAdapter.#platedHoleOuterSize(hole)
325
+ const holeDiameter = CircuitJsonUnits.mmToMil(
326
+ hole?.hole_diameter || hole?.hole_width,
327
+ 0
328
+ )
329
+ return {
330
+ x: CircuitJsonUnits.mmToMil(hole?.x, 0),
331
+ y: CircuitJsonUnits.mmToMil(hole?.y, 0),
332
+ rotation: Number(hole?.ccw_rotation || 0),
333
+ shapeTop: PcbScene3dCircuitJsonAdapter.#padShape(hole),
334
+ shapeMid: PcbScene3dCircuitJsonAdapter.#padShape(hole),
335
+ shapeBottom: PcbScene3dCircuitJsonAdapter.#padShape(hole),
336
+ sizeTopX: size.width,
337
+ sizeTopY: size.height,
338
+ sizeMidX: size.width,
339
+ sizeMidY: size.height,
340
+ sizeBottomX: size.width,
341
+ sizeBottomY: size.height,
342
+ holeDiameter,
343
+ hasTopSolderMaskOpening: true,
344
+ hasBottomSolderMaskOpening: true
345
+ }
346
+ }
347
+ )
348
+ }
349
+
350
+ /**
351
+ * Builds drill-only hole detail primitives.
352
+ * @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
353
+ * @returns {object[]}
354
+ */
355
+ static #buildNonPlatedHoles(index) {
356
+ return (index.elementsByType.get('pcb_hole') || []).map((hole) => {
357
+ const diameter = CircuitJsonUnits.mmToMil(
358
+ hole?.hole_diameter || hole?.hole_width,
359
+ 0
360
+ )
361
+ return {
362
+ x: CircuitJsonUnits.mmToMil(hole?.x, 0),
363
+ y: CircuitJsonUnits.mmToMil(hole?.y, 0),
364
+ rotation: Number(hole?.ccw_rotation || 0),
365
+ shapeTop: 0,
366
+ shapeMid: 0,
367
+ shapeBottom: 0,
368
+ sizeTopX: 0,
369
+ sizeTopY: 0,
370
+ sizeMidX: 0,
371
+ sizeMidY: 0,
372
+ sizeBottomX: 0,
373
+ sizeBottomY: 0,
374
+ holeDiameter: diameter,
375
+ hasTopSolderMaskOpening: false,
376
+ hasBottomSolderMaskOpening: false
377
+ }
378
+ })
379
+ }
380
+
381
+ /**
382
+ * Builds via detail primitives.
383
+ * @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
384
+ * @returns {object[]}
385
+ */
386
+ static #buildVias(index) {
387
+ return (index.elementsByType.get('pcb_via') || []).map((via) => ({
388
+ x: CircuitJsonUnits.mmToMil(via?.x, 0),
389
+ y: CircuitJsonUnits.mmToMil(via?.y, 0),
390
+ diameter: CircuitJsonUnits.mmToMil(via?.outer_diameter, 0),
391
+ holeDiameter: CircuitJsonUnits.mmToMil(via?.hole_diameter, 0),
392
+ isTentingTop: false,
393
+ isTentingBottom: false
394
+ }))
395
+ }
396
+
397
+ /**
398
+ * Builds copper track detail primitives from `pcb_trace` wire routes.
399
+ * @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
400
+ * @returns {object[]}
401
+ */
402
+ static #buildTracks(index) {
403
+ const tracks = []
404
+ ;(index.elementsByType.get('pcb_trace') || []).forEach((trace) => {
405
+ const route = Array.isArray(trace?.route) ? trace.route : []
406
+ for (let index = 0; index < route.length - 1; index += 1) {
407
+ const start = route[index]
408
+ const end = route[index + 1]
409
+ if (
410
+ String(start?.route_type || 'wire') !== 'wire' ||
411
+ String(end?.route_type || 'wire') !== 'wire'
412
+ ) {
413
+ continue
414
+ }
415
+ const layer = start?.layer || end?.layer
416
+ tracks.push({
417
+ x1: CircuitJsonUnits.mmToMil(start?.x, 0),
418
+ y1: CircuitJsonUnits.mmToMil(start?.y, 0),
419
+ x2: CircuitJsonUnits.mmToMil(end?.x, 0),
420
+ y2: CircuitJsonUnits.mmToMil(end?.y, 0),
421
+ width: CircuitJsonUnits.mmToMil(
422
+ start?.width || end?.width,
423
+ 0.1524
424
+ ),
425
+ layerId:
426
+ PcbScene3dCircuitJsonAdapter.#layerSide(layer) ===
427
+ 'bottom'
428
+ ? BOTTOM_LAYER_ID
429
+ : TOP_LAYER_ID,
430
+ solderMaskOpening: true
431
+ })
432
+ }
433
+ })
434
+ return tracks
435
+ }
436
+
437
+ /**
438
+ * Builds basic silkscreen detail from known CircuitJSON drawing elements.
439
+ * @param {{ elementsByType: Map<string, object[]> }} index CircuitJSON index.
440
+ * @returns {{ top: object, bottom: object }}
441
+ */
442
+ static #buildSilkscreen(index) {
443
+ const top = { tracks: [], arcs: [], fills: [], texts: [] }
444
+ const bottom = { tracks: [], arcs: [], fills: [], texts: [] }
445
+ ;(index.elementsByType.get('pcb_silkscreen_line') || []).forEach(
446
+ (line) => {
447
+ const target =
448
+ PcbScene3dCircuitJsonAdapter.#layerSide(line?.layer) ===
449
+ 'bottom'
450
+ ? bottom
451
+ : top
452
+ target.tracks.push({
453
+ x1: CircuitJsonUnits.mmToMil(line?.x1, 0),
454
+ y1: CircuitJsonUnits.mmToMil(line?.y1, 0),
455
+ x2: CircuitJsonUnits.mmToMil(line?.x2, 0),
456
+ y2: CircuitJsonUnits.mmToMil(line?.y2, 0),
457
+ width: CircuitJsonUnits.mmToMil(line?.stroke_width, 0.12)
458
+ })
459
+ }
460
+ )
461
+ ;(index.elementsByType.get('pcb_silkscreen_text') || []).forEach(
462
+ (text) => {
463
+ const target =
464
+ PcbScene3dCircuitJsonAdapter.#layerSide(text?.layer) ===
465
+ 'bottom'
466
+ ? bottom
467
+ : top
468
+ target.texts.push({
469
+ value: String(text?.text || text?.value || ''),
470
+ x: CircuitJsonUnits.mmToMil(text?.x, 0),
471
+ y: CircuitJsonUnits.mmToMil(text?.y, 0),
472
+ rotation: Number(text?.ccw_rotation || 0),
473
+ sizeX: CircuitJsonUnits.mmToMil(text?.font_size, 1),
474
+ sizeY: CircuitJsonUnits.mmToMil(text?.font_size, 1),
475
+ width: CircuitJsonUnits.mmToMil(text?.stroke_width, 0.12)
476
+ })
477
+ }
478
+ )
479
+ return { top, bottom }
480
+ }
481
+
482
+ /**
483
+ * Resolves pad copper size from CircuitJSON pad fields.
484
+ * @param {object} pad Pad element.
485
+ * @returns {{ width: number, height: number }}
486
+ */
487
+ static #padSize(pad) {
488
+ if (String(pad?.shape || '') === 'circle') {
489
+ const diameter = CircuitJsonUnits.mmToMil(
490
+ Number(pad?.radius || 0) * 2 || pad?.diameter,
491
+ 1
492
+ )
493
+ return { width: diameter, height: diameter }
494
+ }
495
+ return {
496
+ width: CircuitJsonUnits.mmToMil(pad?.width, 1),
497
+ height: CircuitJsonUnits.mmToMil(pad?.height, 1)
498
+ }
499
+ }
500
+
501
+ /**
502
+ * Resolves plated-hole copper size from CircuitJSON fields.
503
+ * @param {object} hole Plated-hole element.
504
+ * @returns {{ width: number, height: number }}
505
+ */
506
+ static #platedHoleOuterSize(hole) {
507
+ if (Number(hole?.outer_diameter || 0) > 0) {
508
+ const diameter = CircuitJsonUnits.mmToMil(hole.outer_diameter, 1)
509
+ return { width: diameter, height: diameter }
510
+ }
511
+ return {
512
+ width: CircuitJsonUnits.mmToMil(
513
+ hole?.outer_width || hole?.rect_pad_width,
514
+ 1
515
+ ),
516
+ height: CircuitJsonUnits.mmToMil(
517
+ hole?.outer_height || hole?.rect_pad_height,
518
+ 1
519
+ )
520
+ }
521
+ }
522
+
523
+ /**
524
+ * Resolves the viewer pad shape code.
525
+ * @param {object} pad CircuitJSON pad element.
526
+ * @returns {number}
527
+ */
528
+ static #padShape(pad) {
529
+ return String(pad?.shape || '').includes('rect')
530
+ ? RECTANGULAR_PAD_SHAPE
531
+ : CIRCULAR_PAD_SHAPE
532
+ }
533
+
534
+ /**
535
+ * Resolves a board side from a CircuitJSON layer name.
536
+ * @param {unknown} layer Layer value.
537
+ * @returns {'top' | 'bottom'}
538
+ */
539
+ static #layerSide(layer) {
540
+ const value = String(layer || 'top').toLowerCase()
541
+ return value.includes('bottom') || value === 'b.cu' || value === 'back'
542
+ ? 'bottom'
543
+ : 'top'
544
+ }
545
+ }