altium-toolkit 1.1.25 → 1.1.26

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,296 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Builds source-neutral package model suggestion rows for library compatibility.
7
+ */
8
+ export class LibraryCompatibilityModelHintBuilder {
9
+ /**
10
+ * Builds model suggestion rows from package-like footprint names.
11
+ * @param {object[]} pcbLibraries PCB library models.
12
+ * @returns {object[]}
13
+ */
14
+ static build(pcbLibraries) {
15
+ const suggestions = []
16
+
17
+ for (const library of pcbLibraries || []) {
18
+ const libraryFileName = String(library?.fileName || '')
19
+ for (const footprint of library?.pcbLibrary?.footprints || []) {
20
+ const suggestion =
21
+ LibraryCompatibilityModelHintBuilder.#modelSuggestion(
22
+ libraryFileName,
23
+ footprint
24
+ )
25
+ if (suggestion) suggestions.push(suggestion)
26
+ }
27
+ }
28
+
29
+ return suggestions
30
+ }
31
+
32
+ /**
33
+ * Builds one footprint model suggestion row.
34
+ * @param {string} libraryFileName Source library file name.
35
+ * @param {object} footprint Footprint row.
36
+ * @returns {object | null}
37
+ */
38
+ static #modelSuggestion(libraryFileName, footprint) {
39
+ if (
40
+ (footprint?.embeddedModels || []).length ||
41
+ (footprint?.componentBodies || []).length ||
42
+ (footprint?.componentModels || []).length
43
+ ) {
44
+ return null
45
+ }
46
+
47
+ const name = String(footprint?.name || '').trim()
48
+ const keys = LibraryCompatibilityModelHintBuilder.#packageKeys(name)
49
+ const packageClass = keys[0]
50
+ if (
51
+ !LibraryCompatibilityModelHintBuilder.#knownPackageClass(
52
+ packageClass
53
+ )
54
+ ) {
55
+ return null
56
+ }
57
+
58
+ return {
59
+ libraryFileName,
60
+ footprintName: name,
61
+ packageClass,
62
+ keys,
63
+ ...LibraryCompatibilityModelHintBuilder.#modelRotationHint(
64
+ footprint
65
+ ),
66
+ reason: 'footprint has no embedded or body-level model reference'
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Builds package matching keys from one footprint name.
72
+ * @param {string} name Footprint name.
73
+ * @returns {string[]}
74
+ */
75
+ static #packageKeys(name) {
76
+ const keys = []
77
+
78
+ for (const rawToken of String(name || '').split(/[_()\s]+/u)) {
79
+ const token = rawToken.trim().toUpperCase()
80
+ if (!token) continue
81
+ LibraryCompatibilityModelHintBuilder.#appendPackageToken(
82
+ keys,
83
+ token
84
+ )
85
+ }
86
+
87
+ return keys
88
+ }
89
+
90
+ /**
91
+ * Appends one package token and useful derived token fragments.
92
+ * @param {string[]} keys Destination keys.
93
+ * @param {string} token Source token.
94
+ * @returns {void}
95
+ */
96
+ static #appendPackageToken(keys, token) {
97
+ const parts = token.includes('-')
98
+ ? token.split('-').filter(Boolean)
99
+ : [token]
100
+
101
+ for (const part of parts) {
102
+ if (!LibraryCompatibilityModelHintBuilder.#isPitchToken(part)) {
103
+ LibraryCompatibilityModelHintBuilder.#appendUnique(keys, part)
104
+ }
105
+ LibraryCompatibilityModelHintBuilder.#appendDerivedKeys(keys, part)
106
+ }
107
+
108
+ LibraryCompatibilityModelHintBuilder.#appendCompoundPackageKeys(
109
+ keys,
110
+ token
111
+ )
112
+ }
113
+
114
+ /**
115
+ * Appends package-derived keys for one token.
116
+ * @param {string[]} keys Destination keys.
117
+ * @param {string} token Source token.
118
+ * @returns {void}
119
+ */
120
+ static #appendDerivedKeys(keys, token) {
121
+ if (/^\d+X\d+$/u.test(token)) {
122
+ LibraryCompatibilityModelHintBuilder.#appendUnique(keys, 'ARRAY')
123
+ }
124
+
125
+ const pitch = /^(?:PITCH|P)?(\d+(?:\.\d+)?)P?$/u.exec(token)
126
+ if (pitch && token.includes('P')) {
127
+ LibraryCompatibilityModelHintBuilder.#appendUnique(
128
+ keys,
129
+ 'PITCH-' + pitch[1]
130
+ )
131
+ }
132
+
133
+ if (token.endsWith('1EP')) {
134
+ LibraryCompatibilityModelHintBuilder.#appendUnique(keys, 'EP')
135
+ }
136
+
137
+ const pinCount = /^(\d+)PIN$/u.exec(token)
138
+ if (pinCount) {
139
+ LibraryCompatibilityModelHintBuilder.#appendUnique(
140
+ keys,
141
+ pinCount[1]
142
+ )
143
+ LibraryCompatibilityModelHintBuilder.#appendUnique(
144
+ keys,
145
+ pinCount[1] + 'P'
146
+ )
147
+ }
148
+ }
149
+
150
+ /**
151
+ * Appends derived keys that need the original compound token.
152
+ * @param {string[]} keys Destination keys.
153
+ * @param {string} token Source token.
154
+ * @returns {void}
155
+ */
156
+ static #appendCompoundPackageKeys(keys, token) {
157
+ const smd = /^(SMD)-(\d+)$/u.exec(token)
158
+ if (smd) {
159
+ LibraryCompatibilityModelHintBuilder.#appendUnique(keys, smd[1])
160
+ LibraryCompatibilityModelHintBuilder.#appendUnique(keys, smd[2])
161
+ }
162
+
163
+ const pinRange = /^(\d+)-(\d+)PIN$/u.exec(token)
164
+ if (pinRange) {
165
+ LibraryCompatibilityModelHintBuilder.#appendUnique(
166
+ keys,
167
+ pinRange[1]
168
+ )
169
+ LibraryCompatibilityModelHintBuilder.#appendUnique(
170
+ keys,
171
+ pinRange[2] + 'P'
172
+ )
173
+ }
174
+ }
175
+
176
+ /**
177
+ * Builds optional 3D model rotation metadata from pin-one pad position.
178
+ * @param {object} footprint Footprint row.
179
+ * @returns {object}
180
+ */
181
+ static #modelRotationHint(footprint) {
182
+ const pad = LibraryCompatibilityModelHintBuilder.#pinOnePad(footprint)
183
+ const position = LibraryCompatibilityModelHintBuilder.#point(pad)
184
+ if (!pad || !position) return {}
185
+
186
+ return {
187
+ pinOneDesignator: String(pad.designator || ''),
188
+ pinOnePosition: position,
189
+ rotationHint:
190
+ LibraryCompatibilityModelHintBuilder.#rotationHintFromPosition(
191
+ position
192
+ )
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Finds the pad most likely to define package model orientation.
198
+ * @param {object} footprint Footprint row.
199
+ * @returns {object | null}
200
+ */
201
+ static #pinOnePad(footprint) {
202
+ const pinOneNames = new Set(['1', 'A1', 'A2', 'A3', 'K'])
203
+
204
+ return (
205
+ (footprint?.pads || []).find((pad) =>
206
+ pinOneNames.has(String(pad?.designator || '').toUpperCase())
207
+ ) || null
208
+ )
209
+ }
210
+
211
+ /**
212
+ * Derives a package rotation hint from pin-one quadrant.
213
+ * @param {{ x: number, y: number }} position Pin-one position.
214
+ * @returns {number}
215
+ */
216
+ static #rotationHintFromPosition(position) {
217
+ if (position.x < 0) {
218
+ if (position.y > 0) return -90
219
+ return 0
220
+ }
221
+ if (position.x > 0) {
222
+ if (position.y < 0) return 90
223
+ return 180
224
+ }
225
+
226
+ return position.y > 0 ? 180 : 0
227
+ }
228
+
229
+ /**
230
+ * Returns true when one token can be represented as a pitch key.
231
+ * @param {string} token Source token.
232
+ * @returns {boolean}
233
+ */
234
+ static #isPitchToken(token) {
235
+ return /^(?:PITCH|P)\d+(?:\.\d+)?P?$|^\d+(?:\.\d+)?P$/u.test(token)
236
+ }
237
+
238
+ /**
239
+ * Returns true for common package family tokens.
240
+ * @param {string} value Package class token.
241
+ * @returns {boolean}
242
+ */
243
+ static #knownPackageClass(value) {
244
+ return new Set([
245
+ 'BGA',
246
+ 'CSP',
247
+ 'DFN',
248
+ 'DIP',
249
+ 'LGA',
250
+ 'LQFP',
251
+ 'QFN',
252
+ 'QFP',
253
+ 'SO',
254
+ 'SOIC',
255
+ 'SOP',
256
+ 'SOT',
257
+ 'SSOP',
258
+ 'TQFP',
259
+ 'TSSOP'
260
+ ]).has(value)
261
+ }
262
+
263
+ /**
264
+ * Appends a value once.
265
+ * @param {string[]} rows Destination rows.
266
+ * @param {string} value Candidate value.
267
+ * @returns {void}
268
+ */
269
+ static #appendUnique(rows, value) {
270
+ if (value && !rows.includes(value)) rows.push(value)
271
+ }
272
+
273
+ /**
274
+ * Returns one finite point from a row with x/y fields.
275
+ * @param {object} value Candidate row.
276
+ * @returns {{ x: number, y: number } | null}
277
+ */
278
+ static #point(value) {
279
+ const x = LibraryCompatibilityModelHintBuilder.#finiteNumber(value?.x)
280
+ const y = LibraryCompatibilityModelHintBuilder.#finiteNumber(value?.y)
281
+
282
+ if (x === null || y === null) return null
283
+
284
+ return { x, y }
285
+ }
286
+
287
+ /**
288
+ * Converts one value to a finite number.
289
+ * @param {unknown} value Candidate number.
290
+ * @returns {number | null}
291
+ */
292
+ static #finiteNumber(value) {
293
+ const numeric = Number(value)
294
+ return Number.isFinite(numeric) ? numeric : null
295
+ }
296
+ }