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,417 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds deterministic render/export manifests for read-only library models.
|
|
7
|
+
*/
|
|
8
|
+
export class LibraryRenderManifestBuilder {
|
|
9
|
+
static #SCHEMA = 'altium-toolkit.library.render-manifest.a1'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Builds a schematic-symbol library render manifest.
|
|
13
|
+
* @param {{ symbols?: object[] } | { schematicLibrary?: { symbols?: object[] } }} library Parsed schematic library.
|
|
14
|
+
* @returns {{ schema: string, libraryKind: string, outputs: object[], embeddedAssets: object[] }}
|
|
15
|
+
*/
|
|
16
|
+
static buildSchematicLibraryManifest(library) {
|
|
17
|
+
const schematicLibrary = library?.schematicLibrary || library || {}
|
|
18
|
+
const symbols = Array.isArray(schematicLibrary.symbols)
|
|
19
|
+
? schematicLibrary.symbols
|
|
20
|
+
: []
|
|
21
|
+
const outputs = symbols.flatMap((symbol, symbolIndex) =>
|
|
22
|
+
LibraryRenderManifestBuilder.#schematicSymbolOutputs(
|
|
23
|
+
symbol,
|
|
24
|
+
symbolIndex
|
|
25
|
+
)
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
schema: LibraryRenderManifestBuilder.#SCHEMA,
|
|
30
|
+
libraryKind: 'schematic-symbols',
|
|
31
|
+
outputs,
|
|
32
|
+
embeddedAssets: LibraryRenderManifestBuilder.#dedupeEmbeddedAssets(
|
|
33
|
+
outputs.flatMap((output) => output.embeddedAssets || [])
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Builds a PCB footprint-library render manifest.
|
|
40
|
+
* @param {{ footprints?: object[], embeddedAssets?: object[] } | { pcbLibrary?: { footprints?: object[], embeddedAssets?: object[] } }} library Parsed PCB library.
|
|
41
|
+
* @returns {{ schema: string, libraryKind: string, outputs: object[], embeddedAssets: object[] }}
|
|
42
|
+
*/
|
|
43
|
+
static buildPcbLibraryManifest(library) {
|
|
44
|
+
const pcbLibrary = library?.pcbLibrary || library || {}
|
|
45
|
+
const footprints = Array.isArray(pcbLibrary.footprints)
|
|
46
|
+
? pcbLibrary.footprints
|
|
47
|
+
: []
|
|
48
|
+
const outputs = footprints.map((footprint, footprintIndex) =>
|
|
49
|
+
LibraryRenderManifestBuilder.#pcbFootprintOutput(
|
|
50
|
+
footprint,
|
|
51
|
+
footprintIndex
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
schema: LibraryRenderManifestBuilder.#SCHEMA,
|
|
57
|
+
libraryKind: 'pcb-footprints',
|
|
58
|
+
outputs,
|
|
59
|
+
embeddedAssets: LibraryRenderManifestBuilder.#dedupeEmbeddedAssets([
|
|
60
|
+
...(pcbLibrary.embeddedAssets || []),
|
|
61
|
+
...outputs.flatMap((output) => output.embeddedAssets || [])
|
|
62
|
+
])
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Builds a read-only manifest for symbols extractable from placed
|
|
68
|
+
* schematic components.
|
|
69
|
+
* @param {{ fileName?: string, schematic?: object } | { schematic?: object }} documentModel Parsed schematic document model.
|
|
70
|
+
* @returns {{ schema: string, sourceDocument: string, outputs: object[], embeddedAssets: object[] }}
|
|
71
|
+
*/
|
|
72
|
+
static buildSchematicExtractionManifest(documentModel) {
|
|
73
|
+
const schematic = documentModel?.schematic || documentModel || {}
|
|
74
|
+
const components = Array.isArray(schematic.components)
|
|
75
|
+
? schematic.components
|
|
76
|
+
: []
|
|
77
|
+
const outputs = components.map((component, componentIndex) =>
|
|
78
|
+
LibraryRenderManifestBuilder.#schematicExtractionOutput(
|
|
79
|
+
documentModel?.fileName || '',
|
|
80
|
+
schematic,
|
|
81
|
+
component,
|
|
82
|
+
componentIndex
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
schema: 'altium-toolkit.schematic.extraction-manifest.a1',
|
|
88
|
+
sourceDocument: String(documentModel?.fileName || ''),
|
|
89
|
+
outputs,
|
|
90
|
+
embeddedAssets: LibraryRenderManifestBuilder.#dedupeEmbeddedAssets(
|
|
91
|
+
outputs.flatMap((output) => output.embeddedAssets || [])
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Builds render outputs for one schematic symbol.
|
|
98
|
+
* @param {object} symbol Symbol record.
|
|
99
|
+
* @param {number} symbolIndex Symbol index.
|
|
100
|
+
* @returns {object[]}
|
|
101
|
+
*/
|
|
102
|
+
static #schematicSymbolOutputs(symbol, symbolIndex) {
|
|
103
|
+
const symbolKey =
|
|
104
|
+
'symbol-' +
|
|
105
|
+
symbolIndex +
|
|
106
|
+
'-' +
|
|
107
|
+
LibraryRenderManifestBuilder.#slug(symbol?.name || symbolIndex)
|
|
108
|
+
const parts =
|
|
109
|
+
Array.isArray(symbol?.parts) && symbol.parts.length
|
|
110
|
+
? symbol.parts
|
|
111
|
+
: [{ partId: 'default' }]
|
|
112
|
+
const embeddedAssets =
|
|
113
|
+
LibraryRenderManifestBuilder.#embeddedAssets(symbol)
|
|
114
|
+
|
|
115
|
+
return parts.map((part, partIndex) => {
|
|
116
|
+
const partId = String(part?.partId || part?.id || partIndex)
|
|
117
|
+
const partKey =
|
|
118
|
+
symbolKey +
|
|
119
|
+
'/part-' +
|
|
120
|
+
LibraryRenderManifestBuilder.#slug(partId)
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
kind: 'symbol',
|
|
124
|
+
symbolKey,
|
|
125
|
+
name: String(symbol?.name || ''),
|
|
126
|
+
partKey,
|
|
127
|
+
partId,
|
|
128
|
+
outputSvgKey: 'schematic-library/' + partKey + '.svg',
|
|
129
|
+
embeddedAssets
|
|
130
|
+
}
|
|
131
|
+
})
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Builds one PCB footprint output descriptor.
|
|
136
|
+
* @param {object} footprint Footprint record.
|
|
137
|
+
* @param {number} footprintIndex Footprint index.
|
|
138
|
+
* @returns {object}
|
|
139
|
+
*/
|
|
140
|
+
static #pcbFootprintOutput(footprint, footprintIndex) {
|
|
141
|
+
const footprintKey =
|
|
142
|
+
'footprint-' +
|
|
143
|
+
footprintIndex +
|
|
144
|
+
'-' +
|
|
145
|
+
LibraryRenderManifestBuilder.#slug(
|
|
146
|
+
footprint?.name || footprintIndex
|
|
147
|
+
)
|
|
148
|
+
const layerSvgs = LibraryRenderManifestBuilder.#footprintLayers(
|
|
149
|
+
footprint
|
|
150
|
+
).map((layer) => ({
|
|
151
|
+
layerKey: layer.layerKey,
|
|
152
|
+
layerId: layer.layerId,
|
|
153
|
+
displayName: layer.displayName,
|
|
154
|
+
outputSvgKey:
|
|
155
|
+
'pcb-library/' + footprintKey + '/' + layer.layerKey + '.svg'
|
|
156
|
+
}))
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
kind: 'footprint',
|
|
160
|
+
footprintKey,
|
|
161
|
+
name: String(footprint?.name || ''),
|
|
162
|
+
sourceStorage: String(footprint?.sourceStorage || ''),
|
|
163
|
+
outputSvgKey: 'pcb-library/' + footprintKey + '.svg',
|
|
164
|
+
layerSvgs,
|
|
165
|
+
embeddedAssets:
|
|
166
|
+
LibraryRenderManifestBuilder.#embeddedAssets(footprint)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Builds one placed-component extraction output.
|
|
172
|
+
* @param {string} sourceDocument Source document name.
|
|
173
|
+
* @param {object} schematic Schematic model.
|
|
174
|
+
* @param {object} component Component row.
|
|
175
|
+
* @param {number} componentIndex Component index.
|
|
176
|
+
* @returns {object}
|
|
177
|
+
*/
|
|
178
|
+
static #schematicExtractionOutput(
|
|
179
|
+
sourceDocument,
|
|
180
|
+
schematic,
|
|
181
|
+
component,
|
|
182
|
+
componentIndex
|
|
183
|
+
) {
|
|
184
|
+
const symbolName =
|
|
185
|
+
component?.libReference || component?.designator || ''
|
|
186
|
+
const symbolKey =
|
|
187
|
+
'symbol-extract-' +
|
|
188
|
+
componentIndex +
|
|
189
|
+
'-' +
|
|
190
|
+
LibraryRenderManifestBuilder.#slug(symbolName || componentIndex)
|
|
191
|
+
const ownerIndex = String(component?.ownerIndex || '').trim()
|
|
192
|
+
const children = LibraryRenderManifestBuilder.#schematicOwnerChildren(
|
|
193
|
+
schematic,
|
|
194
|
+
ownerIndex
|
|
195
|
+
)
|
|
196
|
+
const embeddedAssets =
|
|
197
|
+
LibraryRenderManifestBuilder.#dedupeEmbeddedAssets(
|
|
198
|
+
children.images.map((image, index) =>
|
|
199
|
+
LibraryRenderManifestBuilder.#imageAssetDescriptor(
|
|
200
|
+
image,
|
|
201
|
+
'symbol-image-' + index
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
return {
|
|
207
|
+
kind: 'symbol-extraction',
|
|
208
|
+
symbolKey,
|
|
209
|
+
sourceComponent: LibraryRenderManifestBuilder.#stripUndefined({
|
|
210
|
+
designator: component?.designator,
|
|
211
|
+
libReference: component?.libReference,
|
|
212
|
+
uniqueId: component?.uniqueId,
|
|
213
|
+
ownerIndex
|
|
214
|
+
}),
|
|
215
|
+
outputLibraryKey: 'schematic-extract/' + symbolKey + '.SchLib',
|
|
216
|
+
renderManifestKey:
|
|
217
|
+
'schematic-extract/' + symbolKey + '.render.json',
|
|
218
|
+
partKeys: [symbolKey + '/part-default'],
|
|
219
|
+
childCounts: {
|
|
220
|
+
pins: children.pins.length,
|
|
221
|
+
graphics: children.graphics.length,
|
|
222
|
+
texts: children.texts.length,
|
|
223
|
+
images: children.images.length
|
|
224
|
+
},
|
|
225
|
+
embeddedAssets
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Collects schematic primitives owned by one component owner index.
|
|
231
|
+
* @param {object} schematic Schematic model.
|
|
232
|
+
* @param {string} ownerIndex Owner index.
|
|
233
|
+
* @returns {{ pins: object[], graphics: object[], texts: object[], images: object[] }}
|
|
234
|
+
*/
|
|
235
|
+
static #schematicOwnerChildren(schematic, ownerIndex) {
|
|
236
|
+
const ownerMatches = (item) =>
|
|
237
|
+
ownerIndex && String(item?.ownerIndex || '').trim() === ownerIndex
|
|
238
|
+
const graphics = [
|
|
239
|
+
...(schematic?.lines || []),
|
|
240
|
+
...(schematic?.polygons || []),
|
|
241
|
+
...(schematic?.rectangles || []),
|
|
242
|
+
...(schematic?.ellipses || []),
|
|
243
|
+
...(schematic?.arcs || []),
|
|
244
|
+
...(schematic?.beziers || []),
|
|
245
|
+
...(schematic?.pies || []),
|
|
246
|
+
...(schematic?.regions || [])
|
|
247
|
+
].filter(ownerMatches)
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
pins: (schematic?.pins || []).filter(ownerMatches),
|
|
251
|
+
graphics,
|
|
252
|
+
texts: (schematic?.texts || []).filter(ownerMatches),
|
|
253
|
+
images: (schematic?.images || []).filter(ownerMatches)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Collects layer descriptors touched by one footprint.
|
|
259
|
+
* @param {object} footprint Footprint record.
|
|
260
|
+
* @returns {{ layerKey: string, layerId?: number, displayName: string }[]}
|
|
261
|
+
*/
|
|
262
|
+
static #footprintLayers(footprint) {
|
|
263
|
+
const layerMap = new Map()
|
|
264
|
+
const primitiveFamilies = [
|
|
265
|
+
'pads',
|
|
266
|
+
'tracks',
|
|
267
|
+
'arcs',
|
|
268
|
+
'vias',
|
|
269
|
+
'fills',
|
|
270
|
+
'texts',
|
|
271
|
+
'regions',
|
|
272
|
+
'shapeBasedRegions'
|
|
273
|
+
]
|
|
274
|
+
|
|
275
|
+
for (const family of primitiveFamilies) {
|
|
276
|
+
for (const primitive of footprint?.[family] || []) {
|
|
277
|
+
const layer =
|
|
278
|
+
LibraryRenderManifestBuilder.#layerDescriptor(primitive)
|
|
279
|
+
if (layer) {
|
|
280
|
+
layerMap.set(layer.layerKey, layer)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return [...layerMap.values()].sort((left, right) =>
|
|
286
|
+
left.layerKey.localeCompare(right.layerKey)
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Builds a normalized layer descriptor for one primitive.
|
|
292
|
+
* @param {object} primitive Primitive record.
|
|
293
|
+
* @returns {{ layerKey: string, layerId?: number, displayName: string } | null}
|
|
294
|
+
*/
|
|
295
|
+
static #layerDescriptor(primitive) {
|
|
296
|
+
const layerId = Number.isInteger(primitive?.layerId)
|
|
297
|
+
? primitive.layerId
|
|
298
|
+
: null
|
|
299
|
+
const layerName = String(
|
|
300
|
+
primitive?.layerName || primitive?.layer || ''
|
|
301
|
+
).trim()
|
|
302
|
+
|
|
303
|
+
if (layerId === null && !layerName) {
|
|
304
|
+
return null
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const layerKey =
|
|
308
|
+
layerId === null
|
|
309
|
+
? 'layer-' + LibraryRenderManifestBuilder.#slug(layerName)
|
|
310
|
+
: 'L' + layerId
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
layerKey,
|
|
314
|
+
...(layerId === null ? {} : { layerId }),
|
|
315
|
+
displayName: layerName || layerKey
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Collects embedded assets from a library item.
|
|
321
|
+
* @param {object} item Library item.
|
|
322
|
+
* @returns {object[]}
|
|
323
|
+
*/
|
|
324
|
+
static #embeddedAssets(item) {
|
|
325
|
+
return [
|
|
326
|
+
...(Array.isArray(item?.embeddedAssets) ? item.embeddedAssets : []),
|
|
327
|
+
...(Array.isArray(item?.embeddedModels)
|
|
328
|
+
? item.embeddedModels.map((model, index) => ({
|
|
329
|
+
key: model.key || model.id || 'model-' + index,
|
|
330
|
+
format: model.format,
|
|
331
|
+
sourceStream: model.sourceStream,
|
|
332
|
+
name: model.name
|
|
333
|
+
}))
|
|
334
|
+
: []),
|
|
335
|
+
...(Array.isArray(item?.embeddedFonts)
|
|
336
|
+
? item.embeddedFonts.map((font, index) => ({
|
|
337
|
+
key: font.key || font.name || 'font-' + index,
|
|
338
|
+
format: 'font',
|
|
339
|
+
sourceStream: font.sourceStream,
|
|
340
|
+
name: font.name
|
|
341
|
+
}))
|
|
342
|
+
: [])
|
|
343
|
+
].map((asset) =>
|
|
344
|
+
LibraryRenderManifestBuilder.#stripUndefined(asset || {})
|
|
345
|
+
)
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Builds an asset descriptor for a schematic image payload.
|
|
350
|
+
* @param {object} image Schematic image record.
|
|
351
|
+
* @param {string} fallbackKey Fallback asset key.
|
|
352
|
+
* @returns {object}
|
|
353
|
+
*/
|
|
354
|
+
static #imageAssetDescriptor(image, fallbackKey) {
|
|
355
|
+
return LibraryRenderManifestBuilder.#stripUndefined({
|
|
356
|
+
key: image?.key || image?.id || fallbackKey,
|
|
357
|
+
format: image?.format,
|
|
358
|
+
nativeFormat: image?.nativeFormat,
|
|
359
|
+
wrapperType: image?.wrapperType,
|
|
360
|
+
byteSize: image?.byteSize,
|
|
361
|
+
checksum: image?.checksum,
|
|
362
|
+
sourceStream: image?.sourceStream,
|
|
363
|
+
name: image?.name,
|
|
364
|
+
diagnostics: image?.diagnostics
|
|
365
|
+
})
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Deduplicates embedded asset descriptors.
|
|
370
|
+
* @param {object[]} assets Asset descriptors.
|
|
371
|
+
* @returns {object[]}
|
|
372
|
+
*/
|
|
373
|
+
static #dedupeEmbeddedAssets(assets) {
|
|
374
|
+
const seen = new Set()
|
|
375
|
+
const deduped = []
|
|
376
|
+
|
|
377
|
+
for (const asset of assets || []) {
|
|
378
|
+
const key = JSON.stringify(asset)
|
|
379
|
+
if (seen.has(key)) {
|
|
380
|
+
continue
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
seen.add(key)
|
|
384
|
+
deduped.push(asset)
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return deduped
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Removes undefined values from one object.
|
|
392
|
+
* @param {object} value Source object.
|
|
393
|
+
* @returns {object}
|
|
394
|
+
*/
|
|
395
|
+
static #stripUndefined(value) {
|
|
396
|
+
return Object.fromEntries(
|
|
397
|
+
Object.entries(value).filter(
|
|
398
|
+
([, entryValue]) => entryValue !== undefined
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Converts a display value to a deterministic lowercase key segment.
|
|
405
|
+
* @param {unknown} value Source value.
|
|
406
|
+
* @returns {string}
|
|
407
|
+
*/
|
|
408
|
+
static #slug(value) {
|
|
409
|
+
return (
|
|
410
|
+
String(value || '')
|
|
411
|
+
.trim()
|
|
412
|
+
.toLowerCase()
|
|
413
|
+
.replace(/[^a-z0-9]+/gu, '-')
|
|
414
|
+
.replace(/^-+|-+$/gu, '') || 'item'
|
|
415
|
+
)
|
|
416
|
+
}
|
|
417
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Provides exact and lightweight fuzzy lookups over parsed library read models.
|
|
7
|
+
*/
|
|
8
|
+
export class LibrarySearchIndex {
|
|
9
|
+
/**
|
|
10
|
+
* Searches PCB footprint records.
|
|
11
|
+
* @param {{ footprints?: object[] } | { pcbLibrary?: { footprints?: object[] } }} library Parsed PCB library.
|
|
12
|
+
* @param {string} query Search query.
|
|
13
|
+
* @param {{ limit?: number }} options Search options.
|
|
14
|
+
* @returns {{ query: string, matches: object[] }}
|
|
15
|
+
*/
|
|
16
|
+
static searchPcbFootprints(library, query, options = {}) {
|
|
17
|
+
const pcbLibrary = library?.pcbLibrary || library || {}
|
|
18
|
+
const footprints = Array.isArray(pcbLibrary.footprints)
|
|
19
|
+
? pcbLibrary.footprints
|
|
20
|
+
: []
|
|
21
|
+
|
|
22
|
+
return LibrarySearchIndex.#searchCollection(
|
|
23
|
+
'footprint',
|
|
24
|
+
footprints,
|
|
25
|
+
query,
|
|
26
|
+
options
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Searches schematic symbol records.
|
|
32
|
+
* @param {{ symbols?: object[] } | { schematicLibrary?: { symbols?: object[] } }} library Parsed schematic library.
|
|
33
|
+
* @param {string} query Search query.
|
|
34
|
+
* @param {{ limit?: number }} options Search options.
|
|
35
|
+
* @returns {{ query: string, matches: object[] }}
|
|
36
|
+
*/
|
|
37
|
+
static searchSchematicSymbols(library, query, options = {}) {
|
|
38
|
+
const schematicLibrary = library?.schematicLibrary || library || {}
|
|
39
|
+
const symbols = Array.isArray(schematicLibrary.symbols)
|
|
40
|
+
? schematicLibrary.symbols
|
|
41
|
+
: []
|
|
42
|
+
|
|
43
|
+
return LibrarySearchIndex.#searchCollection(
|
|
44
|
+
'symbol',
|
|
45
|
+
symbols,
|
|
46
|
+
query,
|
|
47
|
+
options
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Searches one library collection.
|
|
53
|
+
* @param {string} kind Public item kind.
|
|
54
|
+
* @param {object[]} items Library items.
|
|
55
|
+
* @param {string} query Search query.
|
|
56
|
+
* @param {{ limit?: number }} options Search options.
|
|
57
|
+
* @returns {{ query: string, matches: object[] }}
|
|
58
|
+
*/
|
|
59
|
+
static #searchCollection(kind, items, query, options) {
|
|
60
|
+
const normalizedQuery = LibrarySearchIndex.#normalize(query)
|
|
61
|
+
const limit = Math.max(Number(options.limit || 25), 1)
|
|
62
|
+
const matches = (items || [])
|
|
63
|
+
.map((item, index) =>
|
|
64
|
+
LibrarySearchIndex.#scoreItem(
|
|
65
|
+
kind,
|
|
66
|
+
item,
|
|
67
|
+
index,
|
|
68
|
+
normalizedQuery
|
|
69
|
+
)
|
|
70
|
+
)
|
|
71
|
+
.filter(Boolean)
|
|
72
|
+
.sort(
|
|
73
|
+
(left, right) =>
|
|
74
|
+
left.score - right.score ||
|
|
75
|
+
left.name.localeCompare(right.name)
|
|
76
|
+
)
|
|
77
|
+
.slice(0, limit)
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
query: String(query || ''),
|
|
81
|
+
matches
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Scores one candidate library item.
|
|
87
|
+
* @param {string} kind Public item kind.
|
|
88
|
+
* @param {object} item Library item.
|
|
89
|
+
* @param {number} index Source index.
|
|
90
|
+
* @param {string} normalizedQuery Normalized query.
|
|
91
|
+
* @returns {object | null}
|
|
92
|
+
*/
|
|
93
|
+
static #scoreItem(kind, item, index, normalizedQuery) {
|
|
94
|
+
if (!normalizedQuery) {
|
|
95
|
+
return null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const name = String(item?.name || item?.libReference || '')
|
|
99
|
+
const normalizedName = LibrarySearchIndex.#normalize(name)
|
|
100
|
+
const keywords = LibrarySearchIndex.#keywords(item)
|
|
101
|
+
const match = LibrarySearchIndex.#match(
|
|
102
|
+
normalizedQuery,
|
|
103
|
+
normalizedName,
|
|
104
|
+
keywords
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
if (!match) {
|
|
108
|
+
return null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
kind,
|
|
113
|
+
name,
|
|
114
|
+
index,
|
|
115
|
+
score: match.score,
|
|
116
|
+
matchKind: match.matchKind,
|
|
117
|
+
keywords
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Matches a normalized query against name and keywords.
|
|
123
|
+
* @param {string} query Normalized query.
|
|
124
|
+
* @param {string} name Normalized name.
|
|
125
|
+
* @param {string[]} keywords Keyword list.
|
|
126
|
+
* @returns {{ score: number, matchKind: string } | null}
|
|
127
|
+
*/
|
|
128
|
+
static #match(query, name, keywords) {
|
|
129
|
+
const normalizedKeywords = keywords.map((keyword) =>
|
|
130
|
+
LibrarySearchIndex.#normalize(keyword)
|
|
131
|
+
)
|
|
132
|
+
const compactQuery = LibrarySearchIndex.#compact(query)
|
|
133
|
+
const compactName = LibrarySearchIndex.#compact(name)
|
|
134
|
+
|
|
135
|
+
if (name === query) {
|
|
136
|
+
return { score: 0, matchKind: 'exact' }
|
|
137
|
+
}
|
|
138
|
+
if (name.startsWith(query)) {
|
|
139
|
+
return { score: 10, matchKind: 'prefix' }
|
|
140
|
+
}
|
|
141
|
+
if (name.includes(query)) {
|
|
142
|
+
return { score: 20, matchKind: 'substring' }
|
|
143
|
+
}
|
|
144
|
+
if (normalizedKeywords.some((keyword) => keyword.includes(query))) {
|
|
145
|
+
return { score: 30, matchKind: 'keyword' }
|
|
146
|
+
}
|
|
147
|
+
if (
|
|
148
|
+
compactName.includes(compactQuery) ||
|
|
149
|
+
LibrarySearchIndex.#isOrderedSubsequence(compactQuery, compactName)
|
|
150
|
+
) {
|
|
151
|
+
return { score: 40, matchKind: 'fuzzy' }
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return null
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Builds keywords from item metadata.
|
|
159
|
+
* @param {object} item Library item.
|
|
160
|
+
* @returns {string[]}
|
|
161
|
+
*/
|
|
162
|
+
static #keywords(item) {
|
|
163
|
+
return [
|
|
164
|
+
item?.name,
|
|
165
|
+
item?.dataName,
|
|
166
|
+
item?.sourceStorage,
|
|
167
|
+
...Object.values(item?.parameters || {}),
|
|
168
|
+
...Object.values(item?.componentParams || {}),
|
|
169
|
+
...Object.values(item?.componentParams?.properties || {})
|
|
170
|
+
]
|
|
171
|
+
.map((value) => String(value || '').trim())
|
|
172
|
+
.filter(Boolean)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Returns true when all query chars appear in order in the candidate.
|
|
177
|
+
* @param {string} query Normalized compact query.
|
|
178
|
+
* @param {string} candidate Normalized compact candidate.
|
|
179
|
+
* @returns {boolean}
|
|
180
|
+
*/
|
|
181
|
+
static #isOrderedSubsequence(query, candidate) {
|
|
182
|
+
let cursor = 0
|
|
183
|
+
|
|
184
|
+
for (const char of candidate) {
|
|
185
|
+
if (char === query[cursor]) {
|
|
186
|
+
cursor += 1
|
|
187
|
+
}
|
|
188
|
+
if (cursor === query.length) {
|
|
189
|
+
return true
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return false
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Normalizes text for case-insensitive lookup.
|
|
198
|
+
* @param {unknown} value Source value.
|
|
199
|
+
* @returns {string}
|
|
200
|
+
*/
|
|
201
|
+
static #normalize(value) {
|
|
202
|
+
return String(value || '')
|
|
203
|
+
.trim()
|
|
204
|
+
.toLowerCase()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Removes separators for fuzzy package-name matching.
|
|
209
|
+
* @param {string} value Normalized value.
|
|
210
|
+
* @returns {string}
|
|
211
|
+
*/
|
|
212
|
+
static #compact(value) {
|
|
213
|
+
return String(value || '').replace(/[^a-z0-9]+/gu, '')
|
|
214
|
+
}
|
|
215
|
+
}
|
|
@@ -22,7 +22,43 @@ export class NormalizedModelSchema {
|
|
|
22
22
|
...model
|
|
23
23
|
}
|
|
24
24
|
normalizedModel.schema = NormalizedModelSchema.CURRENT_SCHEMA_ID
|
|
25
|
+
if (Array.isArray(normalizedModel.diagnostics)) {
|
|
26
|
+
normalizedModel.diagnostics =
|
|
27
|
+
NormalizedModelSchema.#normalizeDiagnostics(
|
|
28
|
+
normalizedModel.diagnostics
|
|
29
|
+
)
|
|
30
|
+
}
|
|
25
31
|
|
|
26
32
|
return normalizedModel
|
|
27
33
|
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Adds machine-readable codes to parser diagnostics.
|
|
37
|
+
* @param {object[]} diagnostics Parser diagnostics.
|
|
38
|
+
* @returns {object[]}
|
|
39
|
+
*/
|
|
40
|
+
static #normalizeDiagnostics(diagnostics) {
|
|
41
|
+
return diagnostics.map((diagnostic) => ({
|
|
42
|
+
code:
|
|
43
|
+
typeof diagnostic?.code === 'string' && diagnostic.code
|
|
44
|
+
? diagnostic.code
|
|
45
|
+
: NormalizedModelSchema.#deriveDiagnosticCode(diagnostic),
|
|
46
|
+
...diagnostic
|
|
47
|
+
}))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Derives a stable fallback code from one diagnostic message.
|
|
52
|
+
* @param {object} diagnostic Parser diagnostic.
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
static #deriveDiagnosticCode(diagnostic) {
|
|
56
|
+
const slug = String(diagnostic?.message || 'diagnostic')
|
|
57
|
+
.toLowerCase()
|
|
58
|
+
.replace(/[^a-z0-9]+/gu, '.')
|
|
59
|
+
.replace(/^\.+|\.+$/gu, '')
|
|
60
|
+
.slice(0, 80)
|
|
61
|
+
|
|
62
|
+
return 'parser.' + (slug || 'diagnostic')
|
|
63
|
+
}
|
|
28
64
|
}
|