altium-toolkit 1.1.33 → 1.1.35
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/docs/model-format.md +2 -3
- package/package.json +1 -1
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +255 -1
- package/src/core/altium/AltiumPcbLibExporter.mjs +6 -1
- package/src/core/altium/PcbComponentBodyPlacementNormalizer.mjs +120 -15
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +14 -3
- package/src/core/altium/PcbShapeBasedBodyGeometryParser.mjs +85 -11
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +31 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/ui/AltiumScene3dAuthoredBodyAnchorAdapter.mjs +30 -2
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +6 -2
- package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +44 -0
- package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +171 -6
- package/src/ui/AltiumScene3dShapeStackOwnerAdapter.mjs +82 -3
- package/src/ui/AltiumScene3dTwoRowFootprintDetector.mjs +90 -0
- package/src/ui/PcbScene3dBuilder.mjs +470 -20
- package/src/ui/PcbScene3dCopperRegionDetailBuilder.mjs +280 -0
- package/src/ui/PcbScene3dModelRegistry.mjs +16 -0
- package/src/ui/PcbScene3dPackageDimensionResolver.mjs +107 -0
- package/src/ui/PcbScene3dPackages.mjs +15 -3
- package/src/ui/PcbScene3dPadYawResolver.mjs +200 -0
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +335 -29
- package/src/ui/PcbScene3dStaticBodyRecovery.mjs +891 -0
- package/src/ui/PcbScene3dStaticBodySelectionKeyBuilder.mjs +358 -0
- package/src/ui/PcbScene3dStaticBodySymmetryRecovery.mjs +876 -0
|
@@ -7,7 +7,10 @@ export class AltiumScene3dAuthoredBodyAnchorAdapter {
|
|
|
7
7
|
static #BODY_ANCHOR_TOLERANCE_MIL = 5
|
|
8
8
|
static #AUTHORED_SOURCE = 'authored-body-anchor'
|
|
9
9
|
static #AUTHORED_ANCHOR_IDENTITY_PATTERN =
|
|
10
|
-
/(?:^|[^a-z0-9])(?:antenna|coax|connector|edge|header|jack|mechanical|module|mount|shield|sma|socket
|
|
10
|
+
/(?:^|[^a-z0-9])(?:antenna|coax|connector|edge|header|jack|mechanical|module|mount|shield|sma|socket)(?:$|[^a-z0-9])/i
|
|
11
|
+
static #USB_ANCHOR_IDENTITY_PATTERN = /(?:^|[^a-z0-9])usb(?:$|[^a-z0-9])/i
|
|
12
|
+
static #INTEGRATED_CIRCUIT_PACKAGE_PATTERN =
|
|
13
|
+
/(?:^|[^a-z0-9])(?:u?qfn|v?qfn|dfn|qfp|lqfp|tqfp|bga|lga|sop|soic|ssop|tssop|msop|so[-_ ]?\d+)(?:[-_ ]?\d+)?(?:$|[^a-z0-9])/i
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* Marks off-anchor explicit Altium body placements so the runtime does not
|
|
@@ -149,11 +152,36 @@ export class AltiumScene3dAuthoredBodyAnchorAdapter {
|
|
|
149
152
|
* @returns {boolean}
|
|
150
153
|
*/
|
|
151
154
|
static #hasAuthoredAnchorIdentity(placement, component) {
|
|
152
|
-
|
|
155
|
+
const identityText =
|
|
153
156
|
AltiumScene3dAuthoredBodyAnchorAdapter.#identityText(
|
|
154
157
|
placement,
|
|
155
158
|
component
|
|
156
159
|
)
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
AltiumScene3dAuthoredBodyAnchorAdapter.#AUTHORED_ANCHOR_IDENTITY_PATTERN.test(
|
|
163
|
+
identityText
|
|
164
|
+
) ||
|
|
165
|
+
AltiumScene3dAuthoredBodyAnchorAdapter.#hasUsbHardwareAnchorIdentity(
|
|
166
|
+
identityText
|
|
167
|
+
)
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Allows USB connector-like anchors without treating USB interface ICs as
|
|
173
|
+
* authored mechanical anchors.
|
|
174
|
+
* @param {string} identityText Searchable package identity.
|
|
175
|
+
* @returns {boolean}
|
|
176
|
+
*/
|
|
177
|
+
static #hasUsbHardwareAnchorIdentity(identityText) {
|
|
178
|
+
return (
|
|
179
|
+
AltiumScene3dAuthoredBodyAnchorAdapter.#USB_ANCHOR_IDENTITY_PATTERN.test(
|
|
180
|
+
identityText
|
|
181
|
+
) &&
|
|
182
|
+
!AltiumScene3dAuthoredBodyAnchorAdapter.#INTEGRATED_CIRCUIT_PACKAGE_PATTERN.test(
|
|
183
|
+
identityText
|
|
184
|
+
)
|
|
157
185
|
)
|
|
158
186
|
}
|
|
159
187
|
|
|
@@ -752,8 +752,12 @@ export class AltiumScene3dExternalPlacementAdapter {
|
|
|
752
752
|
180
|
|
753
753
|
const cos = Math.cos(rotationRad)
|
|
754
754
|
const sin = Math.sin(rotationRad)
|
|
755
|
-
const
|
|
756
|
-
|
|
755
|
+
const sourceY =
|
|
756
|
+
AltiumScene3dExternalPlacementAdapter.#isBottomPlacement(placement)
|
|
757
|
+
? -Number(offset?.y || 0)
|
|
758
|
+
: Number(offset?.y || 0)
|
|
759
|
+
const x = Number(offset?.x || 0) * cos - sourceY * sin
|
|
760
|
+
const y = Number(offset?.x || 0) * sin + sourceY * cos
|
|
757
761
|
|
|
758
762
|
return {
|
|
759
763
|
x: Math.abs(x) < Number.EPSILON ? 0 : Number(x.toFixed(10)),
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { AltiumScene3dTwoRowFootprintDetector } from './AltiumScene3dTwoRowFootprintDetector.mjs'
|
|
2
|
+
|
|
1
3
|
const PASSIVE_BODY_PATTERN =
|
|
2
4
|
/(?:^|[^a-z0-9])(?:cap|capacitor|res|resistor|ind|inductor|ferrite|bead|crystal|xtal|lqw|lqg)(?:$|[^a-z0-9])/i
|
|
3
5
|
const CHIP_PASSIVE_BODY_PATTERN =
|
|
@@ -6,6 +8,8 @@ const LOCAL_Y_CHIP_PASSIVE_MODEL_PATTERN =
|
|
|
6
8
|
/(?:^|[^a-z0-9])local[-_ ]?y(?:$|[^a-z0-9])/i
|
|
7
9
|
const PIN_ONE_CORNER_PACKAGE_PATTERN =
|
|
8
10
|
/(?:^|[^a-z0-9])(?:[avw]?qfn|vfqfn|wqfn|v?qfp|lqfp|tqfp|pqfp|mqfp)(?:[0-9]+)?(?:$|[^a-z0-9])/i
|
|
11
|
+
const TWO_ROW_PIN_ONE_PACKAGE_PATTERN =
|
|
12
|
+
/(?:^|[^a-z0-9])(?:msop|qsop|soic|sop|ssop|tssop|tsop|vsop)(?:[-_ ]?\d+)?(?:$|[^a-z0-9])/i
|
|
9
13
|
const FIVE_LEAD_SOT_PATTERN =
|
|
10
14
|
/(?:^|[^a-z0-9])sot[-_ ]?(?:23[-_ ]?5|25|5)(?:$|[^a-z0-9])/i
|
|
11
15
|
const THREE_LEAD_SOT23_PATTERN =
|
|
@@ -39,6 +43,9 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
39
43
|
AltiumScene3dPlacementRotationPolicy.#needsSquarePinOneCorrection(
|
|
40
44
|
context
|
|
41
45
|
) ||
|
|
46
|
+
AltiumScene3dPlacementRotationPolicy.#needsTwoRowPinOneCorrection(
|
|
47
|
+
context
|
|
48
|
+
) ||
|
|
42
49
|
AltiumScene3dPlacementRotationPolicy.#needsFiveLeadSotCorrection(
|
|
43
50
|
context
|
|
44
51
|
) ||
|
|
@@ -211,6 +218,43 @@ export class AltiumScene3dPlacementRotationPolicy {
|
|
|
211
218
|
)
|
|
212
219
|
}
|
|
213
220
|
|
|
221
|
+
/**
|
|
222
|
+
* Detects two-row IC packages whose embedded source frame puts the
|
|
223
|
+
* package pin-one corner opposite the footprint convention.
|
|
224
|
+
* @param {{ placement?: object, component?: object | null, componentBody?: object | null, pads?: object[] }} context Rotation context.
|
|
225
|
+
* @returns {boolean}
|
|
226
|
+
*/
|
|
227
|
+
static #needsTwoRowPinOneCorrection(context) {
|
|
228
|
+
const { placement, component, componentBody, pads } = context || {}
|
|
229
|
+
if (
|
|
230
|
+
!AltiumScene3dPlacementRotationPolicy.#isPadFallback(placement) ||
|
|
231
|
+
!component ||
|
|
232
|
+
String(placement?.mountSide || '').toLowerCase() !== 'top' ||
|
|
233
|
+
AltiumScene3dPlacementRotationPolicy.#isGenericPassiveBody(
|
|
234
|
+
componentBody
|
|
235
|
+
) ||
|
|
236
|
+
!AltiumScene3dPlacementRotationPolicy.#hasRightAngleModelTilt(
|
|
237
|
+
componentBody
|
|
238
|
+
)
|
|
239
|
+
) {
|
|
240
|
+
return false
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const identityText =
|
|
244
|
+
AltiumScene3dPlacementRotationPolicy.#packageIdentityText(
|
|
245
|
+
component,
|
|
246
|
+
componentBody
|
|
247
|
+
)
|
|
248
|
+
if (!TWO_ROW_PIN_ONE_PACKAGE_PATTERN.test(identityText)) {
|
|
249
|
+
return false
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return AltiumScene3dTwoRowFootprintDetector.hasTwoRowSurfaceFootprint(
|
|
253
|
+
component,
|
|
254
|
+
pads
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
214
258
|
/**
|
|
215
259
|
* Checks whether a source package yaw already matches the footprint and
|
|
216
260
|
* does not need another pin-one half-turn.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { PcbScene3dPackageDimensionResolver } from './PcbScene3dPackageDimensionResolver.mjs'
|
|
2
|
+
|
|
1
3
|
const CONNECTOR_TOKENS = new Set([
|
|
2
4
|
'antenna',
|
|
3
5
|
'coax',
|
|
@@ -14,9 +16,14 @@ const TIMING_PACKAGE_PATTERN =
|
|
|
14
16
|
/(?:^|[^a-z0-9])(?:clock|crystal|osc|oscillator|resonator|tcxo|txco|xtal)(?:$|[^a-z0-9])/i
|
|
15
17
|
const TIMING_DESIGNATOR_PATTERN = /^(?:y|xo)\d+[a-z]?$/i
|
|
16
18
|
const AUTHORED_ANCHOR_IDENTITY_PATTERN =
|
|
17
|
-
/(?:^|[^a-z0-9])(?:antenna|coax|connector|edge|header|jack|mechanical|module|mount|shield|sma|socket
|
|
19
|
+
/(?:^|[^a-z0-9])(?:antenna|coax|connector|edge|header|jack|mechanical|module|mount|shield|sma|socket)(?:$|[^a-z0-9])/i
|
|
18
20
|
const PAD_FALLBACK_AUTHORED_ANCHOR_PATTERN =
|
|
19
|
-
/(?:^|[^a-z0-9])(?:antenna|coax|conn|connector|edge|flex|fpc|frame|hardware|header|jack|mechanical|module|shield|sma|socket
|
|
21
|
+
/(?:^|[^a-z0-9])(?:antenna|coax|conn|connector|edge|flex|fpc|frame|hardware|header|jack|mechanical|module|shield|sma|socket)(?:$|[^a-z0-9])/i
|
|
22
|
+
const USB_ANCHOR_IDENTITY_PATTERN = /(?:^|[^a-z0-9])usb(?:$|[^a-z0-9])/i
|
|
23
|
+
const INTEGRATED_CIRCUIT_PACKAGE_PATTERN =
|
|
24
|
+
/(?:^|[^a-z0-9])(?:u?qfn|v?qfn|dfn|qfp|lqfp|tqfp|bga|lga|sop|soic|ssop|tssop|msop|so[-_ ]?\d+)(?:[-_ ]?\d+)?(?:$|[^a-z0-9])/i
|
|
25
|
+
const SHIELD_COVER_TOKEN_PATTERN =
|
|
26
|
+
/(?:^|[^a-z0-9])(?:rf|emi|rfi|shield|cover)(?:$|[^a-z0-9])/gi
|
|
20
27
|
|
|
21
28
|
/**
|
|
22
29
|
* Repairs repeated Altium model-anchor bodies by matching their shared source
|
|
@@ -228,8 +235,20 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
|
|
|
228
235
|
String(placement?.projection?.source || '').toLowerCase() ===
|
|
229
236
|
'pad-fallback'
|
|
230
237
|
) {
|
|
238
|
+
if (
|
|
239
|
+
AltiumScene3dRepeatedModelOwnerRepair.#hasShieldCoverSourceOriginOffset(
|
|
240
|
+
placement,
|
|
241
|
+
component,
|
|
242
|
+
identityText
|
|
243
|
+
)
|
|
244
|
+
) {
|
|
245
|
+
return false
|
|
246
|
+
}
|
|
247
|
+
|
|
231
248
|
return (
|
|
232
|
-
|
|
249
|
+
AltiumScene3dRepeatedModelOwnerRepair.#hasPadFallbackAuthoredAnchorIdentity(
|
|
250
|
+
identityText
|
|
251
|
+
) ||
|
|
233
252
|
AltiumScene3dRepeatedModelOwnerRepair.#hasCompactIcSourceOriginOffset(
|
|
234
253
|
placement,
|
|
235
254
|
component,
|
|
@@ -238,7 +257,149 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
|
|
|
238
257
|
)
|
|
239
258
|
}
|
|
240
259
|
|
|
241
|
-
return
|
|
260
|
+
return AltiumScene3dRepeatedModelOwnerRepair.#hasAuthoredAnchorIdentity(
|
|
261
|
+
identityText
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Checks whether pad-fallback identity describes an authored hardware
|
|
267
|
+
* anchor instead of a source-origin-biased IC body.
|
|
268
|
+
* @param {string} identityText Searchable package identity.
|
|
269
|
+
* @returns {boolean}
|
|
270
|
+
*/
|
|
271
|
+
static #hasPadFallbackAuthoredAnchorIdentity(identityText) {
|
|
272
|
+
return (
|
|
273
|
+
PAD_FALLBACK_AUTHORED_ANCHOR_PATTERN.test(identityText) ||
|
|
274
|
+
AltiumScene3dRepeatedModelOwnerRepair.#hasUsbHardwareAnchorIdentity(
|
|
275
|
+
identityText
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Checks whether non-pad-fallback identity describes authored hardware.
|
|
282
|
+
* @param {string} identityText Searchable package identity.
|
|
283
|
+
* @returns {boolean}
|
|
284
|
+
*/
|
|
285
|
+
static #hasAuthoredAnchorIdentity(identityText) {
|
|
286
|
+
return (
|
|
287
|
+
AUTHORED_ANCHOR_IDENTITY_PATTERN.test(identityText) ||
|
|
288
|
+
AltiumScene3dRepeatedModelOwnerRepair.#hasUsbHardwareAnchorIdentity(
|
|
289
|
+
identityText
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Allows USB connector-like anchors without treating USB interface ICs as
|
|
296
|
+
* authored mechanical anchors.
|
|
297
|
+
* @param {string} identityText Searchable package identity.
|
|
298
|
+
* @returns {boolean}
|
|
299
|
+
*/
|
|
300
|
+
static #hasUsbHardwareAnchorIdentity(identityText) {
|
|
301
|
+
return (
|
|
302
|
+
USB_ANCHOR_IDENTITY_PATTERN.test(identityText) &&
|
|
303
|
+
!INTEGRATED_CIRCUIT_PACKAGE_PATTERN.test(identityText)
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Checks whether a shield cover body origin is a package corner.
|
|
309
|
+
* @param {object} placement Scene placement.
|
|
310
|
+
* @param {object} component PCB component.
|
|
311
|
+
* @param {string} identityText Searchable package identity.
|
|
312
|
+
* @returns {boolean}
|
|
313
|
+
*/
|
|
314
|
+
static #hasShieldCoverSourceOriginOffset(
|
|
315
|
+
placement,
|
|
316
|
+
component,
|
|
317
|
+
identityText
|
|
318
|
+
) {
|
|
319
|
+
const tokens = new Set(
|
|
320
|
+
[...String(identityText || '').matchAll(SHIELD_COVER_TOKEN_PATTERN)]
|
|
321
|
+
.map((match) => String(match[0] || '').toLowerCase())
|
|
322
|
+
.map((token) => token.replace(/[^a-z0-9]+/giu, ''))
|
|
323
|
+
)
|
|
324
|
+
if (
|
|
325
|
+
!tokens.has('cover') ||
|
|
326
|
+
!['rf', 'emi', 'rfi', 'shield'].some((token) => tokens.has(token))
|
|
327
|
+
) {
|
|
328
|
+
return false
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const size =
|
|
332
|
+
PcbScene3dPackageDimensionResolver.resolvePlanarSize(component)
|
|
333
|
+
if (!size) {
|
|
334
|
+
return false
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const offset = {
|
|
338
|
+
x:
|
|
339
|
+
Number(placement?.bodyPositionMil?.x || 0) -
|
|
340
|
+
Number(component?.x || 0),
|
|
341
|
+
y:
|
|
342
|
+
Number(placement?.bodyPositionMil?.y || 0) -
|
|
343
|
+
Number(component?.y || 0)
|
|
344
|
+
}
|
|
345
|
+
if (
|
|
346
|
+
!Number.isFinite(offset.x) ||
|
|
347
|
+
!Number.isFinite(offset.y) ||
|
|
348
|
+
Math.hypot(offset.x, offset.y) <=
|
|
349
|
+
AltiumScene3dRepeatedModelOwnerRepair
|
|
350
|
+
.#MIN_PAD_FALLBACK_OWNER_OFFSET_MIL
|
|
351
|
+
) {
|
|
352
|
+
return false
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return AltiumScene3dRepeatedModelOwnerRepair.#matchesCornerOriginSize(
|
|
356
|
+
Math.abs(offset.x) * 2,
|
|
357
|
+
Math.abs(offset.y) * 2,
|
|
358
|
+
size
|
|
359
|
+
)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/**
|
|
363
|
+
* Checks whether doubled owner offset matches package dimensions.
|
|
364
|
+
* @param {number} offsetWidth Doubled X offset.
|
|
365
|
+
* @param {number} offsetDepth Doubled Y offset.
|
|
366
|
+
* @param {{ width: number, depth: number }} size Explicit package size.
|
|
367
|
+
* @returns {boolean}
|
|
368
|
+
*/
|
|
369
|
+
static #matchesCornerOriginSize(offsetWidth, offsetDepth, size) {
|
|
370
|
+
return (
|
|
371
|
+
(AltiumScene3dRepeatedModelOwnerRepair.#matchesDimension(
|
|
372
|
+
offsetWidth,
|
|
373
|
+
size.width
|
|
374
|
+
) &&
|
|
375
|
+
AltiumScene3dRepeatedModelOwnerRepair.#matchesDimension(
|
|
376
|
+
offsetDepth,
|
|
377
|
+
size.depth
|
|
378
|
+
)) ||
|
|
379
|
+
(AltiumScene3dRepeatedModelOwnerRepair.#matchesDimension(
|
|
380
|
+
offsetWidth,
|
|
381
|
+
size.depth
|
|
382
|
+
) &&
|
|
383
|
+
AltiumScene3dRepeatedModelOwnerRepair.#matchesDimension(
|
|
384
|
+
offsetDepth,
|
|
385
|
+
size.width
|
|
386
|
+
))
|
|
387
|
+
)
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Checks whether two dimensions are close enough for source-origin repair.
|
|
392
|
+
* @param {number} actual Actual dimension.
|
|
393
|
+
* @param {number} expected Expected dimension.
|
|
394
|
+
* @returns {boolean}
|
|
395
|
+
*/
|
|
396
|
+
static #matchesDimension(actual, expected) {
|
|
397
|
+
if (!Number.isFinite(actual) || !Number.isFinite(expected)) {
|
|
398
|
+
return false
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const tolerance = Math.max(35, Math.abs(expected) * 0.08)
|
|
402
|
+
return Math.abs(actual - expected) <= tolerance
|
|
242
403
|
}
|
|
243
404
|
|
|
244
405
|
/**
|
|
@@ -557,8 +718,12 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
|
|
|
557
718
|
180
|
|
558
719
|
const cos = Math.cos(rotationRad)
|
|
559
720
|
const sin = Math.sin(rotationRad)
|
|
560
|
-
const
|
|
561
|
-
|
|
721
|
+
const sourceY =
|
|
722
|
+
AltiumScene3dRepeatedModelOwnerRepair.#isBottomPlacement(placement)
|
|
723
|
+
? -Number(offset?.y || 0)
|
|
724
|
+
: Number(offset?.y || 0)
|
|
725
|
+
const x = Number(offset?.x || 0) * cos - sourceY * sin
|
|
726
|
+
const y = Number(offset?.x || 0) * sin + sourceY * cos
|
|
562
727
|
const z = Number(
|
|
563
728
|
modelTransform?.offsetMil?.z ?? modelTransform?.dzMil ?? 0
|
|
564
729
|
)
|
|
@@ -7,6 +7,7 @@ const TIMING_DESIGNATOR_PATTERN = /^(?:y|xo)\d+[a-z]?$/i
|
|
|
7
7
|
*/
|
|
8
8
|
export class AltiumScene3dShapeStackOwnerAdapter {
|
|
9
9
|
static #BASE_OWNER_TOLERANCE_MIL = 60
|
|
10
|
+
static #EXACT_OWNER_TOLERANCE_MIL = 1
|
|
10
11
|
static #STACK_BODY_RADIUS_MIL = 220
|
|
11
12
|
static #HEIGHT_TOLERANCE_MIL = 0.1
|
|
12
13
|
|
|
@@ -69,7 +70,8 @@ export class AltiumScene3dShapeStackOwnerAdapter {
|
|
|
69
70
|
AltiumScene3dShapeStackOwnerAdapter.#repairStaticPlacements(
|
|
70
71
|
sceneDescription.staticBodyPlacements,
|
|
71
72
|
componentBodies,
|
|
72
|
-
assignments
|
|
73
|
+
assignments,
|
|
74
|
+
sceneDescription.board
|
|
73
75
|
)
|
|
74
76
|
}
|
|
75
77
|
}
|
|
@@ -146,9 +148,15 @@ export class AltiumScene3dShapeStackOwnerAdapter {
|
|
|
146
148
|
* @param {object[] | undefined} placements Scene static body placements.
|
|
147
149
|
* @param {object[]} componentBodies Source component-body rows.
|
|
148
150
|
* @param {object[]} assignments Carrier assignments.
|
|
151
|
+
* @param {object | undefined} board Scene board metadata.
|
|
149
152
|
* @returns {object[]}
|
|
150
153
|
*/
|
|
151
|
-
static #repairStaticPlacements(
|
|
154
|
+
static #repairStaticPlacements(
|
|
155
|
+
placements,
|
|
156
|
+
componentBodies,
|
|
157
|
+
assignments,
|
|
158
|
+
board
|
|
159
|
+
) {
|
|
152
160
|
return (Array.isArray(placements) ? placements : []).map(
|
|
153
161
|
(placement) => {
|
|
154
162
|
const assignment = assignments.find((candidate) =>
|
|
@@ -164,12 +172,37 @@ export class AltiumScene3dShapeStackOwnerAdapter {
|
|
|
164
172
|
return {
|
|
165
173
|
...placement,
|
|
166
174
|
designator: String(assignment.owner.designator || ''),
|
|
175
|
+
positionMil:
|
|
176
|
+
AltiumScene3dShapeStackOwnerAdapter.#withOwnerAnchor(
|
|
177
|
+
placement?.positionMil,
|
|
178
|
+
assignment.owner,
|
|
179
|
+
board
|
|
180
|
+
),
|
|
167
181
|
coLocatedVariantGroupKey: assignment.groupKey
|
|
168
182
|
}
|
|
169
183
|
}
|
|
170
184
|
)
|
|
171
185
|
}
|
|
172
186
|
|
|
187
|
+
/**
|
|
188
|
+
* Re-anchors a repaired static carrier on the owning component position.
|
|
189
|
+
* @param {object | undefined} position Existing render position.
|
|
190
|
+
* @param {{ x?: number, y?: number }} owner Owner component.
|
|
191
|
+
* @param {object | undefined} board Scene board metadata.
|
|
192
|
+
* @returns {{ x: number, y: number, z: number }}
|
|
193
|
+
*/
|
|
194
|
+
static #withOwnerAnchor(position, owner, board) {
|
|
195
|
+
return {
|
|
196
|
+
...(position || {}),
|
|
197
|
+
x: AltiumScene3dShapeStackOwnerAdapter.#round(
|
|
198
|
+
Number(owner?.x || 0) - Number(board?.centerX || 0)
|
|
199
|
+
),
|
|
200
|
+
y: AltiumScene3dShapeStackOwnerAdapter.#round(
|
|
201
|
+
Number(owner?.y || 0) - Number(board?.centerY || 0)
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
173
206
|
/**
|
|
174
207
|
* Builds carrier-base to owner-component assignments.
|
|
175
208
|
* @param {object[]} componentBodies Source component-body rows.
|
|
@@ -187,6 +220,15 @@ export class AltiumScene3dShapeStackOwnerAdapter {
|
|
|
187
220
|
AltiumScene3dShapeStackOwnerAdapter.#groupCarrierBases(bases)
|
|
188
221
|
|
|
189
222
|
return groups.flatMap((group) => {
|
|
223
|
+
if (
|
|
224
|
+
AltiumScene3dShapeStackOwnerAdapter.#hasExactNonTimingOwner(
|
|
225
|
+
group.anchor,
|
|
226
|
+
components
|
|
227
|
+
)
|
|
228
|
+
) {
|
|
229
|
+
return []
|
|
230
|
+
}
|
|
231
|
+
|
|
190
232
|
const owners = timingComponents
|
|
191
233
|
.filter(
|
|
192
234
|
(component) =>
|
|
@@ -242,6 +284,39 @@ export class AltiumScene3dShapeStackOwnerAdapter {
|
|
|
242
284
|
})
|
|
243
285
|
}
|
|
244
286
|
|
|
287
|
+
/**
|
|
288
|
+
* Checks whether one carrier base already sits on a non-timing component.
|
|
289
|
+
* @param {{ x?: number, y?: number }} anchor Carrier base anchor.
|
|
290
|
+
* @param {object[]} components Source components.
|
|
291
|
+
* @returns {boolean}
|
|
292
|
+
*/
|
|
293
|
+
static #hasExactNonTimingOwner(anchor, components) {
|
|
294
|
+
const exactOwners = (
|
|
295
|
+
Array.isArray(components) ? components : []
|
|
296
|
+
).filter(
|
|
297
|
+
(component) =>
|
|
298
|
+
AltiumScene3dShapeStackOwnerAdapter.#distance(
|
|
299
|
+
anchor,
|
|
300
|
+
component
|
|
301
|
+
) <=
|
|
302
|
+
AltiumScene3dShapeStackOwnerAdapter.#EXACT_OWNER_TOLERANCE_MIL
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
return (
|
|
306
|
+
exactOwners.some(
|
|
307
|
+
(component) =>
|
|
308
|
+
!AltiumScene3dShapeStackOwnerAdapter.#isTimingComponent(
|
|
309
|
+
component
|
|
310
|
+
)
|
|
311
|
+
) &&
|
|
312
|
+
!exactOwners.some((component) =>
|
|
313
|
+
AltiumScene3dShapeStackOwnerAdapter.#isTimingComponent(
|
|
314
|
+
component
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
245
320
|
/**
|
|
246
321
|
* Groups carrier bases that share one source anchor.
|
|
247
322
|
* @param {object[]} bases Candidate carrier bases.
|
|
@@ -469,7 +544,11 @@ export class AltiumScene3dShapeStackOwnerAdapter {
|
|
|
469
544
|
return (
|
|
470
545
|
componentBodies
|
|
471
546
|
.filter(
|
|
472
|
-
(componentBody) =>
|
|
547
|
+
(componentBody) =>
|
|
548
|
+
!usedComponentBodies.has(componentBody) &&
|
|
549
|
+
!AltiumScene3dShapeStackOwnerAdapter.#isCarrierBase(
|
|
550
|
+
componentBody
|
|
551
|
+
)
|
|
473
552
|
)
|
|
474
553
|
.map((componentBody) => ({
|
|
475
554
|
componentBody,
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detects two-row surface-mount footprints from normalized PCB pads.
|
|
3
|
+
*/
|
|
4
|
+
export class AltiumScene3dTwoRowFootprintDetector {
|
|
5
|
+
/**
|
|
6
|
+
* Checks whether a component owns a symmetric two-row surface footprint.
|
|
7
|
+
* @param {object} component PCB component.
|
|
8
|
+
* @param {object[]} pads Source PCB pads.
|
|
9
|
+
* @returns {boolean}
|
|
10
|
+
*/
|
|
11
|
+
static hasTwoRowSurfaceFootprint(component, pads) {
|
|
12
|
+
const surfacePads = AltiumScene3dTwoRowFootprintDetector.#surfacePads(
|
|
13
|
+
component,
|
|
14
|
+
pads
|
|
15
|
+
)
|
|
16
|
+
if (surfacePads.length < 6) {
|
|
17
|
+
return false
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const xSpread = AltiumScene3dTwoRowFootprintDetector.#spread(
|
|
21
|
+
surfacePads,
|
|
22
|
+
'x'
|
|
23
|
+
)
|
|
24
|
+
const ySpread = AltiumScene3dTwoRowFootprintDetector.#spread(
|
|
25
|
+
surfacePads,
|
|
26
|
+
'y'
|
|
27
|
+
)
|
|
28
|
+
const rowAxis = xSpread >= ySpread ? 'y' : 'x'
|
|
29
|
+
const lineAxis = rowAxis === 'x' ? 'y' : 'x'
|
|
30
|
+
const rowSpread = AltiumScene3dTwoRowFootprintDetector.#spread(
|
|
31
|
+
surfacePads,
|
|
32
|
+
rowAxis
|
|
33
|
+
)
|
|
34
|
+
const lineSpread = AltiumScene3dTwoRowFootprintDetector.#spread(
|
|
35
|
+
surfacePads,
|
|
36
|
+
lineAxis
|
|
37
|
+
)
|
|
38
|
+
if (rowSpread <= 0 || lineSpread <= 0) {
|
|
39
|
+
return false
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const values = surfacePads.map((pad) => Number(pad?.[rowAxis] || 0))
|
|
43
|
+
const midpoint = (Math.min(...values) + Math.max(...values)) / 2
|
|
44
|
+
const lowerCount = values.filter((value) => value <= midpoint).length
|
|
45
|
+
const upperCount = values.length - lowerCount
|
|
46
|
+
|
|
47
|
+
return Math.min(lowerCount, upperCount) >= 3
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Collects surface pads owned by one component.
|
|
52
|
+
* @param {object} component PCB component.
|
|
53
|
+
* @param {object[]} pads Source PCB pads.
|
|
54
|
+
* @returns {object[]}
|
|
55
|
+
*/
|
|
56
|
+
static #surfacePads(component, pads) {
|
|
57
|
+
const componentIndex = Number(component?.componentIndex)
|
|
58
|
+
if (!Number.isFinite(componentIndex)) {
|
|
59
|
+
return []
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const ownedPads = (Array.isArray(pads) ? pads : []).filter(
|
|
63
|
+
(pad) => Number(pad?.componentIndex) === componentIndex
|
|
64
|
+
)
|
|
65
|
+
const bottom =
|
|
66
|
+
String(component?.layer || '')
|
|
67
|
+
.toUpperCase()
|
|
68
|
+
.includes('BOTTOM') ||
|
|
69
|
+
String(component?.layer || '').toUpperCase() === 'BOT'
|
|
70
|
+
const surfacePads = ownedPads.filter((pad) =>
|
|
71
|
+
bottom
|
|
72
|
+
? Boolean(pad?.hasBottomPasteMaskOpening)
|
|
73
|
+
: Boolean(pad?.hasTopPasteMaskOpening)
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
return surfacePads.length ? surfacePads : ownedPads
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Measures pad center spread on one axis.
|
|
81
|
+
* @param {object[]} pads Source PCB pads.
|
|
82
|
+
* @param {'x' | 'y'} axis Axis key.
|
|
83
|
+
* @returns {number}
|
|
84
|
+
*/
|
|
85
|
+
static #spread(pads, axis) {
|
|
86
|
+
const values = pads.map((pad) => Number(pad?.[axis] || 0))
|
|
87
|
+
|
|
88
|
+
return Math.max(...values) - Math.min(...values)
|
|
89
|
+
}
|
|
90
|
+
}
|