altium-toolkit 1.1.33 → 1.1.36
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 +2 -2
- 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/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/core/altium/SourceComponentBundleNormalizer.mjs +31 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/parser.mjs +1 -0
- 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 +225 -17
- 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 +31 -4
- package/src/ui/PcbScene3dPadYawResolver.mjs +200 -0
- package/src/ui/PcbScene3dStaticBodyOwnerPromotion.mjs +899 -0
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +471 -35
- package/src/ui/PcbScene3dStaticBodyPrototypeRecovery.mjs +445 -0
- package/src/ui/PcbScene3dStaticBodyRecovery.mjs +897 -0
- package/src/ui/PcbScene3dStaticBodySelectionKeyBuilder.mjs +555 -0
- package/src/ui/PcbScene3dStaticBodySymmetricOwnerPromotion.mjs +438 -0
- package/src/ui/PcbScene3dStaticBodySymmetryRecovery.mjs +897 -0
- package/src/ui/SchematicColorResolver.mjs +44 -0
- package/src/ui/SchematicLineColorResolver.mjs +1 -1
- package/src/ui/SchematicShapeRenderer.mjs +13 -13
|
@@ -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,8 +7,11 @@ 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
|
|
13
|
+
static #TIMING_PARAMETER_NAME_PATTERN =
|
|
14
|
+
/(?:^|[^a-z0-9])(?:comment|description|device|function|part|type|value)(?:$|[^a-z0-9])/i
|
|
12
15
|
|
|
13
16
|
/**
|
|
14
17
|
* Applies authored stack ownership to static and external placements.
|
|
@@ -69,7 +72,8 @@ export class AltiumScene3dShapeStackOwnerAdapter {
|
|
|
69
72
|
AltiumScene3dShapeStackOwnerAdapter.#repairStaticPlacements(
|
|
70
73
|
sceneDescription.staticBodyPlacements,
|
|
71
74
|
componentBodies,
|
|
72
|
-
assignments
|
|
75
|
+
assignments,
|
|
76
|
+
sceneDescription.board
|
|
73
77
|
)
|
|
74
78
|
}
|
|
75
79
|
}
|
|
@@ -146,9 +150,15 @@ export class AltiumScene3dShapeStackOwnerAdapter {
|
|
|
146
150
|
* @param {object[] | undefined} placements Scene static body placements.
|
|
147
151
|
* @param {object[]} componentBodies Source component-body rows.
|
|
148
152
|
* @param {object[]} assignments Carrier assignments.
|
|
153
|
+
* @param {object | undefined} board Scene board metadata.
|
|
149
154
|
* @returns {object[]}
|
|
150
155
|
*/
|
|
151
|
-
static #repairStaticPlacements(
|
|
156
|
+
static #repairStaticPlacements(
|
|
157
|
+
placements,
|
|
158
|
+
componentBodies,
|
|
159
|
+
assignments,
|
|
160
|
+
board
|
|
161
|
+
) {
|
|
152
162
|
return (Array.isArray(placements) ? placements : []).map(
|
|
153
163
|
(placement) => {
|
|
154
164
|
const assignment = assignments.find((candidate) =>
|
|
@@ -161,15 +171,122 @@ export class AltiumScene3dShapeStackOwnerAdapter {
|
|
|
161
171
|
return placement
|
|
162
172
|
}
|
|
163
173
|
|
|
164
|
-
return
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
174
|
+
return AltiumScene3dShapeStackOwnerAdapter.#withStaticOwner(
|
|
175
|
+
placement,
|
|
176
|
+
assignment,
|
|
177
|
+
board
|
|
178
|
+
)
|
|
169
179
|
}
|
|
170
180
|
)
|
|
171
181
|
}
|
|
172
182
|
|
|
183
|
+
/**
|
|
184
|
+
* Applies one stack owner to a repaired static carrier body.
|
|
185
|
+
* @param {object} placement Static body placement.
|
|
186
|
+
* @param {{ owner: object, groupKey: string }} assignment Stack assignment.
|
|
187
|
+
* @param {object | undefined} board Scene board metadata.
|
|
188
|
+
* @returns {object}
|
|
189
|
+
*/
|
|
190
|
+
static #withStaticOwner(placement, assignment, board) {
|
|
191
|
+
const ownerDesignator = String(assignment?.owner?.designator || '')
|
|
192
|
+
const mountSide =
|
|
193
|
+
AltiumScene3dShapeStackOwnerAdapter.#mountSide(assignment?.owner) ||
|
|
194
|
+
placement?.mountSide
|
|
195
|
+
const sidePlacement =
|
|
196
|
+
AltiumScene3dShapeStackOwnerAdapter.#withStaticMountSide(
|
|
197
|
+
placement,
|
|
198
|
+
mountSide
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
...sidePlacement,
|
|
203
|
+
designator: ownerDesignator,
|
|
204
|
+
selectionKey: ownerDesignator || sidePlacement.selectionKey,
|
|
205
|
+
positionMil: AltiumScene3dShapeStackOwnerAdapter.#withOwnerAnchor(
|
|
206
|
+
sidePlacement?.positionMil,
|
|
207
|
+
assignment.owner,
|
|
208
|
+
board
|
|
209
|
+
),
|
|
210
|
+
coLocatedVariantGroupKey: assignment.groupKey
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Applies a static placement side while preserving its height magnitude.
|
|
216
|
+
* @param {object} placement Static body placement.
|
|
217
|
+
* @param {string | undefined} mountSide Owner mount side.
|
|
218
|
+
* @returns {object}
|
|
219
|
+
*/
|
|
220
|
+
static #withStaticMountSide(placement, mountSide) {
|
|
221
|
+
const currentSide =
|
|
222
|
+
AltiumScene3dShapeStackOwnerAdapter.#normalizeMountSide(
|
|
223
|
+
placement?.mountSide
|
|
224
|
+
)
|
|
225
|
+
const side = AltiumScene3dShapeStackOwnerAdapter.#normalizeMountSide(
|
|
226
|
+
mountSide || currentSide
|
|
227
|
+
)
|
|
228
|
+
const z = Math.abs(Number(placement?.positionMil?.z || 0))
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
...placement,
|
|
232
|
+
mountSide: side,
|
|
233
|
+
positionMil: {
|
|
234
|
+
...(placement?.positionMil || {}),
|
|
235
|
+
z: side === 'bottom' ? -z : z
|
|
236
|
+
},
|
|
237
|
+
geometry:
|
|
238
|
+
currentSide === side
|
|
239
|
+
? placement?.geometry
|
|
240
|
+
: AltiumScene3dShapeStackOwnerAdapter.#mirrorStaticGeometry(
|
|
241
|
+
placement
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Mirrors source-coordinate static geometry for the opposite board side.
|
|
248
|
+
* @param {{ sourceCoordinateFrame?: boolean, geometry?: object }} placement Static body placement.
|
|
249
|
+
* @returns {object | undefined}
|
|
250
|
+
*/
|
|
251
|
+
static #mirrorStaticGeometry(placement) {
|
|
252
|
+
const geometry = placement?.geometry
|
|
253
|
+
if (
|
|
254
|
+
!placement?.sourceCoordinateFrame ||
|
|
255
|
+
!Array.isArray(geometry?.verticesMil)
|
|
256
|
+
) {
|
|
257
|
+
return geometry
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
...geometry,
|
|
262
|
+
verticesMil: geometry.verticesMil.map((vertex) => ({
|
|
263
|
+
x: AltiumScene3dShapeStackOwnerAdapter.#round(vertex?.x || 0),
|
|
264
|
+
y: AltiumScene3dShapeStackOwnerAdapter.#round(
|
|
265
|
+
-Number(vertex?.y || 0)
|
|
266
|
+
)
|
|
267
|
+
}))
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Re-anchors a repaired static carrier on the owning component position.
|
|
273
|
+
* @param {object | undefined} position Existing render position.
|
|
274
|
+
* @param {{ x?: number, y?: number }} owner Owner component.
|
|
275
|
+
* @param {object | undefined} board Scene board metadata.
|
|
276
|
+
* @returns {{ x: number, y: number, z: number }}
|
|
277
|
+
*/
|
|
278
|
+
static #withOwnerAnchor(position, owner, board) {
|
|
279
|
+
return {
|
|
280
|
+
...(position || {}),
|
|
281
|
+
x: AltiumScene3dShapeStackOwnerAdapter.#round(
|
|
282
|
+
Number(owner?.x || 0) - Number(board?.centerX || 0)
|
|
283
|
+
),
|
|
284
|
+
y: AltiumScene3dShapeStackOwnerAdapter.#round(
|
|
285
|
+
Number(owner?.y || 0) - Number(board?.centerY || 0)
|
|
286
|
+
)
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
173
290
|
/**
|
|
174
291
|
* Builds carrier-base to owner-component assignments.
|
|
175
292
|
* @param {object[]} componentBodies Source component-body rows.
|
|
@@ -187,6 +304,15 @@ export class AltiumScene3dShapeStackOwnerAdapter {
|
|
|
187
304
|
AltiumScene3dShapeStackOwnerAdapter.#groupCarrierBases(bases)
|
|
188
305
|
|
|
189
306
|
return groups.flatMap((group) => {
|
|
307
|
+
if (
|
|
308
|
+
AltiumScene3dShapeStackOwnerAdapter.#hasExactNonTimingOwner(
|
|
309
|
+
group.anchor,
|
|
310
|
+
components
|
|
311
|
+
)
|
|
312
|
+
) {
|
|
313
|
+
return []
|
|
314
|
+
}
|
|
315
|
+
|
|
190
316
|
const owners = timingComponents
|
|
191
317
|
.filter(
|
|
192
318
|
(component) =>
|
|
@@ -242,6 +368,39 @@ export class AltiumScene3dShapeStackOwnerAdapter {
|
|
|
242
368
|
})
|
|
243
369
|
}
|
|
244
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Checks whether one carrier base already sits on a non-timing component.
|
|
373
|
+
* @param {{ x?: number, y?: number }} anchor Carrier base anchor.
|
|
374
|
+
* @param {object[]} components Source components.
|
|
375
|
+
* @returns {boolean}
|
|
376
|
+
*/
|
|
377
|
+
static #hasExactNonTimingOwner(anchor, components) {
|
|
378
|
+
const exactOwners = (
|
|
379
|
+
Array.isArray(components) ? components : []
|
|
380
|
+
).filter(
|
|
381
|
+
(component) =>
|
|
382
|
+
AltiumScene3dShapeStackOwnerAdapter.#distance(
|
|
383
|
+
anchor,
|
|
384
|
+
component
|
|
385
|
+
) <=
|
|
386
|
+
AltiumScene3dShapeStackOwnerAdapter.#EXACT_OWNER_TOLERANCE_MIL
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
return (
|
|
390
|
+
exactOwners.some(
|
|
391
|
+
(component) =>
|
|
392
|
+
!AltiumScene3dShapeStackOwnerAdapter.#isTimingComponent(
|
|
393
|
+
component
|
|
394
|
+
)
|
|
395
|
+
) &&
|
|
396
|
+
!exactOwners.some((component) =>
|
|
397
|
+
AltiumScene3dShapeStackOwnerAdapter.#isTimingComponent(
|
|
398
|
+
component
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
)
|
|
402
|
+
}
|
|
403
|
+
|
|
245
404
|
/**
|
|
246
405
|
* Groups carrier bases that share one source anchor.
|
|
247
406
|
* @param {object[]} bases Candidate carrier bases.
|
|
@@ -469,7 +628,11 @@ export class AltiumScene3dShapeStackOwnerAdapter {
|
|
|
469
628
|
return (
|
|
470
629
|
componentBodies
|
|
471
630
|
.filter(
|
|
472
|
-
(componentBody) =>
|
|
631
|
+
(componentBody) =>
|
|
632
|
+
!usedComponentBodies.has(componentBody) &&
|
|
633
|
+
!AltiumScene3dShapeStackOwnerAdapter.#isCarrierBase(
|
|
634
|
+
componentBody
|
|
635
|
+
)
|
|
473
636
|
)
|
|
474
637
|
.map((componentBody) => ({
|
|
475
638
|
componentBody,
|
|
@@ -551,15 +714,9 @@ export class AltiumScene3dShapeStackOwnerAdapter {
|
|
|
551
714
|
return (
|
|
552
715
|
TIMING_DESIGNATOR_PATTERN.test(designator) ||
|
|
553
716
|
TIMING_PACKAGE_PATTERN.test(
|
|
554
|
-
|
|
555
|
-
component
|
|
556
|
-
|
|
557
|
-
component?.description,
|
|
558
|
-
component?.provenance?.footprintDescription,
|
|
559
|
-
...Object.values(component?.parameters || {})
|
|
560
|
-
]
|
|
561
|
-
.map((value) => String(value || ''))
|
|
562
|
-
.join(' ')
|
|
717
|
+
AltiumScene3dShapeStackOwnerAdapter.#timingIdentityText(
|
|
718
|
+
component
|
|
719
|
+
)
|
|
563
720
|
)
|
|
564
721
|
)
|
|
565
722
|
}
|
|
@@ -612,6 +769,19 @@ export class AltiumScene3dShapeStackOwnerAdapter {
|
|
|
612
769
|
return layer.includes('BOTTOM') || layer === 'BOT' ? 'bottom' : 'top'
|
|
613
770
|
}
|
|
614
771
|
|
|
772
|
+
/**
|
|
773
|
+
* Normalizes a static placement side token.
|
|
774
|
+
* @param {string | undefined} mountSide Candidate mount side.
|
|
775
|
+
* @returns {'top' | 'bottom'}
|
|
776
|
+
*/
|
|
777
|
+
static #normalizeMountSide(mountSide) {
|
|
778
|
+
return String(mountSide || 'top')
|
|
779
|
+
.trim()
|
|
780
|
+
.toLowerCase() === 'bottom'
|
|
781
|
+
? 'bottom'
|
|
782
|
+
: 'top'
|
|
783
|
+
}
|
|
784
|
+
|
|
615
785
|
/**
|
|
616
786
|
* Resolves one board face Z coordinate.
|
|
617
787
|
* @param {string} mountSide Mount side.
|
|
@@ -691,6 +861,44 @@ export class AltiumScene3dShapeStackOwnerAdapter {
|
|
|
691
861
|
.join(' ')
|
|
692
862
|
}
|
|
693
863
|
|
|
864
|
+
/**
|
|
865
|
+
* Builds timing-relevant identity text without material metadata.
|
|
866
|
+
* @param {object} component Source component.
|
|
867
|
+
* @returns {string}
|
|
868
|
+
*/
|
|
869
|
+
static #timingIdentityText(component) {
|
|
870
|
+
return [
|
|
871
|
+
component?.pattern,
|
|
872
|
+
component?.source,
|
|
873
|
+
component?.description,
|
|
874
|
+
component?.provenance?.footprintDescription,
|
|
875
|
+
...AltiumScene3dShapeStackOwnerAdapter.#timingParameterValues(
|
|
876
|
+
component?.parameters
|
|
877
|
+
)
|
|
878
|
+
]
|
|
879
|
+
.map((value) => String(value || ''))
|
|
880
|
+
.join(' ')
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
/**
|
|
884
|
+
* Returns parameter values whose names describe component identity.
|
|
885
|
+
* @param {object | undefined} parameters Source component parameters.
|
|
886
|
+
* @returns {unknown[]}
|
|
887
|
+
*/
|
|
888
|
+
static #timingParameterValues(parameters) {
|
|
889
|
+
if (!parameters || typeof parameters !== 'object') {
|
|
890
|
+
return []
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
return Object.entries(parameters)
|
|
894
|
+
.filter(([key]) =>
|
|
895
|
+
AltiumScene3dShapeStackOwnerAdapter.#TIMING_PARAMETER_NAME_PATTERN.test(
|
|
896
|
+
String(key || '')
|
|
897
|
+
)
|
|
898
|
+
)
|
|
899
|
+
.map(([, value]) => value)
|
|
900
|
+
}
|
|
901
|
+
|
|
694
902
|
/**
|
|
695
903
|
* Rounds one mil value for stable scene output.
|
|
696
904
|
* @param {number} value Candidate value.
|