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
|
@@ -25,10 +25,17 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
25
25
|
const fields = PcbLayerStackReadModelBuilder.#mergeFields(
|
|
26
26
|
input.boardRecords || []
|
|
27
27
|
)
|
|
28
|
+
const nativeLayerOrder = (input.layers || []).length
|
|
29
|
+
? null
|
|
30
|
+
: PcbLayerStackReadModelBuilder.#nativeLayerOrder(
|
|
31
|
+
fields,
|
|
32
|
+
input.primitiveLayers || []
|
|
33
|
+
)
|
|
28
34
|
const layers = PcbLayerStackReadModelBuilder.#layers(
|
|
29
35
|
input.layers || [],
|
|
30
36
|
input.primitiveLayers || [],
|
|
31
|
-
fields
|
|
37
|
+
fields,
|
|
38
|
+
nativeLayerOrder
|
|
32
39
|
)
|
|
33
40
|
const layerById = new Map(
|
|
34
41
|
layers
|
|
@@ -85,6 +92,9 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
85
92
|
cavityRegionCount: cavityReport.cavityRegionCount,
|
|
86
93
|
stiffenerLayerCount: cavityReport.stiffenerLayerCount,
|
|
87
94
|
adhesiveLayerCount: cavityReport.adhesiveLayerCount,
|
|
95
|
+
...(nativeLayerOrder
|
|
96
|
+
? { nativeLayerOrderCount: nativeLayerOrder.layerIds.length }
|
|
97
|
+
: {}),
|
|
88
98
|
diagnosticCount: diagnostics.length
|
|
89
99
|
}
|
|
90
100
|
|
|
@@ -101,6 +111,7 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
101
111
|
topLevelBendLines,
|
|
102
112
|
cavityReport
|
|
103
113
|
),
|
|
114
|
+
...(nativeLayerOrder ? { nativeLayerOrder } : {}),
|
|
104
115
|
layers,
|
|
105
116
|
substacks,
|
|
106
117
|
branches,
|
|
@@ -160,9 +171,10 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
160
171
|
* @param {object[]} layers Parsed physical layers.
|
|
161
172
|
* @param {object[]} primitiveLayers Primitive layer map.
|
|
162
173
|
* @param {Record<string, string | string[]>} fields Source fields.
|
|
174
|
+
* @param {{ layerIds: number[] } | null} nativeLayerOrder Native order.
|
|
163
175
|
* @returns {object[]}
|
|
164
176
|
*/
|
|
165
|
-
static #layers(layers, primitiveLayers, fields) {
|
|
177
|
+
static #layers(layers, primitiveLayers, fields, nativeLayerOrder) {
|
|
166
178
|
if (layers.length) {
|
|
167
179
|
return layers.map((layer) =>
|
|
168
180
|
PcbLayerStackReadModelBuilder.#stripUndefined({
|
|
@@ -187,7 +199,19 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
187
199
|
)
|
|
188
200
|
}
|
|
189
201
|
|
|
190
|
-
|
|
202
|
+
const nativeOrderByLayerId = new Map(
|
|
203
|
+
(nativeLayerOrder?.layerIds || []).map((layerId, index) => [
|
|
204
|
+
layerId,
|
|
205
|
+
index + 1
|
|
206
|
+
])
|
|
207
|
+
)
|
|
208
|
+
const orderedPrimitiveLayers =
|
|
209
|
+
PcbLayerStackReadModelBuilder.#orderPrimitiveLayers(
|
|
210
|
+
primitiveLayers || [],
|
|
211
|
+
nativeLayerOrder
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
return orderedPrimitiveLayers.map((layer, index) =>
|
|
191
215
|
PcbLayerStackReadModelBuilder.#stripUndefined({
|
|
192
216
|
index: index + 1,
|
|
193
217
|
layerId: layer.layerId,
|
|
@@ -196,6 +220,7 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
196
220
|
: undefined,
|
|
197
221
|
name: layer.name,
|
|
198
222
|
kind: layer.kind || layer.role,
|
|
223
|
+
nativeOrderIndex: nativeOrderByLayerId.get(layer.layerId),
|
|
199
224
|
...PcbLayerStackSourceMetadataParser.layerSourceFields(
|
|
200
225
|
fields,
|
|
201
226
|
index + 1
|
|
@@ -204,6 +229,116 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
204
229
|
)
|
|
205
230
|
}
|
|
206
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Parses the native layer linked-list order stored in Board6 records.
|
|
234
|
+
* @param {Record<string, string | string[]>} fields Board fields.
|
|
235
|
+
* @param {object[]} primitiveLayers Primitive layer fallbacks.
|
|
236
|
+
* @returns {{ source: string, layerIds: number[], complete: boolean } | null}
|
|
237
|
+
*/
|
|
238
|
+
static #nativeLayerOrder(fields, primitiveLayers) {
|
|
239
|
+
const layerIds = new Set(
|
|
240
|
+
(primitiveLayers || [])
|
|
241
|
+
.map((layer) => Number(layer.layerId))
|
|
242
|
+
.filter(Number.isFinite)
|
|
243
|
+
)
|
|
244
|
+
if (!layerIds.size) return null
|
|
245
|
+
|
|
246
|
+
const links = PcbLayerStackReadModelBuilder.#nativeLayerLinks(fields)
|
|
247
|
+
const firstLayerId = [...links.entries()].find(
|
|
248
|
+
([, link]) => link.prev === 0 && link.next !== 0
|
|
249
|
+
)?.[0]
|
|
250
|
+
if (!firstLayerId) return null
|
|
251
|
+
|
|
252
|
+
const orderedLayerIds = []
|
|
253
|
+
const visited = new Set()
|
|
254
|
+
let currentLayerId = firstLayerId
|
|
255
|
+
|
|
256
|
+
while (
|
|
257
|
+
Number.isFinite(currentLayerId) &&
|
|
258
|
+
currentLayerId !== 0 &&
|
|
259
|
+
!visited.has(currentLayerId)
|
|
260
|
+
) {
|
|
261
|
+
visited.add(currentLayerId)
|
|
262
|
+
|
|
263
|
+
if (layerIds.has(currentLayerId)) {
|
|
264
|
+
orderedLayerIds.push(currentLayerId)
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const nextLayerId = links.get(currentLayerId)?.next || 0
|
|
268
|
+
currentLayerId = Number(nextLayerId)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (!orderedLayerIds.length) return null
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
source: 'Board6/Data',
|
|
275
|
+
layerIds: orderedLayerIds,
|
|
276
|
+
complete: orderedLayerIds.length === layerIds.size
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Builds native layer link rows from Board6 LAYERnNEXT/PREV fields.
|
|
282
|
+
* @param {Record<string, string | string[]>} fields Board fields.
|
|
283
|
+
* @returns {Map<number, { next: number, prev: number }>}
|
|
284
|
+
*/
|
|
285
|
+
static #nativeLayerLinks(fields) {
|
|
286
|
+
const layerIndexes = PcbLayerStackReadModelBuilder.#indexedRows(
|
|
287
|
+
fields,
|
|
288
|
+
[/^LAYER(\d+)NEXT$/iu, /^LAYER(\d+)PREV$/iu]
|
|
289
|
+
)
|
|
290
|
+
const links = new Map()
|
|
291
|
+
|
|
292
|
+
for (const layerIndex of layerIndexes) {
|
|
293
|
+
const next = Number(
|
|
294
|
+
PcbLayerStackReadModelBuilder.#field(
|
|
295
|
+
fields,
|
|
296
|
+
'LAYER' + layerIndex + 'NEXT'
|
|
297
|
+
) || 0
|
|
298
|
+
)
|
|
299
|
+
const prev = Number(
|
|
300
|
+
PcbLayerStackReadModelBuilder.#field(
|
|
301
|
+
fields,
|
|
302
|
+
'LAYER' + layerIndex + 'PREV'
|
|
303
|
+
) || 0
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
if (Number.isFinite(next) || Number.isFinite(prev)) {
|
|
307
|
+
links.set(layerIndex, {
|
|
308
|
+
next: Number.isFinite(next) ? next : 0,
|
|
309
|
+
prev: Number.isFinite(prev) ? prev : 0
|
|
310
|
+
})
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return links
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Orders primitive fallback layers with native linked-list evidence first.
|
|
319
|
+
* @param {object[]} primitiveLayers Primitive layer rows.
|
|
320
|
+
* @param {{ layerIds: number[] } | null} nativeLayerOrder Native order.
|
|
321
|
+
* @returns {object[]}
|
|
322
|
+
*/
|
|
323
|
+
static #orderPrimitiveLayers(primitiveLayers, nativeLayerOrder) {
|
|
324
|
+
if (!nativeLayerOrder?.layerIds?.length) return primitiveLayers
|
|
325
|
+
|
|
326
|
+
const layerById = new Map(
|
|
327
|
+
primitiveLayers
|
|
328
|
+
.filter((layer) => Number.isFinite(Number(layer.layerId)))
|
|
329
|
+
.map((layer) => [Number(layer.layerId), layer])
|
|
330
|
+
)
|
|
331
|
+
const orderedLayers = nativeLayerOrder.layerIds
|
|
332
|
+
.map((layerId) => layerById.get(layerId))
|
|
333
|
+
.filter(Boolean)
|
|
334
|
+
const orderedIds = new Set(nativeLayerOrder.layerIds)
|
|
335
|
+
const remainingLayers = primitiveLayers.filter(
|
|
336
|
+
(layer) => !orderedIds.has(Number(layer.layerId))
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
return [...orderedLayers, ...remainingLayers]
|
|
340
|
+
}
|
|
341
|
+
|
|
207
342
|
/**
|
|
208
343
|
* Normalizes substacks and links them to board-region rows.
|
|
209
344
|
* @param {object[]} layerSubstacks Parsed substacks.
|
|
@@ -20,7 +20,7 @@ export class PcbLibModelParser {
|
|
|
20
20
|
/**
|
|
21
21
|
* Parses one extracted PcbLib into a normalized read-only model.
|
|
22
22
|
* @param {string} fileName
|
|
23
|
-
* @param {{ libraryHeader?: Record<string, string>, componentParamsToc?: Record<string, object>, sectionKeys?: Record<string, string>, footprints?: object[], embeddedFonts?: { fonts?: object[] }, nativeStreams?: object, streamNames?: string[], diagnostics?: Record<string, number> } | null} extraction
|
|
23
|
+
* @param {{ libraryHeader?: Record<string, string>, componentParamsToc?: Record<string, object>, sectionKeys?: Record<string, string>, footprints?: object[], embeddedFonts?: { fonts?: object[] }, nativeStreams?: object, streamNames?: string[], diagnostics?: Record<string, number | object[]> } | null} extraction
|
|
24
24
|
* @returns {{ schema: string, kind: 'pcb-library', fileType: 'PcbLib', fileName: string, summary: Record<string, number | string>, diagnostics: { severity: 'info' | 'warning', message: string }[], pcbLibrary: { libraryHeader: Record<string, string>, componentParamsToc: Record<string, object>, sectionKeys: Record<string, string>, footprints: object[], embeddedFonts: object[] }, bom: [] }}
|
|
25
25
|
*/
|
|
26
26
|
static parse(fileName, extraction) {
|
|
@@ -72,6 +72,7 @@ export class PcbLibModelParser {
|
|
|
72
72
|
embeddedFonts,
|
|
73
73
|
embeddedModels,
|
|
74
74
|
componentBodies,
|
|
75
|
+
...PcbLibModelParser.#storageDiagnostics(safeExtraction),
|
|
75
76
|
...(safeExtraction.nativeStreams
|
|
76
77
|
? { nativeStreams: safeExtraction.nativeStreams }
|
|
77
78
|
: {}),
|
|
@@ -141,7 +142,7 @@ export class PcbLibModelParser {
|
|
|
141
142
|
* @param {object[]} footprints
|
|
142
143
|
* @param {object[]} embeddedFonts
|
|
143
144
|
* @param {object[]} embeddedModels
|
|
144
|
-
* @param {{ streamNames?: string[], diagnostics?: Record<string, number> }} extraction
|
|
145
|
+
* @param {{ streamNames?: string[], diagnostics?: Record<string, number | object[]> }} extraction
|
|
145
146
|
* @returns {{ severity: 'info' | 'warning', message: string }[]}
|
|
146
147
|
*/
|
|
147
148
|
static #buildDiagnostics(
|
|
@@ -203,6 +204,27 @@ export class PcbLibModelParser {
|
|
|
203
204
|
return diagnostics
|
|
204
205
|
}
|
|
205
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Builds storage-level diagnostics for the public PCB library model.
|
|
209
|
+
* @param {{ diagnostics?: Record<string, number | object[]> }} extraction Extracted PcbLib data.
|
|
210
|
+
* @returns {{ storageDiagnostics?: object }}
|
|
211
|
+
*/
|
|
212
|
+
static #storageDiagnostics(extraction) {
|
|
213
|
+
const missingFootprints = Array.isArray(
|
|
214
|
+
extraction?.diagnostics?.missingFootprints
|
|
215
|
+
)
|
|
216
|
+
? extraction.diagnostics.missingFootprints
|
|
217
|
+
: []
|
|
218
|
+
|
|
219
|
+
return missingFootprints.length
|
|
220
|
+
? {
|
|
221
|
+
storageDiagnostics: {
|
|
222
|
+
missingFootprints
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
: {}
|
|
226
|
+
}
|
|
227
|
+
|
|
206
228
|
/**
|
|
207
229
|
* Normalizes one extracted footprint object with stable primitive arrays.
|
|
208
230
|
* @param {object} footprint
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves declared PcbLib footprint names to compound-document storage names.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbLibStorageNameResolver {
|
|
9
|
+
/**
|
|
10
|
+
* Resolves all declared footprint storage names.
|
|
11
|
+
* @param {Map<string, Uint8Array>} streams PcbLib compound streams.
|
|
12
|
+
* @param {string[]} footprintNames Declared footprint names.
|
|
13
|
+
* @param {Record<string, string>} sectionKeys Section-key mapping.
|
|
14
|
+
* @returns {{ resolvedFootprints: { name: string, storageName: string }[], missingFootprints: object[] }}
|
|
15
|
+
*/
|
|
16
|
+
static resolveFootprints(streams, footprintNames, sectionKeys) {
|
|
17
|
+
const resolvedFootprints = []
|
|
18
|
+
const missingFootprints = []
|
|
19
|
+
|
|
20
|
+
for (const name of footprintNames || []) {
|
|
21
|
+
const candidates = PcbLibStorageNameResolver.#storageCandidates(
|
|
22
|
+
name,
|
|
23
|
+
sectionKeys
|
|
24
|
+
)
|
|
25
|
+
const storageName = PcbLibStorageNameResolver.#resolveStorageName(
|
|
26
|
+
streams,
|
|
27
|
+
candidates
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
if (!storageName) {
|
|
31
|
+
missingFootprints.push({
|
|
32
|
+
footprintName: name,
|
|
33
|
+
candidates,
|
|
34
|
+
reason: 'no matching footprint Data stream'
|
|
35
|
+
})
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
resolvedFootprints.push({ name, storageName })
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return { resolvedFootprints, missingFootprints }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Parses an optional SectionKeys stream that maps full footprint names to
|
|
47
|
+
* shortened OLE storage names.
|
|
48
|
+
* @param {Uint8Array | undefined} bytes SectionKeys stream bytes.
|
|
49
|
+
* @returns {Record<string, string>}
|
|
50
|
+
*/
|
|
51
|
+
static parseSectionKeys(bytes) {
|
|
52
|
+
if (!bytes || bytes.byteLength < 4) {
|
|
53
|
+
return {}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const count = PcbLibStorageNameResolver.#readUint32(bytes, 0)
|
|
57
|
+
const entries = {}
|
|
58
|
+
let offset = 4
|
|
59
|
+
|
|
60
|
+
for (let index = 0; index < count; index += 1) {
|
|
61
|
+
const fullName = PcbLibStorageNameResolver.#readStringBlockAt(
|
|
62
|
+
bytes,
|
|
63
|
+
offset
|
|
64
|
+
)
|
|
65
|
+
if (!fullName) break
|
|
66
|
+
offset = fullName.nextOffset
|
|
67
|
+
|
|
68
|
+
const storageName = PcbLibStorageNameResolver.#readStringBlockAt(
|
|
69
|
+
bytes,
|
|
70
|
+
offset
|
|
71
|
+
)
|
|
72
|
+
if (!storageName) break
|
|
73
|
+
offset = storageName.nextOffset
|
|
74
|
+
entries[fullName.text] = storageName.text
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return entries
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Builds candidate OLE storage names for one declared footprint.
|
|
82
|
+
* @param {string} footprintName Declared footprint name.
|
|
83
|
+
* @param {Record<string, string>} sectionKeys Section-key mapping.
|
|
84
|
+
* @returns {string[]}
|
|
85
|
+
*/
|
|
86
|
+
static #storageCandidates(footprintName, sectionKeys) {
|
|
87
|
+
return [
|
|
88
|
+
footprintName,
|
|
89
|
+
PcbLibStorageNameResolver.#sanitizeStorageName(footprintName),
|
|
90
|
+
sectionKeys[footprintName],
|
|
91
|
+
PcbLibStorageNameResolver.#sanitizeStorageName(
|
|
92
|
+
footprintName.slice(0, 31)
|
|
93
|
+
)
|
|
94
|
+
]
|
|
95
|
+
.filter(Boolean)
|
|
96
|
+
.filter(
|
|
97
|
+
(candidate, index, candidates) =>
|
|
98
|
+
candidates.indexOf(candidate) === index
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Resolves the first candidate with a matching Data stream.
|
|
104
|
+
* @param {Map<string, Uint8Array>} streams PcbLib compound streams.
|
|
105
|
+
* @param {string[]} candidates Candidate storage names.
|
|
106
|
+
* @returns {string}
|
|
107
|
+
*/
|
|
108
|
+
static #resolveStorageName(streams, candidates) {
|
|
109
|
+
return (
|
|
110
|
+
candidates.find((candidate) => streams.has(candidate + '/Data')) ||
|
|
111
|
+
''
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Sanitizes one footprint name for legacy OLE storage lookup.
|
|
117
|
+
* @param {string} name Footprint name.
|
|
118
|
+
* @returns {string}
|
|
119
|
+
*/
|
|
120
|
+
static #sanitizeStorageName(name) {
|
|
121
|
+
return String(name || '')
|
|
122
|
+
.replace(/[<>:"/\\|?*\x00-\x1f]/gu, '_')
|
|
123
|
+
.slice(0, 31)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Reads a little-endian unsigned 32-bit value from one byte array.
|
|
128
|
+
* @param {Uint8Array} bytes Source bytes.
|
|
129
|
+
* @param {number} offset Byte offset.
|
|
130
|
+
* @returns {number}
|
|
131
|
+
*/
|
|
132
|
+
static #readUint32(bytes, offset) {
|
|
133
|
+
if (!bytes || offset + 4 > bytes.byteLength) return 0
|
|
134
|
+
|
|
135
|
+
return new DataView(
|
|
136
|
+
bytes.buffer,
|
|
137
|
+
bytes.byteOffset,
|
|
138
|
+
bytes.byteLength
|
|
139
|
+
).getUint32(offset, true)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Reads one Pascal-style PCB string block from an offset.
|
|
144
|
+
* @param {Uint8Array} bytes Source bytes.
|
|
145
|
+
* @param {number} offset Byte offset.
|
|
146
|
+
* @returns {{ text: string, nextOffset: number } | null}
|
|
147
|
+
*/
|
|
148
|
+
static #readStringBlockAt(bytes, offset) {
|
|
149
|
+
if (!bytes || offset + 5 > bytes.byteLength) return null
|
|
150
|
+
|
|
151
|
+
const byteLength = PcbLibStorageNameResolver.#readUint32(bytes, offset)
|
|
152
|
+
const stringLength = bytes[offset + 4]
|
|
153
|
+
const textOffset = offset + 5
|
|
154
|
+
const nextOffset = offset + 4 + byteLength
|
|
155
|
+
|
|
156
|
+
if (
|
|
157
|
+
byteLength < 1 ||
|
|
158
|
+
textOffset + stringLength > bytes.byteLength ||
|
|
159
|
+
nextOffset > bytes.byteLength
|
|
160
|
+
) {
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
text: new TextDecoder('utf-8').decode(
|
|
166
|
+
bytes.subarray(textOffset, textOffset + stringLength)
|
|
167
|
+
),
|
|
168
|
+
nextOffset
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -7,6 +7,7 @@ import { PcbCustomPadShapeParser } from './PcbCustomPadShapeParser.mjs'
|
|
|
7
7
|
import { PcbEmbeddedFontExtractor } from './PcbEmbeddedFontExtractor.mjs'
|
|
8
8
|
import { PcbEmbeddedModelExtractor } from './PcbEmbeddedModelExtractor.mjs'
|
|
9
9
|
import { PcbExtendedPrimitiveInformationParser } from './PcbExtendedPrimitiveInformationParser.mjs'
|
|
10
|
+
import { PcbLibStorageNameResolver } from './PcbLibStorageNameResolver.mjs'
|
|
10
11
|
import { NativeStreamInventoryBuilder } from './NativeStreamInventoryBuilder.mjs'
|
|
11
12
|
import { PcbRawRecordRegistry } from './PcbRawRecordRegistry.mjs'
|
|
12
13
|
import { OleCompoundDocument } from '../ole/OleCompoundDocument.mjs'
|
|
@@ -119,7 +120,7 @@ export class PcbLibStreamExtractor {
|
|
|
119
120
|
/**
|
|
120
121
|
* Extracts all declared footprints from one PcbLib stream map.
|
|
121
122
|
* @param {Map<string, Uint8Array>} streams
|
|
122
|
-
* @returns {{ libraryHeader: Record<string, string>, componentParamsToc: Record<string, object>, sectionKeys: Record<string, string>, footprints: object[], streamNames: string[], diagnostics: Record<string, number> }}
|
|
123
|
+
* @returns {{ libraryHeader: Record<string, string>, componentParamsToc: Record<string, object>, sectionKeys: Record<string, string>, footprints: object[], streamNames: string[], diagnostics: Record<string, number | object[]> }}
|
|
123
124
|
*/
|
|
124
125
|
static extractFromStreams(streams) {
|
|
125
126
|
const libraryData = streams.get('Library/Data') || new Uint8Array()
|
|
@@ -129,30 +130,24 @@ export class PcbLibStreamExtractor {
|
|
|
129
130
|
PcbLibStreamExtractor.#parseComponentParamsToc(
|
|
130
131
|
streams.get('Library/ComponentParamsTOC/Data')
|
|
131
132
|
)
|
|
132
|
-
const sectionKeys =
|
|
133
|
+
const sectionKeys = PcbLibStorageNameResolver.parseSectionKeys(
|
|
133
134
|
streams.get('SectionKeys')
|
|
134
135
|
)
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if (!resolvedStorage) {
|
|
144
|
-
return []
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return [
|
|
136
|
+
const storageResolution = PcbLibStorageNameResolver.resolveFootprints(
|
|
137
|
+
streams,
|
|
138
|
+
parsedLibraryData.footprintNames,
|
|
139
|
+
sectionKeys
|
|
140
|
+
)
|
|
141
|
+
const footprints = storageResolution.resolvedFootprints.map(
|
|
142
|
+
({ name, storageName }) =>
|
|
148
143
|
PcbLibStreamExtractor.#extractFootprint(
|
|
149
144
|
streams,
|
|
150
145
|
name,
|
|
151
|
-
|
|
146
|
+
storageName,
|
|
152
147
|
componentParamsToc[name] || {}
|
|
153
148
|
)
|
|
154
|
-
|
|
155
|
-
|
|
149
|
+
)
|
|
150
|
+
const missingFootprints = storageResolution.missingFootprints
|
|
156
151
|
const primitiveCount = footprints.reduce(
|
|
157
152
|
(sum, footprint) => sum + footprint.primitiveCount,
|
|
158
153
|
0
|
|
@@ -198,8 +193,8 @@ export class PcbLibStreamExtractor {
|
|
|
198
193
|
embeddedFontCount: embeddedFonts.fonts.length,
|
|
199
194
|
embeddedModelCount: embeddedModels.models.length,
|
|
200
195
|
componentBodyCount: embeddedModels.componentBodies.length,
|
|
201
|
-
missingFootprintCount:
|
|
202
|
-
|
|
196
|
+
missingFootprintCount: missingFootprints.length,
|
|
197
|
+
...(missingFootprints.length ? { missingFootprints } : {})
|
|
203
198
|
}
|
|
204
199
|
}
|
|
205
200
|
}
|
|
@@ -280,68 +275,6 @@ export class PcbLibStreamExtractor {
|
|
|
280
275
|
return entries
|
|
281
276
|
}
|
|
282
277
|
|
|
283
|
-
/**
|
|
284
|
-
* Parses an optional SectionKeys stream that maps full footprint names to
|
|
285
|
-
* shortened OLE storage names.
|
|
286
|
-
* @param {Uint8Array | undefined} bytes
|
|
287
|
-
* @returns {Record<string, string>}
|
|
288
|
-
*/
|
|
289
|
-
static #parseSectionKeys(bytes) {
|
|
290
|
-
if (!bytes || bytes.byteLength < 4) {
|
|
291
|
-
return {}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
const count = PcbLibStreamExtractor.#readUint32(bytes, 0)
|
|
295
|
-
const entries = {}
|
|
296
|
-
let offset = 4
|
|
297
|
-
|
|
298
|
-
for (let index = 0; index < count; index += 1) {
|
|
299
|
-
const fullName = PcbLibStreamExtractor.#readStringBlockAt(
|
|
300
|
-
bytes,
|
|
301
|
-
offset
|
|
302
|
-
)
|
|
303
|
-
if (!fullName) {
|
|
304
|
-
break
|
|
305
|
-
}
|
|
306
|
-
offset = fullName.nextOffset
|
|
307
|
-
|
|
308
|
-
const storageName = PcbLibStreamExtractor.#readStringBlockAt(
|
|
309
|
-
bytes,
|
|
310
|
-
offset
|
|
311
|
-
)
|
|
312
|
-
if (!storageName) {
|
|
313
|
-
break
|
|
314
|
-
}
|
|
315
|
-
offset = storageName.nextOffset
|
|
316
|
-
entries[fullName.text] = storageName.text
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return entries
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Resolves the OLE storage name for one declared footprint.
|
|
324
|
-
* @param {Map<string, Uint8Array>} streams
|
|
325
|
-
* @param {string} footprintName
|
|
326
|
-
* @param {Record<string, string>} sectionKeys
|
|
327
|
-
* @returns {string}
|
|
328
|
-
*/
|
|
329
|
-
static #resolveFootprintStorageName(streams, footprintName, sectionKeys) {
|
|
330
|
-
const candidates = [
|
|
331
|
-
footprintName,
|
|
332
|
-
PcbLibStreamExtractor.#sanitizeStorageName(footprintName),
|
|
333
|
-
sectionKeys[footprintName],
|
|
334
|
-
PcbLibStreamExtractor.#sanitizeStorageName(
|
|
335
|
-
footprintName.slice(0, 31)
|
|
336
|
-
)
|
|
337
|
-
].filter(Boolean)
|
|
338
|
-
|
|
339
|
-
return (
|
|
340
|
-
candidates.find((candidate) => streams.has(candidate + '/Data')) ||
|
|
341
|
-
''
|
|
342
|
-
)
|
|
343
|
-
}
|
|
344
|
-
|
|
345
278
|
/**
|
|
346
279
|
* Extracts one footprint storage into normalized primitive lists.
|
|
347
280
|
* @param {Map<string, Uint8Array>} streams
|
|
@@ -1042,17 +975,6 @@ export class PcbLibStreamExtractor {
|
|
|
1042
975
|
})
|
|
1043
976
|
}
|
|
1044
977
|
|
|
1045
|
-
/**
|
|
1046
|
-
* Sanitizes one footprint name for legacy OLE storage lookup.
|
|
1047
|
-
* @param {string} name
|
|
1048
|
-
* @returns {string}
|
|
1049
|
-
*/
|
|
1050
|
-
static #sanitizeStorageName(name) {
|
|
1051
|
-
return String(name || '')
|
|
1052
|
-
.replace(/[/:\\]/gu, '_')
|
|
1053
|
-
.slice(0, 31)
|
|
1054
|
-
}
|
|
1055
|
-
|
|
1056
978
|
/**
|
|
1057
979
|
* Reads a little-endian unsigned 32-bit value from one byte array.
|
|
1058
980
|
* @param {Uint8Array} bytes
|