altium-toolkit 1.1.24 → 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.
- package/README.md +5 -3
- package/docs/api.md +43 -17
- package/docs/model-format.md +51 -11
- package/docs/schemas/altium_toolkit/library_compatibility_a1.schema.json +116 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +3 -0
- package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +58 -1
- package/package.json +1 -1
- package/spec/library-compatibility.md +65 -0
- package/src/core/altium/AltiumParser.mjs +8 -1
- package/src/core/altium/LibraryCompatibilityGeometry.mjs +618 -0
- package/src/core/altium/LibraryCompatibilityModelHintBuilder.mjs +296 -0
- package/src/core/altium/LibraryCompatibilityReportBuilder.mjs +930 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +29 -1
- package/src/core/altium/PcbDimensionParser.mjs +1 -0
- package/src/core/altium/PcbDimensionReportBuilder.mjs +190 -0
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +140 -10
- package/src/core/altium/PcbFabricationReadinessReportBuilder.mjs +607 -0
- package/src/core/altium/PcbInspectionReportBuilder.mjs +56 -3
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +138 -3
- package/src/core/altium/PcbLibModelParser.mjs +24 -2
- package/src/core/altium/PcbLibStorageNameResolver.mjs +171 -0
- package/src/core/altium/PcbLibStreamExtractor.mjs +15 -93
- package/src/core/altium/PcbPrimitiveFieldOffsetReportBuilder.mjs +352 -0
- package/src/core/altium/PcbRuleImpactReportBuilder.mjs +313 -0
- package/src/core/altium/PcbShapeBasedBodyGeometryParser.mjs +514 -0
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +442 -2
- package/src/core/altium/SchematicImageDiagnosticsBuilder.mjs +214 -0
- package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +523 -3
- package/src/parser.mjs +8 -0
- package/src/styles/altium-renderers.css +36 -0
- package/src/ui/PcbDimensionPrimitiveRenderer.mjs +422 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +6 -0
- package/src/ui/PcbScene3dBuilder.mjs +10 -2
- package/src/ui/PcbScene3dStaticBodyPlacementBuilder.mjs +225 -0
- package/src/ui/PcbSvgRenderer.mjs +78 -2
|
@@ -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
|
+
}
|