altium-toolkit 1.1.31 → 1.1.33

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.
@@ -6,6 +6,8 @@ export class AltiumScene3dAuthoredBodyAnchorAdapter {
6
6
  static #MIN_OWNER_OFFSET_MIL = 25
7
7
  static #BODY_ANCHOR_TOLERANCE_MIL = 5
8
8
  static #AUTHORED_SOURCE = 'authored-body-anchor'
9
+ static #AUTHORED_ANCHOR_IDENTITY_PATTERN =
10
+ /(?:^|[^a-z0-9])(?:antenna|coax|connector|edge|header|jack|mechanical|module|mount|shield|sma|socket|usb)(?:$|[^a-z0-9])/i
9
11
 
10
12
  /**
11
13
  * Marks off-anchor explicit Altium body placements so the runtime does not
@@ -111,25 +113,71 @@ export class AltiumScene3dAuthoredBodyAnchorAdapter {
111
113
  component,
112
114
  board
113
115
  )
116
+ const ownerOffset = AltiumScene3dAuthoredBodyAnchorAdapter.#distance(
117
+ bodyPosition,
118
+ ownerPosition
119
+ )
114
120
 
115
121
  if (
116
- AltiumScene3dAuthoredBodyAnchorAdapter.#distance(
117
- bodyPosition,
118
- ownerPosition
119
- ) < AltiumScene3dAuthoredBodyAnchorAdapter.#MIN_OWNER_OFFSET_MIL
122
+ ownerOffset <
123
+ AltiumScene3dAuthoredBodyAnchorAdapter.#MIN_OWNER_OFFSET_MIL
120
124
  ) {
121
125
  return false
122
126
  }
123
127
 
124
- return (
128
+ if (
125
129
  AltiumScene3dAuthoredBodyAnchorAdapter.#distance(
126
130
  placementPosition,
127
131
  bodyPosition
128
- ) <=
132
+ ) >
129
133
  AltiumScene3dAuthoredBodyAnchorAdapter.#BODY_ANCHOR_TOLERANCE_MIL
134
+ ) {
135
+ return false
136
+ }
137
+
138
+ return AltiumScene3dAuthoredBodyAnchorAdapter.#hasAuthoredAnchorIdentity(
139
+ placement,
140
+ component
130
141
  )
131
142
  }
132
143
 
144
+ /**
145
+ * Checks whether package metadata suggests the offset is an authored
146
+ * connector/mechanical anchor instead of a package source-origin bias.
147
+ * @param {object} placement External placement.
148
+ * @param {object} component Matched scene component.
149
+ * @returns {boolean}
150
+ */
151
+ static #hasAuthoredAnchorIdentity(placement, component) {
152
+ return AltiumScene3dAuthoredBodyAnchorAdapter.#AUTHORED_ANCHOR_IDENTITY_PATTERN.test(
153
+ AltiumScene3dAuthoredBodyAnchorAdapter.#identityText(
154
+ placement,
155
+ component
156
+ )
157
+ )
158
+ }
159
+
160
+ /**
161
+ * Builds searchable package metadata for anchor-preservation checks.
162
+ * @param {object} placement External placement.
163
+ * @param {object} component Matched scene component.
164
+ * @returns {string}
165
+ */
166
+ static #identityText(placement, component) {
167
+ return [
168
+ placement?.designator,
169
+ placement?.externalModel?.name,
170
+ placement?.externalModel?.relativePath,
171
+ component?.pattern,
172
+ component?.source,
173
+ component?.description,
174
+ component?.body?.family,
175
+ ...Object.values(component?.parameters || {})
176
+ ]
177
+ .map((value) => String(value || ''))
178
+ .join(' ')
179
+ }
180
+
133
181
  /**
134
182
  * Marks one placement as authored-anchor based.
135
183
  * @param {object} placement External placement.
@@ -9,11 +9,14 @@ export class AltiumScene3dExternalPlacementAdapter {
9
9
  static #EXACT_ANCHOR_TOLERANCE_MIL = 5
10
10
  static #NEAR_ANCHOR_TOLERANCE_MIL = 20
11
11
  static #FAR_OWNER_DISTANCE_MIL = 100
12
+ static #MODEL_ANCHOR_NEAR_OWNER_TOLERANCE_MIL = 35
12
13
  static #DEFAULT_BOARD_THICKNESS_MIL = 63
13
14
  static #PASSIVE_BODY_PATTERN =
14
15
  /(?:^|[^a-z0-9])(?:cap|capacitor|res|resistor|ind|inductor|ferrite|bead|crystal|xtal|lqw|lqg)(?:$|[^a-z0-9])/i
15
16
  static #MECHANICAL_OWNER_PATTERN =
16
17
  /(?:^|[^a-z0-9])(?:mech|mechanical|shield|frame|cover|hardware)(?:$|[^a-z0-9])/i
18
+ static #MODEL_ANCHOR_OWNER_PATTERN =
19
+ /(?:pin\s*header|pinheader|header|connector|socket|fpc|flex|jtag)/i
17
20
 
18
21
  /**
19
22
  * Applies exact-anchor repairs to Altium external 3D placements.
@@ -86,6 +89,14 @@ export class AltiumScene3dExternalPlacementAdapter {
86
89
  if (!placement?.bodyPositionMil || !placement?.positionMil) {
87
90
  return placement
88
91
  }
92
+ if (
93
+ AltiumScene3dExternalPlacementAdapter.#isAuthoredShapeStackPlacement(
94
+ placement
95
+ )
96
+ ) {
97
+ return placement
98
+ }
99
+
89
100
  const componentBody =
90
101
  AltiumScene3dExternalPlacementAdapter.#resolveComponentBody(
91
102
  placement,
@@ -175,6 +186,19 @@ export class AltiumScene3dExternalPlacementAdapter {
175
186
  resolvedComponent
176
187
  ) || placement.mountSide
177
188
  : placement.mountSide
189
+ const shouldCenterResolvedModelAnchor =
190
+ AltiumScene3dExternalPlacementAdapter.#shouldCenterResolvedModelAnchor(
191
+ placement,
192
+ resolvedComponent,
193
+ componentBody,
194
+ pads
195
+ )
196
+ const ownerAnchorOffset = shouldCenterResolvedModelAnchor
197
+ ? AltiumScene3dExternalPlacementAdapter.#ownerAnchorOffset(
198
+ placement,
199
+ resolvedComponent
200
+ )
201
+ : null
178
202
  const nextPlacement =
179
203
  exactComponent || metadataComponent
180
204
  ? {
@@ -185,11 +209,23 @@ export class AltiumScene3dExternalPlacementAdapter {
185
209
  mountSide,
186
210
  positionMil: {
187
211
  ...placement.positionMil,
212
+ ...(shouldCenterResolvedModelAnchor
213
+ ? AltiumScene3dExternalPlacementAdapter.#ownerPositionMil(
214
+ resolvedComponent,
215
+ board
216
+ )
217
+ : {}),
188
218
  z: AltiumScene3dExternalPlacementAdapter.#resolveFaceZ(
189
219
  mountSide,
190
220
  board
191
221
  )
192
- }
222
+ },
223
+ modelTransform: shouldCenterResolvedModelAnchor
224
+ ? AltiumScene3dExternalPlacementAdapter.#withRenderableOwnerAnchorOffset(
225
+ placement,
226
+ ownerAnchorOffset
227
+ )
228
+ : placement.modelTransform
193
229
  }
194
230
  : placement
195
231
  const shouldUseComponentYaw =
@@ -200,6 +236,17 @@ export class AltiumScene3dExternalPlacementAdapter {
200
236
  currentHasMetadataAffinity &&
201
237
  !currentIsMechanicalOwner
202
238
  )
239
+ const rotationContext = {
240
+ placement: nextPlacement,
241
+ component: resolvedComponent,
242
+ componentBody,
243
+ pads,
244
+ isExactAnchoredOwner
245
+ }
246
+ const footprintYaw =
247
+ AltiumScene3dPlacementRotationPolicy.resolveFootprintYaw(
248
+ rotationContext
249
+ )
203
250
 
204
251
  const repairedPlacement =
205
252
  AltiumScene3dExternalPlacementAdapter.#repairRotation(
@@ -207,13 +254,10 @@ export class AltiumScene3dExternalPlacementAdapter {
207
254
  resolvedComponent,
208
255
  componentBody,
209
256
  shouldUseComponentYaw,
210
- AltiumScene3dPlacementRotationPolicy.shouldCorrectYaw({
211
- placement: nextPlacement,
212
- component: resolvedComponent,
213
- componentBody,
214
- pads,
215
- isExactAnchoredOwner
216
- })
257
+ AltiumScene3dPlacementRotationPolicy.shouldCorrectYaw(
258
+ rotationContext
259
+ ),
260
+ footprintYaw
217
261
  )
218
262
 
219
263
  return AltiumScene3dExternalPlacementAdapter.#withContactPadHints(
@@ -246,6 +290,19 @@ export class AltiumScene3dExternalPlacementAdapter {
246
290
  }
247
291
  }
248
292
 
293
+ /**
294
+ * Checks whether an earlier adapter already resolved an authored carrier
295
+ * stack placement.
296
+ * @param {object} placement External model placement.
297
+ * @returns {boolean}
298
+ */
299
+ static #isAuthoredShapeStackPlacement(placement) {
300
+ return (
301
+ String(placement?.projection?.source || '').toLowerCase() ===
302
+ 'authored-shape-stack'
303
+ )
304
+ }
305
+
249
306
  /**
250
307
  * Resolves board-local SMT pad centers for mixed connector footprints.
251
308
  * @param {object} placement External model placement.
@@ -419,7 +476,11 @@ export class AltiumScene3dExternalPlacementAdapter {
419
476
  String(placement?.projection?.source || '') ===
420
477
  'model-anchor-fallback'
421
478
  ) {
422
- return null
479
+ return AltiumScene3dExternalPlacementAdapter.#nearestModelAnchorOwner(
480
+ placement,
481
+ componentBody,
482
+ components
483
+ )
423
484
  }
424
485
 
425
486
  return AltiumScene3dExternalPlacementAdapter.#nearestAnchorComponent(
@@ -429,6 +490,294 @@ export class AltiumScene3dExternalPlacementAdapter {
429
490
  )
430
491
  }
431
492
 
493
+ /**
494
+ * Finds a nearby compatible owner for a model-anchor fallback body.
495
+ * @param {object} placement External model placement.
496
+ * @param {object | null} componentBody Source component body.
497
+ * @param {object[]} components PCB components.
498
+ * @returns {object | null}
499
+ */
500
+ static #nearestModelAnchorOwner(placement, componentBody, components) {
501
+ const candidates = components
502
+ .map((component) => ({
503
+ component,
504
+ distance: AltiumScene3dExternalPlacementAdapter.#distanceToBody(
505
+ placement,
506
+ component
507
+ )
508
+ }))
509
+ .filter(
510
+ (candidate) =>
511
+ candidate.distance <=
512
+ AltiumScene3dExternalPlacementAdapter
513
+ .#MODEL_ANCHOR_NEAR_OWNER_TOLERANCE_MIL &&
514
+ AltiumScene3dExternalPlacementAdapter.#hasModelAnchorOwnerAffinity(
515
+ placement,
516
+ componentBody,
517
+ candidate.component
518
+ )
519
+ )
520
+ .sort((left, right) => left.distance - right.distance)
521
+
522
+ return candidates[0]?.component || null
523
+ }
524
+
525
+ /**
526
+ * Checks whether a resolved model-anchor fallback should be centered on
527
+ * its nearby compatible owner.
528
+ * @param {object} placement External model placement.
529
+ * @param {object | null | undefined} component Resolved owner component.
530
+ * @param {object | null} componentBody Source component body.
531
+ * @param {object[]} pads Source PCB pads.
532
+ * @returns {boolean}
533
+ */
534
+ static #shouldCenterResolvedModelAnchor(
535
+ placement,
536
+ component,
537
+ componentBody,
538
+ pads
539
+ ) {
540
+ if (
541
+ !component ||
542
+ String(placement?.projection?.source || '') !==
543
+ 'model-anchor-fallback'
544
+ ) {
545
+ return false
546
+ }
547
+
548
+ if (
549
+ AltiumScene3dExternalPlacementAdapter.#distanceToBody(
550
+ placement,
551
+ component
552
+ ) <=
553
+ AltiumScene3dExternalPlacementAdapter
554
+ .#MODEL_ANCHOR_NEAR_OWNER_TOLERANCE_MIL &&
555
+ AltiumScene3dExternalPlacementAdapter.#hasModelAnchorOwnerAffinity(
556
+ placement,
557
+ componentBody,
558
+ component
559
+ )
560
+ ) {
561
+ return true
562
+ }
563
+
564
+ return (
565
+ AltiumScene3dExternalPlacementAdapter.#hasPartCodeAffinity(
566
+ placement,
567
+ componentBody,
568
+ component
569
+ ) &&
570
+ AltiumScene3dExternalPlacementAdapter.#hasModelAnchorOwnerComponentAffinity(
571
+ component
572
+ ) &&
573
+ AltiumScene3dExternalPlacementAdapter.#hasOwnedPadGeometry(
574
+ component,
575
+ pads
576
+ )
577
+ )
578
+ }
579
+
580
+ /**
581
+ * Checks whether model-anchor and component identity both describe
582
+ * connector/header-like hardware.
583
+ * @param {object} placement External model placement.
584
+ * @param {object | null} componentBody Source component body.
585
+ * @param {object} component PCB component.
586
+ * @returns {boolean}
587
+ */
588
+ static #hasModelAnchorOwnerAffinity(placement, componentBody, component) {
589
+ return (
590
+ AltiumScene3dExternalPlacementAdapter.#MODEL_ANCHOR_OWNER_PATTERN.test(
591
+ [
592
+ placement?.designator,
593
+ placement?.externalModel?.name,
594
+ placement?.externalModel?.relativePath,
595
+ componentBody?.identifier,
596
+ componentBody?.name
597
+ ]
598
+ .map((value) => String(value || ''))
599
+ .join(' ')
600
+ ) &&
601
+ AltiumScene3dExternalPlacementAdapter.#MODEL_ANCHOR_OWNER_PATTERN.test(
602
+ [
603
+ component?.designator,
604
+ component?.pattern,
605
+ component?.source,
606
+ component?.description,
607
+ ...Object.values(component?.parameters || {})
608
+ ]
609
+ .map((value) => String(value || ''))
610
+ .join(' ')
611
+ )
612
+ )
613
+ }
614
+
615
+ /**
616
+ * Checks whether component metadata describes connector/header hardware.
617
+ * @param {object} component PCB component.
618
+ * @returns {boolean}
619
+ */
620
+ static #hasModelAnchorOwnerComponentAffinity(component) {
621
+ return AltiumScene3dExternalPlacementAdapter.#MODEL_ANCHOR_OWNER_PATTERN.test(
622
+ [
623
+ component?.designator,
624
+ component?.pattern,
625
+ component?.source,
626
+ component?.description,
627
+ ...Object.values(component?.parameters || {})
628
+ ]
629
+ .map((value) => String(value || ''))
630
+ .join(' ')
631
+ )
632
+ }
633
+
634
+ /**
635
+ * Checks whether the owner has measurable pad geometry for centering.
636
+ * @param {object} component PCB component.
637
+ * @param {object[]} pads Source PCB pads.
638
+ * @returns {boolean}
639
+ */
640
+ static #hasOwnedPadGeometry(component, pads) {
641
+ const componentIndex = Number(component?.componentIndex)
642
+ if (!Number.isFinite(componentIndex)) {
643
+ return false
644
+ }
645
+
646
+ return (
647
+ (Array.isArray(pads) ? pads : []).filter(
648
+ (pad) =>
649
+ Number(pad?.componentIndex) === componentIndex &&
650
+ AltiumScene3dExternalPlacementAdapter.#isMeasurablePad(pad)
651
+ ).length >= 2
652
+ )
653
+ }
654
+
655
+ /**
656
+ * Checks whether one pad has finite coordinates and non-zero dimensions.
657
+ * @param {object} pad Source PCB pad.
658
+ * @returns {boolean}
659
+ */
660
+ static #isMeasurablePad(pad) {
661
+ const width = Math.max(
662
+ Number(pad?.sizeTopX || 0),
663
+ Number(pad?.sizeMidX || 0),
664
+ Number(pad?.sizeBottomX || 0)
665
+ )
666
+ const depth = Math.max(
667
+ Number(pad?.sizeTopY || 0),
668
+ Number(pad?.sizeMidY || 0),
669
+ Number(pad?.sizeBottomY || 0)
670
+ )
671
+
672
+ return (
673
+ Number.isFinite(Number(pad?.x)) &&
674
+ Number.isFinite(Number(pad?.y)) &&
675
+ width > 0 &&
676
+ depth > 0
677
+ )
678
+ }
679
+
680
+ /**
681
+ * Resolves a component-centered scene position.
682
+ * @param {object} component PCB component.
683
+ * @param {object | undefined} board Scene board metadata.
684
+ * @returns {{ x: number, y: number }}
685
+ */
686
+ static #ownerPositionMil(component, board) {
687
+ return {
688
+ x: Number(component?.x || 0) - Number(board?.centerX || 0),
689
+ y: Number(component?.y || 0) - Number(board?.centerY || 0)
690
+ }
691
+ }
692
+
693
+ /**
694
+ * Resolves the source body anchor offset from its component owner.
695
+ * @param {object} placement External model placement.
696
+ * @param {object} component PCB component.
697
+ * @returns {{ x: number, y: number }}
698
+ */
699
+ static #ownerAnchorOffset(placement, component) {
700
+ return {
701
+ x:
702
+ Number(placement?.bodyPositionMil?.x || 0) -
703
+ Number(component?.x || 0),
704
+ y:
705
+ Number(placement?.bodyPositionMil?.y || 0) -
706
+ Number(component?.y || 0)
707
+ }
708
+ }
709
+
710
+ /**
711
+ * Adds owner anchor provenance and a viewer-applied local model offset.
712
+ * @param {object} placement External model placement.
713
+ * @param {{ x?: number, y?: number } | null} offset Source-origin offset.
714
+ * @returns {object}
715
+ */
716
+ static #withRenderableOwnerAnchorOffset(placement, offset) {
717
+ const modelTransform = placement?.modelTransform || {}
718
+ const offsetX = Number(offset?.x || 0)
719
+ const offsetY = Number(offset?.y || 0)
720
+ const renderableOffset =
721
+ AltiumScene3dExternalPlacementAdapter.#renderableOwnerAnchorOffset(
722
+ placement,
723
+ { x: offsetX, y: offsetY }
724
+ )
725
+
726
+ return {
727
+ ...(modelTransform || {}),
728
+ offsetMil: {
729
+ ...(modelTransform?.offsetMil || {}),
730
+ x: renderableOffset.x,
731
+ y: renderableOffset.y
732
+ },
733
+ ownerAnchorOffsetMil: {
734
+ x: offsetX,
735
+ y: offsetY
736
+ }
737
+ }
738
+ }
739
+
740
+ /**
741
+ * Converts a board-space owner anchor offset into mount-rig local XY.
742
+ * @param {{ mountSide?: string, rotationDeg?: number }} placement External placement.
743
+ * @param {{ x: number, y: number }} offset Board-space owner offset.
744
+ * @returns {{ x: number, y: number }}
745
+ */
746
+ static #renderableOwnerAnchorOffset(placement, offset) {
747
+ const rotationRad =
748
+ (-AltiumScene3dExternalPlacementAdapter.#normalizeAngle(
749
+ Number(placement?.rotationDeg || 0)
750
+ ) *
751
+ Math.PI) /
752
+ 180
753
+ const cos = Math.cos(rotationRad)
754
+ const sin = Math.sin(rotationRad)
755
+ const x = Number(offset?.x || 0) * cos - Number(offset?.y || 0) * sin
756
+ const y = Number(offset?.x || 0) * sin + Number(offset?.y || 0) * cos
757
+
758
+ return {
759
+ x: Math.abs(x) < Number.EPSILON ? 0 : Number(x.toFixed(10)),
760
+ y: AltiumScene3dExternalPlacementAdapter.#isBottomPlacement(
761
+ placement
762
+ )
763
+ ? Math.abs(y) < Number.EPSILON
764
+ ? 0
765
+ : Number((-y).toFixed(10))
766
+ : Math.abs(y) < Number.EPSILON
767
+ ? 0
768
+ : Number(y.toFixed(10))
769
+ }
770
+ }
771
+
772
+ /**
773
+ * Checks whether one placement mounts on the board bottom face.
774
+ * @param {{ mountSide?: string } | null | undefined} placement External placement.
775
+ * @returns {boolean}
776
+ */
777
+ static #isBottomPlacement(placement) {
778
+ return String(placement?.mountSide || '').toLowerCase() === 'bottom'
779
+ }
780
+
432
781
  /**
433
782
  * Repairs orientation fields once a source body and owner are known.
434
783
  * @param {object} placement External model placement.
@@ -436,6 +785,7 @@ export class AltiumScene3dExternalPlacementAdapter {
436
785
  * @param {object | null} componentBody Source component body.
437
786
  * @param {boolean} useComponentYaw Whether component yaw should override body yaw.
438
787
  * @param {boolean} correctPinOneYaw Whether a square IC pin-one correction applies.
788
+ * @param {number | null} footprintYaw Footprint-derived yaw when available.
439
789
  * @returns {object}
440
790
  */
