altium-toolkit 1.1.26 → 1.1.31

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,11 +5,17 @@
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'
11
+ import { AltiumScene3dAuthoredBodyAnchorAdapter } from './AltiumScene3dAuthoredBodyAnchorAdapter.mjs'
8
12
  import { PcbFootprintPrimitiveSelector } from './PcbFootprintPrimitiveSelector.mjs'
13
+ import { PcbScene3dPadLocalSpanResolver } from './PcbScene3dPadLocalSpanResolver.mjs'
9
14
  import { PcbScene3dPackages } from './PcbScene3dPackages.mjs'
10
15
  import { PcbScene3dPlacementSideResolver } from './PcbScene3dPlacementSideResolver.mjs'
11
16
  import { PcbScene3dStaticBodyPlacementBuilder } from './PcbScene3dStaticBodyPlacementBuilder.mjs'
12
17
  import { PcbScene3dTextBoxLayoutResolver } from './PcbScene3dTextBoxLayoutResolver.mjs'
18
+ import { PcbFootprintPadAxisNormalizer } from './PcbFootprintPadAxisNormalizer.mjs'
13
19
 
14
20
  /**
15
21
  * Builds deterministic 3D scene data from the normalized PCB model.
@@ -32,7 +38,9 @@ export class PcbScene3dBuilder {
32
38
  * @returns {{ board: { widthMil: number, heightMil: number, thicknessMil: number, minX: number, minY: number, centerX: number, centerY: number, segments: Array<Record<string, number | string>> }, boardAssemblyModel: { origin: string, name: string, format: string, file?: File | Blob | null, relativePath?: string } | null, components: { designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, boardPositionMil: { x: number, y: number, z: number }, pattern: string, source: string, body: { family: string, sizeMil: { width: number, depth: number, height: number } }, externalModel: { name: string, relativePath: string, format: string } | null }[], externalPlacements: { 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 } }[], staticBodyPlacements: { designator: string, mountSide: string, rotationDeg: number, positionMil: { x: number, y: number, z: number }, bodyPositionMil: { x: number, y: number }, geometry: object }[], detail: { pads: any[], tracks: any[], arcs: any[], fills: any[], vias: any[], polygons: any[], silkscreen: { top: { fills: any[], tracks: any[], arcs: any[], texts: any[], fillColor?: number, strokeColor?: number }, bottom: { fills: any[], tracks: any[], arcs: any[], texts: any[], fillColor?: number, strokeColor?: number } } } }}
33
39
  */
34
40
  static build(documentModel, options = {}) {
35
- const pcb = documentModel?.pcb || {}
41
+ const sceneDocumentModel =
42
+ PcbFootprintPadAxisNormalizer.apply(documentModel)
43
+ const pcb = sceneDocumentModel?.pcb || {}
36
44
  const appearance3d = pcb.appearance3d || {}
37
45
  const boardOutline = pcb.boardOutline || {}
38
46
  const primitiveLayers = Array.isArray(pcb.primitiveLayers)
@@ -167,9 +175,19 @@ export class PcbScene3dBuilder {
167
175
  }
168
176
  }
169
177
 
170
- return PcbScene3dBoardOutlineRefiner.refine(
171
- sceneDescription,
172
- documentModel
178
+ return AltiumScene3dAuthoredBodyAnchorAdapter.apply(
179
+ AltiumScene3dBottomPadRotationAdapter.apply(
180
+ AltiumScene3dComponentBodyAdapter.apply(
181
+ AltiumScene3dExternalPlacementAdapter.apply(
182
+ PcbScene3dBoardOutlineRefiner.refine(
183
+ sceneDescription,
184
+ sceneDocumentModel
185
+ ),
186
+ sceneDocumentModel
187
+ ),
188
+ sceneDocumentModel
189
+ )
190
+ )
173
191
  )
174
192
  }
175
193
 
