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
package/package.json
CHANGED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
const BOTTOM_LAYER_ID = 32
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pre-compensates Altium bottom-side pad rotations for shared 3D mirroring.
|
|
5
|
+
*/
|
|
6
|
+
export class AltiumScene3dBottomPadRotationAdapter {
|
|
7
|
+
/**
|
|
8
|
+
* Returns a scene with bottom-side pad rotations mirrored for rendering.
|
|
9
|
+
* @param {object} sceneDescription Scene description.
|
|
10
|
+
* @returns {object}
|
|
11
|
+
*/
|
|
12
|
+
static apply(sceneDescription) {
|
|
13
|
+
const detail = sceneDescription?.detail || {}
|
|
14
|
+
const detailPads = Array.isArray(detail.pads)
|
|
15
|
+
? AltiumScene3dBottomPadRotationAdapter.#mapPads(detail.pads)
|
|
16
|
+
: detail.pads
|
|
17
|
+
const scenePads = Array.isArray(sceneDescription?.pads)
|
|
18
|
+
? sceneDescription.pads === detail.pads
|
|
19
|
+
? detailPads
|
|
20
|
+
: AltiumScene3dBottomPadRotationAdapter.#mapPads(
|
|
21
|
+
sceneDescription.pads
|
|
22
|
+
)
|
|
23
|
+
: sceneDescription?.pads
|
|
24
|
+
|
|
25
|
+
if (
|
|
26
|
+
detailPads === detail.pads &&
|
|
27
|
+
scenePads === sceneDescription?.pads
|
|
28
|
+
) {
|
|
29
|
+
return sceneDescription
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
...sceneDescription,
|
|
34
|
+
pads: scenePads,
|
|
35
|
+
detail: {
|
|
36
|
+
...detail,
|
|
37
|
+
pads: detailPads
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Maps bottom-side pad rotations in one pad list.
|
|
44
|
+
* @param {object[]} pads Scene pad list.
|
|
45
|
+
* @returns {object[]}
|
|
46
|
+
*/
|
|
47
|
+
static #mapPads(pads) {
|
|
48
|
+
let changed = false
|
|
49
|
+
const mappedPads = pads.map((pad) => {
|
|
50
|
+
if (!AltiumScene3dBottomPadRotationAdapter.#isBottomPad(pad)) {
|
|
51
|
+
return pad
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const rotation =
|
|
55
|
+
AltiumScene3dBottomPadRotationAdapter.#normalizeAngle(
|
|
56
|
+
-Number(pad?.rotation || 0)
|
|
57
|
+
)
|
|
58
|
+
if (
|
|
59
|
+
AltiumScene3dBottomPadRotationAdapter.#anglesEqual(
|
|
60
|
+
rotation,
|
|
61
|
+
pad?.rotation
|
|
62
|
+
)
|
|
63
|
+
) {
|
|
64
|
+
return pad
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
changed = true
|
|
68
|
+
return {
|
|
69
|
+
...pad,
|
|
70
|
+
rotation
|
|
71
|
+
}
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
return changed ? mappedPads : pads
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Returns true when one pad belongs to bottom copper.
|
|
79
|
+
* @param {object} pad Scene pad.
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
static #isBottomPad(pad) {
|
|
83
|
+
const layerId = Number(
|
|
84
|
+
pad?.layerId ?? pad?.layerCode ?? pad?.sourceLayerId
|
|
85
|
+
)
|
|
86
|
+
if (Number.isFinite(layerId) && layerId === BOTTOM_LAYER_ID) {
|
|
87
|
+
return true
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const layerName = String(pad?.layer || pad?.layerName || '')
|
|
91
|
+
.trim()
|
|
92
|
+
.toUpperCase()
|
|
93
|
+
if (layerName === 'B.CU' || layerName.includes('BOTTOM')) {
|
|
94
|
+
return true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const sideName = String(pad?.side || pad?.mountSide || '')
|
|
98
|
+
.trim()
|
|
99
|
+
.toLowerCase()
|
|
100
|
+
return sideName === 'bottom' || sideName === 'back'
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Returns true when two rotations are equal after normalization.
|
|
105
|
+
* @param {unknown} left First angle.
|
|
106
|
+
* @param {unknown} right Second angle.
|
|
107
|
+
* @returns {boolean}
|
|
108
|
+
*/
|
|
109
|
+
static #anglesEqual(left, right) {
|
|
110
|
+
return (
|
|
111
|
+
Math.abs(
|
|
112
|
+
AltiumScene3dBottomPadRotationAdapter.#normalizeAngle(left) -
|
|
113
|
+
AltiumScene3dBottomPadRotationAdapter.#normalizeAngle(right)
|
|
114
|
+
) < 0.001
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Normalizes an angle into [0, 360).
|
|
120
|
+
* @param {unknown} angle Angle in degrees.
|
|
121
|
+
* @returns {number}
|
|
122
|
+
*/
|
|
123
|
+
static #normalizeAngle(angle) {
|
|
124
|
+
const value = Number(angle) || 0
|
|
125
|
+
return ((value % 360) + 360) % 360
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { PcbScene3dPadLocalSpanResolver } from './PcbScene3dPadLocalSpanResolver.mjs'
|
|
2
|
+
|
|
3
|
+
const REFINABLE_FAMILIES = new Set(['chip', 'diode', 'generic', 'ic', 'sot'])
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Refines Altium procedural fallback body sizes from component-owned pads.
|
|
7
|
+
*/
|
|
8
|
+
export class AltiumScene3dComponentBodyAdapter {
|
|
9
|
+
static #OVERSIZE_RATIO = 1.75
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Applies owned-pad body refinement to an Altium scene description.
|
|
13
|
+
* @param {object} sceneDescription Scene description.
|
|
14
|
+
* @param {object} documentModel Source document model.
|
|
15
|
+
* @returns {object}
|
|
16
|
+
*/
|
|
17
|
+
static apply(sceneDescription, documentModel) {
|
|
18
|
+
if (
|
|
19
|
+
String(sceneDescription?.sourceFormat || '').toLowerCase() !==
|
|
20
|
+
'altium' ||
|
|
21
|
+
!Array.isArray(sceneDescription?.components)
|
|
22
|
+
) {
|
|
23
|
+
return sceneDescription
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const sourceComponents = Array.isArray(documentModel?.pcb?.components)
|
|
27
|
+
? documentModel.pcb.components
|
|
28
|
+
: []
|
|
29
|
+
const pads = Array.isArray(documentModel?.pcb?.pads)
|
|
30
|
+
? documentModel.pcb.pads
|
|
31
|
+
: []
|
|
32
|
+
if (!sourceComponents.length || !pads.length) {
|
|
33
|
+
return sceneDescription
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const sourceByDesignator = new Map(
|
|
37
|
+
sourceComponents.map((component) => [
|
|
38
|
+
String(component?.designator || ''),
|
|
39
|
+
component
|
|
40
|
+
])
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
...sceneDescription,
|
|
45
|
+
components: sceneDescription.components.map((component) =>
|
|
46
|
+
AltiumScene3dComponentBodyAdapter.#refineComponent(
|
|
47
|
+
component,
|
|
48
|
+
sourceByDesignator.get(String(component?.designator || '')),
|
|
49
|
+
pads
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Refines one procedural component body when nearby pads overinflated it.
|
|
57
|
+
* @param {object} component Scene component.
|
|
58
|
+
* @param {object | undefined} sourceComponent Source PCB component.
|
|
59
|
+
* @param {object[]} pads Source PCB pads.
|
|
60
|
+
* @returns {object}
|
|
61
|
+
*/
|
|
62
|
+
static #refineComponent(component, sourceComponent, pads) {
|
|
63
|
+
const family = String(component?.body?.family || '')
|
|
64
|
+
if (
|
|
65
|
+
component?.externalModel ||
|
|
66
|
+
!sourceComponent ||
|
|
67
|
+
!REFINABLE_FAMILIES.has(family)
|
|
68
|
+
) {
|
|
69
|
+
return component
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const span = AltiumScene3dComponentBodyAdapter.#ownedPadSpan(
|
|
73
|
+
sourceComponent,
|
|
74
|
+
component.mountSide,
|
|
75
|
+
pads
|
|
76
|
+
)
|
|
77
|
+
const size = component?.body?.sizeMil || {}
|
|
78
|
+
if (
|
|
79
|
+
!span ||
|
|
80
|
+
!AltiumScene3dComponentBodyAdapter.#isOversized(size, span)
|
|
81
|
+
) {
|
|
82
|
+
return component
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return {
|
|
86
|
+
...component,
|
|
87
|
+
body: {
|
|
88
|
+
...component.body,
|
|
89
|
+
sizeMil: {
|
|
90
|
+
...size,
|
|
91
|
+
width: span.width,
|
|
92
|
+
depth: span.depth
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolves the owned surface-pad span for one source component.
|
|
100
|
+
* @param {object} component Source component.
|
|
101
|
+
* @param {string} mountSide Component mount side.
|
|
102
|
+
* @param {object[]} pads Source PCB pads.
|
|
103
|
+
* @returns {{ width: number, depth: number } | null}
|
|
104
|
+
*/
|
|
105
|
+
static #ownedPadSpan(component, mountSide, pads) {
|
|
106
|
+
const componentIndex = Number(component?.componentIndex)
|
|
107
|
+
if (!Number.isFinite(componentIndex)) {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const ownedPads = pads.filter(
|
|
112
|
+
(pad) => Number(pad?.componentIndex) === componentIndex
|
|
113
|
+
)
|
|
114
|
+
const surfacePads = ownedPads.filter((pad) =>
|
|
115
|
+
AltiumScene3dComponentBodyAdapter.#isSurfacePad(pad, mountSide)
|
|
116
|
+
)
|
|
117
|
+
const spanPads = surfacePads.length ? surfacePads : ownedPads
|
|
118
|
+
|
|
119
|
+
return PcbScene3dPadLocalSpanResolver.resolve(
|
|
120
|
+
component,
|
|
121
|
+
spanPads,
|
|
122
|
+
mountSide
|
|
123
|
+
)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Checks whether one pad belongs to the component's mounted surface.
|
|
128
|
+
* @param {object} pad Source pad.
|
|
129
|
+
* @param {string} mountSide Component mount side.
|
|
130
|
+
* @returns {boolean}
|
|
131
|
+
*/
|
|
132
|
+
static #isSurfacePad(pad, mountSide) {
|
|
133
|
+
return String(mountSide || '').toLowerCase() === 'bottom'
|
|
134
|
+
? Boolean(pad?.hasBottomPasteMaskOpening)
|
|
135
|
+
: Boolean(pad?.hasTopPasteMaskOpening)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Checks whether the current body is clearly larger than owned pads.
|
|
140
|
+
* @param {object} size Current body size.
|
|
141
|
+
* @param {{ width: number, depth: number }} span Owned pad span.
|
|
142
|
+
* @returns {boolean}
|
|
143
|
+
*/
|
|
144
|
+
static #isOversized(size, span) {
|
|
145
|
+
return (
|
|
146
|
+
Number(size?.width || 0) >
|
|
147
|
+
span.width *
|
|
148
|
+
AltiumScene3dComponentBodyAdapter.#OVERSIZE_RATIO ||
|
|
149
|
+
Number(size?.depth || 0) >
|
|
150
|
+
span.depth * AltiumScene3dComponentBodyAdapter.#OVERSIZE_RATIO
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
}
|