altium-toolkit 1.1.31 → 1.1.32

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "altium-toolkit",
3
- "version": "1.1.31",
3
+ "version": "1.1.32",
4
4
  "description": "Altium document parsing and non-interactive rendering utilities",
5
5
  "keywords": [
6
6
  "altium",
@@ -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.
@@ -175,6 +178,19 @@ export class AltiumScene3dExternalPlacementAdapter {
175
178
  resolvedComponent
176
179
  ) || placement.mountSide
177
180
  : placement.mountSide
181
+ const shouldCenterResolvedModelAnchor =
182
+ AltiumScene3dExternalPlacementAdapter.#shouldCenterResolvedModelAnchor(
183
+ placement,
184
+ resolvedComponent,
185
+ componentBody,
186
+ pads
187
+ )
188
+ const ownerAnchorOffset = shouldCenterResolvedModelAnchor
189
+ ? AltiumScene3dExternalPlacementAdapter.#ownerAnchorOffset(
190
+ placement,
191
+ resolvedComponent
192
+ )
193
+ : null
178
194
  const nextPlacement =
179
195
  exactComponent || metadataComponent
180
196
  ? {
@@ -185,11 +201,23 @@ export class AltiumScene3dExternalPlacementAdapter {
185
201
  mountSide,
186
202
  positionMil: {
187
203
  ...placement.positionMil,
204
+ ...(shouldCenterResolvedModelAnchor
205
+ ? AltiumScene3dExternalPlacementAdapter.#ownerPositionMil(
206
+ resolvedComponent,
207
+ board
208
+ )
209
+ : {}),
188
210
  z: AltiumScene3dExternalPlacementAdapter.#resolveFaceZ(
189
211
  mountSide,
190
212
  board
191
213
  )
192
- }
214
+ },
215
+ modelTransform: shouldCenterResolvedModelAnchor
216
+ ? AltiumScene3dExternalPlacementAdapter.#withRenderableOwnerAnchorOffset(
217
+ placement,
218
+ ownerAnchorOffset
219
+ )
220
+ : placement.modelTransform
193
221
  }
194
222
  : placement
195
223
  const shouldUseComponentYaw =
@@ -200,6 +228,17 @@ export class AltiumScene3dExternalPlacementAdapter {
200
228
  currentHasMetadataAffinity &&
201
229
  !currentIsMechanicalOwner
202
230
  )
231
+ const rotationContext = {
232
+ placement: nextPlacement,
233
+ component: resolvedComponent,
234
+ componentBody,
235
+ pads,
236
+ isExactAnchoredOwner
237
+ }
238
+ const footprintYaw =
239
+ AltiumScene3dPlacementRotationPolicy.resolveFootprintYaw(
240
+ rotationContext
241
+ )
203
242
 
204
243
  const repairedPlacement =
205
244
  AltiumScene3dExternalPlacementAdapter.#repairRotation(
@@ -207,13 +246,10 @@ export class AltiumScene3dExternalPlacementAdapter {
207
246
  resolvedComponent,
208
247
  componentBody,
209
248
  shouldUseComponentYaw,
210
- AltiumScene3dPlacementRotationPolicy.shouldCorrectYaw({
211
- placement: nextPlacement,
212
- component: resolvedComponent,
213
- componentBody,
214
- pads,
215
- isExactAnchoredOwner
216
- })
249
+ AltiumScene3dPlacementRotationPolicy.shouldCorrectYaw(
250
+ rotationContext
251
+ ),
252
+ footprintYaw
217
253
  )
218
254
 
219
255
  return AltiumScene3dExternalPlacementAdapter.#withContactPadHints(
@@ -419,7 +455,11 @@ export class AltiumScene3dExternalPlacementAdapter {
419
455
  String(placement?.projection?.source || '') ===
420
456
  'model-anchor-fallback'
421
457
  ) {
422
- return null
458
+ return AltiumScene3dExternalPlacementAdapter.#nearestModelAnchorOwner(
459
+ placement,
460
+ componentBody,
461
+ components
462
+ )
423
463
  }
424
464
 
425
465
  return AltiumScene3dExternalPlacementAdapter.#nearestAnchorComponent(
@@ -429,6 +469,294 @@ export class AltiumScene3dExternalPlacementAdapter {
429
469
  )
430
470
  }
431
471
 
