altium-toolkit 1.1.32 → 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/AltiumGeneratedLibraryRecordBuilder.mjs +551 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +286 -2
- package/src/core/altium/AltiumPcbLibExporter.mjs +9 -2
- package/src/core/altium/AltiumSchLibExporter.mjs +1 -1
- package/src/core/altium/PcbComponentBodyPlacementNormalizer.mjs +120 -15
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +52 -4
- 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 +27 -2
- package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +44 -0
- package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +171 -6
- package/src/ui/AltiumScene3dShapeStackOwnerAdapter.mjs +782 -0
- package/src/ui/AltiumScene3dTwoRowFootprintDetector.mjs +90 -0
- package/src/ui/PcbScene3dBuilder.mjs +595 -28
- 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/PcbScene3dPlacementSideResolver.mjs +56 -0
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +638 -16
- package/src/ui/PcbScene3dStaticBodyRecovery.mjs +891 -0
- package/src/ui/PcbScene3dStaticBodySelectionKeyBuilder.mjs +358 -0
- package/src/ui/PcbScene3dStaticBodySymmetryRecovery.mjs +876 -0
|
@@ -9,13 +9,16 @@ import { AltiumScene3dExternalPlacementAdapter } from './AltiumScene3dExternalPl
|
|
|
9
9
|
import { AltiumScene3dBottomPadRotationAdapter } from './AltiumScene3dBottomPadRotationAdapter.mjs'
|
|
10
10
|
import { AltiumScene3dComponentBodyAdapter } from './AltiumScene3dComponentBodyAdapter.mjs'
|
|
11
11
|
import { AltiumScene3dAuthoredBodyAnchorAdapter } from './AltiumScene3dAuthoredBodyAnchorAdapter.mjs'
|
|
12
|
+
import { AltiumScene3dShapeStackOwnerAdapter } from './AltiumScene3dShapeStackOwnerAdapter.mjs'
|
|
12
13
|
import { PcbFootprintPrimitiveSelector } from './PcbFootprintPrimitiveSelector.mjs'
|
|
13
14
|
import { PcbScene3dPadLocalSpanResolver } from './PcbScene3dPadLocalSpanResolver.mjs'
|
|
14
15
|
import { PcbScene3dPackages } from './PcbScene3dPackages.mjs'
|
|
15
16
|
import { PcbScene3dPlacementSideResolver } from './PcbScene3dPlacementSideResolver.mjs'
|
|
16
17
|
import { PcbScene3dStaticBodyPlacementBuilder } from './PcbScene3dStaticBodyPlacementBuilder.mjs'
|
|
18
|
+
import { PcbScene3dPadYawResolver } from './PcbScene3dPadYawResolver.mjs'
|
|
17
19
|
import { PcbScene3dTextBoxLayoutResolver } from './PcbScene3dTextBoxLayoutResolver.mjs'
|
|
18
20
|
import { PcbFootprintPadAxisNormalizer } from './PcbFootprintPadAxisNormalizer.mjs'
|
|
21
|
+
import { PcbScene3dCopperRegionDetailBuilder } from './PcbScene3dCopperRegionDetailBuilder.mjs'
|
|
19
22
|
|
|
20
23
|
/**
|
|
21
24
|
* Builds deterministic 3D scene data from the normalized PCB model.
|
|
@@ -31,11 +34,27 @@ export class PcbScene3dBuilder {
|
|
|
31
34
|
static #UNMATCHED_BODY_OVERHANG_RATIO = 0.25
|
|
32
35
|
static #UNMATCHED_BODY_MIN_OVERHANG_MIL = 150
|
|
33
36
|
static #UNMATCHED_BODY_MAX_OVERHANG_MIL = 600
|
|
37
|
+
static #OVERSIZED_GENERIC_FALLBACK_MAX_MIL = 800
|
|
38
|
+
static #TIMING_STACK_BODY_RADIUS_MIL = 220
|
|
34
39
|
static #TRUETYPE_TEXT_WIDTH_RATIO = 0.55
|
|
40
|
+
static #LOW_CONFIDENCE_GENERIC_FOOTPRINT_PATTERN =
|
|
41
|
+
/(?:^|[^a-z0-9])(?:edge|finger|fingers|contact|contacts|mech|mechanical|jumper|jump)(?:$|[^a-z0-9])/i
|
|
35
42
|
static #AUTHORED_BODY_IDENTITY_PATTERN =
|
|
36
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
|
|
37
53
|
static #COMPONENT_PACKAGE_BODY_PATTERN =
|
|
38
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
|
|
39
58
|
|
|
40
59
|
/**
|
|
41
60
|
* Builds a scene description for host 3D renderers.
|
|
@@ -60,6 +79,7 @@ export class PcbScene3dBuilder {
|
|
|
60
79
|
const tracks = Array.isArray(pcb.tracks) ? pcb.tracks : []
|
|
61
80
|
const arcs = Array.isArray(pcb.arcs) ? pcb.arcs : []
|
|
62
81
|
const fills = Array.isArray(pcb.fills) ? pcb.fills : []
|
|
82
|
+
const regionFills = PcbScene3dCopperRegionDetailBuilder.build(pcb)
|
|
63
83
|
const texts = Array.isArray(pcb.texts) ? pcb.texts : []
|
|
64
84
|
const vias = Array.isArray(pcb.vias) ? pcb.vias : []
|
|
65
85
|
const silkscreenRegions =
|
|
@@ -87,6 +107,7 @@ export class PcbScene3dBuilder {
|
|
|
87
107
|
: []
|
|
88
108
|
}
|
|
89
109
|
const componentBodyModels = componentBodies.map((componentBody) =>
|
|
110
|
+
PcbScene3dBuilder.#shouldRenderStaticGeometryOnly(componentBody) ||
|
|
90
111
|
PcbScene3dBuilder.#shouldSuppressLayerlessBodyPlaceholder(
|
|
91
112
|
componentBody,
|
|
92
113
|
componentBodies,
|
|
@@ -141,15 +162,17 @@ export class PcbScene3dBuilder {
|
|
|
141
162
|
boardAssemblyModel:
|
|
142
163
|
modelRegistry?.resolveBoardAssemblyModel?.(documentModel) ||
|
|
143
164
|
null,
|
|
144
|
-
components: components
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
165
|
+
components: components
|
|
166
|
+
.map((component) =>
|
|
167
|
+
PcbScene3dBuilder.#buildComponent(
|
|
168
|
+
component,
|
|
169
|
+
pads,
|
|
170
|
+
board,
|
|
171
|
+
thicknessMil,
|
|
172
|
+
modelRegistry
|
|
173
|
+
)
|
|
151
174
|
)
|
|
152
|
-
|
|
175
|
+
.filter(Boolean),
|
|
153
176
|
externalPlacements: componentBodies
|
|
154
177
|
.map((componentBody, index) =>
|
|
155
178
|
PcbScene3dBuilder.#buildExternalPlacement(
|
|
@@ -167,6 +190,7 @@ export class PcbScene3dBuilder {
|
|
|
167
190
|
componentBodies,
|
|
168
191
|
bodyMatches,
|
|
169
192
|
components,
|
|
193
|
+
pads,
|
|
170
194
|
board,
|
|
171
195
|
thicknessMil
|
|
172
196
|
),
|
|
@@ -177,7 +201,7 @@ export class PcbScene3dBuilder {
|
|
|
177
201
|
pads,
|
|
178
202
|
tracks,
|
|
179
203
|
arcs,
|
|
180
|
-
fills,
|
|
204
|
+
fills: [...fills, ...regionFills],
|
|
181
205
|
vias,
|
|
182
206
|
polygons: Array.isArray(pcb.polygons) ? pcb.polygons : [],
|
|
183
207
|
silkscreen: {
|
|
@@ -191,8 +215,11 @@ export class PcbScene3dBuilder {
|
|
|
191
215
|
AltiumScene3dBottomPadRotationAdapter.apply(
|
|
192
216
|
AltiumScene3dComponentBodyAdapter.apply(
|
|
193
217
|
AltiumScene3dExternalPlacementAdapter.apply(
|
|
194
|
-
|
|
195
|
-
|
|
218
|
+
AltiumScene3dShapeStackOwnerAdapter.apply(
|
|
219
|
+
PcbScene3dBoardOutlineRefiner.refine(
|
|
220
|
+
sceneDescription,
|
|
221
|
+
sceneDocumentModel
|
|
222
|
+
),
|
|
196
223
|
sceneDocumentModel
|
|
197
224
|
),
|
|
198
225
|
sceneDocumentModel
|
|
@@ -210,7 +237,7 @@ export class PcbScene3dBuilder {
|
|
|
210
237
|
* @param {{ centerX: number, centerY: number }} board
|
|
211
238
|
* @param {number} thicknessMil
|
|
212
239
|
* @param {{ resolveComponentModel: (component: any) => { name: string, relativePath: string, format: string } | null } | null} modelRegistry
|
|
213
|
-
* @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 }}
|
|
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}
|
|
214
241
|
*/
|
|
215
242
|
static #buildComponent(
|
|
216
243
|
component,
|
|
@@ -220,8 +247,27 @@ export class PcbScene3dBuilder {
|
|
|
220
247
|
modelRegistry
|
|
221
248
|
) {
|
|
222
249
|
const mountSide = PcbScene3dBuilder.#resolveMountSide(component)
|
|
223
|
-
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
|
+
)
|
|
224
260
|
const body = PcbScene3dPackages.resolve(component, padSpan)
|
|
261
|
+
const externalModel = modelRegistry
|
|
262
|
+
? modelRegistry.resolveComponentModel(component)
|
|
263
|
+
: null
|
|
264
|
+
const suppressFallbackBody =
|
|
265
|
+
PcbScene3dBuilder.#shouldSuppressProceduralComponent(
|
|
266
|
+
component,
|
|
267
|
+
body,
|
|
268
|
+
externalModel
|
|
269
|
+
)
|
|
270
|
+
|
|
225
271
|
const halfBoardThickness = thicknessMil / 2
|
|
226
272
|
const halfBodyHeight = body.sizeMil.height / 2
|
|
227
273
|
const z =
|
|
@@ -230,9 +276,12 @@ export class PcbScene3dBuilder {
|
|
|
230
276
|
: halfBoardThickness + halfBodyHeight
|
|
231
277
|
|
|
232
278
|
return {
|
|
279
|
+
componentIndex: Number.isFinite(Number(component.componentIndex))
|
|
280
|
+
? Number(component.componentIndex)
|
|
281
|
+
: null,
|
|
233
282
|
designator: component.designator,
|
|
234
283
|
mountSide,
|
|
235
|
-
rotationDeg
|
|
284
|
+
rotationDeg,
|
|
236
285
|
positionMil: {
|
|
237
286
|
x: Number(component.x || 0) - Number(board.centerX || 0),
|
|
238
287
|
y: Number(component.y || 0) - Number(board.centerY || 0),
|
|
@@ -253,12 +302,90 @@ export class PcbScene3dBuilder {
|
|
|
253
302
|
? { ...component.parameters }
|
|
254
303
|
: {},
|
|
255
304
|
body,
|
|
256
|
-
externalModel
|
|
257
|
-
|
|
258
|
-
: null
|
|
305
|
+
externalModel,
|
|
306
|
+
...(suppressFallbackBody ? { renderFallbackBody: false } : {})
|
|
259
307
|
}
|
|
260
308
|
}
|
|
261
309
|
|
|
310
|
+
/**
|
|
311
|
+
* Checks whether one generated fallback body is too uncertain to render.
|
|
312
|
+
* @param {{ pattern?: string, source?: string, description?: string }} component Source component.
|
|
313
|
+
* @param {{ family?: string, sizeMil?: { width?: number, depth?: number } }} body Procedural body.
|
|
314
|
+
* @param {object | null} externalModel Resolved external model.
|
|
315
|
+
* @returns {boolean}
|
|
316
|
+
*/
|
|
317
|
+
static #shouldSuppressProceduralComponent(component, body, externalModel) {
|
|
318
|
+
return (
|
|
319
|
+
!externalModel &&
|
|
320
|
+
body?.family === 'generic' &&
|
|
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)
|
|
338
|
+
)
|
|
339
|
+
}
|
|
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
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Checks whether one generic fallback body spans too much of the board.
|
|
361
|
+
* @param {{ sizeMil?: { width?: number, depth?: number } }} body Procedural body.
|
|
362
|
+
* @returns {boolean}
|
|
363
|
+
*/
|
|
364
|
+
static #isOversizedGenericFallback(body) {
|
|
365
|
+
return (
|
|
366
|
+
Math.max(
|
|
367
|
+
Number(body?.sizeMil?.width || 0),
|
|
368
|
+
Number(body?.sizeMil?.depth || 0)
|
|
369
|
+
) > PcbScene3dBuilder.#OVERSIZED_GENERIC_FALLBACK_MAX_MIL
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Checks for footprint identities that describe board features or
|
|
375
|
+
* mechanical placeholders more often than physical package bodies.
|
|
376
|
+
* @param {{ pattern?: string, source?: string, description?: string }} component Source component.
|
|
377
|
+
* @returns {boolean}
|
|
378
|
+
*/
|
|
379
|
+
static #isLowConfidenceGenericFootprint(component) {
|
|
380
|
+
return PcbScene3dBuilder.#LOW_CONFIDENCE_GENERIC_FOOTPRINT_PATTERN.test(
|
|
381
|
+
[
|
|
382
|
+
component?.pattern,
|
|
383
|
+
component?.source,
|
|
384
|
+
component?.description
|
|
385
|
+
].join(' ')
|
|
386
|
+
)
|
|
387
|
+
}
|
|
388
|
+
|
|
262
389
|
/**
|
|
263
390
|
* Resolves one component-body model through the active registry.
|
|
264
391
|
* @param {{ modelId?: string, checksum?: number | null, name?: string }} componentBody Component body metadata.
|
|
@@ -272,7 +399,7 @@ export class PcbScene3dBuilder {
|
|
|
272
399
|
/**
|
|
273
400
|
* Builds one explicit external-model placement from normalized component
|
|
274
401
|
* body metadata.
|
|
275
|
-
* @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
|
|
276
403
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null } | null} matchedComponent
|
|
277
404
|
* @param {{ origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } | null} resolvedModel
|
|
278
405
|
* @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components
|
|
@@ -290,10 +417,24 @@ export class PcbScene3dBuilder {
|
|
|
290
417
|
board,
|
|
291
418
|
thicknessMil
|
|
292
419
|
) {
|
|
420
|
+
if (PcbScene3dBuilder.#shouldRenderStaticGeometryOnly(componentBody)) {
|
|
421
|
+
return null
|
|
422
|
+
}
|
|
423
|
+
|
|
293
424
|
if (!resolvedModel) {
|
|
294
425
|
return null
|
|
295
426
|
}
|
|
296
427
|
|
|
428
|
+
if (
|
|
429
|
+
PcbScene3dBuilder.#isPositiveTimingStackPackageBody(
|
|
430
|
+
componentBody,
|
|
431
|
+
matchedComponent,
|
|
432
|
+
components
|
|
433
|
+
)
|
|
434
|
+
) {
|
|
435
|
+
return null
|
|
436
|
+
}
|
|
437
|
+
|
|
297
438
|
if (
|
|
298
439
|
!matchedComponent &&
|
|
299
440
|
PcbScene3dBuilder.#shouldDropUnmatchedPackageBody(
|
|
@@ -352,7 +493,8 @@ export class PcbScene3dBuilder {
|
|
|
352
493
|
modelTransform: {
|
|
353
494
|
rotationDeg: modelRotation,
|
|
354
495
|
dzMil: PcbScene3dBuilder.#resolveComponentBodyVerticalOffset(
|
|
355
|
-
componentBody
|
|
496
|
+
componentBody,
|
|
497
|
+
matchedComponent
|
|
356
498
|
)
|
|
357
499
|
},
|
|
358
500
|
projection: PcbScene3dBuilder.#resolveProjectionDiagnostics(
|
|
@@ -361,24 +503,126 @@ export class PcbScene3dBuilder {
|
|
|
361
503
|
pads,
|
|
362
504
|
resolvedModel
|
|
363
505
|
),
|
|
506
|
+
...PcbScene3dBuilder.#componentBodyDisplayMetadata(componentBody),
|
|
364
507
|
externalModel: resolvedModel
|
|
365
508
|
}
|
|
366
509
|
}
|
|
367
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
|
+
|
|
368
524
|
/**
|
|
369
525
|
* Resolves the vertical offset that should remain after the viewer seats
|
|
370
526
|
* raw model bounds on the board face.
|
|
371
527
|
* @param {{ dzMil?: number, standoffHeightMil?: number | null }} componentBody Component-body placement metadata.
|
|
528
|
+
* @param {object | null} matchedComponent Matched owner component.
|
|
372
529
|
* @returns {number}
|
|
373
530
|
*/
|
|
374
|
-
static #resolveComponentBodyVerticalOffset(
|
|
531
|
+
static #resolveComponentBodyVerticalOffset(
|
|
532
|
+
componentBody,
|
|
533
|
+
matchedComponent = null
|
|
534
|
+
) {
|
|
375
535
|
const standoffHeightMil = Number(componentBody?.standoffHeightMil)
|
|
376
536
|
if (Number.isFinite(standoffHeightMil)) {
|
|
377
|
-
return standoffHeightMil < 0
|
|
537
|
+
return standoffHeightMil < 0 ||
|
|
538
|
+
PcbScene3dBuilder.#shouldPreservePositiveBodyStandoff(
|
|
539
|
+
componentBody,
|
|
540
|
+
matchedComponent
|
|
541
|
+
)
|
|
542
|
+
? standoffHeightMil
|
|
543
|
+
: 0
|
|
378
544
|
}
|
|
379
545
|
|
|
380
546
|
const dzMil = Number(componentBody?.dzMil)
|
|
381
|
-
return Number.isFinite(dzMil) &&
|
|
547
|
+
return Number.isFinite(dzMil) &&
|
|
548
|
+
(dzMil < 0 ||
|
|
549
|
+
PcbScene3dBuilder.#shouldPreservePositiveBodyStandoff(
|
|
550
|
+
componentBody,
|
|
551
|
+
matchedComponent
|
|
552
|
+
))
|
|
553
|
+
? dzMil
|
|
554
|
+
: 0
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Checks whether a positive shape-body standoff is part of an authored
|
|
559
|
+
* stack instead of a model-origin quirk that should be seated on the board.
|
|
560
|
+
* @param {object | null | undefined} componentBody Component-body row.
|
|
561
|
+
* @param {object | null} matchedComponent Matched owner component.
|
|
562
|
+
* @returns {boolean}
|
|
563
|
+
*/
|
|
564
|
+
static #shouldPreservePositiveBodyStandoff(
|
|
565
|
+
componentBody,
|
|
566
|
+
matchedComponent
|
|
567
|
+
) {
|
|
568
|
+
return (
|
|
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
|
|
610
|
+
)
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Checks whether one body row came from shape-based 3D body metadata.
|
|
615
|
+
* @param {object | null | undefined} componentBody Component-body row.
|
|
616
|
+
* @returns {boolean}
|
|
617
|
+
*/
|
|
618
|
+
static #isShapeBasedComponentBody(componentBody) {
|
|
619
|
+
return (
|
|
620
|
+
String(componentBody?.sourceStream || '').includes(
|
|
621
|
+
'ShapeBasedComponentBodies'
|
|
622
|
+
) ||
|
|
623
|
+
Boolean(componentBody?.staticGeometry) ||
|
|
624
|
+
Boolean(componentBody?.modelTypeName)
|
|
625
|
+
)
|
|
382
626
|
}
|
|
383
627
|
|
|
384
628
|
/**
|
|
@@ -526,7 +770,8 @@ export class PcbScene3dBuilder {
|
|
|
526
770
|
|
|
527
771
|
componentBodies.forEach((componentBody, bodyIndex) => {
|
|
528
772
|
if (
|
|
529
|
-
!PcbScene3dBuilder.#
|
|
773
|
+
!PcbScene3dBuilder.#isMatchableComponentBody(
|
|
774
|
+
componentBody,
|
|
530
775
|
resolvedBodyModels,
|
|
531
776
|
bodyIndex
|
|
532
777
|
)
|
|
@@ -613,7 +858,8 @@ export class PcbScene3dBuilder {
|
|
|
613
858
|
const groupedBodyIndexes = new Map()
|
|
614
859
|
componentBodies.forEach((componentBody, bodyIndex) => {
|
|
615
860
|
if (
|
|
616
|
-
!PcbScene3dBuilder.#
|
|
861
|
+
!PcbScene3dBuilder.#isMatchableComponentBody(
|
|
862
|
+
componentBody,
|
|
617
863
|
resolvedBodyModels,
|
|
618
864
|
bodyIndex
|
|
619
865
|
)
|
|
@@ -682,9 +928,115 @@ export class PcbScene3dBuilder {
|
|
|
682
928
|
})
|
|
683
929
|
})
|
|
684
930
|
|
|
931
|
+
PcbScene3dBuilder.#assignStaticShieldFrameBodyMatches(
|
|
932
|
+
matches,
|
|
933
|
+
componentBodies,
|
|
934
|
+
components
|
|
935
|
+
)
|
|
936
|
+
|
|
685
937
|
return matches
|
|
686
938
|
}
|
|
687
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
|
+
|
|
688
1040
|
/**
|
|
689
1041
|
* Builds reusable identity statistics for body/component matching.
|
|
690
1042
|
* @param {{ modelId?: string, name?: string, identifier?: string }[]} componentBodies
|
|
@@ -703,7 +1055,8 @@ export class PcbScene3dBuilder {
|
|
|
703
1055
|
|
|
704
1056
|
componentBodies.forEach((componentBody, bodyIndex) => {
|
|
705
1057
|
if (
|
|
706
|
-
!PcbScene3dBuilder.#
|
|
1058
|
+
!PcbScene3dBuilder.#isMatchableComponentBody(
|
|
1059
|
+
componentBody,
|
|
707
1060
|
resolvedBodyModels,
|
|
708
1061
|
bodyIndex
|
|
709
1062
|
)
|
|
@@ -740,6 +1093,29 @@ export class PcbScene3dBuilder {
|
|
|
740
1093
|
return { bodyGroupCounts, candidateComponentCounts }
|
|
741
1094
|
}
|
|
742
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
|
+
|
|
743
1119
|
/**
|
|
744
1120
|
* Returns true when one body row can produce a renderable external model.
|
|
745
1121
|
* @param {unknown[]} resolvedBodyModels Resolved body-model entries.
|
|
@@ -754,6 +1130,26 @@ export class PcbScene3dBuilder {
|
|
|
754
1130
|
)
|
|
755
1131
|
}
|
|
756
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
|
+
|
|
757
1153
|
/**
|
|
758
1154
|
* Returns true when the body/component anchors are close enough to be
|
|
759
1155
|
* considered an explicit placement match.
|
|
@@ -923,6 +1319,154 @@ export class PcbScene3dBuilder {
|
|
|
923
1319
|
)
|
|
924
1320
|
}
|
|
925
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
|
+
|
|
926
1470
|
/**
|
|
927
1471
|
* Checks whether a precise body/component pair is a package-family mismatch.
|
|
928
1472
|
* @param {{ name?: string, identifier?: string }} componentBody Component-body record.
|
|
@@ -939,6 +1483,10 @@ export class PcbScene3dBuilder {
|
|
|
939
1483
|
Number(distanceMil || 0) <=
|
|
940
1484
|
PcbScene3dBuilder.#EXACT_BODY_MISMATCH_TOLERANCE_MIL &&
|
|
941
1485
|
PcbScene3dBuilder.#isComponentPackageBody(componentBody) &&
|
|
1486
|
+
!PcbScene3dBuilder.#isTimingStackBodyComponentPair(
|
|
1487
|
+
componentBody,
|
|
1488
|
+
component
|
|
1489
|
+
) &&
|
|
942
1490
|
!PcbScene3dBuilder.#isAuthoredBodyIdentity(componentBody) &&
|
|
943
1491
|
PcbScene3dPlacementSideResolver.scoreBodyComponentAffinity(
|
|
944
1492
|
componentBody,
|
|
@@ -1717,11 +2265,16 @@ export class PcbScene3dBuilder {
|
|
|
1717
2265
|
|
|
1718
2266
|
/**
|
|
1719
2267
|
* Resolves the owned or nearby pad-span box around one component.
|
|
1720
|
-
* @param {{ x: number, y: number, componentIndex?: number, layer?: string }} component
|
|
2268
|
+
* @param {{ x: number, y: number, componentIndex?: number, layer?: string, rotation?: number }} component
|
|
1721
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.
|
|
1722
2271
|
* @returns {{ width: number, depth: number }}
|
|
1723
2272
|
*/
|
|
1724
|
-
static #resolvePadSpan(
|
|
2273
|
+
static #resolvePadSpan(
|
|
2274
|
+
component,
|
|
2275
|
+
pads,
|
|
2276
|
+
rotationDeg = Number(component?.rotation || 0)
|
|
2277
|
+
) {
|
|
1725
2278
|
const componentPads = PcbScene3dBuilder.#componentPads(component, pads)
|
|
1726
2279
|
const nearbyPads = pads.filter((pad) =>
|
|
1727
2280
|
PcbScene3dBuilder.#isPadNearComponent(component, pad)
|
|
@@ -1735,13 +2288,27 @@ export class PcbScene3dBuilder {
|
|
|
1735
2288
|
const mountSide = PcbScene3dBuilder.#resolveMountSide(component)
|
|
1736
2289
|
return (
|
|
1737
2290
|
PcbScene3dPadLocalSpanResolver.resolve(
|
|
1738
|
-
component,
|
|
2291
|
+
{ ...component, rotation: rotationDeg },
|
|
1739
2292
|
spanPads,
|
|
1740
2293
|
mountSide
|
|
1741
2294
|
) || { width: 0, depth: 0 }
|
|
1742
2295
|
)
|
|
1743
2296
|
}
|
|
1744
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
|
+
|
|
1745
2312
|
/**
|
|
1746
2313
|
* Resolves pads explicitly owned by one component, preferring pads on the
|
|
1747
2314
|
* mounted surface when paste-mask side metadata is available.
|