altium-toolkit 1.0.7 → 1.0.9

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.
Files changed (93) hide show
  1. package/README.md +18 -6
  2. package/docs/api.md +78 -16
  3. package/docs/model-format.md +229 -8
  4. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +47 -0
  5. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1661 -104
  6. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +59 -0
  7. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +57 -0
  8. package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
  9. package/docs/testing.md +9 -3
  10. package/package.json +1 -1
  11. package/spec/library-scope.md +7 -1
  12. package/src/core/altium/AltiumLayoutParser.mjs +104 -8
  13. package/src/core/altium/AltiumParser.mjs +191 -45
  14. package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
  15. package/src/core/altium/IntLibModelParser.mjs +240 -0
  16. package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
  17. package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
  18. package/src/core/altium/LibrarySearchIndex.mjs +215 -0
  19. package/src/core/altium/NormalizedModelSchema.mjs +36 -0
  20. package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
  21. package/src/core/altium/PcbDefaultsParser.mjs +171 -0
  22. package/src/core/altium/PcbDimensionParser.mjs +229 -0
  23. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
  24. package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
  25. package/src/core/altium/PcbLibModelParser.mjs +235 -14
  26. package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
  27. package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
  28. package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
  29. package/src/core/altium/PcbModelParser.mjs +466 -28
  30. package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
  31. package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
  32. package/src/core/altium/PcbPadStackParser.mjs +58 -0
  33. package/src/core/altium/PcbPickPlacePositionResolver.mjs +217 -0
  34. package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
  35. package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
  36. package/src/core/altium/PcbRegionPrimitiveParser.mjs +5 -1
  37. package/src/core/altium/PcbRuleParser.mjs +354 -33
  38. package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
  39. package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
  40. package/src/core/altium/PcbStatisticsBuilder.mjs +532 -0
  41. package/src/core/altium/PcbStreamExtractor.mjs +111 -4
  42. package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
  43. package/src/core/altium/PcbUnionParser.mjs +307 -0
  44. package/src/core/altium/PcbViaStackParser.mjs +98 -10
  45. package/src/core/altium/PcbViaStructureParser.mjs +335 -0
  46. package/src/core/altium/PrintableTextDecoder.mjs +53 -3
  47. package/src/core/altium/PrjPcbModelParser.mjs +257 -5
  48. package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
  49. package/src/core/altium/ProjectDesignBundleBuilder.mjs +477 -0
  50. package/src/core/altium/ProjectNetlistExporter.mjs +499 -0
  51. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
  52. package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
  53. package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
  54. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
  55. package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
  56. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
  57. package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
  58. package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
  59. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
  60. package/src/core/altium/SchematicHarnessParser.mjs +302 -0
  61. package/src/core/altium/SchematicImageParser.mjs +474 -3
  62. package/src/core/altium/SchematicImplementationParser.mjs +518 -0
  63. package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
  64. package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
  65. package/src/core/altium/SchematicPinParser.mjs +84 -1
  66. package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
  67. package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
  68. package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
  69. package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
  70. package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
  71. package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
  72. package/src/core/altium/SchematicTemplateParser.mjs +256 -0
  73. package/src/core/altium/SchematicTextParser.mjs +123 -0
  74. package/src/core/ole/OleCompoundDocument.mjs +20 -0
  75. package/src/parser.mjs +29 -0
  76. package/src/renderers.mjs +3 -0
  77. package/src/styles/altium-renderers.css +25 -0
  78. package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
  79. package/src/ui/PcbInteractionGeometry.mjs +350 -0
  80. package/src/ui/PcbInteractionIndex.mjs +593 -0
  81. package/src/ui/PcbInteractionItemRegistry.mjs +66 -0
  82. package/src/ui/PcbInteractionLayerModel.mjs +99 -0
  83. package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +74 -9
  84. package/src/ui/PcbScene3dBuilder.mjs +169 -7
  85. package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
  86. package/src/ui/PcbSvgRenderer.mjs +1187 -34
  87. package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
  88. package/src/ui/SchematicNoteRenderer.mjs +9 -2
  89. package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
  90. package/src/ui/SchematicShapeRenderer.mjs +362 -0
  91. package/src/ui/SchematicSvgRenderer.mjs +1442 -92
  92. package/src/ui/SchematicTypography.mjs +48 -5
  93. package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
