pcb-scene3d-viewer 1.1.2 → 1.1.10

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.
@@ -1,15 +1,28 @@
1
1
  import { PcbScene3dArcUtils } from './PcbScene3dArcUtils.mjs'
2
2
  import { PcbScene3dPadFactory } from './PcbScene3dPadFactory.mjs'
3
3
  import { PcbScene3dCopperTextFactory } from './PcbScene3dCopperTextFactory.mjs'
4
+ import { PcbScene3dMaskCoveredCopperMaterial } from './PcbScene3dMaskCoveredCopperMaterial.mjs'
5
+ import { PcbScene3dGeometryZCompressor } from './PcbScene3dGeometryZCompressor.mjs'
4
6
 
5
7
  /**
6
8
  * Builds copper-detail meshes for the interactive 3D PCB scene.
7
9
  */
8
10
  export class PcbScene3dCopperFactory {
11
+ static #ARC_SEGMENT_DEGREES = 3
9
12
  static #TOP_COPPER_LAYER_ID = 1
10
13
  static #BOTTOM_COPPER_LAYER_ID = 32
11
14
  static #FULL_CIRCLE_EPSILON = 0.001
12
15
  static #ROUND_CAP_SEGMENTS = 16
16
+ static #COPPER_THICKNESS_MIL = 2.2
17
+ static #COPPER_COLOR = 0xd9a61d
18
+
19
+ /**
20
+ * Returns half the visual copper extrusion thickness.
21
+ * @returns {number}
22
+ */
23
+ static visualHalfThicknessMil() {
24
+ return PcbScene3dCopperFactory.#COPPER_THICKNESS_MIL / 2
25
+ }
13
26
 
14
27
  /**
15
28
  * Builds the combined top and bottom copper group.
@@ -78,6 +91,71 @@ export class PcbScene3dCopperFactory {
78
91
  return group
79
92
  }
80
93
 
94
+ /**
95
+ * Builds top and bottom traces that are covered by solder mask.
96
+ * @param {any} THREE
97
+ * @param {{ tracks?: any[], arcs?: any[] }} detail Mask-covered detail.
98
+ * @param {number} topZ
99
+ * @param {number} bottomZ
100
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
101
+ * @param {{ solderMaskColor?: number }} [options]
102
+ * @returns {any}
103
+ */
104
+ static buildMaskCoveredGroup(
105
+ THREE,
106
+ detail,
107
+ topZ,
108
+ bottomZ,
109
+ normalizeBoardPoint,
110
+ options = {}
111
+ ) {
112
+ const group = new THREE.Group()
113
+ const material = PcbScene3dMaskCoveredCopperMaterial.build(
114
+ THREE,
115
+ options
116
+ )
117
+ const topGroup = PcbScene3dCopperFactory.#buildMaskCoveredSideGroup(
118
+ THREE,
119
+ {
120
+ tracks: PcbScene3dCopperFactory.#filterTracks(
121
+ detail?.tracks,
122
+ 'top'
123
+ ),
124
+ arcs: PcbScene3dCopperFactory.#filterArcs(detail?.arcs, 'top')
125
+ },
126
+ Math.abs(Number(topZ || 0)),
127
+ normalizeBoardPoint,
128
+ false,
129
+ material
130
+ )
131
+ const bottomGroup = PcbScene3dCopperFactory.#buildMaskCoveredSideGroup(
132
+ THREE,
133
+ {
134
+ tracks: PcbScene3dCopperFactory.#filterTracks(
135
+ detail?.tracks,
136
+ 'bottom'
137
+ ),
138
+ arcs: PcbScene3dCopperFactory.#filterArcs(
139
+ detail?.arcs,
140
+ 'bottom'
141
+ )
142
+ },
143
+ Math.abs(Number(bottomZ || 0)),
144
+ normalizeBoardPoint,
145
+ true,
146
+ material
147
+ )
148
+
149
+ if (topGroup.children.length) {
150
+ group.add(topGroup)
151
+ }
152
+ if (bottomGroup.children.length) {
153
+ group.add(bottomGroup)
154
+ }
155
+
156
+ return group
157
+ }
158
+
81
159
  /**
82
160
  * Builds one side-specific copper group.
83
161
  * @param {any} THREE
@@ -152,6 +230,65 @@ export class PcbScene3dCopperFactory {
152
230
  return group
153
231
  }
154
232
 
233
+ /**
234
+ * Builds one side of the mask-covered trace relief.
235
+ * @param {any} THREE
236
+ * @param {{ tracks?: any[], arcs?: any[] }} detail Mask-covered detail.
237
+ * @param {number} z
238
+ * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
239
+ * @param {boolean} mirrorY
240
+ * @param {any} material Shared covered-trace material.
241
+ * @returns {any}
242
+ */
243
+ static #buildMaskCoveredSideGroup(
244
+ THREE,
245
+ detail,
246
+ z,
247
+ normalizeBoardPoint,
248
+ mirrorY,
249
+ material
250
+ ) {
251
+ const group = new THREE.Group()
252
+ const trackMesh = PcbScene3dCopperFactory.#buildTrackMesh(
253
+ THREE,
254
+ detail?.tracks || [],
255
+ z,
256
+ normalizeBoardPoint,
257
+ mirrorY,
258
+ material
259
+ )
260
+ const arcMesh = PcbScene3dCopperFactory.#buildArcMesh(
261
+ THREE,
262
+ detail?.arcs || [],
263
+ z,
264
+ normalizeBoardPoint,
265
+ mirrorY,
266
+ material
267
+ )
268
+
269
+ if (trackMesh) {
270
+ PcbScene3dGeometryZCompressor.compressMaskCoveredCopperMesh(
271
+ trackMesh,
272
+ z
273
+ )
274
+ trackMesh.name = 'mask-covered-copper-tracks'
275
+ group.add(trackMesh)
276
+ }
277
+ if (arcMesh) {
278
+ PcbScene3dGeometryZCompressor.compressMaskCoveredCopperMesh(
279
+ arcMesh,
280
+ z
281
+ )
282
+ arcMesh.name = 'mask-covered-copper-arcs'
283
+ group.add(arcMesh)
284
+ }
285
+ if (mirrorY && group.children.length) {
286
+ group.rotation.x = Math.PI
287
+ }
288
+
289
+ return group
290
+ }
291
+
155
292
  /**
156
293
  * Checks whether copper text glyph strokes are already in y-up scene space.
157
294
  * @param {{ coordinateSystem?: string } | undefined} options
@@ -164,13 +301,21 @@ export class PcbScene3dCopperFactory {
164
301
  /**
165
302
  * Builds one widened copper-track mesh for one face.
166
303
  * @param {any} THREE
167
- * @param {{ x1?: number, y1?: number, x2?: number, y2?: number, width?: number }[]} tracks
304
+ * @param {{ x1?: number, y1?: number, x2?: number, y2?: number, width?: number, capStartRound?: boolean, capEndRound?: boolean, capStartSideWall?: boolean, capEndSideWall?: boolean }[]} tracks
168
305
  * @param {number} z
169
306
  * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
170
307
  * @param {boolean} mirrorY
308
+ * @param {any | null} [material] Optional material override.
171
309
  * @returns {any | null}
172
310
  */
