altium-toolkit 1.1.3 → 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 +171 -23
- package/docs/model-format.md +192 -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 +513 -3
- 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 +7 -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/spec/library-scope.md +5 -0
- package/src/core/BinaryReader.mjs +213 -2
- package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
- package/src/core/altium/AltiumParser.mjs +357 -16
- package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
- package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
- package/src/core/altium/AltiumUnits.mjs +205 -0
- package/src/core/altium/AsciiRecordParser.mjs +52 -11
- 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/PcbComponentKindPolicy.mjs +9 -9
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
- 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/PcbOutlineRecovery.mjs +94 -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/SchematicDirectiveParser.mjs +5 -17
- 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/SchematicNoErcSymbolResolver.mjs +36 -0
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
- package/src/core/altium/SchematicPinParser.mjs +99 -65
- package/src/core/altium/SchematicPrimitiveParser.mjs +125 -22
- 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 +107 -21
- 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/SourceBundleExporter.mjs +156 -0
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
- package/src/core/altium/SourceComponentClient.mjs +239 -0
- package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
- package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
- package/src/parser.mjs +43 -1
- package/src/renderers.mjs +1 -0
- package/src/styles/altium-renderers.css +6 -6
- package/src/ui/PcbArcUtils.mjs +19 -2
- package/src/ui/PcbScene3dBuilder.mjs +202 -20
- package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
- package/src/ui/SchematicColorResolver.mjs +185 -0
- package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
- package/src/ui/SchematicLineColorResolver.mjs +88 -0
- package/src/ui/SchematicNoteRenderer.mjs +5 -1
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +269 -8
- package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +229 -62
- package/src/ui/SchematicShapeRenderer.mjs +86 -17
- package/src/ui/SchematicSvgRenderer.mjs +980 -58
- package/src/ui/SchematicTypography.mjs +4 -3
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { EmbeddedFileInventoryBuilder } from './EmbeddedFileInventoryBuilder.mjs'
|
|
6
|
+
import { AsciiRecordParser } from './AsciiRecordParser.mjs'
|
|
7
|
+
import { NativeStreamInventoryBuilder } from './NativeStreamInventoryBuilder.mjs'
|
|
8
|
+
import { SchematicRecordStreamParser } from './SchematicRecordStreamParser.mjs'
|
|
9
|
+
import { OleCompoundDocument } from '../ole/OleCompoundDocument.mjs'
|
|
10
|
+
import { OleConstants } from '../ole/OleConstants.mjs'
|
|
11
|
+
import { unzlibSync } from 'fflate'
|
|
12
|
+
|
|
13
|
+
const PIN_SIDECAR_STREAMS = Object.freeze([
|
|
14
|
+
'PinFrac',
|
|
15
|
+
'PinSymbolLineWidth',
|
|
16
|
+
'PinDesc',
|
|
17
|
+
'PinTextData',
|
|
18
|
+
'PinPackageLength',
|
|
19
|
+
'PinFunctionData'
|
|
20
|
+
])
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Extracts stream-scoped schematic-symbol library content from OLE containers.
|
|
24
|
+
*/
|
|
25
|
+
export class SchLibStreamExtractor {
|
|
26
|
+
/**
|
|
27
|
+
* Returns true when one buffer starts with the OLE compound-document
|
|
28
|
+
* signature.
|
|
29
|
+
* @param {ArrayBuffer} arrayBuffer Source bytes.
|
|
30
|
+
* @returns {boolean}
|
|
31
|
+
*/
|
|
32
|
+
static isCompoundDocument(arrayBuffer) {
|
|
33
|
+
const bytes = new Uint8Array(
|
|
34
|
+
arrayBuffer,
|
|
35
|
+
0,
|
|
36
|
+
Math.min(
|
|
37
|
+
arrayBuffer.byteLength,
|
|
38
|
+
OleConstants.HEADER_SIGNATURE.length
|
|
39
|
+
)
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
if (bytes.byteLength < OleConstants.HEADER_SIGNATURE.length) {
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return OleConstants.HEADER_SIGNATURE.every(
|
|
47
|
+
(value, index) => bytes[index] === value
|
|
48
|
+
)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Extracts schematic-library streams from one OLE-backed SchLib buffer.
|
|
53
|
+
* @param {ArrayBuffer} arrayBuffer Source bytes.
|
|
54
|
+
* @returns {ReturnType<typeof SchLibStreamExtractor.extractFromStreams> | null}
|
|
55
|
+
*/
|
|
56
|
+
static extractFromArrayBuffer(arrayBuffer) {
|
|
57
|
+
if (!SchLibStreamExtractor.isCompoundDocument(arrayBuffer)) {
|
|
58
|
+
return null
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const compoundDocument =
|
|
62
|
+
OleCompoundDocument.fromArrayBuffer(arrayBuffer)
|
|
63
|
+
const streams = new Map()
|
|
64
|
+
|
|
65
|
+
for (const name of compoundDocument.listStreams()) {
|
|
66
|
+
streams.set(name, compoundDocument.getStream(name))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return SchLibStreamExtractor.extractFromStreams(streams)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Extracts library-level and component-level schematic records.
|
|
74
|
+
* @param {Map<string, Uint8Array>} streams Compound-document streams.
|
|
75
|
+
* @returns {{ libraryRecords: object[], fileHeaderRecords: object[], sectionKeys: object[], symbols: object[], streamNames: string[], embeddedFiles: object, opaqueRecords: object[], diagnostics: object }}
|
|
76
|
+
*/
|
|
77
|
+
static extractFromStreams(streams) {
|
|
78
|
+
const streamNames = [...(streams || new Map()).keys()].sort()
|
|
79
|
+
const libraryRecords = SchLibStreamExtractor.#parseRecordStream(
|
|
80
|
+
streams.get('Library/Data'),
|
|
81
|
+
{
|
|
82
|
+
sourceStorage: 'Library',
|
|
83
|
+
sourceStream: 'Library/Data'
|
|
84
|
+
}
|
|
85
|
+
).records
|
|
86
|
+
const fileHeaderRecords = SchLibStreamExtractor.#parseRecordStream(
|
|
87
|
+
streams.get('FileHeader'),
|
|
88
|
+
{
|
|
89
|
+
sourceStorage: '',
|
|
90
|
+
sourceStream: 'FileHeader'
|
|
91
|
+
}
|
|
92
|
+
).records
|
|
93
|
+
const sectionKeyParse = SchLibStreamExtractor.#parseRecordStream(
|
|
94
|
+
streams.get('SectionKeys'),
|
|
95
|
+
{
|
|
96
|
+
sourceStorage: '',
|
|
97
|
+
sourceStream: 'SectionKeys'
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
const sectionKeys = SchLibStreamExtractor.#sectionKeys(
|
|
101
|
+
sectionKeyParse.records
|
|
102
|
+
)
|
|
103
|
+
const symbolStorages = SchLibStreamExtractor.#componentStorages(
|
|
104
|
+
streamNames,
|
|
105
|
+
sectionKeys
|
|
106
|
+
)
|
|
107
|
+
const symbols = symbolStorages.map((sourceStorage) =>
|
|
108
|
+
SchLibStreamExtractor.#symbolExtraction(
|
|
109
|
+
streams,
|
|
110
|
+
streamNames,
|
|
111
|
+
sourceStorage
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
const opaqueRecords = symbols.flatMap(
|
|
115
|
+
(symbol) => symbol.opaqueRecords || []
|
|
116
|
+
)
|
|
117
|
+
const usedStreams = [
|
|
118
|
+
'Library/Data',
|
|
119
|
+
...(fileHeaderRecords.length ? ['FileHeader'] : []),
|
|
120
|
+
...(sectionKeyParse.records.length ? ['SectionKeys'] : []),
|
|
121
|
+
...symbols.flatMap((symbol) => symbol.usedStreamNames || [])
|
|
122
|
+
]
|
|
123
|
+
const embeddedFiles = EmbeddedFileInventoryBuilder.buildFromStreams(
|
|
124
|
+
streams || new Map(),
|
|
125
|
+
{ skipStreamNames: usedStreams }
|
|
126
|
+
)
|
|
127
|
+
const consumedStreamNames = [
|
|
128
|
+
...usedStreams,
|
|
129
|
+
...embeddedFiles.files.map((file) => file.sourceStream),
|
|
130
|
+
...embeddedFiles.diagnostics.map(
|
|
131
|
+
(diagnostic) => diagnostic.sourceStream
|
|
132
|
+
)
|
|
133
|
+
]
|
|
134
|
+
const nativeStreams = NativeStreamInventoryBuilder.buildFromStreams(
|
|
135
|
+
streams || new Map(),
|
|
136
|
+
{
|
|
137
|
+
source: 'schlib',
|
|
138
|
+
consumedStreamNames,
|
|
139
|
+
knownStreamNames:
|
|
140
|
+
SchLibStreamExtractor.#knownStreamNames(streamNames)
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
libraryRecords,
|
|
146
|
+
fileHeaderRecords,
|
|
147
|
+
sectionKeys,
|
|
148
|
+
symbols,
|
|
149
|
+
streamNames,
|
|
150
|
+
nativeStreams,
|
|
151
|
+
embeddedFiles,
|
|
152
|
+
opaqueRecords,
|
|
153
|
+
diagnostics: {
|
|
154
|
+
symbolCount: symbols.length,
|
|
155
|
+
streamCount: streamNames.length,
|
|
156
|
+
sectionKeyCount: sectionKeys.length,
|
|
157
|
+
opaqueRecordCount: opaqueRecords.length,
|
|
158
|
+
embeddedFileCount: embeddedFiles.files.length
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Finds component storage paths that carry a Data stream.
|
|
165
|
+
* @param {string[]} streamNames Stream names.
|
|
166
|
+
* @param {object[]} sectionKeys Section-key mappings.
|
|
167
|
+
* @returns {string[]}
|
|
168
|
+
*/
|
|
169
|
+
static #componentStorages(streamNames, sectionKeys) {
|
|
170
|
+
const storages = new Set()
|
|
171
|
+
|
|
172
|
+
for (const streamName of streamNames || []) {
|
|
173
|
+
if (
|
|
174
|
+
/^Components\/[^/]+\/Data$/iu.test(streamName) ||
|
|
175
|
+
(/^[^/]+\/Data$/iu.test(streamName) &&
|
|
176
|
+
streamName !== 'Library/Data')
|
|
177
|
+
) {
|
|
178
|
+
storages.add(streamName.replace(/\/Data$/iu, ''))
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const sectionKey of sectionKeys || []) {
|
|
183
|
+
if (
|
|
184
|
+
sectionKey.sectionKey &&
|
|
185
|
+
streamNames.includes(sectionKey.sectionKey + '/Data')
|
|
186
|
+
) {
|
|
187
|
+
storages.add(sectionKey.sectionKey)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const sectionOrder = new Map(
|
|
192
|
+
(sectionKeys || []).map((sectionKey, index) => [
|
|
193
|
+
sectionKey.sectionKey,
|
|
194
|
+
index
|
|
195
|
+
])
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
return [...storages].sort((left, right) => {
|
|
199
|
+
const leftOrder = sectionOrder.get(left)
|
|
200
|
+
const rightOrder = sectionOrder.get(right)
|
|
201
|
+
|
|
202
|
+
if (leftOrder !== undefined || rightOrder !== undefined) {
|
|
203
|
+
return (
|
|
204
|
+
(leftOrder ?? Number.MAX_SAFE_INTEGER) -
|
|
205
|
+
(rightOrder ?? Number.MAX_SAFE_INTEGER)
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return left.localeCompare(right)
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Extracts one component storage.
|
|
215
|
+
* @param {Map<string, Uint8Array>} streams Stream map.
|
|
216
|
+
* @param {string[]} streamNames All stream names.
|
|
217
|
+
* @param {string} sourceStorage Component storage path.
|
|
218
|
+
* @returns {object}
|
|
219
|
+
*/
|
|
220
|
+
static #symbolExtraction(streams, streamNames, sourceStorage) {
|
|
221
|
+
const dataStream = sourceStorage + '/Data'
|
|
222
|
+
const dataParse = SchLibStreamExtractor.#parseRecordStream(
|
|
223
|
+
streams.get(dataStream),
|
|
224
|
+
{ sourceStorage, sourceStream: dataStream }
|
|
225
|
+
)
|
|
226
|
+
const sidecarParses = streamNames
|
|
227
|
+
.filter(
|
|
228
|
+
(streamName) =>
|
|
229
|
+
streamName.startsWith(sourceStorage + '/') &&
|
|
230
|
+
streamName !== dataStream &&
|
|
231
|
+
!SchLibStreamExtractor.#isKnownPinSidecar(
|
|
232
|
+
streamName,
|
|
233
|
+
sourceStorage
|
|
234
|
+
) &&
|
|
235
|
+
streamName !== sourceStorage + '/Storage'
|
|
236
|
+
)
|
|
237
|
+
.map((streamName) =>
|
|
238
|
+
SchLibStreamExtractor.#parseRecordStream(
|
|
239
|
+
streams.get(streamName),
|
|
240
|
+
{ sourceStorage, sourceStream: streamName }
|
|
241
|
+
)
|
|
242
|
+
)
|
|
243
|
+
const pinSidecars = SchLibStreamExtractor.#pinSidecars(
|
|
244
|
+
streams,
|
|
245
|
+
sourceStorage
|
|
246
|
+
)
|
|
247
|
+
const storageAsset = SchLibStreamExtractor.#storageAsset(
|
|
248
|
+
streams.get(sourceStorage + '/Storage'),
|
|
249
|
+
sourceStorage + '/Storage'
|
|
250
|
+
)
|
|
251
|
+
const opaqueRecords = [
|
|
252
|
+
...dataParse.opaqueRecords,
|
|
253
|
+
...sidecarParses.flatMap((entry) => entry.opaqueRecords)
|
|
254
|
+
]
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
sourceStorage,
|
|
258
|
+
sourceStream: dataStream,
|
|
259
|
+
records: dataParse.records,
|
|
260
|
+
pinSidecars: pinSidecars.records,
|
|
261
|
+
embeddedAssets: storageAsset ? [storageAsset] : [],
|
|
262
|
+
opaqueRecords,
|
|
263
|
+
usedStreamNames: [
|
|
264
|
+
dataStream,
|
|
265
|
+
...pinSidecars.usedStreamNames,
|
|
266
|
+
...(storageAsset ? [storageAsset.sourceStream] : []),
|
|
267
|
+
...sidecarParses
|
|
268
|
+
.filter((entry) => entry.opaqueRecords.length)
|
|
269
|
+
.map((entry) => entry.sourceStream)
|
|
270
|
+
]
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Parses a stream into records plus opaque native frames.
|
|
276
|
+
* @param {Uint8Array | undefined} bytes Stream bytes.
|
|
277
|
+
* @param {{ sourceStorage: string, sourceStream: string }} options Source metadata.
|
|
278
|
+
* @returns {{ sourceStream: string, records: object[], opaqueRecords: object[] }}
|
|
279
|
+
*/
|
|
280
|
+
static #parseRecordStream(bytes, options) {
|
|
281
|
+
if (!(bytes instanceof Uint8Array) || !bytes.byteLength) {
|
|
282
|
+
return {
|
|
283
|
+
sourceStream: options.sourceStream,
|
|
284
|
+
records: [],
|
|
285
|
+
opaqueRecords: []
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const parsed = SchematicRecordStreamParser.parseWithOpaqueRecords(
|
|
290
|
+
SchLibStreamExtractor.#toArrayBuffer(bytes),
|
|
291
|
+
{
|
|
292
|
+
source: 'schlib',
|
|
293
|
+
sourceStorage: options.sourceStorage,
|
|
294
|
+
sourceStream: options.sourceStream
|
|
295
|
+
}
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
sourceStream: options.sourceStream,
|
|
300
|
+
records: parsed.records.map((record, recordIndex) => ({
|
|
301
|
+
...record,
|
|
302
|
+
recordIndex,
|
|
303
|
+
sourceStorage: options.sourceStorage,
|
|
304
|
+
sourceStream: options.sourceStream
|
|
305
|
+
})),
|
|
306
|
+
opaqueRecords: parsed.opaqueRecords
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Builds section-key mappings from the native SectionKeys stream.
|
|
312
|
+
* @param {object[]} records Parsed SectionKeys records.
|
|
313
|
+
* @returns {{ libReference: string, sectionKey: string, sourceStream: string, recordIndex: number }[]}
|
|
314
|
+
*/
|
|
315
|
+
static #sectionKeys(records) {
|
|
316
|
+
return (records || [])
|
|
317
|
+
.map((record, recordIndex) =>
|
|
318
|
+
SchLibStreamExtractor.#stripUndefined({
|
|
319
|
+
libReference:
|
|
320
|
+
SchLibStreamExtractor.#field(
|
|
321
|
+
record.fields,
|
|
322
|
+
'LibReference'
|
|
323
|
+
) ||
|
|
324
|
+
SchLibStreamExtractor.#field(record.fields, 'LibRef') ||
|
|
325
|
+
SchLibStreamExtractor.#field(record.fields, 'Name'),
|
|
326
|
+
sectionKey:
|
|
327
|
+
SchLibStreamExtractor.#field(
|
|
328
|
+
record.fields,
|
|
329
|
+
'SectionKey'
|
|
330
|
+
) ||
|
|
331
|
+
SchLibStreamExtractor.#field(
|
|
332
|
+
record.fields,
|
|
333
|
+
'Storage'
|
|
334
|
+
) ||
|
|
335
|
+
SchLibStreamExtractor.#field(record.fields, 'Key'),
|
|
336
|
+
sourceStream: record.sourceStream || 'SectionKeys',
|
|
337
|
+
recordIndex
|
|
338
|
+
})
|
|
339
|
+
)
|
|
340
|
+
.filter((record) => record.libReference || record.sectionKey)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Parses known per-pin side streams.
|
|
345
|
+
* @param {Map<string, Uint8Array>} streams Source streams.
|
|
346
|
+
* @param {string} sourceStorage Symbol storage path.
|
|
347
|
+
* @returns {{ records: Record<string, object[]>, usedStreamNames: string[] }}
|
|
348
|
+
*/
|
|
349
|
+
static #pinSidecars(streams, sourceStorage) {
|
|
350
|
+
const records = {}
|
|
351
|
+
const usedStreamNames = []
|
|
352
|
+
|
|
353
|
+
for (const streamName of PIN_SIDECAR_STREAMS) {
|
|
354
|
+
const sourceStream = sourceStorage + '/' + streamName
|
|
355
|
+
const bytes = streams.get(sourceStream)
|
|
356
|
+
if (!(bytes instanceof Uint8Array) || !bytes.byteLength) {
|
|
357
|
+
continue
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const parsed = SchLibStreamExtractor.#parseSidecarRecords(bytes, {
|
|
361
|
+
sourceStorage,
|
|
362
|
+
sourceStream
|
|
363
|
+
})
|
|
364
|
+
if (!parsed.length) {
|
|
365
|
+
continue
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
records[streamName] = parsed
|
|
369
|
+
usedStreamNames.push(sourceStream)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
return { records, usedStreamNames }
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/**
|
|
376
|
+
* Parses one pin side stream, inflating zlib payloads when present.
|
|
377
|
+
* @param {Uint8Array} bytes Side stream bytes.
|
|
378
|
+
* @param {{ sourceStorage: string, sourceStream: string }} options Source metadata.
|
|
379
|
+
* @returns {object[]}
|
|
380
|
+
*/
|
|
381
|
+
static #parseSidecarRecords(bytes, options) {
|
|
382
|
+
const decoded = SchLibStreamExtractor.#inflateIfPossible(bytes).bytes
|
|
383
|
+
|
|
384
|
+
return AsciiRecordParser.parse(
|
|
385
|
+
SchLibStreamExtractor.#toArrayBuffer(decoded)
|
|
386
|
+
).map((record, recordIndex) => ({
|
|
387
|
+
...record,
|
|
388
|
+
recordIndex,
|
|
389
|
+
sourceStorage: options.sourceStorage,
|
|
390
|
+
sourceStream: options.sourceStream
|
|
391
|
+
}))
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Extracts a compressed or raw storage payload as an embedded asset.
|
|
396
|
+
* @param {Uint8Array | undefined} bytes Storage stream bytes.
|
|
397
|
+
* @param {string} sourceStream Source stream name.
|
|
398
|
+
* @returns {object | null}
|
|
399
|
+
*/
|
|
400
|
+
static #storageAsset(bytes, sourceStream) {
|
|
401
|
+
if (!(bytes instanceof Uint8Array) || !bytes.byteLength) {
|
|
402
|
+
return null
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const decoded = SchLibStreamExtractor.#inflateIfPossible(bytes)
|
|
406
|
+
|
|
407
|
+
return {
|
|
408
|
+
sourceStream,
|
|
409
|
+
name: SchLibStreamExtractor.#basename(sourceStream),
|
|
410
|
+
format: SchLibStreamExtractor.#format(decoded.bytes),
|
|
411
|
+
byteLength: decoded.bytes.byteLength,
|
|
412
|
+
...(decoded.compression
|
|
413
|
+
? { compression: decoded.compression }
|
|
414
|
+
: {}),
|
|
415
|
+
checksum: {
|
|
416
|
+
algorithm: 'fnv1a32',
|
|
417
|
+
value: SchLibStreamExtractor.#fnv1a32(decoded.bytes)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Returns true when a stream is a known pin side stream.
|
|
424
|
+
* @param {string} streamName Stream path.
|
|
425
|
+
* @param {string} sourceStorage Symbol storage path.
|
|
426
|
+
* @returns {boolean}
|
|
427
|
+
*/
|
|
428
|
+
static #isKnownPinSidecar(streamName, sourceStorage) {
|
|
429
|
+
return PIN_SIDECAR_STREAMS.some(
|
|
430
|
+
(sidecarName) => streamName === sourceStorage + '/' + sidecarName
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Builds recognized SchLib stream names from one stream listing.
|
|
436
|
+
* @param {string[]} streamNames Native stream names.
|
|
437
|
+
* @returns {string[]}
|
|
438
|
+
*/
|
|
439
|
+
static #knownStreamNames(streamNames) {
|
|
440
|
+
return (streamNames || []).filter((streamName) => {
|
|
441
|
+
if (
|
|
442
|
+
['FileHeader', 'SectionKeys', 'Library/Data'].includes(
|
|
443
|
+
streamName
|
|
444
|
+
)
|
|
445
|
+
) {
|
|
446
|
+
return true
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
const leafName = String(streamName || '')
|
|
450
|
+
.split('/')
|
|
451
|
+
.at(-1)
|
|
452
|
+
return (
|
|
453
|
+
leafName === 'Data' ||
|
|
454
|
+
leafName === 'Storage' ||
|
|
455
|
+
PIN_SIDECAR_STREAMS.includes(leafName)
|
|
456
|
+
)
|
|
457
|
+
})
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Inflates zlib data when possible and otherwise returns the raw bytes.
|
|
462
|
+
* @param {Uint8Array} bytes Source bytes.
|
|
463
|
+
* @returns {{ bytes: Uint8Array, compression?: string }}
|
|
464
|
+
*/
|
|
465
|
+
static #inflateIfPossible(bytes) {
|
|
466
|
+
try {
|
|
467
|
+
return {
|
|
468
|
+
bytes: Uint8Array.from(unzlibSync(bytes)),
|
|
469
|
+
compression: 'zlib'
|
|
470
|
+
}
|
|
471
|
+
} catch (_error) {
|
|
472
|
+
return { bytes }
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
/**
|
|
477
|
+
* Reads one field case-insensitively from a parsed record.
|
|
478
|
+
* @param {object | undefined} fields Source fields.
|
|
479
|
+
* @param {string} key Requested key.
|
|
480
|
+
* @returns {string}
|
|
481
|
+
*/
|
|
482
|
+
static #field(fields, key) {
|
|
483
|
+
if (!fields) return ''
|
|
484
|
+
if (fields[key] !== undefined) return String(fields[key] || '')
|
|
485
|
+
const normalizedKey = key.toLowerCase()
|
|
486
|
+
const matchedKey = Object.keys(fields).find(
|
|
487
|
+
(candidate) => candidate.toLowerCase() === normalizedKey
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
return matchedKey ? String(fields[matchedKey] || '') : ''
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Resolves the terminal path segment.
|
|
495
|
+
* @param {string} value Path-like stream name.
|
|
496
|
+
* @returns {string}
|
|
497
|
+
*/
|
|
498
|
+
static #basename(value) {
|
|
499
|
+
return String(value || '')
|
|
500
|
+
.split('/')
|
|
501
|
+
.filter(Boolean)
|
|
502
|
+
.pop()
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Classifies a compact embedded payload format.
|
|
507
|
+
* @param {Uint8Array} bytes Payload bytes.
|
|
508
|
+
* @returns {string}
|
|
509
|
+
*/
|
|
510
|
+
static #format(bytes) {
|
|
511
|
+
if (SchLibStreamExtractor.#hasPrefix(bytes, [0x89, 0x50, 0x4e, 0x47])) {
|
|
512
|
+
return 'png'
|
|
513
|
+
}
|
|
514
|
+
if (SchLibStreamExtractor.#hasPrefix(bytes, [0xff, 0xd8, 0xff])) {
|
|
515
|
+
return 'jpeg'
|
|
516
|
+
}
|
|
517
|
+
if (SchLibStreamExtractor.#asciiPrefix(bytes).startsWith('GIF')) {
|
|
518
|
+
return 'gif'
|
|
519
|
+
}
|
|
520
|
+
if (SchLibStreamExtractor.#asciiPrefix(bytes).startsWith('BM')) {
|
|
521
|
+
return 'bmp'
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return 'binary'
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Returns true when bytes start with one prefix.
|
|
529
|
+
* @param {Uint8Array} bytes Payload bytes.
|
|
530
|
+
* @param {number[]} prefix Prefix bytes.
|
|
531
|
+
* @returns {boolean}
|
|
532
|
+
*/
|
|
533
|
+
static #hasPrefix(bytes, prefix) {
|
|
534
|
+
return prefix.every((value, index) => bytes[index] === value)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Reads a short Latin-1 prefix.
|
|
539
|
+
* @param {Uint8Array} bytes Payload bytes.
|
|
540
|
+
* @returns {string}
|
|
541
|
+
*/
|
|
542
|
+
static #asciiPrefix(bytes) {
|
|
543
|
+
return new TextDecoder('latin1').decode(bytes.slice(0, 8))
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
/**
|
|
547
|
+
* Computes a stable FNV-1a checksum.
|
|
548
|
+
* @param {Uint8Array} bytes Payload bytes.
|
|
549
|
+
* @returns {string}
|
|
550
|
+
*/
|
|
551
|
+
static #fnv1a32(bytes) {
|
|
552
|
+
let hash = 0x811c9dc5
|
|
553
|
+
|
|
554
|
+
for (const value of bytes) {
|
|
555
|
+
hash ^= value
|
|
556
|
+
hash = Math.imul(hash, 0x01000193) >>> 0
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
return hash.toString(16).padStart(8, '0')
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Removes undefined and empty-string values.
|
|
564
|
+
* @param {object} row Source row.
|
|
565
|
+
* @returns {object}
|
|
566
|
+
*/
|
|
567
|
+
static #stripUndefined(row) {
|
|
568
|
+
return Object.fromEntries(
|
|
569
|
+
Object.entries(row).filter(
|
|
570
|
+
([, value]) => value !== undefined && value !== ''
|
|
571
|
+
)
|
|
572
|
+
)
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Returns an ArrayBuffer view over one byte slice.
|
|
577
|
+
* @param {Uint8Array} bytes Source bytes.
|
|
578
|
+
* @returns {ArrayBuffer}
|
|
579
|
+
*/
|
|
580
|
+
static #toArrayBuffer(bytes) {
|
|
581
|
+
return bytes.buffer.slice(
|
|
582
|
+
bytes.byteOffset,
|
|
583
|
+
bytes.byteOffset + bytes.byteLength
|
|
584
|
+
)
|
|
585
|
+
}
|
|
586
|
+
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { ParserUtils } from './ParserUtils.mjs'
|
|
6
6
|
|
|
7
|
-
const { getField, parseNumericField, toColor } =
|
|
7
|
+
const { getField, parseNumericField, parseSchematicLineWidth, toColor } =
|
|
8
|
+
ParserUtils
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Normalizes authored schematic bus-entry records.
|
|
@@ -37,7 +38,7 @@ export class SchematicBusEntryParser {
|
|
|
37
38
|
x2,
|
|
38
39
|
y2,
|
|
39
40
|
color: toColor(record.fields.Color, '#000080'),
|
|
40
|
-
width:
|
|
41
|
+
width: parseSchematicLineWidth(record.fields),
|
|
41
42
|
renderOrder:
|
|
42
43
|
parseNumericField(record.fields, 'IndexInSheet') ??
|
|
43
44
|
record.recordIndex
|