@@ -4,6 +4,11 @@
4
4
 
5
5
  import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
6
6
  import { ParserUtils } from './ParserUtils.mjs'
7
+ import { LibraryRenderManifestBuilder } from './LibraryRenderManifestBuilder.mjs'
8
+ import { PcbCustomPadShapeParser } from './PcbCustomPadShapeParser.mjs'
9
+ import { PcbDefaultsParser } from './PcbDefaultsParser.mjs'
10
+ import { PcbExtendedPrimitiveInformationParser } from './PcbExtendedPrimitiveInformationParser.mjs'
11
+ import { PcbMaskPasteResolver } from './PcbMaskPasteResolver.mjs'
7
12
 
8
13
  const { stripExtension } = ParserUtils
9
14
 
@@ -19,24 +24,56 @@ export class PcbLibModelParser {
19
24
  */
20
25
  static parse(fileName, extraction) {
21
26
  const safeExtraction = extraction || {}
27
+ const embeddedModels = Array.isArray(
28
+ safeExtraction.embeddedModels?.models
29
+ )
30
+ ? safeExtraction.embeddedModels.models
31
+ : []
32
+ const componentBodies = Array.isArray(
33
+ safeExtraction.embeddedModels?.componentBodies
34
+ )
35
+ ? safeExtraction.embeddedModels.componentBodies
36
+ : []
22
37
  const footprints = Array.isArray(safeExtraction.footprints)
23
38
  ? safeExtraction.footprints.map((footprint) =>
24
- PcbLibModelParser.#normalizeFootprint(footprint)
39
+ PcbLibModelParser.#normalizeFootprint(
40
+ footprint,
41
+ embeddedModels
42
+ )
25
43
  )
26
44
  : []
27
45
  const embeddedFonts = Array.isArray(safeExtraction.embeddedFonts?.fonts)
28
46
  ? safeExtraction.embeddedFonts.fonts
29
47
  : []
48
+ const defaults = PcbDefaultsParser.parse(
49
+ safeExtraction.libraryHeader || {},
50
+ 'pcb-library'
51
+ )
30
52
  const summary = PcbLibModelParser.#buildSummary(
31
53
  fileName,
32
54
  footprints,
33
- embeddedFonts
55
+ embeddedFonts,
56
+ embeddedModels
34
57
  )
35
58
  const diagnostics = PcbLibModelParser.#buildDiagnostics(
36
59
  footprints,
37
60
  embeddedFonts,
61
+ embeddedModels,
38
62
  safeExtraction
39
63
  )
64
+ const pcbLibrary = {
65
+ libraryHeader: safeExtraction.libraryHeader || {},
66
+ componentParamsToc: safeExtraction.componentParamsToc || {},
67
+ sectionKeys: safeExtraction.sectionKeys || {},
68
+ footprints,
69
+ indexes: PcbLibModelParser.#buildIndexes(footprints),
70
+ embeddedFonts,
71
+ embeddedModels,
72
+ componentBodies,
73
+ ...(defaults ? { defaults } : {})
74
+ }
75
+ pcbLibrary.renderManifest =
76
+ LibraryRenderManifestBuilder.buildPcbLibraryManifest(pcbLibrary)
40
77
 
41
78
  return NormalizedModelSchema.attach({
42
79
  kind: 'pcb-library',
@@ -44,13 +81,7 @@ export class PcbLibModelParser {
44
81
  fileName,
45
82
  summary,
46
83
  diagnostics,
47
- pcbLibrary: {
48
- libraryHeader: safeExtraction.libraryHeader || {},
49
- componentParamsToc: safeExtraction.componentParamsToc || {},
50
- sectionKeys: safeExtraction.sectionKeys || {},
51
- footprints,
52
- embeddedFonts
53
- },
84
+ pcbLibrary,
54
85
  bom: []
55
86
  })
56
87
  }