@@ -189,10 +207,7 @@ export class PcbScene3dBuilder {
189
207
  thicknessMil,
190
208
  modelRegistry
191
209
  ) {
192
- const mountSide =
193
- String(component.layer || 'TOP').toUpperCase() === 'BOTTOM'
194
- ? 'bottom'
195
- : 'top'
210
+ const mountSide = PcbScene3dBuilder.#resolveMountSide(component)
196
211
  const padSpan = PcbScene3dBuilder.#resolvePadSpan(component, pads)
197
212
  const body = PcbScene3dPackages.resolve(component, padSpan)
198
213
  const halfBoardThickness = thicknessMil / 2
@@ -1424,33 +1439,66 @@ export class PcbScene3dBuilder {
1424
1439
  }
1425
1440
 
1426
1441
  /**
1427
- * Resolves a rough pad-span box around one component.
1428
- * @param {{ x: number, y: number }} component
1442
+ * Resolves the owned or nearby pad-span box around one component.
1443
+ * @param {{ x: number, y: number, componentIndex?: number, layer?: string }} component
1429
1444
  * @param {{ x: number, y: number, sizeTopX?: number, sizeTopY?: number, sizeMidX?: number, sizeMidY?: number, sizeBottomX?: number, sizeBottomY?: number }[]} pads
1430
1445
  * @returns {{ width: number, depth: number }}
1431
1446
  */
1432
1447
  static #resolvePadSpan(component, pads) {
1448
+ const componentPads = PcbScene3dBuilder.#componentPads(component, pads)
1433
1449
  const nearbyPads = pads.filter((pad) =>
1434
1450
  PcbScene3dBuilder.#isPadNearComponent(component, pad)
1435
1451
  )
1452
+ const spanPads = componentPads.length ? componentPads : nearbyPads
1436
1453
 
1437
- if (!nearbyPads.length) {
1454
+ if (!spanPads.length) {
1438
1455
  return { width: 0, depth: 0 }
1439
1456
  }
1440
1457
 
1441
- const xs = []
1442
- const ys = []
1458
+ const mountSide = PcbScene3dBuilder.#resolveMountSide(component)
1459
+ return (
1460
+ PcbScene3dPadLocalSpanResolver.resolve(
1461
+ component,
1462
+ spanPads,
1463
+ mountSide
1464
+ ) || { width: 0, depth: 0 }
1465
+ )
1466
+ }
1443
1467
 
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)
1468
+ /**
1469
+ * Resolves pads explicitly owned by one component, preferring pads on the
1470
+ * mounted surface when paste-mask side metadata is available.
1471
+ * @param {{ componentIndex?: number, layer?: string }} component PCB component.
1472
+ * @param {object[]} pads PCB pads.
1473
+ * @returns {object[]}
1474
+ */
1475
+ static #componentPads(component, pads) {
1476
+ const componentIndex = Number(component?.componentIndex)
1477
+ if (!Number.isFinite(componentIndex)) {
1478
+ return []
1448
1479
  }
1449
1480
 
1450
- return {
1451
- width: Math.max(...xs) - Math.min(...xs),
1452
- depth: Math.max(...ys) - Math.min(...ys)
1453
- }
1481
+ const ownedPads = pads.filter(
1482
+ (pad) => Number(pad?.componentIndex) === componentIndex
1483
+ )
1484
+ const mountSide = PcbScene3dBuilder.#resolveMountSide(component)
1485
+ const surfacePads = ownedPads.filter((pad) =>
1486
+ PcbScene3dBuilder.#isSurfacePad(pad, mountSide)
1487
+ )
1488
+
1489
+ return surfacePads.length ? surfacePads : ownedPads
1490
+ }
1491
+
1492
+ /**
1493
+ * Checks whether one pad belongs to the requested component surface.
1494
+ * @param {object} pad PCB pad.
1495
+ * @param {'top' | 'bottom'} mountSide Component mount side.
1496
+ * @returns {boolean}
1497
+ */
1498
+ static #isSurfacePad(pad, mountSide) {
1499
+ return mountSide === 'bottom'
1500
+ ? Boolean(pad?.hasBottomPasteMaskOpening)
1501
+ : Boolean(pad?.hasTopPasteMaskOpening)
1454
1502
  }
1455
1503
 
1456
1504
  /**
@@ -1467,19 +1515,14 @@ export class PcbScene3dBuilder {
1467
1515
  }
1468
1516
 
1469
1517
  /**
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 }}
1518
+ * Resolves the PCB surface a component is mounted on.
1519
+ * @param {{ layer?: string }} component PCB component.
1520
+ * @returns {'top' | 'bottom'}
1473
1521
  */
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
- }
1522
+ static #resolveMountSide(component) {
1523
+ return String(component?.layer || 'TOP').toUpperCase() === 'BOTTOM'
1524
+ ? 'bottom'
1525
+ : 'top'
1483
1526
  }
1484
1527
 
1485
1528
  /**
@@ -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
+ }