173
- static #buildTrackMesh(THREE, tracks, z, normalizeBoardPoint, mirrorY) {
311
+ static #buildTrackMesh(
312
+ THREE,
313
+ tracks,
314
+ z,
315
+ normalizeBoardPoint,
316
+ mirrorY,
317
+ material = null
318
+ ) {
174
319
  const positions = []
175
320
 
176
321
  for (const track of tracks) {
@@ -191,11 +336,16 @@ export class PcbScene3dCopperFactory {
191
336
  start,
192
337
  end,
193
338
  Number(track?.width || 0),
194
- z
339
+ z,
340
+ track
195
341
  )
196
342
  }
197
343
 
198
- return PcbScene3dCopperFactory.#buildStrokeMesh(THREE, positions)
344
+ return PcbScene3dCopperFactory.#buildStrokeMesh(
345
+ THREE,
346
+ positions,
347
+ material
348
+ )
199
349
  }
200
350
 
201
351
  /**
@@ -205,9 +355,17 @@ export class PcbScene3dCopperFactory {
205
355
  * @param {number} z
206
356
  * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
207
357
  * @param {boolean} mirrorY
358
+ * @param {any | null} [material] Optional material override.
208
359
  * @returns {any | null}
209
360
  */
210
- static #buildArcMesh(THREE, arcs, z, normalizeBoardPoint, mirrorY) {
361
+ static #buildArcMesh(
362
+ THREE,
363
+ arcs,
364
+ z,
365
+ normalizeBoardPoint,
366
+ mirrorY,
367
+ material = null
368
+ ) {
211
369
  const positions = []
212
370
 
213
371
  for (const arc of arcs) {
@@ -226,16 +384,21 @@ export class PcbScene3dCopperFactory {
226
384
  )
227
385
  }
228
386
 
229
- return PcbScene3dCopperFactory.#buildStrokeMesh(THREE, positions)
387
+ return PcbScene3dCopperFactory.#buildStrokeMesh(
388
+ THREE,
389
+ positions,
390
+ material
391
+ )
230
392
  }
