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,492 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
|
|
6
|
+
import { ProjectVariantViewBuilder } from './ProjectVariantViewBuilder.mjs'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Composes parsed project, schematic, and PCB models into one read-only design
|
|
10
|
+
* bundle for multi-document consumers.
|
|
11
|
+
*/
|
|
12
|
+
export class ProjectDesignBundleBuilder {
|
|
13
|
+
static #UNITS = {
|
|
14
|
+
coordinate: 'mil',
|
|
15
|
+
length: 'mil',
|
|
16
|
+
board: 'mil',
|
|
17
|
+
pnp: 'mil',
|
|
18
|
+
angle: 'deg'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static #PNP_UNITS = {
|
|
22
|
+
coordinate: 'mil',
|
|
23
|
+
angle: 'deg'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Builds a normalized project/design bundle from already parsed models.
|
|
28
|
+
* @param {{ projectModel?: object, documentModels?: object[], annotationModels?: object[], variantName?: string }} options Bundle options.
|
|
29
|
+
* @returns {object}
|
|
30
|
+
*/
|
|
31
|
+
static build(options = {}) {
|
|
32
|
+
const projectModel = options.projectModel || {}
|
|
33
|
+
const documentModels = Array.isArray(options.documentModels)
|
|
34
|
+
? options.documentModels
|
|
35
|
+
: []
|
|
36
|
+
const project = projectModel.project || {}
|
|
37
|
+
const documents = project.documents || []
|
|
38
|
+
const schematicModels = documentModels.filter(
|
|
39
|
+
(model) => model?.kind === 'schematic'
|
|
40
|
+
)
|
|
41
|
+
const pcbModels = documentModels.filter(
|
|
42
|
+
(model) => model?.kind === 'pcb'
|
|
43
|
+
)
|
|
44
|
+
const sheets = ProjectDesignBundleBuilder.#buildSheets(
|
|
45
|
+
schematicModels,
|
|
46
|
+
documents
|
|
47
|
+
)
|
|
48
|
+
const components = ProjectDesignBundleBuilder.#buildComponents(
|
|
49
|
+
schematicModels,
|
|
50
|
+
pcbModels
|
|
51
|
+
)
|
|
52
|
+
const pnp = ProjectDesignBundleBuilder.#buildPnp(pcbModels)
|
|
53
|
+
const nets = ProjectDesignBundleBuilder.#buildNets(
|
|
54
|
+
schematicModels,
|
|
55
|
+
pcbModels
|
|
56
|
+
)
|
|
57
|
+
const bom = ProjectDesignBundleBuilder.#buildBom(documentModels)
|
|
58
|
+
const annotations = ProjectDesignBundleBuilder.#buildAnnotations(
|
|
59
|
+
options.annotationModels || []
|
|
60
|
+
)
|
|
61
|
+
const indexes = ProjectDesignBundleBuilder.#buildIndexes(
|
|
62
|
+
documents,
|
|
63
|
+
sheets,
|
|
64
|
+
components,
|
|
65
|
+
nets,
|
|
66
|
+
pnp
|
|
67
|
+
)
|
|
68
|
+
const bundle = NormalizedModelSchema.attach({
|
|
69
|
+
kind: 'design-bundle',
|
|
70
|
+
fileType: 'ProjectDesignBundle',
|
|
71
|
+
fileName: projectModel.fileName || 'design-bundle.json',
|
|
72
|
+
summary: {
|
|
73
|
+
title:
|
|
74
|
+
projectModel.summary?.title ||
|
|
75
|
+
project.name ||
|
|
76
|
+
'Project design bundle',
|
|
77
|
+
sheetCount: sheets.length,
|
|
78
|
+
componentCount: components.length,
|
|
79
|
+
netCount: nets.length,
|
|
80
|
+
pnpCount: pnp.entries.length,
|
|
81
|
+
variantCount: (project.variants || []).length,
|
|
82
|
+
annotationMappingCount: annotations.mappings.length
|
|
83
|
+
},
|
|
84
|
+
diagnostics: [
|
|
85
|
+
{
|
|
86
|
+
severity: 'info',
|
|
87
|
+
message:
|
|
88
|
+
'Composed ' +
|
|
89
|
+
documentModels.length +
|
|
90
|
+
' parsed document models into a project design bundle.'
|
|
91
|
+
}
|
|
92
|
+
],
|
|
93
|
+
project,
|
|
94
|
+
units: ProjectDesignBundleBuilder.#UNITS,
|
|
95
|
+
variants: project.variants || [],
|
|
96
|
+
sheets,
|
|
97
|
+
components,
|
|
98
|
+
schematic_hierarchy:
|
|
99
|
+
ProjectDesignBundleBuilder.#buildSchematicHierarchy(
|
|
100
|
+
project,
|
|
101
|
+
schematicModels,
|
|
102
|
+
documents
|
|
103
|
+
),
|
|
104
|
+
pnp,
|
|
105
|
+
nets,
|
|
106
|
+
annotations,
|
|
107
|
+
indexes,
|
|
108
|
+
bom
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
if (options.variantName) {
|
|
112
|
+
bundle.effectiveVariant = ProjectVariantViewBuilder.build(bundle, {
|
|
113
|
+
variantName: options.variantName
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return bundle
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Builds sheet entries from schematic models and project document rows.
|
|
122
|
+
* @param {object[]} schematicModels Parsed schematic models.
|
|
123
|
+
* @param {object[]} documents Project document rows.
|
|
124
|
+
* @returns {object[]}
|
|
125
|
+
*/
|
|
126
|
+
static #buildSheets(schematicModels, documents) {
|
|
127
|
+
return schematicModels.map((model, index) => {
|
|
128
|
+
const document = ProjectDesignBundleBuilder.#documentForModel(
|
|
129
|
+
model,
|
|
130
|
+
documents
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
bundleIndex: index,
|
|
135
|
+
fileName: model.fileName,
|
|
136
|
+
title: model.summary?.title || model.fileName,
|
|
137
|
+
documentPath: document?.path || model.fileName,
|
|
138
|
+
uniqueId: document?.uniqueId || '',
|
|
139
|
+
sheet: model.schematic?.sheet || {},
|
|
140
|
+
componentCount: model.schematic?.components?.length || 0,
|
|
141
|
+
netCount: model.schematic?.nets?.length || 0
|
|
142
|
+
}
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Builds component entries joined by designator across schematic and PCB
|
|
148
|
+
* documents.
|
|
149
|
+
* @param {object[]} schematicModels Parsed schematic models.
|
|
150
|
+
* @param {object[]} pcbModels Parsed PCB models.
|
|
151
|
+
* @returns {object[]}
|
|
152
|
+
*/
|
|
153
|
+
static #buildComponents(schematicModels, pcbModels) {
|
|
154
|
+
const componentsByDesignator = new Map()
|
|
155
|
+
|
|
156
|
+
for (const model of schematicModels) {
|
|
157
|
+
for (const component of model.schematic?.components || []) {
|
|
158
|
+
const entry = ProjectDesignBundleBuilder.#componentEntry(
|
|
159
|
+
componentsByDesignator,
|
|
160
|
+
component.designator
|
|
161
|
+
)
|
|
162
|
+
entry.schematic = {
|
|
163
|
+
fileName: model.fileName,
|
|
164
|
+
uniqueId: component.uniqueId || '',
|
|
165
|
+
libReference: component.libReference || '',
|
|
166
|
+
value: component.value || ''
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
for (const model of pcbModels) {
|
|
172
|
+
for (const component of model.pcb?.components || []) {
|
|
173
|
+
const entry = ProjectDesignBundleBuilder.#componentEntry(
|
|
174
|
+
componentsByDesignator,
|
|
175
|
+
component.designator
|
|
176
|
+
)
|
|
177
|
+
entry.pcb = {
|
|
178
|
+
fileName: model.fileName,
|
|
179
|
+
componentIndex: component.componentIndex,
|
|
180
|
+
uniqueId: component.uniqueId || '',
|
|
181
|
+
pattern: component.pattern || ''
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return [...componentsByDesignator.values()].map((component, index) => ({
|
|
187
|
+
bundleIndex: index,
|
|
188
|
+
...component
|
|
189
|
+
}))
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Gets or creates one component bundle entry.
|
|
194
|
+
* @param {Map<string, object>} componentsByDesignator Component map.
|
|
195
|
+
* @param {string} designator Component designator.
|
|
196
|
+
* @returns {object}
|
|
197
|
+
*/
|
|
198
|
+
static #componentEntry(componentsByDesignator, designator) {
|
|
199
|
+
const key = String(designator || '').trim()
|
|
200
|
+
if (!componentsByDesignator.has(key)) {
|
|
201
|
+
componentsByDesignator.set(key, {
|
|
202
|
+
designator: key,
|
|
203
|
+
schematic: null,
|
|
204
|
+
pcb: null
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
return componentsByDesignator.get(key)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Builds a combined pick-place model.
|
|
212
|
+
* @param {object[]} pcbModels Parsed PCB models.
|
|
213
|
+
* @returns {object}
|
|
214
|
+
*/
|
|
215
|
+
static #buildPnp(pcbModels) {
|
|
216
|
+
const entries = []
|
|
217
|
+
let positionMode = ''
|
|
218
|
+
|
|
219
|
+
for (const model of pcbModels) {
|
|
220
|
+
const pnp = model.pnp || model.pcb?.pickPlace || {}
|
|
221
|
+
positionMode ||= pnp.positionMode || ''
|
|
222
|
+
for (const entry of pnp.entries || []) {
|
|
223
|
+
entries.push({
|
|
224
|
+
bundleIndex: entries.length,
|
|
225
|
+
sourceFileName: model.fileName,
|
|
226
|
+
...entry
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
units: ProjectDesignBundleBuilder.#PNP_UNITS,
|
|
233
|
+
positionMode,
|
|
234
|
+
entries,
|
|
235
|
+
modes: {}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Builds combined schematic and PCB net entries.
|
|
241
|
+
* @param {object[]} schematicModels Parsed schematic models.
|
|
242
|
+
* @param {object[]} pcbModels Parsed PCB models.
|
|
243
|
+
* @returns {object[]}
|
|
244
|
+
*/
|
|
245
|
+
static #buildNets(schematicModels, pcbModels) {
|
|
246
|
+
const netsByName = new Map()
|
|
247
|
+
|
|
248
|
+
for (const model of schematicModels) {
|
|
249
|
+
for (const net of model.schematic?.nets || []) {
|
|
250
|
+
const entry = ProjectDesignBundleBuilder.#netEntry(
|
|
251
|
+
netsByName,
|
|
252
|
+
net.name
|
|
253
|
+
)
|
|
254
|
+
entry.schematic.push({
|
|
255
|
+
fileName: model.fileName,
|
|
256
|
+
pins: net.pins || [],
|
|
257
|
+
labels: net.labels || [],
|
|
258
|
+
segments: net.segments || [],
|
|
259
|
+
...(model.schematic?.harnesses
|
|
260
|
+
? { harnesses: model.schematic.harnesses.connectors }
|
|
261
|
+
: {})
|
|
262
|
+
})
|
|
263
|
+
entry.pins.push(...(net.pins || []))
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
for (const model of pcbModels) {
|
|
268
|
+
for (const net of model.pcb?.nets || []) {
|
|
269
|
+
const entry = ProjectDesignBundleBuilder.#netEntry(
|
|
270
|
+
netsByName,
|
|
271
|
+
net.name
|
|
272
|
+
)
|
|
273
|
+
entry.pcb.push({
|
|
274
|
+
fileName: model.fileName,
|
|
275
|
+
netIndex: net.netIndex,
|
|
276
|
+
uniqueId: net.uniqueId || ''
|
|
277
|
+
})
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return [...netsByName.values()].map((net, index) => ({
|
|
282
|
+
bundleIndex: index,
|
|
283
|
+
...net
|
|
284
|
+
}))
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Gets or creates one net bundle entry.
|
|
289
|
+
* @param {Map<string, object>} netsByName Net map.
|
|
290
|
+
* @param {string} name Net name.
|
|
291
|
+
* @returns {object}
|
|
292
|
+
*/
|
|
293
|
+
static #netEntry(netsByName, name) {
|
|
294
|
+
const key = String(name || '').trim()
|
|
295
|
+
if (!netsByName.has(key)) {
|
|
296
|
+
netsByName.set(key, {
|
|
297
|
+
name: key,
|
|
298
|
+
schematic: [],
|
|
299
|
+
pcb: [],
|
|
300
|
+
pins: []
|
|
301
|
+
})
|
|
302
|
+
}
|
|
303
|
+
return netsByName.get(key)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Selects a combined BOM, preferring PCB BOM rows when available.
|
|
308
|
+
* @param {object[]} documentModels Parsed document models.
|
|
309
|
+
* @returns {object[]}
|
|
310
|
+
*/
|
|
311
|
+
static #buildBom(documentModels) {
|
|
312
|
+
const pcbBom = documentModels
|
|
313
|
+
.filter((model) => model?.kind === 'pcb')
|
|
314
|
+
.flatMap((model) => model.bom || [])
|
|
315
|
+
|
|
316
|
+
if (pcbBom.length) {
|
|
317
|
+
return pcbBom
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return documentModels.flatMap((model) => model.bom || [])
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Builds compiled-designator annotation mappings.
|
|
325
|
+
* @param {object[]} annotationModels Parsed annotation models.
|
|
326
|
+
* @returns {{ mappings: object[], bySourceDesignator: Record<string, object>, byCompiledDesignator: Record<string, object> }}
|
|
327
|
+
*/
|
|
328
|
+
static #buildAnnotations(annotationModels) {
|
|
329
|
+
const mappings = []
|
|
330
|
+
|
|
331
|
+
for (const model of annotationModels || []) {
|
|
332
|
+
mappings.push(...(model?.annotations?.mappings || []))
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
mappings,
|
|
337
|
+
bySourceDesignator: ProjectDesignBundleBuilder.#indexFullBy(
|
|
338
|
+
mappings,
|
|
339
|
+
'sourceDesignator'
|
|
340
|
+
),
|
|
341
|
+
byCompiledDesignator: ProjectDesignBundleBuilder.#indexFullBy(
|
|
342
|
+
mappings,
|
|
343
|
+
'compiledDesignator'
|
|
344
|
+
)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Builds schematic hierarchy metadata.
|
|
350
|
+
* @param {object} project Project model.
|
|
351
|
+
* @param {object[]} schematicModels Parsed schematic models.
|
|
352
|
+
* @param {object[]} documents Project document rows.
|
|
353
|
+
* @returns {object}
|
|
354
|
+
*/
|
|
355
|
+
static #buildSchematicHierarchy(project, schematicModels, documents) {
|
|
356
|
+
const hierarchy = {
|
|
357
|
+
mode: project?.design?.HierarchyMode || '',
|
|
358
|
+
modeName: ProjectDesignBundleBuilder.#hierarchyModeName(
|
|
359
|
+
project?.design?.HierarchyMode
|
|
360
|
+
),
|
|
361
|
+
sheets: schematicModels.map((model) => {
|
|
362
|
+
const document = ProjectDesignBundleBuilder.#documentForModel(
|
|
363
|
+
model,
|
|
364
|
+
documents
|
|
365
|
+
)
|
|
366
|
+
return {
|
|
367
|
+
fileName: model.fileName,
|
|
368
|
+
documentPath: document?.path || model.fileName,
|
|
369
|
+
uniqueId: document?.uniqueId || '',
|
|
370
|
+
title: model.summary?.title || model.fileName
|
|
371
|
+
}
|
|
372
|
+
}),
|
|
373
|
+
sheetSymbols: schematicModels.flatMap((model) =>
|
|
374
|
+
(model.schematic?.sheetSymbols || []).map((sheetSymbol) => ({
|
|
375
|
+
sheetFileName: model.fileName,
|
|
376
|
+
uniqueId: sheetSymbol.uniqueId || '',
|
|
377
|
+
entries: (model.schematic?.sheetEntries || []).map(
|
|
378
|
+
(entry) => entry.name
|
|
379
|
+
)
|
|
380
|
+
}))
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
const harnessBundleLinks = schematicModels.flatMap((model) =>
|
|
384
|
+
(model.schematic?.harnesses?.bundleLinks || []).map((link) => ({
|
|
385
|
+
sheetFileName: model.fileName,
|
|
386
|
+
...link
|
|
387
|
+
}))
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
if (harnessBundleLinks.length) {
|
|
391
|
+
hierarchy.harness_bundle_links = harnessBundleLinks
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return hierarchy
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Builds bundle lookup indexes.
|
|
399
|
+
* @param {object[]} documents Project document rows.
|
|
400
|
+
* @param {object[]} sheets Bundle sheets.
|
|
401
|
+
* @param {object[]} components Bundle components.
|
|
402
|
+
* @param {object[]} nets Bundle nets.
|
|
403
|
+
* @param {object} pnp Bundle PnP model.
|
|
404
|
+
* @returns {object}
|
|
405
|
+
*/
|
|
406
|
+
static #buildIndexes(documents, sheets, components, nets, pnp) {
|
|
407
|
+
return {
|
|
408
|
+
documentsByPath: ProjectDesignBundleBuilder.#indexBy(
|
|
409
|
+
documents,
|
|
410
|
+
'normalizedPath'
|
|
411
|
+
),
|
|
412
|
+
sheetsByFileName: ProjectDesignBundleBuilder.#indexBy(
|
|
413
|
+
sheets,
|
|
414
|
+
'fileName'
|
|
415
|
+
),
|
|
416
|
+
componentsByDesignator: ProjectDesignBundleBuilder.#indexBy(
|
|
417
|
+
components,
|
|
418
|
+
'designator'
|
|
419
|
+
),
|
|
420
|
+
netsByName: ProjectDesignBundleBuilder.#indexBy(nets, 'name'),
|
|
421
|
+
pnpByDesignator: ProjectDesignBundleBuilder.#indexBy(
|
|
422
|
+
pnp.entries,
|
|
423
|
+
'designator'
|
|
424
|
+
)
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Builds a compact object index by a field.
|
|
430
|
+
* @param {object[]} records Records to index.
|
|
431
|
+
* @param {string} key Field name.
|
|
432
|
+
* @returns {Record<string, object>}
|
|
433
|
+
*/
|
|
434
|
+
static #indexBy(records, key) {
|
|
435
|
+
const index = {}
|
|
436
|
+
for (const record of records || []) {
|
|
437
|
+
const value = String(record?.[key] || '').trim()
|
|
438
|
+
if (!value) continue
|
|
439
|
+
index[value] = {
|
|
440
|
+
bundleIndex: record.bundleIndex ?? record.index ?? 0
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return index
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Builds a full object index by a field.
|
|
448
|
+
* @param {object[]} records Records to index.
|
|
449
|
+
* @param {string} key Field name.
|
|
450
|
+
* @returns {Record<string, object>}
|
|
451
|
+
*/
|
|
452
|
+
static #indexFullBy(records, key) {
|
|
453
|
+
const index = {}
|
|
454
|
+
for (const record of records || []) {
|
|
455
|
+
const value = String(record?.[key] || '').trim()
|
|
456
|
+
if (value) index[value] = record
|
|
457
|
+
}
|
|
458
|
+
return index
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Finds the project document row corresponding to a parsed model.
|
|
463
|
+
* @param {object} model Parsed document model.
|
|
464
|
+
* @param {object[]} documents Project document rows.
|
|
465
|
+
* @returns {object | null}
|
|
466
|
+
*/
|
|
467
|
+
static #documentForModel(model, documents) {
|
|
468
|
+
return (
|
|
469
|
+
(documents || []).find(
|
|
470
|
+
(document) => document.fileName === model.fileName
|
|
471
|
+
) || null
|
|
472
|
+
)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Resolves a display name for a project hierarchy mode.
|
|
477
|
+
* @param {string | number | undefined} mode Raw hierarchy mode.
|
|
478
|
+
* @returns {string}
|
|
479
|
+
*/
|
|
480
|
+
static #hierarchyModeName(mode) {
|
|
481
|
+
switch (String(mode || '')) {
|
|
482
|
+
case '2':
|
|
483
|
+
return 'hierarchical'
|
|
484
|
+
case '1':
|
|
485
|
+
return 'flat'
|
|
486
|
+
case '3':
|
|
487
|
+
return 'global'
|
|
488
|
+
default:
|
|
489
|
+
return 'unspecified'
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|