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,762 @@
|
|
|
1
|
+
import { PcbAssemblyMeshUtils } from './PcbAssemblyMeshUtils.mjs'
|
|
2
|
+
|
|
3
|
+
const COMPONENT_COLOR = [0.55, 0.56, 0.58]
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds component body and external model meshes for assembly exports.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbAssemblyComponentMeshBuilder {
|
|
9
|
+
/**
|
|
10
|
+
* Builds component meshes and component-model diagnostics.
|
|
11
|
+
* @param {{ components?: object[], externalPlacements?: object[] }} sceneDescription Prepared scene description.
|
|
12
|
+
* @param {{ modelMeshLoader?: (placement: object) => Promise<object | object[]>, includeModels?: boolean, renderFallbackBodies?: boolean }} [options] Build options.
|
|
13
|
+
* @param {{ advance?: (units: number, message: string) => Promise<void> } | null} [progress] Progress tracker.
|
|
14
|
+
* @returns {Promise<{ meshes: object[], diagnostics: object[] }>}
|
|
15
|
+
*/
|
|
16
|
+
static async build(sceneDescription, options = {}, progress = null) {
|
|
17
|
+
const diagnostics = []
|
|
18
|
+
const meshes =
|
|
19
|
+
PcbAssemblyComponentMeshBuilder.#buildFallbackComponentMeshes(
|
|
20
|
+
sceneDescription,
|
|
21
|
+
options
|
|
22
|
+
)
|
|
23
|
+
const loader =
|
|
24
|
+
typeof options.modelMeshLoader === 'function'
|
|
25
|
+
? options.modelMeshLoader
|
|
26
|
+
: null
|
|
27
|
+
|
|
28
|
+
if (loader && options.includeModels !== false) {
|
|
29
|
+
meshes.push(
|
|
30
|
+
...(await PcbAssemblyComponentMeshBuilder.#buildModelMeshes(
|
|
31
|
+
sceneDescription,
|
|
32
|
+
loader,
|
|
33
|
+
diagnostics,
|
|
34
|
+
progress
|
|
35
|
+
))
|
|
36
|
+
)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (options.includeModels !== false) {
|
|
40
|
+
PcbAssemblyComponentMeshBuilder.#appendMissingModelDiagnostics(
|
|
41
|
+
sceneDescription,
|
|
42
|
+
diagnostics
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return { meshes, diagnostics }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Builds external component model meshes.
|
|
51
|
+
* @param {{ externalPlacements?: object[] }} sceneDescription Prepared scene description.
|
|
52
|
+
* @param {(placement: object) => Promise<object | object[]>} loader Model mesh loader.
|
|
53
|
+
* @param {object[]} diagnostics Mutable diagnostics list.
|
|
54
|
+
* @param {{ advance?: (units: number, message: string) => Promise<void> } | null} progress Progress tracker.
|
|
55
|
+
* @returns {Promise<object[]>}
|
|
56
|
+
*/
|
|
57
|
+
static async #buildModelMeshes(
|
|
58
|
+
sceneDescription,
|
|
59
|
+
loader,
|
|
60
|
+
diagnostics,
|
|
61
|
+
progress
|
|
62
|
+
) {
|
|
63
|
+
const meshes = []
|
|
64
|
+
const placements = PcbAssemblyComponentMeshBuilder.#array(
|
|
65
|
+
sceneDescription?.externalPlacements
|
|
66
|
+
).filter((placement) => placement?.externalModel)
|
|
67
|
+
|
|
68
|
+
for (
|
|
69
|
+
let placementIndex = 0;
|
|
70
|
+
placementIndex < placements.length;
|
|
71
|
+
placementIndex += 1
|
|
72
|
+
) {
|
|
73
|
+
const placement = placements[placementIndex]
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const loaded = await loader(placement)
|
|
77
|
+
const loadedMeshes = Array.isArray(loaded) ? loaded : [loaded]
|
|
78
|
+
loadedMeshes.filter(Boolean).forEach((mesh, index) => {
|
|
79
|
+
meshes.push(
|
|
80
|
+
PcbAssemblyComponentMeshBuilder.#componentModelMesh(
|
|
81
|
+
mesh,
|
|
82
|
+
placement,
|
|
83
|
+
loadedMeshes.length,
|
|
84
|
+
index
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
})
|
|
88
|
+
PcbAssemblyComponentMeshBuilder.#appendModelDiagnostic(
|
|
89
|
+
placement,
|
|
90
|
+
diagnostics
|
|
91
|
+
)
|
|
92
|
+
} catch (error) {
|
|
93
|
+
diagnostics.push(
|
|
94
|
+
PcbAssemblyComponentMeshBuilder.#diagnostic(
|
|
95
|
+
'warning',
|
|
96
|
+
'component_model_conversion_failed',
|
|
97
|
+
'Could not convert 3D model for ' +
|
|
98
|
+
String(placement?.designator || 'component') +
|
|
99
|
+
': ' +
|
|
100
|
+
String(error?.message || error)
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
await progress?.advance?.(
|
|
105
|
+
1,
|
|
106
|
+
'Loading component models ' +
|
|
107
|
+
(placementIndex + 1) +
|
|
108
|
+
'/' +
|
|
109
|
+
placements.length
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return meshes
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Prepares and places one loaded component model mesh.
|
|
118
|
+
* @param {object} mesh Loaded mesh.
|
|
119
|
+
* @param {object} placement Component placement.
|
|
120
|
+
* @param {number} meshCount Number of loaded meshes for this placement.
|
|
121
|
+
* @param {number} meshIndex Loaded mesh index.
|
|
122
|
+
* @returns {object}
|
|
123
|
+
*/
|
|
124
|
+
static #componentModelMesh(mesh, placement, meshCount, meshIndex) {
|
|
125
|
+
const designator = PcbAssemblyMeshUtils.safeName(
|
|
126
|
+
placement?.designator || 'component'
|
|
127
|
+
)
|
|
128
|
+
const prepared =
|
|
129
|
+
PcbAssemblyComponentMeshBuilder.#prepareLoadedComponentMesh(
|
|
130
|
+
mesh,
|
|
131
|
+
placement
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
return PcbAssemblyMeshUtils.transformMesh(
|
|
135
|
+
{
|
|
136
|
+
...prepared,
|
|
137
|
+
name:
|
|
138
|
+
'component-' +
|
|
139
|
+
designator +
|
|
140
|
+
(meshCount > 1 ? '-' + (meshIndex + 1) : ''),
|
|
141
|
+
color: prepared.color || COMPONENT_COLOR,
|
|
142
|
+
...PcbAssemblyComponentMeshBuilder.#placementOpacity(
|
|
143
|
+
placement,
|
|
144
|
+
prepared
|
|
145
|
+
)
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
...placement,
|
|
149
|
+
modelTransform: null
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Builds procedural component bodies for components without loaded models.
|
|
156
|
+
* @param {{ components?: object[], externalPlacements?: object[] }} sceneDescription Prepared scene description.
|
|
157
|
+
* @param {{ includeModels?: boolean, renderFallbackBodies?: boolean }} options Build options.
|
|
158
|
+
* @returns {object[]}
|
|
159
|
+
*/
|
|
160
|
+
static #buildFallbackComponentMeshes(sceneDescription, options) {
|
|
161
|
+
if (options.renderFallbackBodies === false) {
|
|
162
|
+
return []
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const externalDesignators = new Set(
|
|
166
|
+
PcbAssemblyComponentMeshBuilder.#array(
|
|
167
|
+
sceneDescription?.externalPlacements
|
|
168
|
+
)
|
|
169
|
+
.filter((placement) => placement?.externalModel)
|
|
170
|
+
.map((placement) => String(placement?.designator || '').trim())
|
|
171
|
+
.filter(Boolean)
|
|
172
|
+
)
|
|
173
|
+
const includeModels = options.includeModels !== false
|
|
174
|
+
|
|
175
|
+
return PcbAssemblyComponentMeshBuilder.#array(
|
|
176
|
+
sceneDescription?.components
|
|
177
|
+
)
|
|
178
|
+
.filter((component) =>
|
|
179
|
+
PcbAssemblyComponentMeshBuilder.#shouldBuildFallbackBody(
|
|
180
|
+
component,
|
|
181
|
+
externalDesignators,
|
|
182
|
+
includeModels
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
.map((component, index) =>
|
|
186
|
+
PcbAssemblyMeshUtils.transformMesh(
|
|
187
|
+
{
|
|
188
|
+
...PcbAssemblyMeshUtils.box(
|
|
189
|
+
'component-' +
|
|
190
|
+
PcbAssemblyMeshUtils.safeName(
|
|
191
|
+
component?.designator ||
|
|
192
|
+
'fallback-' + (index + 1)
|
|
193
|
+
) +
|
|
194
|
+
'-body',
|
|
195
|
+
{
|
|
196
|
+
width:
|
|
197
|
+
Number(component?.body?.sizeMil?.width) ||
|
|
198
|
+
20,
|
|
199
|
+
depth:
|
|
200
|
+
Number(component?.body?.sizeMil?.depth) ||
|
|
201
|
+
20,
|
|
202
|
+
height:
|
|
203
|
+
Number(component?.body?.sizeMil?.height) ||
|
|
204
|
+
10,
|
|
205
|
+
color: COMPONENT_COLOR
|
|
206
|
+
}
|
|
207
|
+
),
|
|
208
|
+
...PcbAssemblyComponentMeshBuilder.#placementOpacity(
|
|
209
|
+
component,
|
|
210
|
+
{}
|
|
211
|
+
)
|
|
212
|
+
},
|
|
213
|
+
component
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Returns whether a procedural body should be emitted for one component.
|
|
220
|
+
* @param {object} component Component scene entry.
|
|
221
|
+
* @param {Set<string>} externalDesignators Designators with external placements.
|
|
222
|
+
* @param {boolean} includeModels Whether model export is enabled.
|
|
223
|
+
* @returns {boolean}
|
|
224
|
+
*/
|
|
225
|
+
static #shouldBuildFallbackBody(
|
|
226
|
+
component,
|
|
227
|
+
externalDesignators,
|
|
228
|
+
includeModels
|
|
229
|
+
) {
|
|
230
|
+
if (!component || component.renderFallbackBody === false) {
|
|
231
|
+
return false
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const designator = String(component?.designator || '').trim()
|
|
235
|
+
if (
|
|
236
|
+
includeModels &&
|
|
237
|
+
(component?.externalModel ||
|
|
238
|
+
(designator && externalDesignators.has(designator)))
|
|
239
|
+
) {
|
|
240
|
+
return false
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return Boolean(component?.body?.sizeMil)
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Applies model-local transforms that need whole-mesh context.
|
|
248
|
+
* @param {object} mesh Loaded component mesh.
|
|
249
|
+
* @param {object} placement Component placement.
|
|
250
|
+
* @returns {object}
|
|
251
|
+
*/
|
|
252
|
+
static #prepareLoadedComponentMesh(mesh, placement) {
|
|
253
|
+
const modelTransform = placement?.modelTransform || {}
|
|
254
|
+
let prepared = PcbAssemblyComponentMeshBuilder.#cloneMesh(mesh)
|
|
255
|
+
|
|
256
|
+
prepared = PcbAssemblyComponentMeshBuilder.#scaleMesh(
|
|
257
|
+
prepared,
|
|
258
|
+
PcbAssemblyComponentMeshBuilder.#modelScale(modelTransform)
|
|
259
|
+
)
|
|
260
|
+
prepared = PcbAssemblyComponentMeshBuilder.#rotateMesh(
|
|
261
|
+
prepared,
|
|
262
|
+
modelTransform?.rotationDeg || {}
|
|
263
|
+
)
|
|
264
|
+
prepared = PcbAssemblyComponentMeshBuilder.#applyBoardNormalDirection(
|
|
265
|
+
prepared,
|
|
266
|
+
modelTransform?.boardNormalDirection
|
|
267
|
+
)
|
|
268
|
+
prepared = PcbAssemblyComponentMeshBuilder.#applyModelOrigin(
|
|
269
|
+
prepared,
|
|
270
|
+
modelTransform
|
|
271
|
+
)
|
|
272
|
+
prepared = PcbAssemblyComponentMeshBuilder.#fitMeshToTarget(
|
|
273
|
+
prepared,
|
|
274
|
+
modelTransform
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
return PcbAssemblyComponentMeshBuilder.#translateMesh3d(
|
|
278
|
+
prepared,
|
|
279
|
+
PcbAssemblyComponentMeshBuilder.#modelOffset(modelTransform)
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Clones mesh vertices so model-local transforms do not mutate loader output.
|
|
285
|
+
* @param {object} mesh Source mesh.
|
|
286
|
+
* @returns {object}
|
|
287
|
+
*/
|
|
288
|
+
static #cloneMesh(mesh) {
|
|
289
|
+
return {
|
|
290
|
+
...mesh,
|
|
291
|
+
vertices: PcbAssemblyComponentMeshBuilder.#array(
|
|
292
|
+
mesh?.vertices
|
|
293
|
+
).map((vertex) => [
|
|
294
|
+
Number(vertex?.[0] || 0),
|
|
295
|
+
Number(vertex?.[1] || 0),
|
|
296
|
+
Number(vertex?.[2] || 0)
|
|
297
|
+
])
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Applies model origin metadata.
|
|
303
|
+
* @param {object} mesh Mesh to transform.
|
|
304
|
+
* @param {object} modelTransform Model transform metadata.
|
|
305
|
+
* @returns {object}
|
|
306
|
+
*/
|
|
307
|
+
static #applyModelOrigin(mesh, modelTransform) {
|
|
308
|
+
const origin =
|
|
309
|
+
PcbAssemblyComponentMeshBuilder.#explicitModelOrigin(
|
|
310
|
+
modelTransform
|
|
311
|
+
) ||
|
|
312
|
+
PcbAssemblyComponentMeshBuilder.#alignedModelOrigin(
|
|
313
|
+
mesh,
|
|
314
|
+
modelTransform?.originAlignment
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
return origin
|
|
318
|
+
? PcbAssemblyComponentMeshBuilder.#translateMesh3d(mesh, {
|
|
319
|
+
x: -origin.x,
|
|
320
|
+
y: -origin.y,
|
|
321
|
+
z: -origin.z
|
|
322
|
+
})
|
|
323
|
+
: mesh
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Resolves an explicit model origin.
|
|
328
|
+
* @param {object} modelTransform Model transform metadata.
|
|
329
|
+
* @returns {{ x: number, y: number, z: number } | null}
|
|
330
|
+
*/
|
|
331
|
+
static #explicitModelOrigin(modelTransform) {
|
|
332
|
+
const origin = modelTransform?.originPositionMil
|
|
333
|
+
if (!origin) {
|
|
334
|
+
return null
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
x: Number(origin.x || 0),
|
|
339
|
+
y: Number(origin.y || 0),
|
|
340
|
+
z: Number(origin.z || 0)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Resolves an inferred model origin from alignment metadata.
|
|
346
|
+
* @param {object} mesh Mesh data.
|
|
347
|
+
* @param {string | undefined} alignment Origin alignment.
|
|
348
|
+
* @returns {{ x: number, y: number, z: number } | null}
|
|
349
|
+
*/
|
|
350
|
+
static #alignedModelOrigin(mesh, alignment) {
|
|
351
|
+
const value = String(alignment || '').toLowerCase()
|
|
352
|
+
if (!value) {
|
|
353
|
+
return null
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const bounds = PcbAssemblyComponentMeshBuilder.#meshBounds3d(mesh)
|
|
357
|
+
if (!Number.isFinite(bounds.minX + bounds.maxX)) {
|
|
358
|
+
return null
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const center = {
|
|
362
|
+
x: (bounds.minX + bounds.maxX) / 2,
|
|
363
|
+
y: (bounds.minY + bounds.maxY) / 2,
|
|
364
|
+
z: (bounds.minZ + bounds.maxZ) / 2
|
|
365
|
+
}
|
|
366
|
+
if (value === 'center') {
|
|
367
|
+
return center
|
|
368
|
+
}
|
|
369
|
+
if (value === 'center_of_component_on_board_surface') {
|
|
370
|
+
return {
|
|
371
|
+
x: center.x,
|
|
372
|
+
y: center.y,
|
|
373
|
+
z: bounds.minZ
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return null
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Fits a mesh to an optional target size.
|
|
381
|
+
* @param {object} mesh Mesh data.
|
|
382
|
+
* @param {object} modelTransform Model transform metadata.
|
|
383
|
+
* @returns {object}
|
|
384
|
+
*/
|
|
385
|
+
static #fitMeshToTarget(mesh, modelTransform) {
|
|
386
|
+
const target = modelTransform?.targetSizeMil
|
|
387
|
+
if (!target) {
|
|
388
|
+
return mesh
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const bounds = PcbAssemblyComponentMeshBuilder.#meshBounds3d(mesh)
|
|
392
|
+
const size = {
|
|
393
|
+
x: bounds.maxX - bounds.minX,
|
|
394
|
+
y: bounds.maxY - bounds.minY,
|
|
395
|
+
z: bounds.maxZ - bounds.minZ
|
|
396
|
+
}
|
|
397
|
+
const scale = {
|
|
398
|
+
x: size.x > 0 ? Number(target.x || 0) / size.x : 1,
|
|
399
|
+
y: size.y > 0 ? Number(target.y || 0) / size.y : 1,
|
|
400
|
+
z: size.z > 0 ? Number(target.z || 0) / size.z : 1
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (
|
|
404
|
+
!Number.isFinite(scale.x) ||
|
|
405
|
+
!Number.isFinite(scale.y) ||
|
|
406
|
+
!Number.isFinite(scale.z) ||
|
|
407
|
+
scale.x <= 0 ||
|
|
408
|
+
scale.y <= 0 ||
|
|
409
|
+
scale.z <= 0
|
|
410
|
+
) {
|
|
411
|
+
return mesh
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
if (String(modelTransform?.objectFit || '') === 'fill_bounds') {
|
|
415
|
+
return PcbAssemblyComponentMeshBuilder.#scaleMesh(mesh, scale)
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const uniformScale = Math.min(scale.x, scale.y, scale.z)
|
|
419
|
+
return PcbAssemblyComponentMeshBuilder.#scaleMesh(mesh, {
|
|
420
|
+
x: uniformScale,
|
|
421
|
+
y: uniformScale,
|
|
422
|
+
z: uniformScale
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Applies a model board-normal direction.
|
|
428
|
+
* @param {object} mesh Mesh data.
|
|
429
|
+
* @param {string | undefined} direction Board normal direction.
|
|
430
|
+
* @returns {object}
|
|
431
|
+
*/
|
|
432
|
+
static #applyBoardNormalDirection(mesh, direction) {
|
|
433
|
+
const rotation =
|
|
434
|
+
PcbAssemblyComponentMeshBuilder.#boardNormalRotation(direction)
|
|
435
|
+
return rotation
|
|
436
|
+
? PcbAssemblyComponentMeshBuilder.#rotateMesh(mesh, rotation, false)
|
|
437
|
+
: mesh
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Resolves a rotation that maps the model's board-normal axis to export Z.
|
|
442
|
+
* @param {string | undefined} direction Board normal direction.
|
|
443
|
+
* @returns {{ x?: number, y?: number, z?: number } | null}
|
|
444
|
+
*/
|
|
445
|
+
static #boardNormalRotation(direction) {
|
|
446
|
+
switch (String(direction || '').toLowerCase()) {
|
|
447
|
+
case 'x+':
|
|
448
|
+
return { y: -90 }
|
|
449
|
+
case 'x-':
|
|
450
|
+
return { y: 90 }
|
|
451
|
+
case 'y+':
|
|
452
|
+
return { x: 90 }
|
|
453
|
+
case 'y-':
|
|
454
|
+
return { x: -90 }
|
|
455
|
+
case 'z-':
|
|
456
|
+
return { x: 180 }
|
|
457
|
+
default:
|
|
458
|
+
return null
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Applies per-axis mesh scale.
|
|
464
|
+
* @param {object} mesh Mesh data.
|
|
465
|
+
* @param {{ x?: number, y?: number, z?: number }} scale Scale factors.
|
|
466
|
+
* @returns {object}
|
|
467
|
+
*/
|
|
468
|
+
static #scaleMesh(mesh, scale) {
|
|
469
|
+
return {
|
|
470
|
+
...mesh,
|
|
471
|
+
vertices: PcbAssemblyComponentMeshBuilder.#array(
|
|
472
|
+
mesh?.vertices
|
|
473
|
+
).map((vertex) => [
|
|
474
|
+
Number(vertex?.[0] || 0) * (Number(scale?.x) || 1),
|
|
475
|
+
Number(vertex?.[1] || 0) * (Number(scale?.y) || 1),
|
|
476
|
+
Number(vertex?.[2] || 0) * (Number(scale?.z) || 1)
|
|
477
|
+
])
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Applies model-local rotation.
|
|
483
|
+
* @param {object} mesh Mesh data.
|
|
484
|
+
* @param {{ x?: number, y?: number, z?: number }} rotationDeg Rotation angles.
|
|
485
|
+
* @param {boolean} [invert] Whether to use legacy inverted rotation signs.
|
|
486
|
+
* @returns {object}
|
|
487
|
+
*/
|
|
488
|
+
static #rotateMesh(mesh, rotationDeg, invert = true) {
|
|
489
|
+
const sign = invert ? -1 : 1
|
|
490
|
+
return {
|
|
491
|
+
...mesh,
|
|
492
|
+
vertices: PcbAssemblyComponentMeshBuilder.#array(
|
|
493
|
+
mesh?.vertices
|
|
494
|
+
).map((vertex) =>
|
|
495
|
+
PcbAssemblyComponentMeshBuilder.#rotatePoint(
|
|
496
|
+
{
|
|
497
|
+
x: Number(vertex?.[0] || 0),
|
|
498
|
+
y: Number(vertex?.[1] || 0),
|
|
499
|
+
z: Number(vertex?.[2] || 0)
|
|
500
|
+
},
|
|
501
|
+
{
|
|
502
|
+
x: sign * Number(rotationDeg?.x || 0),
|
|
503
|
+
y: sign * Number(rotationDeg?.y || 0),
|
|
504
|
+
z: sign * Number(rotationDeg?.z || 0)
|
|
505
|
+
}
|
|
506
|
+
)
|
|
507
|
+
)
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Rotates one point around X, Y, then Z.
|
|
513
|
+
* @param {{ x: number, y: number, z: number }} point Source point.
|
|
514
|
+
* @param {{ x?: number, y?: number, z?: number }} rotationDeg Rotation angles.
|
|
515
|
+
* @returns {number[]}
|
|
516
|
+
*/
|
|
517
|
+
static #rotatePoint(point, rotationDeg) {
|
|
518
|
+
const afterX = PcbAssemblyComponentMeshBuilder.#rotatePointX(
|
|
519
|
+
point,
|
|
520
|
+
Number(rotationDeg.x || 0)
|
|
521
|
+
)
|
|
522
|
+
const afterY = PcbAssemblyComponentMeshBuilder.#rotatePointY(
|
|
523
|
+
afterX,
|
|
524
|
+
Number(rotationDeg.y || 0)
|
|
525
|
+
)
|
|
526
|
+
const afterZ = PcbAssemblyComponentMeshBuilder.#rotatePointZ(
|
|
527
|
+
afterY,
|
|
528
|
+
Number(rotationDeg.z || 0)
|
|
529
|
+
)
|
|
530
|
+
return [afterZ.x, afterZ.y, afterZ.z]
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Rotates a point around X.
|
|
535
|
+
* @param {{ x: number, y: number, z: number }} point Source point.
|
|
536
|
+
* @param {number} angleDeg Angle in degrees.
|
|
537
|
+
* @returns {{ x: number, y: number, z: number }}
|
|
538
|
+
*/
|
|
539
|
+
static #rotatePointX(point, angleDeg) {
|
|
540
|
+
const angle = (angleDeg * Math.PI) / 180
|
|
541
|
+
const cos = Math.cos(angle)
|
|
542
|
+
const sin = Math.sin(angle)
|
|
543
|
+
return {
|
|
544
|
+
x: point.x,
|
|
545
|
+
y: point.y * cos - point.z * sin,
|
|
546
|
+
z: point.y * sin + point.z * cos
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
/**
|
|
551
|
+
* Rotates a point around Y.
|
|
552
|
+
* @param {{ x: number, y: number, z: number }} point Source point.
|
|
553
|
+
* @param {number} angleDeg Angle in degrees.
|
|
554
|
+
* @returns {{ x: number, y: number, z: number }}
|
|
555
|
+
*/
|
|
556
|
+
static #rotatePointY(point, angleDeg) {
|
|
557
|
+
const angle = (angleDeg * Math.PI) / 180
|
|
558
|
+
const cos = Math.cos(angle)
|
|
559
|
+
const sin = Math.sin(angle)
|
|
560
|
+
return {
|
|
561
|
+
x: point.x * cos + point.z * sin,
|
|
562
|
+
y: point.y,
|
|
563
|
+
z: -point.x * sin + point.z * cos
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Rotates a point around Z.
|
|
569
|
+
* @param {{ x: number, y: number, z: number }} point Source point.
|
|
570
|
+
* @param {number} angleDeg Angle in degrees.
|
|
571
|
+
* @returns {{ x: number, y: number, z: number }}
|
|
572
|
+
*/
|
|
573
|
+
static #rotatePointZ(point, angleDeg) {
|
|
574
|
+
const angle = (angleDeg * Math.PI) / 180
|
|
575
|
+
const cos = Math.cos(angle)
|
|
576
|
+
const sin = Math.sin(angle)
|
|
577
|
+
return {
|
|
578
|
+
x: point.x * cos - point.y * sin,
|
|
579
|
+
y: point.x * sin + point.y * cos,
|
|
580
|
+
z: point.z
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Translates mesh vertices in 3D.
|
|
586
|
+
* @param {object} mesh Mesh data.
|
|
587
|
+
* @param {{ x?: number, y?: number, z?: number }} offset Translation.
|
|
588
|
+
* @returns {object}
|
|
589
|
+
*/
|
|
590
|
+
static #translateMesh3d(mesh, offset) {
|
|
591
|
+
return {
|
|
592
|
+
...mesh,
|
|
593
|
+
vertices: PcbAssemblyComponentMeshBuilder.#array(
|
|
594
|
+
mesh?.vertices
|
|
595
|
+
).map((vertex) => [
|
|
596
|
+
Number(vertex?.[0] || 0) + Number(offset?.x || 0),
|
|
597
|
+
Number(vertex?.[1] || 0) + Number(offset?.y || 0),
|
|
598
|
+
Number(vertex?.[2] || 0) + Number(offset?.z || 0)
|
|
599
|
+
])
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Computes 3D mesh bounds.
|
|
605
|
+
* @param {object} mesh Mesh data.
|
|
606
|
+
* @returns {{ minX: number, maxX: number, minY: number, maxY: number, minZ: number, maxZ: number }}
|
|
607
|
+
*/
|
|
608
|
+
static #meshBounds3d(mesh) {
|
|
609
|
+
return PcbAssemblyComponentMeshBuilder.#array(mesh?.vertices).reduce(
|
|
610
|
+
(bounds, vertex) => ({
|
|
611
|
+
minX: Math.min(bounds.minX, Number(vertex?.[0] || 0)),
|
|
612
|
+
maxX: Math.max(bounds.maxX, Number(vertex?.[0] || 0)),
|
|
613
|
+
minY: Math.min(bounds.minY, Number(vertex?.[1] || 0)),
|
|
614
|
+
maxY: Math.max(bounds.maxY, Number(vertex?.[1] || 0)),
|
|
615
|
+
minZ: Math.min(bounds.minZ, Number(vertex?.[2] || 0)),
|
|
616
|
+
maxZ: Math.max(bounds.maxZ, Number(vertex?.[2] || 0))
|
|
617
|
+
}),
|
|
618
|
+
{
|
|
619
|
+
minX: Infinity,
|
|
620
|
+
maxX: -Infinity,
|
|
621
|
+
minY: Infinity,
|
|
622
|
+
maxY: -Infinity,
|
|
623
|
+
minZ: Infinity,
|
|
624
|
+
maxZ: -Infinity
|
|
625
|
+
}
|
|
626
|
+
)
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Resolves model-local scale.
|
|
631
|
+
* @param {object} modelTransform Model transform metadata.
|
|
632
|
+
* @returns {{ x: number, y: number, z: number }}
|
|
633
|
+
*/
|
|
634
|
+
static #modelScale(modelTransform) {
|
|
635
|
+
const scale = modelTransform?.scale || {}
|
|
636
|
+
return {
|
|
637
|
+
x: Number(scale.x ?? 1) || 1,
|
|
638
|
+
y: Number(scale.y ?? 1) || 1,
|
|
639
|
+
z: Number(scale.z ?? 1) || 1
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/**
|
|
644
|
+
* Resolves model-local offset.
|
|
645
|
+
* @param {object} modelTransform Model transform metadata.
|
|
646
|
+
* @returns {{ x: number, y: number, z: number }}
|
|
647
|
+
*/
|
|
648
|
+
static #modelOffset(modelTransform) {
|
|
649
|
+
const offset = modelTransform?.offsetMil || {}
|
|
650
|
+
return {
|
|
651
|
+
x: Number(offset.x ?? modelTransform?.dxMil ?? 0),
|
|
652
|
+
y: Number(offset.y ?? modelTransform?.dyMil ?? 0),
|
|
653
|
+
z: Number(offset.z ?? modelTransform?.dzMil ?? 0)
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Resolves opacity metadata to preserve in exported materials.
|
|
659
|
+
* @param {object} placement Component placement or fallback component.
|
|
660
|
+
* @param {object} mesh Mesh metadata.
|
|
661
|
+
* @returns {{ opacity?: number }}
|
|
662
|
+
*/
|
|
663
|
+
static #placementOpacity(placement, mesh) {
|
|
664
|
+
const opacity = Number(mesh?.opacity ?? placement?.bodyOpacity)
|
|
665
|
+
return Number.isFinite(opacity) && opacity > 0 && opacity < 1
|
|
666
|
+
? { opacity }
|
|
667
|
+
: {}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Adds a format-specific component model diagnostic.
|
|
672
|
+
* @param {object} placement Component placement.
|
|
673
|
+
* @param {object[]} diagnostics Mutable diagnostics list.
|
|
674
|
+
* @returns {void}
|
|
675
|
+
*/
|
|
676
|
+
static #appendModelDiagnostic(placement, diagnostics) {
|
|
677
|
+
const format = String(
|
|
678
|
+
placement?.externalModel?.format || ''
|
|
679
|
+
).toLowerCase()
|
|
680
|
+
if (format === 'wrl' || format === 'vrml') {
|
|
681
|
+
diagnostics.push(
|
|
682
|
+
PcbAssemblyComponentMeshBuilder.#diagnostic(
|
|
683
|
+
'info',
|
|
684
|
+
'component_wrl_faceted_step',
|
|
685
|
+
'Converted WRL model for ' +
|
|
686
|
+
String(placement?.designator || 'component') +
|
|
687
|
+
' as faceted geometry.'
|
|
688
|
+
)
|
|
689
|
+
)
|
|
690
|
+
return
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
if (format === 'step' || format === 'stp') {
|
|
694
|
+
diagnostics.push(
|
|
695
|
+
PcbAssemblyComponentMeshBuilder.#diagnostic(
|
|
696
|
+
'info',
|
|
697
|
+
'component_step_faceted_export',
|
|
698
|
+
'Included STEP model for ' +
|
|
699
|
+
String(placement?.designator || 'component') +
|
|
700
|
+
' through export mesh geometry.'
|
|
701
|
+
)
|
|
702
|
+
)
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/**
|
|
707
|
+
* Adds diagnostics for components without resolved external models.
|
|
708
|
+
* @param {{ components?: object[], externalPlacements?: object[] }} sceneDescription Scene description.
|
|
709
|
+
* @param {object[]} diagnostics Mutable diagnostics list.
|
|
710
|
+
* @returns {void}
|
|
711
|
+
*/
|
|
712
|
+
static #appendMissingModelDiagnostics(sceneDescription, diagnostics) {
|
|
713
|
+
const placementDesignators = new Set(
|
|
714
|
+
PcbAssemblyComponentMeshBuilder.#array(
|
|
715
|
+
sceneDescription?.externalPlacements
|
|
716
|
+
)
|
|
717
|
+
.map((placement) => String(placement?.designator || '').trim())
|
|
718
|
+
.filter(Boolean)
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
PcbAssemblyComponentMeshBuilder.#array(
|
|
722
|
+
sceneDescription?.components
|
|
723
|
+
).forEach((component) => {
|
|
724
|
+
const designator = String(component?.designator || '').trim()
|
|
725
|
+
if (
|
|
726
|
+
!designator ||
|
|
727
|
+
component?.externalModel ||
|
|
728
|
+
placementDesignators.has(designator)
|
|
729
|
+
) {
|
|
730
|
+
return
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
diagnostics.push(
|
|
734
|
+
PcbAssemblyComponentMeshBuilder.#diagnostic(
|
|
735
|
+
'warning',
|
|
736
|
+
'component_model_missing',
|
|
737
|
+
'No resolved 3D model was available for ' + designator + '.'
|
|
738
|
+
)
|
|
739
|
+
)
|
|
740
|
+
})
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Creates a normalized diagnostic object.
|
|
745
|
+
* @param {string} severity Diagnostic severity.
|
|
746
|
+
* @param {string} code Diagnostic code.
|
|
747
|
+
* @param {string} message User-facing message.
|
|
748
|
+
* @returns {object}
|
|
749
|
+
*/
|
|
750
|
+
static #diagnostic(severity, code, message) {
|
|
751
|
+
return { severity, code, message }
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Normalizes a value to an array.
|
|
756
|
+
* @param {unknown} value Candidate value.
|
|
757
|
+
* @returns {any[]}
|
|
758
|
+
*/
|
|
759
|
+
static #array(value) {
|
|
760
|
+
return Array.isArray(value) ? value : []
|
|
761
|
+
}
|
|
762
|
+
}
|