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 +1 -1
- package/src/ui/AltiumScene3dAuthoredBodyAnchorAdapter.mjs +54 -6
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +364 -22
- package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +568 -4
- package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +149 -23
- package/src/ui/PcbScene3dBuilder.mjs +297 -5
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +34 -2
|
@@ -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
|
|
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
|
-
|
|
175
|
-
|
|
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
|
}
|