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,623 @@
1
+ /**
2
+ * Splits translucent meshes into independently sortable render chunks.
3
+ */
4
+ export class PcbScene3dTransparentMeshSplitter {
5
+ static #PLANE_QUANTIZATION = 10000
6
+
7
+ static #TRIANGLE_VERTEX_COUNT = 3
8
+
9
+ /**
10
+ * Replaces translucent meshes below one root with sortable chunk groups.
11
+ * @param {any} THREE Three.js namespace.
12
+ * @param {any} rootObject Root object to mutate.
13
+ * @returns {void}
14
+ */
15
+ static split(THREE, rootObject) {
16
+ if (!rootObject) {
17
+ return
18
+ }
19
+
20
+ PcbScene3dTransparentMeshSplitter.#collectMeshes(rootObject).forEach(
21
+ (mesh) =>
22
+ PcbScene3dTransparentMeshSplitter.#replaceWithSortableGroup(
23
+ THREE,
24
+ mesh
25
+ )
26
+ )
27
+ }
28
+
29
+ /**
30
+ * Builds a sortable replacement for one translucent mesh when useful.
31
+ * @param {any} THREE Three.js namespace.
32
+ * @param {any} mesh Source mesh.
33
+ * @returns {any}
34
+ */
35
+ static build(THREE, mesh) {
36
+ if (!PcbScene3dTransparentMeshSplitter.#canBuild(THREE, mesh)) {
37
+ return mesh
38
+ }
39
+
40
+ const chunks = PcbScene3dTransparentMeshSplitter.#buildChunks(
41
+ THREE,
42
+ mesh
43
+ )
44
+ if (!chunks.length) {
45
+ return mesh
46
+ }
47
+
48
+ return PcbScene3dTransparentMeshSplitter.#buildGroup(
49
+ THREE,
50
+ mesh,
51
+ chunks
52
+ )
53
+ }
54
+
55
+ /**
56
+ * Determines whether the required Three.js constructors are available.
57
+ * @param {any} THREE Three.js namespace.
58
+ * @param {any} mesh Source mesh.
59
+ * @returns {boolean}
60
+ */
61
+ static #canBuild(THREE, mesh) {
62
+ return Boolean(
63
+ THREE?.Group &&
64
+ THREE?.Mesh &&
65
+ THREE?.BufferGeometry &&
66
+ THREE?.BufferAttribute &&
67
+ mesh?.geometry &&
68
+ mesh?.material &&
69
+ !mesh?.userData?.scene3dTransparentMeshChunk &&
70
+ PcbScene3dTransparentMeshSplitter.#hasTransparentMaterial(
71
+ mesh.material
72
+ )
73
+ )
74
+ }
75
+
76
+ /**
77
+ * Collects transparent meshes before mutating the object tree.
78
+ * @param {any} rootObject Root object.
79
+ * @returns {any[]}
80
+ */
81
+ static #collectMeshes(rootObject) {
82
+ const meshes = []
83
+ rootObject?.traverse?.((object) => {
84
+ if (
85
+ object?.geometry &&
86
+ object?.material &&
87
+ !object?.userData?.scene3dTransparentMeshChunk &&
88
+ PcbScene3dTransparentMeshSplitter.#hasTransparentMaterial(
89
+ object.material
90
+ )
91
+ ) {
92
+ meshes.push(object)
93
+ }
94
+ })
95
+
96
+ return meshes
97
+ }
98
+
99
+ /**
100
+ * Replaces one mesh in its parent while preserving sibling order.
101
+ * @param {any} THREE Three.js namespace.
102
+ * @param {any} mesh Source mesh.
103
+ * @returns {void}
104
+ */
105
+ static #replaceWithSortableGroup(THREE, mesh) {
106
+ const parent = mesh?.parent
107
+ if (!parent) {
108
+ return
109
+ }
110
+
111
+ const replacement = PcbScene3dTransparentMeshSplitter.build(THREE, mesh)
112
+ if (replacement === mesh) {
113
+ return
114
+ }
115
+
116
+ const insertionIndex = Array.isArray(parent.children)
117
+ ? parent.children.indexOf(mesh)
118
+ : -1
119
+ parent.remove?.(mesh)
120
+ parent.add?.(replacement)
121
+
122
+ if (insertionIndex < 0 || !Array.isArray(parent.children)) {
123
+ return
124
+ }
125
+
126
+ const replacementIndex = parent.children.indexOf(replacement)
127
+ if (replacementIndex < 0 || replacementIndex === insertionIndex) {
128
+ return
129
+ }
130
+
131
+ parent.children.splice(replacementIndex, 1)
132
+ parent.children.splice(
133
+ Math.min(insertionIndex, parent.children.length),
134
+ 0,
135
+ replacement
136
+ )
137
+ }
138
+
139
+ /**
140
+ * Builds one group that carries the original mesh transform.
141
+ * @param {any} THREE Three.js namespace.
142
+ * @param {any} mesh Source mesh.
143
+ * @param {any[]} chunks Chunk meshes.
144
+ * @returns {any}
145
+ */
146
+ static #buildGroup(THREE, mesh, chunks) {
147
+ const group = new THREE.Group()
148
+ group.name = mesh.name || ''
149
+ group.visible = mesh.visible !== false
150
+ group.renderOrder = Number(mesh.renderOrder || 0)
151
+ group.frustumCulled = mesh.frustumCulled !== false
152
+ group.userData = {
153
+ ...(mesh.userData || {}),
154
+ scene3dTransparentMeshChunks: true
155
+ }
156
+ PcbScene3dTransparentMeshSplitter.#copyTransform(mesh, group)
157
+ chunks.forEach((chunk) => group.add(chunk))
158
+ return group
159
+ }
160
+
161
+ /**
162
+ * Copies the source Object3D transform state to a replacement group.
163
+ * @param {any} source Source object.
164
+ * @param {any} target Target object.
165
+ * @returns {void}
166
+ */
167
+ static #copyTransform(source, target) {
168
+ target.position?.copy?.(source.position)
169
+ target.quaternion?.copy?.(source.quaternion)
170
+ target.scale?.copy?.(source.scale)
171
+ if (source.matrix && target.matrix?.copy) {
172
+ target.matrix.copy(source.matrix)
173
+ }
174
+ if (typeof source.matrixAutoUpdate === 'boolean') {
175
+ target.matrixAutoUpdate = source.matrixAutoUpdate
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Builds centroid-positioned coplanar face chunks for one mesh.
181
+ * @param {any} THREE Three.js namespace.
182
+ * @param {any} mesh Source mesh.
183
+ * @returns {any[]}
184
+ */
185
+ static #buildChunks(THREE, mesh) {
186
+ const geometry = mesh.geometry
187
+ const positionAttribute =
188
+ PcbScene3dTransparentMeshSplitter.#getAttribute(
189
+ geometry,
190
+ 'position'
191
+ )
192
+ if (!positionAttribute) {
193
+ return []
194
+ }
195
+
196
+ const indexArray = geometry.index?.array || null
197
+ const elementCount = indexArray
198
+ ? indexArray.length
199
+ : Number(positionAttribute.count || 0)
200
+ const chunkGroups = new Map()
201
+
202
+ PcbScene3dTransparentMeshSplitter.#resolveGroups(
203
+ geometry,
204
+ elementCount
205
+ ).forEach((group) => {
206
+ const material = PcbScene3dTransparentMeshSplitter.#resolveMaterial(
207
+ mesh.material,
208
+ group.materialIndex
209
+ )
210
+ const end = Math.min(group.start + group.count, elementCount)
211
+ for (
212
+ let offset = group.start;
213
+ offset + 2 < end;
214
+ offset +=
215
+ PcbScene3dTransparentMeshSplitter.#TRIANGLE_VERTEX_COUNT
216
+ ) {
217
+ const sourceIndices =
218
+ PcbScene3dTransparentMeshSplitter.#readTriangleIndices(
219
+ indexArray,
220
+ offset
221
+ )
222
+ PcbScene3dTransparentMeshSplitter.#appendPlaneChunkGroup(
223
+ chunkGroups,
224
+ positionAttribute,
225
+ group.materialIndex,
226
+ material,
227
+ sourceIndices
228
+ )
229
+ }
230
+ })
231
+
232
+ return Array.from(chunkGroups.values())
233
+ .map((chunkGroup) =>
234
+ PcbScene3dTransparentMeshSplitter.#buildChunkMesh(
235
+ THREE,
236
+ mesh,
237
+ chunkGroup.material,
238
+ chunkGroup.sourceIndices
239
+ )
240
+ )
241
+ .filter(Boolean)
242
+ }
243
+
244
+ /**
245
+ * Appends one triangle to its coplanar material chunk group.
246
+ * @param {Map<string, { material: any, sourceIndices: number[] }>} chunkGroups Chunk groups.
247
+ * @param {any} positionAttribute Source position attribute.
248
+ * @param {number} materialIndex Material index.
249
+ * @param {any} material Material instance.
250
+ * @param {number[]} sourceIndices Triangle source indices.
251
+ * @returns {void}
252
+ */
253
+ static #appendPlaneChunkGroup(
254
+ chunkGroups,
255
+ positionAttribute,
256
+ materialIndex,
257
+ material,
258
+ sourceIndices
259
+ ) {
260
+ const planeKey = PcbScene3dTransparentMeshSplitter.#buildPlaneKey(
261
+ positionAttribute,
262
+ sourceIndices
263
+ )
264
+ const groupKey = `${materialIndex}:${planeKey}`
265
+ if (!chunkGroups.has(groupKey)) {
266
+ chunkGroups.set(groupKey, {
267
+ material,
268
+ sourceIndices: []
269
+ })
270
+ }
271
+
272
+ chunkGroups.get(groupKey).sourceIndices.push(...sourceIndices)
273
+ }
274
+
275
+ /**
276
+ * Builds one centered triangle mesh.
277
+ * @param {any} THREE Three.js namespace.
278
+ * @param {any} mesh Source mesh.
279
+ * @param {any} material Material for this chunk.
280
+ * @param {number[]} sourceIndices Source vertex indices.
281
+ * @returns {any | null}
282
+ */
283
+ static #buildChunkMesh(THREE, mesh, material, sourceIndices) {
284
+ const chunkData = PcbScene3dTransparentMeshSplitter.#buildChunkGeometry(
285
+ THREE,
286
+ mesh.geometry,
287
+ sourceIndices
288
+ )
289
+ if (!chunkData) {
290
+ return null
291
+ }
292
+
293
+ const chunk = new THREE.Mesh(chunkData.geometry, material)
294
+ chunk.name = mesh.name || ''
295
+ chunk.position.set(
296
+ chunkData.centroid.x,
297
+ chunkData.centroid.y,
298
+ chunkData.centroid.z
299
+ )
300
+ chunk.visible = mesh.visible !== false
301
+ chunk.renderOrder = Number(mesh.renderOrder || 0)
302
+ chunk.frustumCulled = mesh.frustumCulled !== false
303
+ chunk.castShadow = mesh.castShadow === true
304
+ chunk.receiveShadow = mesh.receiveShadow === true
305
+ chunk.userData = {
306
+ ...(mesh.userData || {}),
307
+ scene3dTransparentMeshChunk: true
308
+ }
309
+ return chunk
310
+ }
311
+
312
+ /**
313
+ * Builds centered geometry for one triangle chunk.
314
+ * @param {any} THREE Three.js namespace.
315
+ * @param {any} geometry Source geometry.
316
+ * @param {number[]} sourceIndices Source vertex indices.
317
+ * @returns {{ geometry: any, centroid: { x: number, y: number, z: number }} | null}
318
+ */
319
+ static #buildChunkGeometry(THREE, geometry, sourceIndices) {
320
+ const attributes = geometry?.attributes || {}
321
+ const positionAttribute =
322
+ PcbScene3dTransparentMeshSplitter.#getAttribute(
323
+ geometry,
324
+ 'position'
325
+ )
326
+ if (!positionAttribute) {
327
+ return null
328
+ }
329
+
330
+ const centroid = PcbScene3dTransparentMeshSplitter.#calculateCentroid(
331
+ positionAttribute,
332
+ sourceIndices
333
+ )
334
+ const chunkGeometry = new THREE.BufferGeometry()
335
+
336
+ Object.entries(attributes).forEach(([name, attribute]) => {
337
+ const values =
338
+ PcbScene3dTransparentMeshSplitter.#copyAttributeValues(
339
+ attribute,
340
+ sourceIndices
341
+ )
342
+ if (!values.length) {
343
+ return
344
+ }
345
+ if (name === 'position') {
346
+ PcbScene3dTransparentMeshSplitter.#subtractCentroid(
347
+ values,
348
+ attribute.itemSize,
349
+ centroid
350
+ )
351
+ }
352
+ chunkGeometry.setAttribute(
353
+ name,
354
+ new THREE.BufferAttribute(
355
+ new attribute.array.constructor(values),
356
+ attribute.itemSize,
357
+ attribute.normalized === true
358
+ )
359
+ )
360
+ })
361
+
362
+ return { geometry: chunkGeometry, centroid }
363
+ }
364
+
365
+ /**
366
+ * Reads an attribute from BufferGeometry.
367
+ * @param {any} geometry Source geometry.
368
+ * @param {string} name Attribute name.
369
+ * @returns {any | null}
370
+ */
371
+ static #getAttribute(geometry, name) {
372
+ return geometry?.getAttribute?.(name) || geometry?.attributes?.[name]
373
+ }
374
+
375
+ /**
376
+ * Resolves render groups for indexed or non-indexed geometry.
377
+ * @param {any} geometry Source geometry.
378
+ * @param {number} elementCount Total index or vertex elements.
379
+ * @returns {{ start: number, count: number, materialIndex: number }[]}
380
+ */
381
+ static #resolveGroups(geometry, elementCount) {
382
+ const groups = Array.isArray(geometry?.groups) ? geometry.groups : []
383
+ const normalizedGroups = groups
384
+ .map((group) => ({
385
+ start: Math.max(0, Number(group.start || 0)),
386
+ count: Math.max(0, Number(group.count || 0)),
387
+ materialIndex: Math.max(0, Number(group.materialIndex || 0))
388
+ }))
389
+ .filter((group) => group.count > 0)
390
+
391
+ if (normalizedGroups.length) {
392
+ return normalizedGroups
393
+ }
394
+
395
+ return [
396
+ {
397
+ start: 0,
398
+ count: Math.max(0, Number(elementCount || 0)),
399
+ materialIndex: 0
400
+ }
401
+ ]
402
+ }
403
+
404
+ /**
405
+ * Resolves one material from a mesh material or material array.
406
+ * @param {any | any[]} material Mesh material.
407
+ * @param {number} materialIndex Material index.
408
+ * @returns {any}
409
+ */
410
+ static #resolveMaterial(material, materialIndex) {
411
+ if (!Array.isArray(material)) {
412
+ return material
413
+ }
414
+
415
+ return material[materialIndex] || material[0]
416
+ }
417
+
418
+ /**
419
+ * Reads one triangle's source vertex indices.
420
+ * @param {ArrayLike<number> | null} indexArray Optional geometry index.
421
+ * @param {number} offset Triangle start offset.
422
+ * @returns {number[]}
423
+ */
424
+ static #readTriangleIndices(indexArray, offset) {
425
+ return [0, 1, 2].map((index) =>
426
+ Number(indexArray ? indexArray[offset + index] : offset + index)
427
+ )
428
+ }
429
+
430
+ /**
431
+ * Builds a stable key for one triangle's geometric plane.
432
+ * @param {any} attribute Position attribute.
433
+ * @param {number[]} sourceIndices Triangle source indices.
434
+ * @returns {string}
435
+ */
436
+ static #buildPlaneKey(attribute, sourceIndices) {
437
+ const points = sourceIndices.map((sourceIndex) =>
438
+ PcbScene3dTransparentMeshSplitter.#readPoint(attribute, sourceIndex)
439
+ )
440
+ const normal = PcbScene3dTransparentMeshSplitter.#calculateNormal(
441
+ points[0],
442
+ points[1],
443
+ points[2]
444
+ )
445
+ if (!normal) {
446
+ return `triangle:${sourceIndices.join(',')}`
447
+ }
448
+
449
+ const canonicalNormal =
450
+ PcbScene3dTransparentMeshSplitter.#canonicalizeNormal(normal)
451
+ const planeOffset =
452
+ canonicalNormal.x * points[0].x +
453
+ canonicalNormal.y * points[0].y +
454
+ canonicalNormal.z * points[0].z
455
+
456
+ return [
457
+ canonicalNormal.x,
458
+ canonicalNormal.y,
459
+ canonicalNormal.z,
460
+ planeOffset
461
+ ]
462
+ .map((value) =>
463
+ PcbScene3dTransparentMeshSplitter.#quantizePlaneValue(value)
464
+ )
465
+ .join(':')
466
+ }
467
+
468
+ /**
469
+ * Reads one source point from a position attribute.
470
+ * @param {any} attribute Position attribute.
471
+ * @param {number} sourceIndex Source vertex index.
472
+ * @returns {{ x: number, y: number, z: number }}
473
+ */
474
+ static #readPoint(attribute, sourceIndex) {
475
+ const itemSize = Number(attribute?.itemSize || 0)
476
+ const base = sourceIndex * itemSize
477
+ return {
478
+ x: Number(attribute?.array?.[base] || 0),
479
+ y: Number(attribute?.array?.[base + 1] || 0),
480
+ z: Number(attribute?.array?.[base + 2] || 0)
481
+ }
482
+ }
483
+
484
+ /**
485
+ * Calculates a triangle normal from three source points.
486
+ * @param {{ x: number, y: number, z: number }} first First point.
487
+ * @param {{ x: number, y: number, z: number }} second Second point.
488
+ * @param {{ x: number, y: number, z: number }} third Third point.
489
+ * @returns {{ x: number, y: number, z: number } | null}
490
+ */
491
+ static #calculateNormal(first, second, third) {
492
+ const ux = second.x - first.x
493
+ const uy = second.y - first.y
494
+ const uz = second.z - first.z
495
+ const vx = third.x - first.x
496
+ const vy = third.y - first.y
497
+ const vz = third.z - first.z
498
+ const normal = {
499
+ x: uy * vz - uz * vy,
500
+ y: uz * vx - ux * vz,
501
+ z: ux * vy - uy * vx
502
+ }
503
+ const length = Math.hypot(normal.x, normal.y, normal.z)
504
+ if (!(length > 0)) {
505
+ return null
506
+ }
507
+
508
+ return {
509
+ x: normal.x / length,
510
+ y: normal.y / length,
511
+ z: normal.z / length
512
+ }
513
+ }
514
+
515
+ /**
516
+ * Makes opposite-wound triangles on the same plane share one normal.
517
+ * @param {{ x: number, y: number, z: number }} normal Unit normal.
518
+ * @returns {{ x: number, y: number, z: number }}
519
+ */
520
+ static #canonicalizeNormal(normal) {
521
+ const dominantAxis = ['x', 'y', 'z'].reduce((bestAxis, axis) =>
522
+ Math.abs(normal[axis]) > Math.abs(normal[bestAxis])
523
+ ? axis
524
+ : bestAxis
525
+ )
526
+ const sign = normal[dominantAxis] < 0 ? -1 : 1
527
+ return {
528
+ x: normal.x * sign,
529
+ y: normal.y * sign,
530
+ z: normal.z * sign
531
+ }
532
+ }
533
+
534
+ /**
535
+ * Quantizes plane key values to merge floating-point STEP tessellation noise.
536
+ * @param {number} value Plane value.
537
+ * @returns {string}
538
+ */
539
+ static #quantizePlaneValue(value) {
540
+ const rounded = Math.round(
541
+ Number(value || 0) *
542
+ PcbScene3dTransparentMeshSplitter.#PLANE_QUANTIZATION
543
+ )
544
+ return String(Object.is(rounded, -0) ? 0 : rounded)
545
+ }
546
+
547
+ /**
548
+ * Calculates a source triangle centroid.
549
+ * @param {any} attribute Position attribute.
550
+ * @param {number[]} sourceIndices Source vertex indices.
551
+ * @returns {{ x: number, y: number, z: number }}
552
+ */
553
+ static #calculateCentroid(attribute, sourceIndices) {
554
+ const itemSize = Number(attribute.itemSize || 0)
555
+ const centroid = { x: 0, y: 0, z: 0 }
556
+ if (itemSize < 2 || !sourceIndices.length) {
557
+ return centroid
558
+ }
559
+
560
+ sourceIndices.forEach((sourceIndex) => {
561
+ const base = sourceIndex * itemSize
562
+ centroid.x += Number(attribute.array[base] || 0)
563
+ centroid.y += Number(attribute.array[base + 1] || 0)
564
+ centroid.z += Number(attribute.array[base + 2] || 0)
565
+ })
566
+ centroid.x /= sourceIndices.length
567
+ centroid.y /= sourceIndices.length
568
+ centroid.z /= sourceIndices.length
569
+ return centroid
570
+ }
571
+
572
+ /**
573
+ * Copies one geometry attribute for a set of source vertices.
574
+ * @param {any} attribute Source attribute.
575
+ * @param {number[]} sourceIndices Source vertex indices.
576
+ * @returns {number[]}
577
+ */
578
+ static #copyAttributeValues(attribute, sourceIndices) {
579
+ const itemSize = Number(attribute?.itemSize || 0)
580
+ if (!attribute?.array || itemSize <= 0) {
581
+ return []
582
+ }
583
+
584
+ const values = []
585
+ sourceIndices.forEach((sourceIndex) => {
586
+ const base = sourceIndex * itemSize
587
+ for (let offset = 0; offset < itemSize; offset += 1) {
588
+ values.push(Number(attribute.array[base + offset] || 0))
589
+ }
590
+ })
591
+ return values
592
+ }
593
+
594
+ /**
595
+ * Recenters copied position values around a chunk centroid.
596
+ * @param {number[]} values Copied position values.
597
+ * @param {number} itemSize Position item size.
598
+ * @param {{ x: number, y: number, z: number }} centroid Chunk centroid.
599
+ * @returns {void}
600
+ */
601
+ static #subtractCentroid(values, itemSize, centroid) {
602
+ for (let offset = 0; offset < values.length; offset += itemSize) {
603
+ values[offset] -= centroid.x
604
+ values[offset + 1] -= centroid.y
605
+ if (itemSize > 2) {
606
+ values[offset + 2] -= centroid.z
607
+ }
608
+ }
609
+ }
610
+
611
+ /**
612
+ * Detects whether at least one material should be sorted transparently.
613
+ * @param {any | any[]} material Mesh material.
614
+ * @returns {boolean}
615
+ */
616
+ static #hasTransparentMaterial(material) {
617
+ const materials = Array.isArray(material) ? material : [material]
618
+ return materials.some(
619
+ (entry) =>
620
+ entry?.transparent === true && Number(entry?.opacity ?? 1) < 1
621
+ )
622
+ }
623
+ }
@@ -14,16 +14,18 @@ export class PcbScene3dViaFactory {
14
14
  * @param {{ diameter?: number, holeDiameter?: number, x?: number, y?: number, barrelOnly?: boolean }[]} vias
15
15
  * @param {number} thicknessMil
16
16
  * @param {(x: number, y: number) => { x: number, y: number }} normalizeBoardPoint
17
+ * @param {{ material?: any }} [options]
17
18
  * @returns {any}
18
19
  */
19
- static buildGroup(THREE, vias, thicknessMil, normalizeBoardPoint) {
20
+ static buildGroup(
21
+ THREE,
22
+ vias,
23
+ thicknessMil,
24
+ normalizeBoardPoint,
25
+ options = {}
26
+ ) {
20
27
  const group = new THREE.Group()
21
- const material = new THREE.MeshStandardMaterial({
22
- color: 0xcaa24e,
23
- roughness: 0.48,
24
- metalness: 0.42,
25
- side: THREE.DoubleSide
26
- })
28
+ const material = PcbScene3dViaFactory.#resolveMaterial(THREE, options)
27
29
  const geometryCache = new Map()
28
30
 
29
31
  ;(vias || []).forEach((via) => {
@@ -48,6 +50,24 @@ export class PcbScene3dViaFactory {
48
50
  return group
49
51
  }
50
52
 
53
+ /**
54
+ * Resolves the via material.
55
+ * @param {any} THREE
56
+ * @param {{ material?: any }} options
57
+ * @returns {any}
58
+ */
59
+ static #resolveMaterial(THREE, options) {
60
+ return (
61
+ options?.material ||
62
+ new THREE.MeshStandardMaterial({
63
+ color: 0xcaa24e,
64
+ roughness: 0.48,
65
+ metalness: 0.42,
66
+ side: THREE.DoubleSide
67
+ })
68
+ )
69
+ }
70
+
51
71
  /**
52
72
  * Resolves one reusable via geometry from the via drill spec.
53
73
  * @param {any} THREE
package/src/scene3d.mjs CHANGED
@@ -3,6 +3,8 @@ export { PcbAssemblyBoardSubstrateBuilder } from './PcbAssemblyBoardSubstrateBui
3
3
  export { PcbAssemblyExportCoordinateFrame } from './PcbAssemblyExportCoordinateFrame.mjs'
4
4
  export { PcbAssemblyGeometryBuildProgress } from './PcbAssemblyGeometryBuildProgress.mjs'
5
5
  export { PcbAssemblyGeometryBuilder } from './PcbAssemblyGeometryBuilder.mjs'
6
+ export { PcbAssemblyGltfValidator } from './PcbAssemblyGltfValidator.mjs'
7
+ export { PcbAssemblyGltfWriter } from './PcbAssemblyGltfWriter.mjs'
6
8
  export { PcbAssemblyMeshUtils } from './PcbAssemblyMeshUtils.mjs'
7
9
  export { PcbAssemblyModelMeshLoader } from './PcbAssemblyModelMeshLoader.mjs'
8
10
  export { PcbAssemblyPolygonTriangulator } from './PcbAssemblyPolygonTriangulator.mjs'
@@ -38,6 +40,7 @@ export { PcbScene3dRenderGroupVisibility } from './PcbScene3dRenderGroupVisibili
38
40
  export { PcbScene3dRenderScheduler } from './PcbScene3dRenderScheduler.mjs'
39
41
  export { PcbScene3dRuntime } from './PcbScene3dRuntime.mjs'
40
42
  export { PcbScene3dRuntimeBoardMeshes } from './PcbScene3dRuntimeBoardMeshes.mjs'
43
+ export { PcbScene3dSelectionMarkerFactory } from './PcbScene3dSelectionMarkerFactory.mjs'
41
44
  export { PcbScene3dSelectionStyler } from './PcbScene3dSelectionStyler.mjs'
42
45
  export { PcbScene3dSelectionVisibilityBinder } from './PcbScene3dSelectionVisibilityBinder.mjs'
43
46
  export { PcbScene3dShapePathFactory } from './PcbScene3dShapePathFactory.mjs'