altium-toolkit 1.1.35 → 1.1.37
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 +17 -2
- package/package.json +1 -1
- package/src/core/altium/PrintableTextDecoder.mjs +20 -1
- package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +288 -7
- package/src/core/altium/SchematicTextParser.mjs +18 -253
- package/src/core/altium/SchematicTitleBlockParser.mjs +410 -0
- package/src/core/altium/SelectedPartAltiumExportAdapter.mjs +215 -0
- package/src/parser.mjs +1 -0
- 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 +766 -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 +286 -135
- 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/PcbScene3dPackages.mjs +16 -1
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +262 -22
- package/src/ui/PcbScene3dStaticBodyOwnerPromotion.mjs +984 -0
- package/src/ui/PcbScene3dStaticBodyPadOwnerPromotion.mjs +378 -0
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +169 -23
- package/src/ui/PcbScene3dStaticBodyPrototypeRecovery.mjs +445 -0
- package/src/ui/PcbScene3dStaticBodyRecovery.mjs +7 -1
- package/src/ui/PcbScene3dStaticBodySelectionKeyBuilder.mjs +199 -2
- package/src/ui/PcbScene3dStaticBodySymmetricOwnerPromotion.mjs +439 -0
- package/src/ui/PcbScene3dStaticBodySymmetryRecovery.mjs +23 -2
- package/src/ui/SchematicColorResolver.mjs +44 -0
- package/src/ui/SchematicLineColorResolver.mjs +1 -1
- package/src/ui/SchematicShapeRenderer.mjs +13 -13
|
@@ -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.
|
|
@@ -354,6 +744,10 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
354
744
|
placement,
|
|
355
745
|
component,
|
|
356
746
|
componentBody
|
|
747
|
+
) &&
|
|
748
|
+
!AltiumScene3dPlacementRotationPolicy.#hasEmbeddedModelBoundsSourceHalfTurn(
|
|
749
|
+
placement,
|
|
750
|
+
component
|
|
357
751
|
)
|
|
358
752
|
) {
|
|
359
753
|
return false
|
|
@@ -365,6 +759,30 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
365
759
|
)
|
|
366
760
|
}
|
|
367
761
|
|
|
762
|
+
/**
|
|
763
|
+
* Detects display modules whose full model-bounds source was authored from
|
|
764
|
+
* the connector edge but currently extends away from its pad row.
|
|
765
|
+
* @param {{ placement?: object, component?: object | null, componentBody?: object | null, pads?: object[] }} context Rotation context.
|
|
766
|
+
* @returns {boolean}
|
|
767
|
+
*/
|
|
768
|
+
static #needsDisplayModelBoundsEdgeCorrection(context) {
|
|
769
|
+
const { placement, component, componentBody, pads } = context || {}
|
|
770
|
+
if (!component) {
|
|
771
|
+
return false
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return AltiumScene3dDisplayModuleYawPolicy.shouldReverseModelBoundsYaw({
|
|
775
|
+
placement,
|
|
776
|
+
component,
|
|
777
|
+
pads,
|
|
778
|
+
identityText:
|
|
779
|
+
AltiumScene3dPlacementRotationPolicy.#packageIdentityText(
|
|
780
|
+
component,
|
|
781
|
+
componentBody
|
|
782
|
+
)
|
|
783
|
+
})
|
|
784
|
+
}
|
|
785
|
+
|
|
368
786
|
/**
|
|
369
787
|
* Detects top-side edge connectors whose tilted model frame points inward
|
|
370
788
|
* unless the authored board-facing yaw is reversed.
|
|
@@ -399,6 +817,55 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
399
817
|
)
|
|
400
818
|
}
|
|
401
819
|
|
|
820
|
+
/**
|
|
821
|
+
* Detects top-side through-hole connectors whose embedded source frame is
|
|
822
|
+
* aligned numerically but board-facing backwards after source mirroring.
|
|
823
|
+
* @param {{ placement?: object, component?: object | null, componentBody?: object | null, pads?: object[], isExactAnchoredOwner?: boolean }} context Rotation context.
|
|
824
|
+
* @returns {boolean}
|
|
825
|
+
*/
|
|
826
|
+
static #needsAlignedThroughHoleConnectorCorrection(context) {
|
|
827
|
+
const { placement, component, componentBody, pads } = context || {}
|
|
828
|
+
const hasAuthoredProjection =
|
|
829
|
+
AltiumScene3dPlacementRotationPolicy.#isAuthoredOrAnchoredProjection(
|
|
830
|
+
context
|
|
831
|
+
) ||
|
|
832
|
+
AltiumScene3dPlacementRotationPolicy.#isModelBoundsProjection(
|
|
833
|
+
placement
|
|
834
|
+
)
|
|
835
|
+
|
|
836
|
+
if (
|
|
837
|
+
!component ||
|
|
838
|
+
String(placement?.mountSide || '').toLowerCase() !== 'top' ||
|
|
839
|
+
String(placement?.externalModel?.origin || '').toLowerCase() !==
|
|
840
|
+
'embedded' ||
|
|
841
|
+
!hasAuthoredProjection ||
|
|
842
|
+
!AltiumScene3dPlacementRotationPolicy.#hasAuthoredBodyYaw(
|
|
843
|
+
componentBody
|
|
844
|
+
) ||
|
|
845
|
+
!AltiumScene3dPlacementRotationPolicy.#sourceYawMatchesComponentYaw(
|
|
846
|
+
placement,
|
|
847
|
+
component,
|
|
848
|
+
componentBody
|
|
849
|
+
)
|
|
850
|
+
) {
|
|
851
|
+
return false
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
const identityText =
|
|
855
|
+
AltiumScene3dPlacementRotationPolicy.#packageIdentityText(
|
|
856
|
+
component,
|
|
857
|
+
componentBody
|
|
858
|
+
)
|
|
859
|
+
if (!THROUGH_HOLE_CONNECTOR_PATTERN.test(identityText)) {
|
|
860
|
+
return false
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
return AltiumScene3dPlacementRotationPolicy.#hasThroughHoleOnlyPads(
|
|
864
|
+
component,
|
|
865
|
+
pads
|
|
866
|
+
)
|
|
867
|
+
}
|
|
868
|
+
|
|
402
869
|
/**
|
|
403
870
|
* Checks whether one placement is exact-anchored or pad-fallback projected.
|
|
404
871
|
* @param {{ placement?: object, isExactAnchoredOwner?: boolean }} context Rotation context.
|
|
@@ -413,6 +880,43 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
413
880
|
)
|
|
414
881
|
}
|
|
415
882
|
|
|
883
|
+
/**
|
|
884
|
+
* Checks whether one projection can use square-package pin-one correction.
|
|
885
|
+
* @param {{ placement?: object, componentBody?: object | null, isExactAnchoredOwner?: boolean }} context Rotation context.
|
|
886
|
+
* @returns {boolean}
|
|
887
|
+
*/
|
|
888
|
+
static #supportsSquarePinOneProjection(context) {
|
|
889
|
+
return (
|
|
890
|
+
AltiumScene3dPlacementRotationPolicy.#isAnchoredOrPadFallback(
|
|
891
|
+
context
|
|
892
|
+
) ||
|
|
893
|
+
AltiumScene3dPlacementRotationPolicy.#isBottomModelBoundsProjection(
|
|
894
|
+
context
|
|
895
|
+
)
|
|
896
|
+
)
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Checks whether a bottom-side embedded model-bounds projection still
|
|
901
|
+
* carries the source-frame signals needed for pin-one normalization.
|
|
902
|
+
* @param {{ placement?: object, componentBody?: object | null }} context Rotation context.
|
|
903
|
+
* @returns {boolean}
|
|
904
|
+
*/
|
|
905
|
+
static #isBottomModelBoundsProjection(context) {
|
|
906
|
+
const placement = context?.placement || {}
|
|
907
|
+
|
|
908
|
+
return (
|
|
909
|
+
String(placement?.projection?.source || '').toLowerCase() ===
|
|
910
|
+
'model-bounds' &&
|
|
911
|
+
String(placement?.mountSide || '').toLowerCase() === 'bottom' &&
|
|
912
|
+
String(placement?.externalModel?.origin || '').toLowerCase() ===
|
|
913
|
+
'embedded' &&
|
|
914
|
+
AltiumScene3dPlacementRotationPolicy.#hasRightAngleModelTilt(
|
|
915
|
+
context?.componentBody
|
|
916
|
+
)
|
|
917
|
+
)
|
|
918
|
+
}
|
|
919
|
+
|
|
416
920
|
/**
|
|
417
921
|
* Checks whether a placement was synthesized from footprint pads.
|
|
418
922
|
* @param {object | undefined} placement External model placement.
|
|
@@ -422,6 +926,70 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
422
926
|
return String(placement?.projection?.source || '') === 'pad-fallback'
|
|
423
927
|
}
|
|
424
928
|
|
|
929
|
+
/**
|
|
930
|
+
* Checks whether one projection keeps an authored Altium owner/body anchor.
|
|
931
|
+
* @param {{ placement?: object, isExactAnchoredOwner?: boolean }} context Rotation context.
|
|
932
|
+
* @returns {boolean}
|
|
933
|
+
*/
|
|
934
|
+
static #isAuthoredOrAnchoredProjection(context) {
|
|
935
|
+
const projectionSource = String(
|
|
936
|
+
context?.placement?.projection?.source || ''
|
|
937
|
+
).toLowerCase()
|
|
938
|
+
|
|
939
|
+
return (
|
|
940
|
+
AltiumScene3dPlacementRotationPolicy.#isAnchoredOrPadFallback(
|
|
941
|
+
context
|
|
942
|
+
) || projectionSource === 'authored-body-anchor'
|
|
943
|
+
)
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Checks whether one placement was projected from resolved model bounds.
|
|
948
|
+
* @param {object | undefined} placement External model placement.
|
|
949
|
+
* @returns {boolean}
|
|
950
|
+
*/
|
|
951
|
+
static #isModelBoundsProjection(placement) {
|
|
952
|
+
return (
|
|
953
|
+
String(placement?.projection?.source || '').toLowerCase() ===
|
|
954
|
+
'model-bounds'
|
|
955
|
+
)
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
/**
|
|
959
|
+
* Checks whether recovered embedded model-bounds metadata carries a source
|
|
960
|
+
* yaw opposite the owning footprint yaw.
|
|
961
|
+
* @param {object | undefined} placement External model placement.
|
|
962
|
+
* @param {object} component PCB component.
|
|
963
|
+
* @returns {boolean}
|
|
964
|
+
*/
|
|
965
|
+
static #hasEmbeddedModelBoundsSourceHalfTurn(placement, component) {
|
|
966
|
+
if (
|
|
967
|
+
!AltiumScene3dPlacementRotationPolicy.#isModelBoundsProjection(
|
|
968
|
+
placement
|
|
969
|
+
) ||
|
|
970
|
+
String(placement?.externalModel?.origin || '').toLowerCase() !==
|
|
971
|
+
'embedded'
|
|
972
|
+
) {
|
|
973
|
+
return false
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
const sourceYaw = Number(
|
|
977
|
+
placement?.externalModel?.transform?.rotationDeg?.z
|
|
978
|
+
)
|
|
979
|
+
if (!Number.isFinite(sourceYaw)) {
|
|
980
|
+
return false
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const componentYaw =
|
|
984
|
+
AltiumScene3dPlacementRotationPolicy.#normalizeAngle(
|
|
985
|
+
Number(component?.rotation || 0)
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
return AltiumScene3dPlacementRotationPolicy.#isHalfTurnAngle(
|
|
989
|
+
sourceYaw - componentYaw
|
|
990
|
+
)
|
|
991
|
+
}
|
|
992
|
+
|
|
425
993
|
/**
|
|
426
994
|
* Checks whether a component owns an asymmetric five-pad footprint.
|
|
427
995
|
* @param {object} component PCB component.
|
|
@@ -688,6 +1256,18 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
688
1256
|
)
|
|
689
1257
|
? bodyLongAxisAngle
|
|
690
1258
|
: bodyLongAxisAngle - 90
|
|
1259
|
+
const bottomFlatTieYaw =
|
|
1260
|
+
AltiumScene3dPlacementRotationPolicy.#resolveBottomFlatThreeLeadSotTieYaw(
|
|
1261
|
+
component,
|
|
1262
|
+
surfacePads,
|
|
1263
|
+
identityText,
|
|
1264
|
+
componentBody,
|
|
1265
|
+
componentYaw,
|
|
1266
|
+
modelYaw
|
|
1267
|
+
)
|
|
1268
|
+
if (bottomFlatTieYaw !== null) {
|
|
1269
|
+
return bottomFlatTieYaw
|
|
1270
|
+
}
|
|
691
1271
|
|
|
692
1272
|
return AltiumScene3dPlacementRotationPolicy.#nearestLineAngle(
|
|
693
1273
|
componentYaw,
|
|
@@ -695,6 +1275,66 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
695
1275
|
)
|
|
696
1276
|
}
|
|
697
1277
|
|
|
1278
|
+
/**
|
|
1279
|
+
* Resolves the mirrored component pin side for flat bottom-side SOT23-3 source
|
|
1280
|
+
* models when long-axis alignment has an exact 180-degree tie.
|
|
1281
|
+
* @param {object} component PCB component.
|
|
1282
|
+
* @param {object[]} surfacePads Surface pads owned by the component.
|
|
1283
|
+
* @param {string} identityText Package metadata text.
|
|
1284
|
+
* @param {object | null | undefined} componentBody Source component body.
|
|
1285
|
+
* @param {number} componentYaw Component yaw.
|
|
1286
|
+
* @param {number} modelYaw Candidate footprint yaw.
|
|
1287
|
+
* @returns {number | null}
|
|
1288
|
+
*/
|
|
1289
|
+
static #resolveBottomFlatThreeLeadSotTieYaw(
|
|
1290
|
+
component,
|
|
1291
|
+
surfacePads,
|
|
1292
|
+
identityText,
|
|
1293
|
+
componentBody,
|
|
1294
|
+
componentYaw,
|
|
1295
|
+
modelYaw
|
|
1296
|
+
) {
|
|
1297
|
+
if (
|
|
1298
|
+
!THREE_LEAD_SOT23_PATTERN.test(identityText) ||
|
|
1299
|
+
surfacePads.length !== 3 ||
|
|
1300
|
+
!AltiumScene3dPlacementRotationPolicy.#isBottomComponent(
|
|
1301
|
+
component
|
|
1302
|
+
) ||
|
|
1303
|
+
AltiumScene3dPlacementRotationPolicy.#hasRightAngleModelTilt(
|
|
1304
|
+
componentBody
|
|
1305
|
+
) ||
|
|
1306
|
+
!AltiumScene3dPlacementRotationPolicy.#usesSourceXLongAxisSot(
|
|
1307
|
+
identityText,
|
|
1308
|
+
componentBody
|
|
1309
|
+
)
|
|
1310
|
+
) {
|
|
1311
|
+
return null
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
const normalizedModelYaw =
|
|
1315
|
+
AltiumScene3dPlacementRotationPolicy.#normalizeAngle(modelYaw)
|
|
1316
|
+
const oppositeModelYaw =
|
|
1317
|
+
AltiumScene3dPlacementRotationPolicy.#normalizeAngle(
|
|
1318
|
+
normalizedModelYaw + 180
|
|
1319
|
+
)
|
|
1320
|
+
const modelDistance =
|
|
1321
|
+
AltiumScene3dPlacementRotationPolicy.#angleDistance(
|
|
1322
|
+
componentYaw,
|
|
1323
|
+
normalizedModelYaw
|
|
1324
|
+
)
|
|
1325
|
+
const oppositeDistance =
|
|
1326
|
+
AltiumScene3dPlacementRotationPolicy.#angleDistance(
|
|
1327
|
+
componentYaw,
|
|
1328
|
+
oppositeModelYaw
|
|
1329
|
+
)
|
|
1330
|
+
|
|
1331
|
+
return Math.abs(modelDistance - oppositeDistance) < 0.001
|
|
1332
|
+
? AltiumScene3dPlacementRotationPolicy.#normalizeAngle(
|
|
1333
|
+
componentYaw + 180
|
|
1334
|
+
)
|
|
1335
|
+
: null
|
|
1336
|
+
}
|
|
1337
|
+
|
|
698
1338
|
/**
|
|
699
1339
|
* Checks whether SOT pad geometry should override the authored yaw.
|
|
700
1340
|
* @param {object | undefined} placement External model placement.
|
|
@@ -765,13 +1405,9 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
765
1405
|
* @returns {object[]}
|
|
766
1406
|
*/
|
|
767
1407
|
static #surfacePads(component, pads) {
|
|
768
|
-
const
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
const ownedPads = (Array.isArray(pads) ? pads : []).filter(
|
|
774
|
-
(pad) => Number(pad?.componentIndex) === componentIndex
|
|
1408
|
+
const ownedPads = AltiumScene3dPlacementRotationPolicy.#ownedPads(
|
|
1409
|
+
component,
|
|
1410
|
+
pads
|
|
775
1411
|
)
|
|
776
1412
|
const bottom =
|
|
777
1413
|
String(component?.layer || '')
|
|
@@ -787,6 +1423,96 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
787
1423
|
return surfacePads.length ? surfacePads : ownedPads
|
|
788
1424
|
}
|
|
789
1425
|
|
|
1426
|
+
/**
|
|
1427
|
+
* Checks whether one component has only directional through-hole pads.
|
|
1428
|
+
* @param {object} component PCB component.
|
|
1429
|
+
* @param {object[] | undefined} pads Source PCB pads.
|
|
1430
|
+
* @returns {boolean}
|
|
1431
|
+
*/
|
|
1432
|
+
static #hasThroughHoleOnlyPads(component, pads) {
|
|
1433
|
+
const measurablePads = AltiumScene3dPlacementRotationPolicy.#ownedPads(
|
|
1434
|
+
component,
|
|
1435
|
+
pads
|
|
1436
|
+
).filter((pad) =>
|
|
1437
|
+
AltiumScene3dPlacementRotationPolicy.#hasMeasurablePadSize(pad)
|
|
1438
|
+
)
|
|
1439
|
+
if (measurablePads.length < 2) {
|
|
1440
|
+
return false
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
return measurablePads.every(
|
|
1444
|
+
(pad) =>
|
|
1445
|
+
AltiumScene3dPlacementRotationPolicy.#hasDrilledPadOpening(
|
|
1446
|
+
pad
|
|
1447
|
+
) &&
|
|
1448
|
+
!pad?.hasTopPasteMaskOpening &&
|
|
1449
|
+
!pad?.hasBottomPasteMaskOpening
|
|
1450
|
+
)
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
/**
|
|
1454
|
+
* Returns pads owned by one component index.
|
|
1455
|
+
* @param {object} component PCB component.
|
|
1456
|
+
* @param {object[] | undefined} pads Source PCB pads.
|
|
1457
|
+
* @returns {object[]}
|
|
1458
|
+
*/
|
|
1459
|
+
static #ownedPads(component, pads) {
|
|
1460
|
+
const componentIndex = Number(component?.componentIndex)
|
|
1461
|
+
if (!Number.isFinite(componentIndex)) {
|
|
1462
|
+
return []
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
return (Array.isArray(pads) ? pads : []).filter(
|
|
1466
|
+
(pad) => Number(pad?.componentIndex) === componentIndex
|
|
1467
|
+
)
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Checks whether one pad exposes enough physical size to guide yaw.
|
|
1472
|
+
* @param {object} pad Source PCB pad.
|
|
1473
|
+
* @returns {boolean}
|
|
1474
|
+
*/
|
|
1475
|
+
static #hasMeasurablePadSize(pad) {
|
|
1476
|
+
const width = Math.max(
|
|
1477
|
+
Number(pad?.sizeTopX || 0),
|
|
1478
|
+
Number(pad?.sizeMidX || 0),
|
|
1479
|
+
Number(pad?.sizeBottomX || 0)
|
|
1480
|
+
)
|
|
1481
|
+
const depth = Math.max(
|
|
1482
|
+
Number(pad?.sizeTopY || 0),
|
|
1483
|
+
Number(pad?.sizeMidY || 0),
|
|
1484
|
+
Number(pad?.sizeBottomY || 0)
|
|
1485
|
+
)
|
|
1486
|
+
|
|
1487
|
+
return width > 0 && depth > 0
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
/**
|
|
1491
|
+
* Checks whether one pad is drilled or otherwise through-hole-like.
|
|
1492
|
+
* @param {object} pad Source PCB pad.
|
|
1493
|
+
* @returns {boolean}
|
|
1494
|
+
*/
|
|
1495
|
+
static #hasDrilledPadOpening(pad) {
|
|
1496
|
+
return (
|
|
1497
|
+
Number(pad?.holeDiameter || 0) > 0 ||
|
|
1498
|
+
Number(pad?.holeSize || 0) > 0 ||
|
|
1499
|
+
Number(pad?.drillDiameter || 0) > 0 ||
|
|
1500
|
+
Number(pad?.holeShape || 0) > 0 ||
|
|
1501
|
+
Number(pad?.layerCode || 0) > 16
|
|
1502
|
+
)
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* Checks whether a source component is mounted on the PCB bottom.
|
|
1507
|
+
* @param {object | null | undefined} component PCB component.
|
|
1508
|
+
* @returns {boolean}
|
|
1509
|
+
*/
|
|
1510
|
+
static #isBottomComponent(component) {
|
|
1511
|
+
const layer = String(component?.layer || '').toUpperCase()
|
|
1512
|
+
|
|
1513
|
+
return layer.includes('BOTTOM') || layer === 'BOT'
|
|
1514
|
+
}
|
|
1515
|
+
|
|
790
1516
|
/**
|
|
791
1517
|
* Measures pad center spread on one axis.
|
|
792
1518
|
* @param {object[]} pads Source PCB pads.
|
|
@@ -833,6 +1559,15 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
833
1559
|
return angle === 90 || angle === 270
|
|
834
1560
|
}
|
|
835
1561
|
|
|
1562
|
+
/**
|
|
1563
|
+
* Checks whether one body row carries an explicit board-facing yaw.
|
|
1564
|
+
* @param {object | null | undefined} componentBody Source component body.
|
|
1565
|
+
* @returns {boolean}
|
|
1566
|
+
*/
|
|
1567
|
+
static #hasAuthoredBodyYaw(componentBody) {
|
|
1568
|
+
return Number.isFinite(Number(componentBody?.modelRotationDeg?.z))
|
|
1569
|
+
}
|
|
1570
|
+
|
|
836
1571
|
/**
|
|
837
1572
|
* Counts generic edge-connector identity tokens in package metadata.
|
|
838
1573
|
* @param {string} identityText Package metadata text.
|
|
@@ -906,6 +1641,18 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
906
1641
|
)
|
|
907
1642
|
}
|
|
908
1643
|
|
|
1644
|
+
/**
|
|
1645
|
+
* Checks whether an angle is a quarter-turn after normalization.
|
|
1646
|
+
* @param {number} angle Source angle.
|
|
1647
|
+
* @returns {boolean}
|
|
1648
|
+
*/
|
|
1649
|
+
static #isQuarterTurnAngle(angle) {
|
|
1650
|
+
const normalized =
|
|
1651
|
+
AltiumScene3dPlacementRotationPolicy.#normalizeAngle(angle)
|
|
1652
|
+
|
|
1653
|
+
return normalized === 90 || normalized === 270
|
|
1654
|
+
}
|
|
1655
|
+
|
|
909
1656
|
/**
|
|
910
1657
|
* Checks whether an angle is a standard orthogonal footprint yaw.
|
|
911
1658
|
* @param {number} angle Source angle.
|