pcb-scene3d-viewer 1.1.49 → 1.2.0

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 (63) hide show
  1. package/README.md +46 -21
  2. package/docs/api.md +109 -8
  3. package/docs/circuitjson.md +143 -29
  4. package/docs/model-format.md +50 -7
  5. package/docs/release-notes-v1.2.0.md +122 -0
  6. package/docs/testing.md +30 -1
  7. package/package.json +6 -3
  8. package/spec/library-scope.md +10 -2
  9. package/src/CircuitJsonCadModelAssetResolver.mjs +697 -83
  10. package/src/PcbAssemblyBoardSubstrateBuilder.mjs +43 -0
  11. package/src/PcbAssemblyGeometryBuilder.mjs +40 -18
  12. package/src/PcbAssemblyModelMeshLoader.mjs +101 -150
  13. package/src/PcbAssemblyPadMeshBuilder.mjs +22 -0
  14. package/src/PcbModelArchiveExporter.mjs +28 -112
  15. package/src/PcbModelArchiveSourceBundle.mjs +326 -0
  16. package/src/PcbScene3dAabbIndex.mjs +464 -0
  17. package/src/PcbScene3dBoardAssemblyPresentation.mjs +1 -1
  18. package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +9 -5
  19. package/src/PcbScene3dBoardMaterialPalette.mjs +29 -0
  20. package/src/PcbScene3dBoardShapeFactory.mjs +11 -118
  21. package/src/PcbScene3dBoardSolderMaskFactory.mjs +65 -39
  22. package/src/PcbScene3dCircuitJsonAdapter.mjs +151 -48
  23. package/src/PcbScene3dCircuitJsonDrillDetail.mjs +31 -0
  24. package/src/PcbScene3dCircuitJsonGeometry.mjs +184 -33
  25. package/src/PcbScene3dCircuitJsonInput.mjs +132 -0
  26. package/src/PcbScene3dCircuitJsonModelAsset.mjs +40 -0
  27. package/src/PcbScene3dController.mjs +40 -34
  28. package/src/PcbScene3dCopperFactory.mjs +36 -22
  29. package/src/PcbScene3dCopperFillAreaClipper.mjs +133 -235
  30. package/src/PcbScene3dCopperFillCoverageContext.mjs +204 -0
  31. package/src/PcbScene3dCopperFillLoopSetResolver.mjs +192 -0
  32. package/src/PcbScene3dCopperFillMeshBuilder.mjs +77 -295
  33. package/src/PcbScene3dCopperTextFactory.mjs +12 -4
  34. package/src/PcbScene3dCutoutCircleDetector.mjs +34 -17
  35. package/src/PcbScene3dCutoutGeometryFilter.mjs +104 -269
  36. package/src/PcbScene3dCutoutGridIndex.mjs +184 -0
  37. package/src/PcbScene3dDeferredModelFinalizer.mjs +52 -0
  38. package/src/PcbScene3dDescriptorSafeRecord.mjs +38 -0
  39. package/src/PcbScene3dDrillCutoutFilter.mjs +149 -143
  40. package/src/PcbScene3dDrillPathFactory.mjs +86 -16
  41. package/src/PcbScene3dDrillVoidFactory.mjs +35 -10
  42. package/src/PcbScene3dExternalModelGroupLoader.mjs +472 -31
  43. package/src/PcbScene3dExternalModels.mjs +23 -24
  44. package/src/PcbScene3dFacetedModelGroupBuilder.mjs +217 -0
  45. package/src/PcbScene3dGeometryZCompressor.mjs +4 -2
  46. package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +18 -4
  47. package/src/PcbScene3dMaskCoveredCopperSurfaceFilter.mjs +75 -10
  48. package/src/PcbScene3dModelContent.mjs +236 -0
  49. package/src/PcbScene3dModelFetchPolicy.mjs +304 -0
  50. package/src/PcbScene3dModelIdentity.mjs +106 -0
  51. package/src/PcbScene3dPlatedDrillSpecResolver.mjs +141 -0
  52. package/src/PcbScene3dPreparedPolygon.mjs +709 -0
  53. package/src/PcbScene3dPreparedPolygonSet.mjs +70 -0
  54. package/src/PcbScene3dRuntime.mjs +45 -45
  55. package/src/PcbScene3dRuntimeBoardMeshes.mjs +85 -1
  56. package/src/PcbScene3dShapeHoleGeometryCleaner.mjs +4 -2
  57. package/src/PcbScene3dShellRenderer.mjs +75 -6
  58. package/src/PcbScene3dSilkscreenCutoutContext.mjs +255 -0
  59. package/src/PcbScene3dSilkscreenFactory.mjs +87 -127
  60. package/src/PcbScene3dSilkscreenFillSeamBuilder.mjs +11 -5
  61. package/src/PcbScene3dStepLoader.mjs +11 -10
  62. package/src/PcbScene3dText.mjs +1 -1
  63. package/src/PcbScene3dTriangleVertexQueryBounds.mjs +302 -0
