altium-toolkit 1.1.36 → 1.1.38

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.
@@ -1,4 +1,5 @@
1
1
  import { PcbScene3dPackageDimensionResolver } from './PcbScene3dPackageDimensionResolver.mjs'
2
+ import { AltiumScene3dRepeatedFullFootprintBodyCollapse } from './AltiumScene3dRepeatedFullFootprintBodyCollapse.mjs'
2
3
 
3
4
  const CONNECTOR_TOKENS = new Set([
4
5
  'antenna',
@@ -12,6 +13,8 @@ const PASSIVE_BODY_PATTERN =
12
13
  /(?:^|[^a-z0-9])(?:cap|capacitor|res|resistor|ind|inductor|ferrite|bead|lqw|lqg)(?:$|[^a-z0-9])/i
13
14
  const COMPACT_IC_BODY_PATTERN =
14
15
  /(?:^|[^a-z0-9])(?:u?qfn(?:[-_ ]?\d+)?|dfn(?:[-_ ]?\d+)?|qfp(?:[-_ ]?\d+)?|bga(?:[-_ ]?\d+)?|lga(?:[-_ ]?\d+)?)(?:$|[^a-z0-9])/i
16
+ const MODEL_BOUNDS_CORNER_ORIGIN_PACKAGE_PATTERN =
17
+ /(?:^|[^a-z0-9])(?:[a-z0-9]*qfn[a-z0-9]*|[a-z0-9]*dfn[a-z0-9]*|qfp|lqfp|tqfp|bga|lga)(?:[-_ ]?\d+)?(?:$|[^a-z0-9])/i
15
18
  const TIMING_PACKAGE_PATTERN =
16
19
  /(?:^|[^a-z0-9])(?:clock|crystal|osc|oscillator|resonator|tcxo|txco|xtal)(?:$|[^a-z0-9])/i
17
20
  const TIMING_DESIGNATOR_PATTERN = /^(?:y|xo)\d+[a-z]?$/i
@@ -52,6 +55,9 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
52
55
  const components = Array.isArray(documentModel?.pcb?.components)
53
56
  ? documentModel.pcb.components
54
57
  : []
58
+ const pads = Array.isArray(documentModel?.pcb?.pads)
59
+ ? documentModel.pcb.pads
60
+ : []
55
61
  if (!components.length) {
56
62
  return sceneDescription
57
63
  }
@@ -62,15 +68,18 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
62
68
  component
63
69
  ])
64
70
  )
65
- const placements = sceneDescription.externalPlacements.map(
66
- (placement) =>
67
- AltiumScene3dRepeatedModelOwnerRepair.#withSingleOwnerCenter(
68
- placement,
69
- componentByDesignator.get(
70
- String(placement?.designator || '')
71
- ),
72
- sceneDescription.board
73
- )
71
+ const collapsedPlacements =
72
+ AltiumScene3dRepeatedFullFootprintBodyCollapse.apply(
73
+ sceneDescription.externalPlacements,
74
+ componentByDesignator,
75
+ pads
76
+ )
77
+ const placements = collapsedPlacements.map((placement) =>
78
+ AltiumScene3dRepeatedModelOwnerRepair.#withSingleOwnerCenter(
79
+ placement,
80
+ componentByDesignator.get(String(placement?.designator || '')),
81
+ sceneDescription.board
82
+ )
74
83
  )
75
84
 
76
85
  for (const group of AltiumScene3dRepeatedModelOwnerRepair.#groups(
@@ -178,9 +187,13 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
178
187
  * @returns {object}
179
188
  */
180
189
  static #withSingleOwnerCenter(placement, component, board) {
190
+ const projectionSource = String(
191
+ placement?.projection?.source || ''
192
+ ).toLowerCase()
181
193
  if (
182
194
  !component ||
183
- String(placement?.projection?.source || '') !== 'pad-fallback' ||
195
+ (projectionSource !== 'pad-fallback' &&
196
+ projectionSource !== 'model-bounds') ||
184
197
  AltiumScene3dRepeatedModelOwnerRepair.#isAuthoredAnchorPlacement(
185
198
  placement,
186
199
  component
@@ -204,13 +217,58 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
204
217
  ) {
205
218
  return placement
206
219
  }
220
+ if (
221
+ projectionSource === 'model-bounds' &&
222
+ !AltiumScene3dRepeatedModelOwnerRepair.#hasModelBoundsCornerSourceOriginOffset(
223
+ placement,
224
+ component,
225
+ offset
226
+ )
227
+ ) {
228
+ return placement
229
+ }
207
230
 
