altium-toolkit 1.0.8 → 1.0.10

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 (102) 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/ci_artifact_bundle_a1.schema.json +76 -0
  5. package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +35 -0
  6. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +53 -0
  7. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1826 -110
  8. package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
  9. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +86 -0
  10. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +63 -0
  11. package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
  12. package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
  13. package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
  14. package/docs/testing.md +9 -3
  15. package/package.json +1 -1
  16. package/spec/library-scope.md +7 -1
  17. package/src/core/altium/AltiumLayoutParser.mjs +104 -8
  18. package/src/core/altium/AltiumParser.mjs +196 -45
  19. package/src/core/altium/CiArtifactBundleBuilder.mjs +202 -0
  20. package/src/core/altium/DraftsmanDigestParser.mjs +689 -0
  21. package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
  22. package/src/core/altium/IntLibModelParser.mjs +240 -0
  23. package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
  24. package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
  25. package/src/core/altium/LibrarySearchIndex.mjs +215 -0
  26. package/src/core/altium/NormalizedModelSchema.mjs +36 -0
  27. package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
  28. package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
  29. package/src/core/altium/PcbDefaultsParser.mjs +171 -0
  30. package/src/core/altium/PcbDimensionParser.mjs +229 -0
  31. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
  32. package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
  33. package/src/core/altium/PcbLibModelParser.mjs +235 -14
  34. package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
  35. package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
  36. package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
  37. package/src/core/altium/PcbModelParser.mjs +495 -32
  38. package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
  39. package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
  40. package/src/core/altium/PcbPadStackParser.mjs +229 -2
  41. package/src/core/altium/PcbPickPlacePositionResolver.mjs +224 -0
  42. package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
  43. package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
  44. package/src/core/altium/PcbRegionPrimitiveParser.mjs +76 -3
  45. package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
  46. package/src/core/altium/PcbRuleParser.mjs +354 -33
  47. package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
  48. package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
  49. package/src/core/altium/PcbStatisticsBuilder.mjs +541 -0
  50. package/src/core/altium/PcbStreamExtractor.mjs +111 -4
  51. package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
  52. package/src/core/altium/PcbUnionParser.mjs +307 -0
  53. package/src/core/altium/PcbViaStackParser.mjs +98 -10
  54. package/src/core/altium/PcbViaStructureParser.mjs +335 -0
  55. package/src/core/altium/PrintableTextDecoder.mjs +53 -3
  56. package/src/core/altium/PrjPcbModelParser.mjs +281 -7
  57. package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
  58. package/src/core/altium/ProjectDesignBundleBuilder.mjs +492 -0
  59. package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
  60. package/src/core/altium/ProjectNetlistExporter.mjs +503 -0
  61. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
  62. package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
  63. package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
  64. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
  65. package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
  66. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
  67. package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
  68. package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
  69. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
  70. package/src/core/altium/SchematicHarnessParser.mjs +302 -0
  71. package/src/core/altium/SchematicImageParser.mjs +474 -3
  72. package/src/core/altium/SchematicImplementationParser.mjs +518 -0
  73. package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
  74. package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
  75. package/src/core/altium/SchematicPinParser.mjs +84 -1
  76. package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
  77. package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
  78. package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
  79. package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
  80. package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
  81. package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
  82. package/src/core/altium/SchematicTemplateParser.mjs +256 -0
  83. package/src/core/altium/SchematicTextParser.mjs +123 -0
  84. package/src/core/altium/SvgModelCrossLinkValidator.mjs +402 -0
  85. package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +136 -96
  86. package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
  87. package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
  88. package/src/core/ole/OleCompoundDocument.mjs +20 -0
  89. package/src/parser.mjs +35 -0
  90. package/src/styles/altium-renderers.css +19 -0
  91. package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
  92. package/src/ui/PcbInteractionIndex.mjs +9 -4
  93. package/src/ui/PcbScene3dBuilder.mjs +137 -3
  94. package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
  95. package/src/ui/PcbSvgRenderer.mjs +1252 -34
  96. package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
  97. package/src/ui/SchematicNoteRenderer.mjs +9 -2
  98. package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
  99. package/src/ui/SchematicShapeRenderer.mjs +362 -0
  100. package/src/ui/SchematicSvgRenderer.mjs +1442 -92
  101. package/src/ui/SchematicTypography.mjs +48 -5
  102. package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