@@ -272,6 +272,30 @@ export class PcbAssemblyBoardSubstrateBuilder {
272
272
  return null
273
273
  }
274
274
 
275
+ if (Number(pad?.holeShape) === 1) {
276
+ const width = PcbAssemblyBoardSubstrateBuilder.#firstPositive([
277
+ pad?.holeWidth,
278
+ pad?.holeGeometry?.width,
279
+ diameter
280
+ ])
281
+ const height = PcbAssemblyBoardSubstrateBuilder.#firstPositive([
282
+ pad?.holeHeight,
283
+ pad?.holeGeometry?.height,
284
+ diameter
285
+ ])
286
+ return PcbAssemblyBoardSubstrateBuilder.#rotatedPoints(
287
+ PcbAssemblyBoardSubstrateBuilder.#rectanglePoints(
288
+ x,
289
+ y,
290
+ width,
291
+ height
292
+ ),
293
+ x,
294
+ y,
295
+ Number(pad?.holeRotation ?? pad?.rotation ?? 0)
296
+ )
297
+ }
298
+
275
299
  const slotLength = PcbAssemblyBoardSubstrateBuilder.#firstPositive([
276
300
  pad?.holeSlotLength,
277
301
  pad?.slotLength,
@@ -296,6 +320,25 @@ export class PcbAssemblyBoardSubstrateBuilder {
296
320
  return PcbAssemblyMeshUtils.circlePoints(x, y, diameter / 2, pointCount)
297
321
  }
298
322
 
323
+ /**
324
+ * Builds axis-aligned rectangular aperture points.
325
+ * @param {number} x Center X.
326
+ * @param {number} y Center Y.
327
+ * @param {number} width Width.
328
+ * @param {number} height Height.
329
+ * @returns {number[][]} Rectangle points.
330
+ */
331
+ static #rectanglePoints(x, y, width, height) {
332
+ const halfWidth = width / 2
333
+ const halfHeight = height / 2
334
+ return [
335
+ [x - halfWidth, y - halfHeight],
336
+ [x + halfWidth, y - halfHeight],
337
+ [x + halfWidth, y + halfHeight],
338
+ [x - halfWidth, y + halfHeight]
339
+ ]
340
+ }
341
+
299
342
  /**
300
343
  * Builds a fallback via drill loop.
301
344
  * @param {object} via Via primitive.
@@ -82,30 +82,52 @@ export class PcbAssemblyGeometryBuilder {
82
82
  */
83
83
  static #buildBoardMeshes(sceneDescription) {
84
84
  const board = sceneDescription?.board || {}
