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.
- package/README.md +34 -5
- package/docs/api.md +134 -23
- package/docs/model-format.md +174 -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 +511 -1
- 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 +2 -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/src/core/BinaryReader.mjs +213 -2
- package/src/core/altium/AltiumParser.mjs +352 -14
- package/src/core/altium/AltiumUnits.mjs +205 -0
- package/src/core/altium/AsciiRecordParser.mjs +9 -0
- 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/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/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/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/SchematicOwnershipGraphParser.mjs +102 -3
- package/src/core/altium/SchematicPinParser.mjs +12 -45
- package/src/core/altium/SchematicPrimitiveParser.mjs +9 -14
- 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 +64 -25
- 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/UnsupportedFeatureReportBuilder.mjs +380 -0
- package/src/parser.mjs +35 -1
- package/src/renderers.mjs +1 -0
- package/src/ui/SchematicShapeRenderer.mjs +49 -6
- package/src/ui/SchematicSvgRenderer.mjs +37 -8
- 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
|
|
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.
|