altium-toolkit 1.1.3 → 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.
- package/README.md +34 -5
- package/docs/api.md +171 -23
- package/docs/model-format.md +192 -20
- package/docs/schemas/altium_toolkit/embedded_assets_a1.schema.json +56 -0
- package/docs/schemas/altium_toolkit/fixture_coverage_matrix_a1.schema.json +89 -0
- package/docs/schemas/altium_toolkit/geometry_bounds_a1.schema.json +86 -0
- package/docs/schemas/altium_toolkit/library_catalog_a1.schema.json +65 -0
- package/docs/schemas/altium_toolkit/library_diff_a1.schema.json +54 -0
- package/docs/schemas/altium_toolkit/library_inspection_a1.schema.json +94 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +4 -0
- package/docs/schemas/altium_toolkit/native_stream_inventory_a1.schema.json +66 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +513 -3
- package/docs/schemas/altium_toolkit/parameter_record_inventory_a1.schema.json +84 -0
- package/docs/schemas/altium_toolkit/parser_diagnostics_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/parser_value_verification_a1.schema.json +74 -0
- package/docs/schemas/altium_toolkit/pcb_class_report_a1.schema.json +79 -0
- package/docs/schemas/altium_toolkit/pcb_inspection_a1.schema.json +65 -0
- package/docs/schemas/altium_toolkit/pcb_net_membership_a1.schema.json +98 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +3 -0
- package/docs/schemas/altium_toolkit/project_hierarchy_a1.schema.json +79 -0
- package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +212 -0
- package/docs/testing.md +7 -0
- package/examples/README.md +21 -0
- package/examples/cli-utils.mjs +148 -0
- package/examples/corpus-smoke.mjs +523 -0
- package/examples/extract-bom.mjs +47 -0
- package/examples/generate-pnp.mjs +59 -0
- package/examples/inspect-board.mjs +70 -0
- package/examples/inspect-schematic.mjs +406 -0
- package/examples/library-catalog.mjs +115 -0
- package/examples/net-report.mjs +61 -0
- package/examples/validate-library.mjs +59 -0
- package/package.json +1 -1
- package/spec/library-scope.md +5 -0
- package/src/core/BinaryReader.mjs +213 -2
- package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
- package/src/core/altium/AltiumParser.mjs +357 -16
- package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
- package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
- package/src/core/altium/AltiumUnits.mjs +205 -0
- package/src/core/altium/AsciiRecordParser.mjs +52 -11
- package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
- package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
- package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
- package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
- package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
- package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
- package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
- package/src/core/altium/NormalizedModelSchema.mjs +3 -31
- package/src/core/altium/ParameterCollection.mjs +431 -0
- package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
- package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
- package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
- package/src/core/altium/ParserErrors.mjs +90 -0
- package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
- package/src/core/altium/ParserUtils.mjs +24 -0
- package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
- package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
- package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
- package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
- package/src/core/altium/PcbLayerGroups.mjs +308 -0
- package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
- package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
- package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
- package/src/core/altium/PcbLibModelParser.mjs +20 -4
- package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
- package/src/core/altium/PcbModelParser.mjs +223 -4
- package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
- package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
- package/src/core/altium/PcbStreamExtractor.mjs +130 -6
- package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
- package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
- package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
- package/src/core/altium/SchLibModelParser.mjs +840 -0
- package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
- package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
- package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
- package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
- package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
- package/src/core/altium/SchematicHarnessParser.mjs +9 -3
- package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
- package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
- package/src/core/altium/SchematicPinParser.mjs +99 -65
- package/src/core/altium/SchematicPrimitiveParser.mjs +125 -22
- package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
- package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
- package/src/core/altium/SchematicSheetParser.mjs +8 -2
- package/src/core/altium/SchematicStreamExtractor.mjs +107 -21
- package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
- package/src/core/altium/SchematicTextParser.mjs +28 -12
- package/src/core/altium/SchematicTextRunParser.mjs +81 -0
- package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
- package/src/core/altium/SourceBundleExporter.mjs +156 -0
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
- package/src/core/altium/SourceComponentClient.mjs +239 -0
- package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
- package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
- package/src/parser.mjs +43 -1
- package/src/renderers.mjs +1 -0
- package/src/styles/altium-renderers.css +6 -6
- package/src/ui/PcbArcUtils.mjs +19 -2
- package/src/ui/PcbScene3dBuilder.mjs +202 -20
- package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
- package/src/ui/SchematicColorResolver.mjs +185 -0
- package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
- package/src/ui/SchematicLineColorResolver.mjs +88 -0
- package/src/ui/SchematicNoteRenderer.mjs +5 -1
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +269 -8
- package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +229 -62
- package/src/ui/SchematicShapeRenderer.mjs +86 -17
- package/src/ui/SchematicSvgRenderer.mjs +980 -58
- package/src/ui/SchematicTypography.mjs +4 -3
|
@@ -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
|
|
36
|
-
|
|
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(/"/gu, '"')
|
|
771
|
+
.replace(/'/gu, "'")
|
|
772
|
+
.replace(/</gu, '<')
|
|
773
|
+
.replace(/>/gu, '>')
|
|
774
|
+
.replace(/&/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.
|
|
@@ -62,6 +62,7 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
62
62
|
layerById,
|
|
63
63
|
'backdrill'
|
|
64
64
|
)
|
|
65
|
+
const stackup = PcbLayerStackReadModelBuilder.#stackup(fields)
|
|
65
66
|
const diagnostics = PcbLayerStackReadModelBuilder.#diagnostics({
|
|
66
67
|
substacks,
|
|
67
68
|
branches,
|
|
@@ -105,6 +106,7 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
105
106
|
branches,
|
|
106
107
|
topLevelBendLines,
|
|
107
108
|
cavityReport,
|
|
109
|
+
...(stackup ? { stackup } : {}),
|
|
108
110
|
impedanceProfiles,
|
|
109
111
|
transmissionLines,
|
|
110
112
|
viaSpans,
|
|
@@ -166,9 +168,9 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
166
168
|
PcbLayerStackReadModelBuilder.#stripUndefined({
|
|
167
169
|
index: layer.index,
|
|
168
170
|
layerId: layer.layerId,
|
|
169
|
-
layerKey:
|
|
170
|
-
layer.layerId
|
|
171
|
-
|
|
171
|
+
layerKey: Number.isFinite(layer.layerId)
|
|
172
|
+
? 'L' + layer.layerId
|
|
173
|
+
: undefined,
|
|
172
174
|
name: layer.name,
|
|
173
175
|
kind: layer.kind,
|
|
174
176
|
material: layer.material,
|
|
@@ -189,9 +191,9 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
189
191
|
PcbLayerStackReadModelBuilder.#stripUndefined({
|
|
190
192
|
index: index + 1,
|
|
191
193
|
layerId: layer.layerId,
|
|
192
|
-
layerKey:
|
|
193
|
-
layer.layerId
|
|
194
|
-
|
|
194
|
+
layerKey: Number.isFinite(layer.layerId)
|
|
195
|
+
? 'L' + layer.layerId
|
|
196
|
+
: undefined,
|
|
195
197
|
name: layer.name,
|
|
196
198
|
kind: layer.kind || layer.role,
|
|
197
199
|
...PcbLayerStackSourceMetadataParser.layerSourceFields(
|
|
@@ -592,6 +594,81 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
592
594
|
)
|
|
593
595
|
}
|
|
594
596
|
|
|
597
|
+
/**
|
|
598
|
+
* Parses document-level layer-stack metadata.
|
|
599
|
+
* @param {Record<string, string | string[]>} fields Board fields.
|
|
600
|
+
* @returns {object | undefined}
|
|
601
|
+
*/
|
|
602
|
+
static #stackup(fields) {
|
|
603
|
+
const features = PcbLayerStackReadModelBuilder.#stackupFeatures(fields)
|
|
604
|
+
const stackup = PcbLayerStackReadModelBuilder.#stripUndefined({
|
|
605
|
+
serializerVersion: PcbLayerStackReadModelBuilder.#field(
|
|
606
|
+
fields,
|
|
607
|
+
'STACKUPX_SERIALIZER_VERSION'
|
|
608
|
+
),
|
|
609
|
+
revisionId: PcbLayerStackReadModelBuilder.#field(
|
|
610
|
+
fields,
|
|
611
|
+
'STACKUPX_REVISION_ID'
|
|
612
|
+
),
|
|
613
|
+
type: PcbLayerStackReadModelBuilder.#field(fields, 'STACKUPX_TYPE'),
|
|
614
|
+
roughnessType: PcbLayerStackReadModelBuilder.#field(
|
|
615
|
+
fields,
|
|
616
|
+
'STACKUPX_ROUGHNESS_TYPE'
|
|
617
|
+
),
|
|
618
|
+
roughnessFactorSR: PcbLayerStackReadModelBuilder.#field(
|
|
619
|
+
fields,
|
|
620
|
+
'STACKUPX_ROUGHNESS_FACTOR_SR'
|
|
621
|
+
),
|
|
622
|
+
roughnessFactor: PcbLayerStackReadModelBuilder.#field(
|
|
623
|
+
fields,
|
|
624
|
+
'STACKUPX_ROUGHNESS_FACTOR'
|
|
625
|
+
),
|
|
626
|
+
realisticRatio: PcbLayerStackReadModelBuilder.#optionalBoolean(
|
|
627
|
+
PcbLayerStackReadModelBuilder.#field(
|
|
628
|
+
fields,
|
|
629
|
+
'STACKUPX_REALISTIC_RATIO'
|
|
630
|
+
)
|
|
631
|
+
),
|
|
632
|
+
features: PcbLayerStackSourceMetadataParser.optionalArray(features)
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
return Object.keys(stackup).length ? stackup : undefined
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* Parses stackup feature rows.
|
|
640
|
+
* @param {Record<string, string | string[]>} fields Board fields.
|
|
641
|
+
* @returns {object[]}
|
|
642
|
+
*/
|
|
643
|
+
static #stackupFeatures(fields) {
|
|
644
|
+
return PcbLayerStackReadModelBuilder.#indexedRows(fields, [
|
|
645
|
+
/^STACKUPX_FEATURE(\d+)_ID$/iu,
|
|
646
|
+
/^STACKUP_FEATURE(\d+)_ID$/iu
|
|
647
|
+
]).map((index) =>
|
|
648
|
+
PcbLayerStackReadModelBuilder.#stripUndefined({
|
|
649
|
+
index,
|
|
650
|
+
id: PcbLayerStackReadModelBuilder.#indexedField(
|
|
651
|
+
fields,
|
|
652
|
+
['STACKUPX_FEATURE', 'STACKUP_FEATURE'],
|
|
653
|
+
index,
|
|
654
|
+
['ID']
|
|
655
|
+
),
|
|
656
|
+
name: PcbLayerStackReadModelBuilder.#indexedField(
|
|
657
|
+
fields,
|
|
658
|
+
['STACKUPX_FEATURE', 'STACKUP_FEATURE'],
|
|
659
|
+
index,
|
|
660
|
+
['NAME']
|
|
661
|
+
),
|
|
662
|
+
kind: PcbLayerStackReadModelBuilder.#indexedField(
|
|
663
|
+
fields,
|
|
664
|
+
['STACKUPX_FEATURE', 'STACKUP_FEATURE'],
|
|
665
|
+
index,
|
|
666
|
+
['KIND']
|
|
667
|
+
)
|
|
668
|
+
})
|
|
669
|
+
)
|
|
670
|
+
}
|
|
671
|
+
|
|
595
672
|
/**
|
|
596
673
|
* Builds preservation-first diagnostics for unresolved references.
|
|
597
674
|
* @param {object} input Sidecar sections.
|
|
@@ -903,15 +980,6 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
903
980
|
return Number.isFinite(parsed) ? parsed : undefined
|
|
904
981
|
}
|
|
905
982
|
|
|
906
|
-
/**
|
|
907
|
-
* Builds a stable layer key.
|
|
908
|
-
* @param {number | null | undefined} layerId Layer id.
|
|
909
|
-
* @returns {string | undefined}
|
|
910
|
-
*/
|
|
911
|
-
static #layerKey(layerId) {
|
|
912
|
-
return Number.isFinite(layerId) ? 'L' + layerId : undefined
|
|
913
|
-
}
|
|
914
|
-
|
|
915
983
|
/**
|
|
916
984
|
* Removes undefined and empty string values while keeping false and empty
|
|
917
985
|
* arrays stable.
|