pcb-scene3d-viewer 1.1.21 → 1.1.31

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 (55) hide show
  1. package/README.md +3 -2
  2. package/docs/api.md +27 -1
  3. package/docs/circuitjson.md +63 -18
  4. package/docs/model-format.md +13 -2
  5. package/package.json +1 -1
  6. package/spec/library-scope.md +2 -2
  7. package/src/PcbAssemblyBoardSubstrateBuilder.mjs +25 -5
  8. package/src/PcbAssemblyComponentMeshBuilder.mjs +762 -0
  9. package/src/PcbAssemblyFillGeometryResolver.mjs +579 -0
  10. package/src/PcbAssemblyFillRingNormalizer.mjs +209 -0
  11. package/src/PcbAssemblyGeometryBuilder.mjs +41 -301
  12. package/src/PcbAssemblyGltfModelMeshParser.mjs +912 -0
  13. package/src/PcbAssemblyGltfValidator.mjs +460 -0
  14. package/src/PcbAssemblyGltfWriter.mjs +801 -0
  15. package/src/PcbAssemblyMeshUtils.mjs +117 -0
  16. package/src/PcbAssemblyModelMeshLoader.mjs +18 -0
  17. package/src/PcbAssemblyPadMeshBuilder.mjs +394 -0
  18. package/src/PcbAssemblyStepWriter.mjs +24 -2
  19. package/src/PcbAssemblyTextModelMeshParser.mjs +602 -0
  20. package/src/PcbModelArchiveExporter.mjs +521 -7
  21. package/src/PcbScene3dCircuitJsonAdapter.mjs +409 -7
  22. package/src/PcbScene3dCircuitJsonModelTransform.mjs +232 -0
  23. package/src/PcbScene3dCompanionBasePlacementAdjuster.mjs +242 -0
  24. package/src/PcbScene3dComponentVisibility.mjs +100 -5
  25. package/src/PcbScene3dController.mjs +25 -55
  26. package/src/PcbScene3dCopperDetailFilter.mjs +86 -9
  27. package/src/PcbScene3dCopperDetailGroupBuilder.mjs +186 -15
  28. package/src/PcbScene3dCopperDrillCutoutBuilder.mjs +258 -0
  29. package/src/PcbScene3dCopperFactory.mjs +99 -85
  30. package/src/PcbScene3dCopperFillMeshBuilder.mjs +393 -0
  31. package/src/PcbScene3dCopperLayerFilter.mjs +32 -3
  32. package/src/PcbScene3dCutoutGeometryFilter.mjs +17 -14
  33. package/src/PcbScene3dExternalCompanionFallback.mjs +202 -0
  34. package/src/PcbScene3dExternalModelCenteringPolicy.mjs +21 -0
  35. package/src/PcbScene3dExternalModelOpacity.mjs +51 -0
  36. package/src/PcbScene3dExternalModelPlacementRepair.mjs +5 -2
  37. package/src/PcbScene3dExternalModelSourceOriginPolicy.mjs +41 -0
  38. package/src/PcbScene3dExternalModels.mjs +16 -7
  39. package/src/PcbScene3dFallbackBodyFactory.mjs +105 -0
  40. package/src/PcbScene3dFallbackVisibility.mjs +58 -1
  41. package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +68 -0
  42. package/src/PcbScene3dPadFactory.mjs +35 -2
  43. package/src/PcbScene3dRenderGroupVisibility.mjs +8 -1
  44. package/src/PcbScene3dRuntime.mjs +94 -100
  45. package/src/PcbScene3dRuntimeHelpers.mjs +39 -0
  46. package/src/PcbScene3dSelectionIndexBuilder.mjs +87 -0
  47. package/src/PcbScene3dSelectionInspectorRenderer.mjs +50 -11
  48. package/src/PcbScene3dSelectionMarkerFactory.mjs +333 -0
  49. package/src/PcbScene3dSelectionMarkerOverlay.mjs +70 -0
  50. package/src/PcbScene3dSelectionStyler.mjs +52 -2
  51. package/src/PcbScene3dStaticBodyFactory.mjs +263 -0
  52. package/src/PcbScene3dText.mjs +1 -0
  53. package/src/PcbScene3dTransparentMeshSplitter.mjs +623 -0
  54. package/src/PcbScene3dViaFactory.mjs +27 -7
  55. package/src/scene3d.mjs +3 -0
