altium-toolkit 1.1.36 → 1.1.37
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 +15 -0
- package/package.json +1 -1
- package/src/ui/AltiumScene3dAuthoredBodyAnchorAdapter.mjs +245 -12
- package/src/ui/AltiumScene3dAuthoredConnectorYawPolicy.mjs +176 -0
- package/src/ui/AltiumScene3dBottomSourceHalfTurnPolicy.mjs +93 -0
- package/src/ui/AltiumScene3dDisplayModuleYawPolicy.mjs +284 -0
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +521 -23
- package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +766 -19
- package/src/ui/AltiumScene3dQfnFootprintDetector.mjs +307 -0
- package/src/ui/AltiumScene3dRepeatedFullFootprintBodyCollapse.mjs +518 -0
- package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +121 -17
- package/src/ui/AltiumScene3dShapeStackOwnerAdapter.mjs +180 -158
- package/src/ui/AltiumScene3dShapeStackOwnerConflictPolicy.mjs +299 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +22 -0
- package/src/ui/PcbScene3dBuilder.mjs +613 -117
- package/src/ui/PcbScene3dModelRegistry.mjs +222 -10
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +262 -22
- package/src/ui/PcbScene3dStaticBodyOwnerPromotion.mjs +87 -2
- package/src/ui/PcbScene3dStaticBodyPadOwnerPromotion.mjs +378 -0
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +32 -16
- package/src/ui/PcbScene3dStaticBodySymmetricOwnerPromotion.mjs +1 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { AltiumScene3dIdentityTokens } from './AltiumScene3dIdentityTokens.mjs'
|
|
2
|
+
import { AltiumScene3dAuthoredConnectorYawPolicy } from './AltiumScene3dAuthoredConnectorYawPolicy.mjs'
|
|
2
3
|
import { AltiumScene3dPlacementRotationPolicy } from './AltiumScene3dPlacementRotationPolicy.mjs'
|
|
3
4
|
import { AltiumScene3dRepeatedModelOwnerRepair } from './AltiumScene3dRepeatedModelOwnerRepair.mjs'
|
|
5
|
+
import { AltiumScene3dBottomSourceHalfTurnPolicy } from './AltiumScene3dBottomSourceHalfTurnPolicy.mjs'
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* Repairs Altium explicit 3D body placements after toolkit scene conversion.
|
|
@@ -10,6 +12,8 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
10
12
|
static #NEAR_ANCHOR_TOLERANCE_MIL = 20
|
|
11
13
|
static #FAR_OWNER_DISTANCE_MIL = 100
|
|
12
14
|
static #MODEL_ANCHOR_NEAR_OWNER_TOLERANCE_MIL = 35
|
|
15
|
+
static #MODEL_ANCHOR_PAD_PROXIMITY_TOLERANCE_MIL = 10
|
|
16
|
+
static #MODEL_ANCHOR_METADATA_PROXIMITY_MIL = 350
|
|
13
17
|
static #DEFAULT_BOARD_THICKNESS_MIL = 63
|
|
14
18
|
static #PASSIVE_BODY_PATTERN =
|
|
15
19
|
/(?:^|[^a-z0-9])(?:cap|capacitor|res|resistor|ind|inductor|ferrite|bead|crystal|xtal|lqw|lqg)(?:$|[^a-z0-9])/i
|
|
@@ -111,6 +115,15 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
111
115
|
currentComponent
|
|
112
116
|
)
|
|
113
117
|
: Number.POSITIVE_INFINITY
|
|
118
|
+
const currentHasOwnedDrilledPadAnchor = currentComponent
|
|
119
|
+
? AltiumScene3dExternalPlacementAdapter.#distanceToNearestOwnedPad(
|
|
120
|
+
placement,
|
|
121
|
+
currentComponent,
|
|
122
|
+
pads
|
|
123
|
+
) <=
|
|
124
|
+
AltiumScene3dExternalPlacementAdapter
|
|
125
|
+
.#MODEL_ANCHOR_PAD_PROXIMITY_TOLERANCE_MIL
|
|
126
|
+
: false
|
|
114
127
|
const currentHasMetadataAffinity = currentComponent
|
|
115
128
|
? AltiumScene3dExternalPlacementAdapter.#hasMetadataAffinity(
|
|
116
129
|
placement,
|
|
@@ -140,13 +153,15 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
140
153
|
components,
|
|
141
154
|
currentHasMetadataAffinity,
|
|
142
155
|
currentHasPartCodeAffinity,
|
|
143
|
-
componentBody
|
|
156
|
+
componentBody,
|
|
157
|
+
pads
|
|
144
158
|
)
|
|
145
159
|
const isExactAnchoredOwner =
|
|
146
160
|
(currentComponent &&
|
|
147
161
|
currentDistance <=
|
|
148
162
|
AltiumScene3dExternalPlacementAdapter
|
|
149
163
|
.#EXACT_ANCHOR_TOLERANCE_MIL) ||
|
|
164
|
+
currentHasOwnedDrilledPadAnchor ||
|
|
150
165
|
(exactComponent &&
|
|
151
166
|
AltiumScene3dExternalPlacementAdapter.#distanceToBody(
|
|
152
167
|
placement,
|
|
@@ -156,6 +171,7 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
156
171
|
.#EXACT_ANCHOR_TOLERANCE_MIL)
|
|
157
172
|
const isFarCurrentOwner =
|
|
158
173
|
currentComponent &&
|
|
174
|
+
!currentHasOwnedDrilledPadAnchor &&
|
|
159
175
|
currentDistance >
|
|
160
176
|
AltiumScene3dExternalPlacementAdapter.#FAR_OWNER_DISTANCE_MIL
|
|
161
177
|
const metadataComponent =
|
|
@@ -228,14 +244,24 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
228
244
|
: placement.modelTransform
|
|
229
245
|
}
|
|
230
246
|
: placement
|
|
247
|
+
const shouldPreserveAuthoredConnectorYaw =
|
|
248
|
+
AltiumScene3dAuthoredConnectorYawPolicy.shouldPreserve({
|
|
249
|
+
placement: nextPlacement,
|
|
250
|
+
component: resolvedComponent,
|
|
251
|
+
pads,
|
|
252
|
+
ownerOffsetToleranceMil:
|
|
253
|
+
AltiumScene3dExternalPlacementAdapter
|
|
254
|
+
.#MODEL_ANCHOR_NEAR_OWNER_TOLERANCE_MIL
|
|
255
|
+
})
|
|
231
256
|
const shouldUseComponentYaw =
|
|
232
|
-
|
|
233
|
-
Boolean(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
257
|
+
!shouldPreserveAuthoredConnectorYaw &&
|
|
258
|
+
(Boolean(metadataComponent && !exactComponent) ||
|
|
259
|
+
Boolean(
|
|
260
|
+
!exactComponent &&
|
|
261
|
+
isFarCurrentOwner &&
|
|
262
|
+
currentHasMetadataAffinity &&
|
|
263
|
+
!currentIsMechanicalOwner
|
|
264
|
+
))
|
|
239
265
|
const rotationContext = {
|
|
240
266
|
placement: nextPlacement,
|
|
241
267
|
component: resolvedComponent,
|
|
@@ -257,7 +283,11 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
257
283
|
AltiumScene3dPlacementRotationPolicy.shouldCorrectYaw(
|
|
258
284
|
rotationContext
|
|
259
285
|
),
|
|
260
|
-
footprintYaw
|
|
286
|
+
footprintYaw,
|
|
287
|
+
AltiumScene3dPlacementRotationPolicy.shouldMirrorSourceZ(
|
|
288
|
+
rotationContext
|
|
289
|
+
),
|
|
290
|
+
pads
|
|
261
291
|
)
|
|
262
292
|
|
|
263
293
|
return AltiumScene3dExternalPlacementAdapter.#withContactPadHints(
|
|
@@ -372,6 +402,41 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
372
402
|
)
|
|
373
403
|
}
|
|
374
404
|
|
|
405
|
+
/**
|
|
406
|
+
* Checks whether a component owns drilled or slotted pads.
|
|
407
|
+
* @param {object | null | undefined} component Owning component.
|
|
408
|
+
* @param {object[]} pads Source PCB pads.
|
|
409
|
+
* @returns {boolean}
|
|
410
|
+
*/
|
|
411
|
+
static #componentHasThroughHolePads(component, pads) {
|
|
412
|
+
return AltiumScene3dExternalPlacementAdapter.#componentPads(
|
|
413
|
+
component,
|
|
414
|
+
pads
|
|
415
|
+
).some((pad) =>
|
|
416
|
+
AltiumScene3dExternalPlacementAdapter.#hasDrilledPadOpening(pad)
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Checks whether one pad contains a drilled or slotted board opening.
|
|
422
|
+
* @param {object} pad Source PCB pad.
|
|
423
|
+
* @returns {boolean}
|
|
424
|
+
*/
|
|
425
|
+
static #hasDrilledPadOpening(pad) {
|
|
426
|
+
const holeGeometry = pad?.holeGeometry || {}
|
|
427
|
+
|
|
428
|
+
return [
|
|
429
|
+
pad?.holeDiameter,
|
|
430
|
+
pad?.drillDiameter,
|
|
431
|
+
pad?.holeSize,
|
|
432
|
+
pad?.holeSlotLength,
|
|
433
|
+
pad?.slotLength,
|
|
434
|
+
holeGeometry?.diameter,
|
|
435
|
+
holeGeometry?.length,
|
|
436
|
+
holeGeometry?.slotLength
|
|
437
|
+
].some((value) => Number(value || 0) > 0)
|
|
438
|
+
}
|
|
439
|
+
|
|
375
440
|
/**
|
|
376
441
|
* Checks whether a pad exposes top paste and should be soldered on the
|
|
377
442
|
* top face.
|
|
@@ -408,6 +473,7 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
408
473
|
* @param {boolean} currentHasMetadataAffinity Whether source metadata confirms the current owner.
|
|
409
474
|
* @param {boolean} currentHasPartCodeAffinity Whether a strong part code confirms the current owner.
|
|
410
475
|
* @param {object | null} componentBody Source component body.
|
|
476
|
+
* @param {object[]} pads Source PCB pads.
|
|
411
477
|
* @returns {object | null}
|
|
412
478
|
*/
|
|
413
479
|
static #resolveAnchorComponent(
|
|
@@ -416,7 +482,8 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
416
482
|
components,
|
|
417
483
|
currentHasMetadataAffinity,
|
|
418
484
|
currentHasPartCodeAffinity,
|
|
419
|
-
componentBody
|
|
485
|
+
componentBody,
|
|
486
|
+
pads
|
|
420
487
|
) {
|
|
421
488
|
const currentDistance = currentComponent
|
|
422
489
|
? AltiumScene3dExternalPlacementAdapter.#distanceToBody(
|
|
@@ -479,12 +546,14 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
479
546
|
return AltiumScene3dExternalPlacementAdapter.#nearestModelAnchorOwner(
|
|
480
547
|
placement,
|
|
481
548
|
componentBody,
|
|
482
|
-
components
|
|
549
|
+
components,
|
|
550
|
+
pads
|
|
483
551
|
)
|
|
484
552
|
}
|
|
485
553
|
|
|
486
|
-
return AltiumScene3dExternalPlacementAdapter.#
|
|
554
|
+
return AltiumScene3dExternalPlacementAdapter.#nearestCompatibleAnchorComponent(
|
|
487
555
|
placement,
|
|
556
|
+
componentBody,
|
|
488
557
|
components,
|
|
489
558
|
AltiumScene3dExternalPlacementAdapter.#NEAR_ANCHOR_TOLERANCE_MIL
|
|
490
559
|
)
|
|
@@ -495,22 +564,37 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
495
564
|
* @param {object} placement External model placement.
|
|
496
565
|
* @param {object | null} componentBody Source component body.
|
|
497
566
|
* @param {object[]} components PCB components.
|
|
567
|
+
* @param {object[]} pads Source PCB pads.
|
|
498
568
|
* @returns {object | null}
|
|
499
569
|
*/
|
|
500
|
-
static #nearestModelAnchorOwner(
|
|
570
|
+
static #nearestModelAnchorOwner(
|
|
571
|
+
placement,
|
|
572
|
+
componentBody,
|
|
573
|
+
components,
|
|
574
|
+
pads
|
|
575
|
+
) {
|
|
501
576
|
const candidates = components
|
|
502
577
|
.map((component) => ({
|
|
503
578
|
component,
|
|
504
579
|
distance: AltiumScene3dExternalPlacementAdapter.#distanceToBody(
|
|
505
580
|
placement,
|
|
506
581
|
component
|
|
507
|
-
)
|
|
582
|
+
),
|
|
583
|
+
padDistance:
|
|
584
|
+
AltiumScene3dExternalPlacementAdapter.#distanceToNearestOwnedPad(
|
|
585
|
+
placement,
|
|
586
|
+
component,
|
|
587
|
+
pads
|
|
588
|
+
)
|
|
508
589
|
}))
|
|
509
590
|
.filter(
|
|
510
591
|
(candidate) =>
|
|
511
|
-
candidate.distance <=
|
|
592
|
+
(candidate.distance <=
|
|
512
593
|
AltiumScene3dExternalPlacementAdapter
|
|
513
|
-
.#MODEL_ANCHOR_NEAR_OWNER_TOLERANCE_MIL
|
|
594
|
+
.#MODEL_ANCHOR_NEAR_OWNER_TOLERANCE_MIL ||
|
|
595
|
+
candidate.padDistance <=
|
|
596
|
+
AltiumScene3dExternalPlacementAdapter
|
|
597
|
+
.#MODEL_ANCHOR_PAD_PROXIMITY_TOLERANCE_MIL) &&
|
|
514
598
|
AltiumScene3dExternalPlacementAdapter.#hasModelAnchorOwnerAffinity(
|
|
515
599
|
placement,
|
|
516
600
|
componentBody,
|
|
@@ -522,6 +606,71 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
522
606
|
return candidates[0]?.component || null
|
|
523
607
|
}
|
|
524
608
|
|
|
609
|
+
/**
|
|
610
|
+
* Measures how far one model anchor sits outside pads owned by a component.
|
|
611
|
+
* @param {object} placement External model placement.
|
|
612
|
+
* @param {object} component Candidate owner component.
|
|
613
|
+
* @param {object[]} pads Source PCB pads.
|
|
614
|
+
* @returns {number}
|
|
615
|
+
*/
|
|
616
|
+
static #distanceToNearestOwnedPad(placement, component, pads) {
|
|
617
|
+
const ownedPads = AltiumScene3dExternalPlacementAdapter.#componentPads(
|
|
618
|
+
component,
|
|
619
|
+
pads
|
|
620
|
+
).filter((pad) =>
|
|
621
|
+
AltiumScene3dExternalPlacementAdapter.#hasDrilledPadOpening(pad)
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
if (!ownedPads.length) {
|
|
625
|
+
return Number.POSITIVE_INFINITY
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return Math.min(
|
|
629
|
+
...ownedPads.map((pad) => {
|
|
630
|
+
const distance =
|
|
631
|
+
AltiumScene3dExternalPlacementAdapter.#distanceBetweenPoints(
|
|
632
|
+
placement?.bodyPositionMil,
|
|
633
|
+
{ x: pad?.x, y: pad?.y }
|
|
634
|
+
)
|
|
635
|
+
const radius =
|
|
636
|
+
AltiumScene3dExternalPlacementAdapter.#padAnchorRadiusMil(
|
|
637
|
+
pad
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
return radius > 0
|
|
641
|
+
? Math.max(0, distance - radius)
|
|
642
|
+
: Number.POSITIVE_INFINITY
|
|
643
|
+
})
|
|
644
|
+
)
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
/**
|
|
648
|
+
* Resolves the effective XY radius around a drilled pad center.
|
|
649
|
+
* @param {object} pad Source PCB pad.
|
|
650
|
+
* @returns {number}
|
|
651
|
+
*/
|
|
652
|
+
static #padAnchorRadiusMil(pad) {
|
|
653
|
+
const holeGeometry = pad?.holeGeometry || {}
|
|
654
|
+
const diameter = Math.max(
|
|
655
|
+
Number(pad?.sizeTopX || 0),
|
|
656
|
+
Number(pad?.sizeTopY || 0),
|
|
657
|
+
Number(pad?.sizeMidX || 0),
|
|
658
|
+
Number(pad?.sizeMidY || 0),
|
|
659
|
+
Number(pad?.sizeBottomX || 0),
|
|
660
|
+
Number(pad?.sizeBottomY || 0),
|
|
661
|
+
Number(pad?.holeDiameter || 0),
|
|
662
|
+
Number(pad?.drillDiameter || 0),
|
|
663
|
+
Number(pad?.holeSize || 0),
|
|
664
|
+
Number(pad?.holeSlotLength || 0),
|
|
665
|
+
Number(pad?.slotLength || 0),
|
|
666
|
+
Number(holeGeometry?.diameter || 0),
|
|
667
|
+
Number(holeGeometry?.length || 0),
|
|
668
|
+
Number(holeGeometry?.slotLength || 0)
|
|
669
|
+
)
|
|
670
|
+
|
|
671
|
+
return Number.isFinite(diameter) && diameter > 0 ? diameter / 2 : 0
|
|
672
|
+
}
|
|
673
|
+
|
|
525
674
|
/**
|
|
526
675
|
* Checks whether a resolved model-anchor fallback should be centered on
|
|
527
676
|
* its nearby compatible owner.
|
|
@@ -537,10 +686,22 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
537
686
|
componentBody,
|
|
538
687
|
pads
|
|
539
688
|
) {
|
|
689
|
+
const projectionSource = String(placement?.projection?.source || '')
|
|
540
690
|
if (
|
|
541
691
|
!component ||
|
|
542
|
-
|
|
543
|
-
'model-
|
|
692
|
+
(projectionSource !== 'model-anchor-fallback' &&
|
|
693
|
+
projectionSource !== 'model-bounds')
|
|
694
|
+
) {
|
|
695
|
+
return false
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
if (
|
|
699
|
+
AltiumScene3dExternalPlacementAdapter.#distanceToNearestOwnedPad(
|
|
700
|
+
placement,
|
|
701
|
+
component,
|
|
702
|
+
pads
|
|
703
|
+
) <=
|
|
704
|
+
AltiumScene3dExternalPlacementAdapter.#EXACT_ANCHOR_TOLERANCE_MIL
|
|
544
705
|
) {
|
|
545
706
|
return false
|
|
546
707
|
}
|
|
@@ -562,11 +723,16 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
562
723
|
}
|
|
563
724
|
|
|
564
725
|
return (
|
|
565
|
-
AltiumScene3dExternalPlacementAdapter.#hasPartCodeAffinity(
|
|
726
|
+
(AltiumScene3dExternalPlacementAdapter.#hasPartCodeAffinity(
|
|
566
727
|
placement,
|
|
567
728
|
componentBody,
|
|
568
729
|
component
|
|
569
|
-
)
|
|
730
|
+
) ||
|
|
731
|
+
AltiumScene3dExternalPlacementAdapter.#hasModelAnchorOwnerAffinity(
|
|
732
|
+
placement,
|
|
733
|
+
componentBody,
|
|
734
|
+
component
|
|
735
|
+
)) &&
|
|
570
736
|
AltiumScene3dExternalPlacementAdapter.#hasModelAnchorOwnerComponentAffinity(
|
|
571
737
|
component
|
|
572
738
|
) &&
|
|
@@ -722,13 +888,19 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
722
888
|
placement,
|
|
723
889
|
{ x: offsetX, y: offsetY }
|
|
724
890
|
)
|
|
891
|
+
const offsetZ = Number(
|
|
892
|
+
modelTransform?.offsetMil?.z ?? modelTransform?.dzMil ?? 0
|
|
893
|
+
)
|
|
894
|
+
const renderableZ = Number.isFinite(offsetZ) ? offsetZ : 0
|
|
725
895
|
|
|
726
896
|
return {
|
|
727
897
|
...(modelTransform || {}),
|
|
898
|
+
dzMil: renderableZ,
|
|
728
899
|
offsetMil: {
|
|
729
900
|
...(modelTransform?.offsetMil || {}),
|
|
730
901
|
x: renderableOffset.x,
|
|
731
|
-
y: renderableOffset.y
|
|
902
|
+
y: renderableOffset.y,
|
|
903
|
+
z: renderableZ
|
|
732
904
|
},
|
|
733
905
|
ownerAnchorOffsetMil: {
|
|
734
906
|
x: offsetX,
|
|
@@ -790,6 +962,8 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
790
962
|
* @param {boolean} useComponentYaw Whether component yaw should override body yaw.
|
|
791
963
|
* @param {boolean} correctPinOneYaw Whether a square IC pin-one correction applies.
|
|
792
964
|
* @param {number | null} footprintYaw Footprint-derived yaw when available.
|
|
965
|
+
* @param {boolean} mirrorSourceZ Whether the model source length should be mirrored.
|
|
966
|
+
* @param {object[]} pads Source PCB pads.
|
|
793
967
|
* @returns {object}
|
|
794
968
|
*/
|
|
795
969
|
static #repairRotation(
|
|
@@ -798,9 +972,11 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
798
972
|
componentBody,
|
|
799
973
|
useComponentYaw,
|
|
800
974
|
correctPinOneYaw,
|
|
801
|
-
footprintYaw
|
|
975
|
+
footprintYaw,
|
|
976
|
+
mirrorSourceZ,
|
|
977
|
+
pads
|
|
802
978
|
) {
|
|
803
|
-
const
|
|
979
|
+
const baseModelTransform =
|
|
804
980
|
AltiumScene3dExternalPlacementAdapter.#repairModelTransform(
|
|
805
981
|
placement?.modelTransform,
|
|
806
982
|
componentBody
|
|
@@ -835,6 +1011,28 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
835
1011
|
baseRotation + 180
|
|
836
1012
|
)
|
|
837
1013
|
: baseRotation
|
|
1014
|
+
const yawAdjustedModelTransform =
|
|
1015
|
+
AltiumScene3dExternalPlacementAdapter.#withFootprintRelativeModelYaw(
|
|
1016
|
+
baseModelTransform,
|
|
1017
|
+
placement,
|
|
1018
|
+
componentBody,
|
|
1019
|
+
hasFootprintYaw && !isGenericPassiveBody,
|
|
1020
|
+
rotationDeg
|
|
1021
|
+
)
|
|
1022
|
+
const mirroredModelTransform =
|
|
1023
|
+
mirrorSourceZ && !isGenericPassiveBody
|
|
1024
|
+
? AltiumScene3dExternalPlacementAdapter.#withMirroredSourceZScale(
|
|
1025
|
+
yawAdjustedModelTransform
|
|
1026
|
+
)
|
|
1027
|
+
: yawAdjustedModelTransform
|
|
1028
|
+
const modelTransform =
|
|
1029
|
+
AltiumScene3dExternalPlacementAdapter.#normalizeBottomSurfaceMountHalfTurn(
|
|
1030
|
+
mirroredModelTransform,
|
|
1031
|
+
placement,
|
|
1032
|
+
component,
|
|
1033
|
+
componentBody,
|
|
1034
|
+
pads
|
|
1035
|
+
)
|
|
838
1036
|
|
|
839
1037
|
const repairedPlacement = {
|
|
840
1038
|
...placement,
|
|
@@ -854,6 +1052,143 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
854
1052
|
: repairedPlacement
|
|
855
1053
|
}
|
|
856
1054
|
|
|
1055
|
+
/**
|
|
1056
|
+
* Preserves authored embedded-model yaw relative to a footprint yaw
|
|
1057
|
+
* override by moving the signed yaw delta into the model-local transform.
|
|
1058
|
+
* @param {object | null | undefined} modelTransform Placement transform.
|
|
1059
|
+
* @param {object | null | undefined} placement External placement.
|
|
1060
|
+
* @param {object | null | undefined} componentBody Source component body.
|
|
1061
|
+
* @param {boolean} enabled Whether footprint yaw overrode body yaw.
|
|
1062
|
+
* @param {number} placementYawDeg Final board-facing placement yaw.
|
|
1063
|
+
* @returns {object | null | undefined}
|
|
1064
|
+
*/
|
|
1065
|
+
static #withFootprintRelativeModelYaw(
|
|
1066
|
+
modelTransform,
|
|
1067
|
+
placement,
|
|
1068
|
+
componentBody,
|
|
1069
|
+
enabled,
|
|
1070
|
+
placementYawDeg
|
|
1071
|
+
) {
|
|
1072
|
+
const sourceYaw = Number(
|
|
1073
|
+
componentBody?.modelRotationDeg?.z ?? placement?.rotationDeg
|
|
1074
|
+
)
|
|
1075
|
+
if (!enabled || !Number.isFinite(sourceYaw)) {
|
|
1076
|
+
return modelTransform
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
const rotationDeg = modelTransform?.rotationDeg || {}
|
|
1080
|
+
const localYaw =
|
|
1081
|
+
AltiumScene3dExternalPlacementAdapter.#resolveFootprintRelativeModelYaw(
|
|
1082
|
+
placement,
|
|
1083
|
+
sourceYaw,
|
|
1084
|
+
placementYawDeg
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
return {
|
|
1088
|
+
...(modelTransform || {}),
|
|
1089
|
+
rotationDeg: {
|
|
1090
|
+
...rotationDeg,
|
|
1091
|
+
z: localYaw
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/**
|
|
1097
|
+
* Resolves model-local yaw after a footprint yaw override. Model-bounds
|
|
1098
|
+
* placements retain full source geometry, so their source yaw delta uses
|
|
1099
|
+
* the opposite sign from pad-fallback bodies that were synthesized from
|
|
1100
|
+
* footprint pads.
|
|
1101
|
+
* @param {object | null | undefined} placement External placement.
|
|
1102
|
+
* @param {number} sourceYaw Source STEP yaw.
|
|
1103
|
+
* @param {number} placementYawDeg Board-facing placement yaw.
|
|
1104
|
+
* @returns {number}
|
|
1105
|
+
*/
|
|
1106
|
+
static #resolveFootprintRelativeModelYaw(
|
|
1107
|
+
placement,
|
|
1108
|
+
sourceYaw,
|
|
1109
|
+
placementYawDeg
|
|
1110
|
+
) {
|
|
1111
|
+
const projectionSource = String(
|
|
1112
|
+
placement?.projection?.source || ''
|
|
1113
|
+
).toLowerCase()
|
|
1114
|
+
const delta =
|
|
1115
|
+
projectionSource === 'model-bounds'
|
|
1116
|
+
? sourceYaw - Number(placementYawDeg || 0)
|
|
1117
|
+
: Number(placementYawDeg || 0) - sourceYaw
|
|
1118
|
+
|
|
1119
|
+
return AltiumScene3dExternalPlacementAdapter.#normalizeSignedAngle(
|
|
1120
|
+
delta
|
|
1121
|
+
)
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
/**
|
|
1125
|
+
* Mirrors a source model along its local Z axis while preserving any
|
|
1126
|
+
* existing caller-provided scale on the other axes.
|
|
1127
|
+
* @param {object | null | undefined} modelTransform Placement transform.
|
|
1128
|
+
* @returns {object}
|
|
1129
|
+
*/
|
|
1130
|
+
static #withMirroredSourceZScale(modelTransform) {
|
|
1131
|
+
const scale = modelTransform?.scale || {}
|
|
1132
|
+
const zScale = Number(scale.z ?? 1)
|
|
1133
|
+
const mirroredZScale =
|
|
1134
|
+
Number.isFinite(zScale) && zScale !== 0 ? -Math.abs(zScale) : -1
|
|
1135
|
+
|
|
1136
|
+
return {
|
|
1137
|
+
...(modelTransform || {}),
|
|
1138
|
+
scale: {
|
|
1139
|
+
...scale,
|
|
1140
|
+
z: mirroredZScale
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
|
|
1145
|
+
/**
|
|
1146
|
+
* Clears bottom-side surface-mount half-turns that the mount rig already
|
|
1147
|
+
* supplies through its underside mirror.
|
|
1148
|
+
* @param {object | null | undefined} modelTransform Placement transform.
|
|
1149
|
+
* @param {object} placement External placement.
|
|
1150
|
+
* @param {object | null | undefined} component Owning component.
|
|
1151
|
+
* @param {object | null | undefined} componentBody Source component body.
|
|
1152
|
+
* @param {object[]} pads Source PCB pads.
|
|
1153
|
+
* @returns {object | null | undefined}
|
|
1154
|
+
*/
|
|
1155
|
+
static #normalizeBottomSurfaceMountHalfTurn(
|
|
1156
|
+
modelTransform,
|
|
1157
|
+
placement,
|
|
1158
|
+
component,
|
|
1159
|
+
componentBody,
|
|
1160
|
+
pads
|
|
1161
|
+
) {
|
|
1162
|
+
const rotation = modelTransform?.rotationDeg || {}
|
|
1163
|
+
if (
|
|
1164
|
+
String(placement?.mountSide || '').toLowerCase() !== 'bottom' ||
|
|
1165
|
+
!component ||
|
|
1166
|
+
AltiumScene3dExternalPlacementAdapter.#normalizeAngle(
|
|
1167
|
+
rotation.x
|
|
1168
|
+
) !== 180 ||
|
|
1169
|
+
AltiumScene3dBottomSourceHalfTurnPolicy.shouldPreserve({
|
|
1170
|
+
component,
|
|
1171
|
+
componentBody,
|
|
1172
|
+
placement,
|
|
1173
|
+
modelTransform
|
|
1174
|
+
}) ||
|
|
1175
|
+
AltiumScene3dExternalPlacementAdapter.#componentHasThroughHolePads(
|
|
1176
|
+
component,
|
|
1177
|
+
pads
|
|
1178
|
+
)
|
|
1179
|
+
) {
|
|
1180
|
+
return modelTransform
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
return {
|
|
1184
|
+
...(modelTransform || {}),
|
|
1185
|
+
rotationDeg: {
|
|
1186
|
+
...rotation,
|
|
1187
|
+
x: 0
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
857
1192
|
/**
|
|
858
1193
|
* Finds the nearest component whose anchor is effectively the body anchor.
|
|
859
1194
|
* @param {object} placement External model placement.
|
|
@@ -874,6 +1209,107 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
874
1209
|
return candidates[0]?.component || null
|
|
875
1210
|
}
|
|
876
1211
|
|
|
1212
|
+
/**
|
|
1213
|
+
* Finds the nearest component anchor that is compatible with a weak body
|
|
1214
|
+
* fallback assignment.
|
|
1215
|
+
* @param {object} placement External model placement.
|
|
1216
|
+
* @param {object | null} componentBody Source component body.
|
|
1217
|
+
* @param {object[]} components PCB components.
|
|
1218
|
+
* @param {number} toleranceMil Maximum XY body-to-component distance.
|
|
1219
|
+
* @returns {object | null}
|
|
1220
|
+
*/
|
|
1221
|
+
static #nearestCompatibleAnchorComponent(
|
|
1222
|
+
placement,
|
|
1223
|
+
componentBody,
|
|
1224
|
+
components,
|
|
1225
|
+
toleranceMil
|
|
1226
|
+
) {
|
|
1227
|
+
const candidates = components
|
|
1228
|
+
.map((component) => ({
|
|
1229
|
+
component,
|
|
1230
|
+
distance: AltiumScene3dExternalPlacementAdapter.#distanceToBody(
|
|
1231
|
+
placement,
|
|
1232
|
+
component
|
|
1233
|
+
)
|
|
1234
|
+
}))
|
|
1235
|
+
.filter(
|
|
1236
|
+
(candidate) =>
|
|
1237
|
+
candidate.distance <= toleranceMil &&
|
|
1238
|
+
AltiumScene3dExternalPlacementAdapter.#canUseNearAnchorComponent(
|
|
1239
|
+
placement,
|
|
1240
|
+
componentBody,
|
|
1241
|
+
candidate.component
|
|
1242
|
+
)
|
|
1243
|
+
)
|
|
1244
|
+
.sort((left, right) => left.distance - right.distance)
|
|
1245
|
+
|
|
1246
|
+
return candidates[0]?.component || null
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
/**
|
|
1250
|
+
* Checks whether a near-anchor owner is strong enough for a weak fallback.
|
|
1251
|
+
* @param {object} placement External model placement.
|
|
1252
|
+
* @param {object | null} componentBody Source component body.
|
|
1253
|
+
* @param {object} component PCB component.
|
|
1254
|
+
* @returns {boolean}
|
|
1255
|
+
*/
|
|
1256
|
+
static #canUseNearAnchorComponent(placement, componentBody, component) {
|
|
1257
|
+
if (
|
|
1258
|
+
!AltiumScene3dExternalPlacementAdapter.#hasPlacementComponentSideConflict(
|
|
1259
|
+
placement,
|
|
1260
|
+
component
|
|
1261
|
+
)
|
|
1262
|
+
) {
|
|
1263
|
+
return true
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
return (
|
|
1267
|
+
AltiumScene3dExternalPlacementAdapter.#hasMetadataAffinity(
|
|
1268
|
+
placement,
|
|
1269
|
+
componentBody,
|
|
1270
|
+
component
|
|
1271
|
+
) ||
|
|
1272
|
+
AltiumScene3dExternalPlacementAdapter.#hasPartCodeAffinity(
|
|
1273
|
+
placement,
|
|
1274
|
+
componentBody,
|
|
1275
|
+
component
|
|
1276
|
+
)
|
|
1277
|
+
)
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
/**
|
|
1281
|
+
* Checks whether a placement and candidate owner are on opposite board
|
|
1282
|
+
* faces.
|
|
1283
|
+
* @param {object} placement External model placement.
|
|
1284
|
+
* @param {object} component PCB component.
|
|
1285
|
+
* @returns {boolean}
|
|
1286
|
+
*/
|
|
1287
|
+
static #hasPlacementComponentSideConflict(placement, component) {
|
|
1288
|
+
const placementSide =
|
|
1289
|
+
AltiumScene3dExternalPlacementAdapter.#normalizeMountSide(
|
|
1290
|
+
placement?.mountSide
|
|
1291
|
+
)
|
|
1292
|
+
const componentSide =
|
|
1293
|
+
AltiumScene3dExternalPlacementAdapter.#resolveComponentMountSide(
|
|
1294
|
+
component
|
|
1295
|
+
)
|
|
1296
|
+
|
|
1297
|
+
return Boolean(
|
|
1298
|
+
placementSide && componentSide && placementSide !== componentSide
|
|
1299
|
+
)
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
/**
|
|
1303
|
+
* Normalizes external placement mount-side labels.
|
|
1304
|
+
* @param {unknown} value Source mount-side value.
|
|
1305
|
+
* @returns {'top' | 'bottom' | null}
|
|
1306
|
+
*/
|
|
1307
|
+
static #normalizeMountSide(value) {
|
|
1308
|
+
const side = String(value || '').toLowerCase()
|
|
1309
|
+
|
|
1310
|
+
return side === 'top' || side === 'bottom' ? side : null
|
|
1311
|
+
}
|
|
1312
|
+
|
|
877
1313
|
/**
|
|
878
1314
|
* Builds package metadata text for generic package-family checks.
|
|
879
1315
|
* @param {object} component PCB component.
|
|
@@ -941,9 +1377,59 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
941
1377
|
right.score - left.score || left.distance - right.distance
|
|
942
1378
|
)
|
|
943
1379
|
|
|
1380
|
+
const nearbyModelAnchorCandidate =
|
|
1381
|
+
AltiumScene3dExternalPlacementAdapter.#nearestModelAnchorMetadataCandidate(
|
|
1382
|
+
placement,
|
|
1383
|
+
componentBody,
|
|
1384
|
+
candidates
|
|
1385
|
+
)
|
|
1386
|
+
if (nearbyModelAnchorCandidate) {
|
|
1387
|
+
return nearbyModelAnchorCandidate
|
|
1388
|
+
}
|
|
1389
|
+
|
|
944
1390
|
return candidates[0]?.component || null
|
|
945
1391
|
}
|
|
946
1392
|
|
|
1393
|
+
/**
|
|
1394
|
+
* Resolves nearby model-anchor owners before stronger far metadata claims
|
|
1395
|
+
* steal repeated sub-bodies from their physical connector row.
|
|
1396
|
+
* @param {object} placement External model placement.
|
|
1397
|
+
* @param {object | null} componentBody Source component body.
|
|
1398
|
+
* @param {{ component: object, score: number, distance: number }[]} candidates Metadata candidates.
|
|
1399
|
+
* @returns {object | null}
|
|
1400
|
+
*/
|
|
1401
|
+
static #nearestModelAnchorMetadataCandidate(
|
|
1402
|
+
placement,
|
|
1403
|
+
componentBody,
|
|
1404
|
+
candidates
|
|
1405
|
+
) {
|
|
1406
|
+
if (
|
|
1407
|
+
String(placement?.projection?.source || '') !==
|
|
1408
|
+
'model-anchor-fallback'
|
|
1409
|
+
) {
|
|
1410
|
+
return null
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const nearbyCandidates = candidates
|
|
1414
|
+
.filter(
|
|
1415
|
+
(candidate) =>
|
|
1416
|
+
candidate.distance <=
|
|
1417
|
+
AltiumScene3dExternalPlacementAdapter
|
|
1418
|
+
.#MODEL_ANCHOR_METADATA_PROXIMITY_MIL &&
|
|
1419
|
+
AltiumScene3dExternalPlacementAdapter.#hasPartCodeAffinity(
|
|
1420
|
+
placement,
|
|
1421
|
+
componentBody,
|
|
1422
|
+
candidate.component
|
|
1423
|
+
)
|
|
1424
|
+
)
|
|
1425
|
+
.sort(
|
|
1426
|
+
(left, right) =>
|
|
1427
|
+
left.distance - right.distance || right.score - left.score
|
|
1428
|
+
)
|
|
1429
|
+
|
|
1430
|
+
return nearbyCandidates[0]?.component || null
|
|
1431
|
+
}
|
|
1432
|
+
|
|
947
1433
|
/**
|
|
948
1434
|
* Checks whether a weak far owner is still supported by source metadata.
|
|
949
1435
|
* @param {object} placement External model placement.
|
|
@@ -1271,6 +1757,18 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
1271
1757
|
return normalized < 0 ? normalized + 360 : normalized
|
|
1272
1758
|
}
|
|
1273
1759
|
|
|
1760
|
+
/**
|
|
1761
|
+
* Normalizes one angle into the compact signed range [-180, 180).
|
|
1762
|
+
* @param {number} angle Candidate angle.
|
|
1763
|
+
* @returns {number}
|
|
1764
|
+
*/
|
|
1765
|
+
static #normalizeSignedAngle(angle) {
|
|
1766
|
+
const normalized =
|
|
1767
|
+
AltiumScene3dExternalPlacementAdapter.#normalizeAngle(angle)
|
|
1768
|
+
|
|
1769
|
+
return normalized >= 180 ? normalized - 360 : normalized
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1274
1772
|
/**
|
|
1275
1773
|
* Normalizes identity strings for exact substring matching.
|
|
1276
1774
|
* @param {unknown[]} values Source values.
|