208
231
  return AltiumScene3dRepeatedModelOwnerRepair.#withOwner(
209
232
  placement,
210
233
  component,
211
234
  board,
212
235
  offset,
213
- { preserveRotation: true }
236
+ {
237
+ preserveRotation: true,
238
+ carryRenderableSourceOffset: projectionSource !== 'model-bounds'
239
+ }
240
+ )
241
+ }
242
+
243
+ /**
244
+ * Checks whether a model-bounds IC body uses a package-corner source
245
+ * origin rather than an authored mechanical anchor.
246
+ * @param {object} placement Scene placement.
247
+ * @param {object} component PCB component.
248
+ * @param {{ x: number, y: number }} offset Body anchor offset from owner.
249
+ * @returns {boolean}
250
+ */
251
+ static #hasModelBoundsCornerSourceOriginOffset(
252
+ placement,
253
+ component,
254
+ offset
255
+ ) {
256
+ const identityText =
257
+ AltiumScene3dRepeatedModelOwnerRepair.#identityTextForPlacement(
258
+ placement,
259
+ component
260
+ )
261
+ if (!MODEL_BOUNDS_CORNER_ORIGIN_PACKAGE_PATTERN.test(identityText)) {
262
+ return false
263
+ }
264
+
265
+ const offsetWidth = Math.abs(Number(offset?.x || 0)) * 2
266
+ const offsetDepth = Math.abs(Number(offset?.y || 0)) * 2
267
+
268
+ return AltiumScene3dRepeatedModelOwnerRepair.#matchesCornerOriginBounds(
269
+ offsetWidth,
270
+ offsetDepth,
271
+ placement?.projection?.boundsMil
214
272
  )
215
273
  }
216
274
 
@@ -387,6 +445,35 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
387
445
  )
388
446
  }
389
447
 
448
+ /**
449
+ * Checks whether doubled owner offsets match any planar pair from measured
450
+ * model bounds. Tilted STEP bodies often expose package height on `depth`
451
+ * and package width/depth on `width` plus `height`.
452
+ * @param {number} offsetWidth Doubled X offset.
453
+ * @param {number} offsetDepth Doubled Y offset.
454
+ * @param {{ width?: number, depth?: number, height?: number } | null | undefined} bounds Model bounds.
455
+ * @returns {boolean}
456
+ */
457
+ static #matchesCornerOriginBounds(offsetWidth, offsetDepth, bounds) {
458
+ const dimensions = [
459
+ Number(bounds?.width),
460
+ Number(bounds?.depth),
461
+ Number(bounds?.height)
462
+ ].filter((dimension) => Number.isFinite(dimension) && dimension > 0)
463
+
464
+ return dimensions.some((width, widthIndex) =>
465
+ dimensions.some(
466
+ (depth, depthIndex) =>
467
+ widthIndex !== depthIndex &&
468
+ AltiumScene3dRepeatedModelOwnerRepair.#matchesCornerOriginSize(
469
+ offsetWidth,
470
+ offsetDepth,
471
+ { width, depth }
472
+ )
473
+ )
474
+ )
475
+ }
476
+
390
477
  /**
391
478
  * Checks whether two dimensions are close enough for source-origin repair.
392
479
  * @param {number} actual Actual dimension.
@@ -656,7 +743,7 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
656
743
  * @param {object} component Resolved owner.
657
744
  * @param {object} board Scene board metadata.
658
745
  * @param {{ x: number, y: number }} offset Source-origin offset.
659
- * @param {{ preserveRotation?: boolean }} [options] Repair options.
746
+ * @param {{ preserveRotation?: boolean, carryRenderableSourceOffset?: boolean }} [options] Repair options.
660
747
  * @returns {object}
661
748
  */
662
749
  static #withOwner(placement, component, board, offset, options = {}) {