231
393
 
232
394
  /**
233
395
  * Builds one copper stroke mesh from triangle positions.
234
396
  * @param {any} THREE
235
397
  * @param {number[]} positions
398
+ * @param {any | null} [material] Optional material override.
236
399
  * @returns {any | null}
237
400
  */
238
- static #buildStrokeMesh(THREE, positions) {
401
+ static #buildStrokeMesh(THREE, positions, material = null) {
239
402
  if (!positions.length) {
240
403
  return null
241
404
  }
@@ -249,7 +412,7 @@ export class PcbScene3dCopperFactory {
249
412
 
250
413
  return new THREE.Mesh(
251
414
  geometry,
252
- PcbScene3dCopperFactory.#buildMaterial(THREE)
415
+ material || PcbScene3dCopperFactory.#buildMaterial(THREE)
253
416
  )
254
417
  }
255
418
 
@@ -260,7 +423,7 @@ export class PcbScene3dCopperFactory {
260
423
  */
261
424
  static #buildMaterial(THREE) {
262
425
  return new THREE.MeshStandardMaterial({
263
- color: 0xd9a61d,
426
+ color: PcbScene3dCopperFactory.#COPPER_COLOR,
264
427
  roughness: 0.38,
265
428
  metalness: 0.55,
266
429
  side: THREE.DoubleSide,
@@ -318,9 +481,17 @@ export class PcbScene3dCopperFactory {
318
481
  * @param {{ x: number, y: number }} end
319
482
  * @param {number} width
320
483
  * @param {number} z
484
+ * @param {{ capStartRound?: boolean, capEndRound?: boolean, capStartSideWall?: boolean, capEndSideWall?: boolean }} [options]
321
485
  * @returns {void}
322
486
  */
323
- static #appendTrackTriangles(positions, start, end, width, z) {
487
+ static #appendTrackTriangles(
488
+ positions,
489
+ start,
490
+ end,
491
+ width,
492
+ z,
493
+ options = {}
494
+ ) {
324
495
  const dx = end.x - start.x
325
496
  const dy = end.y - start.y
326
497
  const length = Math.hypot(dx, dy)
@@ -352,25 +523,47 @@ export class PcbScene3dCopperFactory {
352
523
  { x: start.x - normalX, y: start.y - normalY },
353
524
  z
354
525
  )
355
- PcbScene3dCopperFactory.#appendDiscTriangles(
526
+ PcbScene3dCopperFactory.#appendBoundarySideTriangles(
356
527
  positions,
357
- start,
358
- halfWidth,
528
+ { x: start.x + normalX, y: start.y + normalY },
529
+ { x: end.x + normalX, y: end.y + normalY },
359
530
  z
360
531
  )
361
- PcbScene3dCopperFactory.#appendDiscTriangles(
532
+ PcbScene3dCopperFactory.#appendBoundarySideTriangles(
362
533
  positions,
363
- end,
364
- halfWidth,
534
+ { x: end.x - normalX, y: end.y - normalY },
535
+ { x: start.x - normalX, y: start.y - normalY },
365
536
  z
366
537
  )
538
+ if (options?.capStartRound !== false) {
539
+ PcbScene3dCopperFactory.#appendRoundCapTriangles(
540
+ positions,
541
+ start,
542
+ halfWidth,
543
+ z,
544
+ -dx / length,
545
+ -dy / length,
546
+ options?.capStartSideWall !== false
547
+ )
548
+ }
549
+ if (options?.capEndRound !== false) {
550
+ PcbScene3dCopperFactory.#appendRoundCapTriangles(
551
+ positions,
552
+ end,
553
+ halfWidth,
554
+ z,
555
+ dx / length,
556
+ dy / length,
557
+ options?.capEndSideWall !== false
558
+ )
559
+ }
367
560
  }