85
- const width = Math.max(Number(board.widthMil || 0), 1)
86
- const height = Math.max(Number(board.heightMil || 0), 1)
87
- const thickness = Math.max(Number(board.thicknessMil || 63), 1)
88
- const outlinePoints =
89
- PcbAssemblyGeometryBuilder.#boardOutlinePoints(board)
90
- const outlineMesh = PcbAssemblyBoardSubstrateBuilder.build(
91
- 'board',
92
- outlinePoints,
93
- sceneDescription,
94
- thickness,
95
- BOARD_COLOR
96
- )
97
- if (outlineMesh) {
98
- return [outlineMesh]
99
- }
85
+ const contours = PcbAssemblyGeometryBuilder.#boardContours(board)
86
+ return contours.map((contour, index) => {
87
+ const width = Math.max(Number(contour.widthMil || 0), 1)
88
+ const height = Math.max(Number(contour.heightMil || 0), 1)
89
+ const thickness = Math.max(
90
+ Number(contour.thicknessMil || board.thicknessMil || 63),
91
+ 1
92
+ )
93
+ const name = contours.length === 1 ? 'board' : `board-${index + 1}`
94
+ const outlinePoints =
95
+ PcbAssemblyGeometryBuilder.#boardOutlinePoints(contour)
96
+ const outlineMesh = PcbAssemblyBoardSubstrateBuilder.build(
97
+ name,
98
+ outlinePoints,
99
+ { ...sceneDescription, board: contour },
100
+ thickness,
101
+ BOARD_COLOR
102
+ )
103
+ if (outlineMesh) return outlineMesh
100
104
 
101
- return [
102
- PcbAssemblyMeshUtils.box('board', {
105
+ return PcbAssemblyMeshUtils.box(name, {
106
+ x: Number(contour.minX || 0) + width / 2,
107
+ y: Number(contour.minY || 0) + height / 2,
103
108
  width,
104
109
  depth: height,
105
110
  height: thickness,
106
111
  color: BOARD_COLOR
107
112
  })
108
- ]
113
+ })
114
+ }
115
+
116
+ /**
117
+ * Resolves independently exportable board contours.
118
+ * @param {object} board Aggregate board metadata.
119
+ * @returns {object[]}
120
+ */
121
+ static #boardContours(board) {
122
+ const contours = Array.isArray(board?.contours)
123
+ ? board.contours.filter(Boolean)
124
+ : []
125
+ if (!contours.length) return [board]
126
+ return contours.map((contour) => ({
127
+ ...board,
128
+ ...contour,
129
+ contours: []
130
+ }))
109
131
  }
110
132
 
111
133
  /**
@@ -1,5 +1,7 @@
1
1
  import { PcbAssemblyGltfModelMeshParser } from './PcbAssemblyGltfModelMeshParser.mjs'
2
2
  import { PcbAssemblyTextModelMeshParser } from './PcbAssemblyTextModelMeshParser.mjs'
3
+ import { PcbScene3dDescriptorSafeRecord } from './PcbScene3dDescriptorSafeRecord.mjs'
4
+ import { PcbScene3dModelContent } from './PcbScene3dModelContent.mjs'
3
5
  import { PcbScene3dStepLoader } from './PcbScene3dStepLoader.mjs'
4
6
 
5
7
  /**
@@ -10,39 +12,24 @@ export class PcbAssemblyModelMeshLoader {
10
12
  /** @type {PcbScene3dStepLoader} */
11
13
  #stepLoader
12
14
 
13
- /** @type {((url: string, options: object) => Promise<any>) | null} */
14
- #fetch
15
+ /** @type {boolean} */
16
+ #canFetch
15
17
 
16
- /** @type {Record<string, string>} */
17
- #authHeaders
18
-
19
- /** @type {number} */
20
- #fetchTimeoutMs
21
-
22
- /** @type {Map<string, Promise<Uint8Array>>} */
23
- #modelCache
18
+ /** @type {object} */
19
+ #modelOptions
24
20
 
25
21
  /**
26
22
  * @param {{ stepLoader?: PcbScene3dStepLoader, fetch?: (url: string, options: object) => Promise<any>, allowNetworkModelFetch?: boolean, authHeaders?: Record<string, string>, fetchTimeoutMs?: number, modelCache?: Map<string, Promise<Uint8Array>> }} [options] Loader options.
27
23
  */
28
24
  constructor(options = {}) {
29
25
  this.#stepLoader = options.stepLoader || new PcbScene3dStepLoader()
30
- this.#fetch =
31
- typeof options.fetch === 'function'
32
- ? options.fetch
33
- : options.allowNetworkModelFetch === true &&
34
- typeof globalThis.fetch === 'function'
35
- ? globalThis.fetch.bind(globalThis)
36
- : null
37
- this.#authHeaders = PcbAssemblyModelMeshLoader.#headers(
38
- options.authHeaders
39
- )
40
- this.#fetchTimeoutMs = Math.max(
41
- Number(options.fetchTimeoutMs || 30_000),
42
- 1
43
- )
44
- this.#modelCache =
26
+ const modelCache =
45
27
  options.modelCache instanceof Map ? options.modelCache : new Map()
