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.
- package/package.json +1 -1
- package/src/core/altium/AltiumParser.mjs +34 -1
- package/src/core/altium/AltiumSchematicArcAngleNormalizer.mjs +82 -0
- package/src/core/altium/AltiumSchematicFreeGraphicStrokeNormalizer.mjs +105 -0
- package/src/core/altium/AltiumSchematicHiddenDesignatorResolver.mjs +191 -0
- package/src/core/altium/AltiumSchematicPackedImageResolver.mjs +433 -0
- package/src/core/altium/AltiumSchematicSheetBoundsNormalizer.mjs +287 -0
- package/src/parser.mjs +5 -0
- package/src/renderers.mjs +2 -0
- package/src/scene3d.mjs +1 -0
- package/src/ui/AltiumPcbBottomViewMirror.mjs +394 -0
- package/src/ui/AltiumScene3dAuthoredBodyAnchorAdapter.mjs +252 -0
- 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/PcbFootprintPadAxisNormalizer.mjs +609 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +54 -11
- package/src/ui/PcbScene3dBuilder.mjs +76 -33
- package/src/ui/PcbScene3dPadLocalSpanResolver.mjs +109 -0
|
@@ -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
|
|
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
|
|
171
|
-
|
|
172
|
-
|
|
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
|
|
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 (!
|
|
1454
|
+
if (!spanPads.length) {
|
|
1438
1455
|
return { width: 0, depth: 0 }
|
|
1439
1456
|
}
|
|
1440
1457
|
|
|
1441
|
-
const
|
|
1442
|
-
|
|
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
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
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
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
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
|
|
1471
|
-
* @param {{
|
|
1472
|
-
* @returns {
|
|
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 #
|
|
1475
|
-
return
|
|
1476
|
-
|
|
1477
|
-
|
|
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
|
+
}
|