472
+ /**
473
+ * Finds a nearby compatible owner for a model-anchor fallback body.
474
+ * @param {object} placement External model placement.
475
+ * @param {object | null} componentBody Source component body.
476
+ * @param {object[]} components PCB components.
477
+ * @returns {object | null}
478
+ */
479
+ static #nearestModelAnchorOwner(placement, componentBody, components) {
480
+ const candidates = components
481
+ .map((component) => ({
482
+ component,
483
+ distance: AltiumScene3dExternalPlacementAdapter.#distanceToBody(
484
+ placement,
485
+ component
486
+ )
487
+ }))
488
+ .filter(
489
+ (candidate) =>
490
+ candidate.distance <=
491
+ AltiumScene3dExternalPlacementAdapter
492
+ .#MODEL_ANCHOR_NEAR_OWNER_TOLERANCE_MIL &&
493
+ AltiumScene3dExternalPlacementAdapter.#hasModelAnchorOwnerAffinity(
494
+ placement,
495
+ componentBody,
496
+ candidate.component
497
+ )
498
+ )
499
+ .sort((left, right) => left.distance - right.distance)
500
+
501
+ return candidates[0]?.component || null
502
+ }
503
+
504
+ /**
505
+ * Checks whether a resolved model-anchor fallback should be centered on
506
+ * its nearby compatible owner.
507
+ * @param {object} placement External model placement.
508
+ * @param {object | null | undefined} component Resolved owner component.
509
+ * @param {object | null} componentBody Source component body.
510
+ * @param {object[]} pads Source PCB pads.
511
+ * @returns {boolean}
512
+ */
513
+ static #shouldCenterResolvedModelAnchor(
514
+ placement,
515
+ component,
516
+ componentBody,
517
+ pads
518
+ ) {
519
+ if (
520
+ !component ||
521
+ String(placement?.projection?.source || '') !==
522
+ 'model-anchor-fallback'
523
+ ) {
524
+ return false
525
+ }
526
+
527
+ if (
528
+ AltiumScene3dExternalPlacementAdapter.#distanceToBody(
529
+ placement,
530
+ component
531
+ ) <=
532
+ AltiumScene3dExternalPlacementAdapter
533
+ .#MODEL_ANCHOR_NEAR_OWNER_TOLERANCE_MIL &&
534
+ AltiumScene3dExternalPlacementAdapter.#hasModelAnchorOwnerAffinity(
535
+ placement,
536
+ componentBody,
537
+ component
538
+ )
539
+ ) {
540
+ return true
541
+ }
542
+
543
+ return (
544
+ AltiumScene3dExternalPlacementAdapter.#hasPartCodeAffinity(
545
+ placement,
546
+ componentBody,
547
+ component
548
+ ) &&
549
+ AltiumScene3dExternalPlacementAdapter.#hasModelAnchorOwnerComponentAffinity(
550
+ component
551
+ ) &&
552
+ AltiumScene3dExternalPlacementAdapter.#hasOwnedPadGeometry(
553
+ component,
554
+ pads
555
+ )
556
+ )
557
+ }
558
+
559
+ /**
560
+ * Checks whether model-anchor and component identity both describe
561
+ * connector/header-like hardware.
562
+ * @param {object} placement External model placement.
563
+ * @param {object | null} componentBody Source component body.
564
+ * @param {object} component PCB component.
565
+ * @returns {boolean}
566
+ */
567
+ static #hasModelAnchorOwnerAffinity(placement, componentBody, component) {
568
+ return (
569
+ AltiumScene3dExternalPlacementAdapter.#MODEL_ANCHOR_OWNER_PATTERN.test(
570
+ [
571
+ placement?.designator,
572
+ placement?.externalModel?.name,
573
+ placement?.externalModel?.relativePath,
574
+ componentBody?.identifier,
575
+ componentBody?.name
576
+ ]
577
+ .map((value) => String(value || ''))
578
+ .join(' ')
579
+ ) &&
580
+ AltiumScene3dExternalPlacementAdapter.#MODEL_ANCHOR_OWNER_PATTERN.test(
581
+ [
582
+ component?.designator,
583
+ component?.pattern,
584
+ component?.source,
585
+ component?.description,
586
+ ...Object.values(component?.parameters || {})
587
+ ]
588
+ .map((value) => String(value || ''))
589
+ .join(' ')
590
+ )
591
+ )
592
+ }
593
+
594
+ /**
595
+ * Checks whether component metadata describes connector/header hardware.
596
+ * @param {object} component PCB component.
597
+ * @returns {boolean}
598
+ */
599
+ static #hasModelAnchorOwnerComponentAffinity(component) {
600
+ return AltiumScene3dExternalPlacementAdapter.#MODEL_ANCHOR_OWNER_PATTERN.test(
601
+ [
602
+ component?.designator,
603
+ component?.pattern,
604
+ component?.source,
605
+ component?.description,
606
+ ...Object.values(component?.parameters || {})
607
+ ]
608
+ .map((value) => String(value || ''))
609
+ .join(' ')
610
+ )
611
+ }
612
+
613
+ /**
614
+ * Checks whether the owner has measurable pad geometry for centering.
615
+ * @param {object} component PCB component.
616
+ * @param {object[]} pads Source PCB pads.
617
+ * @returns {boolean}
618
+ */
619
+ static #hasOwnedPadGeometry(component, pads) {
620
+ const componentIndex = Number(component?.componentIndex)
621
+ if (!Number.isFinite(componentIndex)) {
622
+ return false
623
+ }
624
+
625
+ return (
626
+ (Array.isArray(pads) ? pads : []).filter(
627
+ (pad) =>
628
+ Number(pad?.componentIndex) === componentIndex &&
629
+ AltiumScene3dExternalPlacementAdapter.#isMeasurablePad(pad)
630
+ ).length >= 2
631
+ )
632
+ }
633
+
634
+ /**
635
+ * Checks whether one pad has finite coordinates and non-zero dimensions.
636
+ * @param {object} pad Source PCB pad.
637
+ * @returns {boolean}
638
+ */
639
+ static #isMeasurablePad(pad) {
640
+ const width = Math.max(
641
+ Number(pad?.sizeTopX || 0),
642
+ Number(pad?.sizeMidX || 0),
643
+ Number(pad?.sizeBottomX || 0)
644
+ )
645
+ const depth = Math.max(
646
+ Number(pad?.sizeTopY || 0),
647
+ Number(pad?.sizeMidY || 0),
648
+ Number(pad?.sizeBottomY || 0)
649
+ )
650
+
651
+ return (
652
+ Number.isFinite(Number(pad?.x)) &&
653
+ Number.isFinite(Number(pad?.y)) &&
654
+ width > 0 &&
655
+ depth > 0
656
+ )
657
+ }
658
+
659
+ /**
660
+ * Resolves a component-centered scene position.
661
+ * @param {object} component PCB component.
662
+ * @param {object | undefined} board Scene board metadata.
663
+ * @returns {{ x: number, y: number }}
664
+ */
665
+ static #ownerPositionMil(component, board) {
666
+ return {
667
+ x: Number(component?.x || 0) - Number(board?.centerX || 0),
668
+ y: Number(component?.y || 0) - Number(board?.centerY || 0)
669
+ }
670
+ }
671
+
672
+ /**
673
+ * Resolves the source body anchor offset from its component owner.
674
+ * @param {object} placement External model placement.
675
+ * @param {object} component PCB component.
676
+ * @returns {{ x: number, y: number }}
677
+ */
678
+ static #ownerAnchorOffset(placement, component) {
679
+ return {
680
+ x:
681
+ Number(placement?.bodyPositionMil?.x || 0) -
682
+ Number(component?.x || 0),
683
+ y:
684
+ Number(placement?.bodyPositionMil?.y || 0) -
685
+ Number(component?.y || 0)
686
+ }
687
+ }
688
+
689
+ /**
690
+ * Adds owner anchor provenance and a viewer-applied local model offset.
691
+ * @param {object} placement External model placement.
692
+ * @param {{ x?: number, y?: number } | null} offset Source-origin offset.
693
+ * @returns {object}
694
+ */
695
+ static #withRenderableOwnerAnchorOffset(placement, offset) {
696
+ const modelTransform = placement?.modelTransform || {}
697
+ const offsetX = Number(offset?.x || 0)
698
+ const offsetY = Number(offset?.y || 0)
699
+ const renderableOffset =
700
+ AltiumScene3dExternalPlacementAdapter.#renderableOwnerAnchorOffset(
701
+ placement,
702
+ { x: offsetX, y: offsetY }
703
+ )
704
+
705
+ return {
706
+ ...(modelTransform || {}),
707
+ offsetMil: {
708
+ ...(modelTransform?.offsetMil || {}),
709
+ x: renderableOffset.x,
710
+ y: renderableOffset.y
711
+ },
712
+ ownerAnchorOffsetMil: {
713
+ x: offsetX,
714
+ y: offsetY
715
+ }
716
+ }
717
+ }
718
+
719
+ /**
720
+ * Converts a board-space owner anchor offset into mount-rig local XY.
721
+ * @param {{ mountSide?: string, rotationDeg?: number }} placement External placement.
722
+ * @param {{ x: number, y: number }} offset Board-space owner offset.
723
+ * @returns {{ x: number, y: number }}
724
+ */
725
+ static #renderableOwnerAnchorOffset(placement, offset) {
726
+ const rotationRad =
727
+ (-AltiumScene3dExternalPlacementAdapter.#normalizeAngle(
728
+ Number(placement?.rotationDeg || 0)
729
+ ) *
730
+ Math.PI) /
731
+ 180
732
+ const cos = Math.cos(rotationRad)
733
+ const sin = Math.sin(rotationRad)
734
+ const x = Number(offset?.x || 0) * cos - Number(offset?.y || 0) * sin
735
+ const y = Number(offset?.x || 0) * sin + Number(offset?.y || 0) * cos
736
+
737
+ return {
738
+ x: Math.abs(x) < Number.EPSILON ? 0 : Number(x.toFixed(10)),
739
+ y: AltiumScene3dExternalPlacementAdapter.#isBottomPlacement(
740
+ placement
741
+ )
742
+ ? Math.abs(y) < Number.EPSILON
743
+ ? 0
744
+ : Number((-y).toFixed(10))
745
+ : Math.abs(y) < Number.EPSILON
746
+ ? 0
747
+ : Number(y.toFixed(10))
748
+ }
749
+ }
750
+
751
+ /**
752
+ * Checks whether one placement mounts on the board bottom face.
753
+ * @param {{ mountSide?: string } | null | undefined} placement External placement.
754
+ * @returns {boolean}
755
+ */
756
+ static #isBottomPlacement(placement) {
757
+ return String(placement?.mountSide || '').toLowerCase() === 'bottom'
758
+ }
759
+
432
760
  /**
433
761
  * Repairs orientation fields once a source body and owner are known.
434
762
  * @param {object} placement External model placement.
@@ -436,6 +764,7 @@ export class AltiumScene3dExternalPlacementAdapter {
436
764
  * @param {object | null} componentBody Source component body.
437
765
  * @param {boolean} useComponentYaw Whether component yaw should override body yaw.
438
766
  * @param {boolean} correctPinOneYaw Whether a square IC pin-one correction applies.
767
+ * @param {number | null} footprintYaw Footprint-derived yaw when available.
439
768
  * @returns {object}
440
769
  */
