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
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { zipSync } from 'fflate'
|
|
2
|
+
import { PcbAssemblyMeshUtils } from './PcbAssemblyMeshUtils.mjs'
|
|
3
|
+
import { PcbAssemblyModelMeshLoader } from './PcbAssemblyModelMeshLoader.mjs'
|
|
4
|
+
import { PcbAssemblyStepWriter } from './PcbAssemblyStepWriter.mjs'
|
|
5
|
+
|
|
6
|
+
const STATIC_BODY_COLOR = [0.5, 0.5, 0.5]
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* Builds browser-downloadable ZIP archives from resolved PCB component model
|
|
@@ -7,16 +12,19 @@ import { zipSync } from 'fflate'
|
|
|
7
12
|
export class PcbModelArchiveExporter {
|
|
8
13
|
/**
|
|
9
14
|
* Builds one ZIP archive from the resolved scene description.
|
|
10
|
-
* @param {{ archiveBaseName?: string, sceneDescription?: { components?: any[], externalPlacements?: any[] } }} [options]
|
|
11
|
-
* @returns {Promise<{ archiveName: string, archiveBytes: Uint8Array, exportedEntries: { archivePath: string, pattern: string, designator: string, format: string, modelName: string }[], skippedEntries: { designator: string, reason: string }[] }>}
|
|
15
|
+
* @param {{ archiveBaseName?: string, sceneDescription?: { components?: any[], externalPlacements?: any[], staticBodyPlacements?: any[] }, modelMeshLoader?: ((placement: object) => Promise<object | object[]> | object | object[]) | PcbAssemblyModelMeshLoader, includeRawModels?: boolean, includeStitchedComponents?: boolean, stitchedDesignators?: string[] }} [options]
|
|
16
|
+
* @returns {Promise<{ archiveName: string, archiveBytes: Uint8Array, exportedEntries: { archivePath: string, pattern: string, designator: string, format: string, modelName: string, kind?: string }[], skippedEntries: { designator: string, reason: string }[] }>}
|
|
12
17
|
*/
|
|
13
18
|
static async buildArchive(options = {}) {
|
|
14
19
|
const archiveName = PcbModelArchiveExporter.#resolveArchiveName(
|
|
15
20
|
options.archiveBaseName
|
|
16
21
|
)
|
|
17
|
-
const resolvedEntries =
|
|
18
|
-
options.
|
|
19
|
-
|
|
22
|
+
const resolvedEntries =
|
|
23
|
+
options.includeRawModels === false
|
|
24
|
+
? []
|
|
25
|
+
: PcbModelArchiveExporter.#collectResolvedEntries(
|
|
26
|
+
options.sceneDescription
|
|
27
|
+
)
|
|
20
28
|
const exportedEntries = []
|
|
21
29
|
const skippedEntries = []
|
|
22
30
|
const archiveInput = {}
|
|
@@ -47,6 +55,45 @@ export class PcbModelArchiveExporter {
|
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
57
|
|
|
58
|
+
const stitchedEntries =
|
|
59
|
+
options.includeStitchedComponents === false
|
|
60
|
+
? { entries: [], skippedEntries: [] }
|
|
61
|
+
: await PcbModelArchiveExporter.#buildStitchedEntries(
|
|
62
|
+
options.sceneDescription,
|
|
63
|
+
options.modelMeshLoader,
|
|
64
|
+
options.stitchedDesignators
|
|
65
|
+
)
|
|
66
|
+
skippedEntries.push(...stitchedEntries.skippedEntries)
|
|
67
|
+
for (const entry of stitchedEntries.entries) {
|
|
68
|
+
try {
|
|
69
|
+
const archivePath =
|
|
70
|
+
PcbModelArchiveExporter.#resolveStitchedArchivePath(
|
|
71
|
+
entry,
|
|
72
|
+
exportedEntries
|
|
73
|
+
)
|
|
74
|
+
archiveInput[archivePath] = new TextEncoder().encode(
|
|
75
|
+
entry.payloadText
|
|
76
|
+
)
|
|
77
|
+
exportedEntries.push({
|
|
78
|
+
archivePath,
|
|
79
|
+
pattern: entry.pattern,
|
|
80
|
+
designator: entry.designator,
|
|
81
|
+
format: 'step',
|
|
82
|
+
modelName: entry.modelName,
|
|
83
|
+
kind: 'stitched-component'
|
|
84
|
+
})
|
|
85
|
+
} catch (error) {
|
|
86
|
+
skippedEntries.push({
|
|
87
|
+
designator: entry.designator,
|
|
88
|
+
reason: String(
|
|
89
|
+
error?.message ||
|
|
90
|
+
error ||
|
|
91
|
+
'Stitched component export failed.'
|
|
92
|
+
)
|
|
93
|
+
})
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
50
97
|
return {
|
|
51
98
|
archiveName,
|
|
52
99
|
archiveBytes: zipSync(archiveInput),
|
|
@@ -145,6 +192,387 @@ export class PcbModelArchiveExporter {
|
|
|
145
192
|
return dedupedEntries
|
|
146
193
|
}
|
|
147
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Builds generated STEP entries for authored multi-body components.
|
|
197
|
+
* @param {{ components?: object[], externalPlacements?: object[], staticBodyPlacements?: object[] } | undefined} sceneDescription Scene data.
|
|
198
|
+
* @param {((placement: object) => Promise<object | object[]> | object | object[]) | PcbAssemblyModelMeshLoader | undefined} modelMeshLoader Mesh loader override.
|
|
199
|
+
* @param {string[] | undefined} stitchedDesignators Optional designator filter.
|
|
200
|
+
* @returns {Promise<{ entries: { pattern: string, designator: string, modelName: string, payloadText: string }[], skippedEntries: { designator: string, reason: string }[] }>}
|
|
201
|
+
*/
|
|
202
|
+
static async #buildStitchedEntries(
|
|
203
|
+
sceneDescription,
|
|
204
|
+
modelMeshLoader,
|
|
205
|
+
stitchedDesignators
|
|
206
|
+
) {
|
|
207
|
+
const candidates = PcbModelArchiveExporter.#collectStitchedCandidates(
|
|
208
|
+
sceneDescription,
|
|
209
|
+
stitchedDesignators
|
|
210
|
+
)
|
|
211
|
+
if (!candidates.length) {
|
|
212
|
+
return { entries: [], skippedEntries: [] }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const loader =
|
|
216
|
+
PcbModelArchiveExporter.#resolveModelMeshLoader(modelMeshLoader)
|
|
217
|
+
const ownsLoader =
|
|
218
|
+
!(
|
|
219
|
+
typeof modelMeshLoader === 'function' ||
|
|
220
|
+
modelMeshLoader instanceof PcbAssemblyModelMeshLoader
|
|
221
|
+
) && loader instanceof PcbAssemblyModelMeshLoader
|
|
222
|
+
try {
|
|
223
|
+
const entries = []
|
|
224
|
+
const skippedEntries = []
|
|
225
|
+
for (const candidate of candidates) {
|
|
226
|
+
try {
|
|
227
|
+
const meshes =
|
|
228
|
+
await PcbModelArchiveExporter.#buildStitchedMeshes(
|
|
229
|
+
candidate,
|
|
230
|
+
loader
|
|
231
|
+
)
|
|
232
|
+
if (!meshes.length) {
|
|
233
|
+
continue
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const modelName =
|
|
237
|
+
candidate.designator + ' stitched component'
|
|
238
|
+
entries.push({
|
|
239
|
+
pattern: candidate.pattern,
|
|
240
|
+
designator: candidate.designator,
|
|
241
|
+
modelName,
|
|
242
|
+
payloadText: PcbAssemblyStepWriter.write({
|
|
243
|
+
name: modelName,
|
|
244
|
+
meshes
|
|
245
|
+
})
|
|
246
|
+
})
|
|
247
|
+
} catch (error) {
|
|
248
|
+
skippedEntries.push({
|
|
249
|
+
designator: candidate.designator,
|
|
250
|
+
reason: String(
|
|
251
|
+
error?.message ||
|
|
252
|
+
error ||
|
|
253
|
+
'Stitched component export failed.'
|
|
254
|
+
)
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return { entries, skippedEntries }
|
|
260
|
+
} finally {
|
|
261
|
+
if (ownsLoader) {
|
|
262
|
+
loader.dispose?.()
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Collects component stack candidates keyed by designator.
|
|
269
|
+
* @param {{ components?: object[], externalPlacements?: object[], staticBodyPlacements?: object[] } | undefined} sceneDescription Scene data.
|
|
270
|
+
* @param {string[] | undefined} stitchedDesignators Optional designator filter.
|
|
271
|
+
* @returns {{ designator: string, pattern: string, externalPlacements: object[], staticBodyPlacements: object[] }[]}
|
|
272
|
+
*/
|
|
273
|
+
static #collectStitchedCandidates(sceneDescription, stitchedDesignators) {
|
|
274
|
+
const componentByDesignator =
|
|
275
|
+
PcbModelArchiveExporter.#componentByDesignator(sceneDescription)
|
|
276
|
+
const designatorSet =
|
|
277
|
+
PcbModelArchiveExporter.#normalizedDesignatorSet(
|
|
278
|
+
stitchedDesignators
|
|
279
|
+
)
|
|
280
|
+
const groups = new Map()
|
|
281
|
+
|
|
282
|
+
PcbModelArchiveExporter.#array(
|
|
283
|
+
sceneDescription?.staticBodyPlacements
|
|
284
|
+
).forEach((placement) => {
|
|
285
|
+
const designator = String(placement?.designator || '').trim()
|
|
286
|
+
if (!designator) {
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
PcbModelArchiveExporter.#stitchedGroup(
|
|
291
|
+
groups,
|
|
292
|
+
designator
|
|
293
|
+
).staticBodyPlacements.push(placement)
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
PcbModelArchiveExporter.#array(sceneDescription?.externalPlacements)
|
|
297
|
+
.filter((placement) => placement?.externalModel)
|
|
298
|
+
.forEach((placement) => {
|
|
299
|
+
const designator = String(placement?.designator || '').trim()
|
|
300
|
+
if (!designator) {
|
|
301
|
+
return
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
PcbModelArchiveExporter.#stitchedGroup(
|
|
305
|
+
groups,
|
|
306
|
+
designator
|
|
307
|
+
).externalPlacements.push(placement)
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
return [...groups.values()]
|
|
311
|
+
.filter(
|
|
312
|
+
(group) =>
|
|
313
|
+
!designatorSet ||
|
|
314
|
+
designatorSet.has(
|
|
315
|
+
PcbModelArchiveExporter.#normalizeDesignator(
|
|
316
|
+
group.designator
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
)
|
|
320
|
+
.filter((group) =>
|
|
321
|
+
PcbModelArchiveExporter.#isStitchedCandidate(group)
|
|
322
|
+
)
|
|
323
|
+
.map((group) => ({
|
|
324
|
+
...group,
|
|
325
|
+
pattern: PcbModelArchiveExporter.#resolvePattern(
|
|
326
|
+
componentByDesignator.get(group.designator) || null,
|
|
327
|
+
group.externalPlacements[0] || null
|
|
328
|
+
)
|
|
329
|
+
}))
|
|
330
|
+
.sort((left, right) =>
|
|
331
|
+
left.designator.localeCompare(right.designator)
|
|
332
|
+
)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Builds a normalized designator filter set.
|
|
337
|
+
* @param {unknown} designators Candidate designator list.
|
|
338
|
+
* @returns {Set<string> | null}
|
|
339
|
+
*/
|
|
340
|
+
static #normalizedDesignatorSet(designators) {
|
|
341
|
+
if (!Array.isArray(designators) || !designators.length) {
|
|
342
|
+
return null
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const normalized = designators
|
|
346
|
+
.map((designator) =>
|
|
347
|
+
PcbModelArchiveExporter.#normalizeDesignator(designator)
|
|
348
|
+
)
|
|
349
|
+
.filter(Boolean)
|
|
350
|
+
|
|
351
|
+
return normalized.length ? new Set(normalized) : null
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Normalizes a component designator for matching.
|
|
356
|
+
* @param {unknown} designator Candidate designator.
|
|
357
|
+
* @returns {string}
|
|
358
|
+
*/
|
|
359
|
+
static #normalizeDesignator(designator) {
|
|
360
|
+
return String(designator || '')
|
|
361
|
+
.trim()
|
|
362
|
+
.toUpperCase()
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Returns true when one designator represents a multi-body component.
|
|
367
|
+
* @param {{ externalPlacements: object[], staticBodyPlacements: object[] }} group Candidate group.
|
|
368
|
+
* @returns {boolean}
|
|
369
|
+
*/
|
|
370
|
+
static #isStitchedCandidate(group) {
|
|
371
|
+
const hasVariantMetadata = [
|
|
372
|
+
...group.externalPlacements,
|
|
373
|
+
...group.staticBodyPlacements
|
|
374
|
+
].some((placement) =>
|
|
375
|
+
String(placement?.coLocatedVariantGroupKey || '').trim()
|
|
376
|
+
)
|
|
377
|
+
return (
|
|
378
|
+
group.externalPlacements.length > 0 &&
|
|
379
|
+
(group.externalPlacements.length > 1 ||
|
|
380
|
+
group.staticBodyPlacements.length > 0 ||
|
|
381
|
+
hasVariantMetadata)
|
|
382
|
+
)
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Builds transformed, component-local meshes for one stitched component.
|
|
387
|
+
* @param {{ designator: string, externalPlacements: object[], staticBodyPlacements: object[] }} candidate Stitched component candidate.
|
|
388
|
+
* @param {((placement: object) => Promise<object | object[]> | object | object[]) | PcbAssemblyModelMeshLoader} loader Mesh loader.
|
|
389
|
+
* @returns {Promise<object[]>}
|
|
390
|
+
*/
|
|
391
|
+
static async #buildStitchedMeshes(candidate, loader) {
|
|
392
|
+
const anchor = PcbModelArchiveExporter.#stitchedAnchor(candidate)
|
|
393
|
+
const staticMeshes = candidate.staticBodyPlacements
|
|
394
|
+
.map((placement) =>
|
|
395
|
+
PcbModelArchiveExporter.#staticBodyMesh(
|
|
396
|
+
candidate.designator,
|
|
397
|
+
placement
|
|
398
|
+
)
|
|
399
|
+
)
|
|
400
|
+
.filter(Boolean)
|
|
401
|
+
const externalMeshes = []
|
|
402
|
+
|
|
403
|
+
for (const placement of candidate.externalPlacements) {
|
|
404
|
+
const loaded = await PcbModelArchiveExporter.#loadPlacementMeshes(
|
|
405
|
+
loader,
|
|
406
|
+
placement
|
|
407
|
+
)
|
|
408
|
+
loaded.filter(Boolean).forEach((mesh, index) => {
|
|
409
|
+
externalMeshes.push(
|
|
410
|
+
PcbAssemblyMeshUtils.transformMesh(
|
|
411
|
+
{
|
|
412
|
+
...mesh,
|
|
413
|
+
name:
|
|
414
|
+
mesh.name ||
|
|
415
|
+
candidate.designator + '-model-' + (index + 1)
|
|
416
|
+
},
|
|
417
|
+
placement
|
|
418
|
+
)
|
|
419
|
+
)
|
|
420
|
+
})
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return PcbModelArchiveExporter.#translateMeshes(
|
|
424
|
+
[...staticMeshes, ...externalMeshes],
|
|
425
|
+
{
|
|
426
|
+
x: -anchor.x,
|
|
427
|
+
y: -anchor.y,
|
|
428
|
+
z: -anchor.z
|
|
429
|
+
}
|
|
430
|
+
)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Builds one mesh for an authored static carrier body.
|
|
435
|
+
* @param {string} designator Component designator.
|
|
436
|
+
* @param {object} placement Static body placement.
|
|
437
|
+
* @returns {object | null}
|
|
438
|
+
*/
|
|
439
|
+
static #staticBodyMesh(designator, placement) {
|
|
440
|
+
const geometry = placement?.geometry || {}
|
|
441
|
+
const kind = String(geometry?.kind || '').toLowerCase()
|
|
442
|
+
const height = Number(geometry?.heightMil || 0)
|
|
443
|
+
if (!(height > 0)) {
|
|
444
|
+
return null
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
let mesh = null
|
|
448
|
+
if (kind === 'extruded-polygon') {
|
|
449
|
+
const points = PcbModelArchiveExporter.#array(
|
|
450
|
+
geometry.verticesMil
|
|
451
|
+
).map((point) => [Number(point?.x || 0), Number(point?.y || 0)])
|
|
452
|
+
mesh = PcbAssemblyMeshUtils.prism(
|
|
453
|
+
'static-' + PcbAssemblyMeshUtils.safeName(designator),
|
|
454
|
+
points,
|
|
455
|
+
0,
|
|
456
|
+
height,
|
|
457
|
+
PcbModelArchiveExporter.#bodyColor(placement)
|
|
458
|
+
)
|
|
459
|
+
} else if (kind === 'cylinder') {
|
|
460
|
+
mesh = PcbAssemblyMeshUtils.cylinder(
|
|
461
|
+
'static-' + PcbAssemblyMeshUtils.safeName(designator),
|
|
462
|
+
{
|
|
463
|
+
radius: Number(geometry?.radiusMil || 0),
|
|
464
|
+
height,
|
|
465
|
+
color: PcbModelArchiveExporter.#bodyColor(placement)
|
|
466
|
+
}
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return mesh ? PcbAssemblyMeshUtils.transformMesh(mesh, placement) : null
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Resolves a mesh loader for generated stitched components.
|
|
475
|
+
* @param {((placement: object) => Promise<object | object[]> | object | object[]) | PcbAssemblyModelMeshLoader | undefined} modelMeshLoader Loader option.
|
|
476
|
+
* @returns {((placement: object) => Promise<object | object[]> | object | object[]) | PcbAssemblyModelMeshLoader}
|
|
477
|
+
*/
|
|
478
|
+
static #resolveModelMeshLoader(modelMeshLoader) {
|
|
479
|
+
if (
|
|
480
|
+
typeof modelMeshLoader === 'function' ||
|
|
481
|
+
modelMeshLoader instanceof PcbAssemblyModelMeshLoader
|
|
482
|
+
) {
|
|
483
|
+
return modelMeshLoader
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return new PcbAssemblyModelMeshLoader()
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Loads meshes for one external placement.
|
|
491
|
+
* @param {((placement: object) => Promise<object | object[]> | object | object[]) | PcbAssemblyModelMeshLoader} loader Mesh loader.
|
|
492
|
+
* @param {object} placement External placement.
|
|
493
|
+
* @returns {Promise<object[]>}
|
|
494
|
+
*/
|
|
495
|
+
static async #loadPlacementMeshes(loader, placement) {
|
|
496
|
+
const loaded =
|
|
497
|
+
loader instanceof PcbAssemblyModelMeshLoader
|
|
498
|
+
? await loader.loadPlacement(placement)
|
|
499
|
+
: await loader(placement)
|
|
500
|
+
return Array.isArray(loaded) ? loaded : [loaded]
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/**
|
|
504
|
+
* Translates meshes by a fixed offset.
|
|
505
|
+
* @param {object[]} meshes Source meshes.
|
|
506
|
+
* @param {{ x?: number, y?: number, z?: number }} offset Translation.
|
|
507
|
+
* @returns {object[]}
|
|
508
|
+
*/
|
|
509
|
+
static #translateMeshes(meshes, offset) {
|
|
510
|
+
return meshes.map((mesh) => ({
|
|
511
|
+
...mesh,
|
|
512
|
+
vertices: PcbModelArchiveExporter.#array(mesh.vertices).map(
|
|
513
|
+
(vertex) => [
|
|
514
|
+
Number(vertex?.[0] || 0) + Number(offset.x || 0),
|
|
515
|
+
Number(vertex?.[1] || 0) + Number(offset.y || 0),
|
|
516
|
+
Number(vertex?.[2] || 0) + Number(offset.z || 0)
|
|
517
|
+
]
|
|
518
|
+
)
|
|
519
|
+
}))
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* Resolves one component-local origin for a stitched export.
|
|
524
|
+
* @param {{ externalPlacements: object[], staticBodyPlacements: object[] }} candidate Stitched component candidate.
|
|
525
|
+
* @returns {{ x: number, y: number, z: number }}
|
|
526
|
+
*/
|
|
527
|
+
static #stitchedAnchor(candidate) {
|
|
528
|
+
const placement =
|
|
529
|
+
candidate.externalPlacements[0] ||
|
|
530
|
+
candidate.staticBodyPlacements[0] ||
|
|
531
|
+
{}
|
|
532
|
+
return {
|
|
533
|
+
x: Number(placement?.positionMil?.x || 0),
|
|
534
|
+
y: Number(placement?.positionMil?.y || 0),
|
|
535
|
+
z: Number(placement?.positionMil?.z || 0)
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Gets or creates one stitched candidate group.
|
|
541
|
+
* @param {Map<string, { designator: string, externalPlacements: object[], staticBodyPlacements: object[] }>} groups Mutable groups.
|
|
542
|
+
* @param {string} designator Component designator.
|
|
543
|
+
* @returns {{ designator: string, externalPlacements: object[], staticBodyPlacements: object[] }}
|
|
544
|
+
*/
|
|
545
|
+
static #stitchedGroup(groups, designator) {
|
|
546
|
+
if (!groups.has(designator)) {
|
|
547
|
+
groups.set(designator, {
|
|
548
|
+
designator,
|
|
549
|
+
externalPlacements: [],
|
|
550
|
+
staticBodyPlacements: []
|
|
551
|
+
})
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return groups.get(designator)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Builds a designator lookup for scene components.
|
|
559
|
+
* @param {{ components?: object[] } | undefined} sceneDescription Scene data.
|
|
560
|
+
* @returns {Map<string, object>}
|
|
561
|
+
*/
|
|
562
|
+
static #componentByDesignator(sceneDescription) {
|
|
563
|
+
const componentByDesignator = new Map()
|
|
564
|
+
PcbModelArchiveExporter.#array(sceneDescription?.components).forEach(
|
|
565
|
+
(component) => {
|
|
566
|
+
const designator = String(component?.designator || '').trim()
|
|
567
|
+
if (designator) {
|
|
568
|
+
componentByDesignator.set(designator, component)
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
return componentByDesignator
|
|
574
|
+
}
|
|
575
|
+
|
|
148
576
|
/**
|
|
149
577
|
* Resolves one archive file name for the exported model.
|
|
150
578
|
* @param {{ pattern: string, model: { format?: string } }} entry
|
|
@@ -175,6 +603,37 @@ export class PcbModelArchiveExporter {
|
|
|
175
603
|
return archivePath
|
|
176
604
|
}
|
|
177
605
|
|
|
606
|
+
/**
|
|
607
|
+
* Resolves one archive path for a stitched component STEP.
|
|
608
|
+
* @param {{ designator: string }} entry Stitched entry.
|
|
609
|
+
* @param {{ archivePath: string }[]} exportedEntries Existing entries.
|
|
610
|
+
* @returns {string}
|
|
611
|
+
*/
|
|
612
|
+
static #resolveStitchedArchivePath(entry, exportedEntries) {
|
|
613
|
+
const sanitizedDesignator = PcbModelArchiveExporter.#sanitizeFileToken(
|
|
614
|
+
entry.designator
|
|
615
|
+
)
|
|
616
|
+
const basePath = 'stitched-components/' + sanitizedDesignator + '.step'
|
|
617
|
+
let archivePath = basePath
|
|
618
|
+
let duplicateIndex = 2
|
|
619
|
+
|
|
620
|
+
while (
|
|
621
|
+
exportedEntries.some(
|
|
622
|
+
(exportedEntry) => exportedEntry.archivePath === archivePath
|
|
623
|
+
)
|
|
624
|
+
) {
|
|
625
|
+
archivePath =
|
|
626
|
+
'stitched-components/' +
|
|
627
|
+
sanitizedDesignator +
|
|
628
|
+
'--' +
|
|
629
|
+
duplicateIndex +
|
|
630
|
+
'.step'
|
|
631
|
+
duplicateIndex += 1
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
return archivePath
|
|
635
|
+
}
|
|
636
|
+
|
|
178
637
|
/**
|
|
179
638
|
* Reads one resolved model into ZIP-ready bytes.
|
|
180
639
|
* @param {{ origin?: string, payloadText?: string, file?: Blob | File | null }} model
|
|
@@ -272,13 +731,68 @@ export class PcbModelArchiveExporter {
|
|
|
272
731
|
const normalizedFormat = String(format || '')
|
|
273
732
|
.trim()
|
|
274
733
|
.toLowerCase()
|
|
275
|
-
if (
|
|
276
|
-
|
|
734
|
+
if (
|
|
735
|
+
[
|
|
736
|
+
'step',
|
|
737
|
+
'stp',
|
|
738
|
+
'wrl',
|
|
739
|
+
'vrml',
|
|
740
|
+
'glb',
|
|
741
|
+
'gltf',
|
|
742
|
+
'stl',
|
|
743
|
+
'obj'
|
|
744
|
+
].includes(normalizedFormat)
|
|
745
|
+
) {
|
|
746
|
+
return normalizedFormat === 'stp'
|
|
747
|
+
? 'step'
|
|
748
|
+
: normalizedFormat === 'vrml'
|
|
749
|
+
? 'wrl'
|
|
750
|
+
: normalizedFormat
|
|
277
751
|
}
|
|
278
752
|
|
|
279
753
|
return 'step'
|
|
280
754
|
}
|
|
281
755
|
|
|
756
|
+
/**
|
|
757
|
+
* Resolves a normalized RGB color for a static body mesh.
|
|
758
|
+
* @param {{ bodyColor?: object, geometry?: object }} placement Static body placement.
|
|
759
|
+
* @returns {number[]}
|
|
760
|
+
*/
|
|
761
|
+
static #bodyColor(placement) {
|
|
762
|
+
const rgb =
|
|
763
|
+
placement?.bodyColor?.rgb || placement?.geometry?.bodyColor?.rgb
|
|
764
|
+
if (rgb) {
|
|
765
|
+
return [
|
|
766
|
+
PcbModelArchiveExporter.#colorChannel(rgb.red),
|
|
767
|
+
PcbModelArchiveExporter.#colorChannel(rgb.green),
|
|
768
|
+
PcbModelArchiveExporter.#colorChannel(rgb.blue)
|
|
769
|
+
]
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return STATIC_BODY_COLOR
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
/**
|
|
776
|
+
* Normalizes one 8-bit color channel to [0, 1].
|
|
777
|
+
* @param {number | undefined} channel Source channel.
|
|
778
|
+
* @returns {number}
|
|
779
|
+
*/
|
|
780
|
+
static #colorChannel(channel) {
|
|
781
|
+
const value = Number(channel)
|
|
782
|
+
return Number.isFinite(value)
|
|
783
|
+
? Math.min(1, Math.max(0, value / 255))
|
|
784
|
+
: 0.5
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Normalizes a value to an array.
|
|
789
|
+
* @param {unknown} value Candidate value.
|
|
790
|
+
* @returns {any[]}
|
|
791
|
+
*/
|
|
792
|
+
static #array(value) {
|
|
793
|
+
return Array.isArray(value) ? value : []
|
|
794
|
+
}
|
|
795
|
+
|
|
282
796
|
/**
|
|
283
797
|
* Removes the trailing extension from one file name.
|
|
284
798
|
* @param {string} value
|