368
561
 
369
562
  /**
370
563
  * Appends one widened arc band as triangles.
371
564
  * @param {number[]} positions
372
565
  * @param {{ x: number, y: number }} center
373
- * @param {{ radius?: number, width?: number, startAngle?: number, endAngle?: number }} arc
566
+ * @param {{ radius?: number, width?: number, startAngle?: number, endAngle?: number, sweepAngle?: number }} arc
374
567
  * @param {number} z
375
568
  * @param {boolean} mirrorY
376
569
  * @returns {void}
@@ -381,10 +574,7 @@ export class PcbScene3dCopperFactory {
381
574
  const outerRadius = radius + strokeWidth / 2
382
575
  const innerRadius = Math.max(radius - strokeWidth / 2, 0)
383
576
  const startAngleRad = (Number(arc?.startAngle || 0) * Math.PI) / 180
384
- const deltaAngleDeg = PcbScene3dArcUtils.resolveSweepDelta(
385
- Number(arc?.startAngle || 0),
386
- Number(arc?.endAngle || 0)
387
- )
577
+ const deltaAngleDeg = PcbScene3dArcUtils.resolveArcSweepDelta(arc)
388
578
  const isFullCircle =
389
579
  Math.abs(deltaAngleDeg) <=
390
580
  PcbScene3dCopperFactory.#FULL_CIRCLE_EPSILON ||
@@ -395,7 +585,10 @@ export class PcbScene3dCopperFactory {
395
585
  : (deltaAngleDeg * Math.PI) / 180
396
586
  const segments = Math.max(
397
587
  isFullCircle ? 20 : 8,
398
- Math.ceil((Math.abs(deltaAngleRad) / Math.PI) * 18)
588
+ Math.ceil(
589
+ Math.abs(deltaAngleDeg) /
590
+ PcbScene3dCopperFactory.#ARC_SEGMENT_DEGREES
591
+ )
399
592
  )
400
593
  const yDirection = mirrorY ? -1 : 1
401
594
 
@@ -421,6 +614,12 @@ export class PcbScene3dCopperFactory {
421
614
  outerEnd,
422
615
  z
423
616
  )
617
+ PcbScene3dCopperFactory.#appendBoundarySideTriangles(
618
+ positions,
619
+ outerStart,
620
+ outerEnd,
621
+ z
622
+ )
424
623
  continue
425
624
  }
426
625
 
@@ -441,19 +640,45 @@ export class PcbScene3dCopperFactory {
441
640
  innerStart,
442
641
  z
443
642
  )
643
+ PcbScene3dCopperFactory.#appendBoundarySideTriangles(
644
+ positions,
645
+ outerStart,
646
+ outerEnd,
647
+ z
648
+ )
649
+ PcbScene3dCopperFactory.#appendBoundarySideTriangles(
650
+ positions,
651
+ innerEnd,
652
+ innerStart,
653
+ z
654
+ )
444
655
  }
445
656
 
446
657
  if (!isFullCircle) {
447
- PcbScene3dCopperFactory.#appendDiscTriangles(
658
+ const sweepDirection = deltaAngleRad < 0 ? -1 : 1
659
+ const startTangent = PcbScene3dCopperFactory.#resolveArcTangent(
660
+ startAngleRad,
661
+ yDirection,
662
+ sweepDirection
663
+ )
664
+ const endTangent = PcbScene3dCopperFactory.#resolveArcTangent(
665
+ startAngleRad + deltaAngleRad,
666
+ yDirection,
667
+ sweepDirection
668
+ )
669
+
670
+ PcbScene3dCopperFactory.#appendRoundCapTriangles(
448
671
  positions,
449
672
  {
450
673
  x: center.x + Math.cos(startAngleRad) * radius,
451
674
  y: center.y + Math.sin(startAngleRad) * radius * yDirection
452
675
  },
453
676
  strokeWidth / 2,
454
- z
677
+ z,
678
+ -startTangent.x,
679
+ -startTangent.y
455
680
  )
456
- PcbScene3dCopperFactory.#appendDiscTriangles(
681
+ PcbScene3dCopperFactory.#appendRoundCapTriangles(
457
682
  positions,
458
683
  {
459
684
  x:
@@ -466,7 +691,9 @@ export class PcbScene3dCopperFactory {
466
691
  yDirection
467
692
  },
468
693
  strokeWidth / 2,
469
- z
694
+ z,
695
+ endTangent.x,
696
+ endTangent.y
470
697
  )
471
698
  }
472
699
  }
@@ -525,6 +752,153 @@ export class PcbScene3dCopperFactory {
525
752
  },
526
753
  z
527
754
  )
755
+ PcbScene3dCopperFactory.#appendBoundarySideTriangles(
756
+ positions,
757
+ {
758
+ x: center.x + Math.cos(startAngle) * safeRadius,
759
+ y: center.y + Math.sin(startAngle) * safeRadius
760
+ },
761
+ {
762
+ x: center.x + Math.cos(endAngle) * safeRadius,
763
+ y: center.y + Math.sin(endAngle) * safeRadius
764
+ },
765
+ z
766
+ )
767
+ }
768
+ }
769
+
770
+ /**
771
+ * Appends only the exposed half of one rounded stroke endpoint.
772
+ * @param {number[]} positions Position buffer.
773
+ * @param {{ x: number, y: number }} center Cap center.
774
+ * @param {number} radius Cap radius.
775
+ * @param {number} z Center Z position.
776
+ * @param {number} outwardX Unit X direction pointing out of the stroke.
777
+ * @param {number} outwardY Unit Y direction pointing out of the stroke.
778
+ * @param {boolean} [includeSideWall] Whether to emit the cap perimeter wall.
779
+ * @returns {void}
780
+ */
781
+ static #appendRoundCapTriangles(
782
+ positions,
783
+ center,
784
+ radius,
785
+ z,
786
+ outwardX,
787
+ outwardY,
788
+ includeSideWall = true
789
+ ) {
790
+ const safeRadius = Math.max(Number(radius || 0), 0)
791
+ const outwardLength = Math.hypot(outwardX, outwardY)
792
+ if (safeRadius <= 0) {
793
+ return
794
+ }
795
+ if (outwardLength <= 0.001) {
796
+ PcbScene3dCopperFactory.#appendDiscTriangles(
797
+ positions,
798
+ center,
799
+ safeRadius,
800
+ z
801
+ )
802
+ return
803
+ }
804
+
805
+ const unitX = outwardX / outwardLength
806
+ const unitY = outwardY / outwardLength
807
+ const normalX = -unitY
808
+ const normalY = unitX
809
+
810
+ for (
811
+ let index = 0;
812
+ index < PcbScene3dCopperFactory.#ROUND_CAP_SEGMENTS;
813
+ index += 1
814
+ ) {
815
+ const startAngle =
816
+ -Math.PI / 2 +
817
+ (Math.PI * index) / PcbScene3dCopperFactory.#ROUND_CAP_SEGMENTS
818
+ const endAngle =
819
+ -Math.PI / 2 +
820
+ (Math.PI * (index + 1)) /
821
+ PcbScene3dCopperFactory.#ROUND_CAP_SEGMENTS
822
+ const start = PcbScene3dCopperFactory.#resolveCapPoint(
823
+ center,
824
+ safeRadius,
825
+ unitX,
826
+ unitY,
827
+ normalX,
828
+ normalY,
829
+ startAngle
830
+ )
831
+ const end = PcbScene3dCopperFactory.#resolveCapPoint(
832
+ center,
833
+ safeRadius,
834
+ unitX,
835
+ unitY,
836
+ normalX,
837
+ normalY,
838
+ endAngle
839
+ )
840
+
841
+ PcbScene3dCopperFactory.#appendTriangle(
842
+ positions,
843
+ center,
844
+ start,
845
+ end,
846
+ z
847
+ )
848
+ if (includeSideWall) {
849
+ PcbScene3dCopperFactory.#appendBoundarySideTriangles(
850
+ positions,
851
+ start,
852
+ end,
853
+ z
854
+ )
855
+ }
856
+ }
857
+ }
858
+
859
+ /**
860
+ * Resolves one point on an oriented round cap boundary.
861
+ * @param {{ x: number, y: number }} center Cap center.
862
+ * @param {number} radius Cap radius.
863
+ * @param {number} unitX Outward X direction.
864
+ * @param {number} unitY Outward Y direction.
865
+ * @param {number} normalX Cap normal X direction.
866
+ * @param {number} normalY Cap normal Y direction.
867
+ * @param {number} angle Local cap angle.
868
+ * @returns {{ x: number, y: number }}
869
+ */
870
+ static #resolveCapPoint(
871
+ center,
872
+ radius,
873
+ unitX,
874
+ unitY,
875
+ normalX,
876
+ normalY,
877
+ angle
878
+ ) {
879
+ return {
880
+ x:
881
+ center.x +
882
+ unitX * Math.cos(angle) * radius +
883
+ normalX * Math.sin(angle) * radius,
884
+ y:
885
+ center.y +
886
+ unitY * Math.cos(angle) * radius +
887
+ normalY * Math.sin(angle) * radius
888
+ }
889
+ }
890
+
891
+ /**
892
+ * Resolves one normalized centerline tangent for an arc endpoint.
893
+ * @param {number} angle Endpoint angle in radians.
894
+ * @param {number} yDirection Mirrored Y direction.
895
+ * @param {number} sweepDirection Arc sweep direction.
896
+ * @returns {{ x: number, y: number }}
897
+ */
898
+ static #resolveArcTangent(angle, yDirection, sweepDirection) {
899
+ return {
900
+ x: -Math.sin(angle) * sweepDirection,
901
+ y: Math.cos(angle) * yDirection * sweepDirection
528
902
  }
