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.
@@ -3,6 +3,8 @@
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
5
  import { PcbScene3dPlacementSideResolver } from './PcbScene3dPlacementSideResolver.mjs'
6
+ import { PcbScene3dStaticBodyRecovery } from './PcbScene3dStaticBodyRecovery.mjs'
7
+ import { PcbScene3dStaticBodySelectionKeyBuilder } from './PcbScene3dStaticBodySelectionKeyBuilder.mjs'
6
8
 
7
9
  /**
8
10
  * Builds scene placements for static shape-based 3D bodies.
@@ -12,8 +14,11 @@ export class PcbScene3dStaticBodyPlacementBuilder {
12
14
  static #UNMATCHED_BODY_MIN_OVERHANG_MIL = 150
13
15
  static #UNMATCHED_BODY_MAX_OVERHANG_MIL = 600
14
16
  static #OWNER_AFFINITY_DISTANCE_MIL = 600
17
+ static #OWNER_BOUNDS_TOLERANCE_MIL = 20
15
18
  static #OWNER_EXACT_DISTANCE_MIL = 5
16
19
  static #OWNER_EXACT_MAX_SPAN_MIL = 500
20
+ static #SOURCE_COORDINATE_MIRROR_TOLERANCE_MIL = 5
21
+ static #SOURCE_COORDINATE_MIRROR_MIN_OFFSET_MIL = 20
17
22
  static #GENERIC_MECHANICAL_IDENTITY_TOKENS = new Set([
18
23
  'can',
19
24
  'cover',
@@ -34,7 +39,7 @@ export class PcbScene3dStaticBodyPlacementBuilder {
34
39
  * @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[]} pads Pads.
35
40
  * @param {{ centerX: number, centerY: number, minX?: number, minY?: number, widthMil?: number, heightMil?: number }} board Board.
36
41
  * @param {number} thicknessMil Board thickness.
37
- * @returns {{ designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, geometry: object }[]}
42
+ * @returns {{ designator: string, selectionKey: string, sourceIdentityKey: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, geometry: object }[]}
38
43
  */
39
44
  static build(
40
45
  componentBodies,
@@ -44,9 +49,14 @@ export class PcbScene3dStaticBodyPlacementBuilder {
44
49
  board,
45
50
  thicknessMil
46
51
  ) {
47
- return (Array.isArray(componentBodies) ? componentBodies : [])
52
+ const recoveredBodies = PcbScene3dStaticBodyRecovery.recover(
53
+ componentBodies,
54
+ bodyMatches
55
+ )
56
+
57
+ const placementRows = recoveredBodies
48
58
  .map((componentBody, index) =>
49
- PcbScene3dStaticBodyPlacementBuilder.#buildPlacement(
59
+ PcbScene3dStaticBodyPlacementBuilder.#buildPlacementRow(
50
60
  componentBody,
51
61
  bodyMatches?.[index] || null,
52
62
  components,
@@ -55,6 +65,8 @@ export class PcbScene3dStaticBodyPlacementBuilder {
55
65
  )
56
66
  )
57
67
  .filter(Boolean)
68
+
69
+ return PcbScene3dStaticBodySelectionKeyBuilder.assign(placementRows)
58
70
  }
59
71
 
60
72
  /**
@@ -66,6 +78,38 @@ export class PcbScene3dStaticBodyPlacementBuilder {
66
78
  return Boolean(geometry && geometry.status === 'complete')
67
79
  }
68
80
 
81
+ /**
82
+ * Builds one static body placement row with its matched owner context.
83
+ * @param {{ componentIndex?: number, embedded?: boolean, identifier?: string, name?: string, layer?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, standoffHeightMil?: number | null, overallHeightMil?: number | null, bodyOpacity?: number | string, staticGeometry?: object }} componentBody Component body.
84
+ * @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null } | null} matchedComponent Matched component.
85
+ * @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components Components.
86
+ * @param {{ centerX: number, centerY: number, minX?: number, minY?: number, widthMil?: number, heightMil?: number }} board Board.
87
+ * @param {number} thicknessMil Board thickness.
88
+ * @returns {{ placement: { designator: string, sourceIdentityKey: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, geometry: object }, matchedComponent: object | null } | null}
89
+ */
90
+ static #buildPlacementRow(
91
+ componentBody,
92
+ matchedComponent,
93
+ components,
94
+ board,
95
+ thicknessMil
96
+ ) {
97
+ const placement = PcbScene3dStaticBodyPlacementBuilder.#buildPlacement(
98
+ componentBody,
99
+ matchedComponent,
100
+ components,
101
+ board,
102
+ thicknessMil
103
+ )
104
+
105
+ return placement
106
+ ? {
107
+ placement,
108
+ matchedComponent
109
+ }
110
+ : null
111
+ }
112
+
69
113
  /**
70
114
  * Builds one static shape-body scene placement.
71
115
  * @param {{ componentIndex?: number, embedded?: boolean, identifier?: string, name?: string, layer?: string, positionMil?: { x?: number, y?: number }, rotationDeg?: number, standoffHeightMil?: number | null, overallHeightMil?: number | null, bodyOpacity?: number | string, staticGeometry?: object }} componentBody Component body.
@@ -73,7 +117,7 @@ export class PcbScene3dStaticBodyPlacementBuilder {
73
117
  * @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components Components.
74
118
  * @param {{ centerX: number, centerY: number, minX?: number, minY?: number, widthMil?: number, heightMil?: number }} board Board.
75
119
  * @param {number} thicknessMil Board thickness.
76
- * @returns {{ designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, geometry: object } | null}
120
+ * @returns {{ designator: string, sourceIdentityKey: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, geometry: object } | null}
77
121
  */
