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,205 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Provides deterministic conversions for common Altium length units.
|
|
7
|
+
*/
|
|
8
|
+
export class AltiumUnits {
|
|
9
|
+
static RAW_UNITS_PER_MIL = 10000
|
|
10
|
+
|
|
11
|
+
static MM_PER_MIL = 0.0254
|
|
12
|
+
|
|
13
|
+
static MIL_PER_INCH = 1000
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Converts millimeters to mils.
|
|
17
|
+
* @param {number} value Length in millimeters.
|
|
18
|
+
* @returns {number}
|
|
19
|
+
*/
|
|
20
|
+
static mmToMil(value) {
|
|
21
|
+
return AltiumUnits.#round(Number(value) / AltiumUnits.MM_PER_MIL)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Converts mils to millimeters.
|
|
26
|
+
* @param {number} value Length in mils.
|
|
27
|
+
* @returns {number}
|
|
28
|
+
*/
|
|
29
|
+
static milToMm(value) {
|
|
30
|
+
return AltiumUnits.#round(Number(value) * AltiumUnits.MM_PER_MIL)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Converts inches to mils.
|
|
35
|
+
* @param {number} value Length in inches.
|
|
36
|
+
* @returns {number}
|
|
37
|
+
*/
|
|
38
|
+
static inchToMil(value) {
|
|
39
|
+
return AltiumUnits.#round(Number(value) * AltiumUnits.MIL_PER_INCH)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Converts mils to inches.
|
|
44
|
+
* @param {number} value Length in mils.
|
|
45
|
+
* @returns {number}
|
|
46
|
+
*/
|
|
47
|
+
static milToInch(value) {
|
|
48
|
+
return AltiumUnits.#round(Number(value) / AltiumUnits.MIL_PER_INCH)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Converts raw Altium coordinate units to mils.
|
|
53
|
+
* @param {number} value Raw coordinate units.
|
|
54
|
+
* @returns {number}
|
|
55
|
+
*/
|
|
56
|
+
static rawToMil(value) {
|
|
57
|
+
return AltiumUnits.#round(Number(value) / AltiumUnits.RAW_UNITS_PER_MIL)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Converts mils to raw Altium coordinate units.
|
|
62
|
+
* @param {number} value Length in mils.
|
|
63
|
+
* @returns {number}
|
|
64
|
+
*/
|
|
65
|
+
static milToRaw(value) {
|
|
66
|
+
return AltiumUnits.#round(Number(value) * AltiumUnits.RAW_UNITS_PER_MIL)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parses one numeric or textual length into a deterministic unit bundle.
|
|
71
|
+
* @param {unknown} value Length value.
|
|
72
|
+
* @param {{ defaultUnit?: 'mil' | 'mm' | 'in' | 'raw' }} [options]
|
|
73
|
+
* @returns {{ value: number, unit: string, mil: number, mm: number, inch: number, raw: number } | null}
|
|
74
|
+
*/
|
|
75
|
+
static parseLength(value, options = {}) {
|
|
76
|
+
const parsed = AltiumUnits.#parseNumericAndUnit(value, options)
|
|
77
|
+
if (!parsed) return null
|
|
78
|
+
|
|
79
|
+
const mil = AltiumUnits.#toMil(parsed.value, parsed.unit)
|
|
80
|
+
if (!Number.isFinite(mil)) return null
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
value: AltiumUnits.#round(parsed.value),
|
|
84
|
+
unit: parsed.unit,
|
|
85
|
+
mil: AltiumUnits.#round(mil),
|
|
86
|
+
mm: AltiumUnits.milToMm(mil),
|
|
87
|
+
inch: AltiumUnits.milToInch(mil),
|
|
88
|
+
raw: AltiumUnits.milToRaw(mil)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Formats a mil value in the requested target unit.
|
|
94
|
+
* @param {number} value Length in mils.
|
|
95
|
+
* @param {'mil' | 'mm' | 'in' | 'raw'} [unit]
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
static formatMil(value, unit = 'mil') {
|
|
99
|
+
const mil = Number(value)
|
|
100
|
+
if (!Number.isFinite(mil)) return ''
|
|
101
|
+
|
|
102
|
+
const normalizedUnit = AltiumUnits.#normalizeUnit(unit)
|
|
103
|
+
if (normalizedUnit === 'mm') {
|
|
104
|
+
return AltiumUnits.#formatNumber(AltiumUnits.milToMm(mil)) + 'mm'
|
|
105
|
+
}
|
|
106
|
+
if (normalizedUnit === 'in') {
|
|
107
|
+
return AltiumUnits.#formatNumber(AltiumUnits.milToInch(mil)) + 'in'
|
|
108
|
+
}
|
|
109
|
+
if (normalizedUnit === 'raw') {
|
|
110
|
+
return AltiumUnits.#formatNumber(AltiumUnits.milToRaw(mil)) + 'raw'
|
|
111
|
+
}
|
|
112
|
+
return AltiumUnits.#formatNumber(mil) + 'mil'
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Parses an input value into a number and normalized unit token.
|
|
117
|
+
* @param {unknown} value Length input.
|
|
118
|
+
* @param {{ defaultUnit?: string }} options Parse options.
|
|
119
|
+
* @returns {{ value: number, unit: 'mil' | 'mm' | 'in' | 'raw' } | null}
|
|
120
|
+
*/
|
|
121
|
+
static #parseNumericAndUnit(value, options) {
|
|
122
|
+
const defaultUnit = AltiumUnits.#normalizeUnit(
|
|
123
|
+
options?.defaultUnit || 'mil'
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if (typeof value === 'number') {
|
|
127
|
+
return Number.isFinite(value) ? { value, unit: defaultUnit } : null
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const text = String(value ?? '').trim()
|
|
131
|
+
const match = text.match(
|
|
132
|
+
/^([-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[-+]?\d+)?)\s*([A-Za-z]*)$/u
|
|
133
|
+
)
|
|
134
|
+
if (!match) return null
|
|
135
|
+
|
|
136
|
+
const numericValue = Number(match[1])
|
|
137
|
+
if (!Number.isFinite(numericValue)) return null
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
value: numericValue,
|
|
141
|
+
unit: AltiumUnits.#normalizeUnit(match[2] || defaultUnit)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Converts one normalized length value to mils.
|
|
147
|
+
* @param {number} value Numeric length.
|
|
148
|
+
* @param {'mil' | 'mm' | 'in' | 'raw'} unit Length unit.
|
|
149
|
+
* @returns {number}
|
|
150
|
+
*/
|
|
151
|
+
static #toMil(value, unit) {
|
|
152
|
+
if (unit === 'mm') return AltiumUnits.mmToMil(value)
|
|
153
|
+
if (unit === 'in') return AltiumUnits.inchToMil(value)
|
|
154
|
+
if (unit === 'raw') return AltiumUnits.rawToMil(value)
|
|
155
|
+
return Number(value)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Normalizes one unit label.
|
|
160
|
+
* @param {unknown} unit Unit label.
|
|
161
|
+
* @returns {'mil' | 'mm' | 'in' | 'raw'}
|
|
162
|
+
*/
|
|
163
|
+
static #normalizeUnit(unit) {
|
|
164
|
+
const normalized = String(unit || '')
|
|
165
|
+
.trim()
|
|
166
|
+
.toLowerCase()
|
|
167
|
+
if (
|
|
168
|
+
normalized === 'mm' ||
|
|
169
|
+
normalized === 'millimeter' ||
|
|
170
|
+
normalized === 'millimeters'
|
|
171
|
+
) {
|
|
172
|
+
return 'mm'
|
|
173
|
+
}
|
|
174
|
+
if (
|
|
175
|
+
normalized === 'in' ||
|
|
176
|
+
normalized === 'inch' ||
|
|
177
|
+
normalized === 'inches'
|
|
178
|
+
) {
|
|
179
|
+
return 'in'
|
|
180
|
+
}
|
|
181
|
+
if (normalized === 'raw') {
|
|
182
|
+
return 'raw'
|
|
183
|
+
}
|
|
184
|
+
return 'mil'
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Formats a number without unstable floating-point tails.
|
|
189
|
+
* @param {number} value Numeric value.
|
|
190
|
+
* @returns {string}
|
|
191
|
+
*/
|
|
192
|
+
static #formatNumber(value) {
|
|
193
|
+
return String(AltiumUnits.#round(value))
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Rounds a numeric value to stable JSON/report precision.
|
|
198
|
+
* @param {number} value Numeric value.
|
|
199
|
+
* @returns {number}
|
|
200
|
+
*/
|
|
201
|
+
static #round(value) {
|
|
202
|
+
const numeric = Number(value)
|
|
203
|
+
return Number.isFinite(numeric) ? Number(numeric.toFixed(9)) : numeric
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -63,6 +63,15 @@ export class AsciiRecordParser {
|
|
|
63
63
|
return records
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
/**
|
|
67
|
+
* Parses one pipe-delimited record string into a field object.
|
|
68
|
+
* @param {string} raw Raw record text.
|
|
69
|
+
* @returns {{ raw: string, fields: Record<string, string | string[]> }}
|
|
70
|
+
*/
|
|
71
|
+
static parseRecord(raw) {
|
|
72
|
+
return AsciiRecordParser.#parseRecord(raw)
|
|
73
|
+
}
|
|
74
|
+
|
|
66
75
|
/**
|
|
67
76
|
* Returns true when a printable run looks like an Altium record block.
|
|
68
77
|
* @param {string} candidate
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds deterministic inventories of embedded assets across parser roots.
|
|
7
|
+
*/
|
|
8
|
+
export class EmbeddedAssetReportBuilder {
|
|
9
|
+
static SCHEMA = 'altium-toolkit.embedded-assets.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds an embedded-asset report.
|
|
13
|
+
* @param {{ models?: object[] } | object[]} input Parser-root models.
|
|
14
|
+
* @returns {object}
|
|
15
|
+
*/
|
|
16
|
+
static build(input = {}) {
|
|
17
|
+
const models = EmbeddedAssetReportBuilder.#models(input)
|
|
18
|
+
const assets = EmbeddedAssetReportBuilder.#dedupeAssets(
|
|
19
|
+
models.flatMap((model) =>
|
|
20
|
+
EmbeddedAssetReportBuilder.#assetsForModel(model)
|
|
21
|
+
)
|
|
22
|
+
).sort(EmbeddedAssetReportBuilder.#compareAssets)
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
schema: EmbeddedAssetReportBuilder.SCHEMA,
|
|
26
|
+
summary: {
|
|
27
|
+
modelCount: models.length,
|
|
28
|
+
assetCount: assets.length,
|
|
29
|
+
totalByteCount: assets.reduce(
|
|
30
|
+
(count, asset) => count + asset.byteLength,
|
|
31
|
+
0
|
|
32
|
+
),
|
|
33
|
+
byKind: EmbeddedAssetReportBuilder.#byKind(assets)
|
|
34
|
+
},
|
|
35
|
+
assets
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Normalizes builder input to a model array.
|
|
41
|
+
* @param {{ models?: object[] } | object[]} input Builder input.
|
|
42
|
+
* @returns {object[]}
|
|
43
|
+
*/
|
|
44
|
+
static #models(input) {
|
|
45
|
+
if (Array.isArray(input)) return input
|
|
46
|
+
return Array.isArray(input?.models) ? input.models : []
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Collects embedded asset rows from one parser-root model.
|
|
51
|
+
* @param {object} model Parser-root model.
|
|
52
|
+
* @returns {object[]}
|
|
53
|
+
*/
|
|
54
|
+
static #assetsForModel(model) {
|
|
55
|
+
const base = {
|
|
56
|
+
modelFileName: String(model?.fileName || ''),
|
|
57
|
+
modelKind: String(model?.kind || '')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return [
|
|
61
|
+
...EmbeddedAssetReportBuilder.#schematicAssets(base, model),
|
|
62
|
+
...EmbeddedAssetReportBuilder.#schematicLibraryAssets(base, model),
|
|
63
|
+
...EmbeddedAssetReportBuilder.#pcbAssets(base, model),
|
|
64
|
+
...EmbeddedAssetReportBuilder.#pcbLibraryAssets(base, model),
|
|
65
|
+
...EmbeddedAssetReportBuilder.#integratedLibraryAssets(base, model)
|
|
66
|
+
].filter(Boolean)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Collects schematic-document assets.
|
|
71
|
+
* @param {object} base Shared row fields.
|
|
72
|
+
* @param {object} model Parser-root model.
|
|
73
|
+
* @returns {object[]}
|
|
74
|
+
*/
|
|
75
|
+
static #schematicAssets(base, model) {
|
|
76
|
+
const schematic = model?.schematic || {}
|
|
77
|
+
|
|
78
|
+
return [
|
|
79
|
+
...EmbeddedAssetReportBuilder.#collectionAssets(
|
|
80
|
+
base,
|
|
81
|
+
'embedded-file',
|
|
82
|
+
schematic.embeddedFiles?.files || []
|
|
83
|
+
),
|
|
84
|
+
...EmbeddedAssetReportBuilder.#collectionAssets(
|
|
85
|
+
base,
|
|
86
|
+
'schematic-image',
|
|
87
|
+
schematic.images || [],
|
|
88
|
+
{ fallbackName: 'image' }
|
|
89
|
+
),
|
|
90
|
+
...EmbeddedAssetReportBuilder.#collectionAssets(
|
|
91
|
+
base,
|
|
92
|
+
'schematic-thumbnail',
|
|
93
|
+
schematic.thumbnails || [],
|
|
94
|
+
{ fallbackName: 'thumbnail' }
|
|
95
|
+
)
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Collects schematic-library assets.
|
|
101
|
+
* @param {object} base Shared row fields.
|
|
102
|
+
* @param {object} model Parser-root model.
|
|
103
|
+
* @returns {object[]}
|
|
104
|
+
*/
|
|
105
|
+
static #schematicLibraryAssets(base, model) {
|
|
106
|
+
const schematicLibrary = model?.schematicLibrary || {}
|
|
107
|
+
const embeddedFiles = schematicLibrary.embeddedFiles?.files || []
|
|
108
|
+
const symbolAssets = (schematicLibrary.symbols || []).flatMap(
|
|
109
|
+
(symbol) =>
|
|
110
|
+
(symbol.embeddedAssets || []).map((asset) => ({
|
|
111
|
+
...asset,
|
|
112
|
+
symbolName: symbol.name
|
|
113
|
+
}))
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
return EmbeddedAssetReportBuilder.#collectionAssets(
|
|
117
|
+
base,
|
|
118
|
+
'embedded-file',
|
|
119
|
+
embeddedFiles.length ? embeddedFiles : symbolAssets
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Collects PCB-document assets.
|
|
125
|
+
* @param {object} base Shared row fields.
|
|
126
|
+
* @param {object} model Parser-root model.
|
|
127
|
+
* @returns {object[]}
|
|
128
|
+
*/
|
|
129
|
+
static #pcbAssets(base, model) {
|
|
130
|
+
const pcb = model?.pcb || {}
|
|
131
|
+
|
|
132
|
+
return [
|
|
133
|
+
...EmbeddedAssetReportBuilder.#collectionAssets(
|
|
134
|
+
base,
|
|
135
|
+
'pcb-font',
|
|
136
|
+
pcb.embeddedFonts || []
|
|
137
|
+
),
|
|
138
|
+
...EmbeddedAssetReportBuilder.#collectionAssets(
|
|
139
|
+
base,
|
|
140
|
+
'pcb-model',
|
|
141
|
+
pcb.embeddedModels || []
|
|
142
|
+
)
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Collects PCB-library assets.
|
|
148
|
+
* @param {object} base Shared row fields.
|
|
149
|
+
* @param {object} model Parser-root model.
|
|
150
|
+
* @returns {object[]}
|
|
151
|
+
*/
|
|
152
|
+
static #pcbLibraryAssets(base, model) {
|
|
153
|
+
const pcbLibrary = model?.pcbLibrary || {}
|
|
154
|
+
const footprintModels = (pcbLibrary.footprints || []).flatMap(
|
|
155
|
+
(footprint) =>
|
|
156
|
+
(footprint.embeddedModels || []).map((asset) => ({
|
|
157
|
+
...asset,
|
|
158
|
+
footprintName: footprint.name
|
|
159
|
+
}))
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return [
|
|
163
|
+
...EmbeddedAssetReportBuilder.#collectionAssets(
|
|
164
|
+
base,
|
|
165
|
+
'pcb-font',
|
|
166
|
+
pcbLibrary.embeddedFonts || []
|
|
167
|
+
),
|
|
168
|
+
...EmbeddedAssetReportBuilder.#collectionAssets(
|
|
169
|
+
base,
|
|
170
|
+
'pcb-model',
|
|
171
|
+
pcbLibrary.embeddedModels || []
|
|
172
|
+
),
|
|
173
|
+
...EmbeddedAssetReportBuilder.#collectionAssets(
|
|
174
|
+
base,
|
|
175
|
+
'pcb-model',
|
|
176
|
+
footprintModels
|
|
177
|
+
)
|
|
178
|
+
]
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Collects integrated-library source assets.
|
|
183
|
+
* @param {object} base Shared row fields.
|
|
184
|
+
* @param {object} model Parser-root model.
|
|
185
|
+
* @returns {object[]}
|
|
186
|
+
*/
|
|
187
|
+
static #integratedLibraryAssets(base, model) {
|
|
188
|
+
return EmbeddedAssetReportBuilder.#collectionAssets(
|
|
189
|
+
base,
|
|
190
|
+
'integrated-library-source',
|
|
191
|
+
model?.integratedLibrary?.sources || []
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Normalizes one asset collection.
|
|
197
|
+
* @param {object} base Shared row fields.
|
|
198
|
+
* @param {string} kind Asset kind.
|
|
199
|
+
* @param {object[]} assets Source assets.
|
|
200
|
+
* @param {{ fallbackName?: string }} options Collection options.
|
|
201
|
+
* @returns {object[]}
|
|
202
|
+
*/
|
|
203
|
+
static #collectionAssets(base, kind, assets, options = {}) {
|
|
204
|
+
return (Array.isArray(assets) ? assets : []).map((asset, index) =>
|
|
205
|
+
EmbeddedAssetReportBuilder.#assetRow(base, kind, asset, index, {
|
|
206
|
+
fallbackName: options.fallbackName || kind
|
|
207
|
+
})
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Normalizes one asset row.
|
|
213
|
+
* @param {object} base Shared row fields.
|
|
214
|
+
* @param {string} kind Asset kind.
|
|
215
|
+
* @param {object} asset Source asset.
|
|
216
|
+
* @param {number} index Collection index.
|
|
217
|
+
* @param {{ fallbackName: string }} options Row options.
|
|
218
|
+
* @returns {object}
|
|
219
|
+
*/
|
|
220
|
+
static #assetRow(base, kind, asset, index, options) {
|
|
221
|
+
return EmbeddedAssetReportBuilder.#stripUndefined({
|
|
222
|
+
...base,
|
|
223
|
+
kind,
|
|
224
|
+
name: EmbeddedAssetReportBuilder.#assetName(
|
|
225
|
+
asset,
|
|
226
|
+
index,
|
|
227
|
+
options.fallbackName
|
|
228
|
+
),
|
|
229
|
+
format: EmbeddedAssetReportBuilder.#format(asset),
|
|
230
|
+
sourceStream: EmbeddedAssetReportBuilder.#sourceStream(asset),
|
|
231
|
+
byteLength: EmbeddedAssetReportBuilder.#byteLength(asset),
|
|
232
|
+
symbolName: asset?.symbolName,
|
|
233
|
+
footprintName: asset?.footprintName
|
|
234
|
+
})
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Resolves a stable asset name.
|
|
239
|
+
* @param {object} asset Source asset.
|
|
240
|
+
* @param {number} index Collection index.
|
|
241
|
+
* @param {string} fallbackName Fallback prefix.
|
|
242
|
+
* @returns {string}
|
|
243
|
+
*/
|
|
244
|
+
static #assetName(asset, index, fallbackName) {
|
|
245
|
+
return String(
|
|
246
|
+
asset?.name ||
|
|
247
|
+
asset?.family ||
|
|
248
|
+
asset?.fileName ||
|
|
249
|
+
asset?.key ||
|
|
250
|
+
asset?.id ||
|
|
251
|
+
EmbeddedAssetReportBuilder.#basename(
|
|
252
|
+
EmbeddedAssetReportBuilder.#sourceStream(asset)
|
|
253
|
+
) ||
|
|
254
|
+
fallbackName + '-' + index
|
|
255
|
+
)
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Resolves a source stream or source path.
|
|
260
|
+
* @param {object} asset Source asset.
|
|
261
|
+
* @returns {string}
|
|
262
|
+
*/
|
|
263
|
+
static #sourceStream(asset) {
|
|
264
|
+
return String(asset?.sourceStream || asset?.path || asset?.stream || '')
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Resolves asset byte length.
|
|
269
|
+
* @param {object} asset Source asset.
|
|
270
|
+
* @returns {number}
|
|
271
|
+
*/
|
|
272
|
+
static #byteLength(asset) {
|
|
273
|
+
const value =
|
|
274
|
+
asset?.byteLength ??
|
|
275
|
+
asset?.byteCount ??
|
|
276
|
+
asset?.payloadByteLength ??
|
|
277
|
+
asset?.size ??
|
|
278
|
+
0
|
|
279
|
+
const numeric = Number(value)
|
|
280
|
+
return Number.isFinite(numeric) ? numeric : 0
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Resolves a compact asset format.
|
|
285
|
+
* @param {object} asset Source asset.
|
|
286
|
+
* @returns {string}
|
|
287
|
+
*/
|
|
288
|
+
static #format(asset) {
|
|
289
|
+
const explicit =
|
|
290
|
+
asset?.format || asset?.fileType || asset?.type || asset?.mimeType
|
|
291
|
+
const normalized = String(explicit || '').trim()
|
|
292
|
+
|
|
293
|
+
if (!normalized) return 'unknown'
|
|
294
|
+
if (normalized.includes('/')) {
|
|
295
|
+
return normalized.split('/').pop()
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return normalized
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Builds a sorted count map by asset kind.
|
|
303
|
+
* @param {object[]} assets Asset rows.
|
|
304
|
+
* @returns {Record<string, number>}
|
|
305
|
+
*/
|
|
306
|
+
static #byKind(assets) {
|
|
307
|
+
const counts = new Map()
|
|
308
|
+
|
|
309
|
+
for (const asset of assets) {
|
|
310
|
+
counts.set(asset.kind, (counts.get(asset.kind) || 0) + 1)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
return Object.fromEntries(
|
|
314
|
+
[...counts.entries()].sort(([left], [right]) =>
|
|
315
|
+
left.localeCompare(right)
|
|
316
|
+
)
|
|
317
|
+
)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Dedupe exact asset references.
|
|
322
|
+
* @param {object[]} assets Asset rows.
|
|
323
|
+
* @returns {object[]}
|
|
324
|
+
*/
|
|
325
|
+
static #dedupeAssets(assets) {
|
|
326
|
+
const byKey = new Map()
|
|
327
|
+
|
|
328
|
+
for (const asset of assets) {
|
|
329
|
+
const key = [
|
|
330
|
+
asset.modelFileName,
|
|
331
|
+
asset.modelKind,
|
|
332
|
+
asset.kind,
|
|
333
|
+
asset.sourceStream,
|
|
334
|
+
asset.name
|
|
335
|
+
].join('\0')
|
|
336
|
+
if (!byKey.has(key)) {
|
|
337
|
+
byKey.set(key, asset)
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
return [...byKey.values()]
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Sorts report rows deterministically.
|
|
346
|
+
* @param {object} left Left row.
|
|
347
|
+
* @param {object} right Right row.
|
|
348
|
+
* @returns {number}
|
|
349
|
+
*/
|
|
350
|
+
static #compareAssets(left, right) {
|
|
351
|
+
return (
|
|
352
|
+
left.modelFileName.localeCompare(right.modelFileName) ||
|
|
353
|
+
left.kind.localeCompare(right.kind) ||
|
|
354
|
+
left.name.localeCompare(right.name) ||
|
|
355
|
+
left.sourceStream.localeCompare(right.sourceStream)
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Resolves a path basename.
|
|
361
|
+
* @param {string} value Path-like value.
|
|
362
|
+
* @returns {string}
|
|
363
|
+
*/
|
|
364
|
+
static #basename(value) {
|
|
365
|
+
return (
|
|
366
|
+
String(value || '')
|
|
367
|
+
.split('/')
|
|
368
|
+
.filter(Boolean)
|
|
369
|
+
.pop() || ''
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Removes undefined values from a row.
|
|
375
|
+
* @param {object} row Row to normalize.
|
|
376
|
+
* @returns {object}
|
|
377
|
+
*/
|
|
378
|
+
static #stripUndefined(row) {
|
|
379
|
+
return Object.fromEntries(
|
|
380
|
+
Object.entries(row).filter(([, value]) => value !== undefined)
|
|
381
|
+
)
|
|
382
|
+
}
|
|
383
|
+
}
|