28
+ this.#modelOptions = PcbScene3dModelContent.createFetchScope({
29
+ ...PcbScene3dDescriptorSafeRecord.copy(options),
30
+ modelCache
31
+ })
32
+ this.#canFetch = PcbScene3dModelContent.canFetch(this.#modelOptions)
46
33
  }
47
34
 
48
35
  /**
@@ -51,9 +38,10 @@ export class PcbAssemblyModelMeshLoader {
51
38
  * @returns {Promise<object[]>}
52
39
  */
53
40
  async loadPlacement(placement) {
54
- const model = await this.#modelWithFetchedContent(
41
+ const suppliedModel = PcbAssemblyModelMeshLoader.#withCanonicalTextData(
55
42
  placement?.externalModel || null
56
43
  )
44
+ const model = await this.#modelWithFetchedContent(suppliedModel)
57
45
  const format = String(model?.format || '').toLowerCase()
58
46
  if (format === 'step' || format === 'stp') {
59
47
  return this.#loadStepMeshes(model)
@@ -88,12 +76,15 @@ export class PcbAssemblyModelMeshLoader {
88
76
  * @returns {Promise<object | null>}
89
77
  */
90
78
  async #modelWithFetchedContent(model) {
91
- if (!model || PcbAssemblyModelMeshLoader.#hasModelContent(model)) {
92
- return model
93
- }
94
-
79
+ if (!model) return model
95
80
  const url = String(model.resolvedUrl || model.sourceUrl || '').trim()
96
- if (!url || !this.#fetch) {
81
+ const format = String(model?.format || '').toLowerCase()
82
+ if (PcbAssemblyModelMeshLoader.#hasModelContent(model)) {
83
+ return format === 'gltf' && url && this.#canFetch
84
+ ? await this.#modelWithFetchedGltfBuffers(model, url)
85
+ : model
86
+ }
87
+ if (!url || !this.#canFetch) {
97
88
  return model
98
89
  }
99
90
 
@@ -102,7 +93,7 @@ export class PcbAssemblyModelMeshLoader {
102
93
  ...model,
103
94
  payloadBytes: bytes
104
95
  }
105
- if (String(model?.format || '').toLowerCase() === 'gltf') {
96
+ if (format === 'gltf') {
106
97
  return await this.#modelWithFetchedGltfBuffers(fetchedModel, url)
107
98
  }
108
99
 
@@ -114,21 +105,16 @@ export class PcbAssemblyModelMeshLoader {
114
105
  * @param {string} url Resolved model URL.
115
106
  * @returns {Promise<Uint8Array>}
116
107
  */
117
- async #fetchModelBytes(url) {
118
- const cacheKey = String(url || '')
119
- const cached = this.#modelCache.get(cacheKey)
120
- if (cached) {
121
- return await cached
122
- }
123
-
124
- const pending = this.#fetchModelBytesUncached(url)
125
- this.#modelCache.set(cacheKey, pending)
126
- try {
127
- return await pending
128
- } catch (error) {
129
- this.#modelCache.delete(cacheKey)
130
- throw error
131
- }
108
+ async #fetchModelBytes(url, mainUrl = url) {
109
+ return PcbScene3dModelContent.bytes(
110
+ {
111
+ format: 'model-resource',
112
+ resolvedUrl: url,
113
+ mainModelUrl: mainUrl
114
+ },
115
+ this.#modelOptions,
116
+ 'Model'
117
+ )
132
118
  }
133
119
 
134
120
  /**
@@ -138,7 +124,19 @@ export class PcbAssemblyModelMeshLoader {
138
124
  * @returns {Promise<object>}
139
125
  */
140
126
  async #modelWithFetchedGltfBuffers(model, modelUrl) {
141
- const uris = PcbAssemblyModelMeshLoader.#externalGltfBufferUris(model)
127
+ const existingBuffers = PcbAssemblyModelMeshLoader.#resourceArray(
128
+ model?.externalBuffers
129
+ )
130
+ const existingUris = new Set(
131
+ existingBuffers
132
+ .map((resource) =>
133
+ String(resource?.uri || resource?.name || '').trim()
134
+ )
135
+ .filter(Boolean)
136
+ )
137
+ const uris = PcbAssemblyModelMeshLoader.#externalGltfBufferUris(
138
+ model
139
+ ).filter((uri) => !existingUris.has(uri))
142
140
  if (!uris.length) {
143
141
  return model
144
142
  }
@@ -153,18 +151,13 @@ export class PcbAssemblyModelMeshLoader {
153
151
  uri,
154
152
  name: uri,
155
153
  sourceUrl,
156
- payloadBytes: await this.#fetchModelBytes(sourceUrl)
154
+ payloadBytes: await this.#fetchModelBytes(sourceUrl, modelUrl)
157
155
  })
158
156
  }
