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.
@@ -1,9 +1,20 @@
1
1
  const PASSIVE_BODY_PATTERN =
2
2
  /(?:^|[^a-z0-9])(?:cap|capacitor|res|resistor|ind|inductor|ferrite|bead|crystal|xtal|lqw|lqg)(?:$|[^a-z0-9])/i
3
+ const CHIP_PASSIVE_BODY_PATTERN =
4
+ /(?:^|[^a-z0-9])(?:cap|capacitor|res|resistor|ind|inductor|ferrite|bead|lqw|lqg)(?:$|[^a-z0-9])/i
5
+ const LOCAL_Y_CHIP_PASSIVE_MODEL_PATTERN =
6
+ /(?:^|[^a-z0-9])local[-_ ]?y(?:$|[^a-z0-9])/i
3
7
  const PIN_ONE_CORNER_PACKAGE_PATTERN =
4
8
  /(?:^|[^a-z0-9])(?:[avw]?qfn|vfqfn|wqfn|v?qfp|lqfp|tqfp|pqfp|mqfp)(?:[0-9]+)?(?:$|[^a-z0-9])/i
5
9
  const FIVE_LEAD_SOT_PATTERN =
6
10
  /(?:^|[^a-z0-9])sot[-_ ]?(?:23[-_ ]?5|25|5)(?:$|[^a-z0-9])/i
11
+ const THREE_LEAD_SOT23_PATTERN =
12
+ /(?:^|[^a-z0-9])sot[-_ ]?23[-_ ]?3(?:$|[^a-z0-9])/i
13
+ const FLAT_SOURCE_X_SOT_PATTERN =
14
+ /(?:^|[^a-z0-9])sot[-_ ]?(?:23[-_ ]?3|23[-_ ]?5|25|5)(?:$|[^a-z0-9])/i
15
+ const SOT_PACKAGE_PATTERN =
16
+ /(?:^|[^a-z0-9])sot[-_ ]?(?:23|25|323|343|353|523|553)(?:[-_ ]?\d+)?(?:$|[^a-z0-9])/i
17
+ const SOT523_PACKAGE_PATTERN = /(?:^|[^a-z0-9])sot[-_ ]?523(?:$|[^a-z0-9])/i
7
18
  const EDGE_CONNECTOR_TOKENS = new Set([
8
19
  'antenna',
9
20
  'coax',
@@ -31,12 +42,100 @@ export class AltiumScene3dPlacementRotationPolicy {
31
42
  AltiumScene3dPlacementRotationPolicy.#needsFiveLeadSotCorrection(
32
43
  context
33
44
  ) ||
45
+ AltiumScene3dPlacementRotationPolicy.#needsThreeLeadSot23Correction(
46
+ context
47
+ ) ||
34
48
  AltiumScene3dPlacementRotationPolicy.#needsTiltedEdgeCorrection(
35
49
  context
36
50
  )
37
51
  )
38
52
  }
39
53
 