78
122
  static #buildPlacement(
79
123
  componentBody,
@@ -82,24 +126,30 @@ export class PcbScene3dStaticBodyPlacementBuilder {
82
126
  board,
83
127
  thicknessMil
84
128
  ) {
85
- const geometry =
129
+ const staticGeometry =
86
130
  PcbScene3dStaticBodyPlacementBuilder.#normalizeStaticGeometry(
87
131
  componentBody,
88
- componentBody?.staticGeometry
132
+ componentBody?.staticGeometry,
133
+ matchedComponent
89
134
  )
135
+ const geometry = staticGeometry.geometry
90
136
 
91
137
  if (
92
138
  !PcbScene3dStaticBodyPlacementBuilder.#isCompleteGeometry(geometry)
93
139
  ) {
94
140
  return null
95
141
  }
142
+ const sourcePosition =
143
+ staticGeometry.placementCenterMil ||
144
+ PcbScene3dStaticBodyPlacementBuilder.#sourcePosition(componentBody)
96
145
 
97
146
  if (
98
147
  !matchedComponent &&
99
148
  PcbScene3dStaticBodyPlacementBuilder.#shouldSuppressStaticBody(
100
149
  componentBody,
101
150
  geometry,
102
- components
151
+ components,
152
+ sourcePosition
103
153
  )