441
791
  static #repairRotation(
@@ -443,7 +793,8 @@ export class AltiumScene3dExternalPlacementAdapter {
443
793
  component,
444
794
  componentBody,
445
795
  useComponentYaw,
446
- correctPinOneYaw
796
+ correctPinOneYaw,
797
+ footprintYaw
447
798
  ) {
448
799
  const modelTransform =
449
800
  AltiumScene3dExternalPlacementAdapter.#repairModelTransform(
@@ -462,29 +813,41 @@ export class AltiumScene3dExternalPlacementAdapter {
462
813
  AltiumScene3dExternalPlacementAdapter.#normalizeAngle(
463
814
  Number(placement?.rotationDeg || 0)
464
815
  )
816
+ const hasFootprintYaw =
817
+ footprintYaw !== null &&
818
+ footprintYaw !== undefined &&
819
+ Number.isFinite(Number(footprintYaw))
465
820
  const baseRotation =
466
- component && useComponentYaw && !isGenericPassiveBody
467
- ? componentYaw
468
- : placementYaw
821
+ component && hasFootprintYaw
822
+ ? AltiumScene3dExternalPlacementAdapter.#normalizeAngle(
823
+ Number(footprintYaw)
824
+ )
825
+ : component && useComponentYaw && !isGenericPassiveBody
826
+ ? componentYaw
827
+ : placementYaw
469
828
  const rotationDeg =