54
+ /**
55
+ * Checks whether the component footprint yaw should override the source
56
+ * body yaw for anchored or pad-fallback packages whose footprint geometry
57
+ * is more reliable than the source model yaw.
58
+ * @param {{ placement?: object, component?: object | null, componentBody?: object | null, pads?: object[], isExactAnchoredOwner?: boolean }} context Rotation context.
59
+ * @returns {boolean}
60
+ */
61
+ static shouldUseFootprintYaw(context) {
62
+ const footprintYaw =
63
+ AltiumScene3dPlacementRotationPolicy.resolveFootprintYaw(context)
64
+
65
+ return (
66
+ footprintYaw !== null &&
67
+ footprintYaw !== undefined &&
68
+ Number.isFinite(Number(footprintYaw))
69
+ )
70
+ }
71
+
72
+ /**
73
+ * Resolves a board-space yaw from footprint pad geometry when the model
74
+ * source yaw cannot be trusted.
75
+ * @param {{ placement?: object, component?: object | null, componentBody?: object | null, pads?: object[], isExactAnchoredOwner?: boolean }} context Rotation context.
76
+ * @returns {number | null}
77
+ */
78
+ static resolveFootprintYaw(context) {
79
+ const { placement, component, componentBody, pads } = context || {}
80
+ const mountSide = String(placement?.mountSide || '').toLowerCase()
81
+ if (
82
+ !AltiumScene3dPlacementRotationPolicy.#isAnchoredOrPadFallback(
83
+ context
84
+ ) ||
85
+ !component ||
86
+ (mountSide !== 'top' && mountSide !== 'bottom')
87
+ ) {
88
+ return null
89
+ }
90
+
91
+ const identityText =
92
+ AltiumScene3dPlacementRotationPolicy.#packageIdentityText(
93
+ component,
94
+ componentBody
95
+ )
96
+ if (
97
+ PASSIVE_BODY_PATTERN.test(identityText) &&
98
+ AltiumScene3dPlacementRotationPolicy.#hasPassiveFootprint(
99
+ component,
100
+ pads
101
+ ) &&
102
+ AltiumScene3dPlacementRotationPolicy.#shouldUsePassiveFootprintYaw(
103
+ component,
104
+ identityText,
105
+ placement
106
+ )
107
+ ) {
108
+ return AltiumScene3dPlacementRotationPolicy.#resolvePassiveFootprintYaw(
109
+ component,
110
+ pads,
111
+ identityText,
112
+ placement
113
+ )
114
+ }
115
+
116
+ if (
117
+ SOT_PACKAGE_PATTERN.test(identityText) &&
118
+ AltiumScene3dPlacementRotationPolicy.#hasSmallSotFootprint(
119
+ component,
120
+ pads
121
+ ) &&
122
+ AltiumScene3dPlacementRotationPolicy.#shouldUseSotFootprintYaw(
123
+ placement,
124
+ identityText,
125
+ componentBody
126
+ )
127
+ ) {
128
+ return AltiumScene3dPlacementRotationPolicy.#resolveSotFootprintYaw(
129
+ component,
130
+ pads,
131
+ identityText,
132
+ componentBody
133
+ )
134
+ }
135
+
136
+ return null
137
+ }
138
+
40
139
  /**
41
140
  * Detects square IC packages whose embedded source frame places the
42
141
  * pin-one marker opposite the rendered footprint.
@@ -82,11 +181,23 @@ export class AltiumScene3dPlacementRotationPolicy {
82
181
  static #hasSquarePinOneFrameMismatch(placement, component, componentBody) {
83
182
  const mountSide = String(placement?.mountSide || '').toLowerCase()
84
183
  if (mountSide === 'top') {
85
- return true
184
+ return (
185
+ !AltiumScene3dPlacementRotationPolicy.#isPadFallback(
186
+ placement
187
+ ) ||
188
+ !AltiumScene3dPlacementRotationPolicy.#sourceYawMatchesComponentYaw(
189
+ placement,
190
+ component,
191
+ componentBody
192
+ )
193
+ )
86
194
  }
87
195
 
88
196
  return (
89
197
  mountSide === 'bottom' &&
198
+ AltiumScene3dPlacementRotationPolicy.#hasRightAngleModelTilt(
199
+ componentBody
200
+ ) &&
90
201
  AltiumScene3dPlacementRotationPolicy.#isHalfTurnAngle(
91
202
  placement?.rotationDeg
92
203
  ) &&
@@ -100,6 +211,35 @@ export class AltiumScene3dPlacementRotationPolicy {
100
211
  )
101
212
  }
102
213
 
214
+ /**
215
+ * Checks whether a source package yaw already matches the footprint and
216
+ * does not need another pin-one half-turn.
217
+ * @param {object | undefined} placement External model placement.
218
+ * @param {object} component PCB component.
219
+ * @param {object | null | undefined} componentBody Source component body.
220
+ * @returns {boolean}
221
+ */
222
+ static #sourceYawMatchesComponentYaw(placement, component, componentBody) {
223
+ const componentYaw =
224
+ AltiumScene3dPlacementRotationPolicy.#normalizeAngle(
225
+ Number(component?.rotation || 0)
226
+ )
227
+ const placementYaw =
228
+ AltiumScene3dPlacementRotationPolicy.#normalizeAngle(
229
+ Number(placement?.rotationDeg || 0)
230
+ )
231
+ const bodyYaw = AltiumScene3dPlacementRotationPolicy.#normalizeAngle(
232
+ Number(
233
+ componentBody?.modelRotationDeg?.z ??
234
+ placement?.modelTransform?.rotationDeg?.z ??
235
+ placement?.rotationDeg ??
236
+ 0
237
+ )
238
+ )
239
+
240
+ return placementYaw === componentYaw || bodyYaw === componentYaw
241
+ }
242
+
103
243
  /**
104
244
  * Detects exact five-lead SOT packages whose STEP source pin-one
105
245
  * convention is opposite the asymmetric footprint pad convention.
@@ -107,7 +247,7 @@ export class AltiumScene3dPlacementRotationPolicy {
107
247
  * @returns {boolean}
108
248
  */
