altium-toolkit 1.1.32 → 1.1.35
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/docs/model-format.md +2 -3
- package/package.json +1 -1
- package/src/core/altium/AltiumGeneratedLibraryRecordBuilder.mjs +551 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +286 -2
- package/src/core/altium/AltiumPcbLibExporter.mjs +9 -2
- package/src/core/altium/AltiumSchLibExporter.mjs +1 -1
- package/src/core/altium/PcbComponentBodyPlacementNormalizer.mjs +120 -15
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +52 -4
- package/src/core/altium/PcbShapeBasedBodyGeometryParser.mjs +85 -11
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +31 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/ui/AltiumScene3dAuthoredBodyAnchorAdapter.mjs +30 -2
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +27 -2
- package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +44 -0
- package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +171 -6
- package/src/ui/AltiumScene3dShapeStackOwnerAdapter.mjs +782 -0
- package/src/ui/AltiumScene3dTwoRowFootprintDetector.mjs +90 -0
- package/src/ui/PcbScene3dBuilder.mjs +595 -28
- package/src/ui/PcbScene3dCopperRegionDetailBuilder.mjs +280 -0
- package/src/ui/PcbScene3dModelRegistry.mjs +16 -0
- package/src/ui/PcbScene3dPackageDimensionResolver.mjs +107 -0
- package/src/ui/PcbScene3dPackages.mjs +15 -3
- package/src/ui/PcbScene3dPadYawResolver.mjs +200 -0
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +56 -0
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +638 -16
- package/src/ui/PcbScene3dStaticBodyRecovery.mjs +891 -0
- package/src/ui/PcbScene3dStaticBodySelectionKeyBuilder.mjs +358 -0
- package/src/ui/PcbScene3dStaticBodySymmetryRecovery.mjs +876 -0
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
|
+
import { AltiumGeneratedLibraryRecordBuilder } from './AltiumGeneratedLibraryRecordBuilder.mjs'
|
|
6
|
+
|
|
5
7
|
/**
|
|
6
8
|
* Builds deterministic textual and byte records for generated Altium libraries.
|
|
7
9
|
*/
|
|
@@ -22,6 +24,17 @@ export class AltiumLibraryRecordBuilder {
|
|
|
22
24
|
})
|
|
23
25
|
}
|
|
24
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Builds a full schematic component data record stream.
|
|
29
|
+
* @param {object} bundle Normalized component bundle.
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
static buildSchematicComponentDataRecord(bundle) {
|
|
33
|
+
return AltiumGeneratedLibraryRecordBuilder.buildSchematicComponentData(
|
|
34
|
+
bundle
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
/**
|
|
26
39
|
* Builds a PCB footprint record.
|
|
27
40
|
* @param {object} bundle Normalized component bundle.
|
|
@@ -35,10 +48,24 @@ export class AltiumLibraryRecordBuilder {
|
|
|
35
48
|
DisplayName: bundle.name,
|
|
36
49
|
PadCount: String(bundle.footprint.pads.length),
|
|
37
50
|
TrackCount: String(bundle.footprint.tracks.length),
|
|
51
|
+
ArcCount: String(bundle.footprint.arcs.length),
|
|
52
|
+
FillCount: String(bundle.footprint.fills.length),
|
|
53
|
+
TextCount: String(bundle.footprint.texts.length),
|
|
38
54
|
ModelCount: String(bundle.models.length)
|
|
39
55
|
})
|
|
40
56
|
}
|
|
41
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Counts generated footprint primitives.
|
|
60
|
+
* @param {object} bundle Normalized bundle.
|
|
61
|
+
* @returns {number}
|
|
62
|
+
*/
|
|
63
|
+
static footprintPrimitiveCount(bundle) {
|
|
64
|
+
return AltiumGeneratedLibraryRecordBuilder.footprintPrimitiveCount(
|
|
65
|
+
bundle.footprint
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
42
69
|
/**
|
|
43
70
|
* Builds a PcbLib Library/Data stream.
|
|
44
71
|
* @param {object[]} bundles Normalized component bundles.
|
|
@@ -113,7 +140,10 @@ export class AltiumLibraryRecordBuilder {
|
|
|
113
140
|
*/
|
|
114
141
|
static buildFootprintData(bundle) {
|
|
115
142
|
return AltiumLibraryRecordBuilder.concatBytes([
|
|
116
|
-
AltiumLibraryRecordBuilder.createStringBlock(bundle.footprint.name)
|
|
143
|
+
AltiumLibraryRecordBuilder.createStringBlock(bundle.footprint.name),
|
|
144
|
+
...AltiumGeneratedLibraryRecordBuilder.buildFootprintPrimitiveRecords(
|
|
145
|
+
bundle.footprint
|
|
146
|
+
)
|
|
117
147
|
])
|
|
118
148
|
}
|
|
119
149
|
|
|
@@ -133,7 +163,7 @@ export class AltiumLibraryRecordBuilder {
|
|
|
133
163
|
|
|
134
164
|
/**
|
|
135
165
|
* Builds model metadata entries.
|
|
136
|
-
* @param {{ model: object, id: string, checksum: number }[]} models Model rows.
|
|
166
|
+
* @param {{ bundle: object, model: object, id: string, checksum: number }[]} models Model rows.
|
|
137
167
|
* @returns {Uint8Array}
|
|
138
168
|
*/
|
|
139
169
|
static buildModelsData(models) {
|
|
@@ -141,7 +171,12 @@ export class AltiumLibraryRecordBuilder {
|
|
|
141
171
|
models.map((row) =>
|
|
142
172
|
AltiumLibraryRecordBuilder.createLengthPrefixedAscii(
|
|
143
173
|
AltiumLibraryRecordBuilder.#pipeRecord({
|
|
174
|
+
EMBED: 'TRUE',
|
|
175
|
+
MODELSOURCE: 'Undefined',
|
|
144
176
|
ID: row.id,
|
|
177
|
+
...AltiumLibraryRecordBuilder.#modelMetadataTransform(
|
|
178
|
+
row
|
|
179
|
+
),
|
|
145
180
|
NAME: row.model.name,
|
|
146
181
|
CHECKSUM: String(row.checksum),
|
|
147
182
|
FORMAT: row.model.format
|
|
@@ -151,6 +186,23 @@ export class AltiumLibraryRecordBuilder {
|
|
|
151
186
|
)
|
|
152
187
|
}
|
|
153
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Builds component-body placement records for embedded model rows.
|
|
191
|
+
* @param {{ bundle: object, model: object, id: string, checksum: number }[]} models Model rows.
|
|
192
|
+
* @returns {Uint8Array}
|
|
193
|
+
*/
|
|
194
|
+
static buildComponentBodiesData(models) {
|
|
195
|
+
return AltiumLibraryRecordBuilder.concatBytes(
|
|
196
|
+
models.map((row) =>
|
|
197
|
+
new TextEncoder().encode(
|
|
198
|
+
AltiumLibraryRecordBuilder.#pipeRecord(
|
|
199
|
+
AltiumLibraryRecordBuilder.#componentBodyFields(row)
|
|
200
|
+
) + '\u0000'
|
|
201
|
+
)
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
|
|
154
206
|
/**
|
|
155
207
|
* Creates a little-endian count header.
|
|
156
208
|
* @param {number} count Count value.
|
|
@@ -249,6 +301,238 @@ export class AltiumLibraryRecordBuilder {
|
|
|
249
301
|
)
|
|
250
302
|
}
|
|
251
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Builds model metadata transform fields.
|
|
306
|
+
* @param {{ bundle: object, model: object }} row Model row.
|
|
307
|
+
* @returns {Record<string, string>}
|
|
308
|
+
*/
|
|
309
|
+
static #modelMetadataTransform(row) {
|
|
310
|
+
const rotation = AltiumLibraryRecordBuilder.#modelRotation(
|
|
311
|
+
row.model,
|
|
312
|
+
AltiumLibraryRecordBuilder.#rowComponent(row)
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
ROTX: AltiumLibraryRecordBuilder.#numberText(rotation.x),
|
|
317
|
+
ROTY: AltiumLibraryRecordBuilder.#numberText(rotation.y),
|
|
318
|
+
ROTZ: AltiumLibraryRecordBuilder.#numberText(rotation.z),
|
|
319
|
+
DZ: AltiumLibraryRecordBuilder.#milText(
|
|
320
|
+
AltiumLibraryRecordBuilder.#modelDzMil(row.model)
|
|
321
|
+
)
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Builds one component-body placement record.
|
|
327
|
+
* @param {{ bundle: object, model: object, id: string, checksum: number }} row Model row.
|
|
328
|
+
* @returns {Record<string, string>}
|
|
329
|
+
*/
|
|
330
|
+
static #componentBodyFields(row) {
|
|
331
|
+
const offset = AltiumLibraryRecordBuilder.#modelOffsetMil(row.model)
|
|
332
|
+
const rotation = AltiumLibraryRecordBuilder.#modelRotation(
|
|
333
|
+
row.model,
|
|
334
|
+
AltiumLibraryRecordBuilder.#rowComponent(row)
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
V7_LAYER: 'MECHANICAL1',
|
|
339
|
+
KIND: '0',
|
|
340
|
+
SUBPOLYINDEX: '-1',
|
|
341
|
+
UNIONINDEX: '0',
|
|
342
|
+
ISSHAPEBASED: 'FALSE',
|
|
343
|
+
STANDOFFHEIGHT: '0mil',
|
|
344
|
+
OVERALLHEIGHT: '0mil',
|
|
345
|
+
IDENTIFIER: AltiumLibraryRecordBuilder.#identifierBytes(
|
|
346
|
+
row.bundle?.footprint?.name || row.model?.name || row.id
|
|
347
|
+
),
|
|
348
|
+
MODELID: row.id,
|
|
349
|
+
'MODEL.CHECKSUM': String(row.checksum),
|
|
350
|
+
'MODEL.EMBED': 'TRUE',
|
|
351
|
+
'MODEL.NAME': row.model.name,
|
|
352
|
+
'MODEL.2D.X': AltiumLibraryRecordBuilder.#milText(offset.x),
|
|
353
|
+
'MODEL.2D.Y': AltiumLibraryRecordBuilder.#milText(offset.y),
|
|
354
|
+
'MODEL.2D.ROTATION': '0',
|
|
355
|
+
'MODEL.3D.ROTX': AltiumLibraryRecordBuilder.#numberText(rotation.x),
|
|
356
|
+
'MODEL.3D.ROTY': AltiumLibraryRecordBuilder.#numberText(rotation.y),
|
|
357
|
+
'MODEL.3D.ROTZ': AltiumLibraryRecordBuilder.#numberText(rotation.z),
|
|
358
|
+
'MODEL.3D.DZ': AltiumLibraryRecordBuilder.#milText(
|
|
359
|
+
AltiumLibraryRecordBuilder.#modelDzMil(row.model)
|
|
360
|
+
),
|
|
361
|
+
'MODEL.MODELSOURCE': 'Undefined'
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Resolves the source component attached to a model row.
|
|
367
|
+
* @param {{ bundle?: object }} row Model row.
|
|
368
|
+
* @returns {object}
|
|
369
|
+
*/
|
|
370
|
+
static #rowComponent(row) {
|
|
371
|
+
return (
|
|
372
|
+
row?.bundle?.footprint?.component ||
|
|
373
|
+
row?.bundle?.footprint?.raw?.component ||
|
|
374
|
+
{}
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Resolves one model transform object.
|
|
380
|
+
* @param {object} model Model asset.
|
|
381
|
+
* @returns {object}
|
|
382
|
+
*/
|
|
383
|
+
static #modelTransform(model) {
|
|
384
|
+
return model?.transform || model?.raw?.transform || {}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Resolves model offset in mils.
|
|
389
|
+
* @param {object} model Model asset.
|
|
390
|
+
* @returns {{ x: number, y: number }}
|
|
391
|
+
*/
|
|
392
|
+
static #modelOffsetMil(model) {
|
|
393
|
+
const transform = AltiumLibraryRecordBuilder.#modelTransform(model)
|
|
394
|
+
const offsetMil = transform.offsetMil || {}
|
|
395
|
+
|
|
396
|
+
return {
|
|
397
|
+
x: AltiumLibraryRecordBuilder.#round(
|
|
398
|
+
AltiumLibraryRecordBuilder.#number(
|
|
399
|
+
offsetMil.x ?? transform.dxMil,
|
|
400
|
+
0
|
|
401
|
+
)
|
|
402
|
+
),
|
|
403
|
+
y: AltiumLibraryRecordBuilder.#round(
|
|
404
|
+
AltiumLibraryRecordBuilder.#number(
|
|
405
|
+
offsetMil.y ?? transform.dyMil,
|
|
406
|
+
0
|
|
407
|
+
)
|
|
408
|
+
)
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/**
|
|
413
|
+
* Resolves one model Z offset in mils.
|
|
414
|
+
* @param {object} model Model asset.
|
|
415
|
+
* @returns {number}
|
|
416
|
+
*/
|
|
417
|
+
static #modelDzMil(model) {
|
|
418
|
+
const transform = AltiumLibraryRecordBuilder.#modelTransform(model)
|
|
419
|
+
const offsetMil = transform.offsetMil || {}
|
|
420
|
+
|
|
421
|
+
return AltiumLibraryRecordBuilder.#round(
|
|
422
|
+
AltiumLibraryRecordBuilder.#number(
|
|
423
|
+
offsetMil.z ?? transform.dzMil,
|
|
424
|
+
0
|
|
425
|
+
)
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Resolves model 3D rotation in footprint-local degrees.
|
|
431
|
+
* @param {object} model Model asset.
|
|
432
|
+
* @param {object} component Selected footprint component origin.
|
|
433
|
+
* @returns {{ x: number, y: number, z: number }}
|
|
434
|
+
*/
|
|
435
|
+
static #modelRotation(model, component) {
|
|
436
|
+
const transform = AltiumLibraryRecordBuilder.#modelTransform(model)
|
|
437
|
+
const rotation = transform.rotationDeg || {}
|
|
438
|
+
const generatedRotation =
|
|
439
|
+
AltiumLibraryRecordBuilder.#generatedFrameRotation(model, component)
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
x: AltiumLibraryRecordBuilder.#round(
|
|
443
|
+
AltiumLibraryRecordBuilder.#number(rotation.x, 0) +
|
|
444
|
+
generatedRotation.x
|
|
445
|
+
),
|
|
446
|
+
y: AltiumLibraryRecordBuilder.#round(
|
|
447
|
+
AltiumLibraryRecordBuilder.#number(rotation.y, 0) +
|
|
448
|
+
generatedRotation.y
|
|
449
|
+
),
|
|
450
|
+
z: AltiumLibraryRecordBuilder.#round(
|
|
451
|
+
AltiumLibraryRecordBuilder.#number(rotation.z, 0) +
|
|
452
|
+
generatedRotation.z
|
|
453
|
+
)
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Resolves correction for generated stitched STEP files.
|
|
459
|
+
* @param {object} model Model asset.
|
|
460
|
+
* @param {object} component Selected footprint component origin.
|
|
461
|
+
* @returns {{ x: number, y: number, z: number }}
|
|
462
|
+
*/
|
|
463
|
+
static #generatedFrameRotation(model, component) {
|
|
464
|
+
if (model?.generated !== true && model?.raw?.generated !== true) {
|
|
465
|
+
return { x: 0, y: 0, z: 0 }
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
x: -90,
|
|
470
|
+
y: 0,
|
|
471
|
+
z: AltiumLibraryRecordBuilder.#normalizeAngle(
|
|
472
|
+
-AltiumLibraryRecordBuilder.#number(component?.rotation, 0)
|
|
473
|
+
)
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Encodes one Altium identifier as comma-separated character codes.
|
|
479
|
+
* @param {string} text Source text.
|
|
480
|
+
* @returns {string}
|
|
481
|
+
*/
|
|
482
|
+
static #identifierBytes(text) {
|
|
483
|
+
return [...String(text || '')]
|
|
484
|
+
.map((character) => character.charCodeAt(0))
|
|
485
|
+
.join(',')
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Formats a model distance in mils.
|
|
490
|
+
* @param {number} value Value in mils.
|
|
491
|
+
* @returns {string}
|
|
492
|
+
*/
|
|
493
|
+
static #milText(value) {
|
|
494
|
+
return AltiumLibraryRecordBuilder.#numberText(value) + 'mil'
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Formats one numeric record field.
|
|
499
|
+
* @param {number} value Numeric value.
|
|
500
|
+
* @returns {string}
|
|
501
|
+
*/
|
|
502
|
+
static #numberText(value) {
|
|
503
|
+
return String(AltiumLibraryRecordBuilder.#round(value))
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* Reads a finite number with fallback.
|
|
508
|
+
* @param {unknown} value Candidate value.
|
|
509
|
+
* @param {number} fallback Fallback value.
|
|
510
|
+
* @returns {number}
|
|
511
|
+
*/
|
|
512
|
+
static #number(value, fallback) {
|
|
513
|
+
const number = Number(value)
|
|
514
|
+
return Number.isFinite(number) ? number : fallback
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Rounds generated numeric fields.
|
|
519
|
+
* @param {number} value Numeric value.
|
|
520
|
+
* @returns {number}
|
|
521
|
+
*/
|
|
522
|
+
static #round(value) {
|
|
523
|
+
const rounded = Number(Number(value || 0).toFixed(6))
|
|
524
|
+
return Object.is(rounded, -0) ? 0 : rounded
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Normalizes a degree angle to the 0-360 range.
|
|
529
|
+
* @param {number} angle Angle in degrees.
|
|
530
|
+
* @returns {number}
|
|
531
|
+
*/
|
|
532
|
+
static #normalizeAngle(angle) {
|
|
533
|
+
return ((angle % 360) + 360) % 360
|
|
534
|
+
}
|
|
535
|
+
|
|
252
536
|
/**
|
|
253
537
|
* Builds one pipe-delimited record.
|
|
254
538
|
* @param {Record<string, string>} fields Record fields.
|
|
@@ -42,7 +42,9 @@ export class AltiumPcbLibExporter {
|
|
|
42
42
|
)
|
|
43
43
|
streams.set(
|
|
44
44
|
storageName + '/Header',
|
|
45
|
-
AltiumLibraryRecordBuilder.createCountHeader(
|
|
45
|
+
AltiumLibraryRecordBuilder.createCountHeader(
|
|
46
|
+
AltiumLibraryRecordBuilder.footprintPrimitiveCount(bundle)
|
|
47
|
+
)
|
|
46
48
|
)
|
|
47
49
|
streams.set(
|
|
48
50
|
storageName + '/Parameters',
|
|
@@ -65,6 +67,10 @@ export class AltiumPcbLibExporter {
|
|
|
65
67
|
'Models/Data',
|
|
66
68
|
AltiumLibraryRecordBuilder.buildModelsData(modelRows)
|
|
67
69
|
)
|
|
70
|
+
streams.set(
|
|
71
|
+
'ComponentBodies6/Data',
|
|
72
|
+
AltiumLibraryRecordBuilder.buildComponentBodiesData(modelRows)
|
|
73
|
+
)
|
|
68
74
|
modelRows.forEach((row, index) => {
|
|
69
75
|
streams.set('Models/' + index, row.model.bytes)
|
|
70
76
|
})
|
|
@@ -76,11 +82,12 @@ export class AltiumPcbLibExporter {
|
|
|
76
82
|
/**
|
|
77
83
|
* Collects model rows with deterministic generated ids.
|
|
78
84
|
* @param {object[]} bundles Normalized bundles.
|
|
79
|
-
* @returns {{ model: object, id: string, checksum: number }[]}
|
|
85
|
+
* @returns {{ bundle: object, model: object, id: string, checksum: number }[]}
|
|
80
86
|
*/
|
|
81
87
|
static #collectModels(bundles) {
|
|
82
88
|
return bundles.flatMap((bundle, bundleIndex) =>
|
|
83
89
|
bundle.models.map((model, modelIndex) => ({
|
|
90
|
+
bundle,
|
|
84
91
|
model,
|
|
85
92
|
id: 'model-' + bundleIndex + '-' + modelIndex,
|
|
86
93
|
checksum: AltiumLibraryRecordBuilder.checksumBytes(model.bytes)
|
|
@@ -20,23 +20,128 @@ export class PcbComponentBodyPlacementNormalizer {
|
|
|
20
20
|
const mirrorY = (value) =>
|
|
21
21
|
Number(boardOutline?.minY || 0) + maxY - Number(value || 0)
|
|
22
22
|
|
|
23
|
-
return componentBodies.map((componentBody) =>
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
23
|
+
return componentBodies.map((componentBody) => {
|
|
24
|
+
const normalizedBody = {
|
|
25
|
+
...componentBody,
|
|
26
|
+
positionMil: {
|
|
27
|
+
x: Number(componentBody.positionMil?.x || 0),
|
|
28
|
+
y: mirrorY(componentBody.positionMil?.y || 0)
|
|
29
|
+
},
|
|
30
|
+
rotationDeg:
|
|
31
|
+
PcbComponentBodyPlacementNormalizer.#normalizeAngle(
|
|
32
|
+
360 - Number(componentBody.rotationDeg || 0)
|
|
33
|
+
),
|
|
34
|
+
modelRotationDeg: {
|
|
35
|
+
x: Number(componentBody.modelRotationDeg?.x || 0),
|
|
36
|
+
y: Number(componentBody.modelRotationDeg?.y || 0),
|
|
37
|
+
z: PcbComponentBodyPlacementNormalizer.#normalizeAngle(
|
|
38
|
+
360 - Number(componentBody.modelRotationDeg?.z || 0)
|
|
39
|
+
)
|
|
40
|
+
}
|
|
38
41
|
}
|
|
42
|
+
const staticGeometry =
|
|
43
|
+
PcbComponentBodyPlacementNormalizer.#normalizeStaticGeometry(
|
|
44
|
+
componentBody,
|
|
45
|
+
mirrorY
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
return staticGeometry
|
|
49
|
+
? {
|
|
50
|
+
...normalizedBody,
|
|
51
|
+
staticGeometry
|
|
52
|
+
}
|
|
53
|
+
: normalizedBody
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Mirrors source-coordinate static geometry into the viewer coordinate
|
|
59
|
+
* system.
|
|
60
|
+
* @param {{ positionMil?: { x?: number, y?: number }, staticGeometry?: object }} componentBody Component body.
|
|
61
|
+
* @param {(value: number | string | undefined) => number} mirrorY Source Y mirror.
|
|
62
|
+
* @returns {object | undefined}
|
|
63
|
+
*/
|
|
64
|
+
static #normalizeStaticGeometry(componentBody, mirrorY) {
|
|
65
|
+
const staticGeometry = componentBody?.staticGeometry
|
|
66
|
+
if (!staticGeometry || typeof staticGeometry !== 'object') {
|
|
67
|
+
return staticGeometry
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const vertices = Array.isArray(staticGeometry.verticesMil)
|
|
71
|
+
? staticGeometry.verticesMil
|
|
72
|
+
: []
|
|
73
|
+
if (
|
|
74
|
+
!PcbComponentBodyPlacementNormalizer.#shouldMirrorStaticVertices(
|
|
75
|
+
componentBody,
|
|
76
|
+
vertices
|
|
77
|
+
)
|
|
78
|
+
) {
|
|
79
|
+
return staticGeometry
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
...staticGeometry,
|
|
84
|
+
verticesMil: vertices.map((vertex) => ({
|
|
85
|
+
...vertex,
|
|
86
|
+
x: Number(vertex?.x || 0),
|
|
87
|
+
y: mirrorY(vertex?.y || 0)
|
|
88
|
+
}))
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Checks whether polygon vertices share the body source-coordinate frame.
|
|
94
|
+
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Component body.
|
|
95
|
+
* @param {{ x?: number, y?: number }[]} vertices Static polygon vertices.
|
|
96
|
+
* @returns {boolean}
|
|
97
|
+
*/
|
|
98
|
+
static #shouldMirrorStaticVertices(componentBody, vertices) {
|
|
99
|
+
if (vertices.length < 3) {
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const center =
|
|
104
|
+
PcbComponentBodyPlacementNormalizer.#polygonBoundsCenter(vertices)
|
|
105
|
+
const source = {
|
|
106
|
+
x: Number(componentBody?.positionMil?.x || 0),
|
|
107
|
+
y: Number(componentBody?.positionMil?.y || 0)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
PcbComponentBodyPlacementNormalizer.#distance(center, source) <= 5
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Resolves an axis-aligned polygon bounds center.
|
|
117
|
+
* @param {{ x?: number, y?: number }[]} vertices Static polygon vertices.
|
|
118
|
+
* @returns {{ x: number, y: number }}
|
|
119
|
+
*/
|
|
120
|
+
static #polygonBoundsCenter(vertices) {
|
|
121
|
+
const points = vertices.map((vertex) => ({
|
|
122
|
+
x: Number(vertex?.x || 0),
|
|
123
|
+
y: Number(vertex?.y || 0)
|
|
39
124
|
}))
|
|
125
|
+
const xs = points.map((point) => point.x)
|
|
126
|
+
const ys = points.map((point) => point.y)
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
x: (Math.min(...xs) + Math.max(...xs)) / 2,
|
|
130
|
+
y: (Math.min(...ys) + Math.max(...ys)) / 2
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Measures XY distance between two points.
|
|
136
|
+
* @param {{ x?: number, y?: number }} first First point.
|
|
137
|
+
* @param {{ x?: number, y?: number }} second Second point.
|
|
138
|
+
* @returns {number}
|
|
139
|
+
*/
|
|
140
|
+
static #distance(first, second) {
|
|
141
|
+
return Math.hypot(
|
|
142
|
+
Number(first?.x || 0) - Number(second?.x || 0),
|
|
143
|
+
Number(first?.y || 0) - Number(second?.y || 0)
|
|
144
|
+
)
|
|
40
145
|
}
|
|
41
146
|
|
|
42
147
|
/**
|
|
@@ -419,12 +419,23 @@ export class PcbEmbeddedModelExtractor {
|
|
|
419
419
|
? undefined
|
|
420
420
|
: PcbEmbeddedModelExtractor.#decodeBodyColor(bodyColorRaw),
|
|
421
421
|
bodyOpacity:
|
|
422
|
-
|
|
423
|
-
? undefined
|
|
424
|
-
: bodyOpacity
|
|
422
|
+
PcbEmbeddedModelExtractor.#normalizeBodyOpacity(bodyOpacity)
|
|
425
423
|
})
|
|
426
424
|
}
|
|
427
425
|
|
|
426
|
+
/**
|
|
427
|
+
* Normalizes native body opacity metadata to authored translucency only.
|
|
428
|
+
* @param {number | null} bodyOpacity Native body opacity value.
|
|
429
|
+
* @returns {number | undefined}
|
|
430
|
+
*/
|
|
431
|
+
static #normalizeBodyOpacity(bodyOpacity) {
|
|
432
|
+
if (!Number.isFinite(bodyOpacity) || Number(bodyOpacity) <= 0) {
|
|
433
|
+
return undefined
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return bodyOpacity
|
|
437
|
+
}
|
|
438
|
+
|
|
428
439
|
/**
|
|
429
440
|
* Maps a numeric body model type to a stable semantic label.
|
|
430
441
|
* @param {number} modelType Numeric model type.
|
|
@@ -498,7 +509,14 @@ export class PcbEmbeddedModelExtractor {
|
|
|
498
509
|
componentBody.dzMil
|
|
499
510
|
].join('\u0000')
|
|
500
511
|
|
|
501
|
-
|
|
512
|
+
const existingBody = uniqueBodies.get(key)
|
|
513
|
+
if (
|
|
514
|
+
!existingBody ||
|
|
515
|
+
PcbEmbeddedModelExtractor.#shouldPreferComponentBody(
|
|
516
|
+
componentBody,
|
|
517
|
+
existingBody
|
|
518
|
+
)
|
|
519
|
+
) {
|
|
502
520
|
uniqueBodies.set(key, componentBody)
|
|
503
521
|
}
|
|
504
522
|
}
|
|
@@ -506,6 +524,36 @@ export class PcbEmbeddedModelExtractor {
|
|
|
506
524
|
return [...uniqueBodies.values()]
|
|
507
525
|
}
|
|
508
526
|
|
|
527
|
+
/**
|
|
528
|
+
* Checks whether one component-body row carries better static geometry.
|
|
529
|
+
* @param {{ staticGeometry?: object }} candidate Candidate body row.
|
|
530
|
+
* @param {{ staticGeometry?: object }} existing Existing body row.
|
|
531
|
+
* @returns {boolean}
|
|
532
|
+
*/
|
|
533
|
+
static #shouldPreferComponentBody(candidate, existing) {
|
|
534
|
+
return (
|
|
535
|
+
PcbEmbeddedModelExtractor.#staticGeometryRank(candidate) >
|
|
536
|
+
PcbEmbeddedModelExtractor.#staticGeometryRank(existing)
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Ranks static geometry completeness for duplicate body rows.
|
|
542
|
+
* @param {{ staticGeometry?: object }} componentBody Component body row.
|
|
543
|
+
* @returns {number}
|
|
544
|
+
*/
|
|
545
|
+
static #staticGeometryRank(componentBody) {
|
|
546
|
+
if (componentBody?.staticGeometry?.status === 'complete') {
|
|
547
|
+
return 2
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (componentBody?.staticGeometry) {
|
|
551
|
+
return 1
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
return 0
|
|
555
|
+
}
|
|
556
|
+
|
|
509
557
|
/**
|
|
510
558
|
* Builds model metadata and payload integrity diagnostics.
|
|
511
559
|
* @param {object[]} metadataRows Parsed model metadata rows.
|