pcb-scene3d-viewer 1.1.20 → 1.1.22

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pcb-scene3d-viewer",
3
- "version": "1.1.20",
3
+ "version": "1.1.22",
4
4
  "description": "Reusable Three.js PCB 3D scene viewer for normalized ECAD and CircuitJSON scene descriptions",
5
5
  "keywords": [
6
6
  "pcb",
@@ -145,11 +145,13 @@ export class PcbAssemblyBoardSubstrateBuilder {
145
145
  * @returns {number[][][]}
146
146
  */
147
147
  static #boardHoleLoops(sceneDescription, outer) {
148
- const contourLoops =
149
- PcbAssemblyBoardSubstrateBuilder.#drillCutoutLoops(sceneDescription)
150
- const rawLoops = contourLoops.length
151
- ? contourLoops
152
- : PcbAssemblyBoardSubstrateBuilder.#primitiveHoleLoops(
148
+ const primitiveLoops =
149
+ PcbAssemblyBoardSubstrateBuilder.#primitiveHoleLoops(
150
+ sceneDescription
151
+ )
152
+ const rawLoops = primitiveLoops.length
153
+ ? primitiveLoops
154
+ : PcbAssemblyBoardSubstrateBuilder.#drillCutoutLoops(
153
155
  sceneDescription
154
156
  )
155
157
  const seen = new Set()
@@ -1,5 +1,6 @@
1
1
  import { PcbAssemblyBoardSubstrateBuilder } from './PcbAssemblyBoardSubstrateBuilder.mjs'
2
2
  import { PcbAssemblyMeshUtils } from './PcbAssemblyMeshUtils.mjs'
3
+ import { PcbAssemblyPadMeshBuilder } from './PcbAssemblyPadMeshBuilder.mjs'
3
4
 
4
5
  const COPPER_THICKNESS_MIL = 2.2
5
6
  const SILKSCREEN_THICKNESS_MIL = 0.8