@@ -60,9 +91,10 @@ export class PcbLibModelParser {
60
91
  * @param {string} fileName
61
92
  * @param {object[]} footprints
62
93
  * @param {object[]} embeddedFonts
94
+ * @param {object[]} embeddedModels
63
95
  * @returns {Record<string, number | string>}
64
96
  */
65
- static #buildSummary(fileName, footprints, embeddedFonts) {
97
+ static #buildSummary(fileName, footprints, embeddedFonts, embeddedModels) {
66
98
  return {
67
99
  title: stripExtension(fileName),
68
100
  footprintCount: footprints.length,
@@ -81,7 +113,8 @@ export class PcbLibModelParser {
81
113
  footprints,
82
114
  'rawRecords'
83
115
  ),
84
- embeddedFontCount: embeddedFonts.length
116
+ embeddedFontCount: embeddedFonts.length,
117
+ embeddedModelCount: embeddedModels.length
85
118
  }
86
119
  }
87
120
 
@@ -89,10 +122,16 @@ export class PcbLibModelParser {
89
122
  * Builds parser diagnostics from extraction metadata.
90
123
  * @param {object[]} footprints
91
124
  * @param {object[]} embeddedFonts
125
+ * @param {object[]} embeddedModels
92
126
  * @param {{ streamNames?: string[], diagnostics?: Record<string, number> }} extraction
93
127
  * @returns {{ severity: 'info' | 'warning', message: string }[]}
94
128
  */
95
- static #buildDiagnostics(footprints, embeddedFonts, extraction) {
129
+ static #buildDiagnostics(
130
+ footprints,
131
+ embeddedFonts,
132
+ embeddedModels,
133
+ extraction
134
+ ) {
96
135
  const diagnostics = [
97
136
  {
98
137
  severity: 'info',
@@ -123,6 +162,16 @@ export class PcbLibModelParser {
123
162
  })
124
163
  }
125
164
 
165
+ if (embeddedModels.length) {
166
+ diagnostics.push({
167
+ severity: 'info',
168
+ message:
169
+ 'Recovered ' +
170
+ embeddedModels.length +
171
+ ' embedded PcbLib model payloads.'
172
+ })
173
+ }
174
+
126
175
  if (extraction.diagnostics?.missingFootprintCount) {
127
176
  diagnostics.push({
128
177
  severity: 'warning',
@@ -139,9 +188,10 @@ export class PcbLibModelParser {
139
188
  /**
140
189
  * Normalizes one extracted footprint object with stable primitive arrays.
141
190
  * @param {object} footprint
191
+ * @param {object[]} libraryEmbeddedModels Library-level embedded models.
142
192
  * @returns {object}
143
193
  */
144
- static #normalizeFootprint(footprint) {
194
+ static #normalizeFootprint(footprint, libraryEmbeddedModels = []) {
145
195
  const normalized = {
146
196
  name: String(footprint.name || ''),
147
197
  dataName: String(footprint.dataName || footprint.name || ''),
@@ -158,6 +208,13 @@ export class PcbLibModelParser {
158
208
  unknownRecords: Array.isArray(footprint.unknownRecords)
159
209
  ? footprint.unknownRecords
160
210
  : [],
211
+ implementations: PcbLibModelParser.#array(
212
+ footprint.implementations
213
+ ),
214
+ componentModels: PcbLibModelParser.#array(
215
+ footprint.componentModels
216
+ ),
217
+ pinDisplayModes: footprint.pinDisplayModes || {},
161
218
  rawRecords: PcbLibModelParser.#array(footprint.rawRecords),
162
219
  pads: PcbLibModelParser.#array(footprint.pads),
163
220
  tracks: PcbLibModelParser.#array(footprint.tracks),
@@ -165,8 +222,51 @@ export class PcbLibModelParser {
165
222
  vias: PcbLibModelParser.#array(footprint.vias),
166
223
  fills: PcbLibModelParser.#array(footprint.fills),
167
224
  texts: PcbLibModelParser.#array(footprint.texts),
168
- regions: PcbLibModelParser.#array(footprint.regions)
225
+ regions: PcbLibModelParser.#array(footprint.regions),
226
+ shapeBasedRegions: PcbLibModelParser.#array(
227
+ footprint.shapeBasedRegions
228
+ ),
229
+ extendedPrimitiveInformation:
230
+ footprint.extendedPrimitiveInformation || {
231
+ entries: [],
232
+ byPrimitiveIndex: {},
233
+ byPrimitiveKey: {}
234
+ },
235
+ customPadShapes: footprint.customPadShapes || {
236
+ entries: [],
237
+ byPrimitiveIndex: {}
238
+ },
239
+ embeddedModels: PcbLibModelParser.#array(footprint.embeddedModels),
240
+ componentBodies: PcbLibModelParser.#array(footprint.componentBodies)
169
241
  }
242
+ normalized.defaults = PcbDefaultsParser.parse(
243
+ {
244
+ ...(footprint.defaults || {}),
245
+ ...(footprint.parameters || {})
246
+ },
247
+ 'pcb-library-footprint'
248
+ )
249
+ PcbExtendedPrimitiveInformationParser.attachToPrimitives(
250
+ normalized,
251
+ normalized.extendedPrimitiveInformation
252
+ )
253
+ PcbCustomPadShapeParser.attachToPads(
254
+ normalized.pads,
255
+ normalized.customPadShapes,
256
+ normalized
257
+ )
258
+ normalized.componentBodies = PcbLibModelParser.#annotateComponentBodies(
259
+ normalized.componentBodies,
260
+ normalized.embeddedModels.length
261
+ ? normalized.embeddedModels
262
+ : libraryEmbeddedModels
263
+ )
264
+ normalized.maskPaste = PcbMaskPasteResolver.build({
265
+ pads: normalized.pads,
266
+ vias: normalized.vias,
267
+ rules: footprint.rules || [],
268
+ defaults: normalized.defaults
269
+ })
170
270
 
171
271
  return {
172
272
  ...normalized,
@@ -176,6 +276,127 @@ export class PcbLibModelParser {
176
276
  }
177
277
  }
178
278
 
279
+ /**
280
+ * Adds deterministic projection diagnostics to footprint body records.
281
+ * @param {object[]} componentBodies Component body records.
282
+ * @param {object[]} embeddedModels Embedded model records.
283
+ * @returns {object[]}
284
+ */
285
+ static #annotateComponentBodies(componentBodies, embeddedModels) {
286
+ return componentBodies.map((componentBody) => ({
287
+ ...componentBody,
288
+ projectionDiagnostics: PcbLibModelParser.#projectionDiagnostics(
289
+ componentBody,
290
+ embeddedModels
291
+ )
292
+ }))
293
+ }
294
+
295
+ /**
296
+ * Resolves one component-body projection diagnostic.
297
+ * @param {object} componentBody Component body record.
298
+ * @param {object[]} embeddedModels Embedded model records.
299
+ * @returns {{ source: string, reason: string }}
300
+ */
301
+ static #projectionDiagnostics(componentBody, embeddedModels) {
302
+ const matchedModel = (embeddedModels || []).find(
303
+ (model) =>
304
+ PcbLibModelParser.#sameNonEmptyValue(
305
+ model?.id,
306
+ componentBody?.modelId
307
+ ) ||
308
+ PcbLibModelParser.#sameNonEmptyValue(
309
+ model?.checksum,
310
+ componentBody?.checksum
311
+ ) ||
312
+ PcbLibModelParser.#sameNonEmptyValue(
313
+ model?.name,
314
+ componentBody?.name
315
+ )
316
+ )
317
+
318
+ if (matchedModel) {
319
+ return {
320
+ source: 'embedded-model',
321
+ reason: 'matched embedded model payload'
322
+ }
323
+ }
324
+
325
+ return {
326
+ source: 'fallback',
327
+ reason: 'no embedded model payload matched this body'
328
+ }
329
+ }
330
+
331
+ /**
332
+ * Compares two values only when both are present.
333
+ * @param {unknown} left First value.
334
+ * @param {unknown} right Second value.
335
+ * @returns {boolean}
336
+ */
337
+ static #sameNonEmptyValue(left, right) {
338
+ return (
339
+ left !== null &&
340
+ left !== undefined &&
341
+ left !== '' &&
342
+ right !== null &&
343
+ right !== undefined &&
344
+ right !== '' &&
345
+ String(left) === String(right)
346
+ )
347
+ }
348
+
349
+ /**
350
+ * Builds footprint lookup indexes for library consumers.
351
+ * @param {object[]} footprints Normalized footprints.
352
+ * @returns {object}
353
+ */
354
+ static #buildIndexes(footprints) {
355
+ const footprintsByName = {}
356
+
357
+ for (const [index, footprint] of footprints.entries()) {
358
+ footprintsByName[footprint.name] =
359
+ PcbLibModelParser.#footprintIndexEntry(footprint, index)
360
+ }
361
+
362
+ return { footprintsByName }
363
+ }
364
+
365
+ /**
366
+ * Builds one footprint index entry.
367
+ * @param {object} footprint Normalized footprint.
368
+ * @param {number} index Footprint index.
369
+ * @returns {object}
370
+ */
371
+ static #footprintIndexEntry(footprint, index) {
372
+ return {
373
+ index,
374
+ name: footprint.name,
375
+ dataName: footprint.dataName,
376
+ sourceStorage: footprint.sourceStorage,
377
+ primitiveCount: footprint.primitiveCount,
378
+ padCount: footprint.pads.length,
379
+ textCount: footprint.texts.length,
380
+ keywords: PcbLibModelParser.#buildFootprintKeywords(footprint)
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Builds searchable metadata tokens for one footprint.
386
+ * @param {object} footprint Normalized footprint.
387
+ * @returns {string[]}
388
+ */
389
+ static #buildFootprintKeywords(footprint) {
390
+ return [
391
+ footprint.name,
392
+ footprint.dataName,
393
+ ...Object.values(footprint.parameters || {}),
394
+ ...Object.values(footprint.componentParams || {})
395
+ ]
396
+ .map((value) => String(value || '').trim())
397
+ .filter(Boolean)
398
+ }
399
+
179
400
  /**
180
401
  * Counts either a scalar footprint field or an array-valued family.
181
402
  * @param {object[]} footprints
@@ -3,7 +3,10 @@
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
5
  import { PcbBinaryPrimitiveParser } from './PcbBinaryPrimitiveParser.mjs'
6
+ import { PcbCustomPadShapeParser } from './PcbCustomPadShapeParser.mjs'
6
7
  import { PcbEmbeddedFontExtractor } from './PcbEmbeddedFontExtractor.mjs'
8
+ import { PcbEmbeddedModelExtractor } from './PcbEmbeddedModelExtractor.mjs'
9
+ import { PcbExtendedPrimitiveInformationParser } from './PcbExtendedPrimitiveInformationParser.mjs'
7
10
  import { PcbRawRecordRegistry } from './PcbRawRecordRegistry.mjs'
8
11
  import { OleCompoundDocument } from '../ole/OleCompoundDocument.mjs'
9
12
  import { OleConstants } from '../ole/OleConstants.mjs'
@@ -159,6 +162,8 @@ export class PcbLibStreamExtractor {
159
162
  )
160
163
  const embeddedFonts =
161
164
  PcbEmbeddedFontExtractor.extractFromStreams(streams)
165
+ const embeddedModels =
166
+ PcbEmbeddedModelExtractor.extractFromStreams(streams)
162
167
 
163
168
  return {
164
169
  libraryHeader: parsedLibraryData.libraryHeader,
@@ -168,15 +173,19 @@ export class PcbLibStreamExtractor {
168
173
  streamNames: PcbLibStreamExtractor.#collectUsedStreamNames(
169
174
  footprints,
170
175
  streams,
171
- embeddedFonts
176
+ embeddedFonts,
177
+ embeddedModels
172
178
  ),
173
179
  embeddedFonts,
180
+ embeddedModels,
174
181
  diagnostics: {
175
182
  declaredFootprintCount: parsedLibraryData.footprintNames.length,
176
183
  footprintCount: footprints.length,
177
184
  primitiveCount,
178
185
  rawRecordCount,
179
186
  embeddedFontCount: embeddedFonts.fonts.length,
187
+ embeddedModelCount: embeddedModels.models.length,
188
+ componentBodyCount: embeddedModels.componentBodies.length,
180
189
  missingFootprintCount:
181
190
  parsedLibraryData.footprintNames.length - footprints.length
182
191
  }
@@ -339,6 +348,21 @@ export class PcbLibStreamExtractor {
339
348
  const wideStrings = PcbLibStreamExtractor.#parseWideStrings(
340
349
  streams.get(storageName + '/WideStrings')
341
350
  )
351
+ const extendedPrimitiveInformation =
352
+ PcbExtendedPrimitiveInformationParser.parse(
353
+ PcbLibStreamExtractor.#firstStream(streams, [
354
+ storageName + '/ExtendedPrimitiveInformation/Data',
355
+ storageName + '/ExtendedPrimitiveInformation'
356
+ ]),
357
+ storageName + '/ExtendedPrimitiveInformation/Data'
358
+ )
359
+ const customPadShapes = PcbCustomPadShapeParser.parse(
360
+ PcbLibStreamExtractor.#firstStream(streams, [
361
+ storageName + '/CustomShapes/Data',
362
+ storageName + '/CustomShapes'
363
+ ]),
364
+ storageName + '/CustomShapes/Data'
365
+ )
342
366
  const parsedData = PcbLibStreamExtractor.#parseFootprintData(
343
367
  streams.get(storageName + '/Data') || new Uint8Array(),
344
368
  declaredPrimitiveCount,
@@ -364,10 +388,22 @@ export class PcbLibStreamExtractor {
364
388
  vias: parsedData.vias,
365
389
  fills: parsedData.fills,
366
390
  texts: parsedData.texts,
367
- regions: parsedData.regions
391
+ regions: parsedData.regions,
392
+ extendedPrimitiveInformation,
393
+ customPadShapes
368
394
  }
369
395
  }
370
396
 
397
+ /**
398
+ * Returns the first present stream from a candidate list.
399
+ * @param {Map<string, Uint8Array>} streams Stream map.
400
+ * @param {string[]} candidates Candidate stream names.
401
+ * @returns {Uint8Array | undefined}
402
+ */
403
+ static #firstStream(streams, candidates) {
404
+ return candidates.map((name) => streams.get(name)).find(Boolean)
405
+ }
406
+
371
407
  /**
372
408
  * Parses one footprint Data stream after its leading name block.
373
409
  * @param {Uint8Array} bytes
@@ -903,9 +939,15 @@ export class PcbLibStreamExtractor {
903
939
  * @param {object[]} footprints
904
940
  * @param {Map<string, Uint8Array>} streams
905
941
  * @param {{ fonts?: { sourceStream: string }[] }} embeddedFonts
942
+ * @param {{ models?: { sourceStream: string }[], componentBodies?: { sourceStream: string }[] }} embeddedModels
906
943
  * @returns {string[]}
907
944
  */