159
157
 
160
158
  return {
161
159
  ...model,
162
- externalBuffers: [
163
- ...PcbAssemblyModelMeshLoader.#resourceArray(
164
- model?.externalBuffers
165
- ),
166
- ...fetchedBuffers
167
- ]
160
+ externalBuffers: [...existingBuffers, ...fetchedBuffers]
168
161
  }
169
162
  }
170
163
 
@@ -215,47 +208,32 @@ export class PcbAssemblyModelMeshLoader {
215
208
  * @returns {string}
216
209
  */
217
210
  static #resolveSidecarUrl(uri, modelUrl) {
218
- try {
219
- return new URL(uri, modelUrl).toString()
220
- } catch (_error) {
221
- return uri
222
- }
211
+ return PcbScene3dModelContent.resolveRelativeUrl(uri, modelUrl)
223
212
  }
224
213
 
225
214
  /**
226
- * Reads model bytes from the configured fetcher.
227
- * @param {string} url Resolved model URL.
228
- * @returns {Promise<Uint8Array>}
215
+ * Maps canonical string `text` or `data` to the text parser contract.
216
+ * @param {object | null} model External model metadata.
217
+ * @returns {object | null}
229
218
  */
230
- async #fetchModelBytesUncached(url) {
231
- const controller =
232
- typeof AbortController === 'function' ? new AbortController() : null
233
- const timeout = controller
234
- ? setTimeout(() => controller.abort(), this.#fetchTimeoutMs)
235
- : null
236
-
219
+ static #withCanonicalTextData(model) {
220
+ if (!model || typeof model !== 'object') return model
221
+ let descriptors
237
222
  try {
238
- const response = await this.#fetch(url, {
239
- headers: { ...this.#authHeaders },
240
- signal: controller?.signal
241
- })
242
- return await PcbAssemblyModelMeshLoader.#bytesFromFetchResponse(
243
- response
244
- )
245
- } catch (error) {
246
- if (error?.name === 'AbortError') {
247
- throw new Error(
248
- 'Model fetch timed out after ' +
249
- this.#fetchTimeoutMs +
250
- 'ms: ' +
251
- url
252
- )
253
- }
254
- throw error
255
- } finally {
256
- if (timeout) {
257
- clearTimeout(timeout)
258
- }
223
+ descriptors = Object.getOwnPropertyDescriptors(model)
224
+ } catch {
225
+ return model
226
+ }
227
+ if (typeof descriptors.payloadText?.value === 'string') {
228
+ return model
229
+ }
230
+ const text = ['text', 'data']
231
+ .map((key) => descriptors[key]?.value)
232
+ .find((value) => typeof value === 'string')
233
+ if (typeof text !== 'string') return model
234
+ return {
235
+ ...PcbScene3dDescriptorSafeRecord.copy(model),
236
+ payloadText: text
259
237
  }
260
238
  }
261
239
 