@@ -671,11 +758,15 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
671
758
  component?.rotation
672
759
  )
673
760
  const renderableOffset =
674
- AltiumScene3dRepeatedModelOwnerRepair.#renderableOwnerAnchorOffset(
675
- { mountSide, rotationDeg },
676
- offset,
677
- placement?.modelTransform
678
- )
761
+ options?.carryRenderableSourceOffset === false
762
+ ? AltiumScene3dRepeatedModelOwnerRepair.#renderableZOffset(
763
+ placement?.modelTransform
764
+ )
765
+ : AltiumScene3dRepeatedModelOwnerRepair.#renderableOwnerAnchorOffset(
766
+ { mountSide, rotationDeg },
767
+ offset,
768
+ placement?.modelTransform
769
+ )
679
770
 
680
771
  return {
681
772
  ...placement,
@@ -702,6 +793,19 @@ export class AltiumScene3dRepeatedModelOwnerRepair {
702
793
  }
703
794
  }
704
795
 
796
+ /**
797
+ * Preserves authored model Z offset while suppressing lateral render shift.
798
+ * @param {object | undefined} modelTransform Existing model transform.
799
+ * @returns {{ x: number, y: number, z: number }}
800
+ */
801
+ static #renderableZOffset(modelTransform) {
802
+ const z = Number(
803
+ modelTransform?.offsetMil?.z ?? modelTransform?.dzMil ?? 0
804
+ )
805
+
806
+ return { x: 0, y: 0, z: Number.isFinite(z) ? z : 0 }
807
+ }
808
+
705
809
  /**
706
810
  * Converts a board-space source-origin offset into mount-rig local XY.
707
811
  * @param {{ mountSide?: string, rotationDeg?: number }} placement Resolved placement fields.
@@ -1,6 +1,4 @@
1
- const TIMING_PACKAGE_PATTERN =
2
- /(?:^|[^a-z0-9])(?:clock|crystal|osc|oscillator|resonator|tcxo|txco|xtal)(?:$|[^a-z0-9])/i
3
- const TIMING_DESIGNATOR_PATTERN = /^(?:y|xo)\d+[a-z]?$/i
1
+ import { AltiumScene3dShapeStackOwnerConflictPolicy } from './AltiumScene3dShapeStackOwnerConflictPolicy.mjs'
4
2
 
5
3
  /**
6
4
  * Keeps authored shape-based sub-bodies grouped under their carrier owner.
@@ -10,9 +8,8 @@ export class AltiumScene3dShapeStackOwnerAdapter {
10
8
  static #EXACT_OWNER_TOLERANCE_MIL = 1
11
9
  static #STACK_BODY_RADIUS_MIL = 220
12
10
  static #HEIGHT_TOLERANCE_MIL = 0.1
13
- static #TIMING_PARAMETER_NAME_PATTERN =
14
- /(?:^|[^a-z0-9])(?:comment|description|device|function|part|type|value)(?:$|[^a-z0-9])/i
15
-
11
+ static #OPPOSITE_SIDE_OWNER_DISTANCE_MARGIN_MIL = 10
12
+ static #STACK_BODY_OWNER_CONFLICT_MARGIN_MIL = 5
16
13
  /**
17
14
  * Applies authored stack ownership to static and external placements.
18
15
  * @param {object} sceneDescription Built scene description.
@@ -55,23 +52,25 @@ export class AltiumScene3dShapeStackOwnerAdapter {
55
52
  ])
56
53
  )
57
54
 
55
+ const repairedExternalPlacements =
56
+ AltiumScene3dShapeStackOwnerAdapter.#repairExternalPlacements(
57
+ sceneDescription.externalPlacements,
58
+ componentBodies,
59
+ assignments,
60
+ sceneDescription.board,
61
+ components
62
+ )
63
+
58
64
  return {
59
65
  ...sceneDescription,
60
66
  components: AltiumScene3dShapeStackOwnerAdapter.#markComponents(
61
67
  sceneDescription.components,
62
68
  ownerGroupKeys
63
69
  ),
64
- externalPlacements:
65
- AltiumScene3dShapeStackOwnerAdapter.#repairExternalPlacements(
66
- sceneDescription.externalPlacements,
67
- componentBodies,
68
- assignments,
69
- sceneDescription.board
70
- ),
70
+ externalPlacements: repairedExternalPlacements,
71
71
  staticBodyPlacements:
72
72
  AltiumScene3dShapeStackOwnerAdapter.#repairStaticPlacements(
73
73
  sceneDescription.staticBodyPlacements,
74
- componentBodies,
75
74
  assignments,
76
75
  sceneDescription.board
77
76
  )
@@ -104,17 +103,19 @@ export class AltiumScene3dShapeStackOwnerAdapter {
104
103
  * @param {object[]} componentBodies Source component-body rows.
105
104
  * @param {object[]} assignments Carrier assignments.
106
105
  * @param {object | undefined} board Scene board metadata.
106
+ * @param {object[]} components Source components.
107
107
  * @returns {object[]}
108
108
  */
