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,660 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds a read-only schematic hierarchy report from parsed project sheets.
|
|
7
|
+
*/
|
|
8
|
+
export class ProjectHierarchyReportBuilder {
|
|
9
|
+
static SCHEMA_ID = 'altium-toolkit.project.hierarchy.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds a schematic hierarchy report from parsed project and sheet models.
|
|
13
|
+
* @param {{ projectModel?: object, documentModels?: object[] }} [options] Report options.
|
|
14
|
+
* @returns {object}
|
|
15
|
+
*/
|
|
16
|
+
static build(options = {}) {
|
|
17
|
+
const project =
|
|
18
|
+
options.projectModel?.project || options.projectModel || {}
|
|
19
|
+
const schematicModels = (options.documentModels || []).filter(
|
|
20
|
+
(model) => model?.kind === 'schematic'
|
|
21
|
+
)
|
|
22
|
+
const sheets = ProjectHierarchyReportBuilder.#sheetRows(
|
|
23
|
+
project,
|
|
24
|
+
schematicModels
|
|
25
|
+
)
|
|
26
|
+
const sheetByPath = new Map(
|
|
27
|
+
sheets.map((sheet) => [sheet.normalizedPath, sheet])
|
|
28
|
+
)
|
|
29
|
+
const links = ProjectHierarchyReportBuilder.#linkRows(
|
|
30
|
+
schematicModels,
|
|
31
|
+
sheetByPath
|
|
32
|
+
)
|
|
33
|
+
const roots = ProjectHierarchyReportBuilder.#treeRoots(
|
|
34
|
+
sheets,
|
|
35
|
+
links,
|
|
36
|
+
project
|
|
37
|
+
)
|
|
38
|
+
const diagnostics = ProjectHierarchyReportBuilder.#diagnostics(links)
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
schema: ProjectHierarchyReportBuilder.SCHEMA_ID,
|
|
42
|
+
summary: {
|
|
43
|
+
sheetCount: sheets.length,
|
|
44
|
+
rootSheetCount: roots.length,
|
|
45
|
+
linkCount: links.length,
|
|
46
|
+
resolvedLinkCount: links.filter(
|
|
47
|
+
(link) => link.status === 'resolved'
|
|
48
|
+
).length,
|
|
49
|
+
missingSheetCount: links.filter(
|
|
50
|
+
(link) => link.status === 'missing'
|
|
51
|
+
).length,
|
|
52
|
+
cycleCount: links.filter((link) => link.status === 'cycle')
|
|
53
|
+
.length,
|
|
54
|
+
repeatedReferenceCount: links.filter(
|
|
55
|
+
(link) => link.status === 'repeated'
|
|
56
|
+
).length
|
|
57
|
+
},
|
|
58
|
+
hierarchyMode: {
|
|
59
|
+
value: String(project?.design?.HierarchyMode || ''),
|
|
60
|
+
name: ProjectHierarchyReportBuilder.#hierarchyModeName(
|
|
61
|
+
project?.design?.HierarchyMode
|
|
62
|
+
)
|
|
63
|
+
},
|
|
64
|
+
sheets,
|
|
65
|
+
links: links.sort(
|
|
66
|
+
(left, right) =>
|
|
67
|
+
left.parentSheetFileName.localeCompare(
|
|
68
|
+
right.parentSheetFileName
|
|
69
|
+
) ||
|
|
70
|
+
left.childSheetFileName.localeCompare(
|
|
71
|
+
right.childSheetFileName
|
|
72
|
+
) ||
|
|
73
|
+
left.symbolKey.localeCompare(right.symbolKey)
|
|
74
|
+
),
|
|
75
|
+
roots,
|
|
76
|
+
diagnostics
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Builds sheet rows from project document rows and parsed sheet models.
|
|
82
|
+
* @param {object} project Parsed project model.
|
|
83
|
+
* @param {object[]} schematicModels Parsed schematic models.
|
|
84
|
+
* @returns {object[]}
|
|
85
|
+
*/
|
|
86
|
+
static #sheetRows(project, schematicModels) {
|
|
87
|
+
const documentsByFileName = new Map()
|
|
88
|
+
const documents = (project?.documents || []).filter((document) =>
|
|
89
|
+
ProjectHierarchyReportBuilder.#isSchematicDocument(document)
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
for (const document of documents) {
|
|
93
|
+
documentsByFileName.set(
|
|
94
|
+
ProjectHierarchyReportBuilder.#basename(
|
|
95
|
+
document.normalizedPath ||
|
|
96
|
+
document.path ||
|
|
97
|
+
document.fileName
|
|
98
|
+
).toLowerCase(),
|
|
99
|
+
document
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const rows = schematicModels.map((model, index) => {
|
|
104
|
+
const document = documentsByFileName.get(
|
|
105
|
+
String(model.fileName || '').toLowerCase()
|
|
106
|
+
)
|
|
107
|
+
const normalizedPath = ProjectHierarchyReportBuilder.#normalizePath(
|
|
108
|
+
document?.normalizedPath ||
|
|
109
|
+
document?.path ||
|
|
110
|
+
model.fileName ||
|
|
111
|
+
''
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return ProjectHierarchyReportBuilder.#stripUndefined({
|
|
115
|
+
index,
|
|
116
|
+
fileName: model.fileName || document?.fileName || '',
|
|
117
|
+
title: model.summary?.title || model.fileName || '',
|
|
118
|
+
documentPath: document?.path || normalizedPath,
|
|
119
|
+
normalizedPath,
|
|
120
|
+
uniqueId: document?.uniqueId || '',
|
|
121
|
+
sheetSymbolCount: (model.schematic?.sheetSymbols || []).length,
|
|
122
|
+
sheetEntryCount: (model.schematic?.sheetEntries || []).length,
|
|
123
|
+
status: 'present'
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
const modelPathKeys = new Set(
|
|
127
|
+
rows.map((row) => row.normalizedPath.toLowerCase())
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
for (const document of documents) {
|
|
131
|
+
const normalizedPath = ProjectHierarchyReportBuilder.#normalizePath(
|
|
132
|
+
document.normalizedPath || document.path || document.fileName
|
|
133
|
+
)
|
|
134
|
+
if (modelPathKeys.has(normalizedPath.toLowerCase())) {
|
|
135
|
+
continue
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
rows.push(
|
|
139
|
+
ProjectHierarchyReportBuilder.#stripUndefined({
|
|
140
|
+
index: rows.length,
|
|
141
|
+
fileName:
|
|
142
|
+
document.fileName ||
|
|
143
|
+
ProjectHierarchyReportBuilder.#basename(normalizedPath),
|
|
144
|
+
title:
|
|
145
|
+
document.fileName ||
|
|
146
|
+
ProjectHierarchyReportBuilder.#basename(normalizedPath),
|
|
147
|
+
documentPath: document.path || normalizedPath,
|
|
148
|
+
normalizedPath,
|
|
149
|
+
uniqueId: document.uniqueId || '',
|
|
150
|
+
sheetSymbolCount: 0,
|
|
151
|
+
sheetEntryCount: 0,
|
|
152
|
+
status: 'unparsed'
|
|
153
|
+
})
|
|
154
|
+
)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return rows
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Builds hierarchy link rows from sheet symbols.
|
|
162
|
+
* @param {object[]} schematicModels Parsed schematic models.
|
|
163
|
+
* @param {Map<string, object>} sheetByPath Sheets by normalized path.
|
|
164
|
+
* @returns {object[]}
|
|
165
|
+
*/
|
|
166
|
+
static #linkRows(schematicModels, sheetByPath) {
|
|
167
|
+
const modelPathByFileName = new Map(
|
|
168
|
+
[...sheetByPath.values()].map((sheet) => [
|
|
169
|
+
sheet.fileName.toLowerCase(),
|
|
170
|
+
sheet.normalizedPath
|
|
171
|
+
])
|
|
172
|
+
)
|
|
173
|
+
const links = []
|
|
174
|
+
|
|
175
|
+
for (const model of schematicModels) {
|
|
176
|
+
const parentPath =
|
|
177
|
+
modelPathByFileName.get(
|
|
178
|
+
String(model.fileName || '').toLowerCase()
|
|
179
|
+
) ||
|
|
180
|
+
ProjectHierarchyReportBuilder.#normalizePath(model.fileName)
|
|
181
|
+
const parentSheet = sheetByPath.get(parentPath) || {
|
|
182
|
+
fileName: model.fileName || '',
|
|
183
|
+
normalizedPath: parentPath
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for (const [symbolIndex, sheetSymbol] of (
|
|
187
|
+
model.schematic?.sheetSymbols || []
|
|
188
|
+
).entries()) {
|
|
189
|
+
const childSheetFileName =
|
|
190
|
+
ProjectHierarchyReportBuilder.#childSheetFileName(
|
|
191
|
+
sheetSymbol
|
|
192
|
+
)
|
|
193
|
+
if (!childSheetFileName) {
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const resolvedChildPath =
|
|
198
|
+
ProjectHierarchyReportBuilder.#resolveChildPath(
|
|
199
|
+
parentPath,
|
|
200
|
+
childSheetFileName,
|
|
201
|
+
modelPathByFileName
|
|
202
|
+
)
|
|
203
|
+
const childSheet = sheetByPath.get(resolvedChildPath)
|
|
204
|
+
const sheetEntryNames =
|
|
205
|
+
ProjectHierarchyReportBuilder.#sheetEntryNames(
|
|
206
|
+
sheetSymbol,
|
|
207
|
+
model.schematic?.sheetEntries || []
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
links.push(
|
|
211
|
+
ProjectHierarchyReportBuilder.#stripUndefined({
|
|
212
|
+
key:
|
|
213
|
+
parentPath +
|
|
214
|
+
'->' +
|
|
215
|
+
resolvedChildPath +
|
|
216
|
+
'#' +
|
|
217
|
+
symbolIndex,
|
|
218
|
+
parentSheetFileName: parentSheet.fileName,
|
|
219
|
+
parentDocumentPath: parentPath,
|
|
220
|
+
childSheetFileName:
|
|
221
|
+
childSheet?.fileName ||
|
|
222
|
+
ProjectHierarchyReportBuilder.#basename(
|
|
223
|
+
childSheetFileName
|
|
224
|
+
),
|
|
225
|
+
childDocumentPath: resolvedChildPath,
|
|
226
|
+
symbolKey:
|
|
227
|
+
sheetSymbol.uniqueId ||
|
|
228
|
+
'sheet-symbol-' + symbolIndex,
|
|
229
|
+
symbolName: sheetSymbol.name || '',
|
|
230
|
+
sheetEntryNames,
|
|
231
|
+
status: childSheet ? 'resolved' : 'missing'
|
|
232
|
+
})
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
ProjectHierarchyReportBuilder.#markTraversalStatuses(links, [
|
|
238
|
+
...sheetByPath.values()
|
|
239
|
+
])
|
|
240
|
+
|
|
241
|
+
return links
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Updates resolved link rows with cycle and repeated-reference statuses.
|
|
246
|
+
* @param {object[]} links Link rows.
|
|
247
|
+
* @param {object[]} sheets Sheet rows.
|
|
248
|
+
*/
|
|
249
|
+
static #markTraversalStatuses(links, sheets) {
|
|
250
|
+
const linksByParent =
|
|
251
|
+
ProjectHierarchyReportBuilder.#linksByParent(links)
|
|
252
|
+
const roots = ProjectHierarchyReportBuilder.#rootSheetsFromLinks(
|
|
253
|
+
sheets,
|
|
254
|
+
links
|
|
255
|
+
)
|
|
256
|
+
const visited = new Set()
|
|
257
|
+
|
|
258
|
+
for (const root of roots) {
|
|
259
|
+
ProjectHierarchyReportBuilder.#walkLinks(
|
|
260
|
+
root.normalizedPath,
|
|
261
|
+
linksByParent,
|
|
262
|
+
visited,
|
|
263
|
+
[]
|
|
264
|
+
)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Walks resolved links from one parent path.
|
|
270
|
+
* @param {string} parentPath Parent sheet path.
|
|
271
|
+
* @param {Map<string, object[]>} linksByParent Links by parent path.
|
|
272
|
+
* @param {Set<string>} visited Globally visited sheet paths.
|
|
273
|
+
* @param {string[]} stack Current traversal stack.
|
|
274
|
+
*/
|
|
275
|
+
static #walkLinks(parentPath, linksByParent, visited, stack) {
|
|
276
|
+
if (stack.includes(parentPath)) {
|
|
277
|
+
return
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
visited.add(parentPath)
|
|
281
|
+
const nextStack = [...stack, parentPath]
|
|
282
|
+
|
|
283
|
+
for (const link of linksByParent.get(parentPath) || []) {
|
|
284
|
+
if (link.status !== 'resolved') {
|
|
285
|
+
continue
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (nextStack.includes(link.childDocumentPath)) {
|
|
289
|
+
link.status = 'cycle'
|
|
290
|
+
continue
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (visited.has(link.childDocumentPath)) {
|
|
294
|
+
link.status = 'repeated'
|
|
295
|
+
continue
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
ProjectHierarchyReportBuilder.#walkLinks(
|
|
299
|
+
link.childDocumentPath,
|
|
300
|
+
linksByParent,
|
|
301
|
+
visited,
|
|
302
|
+
nextStack
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Builds root tree rows.
|
|
309
|
+
* @param {object[]} sheets Sheet rows.
|
|
310
|
+
* @param {object[]} links Link rows.
|
|
311
|
+
* @param {object} project Parsed project model.
|
|
312
|
+
* @returns {object[]}
|
|
313
|
+
*/
|
|
314
|
+
static #treeRoots(sheets, links, project) {
|
|
315
|
+
const sheetByPath = new Map(
|
|
316
|
+
sheets.map((sheet) => [sheet.normalizedPath, sheet])
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
return ProjectHierarchyReportBuilder.#rootSheetsFromLinks(
|
|
320
|
+
sheets,
|
|
321
|
+
links,
|
|
322
|
+
project
|
|
323
|
+
).map((root) =>
|
|
324
|
+
ProjectHierarchyReportBuilder.#treeNode(
|
|
325
|
+
root.normalizedPath,
|
|
326
|
+
sheetByPath,
|
|
327
|
+
links,
|
|
328
|
+
[]
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Selects likely hierarchy roots.
|
|
335
|
+
* @param {object[]} sheets Sheet rows.
|
|
336
|
+
* @param {object[]} links Link rows.
|
|
337
|
+
* @param {object} [project] Parsed project model.
|
|
338
|
+
* @returns {object[]}
|
|
339
|
+
*/
|
|
340
|
+
static #rootSheetsFromLinks(sheets, links, project = {}) {
|
|
341
|
+
const referencedPaths = new Set(
|
|
342
|
+
links
|
|
343
|
+
.filter((link) => link.status === 'resolved')
|
|
344
|
+
.map((link) => link.childDocumentPath)
|
|
345
|
+
)
|
|
346
|
+
const primaryRootPath =
|
|
347
|
+
ProjectHierarchyReportBuilder.#primaryProjectSheetPath(
|
|
348
|
+
project,
|
|
349
|
+
sheets
|
|
350
|
+
)
|
|
351
|
+
const rootPaths = new Set()
|
|
352
|
+
if (primaryRootPath) {
|
|
353
|
+
rootPaths.add(primaryRootPath)
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
for (const sheet of sheets) {
|
|
357
|
+
if (!referencedPaths.has(sheet.normalizedPath)) {
|
|
358
|
+
rootPaths.add(sheet.normalizedPath)
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return sheets.filter((sheet) => rootPaths.has(sheet.normalizedPath))
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Builds one tree node.
|
|
367
|
+
* @param {string} path Sheet path.
|
|
368
|
+
* @param {Map<string, object>} sheetByPath Sheet lookup.
|
|
369
|
+
* @param {object[]} links Link rows.
|
|
370
|
+
* @param {string[]} stack Current traversal stack.
|
|
371
|
+
* @returns {object}
|
|
372
|
+
*/
|
|
373
|
+
static #treeNode(path, sheetByPath, links, stack) {
|
|
374
|
+
const sheet = sheetByPath.get(path)
|
|
375
|
+
const childLinks = links.filter(
|
|
376
|
+
(link) => link.parentDocumentPath === path
|
|
377
|
+
)
|
|
378
|
+
const nextStack = [...stack, path]
|
|
379
|
+
|
|
380
|
+
return ProjectHierarchyReportBuilder.#stripUndefined({
|
|
381
|
+
fileName:
|
|
382
|
+
sheet?.fileName ||
|
|
383
|
+
ProjectHierarchyReportBuilder.#basename(path),
|
|
384
|
+
documentPath: path,
|
|
385
|
+
status: sheet?.status || 'missing',
|
|
386
|
+
children: childLinks.map((link) =>
|
|
387
|
+
ProjectHierarchyReportBuilder.#treeChild(
|
|
388
|
+
link,
|
|
389
|
+
sheetByPath,
|
|
390
|
+
links,
|
|
391
|
+
nextStack
|
|
392
|
+
)
|
|
393
|
+
)
|
|
394
|
+
})
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Builds one child tree node from a hierarchy link.
|
|
399
|
+
* @param {object} link Link row.
|
|
400
|
+
* @param {Map<string, object>} sheetByPath Sheet lookup.
|
|
401
|
+
* @param {object[]} links Link rows.
|
|
402
|
+
* @param {string[]} stack Current traversal stack.
|
|
403
|
+
* @returns {object}
|
|
404
|
+
*/
|
|
405
|
+
static #treeChild(link, sheetByPath, links, stack) {
|
|
406
|
+
if (
|
|
407
|
+
link.status !== 'resolved' ||
|
|
408
|
+
stack.includes(link.childDocumentPath)
|
|
409
|
+
) {
|
|
410
|
+
return {
|
|
411
|
+
fileName: link.childSheetFileName,
|
|
412
|
+
documentPath: link.childDocumentPath,
|
|
413
|
+
status: link.status,
|
|
414
|
+
children: []
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return ProjectHierarchyReportBuilder.#treeNode(
|
|
419
|
+
link.childDocumentPath,
|
|
420
|
+
sheetByPath,
|
|
421
|
+
links,
|
|
422
|
+
stack
|
|
423
|
+
)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Builds diagnostics for non-resolved hierarchy links.
|
|
428
|
+
* @param {object[]} links Link rows.
|
|
429
|
+
* @returns {object[]}
|
|
430
|
+
*/
|
|
431
|
+
static #diagnostics(links) {
|
|
432
|
+
return links
|
|
433
|
+
.filter((link) => link.status !== 'resolved')
|
|
434
|
+
.map((link) =>
|
|
435
|
+
ProjectHierarchyReportBuilder.#stripUndefined({
|
|
436
|
+
code: 'project.hierarchy.' + link.status + '-sheet',
|
|
437
|
+
severity: 'warning',
|
|
438
|
+
parentSheetFileName: link.parentSheetFileName,
|
|
439
|
+
childSheetFileName: link.childSheetFileName,
|
|
440
|
+
symbolKey: link.symbolKey,
|
|
441
|
+
message:
|
|
442
|
+
link.status === 'missing'
|
|
443
|
+
? 'Sheet symbol references a child sheet that was not present.'
|
|
444
|
+
: link.status === 'cycle'
|
|
445
|
+
? 'Sheet symbol creates a recursive sheet hierarchy.'
|
|
446
|
+
: 'Sheet is referenced more than once in the hierarchy.'
|
|
447
|
+
})
|
|
448
|
+
)
|
|
449
|
+
.map((diagnostic) =>
|
|
450
|
+
diagnostic.code === 'project.hierarchy.cycle-sheet'
|
|
451
|
+
? { ...diagnostic, code: 'project.hierarchy.cycle' }
|
|
452
|
+
: diagnostic
|
|
453
|
+
)
|
|
454
|
+
.map((diagnostic) =>
|
|
455
|
+
diagnostic.code === 'project.hierarchy.missing-sheet'
|
|
456
|
+
? diagnostic
|
|
457
|
+
: diagnostic
|
|
458
|
+
)
|
|
459
|
+
.sort(
|
|
460
|
+
(left, right) =>
|
|
461
|
+
left.parentSheetFileName.localeCompare(
|
|
462
|
+
right.parentSheetFileName
|
|
463
|
+
) ||
|
|
464
|
+
left.childSheetFileName.localeCompare(
|
|
465
|
+
right.childSheetFileName
|
|
466
|
+
) ||
|
|
467
|
+
left.code.localeCompare(right.code)
|
|
468
|
+
)
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Groups links by parent document path.
|
|
473
|
+
* @param {object[]} links Link rows.
|
|
474
|
+
* @returns {Map<string, object[]>}
|
|
475
|
+
*/
|
|
476
|
+
static #linksByParent(links) {
|
|
477
|
+
const byParent = new Map()
|
|
478
|
+
|
|
479
|
+
for (const link of links) {
|
|
480
|
+
byParent.set(link.parentDocumentPath, [
|
|
481
|
+
...(byParent.get(link.parentDocumentPath) || []),
|
|
482
|
+
link
|
|
483
|
+
])
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return byParent
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Selects the first schematic document as the primary project root.
|
|
491
|
+
* @param {object} project Parsed project model.
|
|
492
|
+
* @param {object[]} sheets Sheet rows.
|
|
493
|
+
* @returns {string}
|
|
494
|
+
*/
|
|
495
|
+
static #primaryProjectSheetPath(project, sheets) {
|
|
496
|
+
const firstDocument = (project?.documents || []).find((document) =>
|
|
497
|
+
ProjectHierarchyReportBuilder.#isSchematicDocument(document)
|
|
498
|
+
)
|
|
499
|
+
const firstDocumentPath = ProjectHierarchyReportBuilder.#normalizePath(
|
|
500
|
+
firstDocument?.normalizedPath ||
|
|
501
|
+
firstDocument?.path ||
|
|
502
|
+
firstDocument?.fileName ||
|
|
503
|
+
''
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
if (firstDocumentPath) {
|
|
507
|
+
return (
|
|
508
|
+
sheets.find(
|
|
509
|
+
(sheet) =>
|
|
510
|
+
sheet.normalizedPath.toLowerCase() ===
|
|
511
|
+
firstDocumentPath.toLowerCase()
|
|
512
|
+
)?.normalizedPath || ''
|
|
513
|
+
)
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return sheets[0]?.normalizedPath || ''
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Returns child sheet file name metadata from a sheet symbol.
|
|
521
|
+
* @param {object} sheetSymbol Sheet symbol row.
|
|
522
|
+
* @returns {string}
|
|
523
|
+
*/
|
|
524
|
+
static #childSheetFileName(sheetSymbol) {
|
|
525
|
+
return String(
|
|
526
|
+
sheetSymbol?.fileName ||
|
|
527
|
+
sheetSymbol?.sheetFileName ||
|
|
528
|
+
sheetSymbol?.childSheetFileName ||
|
|
529
|
+
''
|
|
530
|
+
).trim()
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Resolves sheet-entry names owned by one sheet symbol.
|
|
535
|
+
* @param {object} sheetSymbol Sheet symbol row.
|
|
536
|
+
* @param {object[]} sheetEntries Sheet entries.
|
|
537
|
+
* @returns {string[]}
|
|
538
|
+
*/
|
|
539
|
+
static #sheetEntryNames(sheetSymbol, sheetEntries) {
|
|
540
|
+
const ownerKeys = new Set(
|
|
541
|
+
[
|
|
542
|
+
sheetSymbol.ownerIndex,
|
|
543
|
+
sheetSymbol.indexInSheet,
|
|
544
|
+
Number.isInteger(sheetSymbol.indexInSheet)
|
|
545
|
+
? sheetSymbol.indexInSheet + 1
|
|
546
|
+
: undefined
|
|
547
|
+
]
|
|
548
|
+
.filter((value) => value !== undefined && value !== '')
|
|
549
|
+
.map((value) => String(value))
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
return (sheetEntries || [])
|
|
553
|
+
.filter((entry) => ownerKeys.has(String(entry.ownerIndex || '')))
|
|
554
|
+
.map((entry) => String(entry.name || '').trim())
|
|
555
|
+
.filter(Boolean)
|
|
556
|
+
.sort((left, right) => left.localeCompare(right))
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* Resolves a child sheet path against known sheet file names.
|
|
561
|
+
* @param {string} parentPath Parent sheet path.
|
|
562
|
+
* @param {string} childSheetFileName Raw child filename.
|
|
563
|
+
* @param {Map<string, string>} modelPathByFileName Known sheet paths.
|
|
564
|
+
* @returns {string}
|
|
565
|
+
*/
|
|
566
|
+
static #resolveChildPath(
|
|
567
|
+
parentPath,
|
|
568
|
+
childSheetFileName,
|
|
569
|
+
modelPathByFileName
|
|
570
|
+
) {
|
|
571
|
+
const normalizedChild =
|
|
572
|
+
ProjectHierarchyReportBuilder.#normalizePath(childSheetFileName)
|
|
573
|
+
const byFileName = modelPathByFileName.get(
|
|
574
|
+
ProjectHierarchyReportBuilder.#basename(
|
|
575
|
+
normalizedChild
|
|
576
|
+
).toLowerCase()
|
|
577
|
+
)
|
|
578
|
+
if (byFileName) {
|
|
579
|
+
return byFileName
|
|
580
|
+
}
|
|
581
|
+
if (normalizedChild.includes('/')) {
|
|
582
|
+
return normalizedChild
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const parentDirectory = parentPath.includes('/')
|
|
586
|
+
? parentPath.replace(/\/[^/]*$/u, '')
|
|
587
|
+
: ''
|
|
588
|
+
return parentDirectory
|
|
589
|
+
? parentDirectory + '/' + normalizedChild
|
|
590
|
+
: normalizedChild
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Returns true when a project document row refers to a schematic sheet.
|
|
595
|
+
* @param {object} document Project document row.
|
|
596
|
+
* @returns {boolean}
|
|
597
|
+
*/
|
|
598
|
+
static #isSchematicDocument(document) {
|
|
599
|
+
const kind = String(document?.kind || '').toLowerCase()
|
|
600
|
+
const path = String(
|
|
601
|
+
document?.normalizedPath ||
|
|
602
|
+
document?.path ||
|
|
603
|
+
document?.fileName ||
|
|
604
|
+
''
|
|
605
|
+
).toLowerCase()
|
|
606
|
+
|
|
607
|
+
return kind === 'schematic' || path.endsWith('.schdoc')
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Resolves a display name for a project hierarchy mode.
|
|
612
|
+
* @param {string | number | undefined} mode Raw hierarchy mode.
|
|
613
|
+
* @returns {string}
|
|
614
|
+
*/
|
|
615
|
+
static #hierarchyModeName(mode) {
|
|
616
|
+
switch (String(mode || '')) {
|
|
617
|
+
case '2':
|
|
618
|
+
return 'hierarchical'
|
|
619
|
+
case '1':
|
|
620
|
+
return 'flat'
|
|
621
|
+
case '3':
|
|
622
|
+
return 'global'
|
|
623
|
+
default:
|
|
624
|
+
return 'unspecified'
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/**
|
|
629
|
+
* Normalizes path separators.
|
|
630
|
+
* @param {unknown} path Path value.
|
|
631
|
+
* @returns {string}
|
|
632
|
+
*/
|
|
633
|
+
static #normalizePath(path) {
|
|
634
|
+
return String(path || '').replace(/\\/gu, '/')
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Extracts a basename from a normalized or native path.
|
|
639
|
+
* @param {unknown} path Path value.
|
|
640
|
+
* @returns {string}
|
|
641
|
+
*/
|
|
642
|
+
static #basename(path) {
|
|
643
|
+
const parts =
|
|
644
|
+
ProjectHierarchyReportBuilder.#normalizePath(path).split('/')
|
|
645
|
+
return parts.at(-1) || ''
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
/**
|
|
649
|
+
* Removes undefined and empty-string fields.
|
|
650
|
+
* @param {object} row Source row.
|
|
651
|
+
* @returns {object}
|
|
652
|
+
*/
|
|
653
|
+
static #stripUndefined(row) {
|
|
654
|
+
return Object.fromEntries(
|
|
655
|
+
Object.entries(row || {}).filter(
|
|
656
|
+
([, value]) => value !== undefined && value !== ''
|
|
657
|
+
)
|
|
658
|
+
)
|
|
659
|
+
}
|
|
660
|
+
}
|
|
@@ -308,6 +308,8 @@ export class ProjectNetlistExporter {
|
|
|
308
308
|
*/
|
|
309
309
|
static #sourceAliases(source) {
|
|
310
310
|
return ProjectNetlistExporter.#dedupe([
|
|
311
|
+
source?.autoName,
|
|
312
|
+
...(source?.aliasCandidates || []),
|
|
311
313
|
...(source?.labels || []).map((label) => label.text),
|
|
312
314
|
...(source?.powerPorts || []).map((port) => port.text),
|
|
313
315
|
...(source?.ports || []).map((port) => port.name),
|