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,435 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validates that semantic SVG links point back to normalized model entries.
|
|
7
|
+
*/
|
|
8
|
+
export class SvgModelCrossLinkValidator {
|
|
9
|
+
static SCHEMA = 'altium-toolkit.svg-model-cross-link.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Validates semantic SVG data attributes against a normalized model.
|
|
13
|
+
* @param {object} documentModel Normalized schematic or PCB model.
|
|
14
|
+
* @param {string} svgMarkup SVG markup.
|
|
15
|
+
* @returns {object}
|
|
16
|
+
*/
|
|
17
|
+
static validate(documentModel, svgMarkup) {
|
|
18
|
+
return SvgModelCrossLinkValidator.validateSet(documentModel, [
|
|
19
|
+
svgMarkup
|
|
20
|
+
])
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Validates a set of semantic SVG fragments against one normalized model.
|
|
25
|
+
* @param {object} documentModel Normalized schematic or PCB model.
|
|
26
|
+
* @param {string[]} svgMarkups SVG markup strings.
|
|
27
|
+
* @returns {object}
|
|
28
|
+
*/
|
|
29
|
+
static validateSet(documentModel, svgMarkups) {
|
|
30
|
+
const documentKind =
|
|
31
|
+
SvgModelCrossLinkValidator.#documentKind(documentModel)
|
|
32
|
+
const expectedElements =
|
|
33
|
+
SvgModelCrossLinkValidator.#expectedElements(documentModel)
|
|
34
|
+
const expectedByKey = new Map(
|
|
35
|
+
expectedElements.map((element) => [element.elementKey, element])
|
|
36
|
+
)
|
|
37
|
+
const svgElements = (svgMarkups || []).flatMap((svgMarkup) =>
|
|
38
|
+
SvgModelCrossLinkValidator.#svgElements(svgMarkup)
|
|
39
|
+
)
|
|
40
|
+
const renderedKeys = new Set(
|
|
41
|
+
svgElements.map((element) => element.elementKey).filter(Boolean)
|
|
42
|
+
)
|
|
43
|
+
const orphanElements = SvgModelCrossLinkValidator.#orphanElements(
|
|
44
|
+
svgElements,
|
|
45
|
+
expectedByKey,
|
|
46
|
+
documentKind
|
|
47
|
+
)
|
|
48
|
+
const missingElements = SvgModelCrossLinkValidator.#missingElements(
|
|
49
|
+
expectedElements,
|
|
50
|
+
renderedKeys,
|
|
51
|
+
documentModel.diagnostics || []
|
|
52
|
+
)
|
|
53
|
+
const unresolvedReferences =
|
|
54
|
+
SvgModelCrossLinkValidator.#unresolvedReferences(
|
|
55
|
+
documentModel,
|
|
56
|
+
svgElements
|
|
57
|
+
)
|
|
58
|
+
const metadata = SvgModelCrossLinkValidator.#metadataSet(svgMarkups)
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
schema: SvgModelCrossLinkValidator.SCHEMA,
|
|
62
|
+
documentKind,
|
|
63
|
+
summary: {
|
|
64
|
+
svgCount: (svgMarkups || []).length,
|
|
65
|
+
expectedElementCount: expectedElements.length,
|
|
66
|
+
renderedElementCount: renderedKeys.size,
|
|
67
|
+
linkedElementCount:
|
|
68
|
+
expectedElements.length - missingElements.length,
|
|
69
|
+
missingElementCount: missingElements.length,
|
|
70
|
+
orphanElementCount: orphanElements.length,
|
|
71
|
+
unresolvedReferenceCount: unresolvedReferences.length,
|
|
72
|
+
metadataElementCount: metadata.elements.length
|
|
73
|
+
},
|
|
74
|
+
missingElements,
|
|
75
|
+
orphanElements,
|
|
76
|
+
unresolvedReferences,
|
|
77
|
+
metadata
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Determines the model kind for report output.
|
|
83
|
+
* @param {object} documentModel Normalized document model.
|
|
84
|
+
* @returns {'schematic' | 'pcb' | 'unknown'}
|
|
85
|
+
*/
|
|
86
|
+
static #documentKind(documentModel) {
|
|
87
|
+
if (documentModel?.schematic) return 'schematic'
|
|
88
|
+
if (documentModel?.pcb) return 'pcb'
|
|
89
|
+
return 'unknown'
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Builds expected semantic element keys from a normalized model.
|
|
94
|
+
* @param {object} documentModel Normalized document model.
|
|
95
|
+
* @returns {object[]}
|
|
96
|
+
*/
|
|
97
|
+
static #expectedElements(documentModel) {
|
|
98
|
+
if (documentModel?.schematic) {
|
|
99
|
+
return SvgModelCrossLinkValidator.#schematicExpectedElements(
|
|
100
|
+
documentModel.schematic
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
if (documentModel?.pcb) {
|
|
104
|
+
return SvgModelCrossLinkValidator.#pcbExpectedElements(
|
|
105
|
+
documentModel.pcb
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
return []
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Builds expected schematic element descriptors.
|
|
113
|
+
* @param {object} schematic Normalized schematic payload.
|
|
114
|
+
* @returns {object[]}
|
|
115
|
+
*/
|
|
116
|
+
static #schematicExpectedElements(schematic) {
|
|
117
|
+
return SvgModelCrossLinkValidator.#collectionElements('schematic', [
|
|
118
|
+
['lines', 'line', schematic?.lines || []],
|
|
119
|
+
['polygons', 'polygon', schematic?.polygons || []],
|
|
120
|
+
['rectangles', 'rectangle', schematic?.rectangles || []],
|
|
121
|
+
[
|
|
122
|
+
'roundedRectangles',
|
|
123
|
+
'rounded-rectangle',
|
|
124
|
+
schematic?.roundedRectangles || []
|
|
125
|
+
],
|
|
126
|
+
['ellipses', 'ellipse', schematic?.ellipses || []],
|
|
127
|
+
['arcs', 'arc', schematic?.arcs || []],
|
|
128
|
+
['beziers', 'bezier', schematic?.beziers || []],
|
|
129
|
+
['pies', 'pie', schematic?.pies || []],
|
|
130
|
+
['ieeeSymbols', 'ieee-symbol', schematic?.ieeeSymbols || []],
|
|
131
|
+
['texts', 'text', schematic?.texts || []],
|
|
132
|
+
['pins', 'pin', schematic?.pins || []],
|
|
133
|
+
['ports', 'port', schematic?.ports || []],
|
|
134
|
+
['directives', 'directive', schematic?.directives || []]
|
|
135
|
+
])
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Builds expected PCB element descriptors.
|
|
140
|
+
* @param {object} pcb Normalized PCB payload.
|
|
141
|
+
* @returns {object[]}
|
|
142
|
+
*/
|
|
143
|
+
static #pcbExpectedElements(pcb) {
|
|
144
|
+
return SvgModelCrossLinkValidator.#collectionElements('pcb', [
|
|
145
|
+
['polygons', 'polygon', pcb?.polygons || []],
|
|
146
|
+
['fills', 'fill', pcb?.fills || []],
|
|
147
|
+
['tracks', 'track', pcb?.tracks || []],
|
|
148
|
+
['arcs', 'arc', pcb?.arcs || []],
|
|
149
|
+
['vias', 'via', pcb?.vias || []],
|
|
150
|
+
['pads', 'pad', pcb?.pads || []],
|
|
151
|
+
['texts', 'text', pcb?.texts || []],
|
|
152
|
+
['components', 'component', pcb?.components || []]
|
|
153
|
+
])
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Builds descriptors for primitive collections.
|
|
158
|
+
* @param {'schematic' | 'pcb'} prefix SVG element prefix.
|
|
159
|
+
* @param {[string, string, object[]][]} collections Collections to inspect.
|
|
160
|
+
* @returns {object[]}
|
|
161
|
+
*/
|
|
162
|
+
static #collectionElements(prefix, collections) {
|
|
163
|
+
const elements = []
|
|
164
|
+
for (const [collectionKey, primitiveKind, records] of collections) {
|
|
165
|
+
for (const [index, record] of (records || []).entries()) {
|
|
166
|
+
elements.push({
|
|
167
|
+
elementKey: prefix + '-' + primitiveKind + '-' + index,
|
|
168
|
+
collectionKey,
|
|
169
|
+
primitiveKind,
|
|
170
|
+
recordId:
|
|
171
|
+
record?.recordId ??
|
|
172
|
+
record?.sourceRecordId ??
|
|
173
|
+
record?.sourceRecordIndex ??
|
|
174
|
+
''
|
|
175
|
+
})
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return elements
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Extracts SVG elements that carry semantic data attributes.
|
|
183
|
+
* @param {string} svgMarkup SVG markup.
|
|
184
|
+
* @returns {object[]}
|
|
185
|
+
*/
|
|
186
|
+
static #svgElements(svgMarkup) {
|
|
187
|
+
const elements = []
|
|
188
|
+
const tagPattern = /<[^>]+data-element-key="[^"]+"[^>]*>/gu
|
|
189
|
+
let match = tagPattern.exec(String(svgMarkup || ''))
|
|
190
|
+
while (match) {
|
|
191
|
+
const attrs = SvgModelCrossLinkValidator.#dataAttributes(match[0])
|
|
192
|
+
elements.push({
|
|
193
|
+
elementKey: attrs.elementKey || '',
|
|
194
|
+
primitive: attrs.primitive || '',
|
|
195
|
+
component: attrs.component || '',
|
|
196
|
+
net: attrs.net || '',
|
|
197
|
+
pin: attrs.pin || '',
|
|
198
|
+
attrs
|
|
199
|
+
})
|
|
200
|
+
match = tagPattern.exec(String(svgMarkup || ''))
|
|
201
|
+
}
|
|
202
|
+
return elements
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Extracts data attributes from one SVG tag.
|
|
207
|
+
* @param {string} tag SVG tag markup.
|
|
208
|
+
* @returns {Record<string, string>}
|
|
209
|
+
*/
|
|
210
|
+
static #dataAttributes(tag) {
|
|
211
|
+
const attrs = {}
|
|
212
|
+
const attrPattern = /data-([a-z0-9-]+)="([^"]*)"/giu
|
|
213
|
+
let match = attrPattern.exec(tag || '')
|
|
214
|
+
while (match) {
|
|
215
|
+
attrs[SvgModelCrossLinkValidator.#camelCase(match[1])] =
|
|
216
|
+
SvgModelCrossLinkValidator.#decodeEntities(match[2])
|
|
217
|
+
match = attrPattern.exec(tag || '')
|
|
218
|
+
}
|
|
219
|
+
return attrs
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Finds rendered elements not represented by the normalized model.
|
|
224
|
+
* @param {object[]} svgElements Rendered semantic SVG rows.
|
|
225
|
+
* @param {Map<string, object>} expectedByKey Expected element map.
|
|
226
|
+
* @param {'schematic' | 'pcb' | 'unknown'} documentKind Document kind.
|
|
227
|
+
* @returns {object[]}
|
|
228
|
+
*/
|
|
229
|
+
static #orphanElements(svgElements, expectedByKey, documentKind) {
|
|
230
|
+
return svgElements
|
|
231
|
+
.filter(
|
|
232
|
+
(element) =>
|
|
233
|
+
!expectedByKey.has(element.elementKey) &&
|
|
234
|
+
!SvgModelCrossLinkValidator.#isRendererOwnedElement(
|
|
235
|
+
element.elementKey,
|
|
236
|
+
documentKind
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
.map((element) => ({
|
|
240
|
+
elementKey: element.elementKey,
|
|
241
|
+
primitive: element.primitive || ''
|
|
242
|
+
}))
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Finds normalized elements that are not represented in SVG output.
|
|
247
|
+
* @param {object[]} expectedElements Expected model element rows.
|
|
248
|
+
* @param {Set<string>} renderedKeys Rendered SVG element keys.
|
|
249
|
+
* @param {object[]} diagnostics Model diagnostics.
|
|
250
|
+
* @returns {object[]}
|
|
251
|
+
*/
|
|
252
|
+
static #missingElements(expectedElements, renderedKeys, diagnostics) {
|
|
253
|
+
return expectedElements
|
|
254
|
+
.filter(
|
|
255
|
+
(element) =>
|
|
256
|
+
!renderedKeys.has(element.elementKey) &&
|
|
257
|
+
!SvgModelCrossLinkValidator.#hasDiagnostic(
|
|
258
|
+
element,
|
|
259
|
+
diagnostics
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
.map((element) => ({
|
|
263
|
+
elementKey: element.elementKey,
|
|
264
|
+
collectionKey: element.collectionKey,
|
|
265
|
+
primitiveKind: element.primitiveKind,
|
|
266
|
+
recordId: element.recordId || undefined
|
|
267
|
+
}))
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Checks whether a missing element has an explicit diagnostic.
|
|
272
|
+
* @param {object} element Expected element row.
|
|
273
|
+
* @param {object[]} diagnostics Model diagnostics.
|
|
274
|
+
* @returns {boolean}
|
|
275
|
+
*/
|
|
276
|
+
static #hasDiagnostic(element, diagnostics) {
|
|
277
|
+
return (diagnostics || []).some((diagnostic) => {
|
|
278
|
+
const message = String(diagnostic?.message || '')
|
|
279
|
+
return (
|
|
280
|
+
diagnostic?.elementKey === element.elementKey ||
|
|
281
|
+
(element.recordId &&
|
|
282
|
+
diagnostic?.recordId === element.recordId) ||
|
|
283
|
+
message.includes(element.elementKey)
|
|
284
|
+
)
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Finds component/net references that cannot be resolved in the model.
|
|
290
|
+
* @param {object} documentModel Normalized document model.
|
|
291
|
+
* @param {object[]} svgElements Rendered semantic SVG rows.
|
|
292
|
+
* @returns {object[]}
|
|
293
|
+
*/
|
|
294
|
+
static #unresolvedReferences(documentModel, svgElements) {
|
|
295
|
+
const components =
|
|
296
|
+
SvgModelCrossLinkValidator.#componentNames(documentModel)
|
|
297
|
+
const nets = SvgModelCrossLinkValidator.#netNames(documentModel)
|
|
298
|
+
const unresolved = []
|
|
299
|
+
|
|
300
|
+
for (const element of svgElements) {
|
|
301
|
+
if (element.component && !components.has(element.component)) {
|
|
302
|
+
unresolved.push({
|
|
303
|
+
elementKey: element.elementKey,
|
|
304
|
+
referenceKind: 'component',
|
|
305
|
+
value: element.component
|
|
306
|
+
})
|
|
307
|
+
}
|
|
308
|
+
if (element.net && !nets.has(element.net)) {
|
|
309
|
+
unresolved.push({
|
|
310
|
+
elementKey: element.elementKey,
|
|
311
|
+
referenceKind: 'net',
|
|
312
|
+
value: element.net
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return unresolved
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Collects normalized component designators.
|
|
322
|
+
* @param {object} documentModel Normalized document model.
|
|
323
|
+
* @returns {Set<string>}
|
|
324
|
+
*/
|
|
325
|
+
static #componentNames(documentModel) {
|
|
326
|
+
const components =
|
|
327
|
+
documentModel?.schematic?.components ||
|
|
328
|
+
documentModel?.pcb?.components ||
|
|
329
|
+
[]
|
|
330
|
+
return new Set(
|
|
331
|
+
components
|
|
332
|
+
.map((component) => String(component?.designator || ''))
|
|
333
|
+
.filter(Boolean)
|
|
334
|
+
)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Collects normalized net names.
|
|
339
|
+
* @param {object} documentModel Normalized document model.
|
|
340
|
+
* @returns {Set<string>}
|
|
341
|
+
*/
|
|
342
|
+
static #netNames(documentModel) {
|
|
343
|
+
const nets =
|
|
344
|
+
documentModel?.schematic?.nets || documentModel?.pcb?.nets || []
|
|
345
|
+
return new Set(
|
|
346
|
+
nets.map((net) => String(net?.name || '')).filter(Boolean)
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Returns true for renderer-owned semantic helpers.
|
|
352
|
+
* @param {string} elementKey SVG element key.
|
|
353
|
+
* @param {'schematic' | 'pcb' | 'unknown'} documentKind Document kind.
|
|
354
|
+
* @returns {boolean}
|
|
355
|
+
*/
|
|
356
|
+
static #isRendererOwnedElement(elementKey, documentKind) {
|
|
357
|
+
if (documentKind !== 'pcb') return false
|
|
358
|
+
return (
|
|
359
|
+
elementKey === 'pcb-board-outline' ||
|
|
360
|
+
elementKey === 'pcb-board-outline-stroke' ||
|
|
361
|
+
/^pcb-board-cutout-\d+$/u.test(elementKey) ||
|
|
362
|
+
/^pcb-(via|pad)-hole-\d+$/u.test(elementKey)
|
|
363
|
+
)
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Extracts the semantic metadata JSON sidecar when present.
|
|
368
|
+
* @param {string} svgMarkup SVG markup.
|
|
369
|
+
* @returns {{ schema: string, elements: object[] }}
|
|
370
|
+
*/
|
|
371
|
+
static #metadata(svgMarkup) {
|
|
372
|
+
const match = String(svgMarkup || '').match(
|
|
373
|
+
/<metadata id="(?:schematic|pcb)-semantic-metadata"[^>]*>([^<]*)<\/metadata>/u
|
|
374
|
+
)
|
|
375
|
+
if (!match) {
|
|
376
|
+
return { schema: '', elements: [] }
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
const metadata = JSON.parse(
|
|
380
|
+
SvgModelCrossLinkValidator.#decodeEntities(match[1])
|
|
381
|
+
)
|
|
382
|
+
return {
|
|
383
|
+
schema: metadata.schema || '',
|
|
384
|
+
elements: Array.isArray(metadata.elements)
|
|
385
|
+
? metadata.elements
|
|
386
|
+
: []
|
|
387
|
+
}
|
|
388
|
+
} catch {
|
|
389
|
+
return { schema: '', elements: [] }
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Extracts semantic metadata from a set of SVG fragments.
|
|
395
|
+
* @param {string[]} svgMarkups SVG markup strings.
|
|
396
|
+
* @returns {{ schema: string, elements: object[] }}
|
|
397
|
+
*/
|
|
398
|
+
static #metadataSet(svgMarkups) {
|
|
399
|
+
const metadataRows = (svgMarkups || []).map((svgMarkup) =>
|
|
400
|
+
SvgModelCrossLinkValidator.#metadata(svgMarkup)
|
|
401
|
+
)
|
|
402
|
+
const schema =
|
|
403
|
+
metadataRows.find((metadata) => metadata.schema)?.schema || ''
|
|
404
|
+
|
|
405
|
+
return {
|
|
406
|
+
schema,
|
|
407
|
+
elements: metadataRows.flatMap((metadata) => metadata.elements)
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Converts a data attribute token to a camelCase object key.
|
|
413
|
+
* @param {string} value Attribute token.
|
|
414
|
+
* @returns {string}
|
|
415
|
+
*/
|
|
416
|
+
static #camelCase(value) {
|
|
417
|
+
return String(value || '').replace(/-([a-z0-9])/giu, (_match, char) =>
|
|
418
|
+
String(char).toUpperCase()
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Decodes basic XML entities.
|
|
424
|
+
* @param {string} value Encoded value.
|
|
425
|
+
* @returns {string}
|
|
426
|
+
*/
|
|
427
|
+
static #decodeEntities(value) {
|
|
428
|
+
return String(value || '')
|
|
429
|
+
.replace(/"/gu, '"')
|
|
430
|
+
.replace(/'/gu, "'")
|
|
431
|
+
.replace(/</gu, '<')
|
|
432
|
+
.replace(/>/gu, '>')
|
|
433
|
+
.replace(/&/gu, '&')
|
|
434
|
+
}
|
|
435
|
+
}
|