altium-toolkit 1.1.22 → 1.1.23

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 (99) hide show
  1. package/README.md +34 -5
  2. package/docs/api.md +134 -23
  3. package/docs/model-format.md +174 -20
  4. package/docs/schemas/altium_toolkit/embedded_assets_a1.schema.json +56 -0
  5. package/docs/schemas/altium_toolkit/fixture_coverage_matrix_a1.schema.json +89 -0
  6. package/docs/schemas/altium_toolkit/geometry_bounds_a1.schema.json +86 -0
  7. package/docs/schemas/altium_toolkit/library_catalog_a1.schema.json +65 -0
  8. package/docs/schemas/altium_toolkit/library_diff_a1.schema.json +54 -0
  9. package/docs/schemas/altium_toolkit/library_inspection_a1.schema.json +94 -0
  10. package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +4 -0
  11. package/docs/schemas/altium_toolkit/native_stream_inventory_a1.schema.json +66 -0
  12. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +511 -1
  13. package/docs/schemas/altium_toolkit/parameter_record_inventory_a1.schema.json +84 -0
  14. package/docs/schemas/altium_toolkit/parser_diagnostics_a1.schema.json +63 -0
  15. package/docs/schemas/altium_toolkit/parser_value_verification_a1.schema.json +74 -0
  16. package/docs/schemas/altium_toolkit/pcb_class_report_a1.schema.json +79 -0
  17. package/docs/schemas/altium_toolkit/pcb_inspection_a1.schema.json +65 -0
  18. package/docs/schemas/altium_toolkit/pcb_net_membership_a1.schema.json +98 -0
  19. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +3 -0
  20. package/docs/schemas/altium_toolkit/project_hierarchy_a1.schema.json +79 -0
  21. package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +212 -0
  22. package/docs/testing.md +2 -0
  23. package/examples/README.md +21 -0
  24. package/examples/cli-utils.mjs +148 -0
  25. package/examples/corpus-smoke.mjs +523 -0
  26. package/examples/extract-bom.mjs +47 -0
  27. package/examples/generate-pnp.mjs +59 -0
  28. package/examples/inspect-board.mjs +70 -0
  29. package/examples/inspect-schematic.mjs +406 -0
  30. package/examples/library-catalog.mjs +115 -0
  31. package/examples/net-report.mjs +61 -0
  32. package/examples/validate-library.mjs +59 -0
  33. package/package.json +1 -1
  34. package/src/core/BinaryReader.mjs +213 -2
  35. package/src/core/altium/AltiumParser.mjs +352 -14
  36. package/src/core/altium/AltiumUnits.mjs +205 -0
  37. package/src/core/altium/AsciiRecordParser.mjs +9 -0
  38. package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
  39. package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
  40. package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
  41. package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
  42. package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
  43. package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
  44. package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
  45. package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
  46. package/src/core/altium/NormalizedModelSchema.mjs +3 -31
  47. package/src/core/altium/ParameterCollection.mjs +431 -0
  48. package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
  49. package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
  50. package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
  51. package/src/core/altium/ParserErrors.mjs +90 -0
  52. package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
  53. package/src/core/altium/ParserUtils.mjs +24 -0
  54. package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
  55. package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
  56. package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
  57. package/src/core/altium/PcbLayerGroups.mjs +308 -0
  58. package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
  59. package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
  60. package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
  61. package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
  62. package/src/core/altium/PcbLibModelParser.mjs +20 -4
  63. package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
  64. package/src/core/altium/PcbModelParser.mjs +223 -4
  65. package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
  66. package/src/core/altium/PcbStreamExtractor.mjs +130 -6
  67. package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
  68. package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
  69. package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
  70. package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
  71. package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
  72. package/src/core/altium/SchLibModelParser.mjs +840 -0
  73. package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
  74. package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
  75. package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
  76. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
  77. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
  78. package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
  79. package/src/core/altium/SchematicHarnessParser.mjs +9 -3
  80. package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
  81. package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
  82. package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
  83. package/src/core/altium/SchematicPinParser.mjs +12 -45
  84. package/src/core/altium/SchematicPrimitiveParser.mjs +9 -14
  85. package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
  86. package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
  87. package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
  88. package/src/core/altium/SchematicSheetParser.mjs +8 -2
  89. package/src/core/altium/SchematicStreamExtractor.mjs +64 -25
  90. package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
  91. package/src/core/altium/SchematicTextParser.mjs +28 -12
  92. package/src/core/altium/SchematicTextRunParser.mjs +81 -0
  93. package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
  94. package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
  95. package/src/parser.mjs +35 -1
  96. package/src/renderers.mjs +1 -0
  97. package/src/ui/SchematicShapeRenderer.mjs +49 -6
  98. package/src/ui/SchematicSvgRenderer.mjs +37 -8
  99. package/src/ui/SchematicTypography.mjs +4 -3