@@ -0,0 +1,209 @@
1
+ const COORDINATE_EPSILON = 0.001
2
+ const AREA_EPSILON = 0.001
3
+
4
+ /**
5
+ * Normalizes saved copper fill rings before triangulation and export.
6
+ */
7
+ export class PcbAssemblyFillRingNormalizer {
8
+ /**
9
+ * Normalizes one ring into a clean, optionally oriented loop.
10
+ * @param {unknown[]} points Candidate point rows.
11
+ * @param {{ role?: string, shapeIndex?: number, ringIndex?: number, winding?: 'positive' | 'negative' }} options Normalization options.
12
+ * @returns {{ loop: number[][], diagnostic: object | null, area: number }}
13
+ */
14
+ static normalize(points, options = {}) {
15
+ const rows = PcbAssemblyFillRingNormalizer.#pointRows(points)
16
+ const hasNonFinitePoint = rows.some((row) => !row.isFinite)
17
+ if (hasNonFinitePoint) {
18
+ return PcbAssemblyFillRingNormalizer.#result(
19
+ [],
20
+ 'non-finite-point',
21
+ options,
22
+ rows.length,
23
+ 0
24
+ )
25
+ }
26
+
27
+ const loop = PcbAssemblyFillRingNormalizer.#cleanLoop(
28
+ rows.map((row) => [row.x, row.y])
29
+ )
30
+ const signedArea = PcbAssemblyFillRingNormalizer.signedArea(loop)
31
+ const area = Math.abs(signedArea)
32
+ if (loop.length < 3) {
33
+ return PcbAssemblyFillRingNormalizer.#result(
34
+ loop,
35
+ 'too-few-points',
36
+ options,
37
+ loop.length,
38
+ area
39
+ )
40
+ }
41
+ if (area <= AREA_EPSILON) {
42
+ return PcbAssemblyFillRingNormalizer.#result(
43
+ loop,
44
+ 'near-zero-area',
45
+ options,
46
+ loop.length,
47
+ area
48
+ )
49
+ }
50
+
51
+ return {
52
+ loop: PcbAssemblyFillRingNormalizer.#orientLoop(
53
+ loop,
54
+ signedArea,
55
+ options.winding
56
+ ),
57
+ diagnostic: null,
58
+ area
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Checks whether one loop has enough non-collinear area.
64
+ * @param {number[][]} loop Candidate loop.
65
+ * @returns {boolean}
66
+ */
67
+ static isValidLoop(loop) {
68
+ return (
69
+ Array.isArray(loop) &&
70
+ loop.length >= 3 &&
71
+ Math.abs(PcbAssemblyFillRingNormalizer.signedArea(loop)) >
72
+ AREA_EPSILON
73
+ )
74
+ }
75
+
76
+ /**
77
+ * Computes signed polygon area in source units.
78
+ * @param {number[][]} loop Candidate loop.
79
+ * @returns {number}
80
+ */
81
+ static signedArea(loop) {
82
+ let area = 0
83
+ for (let index = 0; index < (loop || []).length; index += 1) {
84
+ const current = loop[index]
85
+ const next = loop[(index + 1) % loop.length]
86
+ area += current[0] * next[1] - next[0] * current[1]
87
+ }
88
+ return area / 2
89
+ }
90
+
91
+ /**
92
+ * Normalizes candidate point rows into numeric values.
93
+ * @param {unknown[]} points Candidate point rows.
94
+ * @returns {{ x: number, y: number, isFinite: boolean }[]}
95
+ */
96
+ static #pointRows(points) {
97
+ return (Array.isArray(points) ? points : []).map((point) => {
98
+ const x = Number(Array.isArray(point) ? point[0] : point?.x)
99
+ const y = Number(Array.isArray(point) ? point[1] : point?.y)
100
+ return {
101
+ x,
102
+ y,
103
+ isFinite: Number.isFinite(x) && Number.isFinite(y)
104
+ }
105
+ })
106
+ }
107
+
108
+ /**
109
+ * Removes invalid, repeated, and closing points from one loop.
110
+ * @param {number[][]} points Candidate numeric points.
111
+ * @returns {number[][]}
112
+ */
113
+ static #cleanLoop(points) {
114
+ const loop = []
115
+ for (const point of points || []) {
116
+ const x = Number(point?.[0])
117
+ const y = Number(point?.[1])
118
+ if (!Number.isFinite(x) || !Number.isFinite(y)) {
119
+ continue
120
+ }
121
+
122
+ const previous = loop[loop.length - 1]
123
+ if (
124
+ previous &&
125
+ Math.abs(previous[0] - x) < COORDINATE_EPSILON &&
126
+ Math.abs(previous[1] - y) < COORDINATE_EPSILON
127
+ ) {
128
+ continue
129
+ }
130
+ loop.push([x, y])
131
+ }
132
+
133
+ const first = loop[0]
134
+ const last = loop[loop.length - 1]
135
+ if (
136
+ first &&
137
+ last &&
138
+ Math.abs(first[0] - last[0]) < COORDINATE_EPSILON &&
139
+ Math.abs(first[1] - last[1]) < COORDINATE_EPSILON
140
+ ) {
141
+ loop.pop()
142
+ }
143
+
144
+ return loop
145
+ }
146
+
147
+ /**
148
+ * Orients one loop to the requested signed-area winding.
149
+ * @param {number[][]} loop Candidate loop.
150
+ * @param {number} signedArea Signed loop area.
151
+ * @param {'positive' | 'negative' | undefined} winding Requested winding.
152
+ * @returns {number[][]}
153
+ */
154
+ static #orientLoop(loop, signedArea, winding) {
155
+ if (winding === 'positive' && signedArea < 0) {
156
+ return [...loop].reverse()
157
+ }
158
+ if (winding === 'negative' && signedArea > 0) {
159
+ return [...loop].reverse()
160
+ }
161
+ return loop
162
+ }
163
+
164
+ /**
165
+ * Builds a normalization result.
166
+ * @param {number[][]} loop Normalized loop.
167
+ * @param {string} reason Drop reason.
168
+ * @param {{ role?: string, shapeIndex?: number, ringIndex?: number }} options Normalization options.
169
+ * @param {number} pointCount Clean point count.
170
+ * @param {number} area Absolute loop area.
171
+ * @returns {{ loop: number[][], diagnostic: object, area: number }}
172
+ */
173
+ static #result(loop, reason, options, pointCount, area) {
174
+ return {
175
+ loop,
176
+ diagnostic: PcbAssemblyFillRingNormalizer.#diagnostic(
177
+ reason,
178
+ options,
179
+ pointCount,
180
+ area
181
+ ),
182
+ area
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Builds one dropped-ring diagnostic row.
188
+ * @param {string} reason Drop reason.
189
+ * @param {{ role?: string, shapeIndex?: number, ringIndex?: number }} options Normalization options.
190
+ * @param {number} pointCount Clean point count.
191
+ * @param {number} area Absolute loop area.
192
+ * @returns {object}
193
+ */
194
+ static #diagnostic(reason, options, pointCount, area) {
195
+ const diagnostic = {
196
+ reason,
197
+ role: String(options.role || 'ring'),
198
+ pointCount,
199
+ area
200
+ }
201
+ if (Number.isInteger(options.shapeIndex)) {
202
+ diagnostic.shapeIndex = options.shapeIndex
203
+ }
204
+ if (Number.isInteger(options.ringIndex)) {
205
+ diagnostic.ringIndex = options.ringIndex
206
+ }
207
+ return diagnostic
208
+ }
209
+ }
@@ -1,12 +1,14 @@
1
1
  import { PcbAssemblyBoardSubstrateBuilder } from './PcbAssemblyBoardSubstrateBuilder.mjs'