441
770
  static #repairRotation(
@@ -443,7 +772,8 @@ export class AltiumScene3dExternalPlacementAdapter {
443
772
  component,
444
773
  componentBody,
445
774
  useComponentYaw,
446
- correctPinOneYaw
775
+ correctPinOneYaw,
776
+ footprintYaw
447
777
  ) {
448
778
  const modelTransform =
449
779
  AltiumScene3dExternalPlacementAdapter.#repairModelTransform(
@@ -462,29 +792,41 @@ export class AltiumScene3dExternalPlacementAdapter {
462
792
  AltiumScene3dExternalPlacementAdapter.#normalizeAngle(
463
793
  Number(placement?.rotationDeg || 0)
464
794
  )
795
+ const hasFootprintYaw =
796
+ footprintYaw !== null &&
797
+ footprintYaw !== undefined &&
798
+ Number.isFinite(Number(footprintYaw))
465
799
  const baseRotation =
466
- component && useComponentYaw && !isGenericPassiveBody
467
- ? componentYaw
468
- : placementYaw
800
+ component && hasFootprintYaw
801
+ ? AltiumScene3dExternalPlacementAdapter.#normalizeAngle(
802
+ Number(footprintYaw)
803
+ )
804
+ : component && useComponentYaw && !isGenericPassiveBody
805
+ ? componentYaw
806
+ : placementYaw
469
807
  const rotationDeg =