@@ -0,0 +1,256 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { PcbSidecarRecordParser } from './PcbSidecarRecordParser.mjs'
6
+
7
+ /**
8
+ * Decodes extended primitive sidecar records such as mask-expansion overrides.
9
+ */
10
+ export class PcbExtendedPrimitiveInformationParser {
11
+ static #SOURCE_STREAM = 'ExtendedPrimitiveInformation/Data'
12
+
13
+ static #OBJECT_ID_TO_COLLECTION = {
14
+ 1: ['arcs', 'arc'],
15
+ 2: ['pads', 'pad'],
16
+ 3: ['vias', 'via'],
17
+ 4: ['tracks', 'track'],
18
+ 5: ['texts', 'text'],
19
+ 6: ['fills', 'fill'],
20
+ 11: ['regions', 'region']
21
+ }
22
+
23
+ /**
24
+ * Parses extended primitive information records.
25
+ * @param {Uint8Array | ArrayBuffer | undefined} dataBytes
26
+ * @param {string} [sourceStream]
27
+ * @returns {{ entries: object[], byPrimitiveIndex: Record<string, object>, byPrimitiveKey: Record<string, object> }}
28
+ */
29
+ static parse(
30
+ dataBytes,
31
+ sourceStream = PcbExtendedPrimitiveInformationParser.#SOURCE_STREAM
32
+ ) {
33
+ const entries = PcbSidecarRecordParser.parseLengthPrefixedRecords(
34
+ dataBytes,
35
+ sourceStream
36
+ )
37
+ .map((record) =>
38
+ PcbExtendedPrimitiveInformationParser.#normalizeRecord(record)
39
+ )
40
+ .filter(Boolean)
41
+
42
+ return PcbExtendedPrimitiveInformationParser.#buildLookups(entries)
43
+ }
44
+
45
+ /**
46
+ * Adds extended primitive information to matching decoded primitives.
47
+ * @param {Record<string, object[]>} binaryPrimitives
48
+ * @param {{ entries?: object[] }} extendedInformation
49
+ */
50
+ static attachToPrimitives(binaryPrimitives, extendedInformation) {
51
+ if (!binaryPrimitives || !Array.isArray(extendedInformation?.entries)) {
52
+ return
53
+ }
54
+
55
+ for (const entry of extendedInformation.entries) {
56
+ const collectionName =
57
+ PcbExtendedPrimitiveInformationParser.#collectionNameForEntry(
58
+ entry
59
+ )
60
+ const collection = binaryPrimitives[collectionName]
61
+
62
+ if (!Array.isArray(collection)) {
63
+ continue
64
+ }
65
+
66
+ const primitive = collection[entry.primitiveIndex]
67
+ if (!primitive) {
68
+ continue
69
+ }
70
+
71
+ primitive.extendedPrimitiveInformation =
72
+ PcbExtendedPrimitiveInformationParser.#publicEntry(entry)
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Normalizes one decoded sidecar record.
78
+ * @param {{ fields: Record<string, string>, sourceStream: string, recordIndex: number }} record
79
+ * @returns {object | null}
80
+ */
81
+ static #normalizeRecord(record) {
82
+ const primitiveIndex = PcbSidecarRecordParser.parseInteger(
83
+ PcbSidecarRecordParser.firstField(record.fields, [
84
+ 'PRIMITIVEINDEX',
85
+ 'INDEX'
86
+ ])
87
+ )
88
+
89
+ if (primitiveIndex === null) {
90
+ return null
91
+ }
92
+
93
+ const primitiveObjectId = PcbSidecarRecordParser.parseInteger(
94
+ PcbSidecarRecordParser.firstField(record.fields, [
95
+ 'PRIMITIVEOBJECTID',
96
+ 'OBJECTID'
97
+ ])
98
+ )
99
+ const objectInfo =
100
+ PcbExtendedPrimitiveInformationParser.#OBJECT_ID_TO_COLLECTION[
101
+ primitiveObjectId
102
+ ] || []
103
+ const type = PcbSidecarRecordParser.firstField(record.fields, ['TYPE'])
104
+ const primitiveType =
105
+ objectInfo[1] ||
106
+ PcbExtendedPrimitiveInformationParser.#normalizePrimitiveType(type)
107
+
108
+ return {
109
+ primitiveIndex,
110
+ primitiveObjectId,
111
+ primitiveType,
112
+ type,
113
+ sourceStream: record.sourceStream,
114
+ maskExpansion:
115
+ PcbExtendedPrimitiveInformationParser.#parseMaskExpansion(
116
+ record.fields
117
+ ),
118
+ fields: record.fields
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Parses paste and solder mask-expansion fields.
124
+ * @param {Record<string, string>} fields
125
+ * @returns {{ paste: object, solder: object }}
126
+ */
127
+ static #parseMaskExpansion(fields) {
128
+ const pasteMode = PcbSidecarRecordParser.parseInteger(
129
+ PcbSidecarRecordParser.firstField(fields, [
130
+ 'PASTEMASKEXPANSIONMODE',
131
+ 'PASTEMASKEXPANSION_MODE'
132
+ ])
133
+ )
134
+ const solderMode = PcbSidecarRecordParser.parseInteger(
135
+ PcbSidecarRecordParser.firstField(fields, [
136
+ 'SOLDERMASKEXPANSIONMODE',
137
+ 'SOLDERMASKEXPANSION_MODE'
138
+ ])
139
+ )
140
+
141
+ return {
142
+ paste: {
143
+ mode: pasteMode,
144
+ source: PcbExtendedPrimitiveInformationParser.#maskExpansionSource(
145
+ pasteMode
146
+ ),
147
+ manualExpansion: PcbSidecarRecordParser.parseNumber(
148
+ PcbSidecarRecordParser.firstField(fields, [
149
+ 'PASTEMASKEXPANSION_MANUAL',
150
+ 'PASTEMASKEXPANSIONMANUAL'
151
+ ])
152
+ )
153
+ },
154
+ solder: {
155
+ mode: solderMode,
156
+ source: PcbExtendedPrimitiveInformationParser.#maskExpansionSource(
157
+ solderMode
158
+ ),
159
+ manualExpansion: PcbSidecarRecordParser.parseNumber(
160
+ PcbSidecarRecordParser.firstField(fields, [
161
+ 'SOLDERMASKEXPANSION_MANUAL',
162
+ 'SOLDERMASKEXPANSIONMANUAL'
163
+ ])
164
+ )
165
+ }
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Builds primitive-index lookups.
171
+ * @param {object[]} entries
172
+ * @returns {{ entries: object[], byPrimitiveIndex: Record<string, object>, byPrimitiveKey: Record<string, object> }}
173
+ */
174
+ static #buildLookups(entries) {
175
+ const byPrimitiveIndex = {}
176
+ const byPrimitiveKey = {}
177
+
178
+ for (const entry of entries) {
179
+ byPrimitiveIndex[String(entry.primitiveIndex)] = entry
180
+ if (Number.isInteger(entry.primitiveObjectId)) {
181
+ byPrimitiveKey[
182
+ entry.primitiveObjectId + ':' + entry.primitiveIndex
183
+ ] = entry
184
+ }
185
+ }
186
+
187
+ return {
188
+ entries,
189
+ byPrimitiveIndex,
190
+ byPrimitiveKey
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Resolves one binary primitive collection for a sidecar entry.
196
+ * @param {object} entry
197
+ * @returns {string}
198
+ */
199
+ static #collectionNameForEntry(entry) {
200
+ const objectInfo =
201
+ PcbExtendedPrimitiveInformationParser.#OBJECT_ID_TO_COLLECTION[
202
+ entry.primitiveObjectId
203
+ ]
204
+ if (objectInfo) {
205
+ return objectInfo[0]
206
+ }
207
+
208
+ const primitiveType = String(entry.primitiveType || '').toLowerCase()
209
+ return primitiveType ? primitiveType + 's' : ''
210
+ }
211
+
212
+ /**
213
+ * Builds the public primitive-attached entry.
214
+ * @param {object} entry
215
+ * @returns {object}
216
+ */
217
+ static #publicEntry(entry) {
218
+ return {
219
+ primitiveIndex: entry.primitiveIndex,
220
+ primitiveObjectId: entry.primitiveObjectId,
221
+ primitiveType: entry.primitiveType,
222
+ type: entry.type,
223
+ sourceStream: entry.sourceStream,
224
+ maskExpansion: entry.maskExpansion
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Maps one mask-expansion mode to the existing source vocabulary.
230
+ * @param {number | null} mode
231
+ * @returns {string}
232
+ */
233
+ static #maskExpansionSource(mode) {
234
+ if (mode === 1) return 'rule'
235
+ if (mode === 2) return 'manual'
236
+ if (mode === 0) return 'default'
237
+ if (mode === null) return 'unknown'
238
+
239
+ return 'unknown-' + mode
240
+ }
241
+
242
+ /**
243
+ * Normalizes one textual primitive type hint.
244
+ * @param {string} value
245
+ * @returns {string}
246
+ */
247
+ static #normalizePrimitiveType(value) {
248
+ const normalized = String(value || '')
249
+ .trim()
250
+ .toLowerCase()
251
+ .replace(/^e/iu, '')
252
+ .replace(/object$/iu, '')
253
+
254
+ return normalized
255
+ }
256
+ }
@@ -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