104
154
  ) {
105
155
  return null
@@ -122,8 +172,6 @@ export class PcbScene3dStaticBodyPlacementBuilder {
122
172
  components,
123
173
  board
124
174
  )
125
- const sourcePosition =
126
- PcbScene3dStaticBodyPlacementBuilder.#sourcePosition(componentBody)
127
175
  const heightMil =
128
176
  PcbScene3dStaticBodyPlacementBuilder.#geometryHeight(geometry)
129
177
  const standoffMil = Math.abs(
@@ -142,6 +190,10 @@ export class PcbScene3dStaticBodyPlacementBuilder {
142
190
  String(
143
191
  componentBody.identifier || componentBody.name || '3D body'
144
192
  ),
193
+ sourceIdentityKey:
194
+ PcbScene3dStaticBodyPlacementBuilder.#sourceIdentityKey(
195
+ componentBody
196
+ ),
145
197
  mountSide,
146
198
  rotationDeg: PcbScene3dStaticBodyPlacementBuilder.#normalizeAngle(
147
199
  Number(componentBody.rotationDeg || 0) +
@@ -173,9 +225,10 @@ export class PcbScene3dStaticBodyPlacementBuilder {
173
225
  * Converts source-space polygon vertices into render-local geometry.
174
226
  * @param {{ positionMil?: { x?: number, y?: number } }} componentBody Component body.
175
227
  * @param {object | undefined} geometry Static geometry.
176
- * @returns {object | undefined}
228
+ * @param {{ x?: number, y?: number } | null} matchedComponent Matched owner component.
229
+ * @returns {{ geometry: object | undefined, placementCenterMil?: { x: number, y: number } }}
177
230
  */
178
- static #normalizeStaticGeometry(componentBody, geometry) {
231
+ static #normalizeStaticGeometry(componentBody, geometry, matchedComponent) {
179
232
  if (
180
233
  String(geometry?.kind || '').toLowerCase() !== 'extruded-polygon' ||
181
234
  !Array.isArray(geometry?.verticesMil) ||
@@ -185,24 +238,142 @@ export class PcbScene3dStaticBodyPlacementBuilder {
185
238
  geometry.verticesMil
186
239
  )
187
240
  ) {
188
- return geometry
241
+ return { geometry }
189
242
  }
190
243
 
191
244
  const center =
192
245
  PcbScene3dStaticBodyPlacementBuilder.#polygonBoundsCenter(
193
246
  geometry.verticesMil
194
247
  )
248
+ const mirror =
249
+ PcbScene3dStaticBodyPlacementBuilder.#sourceCoordinateMirror(
250
+ componentBody,
251
+ center,
252
+ matchedComponent
253
+ )
254
+ const placementCenter = mirror?.placementCenterMil || center
195
255
 
196
256
  return {
197
- ...geometry,
198
- verticesMil: geometry.verticesMil.map((vertex) => ({
199
- x: PcbScene3dStaticBodyPlacementBuilder.#roundMil(
200
- Number(vertex?.x || 0) - center.x
201
- ),
202
- y: PcbScene3dStaticBodyPlacementBuilder.#roundMil(
203
- Number(vertex?.y || 0) - center.y
204
- )
205
- }))
257
+ placementCenterMil: placementCenter,
258
+ geometry: {
259
+ ...geometry,
260
+ verticesMil: geometry.verticesMil.map((vertex) => {
261
+ const normalizedVertex =
262
+ PcbScene3dStaticBodyPlacementBuilder.#normalizeVertex(
263
+ vertex,
264
+ mirror
265
+ )
266
+
267
+ return {
268
+ x: PcbScene3dStaticBodyPlacementBuilder.#roundMil(
269
+ normalizedVertex.x - placementCenter.x
270
+ ),
271
+ y: PcbScene3dStaticBodyPlacementBuilder.#roundMil(
272
+ normalizedVertex.y - placementCenter.y
273
+ )
274
+ }
275
+ })
276
+ }
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Resolves owner-axis mirror transforms for source-coordinate polygons
282
+ * whose body anchor is opposite their raw polygon bounds.
283
+ * @param {{ positionMil?: { x?: number, y?: number } }} componentBody Component body.
284
+ * @param {{ x: number, y: number }} boundsCenter Source-coordinate polygon bounds center.
285
+ * @param {{ x?: number, y?: number } | null} matchedComponent Matched owner component.
286
+ * @returns {{ mirrorAxisX?: number, mirrorAxisY?: number, placementCenterMil: { x: number, y: number } } | null}
287
+ */
288
+ static #sourceCoordinateMirror(
289
+ componentBody,
290
+ boundsCenter,
291
+ matchedComponent
292
+ ) {
293
+ const source =
294
+ PcbScene3dStaticBodyPlacementBuilder.#sourcePosition(componentBody)
295
+ const xDecision =
296
+ PcbScene3dStaticBodyPlacementBuilder.#sourceCoordinateMirrorAxis(
297
+ boundsCenter.x,
298
+ source.x,
299
+ Number(matchedComponent?.x)
300
+ )
301
+ const yDecision =
302
+ PcbScene3dStaticBodyPlacementBuilder.#sourceCoordinateMirrorAxis(
303
+ boundsCenter.y,
304
+ source.y,
305
+ Number(matchedComponent?.y)
306
+ )
307
+
308
+ if (
309
+ !xDecision.valid ||
310
+ !yDecision.valid ||
311
+ (!xDecision.mirrored && !yDecision.mirrored)
312
+ ) {
313
+ return null
314
+ }
315
+
316
+ return {
317
+ mirrorAxisX: xDecision.mirrored
318
+ ? Number(matchedComponent?.x)
319
+ : undefined,
320
+ mirrorAxisY: yDecision.mirrored
321
+ ? Number(matchedComponent?.y)
322
+ : undefined,
323
+ placementCenterMil: {
324
+ x: source.x,
325
+ y: source.y
326
+ }
327
+ }
328
+ }
329
+
330
+ /**
331
+ * Checks whether one source-coordinate axis is aligned or mirrored.
332
+ * @param {number} center Raw polygon center coordinate.
333
+ * @param {number} source Body source coordinate.
334
+ * @param {number} ownerAxis Owner coordinate.
335
+ * @returns {{ valid: boolean, mirrored: boolean }}
336
+ */
337
+ static #sourceCoordinateMirrorAxis(center, source, ownerAxis) {
338
+ const tolerance =
339
+ PcbScene3dStaticBodyPlacementBuilder
340
+ .#SOURCE_COORDINATE_MIRROR_TOLERANCE_MIL
341
+ const offset = Math.abs(Number(center || 0) - Number(source || 0))
342
+ if (offset <= tolerance) {
343
+ return { valid: true, mirrored: false }
344
+ }
345
+
346
+ const mirroredCenter = 2 * Number(ownerAxis) - Number(center || 0)
347
+ const mirrorError = Math.abs(mirroredCenter - Number(source || 0))
348
+
349
+ return {
350
+ valid:
351
+ Number.isFinite(ownerAxis) &&
352
+ offset >
353
+ PcbScene3dStaticBodyPlacementBuilder
354
+ .#SOURCE_COORDINATE_MIRROR_MIN_OFFSET_MIL &&
355
+ mirrorError <= tolerance,
356
+ mirrored: true
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Converts one source-space vertex into render source-space coordinates.
362
+ * @param {{ x?: number, y?: number }} vertex Source vertex.
363
+ * @param {{ mirrorAxisX?: number, mirrorAxisY?: number } | null} mirror Optional mirror transform.
364
+ * @returns {{ x: number, y: number }}
365
+ */
366
+ static #normalizeVertex(vertex, mirror) {
367
+ const sourceX = Number(vertex?.x || 0)
368
+ const sourceY = Number(vertex?.y || 0)
369
+
370
+ return {
371
+ x: Number.isFinite(mirror?.mirrorAxisX)
372
+ ? 2 * mirror.mirrorAxisX - sourceX
373
+ : sourceX,
374
+ y: Number.isFinite(mirror?.mirrorAxisY)
375
+ ? 2 * mirror.mirrorAxisY - sourceY
376
+ : sourceY
206
377
  }
207
378
  }