470
808
  correctPinOneYaw && !isGenericPassiveBody
471
809
  ? AltiumScene3dExternalPlacementAdapter.#normalizeAngle(
472
810
  baseRotation + 180
473
811
  )
474
812
  : baseRotation
475
- if (!component || !useComponentYaw || isGenericPassiveBody) {
476
- return {
477
- ...placement,
478
- rotationDeg,
479
- modelTransform
480
- }
481
- }
482
813
 
483
- return {
814
+ const repairedPlacement = {
484
815
  ...placement,
485
816
  rotationDeg,
486
817
  modelTransform
487
818
  }
819
+
820
+ return modelTransform?.ownerAnchorOffsetMil
821
+ ? {
822
+ ...repairedPlacement,
823
+ modelTransform:
824
+ AltiumScene3dExternalPlacementAdapter.#withRenderableOwnerAnchorOffset(
825
+ repairedPlacement,
826
+ modelTransform.ownerAnchorOffsetMil
827
+ )
828
+ }
829
+ : repairedPlacement
488
830
  }
489
831
 
490
832
  /**
@@ -617,8 +959,8 @@ export class AltiumScene3dExternalPlacementAdapter {
617
959
  componentBody
618
960
  ).some(
619
961
  (token) =>
620
- /[a-z]/u.test(token) &&
621
962
  /\d/u.test(token) &&
963
+ (/[a-z]/u.test(token) || token.length >= 6) &&
622
964
  haystack.includes(token)
623
965
  )
624
966
  }