2
+ import { PcbAssemblyComponentMeshBuilder } from './PcbAssemblyComponentMeshBuilder.mjs'
3
+ import { PcbAssemblyFillGeometryResolver } from './PcbAssemblyFillGeometryResolver.mjs'
2
4
  import { PcbAssemblyMeshUtils } from './PcbAssemblyMeshUtils.mjs'
5
+ import { PcbAssemblyPadMeshBuilder } from './PcbAssemblyPadMeshBuilder.mjs'
3
6
 
4
7
  const COPPER_THICKNESS_MIL = 2.2
5
8
  const SILKSCREEN_THICKNESS_MIL = 0.8
6
9
  const BOARD_COLOR = [0.05, 0.32, 0.18]
7
10
  const COPPER_COLOR = [0.85, 0.62, 0.12]
8
11
  const SILKSCREEN_COLOR = [0.96, 0.95, 0.9]
9
- const COMPONENT_COLOR = [0.55, 0.56, 0.58]
10
12
 
11
13
  /**
12
14
  * Converts a prepared 3D scene description into exportable faceted meshes.
@@ -15,7 +17,7 @@ export class PcbAssemblyGeometryBuilder {
15
17
  /**
16
18
  * Builds assembly meshes and diagnostics.
17
19
  * @param {{ board?: object, detail?: object, components?: object[], externalPlacements?: object[] }} sceneDescription Prepared scene description.
18
- * @param {{ modelMeshLoader?: (placement: object) => Promise<object | object[]>, progress?: { advance?: (units: number, message: string) => Promise<void>, finish?: (message: string) => Promise<void> } }} [options] Build options.
20
+ * @param {{ modelMeshLoader?: (placement: object) => Promise<object | object[]>, includeModels?: boolean, renderFallbackBodies?: boolean, progress?: { advance?: (units: number, message: string) => Promise<void>, finish?: (message: string) => Promise<void> } }} [options] Build options.
19
21
  * @returns {Promise<{ meshes: object[], diagnostics: object[] }>}
20
22
  */
