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.
- package/README.md +3 -2
- package/docs/api.md +27 -1
- package/docs/circuitjson.md +63 -18
- package/docs/model-format.md +13 -2
- package/package.json +1 -1
- package/spec/library-scope.md +2 -2
- package/src/PcbAssemblyBoardSubstrateBuilder.mjs +25 -5
- package/src/PcbAssemblyComponentMeshBuilder.mjs +762 -0
- package/src/PcbAssemblyFillGeometryResolver.mjs +579 -0
- package/src/PcbAssemblyFillRingNormalizer.mjs +209 -0
- package/src/PcbAssemblyGeometryBuilder.mjs +41 -301
- package/src/PcbAssemblyGltfModelMeshParser.mjs +912 -0
- package/src/PcbAssemblyGltfValidator.mjs +460 -0
- package/src/PcbAssemblyGltfWriter.mjs +801 -0
- package/src/PcbAssemblyMeshUtils.mjs +117 -0
- package/src/PcbAssemblyModelMeshLoader.mjs +18 -0
- package/src/PcbAssemblyPadMeshBuilder.mjs +394 -0
- package/src/PcbAssemblyStepWriter.mjs +24 -2
- package/src/PcbAssemblyTextModelMeshParser.mjs +602 -0
- package/src/PcbModelArchiveExporter.mjs +521 -7
- package/src/PcbScene3dCircuitJsonAdapter.mjs +409 -7
- package/src/PcbScene3dCircuitJsonModelTransform.mjs +232 -0
- package/src/PcbScene3dCompanionBasePlacementAdjuster.mjs +242 -0
- package/src/PcbScene3dComponentVisibility.mjs +100 -5
- package/src/PcbScene3dController.mjs +25 -55
- package/src/PcbScene3dCopperDetailFilter.mjs +86 -9
- package/src/PcbScene3dCopperDetailGroupBuilder.mjs +186 -15
- package/src/PcbScene3dCopperDrillCutoutBuilder.mjs +258 -0
- package/src/PcbScene3dCopperFactory.mjs +99 -85
- package/src/PcbScene3dCopperFillMeshBuilder.mjs +393 -0
- package/src/PcbScene3dCopperLayerFilter.mjs +32 -3
- package/src/PcbScene3dCutoutGeometryFilter.mjs +17 -14
- package/src/PcbScene3dExternalCompanionFallback.mjs +202 -0
- package/src/PcbScene3dExternalModelCenteringPolicy.mjs +21 -0
- package/src/PcbScene3dExternalModelOpacity.mjs +51 -0
- package/src/PcbScene3dExternalModelPlacementRepair.mjs +5 -2
- package/src/PcbScene3dExternalModelSourceOriginPolicy.mjs +41 -0
- package/src/PcbScene3dExternalModels.mjs +16 -7
- package/src/PcbScene3dFallbackBodyFactory.mjs +105 -0
- package/src/PcbScene3dFallbackVisibility.mjs +58 -1
- package/src/PcbScene3dMaskCoveredCopperSideGroupBuilder.mjs +68 -0
- package/src/PcbScene3dPadFactory.mjs +35 -2
- package/src/PcbScene3dRenderGroupVisibility.mjs +8 -1
- package/src/PcbScene3dRuntime.mjs +94 -100
- package/src/PcbScene3dRuntimeHelpers.mjs +39 -0
- package/src/PcbScene3dSelectionIndexBuilder.mjs +87 -0
- package/src/PcbScene3dSelectionInspectorRenderer.mjs +50 -11
- package/src/PcbScene3dSelectionMarkerFactory.mjs +333 -0
- package/src/PcbScene3dSelectionMarkerOverlay.mjs +70 -0
- package/src/PcbScene3dSelectionStyler.mjs +52 -2
- package/src/PcbScene3dStaticBodyFactory.mjs +263 -0
- package/src/PcbScene3dText.mjs +1 -0
- package/src/PcbScene3dTransparentMeshSplitter.mjs +623 -0
- package/src/PcbScene3dViaFactory.mjs +27 -7
- 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(
|
|
20
|
+
static buildGroup(
|
|
21
|
+
THREE,
|
|
22
|
+
vias,
|
|
23
|
+
thicknessMil,
|
|
24
|
+
normalizeBoardPoint,
|
|
25
|
+
options = {}
|
|
26
|
+
) {
|
|
20
27
|
const group = new THREE.Group()
|
|
21
|
-
const material =
|
|
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'
|