208
379
 
@@ -264,23 +435,45 @@ export class PcbScene3dStaticBodyPlacementBuilder {
264
435
  metadata.bodyColor = componentBody.bodyColor
265
436
  }
266
437
 
267
- const opacity = Number(componentBody?.bodyOpacity)
268
- if (Number.isFinite(opacity)) {
438
+ const opacity = PcbScene3dStaticBodyRecovery.renderableOpacity(
439
+ componentBody?.bodyOpacity
440
+ )
441
+ if (opacity !== undefined) {
269
442
  metadata.bodyOpacity = opacity
270
443
  }
271
444
 
272
445
  return metadata
273
446
  }
274
447
 
448
+ /**
449
+ * Resolves the stable source identity shared by sibling static bodies.
450
+ * @param {{ identifier?: string, name?: string, modelId?: string }} componentBody Component body.
451
+ * @returns {string}
452
+ */
453
+ static #sourceIdentityKey(componentBody) {
454
+ return String(
455
+ componentBody?.identifier ||
456
+ componentBody?.name ||
457
+ componentBody?.modelId ||
458
+ ''
459
+ ).trim()
460
+ }
461
+
275
462
  /**
276
463
  * Checks whether an unowned static body is likely a translucent mechanical
277
464
  * overlay instead of a component package.
278
465
  * @param {{ componentIndex?: number, embedded?: boolean, bodyOpacity?: number | string, identifier?: string, name?: string, positionMil?: { x?: number, y?: number } }} componentBody Component body.
279
466
  * @param {object} geometry Static geometry.
280
467
  * @param {{ componentIndex?: number, x?: number, y?: number, pattern?: string, source?: string, modelPath?: string, description?: string, parameters?: object, provenance?: object }[]} components Components.
468
+ * @param {{ x: number, y: number }} sourcePosition Source-space placement center.
281
469
  * @returns {boolean}
282
470
  */
