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.
- package/docs/api.md +17 -0
- package/docs/migration.md +13 -3
- package/docs/testing.md +17 -5
- package/package.json +2 -2
- package/spec/feature-preservation.json +1 -1
- package/spec/library-scope.md +4 -3
- package/src/convergence/AltiumWorkerClient.mjs +3 -12
- package/src/ui/AltiumScene3dBodyPlacementIndex.mjs +95 -0
- package/src/ui/AltiumScene3dComponentBodyAdapter.mjs +12 -32
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +5 -61
- package/src/ui/PcbInteractionIndex.mjs +59 -78
- package/src/ui/PcbInteractionLayerModel.mjs +2 -32
- package/src/ui/PcbInteractionSourceMetadata.mjs +122 -0
- package/src/ui/PcbScene3dBuilder.mjs +38 -267
- package/src/ui/PcbScene3dComponentGeometryResolver.mjs +249 -0
- package/src/ui/PcbScene3dCopperRegionDetailBuilder.mjs +36 -2
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
266
|
+
componentGeometry,
|
|
264
267
|
board,
|
|
265
268
|
thicknessMil,
|
|
266
269
|
modelRegistry
|
|
267
270
|
) {
|
|
268
271
|
const mountSide = PcbScene3dBuilder.#resolveMountSide(component)
|
|
269
|
-
const rotationDeg =
|
|
272
|
+
const rotationDeg = componentGeometry.resolveComponentRotation(
|
|
270
273
|
component,
|
|
271
|
-
pads,
|
|
272
274
|
mountSide
|
|
273
275
|
)
|
|
274
|
-
const padSpan =
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 =
|
|
404
|
-
sourceComponent
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
444
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 =
|
|
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 {
|
|
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
|
-
|
|
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
|
-
!
|
|
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.
|