109
249
  static #needsFiveLeadSotCorrection(context) {
110
- const { component, componentBody, pads } = context || {}
250
+ const { placement, component, componentBody, pads } = context || {}
111
251
  if (
112
252
  !AltiumScene3dPlacementRotationPolicy.#isAnchoredOrPadFallback(
113
253
  context
@@ -123,12 +263,64 @@ export class AltiumScene3dPlacementRotationPolicy {
123
263
  return false
124
264
  }
125
265
 
266
+ if (
267
+ AltiumScene3dPlacementRotationPolicy.#sourceYawMatchesComponentYaw(
268
+ placement,
269
+ component,
270
+ componentBody
271
+ )
272
+ ) {
273
+ return false
274
+ }
275
+
126
276
  return AltiumScene3dPlacementRotationPolicy.#hasAsymmetricFivePads(
127
277
  component,
128
278
  pads
129
279
  )
130
280
  }
131
281
 
282
+ /**
283
+ * Detects top-side SOT23-3 packages whose embedded source pin side is
284
+ * opposite the asymmetric footprint pad convention.
285
+ * @param {{ placement?: object, component?: object | null, componentBody?: object | null, pads?: object[], isExactAnchoredOwner?: boolean }} context Rotation context.
286
+ * @returns {boolean}
287
+ */
288
+ static #needsThreeLeadSot23Correction(context) {
289
+ const { placement, component, componentBody, pads } = context || {}
290
+ if (
291
+ !AltiumScene3dPlacementRotationPolicy.#isAnchoredOrPadFallback(
292
+ context
293
+ ) ||
294
+ !component ||
295
+ String(placement?.mountSide || '').toLowerCase() !== 'top'
296
+ ) {
297
+ return false
298
+ }
299
+
300
+ const identityText =
301
+ AltiumScene3dPlacementRotationPolicy.#packageIdentityText(
302
+ component,
303
+ componentBody
304
+ )
305
+ if (!THREE_LEAD_SOT23_PATTERN.test(identityText)) {
306
+ return false
307
+ }
308
+ if (
309
+ AltiumScene3dPlacementRotationPolicy.#sourceYawMatchesComponentYaw(
310
+ placement,
311
+ component,
312
+ componentBody
313
+ )
314
+ ) {
315
+ return false
316
+ }
317
+
318
+ return AltiumScene3dPlacementRotationPolicy.#hasAsymmetricThreePads(
319
+ component,
320
+ pads
321
+ )
322
+ }
323
+
132
324
  /**
133
325
  * Detects top-side edge connectors whose tilted model frame points inward
134
326
  * unless the authored board-facing yaw is reversed.
@@ -171,11 +363,21 @@ export class AltiumScene3dPlacementRotationPolicy {
171
363
  static #isAnchoredOrPadFallback(context) {
172
364
  return (
173
365
  Boolean(context?.isExactAnchoredOwner) ||
174
- String(context?.placement?.projection?.source || '') ===
175
- 'pad-fallback'
366
+ AltiumScene3dPlacementRotationPolicy.#isPadFallback(
367
+ context?.placement
368
+ )
176
369
  )
177
370
  }
178
371
 
372
+ /**
373
+ * Checks whether a placement was synthesized from footprint pads.
374
+ * @param {object | undefined} placement External model placement.
375
+ * @returns {boolean}
376
+ */
377
+ static #isPadFallback(placement) {
378
+ return String(placement?.projection?.source || '') === 'pad-fallback'
379
+ }
380
+
179
381
  /**
180
382
  * Checks whether a component owns an asymmetric five-pad footprint.
181
383
  * @param {object} component PCB component.
@@ -207,6 +409,311 @@ export class AltiumScene3dPlacementRotationPolicy {
207
409
  )
208
410
  }
209
411
 
412
+ /**
413
+ * Checks whether a component owns a three-pad footprint split 2+1 across
414
+ * the package width.
415
+ * @param {object} component PCB component.
416
+ * @param {object[]} pads Source PCB pads.
417
+ * @returns {boolean}
418
+ */
419
+ static #hasAsymmetricThreePads(component, pads) {
420
+ const surfacePads = AltiumScene3dPlacementRotationPolicy.#surfacePads(
421
+ component,
422
+ pads
423
+ )
424
+ if (surfacePads.length !== 3) {
425
+ return false
426
+ }
427
+
428
+ const axis =
429
+ AltiumScene3dPlacementRotationPolicy.#spread(surfacePads, 'x') >=
430
+ AltiumScene3dPlacementRotationPolicy.#spread(surfacePads, 'y')
431
+ ? 'x'
432
+ : 'y'
433
+ const values = surfacePads.map((pad) => Number(pad?.[axis] || 0))
434
+ const midpoint = (Math.min(...values) + Math.max(...values)) / 2
435
+ const lowerCount = values.filter((value) => value <= midpoint).length
436
+ const upperCount = values.length - lowerCount
437
+
438
+ return (
439
+ Math.min(lowerCount, upperCount) === 1 &&
440
+ Math.max(lowerCount, upperCount) === 2
441
+ )
442
+ }
443
+
444
+ /**
445
+ * Checks whether a component owns a two-terminal surface footprint.
446
+ * @param {object} component PCB component.
447
+ * @param {object[]} pads Source PCB pads.
448
+ * @returns {boolean}
449
+ */
450
+ static #hasPassiveFootprint(component, pads) {
451
+ return (
452
+ AltiumScene3dPlacementRotationPolicy.#surfacePads(component, pads)
453
+ .length >= 2
454
+ )
455
+ }
456
+
457
+ /**
458
+ * Checks whether passive pad geometry should override the authored yaw.
459
+ * @param {object} component PCB component.
460
+ * @param {string} identityText Package metadata text.
461
+ * @param {object | undefined} placement External model placement.
462
+ * @returns {boolean}
463
+ */
464
+ static #shouldUsePassiveFootprintYaw(component, identityText, placement) {
465
+ return (
466
+ AltiumScene3dPlacementRotationPolicy.#hasLocalYPassiveModel(
467
+ identityText,
468
+ placement
469
+ ) ||
470
+ !AltiumScene3dPlacementRotationPolicy.#isRightAngle(
471
+ Number(component?.rotation || 0)
472
+ )
473
+ )
474
+ }
475
+
476
+ /**
477
+ * Resolves a passive package yaw from the solder pad axis.
478
+ * @param {object} component PCB component.
479
+ * @param {object[]} pads Source PCB pads.
480
+ * @param {string} identityText Package metadata text.
481
+ * @param {object | undefined} placement External model placement.
482
+ * @returns {number | null}
483
+ */
484
+ static #resolvePassiveFootprintYaw(
485
+ component,
486
+ pads,
487
+ identityText,
488
+ placement
489
+ ) {
490
+ const surfacePads = AltiumScene3dPlacementRotationPolicy.#surfacePads(
491
+ component,
492
+ pads
493
+ )
494
+ const componentYaw =
495
+ AltiumScene3dPlacementRotationPolicy.#sourceFrameAngle(
496
+ Number(component?.rotation || 0),
497
+ placement
498
+ )
499
+ const modelAxisOffset =
500
+ AltiumScene3dPlacementRotationPolicy.#passiveModelAxisOffset(
501
+ identityText,
502
+ placement
503
+ )
504
+
505
+ if (surfacePads.length === 2) {
506
+ const angle =
507
+ (Math.atan2(
508
+ Number(surfacePads[1]?.y || 0) -
509
+ Number(surfacePads[0]?.y || 0),
510
+ Number(surfacePads[1]?.x || 0) -
511
+ Number(surfacePads[0]?.x || 0)
512
+ ) *
513
+ 180) /
514
+ Math.PI
515
+
516
+ return AltiumScene3dPlacementRotationPolicy.#nearestLineAngle(
517
+ componentYaw,
518
+ AltiumScene3dPlacementRotationPolicy.#sourceFrameAngle(
519
+ angle,
520
+ placement
521
+ ) + modelAxisOffset
522
+ )
523
+ }
524
+
525
+ const lineAngle =
526
+ AltiumScene3dPlacementRotationPolicy.#spread(surfacePads, 'x') >=
527
+ AltiumScene3dPlacementRotationPolicy.#spread(surfacePads, 'y')
528
+ ? 0
529
+ : 90
530
+
531
+ return AltiumScene3dPlacementRotationPolicy.#nearestLineAngle(
532
+ componentYaw,
533
+ AltiumScene3dPlacementRotationPolicy.#sourceFrameAngle(
534
+ lineAngle,
535
+ placement
536
+ ) + modelAxisOffset
537
+ )
538
+ }
539
+
540
+ /**
541
+ * Returns the model-yaw offset needed to place the body long axis on pads.
542
+ * @param {string} identityText Package metadata text.
543
+ * @param {object | undefined} placement External model placement.
544
+ * @returns {number}
545
+ */
546
+ static #passiveModelAxisOffset(identityText, placement) {
547
+ return AltiumScene3dPlacementRotationPolicy.#hasLocalYPassiveModel(
548
+ identityText,
549
+ placement
550
+ )
551
+ ? -90
552
+ : 0
553
+ }
554
+
555
+ /**
556
+ * Checks whether metadata identifies a local-Y chip passive model.
557
+ * @param {string} identityText Package metadata text.
558
+ * @param {object | undefined} placement External model placement.
559
+ * @returns {boolean}
560
+ */
561
+ static #hasLocalYPassiveModel(identityText, placement) {
562
+ const modelText = [
563
+ placement?.model,
564
+ placement?.externalModel?.name,
565
+ placement?.externalModel?.relativePath,
566
+ placement?.externalModel?.sourcePath
567
+ ]
568
+ .map((value) => String(value || ''))
569
+ .join(' ')
570
+ if (
571
+ CHIP_PASSIVE_BODY_PATTERN.test(identityText) &&
572
+ LOCAL_Y_CHIP_PASSIVE_MODEL_PATTERN.test(modelText)
573
+ ) {
574
+ return true
575
+ }
576
+
577
+ return false
578
+ }
579
+
580
+ /**
581
+ * Converts a board-space visual angle into the model source frame used for
582
+ * embedded Altium STEP placements.
583
+ * @param {number} angle Board-space visual angle.
584
+ * @param {object | undefined} placement External model placement.
585
+ * @returns {number}
586
+ */
587
+ static #sourceFrameAngle(angle, placement) {
588
+ return AltiumScene3dPlacementRotationPolicy.#usesEmbeddedSourceMirror(
589
+ placement
590
+ )
591
+ ? -Number(angle || 0)
592
+ : Number(angle || 0)
593
+ }
594
+
595
+ /**
596
+ * Checks whether runtime rendering mirrors the embedded source frame before
597
+ * applying placement yaw.
598
+ * @param {object | undefined} placement External model placement.
599
+ * @returns {boolean}
600
+ */
601
+ static #usesEmbeddedSourceMirror(placement) {
602
+ return (
603
+ String(placement?.externalModel?.origin || '').toLowerCase() ===
604
+ 'embedded'
605
+ )
606
+ }
607
+
608
+ /**
609
+ * Resolves compact SOT yaw from the two pad rows.
610
+ * @param {object} component PCB component.
611
+ * @param {object[]} pads Source PCB pads.
612
+ * @param {string} identityText Package metadata text.
613
+ * @param {object | null | undefined} componentBody Source component body.
614
+ * @returns {number | null}
615
+ */
616
+ static #resolveSotFootprintYaw(
617
+ component,
618
+ pads,
619
+ identityText,
620
+ componentBody
621
+ ) {
622
+ const surfacePads = AltiumScene3dPlacementRotationPolicy.#surfacePads(
623
+ component,
624
+ pads
625
+ )
626
+ if (surfacePads.length < 3) {
627
+ return null
628
+ }
629
+
630
+ const componentYaw =
631
+ AltiumScene3dPlacementRotationPolicy.#normalizeAngle(
632
+ Number(component?.rotation || 0)
633
+ )
634
+ const rowSpacingAxis =
635
+ AltiumScene3dPlacementRotationPolicy.#spread(surfacePads, 'x') >=
636
+ AltiumScene3dPlacementRotationPolicy.#spread(surfacePads, 'y')
637
+ ? 'x'
638
+ : 'y'
639
+ const bodyLongAxisAngle = rowSpacingAxis === 'x' ? 90 : 0
640
+ const modelYaw =
641
+ AltiumScene3dPlacementRotationPolicy.#usesSourceXLongAxisSot(
642
+ identityText,
643
+ componentBody
644
+ )
645
+ ? bodyLongAxisAngle
646
+ : bodyLongAxisAngle - 90
647
+
648
+ return AltiumScene3dPlacementRotationPolicy.#nearestLineAngle(
649
+ componentYaw,
650
+ modelYaw
651
+ )
652
+ }
653
+
654
+ /**
655
+ * Checks whether SOT pad geometry should override the authored yaw.
656
+ * @param {object | undefined} placement External model placement.
657
+ * @param {string} identityText Package metadata text.
658
+ * @param {object | null | undefined} componentBody Source component body.
659
+ * @returns {boolean}
660
+ */
661
+ static #shouldUseSotFootprintYaw(placement, identityText, componentBody) {
662
+ return (
663
+ String(placement?.mountSide || '').toLowerCase() === 'bottom' &&
664
+ AltiumScene3dPlacementRotationPolicy.#usesSourceXLongAxisSot(
665
+ identityText,
666
+ componentBody
667
+ )
668
+ )
669
+ }
670
+
671
+ /**
672
+ * Checks whether a compact SOT source model uses local X as its long body
673
+ * axis after the authored Altium tilt is applied.
674
+ * @param {string} identityText Package metadata text.
675
+ * @param {object | null | undefined} componentBody Source component body.
676
+ * @returns {boolean}
677
+ */
678
+ static #usesSourceXLongAxisSot(identityText, componentBody) {
679
+ const modelYaw = AltiumScene3dPlacementRotationPolicy.#normalizeAngle(
680
+ Number(componentBody?.modelRotationDeg?.z || 0)
681
+ )
682
+
683
+ if (
684
+ FLAT_SOURCE_X_SOT_PATTERN.test(identityText) &&
685
+ !AltiumScene3dPlacementRotationPolicy.#hasRightAngleModelTilt(
686
+ componentBody
687
+ ) &&
688
+ (modelYaw === 90 || modelYaw === 270)
689
+ ) {
690
+ return true
691
+ }
692
+
693
+ return (
694
+ SOT523_PACKAGE_PATTERN.test(identityText) &&
695
+ AltiumScene3dPlacementRotationPolicy.#hasRightAngleModelTilt(
696
+ componentBody
697
+ ) &&
698
+ modelYaw === 90
699
+ )
700
+ }
701
+
702
+ /**
703
+ * Checks whether a component owns a compact SOT-style surface footprint.
704
+ * @param {object} component PCB component.
705
+ * @param {object[]} pads Source PCB pads.
706
+ * @returns {boolean}
707
+ */
708
+ static #hasSmallSotFootprint(component, pads) {
709
+ const surfacePads = AltiumScene3dPlacementRotationPolicy.#surfacePads(
710
+ component,
711
+ pads
712
+ )
713
+
714
+ return surfacePads.length >= 3 && surfacePads.length <= 8
715
+ }
716
+
210
717
  /**
211
718
  * Collects surface pads owned by one component.
212
719
  * @param {object} component PCB component.
@@ -354,4 +861,61 @@ export class AltiumScene3dPlacementRotationPolicy {
354
861
  AltiumScene3dPlacementRotationPolicy.#normalizeAngle(angle) === 180
355
862
  )
356
863
  }
864
+
865
+ /**
866
+ * Checks whether an angle is a standard orthogonal footprint yaw.
867
+ * @param {number} angle Source angle.
868
+ * @returns {boolean}
869
+ */
870
+ static #isRightAngle(angle) {
871
+ return (
872
+ AltiumScene3dPlacementRotationPolicy.#normalizeAngle(angle) % 90 ===
873
+ 0
874
+ )
875
+ }
876
+
877
+ /**
878
+ * Chooses the equivalent 180-degree line angle closest to a reference yaw.
879
+ * @param {number} referenceAngle Reference yaw.
880
+ * @param {number} lineAngle Axis angle.
881
+ * @returns {number}
882
+ */
883
+ static #nearestLineAngle(referenceAngle, lineAngle) {
884
+ const normalizedReference =
885
+ AltiumScene3dPlacementRotationPolicy.#normalizeAngle(referenceAngle)
886
+ const normalizedLine =
887
+ AltiumScene3dPlacementRotationPolicy.#normalizeAngle(lineAngle)
888
+ const oppositeLine =
889
+ AltiumScene3dPlacementRotationPolicy.#normalizeAngle(
890
+ normalizedLine + 180
891
+ )
892
+
893
+ return AltiumScene3dPlacementRotationPolicy.#angleDistance(
894
+ normalizedReference,
895
+ normalizedLine
896
+ ) <=
897
+ AltiumScene3dPlacementRotationPolicy.#angleDistance(
898
+ normalizedReference,
899
+ oppositeLine
900
+ )
901
+ ? normalizedLine
902
+ : oppositeLine
903
+ }
904
+
905
+ /**
906
+ * Returns the shortest distance between two normalized angles.
907
+ * @param {number} firstAngle First angle.
908
+ * @param {number} secondAngle Second angle.
909
+ * @returns {number}
910
+ */
911
+ static #angleDistance(firstAngle, secondAngle) {
912
+ const delta = Math.abs(
913
+ AltiumScene3dPlacementRotationPolicy.#normalizeAngle(firstAngle) -
914
+ AltiumScene3dPlacementRotationPolicy.#normalizeAngle(
915
+ secondAngle
916
+ )
917
+ )
918
+
919
+ return Math.min(delta, 360 - delta)
920
+ }
357
921
  }