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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "altium-toolkit",
3
- "version": "1.1.26",
3
+ "version": "1.1.30",
4
4
  "description": "Altium document parsing and non-interactive rendering utilities",
5
5
  "keywords": [
6
6
  "altium",
@@ -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
+ }