altium-toolkit 1.1.36 → 1.1.38
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 +762 -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,4 +1,6 @@
|
|
|
1
1
|
import { AltiumScene3dTwoRowFootprintDetector } from './AltiumScene3dTwoRowFootprintDetector.mjs'
|
|
2
|
+
import { AltiumScene3dQfnFootprintDetector } from './AltiumScene3dQfnFootprintDetector.mjs'
|
|
3
|
+
import { AltiumScene3dDisplayModuleYawPolicy } from './AltiumScene3dDisplayModuleYawPolicy.mjs'
|
|
2
4
|
|
|
3
5
|
const PASSIVE_BODY_PATTERN =
|
|
4
6
|
/(?:^|[^a-z0-9])(?:cap|capacitor|res|resistor|ind|inductor|ferrite|bead|crystal|xtal|lqw|lqg)(?:$|[^a-z0-9])/i
|
|
@@ -7,9 +9,11 @@ const CHIP_PASSIVE_BODY_PATTERN =
|
|
|
7
9
|
const LOCAL_Y_CHIP_PASSIVE_MODEL_PATTERN =
|
|
8
10
|
/(?:^|[^a-z0-9])local[-_ ]?y(?:$|[^a-z0-9])/i
|
|
9
11
|
const PIN_ONE_CORNER_PACKAGE_PATTERN =
|
|
10
|
-
/(?:^|[^a-z0-9])(?:[
|
|
12
|
+
/(?:^|[^a-z0-9])(?:[auvw]?qfn|[uv]?fqfn|v?qfp|lqfp|tqfp|pqfp|mqfp)(?:[0-9]+)?(?:$|[^a-z0-9])/i
|
|
11
13
|
const TWO_ROW_PIN_ONE_PACKAGE_PATTERN =
|
|
12
14
|
/(?:^|[^a-z0-9])(?:msop|qsop|soic|sop|ssop|tssop|tsop|vsop)(?:[-_ ]?\d+)?(?:$|[^a-z0-9])/i
|
|
15
|
+
const DFN_FOOTPRINT_YAW_PATTERN =
|
|
16
|
+
/(?:^|[^a-z0-9])(?:[a-z0-9]*dfn)(?:[-_ ]?[a-z0-9]+)*(?:$|[^a-z0-9])/i
|
|
13
17
|
const FIVE_LEAD_SOT_PATTERN =
|
|
14
18
|
/(?:^|[^a-z0-9])sot[-_ ]?(?:23[-_ ]?5|25|5)(?:$|[^a-z0-9])/i
|
|
15
19
|
const THREE_LEAD_SOT23_PATTERN =
|
|
@@ -28,6 +32,8 @@ const EDGE_CONNECTOR_TOKENS = new Set([
|
|
|
28
32
|
'sma',
|
|
29
33
|
'socket'
|
|
30
34
|
])
|
|
35
|
+
const THROUGH_HOLE_CONNECTOR_PATTERN =
|
|
36
|
+
/(?:^|[^a-z0-9])(?:connector|header|socket|terminal|wire[-_ ]?to[-_ ]?board)(?:$|[^a-z0-9])/i
|
|
31
37
|
|
|
32
38
|
/**
|
|
33
39
|
* Resolves generic Altium external-model yaw correction rules.
|
|
@@ -52,6 +58,12 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
52
58
|
AltiumScene3dPlacementRotationPolicy.#needsThreeLeadSot23Correction(
|
|
53
59
|
context
|
|
54
60
|
) ||
|
|
61
|
+
AltiumScene3dPlacementRotationPolicy.#needsDisplayModelBoundsEdgeCorrection(
|
|
62
|
+
context
|
|
63
|
+
) ||
|
|
64
|
+
AltiumScene3dPlacementRotationPolicy.#needsAlignedThroughHoleConnectorCorrection(
|
|
65
|
+
context
|
|
66
|
+
) ||
|
|
55
67
|
AltiumScene3dPlacementRotationPolicy.#needsTiltedEdgeCorrection(
|
|
56
68
|
context
|
|
57
69
|
)
|
|
@@ -76,6 +88,40 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
76
88
|
)
|
|
77
89
|
}
|
|
78
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Checks whether an embedded source model should be mirrored along its
|
|
93
|
+
* local length axis after the Altium tilt is applied.
|
|
94
|
+
* @param {{ placement?: object, component?: object | null, componentBody?: object | null, pads?: object[], isExactAnchoredOwner?: boolean }} context Rotation context.
|
|
95
|
+
* @returns {boolean}
|
|
96
|
+
*/
|
|
97
|
+
static shouldMirrorSourceZ(context) {
|
|
98
|
+
const { placement, component, componentBody, pads } = context || {}
|
|
99
|
+
if (!component) {
|
|
100
|
+
return false
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const identityText =
|
|
104
|
+
AltiumScene3dPlacementRotationPolicy.#packageIdentityText(
|
|
105
|
+
component,
|
|
106
|
+
componentBody
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
AltiumScene3dPlacementRotationPolicy.#isPadFallback(placement) &&
|
|
111
|
+
AltiumScene3dPlacementRotationPolicy.#shouldUseDfnFootprintYaw(
|
|
112
|
+
placement,
|
|
113
|
+
component,
|
|
114
|
+
componentBody,
|
|
115
|
+
pads,
|
|
116
|
+
identityText
|
|
117
|
+
) &&
|
|
118
|
+
AltiumScene3dPlacementRotationPolicy.#hasPowerDfnPinOneEndPattern(
|
|
119
|
+
component,
|
|
120
|
+
pads
|
|
121
|
+
)
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
79
125
|
/**
|
|
80
126
|
* Resolves a board-space yaw from footprint pad geometry when the model
|
|
81
127
|
* source yaw cannot be trusted.
|
|
@@ -140,6 +186,38 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
140
186
|
)
|
|
141
187
|
}
|
|
142
188
|
|
|
189
|
+
if (
|
|
190
|
+
AltiumScene3dDisplayModuleYawPolicy.shouldUseComponentYaw({
|
|
191
|
+
placement,
|
|
192
|
+
component,
|
|
193
|
+
pads,
|
|
194
|
+
identityText
|
|
195
|
+
}) &&
|
|
196
|
+
!AltiumScene3dPlacementRotationPolicy.#sourceYawMatchesComponentYaw(
|
|
197
|
+
placement,
|
|
198
|
+
component,
|
|
199
|
+
componentBody
|
|
200
|
+
)
|
|
201
|
+
) {
|
|
202
|
+
return AltiumScene3dPlacementRotationPolicy.#normalizeAngle(
|
|
203
|
+
Number(component?.rotation || 0)
|
|
204
|
+
)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (
|
|
208
|
+
AltiumScene3dPlacementRotationPolicy.#shouldUseDfnFootprintYaw(
|
|
209
|
+
placement,
|
|
210
|
+
component,
|
|
211
|
+
componentBody,
|
|
212
|
+
pads,
|
|
213
|
+
identityText
|
|
214
|
+
)
|
|
215
|
+
) {
|
|
216
|
+
return AltiumScene3dPlacementRotationPolicy.#normalizeAngle(
|
|
217
|
+
Number(component?.rotation || 0)
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
143
221
|
return null
|
|
144
222
|
}
|
|
145
223
|
|
|
@@ -150,9 +228,9 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
150
228
|
* @returns {boolean}
|
|
151
229
|
*/
|
|
152
230
|
static #needsSquarePinOneCorrection(context) {
|
|
153
|
-
const { placement, component, componentBody } = context || {}
|
|
231
|
+
const { placement, component, componentBody, pads } = context || {}
|
|
154
232
|
if (
|
|
155
|
-
!AltiumScene3dPlacementRotationPolicy.#
|
|
233
|
+
!AltiumScene3dPlacementRotationPolicy.#supportsSquarePinOneProjection(
|
|
156
234
|
context
|
|
157
235
|
) ||
|
|
158
236
|
!component ||
|
|
@@ -172,7 +250,8 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
172
250
|
? AltiumScene3dPlacementRotationPolicy.#hasSquarePinOneFrameMismatch(
|
|
173
251
|
placement,
|
|
174
252
|
component,
|
|
175
|
-
componentBody
|
|
253
|
+
componentBody,
|
|
254
|
+
pads
|
|
176
255
|
)
|
|
177
256
|
: false
|
|
178
257
|
}
|
|
@@ -183,26 +262,77 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
183
262
|
* @param {object | undefined} placement External model placement.
|
|
184
263
|
* @param {object} component PCB component.
|
|
185
264
|
* @param {object | null | undefined} componentBody Source component body.
|
|
265
|
+
* @param {object[]} pads Source PCB pads.
|
|
186
266
|
* @returns {boolean}
|
|
187
267
|
*/
|
|
188
|
-
static #hasSquarePinOneFrameMismatch(
|
|
268
|
+
static #hasSquarePinOneFrameMismatch(
|
|
269
|
+
placement,
|
|
270
|
+
component,
|
|
271
|
+
componentBody,
|
|
272
|
+
pads
|
|
273
|
+
) {
|
|
189
274
|
const mountSide = String(placement?.mountSide || '').toLowerCase()
|
|
190
275
|
if (mountSide === 'top') {
|
|
191
|
-
|
|
192
|
-
!AltiumScene3dPlacementRotationPolicy.#isPadFallback(
|
|
193
|
-
|
|
276
|
+
if (
|
|
277
|
+
!AltiumScene3dPlacementRotationPolicy.#isPadFallback(placement)
|
|
278
|
+
) {
|
|
279
|
+
if (
|
|
280
|
+
AltiumScene3dPlacementRotationPolicy.#hasEmbeddedModelBoundsSourceHalfTurn(
|
|
281
|
+
placement,
|
|
282
|
+
component
|
|
283
|
+
) ||
|
|
284
|
+
!AltiumScene3dPlacementRotationPolicy.#sourceYawMatchesComponentYaw(
|
|
285
|
+
placement,
|
|
286
|
+
component,
|
|
287
|
+
componentBody
|
|
288
|
+
)
|
|
289
|
+
) {
|
|
290
|
+
return true
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return (
|
|
294
|
+
(AltiumScene3dPlacementRotationPolicy.#hasRightAngleModelTilt(
|
|
295
|
+
componentBody
|
|
296
|
+
) &&
|
|
297
|
+
AltiumScene3dQfnFootprintDetector.hasExposedPadPerimeterSequence(
|
|
298
|
+
component,
|
|
299
|
+
pads
|
|
300
|
+
)) ||
|
|
301
|
+
AltiumScene3dPlacementRotationPolicy.#surfacePads(
|
|
302
|
+
component,
|
|
303
|
+
pads
|
|
304
|
+
).length === 0
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
if (
|
|
308
|
+
AltiumScene3dPlacementRotationPolicy.#hasEmbeddedModelBoundsSourceHalfTurn(
|
|
309
|
+
placement,
|
|
310
|
+
component
|
|
194
311
|
) ||
|
|
195
312
|
!AltiumScene3dPlacementRotationPolicy.#sourceYawMatchesComponentYaw(
|
|
196
313
|
placement,
|
|
197
314
|
component,
|
|
198
315
|
componentBody
|
|
199
316
|
)
|
|
317
|
+
) {
|
|
318
|
+
return true
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return (
|
|
322
|
+
AltiumScene3dPlacementRotationPolicy.#hasRightAngleModelTilt(
|
|
323
|
+
componentBody
|
|
324
|
+
) &&
|
|
325
|
+
AltiumScene3dQfnFootprintDetector.hasExposedPadPerimeterSequence(
|
|
326
|
+
component,
|
|
327
|
+
pads
|
|
328
|
+
)
|
|
200
329
|
)
|
|
201
330
|
}
|
|
202
331
|
|
|
203
332
|
return (
|
|
204
333
|
mountSide === 'bottom' &&
|
|
205
|
-
AltiumScene3dPlacementRotationPolicy.#
|
|
334
|
+
AltiumScene3dPlacementRotationPolicy.#isPadFallback(placement) &&
|
|
335
|
+
AltiumScene3dPlacementRotationPolicy.#hasBottomPinOneFrame(
|
|
206
336
|
componentBody
|
|
207
337
|
) &&
|
|
208
338
|
AltiumScene3dPlacementRotationPolicy.#isHalfTurnAngle(
|
|
@@ -218,6 +348,18 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
218
348
|
)
|
|
219
349
|
}
|
|
220
350
|
|
|
351
|
+
/**
|
|
352
|
+
* Checks whether a bottom-side square package source frame needs the
|
|
353
|
+
* mirrored pin-one comparison path.
|
|
354
|
+
* @param {object | null | undefined} componentBody Source component body.
|
|
355
|
+
* @returns {boolean}
|
|
356
|
+
*/
|
|
357
|
+
static #hasBottomPinOneFrame(componentBody) {
|
|
358
|
+
return AltiumScene3dPlacementRotationPolicy.#hasRightAngleModelTilt(
|
|
359
|
+
componentBody
|
|
360
|
+
)
|
|
361
|
+
}
|
|
362
|
+
|
|
221
363
|
/**
|
|
222
364
|
* Detects two-row IC packages whose embedded source frame puts the
|
|
223
365
|
* package pin-one corner opposite the footprint convention.
|
|
@@ -255,6 +397,189 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
255
397
|
)
|
|
256
398
|
}
|
|
257
399
|
|
|
400
|
+
/**
|
|
401
|
+
* Checks whether a DFN-style pad-fallback body should follow footprint yaw.
|
|
402
|
+
* @param {object | undefined} placement External model placement.
|
|
403
|
+
* @param {object} component PCB component.
|
|
404
|
+
* @param {object | null | undefined} componentBody Source component body.
|
|
405
|
+
* @param {object[]} pads Source PCB pads.
|
|
406
|
+
* @param {string} identityText Package metadata text.
|
|
407
|
+
* @returns {boolean}
|
|
408
|
+
*/
|
|
409
|
+
static #shouldUseDfnFootprintYaw(
|
|
410
|
+
placement,
|
|
411
|
+
component,
|
|
412
|
+
componentBody,
|
|
413
|
+
pads,
|
|
414
|
+
identityText
|
|
415
|
+
) {
|
|
416
|
+
return (
|
|
417
|
+
String(placement?.mountSide || '').toLowerCase() === 'top' &&
|
|
418
|
+
AltiumScene3dPlacementRotationPolicy.#isFootprintDerivedProjection(
|
|
419
|
+
placement
|
|
420
|
+
) &&
|
|
421
|
+
String(placement?.externalModel?.origin || '').toLowerCase() ===
|
|
422
|
+
'embedded' &&
|
|
423
|
+
DFN_FOOTPRINT_YAW_PATTERN.test(identityText) &&
|
|
424
|
+
AltiumScene3dPlacementRotationPolicy.#hasRightAngleModelTilt(
|
|
425
|
+
componentBody
|
|
426
|
+
) &&
|
|
427
|
+
!AltiumScene3dPlacementRotationPolicy.#sourceYawMatchesComponentYaw(
|
|
428
|
+
placement,
|
|
429
|
+
component,
|
|
430
|
+
componentBody
|
|
431
|
+
) &&
|
|
432
|
+
AltiumScene3dTwoRowFootprintDetector.hasTwoRowSurfaceFootprint(
|
|
433
|
+
component,
|
|
434
|
+
pads
|
|
435
|
+
)
|
|
436
|
+
)
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Checks whether placement projection came from footprint geometry or
|
|
441
|
+
* footprint-matched model bounds.
|
|
442
|
+
* @param {object | undefined} placement External placement.
|
|
443
|
+
* @returns {boolean}
|
|
444
|
+
*/
|
|
445
|
+
static #isFootprintDerivedProjection(placement) {
|
|
446
|
+
return ['pad-fallback', 'model-bounds'].includes(
|
|
447
|
+
String(placement?.projection?.source || '').toLowerCase()
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Detects two-column power DFN footprints where one side is a tied power
|
|
453
|
+
* row and the opposite side has a single isolated control pad at one end.
|
|
454
|
+
* @param {object} component PCB component.
|
|
455
|
+
* @param {object[]} pads Source PCB pads.
|
|
456
|
+
* @returns {boolean}
|
|
457
|
+
*/
|
|
458
|
+
static #hasPowerDfnPinOneEndPattern(component, pads) {
|
|
459
|
+
const surfacePads = AltiumScene3dPlacementRotationPolicy.#surfacePads(
|
|
460
|
+
component,
|
|
461
|
+
pads
|
|
462
|
+
)
|
|
463
|
+
if (surfacePads.length !== 8) {
|
|
464
|
+
return false
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const xSpread = AltiumScene3dPlacementRotationPolicy.#spread(
|
|
468
|
+
surfacePads,
|
|
469
|
+
'x'
|
|
470
|
+
)
|
|
471
|
+
const ySpread = AltiumScene3dPlacementRotationPolicy.#spread(
|
|
472
|
+
surfacePads,
|
|
473
|
+
'y'
|
|
474
|
+
)
|
|
475
|
+
const sideAxis = xSpread >= ySpread ? 'x' : 'y'
|
|
476
|
+
const endAxis = sideAxis === 'x' ? 'y' : 'x'
|
|
477
|
+
const sides = AltiumScene3dPlacementRotationPolicy.#splitByMidpoint(
|
|
478
|
+
surfacePads,
|
|
479
|
+
sideAxis
|
|
480
|
+
)
|
|
481
|
+
if (sides.lower.length !== 4 || sides.upper.length !== 4) {
|
|
482
|
+
return false
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return (
|
|
486
|
+
(AltiumScene3dPlacementRotationPolicy.#hasUniformNet(sides.lower) &&
|
|
487
|
+
AltiumScene3dPlacementRotationPolicy.#hasThreePlusIsolatedEndNet(
|
|
488
|
+
sides.upper,
|
|
489
|
+
endAxis
|
|
490
|
+
)) ||
|
|
491
|
+
(AltiumScene3dPlacementRotationPolicy.#hasUniformNet(sides.upper) &&
|
|
492
|
+
AltiumScene3dPlacementRotationPolicy.#hasThreePlusIsolatedEndNet(
|
|
493
|
+
sides.lower,
|
|
494
|
+
endAxis
|
|
495
|
+
))
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
/**
|
|
500
|
+
* Splits pads into two side groups around one axis midpoint.
|
|
501
|
+
* @param {object[]} pads Source PCB pads.
|
|
502
|
+
* @param {'x' | 'y'} axis Axis key.
|
|
503
|
+
* @returns {{ lower: object[], upper: object[] }}
|
|
504
|
+
*/
|
|
505
|
+
static #splitByMidpoint(pads, axis) {
|
|
506
|
+
const values = pads.map((pad) => Number(pad?.[axis] || 0))
|
|
507
|
+
const midpoint = (Math.min(...values) + Math.max(...values)) / 2
|
|
508
|
+
|
|
509
|
+
return {
|
|
510
|
+
lower: pads.filter((pad) => Number(pad?.[axis] || 0) <= midpoint),
|
|
511
|
+
upper: pads.filter((pad) => Number(pad?.[axis] || 0) > midpoint)
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Checks whether every pad in a side group shares one routed net.
|
|
517
|
+
* @param {object[]} pads Source PCB pads.
|
|
518
|
+
* @returns {boolean}
|
|
519
|
+
*/
|
|
520
|
+
static #hasUniformNet(pads) {
|
|
521
|
+
const netKeys = pads
|
|
522
|
+
.map((pad) => AltiumScene3dPlacementRotationPolicy.#netKey(pad))
|
|
523
|
+
.filter(Boolean)
|
|
524
|
+
|
|
525
|
+
return netKeys.length === pads.length && new Set(netKeys).size === 1
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Checks for a side group with three tied pads plus one isolated end pad.
|
|
530
|
+
* @param {object[]} pads Source PCB pads.
|
|
531
|
+
* @param {'x' | 'y'} endAxis Axis that orders pins along a side.
|
|
532
|
+
* @returns {boolean}
|
|
533
|
+
*/
|
|
534
|
+
static #hasThreePlusIsolatedEndNet(pads, endAxis) {
|
|
535
|
+
const groups = new Map()
|
|
536
|
+
for (const pad of pads) {
|
|
537
|
+
const netKey = AltiumScene3dPlacementRotationPolicy.#netKey(pad)
|
|
538
|
+
if (!netKey) {
|
|
539
|
+
return false
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
groups.set(netKey, [...(groups.get(netKey) || []), pad])
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const groupSizes = [...groups.values()]
|
|
546
|
+
.map((group) => group.length)
|
|
547
|
+
.sort((a, b) => a - b)
|
|
548
|
+
if (
|
|
549
|
+
groupSizes.length !== 2 ||
|
|
550
|
+
groupSizes[0] !== 1 ||
|
|
551
|
+
groupSizes[1] !== 3
|
|
552
|
+
) {
|
|
553
|
+
return false
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
const isolatedPad = [...groups.values()].find(
|
|
557
|
+
(group) => group.length === 1
|
|
558
|
+
)?.[0]
|
|
559
|
+
const orderedValues = pads.map((pad) => Number(pad?.[endAxis] || 0))
|
|
560
|
+
const isolatedValue = Number(isolatedPad?.[endAxis] || 0)
|
|
561
|
+
|
|
562
|
+
return (
|
|
563
|
+
isolatedValue === Math.min(...orderedValues) ||
|
|
564
|
+
isolatedValue === Math.max(...orderedValues)
|
|
565
|
+
)
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Builds a stable routed-net key for grouping footprint pads.
|
|
570
|
+
* @param {object} pad Source PCB pad.
|
|
571
|
+
* @returns {string}
|
|
572
|
+
*/
|
|
573
|
+
static #netKey(pad) {
|
|
574
|
+
const netName = String(pad?.netName || '').trim()
|
|
575
|
+
if (netName) {
|
|
576
|
+
return 'name:' + netName
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const netIndex = Number(pad?.netIndex)
|
|
580
|
+
return Number.isFinite(netIndex) ? 'index:' + netIndex : ''
|
|
581
|
+
}
|
|
582
|
+
|
|
258
583
|
/**
|
|
259
584
|
* Checks whether a source package yaw already matches the footprint and
|
|
260
585
|
* does not need another pin-one half-turn.
|
|
@@ -308,21 +633,86 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
308
633
|
}
|
|
309
634
|
|
|
310
635
|
if (
|
|
311
|
-
AltiumScene3dPlacementRotationPolicy.#
|
|
636
|
+
!AltiumScene3dPlacementRotationPolicy.#hasAsymmetricFivePads(
|
|
637
|
+
component,
|
|
638
|
+
pads
|
|
639
|
+
)
|
|
640
|
+
) {
|
|
641
|
+
return false
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (
|
|
645
|
+
AltiumScene3dPlacementRotationPolicy.#hasEmbeddedModelBoundsSourceHalfTurn(
|
|
646
|
+
placement,
|
|
647
|
+
component
|
|
648
|
+
) ||
|
|
649
|
+
!AltiumScene3dPlacementRotationPolicy.#sourceYawMatchesComponentYaw(
|
|
312
650
|
placement,
|
|
313
651
|
component,
|
|
314
652
|
componentBody
|
|
315
653
|
)
|
|
316
654
|
) {
|
|
317
|
-
return
|
|
655
|
+
return true
|
|
318
656
|
}
|
|
319
657
|
|
|
320
|
-
return AltiumScene3dPlacementRotationPolicy.#
|
|
658
|
+
return AltiumScene3dPlacementRotationPolicy.#hasTopQuarterTurnFiveLeadSotPinSideMismatch(
|
|
659
|
+
placement,
|
|
321
660
|
component,
|
|
661
|
+
componentBody,
|
|
322
662
|
pads
|
|
323
663
|
)
|
|
324
664
|
}
|
|
325
665
|
|
|
666
|
+
/**
|
|
667
|
+
* Checks for layerless top SOT23-5 source frames whose yaw equals the
|
|
668
|
+
* component yaw but whose horizontal asymmetric pad row needs a half-turn.
|
|
669
|
+
* @param {object | undefined} placement External model placement.
|
|
670
|
+
* @param {object} component PCB component.
|
|
671
|
+
* @param {object | null | undefined} componentBody Source component body.
|
|
672
|
+
* @param {object[]} pads Source PCB pads.
|
|
673
|
+
* @returns {boolean}
|
|
674
|
+
*/
|
|
675
|
+
static #hasTopQuarterTurnFiveLeadSotPinSideMismatch(
|
|
676
|
+
placement,
|
|
677
|
+
component,
|
|
678
|
+
componentBody,
|
|
679
|
+
pads
|
|
680
|
+
) {
|
|
681
|
+
const mountSide = String(placement?.mountSide || '').toLowerCase()
|
|
682
|
+
const componentYaw =
|
|
683
|
+
AltiumScene3dPlacementRotationPolicy.#normalizeAngle(
|
|
684
|
+
Number(component?.rotation || 0)
|
|
685
|
+
)
|
|
686
|
+
const bodyYaw = AltiumScene3dPlacementRotationPolicy.#normalizeAngle(
|
|
687
|
+
Number(
|
|
688
|
+
componentBody?.modelRotationDeg?.z ??
|
|
689
|
+
placement?.modelTransform?.rotationDeg?.z ??
|
|
690
|
+
placement?.rotationDeg ??
|
|
691
|
+
0
|
|
692
|
+
)
|
|
693
|
+
)
|
|
694
|
+
const surfacePads = AltiumScene3dPlacementRotationPolicy.#surfacePads(
|
|
695
|
+
component,
|
|
696
|
+
pads
|
|
697
|
+
)
|
|
698
|
+
|
|
699
|
+
return (
|
|
700
|
+
mountSide === 'top' &&
|
|
701
|
+
AltiumScene3dPlacementRotationPolicy.#isPadFallback(placement) &&
|
|
702
|
+
String(componentBody?.layer || '').trim() === '' &&
|
|
703
|
+
String(componentBody?.sourceStream || '').includes(
|
|
704
|
+
'ShapeBasedComponentBodies'
|
|
705
|
+
) &&
|
|
706
|
+
AltiumScene3dPlacementRotationPolicy.#isQuarterTurnAngle(
|
|
707
|
+
componentYaw
|
|
708
|
+
) &&
|
|
709
|
+
bodyYaw === componentYaw &&
|
|
710
|
+
surfacePads.length === 5 &&
|
|
711
|
+
AltiumScene3dPlacementRotationPolicy.#spread(surfacePads, 'y') >
|
|
712
|
+
AltiumScene3dPlacementRotationPolicy.#spread(surfacePads, 'x')
|
|
713
|
+
)
|
|
714
|
+
}
|
|
715
|
+
|
|
326
716
|
/**
|
|
327
717
|
* Detects top-side SOT23-3 packages whose embedded source pin side is
|
|
328
718
|
* opposite the asymmetric footprint pad convention.
|
|
@@ -365,6 +755,30 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
365
755
|
)
|
|
366
756
|
}
|
|
367
757
|
|
|
758
|
+
/**
|
|
759
|
+
* Detects display modules whose full model-bounds source was authored from
|
|
760
|
+
* the connector edge but currently extends away from its pad row.
|
|
761
|
+
* @param {{ placement?: object, component?: object | null, componentBody?: object | null, pads?: object[] }} context Rotation context.
|
|
762
|
+
* @returns {boolean}
|
|
763
|
+
*/
|
|
764
|
+
static #needsDisplayModelBoundsEdgeCorrection(context) {
|
|
765
|
+
const { placement, component, componentBody, pads } = context || {}
|
|
766
|
+
if (!component) {
|
|
767
|
+
return false
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
return AltiumScene3dDisplayModuleYawPolicy.shouldReverseModelBoundsYaw({
|
|
771
|
+
placement,
|
|
772
|
+
component,
|
|
773
|
+
pads,
|
|
774
|
+
identityText:
|
|
775
|
+
AltiumScene3dPlacementRotationPolicy.#packageIdentityText(
|
|
776
|
+
component,
|
|
777
|
+
componentBody
|
|
778
|
+
)
|
|
779
|
+
})
|
|
780
|
+
}
|
|
781
|
+
|
|
368
782
|
/**
|
|
369
783
|
* Detects top-side edge connectors whose tilted model frame points inward
|
|
370
784
|
* unless the authored board-facing yaw is reversed.
|
|
@@ -399,6 +813,55 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
399
813
|
)
|
|
400
814
|
}
|
|
401
815
|
|
|
816
|
+
/**
|
|
817
|
+
* Detects top-side through-hole connectors whose embedded source frame is
|
|
818
|
+
* aligned numerically but board-facing backwards after source mirroring.
|
|
819
|
+
* @param {{ placement?: object, component?: object | null, componentBody?: object | null, pads?: object[], isExactAnchoredOwner?: boolean }} context Rotation context.
|
|
820
|
+
* @returns {boolean}
|
|
821
|
+
*/
|
|
822
|
+
static #needsAlignedThroughHoleConnectorCorrection(context) {
|
|
823
|
+
const { placement, component, componentBody, pads } = context || {}
|
|
824
|
+
const hasAuthoredProjection =
|
|
825
|
+
AltiumScene3dPlacementRotationPolicy.#isAuthoredOrAnchoredProjection(
|
|
826
|
+
context
|
|
827
|
+
) ||
|
|
828
|
+
AltiumScene3dPlacementRotationPolicy.#isModelBoundsProjection(
|
|
829
|
+
placement
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
if (
|
|
833
|
+
!component ||
|
|
834
|
+
String(placement?.mountSide || '').toLowerCase() !== 'top' ||
|
|
835
|
+
String(placement?.externalModel?.origin || '').toLowerCase() !==
|
|
836
|
+
'embedded' ||
|
|
837
|
+
!hasAuthoredProjection ||
|
|
838
|
+
!AltiumScene3dPlacementRotationPolicy.#hasAuthoredBodyYaw(
|
|
839
|
+
componentBody
|
|
840
|
+
) ||
|
|
841
|
+
!AltiumScene3dPlacementRotationPolicy.#sourceYawMatchesComponentYaw(
|
|
842
|
+
placement,
|
|
843
|
+
component,
|
|
844
|
+
componentBody
|
|
845
|
+
)
|
|
846
|
+
) {
|
|
847
|
+
return false
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
const identityText =
|
|
851
|
+
AltiumScene3dPlacementRotationPolicy.#packageIdentityText(
|
|
852
|
+
component,
|
|
853
|
+
componentBody
|
|
854
|
+
)
|
|
855
|
+
if (!THROUGH_HOLE_CONNECTOR_PATTERN.test(identityText)) {
|
|
856
|
+
return false
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
return AltiumScene3dPlacementRotationPolicy.#hasThroughHoleOnlyPads(
|
|
860
|
+
component,
|
|
861
|
+
pads
|
|
862
|
+
)
|
|
863
|
+
}
|
|
864
|
+
|
|
402
865
|
/**
|
|
403
866
|
* Checks whether one placement is exact-anchored or pad-fallback projected.
|
|
404
867
|
* @param {{ placement?: object, isExactAnchoredOwner?: boolean }} context Rotation context.
|
|
@@ -413,6 +876,43 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
413
876
|
)
|
|
414
877
|
}
|
|
415
878
|
|
|
879
|
+
/**
|
|
880
|
+
* Checks whether one projection can use square-package pin-one correction.
|
|
881
|
+
* @param {{ placement?: object, componentBody?: object | null, isExactAnchoredOwner?: boolean }} context Rotation context.
|
|
882
|
+
* @returns {boolean}
|
|
883
|
+
*/
|
|
884
|
+
static #supportsSquarePinOneProjection(context) {
|
|
885
|
+
return (
|
|
886
|
+
AltiumScene3dPlacementRotationPolicy.#isAnchoredOrPadFallback(
|
|
887
|
+
context
|
|
888
|
+
) ||
|
|
889
|
+
AltiumScene3dPlacementRotationPolicy.#isBottomModelBoundsProjection(
|
|
890
|
+
context
|
|
891
|
+
)
|
|
892
|
+
)
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Checks whether a bottom-side embedded model-bounds projection still
|
|
897
|
+
* carries the source-frame signals needed for pin-one normalization.
|
|
898
|
+
* @param {{ placement?: object, componentBody?: object | null }} context Rotation context.
|
|
899
|
+
* @returns {boolean}
|
|
900
|
+
*/
|
|
901
|
+
static #isBottomModelBoundsProjection(context) {
|
|
902
|
+
const placement = context?.placement || {}
|
|
903
|
+
|
|
904
|
+
return (
|
|
905
|
+
String(placement?.projection?.source || '').toLowerCase() ===
|
|
906
|
+
'model-bounds' &&
|
|
907
|
+
String(placement?.mountSide || '').toLowerCase() === 'bottom' &&
|
|
908
|
+
String(placement?.externalModel?.origin || '').toLowerCase() ===
|
|
909
|
+
'embedded' &&
|
|
910
|
+
AltiumScene3dPlacementRotationPolicy.#hasRightAngleModelTilt(
|
|
911
|
+
context?.componentBody
|
|
912
|
+
)
|
|
913
|
+
)
|
|
914
|
+
}
|
|
915
|
+
|
|
416
916
|
/**
|
|
417
917
|
* Checks whether a placement was synthesized from footprint pads.
|
|
418
918
|
* @param {object | undefined} placement External model placement.
|
|
@@ -422,6 +922,70 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
422
922
|
return String(placement?.projection?.source || '') === 'pad-fallback'
|
|
423
923
|
}
|
|
424
924
|
|
|
925
|
+
/**
|
|
926
|
+
* Checks whether one projection keeps an authored Altium owner/body anchor.
|
|
927
|
+
* @param {{ placement?: object, isExactAnchoredOwner?: boolean }} context Rotation context.
|
|
928
|
+
* @returns {boolean}
|
|
929
|
+
*/
|
|
930
|
+
static #isAuthoredOrAnchoredProjection(context) {
|
|
931
|
+
const projectionSource = String(
|
|
932
|
+
context?.placement?.projection?.source || ''
|
|
933
|
+
).toLowerCase()
|
|
934
|
+
|
|
935
|
+
return (
|
|
936
|
+
AltiumScene3dPlacementRotationPolicy.#isAnchoredOrPadFallback(
|
|
937
|
+
context
|
|
938
|
+
) || projectionSource === 'authored-body-anchor'
|
|
939
|
+
)
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
/**
|
|
943
|
+
* Checks whether one placement was projected from resolved model bounds.
|
|
944
|
+
* @param {object | undefined} placement External model placement.
|
|
945
|
+
* @returns {boolean}
|
|
946
|
+
*/
|
|
947
|
+
static #isModelBoundsProjection(placement) {
|
|
948
|
+
return (
|
|
949
|
+
String(placement?.projection?.source || '').toLowerCase() ===
|
|
950
|
+
'model-bounds'
|
|
951
|
+
)
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Checks whether recovered embedded model-bounds metadata carries a source
|
|
956
|
+
* yaw opposite the owning footprint yaw.
|
|
957
|
+
* @param {object | undefined} placement External model placement.
|
|
958
|
+
* @param {object} component PCB component.
|
|
959
|
+
* @returns {boolean}
|
|
960
|
+
*/
|
|
961
|
+
static #hasEmbeddedModelBoundsSourceHalfTurn(placement, component) {
|
|
962
|
+
if (
|
|
963
|
+
!AltiumScene3dPlacementRotationPolicy.#isModelBoundsProjection(
|
|
964
|
+
placement
|
|
965
|
+
) ||
|
|
966
|
+
String(placement?.externalModel?.origin || '').toLowerCase() !==
|
|
967
|
+
'embedded'
|
|
968
|
+
) {
|
|
969
|
+
return false
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const sourceYaw = Number(
|
|
973
|
+
placement?.externalModel?.transform?.rotationDeg?.z
|
|
974
|
+
)
|
|
975
|
+
if (!Number.isFinite(sourceYaw)) {
|
|
976
|
+
return false
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
const componentYaw =
|
|
980
|
+
AltiumScene3dPlacementRotationPolicy.#normalizeAngle(
|
|
981
|
+
Number(component?.rotation || 0)
|
|
982
|
+
)
|
|
983
|
+
|
|
984
|
+
return AltiumScene3dPlacementRotationPolicy.#isHalfTurnAngle(
|
|
985
|
+
sourceYaw - componentYaw
|
|
986
|
+
)
|
|
987
|
+
}
|
|
988
|
+
|
|
425
989
|
/**
|
|
426
990
|
* Checks whether a component owns an asymmetric five-pad footprint.
|
|
427
991
|
* @param {object} component PCB component.
|
|
@@ -688,6 +1252,18 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
688
1252
|
)
|
|
689
1253
|
? bodyLongAxisAngle
|
|
690
1254
|
: bodyLongAxisAngle - 90
|
|
1255
|
+
const bottomFlatTieYaw =
|
|
1256
|
+
AltiumScene3dPlacementRotationPolicy.#resolveBottomFlatThreeLeadSotTieYaw(
|
|
1257
|
+
component,
|
|
1258
|
+
surfacePads,
|
|
1259
|
+
identityText,
|
|
1260
|
+
componentBody,
|
|
1261
|
+
componentYaw,
|
|
1262
|
+
modelYaw
|
|
1263
|
+
)
|
|
1264
|
+
if (bottomFlatTieYaw !== null) {
|
|
1265
|
+
return bottomFlatTieYaw
|
|
1266
|
+
}
|
|
691
1267
|
|
|
692
1268
|
return AltiumScene3dPlacementRotationPolicy.#nearestLineAngle(
|
|
693
1269
|
componentYaw,
|
|
@@ -695,6 +1271,66 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
695
1271
|
)
|
|
696
1272
|
}
|
|
697
1273
|
|
|
1274
|
+
/**
|
|
1275
|
+
* Resolves the mirrored component pin side for flat bottom-side SOT23-3 source
|
|
1276
|
+
* models when long-axis alignment has an exact 180-degree tie.
|
|
1277
|
+
* @param {object} component PCB component.
|
|
1278
|
+
* @param {object[]} surfacePads Surface pads owned by the component.
|
|
1279
|
+
* @param {string} identityText Package metadata text.
|
|
1280
|
+
* @param {object | null | undefined} componentBody Source component body.
|
|
1281
|
+
* @param {number} componentYaw Component yaw.
|
|
1282
|
+
* @param {number} modelYaw Candidate footprint yaw.
|
|
1283
|
+
* @returns {number | null}
|
|
1284
|
+
*/
|
|
1285
|
+
static #resolveBottomFlatThreeLeadSotTieYaw(
|
|
1286
|
+
component,
|
|
1287
|
+
surfacePads,
|
|
1288
|
+
identityText,
|
|
1289
|
+
componentBody,
|
|
1290
|
+
componentYaw,
|
|
1291
|
+
modelYaw
|
|
1292
|
+
) {
|
|
1293
|
+
if (
|
|
1294
|
+
!THREE_LEAD_SOT23_PATTERN.test(identityText) ||
|
|
1295
|
+
surfacePads.length !== 3 ||
|
|
1296
|
+
!AltiumScene3dPlacementRotationPolicy.#isBottomComponent(
|
|
1297
|
+
component
|
|
1298
|
+
) ||
|
|
1299
|
+
AltiumScene3dPlacementRotationPolicy.#hasRightAngleModelTilt(
|
|
1300
|
+
componentBody
|
|
1301
|
+
) ||
|
|
1302
|
+
!AltiumScene3dPlacementRotationPolicy.#usesSourceXLongAxisSot(
|
|
1303
|
+
identityText,
|
|
1304
|
+
componentBody
|
|
1305
|
+
)
|
|
1306
|
+
) {
|
|
1307
|
+
return null
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
const normalizedModelYaw =
|
|
1311
|
+
AltiumScene3dPlacementRotationPolicy.#normalizeAngle(modelYaw)
|
|
1312
|
+
const oppositeModelYaw =
|
|
1313
|
+
AltiumScene3dPlacementRotationPolicy.#normalizeAngle(
|
|
1314
|
+
normalizedModelYaw + 180
|
|
1315
|
+
)
|
|
1316
|
+
const modelDistance =
|
|
1317
|
+
AltiumScene3dPlacementRotationPolicy.#angleDistance(
|
|
1318
|
+
componentYaw,
|
|
1319
|
+
normalizedModelYaw
|
|
1320
|
+
)
|
|
1321
|
+
const oppositeDistance =
|
|
1322
|
+
AltiumScene3dPlacementRotationPolicy.#angleDistance(
|
|
1323
|
+
componentYaw,
|
|
1324
|
+
oppositeModelYaw
|
|
1325
|
+
)
|
|
1326
|
+
|
|
1327
|
+
return Math.abs(modelDistance - oppositeDistance) < 0.001
|
|
1328
|
+
? AltiumScene3dPlacementRotationPolicy.#normalizeAngle(
|
|
1329
|
+
componentYaw + 180
|
|
1330
|
+
)
|
|
1331
|
+
: null
|
|
1332
|
+
}
|
|
1333
|
+
|
|
698
1334
|
/**
|
|
699
1335
|
* Checks whether SOT pad geometry should override the authored yaw.
|
|
700
1336
|
* @param {object | undefined} placement External model placement.
|
|
@@ -765,13 +1401,9 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
765
1401
|
* @returns {object[]}
|
|
766
1402
|
*/
|
|
767
1403
|
static #surfacePads(component, pads) {
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
const ownedPads = (Array.isArray(pads) ? pads : []).filter(
|
|
774
|
-
(pad) => Number(pad?.componentIndex) === componentIndex
|
|
1404
|
+
const ownedPads = AltiumScene3dPlacementRotationPolicy.#ownedPads(
|
|
1405
|
+
component,
|
|
1406
|
+
pads
|
|
775
1407
|
)
|
|
776
1408
|
const bottom =
|
|
777
1409
|
String(component?.layer || '')
|
|
@@ -787,6 +1419,96 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
787
1419
|
return surfacePads.length ? surfacePads : ownedPads
|
|
788
1420
|
}
|
|
789
1421
|
|
|
1422
|
+
/**
|
|
1423
|
+
* Checks whether one component has only directional through-hole pads.
|
|
1424
|
+
* @param {object} component PCB component.
|
|
1425
|
+
* @param {object[] | undefined} pads Source PCB pads.
|
|
1426
|
+
* @returns {boolean}
|
|
1427
|
+
*/
|
|
1428
|
+
static #hasThroughHoleOnlyPads(component, pads) {
|
|
1429
|
+
const measurablePads = AltiumScene3dPlacementRotationPolicy.#ownedPads(
|
|
1430
|
+
component,
|
|
1431
|
+
pads
|
|
1432
|
+
).filter((pad) =>
|
|
1433
|
+
AltiumScene3dPlacementRotationPolicy.#hasMeasurablePadSize(pad)
|
|
1434
|
+
)
|
|
1435
|
+
if (measurablePads.length < 2) {
|
|
1436
|
+
return false
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
return measurablePads.every(
|
|
1440
|
+
(pad) =>
|
|
1441
|
+
AltiumScene3dPlacementRotationPolicy.#hasDrilledPadOpening(
|
|
1442
|
+
pad
|
|
1443
|
+
) &&
|
|
1444
|
+
!pad?.hasTopPasteMaskOpening &&
|
|
1445
|
+
!pad?.hasBottomPasteMaskOpening
|
|
1446
|
+
)
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
/**
|
|
1450
|
+
* Returns pads owned by one component index.
|
|
1451
|
+
* @param {object} component PCB component.
|
|
1452
|
+
* @param {object[] | undefined} pads Source PCB pads.
|
|
1453
|
+
* @returns {object[]}
|
|
1454
|
+
*/
|
|
1455
|
+
static #ownedPads(component, pads) {
|
|
1456
|
+
const componentIndex = Number(component?.componentIndex)
|
|
1457
|
+
if (!Number.isFinite(componentIndex)) {
|
|
1458
|
+
return []
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
return (Array.isArray(pads) ? pads : []).filter(
|
|
1462
|
+
(pad) => Number(pad?.componentIndex) === componentIndex
|
|
1463
|
+
)
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
/**
|
|
1467
|
+
* Checks whether one pad exposes enough physical size to guide yaw.
|
|
1468
|
+
* @param {object} pad Source PCB pad.
|
|
1469
|
+
* @returns {boolean}
|
|
1470
|
+
*/
|
|
1471
|
+
static #hasMeasurablePadSize(pad) {
|
|
1472
|
+
const width = Math.max(
|
|
1473
|
+
Number(pad?.sizeTopX || 0),
|
|
1474
|
+
Number(pad?.sizeMidX || 0),
|
|
1475
|
+
Number(pad?.sizeBottomX || 0)
|
|
1476
|
+
)
|
|
1477
|
+
const depth = Math.max(
|
|
1478
|
+
Number(pad?.sizeTopY || 0),
|
|
1479
|
+
Number(pad?.sizeMidY || 0),
|
|
1480
|
+
Number(pad?.sizeBottomY || 0)
|
|
1481
|
+
)
|
|
1482
|
+
|
|
1483
|
+
return width > 0 && depth > 0
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
/**
|
|
1487
|
+
* Checks whether one pad is drilled or otherwise through-hole-like.
|
|
1488
|
+
* @param {object} pad Source PCB pad.
|
|
1489
|
+
* @returns {boolean}
|
|
1490
|
+
*/
|
|
1491
|
+
static #hasDrilledPadOpening(pad) {
|
|
1492
|
+
return (
|
|
1493
|
+
Number(pad?.holeDiameter || 0) > 0 ||
|
|
1494
|
+
Number(pad?.holeSize || 0) > 0 ||
|
|
1495
|
+
Number(pad?.drillDiameter || 0) > 0 ||
|
|
1496
|
+
Number(pad?.holeShape || 0) > 0 ||
|
|
1497
|
+
Number(pad?.layerCode || 0) > 16
|
|
1498
|
+
)
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
/**
|
|
1502
|
+
* Checks whether a source component is mounted on the PCB bottom.
|
|
1503
|
+
* @param {object | null | undefined} component PCB component.
|
|
1504
|
+
* @returns {boolean}
|
|
1505
|
+
*/
|
|
1506
|
+
static #isBottomComponent(component) {
|
|
1507
|
+
const layer = String(component?.layer || '').toUpperCase()
|
|
1508
|
+
|
|
1509
|
+
return layer.includes('BOTTOM') || layer === 'BOT'
|
|
1510
|
+
}
|
|
1511
|
+
|
|
790
1512
|
/**
|
|
791
1513
|
* Measures pad center spread on one axis.
|
|
792
1514
|
* @param {object[]} pads Source PCB pads.
|
|
@@ -833,6 +1555,15 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
833
1555
|
return angle === 90 || angle === 270
|
|
834
1556
|
}
|
|
835
1557
|
|
|
1558
|
+
/**
|
|
1559
|
+
* Checks whether one body row carries an explicit board-facing yaw.
|
|
1560
|
+
* @param {object | null | undefined} componentBody Source component body.
|
|
1561
|
+
* @returns {boolean}
|
|
1562
|
+
*/
|
|
1563
|
+
static #hasAuthoredBodyYaw(componentBody) {
|
|
1564
|
+
return Number.isFinite(Number(componentBody?.modelRotationDeg?.z))
|
|
1565
|
+
}
|
|
1566
|
+
|
|
836
1567
|
/**
|
|
837
1568
|
* Counts generic edge-connector identity tokens in package metadata.
|
|
838
1569
|
* @param {string} identityText Package metadata text.
|
|
@@ -906,6 +1637,18 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
906
1637
|
)
|
|
907
1638
|
}
|
|
908
1639
|
|
|
1640
|
+
/**
|
|
1641
|
+
* Checks whether an angle is a quarter-turn after normalization.
|
|
1642
|
+
* @param {number} angle Source angle.
|
|
1643
|
+
* @returns {boolean}
|
|
1644
|
+
*/
|
|
1645
|
+
static #isQuarterTurnAngle(angle) {
|
|
1646
|
+
const normalized =
|
|
1647
|
+
AltiumScene3dPlacementRotationPolicy.#normalizeAngle(angle)
|
|
1648
|
+
|
|
1649
|
+
return normalized === 90 || normalized === 270
|
|
1650
|
+
}
|
|
1651
|
+
|
|
909
1652
|
/**
|
|
910
1653
|
* Checks whether an angle is a standard orthogonal footprint yaw.
|
|
911
1654
|
* @param {number} angle Source angle.
|