altium-toolkit 1.0.8 → 1.0.10
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 +18 -6
- package/docs/api.md +78 -16
- package/docs/model-format.md +229 -8
- package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +76 -0
- package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +35 -0
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +53 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1826 -110
- package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +86 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
- package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
- package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
- package/docs/testing.md +9 -3
- package/package.json +1 -1
- package/spec/library-scope.md +7 -1
- package/src/core/altium/AltiumLayoutParser.mjs +104 -8
- package/src/core/altium/AltiumParser.mjs +196 -45
- package/src/core/altium/CiArtifactBundleBuilder.mjs +202 -0
- package/src/core/altium/DraftsmanDigestParser.mjs +689 -0
- package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
- package/src/core/altium/IntLibModelParser.mjs +240 -0
- package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
- package/src/core/altium/LibrarySearchIndex.mjs +215 -0
- package/src/core/altium/NormalizedModelSchema.mjs +36 -0
- package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
- package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
- package/src/core/altium/PcbDefaultsParser.mjs +171 -0
- package/src/core/altium/PcbDimensionParser.mjs +229 -0
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
- package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
- package/src/core/altium/PcbLibModelParser.mjs +235 -14
- package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
- package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
- package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
- package/src/core/altium/PcbModelParser.mjs +495 -32
- package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
- package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
- package/src/core/altium/PcbPadStackParser.mjs +229 -2
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +224 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
- package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +76 -3
- package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
- package/src/core/altium/PcbRuleParser.mjs +354 -33
- package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
- package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +541 -0
- package/src/core/altium/PcbStreamExtractor.mjs +111 -4
- package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
- package/src/core/altium/PcbUnionParser.mjs +307 -0
- package/src/core/altium/PcbViaStackParser.mjs +98 -10
- package/src/core/altium/PcbViaStructureParser.mjs +335 -0
- package/src/core/altium/PrintableTextDecoder.mjs +53 -3
- package/src/core/altium/PrjPcbModelParser.mjs +281 -7
- package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +492 -0
- package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +503 -0
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
- package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
- package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
- package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
- package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
- package/src/core/altium/SchematicHarnessParser.mjs +302 -0
- package/src/core/altium/SchematicImageParser.mjs +474 -3
- package/src/core/altium/SchematicImplementationParser.mjs +518 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
- package/src/core/altium/SchematicPinParser.mjs +84 -1
- package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
- package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
- package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
- package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
- package/src/core/altium/SchematicTemplateParser.mjs +256 -0
- package/src/core/altium/SchematicTextParser.mjs +123 -0
- package/src/core/altium/SvgModelCrossLinkValidator.mjs +402 -0
- package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +136 -96
- package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
- package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
- package/src/core/ole/OleCompoundDocument.mjs +20 -0
- package/src/parser.mjs +35 -0
- package/src/styles/altium-renderers.css +19 -0
- package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
- package/src/ui/PcbInteractionIndex.mjs +9 -4
- package/src/ui/PcbScene3dBuilder.mjs +137 -3
- package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
- package/src/ui/PcbSvgRenderer.mjs +1252 -34
- package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
- package/src/ui/SchematicNoteRenderer.mjs +9 -2
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
- package/src/ui/SchematicShapeRenderer.mjs +362 -0
- package/src/ui/SchematicSvgRenderer.mjs +1442 -92
- package/src/ui/SchematicTypography.mjs +48 -5
- package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolves schematic-visible project parameters and special strings.
|
|
7
|
+
*/
|
|
8
|
+
export class SchematicProjectParameterResolver {
|
|
9
|
+
static #TITLE_BLOCK_FIELDS = [
|
|
10
|
+
'title',
|
|
11
|
+
'revision',
|
|
12
|
+
'documentNumber',
|
|
13
|
+
'sheetNumber',
|
|
14
|
+
'sheetTotal',
|
|
15
|
+
'date',
|
|
16
|
+
'drawnBy'
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolves one schematic text expression against project parameters.
|
|
21
|
+
* @param {string} text Raw schematic text.
|
|
22
|
+
* @param {Record<string, string | number | boolean | null | undefined>} parameters Project parameters.
|
|
23
|
+
* @returns {{ rawText: string, resolvedText: string, parameterNames: string[], expressionParts: object[] } | null}
|
|
24
|
+
*/
|
|
25
|
+
static resolveText(text, parameters = {}) {
|
|
26
|
+
const rawText = String(text ?? '')
|
|
27
|
+
const lookup =
|
|
28
|
+
SchematicProjectParameterResolver.#buildParameterLookup(parameters)
|
|
29
|
+
const expressionParts =
|
|
30
|
+
SchematicProjectParameterResolver.#parseExpressionParts(
|
|
31
|
+
rawText,
|
|
32
|
+
lookup
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
if (
|
|
36
|
+
!expressionParts.some((part) => part.kind === 'parameter') ||
|
|
37
|
+
!expressionParts.length
|
|
38
|
+
) {
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const parameterNames = [
|
|
43
|
+
...new Set(
|
|
44
|
+
expressionParts
|
|
45
|
+
.filter((part) => part.kind === 'parameter')
|
|
46
|
+
.map((part) => part.name)
|
|
47
|
+
)
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
rawText,
|
|
52
|
+
resolvedText: expressionParts
|
|
53
|
+
.map((part) => String(part.value ?? ''))
|
|
54
|
+
.join(''),
|
|
55
|
+
parameterNames,
|
|
56
|
+
expressionParts
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns a schematic copy with project-parameter annotations.
|
|
62
|
+
* @param {object} schematic Normalized schematic model.
|
|
63
|
+
* @param {Record<string, string | number | boolean | null | undefined>} parameters Project parameters.
|
|
64
|
+
* @param {{ replaceText?: boolean }} options Resolver options.
|
|
65
|
+
* @returns {object}
|
|
66
|
+
*/
|
|
67
|
+
static applyToSchematic(schematic, parameters = {}, options = {}) {
|
|
68
|
+
const resolvedTexts = Array.isArray(schematic?.texts)
|
|
69
|
+
? schematic.texts.map((text) =>
|
|
70
|
+
SchematicProjectParameterResolver.#annotateText(
|
|
71
|
+
text,
|
|
72
|
+
parameters,
|
|
73
|
+
options
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
: []
|
|
77
|
+
const sheet = schematic?.sheet
|
|
78
|
+
? {
|
|
79
|
+
...schematic.sheet,
|
|
80
|
+
titleBlock:
|
|
81
|
+
SchematicProjectParameterResolver.#annotateTitleBlock(
|
|
82
|
+
schematic.sheet.titleBlock,
|
|
83
|
+
parameters,
|
|
84
|
+
options
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
: schematic?.sheet
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
...(schematic || {}),
|
|
91
|
+
...(sheet ? { sheet } : {}),
|
|
92
|
+
texts: resolvedTexts
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Returns a document-model copy with project-parameter annotations.
|
|
98
|
+
* @param {object} documentModel Normalized document model.
|
|
99
|
+
* @param {Record<string, string | number | boolean | null | undefined>} parameters Project parameters.
|
|
100
|
+
* @param {{ replaceText?: boolean }} options Resolver options.
|
|
101
|
+
* @returns {object}
|
|
102
|
+
*/
|
|
103
|
+
static applyToDocumentModel(documentModel, parameters = {}, options = {}) {
|
|
104
|
+
if (!documentModel?.schematic) {
|
|
105
|
+
return documentModel
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
...documentModel,
|
|
110
|
+
schematic: SchematicProjectParameterResolver.applyToSchematic(
|
|
111
|
+
documentModel.schematic,
|
|
112
|
+
parameters,
|
|
113
|
+
options
|
|
114
|
+
)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Builds a case-insensitive project-parameter lookup.
|
|
120
|
+
* @param {Record<string, string | number | boolean | null | undefined>} parameters Project parameters.
|
|
121
|
+
* @returns {Map<string, { name: string, value: string }>}
|
|
122
|
+
*/
|
|
123
|
+
static #buildParameterLookup(parameters) {
|
|
124
|
+
return new Map(
|
|
125
|
+
Object.entries(parameters || {}).map(([name, value]) => [
|
|
126
|
+
name.trim().toLowerCase(),
|
|
127
|
+
{
|
|
128
|
+
name,
|
|
129
|
+
value:
|
|
130
|
+
value === null || value === undefined
|
|
131
|
+
? ''
|
|
132
|
+
: String(value)
|
|
133
|
+
}
|
|
134
|
+
])
|
|
135
|
+
)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Parses one parameter expression into renderable parts.
|
|
140
|
+
* @param {string} text Raw text.
|
|
141
|
+
* @param {Map<string, { name: string, value: string }>} lookup Parameter lookup.
|
|
142
|
+
* @returns {object[]}
|
|
143
|
+
*/
|
|
144
|
+
static #parseExpressionParts(text, lookup) {
|
|
145
|
+
const tokens =
|
|
146
|
+
SchematicProjectParameterResolver.#splitConcatenation(text)
|
|
147
|
+
|
|
148
|
+
if (tokens.length > 1) {
|
|
149
|
+
return tokens.flatMap((token) =>
|
|
150
|
+
SchematicProjectParameterResolver.#parseToken(token, lookup)
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return SchematicProjectParameterResolver.#parseInlineText(text, lookup)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Parses one concatenation token.
|
|
159
|
+
* @param {string} token Expression token.
|
|
160
|
+
* @param {Map<string, { name: string, value: string }>} lookup Parameter lookup.
|
|
161
|
+
* @returns {object[]}
|
|
162
|
+
*/
|
|
163
|
+
static #parseToken(token, lookup) {
|
|
164
|
+
const trimmed = token.trim()
|
|
165
|
+
const quoted = trimmed.match(/^"((?:[^"\\]|\\.)*)"$/u)
|
|
166
|
+
|
|
167
|
+
if (quoted) {
|
|
168
|
+
return [
|
|
169
|
+
{
|
|
170
|
+
kind: 'literal',
|
|
171
|
+
value: SchematicProjectParameterResolver.#unescapeQuoted(
|
|
172
|
+
quoted[1]
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
]
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const directParameter = trimmed.match(/^[.=]([A-Za-z_][\w.-]*)$/u)
|
|
179
|
+
if (directParameter) {
|
|
180
|
+
return [
|
|
181
|
+
SchematicProjectParameterResolver.#parameterPart(
|
|
182
|
+
directParameter[1],
|
|
183
|
+
trimmed[0],
|
|
184
|
+
lookup
|
|
185
|
+
)
|
|
186
|
+
]
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return SchematicProjectParameterResolver.#parseInlineText(token, lookup)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Parses inline text and parameter references.
|
|
194
|
+
* @param {string} text Raw text.
|
|
195
|
+
* @param {Map<string, { name: string, value: string }>} lookup Parameter lookup.
|
|
196
|
+
* @returns {object[]}
|
|
197
|
+
*/
|
|
198
|
+
static #parseInlineText(text, lookup) {
|
|
199
|
+
const parts = []
|
|
200
|
+
const pattern = /([.=])([A-Za-z_][\w.-]*)/gu
|
|
201
|
+
let lastIndex = 0
|
|
202
|
+
let match = pattern.exec(text)
|
|
203
|
+
|
|
204
|
+
while (match) {
|
|
205
|
+
if (match.index > lastIndex) {
|
|
206
|
+
parts.push({
|
|
207
|
+
kind: 'literal',
|
|
208
|
+
value: text.slice(lastIndex, match.index)
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
parts.push(
|
|
213
|
+
SchematicProjectParameterResolver.#parameterPart(
|
|
214
|
+
match[2],
|
|
215
|
+
match[1],
|
|
216
|
+
lookup
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
lastIndex = pattern.lastIndex
|
|
220
|
+
match = pattern.exec(text)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (lastIndex < text.length) {
|
|
224
|
+
parts.push({ kind: 'literal', value: text.slice(lastIndex) })
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return parts
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Builds one parameter expression part.
|
|
232
|
+
* @param {string} name Parameter name.
|
|
233
|
+
* @param {string} prefix Source prefix.
|
|
234
|
+
* @param {Map<string, { name: string, value: string }>} lookup Parameter lookup.
|
|
235
|
+
* @returns {object}
|
|
236
|
+
*/
|
|
237
|
+
static #parameterPart(name, prefix, lookup) {
|
|
238
|
+
const resolved = lookup.get(String(name || '').toLowerCase())
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
kind: 'parameter',
|
|
242
|
+
name: resolved?.name || name,
|
|
243
|
+
value: resolved?.value ?? prefix + name
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Annotates one schematic text object.
|
|
249
|
+
* @param {object} text Text primitive.
|
|
250
|
+
* @param {Record<string, string | number | boolean | null | undefined>} parameters Project parameters.
|
|
251
|
+
* @param {{ replaceText?: boolean }} options Resolver options.
|
|
252
|
+
* @returns {object}
|
|
253
|
+
*/
|
|
254
|
+
static #annotateText(text, parameters, options) {
|
|
255
|
+
const resolved = SchematicProjectParameterResolver.resolveText(
|
|
256
|
+
text?.text,
|
|
257
|
+
parameters
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
if (!resolved) {
|
|
261
|
+
return { ...(text || {}) }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
...(text || {}),
|
|
266
|
+
rawText: resolved.rawText,
|
|
267
|
+
resolvedText: resolved.resolvedText,
|
|
268
|
+
specialString: resolved,
|
|
269
|
+
...(options.replaceText ? { text: resolved.resolvedText } : {})
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Annotates title-block fields with project-parameter resolution details.
|
|
275
|
+
* @param {object | undefined} titleBlock Title-block object.
|
|
276
|
+
* @param {Record<string, string | number | boolean | null | undefined>} parameters Project parameters.
|
|
277
|
+
* @param {{ replaceText?: boolean }} options Resolver options.
|
|
278
|
+
* @returns {object | undefined}
|
|
279
|
+
*/
|
|
280
|
+
static #annotateTitleBlock(titleBlock, parameters, options) {
|
|
281
|
+
if (!titleBlock) {
|
|
282
|
+
return titleBlock
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
const annotated = { ...titleBlock }
|
|
286
|
+
const specialStrings = { ...(titleBlock.specialStrings || {}) }
|
|
287
|
+
|
|
288
|
+
for (const fieldName of SchematicProjectParameterResolver
|
|
289
|
+
.#TITLE_BLOCK_FIELDS) {
|
|
290
|
+
const resolved = SchematicProjectParameterResolver.resolveText(
|
|
291
|
+
titleBlock[fieldName],
|
|
292
|
+
parameters
|
|
293
|
+
)
|
|
294
|
+
if (!resolved) {
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
specialStrings[fieldName] = resolved
|
|
299
|
+
if (options.replaceText) {
|
|
300
|
+
annotated[fieldName] = resolved.resolvedText
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return Object.keys(specialStrings).length
|
|
305
|
+
? { ...annotated, specialStrings }
|
|
306
|
+
: annotated
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Splits one string by top-level concatenation operators.
|
|
311
|
+
* @param {string} text Raw expression.
|
|
312
|
+
* @returns {string[]}
|
|
313
|
+
*/
|
|
314
|
+
static #splitConcatenation(text) {
|
|
315
|
+
const tokens = []
|
|
316
|
+
let token = ''
|
|
317
|
+
let inQuote = false
|
|
318
|
+
let escaped = false
|
|
319
|
+
|
|
320
|
+
for (const char of String(text || '')) {
|
|
321
|
+
if (escaped) {
|
|
322
|
+
token += char
|
|
323
|
+
escaped = false
|
|
324
|
+
continue
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (char === '\\') {
|
|
328
|
+
token += char
|
|
329
|
+
escaped = true
|
|
330
|
+
continue
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (char === '"') {
|
|
334
|
+
inQuote = !inQuote
|
|
335
|
+
token += char
|
|
336
|
+
continue
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
if (char === '+' && !inQuote) {
|
|
340
|
+
tokens.push(token)
|
|
341
|
+
token = ''
|
|
342
|
+
continue
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
token += char
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
tokens.push(token)
|
|
349
|
+
|
|
350
|
+
return tokens
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Unescapes the subset of quoted string escapes used by parameter fields.
|
|
355
|
+
* @param {string} text Quoted string body.
|
|
356
|
+
* @returns {string}
|
|
357
|
+
*/
|
|
358
|
+
static #unescapeQuoted(text) {
|
|
359
|
+
return String(text || '').replace(/\\(["\\])/gu, '$1')
|
|
360
|
+
}
|
|
361
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { ParserUtils } from './ParserUtils.mjs'
|
|
6
|
+
|
|
7
|
+
const { getDisplayText, getField, parseBoolean, parseNumericField, toColor } =
|
|
8
|
+
ParserUtils
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Builds deterministic read-only QA summaries for schematic documents.
|
|
12
|
+
*/
|
|
13
|
+
export class SchematicQaReportBuilder {
|
|
14
|
+
static SCHEMA_ID = 'altium-toolkit.schematic.qa.a1'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Builds a schematic QA report from parsed records and geometry.
|
|
18
|
+
* @param {{ records: object[], sheet?: object, lines?: object[], texts?: object[] }} input QA input.
|
|
19
|
+
* @returns {object}
|
|
20
|
+
*/
|
|
21
|
+
static build(input) {
|
|
22
|
+
const records = input?.records || []
|
|
23
|
+
const fonts = SchematicQaReportBuilder.#fonts(input?.sheet)
|
|
24
|
+
const colors = SchematicQaReportBuilder.#colors(records)
|
|
25
|
+
const lineWidths = SchematicQaReportBuilder.#lineWidths(records)
|
|
26
|
+
const unresolvedParameters =
|
|
27
|
+
SchematicQaReportBuilder.#unresolvedParameters(records)
|
|
28
|
+
const findings = [
|
|
29
|
+
...SchematicQaReportBuilder.#fontFindings(fonts),
|
|
30
|
+
...SchematicQaReportBuilder.#unresolvedFindings(
|
|
31
|
+
unresolvedParameters
|
|
32
|
+
),
|
|
33
|
+
...SchematicQaReportBuilder.#titleBlockFindings(records)
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
schema: SchematicQaReportBuilder.SCHEMA_ID,
|
|
38
|
+
summary: {
|
|
39
|
+
recordCount: records.length,
|
|
40
|
+
fontFamilyCount: fonts.families.length,
|
|
41
|
+
colorCount: colors.values.length,
|
|
42
|
+
lineWidthCount: lineWidths.values.length,
|
|
43
|
+
unresolvedParameterCount: unresolvedParameters.length,
|
|
44
|
+
findingCount: findings.length
|
|
45
|
+
},
|
|
46
|
+
fonts,
|
|
47
|
+
colors,
|
|
48
|
+
lineWidths,
|
|
49
|
+
unresolvedParameters,
|
|
50
|
+
titleBlockResidue:
|
|
51
|
+
SchematicQaReportBuilder.#titleBlockResidue(records),
|
|
52
|
+
geometryFallbacks: SchematicQaReportBuilder.#geometryFallbacks(
|
|
53
|
+
input?.texts
|
|
54
|
+
),
|
|
55
|
+
findings
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Summarizes sheet fonts.
|
|
61
|
+
* @param {object | undefined} sheet Parsed sheet model.
|
|
62
|
+
* @returns {{ families: string[], entries: object[] }}
|
|
63
|
+
*/
|
|
64
|
+
static #fonts(sheet) {
|
|
65
|
+
const entries = Object.entries(sheet?.fonts || {})
|
|
66
|
+
.map(([fontId, font]) => ({
|
|
67
|
+
fontId,
|
|
68
|
+
family: String(font?.family || '').trim(),
|
|
69
|
+
size: Number(font?.size || 0),
|
|
70
|
+
bold: font?.bold === true,
|
|
71
|
+
italic: font?.italic === true
|
|
72
|
+
}))
|
|
73
|
+
.filter((font) => font.family)
|
|
74
|
+
.sort(
|
|
75
|
+
(left, right) =>
|
|
76
|
+
left.family.localeCompare(right.family) ||
|
|
77
|
+
String(left.fontId).localeCompare(String(right.fontId))
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
families: [...new Set(entries.map((font) => font.family))],
|
|
82
|
+
entries
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Summarizes schematic colors.
|
|
88
|
+
* @param {object[]} records Schematic records.
|
|
89
|
+
* @returns {{ values: string[], records: object[] }}
|
|
90
|
+
*/
|
|
91
|
+
static #colors(records) {
|
|
92
|
+
const rows = []
|
|
93
|
+
|
|
94
|
+
for (const record of records || []) {
|
|
95
|
+
for (const key of ['Color', 'TextColor', 'AreaColor']) {
|
|
96
|
+
if (!(key in (record.fields || {}))) {
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
rows.push({
|
|
101
|
+
recordKey: SchematicQaReportBuilder.#recordKey(record),
|
|
102
|
+
field: key,
|
|
103
|
+
color: toColor(record.fields[key], '#000000')
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
values: [...new Set(rows.map((row) => row.color))].sort(),
|
|
110
|
+
records: rows
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Summarizes authored line widths.
|
|
116
|
+
* @param {object[]} records Schematic records.
|
|
117
|
+
* @returns {{ values: number[], records: object[] }}
|
|
118
|
+
*/
|
|
119
|
+
static #lineWidths(records) {
|
|
120
|
+
const rows = (records || [])
|
|
121
|
+
.map((record) => ({
|
|
122
|
+
recordKey: SchematicQaReportBuilder.#recordKey(record),
|
|
123
|
+
width: parseNumericField(record.fields, 'LineWidth')
|
|
124
|
+
}))
|
|
125
|
+
.filter((row) => row.width !== null)
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
values: [...new Set(rows.map((row) => row.width))].sort(
|
|
129
|
+
(left, right) => left - right
|
|
130
|
+
),
|
|
131
|
+
records: rows
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Collects unresolved `=Parameter` placeholders.
|
|
137
|
+
* @param {object[]} records Schematic records.
|
|
138
|
+
* @returns {string[]}
|
|
139
|
+
*/
|
|
140
|
+
static #unresolvedParameters(records) {
|
|
141
|
+
const metadata = new Set(
|
|
142
|
+
(records || [])
|
|
143
|
+
.filter((record) => getField(record.fields, 'RECORD') === '41')
|
|
144
|
+
.map((record) => getField(record.fields, 'Name'))
|
|
145
|
+
.filter(Boolean)
|
|
146
|
+
)
|
|
147
|
+
const unresolved = []
|
|
148
|
+
|
|
149
|
+
for (const record of records || []) {
|
|
150
|
+
const text = getDisplayText(record.fields)
|
|
151
|
+
const match = text.match(/^=([A-Za-z_][\w.]*)$/)
|
|
152
|
+
if (!match) {
|
|
153
|
+
continue
|
|
154
|
+
}
|
|
155
|
+
if (
|
|
156
|
+
!metadata.has(match[1]) ||
|
|
157
|
+
text === getDisplayText(record.fields)
|
|
158
|
+
) {
|
|
159
|
+
unresolved.push(match[1])
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return [...new Set(unresolved)].sort()
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Builds nonstandard-font findings.
|
|
168
|
+
* @param {{ families: string[] }} fonts Font summary.
|
|
169
|
+
* @returns {object[]}
|
|
170
|
+
*/
|
|
171
|
+
static #fontFindings(fonts) {
|
|
172
|
+
const preferred = new Set(['Arial', 'Times New Roman'])
|
|
173
|
+
|
|
174
|
+
return (fonts.families || [])
|
|
175
|
+
.filter((family) => !preferred.has(family))
|
|
176
|
+
.map((family) => ({
|
|
177
|
+
code: 'schematic.font.nonstandard-family',
|
|
178
|
+
severity: 'info',
|
|
179
|
+
family,
|
|
180
|
+
message:
|
|
181
|
+
'Schematic uses a font family outside the default parser baseline.'
|
|
182
|
+
}))
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Builds unresolved-parameter findings.
|
|
187
|
+
* @param {string[]} parameters Unresolved parameters.
|
|
188
|
+
* @returns {object[]}
|
|
189
|
+
*/
|
|
190
|
+
static #unresolvedFindings(parameters) {
|
|
191
|
+
return (parameters || []).map((parameter) => ({
|
|
192
|
+
code: 'schematic.text.unresolved-parameter',
|
|
193
|
+
severity: 'warning',
|
|
194
|
+
parameter,
|
|
195
|
+
message:
|
|
196
|
+
'Schematic text contains an unresolved project or document parameter.'
|
|
197
|
+
}))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Builds title-block findings.
|
|
202
|
+
* @param {object[]} records Schematic records.
|
|
203
|
+
* @returns {object[]}
|
|
204
|
+
*/
|
|
205
|
+
static #titleBlockFindings(records) {
|
|
206
|
+
const sheet = (records || []).find(
|
|
207
|
+
(record) => getField(record.fields, 'RECORD') === '31'
|
|
208
|
+
)
|
|
209
|
+
if (!sheet || parseBoolean(sheet.fields.TitleBlockOn)) {
|
|
210
|
+
return []
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const residue = SchematicQaReportBuilder.#titleBlockResidue(records)
|
|
214
|
+
return residue.length
|
|
215
|
+
? [
|
|
216
|
+
{
|
|
217
|
+
code: 'schematic.title-block.hidden-residue',
|
|
218
|
+
severity: 'info',
|
|
219
|
+
count: residue.length,
|
|
220
|
+
message:
|
|
221
|
+
'Hidden title-block parameter records remain while the title block is disabled.'
|
|
222
|
+
}
|
|
223
|
+
]
|
|
224
|
+
: []
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Collects hidden title-block-like parameter rows.
|
|
229
|
+
* @param {object[]} records Schematic records.
|
|
230
|
+
* @returns {object[]}
|
|
231
|
+
*/
|
|
232
|
+
static #titleBlockResidue(records) {
|
|
233
|
+
const titleBlockNames = new Set([
|
|
234
|
+
'title',
|
|
235
|
+
'revision',
|
|
236
|
+
'documentnumber',
|
|
237
|
+
'sheetnumber',
|
|
238
|
+
'sheettotal',
|
|
239
|
+
'date',
|
|
240
|
+
'drawnby'
|
|
241
|
+
])
|
|
242
|
+
|
|
243
|
+
return (records || [])
|
|
244
|
+
.filter(
|
|
245
|
+
(record) =>
|
|
246
|
+
getField(record.fields, 'RECORD') === '41' &&
|
|
247
|
+
parseBoolean(record.fields.IsHidden) &&
|
|
248
|
+
titleBlockNames.has(
|
|
249
|
+
getField(record.fields, 'Name')
|
|
250
|
+
.replace(/\s+/g, '')
|
|
251
|
+
.toLowerCase()
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
.map((record) => ({
|
|
255
|
+
recordKey: SchematicQaReportBuilder.#recordKey(record),
|
|
256
|
+
name: getField(record.fields, 'Name'),
|
|
257
|
+
value: getDisplayText(record.fields)
|
|
258
|
+
}))
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Collects text geometry fallback diagnostics already attached by parsers.
|
|
263
|
+
* @param {object[] | undefined} texts Parsed text rows.
|
|
264
|
+
* @returns {object[]}
|
|
265
|
+
*/
|
|
266
|
+
static #geometryFallbacks(texts) {
|
|
267
|
+
return (texts || [])
|
|
268
|
+
.filter((text) => text?.diagnosticState)
|
|
269
|
+
.map((text, index) => ({
|
|
270
|
+
key: 'schematic-text-' + index,
|
|
271
|
+
state: text.diagnosticState,
|
|
272
|
+
text: text.text || ''
|
|
273
|
+
}))
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Builds a stable schematic record key.
|
|
278
|
+
* @param {object} record Schematic record.
|
|
279
|
+
* @returns {string}
|
|
280
|
+
*/
|
|
281
|
+
static #recordKey(record) {
|
|
282
|
+
return 'schematic-record-' + String(record?.recordIndex ?? 0)
|
|
283
|
+
}
|
|
284
|
+
}
|