altium-toolkit 1.4.16 → 1.4.17

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.
@@ -0,0 +1,122 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Shares primitive eligibility and physical layer metadata across PCB views.
7
+ */
8
+ export class PcbInteractionSourceMetadata {
9
+ /**
10
+ * Returns zone sources in selectable-item extraction order.
11
+ * @param {object} pcb PCB model.
12
+ * @returns {object[]}
13
+ */
14
+ static zoneSources(pcb) {
15
+ return [
16
+ ...(Array.isArray(pcb?.regions) ? pcb.regions : []),
17
+ ...(Array.isArray(pcb?.shapeBasedRegions)
18
+ ? pcb.shapeBasedRegions
19
+ : []),
20
+ ...(Array.isArray(pcb?.polygons) ? pcb.polygons : [])
21
+ ]
22
+ }
23
+
24
+ /**
25
+ * Identifies selectable zone geometry without constructing its points.
26
+ * @param {object} zone Zone-like source.
27
+ * @returns {'points' | 'segments' | 'bounds' | null}
28
+ */
29
+ static zoneGeometryKind(zone) {
30
+ if (Array.isArray(zone?.points) && zone.points.length >= 3) {
31
+ return 'points'
32
+ }
33
+ if (Array.isArray(zone?.segments) && zone.segments.length >= 3) {
34
+ return 'segments'
35
+ }
36
+ if (
37
+ Number.isFinite(Number(zone?.x1)) &&
38
+ Number.isFinite(Number(zone?.y1)) &&
39
+ Number.isFinite(Number(zone?.x2)) &&
40
+ Number.isFinite(Number(zone?.y2))
41
+ ) {
42
+ return 'bounds'
43
+ }
44
+ return null
45
+ }
46
+
47
+ /**
48
+ * Creates a layer-name resolver for layer-id based primitives.
49
+ * @param {object} pcb PCB model.
50
+ * @returns {(item: object) => string[]}
51
+ */
52
+ static layerNameResolver(pcb) {
53
+ const byId = new Map()
54
+ const layers = [
55
+ ...(Array.isArray(pcb?.layers) ? pcb.layers : []),
56
+ ...(Array.isArray(pcb?.primitiveLayers) ? pcb.primitiveLayers : [])
57
+ ]
58
+
59
+ for (const layer of layers) {
60
+ const layerId = Number(layer?.layerId)
61
+ const name = String(layer?.name || '').trim()
62
+ if (Number.isInteger(layerId) && name) {
63
+ byId.set(layerId, name)
64
+ }
65
+ }
66
+
67
+ return (item) => {
68
+ const directLayer = String(item?.layer || '').trim()
69
+ if (directLayer) return [directLayer]
70
+
71
+ const layerId = Number(item?.layerId ?? item?.layerCode)
72
+ if (Number.isInteger(layerId) && byId.has(layerId)) {
73
+ return [byId.get(layerId)]
74
+ }
75
+
76
+ return []
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Collects ordered physical layer keys without building selectable geometry.
82
+ * @param {object} pcb PCB model.
83
+ * @returns {Map<string, Set<string>>}
84
+ */
85
+ static layersByObject(pcb) {
86
+ const layerNameFor = PcbInteractionSourceMetadata.layerNameResolver(pcb)
87
+ const layersByObject = new Map()
88
+ const groups = [
89
+ ['zones', PcbInteractionSourceMetadata.zoneSources(pcb)],
90
+ ['tracks', Array.isArray(pcb?.tracks) ? pcb.tracks : []],
91
+ ['pads', Array.isArray(pcb?.pads) ? pcb.pads : []],
92
+ ['footprint-text', Array.isArray(pcb?.texts) ? pcb.texts : []]
93
+ ]
94
+
95
+ // Vias and component bodies have no physical layer keys in the index.
96
+ for (const [objectKey, sources] of groups) {
97
+ const layerSet = new Set()
98
+ layersByObject.set(objectKey, layerSet)
99
+ if (objectKey === 'pads') layersByObject.set('holes', layerSet)
100
+
101
+ for (const source of sources) {
102
+ if (
103
+ objectKey === 'zones' &&
104
+ !PcbInteractionSourceMetadata.zoneGeometryKind(source)
105
+ ) {
106
+ continue
107
+ }
108
+ if (
109
+ objectKey === 'footprint-text' &&
110
+ source?.visible === false
111
+ ) {
112
+ continue
113
+ }
114
+ for (const layerKey of layerNameFor(source)) {
115
+ layerSet.add(layerKey)
116
+ }
117
+ }
118
+ }
119
+
120
+ return layersByObject
121
+ }
122
+ }
@@ -11,11 +11,10 @@ import { AltiumScene3dComponentBodyAdapter } from './AltiumScene3dComponentBodyA
11
11
  import { AltiumScene3dAuthoredBodyAnchorAdapter } from './AltiumScene3dAuthoredBodyAnchorAdapter.mjs'
12
12
  import { AltiumScene3dShapeStackOwnerAdapter } from './AltiumScene3dShapeStackOwnerAdapter.mjs'
13
13
  import { PcbFootprintPrimitiveSelector } from './PcbFootprintPrimitiveSelector.mjs'
14
- import { PcbScene3dPadLocalSpanResolver } from './PcbScene3dPadLocalSpanResolver.mjs'
14
+ import { PcbScene3dComponentGeometryResolver } from './PcbScene3dComponentGeometryResolver.mjs'
15
15
  import { PcbScene3dPackages } from './PcbScene3dPackages.mjs'
16
16
  import { PcbScene3dPlacementSideResolver } from './PcbScene3dPlacementSideResolver.mjs'
17
17
  import { PcbScene3dStaticBodyPlacementBuilder } from './PcbScene3dStaticBodyPlacementBuilder.mjs'
18
- import { PcbScene3dPadYawResolver } from './PcbScene3dPadYawResolver.mjs'
19
18
  import { PcbScene3dTextBoxLayoutResolver } from './PcbScene3dTextBoxLayoutResolver.mjs'
20
19
  import { PcbFootprintPadAxisNormalizer } from './PcbFootprintPadAxisNormalizer.mjs'
21
20
  import { PcbScene3dCopperRegionDetailBuilder } from './PcbScene3dCopperRegionDetailBuilder.mjs'
@@ -84,6 +83,10 @@ export class PcbScene3dBuilder {
84
83
  ? pcb.componentBodies
85
84
  : []
86
85
  const pads = Array.isArray(pcb.pads) ? pcb.pads : []
86
+ const componentGeometry = new PcbScene3dComponentGeometryResolver(
87
+ components,
88
+ pads
89
+ )
87
90
  const tracks = Array.isArray(pcb.tracks) ? pcb.tracks : []
88
91
  const arcs = Array.isArray(pcb.arcs) ? pcb.arcs : []
89
92
  const fills = Array.isArray(pcb.fills) ? pcb.fills : []
@@ -168,7 +171,7 @@ export class PcbScene3dBuilder {
168
171
  .map((component) =>
169
172
  PcbScene3dBuilder.#buildComponent(
170
173
  component,
171
- pads,
174
+ componentGeometry,
172
175
  board,
173
176
  thicknessMil,
174
177
  modelRegistry
@@ -182,7 +185,7 @@ export class PcbScene3dBuilder {
182
185
  bodyMatches[index],
183
186
  componentBodyModels[index],
184
187
  components,
185
- pads,
188
+ componentGeometry,
186
189
  board,
187
190
  thicknessMil,
188
191
  componentBodies
@@ -209,7 +212,7 @@ export class PcbScene3dBuilder {
209
212
  sceneComponents,
210
213
  components,
211
214
  externalPlacements,
212
- pads
215
+ componentGeometry
213
216
  ),
214
217
  externalPlacements,
215
218
  staticBodyPlacements,
@@ -252,7 +255,7 @@ export class PcbScene3dBuilder {
252
255
  /**
253
256
  * Builds one procedural component scene entry.
254
257
  * @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null, source?: string, description?: string, parameters?: Record<string, unknown>, modelPath?: string }} component
255
- * @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[]} pads
258
+ * @param {PcbScene3dComponentGeometryResolver} componentGeometry Build-scoped pad geometry.
256
259
  * @param {{ centerX: number, centerY: number }} board
257
260
  * @param {number} thicknessMil
258
261
  * @param {{ resolveComponentModel: (component: any) => { name: string, relativePath: string, format: string } | null } | null} modelRegistry
@@ -260,22 +263,17 @@ export class PcbScene3dBuilder {
260
263
  */
261
264
  static #buildComponent(
262
265
  component,
263
- pads,
266
+ componentGeometry,
264
267
  board,
265
268
  thicknessMil,
266
269
  modelRegistry
267
270
  ) {
268
271
  const mountSide = PcbScene3dBuilder.#resolveMountSide(component)
269
- const rotationDeg = PcbScene3dBuilder.#resolveComponentRotation(
272
+ const rotationDeg = componentGeometry.resolveComponentRotation(
270
273
  component,
271
- pads,
272
274
  mountSide
273
275
  )
274
- const padSpan = PcbScene3dBuilder.#resolvePadSpan(
275
- component,
276
- pads,
277
- rotationDeg
278
- )
276
+ const padSpan = componentGeometry.resolvePadSpan(component, rotationDeg)
279
277
  const body = PcbScene3dPackages.resolve(component, padSpan)
280
278
  const externalModel = modelRegistry
281
279
  ? modelRegistry.resolveComponentModel(component)
@@ -351,14 +349,14 @@ export class PcbScene3dBuilder {
351
349
  * @param {object[]} sceneComponents Scene component rows.
352
350
  * @param {object[]} sourceComponents Source PCB component rows.
353
351
  * @param {object[]} externalPlacements Built external placements.
354
- * @param {object[]} pads PCB pad rows.
352
+ * @param {PcbScene3dComponentGeometryResolver} componentGeometry PCB pad rows.
355
353
  * @returns {object[]}
356
354
  */
357
355
  static #suppressExternallyCoveredFallbackBodies(
358
356
  sceneComponents,
359
357
  sourceComponents,
360
358
  externalPlacements,
361
- pads
359
+ componentGeometry
362
360
  ) {
363
361
  const sourceByDesignator = new Map(
364
362
  (Array.isArray(sourceComponents) ? sourceComponents : []).map(
@@ -374,7 +372,7 @@ export class PcbScene3dBuilder {
374
372
  ) || component,
375
373
  component,
376
374
  externalPlacements,
377
- pads
375
+ componentGeometry
378
376
  )
379
377
  ? { ...component, renderFallbackBody: false }
380
378
  : component
@@ -386,24 +384,25 @@ export class PcbScene3dBuilder {
386
384
  * @param {object} sourceComponent Source PCB component.
387
385
  * @param {object} sceneComponent Scene component.
388
386
  * @param {object[]} externalPlacements Built external placements.
389
- * @param {object[]} pads PCB pad rows.
387
+ * @param {PcbScene3dComponentGeometryResolver} componentGeometry PCB pad rows.
390
388
  * @returns {boolean}
391
389
  */
392
390
  static #hasExternalPlacementPadCoverage(
393
391
  sourceComponent,
394
392
  sceneComponent,
395
393
  externalPlacements,
396
- pads
394
+ componentGeometry
397
395
  ) {
398
396
  if (sceneComponent?.renderFallbackBody === false) {
399
397
  return false
400
398
  }
401
399
 
402
400
  const designator = String(sceneComponent?.designator || '')
403
- const componentPads = PcbScene3dBuilder.#componentPads(
404
- sourceComponent,
405
- pads
406
- ).filter((pad) => PcbScene3dBuilder.#hasDrilledPadOpening(pad))
401
+ const componentPads = componentGeometry
402
+ .componentPads(sourceComponent)
403
+ .filter((pad) =>
404
+ PcbScene3dComponentGeometryResolver.hasDrilledPadOpening(pad)
405
+ )
407
406
  if (componentPads.length < 2) {
408
407
  return false
409
408
  }
@@ -440,8 +439,10 @@ export class PcbScene3dBuilder {
440
439
  }
441
440
 
442
441
  return (
443
- PcbScene3dBuilder.#distanceToPadAnchor(point, pad) <=
444
- PcbScene3dBuilder.#EXACT_BODY_MISMATCH_TOLERANCE_MIL
442
+ PcbScene3dComponentGeometryResolver.distanceToPadAnchor(
443
+ point,
444
+ pad
445
+ ) <= PcbScene3dBuilder.#EXACT_BODY_MISMATCH_TOLERANCE_MIL
445
446
  )
446
447
  }
447
448
 
@@ -522,7 +523,7 @@ export class PcbScene3dBuilder {
522
523
  * @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, rotation?: number, height?: number | null } | null} matchedComponent
523
524
  * @param {{ origin: string, name: string, format: string, payloadText?: string, sourceStream?: string, relativePath?: string } | null} resolvedModel
524
525
  * @param {{ designator: string, x: number, y: number, layer?: string, pattern?: string, source?: string, modelPath?: string }[]} components
525
- * @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[]} pads
526
+ * @param {PcbScene3dComponentGeometryResolver} componentGeometry Build-scoped pad geometry.
526
527
  * @param {{ centerX: number, centerY: number }} board
527
528
  * @param {number} thicknessMil
528
529
  * @param {object[]} componentBodies All source component bodies.
@@ -533,7 +534,7 @@ export class PcbScene3dBuilder {
533
534
  matchedComponent,
534
535
  resolvedModel,
535
536
  components,
536
- pads,
537
+ componentGeometry,
537
538
  board,
538
539
  thicknessMil,
539
540
  componentBodies
@@ -552,10 +553,8 @@ export class PcbScene3dBuilder {
552
553
  )
553
554
  const resolvedMatchedComponent =
554
555
  matchedComponent ||
555
- PcbScene3dBuilder.#resolveComponentFromOwnedDrilledPad(
556
- sourcePosition,
557
- components,
558
- pads
556
+ componentGeometry.resolveComponentFromOwnedDrilledPad(
557
+ sourcePosition
559
558
  )
560
559
 
561
560
  if (
@@ -596,7 +595,7 @@ export class PcbScene3dBuilder {
596
595
  const modelRotation = PcbScene3dBuilder.#resolveExternalModelRotation(
597
596
  componentBody,
598
597
  resolvedMatchedComponent,
599
- pads,
598
+ componentGeometry,
600
599
  mountSide
601
600
  )
602
601
 
@@ -635,7 +634,7 @@ export class PcbScene3dBuilder {
635
634
  projection: PcbScene3dBuilder.#resolveProjectionDiagnostics(
636
635
  componentBody,
637
636
  resolvedMatchedComponent,
638
- pads,
637
+ componentGeometry,
639
638
  resolvedModel
640
639
  ),
641
640
  ...PcbScene3dBuilder.#componentBodyDisplayMetadata(componentBody),
@@ -819,14 +818,14 @@ export class PcbScene3dBuilder {
819
818
  * Explains which footprint projection source informed one external model.
820
819
  * @param {object} componentBody Normalized component body row.
821
820
  * @param {{ x: number, y: number, height?: number | null } | null} matchedComponent Matched component.
822
- * @param {object[]} pads Normalized pad rows.
821
+ * @param {PcbScene3dComponentGeometryResolver} componentGeometry Normalized pad rows.
823
822
  * @param {object | null} resolvedModel Resolved model metadata.
824
823
  * @returns {{ source: string, reason: string, boundsMil: { width: number, depth: number, height: number } }}
825
824
  */
826
825
  static #resolveProjectionDiagnostics(
827
826
  componentBody,
828
827
  matchedComponent,
829
- pads,
828
+ componentGeometry,
830
829
  resolvedModel
831
830
  ) {
832
831
  const authoredBounds = PcbScene3dBuilder.#firstBounds([
@@ -855,10 +854,7 @@ export class PcbScene3dBuilder {
855
854
  }
856
855
 
857
856
  if (matchedComponent) {
858
- const padSpan = PcbScene3dBuilder.#resolvePadSpan(
859
- matchedComponent,
860
- pads
861
- )
857
+ const padSpan = componentGeometry.resolvePadSpan(matchedComponent)
862
858
  if (padSpan.width > 0 || padSpan.depth > 0) {
863
859
  return {
864
860
  source: 'pad-fallback',
@@ -2186,14 +2182,14 @@ export class PcbScene3dBuilder {
2186
2182
  * rotation fields into the renderer's signed 3D model convention.
2187
2183
  * @param {{ modelRotationDeg?: { x?: number, y?: number, z?: number } }} componentBody Component body.
2188
2184
  * @param {{ componentIndex?: number, layer?: string } | null} matchedComponent Matched component.
2189
- * @param {object[]} pads PCB pads.
2185
+ * @param {PcbScene3dComponentGeometryResolver} componentGeometry Build-scoped pad geometry.
2190
2186
  * @param {'top' | 'bottom'} mountSide Placement mount side.
2191
2187
  * @returns {{ x: number, y: number, z: number }}
2192
2188
  */
2193
2189
  static #resolveExternalModelRotation(
2194
2190
  componentBody,
2195
2191
  matchedComponent,
2196
- pads,
2192
+ componentGeometry,
2197
2193
  mountSide
2198
2194
  ) {
2199
2195
  const rotation = {
@@ -2210,10 +2206,7 @@ export class PcbScene3dBuilder {
2210
2206
  componentBody,
2211
2207
  modelTransform: { rotationDeg: rotation }
2212
2208
  }) &&
2213
- !PcbScene3dBuilder.#componentHasThroughHolePads(
2214
- matchedComponent,
2215
- pads
2216
- )
2209
+ !componentGeometry.componentHasThroughHolePads(matchedComponent)
2217
2210
  ) {
2218
2211
  rotation.x = 0
2219
2212
  }
@@ -2632,228 +2625,6 @@ export class PcbScene3dBuilder {
2632
2625
  return Array.isArray(pcb?.regions) ? pcb.regions : []
2633
2626
  }
2634
2627
 
2635
- /**
2636
- * Resolves the owned or nearby pad-span box around one component.
2637
- * @param {{ x: number, y: number, componentIndex?: number, layer?: string, rotation?: number }} component
2638
- * @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[]} pads
2639
- * @param {number} [rotationDeg] Body-local rotation used for span measurement.
2640
- * @returns {{ width: number, depth: number }}
2641
- */
2642
- static #resolvePadSpan(
2643
- component,
2644
- pads,
2645
- rotationDeg = Number(component?.rotation || 0)
2646
- ) {
2647
- const componentPads = PcbScene3dBuilder.#componentPads(component, pads)
2648
- const nearbyPads = pads.filter((pad) =>
2649
- PcbScene3dBuilder.#isPadNearComponent(component, pad)
2650
- )
2651
- const spanPads = componentPads.length ? componentPads : nearbyPads
2652
-
2653
- if (!spanPads.length) {
2654
- return { width: 0, depth: 0 }
2655
- }
2656
-
2657
- const mountSide = PcbScene3dBuilder.#resolveMountSide(component)
2658
- return (
2659
- PcbScene3dPadLocalSpanResolver.resolve(
2660
- { ...component, rotation: rotationDeg },
2661
- spanPads,
2662
- mountSide
2663
- ) || { width: 0, depth: 0 }
2664
- )
2665
- }
2666
-
2667
- /**
2668
- * Resolves the visible procedural component rotation.
2669
- * @param {{ componentIndex?: number, rotation?: number }} component PCB component.
2670
- * @param {object[]} pads PCB pads.
2671
- * @param {string} mountSide Component mount side.
2672
- * @returns {number}
2673
- */
2674
- static #resolveComponentRotation(component, pads, mountSide) {
2675
- return (
2676
- PcbScene3dPadYawResolver.resolve(component, pads, mountSide) ??
2677
- Number(component?.rotation || 0)
2678
- )
2679
- }
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
-
2789
- /**
2790
- * Resolves pads explicitly owned by one component, preferring pads on the
2791
- * mounted surface when paste-mask side metadata is available.
2792
- * @param {{ componentIndex?: number, layer?: string }} component PCB component.
2793
- * @param {object[]} pads PCB pads.
2794
- * @returns {object[]}
2795
- */
2796
- static #componentPads(component, pads) {
2797
- const componentIndex = Number(component?.componentIndex)
2798
- if (!Number.isFinite(componentIndex)) {
2799
- return []
2800
- }
2801
-
2802
- const ownedPads = pads.filter(
2803
- (pad) => Number(pad?.componentIndex) === componentIndex
2804
- )
2805
- const mountSide = PcbScene3dBuilder.#resolveMountSide(component)
2806
- const surfacePads = ownedPads.filter((pad) =>
2807
- PcbScene3dBuilder.#isSurfacePad(pad, mountSide)
2808
- )
2809
-
2810
- return surfacePads.length ? surfacePads : ownedPads
2811
- }
2812
-
2813
- /**
2814
- * Checks whether one pad belongs to the requested component surface.
2815
- * @param {object} pad PCB pad.
2816
- * @param {'top' | 'bottom'} mountSide Component mount side.
2817
- * @returns {boolean}
2818
- */
2819
- static #isSurfacePad(pad, mountSide) {
2820
- return mountSide === 'bottom'
2821
- ? Boolean(pad?.hasBottomPasteMaskOpening)
2822
- : Boolean(pad?.hasTopPasteMaskOpening)
2823
- }
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
-
2844
- /**
2845
- * Returns true when one pad lies inside the component's local search area.
2846
- * @param {{ x: number, y: number }} component
2847
- * @param {{ x: number, y: number }} pad
2848
- * @returns {boolean}
2849
- */
2850
- static #isPadNearComponent(component, pad) {
2851
- return (
2852
- Math.abs(Number(pad.x || 0) - Number(component.x || 0)) <= 160 &&
2853
- Math.abs(Number(pad.y || 0) - Number(component.y || 0)) <= 160
2854
- )
2855
- }
2856
-
2857
2628
  /**
2858
2629
  * Resolves the PCB surface a component is mounted on.
2859
2630
  * @param {{ layer?: string }} component PCB component.