@@ -0,0 +1,183 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { unzlibSync } from 'fflate'
6
+ import { ParserUtils } from './ParserUtils.mjs'
7
+ import { PcbLayerStackInterchangeParser } from './PcbLayerStackInterchangeParser.mjs'
8
+
9
+ const { getField } = ParserUtils
10
+
11
+ /**
12
+ * Extracts compressed stack custom data from PCB board records.
13
+ */
14
+ export class PcbLayerStackCustomDataParser {
15
+ /**
16
+ * Appends decoded stack custom data records after native board records.
17
+ * @param {{ fields: Record<string, string | string[]>, sourceStream?: string }[]} boardRecords Board records.
18
+ * @returns {{ fields: Record<string, string>, sourceStream?: string, stackCustomData: true }[]}
19
+ */
20
+ static parseBoardRecords(boardRecords) {
21
+ return PcbLayerStackCustomDataParser.parseBoardRecordsWithDiagnostics(
22
+ boardRecords
23
+ ).records
24
+ }
25
+
26
+ /**
27
+ * Decodes stack custom data records with caller-visible diagnostics.
28
+ * @param {{ fields: Record<string, string | string[]>, sourceStream?: string }[]} boardRecords Board records.
29
+ * @returns {{ records: { fields: Record<string, string>, sourceStream?: string, stackCustomData: true }[], diagnostics: object[] }}
30
+ */
31
+ static parseBoardRecordsWithDiagnostics(boardRecords) {
32
+ const records = []
33
+ const diagnostics = []
34
+
35
+ for (const record of boardRecords || []) {
36
+ const parsed =
37
+ PcbLayerStackCustomDataParser.#parseBoardRecord(record)
38
+
39
+ if (parsed.record) {
40
+ records.push(parsed.record)
41
+ }
42
+ diagnostics.push(...parsed.diagnostics)
43
+ }
44
+
45
+ return { records, diagnostics }
46
+ }
47
+
48
+ /**
49
+ * Parses one board record.
50
+ * @param {{ fields: Record<string, string | string[]>, sourceStream?: string }} record Board record.
51
+ * @returns {{ record: { fields: Record<string, string>, sourceStream?: string, stackCustomData: true } | null, diagnostics: object[] }}
52
+ */
53
+ static #parseBoardRecord(record) {
54
+ const encoded = PcbLayerStackCustomDataParser.#encodedField(
55
+ record?.fields
56
+ )
57
+
58
+ if (!encoded.value) {
59
+ return { record: null, diagnostics: [] }
60
+ }
61
+
62
+ const decoded = PcbLayerStackCustomDataParser.#decodePayload(
63
+ encoded.value
64
+ )
65
+
66
+ if (decoded.error) {
67
+ return {
68
+ record: null,
69
+ diagnostics: [
70
+ PcbLayerStackCustomDataParser.#decodeDiagnostic(
71
+ record,
72
+ encoded.name
73
+ )
74
+ ]
75
+ }
76
+ }
77
+
78
+ if (!decoded.text) {
79
+ return { record: null, diagnostics: [] }
80
+ }
81
+
82
+ const fields = PcbLayerStackInterchangeParser.parseTextToFields(
83
+ decoded.text,
84
+ { format: 'stackupx' }
85
+ )
86
+
87
+ if (!Object.keys(fields).length) {
88
+ return { record: null, diagnostics: [] }
89
+ }
90
+
91
+ return {
92
+ record: {
93
+ sourceStream: record?.sourceStream,
94
+ fields,
95
+ stackCustomData: true
96
+ },
97
+ diagnostics: []
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Finds the first stack custom data field.
103
+ * @param {Record<string, string | string[]> | undefined} fields Board fields.
104
+ * @returns {{ name: string, value: string }}
105
+ */
106
+ static #encodedField(fields) {
107
+ const v9Value = getField(fields, 'V9_STACKCUSTOMDATA')
108
+
109
+ if (v9Value) {
110
+ return { name: 'V9_STACKCUSTOMDATA', value: v9Value }
111
+ }
112
+
113
+ return {
114
+ name: 'STACKCUSTOMDATA',
115
+ value: getField(fields, 'STACKCUSTOMDATA') || ''
116
+ }
117
+ }
118
+
119
+ /**
120
+ * Builds one invalid stack custom data diagnostic.
121
+ * @param {{ sourceStream?: string }} record Source record.
122
+ * @param {string} fieldName Encoded field name.
123
+ * @returns {{ code: string, severity: string, message: string, sourceStream?: string, fieldName: string }}
124
+ */
125
+ static #decodeDiagnostic(record, fieldName) {
126
+ return {
127
+ code: 'pcb.layer-stack-custom-data.decode-failed',
128
+ severity: 'warning',
129
+ message:
130
+ 'Compressed PCB layer stack custom data could not be decoded.',
131
+ sourceStream: record?.sourceStream,
132
+ fieldName
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Decodes one base64 zlib stack payload.
138
+ * @param {string} encoded Encoded field value.
139
+ * @returns {{ text: string, error: Error | null }}
140
+ */
141
+ static #decodePayload(encoded) {
142
+ if (!encoded) {
143
+ return { text: '', error: null }
144
+ }
145
+
146
+ try {
147
+ const inflated = unzlibSync(
148
+ PcbLayerStackCustomDataParser.#base64ToBytes(encoded)
149
+ )
150
+ const text = new TextDecoder()
151
+ .decode(inflated)
152
+ .replace(/^\?/, '')
153
+ .replace(/\0+$/u, '')
154
+ .trim()
155
+
156
+ return { text, error: null }
157
+ } catch (error) {
158
+ return { text: '', error }
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Converts base64 text into bytes in browser and Node runtimes.
164
+ * @param {string} encoded Base64 text.
165
+ * @returns {Uint8Array}
166
+ */
167
+ static #base64ToBytes(encoded) {
168
+ const normalized = String(encoded || '').replace(/\s+/gu, '')
169
+
170
+ if (typeof Buffer !== 'undefined') {
171
+ return Uint8Array.from(Buffer.from(normalized, 'base64'))
172
+ }
173
+
174
+ const binary = globalThis.atob(normalized)
175
+ const bytes = new Uint8Array(binary.length)
176
+
177
+ for (let index = 0; index < binary.length; index += 1) {
178
+ bytes[index] = binary.charCodeAt(index) & 0xff
179
+ }
180
+
181
+ return bytes
182
+ }
183
+ }
@@ -31,10 +31,9 @@ export class PcbLayerStackInterchangeParser {
31
31
  */
32
32
  static parseText(fileName, text) {
33
33
  const format = PcbLayerStackInterchangeParser.#format(fileName, text)
34
- const fields =
35
- format === 'stackupx'
36
- ? PcbLayerStackInterchangeParser.#stackupxFields(text)
37
- : PcbLayerStackInterchangeParser.#stackupFields(text)
34
+ const fields = PcbLayerStackInterchangeParser.parseTextToFields(text, {
35
+ format
36
+ })
38
37
  const layers = AltiumLayoutParser.parseLayerStack(fields)
39
38
  const layerSubstacks =
40
39
  PcbBoardRegionSemanticsParser.parseLayerSubstacks([fields])
@@ -57,6 +56,21 @@ export class PcbLayerStackInterchangeParser {
57
56
  }
58
57
  }
59
58
 
59
+ /**
60
+ * Converts layer-stack interchange text into board-compatible fields.
61
+ * @param {string} text Source text.
62
+ * @param {{ format?: 'stackup' | 'stackupx' }} options Parse options.
63
+ * @returns {Record<string, string>}
64
+ */
65
+ static parseTextToFields(text, options = {}) {
66
+ const format =
67
+ options.format || PcbLayerStackInterchangeParser.#format('', text)
68
+
69
+ return format === 'stackupx'
70
+ ? PcbLayerStackInterchangeParser.#stackupxFields(text)
71
+ : PcbLayerStackInterchangeParser.#stackupFields(text)
72
+ }
73
+
60
74
  /**
61
75
  * Detects the interchange format.
62
76
  * @param {string} fileName Source file name.
@@ -114,6 +128,14 @@ export class PcbLayerStackInterchangeParser {
114
128
  static #stackupxFields(text) {
115
129
  const fields = {}
116
130
 
131
+ if (PcbLayerStackInterchangeParser.#hasTag(text, 'StackupDocument')) {
132
+ PcbLayerStackInterchangeParser.#assignStackupDocumentFields(
133
+ fields,
134
+ text
135
+ )
136
+ return fields
137
+ }
138
+
117
139
  for (const layer of PcbLayerStackInterchangeParser.#tagFields(text, [
118
140
  'Layer'
119
141
  ])) {
@@ -169,6 +191,243 @@ export class PcbLayerStackInterchangeParser {
169
191
  return fields
170
192
  }
171
193
 
194
+ /**
195
+ * Converts StackupDocument XML into board-compatible fields.
196
+ * @param {Record<string, string>} fields Target fields.
197
+ * @param {string} text Source XML text.
198
+ * @returns {void}
199
+ */
200
+ static #assignStackupDocumentFields(fields, text) {
201
+ const document = PcbLayerStackInterchangeParser.#tagBlocks(text, [
202
+ 'StackupDocument'
203
+ ])[0]
204
+
205
+ if (!document) {
206
+ return
207
+ }
208
+
209
+ PcbLayerStackInterchangeParser.#assignTopLevelFields(
210
+ fields,
211
+ 'STACKUPX',
212
+ document.fields,
213
+ {
214
+ SerializerVersion: 'SERIALIZER_VERSION',
215
+ RevisionId: 'REVISION_ID'
216
+ }
217
+ )
218
+ PcbLayerStackInterchangeParser.#assignStackupFeatures(
219
+ fields,
220
+ document.body
221
+ )
222
+
223
+ const stackup = PcbLayerStackInterchangeParser.#tagBlocks(
224
+ document.body,
225
+ ['Stackup']
226
+ )[0]
227
+
228
+ if (!stackup) {
229
+ return
230
+ }
231
+
232
+ PcbLayerStackInterchangeParser.#assignTopLevelFields(
233
+ fields,
234
+ 'STACKUPX',
235
+ stackup.fields,
236
+ {
237
+ Type: 'TYPE',
238
+ RoughnessType: 'ROUGHNESS_TYPE',
239
+ RoughnessFactorSR: 'ROUGHNESS_FACTOR_SR',
240
+ RoughnessFactor: 'ROUGHNESS_FACTOR',
241
+ RealisticRatio: 'REALISTIC_RATIO'
242
+ }
243
+ )
244
+
245
+ const stacks = PcbLayerStackInterchangeParser.#stackupStacks(
246
+ stackup.body
247
+ )
248
+ let nextLayerIndex = 1
249
+
250
+ stacks.forEach((stack, stackIndex) => {
251
+ const layers = PcbLayerStackInterchangeParser.#stackLayers(
252
+ stack.body
253
+ )
254
+ const layerIds = []
255
+
256
+ PcbLayerStackInterchangeParser.#assignSubstackFields(
257
+ fields,
258
+ stackIndex,
259
+ {
260
+ Id: stack.fields.Id || stack.fields.ID || '',
261
+ Name: stack.fields.Name || '',
262
+ IsFlex: stack.fields.IsFlex || '',
263
+ StackType: stack.fields.Type || ''
264
+ }
265
+ )
266
+
267
+ for (const layer of layers) {
268
+ const layerIndex =
269
+ PcbLayerStackInterchangeParser.#positiveInteger(
270
+ layer.fields.Index || layer.fields.index
271
+ ) || nextLayerIndex
272
+ const layerId =
273
+ PcbLayerStackInterchangeParser.#positiveInteger(
274
+ layer.fields.LayerId || layer.fields.LayerID
275
+ ) || layerIndex
276
+ const properties =
277
+ PcbLayerStackInterchangeParser.#layerProperties(layer.body)
278
+ const layerValues =
279
+ PcbLayerStackInterchangeParser.#stackupDocumentLayerValues(
280
+ layer,
281
+ layerId,
282
+ properties
283
+ )
284
+
285
+ PcbLayerStackInterchangeParser.#assignLayerFields(
286
+ fields,
287
+ layerIndex,
288
+ layerValues
289
+ )
290
+ layerIds.push(layerId)
291
+ nextLayerIndex = Math.max(nextLayerIndex, layerIndex + 1)
292
+ }
293
+
294
+ if (layerIds.length) {
295
+ fields['V9_SUBSTACK' + stackIndex + '_LAYERS'] =
296
+ layerIds.join(',')
297
+ }
298
+ })
299
+ }
300
+
301
+ /**
302
+ * Assigns StackupDocument feature rows.
303
+ * @param {Record<string, string>} fields Target fields.
304
+ * @param {string} body Document body.
305
+ * @returns {void}
306
+ */
307
+ static #assignStackupFeatures(fields, body) {
308
+ const features = PcbLayerStackInterchangeParser.#tagBlocks(body, [
309
+ 'Feature'
310
+ ])
311
+
312
+ features.forEach((feature, index) => {
313
+ const prefix = 'STACKUPX_FEATURE' + index + '_'
314
+ const featureId = feature.fields.Id || feature.fields.ID || ''
315
+ const featureKind =
316
+ PcbLayerStackInterchangeParser.#featureKindFromId(featureId)
317
+
318
+ fields[prefix + 'ID'] = featureId
319
+ fields[prefix + 'NAME'] =
320
+ PcbLayerStackInterchangeParser.#decodeXmlText(feature.body)
321
+ if (featureKind) {
322
+ fields[prefix + 'KIND'] = featureKind
323
+ }
324
+ })
325
+ }
326
+
327
+ /**
328
+ * Finds stack rows inside the Stackup/Stacks container.
329
+ * @param {string} stackupBody Stackup XML body.
330
+ * @returns {{ tagName: string, fields: Record<string, string>, body: string }[]}
331
+ */
332
+ static #stackupStacks(stackupBody) {
333
+ const stacksContainer = PcbLayerStackInterchangeParser.#tagBlocks(
334
+ stackupBody,
335
+ ['Stacks']
336
+ )[0]
337
+
338
+ return PcbLayerStackInterchangeParser.#tagBlocks(
339
+ stacksContainer?.body || '',
340
+ ['Stack']
341
+ )
342
+ }
343
+
344
+ /**
345
+ * Finds layer rows inside one Stack/Layers container.
346
+ * @param {string} stackBody Stack XML body.
347
+ * @returns {{ tagName: string, fields: Record<string, string>, body: string }[]}
348
+ */
349
+ static #stackLayers(stackBody) {
350
+ const layersContainer = PcbLayerStackInterchangeParser.#tagBlocks(
351
+ stackBody,
352
+ ['Layers']
353
+ )[0]
354
+
355
+ return PcbLayerStackInterchangeParser.#tagBlocks(
356
+ layersContainer?.body || '',
357
+ ['Layer']
358
+ )
359
+ }
360
+
361
+ /**
362
+ * Converts one StackupDocument Layer element into known field names.
363
+ * @param {{ fields: Record<string, string>, body: string }} layer Layer block.
364
+ * @param {number} layerId Normalized layer id.
365
+ * @param {Record<string, string>} properties Layer properties.
366
+ * @returns {Record<string, string>}
367
+ */
368
+ static #stackupDocumentLayerValues(layer, layerId, properties) {
369
+ const typeId = layer.fields.TypeId || layer.fields.TypeID || ''
370
+ const values = {
371
+ Name: layer.fields.Name || '',
372
+ LayerId: String(layerId),
373
+ Kind:
374
+ layer.fields.Kind ||
375
+ layer.fields.Type ||
376
+ PcbLayerStackInterchangeParser.#kindFromLayerTypeId(typeId),
377
+ SourceRecordId: layer.fields.Id || layer.fields.ID || '',
378
+ StackupxShared: layer.fields.IsShared || '',
379
+ StackupxProperties:
380
+ PcbLayerStackInterchangeParser.#propertiesField(properties)
381
+ }
382
+
383
+ const propertyValueNames = {
384
+ Material: 'Material',
385
+ Thickness: 'Thickness',
386
+ Weight: 'CopperWeight',
387
+ CopperThickness: 'CopperThickness',
388
+ DielectricConstant: 'Dk',
389
+ LossTangent: 'Df',
390
+ Process: 'Process',
391
+ PullbackDistance: 'PullbackDistance',
392
+ CopperOrientation: 'CopperOrientation',
393
+ Orientation: 'Orientation',
394
+ Note: 'Note',
395
+ Comment: 'Comment',
396
+ 'Material.Manufacturer': 'MaterialManufacturer',
397
+ 'Material.Description': 'MaterialDescription',
398
+ 'Material.GlassTransitionTemp': 'MaterialGlassTransitionTemp',
399
+ GlassTransTemp: 'GlassTransitionTemp',
400
+ DielectricStrength: 'DielectricStrength',
401
+ VolumeResistivity: 'VolumeResistivity',
402
+ Resin: 'Resin',
403
+ Solid: 'Solid',
404
+ 'Material.Frequency': 'MaterialFrequency',
405
+ Frequency: 'Frequency',
406
+ Constructions: 'Constructions',
407
+ CoverlayExpansion: 'CoverlayExpansion'
408
+ }
409
+
410
+ for (const [propertyName, valueName] of Object.entries(
411
+ propertyValueNames
412
+ )) {
413
+ PcbLayerStackInterchangeParser.#assignPropertyValue(
414
+ values,
415
+ properties,
416
+ propertyName,
417
+ valueName
418
+ )
419
+ }
420
+
421
+ if (PcbLayerStackInterchangeParser.#isAdhesiveLayerType(typeId)) {
422
+ values.IsAdhesive = 'true'
423
+ }
424
+ if (PcbLayerStackInterchangeParser.#isStiffenerLayerType(typeId)) {
425
+ values.IsStiffener = 'true'
426
+ }
427
+
428
+ return values
429
+ }
430
+
172
431
  /**
173
432
  * Assigns one sectioned text field.
174
433
  * @param {Record<string, string>} fields Target fields.
@@ -248,11 +507,31 @@ export class PcbLayerStackInterchangeParser {
248
507
  CopperThickness: 'COPPERTHICKNESS',
249
508
  Dk: 'DK',
250
509
  Df: 'DF',
510
+ Process: 'PROCESS',
511
+ PullbackDistance: 'PULLBACKDISTANCE',
512
+ CopperOrientation: 'COPPERORIENTATION',
513
+ Orientation: 'ORIENTATION',
514
+ Note: 'NOTE',
515
+ Comment: 'COMMENT',
516
+ MaterialManufacturer: 'MATERIALMANUFACTURER',
517
+ MaterialDescription: 'MATERIALDESCRIPTION',
518
+ MaterialGlassTransitionTemp: 'MATERIALGLASSTRANSITIONTEMP',
519
+ GlassTransitionTemp: 'GLASSTRANSITIONTEMP',
520
+ DielectricStrength: 'DIELECTRICSTRENGTH',
521
+ VolumeResistivity: 'VOLUMERESISTIVITY',
522
+ Resin: 'RESIN',
523
+ Solid: 'SOLID',
524
+ MaterialFrequency: 'MATERIALFREQUENCY',
525
+ Frequency: 'FREQUENCY',
526
+ Constructions: 'CONSTRUCTIONS',
251
527
  IsAdhesive: 'ISADHESIVE',
252
528
  IsStiffener: 'ISSTIFFENER',
529
+ CopperWeight: 'COPPERWEIGHT',
530
+ CoverlayExpansion: 'COVERLAYEXPANSION',
253
531
  SurfaceFinish: 'SURFACEFINISH',
254
532
  SourceRecordId: 'SOURCE_RECORD_ID',
255
533
  SourceKeys: 'SOURCE_KEYS',
534
+ StackupxShared: 'STACKUPX_SHARED',
256
535
  StackupxProperties: 'STACKUPX_PROPERTIES'
257
536
  }
258
537
  const suffix = suffixByKey[key]
@@ -292,6 +571,7 @@ export class PcbLayerStackInterchangeParser {
292
571
  Name: 'NAME',
293
572
  IsFlex: 'ISFLEX',
294
573
  Layers: 'LAYERS',
574
+ StackupxShared: 'STACKUPX_SHARED',
295
575
  StackType: 'STACKUPX_STACKTYPE'
296
576
  }
297
577
  const suffix = suffixByKey[key]
@@ -387,6 +667,195 @@ export class PcbLayerStackInterchangeParser {
387
667
  }
388
668
  }
389
669
 
670
+ /**
671
+ * Assigns top-level scalar fields using one prefix.
672
+ * @param {Record<string, string>} fields Target fields.
673
+ * @param {string} prefix Field prefix.
674
+ * @param {Record<string, string>} values Source attributes.
675
+ * @param {Record<string, string>} keyMap Source-to-target suffixes.
676
+ * @returns {void}
677
+ */
678
+ static #assignTopLevelFields(fields, prefix, values, keyMap) {
679
+ for (const [sourceKey, suffix] of Object.entries(keyMap)) {
680
+ if (values[sourceKey]) {
681
+ fields[prefix + '_' + suffix] = values[sourceKey]
682
+ }
683
+ }
684
+ }
685
+
686
+ /**
687
+ * Assigns one known property value to layer values.
688
+ * @param {Record<string, string>} values Target values.
689
+ * @param {Record<string, string>} properties Source properties.
690
+ * @param {string} propertyName Source property name.
691
+ * @param {string} valueName Target value name.
692
+ * @returns {void}
693
+ */
694
+ static #assignPropertyValue(values, properties, propertyName, valueName) {
695
+ if (properties[propertyName]) {
696
+ values[valueName] = properties[propertyName]
697
+ }
698
+ }
699
+
700
+ /**
701
+ * Parses nested layer Property elements into a key/value map.
702
+ * @param {string} body Layer XML body.
703
+ * @returns {Record<string, string>}
704
+ */
705
+ static #layerProperties(body) {
706
+ const propertiesContainer = PcbLayerStackInterchangeParser.#tagBlocks(
707
+ body,
708
+ ['Properties']
709
+ )[0]
710
+ const properties = {}
711
+
712
+ for (const property of PcbLayerStackInterchangeParser.#tagBlocks(
713
+ propertiesContainer?.body || '',
714
+ ['Property']
715
+ )) {
716
+ const name = property.fields.Name || property.fields.name || ''
717
+ if (!name) {
718
+ continue
719
+ }
720
+
721
+ properties[name] = PcbLayerStackInterchangeParser.#decodeXmlText(
722
+ property.body
723
+ )
724
+ }
725
+
726
+ return properties
727
+ }
728
+
729
+ /**
730
+ * Serializes property rows into the existing key/value sidecar field.
731
+ * @param {Record<string, string>} properties Source properties.
732
+ * @returns {string}
733
+ */
734
+ static #propertiesField(properties) {
735
+ return Object.entries(properties)
736
+ .map(([key, value]) => key + '=' + value)
737
+ .join('|')
738
+ }
739
+
740
+ /**
741
+ * Resolves a positive integer.
742
+ * @param {string | number | undefined} value Source value.
743
+ * @returns {number}
744
+ */
745
+ static #positiveInteger(value) {
746
+ const parsed = Number.parseInt(String(value || ''), 10)
747
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : 0
748
+ }
749
+
750
+ /**
751
+ * Returns true when text contains the requested XML-like tag.
752
+ * @param {string} text Source text.
753
+ * @param {string} tagName Tag name.
754
+ * @returns {boolean}
755
+ */
756
+ static #hasTag(text, tagName) {
757
+ return new RegExp('<\\s*' + tagName + '\\b', 'iu').test(
758
+ String(text || '')
759
+ )
760
+ }
761
+
762
+ /**
763
+ * Decodes simple XML text content used by stack metadata.
764
+ * @param {string} value Encoded text.
765
+ * @returns {string}
766
+ */
767
+ static #decodeXmlText(value) {
768
+ return String(value || '')
769
+ .replace(/<!\[CDATA\[([\s\S]*?)\]\]>/gu, '$1')
770
+ .replace(/&quot;/gu, '"')
771
+ .replace(/&apos;/gu, "'")
772
+ .replace(/&lt;/gu, '<')
773
+ .replace(/&gt;/gu, '>')
774
+ .replace(/&amp;/gu, '&')
775
+ .trim()
776
+ }
777
+
778
+ /**
779
+ * Maps known stack layer type ids to broad layer kinds.
780
+ * @param {string} typeId Source type id.
781
+ * @returns {string}
782
+ */
783
+ static #kindFromLayerTypeId(typeId) {
784
+ const normalized = PcbLayerStackInterchangeParser.#normalizeGuid(typeId)
785
+ const kindByTypeId = new Map([
786
+ ['31E48829-E750-4C28-95E0-1A8313F0158E', 'signal'],
787
+ ['F59FAB94-C5ED-467D-94CD-F60A323C5D5B', 'plane'],
788
+ ['F4ECCD87-2CFB-4F37-BE50-4F3A272B4D01', 'signal'],
789
+ ['B0827674-798C-4CF8-807C-8E6C2A11C145', 'finish'],
790
+ ['92B02D5E-8D69-48A8-880E-AC4B77DB099D', 'dielectric'],
791
+ ['136C62EF-1FA6-4897-AE71-7E797B632B92', 'dielectric'],
792
+ ['1A79611A-039D-4D40-A204-53C26C50F8B5', 'dielectric'],
793
+ ['90B89AA0-A48A-45F4-82F5-B3ECA4EC8CCE', 'plating'],
794
+ ['448F9952-79BA-41D8-A8F4-4713EE7A3828', 'mechanical'],
795
+ ['9FD889FA-C97A-401C-A066-E5F746678381', 'mechanical'],
796
+ ['786B5F28-F093-4084-BBA7-46E8F4F24F55', 'mechanical'],
797
+ ['886956F5-B2E9-4114-93F3-F69AF872BFFB', 'marking']
798
+ ])
799
+
800
+ return kindByTypeId.get(normalized) || ''
801
+ }
802
+
803
+ /**
804
+ * Maps known stackup feature ids to stable feature kinds.
805
+ * @param {string} featureId Source feature id.
806
+ * @returns {string}
807
+ */
808
+ static #featureKindFromId(featureId) {
809
+ const normalized =
810
+ PcbLayerStackInterchangeParser.#normalizeGuid(featureId)
811
+ const kindByFeatureId = new Map([
812
+ ['C8939E8A-FD0E-4D52-8860-B7A98F452016', 'standard-stackup'],
813
+ ['E3DF2B86-5F1B-49CA-B266-D1AE57F0BA6F', 'impedance-calculator'],
814
+ ['5277E6A4-9E5F-4F54-951F-DC18CFEB7530', 'rigid-flex'],
815
+ ['68E477FE-0406-4BD2-AD1D-6DD49217052C', 'printed-electronics'],
816
+ ['0A82BA33-E4D8-43F3-9C01-412DC26BDD5E', 'back-drills'],
817
+ ['231FB828-14F8-43F8-9DDF-B2A90A4C5283', 'generic-stackup']
818
+ ])
819
+
820
+ return kindByFeatureId.get(normalized) || ''
821
+ }
822
+
823
+ /**
824
+ * Normalizes GUID text for case-insensitive lookup.
825
+ * @param {string} value Source GUID text.
826
+ * @returns {string}
827
+ */
828
+ static #normalizeGuid(value) {
829
+ return String(value || '')
830
+ .trim()
831
+ .replace(/[{}]/gu, '')
832
+ .toUpperCase()
833
+ }
834
+
835
+ /**
836
+ * Returns true for known adhesive layer type ids.
837
+ * @param {string} typeId Source type id.
838
+ * @returns {boolean}
839
+ */
840
+ static #isAdhesiveLayerType(typeId) {
841
+ return (
842
+ PcbLayerStackInterchangeParser.#normalizeGuid(typeId) ===
843
+ '448F9952-79BA-41D8-A8F4-4713EE7A3828'
844
+ )
845
+ }
846
+
847
+ /**
848
+ * Returns true for known stiffener layer type ids.
849
+ * @param {string} typeId Source type id.
850
+ * @returns {boolean}
851
+ */
852
+ static #isStiffenerLayerType(typeId) {
853
+ return (
854
+ PcbLayerStackInterchangeParser.#normalizeGuid(typeId) ===
855
+ '9FD889FA-C97A-401C-A066-E5F746678381'
856
+ )
857
+ }
858
+
390
859
  /**
391
860
  * Extracts XML-like tag fields.
392
861
  * @param {string} text Source text.