283
- static #shouldSuppressStaticBody(componentBody, geometry, components) {
471
+ static #shouldSuppressStaticBody(
472
+ componentBody,
473
+ geometry,
474
+ components,
475
+ sourcePosition
476
+ ) {
284
477
  return (
285
478
  PcbScene3dStaticBodyPlacementBuilder.#isTranslucentBody(
286
479
  componentBody
@@ -288,7 +481,8 @@ export class PcbScene3dStaticBodyPlacementBuilder {
288
481
  !PcbScene3dStaticBodyPlacementBuilder.#hasLikelyComponentOwner(
289
482
  componentBody,
290
483
  geometry,
291
- components
484
+ components,
485
+ sourcePosition
292
486
  )
293
487
  )
294
488
  }
@@ -299,9 +493,11 @@ export class PcbScene3dStaticBodyPlacementBuilder {
299
493
  * @returns {boolean}
300
494
  */
301
495
  static #isTranslucentBody(componentBody) {
302
- const opacity = Number(componentBody?.bodyOpacity)
303
-
304
- return Number.isFinite(opacity) && opacity >= 0 && opacity < 1
496
+ return (
497
+ PcbScene3dStaticBodyRecovery.renderableOpacity(
498
+ componentBody?.bodyOpacity
499
+ ) !== undefined
500
+ )
305
501
  }
306
502
 
307
503
  /**
@@ -310,9 +506,15 @@ export class PcbScene3dStaticBodyPlacementBuilder {
310
506
  * @param {{ componentIndex?: number, embedded?: boolean, identifier?: string, name?: string, positionMil?: { x?: number, y?: number } }} componentBody Component body.
311
507
  * @param {object} geometry Static geometry.
312
508
  * @param {{ componentIndex?: number, x?: number, y?: number, pattern?: string, source?: string, modelPath?: string, description?: string, parameters?: object, provenance?: object }[]} components Components.
509
+ * @param {{ x: number, y: number }} sourcePosition Source-space placement center.
313
510
  * @returns {boolean}
314
511
  */
315
- static #hasLikelyComponentOwner(componentBody, geometry, components) {
512
+ static #hasLikelyComponentOwner(
513
+ componentBody,
514
+ geometry,
515
+ components,
516
+ sourcePosition
517
+ ) {
316
518
  if (componentBody?.embedded) {
317
519
  return true
318
520
  }
@@ -334,6 +536,11 @@ export class PcbScene3dStaticBodyPlacementBuilder {
334
536
  PcbScene3dStaticBodyPlacementBuilder.#hasGenericMechanicalIdentity(
335
537
  componentBody
336
538
  )
539
+ const sourceBounds =
540
+ PcbScene3dStaticBodyPlacementBuilder.#geometrySourceBounds(
541
+ geometry,
542
+ sourcePosition
543
+ )
337
544
 