@@ -342,22 +320,43 @@ export class PcbAssemblyModelMeshLoader {
342
320
  if (typeof model?.payloadText === 'string') {
343
321
  return model.payloadText
344
322
  }
345
-
346
- if (typeof model?.file?.text === 'function') {
347
- return await model.file.text()
323
+ for (const value of [
324
+ model?.payloadBytes,
325
+ model?.bytes,
326
+ model?.data,
327
+ model?.file
328
+ ]) {
329
+ if (typeof value?.text === 'function') {
330
+ return await value.text()
331
+ }
332
+ const bytes =
333
+ await PcbAssemblyModelMeshLoader.#bytesFromValue(value)
334
+ if (bytes) return new TextDecoder().decode(bytes)
348
335
  }
349
336
 
350
- if (typeof model?.file?.arrayBuffer === 'function') {
351
- return new TextDecoder().decode(
352
- new Uint8Array(await model.file.arrayBuffer())
337
+ throw new Error('WRL model content is not available.')
338
+ }
339
+
340
+ /**
341
+ * Reads one local model payload as bytes.
342
+ * @param {unknown} value Byte or blob candidate.
343
+ * @returns {Promise<Uint8Array | null>}
344
+ */
345
+ static async #bytesFromValue(value) {
346
+ if (!value) return null
347
+ if (value instanceof Uint8Array) return value
348
+ if (value instanceof ArrayBuffer) return new Uint8Array(value)
349
+ if (ArrayBuffer.isView(value)) {
350
+ return new Uint8Array(
351
+ value.buffer,
352
+ value.byteOffset,
353
+ value.byteLength
353
354
  )
354
355
  }
355
-
356
- if (model?.file instanceof Uint8Array) {
357
- return new TextDecoder().decode(model.file)
356
+ if (typeof value.arrayBuffer === 'function') {
357
+ return new Uint8Array(await value.arrayBuffer())
358
358
  }
359
-
360
- throw new Error('WRL model content is not available.')
359
+ return null
361
360
  }
362
361
 
363
362
  /**
@@ -413,54 +412,6 @@ export class PcbAssemblyModelMeshLoader {
413
412
  )
414
413
  }
415
414
 
416
- /**
417
- * Converts a fetch response or byte-like result into bytes.
418
- * @param {any} response Fetch response or direct byte-like value.
419
- * @returns {Promise<Uint8Array>}
420
- */
421
- static async #bytesFromFetchResponse(response) {
422
- if (response?.ok === false) {
423
- throw new Error(
424
- 'Model fetch failed with HTTP status ' +
425
- String(response.status || 'unknown') +
426
- '.'
427
- )
428
- }
429
- if (response instanceof Uint8Array) {
430
- return response
431
- }
432
- if (response instanceof ArrayBuffer) {
433
- return new Uint8Array(response)
434
- }
435
- if (ArrayBuffer.isView(response) && !(response instanceof DataView)) {
436
- return new Uint8Array(
437
- response.buffer,
438
- response.byteOffset,
439
- response.byteLength
440
- )
441
- }
442
- if (typeof response?.arrayBuffer === 'function') {
443
- return new Uint8Array(await response.arrayBuffer())
444
- }
445
- if (typeof response?.text === 'function') {
446
- return new TextEncoder().encode(await response.text())
447
- }
448
- throw new Error('Fetched model content is not readable.')
449
- }
450
-
451
- /**
452
- * Normalizes string headers.
453
- * @param {object | undefined} headers Candidate headers.
454
- * @returns {Record<string, string>}
455
- */
456
- static #headers(headers) {
457
- return Object.fromEntries(
458
- Object.entries(headers || {})
459
- .map(([key, value]) => [String(key), String(value)])
460
- .filter(([key, value]) => key && value)
461
- )
462
- }
463
-
464
415
  /**
465
416
  * Normalizes resource metadata into an array.
466
417
  * @param {unknown} value Candidate resource collection.
@@ -180,6 +180,28 @@ export class PcbAssemblyPadMeshBuilder {
180
180
 
181
181
  const holeX = x + Number(pad?.holeOffsetX || 0)
182
182
  const holeY = y + Number(pad?.holeOffsetY || 0)
183
+ if (Number(pad?.holeShape) === 1) {
184
+ const width = PcbAssemblyPadMeshBuilder.#firstPositive([
185
+ pad?.holeWidth,
186
+ pad?.holeGeometry?.width,
187
+ diameter
188
+ ])
189
+ const height = PcbAssemblyPadMeshBuilder.#firstPositive([
190
+ pad?.holeHeight,
191
+ pad?.holeGeometry?.height,
192
+ diameter
193
+ ])
194
+ return PcbAssemblyPadMeshBuilder.#rotatedPoints(
195
+ PcbAssemblyPadMeshBuilder.#rectanglePoints(holeX, holeY, {
196
+ width,
197
+ height
198
+ }),
199
+ holeX,
200
+ holeY,
201
+ Number(pad?.holeRotation ?? pad?.rotation ?? 0) -
202
+ Number(pad?.rotation || 0)
203
+ )
204
+ }
183
205
  const slotLength = PcbAssemblyPadMeshBuilder.#firstPositive([
184
206
  pad?.holeSlotLength,
185
207
  pad?.slotLength,