21
23
  static async build(sceneDescription, options = {}) {
@@ -35,23 +37,20 @@ export class PcbAssemblyGeometryBuilder {
35
37
  progress
36
38
  )
37
39
  const pcbMeshes = [...boardMeshes, ...copperMeshes, ...silkscreenMeshes]
40
+ const componentResult = await PcbAssemblyComponentMeshBuilder.build(
41
+ sceneDescription,
42
+ options,
43
+ progress
44
+ )
45
+ diagnostics.push(...componentResult.diagnostics)
38
46
  const meshes = [
39
47
  ...PcbAssemblyGeometryBuilder.#translateMeshes(
40
48
  pcbMeshes,
41
49
  PcbAssemblyGeometryBuilder.#boardLocalOffset(sceneDescription)
42
50
  ),
43
- ...(await PcbAssemblyGeometryBuilder.#buildComponentMeshes(
44
- sceneDescription,
45
- options,
46
- diagnostics,
47
- progress
48
- ))
51
+ ...componentResult.meshes
49
52
  ]
50
53
 
51
- PcbAssemblyGeometryBuilder.#appendMissingModelDiagnostics(
52
- sceneDescription,
53
- diagnostics
54
- )
55
54
  await progress?.finish?.('Geometry meshes ready')
56
55
  return {
57
56
  meshes: meshes.filter((mesh) => mesh?.vertices?.length),
@@ -149,14 +148,14 @@ export class PcbAssemblyGeometryBuilder {
149
148
  for (let index = 0; index < fills.length; index += 1) {
150
149
  const fill = fills[index]
151
150
  const side = PcbAssemblyGeometryBuilder.#resolveSide(fill)
152
- const mesh = PcbAssemblyMeshUtils.prism(
151
+ const fillMeshes = PcbAssemblyGeometryBuilder.#fillMeshes(
153
152
  'copper-' + side + '-fill-' + (index + 1),
154
- PcbAssemblyGeometryBuilder.#points(fill),
153
+ fill,
155
154
  side === 'bottom' ? bottomZ : topZ,
156
155
  COPPER_THICKNESS_MIL,
157
156
  COPPER_COLOR
158
157
  )
159
- if (mesh) meshes.push(mesh)
158
+ meshes.push(...fillMeshes)
160
159
  await progress?.advance?.(
161
160
  1,
162
161
  'Building copper fills ' + (index + 1) + '/' + fills.length
@@ -166,19 +165,21 @@ export class PcbAssemblyGeometryBuilder {
166
165
  const pads = PcbAssemblyGeometryBuilder.#array(detail.pads)
167
166
  for (let index = 0; index < pads.length; index += 1) {
168
167
  const pad = pads[index]
169
- const topMesh = PcbAssemblyGeometryBuilder.#padMesh(
168
+ const topMesh = PcbAssemblyPadMeshBuilder.build(
170
169
  'pad-top-' + (index + 1),
171
170
  pad,
172
171
  'top',
173
172
  topZ,
174
- COPPER_THICKNESS_MIL
173
+ COPPER_THICKNESS_MIL,
174
+ COPPER_COLOR
175
175
  )
176
- const bottomMesh = PcbAssemblyGeometryBuilder.#padMesh(
176
+ const bottomMesh = PcbAssemblyPadMeshBuilder.build(
177
177
  'pad-bottom-' + (index + 1),
178
178
  pad,
179
179
  'bottom',
180
180
  bottomZ,
181
- COPPER_THICKNESS_MIL
181
+ COPPER_THICKNESS_MIL,
182
+ COPPER_COLOR
182
183
  )
183
184
  if (topMesh) meshes.push(topMesh)
184
185
  await progress?.advance?.(
@@ -389,164 +390,6 @@ export class PcbAssemblyGeometryBuilder {
389
390
  }
390
391
  }
391
392
 
392
- /**
393
- * Builds component model meshes.
394
- * @param {{ externalPlacements?: object[] }} sceneDescription Prepared scene description.
395
- * @param {{ modelMeshLoader?: (placement: object) => Promise<object | object[]> }} options Build options.
396
- * @param {object[]} diagnostics Mutable diagnostics list.
397
- * @param {{ advance?: (units: number, message: string) => Promise<void> } | null} progress Progress tracker.
398
- * @returns {Promise<object[]>}
399
- */
400
- static async #buildComponentMeshes(
401
- sceneDescription,
402
- options,
403
- diagnostics,
404
- progress = null
405
- ) {
406
- const loader =
407
- typeof options.modelMeshLoader === 'function'
408
- ? options.modelMeshLoader
409
- : null
410
- if (!loader) {
411
- return []
412
- }
413
-
414
- const meshes = []
415
- const placements = PcbAssemblyGeometryBuilder.#array(
416
- sceneDescription?.externalPlacements
417
- ).filter((placement) => placement?.externalModel)
418
- for (
419
- let placementIndex = 0;
420
- placementIndex < placements.length;
421
- placementIndex += 1
422
- ) {
423
- const placement = placements[placementIndex]
424
-
425
- try {
426
- const loaded = await loader(placement)
427
- const loadedMeshes = Array.isArray(loaded) ? loaded : [loaded]
428
- loadedMeshes.filter(Boolean).forEach((mesh, index) => {
429
- const designator = PcbAssemblyMeshUtils.safeName(
430
- placement?.designator || 'component'
431
- )
432
- const transformed = PcbAssemblyMeshUtils.transformMesh(
433
- {
434
- ...mesh,
435
- name:
436
- 'component-' +
437
- designator +
438
- (loadedMeshes.length > 1
439
- ? '-' + (index + 1)
440
- : ''),
441
- color: mesh.color || COMPONENT_COLOR
442
- },
443
- placement
444
- )
445
- meshes.push(transformed)
446
- })
447
- PcbAssemblyGeometryBuilder.#appendModelDiagnostic(
448
- placement,
449
- diagnostics
450
- )
451
- } catch (error) {
452
- diagnostics.push(
453
- PcbAssemblyGeometryBuilder.#diagnostic(
454
- 'warning',
455
- 'component_model_conversion_failed',
456
- 'Could not convert 3D model for ' +
457
- String(placement?.designator || 'component') +
458
- ': ' +
459
- String(error?.message || error)
460
- )
461
- )
462
- }
463
- await progress?.advance?.(
464
- 1,
465
- 'Loading component models ' +
466
- (placementIndex + 1) +
467
- '/' +
468
- placements.length
469
- )
470
- }
471
-
472
- return meshes
473
- }
474
-
475
- /**
476
- * Adds a format-specific component model diagnostic.
477
- * @param {object} placement Component placement.
478
- * @param {object[]} diagnostics Mutable diagnostics list.
479
- * @returns {void}
480
- */
481
- static #appendModelDiagnostic(placement, diagnostics) {
482
- const format = String(
483
- placement?.externalModel?.format || ''
484
- ).toLowerCase()
485
- if (format === 'wrl' || format === 'vrml') {
486
- diagnostics.push(
487
- PcbAssemblyGeometryBuilder.#diagnostic(
488
- 'info',
489
- 'component_wrl_faceted_step',
490
- 'Converted WRL model for ' +
491
- String(placement?.designator || 'component') +
492
- ' as faceted geometry.'
493
- )
494
- )
495
- return
496
- }
497
-
498
- if (format === 'step' || format === 'stp') {
499
- diagnostics.push(
500
- PcbAssemblyGeometryBuilder.#diagnostic(
501
- 'info',
502
- 'component_step_faceted_export',
503
- 'Included STEP model for ' +
504
- String(placement?.designator || 'component') +
505
- ' through export mesh geometry.'
506
- )
507
- )
508
- }
509
- }
510
-
511
- /**
512
- * Adds diagnostics for components without resolved external models.
513
- * @param {{ components?: object[], externalPlacements?: object[] }} sceneDescription Scene description.
514
- * @param {object[]} diagnostics Mutable diagnostics list.
515
- * @returns {void}
516
- */
517
- static #appendMissingModelDiagnostics(sceneDescription, diagnostics) {
518
- const placementDesignators = new Set(
519
- PcbAssemblyGeometryBuilder.#array(
520
- sceneDescription?.externalPlacements
521
- )
522
- .map((placement) => String(placement?.designator || '').trim())
523
- .filter(Boolean)
524
- )
525
-
526
- PcbAssemblyGeometryBuilder.#array(sceneDescription?.components).forEach(
527
- (component) => {
528
- const designator = String(component?.designator || '').trim()
529
- if (
530
- !designator ||
531
- component?.externalModel ||
532
- placementDesignators.has(designator)
533
- ) {
534
- return
535
- }
536
-
537
- diagnostics.push(
538
- PcbAssemblyGeometryBuilder.#diagnostic(
539
- 'warning',
540
- 'component_model_missing',
541
- 'No resolved 3D model was available for ' +
542
- designator +
543
- '.'
544
- )
545
- )
546
- }
547
- )
548
- }
549
-
550
393
  /**
551
394
  * Builds one widened track mesh.
552
395
  * @param {string} name Mesh name.
@@ -584,133 +427,30 @@ export class PcbAssemblyGeometryBuilder {
584
427
  }
585
428
 
586
429
  /**
587
- * Builds one pad mesh.
430
+ * Builds filled polygon meshes.
588
431
  * @param {string} name Mesh name.
589
- * @param {object} pad Pad primitive.
590
- * @param {'top' | 'bottom'} side Pad side.
432
+ * @param {object} fill Filled primitive.
591
433
  * @param {number} z Center Z.
592
- * @param {number} thickness Extrusion thickness.
593
- * @returns {object | null}
594
- */
595
- static #padMesh(name, pad, side, z, thickness) {
596
- const size = PcbAssemblyGeometryBuilder.#padSize(pad, side)
597
- if (!size) {
598
- return null
599
- }
600
-
601
- const offset = PcbAssemblyGeometryBuilder.#padOffset(pad, side)
602
- const x = Number(pad?.x || 0) + offset.x
603
- const y = Number(pad?.y || 0) + offset.y
604
- const shape = PcbAssemblyGeometryBuilder.#padShape(pad, side)
605
- const isCircle =
606
- Math.abs(size.width - size.height) < 0.001 && shape !== 2
607
- const mesh = isCircle
608
- ? PcbAssemblyMeshUtils.cylinder(name, {
609
- x,
610
- y,
611
- z,
612
- radius: size.width / 2,
613
- height: thickness,
614
- color: COPPER_COLOR
615
- })
616
- : PcbAssemblyMeshUtils.box(name, {
617
- x,
618
- y,
619
- z,
620
- width: size.width,
621
- depth: size.height,
622
- height: thickness,
623
- color: COPPER_COLOR
624
- })
625
-
626
- return PcbAssemblyGeometryBuilder.#rotateMeshAroundZ(
627
- mesh,
628
- Number(pad?.rotation || 0),
629
- x,
630
- y
631
- )
632
- }
633
-
634
- /**
635
- * Resolves side-specific pad dimensions.
636
- * @param {object} pad Pad primitive.
637
- * @param {'top' | 'bottom'} side Pad side.
638
- * @returns {{ width: number, height: number } | null}
639
- */
640
- static #padSize(pad, side) {
641
- const prefix = side === 'bottom' ? 'Bottom' : 'Top'
642
- let width = PcbAssemblyGeometryBuilder.#firstPositive([
643
- pad?.['size' + prefix + 'X']
644
- ])
645
- let height = PcbAssemblyGeometryBuilder.#firstPositive([
646
- pad?.['size' + prefix + 'Y']
647
- ])
648
-
649
- if (side === 'top' && !width) {
650
- width = PcbAssemblyGeometryBuilder.#firstPositive([
651
- pad?.width,
652
- pad?.sizeX,
653
- pad?.diameter,
654
- pad?.sizeMidX,
655
- pad?.sizeBottomX
656
- ])
657
- }
658
- if (side === 'top' && !height) {
659
- height = PcbAssemblyGeometryBuilder.#firstPositive([
660
- pad?.height,
661
- pad?.sizeY,
662
- pad?.diameter,
663
- pad?.sizeMidY,
664
- pad?.sizeBottomY,
665
- width
666
- ])
667
- }
668
- if (width && !height) {
669
- height = width
670
- }
671
-
672
- return width && height
673
- ? {
674
- width: Math.max(width, 1),
675
- height: Math.max(height, 1)
676
- }
677
- : null
678
- }
679
-
680
- /**
681
- * Resolves side-specific pad center offset.
682
- * @param {object} pad Pad primitive.
683
- * @param {'top' | 'bottom'} side Pad side.
684
- * @returns {{ x: number, y: number }}
685
- */
686
- static #padOffset(pad, side) {
687
- const prefix = side === 'bottom' ? 'Bottom' : 'Top'
688
-
689
- return {
690
- x: Number(pad?.['offset' + prefix + 'X'] || 0),
691
- y: Number(pad?.['offset' + prefix + 'Y'] || 0)
692
- }
693
- }
694
-
695
- /**
696
- * Resolves the renderer pad shape code for one side.
697
- * @param {object} pad Pad primitive.
698
- * @param {'top' | 'bottom'} side Pad side.
699
- * @returns {number}
434
+ * @param {number} thickness Mesh thickness.
435
+ * @param {number[]} color Mesh color.
436
+ * @returns {object[]}
700
437
  */
701
- static #padShape(pad, side) {
702
- const prefix = side === 'bottom' ? 'Bottom' : 'Top'
703
- const rounded = pad?.['roundedRectShape' + prefix]
704
- if (Number.isInteger(Number(rounded))) {
705
- return Number(rounded)
706
- }
707
-
708
- return Number(
709
- pad?.['shape' + prefix] ||
710
- pad?.shapeMid ||
711
- (side === 'top' ? pad?.shapeBottom : pad?.shapeTop) ||
712
- 0
713
- )
438
+ static #fillMeshes(name, fill, z, thickness, color) {
439
+ const loopSets = PcbAssemblyFillGeometryResolver.resolveAll(fill)
440
+ return loopSets
441
+ .map((loops, index) =>
442
+ PcbAssemblyMeshUtils.prismWithHoles(
443
+ loopSets.length > 1
444
+ ? name + '-island-' + (index + 1)
445
+ : name,
446
+ loops.outer,
447
+ loops.holes,
448
+ z,
449
+ thickness,
450
+ color
451
+ )
452
+ )
453
+ .filter(Boolean)
714
454
  }
715
455
 
716
456
  /**