338
545
  return (Array.isArray(components) ? components : []).some(
339
546
  (component) => {
@@ -349,6 +556,19 @@ export class PcbScene3dStaticBodyPlacementBuilder {
349
556
  )
350
557
 
351
558
  return (
559
+ (genericMechanical &&
560
+ PcbScene3dStaticBodyPlacementBuilder.#isShieldOwner(
561
+ component
562
+ ) &&
563
+ (distance <=
564
+ PcbScene3dStaticBodyPlacementBuilder
565
+ .#OWNER_AFFINITY_DISTANCE_MIL ||
566
+ PcbScene3dStaticBodyPlacementBuilder.#boundsContainPoint(
567
+ sourceBounds,
568
+ component,
569
+ PcbScene3dStaticBodyPlacementBuilder
570
+ .#OWNER_BOUNDS_TOLERANCE_MIL
571
+ ))) ||
352
572
  (!genericMechanical &&
353
573
  affinityScore > 0 &&
354
574
  distance <=
@@ -366,6 +586,91 @@ export class PcbScene3dStaticBodyPlacementBuilder {
366
586
  )
367
587
  }
368
588
 
589
+ /**
590
+ * Resolves source-space bounds for one static polygon geometry.
591
+ * @param {object} geometry Static geometry.
592
+ * @param {{ x?: number, y?: number }} sourcePosition Source-space placement center.
593
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
594
+ */
595
+ static #geometrySourceBounds(geometry, sourcePosition) {
596
+ if (
597
+ !Array.isArray(geometry?.verticesMil) ||
598
+ !geometry.verticesMil.length
599
+ ) {
600
+ return null
601
+ }
602
+
603
+ const points = geometry.verticesMil
604
+ .map((vertex) => ({
605
+ x: Number(vertex?.x || 0) + Number(sourcePosition?.x || 0),
606
+ y: Number(vertex?.y || 0) + Number(sourcePosition?.y || 0)
607
+ }))
608
+ .filter(
609
+ (point) => Number.isFinite(point.x) && Number.isFinite(point.y)
610
+ )
611
+ if (!points.length) {
612
+ return null
613
+ }
614
+
615
+ const xs = points.map((point) => point.x)
616
+ const ys = points.map((point) => point.y)
617
+
618
+ return {
619
+ minX: Math.min(...xs),
620
+ minY: Math.min(...ys),
621
+ maxX: Math.max(...xs),
622
+ maxY: Math.max(...ys)
623
+ }
624
+ }
625
+
626
+ /**
627
+ * Checks whether bounds contain a component point with tolerance.
628
+ * @param {{ minX: number, minY: number, maxX: number, maxY: number } | null} bounds Bounds.
629
+ * @param {{ x?: number, y?: number }} point Candidate point.
630
+ * @param {number} toleranceMil Bounds tolerance.
631
+ * @returns {boolean}
632
+ */
633
+ static #boundsContainPoint(bounds, point, toleranceMil) {
634
+ if (!bounds) {
635
+ return false
636
+ }
637
+
638
+ const x = Number(point?.x || 0)
639
+ const y = Number(point?.y || 0)
640
+ const tolerance = Math.max(Number(toleranceMil || 0), 0)
641
+
642
+ return (
643
+ Number.isFinite(x) &&
644
+ Number.isFinite(y) &&
645
+ x >= bounds.minX - tolerance &&
646
+ x <= bounds.maxX + tolerance &&
647
+ y >= bounds.minY - tolerance &&
648
+ y <= bounds.maxY + tolerance
649
+ )
650
+ }
651
+
652
+ /**
653
+ * Checks whether a component identity describes a shield owner strongly
654
+ * enough to keep generic translucent shield sub-bodies.
655
+ * @param {{ designator?: string, pattern?: string, source?: string, description?: string, parameters?: object, provenance?: object }} component Component row.
656
+ * @returns {boolean}
657
+ */
658
+ static #isShieldOwner(component) {
659
+ const identityText = [
660
+ component?.pattern,
661
+ component?.source,
662
+ component?.description,
663
+ ...Object.values(component?.parameters || {}),
664
+ ...Object.values(component?.provenance || {})
665
+ ]
666
+ .join(' ')
667
+ .toLowerCase()
668
+
669
+ return /(?:^|[^a-z0-9])(?:emi|rfi|shield)(?:$|[^a-z0-9])/u.test(
670
+ identityText
671
+ )
672
+ }
673
+
369
674
  /**
370
675
  * Checks for generic mechanical labels that are too weak to prove package
371
676
  * ownership for translucent static bodies.
@@ -390,6 +695,7 @@ export class PcbScene3dStaticBodyPlacementBuilder {
390
695
  static #identityTokens(componentBody) {
391
696
  return [componentBody?.identifier, componentBody?.name]
392
697
  .join(' ')
698
+ .replace(/([a-z])([A-Z])/gu, '$1 $2')
393
699
  .toLowerCase()
394
700
  .split(/[^a-z0-9]+/g)
395
701
  .flatMap((fragment) => fragment.match(/[a-z]+|\d+/g) || [])