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,256 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { PcbSidecarRecordParser } from './PcbSidecarRecordParser.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Decodes extended primitive sidecar records such as mask-expansion overrides.
|
|
9
|
+
*/
|
|
10
|
+
export class PcbExtendedPrimitiveInformationParser {
|
|
11
|
+
static #SOURCE_STREAM = 'ExtendedPrimitiveInformation/Data'
|
|
12
|
+
|
|
13
|
+
static #OBJECT_ID_TO_COLLECTION = {
|
|
14
|
+
1: ['arcs', 'arc'],
|
|
15
|
+
2: ['pads', 'pad'],
|
|
16
|
+
3: ['vias', 'via'],
|
|
17
|
+
4: ['tracks', 'track'],
|
|
18
|
+
5: ['texts', 'text'],
|
|
19
|
+
6: ['fills', 'fill'],
|
|
20
|
+
11: ['regions', 'region']
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parses extended primitive information records.
|
|
25
|
+
* @param {Uint8Array | ArrayBuffer | undefined} dataBytes
|
|
26
|
+
* @param {string} [sourceStream]
|
|
27
|
+
* @returns {{ entries: object[], byPrimitiveIndex: Record<string, object>, byPrimitiveKey: Record<string, object> }}
|
|
28
|
+
*/
|
|
29
|
+
static parse(
|
|
30
|
+
dataBytes,
|
|
31
|
+
sourceStream = PcbExtendedPrimitiveInformationParser.#SOURCE_STREAM
|
|
32
|
+
) {
|
|
33
|
+
const entries = PcbSidecarRecordParser.parseLengthPrefixedRecords(
|
|
34
|
+
dataBytes,
|
|
35
|
+
sourceStream
|
|
36
|
+
)
|
|
37
|
+
.map((record) =>
|
|
38
|
+
PcbExtendedPrimitiveInformationParser.#normalizeRecord(record)
|
|
39
|
+
)
|
|
40
|
+
.filter(Boolean)
|
|
41
|
+
|
|
42
|
+
return PcbExtendedPrimitiveInformationParser.#buildLookups(entries)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Adds extended primitive information to matching decoded primitives.
|
|
47
|
+
* @param {Record<string, object[]>} binaryPrimitives
|
|
48
|
+
* @param {{ entries?: object[] }} extendedInformation
|
|
49
|
+
*/
|
|
50
|
+
static attachToPrimitives(binaryPrimitives, extendedInformation) {
|
|
51
|
+
if (!binaryPrimitives || !Array.isArray(extendedInformation?.entries)) {
|
|
52
|
+
return
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const entry of extendedInformation.entries) {
|
|
56
|
+
const collectionName =
|
|
57
|
+
PcbExtendedPrimitiveInformationParser.#collectionNameForEntry(
|
|
58
|
+
entry
|
|
59
|
+
)
|
|
60
|
+
const collection = binaryPrimitives[collectionName]
|
|
61
|
+
|
|
62
|
+
if (!Array.isArray(collection)) {
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const primitive = collection[entry.primitiveIndex]
|
|
67
|
+
if (!primitive) {
|
|
68
|
+
continue
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
primitive.extendedPrimitiveInformation =
|
|
72
|
+
PcbExtendedPrimitiveInformationParser.#publicEntry(entry)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Normalizes one decoded sidecar record.
|
|
78
|
+
* @param {{ fields: Record<string, string>, sourceStream: string, recordIndex: number }} record
|
|
79
|
+
* @returns {object | null}
|
|
80
|
+
*/
|
|
81
|
+
static #normalizeRecord(record) {
|
|
82
|
+
const primitiveIndex = PcbSidecarRecordParser.parseInteger(
|
|
83
|
+
PcbSidecarRecordParser.firstField(record.fields, [
|
|
84
|
+
'PRIMITIVEINDEX',
|
|
85
|
+
'INDEX'
|
|
86
|
+
])
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
if (primitiveIndex === null) {
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const primitiveObjectId = PcbSidecarRecordParser.parseInteger(
|
|
94
|
+
PcbSidecarRecordParser.firstField(record.fields, [
|
|
95
|
+
'PRIMITIVEOBJECTID',
|
|
96
|
+
'OBJECTID'
|
|
97
|
+
])
|
|
98
|
+
)
|
|
99
|
+
const objectInfo =
|
|
100
|
+
PcbExtendedPrimitiveInformationParser.#OBJECT_ID_TO_COLLECTION[
|
|
101
|
+
primitiveObjectId
|
|
102
|
+
] || []
|
|
103
|
+
const type = PcbSidecarRecordParser.firstField(record.fields, ['TYPE'])
|
|
104
|
+
const primitiveType =
|
|
105
|
+
objectInfo[1] ||
|
|
106
|
+
PcbExtendedPrimitiveInformationParser.#normalizePrimitiveType(type)
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
primitiveIndex,
|
|
110
|
+
primitiveObjectId,
|
|
111
|
+
primitiveType,
|
|
112
|
+
type,
|
|
113
|
+
sourceStream: record.sourceStream,
|
|
114
|
+
maskExpansion:
|
|
115
|
+
PcbExtendedPrimitiveInformationParser.#parseMaskExpansion(
|
|
116
|
+
record.fields
|
|
117
|
+
),
|
|
118
|
+
fields: record.fields
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Parses paste and solder mask-expansion fields.
|
|
124
|
+
* @param {Record<string, string>} fields
|
|
125
|
+
* @returns {{ paste: object, solder: object }}
|
|
126
|
+
*/
|
|
127
|
+
static #parseMaskExpansion(fields) {
|
|
128
|
+
const pasteMode = PcbSidecarRecordParser.parseInteger(
|
|
129
|
+
PcbSidecarRecordParser.firstField(fields, [
|
|
130
|
+
'PASTEMASKEXPANSIONMODE',
|
|
131
|
+
'PASTEMASKEXPANSION_MODE'
|
|
132
|
+
])
|
|
133
|
+
)
|
|
134
|
+
const solderMode = PcbSidecarRecordParser.parseInteger(
|
|
135
|
+
PcbSidecarRecordParser.firstField(fields, [
|
|
136
|
+
'SOLDERMASKEXPANSIONMODE',
|
|
137
|
+
'SOLDERMASKEXPANSION_MODE'
|
|
138
|
+
])
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
paste: {
|
|
143
|
+
mode: pasteMode,
|
|
144
|
+
source: PcbExtendedPrimitiveInformationParser.#maskExpansionSource(
|
|
145
|
+
pasteMode
|
|
146
|
+
),
|
|
147
|
+
manualExpansion: PcbSidecarRecordParser.parseNumber(
|
|
148
|
+
PcbSidecarRecordParser.firstField(fields, [
|
|
149
|
+
'PASTEMASKEXPANSION_MANUAL',
|
|
150
|
+
'PASTEMASKEXPANSIONMANUAL'
|
|
151
|
+
])
|
|
152
|
+
)
|
|
153
|
+
},
|
|
154
|
+
solder: {
|
|
155
|
+
mode: solderMode,
|
|
156
|
+
source: PcbExtendedPrimitiveInformationParser.#maskExpansionSource(
|
|
157
|
+
solderMode
|
|
158
|
+
),
|
|
159
|
+
manualExpansion: PcbSidecarRecordParser.parseNumber(
|
|
160
|
+
PcbSidecarRecordParser.firstField(fields, [
|
|
161
|
+
'SOLDERMASKEXPANSION_MANUAL',
|
|
162
|
+
'SOLDERMASKEXPANSIONMANUAL'
|
|
163
|
+
])
|
|
164
|
+
)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Builds primitive-index lookups.
|
|
171
|
+
* @param {object[]} entries
|
|
172
|
+
* @returns {{ entries: object[], byPrimitiveIndex: Record<string, object>, byPrimitiveKey: Record<string, object> }}
|
|
173
|
+
*/
|
|
174
|
+
static #buildLookups(entries) {
|
|
175
|
+
const byPrimitiveIndex = {}
|
|
176
|
+
const byPrimitiveKey = {}
|
|
177
|
+
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
byPrimitiveIndex[String(entry.primitiveIndex)] = entry
|
|
180
|
+
if (Number.isInteger(entry.primitiveObjectId)) {
|
|
181
|
+
byPrimitiveKey[
|
|
182
|
+
entry.primitiveObjectId + ':' + entry.primitiveIndex
|
|
183
|
+
] = entry
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
entries,
|
|
189
|
+
byPrimitiveIndex,
|
|
190
|
+
byPrimitiveKey
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Resolves one binary primitive collection for a sidecar entry.
|
|
196
|
+
* @param {object} entry
|
|
197
|
+
* @returns {string}
|
|
198
|
+
*/
|
|
199
|
+
static #collectionNameForEntry(entry) {
|
|
200
|
+
const objectInfo =
|
|
201
|
+
PcbExtendedPrimitiveInformationParser.#OBJECT_ID_TO_COLLECTION[
|
|
202
|
+
entry.primitiveObjectId
|
|
203
|
+
]
|
|
204
|
+
if (objectInfo) {
|
|
205
|
+
return objectInfo[0]
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const primitiveType = String(entry.primitiveType || '').toLowerCase()
|
|
209
|
+
return primitiveType ? primitiveType + 's' : ''
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Builds the public primitive-attached entry.
|
|
214
|
+
* @param {object} entry
|
|
215
|
+
* @returns {object}
|
|
216
|
+
*/
|
|
217
|
+
static #publicEntry(entry) {
|
|
218
|
+
return {
|
|
219
|
+
primitiveIndex: entry.primitiveIndex,
|
|
220
|
+
primitiveObjectId: entry.primitiveObjectId,
|
|
221
|
+
primitiveType: entry.primitiveType,
|
|
222
|
+
type: entry.type,
|
|
223
|
+
sourceStream: entry.sourceStream,
|
|
224
|
+
maskExpansion: entry.maskExpansion
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Maps one mask-expansion mode to the existing source vocabulary.
|
|
230
|
+
* @param {number | null} mode
|
|
231
|
+
* @returns {string}
|
|
232
|
+
*/
|
|
233
|
+
static #maskExpansionSource(mode) {
|
|
234
|
+
if (mode === 1) return 'rule'
|
|
235
|
+
if (mode === 2) return 'manual'
|
|
236
|
+
if (mode === 0) return 'default'
|
|
237
|
+
if (mode === null) return 'unknown'
|
|
238
|
+
|
|
239
|
+
return 'unknown-' + mode
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Normalizes one textual primitive type hint.
|
|
244
|
+
* @param {string} value
|
|
245
|
+
* @returns {string}
|
|
246
|
+
*/
|
|
247
|
+
static #normalizePrimitiveType(value) {
|
|
248
|
+
const normalized = String(value || '')
|
|
249
|
+
.trim()
|
|
250
|
+
.toLowerCase()
|
|
251
|
+
.replace(/^e/iu, '')
|
|
252
|
+
.replace(/object$/iu, '')
|
|
253
|
+
|
|
254
|
+
return normalized
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
|
|
5
5
|
import { NormalizedModelSchema } from './NormalizedModelSchema.mjs'
|
|
6
6
|
import { ParserUtils } from './ParserUtils.mjs'
|
|
7
|
+
import { LibraryRenderManifestBuilder } from './LibraryRenderManifestBuilder.mjs'
|
|
8
|
+
import { PcbCustomPadShapeParser } from './PcbCustomPadShapeParser.mjs'
|
|
9
|
+
import { PcbDefaultsParser } from './PcbDefaultsParser.mjs'
|
|
10
|
+
import { PcbExtendedPrimitiveInformationParser } from './PcbExtendedPrimitiveInformationParser.mjs'
|
|
11
|
+
import { PcbMaskPasteResolver } from './PcbMaskPasteResolver.mjs'
|
|
7
12
|
|
|
8
13
|
const { stripExtension } = ParserUtils
|
|
9
14
|
|
|
@@ -19,24 +24,56 @@ export class PcbLibModelParser {
|
|
|
19
24
|
*/
|
|
20
25
|
static parse(fileName, extraction) {
|
|
21
26
|
const safeExtraction = extraction || {}
|
|
27
|
+
const embeddedModels = Array.isArray(
|
|
28
|
+
safeExtraction.embeddedModels?.models
|
|
29
|
+
)
|
|
30
|
+
? safeExtraction.embeddedModels.models
|
|
31
|
+
: []
|
|
32
|
+
const componentBodies = Array.isArray(
|
|
33
|
+
safeExtraction.embeddedModels?.componentBodies
|
|
34
|
+
)
|
|
35
|
+
? safeExtraction.embeddedModels.componentBodies
|
|
36
|
+
: []
|
|
22
37
|
const footprints = Array.isArray(safeExtraction.footprints)
|
|
23
38
|
? safeExtraction.footprints.map((footprint) =>
|
|
24
|
-
PcbLibModelParser.#normalizeFootprint(
|
|
39
|
+
PcbLibModelParser.#normalizeFootprint(
|
|
40
|
+
footprint,
|
|
41
|
+
embeddedModels
|
|
42
|
+
)
|
|
25
43
|
)
|
|
26
44
|
: []
|
|
27
45
|
const embeddedFonts = Array.isArray(safeExtraction.embeddedFonts?.fonts)
|
|
28
46
|
? safeExtraction.embeddedFonts.fonts
|
|
29
47
|
: []
|
|
48
|
+
const defaults = PcbDefaultsParser.parse(
|
|
49
|
+
safeExtraction.libraryHeader || {},
|
|
50
|
+
'pcb-library'
|
|
51
|
+
)
|
|
30
52
|
const summary = PcbLibModelParser.#buildSummary(
|
|
31
53
|
fileName,
|
|
32
54
|
footprints,
|
|
33
|
-
embeddedFonts
|
|
55
|
+
embeddedFonts,
|
|
56
|
+
embeddedModels
|
|
34
57
|
)
|
|
35
58
|
const diagnostics = PcbLibModelParser.#buildDiagnostics(
|
|
36
59
|
footprints,
|
|
37
60
|
embeddedFonts,
|
|
61
|
+
embeddedModels,
|
|
38
62
|
safeExtraction
|
|
39
63
|
)
|
|
64
|
+
const pcbLibrary = {
|
|
65
|
+
libraryHeader: safeExtraction.libraryHeader || {},
|
|
66
|
+
componentParamsToc: safeExtraction.componentParamsToc || {},
|
|
67
|
+
sectionKeys: safeExtraction.sectionKeys || {},
|
|
68
|
+
footprints,
|
|
69
|
+
indexes: PcbLibModelParser.#buildIndexes(footprints),
|
|
70
|
+
embeddedFonts,
|
|
71
|
+
embeddedModels,
|
|
72
|
+
componentBodies,
|
|
73
|
+
...(defaults ? { defaults } : {})
|
|
74
|
+
}
|
|
75
|
+
pcbLibrary.renderManifest =
|
|
76
|
+
LibraryRenderManifestBuilder.buildPcbLibraryManifest(pcbLibrary)
|
|
40
77
|
|
|
41
78
|
return NormalizedModelSchema.attach({
|
|
42
79
|
kind: 'pcb-library',
|
|
@@ -44,13 +81,7 @@ export class PcbLibModelParser {
|
|
|
44
81
|
fileName,
|
|
45
82
|
summary,
|
|
46
83
|
diagnostics,
|
|
47
|
-
pcbLibrary
|
|
48
|
-
libraryHeader: safeExtraction.libraryHeader || {},
|
|
49
|
-
componentParamsToc: safeExtraction.componentParamsToc || {},
|
|
50
|
-
sectionKeys: safeExtraction.sectionKeys || {},
|
|
51
|
-
footprints,
|
|
52
|
-
embeddedFonts
|
|
53
|
-
},
|
|
84
|
+
pcbLibrary,
|
|
54
85
|
bom: []
|
|
55
86
|
})
|
|
56
87
|
}
|
|
@@ -60,9 +91,10 @@ export class PcbLibModelParser {
|
|
|
60
91
|
* @param {string} fileName
|
|
61
92
|
* @param {object[]} footprints
|
|
62
93
|
* @param {object[]} embeddedFonts
|
|
94
|
+
* @param {object[]} embeddedModels
|
|
63
95
|
* @returns {Record<string, number | string>}
|
|
64
96
|
*/
|
|
65
|
-
static #buildSummary(fileName, footprints, embeddedFonts) {
|
|
97
|
+
static #buildSummary(fileName, footprints, embeddedFonts, embeddedModels) {
|
|
66
98
|
return {
|
|
67
99
|
title: stripExtension(fileName),
|
|
68
100
|
footprintCount: footprints.length,
|
|
@@ -81,7 +113,8 @@ export class PcbLibModelParser {
|
|
|
81
113
|
footprints,
|
|
82
114
|
'rawRecords'
|
|
83
115
|
),
|
|
84
|
-
embeddedFontCount: embeddedFonts.length
|
|
116
|
+
embeddedFontCount: embeddedFonts.length,
|
|
117
|
+
embeddedModelCount: embeddedModels.length
|
|
85
118
|
}
|
|
86
119
|
}
|
|
87
120
|
|
|
@@ -89,10 +122,16 @@ export class PcbLibModelParser {
|
|
|
89
122
|
* Builds parser diagnostics from extraction metadata.
|
|
90
123
|
* @param {object[]} footprints
|
|
91
124
|
* @param {object[]} embeddedFonts
|
|
125
|
+
* @param {object[]} embeddedModels
|
|
92
126
|
* @param {{ streamNames?: string[], diagnostics?: Record<string, number> }} extraction
|
|
93
127
|
* @returns {{ severity: 'info' | 'warning', message: string }[]}
|
|
94
128
|
*/
|
|
95
|
-
static #buildDiagnostics(
|
|
129
|
+
static #buildDiagnostics(
|
|
130
|
+
footprints,
|
|
131
|
+
embeddedFonts,
|
|
132
|
+
embeddedModels,
|
|
133
|
+
extraction
|
|
134
|
+
) {
|
|
96
135
|
const diagnostics = [
|
|
97
136
|
{
|
|
98
137
|
severity: 'info',
|
|
@@ -123,6 +162,16 @@ export class PcbLibModelParser {
|
|
|
123
162
|
})
|
|
124
163
|
}
|
|
125
164
|
|
|
165
|
+
if (embeddedModels.length) {
|
|
166
|
+
diagnostics.push({
|
|
167
|
+
severity: 'info',
|
|
168
|
+
message:
|
|
169
|
+
'Recovered ' +
|
|
170
|
+
embeddedModels.length +
|
|
171
|
+
' embedded PcbLib model payloads.'
|
|
172
|
+
})
|
|
173
|
+
}
|
|
174
|
+
|
|
126
175
|
if (extraction.diagnostics?.missingFootprintCount) {
|
|
127
176
|
diagnostics.push({
|
|
128
177
|
severity: 'warning',
|
|
@@ -139,9 +188,10 @@ export class PcbLibModelParser {
|
|
|
139
188
|
/**
|
|
140
189
|
* Normalizes one extracted footprint object with stable primitive arrays.
|
|
141
190
|
* @param {object} footprint
|
|
191
|
+
* @param {object[]} libraryEmbeddedModels Library-level embedded models.
|
|
142
192
|
* @returns {object}
|
|
143
193
|
*/
|
|
144
|
-
static #normalizeFootprint(footprint) {
|
|
194
|
+
static #normalizeFootprint(footprint, libraryEmbeddedModels = []) {
|
|
145
195
|
const normalized = {
|
|
146
196
|
name: String(footprint.name || ''),
|
|
147
197
|
dataName: String(footprint.dataName || footprint.name || ''),
|
|
@@ -158,6 +208,13 @@ export class PcbLibModelParser {
|
|
|
158
208
|
unknownRecords: Array.isArray(footprint.unknownRecords)
|
|
159
209
|
? footprint.unknownRecords
|
|
160
210
|
: [],
|
|
211
|
+
implementations: PcbLibModelParser.#array(
|
|
212
|
+
footprint.implementations
|
|
213
|
+
),
|
|
214
|
+
componentModels: PcbLibModelParser.#array(
|
|
215
|
+
footprint.componentModels
|
|
216
|
+
),
|
|
217
|
+
pinDisplayModes: footprint.pinDisplayModes || {},
|
|
161
218
|
rawRecords: PcbLibModelParser.#array(footprint.rawRecords),
|
|
162
219
|
pads: PcbLibModelParser.#array(footprint.pads),
|
|
163
220
|
tracks: PcbLibModelParser.#array(footprint.tracks),
|
|
@@ -165,8 +222,51 @@ export class PcbLibModelParser {
|
|
|
165
222
|
vias: PcbLibModelParser.#array(footprint.vias),
|
|
166
223
|
fills: PcbLibModelParser.#array(footprint.fills),
|
|
167
224
|
texts: PcbLibModelParser.#array(footprint.texts),
|
|
168
|
-
regions: PcbLibModelParser.#array(footprint.regions)
|
|
225
|
+
regions: PcbLibModelParser.#array(footprint.regions),
|
|
226
|
+
shapeBasedRegions: PcbLibModelParser.#array(
|
|
227
|
+
footprint.shapeBasedRegions
|
|
228
|
+
),
|
|
229
|
+
extendedPrimitiveInformation:
|
|
230
|
+
footprint.extendedPrimitiveInformation || {
|
|
231
|
+
entries: [],
|
|
232
|
+
byPrimitiveIndex: {},
|
|
233
|
+
byPrimitiveKey: {}
|
|
234
|
+
},
|
|
235
|
+
customPadShapes: footprint.customPadShapes || {
|
|
236
|
+
entries: [],
|
|
237
|
+
byPrimitiveIndex: {}
|
|
238
|
+
},
|
|
239
|
+
embeddedModels: PcbLibModelParser.#array(footprint.embeddedModels),
|
|
240
|
+
componentBodies: PcbLibModelParser.#array(footprint.componentBodies)
|
|
169
241
|
}
|
|
242
|
+
normalized.defaults = PcbDefaultsParser.parse(
|
|
243
|
+
{
|
|
244
|
+
...(footprint.defaults || {}),
|
|
245
|
+
...(footprint.parameters || {})
|
|
246
|
+
},
|
|
247
|
+
'pcb-library-footprint'
|
|
248
|
+
)
|
|
249
|
+
PcbExtendedPrimitiveInformationParser.attachToPrimitives(
|
|
250
|
+
normalized,
|
|
251
|
+
normalized.extendedPrimitiveInformation
|
|
252
|
+
)
|
|
253
|
+
PcbCustomPadShapeParser.attachToPads(
|
|
254
|
+
normalized.pads,
|
|
255
|
+
normalized.customPadShapes,
|
|
256
|
+
normalized
|
|
257
|
+
)
|
|
258
|
+
normalized.componentBodies = PcbLibModelParser.#annotateComponentBodies(
|
|
259
|
+
normalized.componentBodies,
|
|
260
|
+
normalized.embeddedModels.length
|
|
261
|
+
? normalized.embeddedModels
|
|
262
|
+
: libraryEmbeddedModels
|
|
263
|
+
)
|
|
264
|
+
normalized.maskPaste = PcbMaskPasteResolver.build({
|
|
265
|
+
pads: normalized.pads,
|
|
266
|
+
vias: normalized.vias,
|
|
267
|
+
rules: footprint.rules || [],
|
|
268
|
+
defaults: normalized.defaults
|
|
269
|
+
})
|
|
170
270
|
|
|
171
271
|
return {
|
|
172
272
|
...normalized,
|
|
@@ -176,6 +276,127 @@ export class PcbLibModelParser {
|
|
|
176
276
|
}
|
|
177
277
|
}
|
|
178
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Adds deterministic projection diagnostics to footprint body records.
|
|
281
|
+
* @param {object[]} componentBodies Component body records.
|
|
282
|
+
* @param {object[]} embeddedModels Embedded model records.
|
|
283
|
+
* @returns {object[]}
|
|
284
|
+
*/
|
|
285
|
+
static #annotateComponentBodies(componentBodies, embeddedModels) {
|
|
286
|
+
return componentBodies.map((componentBody) => ({
|
|
287
|
+
...componentBody,
|
|
288
|
+
projectionDiagnostics: PcbLibModelParser.#projectionDiagnostics(
|
|
289
|
+
componentBody,
|
|
290
|
+
embeddedModels
|
|
291
|
+
)
|
|
292
|
+
}))
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Resolves one component-body projection diagnostic.
|
|
297
|
+
* @param {object} componentBody Component body record.
|
|
298
|
+
* @param {object[]} embeddedModels Embedded model records.
|
|
299
|
+
* @returns {{ source: string, reason: string }}
|
|
300
|
+
*/
|
|
301
|
+
static #projectionDiagnostics(componentBody, embeddedModels) {
|
|
302
|
+
const matchedModel = (embeddedModels || []).find(
|
|
303
|
+
(model) =>
|
|
304
|
+
PcbLibModelParser.#sameNonEmptyValue(
|
|
305
|
+
model?.id,
|
|
306
|
+
componentBody?.modelId
|
|
307
|
+
) ||
|
|
308
|
+
PcbLibModelParser.#sameNonEmptyValue(
|
|
309
|
+
model?.checksum,
|
|
310
|
+
componentBody?.checksum
|
|
311
|
+
) ||
|
|
312
|
+
PcbLibModelParser.#sameNonEmptyValue(
|
|
313
|
+
model?.name,
|
|
314
|
+
componentBody?.name
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
if (matchedModel) {
|
|
319
|
+
return {
|
|
320
|
+
source: 'embedded-model',
|
|
321
|
+
reason: 'matched embedded model payload'
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return {
|
|
326
|
+
source: 'fallback',
|
|
327
|
+
reason: 'no embedded model payload matched this body'
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Compares two values only when both are present.
|
|
333
|
+
* @param {unknown} left First value.
|
|
334
|
+
* @param {unknown} right Second value.
|
|
335
|
+
* @returns {boolean}
|
|
336
|
+
*/
|
|
337
|
+
static #sameNonEmptyValue(left, right) {
|
|
338
|
+
return (
|
|
339
|
+
left !== null &&
|
|
340
|
+
left !== undefined &&
|
|
341
|
+
left !== '' &&
|
|
342
|
+
right !== null &&
|
|
343
|
+
right !== undefined &&
|
|
344
|
+
right !== '' &&
|
|
345
|
+
String(left) === String(right)
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Builds footprint lookup indexes for library consumers.
|
|
351
|
+
* @param {object[]} footprints Normalized footprints.
|
|
352
|
+
* @returns {object}
|
|
353
|
+
*/
|
|
354
|
+
static #buildIndexes(footprints) {
|
|
355
|
+
const footprintsByName = {}
|
|
356
|
+
|
|
357
|
+
for (const [index, footprint] of footprints.entries()) {
|
|
358
|
+
footprintsByName[footprint.name] =
|
|
359
|
+
PcbLibModelParser.#footprintIndexEntry(footprint, index)
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return { footprintsByName }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Builds one footprint index entry.
|
|
367
|
+
* @param {object} footprint Normalized footprint.
|
|
368
|
+
* @param {number} index Footprint index.
|
|
369
|
+
* @returns {object}
|
|
370
|
+
*/
|
|
371
|
+
static #footprintIndexEntry(footprint, index) {
|
|
372
|
+
return {
|
|
373
|
+
index,
|
|
374
|
+
name: footprint.name,
|
|
375
|
+
dataName: footprint.dataName,
|
|
376
|
+
sourceStorage: footprint.sourceStorage,
|
|
377
|
+
primitiveCount: footprint.primitiveCount,
|
|
378
|
+
padCount: footprint.pads.length,
|
|
379
|
+
textCount: footprint.texts.length,
|
|
380
|
+
keywords: PcbLibModelParser.#buildFootprintKeywords(footprint)
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Builds searchable metadata tokens for one footprint.
|
|
386
|
+
* @param {object} footprint Normalized footprint.
|
|
387
|
+
* @returns {string[]}
|
|
388
|
+
*/
|
|
389
|
+
static #buildFootprintKeywords(footprint) {
|
|
390
|
+
return [
|
|
391
|
+
footprint.name,
|
|
392
|
+
footprint.dataName,
|
|
393
|
+
...Object.values(footprint.parameters || {}),
|
|
394
|
+
...Object.values(footprint.componentParams || {})
|
|
395
|
+
]
|
|
396
|
+
.map((value) => String(value || '').trim())
|
|
397
|
+
.filter(Boolean)
|
|
398
|
+
}
|
|
399
|
+
|
|
179
400
|
/**
|
|
180
401
|
* Counts either a scalar footprint field or an array-valued family.
|
|
181
402
|
* @param {object[]} footprints
|