529
903
  }
530
904
 
@@ -548,15 +922,73 @@ export class PcbScene3dCopperFactory {
548
922
  }
549
923
 
550
924
  /**
551
- * Appends one triangle into the position buffer.
552
- * @param {number[]} positions
553
- * @param {{ x: number, y: number }} a
554
- * @param {{ x: number, y: number }} b
555
- * @param {{ x: number, y: number }} c
556
- * @param {number} z
925
+ * Appends one shallow triangular prism into the position buffer.
926
+ * @param {number[]} positions Position buffer.
927
+ * @param {{ x: number, y: number }} a First point.
928
+ * @param {{ x: number, y: number }} b Second point.
929
+ * @param {{ x: number, y: number }} c Third point.
930
+ * @param {number} z Center Z position.
557
931
  * @returns {void}
558
932
  */
559
933
  static #appendTriangle(positions, a, b, c, z) {
560
- positions.push(a.x, a.y, z, b.x, b.y, z, c.x, c.y, z)
934
+ const halfThickness = PcbScene3dCopperFactory.#COPPER_THICKNESS_MIL / 2
935
+ const topZ = z + halfThickness
936
+ const bottomZ = z - halfThickness
937
+
938
+ positions.push(
939
+ a.x,
940
+ a.y,
941
+ topZ,
942
+ b.x,
943
+ b.y,
944
+ topZ,
945
+ c.x,
946
+ c.y,
947
+ topZ,
948
+ c.x,
949
+ c.y,
950
+ bottomZ,
951
+ b.x,
952
+ b.y,
953
+ bottomZ,
954
+ a.x,
955
+ a.y,
956
+ bottomZ
957
+ )
958
+ }
959
+
960
+ /**
961
+ * Appends a side wall for one actual copper boundary edge.
962
+ * @param {number[]} positions Position buffer.
963
+ * @param {{ x: number, y: number }} start Wall start point.
964
+ * @param {{ x: number, y: number }} end Wall end point.
965
+ * @param {number} z Center Z position.
966
+ * @returns {void}
967
+ */
968
+ static #appendBoundarySideTriangles(positions, start, end, z) {
969
+ const halfThickness = PcbScene3dCopperFactory.#COPPER_THICKNESS_MIL / 2
970
+ const topZ = z + halfThickness
971
+ const bottomZ = z - halfThickness
972
+
973
+ positions.push(
974
+ start.x,
975
+ start.y,
976
+ topZ,
977
+ end.x,
978
+ end.y,
979
+ topZ,
980
+ end.x,
981
+ end.y,
982
+ bottomZ,
983
+ start.x,
984
+ start.y,
985
+ topZ,
986
+ end.x,
987
+ end.y,
988
+ bottomZ,
989
+ start.x,
990
+ start.y,
991
+ bottomZ
992
+ )
561
993
  }
