altium-toolkit 1.1.36 → 1.1.37

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.
@@ -0,0 +1,284 @@
1
+ const DISPLAY_MODULE_PATTERN =
2
+ /(?:^|[^a-z0-9])(?:display|lcd|oled|screen|tft)(?:$|[^a-z0-9])/i
3
+ const MIN_DIRECTIONAL_COSINE = 0.25
4
+
5
+ /**
6
+ * Detects display modules whose footprint yaw is more reliable than the
7
+ * embedded body yaw.
8
+ */
9
+ export class AltiumScene3dDisplayModuleYawPolicy {
10
+ /**
11
+ * Checks whether one placement should use the component yaw.
12
+ * @param {{ placement?: object, component?: object | null, pads?: object[], identityText?: string }} context Placement context.
13
+ * @returns {boolean}
14
+ */
15
+ static shouldUseComponentYaw(context) {
16
+ const { placement, component, pads, identityText } = context || {}
17
+ if (
18
+ !component ||
19
+ String(placement?.mountSide || '').toLowerCase() !== 'top' ||
20
+ String(placement?.projection?.source || '').toLowerCase() !==
21
+ 'pad-fallback' ||
22
+ String(placement?.externalModel?.origin || '').toLowerCase() !==
23
+ 'embedded' ||
24
+ !DISPLAY_MODULE_PATTERN.test(String(identityText || ''))
25
+ ) {
26
+ return false
27
+ }
28
+
29
+ return AltiumScene3dDisplayModuleYawPolicy.#hasOffsetSingleRowPads(
30
+ component,
31
+ pads
32
+ )
33
+ }
34
+
35
+ /**
36
+ * Checks whether a model-bounds display source points away from its edge
37
+ * pad row and needs a half-turn around the board normal.
38
+ * @param {{ placement?: object, component?: object | null, pads?: object[], identityText?: string }} context Placement context.
39
+ * @returns {boolean}
40
+ */
41
+ static shouldReverseModelBoundsYaw(context) {
42
+ const { placement, component, pads, identityText } = context || {}
43
+ if (
44
+ !component ||
45
+ String(placement?.mountSide || '').toLowerCase() !== 'top' ||
46
+ String(placement?.projection?.source || '').toLowerCase() !==
47
+ 'model-bounds' ||
48
+ String(placement?.externalModel?.origin || '').toLowerCase() !==
49
+ 'embedded' ||
50
+ !DISPLAY_MODULE_PATTERN.test(String(identityText || ''))
51
+ ) {
52
+ return false
53
+ }
54
+
55
+ const padOffset =
56
+ AltiumScene3dDisplayModuleYawPolicy.#offsetSingleRowPadVector(
57
+ component,
58
+ pads
59
+ )
60
+ const sourceOffset =
61
+ AltiumScene3dDisplayModuleYawPolicy.#modelBoundsSourceVector(
62
+ placement
63
+ )
64
+ if (!padOffset || !sourceOffset) {
65
+ return false
66
+ }
67
+
68
+ const alignment = AltiumScene3dDisplayModuleYawPolicy.#cosineSimilarity(
69
+ padOffset,
70
+ sourceOffset
71
+ )
72
+
73
+ return (
74
+ Number.isFinite(alignment) && alignment <= -MIN_DIRECTIONAL_COSINE
75
+ )
76
+ }
77
+
78
+ /**
79
+ * Checks whether the footprint has one edge-mounted contact row.
80
+ * @param {object} component PCB component.
81
+ * @param {object[] | undefined} pads Source PCB pads.
82
+ * @returns {boolean}
83
+ */
84
+ static #hasOffsetSingleRowPads(component, pads) {
85
+ return Boolean(
86
+ AltiumScene3dDisplayModuleYawPolicy.#offsetSingleRowPadVector(
87
+ component,
88
+ pads
89
+ )
90
+ )
91
+ }
92
+
93
+ /**
94
+ * Resolves the component-to-pad-row vector for an edge-mounted display
95
+ * connector.
96
+ * @param {object} component PCB component.
97
+ * @param {object[] | undefined} pads Source PCB pads.
98
+ * @returns {{ x: number, y: number } | null}
99
+ */
100
+ static #offsetSingleRowPadVector(component, pads) {
101
+ const measurablePads = AltiumScene3dDisplayModuleYawPolicy.#surfacePads(
102
+ component,
103
+ pads
104
+ ).filter((pad) =>
105
+ AltiumScene3dDisplayModuleYawPolicy.#isMeasurablePad(pad)
106
+ )
107
+ if (measurablePads.length < 6) {
108
+ return null
109
+ }
110
+
111
+ const xValues = measurablePads.map((pad) => Number(pad?.x || 0))
112
+ const yValues = measurablePads.map((pad) => Number(pad?.y || 0))
113
+ const spreadX = Math.max(...xValues) - Math.min(...xValues)
114
+ const spreadY = Math.max(...yValues) - Math.min(...yValues)
115
+ const rowAxis = spreadX >= spreadY ? 'x' : 'y'
116
+ const crossAxis = rowAxis === 'x' ? 'y' : 'x'
117
+ const majorSpread = Math.max(spreadX, spreadY)
118
+ const minorSpread = Math.min(spreadX, spreadY)
119
+ const maxPadSpan = Math.max(
120
+ ...measurablePads.map((pad) =>
121
+ AltiumScene3dDisplayModuleYawPolicy.#maxPadSpan(pad)
122
+ )
123
+ )
124
+ const componentCross = Number(component?.[crossAxis])
125
+ const rowCross =
126
+ measurablePads.reduce(
127
+ (sum, pad) => sum + Number(pad?.[crossAxis] || 0),
128
+ 0
129
+ ) / measurablePads.length
130
+ const componentX = Number(component?.x)
131
+ const componentY = Number(component?.y)
132
+ const rowX =
133
+ measurablePads.reduce((sum, pad) => sum + Number(pad?.x || 0), 0) /
134
+ measurablePads.length
135
+ const rowY =
136
+ measurablePads.reduce((sum, pad) => sum + Number(pad?.y || 0), 0) /
137
+ measurablePads.length
138
+ if (
139
+ !Number.isFinite(componentCross) ||
140
+ !Number.isFinite(rowCross) ||
141
+ !Number.isFinite(componentX) ||
142
+ !Number.isFinite(componentY) ||
143
+ !Number.isFinite(rowX) ||
144
+ !Number.isFinite(rowY) ||
145
+ majorSpread < Math.max(150, maxPadSpan * 2) ||
146
+ minorSpread > Math.max(10, maxPadSpan * 0.25)
147
+ ) {
148
+ return null
149
+ }
150
+
151
+ if (
152
+ Math.abs(rowCross - componentCross) <
153
+ Math.max(100, maxPadSpan * 1.5)
154
+ ) {
155
+ return null
156
+ }
157
+
158
+ return { x: rowX - componentX, y: rowY - componentY }
159
+ }
160
+
161
+ /**
162
+ * Estimates the scene-space vector from a model-bounds source origin to
163
+ * the dominant display body center.
164
+ * @param {object | null | undefined} placement External placement.
165
+ * @returns {{ x: number, y: number } | null}
166
+ */
167
+ static #modelBoundsSourceVector(placement) {
168
+ const bounds = placement?.projection?.boundsMil || {}
169
+ const width = Math.abs(Number(bounds?.width || 0))
170
+ const depth = Math.abs(Number(bounds?.depth || 0))
171
+ if (
172
+ !Number.isFinite(width) ||
173
+ !Number.isFinite(depth) ||
174
+ width <= 0 ||
175
+ depth <= 0
176
+ ) {
177
+ return null
178
+ }
179
+
180
+ const rotationRad =
181
+ (AltiumScene3dDisplayModuleYawPolicy.#normalizeAngle(
182
+ Number(placement?.rotationDeg || 0)
183
+ ) *
184
+ Math.PI) /
185
+ 180
186
+ const cos = Math.cos(rotationRad)
187
+ const sin = Math.sin(rotationRad)
188
+
189
+ return depth >= width
190
+ ? { x: -sin * (depth / 2), y: cos * (depth / 2) }
191
+ : { x: cos * (width / 2), y: sin * (width / 2) }
192
+ }
193
+
194
+ /**
195
+ * Computes the cosine similarity between two planar vectors.
196
+ * @param {{ x?: number, y?: number }} left First vector.
197
+ * @param {{ x?: number, y?: number }} right Second vector.
198
+ * @returns {number}
199
+ */
200
+ static #cosineSimilarity(left, right) {
201
+ const leftX = Number(left?.x || 0)
202
+ const leftY = Number(left?.y || 0)
203
+ const rightX = Number(right?.x || 0)
204
+ const rightY = Number(right?.y || 0)
205
+ const leftMagnitude = Math.hypot(leftX, leftY)
206
+ const rightMagnitude = Math.hypot(rightX, rightY)
207
+ if (leftMagnitude <= 0 || rightMagnitude <= 0) {
208
+ return Number.NaN
209
+ }
210
+
211
+ return (
212
+ (leftX * rightX + leftY * rightY) / leftMagnitude / rightMagnitude
213
+ )
214
+ }
215
+
216
+ /**
217
+ * Normalizes an angle into the renderer's [0, 360) degree range.
218
+ * @param {number} value Raw angle.
219
+ * @returns {number}
220
+ */
221
+ static #normalizeAngle(value) {
222
+ const angle = Number(value || 0) % 360
223
+
224
+ return angle < 0 ? angle + 360 : angle
225
+ }
226
+
227
+ /**
228
+ * Collects surface pads owned by one component.
229
+ * @param {object} component PCB component.
230
+ * @param {object[] | undefined} pads Source PCB pads.
231
+ * @returns {object[]}
232
+ */
233
+ static #surfacePads(component, pads) {
234
+ const componentIndex = Number(component?.componentIndex)
235
+ if (!Number.isFinite(componentIndex)) {
236
+ return []
237
+ }
238
+
239
+ const ownedPads = (Array.isArray(pads) ? pads : []).filter(
240
+ (pad) => Number(pad?.componentIndex) === componentIndex
241
+ )
242
+ const bottom =
243
+ String(component?.layer || '')
244
+ .toUpperCase()
245
+ .includes('BOTTOM') ||
246
+ String(component?.layer || '').toUpperCase() === 'BOT'
247
+ const surfacePads = ownedPads.filter((pad) =>
248
+ bottom
249
+ ? Boolean(pad?.hasBottomPasteMaskOpening)
250
+ : Boolean(pad?.hasTopPasteMaskOpening)
251
+ )
252
+
253
+ return surfacePads.length ? surfacePads : ownedPads
254
+ }
255
+
256
+ /**
257
+ * Checks whether one pad has finite coordinates and non-zero dimensions.
258
+ * @param {object} pad Source PCB pad.
259
+ * @returns {boolean}
260
+ */
261
+ static #isMeasurablePad(pad) {
262
+ return (
263
+ Number.isFinite(Number(pad?.x)) &&
264
+ Number.isFinite(Number(pad?.y)) &&
265
+ AltiumScene3dDisplayModuleYawPolicy.#maxPadSpan(pad) > 0
266
+ )
267
+ }
268
+
269
+ /**
270
+ * Measures the largest available pad dimension.
271
+ * @param {object} pad Source PCB pad.
272
+ * @returns {number}
273
+ */
274
+ static #maxPadSpan(pad) {
275
+ return Math.max(
276
+ Number(pad?.sizeTopX || 0),
277
+ Number(pad?.sizeTopY || 0),
278
+ Number(pad?.sizeMidX || 0),
279
+ Number(pad?.sizeMidY || 0),
280
+ Number(pad?.sizeBottomX || 0),
281
+ Number(pad?.sizeBottomY || 0)
282
+ )
283
+ }
284
+ }