pcb-scene3d-viewer 1.1.48 → 1.1.50

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.48",
3
+ "version": "1.1.50",
4
4
  "description": "Reusable Three.js PCB 3D scene viewer for normalized ECAD and CircuitJSON scene descriptions",
5
5
  "keywords": [
6
6
  "pcb",
@@ -55,6 +55,7 @@
55
55
  "circuitjson-toolkit": "^1.0.10",
56
56
  "earcut": "^3.0.2",
57
57
  "fflate": "^0.8.2",
58
+ "polygon-clipping": "^0.15.7",
58
59
  "three": "^0.183.2"
59
60
  },
60
61
  "devDependencies": {
@@ -420,7 +420,7 @@ export class PcbScene3dBoardAssemblyPresentation {
420
420
  * @returns {number}
421
421
  */
422
422
  static #resolveSurfaceColor(board, options) {
423
- return PcbScene3dBoardMaterialPalette.resolveSurfaceColor(board, {
423
+ return PcbScene3dBoardMaterialPalette.resolveBoardSurfaceColor(board, {
424
424
  hasBoardAssemblyModel: true,
425
425
  sourceFormat: options?.sourceFormat
426
426
  })
@@ -3,6 +3,7 @@
3
3
  */
4
4
  export class PcbScene3dBoardMaterialPalette {
5
5
  static #DEFAULT_SURFACE_COLOR = 0x2a5f27
6
+ static #BOARD_SURFACE_DARKEN_RATIO = 0.88
6
7
 
7
8
  /**
8
9
  * Resolves the solder-mask face color for the generated board shell.
@@ -25,6 +26,19 @@ export class PcbScene3dBoardMaterialPalette {
25
26
  : PcbScene3dBoardMaterialPalette.#DEFAULT_SURFACE_COLOR
26
27
  }
27
28
 
29
+ /**
30
+ * Resolves the darker display color for visible board solder-mask faces.
31
+ * @param {{ surfaceColor?: number } | undefined} board Board metadata.
32
+ * @param {{ hasBoardAssemblyModel?: boolean, sourceFormat?: string }} [options] Scene options.
33
+ * @returns {number}
34
+ */
35
+ static resolveBoardSurfaceColor(board, options = {}) {
36
+ return PcbScene3dBoardMaterialPalette.#darkenHexColor(
37
+ PcbScene3dBoardMaterialPalette.resolveSurfaceColor(board, options),
38
+ PcbScene3dBoardMaterialPalette.#BOARD_SURFACE_DARKEN_RATIO
39
+ )
40
+ }
41
+
28
42
  /**
29
43
  * Returns true when the source format supplies display-stable board colors.
30
44
  * @param {{ sourceFormat?: string }} options Scene options.
@@ -52,4 +66,19 @@ export class PcbScene3dBoardMaterialPalette {
52
66
  static isGeneratedBodyVisible(options = {}) {
53
67
  return true
54
68
  }
69
+
70
+ /**
71
+ * Darkens a packed RGB color while preserving its hue.
72
+ * @param {number} color Packed RGB color.
73
+ * @param {number} ratio Channel multiplier.
74
+ * @returns {number}
75
+ */
76
+ static #darkenHexColor(color, ratio) {
77
+ const multiplier = Math.min(Math.max(Number(ratio || 0), 0), 1)
78
+
79
+ return [16, 8, 0].reduce((output, shift) => {
80
+ const channel = Math.round(((color >> shift) & 255) * multiplier)
81
+ return output | (channel << shift)
82
+ }, 0)
83
+ }
55
84
  }
@@ -609,10 +609,13 @@ export class PcbScene3dBoardSolderMaskFactory {
609
609
  */
610
610
  static #buildMaterial(THREE, board, side, sourceFormat) {
611
611
  return new THREE.MeshStandardMaterial({
612
- color: PcbScene3dBoardMaterialPalette.resolveSurfaceColor(board, {
613
- hasBoardAssemblyModel: true,
614
- sourceFormat
615
- }),
612
+ color: PcbScene3dBoardMaterialPalette.resolveBoardSurfaceColor(
613
+ board,
614
+ {
615
+ hasBoardAssemblyModel: true,
616
+ sourceFormat
617
+ }
618
+ ),
616
619
  ...PcbScene3dMaterialFinish.semiMatteSolderMaskProperties(),
617
620
  polygonOffset: true,
618
621
  polygonOffsetFactor: -1,
@@ -96,6 +96,10 @@ export class PcbScene3dCopperDetailGroupBuilder {
96
96
  ),
97
97
  drillCutouts,
98
98
  drillDetail: sceneDescription?.detail,
99
+ unionCoveredLayerPrimitives:
100
+ PcbScene3dCopperDetailGroupBuilder.#usesGerberLayerUnion(
101
+ sceneDescription
102
+ ),
99
103
  occlusionCutouts:
100
104
  PcbScene3dCopperDetailGroupBuilder.#resolveCoveredCopperOcclusions(
101
105
  sceneDescription?.detail
@@ -136,6 +140,18 @@ export class PcbScene3dCopperDetailGroupBuilder {
136
140
  )
137
141
  }
138
142
 
143
+ /**
144
+ * Checks whether the source format represents copper as Gerber polarity union.
145
+ * @param {object | undefined} sceneDescription Scene description.
146
+ * @returns {boolean}
147
+ */
148
+ static #usesGerberLayerUnion(sceneDescription) {
149
+ return (
150
+ String(sceneDescription?.sourceFormat || '').toLowerCase() ===
151
+ 'gerber'
152
+ )
153
+ }
154
+
139
155
  /**
140
156
  * Builds exposed copper detail.
141
157
  * @param {any} THREE Three.js namespace.
@@ -1,4 +1,5 @@
1
1
  import { PcbScene3dArcUtils } from './PcbScene3dArcUtils.mjs'
2
+ import { PcbScene3dCopperPrismBuilder } from './PcbScene3dCopperPrismBuilder.mjs'
2
3
  import { PcbScene3dPadFactory } from './PcbScene3dPadFactory.mjs'
3
4
  import { PcbScene3dCopperTextFactory } from './PcbScene3dCopperTextFactory.mjs'
4
5
  import { PcbScene3dMaskCoveredCopperMaterial } from './PcbScene3dMaskCoveredCopperMaterial.mjs'
@@ -7,6 +8,7 @@ import { PcbScene3dCopperLayerFilter } from './PcbScene3dCopperLayerFilter.mjs'
7
8
  import { PcbScene3dCopperFillMeshBuilder } from './PcbScene3dCopperFillMeshBuilder.mjs'
8
9
  import { PcbScene3dMaskCoveredCopperSideGroupBuilder } from './PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs'
9
10
  import { PcbScene3dCopperDrillCutoutBuilder } from './PcbScene3dCopperDrillCutoutBuilder.mjs'
11
+ import { PcbScene3dCopperFillAreaClipper } from './PcbScene3dCopperFillAreaClipper.mjs'
10
12
 
11
13
  /**
12
14
  * Builds copper-detail meshes for the interactive 3D PCB scene.
@@ -15,7 +17,6 @@ export class PcbScene3dCopperFactory {
15
17
  static #ARC_SEGMENT_DEGREES = 3
16
18
  static #FULL_CIRCLE_EPSILON = 0.001
17
19
  static #ROUND_CAP_SEGMENTS = 16
18
- static #COPPER_THICKNESS_MIL = 2.2
19
20
  static #COPPER_COLOR = 0xd9a61d
20
21
 
21
22
  /**
@@ -23,7 +24,7 @@ export class PcbScene3dCopperFactory {
23
24
  * @returns {number}
24
25
  */
25
26
  static visualHalfThicknessMil() {
26
- return PcbScene3dCopperFactory.#COPPER_THICKNESS_MIL / 2
27
+ return PcbScene3dCopperPrismBuilder.halfThicknessMil()
27
28
  }
28
29
 
29
30
  /**
@@ -101,7 +102,7 @@ export class PcbScene3dCopperFactory {
101
102
  * @param {number} topZ
102
103
  * @param {number} bottomZ
103
104
  * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
104
- * @param {{ solderMaskColor?: number, occlusionCutouts?: object, drillCutouts?: any[], drillDetail?: object }} [options]
105
+ * @param {{ solderMaskColor?: number, occlusionCutouts?: object, drillCutouts?: any[], drillDetail?: object, unionCoveredLayerPrimitives?: boolean }} [options]
105
106
  * @returns {any}
106
107
  */
107
108
  static buildMaskCoveredGroup(
@@ -149,7 +150,8 @@ export class PcbScene3dCopperFactory {
149
150
  normalizeBoardPoint,
150
151
  false,
151
152
  material,
152
- topCutouts
153
+ topCutouts,
154
+ options
153
155
  )
154
156
  const bottomGroup = PcbScene3dCopperFactory.#buildMaskCoveredSideGroup(
155
157
  THREE,
@@ -168,7 +170,8 @@ export class PcbScene3dCopperFactory {
168
170
  normalizeBoardPoint,
169
171
  true,
170
172
  material,
171
- bottomCutouts
173
+ bottomCutouts,
174
+ options
172
175
  )
173
176
 
174
177
  if (topGroup.children.length) group.add(topGroup)
@@ -221,7 +224,7 @@ export class PcbScene3dCopperFactory {
221
224
  THREE,
222
225
  detail?.fills || [],
223
226
  z,
224
- PcbScene3dCopperFactory.#COPPER_THICKNESS_MIL,
227
+ PcbScene3dCopperPrismBuilder.thicknessMil(),
225
228
  normalizeBoardPoint,
226
229
  mirrorY,
227
230
  PcbScene3dCopperFactory.#buildMaterial(THREE),
@@ -268,6 +271,7 @@ export class PcbScene3dCopperFactory {
268
271
  * @param {boolean} mirrorY
269
272
  * @param {any} material Shared covered-trace material.
270
273
  * @param {{ x: number, y: number }[][]} occlusionCutouts Silkscreen ink polygons covering this copper.
274
+ * @param {{ unionCoveredLayerPrimitives?: boolean }} [options] Build options.
271
275
  * @returns {any}
272
276
  */
273
277
  static #buildMaskCoveredSideGroup(
@@ -277,9 +281,10 @@ export class PcbScene3dCopperFactory {
277
281
  normalizeBoardPoint,
278
282
  mirrorY,
279
283
  material,
280
- occlusionCutouts
284
+ occlusionCutouts,
285
+ options = {}
281
286
  ) {
282
- const trackMesh = PcbScene3dCopperFactory.#buildTrackMesh(
287
+ let trackMesh = PcbScene3dCopperFactory.#buildTrackMesh(
283
288
  THREE,
284
289
  detail?.tracks || [],
285
290
  z,
@@ -288,7 +293,7 @@ export class PcbScene3dCopperFactory {
288
293
  material,
289
294
  occlusionCutouts
290
295
  )
291
- const arcMesh = PcbScene3dCopperFactory.#buildArcMesh(
296
+ let arcMesh = PcbScene3dCopperFactory.#buildArcMesh(
292
297
  THREE,
293
298
  detail?.arcs || [],
294
299
  z,
@@ -297,15 +302,42 @@ export class PcbScene3dCopperFactory {
297
302
  material,
298
303
  occlusionCutouts
299
304
  )
305
+ if (
306
+ PcbScene3dCopperFactory.#shouldUnionCoveredLayerPrimitives(options)
307
+ ) {
308
+ trackMesh = PcbScene3dCopperFillAreaClipper.filter(
309
+ THREE,
310
+ trackMesh,
311
+ detail?.fills || [],
312
+ normalizeBoardPoint,
313
+ mirrorY,
314
+ { subdividePartialTriangles: false }
315
+ )
316
+ arcMesh = PcbScene3dCopperFillAreaClipper.filter(
317
+ THREE,
318
+ arcMesh,
319
+ detail?.fills || [],
320
+ normalizeBoardPoint,
321
+ mirrorY,
322
+ { subdividePartialTriangles: false }
323
+ )
324
+ }
300
325
  const fillMesh = PcbScene3dCopperFillMeshBuilder.build(
301
326
  THREE,
302
327
  detail?.fills || [],
303
328
  z,
304
- PcbScene3dCopperFactory.#COPPER_THICKNESS_MIL,
329
+ PcbScene3dCopperPrismBuilder.thicknessMil(),
305
330
  normalizeBoardPoint,
306
331
  mirrorY,
307
332
  material,
308
- occlusionCutouts
333
+ occlusionCutouts,
334
+ {
335
+ surfaceOnly: true,
336
+ clipContainedFillOverlaps:
337
+ PcbScene3dCopperFactory.#shouldUnionCoveredLayerPrimitives(
338
+ options
339
+ )
340
+ }
309
341
  )
310
342
  return PcbScene3dMaskCoveredCopperSideGroupBuilder.build(THREE, {
311
343
  trackMesh,
@@ -325,6 +357,15 @@ export class PcbScene3dCopperFactory {
325
357
  return String(options?.coordinateSystem || '') === 'kicad-3d-y-up'
326
358
  }
327
359
 
360
+ /**
361
+ * Checks whether covered primitives should render as one layer union.
362
+ * @param {{ unionCoveredLayerPrimitives?: boolean } | undefined} options Build options.
363
+ * @returns {boolean}
364
+ */
365
+ static #shouldUnionCoveredLayerPrimitives(options) {
366
+ return options?.unionCoveredLayerPrimitives === true
367
+ }
368
+
328
369
  /**
329
370
  * Builds one widened copper-track mesh for one face.
330
371
  * @param {any} THREE
@@ -524,13 +565,13 @@ export class PcbScene3dCopperFactory {
524
565
  { x: start.x - normalX, y: start.y - normalY },
525
566
  z
526
567
  )
527
- PcbScene3dCopperFactory.#appendBoundarySideTriangles(
568
+ PcbScene3dCopperPrismBuilder.appendBoundarySideTriangles(
528
569
  positions,
529
570
  { x: start.x + normalX, y: start.y + normalY },
530
571
  { x: end.x + normalX, y: end.y + normalY },
531
572
  z
532
573
  )
533
- PcbScene3dCopperFactory.#appendBoundarySideTriangles(
574
+ PcbScene3dCopperPrismBuilder.appendBoundarySideTriangles(
534
575
  positions,
535
576
  { x: end.x - normalX, y: end.y - normalY },
536
577
  { x: start.x - normalX, y: start.y - normalY },
@@ -608,14 +649,14 @@ export class PcbScene3dCopperFactory {
608
649
  }
609
650
 
610
651
  if (innerRadius <= 0.001) {
611
- PcbScene3dCopperFactory.#appendTriangle(
652
+ PcbScene3dCopperPrismBuilder.appendTriangle(
612
653
  positions,
613
654
  { x: center.x, y: center.y },
614
655
  outerStart,
615
656
  outerEnd,
616
657
  z
617
658
  )
618
- PcbScene3dCopperFactory.#appendBoundarySideTriangles(
659
+ PcbScene3dCopperPrismBuilder.appendBoundarySideTriangles(
619
660
  positions,
620
661
  outerStart,
621
662
  outerEnd,
@@ -641,13 +682,13 @@ export class PcbScene3dCopperFactory {
641
682
  innerStart,
642
683
  z
643
684
  )
644
- PcbScene3dCopperFactory.#appendBoundarySideTriangles(
685
+ PcbScene3dCopperPrismBuilder.appendBoundarySideTriangles(
645
686
  positions,
646
687
  outerStart,
647
688
  outerEnd,
648
689
  z
649
690
  )
650
- PcbScene3dCopperFactory.#appendBoundarySideTriangles(
691
+ PcbScene3dCopperPrismBuilder.appendBoundarySideTriangles(
651
692
  positions,
652
693
  innerEnd,
653
694
  innerStart,
@@ -710,8 +751,8 @@ export class PcbScene3dCopperFactory {
710
751
  * @returns {void}
711
752
  */
712
753
  static #appendQuadTriangles(positions, a, b, c, d, z) {
713
- PcbScene3dCopperFactory.#appendTriangle(positions, a, b, c, z)
714
- PcbScene3dCopperFactory.#appendTriangle(positions, a, c, d, z)
754
+ PcbScene3dCopperPrismBuilder.appendTriangle(positions, a, b, c, z)
755
+ PcbScene3dCopperPrismBuilder.appendTriangle(positions, a, c, d, z)
715
756
  }
716
757
 
717
758
  /**
@@ -738,7 +779,7 @@ export class PcbScene3dCopperFactory {
738
779
  (Math.PI * 2 * (index + 1)) /
739
780
  PcbScene3dCopperFactory.#ROUND_CAP_SEGMENTS
740
781
 
741
- PcbScene3dCopperFactory.#appendTriangle(
782
+ PcbScene3dCopperPrismBuilder.appendTriangle(
742
783
  positions,
743
784
  center,
744
785
  {
@@ -751,7 +792,7 @@ export class PcbScene3dCopperFactory {
751
792
  },
752
793
  z
753
794
  )
754
- PcbScene3dCopperFactory.#appendBoundarySideTriangles(
795
+ PcbScene3dCopperPrismBuilder.appendBoundarySideTriangles(
755
796
  positions,
756
797
  {
757
798
  x: center.x + Math.cos(startAngle) * safeRadius,
@@ -835,7 +876,7 @@ export class PcbScene3dCopperFactory {
835
876
  endAngle
836
877
  )
837
878
 
838
- PcbScene3dCopperFactory.#appendTriangle(
879
+ PcbScene3dCopperPrismBuilder.appendTriangle(
839
880
  positions,
840
881
  center,
841
882
  start,
@@ -843,7 +884,7 @@ export class PcbScene3dCopperFactory {
843
884
  z
844
885
  )
845
886
  if (includeSideWall) {
846
- PcbScene3dCopperFactory.#appendBoundarySideTriangles(
887
+ PcbScene3dCopperPrismBuilder.appendBoundarySideTriangles(
847
888
  positions,
848
889
  start,
849
890
  end,
@@ -917,74 +958,4 @@ export class PcbScene3dCopperFactory {
917
958
  y: mirrorY ? -point.y : point.y
918
959
  }
919
960
  }
920
-
921
- /**
922
- * Appends one shallow triangular prism into the position buffer.
923
- * @param {number[]} positions Position buffer.
924
- * @param {{ x: number, y: number }} a First point.
925
- * @param {{ x: number, y: number }} b Second point.
926
- * @param {{ x: number, y: number }} c Third point.
927
- * @param {number} z Center Z position.
928
- * @returns {void}
929
- */
930
- static #appendTriangle(positions, a, b, c, z) {
931
- const halfThickness = PcbScene3dCopperFactory.#COPPER_THICKNESS_MIL / 2
932
- const topZ = z + halfThickness
933
- const bottomZ = z - halfThickness
934
- positions.push(
935
- a.x,
936
- a.y,
937
- topZ,
938
- b.x,
939
- b.y,
940
- topZ,
941
- c.x,
942
- c.y,
943
- topZ,
944
- c.x,
945
- c.y,
946
- bottomZ,
947
- b.x,
948
- b.y,
949
- bottomZ,
950
- a.x,
951
- a.y,
952
- bottomZ
953
- )
954
- }
955
-
956
- /**
957
- * Appends a side wall for one actual copper boundary edge.
958
- * @param {number[]} positions Position buffer.
959
- * @param {{ x: number, y: number }} start Wall start point.
960
- * @param {{ x: number, y: number }} end Wall end point.
961
- * @param {number} z Center Z position.
962
- * @returns {void}
963
- */
964
- static #appendBoundarySideTriangles(positions, start, end, z) {
965
- const halfThickness = PcbScene3dCopperFactory.#COPPER_THICKNESS_MIL / 2
966
- const topZ = z + halfThickness
967
- const bottomZ = z - halfThickness
968
-
969
- positions.push(
970
- start.x,
971
- start.y,
972
- topZ,
973
- end.x,
974
- end.y,
975
- topZ,
976
- end.x,
977
- end.y,
978
- bottomZ,
979
- start.x,
980
- start.y,
981
- topZ,
982
- end.x,
983
- end.y,
984
- bottomZ,
985
- start.x,
986
- start.y,
987
- bottomZ
988
- )
989
- }
990
961
  }