562
994
  }
@@ -17,7 +17,7 @@ export class PcbScene3dCopperTextFactory {
17
17
  * @param {any[]} texts
18
18
  * @param {number} z
19
19
  * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
20
- * @param {{ side?: 'top' | 'bottom', mirrorY?: boolean, materialColor?: number, filterSide?: boolean, glyphYUp?: boolean, drillCutouts?: { x: number, y: number }[][] }} [options]
20
+ * @param {{ side?: 'top' | 'bottom', mirrorY?: boolean, materialColor?: number, materialProperties?: { roughness?: number, metalness?: number }, filterSide?: boolean, glyphYUp?: boolean, drillCutouts?: { x: number, y: number }[][] }} [options]
21
21
  * @returns {any}
22
22
  */
23
23
  static buildGroup(THREE, texts, z, normalizeBoardPoint, options = {}) {
@@ -74,7 +74,8 @@ export class PcbScene3dCopperTextFactory {
74
74
  THREE,
75
75
  PcbScene3dCopperTextFactory.#resolveMaterialColor(
76
76
  options?.materialColor
77
- )
77
+ ),
78
+ options?.materialProperties
78
79
  )
79
80
  )
80
81
  mesh.name = 'copper-text'
@@ -471,13 +472,15 @@ export class PcbScene3dCopperTextFactory {
471
472
  * Builds the shared copper text material.
472
473
  * @param {any} THREE
473
474
  * @param {number} color
475
+ * @param {{ roughness?: number, metalness?: number }} [materialProperties]
474
476
  * @returns {any}
475
477
  */
476
- static #buildMaterial(THREE, color) {
478
+ static #buildMaterial(THREE, color, materialProperties = {}) {
477
479
  return new THREE.MeshStandardMaterial({
478
480
  color,
479
481
  roughness: 0.38,
480
482
  metalness: 0.55,
483
+ ...materialProperties,
481
484
  side: THREE.DoubleSide
482
485
  })
483
486
  }