@@ -166,19 +167,21 @@ export class PcbAssemblyGeometryBuilder {
166
167
  const pads = PcbAssemblyGeometryBuilder.#array(detail.pads)
167
168
  for (let index = 0; index < pads.length; index += 1) {
168
169
  const pad = pads[index]
169
- const topMesh = PcbAssemblyGeometryBuilder.#padMesh(
170
+ const topMesh = PcbAssemblyPadMeshBuilder.build(
170
171
  'pad-top-' + (index + 1),
171
172
  pad,
172
173
  'top',
173
174
  topZ,
174
- COPPER_THICKNESS_MIL
175
+ COPPER_THICKNESS_MIL,
176
+ COPPER_COLOR
175
177
  )
176
- const bottomMesh = PcbAssemblyGeometryBuilder.#padMesh(
178
+ const bottomMesh = PcbAssemblyPadMeshBuilder.build(
177
179
  'pad-bottom-' + (index + 1),
178
180
  pad,
179
181
  'bottom',
180
182
  bottomZ,
181
- COPPER_THICKNESS_MIL
183
+ COPPER_THICKNESS_MIL,
184
+ COPPER_COLOR
182
185
  )
183
186
  if (topMesh) meshes.push(topMesh)
184
187
  await progress?.advance?.(
@@ -583,136 +586,6 @@ export class PcbAssemblyGeometryBuilder {
583
586
  )
584
587
  }
585
588
 
586
- /**
587
- * Builds one pad mesh.
588
- * @param {string} name Mesh name.
589
- * @param {object} pad Pad primitive.
590
- * @param {'top' | 'bottom'} side Pad side.
591
- * @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}
700
- */
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
- )
714
- }
715
-
716
589
  /**
717
590
  * Builds one via mesh.
718
591
  * @param {string} name Mesh name.
@@ -1,3 +1,5 @@
1
+ import earcut from 'earcut'
2
+
1
3
  const FULL_CIRCLE_DEGREES = 360
2
4
 
3
5
  /**
@@ -88,6 +90,100 @@ export class PcbAssemblyMeshUtils {
88
90
  }
89
91
  }
90
92
 
93
+ /**
94
+ * Builds an extruded polygon mesh with inner cutout loops.
95
+ * @param {string} name Mesh name.
96
+ * @param {number[][]} outerPoints Outer polygon points in mils.
97
+ * @param {number[][][]} holeLoops Inner cutout loops in mils.
98
+ * @param {number} z Center Z in mils.
99
+ * @param {number} thickness Extrusion thickness in mils.
100
+ * @param {number[] | undefined} color Optional RGB color.
101
+ * @returns {{ name: string, vertices: number[][], faces: number[][], color?: number[] } | null}
102
+ */
103
+ static prismWithHoles(
104
+ name,
105
+ outerPoints,
106
+ holeLoops,
107
+ z,
108
+ thickness,
109
+ color = undefined
110
+ ) {
111
+ const outer = PcbAssemblyMeshUtils.cleanLoop(outerPoints)
112
+ const holes = (Array.isArray(holeLoops) ? holeLoops : [])
113
+ .map((loop) => PcbAssemblyMeshUtils.cleanLoop(loop))
114
+ .filter((loop) => loop.length >= 3)
115
+ if (outer.length < 3) {
116
+ return null
117
+ }
118
+ if (!holes.length) {
119
+ return PcbAssemblyMeshUtils.prism(name, outer, z, thickness, color)
120
+ }
121
+
122
+ const loops = [outer, ...holes]
123
+ const holeIndexes = []
124
+ const points = []
125
+ const flat = []
126
+ for (const loop of loops) {
127
+ if (points.length) {
128
+ holeIndexes.push(points.length)
129
+ }
130
+ for (const point of loop) {
131
+ points.push(point)
132
+ flat.push(point[0], point[1])
133
+ }
134
+ }
135
+
136
+ const triangles = earcut(flat, holeIndexes, 2)
137
+ if (!triangles.length) {
138
+ return PcbAssemblyMeshUtils.prism(name, outer, z, thickness, color)
139
+ }
140
+
141
+ const halfThickness = Math.max(Number(thickness || 0), 0.001) / 2
142
+ const bottomZ = Number(z || 0) - halfThickness
143
+ const topZ = Number(z || 0) + halfThickness
144
+ const topOffset = points.length
145
+ const vertices = [
146
+ ...points.map((point) => [point[0], point[1], bottomZ]),
147
+ ...points.map((point) => [point[0], point[1], topZ])
148
+ ]
149
+ const faces = []
150
+
151
+ for (let index = 0; index + 2 < triangles.length; index += 3) {
152
+ const a = triangles[index]
153
+ const b = triangles[index + 1]
154
+ const c = triangles[index + 2]
155
+ faces.push([c, b, a])
156
+ faces.push([a + topOffset, b + topOffset, c + topOffset])
157
+ }
158
+
159
+ let loopStart = 0
160
+ PcbAssemblyMeshUtils.#appendLoopWalls(
161
+ faces,
162
+ loopStart,
163
+ outer.length,
164
+ topOffset,
165
+ false
166
+ )
167
+ loopStart += outer.length
168
+ for (const hole of holes) {
169
+ PcbAssemblyMeshUtils.#appendLoopWalls(
170
+ faces,
171
+ loopStart,
172
+ hole.length,
173
+ topOffset,
174
+ true
175
+ )
176
+ loopStart += hole.length
177
+ }
178
+
179
+ return {
180
+ name,
181
+ vertices,
182
+ faces,
183
+ ...(Array.isArray(color) ? { color } : {})
184
+ }
185
+ }
186
+
91
187
  /**
92
188
  * Builds a cylinder mesh from polygonal rings.
93
189
  * @param {string} name Mesh name.
@@ -538,4 +634,25 @@ export class PcbAssemblyMeshUtils {
538
634
  .replace(/^-+|-+$/gu, '')
539
635
  .slice(0, 80)
540
636
  }
637
+
638
+ /**
639
+ * Appends side-wall faces for one loop.
640
+ * @param {number[][]} faces Mutable face list.
641
+ * @param {number} start Loop start index.
642
+ * @param {number} length Loop length.
643
+ * @param {number} topOffset Top vertex offset.
644
+ * @param {boolean} reverse Whether to reverse wall orientation.
645
+ * @returns {void}
646
+ */
647
+ static #appendLoopWalls(faces, start, length, topOffset, reverse) {
648
+ for (let index = 0; index < length; index += 1) {
649
+ const current = start + index
650
+ const next = start + ((index + 1) % length)
651
+ faces.push(
652
+ reverse
653
+ ? [next, current, current + topOffset, next + topOffset]
654
+ : [current, next, next + topOffset, current + topOffset]
655
+ )
656
+ }
657
+ }
541
658
  }