109
109
  static #repairExternalPlacements(
110
110
  placements,
111
111
  componentBodies,
112
112
  assignments,
113
- board
113
+ board,
114
+ components
114
115
  ) {
115
116
  const usedComponentBodies = new Set()
116
117
 
117
- return (Array.isArray(placements) ? placements : []).map(
118
+ return (Array.isArray(placements) ? placements : []).flatMap(
118
119
  (placement) => {
119
120
  const componentBody =
120
121
  AltiumScene3dShapeStackOwnerAdapter.#resolveComponentBody(
@@ -129,18 +130,27 @@ export class AltiumScene3dShapeStackOwnerAdapter {
129
130
  const assignment =
130
131
  AltiumScene3dShapeStackOwnerAdapter.#assignmentForBody(
131
132
  componentBody,
132
- assignments
133
+ assignments,
134
+ components
133
135
  )
134
136
  if (!assignment) {
135
- return placement
137
+ return AltiumScene3dShapeStackOwnerAdapter.#shouldDropConflictingStackBody(
138
+ componentBody,
139
+ assignments,
140
+ components
141
+ )
142
+ ? []
143
+ : [placement]
136
144
  }
137
145
 
138
- return AltiumScene3dShapeStackOwnerAdapter.#withOwner(
139
- placement,
140
- componentBody,
141
- assignment,
142
- board
143
- )
146
+ return [
147
+ AltiumScene3dShapeStackOwnerAdapter.#withOwner(
148
+ placement,
149
+ componentBody,
150
+ assignment,
151
+ board
152
+ )
153
+ ]
144
154
  }
145
155
  )
146
156
  }
