altium-toolkit 1.1.35 → 1.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +17 -2
- package/package.json +1 -1
- package/src/core/altium/PrintableTextDecoder.mjs +20 -1
- package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +288 -7
- package/src/core/altium/SchematicTextParser.mjs +18 -253
- package/src/core/altium/SchematicTitleBlockParser.mjs +410 -0
- package/src/core/altium/SelectedPartAltiumExportAdapter.mjs +215 -0
- package/src/parser.mjs +1 -0
- package/src/ui/AltiumScene3dAuthoredBodyAnchorAdapter.mjs +245 -12
- package/src/ui/AltiumScene3dAuthoredConnectorYawPolicy.mjs +176 -0
- package/src/ui/AltiumScene3dBottomSourceHalfTurnPolicy.mjs +93 -0
- package/src/ui/AltiumScene3dDisplayModuleYawPolicy.mjs +284 -0
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +521 -23
- package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +766 -19
- package/src/ui/AltiumScene3dQfnFootprintDetector.mjs +307 -0
- package/src/ui/AltiumScene3dRepeatedFullFootprintBodyCollapse.mjs +518 -0
- package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +121 -17
- package/src/ui/AltiumScene3dShapeStackOwnerAdapter.mjs +286 -135
- package/src/ui/AltiumScene3dShapeStackOwnerConflictPolicy.mjs +299 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +22 -0
- package/src/ui/PcbScene3dBuilder.mjs +613 -117
- package/src/ui/PcbScene3dModelRegistry.mjs +222 -10
- package/src/ui/PcbScene3dPackages.mjs +16 -1
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +262 -22
- package/src/ui/PcbScene3dStaticBodyOwnerPromotion.mjs +984 -0
- package/src/ui/PcbScene3dStaticBodyPadOwnerPromotion.mjs +378 -0
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +169 -23
- package/src/ui/PcbScene3dStaticBodyPrototypeRecovery.mjs +445 -0
- package/src/ui/PcbScene3dStaticBodyRecovery.mjs +7 -1
- package/src/ui/PcbScene3dStaticBodySelectionKeyBuilder.mjs +199 -2
- package/src/ui/PcbScene3dStaticBodySymmetricOwnerPromotion.mjs +439 -0
- package/src/ui/PcbScene3dStaticBodySymmetryRecovery.mjs +23 -2
- package/src/ui/SchematicColorResolver.mjs +44 -0
- package/src/ui/SchematicLineColorResolver.mjs +1 -1
- package/src/ui/SchematicShapeRenderer.mjs +13 -13
|
@@ -19,6 +19,7 @@ import { PcbScene3dPadYawResolver } from './PcbScene3dPadYawResolver.mjs'
|
|
|
19
19
|
import { PcbScene3dTextBoxLayoutResolver } from './PcbScene3dTextBoxLayoutResolver.mjs'
|
|
20
20
|
import { PcbFootprintPadAxisNormalizer } from './PcbFootprintPadAxisNormalizer.mjs'
|
|
21
21
|
import { PcbScene3dCopperRegionDetailBuilder } from './PcbScene3dCopperRegionDetailBuilder.mjs'
|
|
22
|
+
import { AltiumScene3dBottomSourceHalfTurnPolicy } from './AltiumScene3dBottomSourceHalfTurnPolicy.mjs'
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Builds deterministic 3D scene data from the normalized PCB model.
|
|
@@ -45,16 +46,23 @@ export class PcbScene3dBuilder {
|
|
|
45
46
|
/(?:^|[^a-z0-9])(?:emi|rf|rfi|shield|cover|can)(?:$|[^a-z0-9])/i
|
|
46
47
|
static #MECHANICAL_SHIELD_FALLBACK_PATTERN =
|
|
47
48
|
/(?:^|[^a-z0-9])(?:emi|rfi|shield|cover|can)(?:$|[^a-z0-9])/i
|
|
48
|
-
static #
|
|
49
|
-
|
|
49
|
+
static #MECHANICAL_SHIELD_FRAME_OWNER_TOKENS = [
|
|
50
|
+
'emi',
|
|
51
|
+
'rfi',
|
|
52
|
+
'rf',
|
|
53
|
+
'shield',
|
|
54
|
+
'can'
|
|
55
|
+
]
|
|
50
56
|
static #MECHANICAL_SHIELD_FRAME_BODY_PATTERN =
|
|
51
57
|
/(?:^|[^a-z0-9])(?:frame[0-9]*|leg|rail|side|wall)(?:$|[^a-z0-9])/i
|
|
52
58
|
static #MECHANICAL_SHIELD_FRAME_OWNER_RADIUS_MIL = 750
|
|
53
59
|
static #COMPONENT_PACKAGE_BODY_PATTERN =
|
|
54
|
-
/(?:^|[^a-z0-9])(?:[a-z0-9]*dfn
|
|
60
|
+
/(?:^|[^a-z0-9])(?:[a-z0-9]*dfn[a-z0-9]*|[a-z0-9]*qfn[a-z0-9]*|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
61
|
static #TIMING_STACK_COMPONENT_PATTERN =
|
|
56
62
|
/(?:^|[^a-z0-9])(?:clock|crystal|osc|oscillator|resonator|tcxo|txco|xtal)(?:$|[^a-z0-9])/i
|
|
57
63
|
static #TIMING_STACK_DESIGNATOR_PATTERN = /^(?:y|xo)\d+[a-z]?$/i
|
|
64
|
+
static #TIMING_STACK_HEIGHT_TOLERANCE_MIL = 0.1
|
|
65
|
+
static #TIMING_STACK_CARRIER_BOUNDS_TOLERANCE_MIL = 5
|
|
58
66
|
|
|
59
67
|
/**
|
|
60
68
|
* Builds a scene description for host 3D renderers.
|
|
@@ -156,44 +164,55 @@ export class PcbScene3dBuilder {
|
|
|
156
164
|
appearance3d
|
|
157
165
|
)
|
|
158
166
|
|
|
167
|
+
const sceneComponents = components
|
|
168
|
+
.map((component) =>
|
|
169
|
+
PcbScene3dBuilder.#buildComponent(
|
|
170
|
+
component,
|
|
171
|
+
pads,
|
|
172
|
+
board,
|
|
173
|
+
thicknessMil,
|
|
174
|
+
modelRegistry
|
|
175
|
+
)
|
|
176
|
+
)
|
|
177
|
+
.filter(Boolean)
|
|
178
|
+
const externalPlacements = componentBodies
|
|
179
|
+
.map((componentBody, index) =>
|
|
180
|
+
PcbScene3dBuilder.#buildExternalPlacement(
|
|
181
|
+
componentBody,
|
|
182
|
+
bodyMatches[index],
|
|
183
|
+
componentBodyModels[index],
|
|
184
|
+
components,
|
|
185
|
+
pads,
|
|
186
|
+
board,
|
|
187
|
+
thicknessMil,
|
|
188
|
+
componentBodies
|
|
189
|
+
)
|
|
190
|
+
)
|
|
191
|
+
.filter(Boolean)
|
|
192
|
+
const staticBodyPlacements = PcbScene3dStaticBodyPlacementBuilder.build(
|
|
193
|
+
componentBodies,
|
|
194
|
+
bodyMatches,
|
|
195
|
+
components,
|
|
196
|
+
pads,
|
|
197
|
+
board,
|
|
198
|
+
thicknessMil
|
|
199
|
+
)
|
|
200
|
+
|
|
159
201
|
const sceneDescription = {
|
|
160
202
|
sourceFormat: 'altium',
|
|
161
203
|
board,
|
|
162
204
|
boardAssemblyModel:
|
|
163
205
|
modelRegistry?.resolveBoardAssemblyModel?.(documentModel) ||
|
|
164
206
|
null,
|
|
165
|
-
components:
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
)
|
|
175
|
-
.filter(Boolean),
|
|
176
|
-
externalPlacements: componentBodies
|
|
177
|
-
.map((componentBody, index) =>
|
|
178
|
-
PcbScene3dBuilder.#buildExternalPlacement(
|
|
179
|
-
componentBody,
|
|
180
|
-
bodyMatches[index],
|
|
181
|
-
componentBodyModels[index],
|
|
182
|
-
components,
|
|
183
|
-
pads,
|
|
184
|
-
board,
|
|
185
|
-
thicknessMil
|
|
186
|
-
)
|
|
187
|
-
)
|
|
188
|
-
.filter(Boolean),
|
|
189
|
-
staticBodyPlacements: PcbScene3dStaticBodyPlacementBuilder.build(
|
|
190
|
-
componentBodies,
|
|
191
|
-
bodyMatches,
|
|
192
|
-
components,
|
|
193
|
-
pads,
|
|
194
|
-
board,
|
|
195
|
-
thicknessMil
|
|
196
|
-
),
|
|
207
|
+
components:
|
|
208
|
+
PcbScene3dBuilder.#suppressExternallyCoveredFallbackBodies(
|
|
209
|
+
sceneComponents,
|
|
210
|
+
components,
|
|
211
|
+
externalPlacements,
|
|
212
|
+
pads
|
|
213
|
+
),
|
|
214
|
+
externalPlacements,
|
|
215
|
+
staticBodyPlacements,
|
|
197
216
|
detail: {
|
|
198
217
|
embeddedFonts: Array.isArray(pcb.embeddedFonts)
|
|
199
218
|
? pcb.embeddedFonts
|
|
@@ -326,6 +345,106 @@ export class PcbScene3dBuilder {
|
|
|
326
345
|
)
|
|
327
346
|
}
|
|
328
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Suppresses procedural fallbacks when explicit body placements already
|
|
350
|
+
* cover every drilled pad owned by the same component.
|
|
351
|
+
* @param {object[]} sceneComponents Scene component rows.
|
|
352
|
+
* @param {object[]} sourceComponents Source PCB component rows.
|
|
353
|
+
* @param {object[]} externalPlacements Built external placements.
|
|
354
|
+
* @param {object[]} pads PCB pad rows.
|
|
355
|
+
* @returns {object[]}
|
|
356
|
+
*/
|
|
357
|
+
static #suppressExternallyCoveredFallbackBodies(
|
|
358
|
+
sceneComponents,
|
|
359
|
+
sourceComponents,
|
|
360
|
+
externalPlacements,
|
|
361
|
+
pads
|
|
362
|
+
) {
|
|
363
|
+
const sourceByDesignator = new Map(
|
|
364
|
+
(Array.isArray(sourceComponents) ? sourceComponents : []).map(
|
|
365
|
+
(component) => [String(component?.designator || ''), component]
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
return (Array.isArray(sceneComponents) ? sceneComponents : []).map(
|
|
370
|
+
(component) =>
|
|
371
|
+
PcbScene3dBuilder.#hasExternalPlacementPadCoverage(
|
|
372
|
+
sourceByDesignator.get(
|
|
373
|
+
String(component?.designator || '')
|
|
374
|
+
) || component,
|
|
375
|
+
component,
|
|
376
|
+
externalPlacements,
|
|
377
|
+
pads
|
|
378
|
+
)
|
|
379
|
+
? { ...component, renderFallbackBody: false }
|
|
380
|
+
: component
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Checks whether explicit placements occupy all drilled pads for a component.
|
|
386
|
+
* @param {object} sourceComponent Source PCB component.
|
|
387
|
+
* @param {object} sceneComponent Scene component.
|
|
388
|
+
* @param {object[]} externalPlacements Built external placements.
|
|
389
|
+
* @param {object[]} pads PCB pad rows.
|
|
390
|
+
* @returns {boolean}
|
|
391
|
+
*/
|
|
392
|
+
static #hasExternalPlacementPadCoverage(
|
|
393
|
+
sourceComponent,
|
|
394
|
+
sceneComponent,
|
|
395
|
+
externalPlacements,
|
|
396
|
+
pads
|
|
397
|
+
) {
|
|
398
|
+
if (sceneComponent?.renderFallbackBody === false) {
|
|
399
|
+
return false
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const designator = String(sceneComponent?.designator || '')
|
|
403
|
+
const componentPads = PcbScene3dBuilder.#componentPads(
|
|
404
|
+
sourceComponent,
|
|
405
|
+
pads
|
|
406
|
+
).filter((pad) => PcbScene3dBuilder.#hasDrilledPadOpening(pad))
|
|
407
|
+
if (componentPads.length < 2) {
|
|
408
|
+
return false
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
const placements = (
|
|
412
|
+
Array.isArray(externalPlacements) ? externalPlacements : []
|
|
413
|
+
).filter(
|
|
414
|
+
(placement) => String(placement?.designator || '') === designator
|
|
415
|
+
)
|
|
416
|
+
if (placements.length < componentPads.length) {
|
|
417
|
+
return false
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return componentPads.every((pad) =>
|
|
421
|
+
placements.some((placement) =>
|
|
422
|
+
PcbScene3dBuilder.#placementCoversPad(placement, pad)
|
|
423
|
+
)
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Checks whether one external placement anchor covers one drilled pad.
|
|
429
|
+
* @param {object} placement External model placement.
|
|
430
|
+
* @param {object} pad PCB pad row.
|
|
431
|
+
* @returns {boolean}
|
|
432
|
+
*/
|
|
433
|
+
static #placementCoversPad(placement, pad) {
|
|
434
|
+
const point = placement?.bodyPositionMil
|
|
435
|
+
if (
|
|
436
|
+
!Number.isFinite(Number(point?.x)) ||
|
|
437
|
+
!Number.isFinite(Number(point?.y))
|
|
438
|
+
) {
|
|
439
|
+
return false
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return (
|
|
443
|
+
PcbScene3dBuilder.#distanceToPadAnchor(point, pad) <=
|
|
444
|
+
PcbScene3dBuilder.#EXACT_BODY_MISMATCH_TOLERANCE_MIL
|
|
445
|
+
)
|
|
446
|
+
}
|
|
447
|
+
|
|
329
448
|
/**
|
|
330
449
|
* Checks whether a generic component row describes authored shield
|
|
331
450
|
* hardware that should not become a filled fallback box.
|
|
@@ -406,6 +525,7 @@ export class PcbScene3dBuilder {
|
|
|
406
525
|
* @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[]} pads
|
|
407
526
|
* @param {{ centerX: number, centerY: number }} board
|
|
408
527
|
* @param {number} thicknessMil
|
|
528
|
+
* @param {object[]} componentBodies All source component bodies.
|
|
409
529
|
* @returns {{ designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, bodyRotationDeg: number, modelTransform: { rotationDeg: { x: number, y: number, z: number }, dzMil: number }, externalModel: { origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } } | null}
|
|
410
530
|
*/
|
|
411
531
|
static #buildExternalPlacement(
|
|
@@ -415,7 +535,8 @@ export class PcbScene3dBuilder {
|
|
|
415
535
|
components,
|
|
416
536
|
pads,
|
|
417
537
|
board,
|
|
418
|
-
thicknessMil
|
|
538
|
+
thicknessMil,
|
|
539
|
+
componentBodies
|
|
419
540
|
) {
|
|
420
541
|
if (PcbScene3dBuilder.#shouldRenderStaticGeometryOnly(componentBody)) {
|
|
421
542
|
return null
|
|
@@ -425,18 +546,31 @@ export class PcbScene3dBuilder {
|
|
|
425
546
|
return null
|
|
426
547
|
}
|
|
427
548
|
|
|
549
|
+
const sourcePosition =
|
|
550
|
+
PcbScene3dBuilder.#resolveExternalPlacementSourcePosition(
|
|
551
|
+
componentBody
|
|
552
|
+
)
|
|
553
|
+
const resolvedMatchedComponent =
|
|
554
|
+
matchedComponent ||
|
|
555
|
+
PcbScene3dBuilder.#resolveComponentFromOwnedDrilledPad(
|
|
556
|
+
sourcePosition,
|
|
557
|
+
components,
|
|
558
|
+
pads
|
|
559
|
+
)
|
|
560
|
+
|
|
428
561
|
if (
|
|
429
562
|
PcbScene3dBuilder.#isPositiveTimingStackPackageBody(
|
|
430
563
|
componentBody,
|
|
431
|
-
|
|
432
|
-
components
|
|
564
|
+
resolvedMatchedComponent,
|
|
565
|
+
components,
|
|
566
|
+
componentBodies
|
|
433
567
|
)
|
|
434
568
|
) {
|
|
435
569
|
return null
|
|
436
570
|
}
|
|
437
571
|
|
|
438
572
|
if (
|
|
439
|
-
!
|
|
573
|
+
!resolvedMatchedComponent &&
|
|
440
574
|
PcbScene3dBuilder.#shouldDropUnmatchedPackageBody(
|
|
441
575
|
componentBody,
|
|
442
576
|
components
|
|
@@ -446,7 +580,7 @@ export class PcbScene3dBuilder {
|
|
|
446
580
|
}
|
|
447
581
|
|
|
448
582
|
if (
|
|
449
|
-
!
|
|
583
|
+
!resolvedMatchedComponent &&
|
|
450
584
|
!PcbScene3dBuilder.#isBodyPositionNearBoard(componentBody, board)
|
|
451
585
|
) {
|
|
452
586
|
return null
|
|
@@ -454,28 +588,28 @@ export class PcbScene3dBuilder {
|
|
|
454
588
|
|
|
455
589
|
const mountSide = PcbScene3dPlacementSideResolver.resolvePlacementSide(
|
|
456
590
|
componentBody,
|
|
457
|
-
|
|
591
|
+
resolvedMatchedComponent,
|
|
458
592
|
components,
|
|
459
593
|
board
|
|
460
594
|
)
|
|
461
595
|
const halfBoardThickness = thicknessMil / 2
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
596
|
+
const modelRotation = PcbScene3dBuilder.#resolveExternalModelRotation(
|
|
597
|
+
componentBody,
|
|
598
|
+
resolvedMatchedComponent,
|
|
599
|
+
pads,
|
|
600
|
+
mountSide
|
|
601
|
+
)
|
|
468
602
|
|
|
469
603
|
return {
|
|
470
604
|
designator:
|
|
471
|
-
|
|
605
|
+
resolvedMatchedComponent?.designator ||
|
|
472
606
|
String(
|
|
473
607
|
componentBody.identifier || componentBody.name || '3D model'
|
|
474
608
|
),
|
|
475
609
|
mountSide,
|
|
476
610
|
rotationDeg: PcbScene3dBuilder.#resolveExternalPlacementRotation(
|
|
477
611
|
componentBody,
|
|
478
|
-
|
|
612
|
+
resolvedMatchedComponent
|
|
479
613
|
),
|
|
480
614
|
positionMil: {
|
|
481
615
|
x: Number(sourcePosition.x || 0) - Number(board.centerX || 0),
|
|
@@ -494,12 +628,13 @@ export class PcbScene3dBuilder {
|
|
|
494
628
|
rotationDeg: modelRotation,
|
|
495
629
|
dzMil: PcbScene3dBuilder.#resolveComponentBodyVerticalOffset(
|
|
496
630
|
componentBody,
|
|
497
|
-
|
|
631
|
+
resolvedMatchedComponent,
|
|
632
|
+
mountSide
|
|
498
633
|
)
|
|
499
634
|
},
|
|
500
635
|
projection: PcbScene3dBuilder.#resolveProjectionDiagnostics(
|
|
501
636
|
componentBody,
|
|
502
|
-
|
|
637
|
+
resolvedMatchedComponent,
|
|
503
638
|
pads,
|
|
504
639
|
resolvedModel
|
|
505
640
|
),
|
|
@@ -526,34 +661,89 @@ export class PcbScene3dBuilder {
|
|
|
526
661
|
* raw model bounds on the board face.
|
|
527
662
|
* @param {{ dzMil?: number, standoffHeightMil?: number | null }} componentBody Component-body placement metadata.
|
|
528
663
|
* @param {object | null} matchedComponent Matched owner component.
|
|
664
|
+
* @param {'top' | 'bottom'} mountSide Resolved mount side.
|
|
529
665
|
* @returns {number}
|
|
530
666
|
*/
|
|
531
667
|
static #resolveComponentBodyVerticalOffset(
|
|
532
668
|
componentBody,
|
|
533
|
-
matchedComponent = null
|
|
669
|
+
matchedComponent = null,
|
|
670
|
+
mountSide = 'top'
|
|
534
671
|
) {
|
|
535
672
|
const standoffHeightMil = Number(componentBody?.standoffHeightMil)
|
|
536
673
|
if (Number.isFinite(standoffHeightMil)) {
|
|
537
|
-
|
|
538
|
-
PcbScene3dBuilder.#
|
|
674
|
+
if (
|
|
675
|
+
PcbScene3dBuilder.#shouldPreserveNegativeBodyOffset(
|
|
676
|
+
standoffHeightMil,
|
|
539
677
|
componentBody,
|
|
540
|
-
|
|
678
|
+
mountSide
|
|
679
|
+
) ||
|
|
680
|
+
(standoffHeightMil > 0 &&
|
|
681
|
+
PcbScene3dBuilder.#shouldPreservePositiveBodyStandoff(
|
|
682
|
+
componentBody,
|
|
683
|
+
matchedComponent
|
|
684
|
+
))
|
|
685
|
+
) {
|
|
686
|
+
return PcbScene3dBuilder.#toMountSideVerticalOffset(
|
|
687
|
+
standoffHeightMil,
|
|
688
|
+
mountSide
|
|
541
689
|
)
|
|
542
|
-
|
|
543
|
-
: 0
|
|
690
|
+
}
|
|
544
691
|
}
|
|
545
692
|
|
|
546
693
|
const dzMil = Number(componentBody?.dzMil)
|
|
547
694
|
return Number.isFinite(dzMil) &&
|
|
548
|
-
(
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
695
|
+
(PcbScene3dBuilder.#shouldPreserveNegativeBodyOffset(
|
|
696
|
+
dzMil,
|
|
697
|
+
componentBody,
|
|
698
|
+
mountSide
|
|
699
|
+
) ||
|
|
700
|
+
(dzMil > 0 &&
|
|
701
|
+
PcbScene3dBuilder.#shouldPreservePositiveBodyStandoff(
|
|
702
|
+
componentBody,
|
|
703
|
+
matchedComponent
|
|
704
|
+
)))
|
|
705
|
+
? PcbScene3dBuilder.#toMountSideVerticalOffset(dzMil, mountSide)
|
|
554
706
|
: 0
|
|
555
707
|
}
|
|
556
708
|
|
|
709
|
+
/**
|
|
710
|
+
* Converts preserved source Z offsets to the viewer mount-side convention.
|
|
711
|
+
* @param {number} value Source vertical offset.
|
|
712
|
+
* @param {'top' | 'bottom'} mountSide Resolved mount side.
|
|
713
|
+
* @returns {number}
|
|
714
|
+
*/
|
|
715
|
+
static #toMountSideVerticalOffset(value, mountSide) {
|
|
716
|
+
return String(mountSide || '').toLowerCase() === 'bottom' && value < 0
|
|
717
|
+
? Math.abs(value)
|
|
718
|
+
: value
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Checks whether a negative source offset describes intentional top-side
|
|
723
|
+
* model penetration instead of a source-origin seating artifact.
|
|
724
|
+
* @param {number} value Source vertical offset.
|
|
725
|
+
* @param {{ overallHeightMil?: number | null }} componentBody Component-body placement metadata.
|
|
726
|
+
* @param {'top' | 'bottom'} mountSide Resolved mount side.
|
|
727
|
+
* @returns {boolean}
|
|
728
|
+
*/
|
|
729
|
+
static #shouldPreserveNegativeBodyOffset(value, componentBody, mountSide) {
|
|
730
|
+
const offset = Number(value)
|
|
731
|
+
if (
|
|
732
|
+
!Number.isFinite(offset) ||
|
|
733
|
+
offset >= 0 ||
|
|
734
|
+
String(mountSide || '').toLowerCase() !== 'top'
|
|
735
|
+
) {
|
|
736
|
+
return false
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
const overallHeight = Number(componentBody?.overallHeightMil)
|
|
740
|
+
return (
|
|
741
|
+
!Number.isFinite(overallHeight) ||
|
|
742
|
+
overallHeight <= 0 ||
|
|
743
|
+
Math.abs(offset) < overallHeight
|
|
744
|
+
)
|
|
745
|
+
}
|
|
746
|
+
|
|
557
747
|
/**
|
|
558
748
|
* Checks whether a positive shape-body standoff is part of an authored
|
|
559
749
|
* stack instead of a model-origin quirk that should be seated on the board.
|
|
@@ -950,50 +1140,71 @@ export class PcbScene3dBuilder {
|
|
|
950
1140
|
componentBodies,
|
|
951
1141
|
components
|
|
952
1142
|
) {
|
|
1143
|
+
const shieldFrameOwners = components.filter((component) =>
|
|
1144
|
+
PcbScene3dBuilder.#isMechanicalShieldFrameOwner(component)
|
|
1145
|
+
)
|
|
1146
|
+
|
|
1147
|
+
if (!shieldFrameOwners.length) {
|
|
1148
|
+
return
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const shieldFrameOwnerSet = new Set(shieldFrameOwners)
|
|
1152
|
+
|
|
953
1153
|
componentBodies.forEach((componentBody, bodyIndex) => {
|
|
954
|
-
if (
|
|
955
|
-
matches[bodyIndex] ||
|
|
956
|
-
!PcbScene3dBuilder.#isStaticShieldFrameBody(componentBody)
|
|
957
|
-
) {
|
|
1154
|
+
if (!PcbScene3dBuilder.#isStaticShieldFrameBody(componentBody)) {
|
|
958
1155
|
return
|
|
959
1156
|
}
|
|
960
1157
|
|
|
961
|
-
const
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
)
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
1158
|
+
const currentOwner = matches[bodyIndex]
|
|
1159
|
+
if (currentOwner && shieldFrameOwnerSet.has(currentOwner)) {
|
|
1160
|
+
return
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
let owner = null
|
|
1164
|
+
let ownerAffinityScore = -Infinity
|
|
1165
|
+
let ownerDistance = Infinity
|
|
1166
|
+
|
|
1167
|
+
shieldFrameOwners.forEach((component) => {
|
|
1168
|
+
if (
|
|
1169
|
+
!PcbScene3dBuilder.#isBodyComponentSideCompatible(
|
|
1170
|
+
componentBody,
|
|
1171
|
+
component
|
|
1172
|
+
)
|
|
1173
|
+
) {
|
|
1174
|
+
return
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
const distance =
|
|
1178
|
+
PcbScene3dBuilder.#distanceBetweenBodyAndComponent(
|
|
1179
|
+
componentBody,
|
|
1180
|
+
component
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
if (
|
|
1184
|
+
!Number.isFinite(distance) ||
|
|
1185
|
+
distance >
|
|
1186
|
+
PcbScene3dBuilder
|
|
1187
|
+
.#MECHANICAL_SHIELD_FRAME_OWNER_RADIUS_MIL
|
|
1188
|
+
) {
|
|
1189
|
+
return
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
const affinityScore =
|
|
1193
|
+
PcbScene3dPlacementSideResolver.scoreBodyComponentAffinity(
|
|
1194
|
+
componentBody,
|
|
1195
|
+
component
|
|
1196
|
+
)
|
|
1197
|
+
const betterOwner =
|
|
1198
|
+
affinityScore > ownerAffinityScore ||
|
|
1199
|
+
(affinityScore === ownerAffinityScore &&
|
|
1200
|
+
distance < ownerDistance)
|
|
1201
|
+
|
|
1202
|
+
if (betterOwner) {
|
|
1203
|
+
owner = component
|
|
1204
|
+
ownerAffinityScore = affinityScore
|
|
1205
|
+
ownerDistance = distance
|
|
1206
|
+
}
|
|
1207
|
+
})
|
|
997
1208
|
|
|
998
1209
|
if (owner) {
|
|
999
1210
|
matches[bodyIndex] = owner
|
|
@@ -1007,9 +1218,30 @@ export class PcbScene3dBuilder {
|
|
|
1007
1218
|
* @returns {boolean}
|
|
1008
1219
|
*/
|
|
1009
1220
|
static #isMechanicalShieldFrameOwner(component) {
|
|
1010
|
-
|
|
1221
|
+
const identityTokens = PcbScene3dBuilder.#componentIdentityTokens(
|
|
1011
1222
|
PcbScene3dBuilder.#componentIdentityText(component)
|
|
1012
1223
|
)
|
|
1224
|
+
|
|
1225
|
+
return (
|
|
1226
|
+
identityTokens.has('frame') &&
|
|
1227
|
+
PcbScene3dBuilder.#MECHANICAL_SHIELD_FRAME_OWNER_TOKENS.some(
|
|
1228
|
+
(token) => identityTokens.has(token)
|
|
1229
|
+
)
|
|
1230
|
+
)
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
/**
|
|
1234
|
+
* Splits component identity text into lowercase alphanumeric tokens.
|
|
1235
|
+
* @param {string} identityText Component identity text.
|
|
1236
|
+
* @returns {Set<string>}
|
|
1237
|
+
*/
|
|
1238
|
+
static #componentIdentityTokens(identityText) {
|
|
1239
|
+
return new Set(
|
|
1240
|
+
String(identityText || '')
|
|
1241
|
+
.toLowerCase()
|
|
1242
|
+
.split(/[^a-z0-9]+/u)
|
|
1243
|
+
.filter(Boolean)
|
|
1244
|
+
)
|
|
1013
1245
|
}
|
|
1014
1246
|
|
|
1015
1247
|
/**
|
|
@@ -1236,7 +1468,21 @@ export class PcbScene3dBuilder {
|
|
|
1236
1468
|
matchContext,
|
|
1237
1469
|
distanceMil
|
|
1238
1470
|
) {
|
|
1471
|
+
const affinityScore =
|
|
1472
|
+
PcbScene3dPlacementSideResolver.scoreBodyComponentAffinity(
|
|
1473
|
+
componentBody,
|
|
1474
|
+
component
|
|
1475
|
+
)
|
|
1476
|
+
const sideCompatible = PcbScene3dBuilder.#isBodyComponentSideCompatible(
|
|
1477
|
+
componentBody,
|
|
1478
|
+
component
|
|
1479
|
+
)
|
|
1480
|
+
|
|
1239
1481
|
if (PcbScene3dBuilder.#isPreciseBodyComponentDistance(distanceMil)) {
|
|
1482
|
+
if (!sideCompatible && affinityScore <= 0) {
|
|
1483
|
+
return false
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1240
1486
|
return !PcbScene3dBuilder.#isIncompatiblePackageBodyMatch(
|
|
1241
1487
|
componentBody,
|
|
1242
1488
|
component,
|
|
@@ -1244,12 +1490,7 @@ export class PcbScene3dBuilder {
|
|
|
1244
1490
|
)
|
|
1245
1491
|
}
|
|
1246
1492
|
|
|
1247
|
-
if (
|
|
1248
|
-
PcbScene3dPlacementSideResolver.scoreBodyComponentAffinity(
|
|
1249
|
-
componentBody,
|
|
1250
|
-
component
|
|
1251
|
-
) <= 0
|
|
1252
|
-
) {
|
|
1493
|
+
if (affinityScore <= 0) {
|
|
1253
1494
|
return false
|
|
1254
1495
|
}
|
|
1255
1496
|
|
|
@@ -1320,17 +1561,19 @@ export class PcbScene3dBuilder {
|
|
|
1320
1561
|
}
|
|
1321
1562
|
|
|
1322
1563
|
/**
|
|
1323
|
-
* Checks whether a package-like shape body is an
|
|
1324
|
-
*
|
|
1564
|
+
* Checks whether a package-like shape body is an unowned timing-stack
|
|
1565
|
+
* support body that is outside a matching carrier surface.
|
|
1325
1566
|
* @param {object} componentBody Component-body record.
|
|
1326
1567
|
* @param {object | null} matchedComponent Matched component.
|
|
1327
1568
|
* @param {{ designator?: string, x?: number, y?: number, pattern?: string, source?: string, description?: string, provenance?: object, parameters?: object }[]} components PCB components.
|
|
1569
|
+
* @param {object[]} componentBodies All source component bodies.
|
|
1328
1570
|
* @returns {boolean}
|
|
1329
1571
|
*/
|
|
1330
1572
|
static #isPositiveTimingStackPackageBody(
|
|
1331
1573
|
componentBody,
|
|
1332
1574
|
matchedComponent,
|
|
1333
|
-
components
|
|
1575
|
+
components,
|
|
1576
|
+
componentBodies
|
|
1334
1577
|
) {
|
|
1335
1578
|
const standoff = Number(componentBody?.standoffHeightMil)
|
|
1336
1579
|
const hasTimingOwner = matchedComponent
|
|
@@ -1354,7 +1597,107 @@ export class PcbScene3dBuilder {
|
|
|
1354
1597
|
!PcbScene3dBuilder.#isTimingStackBodyIdentity(componentBody) &&
|
|
1355
1598
|
!PcbScene3dBuilder.#isAuthoredBodyIdentity(componentBody) &&
|
|
1356
1599
|
hasTimingOwner &&
|
|
1357
|
-
!hasLocalComponentOwner
|
|
1600
|
+
!hasLocalComponentOwner &&
|
|
1601
|
+
!PcbScene3dBuilder.#isRaisedBodySeatedOnCarrier(
|
|
1602
|
+
componentBody,
|
|
1603
|
+
componentBodies
|
|
1604
|
+
)
|
|
1605
|
+
)
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
/**
|
|
1609
|
+
* Checks whether one raised body sits on a same-height authored carrier.
|
|
1610
|
+
* @param {{ standoffHeightMil?: number, positionMil?: { x?: number, y?: number } }} componentBody Raised component body.
|
|
1611
|
+
* @param {object[]} componentBodies All source component bodies.
|
|
1612
|
+
* @returns {boolean}
|
|
1613
|
+
*/
|
|
1614
|
+
static #isRaisedBodySeatedOnCarrier(componentBody, componentBodies) {
|
|
1615
|
+
const standoff = Number(componentBody?.standoffHeightMil)
|
|
1616
|
+
if (!Number.isFinite(standoff) || standoff <= 0) {
|
|
1617
|
+
return false
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
return (Array.isArray(componentBodies) ? componentBodies : []).some(
|
|
1621
|
+
(candidate) =>
|
|
1622
|
+
candidate !== componentBody &&
|
|
1623
|
+
PcbScene3dBuilder.#shouldRenderStaticGeometryOnly(candidate) &&
|
|
1624
|
+
Math.abs(
|
|
1625
|
+
Number(
|
|
1626
|
+
candidate?.staticGeometry?.heightMil ??
|
|
1627
|
+
candidate?.overallHeightMil ??
|
|
1628
|
+
0
|
|
1629
|
+
) - standoff
|
|
1630
|
+
) <= PcbScene3dBuilder.#TIMING_STACK_HEIGHT_TOLERANCE_MIL &&
|
|
1631
|
+
PcbScene3dBuilder.#carrierBoundsContainPoint(
|
|
1632
|
+
candidate,
|
|
1633
|
+
componentBody?.positionMil
|
|
1634
|
+
)
|
|
1635
|
+
)
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
/**
|
|
1639
|
+
* Checks whether a carrier polygon's source or local bounds contain a point.
|
|
1640
|
+
* @param {{ positionMil?: { x?: number, y?: number }, rotationDeg?: number, staticGeometry?: { verticesMil?: object[] } }} carrierBody Carrier body.
|
|
1641
|
+
* @param {{ x?: number, y?: number } | undefined} point Source point.
|
|
1642
|
+
* @returns {boolean}
|
|
1643
|
+
*/
|
|
1644
|
+
static #carrierBoundsContainPoint(carrierBody, point) {
|
|
1645
|
+
const vertices = carrierBody?.staticGeometry?.verticesMil
|
|
1646
|
+
if (!Array.isArray(vertices) || vertices.length < 3) {
|
|
1647
|
+
return false
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
const sourcePoint = {
|
|
1651
|
+
x: Number(point?.x),
|
|
1652
|
+
y: Number(point?.y)
|
|
1653
|
+
}
|
|
1654
|
+
if (
|
|
1655
|
+
!Number.isFinite(sourcePoint.x) ||
|
|
1656
|
+
!Number.isFinite(sourcePoint.y)
|
|
1657
|
+
) {
|
|
1658
|
+
return false
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
const anchor = {
|
|
1662
|
+
x: Number(carrierBody?.positionMil?.x || 0),
|
|
1663
|
+
y: Number(carrierBody?.positionMil?.y || 0)
|
|
1664
|
+
}
|
|
1665
|
+
const rotationRad =
|
|
1666
|
+
(-Number(carrierBody?.rotationDeg || 0) * Math.PI) / 180
|
|
1667
|
+
const dx = sourcePoint.x - anchor.x
|
|
1668
|
+
const dy = sourcePoint.y - anchor.y
|
|
1669
|
+
const localPoint = {
|
|
1670
|
+
x: dx * Math.cos(rotationRad) - dy * Math.sin(rotationRad),
|
|
1671
|
+
y: dx * Math.sin(rotationRad) + dy * Math.cos(rotationRad)
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
return (
|
|
1675
|
+
PcbScene3dBuilder.#boundsContainPoint(sourcePoint, vertices) ||
|
|
1676
|
+
PcbScene3dBuilder.#boundsContainPoint(localPoint, vertices)
|
|
1677
|
+
)
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
/**
|
|
1681
|
+
* Checks whether one axis-aligned vertex bounds contains a point.
|
|
1682
|
+
* @param {{ x: number, y: number }} point Candidate point.
|
|
1683
|
+
* @param {{ x?: number, y?: number }[]} vertices Bounds vertices.
|
|
1684
|
+
* @returns {boolean}
|
|
1685
|
+
*/
|
|
1686
|
+
static #boundsContainPoint(point, vertices) {
|
|
1687
|
+
const points = vertices.map((vertex) => ({
|
|
1688
|
+
x: Number(vertex?.x || 0),
|
|
1689
|
+
y: Number(vertex?.y || 0)
|
|
1690
|
+
}))
|
|
1691
|
+
const xs = points.map((vertex) => vertex.x)
|
|
1692
|
+
const ys = points.map((vertex) => vertex.y)
|
|
1693
|
+
const tolerance =
|
|
1694
|
+
PcbScene3dBuilder.#TIMING_STACK_CARRIER_BOUNDS_TOLERANCE_MIL
|
|
1695
|
+
|
|
1696
|
+
return (
|
|
1697
|
+
point.x >= Math.min(...xs) - tolerance &&
|
|
1698
|
+
point.x <= Math.max(...xs) + tolerance &&
|
|
1699
|
+
point.y >= Math.min(...ys) - tolerance &&
|
|
1700
|
+
point.y <= Math.max(...ys) + tolerance
|
|
1358
1701
|
)
|
|
1359
1702
|
}
|
|
1360
1703
|
|
|
@@ -1841,15 +2184,41 @@ export class PcbScene3dBuilder {
|
|
|
1841
2184
|
/**
|
|
1842
2185
|
* Resolves model-local rotations after converting Altium's positive local
|
|
1843
2186
|
* rotation fields into the renderer's signed 3D model convention.
|
|
1844
|
-
* @param {{ modelRotationDeg?: { x?: number, y?: number, z?: number } }} componentBody
|
|
2187
|
+
* @param {{ modelRotationDeg?: { x?: number, y?: number, z?: number } }} componentBody Component body.
|
|
2188
|
+
* @param {{ componentIndex?: number, layer?: string } | null} matchedComponent Matched component.
|
|
2189
|
+
* @param {object[]} pads PCB pads.
|
|
2190
|
+
* @param {'top' | 'bottom'} mountSide Placement mount side.
|
|
1845
2191
|
* @returns {{ x: number, y: number, z: number }}
|
|
1846
2192
|
*/
|
|
1847
|
-
static #resolveExternalModelRotation(
|
|
1848
|
-
|
|
2193
|
+
static #resolveExternalModelRotation(
|
|
2194
|
+
componentBody,
|
|
2195
|
+
matchedComponent,
|
|
2196
|
+
pads,
|
|
2197
|
+
mountSide
|
|
2198
|
+
) {
|
|
2199
|
+
const rotation = {
|
|
1849
2200
|
x: -Number(componentBody?.modelRotationDeg?.x || 0),
|
|
1850
2201
|
y: -Number(componentBody?.modelRotationDeg?.y || 0),
|
|
1851
2202
|
z: 0
|
|
1852
2203
|
}
|
|
2204
|
+
|
|
2205
|
+
if (
|
|
2206
|
+
mountSide === 'bottom' &&
|
|
2207
|
+
PcbScene3dBuilder.#normalizeAngle(rotation.x) === 180 &&
|
|
2208
|
+
!AltiumScene3dBottomSourceHalfTurnPolicy.shouldPreserve({
|
|
2209
|
+
component: matchedComponent,
|
|
2210
|
+
componentBody,
|
|
2211
|
+
modelTransform: { rotationDeg: rotation }
|
|
2212
|
+
}) &&
|
|
2213
|
+
!PcbScene3dBuilder.#componentHasThroughHolePads(
|
|
2214
|
+
matchedComponent,
|
|
2215
|
+
pads
|
|
2216
|
+
)
|
|
2217
|
+
) {
|
|
2218
|
+
rotation.x = 0
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
return rotation
|
|
1853
2222
|
}
|
|
1854
2223
|
|
|
1855
2224
|
/**
|
|
@@ -2309,6 +2678,114 @@ export class PcbScene3dBuilder {
|
|
|
2309
2678
|
)
|
|
2310
2679
|
}
|
|
2311
2680
|
|
|
2681
|
+
/**
|
|
2682
|
+
* Resolves a component owner when an external body anchor sits on a drilled
|
|
2683
|
+
* pad owned by that component.
|
|
2684
|
+
* @param {{ x?: number, y?: number } | null | undefined} sourcePosition External body anchor.
|
|
2685
|
+
* @param {{ componentIndex?: number, x?: number, y?: number }[]} components PCB components.
|
|
2686
|
+
* @param {object[]} pads PCB pads.
|
|
2687
|
+
* @returns {object | null}
|
|
2688
|
+
*/
|
|
2689
|
+
static #resolveComponentFromOwnedDrilledPad(
|
|
2690
|
+
sourcePosition,
|
|
2691
|
+
components,
|
|
2692
|
+
pads
|
|
2693
|
+
) {
|
|
2694
|
+
const sourceX = Number(sourcePosition?.x)
|
|
2695
|
+
const sourceY = Number(sourcePosition?.y)
|
|
2696
|
+
if (!Number.isFinite(sourceX) || !Number.isFinite(sourceY)) {
|
|
2697
|
+
return null
|
|
2698
|
+
}
|
|
2699
|
+
|
|
2700
|
+
const candidates = (Array.isArray(components) ? components : [])
|
|
2701
|
+
.flatMap((component) =>
|
|
2702
|
+
PcbScene3dBuilder.#componentPads(component, pads)
|
|
2703
|
+
.filter((pad) =>
|
|
2704
|
+
PcbScene3dBuilder.#hasDrilledPadOpening(pad)
|
|
2705
|
+
)
|
|
2706
|
+
.map((pad) => ({
|
|
2707
|
+
component,
|
|
2708
|
+
padDistance: PcbScene3dBuilder.#distanceToPadAnchor(
|
|
2709
|
+
{ x: sourceX, y: sourceY },
|
|
2710
|
+
pad
|
|
2711
|
+
),
|
|
2712
|
+
componentDistance: Math.hypot(
|
|
2713
|
+
Number(component?.x || 0) - sourceX,
|
|
2714
|
+
Number(component?.y || 0) - sourceY
|
|
2715
|
+
)
|
|
2716
|
+
}))
|
|
2717
|
+
)
|
|
2718
|
+
.filter(
|
|
2719
|
+
(candidate) =>
|
|
2720
|
+
candidate.padDistance <=
|
|
2721
|
+
PcbScene3dBuilder.#EXACT_BODY_MISMATCH_TOLERANCE_MIL
|
|
2722
|
+
)
|
|
2723
|
+
.sort(
|
|
2724
|
+
(left, right) =>
|
|
2725
|
+
left.padDistance - right.padDistance ||
|
|
2726
|
+
left.componentDistance - right.componentDistance
|
|
2727
|
+
)
|
|
2728
|
+
|
|
2729
|
+
return candidates[0]?.component || null
|
|
2730
|
+
}
|
|
2731
|
+
|
|
2732
|
+
/**
|
|
2733
|
+
* Measures the distance from a source point to a drilled pad's effective
|
|
2734
|
+
* anchor area.
|
|
2735
|
+
* @param {{ x: number, y: number }} sourcePosition External body anchor.
|
|
2736
|
+
* @param {object} pad PCB pad.
|
|
2737
|
+
* @returns {number}
|
|
2738
|
+
*/
|
|
2739
|
+
static #distanceToPadAnchor(sourcePosition, pad) {
|
|
2740
|
+
const centerDistance = Math.hypot(
|
|
2741
|
+
Number(pad?.x || 0) - Number(sourcePosition.x || 0),
|
|
2742
|
+
Number(pad?.y || 0) - Number(sourcePosition.y || 0)
|
|
2743
|
+
)
|
|
2744
|
+
const radius = PcbScene3dBuilder.#padAnchorRadiusMil(pad)
|
|
2745
|
+
|
|
2746
|
+
return radius > 0
|
|
2747
|
+
? Math.max(0, centerDistance - radius)
|
|
2748
|
+
: Number.POSITIVE_INFINITY
|
|
2749
|
+
}
|
|
2750
|
+
|
|
2751
|
+
/**
|
|
2752
|
+
* Resolves the effective XY radius around a drilled pad center.
|
|
2753
|
+
* @param {object} pad PCB pad.
|
|
2754
|
+
* @returns {number}
|
|
2755
|
+
*/
|
|
2756
|
+
static #padAnchorRadiusMil(pad) {
|
|
2757
|
+
const holeGeometry = pad?.holeGeometry || {}
|
|
2758
|
+
const diameter = Math.max(
|
|
2759
|
+
Number(pad?.sizeTopX || 0),
|
|
2760
|
+
Number(pad?.sizeTopY || 0),
|
|
2761
|
+
Number(pad?.sizeMidX || 0),
|
|
2762
|
+
Number(pad?.sizeMidY || 0),
|
|
2763
|
+
Number(pad?.sizeBottomX || 0),
|
|
2764
|
+
Number(pad?.sizeBottomY || 0),
|
|
2765
|
+
Number(pad?.holeDiameter || 0),
|
|
2766
|
+
Number(pad?.drillDiameter || 0),
|
|
2767
|
+
Number(pad?.holeSlotLength || 0),
|
|
2768
|
+
Number(pad?.slotLength || 0),
|
|
2769
|
+
Number(holeGeometry?.diameter || 0),
|
|
2770
|
+
Number(holeGeometry?.length || 0),
|
|
2771
|
+
Number(holeGeometry?.slotLength || 0)
|
|
2772
|
+
)
|
|
2773
|
+
|
|
2774
|
+
return Number.isFinite(diameter) && diameter > 0 ? diameter / 2 : 0
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
/**
|
|
2778
|
+
* Checks whether a component owns drilled or slotted pads.
|
|
2779
|
+
* @param {{ componentIndex?: number, layer?: string } | null} component PCB component.
|
|
2780
|
+
* @param {object[]} pads PCB pads.
|
|
2781
|
+
* @returns {boolean}
|
|
2782
|
+
*/
|
|
2783
|
+
static #componentHasThroughHolePads(component, pads) {
|
|
2784
|
+
return PcbScene3dBuilder.#componentPads(component, pads).some((pad) =>
|
|
2785
|
+
PcbScene3dBuilder.#hasDrilledPadOpening(pad)
|
|
2786
|
+
)
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2312
2789
|
/**
|
|
2313
2790
|
* Resolves pads explicitly owned by one component, preferring pads on the
|
|
2314
2791
|
* mounted surface when paste-mask side metadata is available.
|
|
@@ -2345,6 +2822,25 @@ export class PcbScene3dBuilder {
|
|
|
2345
2822
|
: Boolean(pad?.hasTopPasteMaskOpening)
|
|
2346
2823
|
}
|
|
2347
2824
|
|
|
2825
|
+
/**
|
|
2826
|
+
* Checks whether one pad contains a drilled or slotted board opening.
|
|
2827
|
+
* @param {object} pad PCB pad.
|
|
2828
|
+
* @returns {boolean}
|
|
2829
|
+
*/
|
|
2830
|
+
static #hasDrilledPadOpening(pad) {
|
|
2831
|
+
const holeGeometry = pad?.holeGeometry || {}
|
|
2832
|
+
|
|
2833
|
+
return [
|
|
2834
|
+
pad?.holeDiameter,
|
|
2835
|
+
pad?.drillDiameter,
|
|
2836
|
+
pad?.holeSlotLength,
|
|
2837
|
+
pad?.slotLength,
|
|
2838
|
+
holeGeometry?.diameter,
|
|
2839
|
+
holeGeometry?.length,
|
|
2840
|
+
holeGeometry?.slotLength
|
|
2841
|
+
].some((value) => Number(value || 0) > 0)
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2348
2844
|
/**
|
|
2349
2845
|
* Returns true when one pad lies inside the component's local search area.
|
|
2350
2846
|
* @param {{ x: number, y: number }} component
|