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,229 @@
|
|
|
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 { getField, parseNumericField } = ParserUtils
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Normalizes read-only PCB dimension records.
|
|
11
|
+
*/
|
|
12
|
+
export class PcbDimensionParser {
|
|
13
|
+
/**
|
|
14
|
+
* Parses Dimensions6/Data records into public dimension objects.
|
|
15
|
+
* @param {{ fields: Record<string, string | string[]>, sourceStream?: string }[]} records
|
|
16
|
+
* @returns {object[]}
|
|
17
|
+
*/
|
|
18
|
+
static parse(records) {
|
|
19
|
+
return (records || [])
|
|
20
|
+
.filter((record) => record.sourceStream === 'Dimensions6/Data')
|
|
21
|
+
.map((record, index) =>
|
|
22
|
+
PcbDimensionParser.#normalizeDimension(record.fields, index)
|
|
23
|
+
)
|
|
24
|
+
.filter(Boolean)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Normalizes one dimension record.
|
|
29
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
30
|
+
* @param {number} index Source record index within the dimensions stream.
|
|
31
|
+
* @returns {object | null}
|
|
32
|
+
*/
|
|
33
|
+
static #normalizeDimension(fields, index) {
|
|
34
|
+
const kindCode = PcbDimensionParser.#firstStringField(fields, [
|
|
35
|
+
'DIMENSIONTYPE',
|
|
36
|
+
'DIMENSIONKIND',
|
|
37
|
+
'KIND',
|
|
38
|
+
'TYPE'
|
|
39
|
+
])
|
|
40
|
+
const references = PcbDimensionParser.#parseReferences(fields)
|
|
41
|
+
const textLocation = PcbDimensionParser.#parsePoint(fields, [
|
|
42
|
+
'TEXTLOCATION',
|
|
43
|
+
'TEXT',
|
|
44
|
+
'LOCATION'
|
|
45
|
+
])
|
|
46
|
+
|
|
47
|
+
if (!kindCode && !references.length && !getField(fields, 'TEXT')) {
|
|
48
|
+
return null
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
dimensionIndex: index,
|
|
53
|
+
kind: PcbDimensionParser.#normalizeKind(kindCode),
|
|
54
|
+
kindCode,
|
|
55
|
+
name: getField(fields, 'NAME'),
|
|
56
|
+
layer: getField(fields, 'LAYER'),
|
|
57
|
+
text: getField(fields, 'TEXT'),
|
|
58
|
+
prefix: getField(fields, 'PREFIX'),
|
|
59
|
+
suffix: PcbDimensionParser.#rawStringField(fields, 'SUFFIX'),
|
|
60
|
+
precision: PcbDimensionParser.#nullableNumber(
|
|
61
|
+
parseNumericField(fields, 'PRECISION')
|
|
62
|
+
),
|
|
63
|
+
measuredValue: PcbDimensionParser.#nullableNumber(
|
|
64
|
+
PcbDimensionParser.#firstNumericField(fields, [
|
|
65
|
+
'MEASUREDVALUE',
|
|
66
|
+
'MEASURED',
|
|
67
|
+
'VALUE'
|
|
68
|
+
])
|
|
69
|
+
),
|
|
70
|
+
angleValue: PcbDimensionParser.#nullableNumber(
|
|
71
|
+
PcbDimensionParser.#firstNumericField(fields, [
|
|
72
|
+
'ANGLEVALUE',
|
|
73
|
+
'ANGLE',
|
|
74
|
+
'MEASUREDANGLE'
|
|
75
|
+
])
|
|
76
|
+
),
|
|
77
|
+
unit: PcbDimensionParser.#resolveUnit(fields),
|
|
78
|
+
references,
|
|
79
|
+
textLocation,
|
|
80
|
+
raw: { ...fields }
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parses reference points from indexed field names.
|
|
86
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
87
|
+
* @returns {{ index: number, x: number, y: number }[]}
|
|
88
|
+
*/
|
|
89
|
+
static #parseReferences(fields) {
|
|
90
|
+
const references = []
|
|
91
|
+
|
|
92
|
+
for (let index = 0; index < 16; index += 1) {
|
|
93
|
+
const point = PcbDimensionParser.#parsePoint(fields, [
|
|
94
|
+
'REFERENCE' + index,
|
|
95
|
+
'REF' + index,
|
|
96
|
+
'POINT' + index
|
|
97
|
+
])
|
|
98
|
+
if (!point) {
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
references.push({ index, ...point })
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return references
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Parses a point from common field prefixes.
|
|
109
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
110
|
+
* @param {string[]} prefixes Candidate field prefixes.
|
|
111
|
+
* @returns {{ x: number, y: number } | null}
|
|
112
|
+
*/
|
|
113
|
+
static #parsePoint(fields, prefixes) {
|
|
114
|
+
for (const prefix of prefixes) {
|
|
115
|
+
const x = PcbDimensionParser.#firstNumericField(fields, [
|
|
116
|
+
prefix + '_X',
|
|
117
|
+
prefix + '.X',
|
|
118
|
+
prefix + 'X'
|
|
119
|
+
])
|
|
120
|
+
const y = PcbDimensionParser.#firstNumericField(fields, [
|
|
121
|
+
prefix + '_Y',
|
|
122
|
+
prefix + '.Y',
|
|
123
|
+
prefix + 'Y'
|
|
124
|
+
])
|
|
125
|
+
|
|
126
|
+
if (x !== null && y !== null) {
|
|
127
|
+
return { x, y }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return null
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Reads the first non-empty string field.
|
|
136
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
137
|
+
* @param {string[]} keys Candidate keys.
|
|
138
|
+
* @returns {string}
|
|
139
|
+
*/
|
|
140
|
+
static #firstStringField(fields, keys) {
|
|
141
|
+
for (const key of keys) {
|
|
142
|
+
const value = getField(fields, key)
|
|
143
|
+
if (value) {
|
|
144
|
+
return value
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return ''
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Reads the first finite numeric field.
|
|
153
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
154
|
+
* @param {string[]} keys Candidate keys.
|
|
155
|
+
* @returns {number | null}
|
|
156
|
+
*/
|
|
157
|
+
static #firstNumericField(fields, keys) {
|
|
158
|
+
for (const key of keys) {
|
|
159
|
+
const value = parseNumericField(fields, key)
|
|
160
|
+
if (value !== null) {
|
|
161
|
+
return value
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Normalizes a dimension kind code.
|
|
170
|
+
* @param {string} kindCode Raw dimension kind.
|
|
171
|
+
* @returns {string}
|
|
172
|
+
*/
|
|
173
|
+
static #normalizeKind(kindCode) {
|
|
174
|
+
const normalized = String(kindCode || '').toLowerCase()
|
|
175
|
+
if (/ang/u.test(normalized)) return 'angular'
|
|
176
|
+
if (/radial|radius/u.test(normalized)) return 'radial'
|
|
177
|
+
if (/datum/u.test(normalized)) return 'datum'
|
|
178
|
+
if (/baseline/u.test(normalized)) return 'baseline'
|
|
179
|
+
if (/ordinate/u.test(normalized)) return 'ordinate'
|
|
180
|
+
return 'linear'
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Returns a number when finite, otherwise null.
|
|
185
|
+
* @param {number | null} value Numeric candidate.
|
|
186
|
+
* @returns {number | null}
|
|
187
|
+
*/
|
|
188
|
+
static #nullableNumber(value) {
|
|
189
|
+
return Number.isFinite(value) ? value : null
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Resolves a display unit from explicit or value-bearing fields.
|
|
194
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
195
|
+
* @returns {string}
|
|
196
|
+
*/
|
|
197
|
+
static #resolveUnit(fields) {
|
|
198
|
+
const explicit = PcbDimensionParser.#firstStringField(fields, [
|
|
199
|
+
'UNIT',
|
|
200
|
+
'UNITS'
|
|
201
|
+
])
|
|
202
|
+
if (explicit) {
|
|
203
|
+
return explicit
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const suffix = getField(fields, 'SUFFIX').trim()
|
|
207
|
+
if (suffix) {
|
|
208
|
+
return suffix
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const measured = getField(fields, 'MEASUREDVALUE')
|
|
212
|
+
const match = measured.match(/[a-zA-Z]+$/u)
|
|
213
|
+
return match ? match[0] : ''
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Reads one field without trimming display-significant whitespace.
|
|
218
|
+
* @param {Record<string, string | string[]>} fields Source fields.
|
|
219
|
+
* @param {string} key Field key.
|
|
220
|
+
* @returns {string}
|
|
221
|
+
*/
|
|
222
|
+
static #rawStringField(fields, key) {
|
|
223
|
+
const value = fields?.[key]
|
|
224
|
+
if (Array.isArray(value)) {
|
|
225
|
+
return String(value.findLast((entry) => entry !== '') || '')
|
|
226
|
+
}
|
|
227
|
+
return String(value || '')
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -24,12 +24,22 @@ export class PcbEmbeddedModelExtractor {
|
|
|
24
24
|
PcbEmbeddedModelExtractor.#parseModelMetadataStream(
|
|
25
25
|
streams.get('Models/Data')
|
|
26
26
|
)
|
|
27
|
-
const
|
|
28
|
-
|
|
27
|
+
const modelMetadataRows = modelMetadataRecords.map((fields, index) => ({
|
|
28
|
+
fields,
|
|
29
|
+
index,
|
|
30
|
+
sourceStream: 'Models/' + index,
|
|
31
|
+
id: PcbEmbeddedModelExtractor.#getField(fields, 'ID'),
|
|
32
|
+
name: PcbEmbeddedModelExtractor.#getField(fields, 'NAME'),
|
|
33
|
+
checksum: PcbEmbeddedModelExtractor.#normalizeChecksum(
|
|
34
|
+
PcbEmbeddedModelExtractor.#parseIntegerField(fields, 'CHECKSUM')
|
|
35
|
+
)
|
|
36
|
+
}))
|
|
37
|
+
const models = modelMetadataRows
|
|
38
|
+
.map((row) =>
|
|
29
39
|
PcbEmbeddedModelExtractor.#normalizeEmbeddedModel(
|
|
30
|
-
|
|
31
|
-
streams.get(
|
|
32
|
-
|
|
40
|
+
row.fields,
|
|
41
|
+
streams.get(row.sourceStream),
|
|
42
|
+
row.sourceStream
|
|
33
43
|
)
|
|
34
44
|
)
|
|
35
45
|
.filter(Boolean)
|
|
@@ -44,10 +54,18 @@ export class PcbEmbeddedModelExtractor {
|
|
|
44
54
|
'ShapeBasedComponentBodies6/Data'
|
|
45
55
|
)
|
|
46
56
|
])
|
|
57
|
+
const integrity = PcbEmbeddedModelExtractor.#buildIntegrityReport(
|
|
58
|
+
modelMetadataRows,
|
|
59
|
+
models,
|
|
60
|
+
componentBodies,
|
|
61
|
+
streams
|
|
62
|
+
)
|
|
47
63
|
|
|
48
64
|
return {
|
|
49
65
|
models,
|
|
50
|
-
componentBodies
|
|
66
|
+
componentBodies,
|
|
67
|
+
integrity,
|
|
68
|
+
diagnostics: integrity.issues
|
|
51
69
|
}
|
|
52
70
|
}
|
|
53
71
|
|
|
@@ -350,6 +368,193 @@ export class PcbEmbeddedModelExtractor {
|
|
|
350
368
|
return [...uniqueBodies.values()]
|
|
351
369
|
}
|
|
352
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Builds model metadata and payload integrity diagnostics.
|
|
373
|
+
* @param {object[]} metadataRows Parsed model metadata rows.
|
|
374
|
+
* @param {object[]} models Recovered payload models.
|
|
375
|
+
* @param {object[]} componentBodies Recovered component bodies.
|
|
376
|
+
* @param {Map<string, Uint8Array>} streams Compound streams.
|
|
377
|
+
* @returns {{ schema: string, issues: object[] }}
|
|
378
|
+
*/
|
|
379
|
+
static #buildIntegrityReport(
|
|
380
|
+
metadataRows,
|
|
381
|
+
models,
|
|
382
|
+
componentBodies,
|
|
383
|
+
streams
|
|
384
|
+
) {
|
|
385
|
+
const issues = [
|
|
386
|
+
...PcbEmbeddedModelExtractor.#missingPayloadIssues(
|
|
387
|
+
metadataRows,
|
|
388
|
+
models,
|
|
389
|
+
streams
|
|
390
|
+
),
|
|
391
|
+
...PcbEmbeddedModelExtractor.#duplicateChecksumIssues(metadataRows),
|
|
392
|
+
...PcbEmbeddedModelExtractor.#unresolvedBodyIssues(
|
|
393
|
+
componentBodies,
|
|
394
|
+
models
|
|
395
|
+
),
|
|
396
|
+
...PcbEmbeddedModelExtractor.#unreferencedModelIssues(
|
|
397
|
+
models,
|
|
398
|
+
componentBodies
|
|
399
|
+
)
|
|
400
|
+
]
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
schema: 'altium-toolkit.pcb.embedded-model-integrity.a1',
|
|
404
|
+
issues
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Reports metadata rows without a recoverable payload stream.
|
|
410
|
+
* @param {object[]} metadataRows Parsed metadata rows.
|
|
411
|
+
* @param {object[]} models Recovered model rows.
|
|
412
|
+
* @param {Map<string, Uint8Array>} streams Compound streams.
|
|
413
|
+
* @returns {object[]}
|
|
414
|
+
*/
|
|
415
|
+
static #missingPayloadIssues(metadataRows, models, streams) {
|
|
416
|
+
return (metadataRows || [])
|
|
417
|
+
.filter(
|
|
418
|
+
(row) =>
|
|
419
|
+
!streams.has(row.sourceStream) ||
|
|
420
|
+
!models.some((model) => model.id === row.id)
|
|
421
|
+
)
|
|
422
|
+
.map((row) => ({
|
|
423
|
+
code: streams.has(row.sourceStream)
|
|
424
|
+
? 'pcb.model.payload-unreadable'
|
|
425
|
+
: 'pcb.model.payload-missing',
|
|
426
|
+
severity: 'warning',
|
|
427
|
+
modelId: row.id,
|
|
428
|
+
checksum: row.checksum,
|
|
429
|
+
name: row.name,
|
|
430
|
+
sourceStream: row.sourceStream,
|
|
431
|
+
message: streams.has(row.sourceStream)
|
|
432
|
+
? 'Embedded model payload stream could not be decoded.'
|
|
433
|
+
: 'Embedded model metadata references a missing payload stream.'
|
|
434
|
+
}))
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Reports duplicate authored model checksums.
|
|
439
|
+
* @param {object[]} metadataRows Parsed metadata rows.
|
|
440
|
+
* @returns {object[]}
|
|
441
|
+
*/
|
|
442
|
+
static #duplicateChecksumIssues(metadataRows) {
|
|
443
|
+
const rowsByChecksum = new Map()
|
|
444
|
+
|
|
445
|
+
for (const row of metadataRows || []) {
|
|
446
|
+
if (!Number.isInteger(row.checksum)) {
|
|
447
|
+
continue
|
|
448
|
+
}
|
|
449
|
+
if (!rowsByChecksum.has(row.checksum)) {
|
|
450
|
+
rowsByChecksum.set(row.checksum, [])
|
|
451
|
+
}
|
|
452
|
+
rowsByChecksum.get(row.checksum).push(row)
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return [...rowsByChecksum.entries()]
|
|
456
|
+
.filter(([, rows]) => rows.length > 1)
|
|
457
|
+
.map(([checksum, rows]) => ({
|
|
458
|
+
code: 'pcb.model.checksum-duplicate',
|
|
459
|
+
severity: 'warning',
|
|
460
|
+
checksum,
|
|
461
|
+
modelIds: rows.map((row) => row.id).filter(Boolean),
|
|
462
|
+
sourceStreams: rows.map((row) => row.sourceStream),
|
|
463
|
+
message:
|
|
464
|
+
'Multiple embedded model metadata rows share one checksum.'
|
|
465
|
+
}))
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Reports component bodies that reference no recovered model.
|
|
470
|
+
* @param {object[]} componentBodies Component-body rows.
|
|
471
|
+
* @param {object[]} models Recovered model rows.
|
|
472
|
+
* @returns {object[]}
|
|
473
|
+
*/
|
|
474
|
+
static #unresolvedBodyIssues(componentBodies, models) {
|
|
475
|
+
return (componentBodies || [])
|
|
476
|
+
.filter(
|
|
477
|
+
(componentBody) =>
|
|
478
|
+
componentBody.embedded &&
|
|
479
|
+
!PcbEmbeddedModelExtractor.#bodyMatchesAnyModel(
|
|
480
|
+
componentBody,
|
|
481
|
+
models
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
.map((componentBody) => ({
|
|
485
|
+
code: 'pcb.model.body-unresolved',
|
|
486
|
+
severity: 'warning',
|
|
487
|
+
modelId: componentBody.modelId,
|
|
488
|
+
checksum: componentBody.checksum,
|
|
489
|
+
name: componentBody.name,
|
|
490
|
+
sourceStream: componentBody.sourceStream,
|
|
491
|
+
message:
|
|
492
|
+
'Component body references an embedded model that was not recovered.'
|
|
493
|
+
}))
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Reports recovered model payloads not referenced by any component body.
|
|
498
|
+
* @param {object[]} models Recovered model rows.
|
|
499
|
+
* @param {object[]} componentBodies Component-body rows.
|
|
500
|
+
* @returns {object[]}
|
|
501
|
+
*/
|
|
502
|
+
static #unreferencedModelIssues(models, componentBodies) {
|
|
503
|
+
if (!(componentBodies || []).length) {
|
|
504
|
+
return []
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
return (models || [])
|
|
508
|
+
.filter(
|
|
509
|
+
(model) =>
|
|
510
|
+
!(componentBodies || []).some((componentBody) =>
|
|
511
|
+
PcbEmbeddedModelExtractor.#bodyMatchesModel(
|
|
512
|
+
componentBody,
|
|
513
|
+
model
|
|
514
|
+
)
|
|
515
|
+
)
|
|
516
|
+
)
|
|
517
|
+
.map((model) => ({
|
|
518
|
+
code: 'pcb.model.payload-unreferenced',
|
|
519
|
+
severity: 'info',
|
|
520
|
+
modelId: model.id,
|
|
521
|
+
checksum: model.checksum,
|
|
522
|
+
name: model.name,
|
|
523
|
+
sourceStream: model.sourceStream,
|
|
524
|
+
message:
|
|
525
|
+
'Embedded model payload was recovered but no component body references it.'
|
|
526
|
+
}))
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Returns true when a component body matches any recovered model.
|
|
531
|
+
* @param {object} componentBody Component body.
|
|
532
|
+
* @param {object[]} models Recovered models.
|
|
533
|
+
* @returns {boolean}
|
|
534
|
+
*/
|
|
535
|
+
static #bodyMatchesAnyModel(componentBody, models) {
|
|
536
|
+
return (models || []).some((model) =>
|
|
537
|
+
PcbEmbeddedModelExtractor.#bodyMatchesModel(componentBody, model)
|
|
538
|
+
)
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Returns true when a component body references a model.
|
|
543
|
+
* @param {object} componentBody Component body.
|
|
544
|
+
* @param {object} model Recovered model.
|
|
545
|
+
* @returns {boolean}
|
|
546
|
+
*/
|
|
547
|
+
static #bodyMatchesModel(componentBody, model) {
|
|
548
|
+
return (
|
|
549
|
+
(componentBody.modelId && componentBody.modelId === model.id) ||
|
|
550
|
+
(Number.isInteger(componentBody.checksum) &&
|
|
551
|
+
componentBody.checksum === model.checksum) ||
|
|
552
|
+
(componentBody.name &&
|
|
553
|
+
String(componentBody.name).toLowerCase() ===
|
|
554
|
+
String(model.name || '').toLowerCase())
|
|
555
|
+
)
|
|
556
|
+
}
|
|
557
|
+
|
|
353
558
|
/**
|
|
354
559
|
* Resolves one model format from metadata and payload text.
|
|
355
560
|
* @param {string} name
|
|
@@ -374,6 +579,27 @@ export class PcbEmbeddedModelExtractor {
|
|
|
374
579
|
return 'wrl'
|
|
375
580
|
}
|
|
376
581
|
|
|
582
|
+
if (
|
|
583
|
+
normalizedName.endsWith('.sldprt') ||
|
|
584
|
+
normalizedName.endsWith('.sldasm')
|
|
585
|
+
) {
|
|
586
|
+
return 'solidworks'
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
if (
|
|
590
|
+
normalizedName.endsWith('.x_t') ||
|
|
591
|
+
normalizedName.endsWith('.xmt_txt')
|
|
592
|
+
) {
|
|
593
|
+
return 'parasolid-text'
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
if (
|
|
597
|
+
normalizedName.endsWith('.x_b') ||
|
|
598
|
+
normalizedName.endsWith('.xmt_bin')
|
|
599
|
+
) {
|
|
600
|
+
return 'parasolid-binary'
|
|
601
|
+
}
|
|
602
|
+
|
|
377
603
|
return 'unknown'
|
|
378
604
|
}
|
|
379
605
|
|