@@ -148,17 +158,11 @@ export class AltiumScene3dShapeStackOwnerAdapter {
148
158
  /**
149
159
  * Repairs carrier static body ownership.
150
160
  * @param {object[] | undefined} placements Scene static body placements.
151
- * @param {object[]} componentBodies Source component-body rows.
152
161
  * @param {object[]} assignments Carrier assignments.
153
162
  * @param {object | undefined} board Scene board metadata.
154
163
  * @returns {object[]}
155
164
  */
156
- static #repairStaticPlacements(
157
- placements,
158
- componentBodies,
159
- assignments,
160
- board
161
- ) {
165
+ static #repairStaticPlacements(placements, assignments, board) {
162
166
  return (Array.isArray(placements) ? placements : []).map(
163
167
  (placement) => {
164
168
  const assignment = assignments.find((candidate) =>
@@ -298,7 +302,9 @@ export class AltiumScene3dShapeStackOwnerAdapter {
298
302
  AltiumScene3dShapeStackOwnerAdapter.#isCarrierBase(body)
299
303
  )
300
304
  const timingComponents = components.filter((component) =>
301
- AltiumScene3dShapeStackOwnerAdapter.#isTimingComponent(component)
305
+ AltiumScene3dShapeStackOwnerConflictPolicy.isTimingComponent(
306
+ component
307
+ )
302
308
  )
303
309
  const groups =
304
310
  AltiumScene3dShapeStackOwnerAdapter.#groupCarrierBases(bases)
@@ -313,20 +319,26 @@ export class AltiumScene3dShapeStackOwnerAdapter {
313
319
  return []
314
320
  }
315
321
 
316
- const owners = timingComponents
317
- .filter(
318
- (component) =>
319
- AltiumScene3dShapeStackOwnerAdapter.#distance(
320
- group.anchor,
321
- component
322
- ) <=
323
- AltiumScene3dShapeStackOwnerAdapter
324
- .#BASE_OWNER_TOLERANCE_MIL
325
- )
326
- .sort(
322
+ const owners =
323
+ AltiumScene3dShapeStackOwnerConflictPolicy.preferredSideOwners(
324
+ group.anchor,
325
+ timingComponents.filter(
326
+ (component) =>
327
+ AltiumScene3dShapeStackOwnerAdapter.#distance(
328
+ group.anchor,
329
+ component
330
+ ) <=
331
+ AltiumScene3dShapeStackOwnerAdapter
332
+ .#BASE_OWNER_TOLERANCE_MIL
333
+ ),
334
+ AltiumScene3dShapeStackOwnerAdapter
335
+ .#OPPOSITE_SIDE_OWNER_DISTANCE_MARGIN_MIL
336
+ ).sort(
327
337
  (left, right) =>
328
- AltiumScene3dShapeStackOwnerAdapter.#ownerRank(right) -
329
- AltiumScene3dShapeStackOwnerAdapter.#ownerRank(
338
+ AltiumScene3dShapeStackOwnerConflictPolicy.ownerRank(
339
+ right
340
+ ) -
341
+ AltiumScene3dShapeStackOwnerConflictPolicy.ownerRank(
330
342
  left
331
343
  ) ||
332
344
  String(left?.designator || '').localeCompare(
@@ -389,12 +401,12 @@ export class AltiumScene3dShapeStackOwnerAdapter {
389
401
  return (
390
402
  exactOwners.some(
391
403
  (component) =>
392
- !AltiumScene3dShapeStackOwnerAdapter.#isTimingComponent(
404
+ !AltiumScene3dShapeStackOwnerConflictPolicy.isTimingComponent(
393
405
  component
394
406
  )
395
407
  ) &&
396
408
  !exactOwners.some((component) =>
397
- AltiumScene3dShapeStackOwnerAdapter.#isTimingComponent(
409
+ AltiumScene3dShapeStackOwnerConflictPolicy.isTimingComponent(
398
410
  component
399
411
  )
400
412
  )
@@ -452,9 +464,109 @@ export class AltiumScene3dShapeStackOwnerAdapter {
452
464
  * Finds a carrier assignment for one positive-height sub-body.
453
465
  * @param {object | null} componentBody Source component-body row.
454
466
  * @param {object[]} assignments Carrier assignments.
467
+ * @param {object[]} components Source components.
455
468
  * @returns {object | null}
456
469
  */
457
- static #assignmentForBody(componentBody, assignments) {
470
+ static #assignmentForBody(componentBody, assignments, components) {
471
+ return (
472
+ AltiumScene3dShapeStackOwnerAdapter.#assignmentCandidatesForBody(
473
+ componentBody,
474
+ assignments
475
+ )
476
+ .filter((candidate) =>
477
+ AltiumScene3dShapeStackOwnerAdapter.#canAssignStackBodyCandidate(
478
+ componentBody,
479
+ candidate,
480
+ components
481
+ )
482
+ )
483
+ .sort(
484
+ (left, right) =>
485
+ left.heightError - right.heightError ||
486
+ left.distance - right.distance
487
+ )[0]?.assignment || null
488
+ )
489
+ }
490
+
491
+ /**
492
+ * Checks whether a candidate stack owner can claim a raised body.
493
+ * @param {object | null} componentBody Source component-body row.
494
+ * @param {{ assignment: { owner: object } }} candidate Candidate assignment row.
495
+ * @param {object[]} components Source components.
496
+ * @returns {boolean}
497
+ */
498
+ static #canAssignStackBodyCandidate(componentBody, candidate, components) {
499
+ const hasConflictingOwner =
500
+ AltiumScene3dShapeStackOwnerConflictPolicy.hasConflictingComponentOwner(
501
+ componentBody,
502
+ candidate.assignment.owner,
503
+ components,
504
+ AltiumScene3dShapeStackOwnerAdapter
505
+ .#STACK_BODY_OWNER_CONFLICT_MARGIN_MIL
506
+ )
507
+ if (!hasConflictingOwner) {
508
+ return true
509
+ }
510
+
511
+ return (
512
+ AltiumScene3dShapeStackOwnerConflictPolicy.hasStackDetailBodyIdentity(
513
+ componentBody
514
+ ) &&
515
+ !AltiumScene3dShapeStackOwnerConflictPolicy.hasExactOppositeSideOwner(
516
+ componentBody,
517
+ candidate.assignment.owner,
518
+ components,
519
+ AltiumScene3dShapeStackOwnerAdapter.#EXACT_OWNER_TOLERANCE_MIL
520
+ )
521
+ )
522
+ }
523
+
524
+ /**
525
+ * Checks whether an unassigned positive-height body should be suppressed
526
+ * because it conflicts with an opposite-side owner but has no exact owner
527
+ * of its own.
528
+ * @param {object | null} componentBody Source component-body row.
529
+ * @param {object[]} assignments Carrier assignments.
530
+ * @param {object[]} components Source components.
531
+ * @returns {boolean}
532
+ */
533
+ static #shouldDropConflictingStackBody(
534
+ componentBody,
535
+ assignments,
536
+ components
537
+ ) {
538
+ const conflict =
539
+ AltiumScene3dShapeStackOwnerAdapter.#assignmentCandidatesForBody(
540
+ componentBody,
541
+ assignments
542
+ ).find((candidate) =>
543
+ AltiumScene3dShapeStackOwnerConflictPolicy.hasConflictingComponentOwner(
544
+ componentBody,
545
+ candidate.assignment.owner,
546
+ components,
547
+ AltiumScene3dShapeStackOwnerAdapter
548
+ .#STACK_BODY_OWNER_CONFLICT_MARGIN_MIL
549
+ )
550
+ )
551
+
552
+ return Boolean(
553
+ conflict &&
554
+ !AltiumScene3dShapeStackOwnerConflictPolicy.hasExactOppositeSideOwner(
555
+ componentBody,
556
+ conflict.assignment.owner,
557
+ components,
558
+ AltiumScene3dShapeStackOwnerAdapter.#EXACT_OWNER_TOLERANCE_MIL
559
+ )
560
+ )
561
+ }
562
+
563
+ /**
564
+ * Builds stack assignment candidates for one positive-height body.
565
+ * @param {object | null} componentBody Source component-body row.
566
+ * @param {object[]} assignments Carrier assignments.
567
+ * @returns {{ assignment: object, heightError: number, distance: number }[]}
568
+ */
569
+ static #assignmentCandidatesForBody(componentBody, assignments) {
458
570
  if (
459
571
  !componentBody ||
460
572
  !AltiumScene3dShapeStackOwnerAdapter.#isShapeBasedBody(
@@ -462,7 +574,7 @@ export class AltiumScene3dShapeStackOwnerAdapter {
462
574
  ) ||
463
575
  Number(componentBody?.standoffHeightMil || 0) <= 0
464
576
  ) {
465
- return null
577
+ return []
466
578
  }
467
579
 
468
580
  const standoff = Number(componentBody.standoffHeightMil)
@@ -470,33 +582,26 @@ export class AltiumScene3dShapeStackOwnerAdapter {
470
582
  componentBody.positionMil
471
583
  )
472
584
 
473
- return (
474
- assignments
475
- .map((assignment) => ({
476
- assignment,
477
- heightError: Math.abs(
478
- Number(assignment.heightMil || 0) - standoff
479
- ),
480
- distance: AltiumScene3dShapeStackOwnerAdapter.#distance(
481
- bodyPosition,
482
- assignment.owner
483
- )
484
- }))
485
- .filter(
486
- (candidate) =>
487
- candidate.heightError <=
488
- AltiumScene3dShapeStackOwnerAdapter
489
- .#HEIGHT_TOLERANCE_MIL &&
490
- candidate.distance <=
491
- AltiumScene3dShapeStackOwnerAdapter
492
- .#STACK_BODY_RADIUS_MIL
585
+ return (Array.isArray(assignments) ? assignments : [])
586
+ .map((assignment) => ({
587
+ assignment,
588
+ heightError: Math.abs(
589
+ Number(assignment.heightMil || 0) - standoff
590
+ ),
591
+ distance: AltiumScene3dShapeStackOwnerAdapter.#distance(
592
+ bodyPosition,
593
+ assignment.owner
493
594
  )
494
- .sort(
495
- (left, right) =>
496
- left.heightError - right.heightError ||
497
- left.distance - right.distance
498
- )[0]?.assignment || null
499
- )
595
+ }))
596
+ .filter(
597
+ (candidate) =>
598
+ candidate.heightError <=
599
+ AltiumScene3dShapeStackOwnerAdapter
600
+ .#HEIGHT_TOLERANCE_MIL &&
601
+ candidate.distance <=
602
+ AltiumScene3dShapeStackOwnerAdapter
603
+ .#STACK_BODY_RADIUS_MIL
604
+ )
500
605
  }
