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
|
@@ -15,8 +15,10 @@ import { PcbScene3dPadLocalSpanResolver } from './PcbScene3dPadLocalSpanResolver
|
|
|
15
15
|
import { PcbScene3dPackages } from './PcbScene3dPackages.mjs'
|
|
16
16
|
import { PcbScene3dPlacementSideResolver } from './PcbScene3dPlacementSideResolver.mjs'
|
|
17
17
|
import { PcbScene3dStaticBodyPlacementBuilder } from './PcbScene3dStaticBodyPlacementBuilder.mjs'
|
|
18
|
+
import { PcbScene3dPadYawResolver } from './PcbScene3dPadYawResolver.mjs'
|
|
18
19
|
import { PcbScene3dTextBoxLayoutResolver } from './PcbScene3dTextBoxLayoutResolver.mjs'
|
|
19
20
|
import { PcbFootprintPadAxisNormalizer } from './PcbFootprintPadAxisNormalizer.mjs'
|
|
21
|
+
import { PcbScene3dCopperRegionDetailBuilder } from './PcbScene3dCopperRegionDetailBuilder.mjs'
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* Builds deterministic 3D scene data from the normalized PCB model.
|
|
@@ -33,13 +35,26 @@ export class PcbScene3dBuilder {
|
|
|
33
35
|
static #UNMATCHED_BODY_MIN_OVERHANG_MIL = 150
|
|
34
36
|
static #UNMATCHED_BODY_MAX_OVERHANG_MIL = 600
|
|
35
37
|
static #OVERSIZED_GENERIC_FALLBACK_MAX_MIL = 800
|
|
38
|
+
static #TIMING_STACK_BODY_RADIUS_MIL = 220
|
|
36
39
|
static #TRUETYPE_TEXT_WIDTH_RATIO = 0.55
|
|
37
40
|
static #LOW_CONFIDENCE_GENERIC_FOOTPRINT_PATTERN =
|
|
38
41
|
/(?:^|[^a-z0-9])(?:edge|finger|fingers|contact|contacts|mech|mechanical|jumper|jump)(?:$|[^a-z0-9])/i
|
|
39
42
|
static #AUTHORED_BODY_IDENTITY_PATTERN =
|
|
40
43
|
/(?:^|[^a-z0-9])(?:antenna|coax|conn|connector|edge|flex|fpc|frame|hardware|header|jack|mechanical|module|mount|shield|sma|socket|usb)(?:$|[^a-z0-9])/i
|
|
44
|
+
static #AUTHORED_COVER_STACK_IDENTITY_PATTERN =
|
|
45
|
+
/(?:^|[^a-z0-9])(?:emi|rf|rfi|shield|cover|can)(?:$|[^a-z0-9])/i
|
|
46
|
+
static #MECHANICAL_SHIELD_FALLBACK_PATTERN =
|
|
47
|
+
/(?:^|[^a-z0-9])(?:emi|rfi|shield|cover|can)(?:$|[^a-z0-9])/i
|
|
48
|
+
static #MECHANICAL_SHIELD_FRAME_OWNER_PATTERN =
|
|
49
|
+
/(?=.*(?:^|[^a-z0-9])(?:emi|rfi|rf|shield|can)(?:$|[^a-z0-9]))(?=.*(?:^|[^a-z0-9])frame(?:$|[^a-z0-9]))/i
|
|
50
|
+
static #MECHANICAL_SHIELD_FRAME_BODY_PATTERN =
|
|
51
|
+
/(?:^|[^a-z0-9])(?:frame[0-9]*|leg|rail|side|wall)(?:$|[^a-z0-9])/i
|
|
52
|
+
static #MECHANICAL_SHIELD_FRAME_OWNER_RADIUS_MIL = 750
|
|
41
53
|
static #COMPONENT_PACKAGE_BODY_PATTERN =
|
|
42
54
|
/(?:^|[^a-z0-9])(?:[a-z0-9]*dfn|[a-z0-9]*qfn|bga|cap|capacitor|crystal|diode|ferrite|ind|inductor|lga|lqg[a-z0-9]*|lqw[a-z0-9]*|osc|qfp|res|resistor|sot|transistor|xtal)(?:$|[^a-z0-9])/i
|
|
55
|
+
static #TIMING_STACK_COMPONENT_PATTERN =
|
|
56
|
+
/(?:^|[^a-z0-9])(?:clock|crystal|osc|oscillator|resonator|tcxo|txco|xtal)(?:$|[^a-z0-9])/i
|
|
57
|
+
static #TIMING_STACK_DESIGNATOR_PATTERN = /^(?:y|xo)\d+[a-z]?$/i
|
|
43
58
|
|
|
44
59
|
/**
|
|
45
60
|
* Builds a scene description for host 3D renderers.
|
|
@@ -64,6 +79,7 @@ export class PcbScene3dBuilder {
|
|
|
64
79
|
const tracks = Array.isArray(pcb.tracks) ? pcb.tracks : []
|
|
65
80
|
const arcs = Array.isArray(pcb.arcs) ? pcb.arcs : []
|
|
66
81
|
const fills = Array.isArray(pcb.fills) ? pcb.fills : []
|
|
82
|
+
const regionFills = PcbScene3dCopperRegionDetailBuilder.build(pcb)
|
|
67
83
|
const texts = Array.isArray(pcb.texts) ? pcb.texts : []
|
|
68
84
|
const vias = Array.isArray(pcb.vias) ? pcb.vias : []
|
|
69
85
|
const silkscreenRegions =
|
|
@@ -91,6 +107,7 @@ export class PcbScene3dBuilder {
|
|
|
91
107
|
: []
|
|
92
108
|
}
|
|
93
109
|
const componentBodyModels = componentBodies.map((componentBody) =>
|
|
110
|
+
PcbScene3dBuilder.#shouldRenderStaticGeometryOnly(componentBody) ||
|
|
94
111
|
PcbScene3dBuilder.#shouldSuppressLayerlessBodyPlaceholder(
|
|
95
112
|
componentBody,
|
|
96
113
|
componentBodies,
|
|
@@ -184,7 +201,7 @@ export class PcbScene3dBuilder {
|
|
|
184
201
|
pads,
|
|
185
202
|
tracks,
|
|
186
203
|
arcs,
|
|
187
|
-
fills,
|
|
204
|
+
fills: [...fills, ...regionFills],
|
|
188
205
|
vias,
|
|
189
206
|
polygons: Array.isArray(pcb.polygons) ? pcb.polygons : [],
|
|
190
207
|
silkscreen: {
|
|
@@ -220,7 +237,7 @@ export class PcbScene3dBuilder {
|
|
|
220
237
|
* @param {{ centerX: number, centerY: number }} board
|
|
221
238
|
* @param {number} thicknessMil
|
|
222
239
|
* @param {{ resolveComponentModel: (component: any) => { name: string, relativePath: string, format: string } | null } | null} modelRegistry
|
|
223
|
-
* @returns {{ designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, boardPositionMil: { x: number, y: number, z: number }, pattern: string, source: string, description: string, parameters: Record<string, unknown>, body: { family: string, sizeMil: { width: number, depth: number, height: number } }, externalModel: { name: string, relativePath: string, format: string } | null } | null}
|
|
240
|
+
* @returns {{ componentIndex: number | null, designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, boardPositionMil: { x: number, y: number, z: number }, pattern: string, source: string, description: string, parameters: Record<string, unknown>, body: { family: string, sizeMil: { width: number, depth: number, height: number } }, externalModel: { name: string, relativePath: string, format: string } | null, renderFallbackBody?: boolean } | null}
|
|
224
241
|
*/
|
|
225
242
|
static #buildComponent(
|
|
226
243
|
component,
|
|
@@ -230,20 +247,26 @@ export class PcbScene3dBuilder {
|
|
|
230
247
|
modelRegistry
|
|
231
248
|
) {
|
|
232
249
|
const mountSide = PcbScene3dBuilder.#resolveMountSide(component)
|
|
233
|
-
const
|
|
250
|
+
const rotationDeg = PcbScene3dBuilder.#resolveComponentRotation(
|
|
251
|
+
component,
|
|
252
|
+
pads,
|
|
253
|
+
mountSide
|
|
254
|
+
)
|
|
255
|
+
const padSpan = PcbScene3dBuilder.#resolvePadSpan(
|
|
256
|
+
component,
|
|
257
|
+
pads,
|
|
258
|
+
rotationDeg
|
|
259
|
+
)
|
|
234
260
|
const body = PcbScene3dPackages.resolve(component, padSpan)
|
|
235
261
|
const externalModel = modelRegistry
|
|
236
262
|
? modelRegistry.resolveComponentModel(component)
|
|
237
263
|
: null
|
|
238
|
-
|
|
264
|
+
const suppressFallbackBody =
|
|
239
265
|
PcbScene3dBuilder.#shouldSuppressProceduralComponent(
|
|
240
266
|
component,
|
|
241
267
|
body,
|
|
242
268
|
externalModel
|
|
243
269
|
)
|
|
244
|
-
) {
|
|
245
|
-
return null
|
|
246
|
-
}
|
|
247
270
|
|
|
248
271
|
const halfBoardThickness = thicknessMil / 2
|
|
249
272
|
const halfBodyHeight = body.sizeMil.height / 2
|
|
@@ -253,9 +276,12 @@ export class PcbScene3dBuilder {
|
|
|
253
276
|
: halfBoardThickness + halfBodyHeight
|
|
254
277
|
|
|
255
278
|
return {
|
|
279
|
+
componentIndex: Number.isFinite(Number(component.componentIndex))
|
|
280
|
+
? Number(component.componentIndex)
|
|
281
|
+
: null,
|
|
256
282
|
designator: component.designator,
|
|
257
283
|
mountSide,
|
|
258
|
-
rotationDeg
|
|
284
|
+
rotationDeg,
|
|
259
285
|
positionMil: {
|
|
260
286
|
x: Number(component.x || 0) - Number(board.centerX || 0),
|
|
261
287
|
y: Number(component.y || 0) - Number(board.centerY || 0),
|
|
@@ -276,7 +302,8 @@ export class PcbScene3dBuilder {
|
|
|
276
302
|
? { ...component.parameters }
|
|
277
303
|
: {},
|
|
278
304
|
body,
|
|
279
|
-
externalModel
|
|
305
|
+
externalModel,
|
|
306
|
+
...(suppressFallbackBody ? { renderFallbackBody: false } : {})
|
|
280
307
|
}
|
|
281
308
|
}
|
|
282
309
|
|
|
@@ -291,11 +318,44 @@ export class PcbScene3dBuilder {
|
|
|
291
318
|
return (
|
|
292
319
|
!externalModel &&
|
|
293
320
|
body?.family === 'generic' &&
|
|
294
|
-
PcbScene3dBuilder.#isOversizedGenericFallback(body) &&
|
|
295
|
-
|
|
321
|
+
((PcbScene3dBuilder.#isOversizedGenericFallback(body) &&
|
|
322
|
+
PcbScene3dBuilder.#isLowConfidenceGenericFootprint(
|
|
323
|
+
component
|
|
324
|
+
)) ||
|
|
325
|
+
PcbScene3dBuilder.#isMechanicalShieldFallback(component))
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* Checks whether a generic component row describes authored shield
|
|
331
|
+
* hardware that should not become a filled fallback box.
|
|
332
|
+
* @param {{ pattern?: string, source?: string, description?: string, parameters?: Record<string, unknown>, provenance?: Record<string, unknown> }} component Source component.
|
|
333
|
+
* @returns {boolean}
|
|
334
|
+
*/
|
|
335
|
+
static #isMechanicalShieldFallback(component) {
|
|
336
|
+
return PcbScene3dBuilder.#MECHANICAL_SHIELD_FALLBACK_PATTERN.test(
|
|
337
|
+
PcbScene3dBuilder.#componentIdentityText(component)
|
|
296
338
|
)
|
|
297
339
|
}
|
|
298
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Builds normalized free-text identity for component classification.
|
|
343
|
+
* @param {{ pattern?: string, source?: string, description?: string, parameters?: Record<string, unknown>, provenance?: Record<string, unknown> }} component Source component.
|
|
344
|
+
* @returns {string}
|
|
345
|
+
*/
|
|
346
|
+
static #componentIdentityText(component) {
|
|
347
|
+
return [
|
|
348
|
+
component?.pattern,
|
|
349
|
+
component?.source,
|
|
350
|
+
component?.description,
|
|
351
|
+
component?.provenance?.sourceLibReference,
|
|
352
|
+
component?.provenance?.footprintDescription,
|
|
353
|
+
...Object.values(component?.parameters || {})
|
|
354
|
+
]
|
|
355
|
+
.map((value) => String(value || ''))
|
|
356
|
+
.join(' ')
|
|
357
|
+
}
|
|
358
|
+
|
|
299
359
|
/**
|
|
300
360
|
* Checks whether one generic fallback body spans too much of the board.
|
|
301
361
|
* @param {{ sizeMil?: { width?: number, depth?: number } }} body Procedural body.
|
|
@@ -339,7 +399,7 @@ export class PcbScene3dBuilder {
|
|
|
339
399
|
/**
|
|
340
400
|
* Builds one explicit external-model placement from normalized component
|
|
341
401
|
* body metadata.
|
|
342
|
-
* @param {{ modelId?: string, checksum?: number | null, embedded?: boolean, name?: string, identifier?: string, layer?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, modelRotationDeg?: { x?: number, y?: number, z?: number }, dzMil?: number }} componentBody
|
|
402
|
+
* @param {{ modelId?: string, checksum?: number | null, embedded?: boolean, name?: string, identifier?: string, layer?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, modelRotationDeg?: { x?: number, y?: number, z?: number }, dzMil?: number, bodyOpacity?: number | string }} componentBody
|
|
343
403
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null } | null} matchedComponent
|
|
344
404
|
* @param {{ origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } | null} resolvedModel
|
|
345
405
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components
|
|
@@ -357,10 +417,24 @@ export class PcbScene3dBuilder {
|
|
|
357
417
|
board,
|
|
358
418
|
thicknessMil
|
|
359
419
|
) {
|
|
420
|
+
if (PcbScene3dBuilder.#shouldRenderStaticGeometryOnly(componentBody)) {
|
|
421
|
+
return null
|
|
422
|
+
}
|
|
423
|
+
|
|
360
424
|
if (!resolvedModel) {
|
|
361
425
|
return null
|
|
362
426
|
}
|
|
363
427
|
|
|
428
|
+
if (
|
|
429
|
+
PcbScene3dBuilder.#isPositiveTimingStackPackageBody(
|
|
430
|
+
componentBody,
|
|
431
|
+
matchedComponent,
|
|
432
|
+
components
|
|
433
|
+
)
|
|
434
|
+
) {
|
|
435
|
+
return null
|
|
436
|
+
}
|
|
437
|
+
|
|
364
438
|
if (
|
|
365
439
|
!matchedComponent &&
|
|
366
440
|
PcbScene3dBuilder.#shouldDropUnmatchedPackageBody(
|
|
@@ -429,10 +503,24 @@ export class PcbScene3dBuilder {
|
|
|
429
503
|
pads,
|
|
430
504
|
resolvedModel
|
|
431
505
|
),
|
|
506
|
+
...PcbScene3dBuilder.#componentBodyDisplayMetadata(componentBody),
|
|
432
507
|
externalModel: resolvedModel
|
|
433
508
|
}
|
|
434
509
|
}
|
|
435
510
|
|
|
511
|
+
/**
|
|
512
|
+
* Resolves optional component-body display metadata for external renderers.
|
|
513
|
+
* @param {{ bodyOpacity?: number | string }} componentBody Component body.
|
|
514
|
+
* @returns {{ bodyOpacity?: number }}
|
|
515
|
+
*/
|
|
516
|
+
static #componentBodyDisplayMetadata(componentBody) {
|
|
517
|
+
const opacity = Number(componentBody?.bodyOpacity)
|
|
518
|
+
|
|
519
|
+
return Number.isFinite(opacity) && opacity > 0 && opacity < 1
|
|
520
|
+
? { bodyOpacity: opacity }
|
|
521
|
+
: {}
|
|
522
|
+
}
|
|
523
|
+
|
|
436
524
|
/**
|
|
437
525
|
* Resolves the vertical offset that should remain after the viewer seats
|
|
438
526
|
* raw model bounds on the board face.
|
|
@@ -478,8 +566,47 @@ export class PcbScene3dBuilder {
|
|
|
478
566
|
matchedComponent
|
|
479
567
|
) {
|
|
480
568
|
return (
|
|
481
|
-
!matchedComponent &&
|
|
482
|
-
|
|
569
|
+
(!matchedComponent &&
|
|
570
|
+
PcbScene3dBuilder.#isShapeBasedComponentBody(componentBody)) ||
|
|
571
|
+
PcbScene3dBuilder.#hasAuthoredCoverStackStandoff(
|
|
572
|
+
componentBody,
|
|
573
|
+
matchedComponent
|
|
574
|
+
)
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Checks whether a positive standoff describes a real mechanical cover
|
|
580
|
+
* stack instead of an embedded model source-origin air gap.
|
|
581
|
+
* @param {object | null | undefined} componentBody Component-body row.
|
|
582
|
+
* @param {object | null} matchedComponent Matched owner component.
|
|
583
|
+
* @returns {boolean}
|
|
584
|
+
*/
|
|
585
|
+
static #hasAuthoredCoverStackStandoff(componentBody, matchedComponent) {
|
|
586
|
+
if (!matchedComponent || !componentBody?.embedded) {
|
|
587
|
+
return false
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const standoff = Number(componentBody?.standoffHeightMil)
|
|
591
|
+
const overallHeight = Number(componentBody?.overallHeightMil)
|
|
592
|
+
if (
|
|
593
|
+
!Number.isFinite(standoff) ||
|
|
594
|
+
!Number.isFinite(overallHeight) ||
|
|
595
|
+
standoff <= 0 ||
|
|
596
|
+
overallHeight <= standoff
|
|
597
|
+
) {
|
|
598
|
+
return false
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const identityText = [
|
|
602
|
+
componentBody?.identifier,
|
|
603
|
+
componentBody?.name,
|
|
604
|
+
matchedComponent?.pattern,
|
|
605
|
+
matchedComponent?.source
|
|
606
|
+
].join(' ')
|
|
607
|
+
|
|
608
|
+
return PcbScene3dBuilder.#AUTHORED_COVER_STACK_IDENTITY_PATTERN.test(
|
|
609
|
+
identityText
|
|
483
610
|
)
|
|
484
611
|
}
|
|
485
612
|
|
|
@@ -643,7 +770,8 @@ export class PcbScene3dBuilder {
|
|
|
643
770
|
|
|
644
771
|
componentBodies.forEach((componentBody, bodyIndex) => {
|
|
645
772
|
if (
|
|
646
|
-
!PcbScene3dBuilder.#
|
|
773
|
+
!PcbScene3dBuilder.#isMatchableComponentBody(
|
|
774
|
+
componentBody,
|
|
647
775
|
resolvedBodyModels,
|
|
648
776
|
bodyIndex
|
|
649
777
|
)
|
|
@@ -730,7 +858,8 @@ export class PcbScene3dBuilder {
|
|
|
730
858
|
const groupedBodyIndexes = new Map()
|
|
731
859
|
componentBodies.forEach((componentBody, bodyIndex) => {
|
|
732
860
|
if (
|
|
733
|
-
!PcbScene3dBuilder.#
|
|
861
|
+
!PcbScene3dBuilder.#isMatchableComponentBody(
|
|
862
|
+
componentBody,
|
|
734
863
|
resolvedBodyModels,
|
|
735
864
|
bodyIndex
|
|
736
865
|
)
|
|
@@ -799,9 +928,115 @@ export class PcbScene3dBuilder {
|
|
|
799
928
|
})
|
|
800
929
|
})
|
|
801
930
|
|
|
931
|
+
PcbScene3dBuilder.#assignStaticShieldFrameBodyMatches(
|
|
932
|
+
matches,
|
|
933
|
+
componentBodies,
|
|
934
|
+
components
|
|
935
|
+
)
|
|
936
|
+
|
|
802
937
|
return matches
|
|
803
938
|
}
|
|
804
939
|
|
|
940
|
+
/**
|
|
941
|
+
* Assigns static shield-frame sub-bodies to their nearest shield-frame
|
|
942
|
+
* component owner. These bodies are renderable without external models, so
|
|
943
|
+
* the external-model ownership pass does not see them.
|
|
944
|
+
* @param {({ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string } | null)[]} matches Mutable match array.
|
|
945
|
+
* @param {{ identifier?: string, name?: string, layer?: string, positionMil?: { x?: number, y?: number }, staticGeometry?: object }[]} componentBodies Component bodies.
|
|
946
|
+
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components PCB components.
|
|
947
|
+
*/
|
|
948
|
+
static #assignStaticShieldFrameBodyMatches(
|
|
949
|
+
matches,
|
|
950
|
+
componentBodies,
|
|
951
|
+
components
|
|
952
|
+
) {
|
|
953
|
+
componentBodies.forEach((componentBody, bodyIndex) => {
|
|
954
|
+
if (
|
|
955
|
+
matches[bodyIndex] ||
|
|
956
|
+
!PcbScene3dBuilder.#isStaticShieldFrameBody(componentBody)
|
|
957
|
+
) {
|
|
958
|
+
return
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
const owner = components
|
|
962
|
+
.filter(
|
|
963
|
+
(component) =>
|
|
964
|
+
PcbScene3dBuilder.#isMechanicalShieldFrameOwner(
|
|
965
|
+
component
|
|
966
|
+
) &&
|
|
967
|
+
PcbScene3dBuilder.#isBodyComponentSideCompatible(
|
|
968
|
+
componentBody,
|
|
969
|
+
component
|
|
970
|
+
)
|
|
971
|
+
)
|
|
972
|
+
.map((component) => ({
|
|
973
|
+
component,
|
|
974
|
+
affinityScore:
|
|
975
|
+
PcbScene3dPlacementSideResolver.scoreBodyComponentAffinity(
|
|
976
|
+
componentBody,
|
|
977
|
+
component
|
|
978
|
+
),
|
|
979
|
+
distance:
|
|
980
|
+
PcbScene3dBuilder.#distanceBetweenBodyAndComponent(
|
|
981
|
+
componentBody,
|
|
982
|
+
component
|
|
983
|
+
)
|
|
984
|
+
}))
|
|
985
|
+
.filter(
|
|
986
|
+
({ distance }) =>
|
|
987
|
+
Number.isFinite(distance) &&
|
|
988
|
+
distance <=
|
|
989
|
+
PcbScene3dBuilder
|
|
990
|
+
.#MECHANICAL_SHIELD_FRAME_OWNER_RADIUS_MIL
|
|
991
|
+
)
|
|
992
|
+
.sort(
|
|
993
|
+
(left, right) =>
|
|
994
|
+
right.affinityScore - left.affinityScore ||
|
|
995
|
+
left.distance - right.distance
|
|
996
|
+
)[0]?.component
|
|
997
|
+
|
|
998
|
+
if (owner) {
|
|
999
|
+
matches[bodyIndex] = owner
|
|
1000
|
+
}
|
|
1001
|
+
})
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
/**
|
|
1005
|
+
* Checks whether one component is a mechanical shield-frame owner.
|
|
1006
|
+
* @param {{ pattern?: string, source?: string, description?: string, parameters?: Record<string, unknown>, provenance?: Record<string, unknown> }} component Source component.
|
|
1007
|
+
* @returns {boolean}
|
|
1008
|
+
*/
|
|
1009
|
+
static #isMechanicalShieldFrameOwner(component) {
|
|
1010
|
+
return PcbScene3dBuilder.#MECHANICAL_SHIELD_FRAME_OWNER_PATTERN.test(
|
|
1011
|
+
PcbScene3dBuilder.#componentIdentityText(component)
|
|
1012
|
+
)
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
/**
|
|
1016
|
+
* Checks whether one static body is a shield-frame sub-body.
|
|
1017
|
+
* @param {{ identifier?: string, name?: string, staticGeometry?: object }} componentBody Component body.
|
|
1018
|
+
* @returns {boolean}
|
|
1019
|
+
*/
|
|
1020
|
+
static #isStaticShieldFrameBody(componentBody) {
|
|
1021
|
+
const geometry = componentBody?.staticGeometry || {}
|
|
1022
|
+
const completeGeometry =
|
|
1023
|
+
geometry.status === 'complete' &&
|
|
1024
|
+
Array.isArray(geometry.verticesMil) &&
|
|
1025
|
+
geometry.verticesMil.length >= 3
|
|
1026
|
+
const recoverableGeometry =
|
|
1027
|
+
geometry.status !== 'complete' && Number(geometry.heightMil) > 0
|
|
1028
|
+
|
|
1029
|
+
return (
|
|
1030
|
+
String(geometry.kind || '').toLowerCase() === 'extruded-polygon' &&
|
|
1031
|
+
(completeGeometry || recoverableGeometry) &&
|
|
1032
|
+
PcbScene3dBuilder.#MECHANICAL_SHIELD_FRAME_BODY_PATTERN.test(
|
|
1033
|
+
[componentBody?.identifier, componentBody?.name]
|
|
1034
|
+
.map((value) => String(value || ''))
|
|
1035
|
+
.join(' ')
|
|
1036
|
+
)
|
|
1037
|
+
)
|
|
1038
|
+
}
|
|
1039
|
+
|
|
805
1040
|
/**
|
|
806
1041
|
* Builds reusable identity statistics for body/component matching.
|
|
807
1042
|
* @param {{ modelId?: string, name?: string, identifier?: string }[]} componentBodies
|
|
@@ -820,7 +1055,8 @@ export class PcbScene3dBuilder {
|
|
|
820
1055
|
|
|
821
1056
|
componentBodies.forEach((componentBody, bodyIndex) => {
|
|
822
1057
|
if (
|
|
823
|
-
!PcbScene3dBuilder.#
|
|
1058
|
+
!PcbScene3dBuilder.#isMatchableComponentBody(
|
|
1059
|
+
componentBody,
|
|
824
1060
|
resolvedBodyModels,
|
|
825
1061
|
bodyIndex
|
|
826
1062
|
)
|
|
@@ -857,6 +1093,29 @@ export class PcbScene3dBuilder {
|
|
|
857
1093
|
return { bodyGroupCounts, candidateComponentCounts }
|
|
858
1094
|
}
|
|
859
1095
|
|
|
1096
|
+
/**
|
|
1097
|
+
* Returns true when one body row can participate in owner matching.
|
|
1098
|
+
* @param {object | null | undefined} componentBody Component body row.
|
|
1099
|
+
* @param {unknown[]} resolvedBodyModels Resolved body-model entries.
|
|
1100
|
+
* @param {number} bodyIndex Body index.
|
|
1101
|
+
* @returns {boolean}
|
|
1102
|
+
*/
|
|
1103
|
+
static #isMatchableComponentBody(
|
|
1104
|
+
componentBody,
|
|
1105
|
+
resolvedBodyModels,
|
|
1106
|
+
bodyIndex
|
|
1107
|
+
) {
|
|
1108
|
+
return (
|
|
1109
|
+
PcbScene3dBuilder.#isResolvableComponentBody(
|
|
1110
|
+
resolvedBodyModels,
|
|
1111
|
+
bodyIndex
|
|
1112
|
+
) ||
|
|
1113
|
+
PcbScene3dBuilder.#isAnonymousLayerlessStaticBodyGeometry(
|
|
1114
|
+
componentBody
|
|
1115
|
+
)
|
|
1116
|
+
)
|
|
1117
|
+
}
|
|
1118
|
+
|
|
860
1119
|
/**
|
|
861
1120
|
* Returns true when one body row can produce a renderable external model.
|
|
862
1121
|
* @param {unknown[]} resolvedBodyModels Resolved body-model entries.
|
|
@@ -871,6 +1130,26 @@ export class PcbScene3dBuilder {
|
|
|
871
1130
|
)
|
|
872
1131
|
}
|
|
873
1132
|
|
|
1133
|
+
/**
|
|
1134
|
+
* Checks whether one anonymous layerless body row already carries
|
|
1135
|
+
* renderable static geometry.
|
|
1136
|
+
* @param {object | null | undefined} componentBody Component body row.
|
|
1137
|
+
* @returns {boolean}
|
|
1138
|
+
*/
|
|
1139
|
+
static #isAnonymousLayerlessStaticBodyGeometry(componentBody) {
|
|
1140
|
+
const identityText = [componentBody?.identifier, componentBody?.name]
|
|
1141
|
+
.map((value) => String(value || '').trim())
|
|
1142
|
+
.join('')
|
|
1143
|
+
|
|
1144
|
+
return (
|
|
1145
|
+
identityText.length === 0 &&
|
|
1146
|
+
String(componentBody?.layer || '').trim().length === 0 &&
|
|
1147
|
+
Boolean(componentBody?.staticGeometry) &&
|
|
1148
|
+
String(componentBody.staticGeometry?.status || '').toLowerCase() ===
|
|
1149
|
+
'complete'
|
|
1150
|
+
)
|
|
1151
|
+
}
|
|
1152
|
+
|
|
874
1153
|
/**
|
|
875
1154
|
* Returns true when the body/component anchors are close enough to be
|
|
876
1155
|
* considered an explicit placement match.
|
|
@@ -1040,6 +1319,154 @@ export class PcbScene3dBuilder {
|
|
|
1040
1319
|
)
|
|
1041
1320
|
}
|
|
1042
1321
|
|
|
1322
|
+
/**
|
|
1323
|
+
* Checks whether a package-like shape body is an authored timing-stack
|
|
1324
|
+
* sub-body that should stay represented by its carrier static geometry.
|
|
1325
|
+
* @param {object} componentBody Component-body record.
|
|
1326
|
+
* @param {object | null} matchedComponent Matched component.
|
|
1327
|
+
* @param {{ designator?: string, x?: number, y?: number, pattern?: string, source?: string, description?: string, provenance?: object, parameters?: object }[]} components PCB components.
|
|
1328
|
+
* @returns {boolean}
|
|
1329
|
+
*/
|
|
1330
|
+
static #isPositiveTimingStackPackageBody(
|
|
1331
|
+
componentBody,
|
|
1332
|
+
matchedComponent,
|
|
1333
|
+
components
|
|
1334
|
+
) {
|
|
1335
|
+
const standoff = Number(componentBody?.standoffHeightMil)
|
|
1336
|
+
const hasTimingOwner = matchedComponent
|
|
1337
|
+
? PcbScene3dBuilder.#isTimingStackComponent(matchedComponent)
|
|
1338
|
+
: PcbScene3dBuilder.#hasNearbyTimingStackOwner(
|
|
1339
|
+
componentBody,
|
|
1340
|
+
components
|
|
1341
|
+
)
|
|
1342
|
+
const hasLocalComponentOwner =
|
|
1343
|
+
PcbScene3dBuilder.#hasNearbyNonTimingPackageOwner(
|
|
1344
|
+
componentBody,
|
|
1345
|
+
components
|
|
1346
|
+
)
|
|
1347
|
+
|
|
1348
|
+
return (
|
|
1349
|
+
PcbScene3dBuilder.#isShapeBasedComponentBody(componentBody) &&
|
|
1350
|
+
componentBody?.embedded === true &&
|
|
1351
|
+
Number.isFinite(standoff) &&
|
|
1352
|
+
standoff > 0 &&
|
|
1353
|
+
PcbScene3dBuilder.#isComponentPackageBody(componentBody) &&
|
|
1354
|
+
!PcbScene3dBuilder.#isTimingStackBodyIdentity(componentBody) &&
|
|
1355
|
+
!PcbScene3dBuilder.#isAuthoredBodyIdentity(componentBody) &&
|
|
1356
|
+
hasTimingOwner &&
|
|
1357
|
+
!hasLocalComponentOwner
|
|
1358
|
+
)
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* Checks whether a shape-based sub-body is the actual timing package
|
|
1363
|
+
* rather than a passive support part inside the timing stack.
|
|
1364
|
+
* @param {{ name?: string, identifier?: string }} componentBody Component-body record.
|
|
1365
|
+
* @returns {boolean}
|
|
1366
|
+
*/
|
|
1367
|
+
static #isTimingStackBodyIdentity(componentBody) {
|
|
1368
|
+
return PcbScene3dBuilder.#TIMING_STACK_COMPONENT_PATTERN.test(
|
|
1369
|
+
[componentBody?.identifier, componentBody?.name]
|
|
1370
|
+
.map((value) => String(value || ''))
|
|
1371
|
+
.join(' ')
|
|
1372
|
+
)
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
/**
|
|
1376
|
+
* Checks whether one package-like body is the authored timing package for
|
|
1377
|
+
* a timing-stack component.
|
|
1378
|
+
* @param {{ name?: string, identifier?: string }} componentBody Component-body record.
|
|
1379
|
+
* @param {object} component PCB component.
|
|
1380
|
+
* @returns {boolean}
|
|
1381
|
+
*/
|
|
1382
|
+
static #isTimingStackBodyComponentPair(componentBody, component) {
|
|
1383
|
+
return (
|
|
1384
|
+
PcbScene3dBuilder.#isTimingStackBodyIdentity(componentBody) &&
|
|
1385
|
+
PcbScene3dBuilder.#isTimingStackComponent(component)
|
|
1386
|
+
)
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
/**
|
|
1390
|
+
* Checks whether a positive package body has its own nearby non-timing
|
|
1391
|
+
* component and should not be treated as an unowned timing-stack detail.
|
|
1392
|
+
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Component-body record.
|
|
1393
|
+
* @param {object[]} components PCB components.
|
|
1394
|
+
* @returns {boolean}
|
|
1395
|
+
*/
|
|
1396
|
+
static #hasNearbyNonTimingPackageOwner(componentBody, components) {
|
|
1397
|
+
return (Array.isArray(components) ? components : []).some(
|
|
1398
|
+
(component) =>
|
|
1399
|
+
!PcbScene3dBuilder.#isTimingStackComponent(component) &&
|
|
1400
|
+
PcbScene3dBuilder.#distanceBetweenBodyAndComponent(
|
|
1401
|
+
componentBody,
|
|
1402
|
+
component
|
|
1403
|
+
) <= PcbScene3dBuilder.#EXACT_BODY_MISMATCH_TOLERANCE_MIL &&
|
|
1404
|
+
PcbScene3dPlacementSideResolver.scoreBodyComponentAffinity(
|
|
1405
|
+
componentBody,
|
|
1406
|
+
component
|
|
1407
|
+
) > 0
|
|
1408
|
+
)
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
/**
|
|
1412
|
+
* Checks whether an unmatched body sits inside a timing-package stack.
|
|
1413
|
+
* @param {{ positionMil?: { x?: number, y?: number } }} componentBody Component-body record.
|
|
1414
|
+
* @param {object[]} components PCB components.
|
|
1415
|
+
* @returns {boolean}
|
|
1416
|
+
*/
|
|
1417
|
+
static #hasNearbyTimingStackOwner(componentBody, components) {
|
|
1418
|
+
return (Array.isArray(components) ? components : []).some(
|
|
1419
|
+
(component) =>
|
|
1420
|
+
PcbScene3dBuilder.#isTimingStackComponent(component) &&
|
|
1421
|
+
PcbScene3dBuilder.#distanceBetweenBodyAndComponent(
|
|
1422
|
+
componentBody,
|
|
1423
|
+
component
|
|
1424
|
+
) <= PcbScene3dBuilder.#TIMING_STACK_BODY_RADIUS_MIL
|
|
1425
|
+
)
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
/**
|
|
1429
|
+
* Checks whether one component is a timing-package stack owner.
|
|
1430
|
+
* @param {object} component PCB component.
|
|
1431
|
+
* @returns {boolean}
|
|
1432
|
+
*/
|
|
1433
|
+
static #isTimingStackComponent(component) {
|
|
1434
|
+
const designator = String(component?.designator || '').trim()
|
|
1435
|
+
if (
|
|
1436
|
+
PcbScene3dBuilder.#TIMING_STACK_DESIGNATOR_PATTERN.test(designator)
|
|
1437
|
+
) {
|
|
1438
|
+
return true
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
return PcbScene3dBuilder.#TIMING_STACK_COMPONENT_PATTERN.test(
|
|
1442
|
+
[
|
|
1443
|
+
component?.pattern,
|
|
1444
|
+
component?.source,
|
|
1445
|
+
component?.description,
|
|
1446
|
+
component?.provenance?.footprintDescription,
|
|
1447
|
+
...Object.values(component?.parameters || {})
|
|
1448
|
+
]
|
|
1449
|
+
.map((value) => String(value || ''))
|
|
1450
|
+
.join(' ')
|
|
1451
|
+
)
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* Checks whether a body already has complete static geometry and should
|
|
1456
|
+
* not also be emitted as an external model placement.
|
|
1457
|
+
* @param {{ embedded?: boolean, staticGeometry?: { status?: string } }} componentBody Component-body record.
|
|
1458
|
+
* @returns {boolean}
|
|
1459
|
+
*/
|
|
1460
|
+
static #shouldRenderStaticGeometryOnly(componentBody) {
|
|
1461
|
+
return (
|
|
1462
|
+
PcbScene3dBuilder.#isShapeBasedComponentBody(componentBody) &&
|
|
1463
|
+
!componentBody?.embedded &&
|
|
1464
|
+
String(
|
|
1465
|
+
componentBody?.staticGeometry?.status || ''
|
|
1466
|
+
).toLowerCase() === 'complete'
|
|
1467
|
+
)
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1043
1470
|
/**
|
|
1044
1471
|
* Checks whether a precise body/component pair is a package-family mismatch.
|
|
1045
1472
|
* @param {{ name?: string, identifier?: string }} componentBody Component-body record.
|
|
@@ -1056,6 +1483,10 @@ export class PcbScene3dBuilder {
|
|
|
1056
1483
|
Number(distanceMil || 0) <=
|
|
1057
1484
|
PcbScene3dBuilder.#EXACT_BODY_MISMATCH_TOLERANCE_MIL &&
|
|
1058
1485
|
PcbScene3dBuilder.#isComponentPackageBody(componentBody) &&
|
|
1486
|
+
!PcbScene3dBuilder.#isTimingStackBodyComponentPair(
|
|
1487
|
+
componentBody,
|
|
1488
|
+
component
|
|
1489
|
+
) &&
|
|
1059
1490
|
!PcbScene3dBuilder.#isAuthoredBodyIdentity(componentBody) &&
|
|
1060
1491
|
PcbScene3dPlacementSideResolver.scoreBodyComponentAffinity(
|
|
1061
1492
|
componentBody,
|
|
@@ -1834,11 +2265,16 @@ export class PcbScene3dBuilder {
|
|
|
1834
2265
|
|
|
1835
2266
|
/**
|
|
1836
2267
|
* Resolves the owned or nearby pad-span box around one component.
|
|
1837
|
-
* @param {{ x: number, y: number, componentIndex?: number, layer?: string }} component
|
|
2268
|
+
* @param {{ x: number, y: number, componentIndex?: number, layer?: string, rotation?: number }} component
|
|
1838
2269
|
* @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[]} pads
|
|
2270
|
+
* @param {number} [rotationDeg] Body-local rotation used for span measurement.
|
|
1839
2271
|
* @returns {{ width: number, depth: number }}
|
|
1840
2272
|
*/
|
|
1841
|
-
static #resolvePadSpan(
|
|
2273
|
+
static #resolvePadSpan(
|
|
2274
|
+
component,
|
|
2275
|
+
pads,
|
|
2276
|
+
rotationDeg = Number(component?.rotation || 0)
|
|
2277
|
+
) {
|
|
1842
2278
|
const componentPads = PcbScene3dBuilder.#componentPads(component, pads)
|
|
1843
2279
|
const nearbyPads = pads.filter((pad) =>
|
|
1844
2280
|
PcbScene3dBuilder.#isPadNearComponent(component, pad)
|
|
@@ -1852,13 +2288,27 @@ export class PcbScene3dBuilder {
|
|
|
1852
2288
|
const mountSide = PcbScene3dBuilder.#resolveMountSide(component)
|
|
1853
2289
|
return (
|
|
1854
2290
|
PcbScene3dPadLocalSpanResolver.resolve(
|
|
1855
|
-
component,
|
|
2291
|
+
{ ...component, rotation: rotationDeg },
|
|
1856
2292
|
spanPads,
|
|
1857
2293
|
mountSide
|
|
1858
2294
|
) || { width: 0, depth: 0 }
|
|
1859
2295
|
)
|
|
1860
2296
|
}
|
|
1861
2297
|
|
|
2298
|
+
/**
|
|
2299
|
+
* Resolves the visible procedural component rotation.
|
|
2300
|
+
* @param {{ componentIndex?: number, rotation?: number }} component PCB component.
|
|
2301
|
+
* @param {object[]} pads PCB pads.
|
|
2302
|
+
* @param {string} mountSide Component mount side.
|
|
2303
|
+
* @returns {number}
|
|
2304
|
+
*/
|
|
2305
|
+
static #resolveComponentRotation(component, pads, mountSide) {
|
|
2306
|
+
return (
|
|
2307
|
+
PcbScene3dPadYawResolver.resolve(component, pads, mountSide) ??
|
|
2308
|
+
Number(component?.rotation || 0)
|
|
2309
|
+
)
|
|
2310
|
+
}
|
|
2311
|
+
|
|
1862
2312
|
/**
|
|
1863
2313
|
* Resolves pads explicitly owned by one component, preferring pads on the
|
|
1864
2314
|
* mounted surface when paste-mask side metadata is available.
|