pcb-scene3d-viewer 1.0.1
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/AGENTS.md +67 -0
- package/COMMERCIAL-LICENSE.md +15 -0
- package/CONTRIBUTING.md +14 -0
- package/LICENSE +19 -0
- package/LICENSES/AGPL-3.0-or-later.txt +235 -0
- package/LICENSES/CC-BY-SA-4.0.txt +170 -0
- package/LICENSES/LGPL-2.1-or-later.txt +176 -0
- package/LICENSES/LicenseRef-PolyForm-Noncommercial-1.0.0.txt +131 -0
- package/NOTICE.md +36 -0
- package/README.md +128 -0
- package/REUSE.toml +16 -0
- package/docs/api.md +148 -0
- package/docs/circuitjson.md +190 -0
- package/docs/model-format.md +117 -0
- package/docs/testing.md +23 -0
- package/package.json +65 -0
- package/spec/library-scope.md +36 -0
- package/src/PcbModelArchiveExporter.mjs +320 -0
- package/src/PcbScene3dArcUtils.mjs +27 -0
- package/src/PcbScene3dBoardAssemblyPlacement.mjs +36 -0
- package/src/PcbScene3dBoardAssemblyPresentation.mjs +859 -0
- package/src/PcbScene3dBoardEdgeCutoutBuilder.mjs +537 -0
- package/src/PcbScene3dBoardMaterialPalette.mjs +40 -0
- package/src/PcbScene3dBoardShapeFactory.mjs +895 -0
- package/src/PcbScene3dBoardSolderMaskFactory.mjs +613 -0
- package/src/PcbScene3dCameraRig.mjs +168 -0
- package/src/PcbScene3dCircuitJsonAdapter.mjs +545 -0
- package/src/PcbScene3dController.mjs +956 -0
- package/src/PcbScene3dCopperDetailFilter.mjs +490 -0
- package/src/PcbScene3dCopperFactory.mjs +559 -0
- package/src/PcbScene3dCopperTextFactory.mjs +534 -0
- package/src/PcbScene3dCutoutGeometryFilter.mjs +873 -0
- package/src/PcbScene3dDetailCoordinateNormalizer.mjs +65 -0
- package/src/PcbScene3dDrillCutoutFilter.mjs +224 -0
- package/src/PcbScene3dDrillPathFactory.mjs +362 -0
- package/src/PcbScene3dDrillVoidFactory.mjs +268 -0
- package/src/PcbScene3dExternalModelLoadOrder.mjs +54 -0
- package/src/PcbScene3dExternalModels.mjs +968 -0
- package/src/PcbScene3dFallbackVisibility.mjs +82 -0
- package/src/PcbScene3dInteractionHints.mjs +56 -0
- package/src/PcbScene3dMountRig.mjs +53 -0
- package/src/PcbScene3dOutlineBuilder.mjs +210 -0
- package/src/PcbScene3dPadFactory.mjs +553 -0
- package/src/PcbScene3dPresetState.mjs +48 -0
- package/src/PcbScene3dRenderGroupVisibility.mjs +134 -0
- package/src/PcbScene3dRuntime.mjs +996 -0
- package/src/PcbScene3dRuntimeBoardMeshes.mjs +99 -0
- package/src/PcbScene3dSelectionStyler.mjs +252 -0
- package/src/PcbScene3dShapePathFactory.mjs +220 -0
- package/src/PcbScene3dShellRenderer.mjs +131 -0
- package/src/PcbScene3dSilkscreenFactory.mjs +854 -0
- package/src/PcbScene3dSilkscreenStrokeWidthResolver.mjs +81 -0
- package/src/PcbScene3dStepLoader.mjs +611 -0
- package/src/PcbScene3dStrokeFont.mjs +671 -0
- package/src/PcbScene3dStrokeGeometryBuilder.mjs +322 -0
- package/src/PcbScene3dText.mjs +99 -0
- package/src/PcbScene3dTrueTypeTextFactory.mjs +885 -0
- package/src/PcbScene3dViaFactory.mjs +176 -0
- package/src/PcbScene3dViewCompensation.mjs +109 -0
- package/src/PcbScene3dViewScale.mjs +24 -0
- package/src/PcbScene3dViewportResize.mjs +35 -0
- package/src/PcbScene3dWorkerClient.mjs +123 -0
- package/src/index.mjs +1 -0
- package/src/scene3d.mjs +44 -0
- package/src/styles/scene3d.css +295 -0
|
@@ -0,0 +1,859 @@
|
|
|
1
|
+
import { PcbScene3dBoardMaterialPalette } from './PcbScene3dBoardMaterialPalette.mjs'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Applies display-only material adjustments to full-board assembly models.
|
|
5
|
+
*/
|
|
6
|
+
export class PcbScene3dBoardAssemblyPresentation {
|
|
7
|
+
/** @type {number} */
|
|
8
|
+
static #BOARD_COVER_AREA_RATIO = 0.15
|
|
9
|
+
|
|
10
|
+
/** @type {number} */
|
|
11
|
+
static #BOARD_COVER_LONG_SPAN_RATIO = 0.25
|
|
12
|
+
|
|
13
|
+
/** @type {number} */
|
|
14
|
+
static #COARSE_COVER_TRIANGLE_LIMIT = 36
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Hides coarse board/shield surfaces in full-board assemblies so the
|
|
18
|
+
* PCB-derived substrate, copper, and silkscreen remain the visual source.
|
|
19
|
+
* @param {any} modelGroup Loaded board assembly group.
|
|
20
|
+
* @param {{ widthMil?: number, heightMil?: number, thicknessMil?: number } | null | undefined} board Board dimensions.
|
|
21
|
+
* @returns {void}
|
|
22
|
+
*/
|
|
23
|
+
static apply(modelGroup, board) {
|
|
24
|
+
const meshRecords =
|
|
25
|
+
PcbScene3dBoardAssemblyPresentation.#collectMeshRecords(modelGroup)
|
|
26
|
+
const boardBounds =
|
|
27
|
+
PcbScene3dBoardAssemblyPresentation.#resolveBoardEnvelope(
|
|
28
|
+
meshRecords,
|
|
29
|
+
board
|
|
30
|
+
)
|
|
31
|
+
const surfaceColor =
|
|
32
|
+
PcbScene3dBoardAssemblyPresentation.#resolveSurfaceColor(board)
|
|
33
|
+
const edgeColor =
|
|
34
|
+
PcbScene3dBoardAssemblyPresentation.#resolveEdgeColor(board)
|
|
35
|
+
const importedSurfaceColor =
|
|
36
|
+
PcbScene3dBoardAssemblyPresentation.#resolveImportedSurfaceColor(
|
|
37
|
+
meshRecords
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if (Number.isInteger(importedSurfaceColor)) {
|
|
41
|
+
modelGroup.userData = modelGroup.userData || {}
|
|
42
|
+
modelGroup.userData.scene3dBoardAssemblySurfaceColor =
|
|
43
|
+
importedSurfaceColor
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
meshRecords.forEach((record) =>
|
|
47
|
+
PcbScene3dBoardAssemblyPresentation.#applyMeshPresentation(
|
|
48
|
+
record,
|
|
49
|
+
boardBounds,
|
|
50
|
+
surfaceColor,
|
|
51
|
+
edgeColor
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
PcbScene3dBoardAssemblyPresentation.#centerSubstrateOnBoardPlane(
|
|
55
|
+
modelGroup,
|
|
56
|
+
boardBounds
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Resolves assembly meshes into presentation records.
|
|
62
|
+
* @param {any} modelGroup Loaded board assembly group.
|
|
63
|
+
* @returns {{ object: any, meshBounds: { minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, sizeX: number, sizeY: number, sizeZ: number }, materialKind: string, triangleCount: number }[]}
|
|
64
|
+
*/
|
|
65
|
+
static #collectMeshRecords(modelGroup) {
|
|
66
|
+
const records = []
|
|
67
|
+
|
|
68
|
+
PcbScene3dBoardAssemblyPresentation.#traverseModelObjects(
|
|
69
|
+
modelGroup,
|
|
70
|
+
(object) => {
|
|
71
|
+
if (!object?.material) {
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const meshBounds =
|
|
76
|
+
PcbScene3dBoardAssemblyPresentation.#resolveMeshBoundsMil(
|
|
77
|
+
object
|
|
78
|
+
)
|
|
79
|
+
if (!meshBounds) {
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
records.push({
|
|
84
|
+
object,
|
|
85
|
+
meshBounds,
|
|
86
|
+
materialKind:
|
|
87
|
+
PcbScene3dBoardAssemblyPresentation.#resolveMaterialKind(
|
|
88
|
+
object.material
|
|
89
|
+
),
|
|
90
|
+
triangleCount:
|
|
91
|
+
PcbScene3dBoardAssemblyPresentation.#resolveTriangleCount(
|
|
92
|
+
object
|
|
93
|
+
)
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
return records
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Applies display treatment to one assembly mesh record.
|
|
103
|
+
* @param {{ object: any, meshBounds: { minX: number, minY: number, maxX: number, maxY: number }, materialKind: string, triangleCount: number }} record Mesh record.
|
|
104
|
+
* @param {{ minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, widthMil: number, heightMil: number, areaMil: number } | null} boardBounds Board envelope.
|
|
105
|
+
* @param {number} surfaceColor Board substrate color.
|
|
106
|
+
* @param {number} edgeColor Board edge color.
|
|
107
|
+
* @returns {void}
|
|
108
|
+
*/
|
|
109
|
+
static #applyMeshPresentation(
|
|
110
|
+
record,
|
|
111
|
+
boardBounds,
|
|
112
|
+
surfaceColor,
|
|
113
|
+
edgeColor
|
|
114
|
+
) {
|
|
115
|
+
if (record?.materialKind === 'board') {
|
|
116
|
+
PcbScene3dBoardAssemblyPresentation.#applyBoardSubstrateMaterial(
|
|
117
|
+
record.object,
|
|
118
|
+
surfaceColor,
|
|
119
|
+
edgeColor
|
|
120
|
+
)
|
|
121
|
+
record.object.visible = false
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (
|
|
126
|
+
record?.materialKind === 'neutral' &&
|
|
127
|
+
PcbScene3dBoardAssemblyPresentation.#isLargeBoardCoverMesh(
|
|
128
|
+
record,
|
|
129
|
+
boardBounds
|
|
130
|
+
)
|
|
131
|
+
) {
|
|
132
|
+
record.object.visible = false
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Applies solder-mask and substrate-edge colors to an imported board mesh.
|
|
138
|
+
* @param {any} object Board-substrate mesh.
|
|
139
|
+
* @param {number} surfaceColor Solder-mask face color.
|
|
140
|
+
* @param {number} edgeColor Substrate edge color.
|
|
141
|
+
* @returns {void}
|
|
142
|
+
*/
|
|
143
|
+
static #applyBoardSubstrateMaterial(object, surfaceColor, edgeColor) {
|
|
144
|
+
if (
|
|
145
|
+
PcbScene3dBoardAssemblyPresentation.#applyBoardFaceGroups(
|
|
146
|
+
object,
|
|
147
|
+
surfaceColor,
|
|
148
|
+
edgeColor
|
|
149
|
+
)
|
|
150
|
+
) {
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
PcbScene3dBoardAssemblyPresentation.#applyMaterialColor(
|
|
155
|
+
object?.material,
|
|
156
|
+
surfaceColor
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Splits board substrate faces into mask and edge material groups.
|
|
162
|
+
* @param {any} object Board-substrate mesh.
|
|
163
|
+
* @param {number} surfaceColor Solder-mask face color.
|
|
164
|
+
* @param {number} edgeColor Substrate edge color.
|
|
165
|
+
* @returns {boolean}
|
|
166
|
+
*/
|
|
167
|
+
static #applyBoardFaceGroups(object, surfaceColor, edgeColor) {
|
|
168
|
+
const geometry = object?.geometry
|
|
169
|
+
const positions = geometry?.attributes?.position?.array
|
|
170
|
+
const triangleCount =
|
|
171
|
+
PcbScene3dBoardAssemblyPresentation.#resolveGeometryTriangleCount(
|
|
172
|
+
geometry
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if (!positions?.length || triangleCount <= 0) {
|
|
176
|
+
return false
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const sourceMaterial = Array.isArray(object.material)
|
|
180
|
+
? object.material[0]
|
|
181
|
+
: object.material
|
|
182
|
+
const surfaceMaterial =
|
|
183
|
+
PcbScene3dBoardAssemblyPresentation.#cloneMaterial(sourceMaterial)
|
|
184
|
+
const edgeMaterial =
|
|
185
|
+
PcbScene3dBoardAssemblyPresentation.#cloneMaterial(sourceMaterial)
|
|
186
|
+
|
|
187
|
+
if (!surfaceMaterial || !edgeMaterial) {
|
|
188
|
+
return false
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
PcbScene3dBoardAssemblyPresentation.#applyMaterialColor(
|
|
192
|
+
surfaceMaterial,
|
|
193
|
+
surfaceColor
|
|
194
|
+
)
|
|
195
|
+
PcbScene3dBoardAssemblyPresentation.#applyMaterialColor(
|
|
196
|
+
edgeMaterial,
|
|
197
|
+
edgeColor
|
|
198
|
+
)
|
|
199
|
+
object.material = [surfaceMaterial, edgeMaterial]
|
|
200
|
+
PcbScene3dBoardAssemblyPresentation.#clearGeometryGroups(geometry)
|
|
201
|
+
PcbScene3dBoardAssemblyPresentation.#appendBoardFaceGroups(
|
|
202
|
+
geometry,
|
|
203
|
+
positions,
|
|
204
|
+
triangleCount
|
|
205
|
+
)
|
|
206
|
+
return true
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Appends contiguous board face material ranges.
|
|
211
|
+
* @param {any} geometry Buffer geometry.
|
|
212
|
+
* @param {ArrayLike<number>} positions Position buffer.
|
|
213
|
+
* @param {number} triangleCount Geometry triangle count.
|
|
214
|
+
* @returns {void}
|
|
215
|
+
*/
|
|
216
|
+
static #appendBoardFaceGroups(geometry, positions, triangleCount) {
|
|
217
|
+
let runStart = 0
|
|
218
|
+
let runMaterialIndex =
|
|
219
|
+
PcbScene3dBoardAssemblyPresentation.#resolveTriangleMaterialIndex(
|
|
220
|
+
geometry,
|
|
221
|
+
positions,
|
|
222
|
+
0
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
for (let index = 1; index < triangleCount; index += 1) {
|
|
226
|
+
const materialIndex =
|
|
227
|
+
PcbScene3dBoardAssemblyPresentation.#resolveTriangleMaterialIndex(
|
|
228
|
+
geometry,
|
|
229
|
+
positions,
|
|
230
|
+
index
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if (materialIndex === runMaterialIndex) {
|
|
234
|
+
continue
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
PcbScene3dBoardAssemblyPresentation.#appendGeometryGroup(
|
|
238
|
+
geometry,
|
|
239
|
+
runStart,
|
|
240
|
+
index,
|
|
241
|
+
runMaterialIndex
|
|
242
|
+
)
|
|
243
|
+
runStart = index
|
|
244
|
+
runMaterialIndex = materialIndex
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
PcbScene3dBoardAssemblyPresentation.#appendGeometryGroup(
|
|
248
|
+
geometry,
|
|
249
|
+
runStart,
|
|
250
|
+
triangleCount,
|
|
251
|
+
runMaterialIndex
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Resolves a board triangle material index from its normal.
|
|
257
|
+
* @param {any} geometry Buffer geometry.
|
|
258
|
+
* @param {ArrayLike<number>} positions Position buffer.
|
|
259
|
+
* @param {number} triangleIndex Triangle index.
|
|
260
|
+
* @returns {0 | 1}
|
|
261
|
+
*/
|
|
262
|
+
static #resolveTriangleMaterialIndex(geometry, positions, triangleIndex) {
|
|
263
|
+
const points = [0, 1, 2].map((cornerIndex) =>
|
|
264
|
+
PcbScene3dBoardAssemblyPresentation.#readTrianglePoint(
|
|
265
|
+
geometry,
|
|
266
|
+
positions,
|
|
267
|
+
triangleIndex,
|
|
268
|
+
cornerIndex
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
const zRatio =
|
|
272
|
+
PcbScene3dBoardAssemblyPresentation.#resolveNormalZRatio(points)
|
|
273
|
+
|
|
274
|
+
return zRatio >= 0.75 ? 0 : 1
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Reads one triangle corner from indexed or unindexed geometry.
|
|
279
|
+
* @param {any} geometry Buffer geometry.
|
|
280
|
+
* @param {ArrayLike<number>} positions Position buffer.
|
|
281
|
+
* @param {number} triangleIndex Triangle index.
|
|
282
|
+
* @param {number} cornerIndex Corner index.
|
|
283
|
+
* @returns {{ x: number, y: number, z: number }}
|
|
284
|
+
*/
|
|
285
|
+
static #readTrianglePoint(geometry, positions, triangleIndex, cornerIndex) {
|
|
286
|
+
const indices = geometry?.index?.array
|
|
287
|
+
const vertexIndex = indices?.length
|
|
288
|
+
? Number(indices[triangleIndex * 3 + cornerIndex] || 0)
|
|
289
|
+
: triangleIndex * 3 + cornerIndex
|
|
290
|
+
const offset = vertexIndex * 3
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
x: Number(positions[offset] || 0),
|
|
294
|
+
y: Number(positions[offset + 1] || 0),
|
|
295
|
+
z: Number(positions[offset + 2] || 0)
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Resolves how closely one triangle normal points along board Z.
|
|
301
|
+
* @param {{ x: number, y: number, z: number }[]} points Triangle points.
|
|
302
|
+
* @returns {number}
|
|
303
|
+
*/
|
|
304
|
+
static #resolveNormalZRatio(points) {
|
|
305
|
+
const [a, b, c] = points
|
|
306
|
+
const ux = b.x - a.x
|
|
307
|
+
const uy = b.y - a.y
|
|
308
|
+
const uz = b.z - a.z
|
|
309
|
+
const vx = c.x - a.x
|
|
310
|
+
const vy = c.y - a.y
|
|
311
|
+
const vz = c.z - a.z
|
|
312
|
+
const nx = uy * vz - uz * vy
|
|
313
|
+
const ny = uz * vx - ux * vz
|
|
314
|
+
const nz = ux * vy - uy * vx
|
|
315
|
+
const length = Math.hypot(nx, ny, nz)
|
|
316
|
+
|
|
317
|
+
return length > 0 ? Math.abs(nz) / length : 1
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Resolves one geometry triangle count.
|
|
322
|
+
* @param {any} geometry Buffer geometry.
|
|
323
|
+
* @returns {number}
|
|
324
|
+
*/
|
|
325
|
+
static #resolveGeometryTriangleCount(geometry) {
|
|
326
|
+
const indexCount =
|
|
327
|
+
Number(geometry?.index?.count) ||
|
|
328
|
+
Number(geometry?.index?.array?.length) ||
|
|
329
|
+
0
|
|
330
|
+
|
|
331
|
+
if (indexCount > 0) {
|
|
332
|
+
return Math.floor(indexCount / 3)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return Math.floor(
|
|
336
|
+
Number(geometry?.attributes?.position?.count || 0) / 3
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Clears existing material groups from one geometry.
|
|
342
|
+
* @param {any} geometry Buffer geometry.
|
|
343
|
+
* @returns {void}
|
|
344
|
+
*/
|
|
345
|
+
static #clearGeometryGroups(geometry) {
|
|
346
|
+
if (typeof geometry?.clearGroups === 'function') {
|
|
347
|
+
geometry.clearGroups()
|
|
348
|
+
return
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (Array.isArray(geometry?.groups)) {
|
|
352
|
+
geometry.groups.length = 0
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Appends one triangle material group.
|
|
358
|
+
* @param {any} geometry Buffer geometry.
|
|
359
|
+
* @param {number} startTriangle First triangle index.
|
|
360
|
+
* @param {number} endTriangle Exclusive end triangle index.
|
|
361
|
+
* @param {number} materialIndex Material index.
|
|
362
|
+
* @returns {void}
|
|
363
|
+
*/
|
|
364
|
+
static #appendGeometryGroup(
|
|
365
|
+
geometry,
|
|
366
|
+
startTriangle,
|
|
367
|
+
endTriangle,
|
|
368
|
+
materialIndex
|
|
369
|
+
) {
|
|
370
|
+
const count = Math.max(endTriangle - startTriangle, 0) * 3
|
|
371
|
+
if (count <= 0) {
|
|
372
|
+
return
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (typeof geometry?.addGroup === 'function') {
|
|
376
|
+
geometry.addGroup(startTriangle * 3, count, materialIndex)
|
|
377
|
+
} else if (Array.isArray(geometry?.groups)) {
|
|
378
|
+
geometry.groups.push({
|
|
379
|
+
start: startTriangle * 3,
|
|
380
|
+
count,
|
|
381
|
+
materialIndex
|
|
382
|
+
})
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Clones one material without sharing mutable color state.
|
|
388
|
+
* @param {any} material Source material.
|
|
389
|
+
* @returns {any | null}
|
|
390
|
+
*/
|
|
391
|
+
static #cloneMaterial(material) {
|
|
392
|
+
if (!material) {
|
|
393
|
+
return null
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
if (typeof material.clone === 'function') {
|
|
397
|
+
return material.clone()
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return {
|
|
401
|
+
...material,
|
|
402
|
+
color:
|
|
403
|
+
typeof material.color?.clone === 'function'
|
|
404
|
+
? material.color.clone()
|
|
405
|
+
: material.color
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Resolves the app-level substrate color for board assembly meshes.
|
|
411
|
+
* @param {{ surfaceColor?: number } | null | undefined} board Board dimensions.
|
|
412
|
+
* @returns {number}
|
|
413
|
+
*/
|
|
414
|
+
static #resolveSurfaceColor(board) {
|
|
415
|
+
return PcbScene3dBoardMaterialPalette.resolveSurfaceColor(board, {
|
|
416
|
+
hasBoardAssemblyModel: true
|
|
417
|
+
})
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Resolves the app-level substrate edge color for board assembly meshes.
|
|
422
|
+
* @param {{ edgeColor?: number } | null | undefined} board Board dimensions.
|
|
423
|
+
* @returns {number}
|
|
424
|
+
*/
|
|
425
|
+
static #resolveEdgeColor(board) {
|
|
426
|
+
return Number.isInteger(board?.edgeColor) ? board.edgeColor : 0xc9ca78
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Applies display treatment to board substrate material.
|
|
431
|
+
* @param {any | any[]} material Material or material list.
|
|
432
|
+
* @param {number} color Hex color.
|
|
433
|
+
* @returns {void}
|
|
434
|
+
*/
|
|
435
|
+
static #applyMaterialColor(material, color) {
|
|
436
|
+
;(Array.isArray(material) ? material : [material])
|
|
437
|
+
.filter(Boolean)
|
|
438
|
+
.forEach((entry) => {
|
|
439
|
+
if (typeof entry.color?.setHex === 'function') {
|
|
440
|
+
entry.color.setHex(color)
|
|
441
|
+
} else {
|
|
442
|
+
entry.color = color
|
|
443
|
+
}
|
|
444
|
+
entry.needsUpdate = true
|
|
445
|
+
})
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Resolves the imported board-substrate color before display overrides.
|
|
450
|
+
* @param {{ materialKind: string, object: { material?: any | any[] } }[]} meshRecords Mesh records.
|
|
451
|
+
* @returns {number | null}
|
|
452
|
+
*/
|
|
453
|
+
static #resolveImportedSurfaceColor(meshRecords) {
|
|
454
|
+
const substrateRecord = (
|
|
455
|
+
Array.isArray(meshRecords) ? meshRecords : []
|
|
456
|
+
).find((record) => record?.materialKind === 'board')
|
|
457
|
+
|
|
458
|
+
return PcbScene3dBoardAssemblyPresentation.#resolveMaterialColor(
|
|
459
|
+
substrateRecord?.object?.material
|
|
460
|
+
)
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/**
|
|
464
|
+
* Resolves a material color as a numeric hex value.
|
|
465
|
+
* @param {any | any[]} material Material or material list.
|
|
466
|
+
* @returns {number | null}
|
|
467
|
+
*/
|
|
468
|
+
static #resolveMaterialColor(material) {
|
|
469
|
+
for (const entry of Array.isArray(material) ? material : [material]) {
|
|
470
|
+
if (typeof entry?.color?.getHex === 'function') {
|
|
471
|
+
return entry.color.getHex()
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const numericColor = Number(entry?.color)
|
|
475
|
+
if (Number.isInteger(numericColor)) {
|
|
476
|
+
return numericColor
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
return null
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Centers an imported substrate's Z span on the app board plane so ECAD
|
|
485
|
+
* copper and silkscreen overlays sit on the physical board faces.
|
|
486
|
+
* @param {{ position?: { z?: number }, userData?: Record<string, any> }} modelGroup Loaded board assembly group.
|
|
487
|
+
* @param {{ minZ?: number, maxZ?: number } | null} boardBounds Board envelope.
|
|
488
|
+
* @returns {void}
|
|
489
|
+
*/
|
|
490
|
+
static #centerSubstrateOnBoardPlane(modelGroup, boardBounds) {
|
|
491
|
+
const minZ = Number(boardBounds?.minZ)
|
|
492
|
+
const maxZ = Number(boardBounds?.maxZ)
|
|
493
|
+
|
|
494
|
+
if (
|
|
495
|
+
!modelGroup?.position ||
|
|
496
|
+
!Number.isFinite(minZ) ||
|
|
497
|
+
!Number.isFinite(maxZ) ||
|
|
498
|
+
!(maxZ > minZ)
|
|
499
|
+
) {
|
|
500
|
+
return
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const zOffset = -((minZ + maxZ) / 2)
|
|
504
|
+
modelGroup.position.z = Number(modelGroup.position.z || 0) + zOffset
|
|
505
|
+
modelGroup.userData = modelGroup.userData || {}
|
|
506
|
+
modelGroup.userData.scene3dBoardAssemblyZOffsetMil = zOffset
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Resolves one mesh material into a coarse presentation category.
|
|
511
|
+
* @param {any | any[]} material Source material.
|
|
512
|
+
* @returns {string}
|
|
513
|
+
*/
|
|
514
|
+
static #resolveMaterialKind(material) {
|
|
515
|
+
const rgb = PcbScene3dBoardAssemblyPresentation.#resolveMaterialRgb(
|
|
516
|
+
(Array.isArray(material) ? material[0] : material)?.color
|
|
517
|
+
)
|
|
518
|
+
if (!rgb) {
|
|
519
|
+
return ''
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
const min = Math.min(rgb.r, rgb.g, rgb.b)
|
|
523
|
+
const max = Math.max(rgb.r, rgb.g, rgb.b)
|
|
524
|
+
const average = (rgb.r + rgb.g + rgb.b) / 3
|
|
525
|
+
const isNeutral = max - min <= 0.08
|
|
526
|
+
const isBoardGreen = rgb.g > rgb.r * 1.35 && rgb.g > rgb.b * 1.35
|
|
527
|
+
|
|
528
|
+
if (isBoardGreen) {
|
|
529
|
+
return 'board'
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (isNeutral && average >= 0.18 && average <= 0.82) {
|
|
533
|
+
return 'neutral'
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return ''
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Returns true for coarse board-cover surfaces that obscure PCB detail.
|
|
541
|
+
* @param {{ meshBounds: { minX: number, minY: number, maxX: number, maxY: number, sizeX: number, sizeY: number }, triangleCount: number }} record Mesh record.
|
|
542
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number, widthMil: number, heightMil: number, areaMil: number } | null} boardBounds Board envelope.
|
|
543
|
+
* @returns {boolean}
|
|
544
|
+
*/
|
|
545
|
+
static #isLargeBoardCoverMesh(record, boardBounds) {
|
|
546
|
+
if (
|
|
547
|
+
!PcbScene3dBoardAssemblyPresentation.#isMostlyInsideBoard(
|
|
548
|
+
record?.meshBounds,
|
|
549
|
+
boardBounds
|
|
550
|
+
)
|
|
551
|
+
) {
|
|
552
|
+
return false
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const meshArea =
|
|
556
|
+
Number(record.meshBounds.sizeX || 0) *
|
|
557
|
+
Number(record.meshBounds.sizeY || 0)
|
|
558
|
+
const boardArea = Number(boardBounds?.areaMil || 0)
|
|
559
|
+
|
|
560
|
+
return (
|
|
561
|
+
PcbScene3dBoardAssemblyPresentation.#hasLargeBoardCoverArea(
|
|
562
|
+
meshArea,
|
|
563
|
+
boardArea
|
|
564
|
+
) ||
|
|
565
|
+
PcbScene3dBoardAssemblyPresentation.#isCoarseLongBoardCoverMesh(
|
|
566
|
+
record,
|
|
567
|
+
boardBounds
|
|
568
|
+
)
|
|
569
|
+
)
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Returns true when one footprint covers a broad board area.
|
|
574
|
+
* @param {number} meshArea Mesh footprint area.
|
|
575
|
+
* @param {number} boardArea Board footprint area.
|
|
576
|
+
* @returns {boolean}
|
|
577
|
+
*/
|
|
578
|
+
static #hasLargeBoardCoverArea(meshArea, boardArea) {
|
|
579
|
+
return (
|
|
580
|
+
boardArea > 0 &&
|
|
581
|
+
meshArea / boardArea >=
|
|
582
|
+
PcbScene3dBoardAssemblyPresentation.#BOARD_COVER_AREA_RATIO
|
|
583
|
+
)
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Returns true for thin low-detail shield walls that span much of a board.
|
|
588
|
+
* @param {{ meshBounds: { sizeX: number, sizeY: number }, triangleCount: number }} record Mesh record.
|
|
589
|
+
* @param {{ widthMil: number, heightMil: number }} boardBounds Board envelope.
|
|
590
|
+
* @returns {boolean}
|
|
591
|
+
*/
|
|
592
|
+
static #isCoarseLongBoardCoverMesh(record, boardBounds) {
|
|
593
|
+
const triangleCount = Number(record?.triangleCount || 0)
|
|
594
|
+
if (
|
|
595
|
+
triangleCount <= 0 ||
|
|
596
|
+
triangleCount >
|
|
597
|
+
PcbScene3dBoardAssemblyPresentation.#COARSE_COVER_TRIANGLE_LIMIT
|
|
598
|
+
) {
|
|
599
|
+
return false
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const xSpanRatio =
|
|
603
|
+
Number(record?.meshBounds?.sizeX || 0) /
|
|
604
|
+
Math.max(Number(boardBounds?.widthMil || 0), 1)
|
|
605
|
+
const ySpanRatio =
|
|
606
|
+
Number(record?.meshBounds?.sizeY || 0) /
|
|
607
|
+
Math.max(Number(boardBounds?.heightMil || 0), 1)
|
|
608
|
+
|
|
609
|
+
return (
|
|
610
|
+
xSpanRatio >=
|
|
611
|
+
PcbScene3dBoardAssemblyPresentation
|
|
612
|
+
.#BOARD_COVER_LONG_SPAN_RATIO ||
|
|
613
|
+
ySpanRatio >=
|
|
614
|
+
PcbScene3dBoardAssemblyPresentation.#BOARD_COVER_LONG_SPAN_RATIO
|
|
615
|
+
)
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
/**
|
|
619
|
+
* Resolves board dimensions into a bounded area descriptor.
|
|
620
|
+
* @param {{ widthMil?: number, heightMil?: number, thicknessMil?: number } | null | undefined} board Board dimensions.
|
|
621
|
+
* @returns {{ minX: number, minY: number, maxX: number, maxY: number, widthMil: number, heightMil: number, minZ: number, maxZ: number, areaMil: number } | null}
|
|
622
|
+
*/
|
|
623
|
+
static #normalizeBoardBounds(board) {
|
|
624
|
+
const widthMil = Number(board?.widthMil || 0)
|
|
625
|
+
const heightMil = Number(board?.heightMil || 0)
|
|
626
|
+
const thicknessMil = Number(board?.thicknessMil || 0)
|
|
627
|
+
|
|
628
|
+
if (!(widthMil > 0) || !(heightMil > 0)) {
|
|
629
|
+
return null
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return {
|
|
633
|
+
minX: 0,
|
|
634
|
+
minY: 0,
|
|
635
|
+
maxX: widthMil,
|
|
636
|
+
maxY: heightMil,
|
|
637
|
+
widthMil,
|
|
638
|
+
heightMil,
|
|
639
|
+
minZ: thicknessMil > 0 ? -thicknessMil / 2 : 0,
|
|
640
|
+
maxZ: thicknessMil > 0 ? thicknessMil / 2 : 0,
|
|
641
|
+
areaMil: widthMil * heightMil
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Resolves the board envelope from assembly substrate meshes when present.
|
|
647
|
+
* @param {{ meshBounds: { minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number }, materialKind: string }[]} meshRecords Mesh records.
|
|
648
|
+
* @param {{ widthMil?: number, heightMil?: number, thicknessMil?: number } | null | undefined} board Board dimensions.
|
|
649
|
+
* @returns {{ minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, widthMil: number, heightMil: number, areaMil: number } | null}
|
|
650
|
+
*/
|
|
651
|
+
static #resolveBoardEnvelope(meshRecords, board) {
|
|
652
|
+
const substrateBounds =
|
|
653
|
+
PcbScene3dBoardAssemblyPresentation.#unionBounds(
|
|
654
|
+
(Array.isArray(meshRecords) ? meshRecords : [])
|
|
655
|
+
.filter((record) => record.materialKind === 'board')
|
|
656
|
+
.map((record) => record.meshBounds)
|
|
657
|
+
)
|
|
658
|
+
|
|
659
|
+
if (substrateBounds) {
|
|
660
|
+
return substrateBounds
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
return PcbScene3dBoardAssemblyPresentation.#normalizeBoardBounds(board)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Builds one combined bounds descriptor.
|
|
668
|
+
* @param {{ minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number }[]} boundsList Bounds list.
|
|
669
|
+
* @returns {{ minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, widthMil: number, heightMil: number, areaMil: number } | null}
|
|
670
|
+
*/
|
|
671
|
+
static #unionBounds(boundsList) {
|
|
672
|
+
if (!boundsList.length) {
|
|
673
|
+
return null
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
const bounds = boundsList.reduce(
|
|
677
|
+
(nextBounds, meshBounds) => ({
|
|
678
|
+
minX: Math.min(nextBounds.minX, meshBounds.minX),
|
|
679
|
+
minY: Math.min(nextBounds.minY, meshBounds.minY),
|
|
680
|
+
minZ: Math.min(nextBounds.minZ, meshBounds.minZ),
|
|
681
|
+
maxX: Math.max(nextBounds.maxX, meshBounds.maxX),
|
|
682
|
+
maxY: Math.max(nextBounds.maxY, meshBounds.maxY),
|
|
683
|
+
maxZ: Math.max(nextBounds.maxZ, meshBounds.maxZ)
|
|
684
|
+
}),
|
|
685
|
+
{
|
|
686
|
+
minX: Number.POSITIVE_INFINITY,
|
|
687
|
+
minY: Number.POSITIVE_INFINITY,
|
|
688
|
+
minZ: Number.POSITIVE_INFINITY,
|
|
689
|
+
maxX: Number.NEGATIVE_INFINITY,
|
|
690
|
+
maxY: Number.NEGATIVE_INFINITY,
|
|
691
|
+
maxZ: Number.NEGATIVE_INFINITY
|
|
692
|
+
}
|
|
693
|
+
)
|
|
694
|
+
const widthMil = bounds.maxX - bounds.minX
|
|
695
|
+
const heightMil = bounds.maxY - bounds.minY
|
|
696
|
+
|
|
697
|
+
if (!(widthMil > 0) || !(heightMil > 0)) {
|
|
698
|
+
return null
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return {
|
|
702
|
+
...bounds,
|
|
703
|
+
widthMil,
|
|
704
|
+
heightMil,
|
|
705
|
+
areaMil: widthMil * heightMil
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Returns true when a mesh footprint is contained in the board envelope.
|
|
711
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number }} meshBounds Mesh bounds.
|
|
712
|
+
* @param {{ minX: number, minY: number, maxX: number, maxY: number, widthMil: number, heightMil: number } | null} boardBounds Board dimensions.
|
|
713
|
+
* @returns {boolean}
|
|
714
|
+
*/
|
|
715
|
+
static #isMostlyInsideBoard(meshBounds, boardBounds) {
|
|
716
|
+
if (!meshBounds || !boardBounds) {
|
|
717
|
+
return false
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const toleranceX = boardBounds.widthMil * 0.05
|
|
721
|
+
const toleranceY = boardBounds.heightMil * 0.05
|
|
722
|
+
|
|
723
|
+
return (
|
|
724
|
+
meshBounds.minX >= boardBounds.minX - toleranceX &&
|
|
725
|
+
meshBounds.maxX <= boardBounds.maxX + toleranceX &&
|
|
726
|
+
meshBounds.minY >= boardBounds.minY - toleranceY &&
|
|
727
|
+
meshBounds.maxY <= boardBounds.maxY + toleranceY
|
|
728
|
+
)
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Resolves a mesh's source geometry bounds in mil.
|
|
733
|
+
* @param {any} object Render object.
|
|
734
|
+
* @returns {{ minX: number, minY: number, minZ: number, maxX: number, maxY: number, maxZ: number, sizeX: number, sizeY: number, sizeZ: number } | null}
|
|
735
|
+
*/
|
|
736
|
+
static #resolveMeshBoundsMil(object) {
|
|
737
|
+
const positions = object?.geometry?.attributes?.position?.array
|
|
738
|
+
if (!positions?.length) {
|
|
739
|
+
return null
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
let minX = Number.POSITIVE_INFINITY
|
|
743
|
+
let minY = Number.POSITIVE_INFINITY
|
|
744
|
+
let minZ = Number.POSITIVE_INFINITY
|
|
745
|
+
let maxX = Number.NEGATIVE_INFINITY
|
|
746
|
+
let maxY = Number.NEGATIVE_INFINITY
|
|
747
|
+
let maxZ = Number.NEGATIVE_INFINITY
|
|
748
|
+
|
|
749
|
+
for (let index = 0; index < positions.length; index += 3) {
|
|
750
|
+
const x = Number(positions[index] || 0) * 1000
|
|
751
|
+
const y = Number(positions[index + 1] || 0) * 1000
|
|
752
|
+
const z = Number(positions[index + 2] || 0) * 1000
|
|
753
|
+
|
|
754
|
+
minX = Math.min(minX, x)
|
|
755
|
+
minY = Math.min(minY, y)
|
|
756
|
+
minZ = Math.min(minZ, z)
|
|
757
|
+
maxX = Math.max(maxX, x)
|
|
758
|
+
maxY = Math.max(maxY, y)
|
|
759
|
+
maxZ = Math.max(maxZ, z)
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (
|
|
763
|
+
!Number.isFinite(minX) ||
|
|
764
|
+
!Number.isFinite(minY) ||
|
|
765
|
+
!Number.isFinite(minZ) ||
|
|
766
|
+
!Number.isFinite(maxX) ||
|
|
767
|
+
!Number.isFinite(maxY) ||
|
|
768
|
+
!Number.isFinite(maxZ)
|
|
769
|
+
) {
|
|
770
|
+
return null
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
return {
|
|
774
|
+
minX,
|
|
775
|
+
minY,
|
|
776
|
+
minZ,
|
|
777
|
+
maxX,
|
|
778
|
+
maxY,
|
|
779
|
+
maxZ,
|
|
780
|
+
sizeX: maxX - minX,
|
|
781
|
+
sizeY: maxY - minY,
|
|
782
|
+
sizeZ: maxZ - minZ
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Resolves an approximate triangle count for one mesh.
|
|
788
|
+
* @param {any} object Render object.
|
|
789
|
+
* @returns {number}
|
|
790
|
+
*/
|
|
791
|
+
static #resolveTriangleCount(object) {
|
|
792
|
+
const indexCount =
|
|
793
|
+
Number(object?.geometry?.index?.count) ||
|
|
794
|
+
Number(object?.geometry?.index?.array?.length) ||
|
|
795
|
+
0
|
|
796
|
+
|
|
797
|
+
if (indexCount > 0) {
|
|
798
|
+
return Math.floor(indexCount / 3)
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
return Math.floor(
|
|
802
|
+
Number(object?.geometry?.attributes?.position?.count || 0) / 3
|
|
803
|
+
)
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Resolves a material color into normalized RGB components.
|
|
808
|
+
* @param {any} color Material color.
|
|
809
|
+
* @returns {{ r: number, g: number, b: number } | null}
|
|
810
|
+
*/
|
|
811
|
+
static #resolveMaterialRgb(color) {
|
|
812
|
+
if (
|
|
813
|
+
Number.isFinite(Number(color?.r)) &&
|
|
814
|
+
Number.isFinite(Number(color?.g)) &&
|
|
815
|
+
Number.isFinite(Number(color?.b))
|
|
816
|
+
) {
|
|
817
|
+
return {
|
|
818
|
+
r: Number(color.r),
|
|
819
|
+
g: Number(color.g),
|
|
820
|
+
b: Number(color.b)
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
if (Number.isInteger(color)) {
|
|
825
|
+
return {
|
|
826
|
+
r: ((color >> 16) & 255) / 255,
|
|
827
|
+
g: ((color >> 8) & 255) / 255,
|
|
828
|
+
b: (color & 255) / 255
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
return null
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/**
|
|
836
|
+
* Traverses a model object tree, including test doubles without `traverse`.
|
|
837
|
+
* @param {any} root Root object.
|
|
838
|
+
* @param {(object: any) => void} visitor Visitor callback.
|
|
839
|
+
* @returns {void}
|
|
840
|
+
*/
|
|
841
|
+
static #traverseModelObjects(root, visitor) {
|
|
842
|
+
if (!root) {
|
|
843
|
+
return
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
if (typeof root.traverse === 'function') {
|
|
847
|
+
root.traverse(visitor)
|
|
848
|
+
return
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
visitor(root)
|
|
852
|
+
;(Array.isArray(root.children) ? root.children : []).forEach((child) =>
|
|
853
|
+
PcbScene3dBoardAssemblyPresentation.#traverseModelObjects(
|
|
854
|
+
child,
|
|
855
|
+
visitor
|
|
856
|
+
)
|
|
857
|
+
)
|
|
858
|
+
}
|
|
859
|
+
}
|