altium-toolkit 1.0.9 → 1.1.0
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/docs/api.md +6 -2
- package/docs/model-format.md +29 -4
- package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +80 -0
- package/docs/schemas/altium_toolkit/contract_gate_a1.schema.json +34 -0
- package/docs/schemas/altium_toolkit/draftsman_board_view_cache_a1.schema.json +115 -0
- package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +166 -0
- package/docs/schemas/altium_toolkit/host_capabilities_a1.schema.json +39 -0
- package/docs/schemas/altium_toolkit/library_merge_plan_a1.schema.json +56 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +70 -0
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +6 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +856 -7
- package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
- package/docs/schemas/altium_toolkit/pcb_bom_profile_a1.schema.json +48 -0
- package/docs/schemas/altium_toolkit/pcb_layer_stack_a1.schema.json +98 -0
- package/docs/schemas/altium_toolkit/pcb_layer_stack_fidelity_a1.schema.json +66 -0
- package/docs/schemas/altium_toolkit/pcb_placed_footprint_extraction_a1.schema.json +31 -0
- package/docs/schemas/altium_toolkit/pcb_review_metadata_a1.schema.json +62 -0
- package/docs/schemas/altium_toolkit/pcb_rigid_flex_topology_a1.schema.json +52 -0
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +27 -0
- package/docs/schemas/altium_toolkit/pcblib_parity_a1.schema.json +24 -0
- package/docs/schemas/altium_toolkit/project_bom_pnp_reconciliation_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +6 -0
- package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
- package/docs/schemas/altium_toolkit/project_outjob_digest_a1.schema.json +46 -0
- package/docs/schemas/altium_toolkit/project_script_a1.schema.json +50 -0
- package/docs/schemas/altium_toolkit/schematic_render_ops_a1.schema.json +55 -0
- package/docs/schemas/altium_toolkit/schematic_template_extraction_a1.schema.json +37 -0
- package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
- package/package.json +1 -1
- package/src/core/altium/AltiumParser.mjs +12 -2
- package/src/core/altium/CiArtifactBundleBuilder.mjs +213 -0
- package/src/core/altium/ContractGateReportBuilder.mjs +351 -0
- package/src/core/altium/DraftsmanBoardViewMetadataBuilder.mjs +653 -0
- package/src/core/altium/DraftsmanDigestParser.mjs +928 -0
- package/src/core/altium/DraftsmanImagePayloadManifestBuilder.mjs +178 -0
- package/src/core/altium/HostCapabilityDiagnosticsBuilder.mjs +271 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +504 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +172 -2
- package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
- package/src/core/altium/PcbBomProfileBuilder.mjs +263 -0
- package/src/core/altium/PcbComponentKindPolicy.mjs +146 -0
- package/src/core/altium/PcbLayerStackFidelityReportBuilder.mjs +141 -0
- package/src/core/altium/PcbLayerStackInterchangeParser.mjs +453 -0
- package/src/core/altium/PcbLayerStackQueryHelper.mjs +195 -0
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +906 -0
- package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +488 -0
- package/src/core/altium/PcbLibModelParser.mjs +2 -0
- package/src/core/altium/PcbLibParityReportBuilder.mjs +242 -0
- package/src/core/altium/PcbModelParser.mjs +211 -22
- package/src/core/altium/PcbPadStackParser.mjs +171 -2
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +11 -1
- package/src/core/altium/PcbPlacedFootprintManifestBuilder.mjs +338 -0
- package/src/core/altium/PcbPolygonRecordParser.mjs +120 -0
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +71 -2
- package/src/core/altium/PcbReviewDrillMetadataBuilder.mjs +301 -0
- package/src/core/altium/PcbReviewMetadataBuilder.mjs +373 -0
- package/src/core/altium/PcbReviewPolygonRealizationBuilder.mjs +269 -0
- package/src/core/altium/PcbReviewRouteHighlightProfileBuilder.mjs +298 -0
- package/src/core/altium/PcbRigidFlexTopologyBuilder.mjs +171 -0
- package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +9 -0
- package/src/core/altium/PrintableTextDecoder.mjs +70 -6
- package/src/core/altium/PrjPcbModelParser.mjs +69 -2
- package/src/core/altium/PrjScrModelParser.mjs +386 -0
- package/src/core/altium/ProjectBomPnpReconciliationBuilder.mjs +237 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +76 -2
- package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +5 -1
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +424 -13
- package/src/core/altium/SvgModelCrossLinkValidator.mjs +435 -0
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +300 -96
- package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/parser.mjs +21 -0
- package/src/ui/PcbFootprintPrimitiveSelector.mjs +13 -1
- package/src/ui/PcbScene3dBuilder.mjs +26 -4
- package/src/ui/PcbSvgRenderer.mjs +65 -0
- package/src/ui/SchematicRenderOpsSidecarBuilder.mjs +554 -0
- package/src/ui/SchematicSvgRenderer.mjs +48 -2
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Classifies layer-stack source evidence and regeneration limits.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbLayerStackFidelityReportBuilder {
|
|
9
|
+
static SCHEMA_ID = 'altium-toolkit.pcb.layer-stack-fidelity.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds a source-fidelity report for a layer-stack read model.
|
|
13
|
+
* @param {object} readModel Layer-stack read model.
|
|
14
|
+
* @returns {object}
|
|
15
|
+
*/
|
|
16
|
+
static build(readModel) {
|
|
17
|
+
const semanticSections =
|
|
18
|
+
PcbLayerStackFidelityReportBuilder.#semanticSections(readModel)
|
|
19
|
+
const nativeCacheSections =
|
|
20
|
+
PcbLayerStackFidelityReportBuilder.#nativeCacheSections(readModel)
|
|
21
|
+
const interchangeOnlySections =
|
|
22
|
+
PcbLayerStackFidelityReportBuilder.#interchangeOnlySections(
|
|
23
|
+
readModel
|
|
24
|
+
)
|
|
25
|
+
const diagnostics = readModel?.diagnostics || []
|
|
26
|
+
const unsupportedRegeneration =
|
|
27
|
+
PcbLayerStackFidelityReportBuilder.#unsupportedRegeneration(
|
|
28
|
+
nativeCacheSections,
|
|
29
|
+
diagnostics
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
schema: PcbLayerStackFidelityReportBuilder.SCHEMA_ID,
|
|
34
|
+
sourceDocument: String(readModel?.source?.fileName || ''),
|
|
35
|
+
summary: {
|
|
36
|
+
semanticLayerCount: (readModel?.layers || []).length,
|
|
37
|
+
nativeCacheFeatureCount: nativeCacheSections.length,
|
|
38
|
+
interchangeOnlyFeatureCount: interchangeOnlySections.length,
|
|
39
|
+
unsupportedRegenerationCount: unsupportedRegeneration.length,
|
|
40
|
+
diagnosticCount: diagnostics.length
|
|
41
|
+
},
|
|
42
|
+
capabilities: {
|
|
43
|
+
semanticRead: semanticSections.length > 0,
|
|
44
|
+
nativeCacheRead: nativeCacheSections.length > 0,
|
|
45
|
+
interchangeRead: interchangeOnlySections.length > 0,
|
|
46
|
+
deterministicReport: true,
|
|
47
|
+
nativeRegeneration: false
|
|
48
|
+
},
|
|
49
|
+
semanticSections,
|
|
50
|
+
nativeCacheSections,
|
|
51
|
+
interchangeOnlySections,
|
|
52
|
+
unsupportedRegeneration,
|
|
53
|
+
diagnostics
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Lists semantic layer-stack sections in the model.
|
|
59
|
+
* @param {object} readModel Layer-stack read model.
|
|
60
|
+
* @returns {string[]}
|
|
61
|
+
*/
|
|
62
|
+
static #semanticSections(readModel) {
|
|
63
|
+
return [
|
|
64
|
+
['layers', readModel?.layers],
|
|
65
|
+
['substacks', readModel?.substacks],
|
|
66
|
+
['branches', readModel?.branches],
|
|
67
|
+
['impedanceProfiles', readModel?.impedanceProfiles]
|
|
68
|
+
]
|
|
69
|
+
.filter(([, values]) => (values || []).length > 0)
|
|
70
|
+
.map(([section]) => section)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Lists native-cache-only sections represented in the model.
|
|
75
|
+
* @param {object} readModel Layer-stack read model.
|
|
76
|
+
* @returns {string[]}
|
|
77
|
+
*/
|
|
78
|
+
static #nativeCacheSections(readModel) {
|
|
79
|
+
const sections = []
|
|
80
|
+
const sourceMap = readModel?.sourceMap || {}
|
|
81
|
+
|
|
82
|
+
if (sourceMap.registryEntryCount > 0) sections.push('source.registry')
|
|
83
|
+
if (sourceMap.sourceKeyCount > 0) sections.push('source.keys')
|
|
84
|
+
if ((readModel?.topLevelBendLines || []).length > 0) {
|
|
85
|
+
sections.push('bendLines')
|
|
86
|
+
}
|
|
87
|
+
if (sourceMap.cavityRegionCount > 0) sections.push('cavities')
|
|
88
|
+
if (sourceMap.surfaceFinishCount > 0) sections.push('surfaceFinish')
|
|
89
|
+
|
|
90
|
+
return sections
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Lists fields that are preserved as interchange-specific metadata.
|
|
95
|
+
* @param {object} readModel Layer-stack read model.
|
|
96
|
+
* @returns {string[]}
|
|
97
|
+
*/
|
|
98
|
+
static #interchangeOnlySections(readModel) {
|
|
99
|
+
const layers = readModel?.layers || []
|
|
100
|
+
const sections = []
|
|
101
|
+
|
|
102
|
+
if (layers.some((layer) => layer.stackupxShared !== undefined)) {
|
|
103
|
+
sections.push('layers.stackupxShared')
|
|
104
|
+
}
|
|
105
|
+
if (
|
|
106
|
+
layers.some(
|
|
107
|
+
(layer) =>
|
|
108
|
+
Object.keys(layer.stackupxProperties || {}).length > 0
|
|
109
|
+
)
|
|
110
|
+
) {
|
|
111
|
+
sections.push('layers.stackupxProperties')
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return sections
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Describes why native stack regeneration is intentionally unsupported.
|
|
119
|
+
* @param {string[]} nativeCacheSections Native cache sections.
|
|
120
|
+
* @param {object[]} diagnostics Read-model diagnostics.
|
|
121
|
+
* @returns {object[]}
|
|
122
|
+
*/
|
|
123
|
+
static #unsupportedRegeneration(nativeCacheSections, diagnostics) {
|
|
124
|
+
const issues = []
|
|
125
|
+
|
|
126
|
+
if (nativeCacheSections.length > 0) {
|
|
127
|
+
issues.push({
|
|
128
|
+
section: 'native-cache',
|
|
129
|
+
reason: 'Native cache metadata is preserved for review but not regenerated.'
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
if ((diagnostics || []).length > 0) {
|
|
133
|
+
issues.push({
|
|
134
|
+
section: 'diagnostics',
|
|
135
|
+
reason: 'Unresolved references prevent equivalent native regeneration.'
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return issues
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { AltiumLayoutParser } from './AltiumLayoutParser.mjs'
|
|
6
|
+
import { PcbBoardRegionSemanticsParser } from './PcbBoardRegionSemanticsParser.mjs'
|
|
7
|
+
import { PcbLayerStackReadModelBuilder } from './PcbLayerStackReadModelBuilder.mjs'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parses read-only layer-stack interchange text into the PCB stack sidecar.
|
|
11
|
+
*/
|
|
12
|
+
export class PcbLayerStackInterchangeParser {
|
|
13
|
+
/**
|
|
14
|
+
* Parses a UTF-8 layer-stack interchange buffer.
|
|
15
|
+
* @param {string} fileName Source file name.
|
|
16
|
+
* @param {ArrayBuffer} arrayBuffer Source bytes.
|
|
17
|
+
* @returns {object}
|
|
18
|
+
*/
|
|
19
|
+
static parseArrayBuffer(fileName, arrayBuffer) {
|
|
20
|
+
return PcbLayerStackInterchangeParser.parseText(
|
|
21
|
+
fileName,
|
|
22
|
+
new TextDecoder().decode(arrayBuffer)
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Parses layer-stack interchange text.
|
|
28
|
+
* @param {string} fileName Source file name.
|
|
29
|
+
* @param {string} text Source text.
|
|
30
|
+
* @returns {object}
|
|
31
|
+
*/
|
|
32
|
+
static parseText(fileName, text) {
|
|
33
|
+
const format = PcbLayerStackInterchangeParser.#format(fileName, text)
|
|
34
|
+
const fields =
|
|
35
|
+
format === 'stackupx'
|
|
36
|
+
? PcbLayerStackInterchangeParser.#stackupxFields(text)
|
|
37
|
+
: PcbLayerStackInterchangeParser.#stackupFields(text)
|
|
38
|
+
const layers = AltiumLayoutParser.parseLayerStack(fields)
|
|
39
|
+
const layerSubstacks =
|
|
40
|
+
PcbBoardRegionSemanticsParser.parseLayerSubstacks([fields])
|
|
41
|
+
const model = PcbLayerStackReadModelBuilder.build({
|
|
42
|
+
fileName,
|
|
43
|
+
boardRecords: [{ fields }],
|
|
44
|
+
streamNames: [],
|
|
45
|
+
layers,
|
|
46
|
+
primitiveLayers: [],
|
|
47
|
+
layerSubstacks,
|
|
48
|
+
boardRegions: []
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
...model,
|
|
53
|
+
source: {
|
|
54
|
+
...model.source,
|
|
55
|
+
interchangeFormat: format
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Detects the interchange format.
|
|
62
|
+
* @param {string} fileName Source file name.
|
|
63
|
+
* @param {string} text Source text.
|
|
64
|
+
* @returns {'stackup' | 'stackupx'}
|
|
65
|
+
*/
|
|
66
|
+
static #format(fileName, text) {
|
|
67
|
+
const lowerName = String(fileName || '').toLowerCase()
|
|
68
|
+
if (lowerName.endsWith('.stackupx')) return 'stackupx'
|
|
69
|
+
if (
|
|
70
|
+
String(text || '')
|
|
71
|
+
.trimStart()
|
|
72
|
+
.startsWith('<')
|
|
73
|
+
)
|
|
74
|
+
return 'stackupx'
|
|
75
|
+
return 'stackup'
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Converts simple sectioned stackup text into board fields.
|
|
80
|
+
* @param {string} text Source text.
|
|
81
|
+
* @returns {Record<string, string>}
|
|
82
|
+
*/
|
|
83
|
+
static #stackupFields(text) {
|
|
84
|
+
const fields = {}
|
|
85
|
+
let section = ''
|
|
86
|
+
|
|
87
|
+
for (const rawLine of String(text || '').split(/\r?\n/u)) {
|
|
88
|
+
const line = rawLine.trim()
|
|
89
|
+
if (!line || line.startsWith('#')) continue
|
|
90
|
+
const sectionMatch = /^\[([^\]]+)\]$/u.exec(line)
|
|
91
|
+
if (sectionMatch) {
|
|
92
|
+
section = sectionMatch[1]
|
|
93
|
+
continue
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const separator = line.indexOf('=')
|
|
97
|
+
if (separator < 0 || !section) continue
|
|
98
|
+
PcbLayerStackInterchangeParser.#assignSectionField(
|
|
99
|
+
fields,
|
|
100
|
+
section,
|
|
101
|
+
line.slice(0, separator).trim(),
|
|
102
|
+
line.slice(separator + 1).trim()
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return fields
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Converts XML-like stackup text into board fields.
|
|
111
|
+
* @param {string} text Source XML text.
|
|
112
|
+
* @returns {Record<string, string>}
|
|
113
|
+
*/
|
|
114
|
+
static #stackupxFields(text) {
|
|
115
|
+
const fields = {}
|
|
116
|
+
|
|
117
|
+
for (const layer of PcbLayerStackInterchangeParser.#tagFields(text, [
|
|
118
|
+
'Layer'
|
|
119
|
+
])) {
|
|
120
|
+
const index = Number.parseInt(layer.Index || layer.index || '0', 10)
|
|
121
|
+
PcbLayerStackInterchangeParser.#assignLayerFields(
|
|
122
|
+
fields,
|
|
123
|
+
index,
|
|
124
|
+
layer
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
for (const substack of PcbLayerStackInterchangeParser.#tagFields(text, [
|
|
128
|
+
'Substack'
|
|
129
|
+
])) {
|
|
130
|
+
const index = Number.parseInt(
|
|
131
|
+
substack.Index || substack.index || '0',
|
|
132
|
+
10
|
|
133
|
+
)
|
|
134
|
+
PcbLayerStackInterchangeParser.#assignSubstackFields(
|
|
135
|
+
fields,
|
|
136
|
+
index,
|
|
137
|
+
substack
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
for (const branch of PcbLayerStackInterchangeParser.#tagBlocks(text, [
|
|
141
|
+
'Branch'
|
|
142
|
+
])) {
|
|
143
|
+
const index = Number.parseInt(
|
|
144
|
+
branch.fields.Index || branch.fields.index || '0',
|
|
145
|
+
10
|
|
146
|
+
)
|
|
147
|
+
PcbLayerStackInterchangeParser.#assignBranchFields(
|
|
148
|
+
fields,
|
|
149
|
+
index,
|
|
150
|
+
branch.fields,
|
|
151
|
+
branch.body
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
for (const span of PcbLayerStackInterchangeParser.#tagFields(text, [
|
|
155
|
+
'ViaSpan',
|
|
156
|
+
'BackdrillSpan'
|
|
157
|
+
])) {
|
|
158
|
+
const index = Number.parseInt(span.Index || span.index || '0', 10)
|
|
159
|
+
const prefix =
|
|
160
|
+
span.__tagName === 'ViaSpan' ? 'VIASPAN' : 'BACKDRILLSPAN'
|
|
161
|
+
PcbLayerStackInterchangeParser.#assignIndexedFields(
|
|
162
|
+
fields,
|
|
163
|
+
prefix,
|
|
164
|
+
index,
|
|
165
|
+
span
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return fields
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Assigns one sectioned text field.
|
|
174
|
+
* @param {Record<string, string>} fields Target fields.
|
|
175
|
+
* @param {string} section Section name.
|
|
176
|
+
* @param {string} key Key.
|
|
177
|
+
* @param {string} value Value.
|
|
178
|
+
* @returns {void}
|
|
179
|
+
*/
|
|
180
|
+
static #assignSectionField(fields, section, key, value) {
|
|
181
|
+
const match =
|
|
182
|
+
/^(Layer|Substack|Branch|ImpedanceProfile|TransmissionLine|ViaSpan|BackdrillSpan)(\d+)$/u.exec(
|
|
183
|
+
section
|
|
184
|
+
)
|
|
185
|
+
if (!match) return
|
|
186
|
+
|
|
187
|
+
const [, family, indexText] = match
|
|
188
|
+
const index = Number.parseInt(indexText, 10)
|
|
189
|
+
if (family === 'Layer') {
|
|
190
|
+
PcbLayerStackInterchangeParser.#assignLayerField(
|
|
191
|
+
fields,
|
|
192
|
+
index,
|
|
193
|
+
key,
|
|
194
|
+
value
|
|
195
|
+
)
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
if (family === 'Substack') {
|
|
199
|
+
PcbLayerStackInterchangeParser.#assignSubstackField(
|
|
200
|
+
fields,
|
|
201
|
+
index,
|
|
202
|
+
key,
|
|
203
|
+
value
|
|
204
|
+
)
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const prefix =
|
|
209
|
+
family === 'ImpedanceProfile'
|
|
210
|
+
? 'IMPEDANCEPROFILE'
|
|
211
|
+
: family.toUpperCase()
|
|
212
|
+
fields[prefix + index + '_' + key.toUpperCase()] = value
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Assigns XML layer fields.
|
|
217
|
+
* @param {Record<string, string>} fields Target fields.
|
|
218
|
+
* @param {number} index Layer index.
|
|
219
|
+
* @param {Record<string, string>} values Source values.
|
|
220
|
+
* @returns {void}
|
|
221
|
+
*/
|
|
222
|
+
static #assignLayerFields(fields, index, values) {
|
|
223
|
+
for (const [key, value] of Object.entries(values)) {
|
|
224
|
+
PcbLayerStackInterchangeParser.#assignLayerField(
|
|
225
|
+
fields,
|
|
226
|
+
index,
|
|
227
|
+
key,
|
|
228
|
+
value
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Assigns one layer field.
|
|
235
|
+
* @param {Record<string, string>} fields Target fields.
|
|
236
|
+
* @param {number} index Layer index.
|
|
237
|
+
* @param {string} key Source key.
|
|
238
|
+
* @param {string} value Source value.
|
|
239
|
+
* @returns {void}
|
|
240
|
+
*/
|
|
241
|
+
static #assignLayerField(fields, index, key, value) {
|
|
242
|
+
const suffixByKey = {
|
|
243
|
+
Name: 'NAME',
|
|
244
|
+
LayerId: 'LAYERID',
|
|
245
|
+
Kind: 'KIND',
|
|
246
|
+
Material: 'MATERIAL',
|
|
247
|
+
Thickness: 'THICKNESS',
|
|
248
|
+
CopperThickness: 'COPPERTHICKNESS',
|
|
249
|
+
Dk: 'DK',
|
|
250
|
+
Df: 'DF',
|
|
251
|
+
IsAdhesive: 'ISADHESIVE',
|
|
252
|
+
IsStiffener: 'ISSTIFFENER',
|
|
253
|
+
SurfaceFinish: 'SURFACEFINISH',
|
|
254
|
+
SourceRecordId: 'SOURCE_RECORD_ID',
|
|
255
|
+
SourceKeys: 'SOURCE_KEYS',
|
|
256
|
+
StackupxProperties: 'STACKUPX_PROPERTIES'
|
|
257
|
+
}
|
|
258
|
+
const suffix = suffixByKey[key]
|
|
259
|
+
if (!suffix) return
|
|
260
|
+
fields['V9_STACK_LAYER' + index + '_' + suffix] = value
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Assigns XML substack fields.
|
|
265
|
+
* @param {Record<string, string>} fields Target fields.
|
|
266
|
+
* @param {number} index Substack index.
|
|
267
|
+
* @param {Record<string, string>} values Source values.
|
|
268
|
+
* @returns {void}
|
|
269
|
+
*/
|
|
270
|
+
static #assignSubstackFields(fields, index, values) {
|
|
271
|
+
for (const [key, value] of Object.entries(values)) {
|
|
272
|
+
PcbLayerStackInterchangeParser.#assignSubstackField(
|
|
273
|
+
fields,
|
|
274
|
+
index,
|
|
275
|
+
key,
|
|
276
|
+
value
|
|
277
|
+
)
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Assigns one substack field.
|
|
283
|
+
* @param {Record<string, string>} fields Target fields.
|
|
284
|
+
* @param {number} index Substack index.
|
|
285
|
+
* @param {string} key Source key.
|
|
286
|
+
* @param {string} value Source value.
|
|
287
|
+
* @returns {void}
|
|
288
|
+
*/
|
|
289
|
+
static #assignSubstackField(fields, index, key, value) {
|
|
290
|
+
const suffixByKey = {
|
|
291
|
+
Id: 'ID',
|
|
292
|
+
Name: 'NAME',
|
|
293
|
+
IsFlex: 'ISFLEX',
|
|
294
|
+
Layers: 'LAYERS',
|
|
295
|
+
StackType: 'STACKUPX_STACKTYPE'
|
|
296
|
+
}
|
|
297
|
+
const suffix = suffixByKey[key]
|
|
298
|
+
if (!suffix) return
|
|
299
|
+
fields['V9_SUBSTACK' + index + '_' + suffix] = value
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Assigns one branch and nested section/stack fields.
|
|
304
|
+
* @param {Record<string, string>} fields Target fields.
|
|
305
|
+
* @param {number} index Branch index.
|
|
306
|
+
* @param {Record<string, string>} values Branch fields.
|
|
307
|
+
* @param {string} body Branch body.
|
|
308
|
+
* @returns {void}
|
|
309
|
+
*/
|
|
310
|
+
static #assignBranchFields(fields, index, values, body) {
|
|
311
|
+
fields['STACKBRANCH' + index + '_ID'] = values.Id || values.ID || ''
|
|
312
|
+
fields['STACKBRANCH' + index + '_NAME'] = values.Name || ''
|
|
313
|
+
|
|
314
|
+
for (const section of PcbLayerStackInterchangeParser.#tagBlocks(body, [
|
|
315
|
+
'Section'
|
|
316
|
+
])) {
|
|
317
|
+
const sectionIndex = Number.parseInt(
|
|
318
|
+
section.fields.Index || '0',
|
|
319
|
+
10
|
|
320
|
+
)
|
|
321
|
+
const sectionPrefix =
|
|
322
|
+
'STACKBRANCH' + index + '_SECTION' + sectionIndex
|
|
323
|
+
fields[sectionPrefix + '_ID'] = section.fields.Id || ''
|
|
324
|
+
fields[sectionPrefix + '_NAME'] = section.fields.Name || ''
|
|
325
|
+
fields[sectionPrefix + '_PARENTID'] = section.fields.ParentId || ''
|
|
326
|
+
|
|
327
|
+
for (const stack of PcbLayerStackInterchangeParser.#tagFields(
|
|
328
|
+
section.body,
|
|
329
|
+
['Stack']
|
|
330
|
+
)) {
|
|
331
|
+
const stackIndex = Number.parseInt(stack.Index || '0', 10)
|
|
332
|
+
const stackPrefix = sectionPrefix + '_STACK' + stackIndex
|
|
333
|
+
PcbLayerStackInterchangeParser.#assignStackFields(
|
|
334
|
+
fields,
|
|
335
|
+
stackPrefix,
|
|
336
|
+
stack
|
|
337
|
+
)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Assigns branch stack fields.
|
|
344
|
+
* @param {Record<string, string>} fields Target fields.
|
|
345
|
+
* @param {string} prefix Field prefix.
|
|
346
|
+
* @param {Record<string, string>} stack Stack fields.
|
|
347
|
+
* @returns {void}
|
|
348
|
+
*/
|
|
349
|
+
static #assignStackFields(fields, prefix, stack) {
|
|
350
|
+
const keyMap = {
|
|
351
|
+
Ref: 'REF',
|
|
352
|
+
MaterialUsage: 'MATERIALUSAGE',
|
|
353
|
+
Source: 'SOURCE',
|
|
354
|
+
IntrusionLeftBottom: 'INTRUSIONLEFTBOTTOM',
|
|
355
|
+
IntrusionLeftTop: 'INTRUSIONLEFTTOP',
|
|
356
|
+
IntrusionRightBottom: 'INTRUSIONRIGHTBOTTOM',
|
|
357
|
+
IntrusionRightTop: 'INTRUSIONRIGHTTOP'
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
for (const [sourceKey, suffix] of Object.entries(keyMap)) {
|
|
361
|
+
if (stack[sourceKey])
|
|
362
|
+
fields[prefix + '_' + suffix] = stack[sourceKey]
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Assigns indexed span fields.
|
|
368
|
+
* @param {Record<string, string>} fields Target fields.
|
|
369
|
+
* @param {string} prefix Field prefix.
|
|
370
|
+
* @param {number} index Row index.
|
|
371
|
+
* @param {Record<string, string>} values Source values.
|
|
372
|
+
* @returns {void}
|
|
373
|
+
*/
|
|
374
|
+
static #assignIndexedFields(fields, prefix, index, values) {
|
|
375
|
+
const keyMap = {
|
|
376
|
+
Id: 'ID',
|
|
377
|
+
Name: 'NAME',
|
|
378
|
+
StartLayer: 'STARTLAYER',
|
|
379
|
+
EndLayer: 'ENDLAYER',
|
|
380
|
+
TargetStub: 'TARGETSTUB'
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
for (const [sourceKey, suffix] of Object.entries(keyMap)) {
|
|
384
|
+
if (values[sourceKey]) {
|
|
385
|
+
fields[prefix + index + '_' + suffix] = values[sourceKey]
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Extracts XML-like tag fields.
|
|
392
|
+
* @param {string} text Source text.
|
|
393
|
+
* @param {string[]} tagNames Tag names.
|
|
394
|
+
* @returns {Record<string, string>[]}
|
|
395
|
+
*/
|
|
396
|
+
static #tagFields(text, tagNames) {
|
|
397
|
+
return PcbLayerStackInterchangeParser.#tagBlocks(text, tagNames).map(
|
|
398
|
+
(block) => ({
|
|
399
|
+
__tagName: block.tagName,
|
|
400
|
+
...block.fields
|
|
401
|
+
})
|
|
402
|
+
)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* Extracts XML-like tag blocks.
|
|
407
|
+
* @param {string} text Source text.
|
|
408
|
+
* @param {string[]} tagNames Tag names.
|
|
409
|
+
* @returns {{ tagName: string, fields: Record<string, string>, body: string }[]}
|
|
410
|
+
*/
|
|
411
|
+
static #tagBlocks(text, tagNames) {
|
|
412
|
+
const names = tagNames
|
|
413
|
+
.map((tagName) => tagName.replace(/[.*+?^${}()|[\]\\]/gu, '\\$&'))
|
|
414
|
+
.join('|')
|
|
415
|
+
const pattern = new RegExp(
|
|
416
|
+
'<\\s*(' +
|
|
417
|
+
names +
|
|
418
|
+
')\\b([^>]*)>([\\s\\S]*?)<\\/\\s*\\1\\s*>|<\\s*(' +
|
|
419
|
+
names +
|
|
420
|
+
')\\b([^>]*)\\/>',
|
|
421
|
+
'giu'
|
|
422
|
+
)
|
|
423
|
+
const blocks = []
|
|
424
|
+
let match = pattern.exec(text || '')
|
|
425
|
+
while (match) {
|
|
426
|
+
blocks.push({
|
|
427
|
+
tagName: match[1] || match[4],
|
|
428
|
+
fields: PcbLayerStackInterchangeParser.#attributes(
|
|
429
|
+
match[2] || match[5] || ''
|
|
430
|
+
),
|
|
431
|
+
body: match[3] || ''
|
|
432
|
+
})
|
|
433
|
+
match = pattern.exec(text || '')
|
|
434
|
+
}
|
|
435
|
+
return blocks
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Parses XML-like attributes.
|
|
440
|
+
* @param {string} text Attribute text.
|
|
441
|
+
* @returns {Record<string, string>}
|
|
442
|
+
*/
|
|
443
|
+
static #attributes(text) {
|
|
444
|
+
const fields = {}
|
|
445
|
+
const pattern = /([A-Za-z0-9_.:-]+)\s*=\s*("([^"]*)"|'([^']*)')/gu
|
|
446
|
+
let match = pattern.exec(text || '')
|
|
447
|
+
while (match) {
|
|
448
|
+
fields[match[1]] = match[3] ?? match[4] ?? ''
|
|
449
|
+
match = pattern.exec(text || '')
|
|
450
|
+
}
|
|
451
|
+
return fields
|
|
452
|
+
}
|
|
453
|
+
}
|