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.
@@ -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 PcbScene3dBoardOutlineRefiner.refine(
171
- sceneDescription,
172
- documentModel
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 a rough pad-span box around one component.
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 (!nearbyPads.length) {
1448
+ if (!spanPads.length) {
1438
1449
  return { width: 0, depth: 0 }
1439
1450
  }
1440
1451
 
1441
- const xs = []
1442
- const ys = []
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
- for (const pad of nearbyPads) {
1445
- const size = PcbScene3dBuilder.#resolvePadSize(pad)
1446
- xs.push(pad.x - size.width / 2, pad.x + size.width / 2)
1447
- ys.push(pad.y - size.depth / 2, pad.y + size.depth / 2)
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
- return {
1451
- width: Math.max(...xs) - Math.min(...xs),
1452
- depth: Math.max(...ys) - Math.min(...ys)
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 one visible pad size.
1471
- * @param {{ sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }} pad
1472
- * @returns {{ width: number, depth: number }}
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 #resolvePadSize(pad) {
1475
- return {
1476
- width:
1477
- Number(pad.sizeTopX || pad.sizeMidX || pad.sizeBottomX || 24) ||
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
+ }