501
606
 
502
607
  /**
@@ -703,51 +808,6 @@ export class AltiumScene3dShapeStackOwnerAdapter {
703
808
  : 0
704
809
  }
705
810
 
706
- /**
707
- * Checks whether a component is a timing-package owner.
708
- * @param {object} component Source component.
709
- * @returns {boolean}
710
- */
711
- static #isTimingComponent(component) {
712
- const designator = String(component?.designator || '').trim()
713
-
714
- return (
715
- TIMING_DESIGNATOR_PATTERN.test(designator) ||
716
- TIMING_PACKAGE_PATTERN.test(
717
- AltiumScene3dShapeStackOwnerAdapter.#timingIdentityText(
718
- component
719
- )
720
- )
721
- )
722
- }
723
-
724
- /**
725
- * Scores how strongly a component looks like the fitted stack owner.
726
- * @param {object} component Source component.
727
- * @returns {number}
728
- */
729
- static #ownerRank(component) {
730
- const parameters = Object.values(component?.parameters || {}).filter(
731
- (value) => String(value || '').trim()
732
- )
733
- return (
734
- parameters.length * 10 +
735
- (TIMING_DESIGNATOR_PATTERN.test(component?.designator) ? 5 : 0) +
736
- (TIMING_PACKAGE_PATTERN.test(
737
- [
738
- component?.pattern,
739
- component?.source,
740
- component?.description,
741
- component?.provenance?.footprintDescription
742
- ]
743
- .map((value) => String(value || ''))
744
- .join(' ')
745
- )
746
- ? 3
747
- : 0)
748
- )
749
- }
750
-
751
811
  /**
752
812
  * Checks whether one body row came from shape-based component metadata.
753
813
  * @param {object | null | undefined} componentBody Source body row.
@@ -861,44 +921,6 @@ export class AltiumScene3dShapeStackOwnerAdapter {
861
921
  .join(' ')
862
922
  }
863
923
 
864
- /**
865
- * Builds timing-relevant identity text without material metadata.
866
- * @param {object} component Source component.
867
- * @returns {string}
868
- */
869
- static #timingIdentityText(component) {
870
- return [
871
- component?.pattern,
872
- component?.source,
873
- component?.description,
874
- component?.provenance?.footprintDescription,
875
- ...AltiumScene3dShapeStackOwnerAdapter.#timingParameterValues(
876
- component?.parameters
877
- )
878
- ]
879
- .map((value) => String(value || ''))
880
- .join(' ')
881
- }
882
-
883
- /**
884
- * Returns parameter values whose names describe component identity.
885
- * @param {object | undefined} parameters Source component parameters.
886
- * @returns {unknown[]}
887
- */
888
- static #timingParameterValues(parameters) {
889
- if (!parameters || typeof parameters !== 'object') {
890
- return []
891
- }
892
-
893
- return Object.entries(parameters)
894
- .filter(([key]) =>
895
- AltiumScene3dShapeStackOwnerAdapter.#TIMING_PARAMETER_NAME_PATTERN.test(
896
- String(key || '')
897
- )
898
- )
899
- .map(([, value]) => value)
900
- }
901
-
902
924
  /**
903
925
  * Rounds one mil value for stable scene output.
904
926
  * @param {number} value Candidate value.