altium-toolkit 1.1.26 → 1.1.30
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/package.json +1 -1
- package/src/ui/AltiumScene3dBottomPadRotationAdapter.mjs +127 -0
- package/src/ui/AltiumScene3dComponentBodyAdapter.mjs +153 -0
- package/src/ui/AltiumScene3dExternalPlacementAdapter.mjs +919 -0
- package/src/ui/AltiumScene3dIdentityTokens.mjs +105 -0
- package/src/ui/AltiumScene3dPlacementRotationPolicy.mjs +357 -0
- package/src/ui/AltiumScene3dRepeatedModelOwnerRepair.mjs +576 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +54 -11
- package/src/ui/PcbScene3dBuilder.mjs +69 -32
- package/src/ui/PcbScene3dPadLocalSpanResolver.mjs +109 -0
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
import { PcbEdgeFacingGlyphNormalizer } from './PcbEdgeFacingGlyphNormalizer.mjs'
|
|
6
6
|
import { PcbScene3dBoardOutlineRefiner } from './PcbScene3dBoardOutlineRefiner.mjs'
|
|
7
7
|
import { PcbScene3dDrillCutoutBuilder } from './PcbScene3dDrillCutoutBuilder.mjs'
|
|
8
|
+
import { AltiumScene3dExternalPlacementAdapter } from './AltiumScene3dExternalPlacementAdapter.mjs'
|
|
9
|
+
import { AltiumScene3dBottomPadRotationAdapter } from './AltiumScene3dBottomPadRotationAdapter.mjs'
|
|
10
|
+
import { AltiumScene3dComponentBodyAdapter } from './AltiumScene3dComponentBodyAdapter.mjs'
|
|
8
11
|
import { PcbFootprintPrimitiveSelector } from './PcbFootprintPrimitiveSelector.mjs'
|
|
12
|
+
import { PcbScene3dPadLocalSpanResolver } from './PcbScene3dPadLocalSpanResolver.mjs'
|
|
9
13
|
import { PcbScene3dPackages } from './PcbScene3dPackages.mjs'
|
|
10
14
|
import { PcbScene3dPlacementSideResolver } from './PcbScene3dPlacementSideResolver.mjs'
|
|
11
15
|
import { PcbScene3dStaticBodyPlacementBuilder } from './PcbScene3dStaticBodyPlacementBuilder.mjs'
|
|
@@ -167,9 +171,17 @@ export class PcbScene3dBuilder {
|
|
|
167
171
|
}
|
|
168
172
|
}
|
|
169
173
|
|
|
170
|
-
return
|
|
171
|
-
|
|
172
|
-
|
|
174
|
+
return AltiumScene3dBottomPadRotationAdapter.apply(
|
|
175
|
+
AltiumScene3dComponentBodyAdapter.apply(
|
|
176
|
+
AltiumScene3dExternalPlacementAdapter.apply(
|
|
177
|
+
PcbScene3dBoardOutlineRefiner.refine(
|
|
178
|
+
sceneDescription,
|
|
179
|
+
documentModel
|
|
180
|
+
),
|
|
181
|
+
documentModel
|
|
182
|
+
),
|
|
183
|
+
documentModel
|
|
184
|
+
)
|
|
173
185
|
)
|
|
174
186
|
}
|
|
175
187
|
|
|
@@ -189,10 +201,7 @@ export class PcbScene3dBuilder {
|
|
|
189
201
|
thicknessMil,
|
|
190
202
|
modelRegistry
|
|
191
203
|
) {
|
|
192
|
-
const mountSide =
|
|
193
|
-
String(component.layer || 'TOP').toUpperCase() === 'BOTTOM'
|
|
194
|
-
? 'bottom'
|
|
195
|
-
: 'top'
|
|
204
|
+
const mountSide = PcbScene3dBuilder.#resolveMountSide(component)
|
|
196
205
|
const padSpan = PcbScene3dBuilder.#resolvePadSpan(component, pads)
|
|
197
206
|
const body = PcbScene3dPackages.resolve(component, padSpan)
|
|
198
207
|
const halfBoardThickness = thicknessMil / 2
|
|
@@ -1424,33 +1433,66 @@ export class PcbScene3dBuilder {
|
|
|
1424
1433
|
}
|
|
1425
1434
|
|
|
1426
1435
|
/**
|
|
1427
|
-
* Resolves
|
|
1428
|
-
* @param {{ x: number, y: number }} component
|
|
1436
|
+
* Resolves the owned or nearby pad-span box around one component.
|
|
1437
|
+
* @param {{ x: number, y: number, componentIndex?: number, layer?: string }} component
|
|
1429
1438
|
* @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[]} pads
|
|
1430
1439
|
* @returns {{ width: number, depth: number }}
|
|
1431
1440
|
*/
|
|
1432
1441
|
static #resolvePadSpan(component, pads) {
|
|
1442
|
+
const componentPads = PcbScene3dBuilder.#componentPads(component, pads)
|
|
1433
1443
|
const nearbyPads = pads.filter((pad) =>
|
|
1434
1444
|
PcbScene3dBuilder.#isPadNearComponent(component, pad)
|
|
1435
1445
|
)
|
|
1446
|
+
const spanPads = componentPads.length ? componentPads : nearbyPads
|
|
1436
1447
|
|
|
1437
|
-
if (!
|
|
1448
|
+
if (!spanPads.length) {
|
|
1438
1449
|
return { width: 0, depth: 0 }
|
|
1439
1450
|
}
|
|
1440
1451
|
|
|
1441
|
-
const
|
|
1442
|
-
|
|
1452
|
+
const mountSide = PcbScene3dBuilder.#resolveMountSide(component)
|
|
1453
|
+
return (
|
|
1454
|
+
PcbScene3dPadLocalSpanResolver.resolve(
|
|
1455
|
+
component,
|
|
1456
|
+
spanPads,
|
|
1457
|
+
mountSide
|
|
1458
|
+
) || { width: 0, depth: 0 }
|
|
1459
|
+
)
|
|
1460
|
+
}
|
|
1443
1461
|
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1462
|
+
/**
|
|
1463
|
+
* Resolves pads explicitly owned by one component, preferring pads on the
|
|
1464
|
+
* mounted surface when paste-mask side metadata is available.
|
|
1465
|
+
* @param {{ componentIndex?: number, layer?: string }} component PCB component.
|
|
1466
|
+
* @param {object[]} pads PCB pads.
|
|
1467
|
+
* @returns {object[]}
|
|
1468
|
+
*/
|
|
1469
|
+
static #componentPads(component, pads) {
|
|
1470
|
+
const componentIndex = Number(component?.componentIndex)
|
|
1471
|
+
if (!Number.isFinite(componentIndex)) {
|
|
1472
|
+
return []
|
|
1448
1473
|
}
|
|
1449
1474
|
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1475
|
+
const ownedPads = pads.filter(
|
|
1476
|
+
(pad) => Number(pad?.componentIndex) === componentIndex
|
|
1477
|
+
)
|
|
1478
|
+
const mountSide = PcbScene3dBuilder.#resolveMountSide(component)
|
|
1479
|
+
const surfacePads = ownedPads.filter((pad) =>
|
|
1480
|
+
PcbScene3dBuilder.#isSurfacePad(pad, mountSide)
|
|
1481
|
+
)
|
|
1482
|
+
|
|
1483
|
+
return surfacePads.length ? surfacePads : ownedPads
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
/**
|
|
1487
|
+
* Checks whether one pad belongs to the requested component surface.
|
|
1488
|
+
* @param {object} pad PCB pad.
|
|
1489
|
+
* @param {'top' | 'bottom'} mountSide Component mount side.
|
|
1490
|
+
* @returns {boolean}
|
|
1491
|
+
*/
|
|
1492
|
+
static #isSurfacePad(pad, mountSide) {
|
|
1493
|
+
return mountSide === 'bottom'
|
|
1494
|
+
? Boolean(pad?.hasBottomPasteMaskOpening)
|
|
1495
|
+
: Boolean(pad?.hasTopPasteMaskOpening)
|
|
1454
1496
|
}
|
|
1455
1497
|
|
|
1456
1498
|
/**
|
|
@@ -1467,19 +1509,14 @@ export class PcbScene3dBuilder {
|
|
|
1467
1509
|
}
|
|
1468
1510
|
|
|
1469
1511
|
/**
|
|
1470
|
-
* Resolves
|
|
1471
|
-
* @param {{
|
|
1472
|
-
* @returns {
|
|
1512
|
+
* Resolves the PCB surface a component is mounted on.
|
|
1513
|
+
* @param {{ layer?: string }} component PCB component.
|
|
1514
|
+
* @returns {'top' | 'bottom'}
|
|
1473
1515
|
*/
|
|
1474
|
-
static #
|
|
1475
|
-
return
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
24,
|
|
1479
|
-
depth:
|
|
1480
|
-
Number(pad.sizeTopY || pad.sizeMidY || pad.sizeBottomY || 24) ||
|
|
1481
|
-
24
|
|
1482
|
-
}
|
|
1516
|
+
static #resolveMountSide(component) {
|
|
1517
|
+
return String(component?.layer || 'TOP').toUpperCase() === 'BOTTOM'
|
|
1518
|
+
? 'bottom'
|
|
1519
|
+
: 'top'
|
|
1483
1520
|
}
|
|
1484
1521
|
|
|
1485
1522
|
/**
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves pad bounds in component-local axes for 3D procedural bodies.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbScene3dPadLocalSpanResolver {
|
|
9
|
+
/**
|
|
10
|
+
* Measures a set of pads after rotating their corners into the component's
|
|
11
|
+
* local coordinate frame.
|
|
12
|
+
* @param {{ x?: number, y?: number, rotation?: number }} component Source component.
|
|
13
|
+
* @param {object[]} pads Pads to measure.
|
|
14
|
+
* @param {'top' | 'bottom'} mountSide Component mount side.
|
|
15
|
+
* @returns {{ width: number, depth: number } | null}
|
|
16
|
+
*/
|
|
17
|
+
static resolve(component, pads, mountSide = 'top') {
|
|
18
|
+
if (!Array.isArray(pads) || !pads.length) {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const originX = Number(component?.x || 0)
|
|
23
|
+
const originY = Number(component?.y || 0)
|
|
24
|
+
const componentRadians =
|
|
25
|
+
(-Number(component?.rotation || 0) * Math.PI) / 180
|
|
26
|
+
const componentCos = Math.cos(componentRadians)
|
|
27
|
+
const componentSin = Math.sin(componentRadians)
|
|
28
|
+
const xs = []
|
|
29
|
+
const ys = []
|
|
30
|
+
|
|
31
|
+
for (const pad of pads) {
|
|
32
|
+
for (const point of PcbScene3dPadLocalSpanResolver.#padCorners(
|
|
33
|
+
pad,
|
|
34
|
+
mountSide
|
|
35
|
+
)) {
|
|
36
|
+
const relativeX = point.x - originX
|
|
37
|
+
const relativeY = point.y - originY
|
|
38
|
+
xs.push(relativeX * componentCos - relativeY * componentSin)
|
|
39
|
+
ys.push(relativeX * componentSin + relativeY * componentCos)
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const width = PcbScene3dPadLocalSpanResolver.#roundMil(
|
|
44
|
+
Math.max(...xs) - Math.min(...xs)
|
|
45
|
+
)
|
|
46
|
+
const depth = PcbScene3dPadLocalSpanResolver.#roundMil(
|
|
47
|
+
Math.max(...ys) - Math.min(...ys)
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
return width > 0 && depth > 0 ? { width, depth } : null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Rounds tiny trigonometric floating-point noise while preserving mil detail.
|
|
55
|
+
* @param {number} value Raw mil value.
|
|
56
|
+
* @returns {number}
|
|
57
|
+
*/
|
|
58
|
+
static #roundMil(value) {
|
|
59
|
+
return Math.round(Number(value || 0) * 1000000) / 1000000
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Builds the visible world-space corners of one rectangular pad.
|
|
64
|
+
* @param {object} pad Source pad.
|
|
65
|
+
* @param {'top' | 'bottom'} mountSide Component mount side.
|
|
66
|
+
* @returns {{ x: number, y: number }[]}
|
|
67
|
+
*/
|
|
68
|
+
static #padCorners(pad, mountSide) {
|
|
69
|
+
const size = PcbScene3dPadLocalSpanResolver.#padSize(pad, mountSide)
|
|
70
|
+
const halfWidth = size.width / 2
|
|
71
|
+
const halfDepth = size.depth / 2
|
|
72
|
+
const radians = (Number(pad?.rotation || 0) * Math.PI) / 180
|
|
73
|
+
const cos = Math.cos(radians)
|
|
74
|
+
const sin = Math.sin(radians)
|
|
75
|
+
const centerX = Number(pad?.x || 0)
|
|
76
|
+
const centerY = Number(pad?.y || 0)
|
|
77
|
+
|
|
78
|
+
return [
|
|
79
|
+
{ x: -halfWidth, y: -halfDepth },
|
|
80
|
+
{ x: halfWidth, y: -halfDepth },
|
|
81
|
+
{ x: halfWidth, y: halfDepth },
|
|
82
|
+
{ x: -halfWidth, y: halfDepth }
|
|
83
|
+
].map((point) => ({
|
|
84
|
+
x: centerX + point.x * cos - point.y * sin,
|
|
85
|
+
y: centerY + point.x * sin + point.y * cos
|
|
86
|
+
}))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Resolves a pad's visible size on the component side.
|
|
91
|
+
* @param {object} pad Source pad.
|
|
92
|
+
* @param {'top' | 'bottom'} mountSide Component mount side.
|
|
93
|
+
* @returns {{ width: number, depth: number }}
|
|
94
|
+
*/
|
|
95
|
+
static #padSize(pad, mountSide) {
|
|
96
|
+
const bottom = String(mountSide || '').toLowerCase() === 'bottom'
|
|
97
|
+
const width = bottom
|
|
98
|
+
? pad?.sizeBottomX || pad?.sizeMidX || pad?.sizeTopX
|
|
99
|
+
: pad?.sizeTopX || pad?.sizeMidX || pad?.sizeBottomX
|
|
100
|
+
const depth = bottom
|
|
101
|
+
? pad?.sizeBottomY || pad?.sizeMidY || pad?.sizeTopY
|
|
102
|
+
: pad?.sizeTopY || pad?.sizeMidY || pad?.sizeBottomY
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
width: Number(width || 24) || 24,
|
|
106
|
+
depth: Number(depth || 24) || 24
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|