470
829
  correctPinOneYaw && !isGenericPassiveBody
471
830
  ? AltiumScene3dExternalPlacementAdapter.#normalizeAngle(
472
831
  baseRotation + 180
473
832
  )
474
833
  : baseRotation
475
- if (!component || !useComponentYaw || isGenericPassiveBody) {
476
- return {
477
- ...placement,
478
- rotationDeg,
479
- modelTransform
480
- }
481
- }
482
834
 
483
- return {
835
+ const repairedPlacement = {
484
836
  ...placement,
485
837
  rotationDeg,
486
838
  modelTransform
487
839
  }
840
+
841
+ return modelTransform?.ownerAnchorOffsetMil
842
+ ? {
843
+ ...repairedPlacement,
844
+ modelTransform:
845
+ AltiumScene3dExternalPlacementAdapter.#withRenderableOwnerAnchorOffset(
846
+ repairedPlacement,
847
+ modelTransform.ownerAnchorOffsetMil
848
+ )
849
+ }
850
+ : repairedPlacement
488
851
  }
489
852
 
490
853
  /**
@@ -617,8 +980,8 @@ export class AltiumScene3dExternalPlacementAdapter {
617
980
  componentBody
618
981
  ).some(
619
982
  (token) =>
620
- /[a-z]/u.test(token) &&
621
983
  /\d/u.test(token) &&
984
+ (/[a-z]/u.test(token) || token.length >= 6) &&
622
985
  haystack.includes(token)
623
986
  )
624
987
  }