908
- static #collectUsedStreamNames(footprints, streams, embeddedFonts) {
945
+ static #collectUsedStreamNames(
946
+ footprints,
947
+ streams,
948
+ embeddedFonts,
949
+ embeddedModels
950
+ ) {
909
951
  const names = new Set()
910
952
 
911
953
  for (const baseName of [
@@ -923,7 +965,11 @@ export class PcbLibStreamExtractor {
923
965
  'Header',
924
966
  'Data',
925
967
  'Parameters',
926
- 'WideStrings'
968
+ 'WideStrings',
969
+ 'ExtendedPrimitiveInformation/Data',
970
+ 'ExtendedPrimitiveInformation',
971
+ 'CustomShapes/Data',
972
+ 'CustomShapes'
927
973
  ]) {
928
974
  const name = footprint.sourceStorage + '/' + suffix
929
975
  if (streams.has(name)) {
@@ -938,6 +984,18 @@ export class PcbLibStreamExtractor {
938
984
  }
939
985
  }
940
986
 
987
+ for (const model of embeddedModels.models || []) {
988
+ if (streams.has(model.sourceStream)) {
989
+ names.add(model.sourceStream)
990
+ }
991
+ }
992
+
993
+ for (const componentBody of embeddedModels.componentBodies || []) {
994
+ if (streams.has(componentBody.sourceStream)) {
995
+ names.add(componentBody.sourceStream)
996
+ }
997
+ }
998
+
941
999
  return [...names].sort((left, right) => left.localeCompare(right))
942
1000
  }
943
1001