altium-toolkit 0.1.0 → 0.1.16
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 +24 -6
- package/docs/api.md +42 -4
- package/docs/model-format.md +95 -5
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +553 -0
- package/docs/testing.md +7 -2
- package/package.json +21 -2
- package/spec/library-scope.md +7 -1
- package/src/core/altium/AltiumParser.mjs +22 -325
- package/src/core/altium/NormalizedModelSchema.mjs +28 -0
- package/src/core/altium/PcbArcPrimitiveParser.mjs +87 -0
- package/src/core/altium/PcbBinaryPrimitiveParser.mjs +43 -370
- package/src/core/altium/PcbBoardRegionSemanticsParser.mjs +477 -0
- package/src/core/altium/PcbComponentAnnotationNormalizer.mjs +290 -0
- package/src/core/altium/PcbComponentBodyPlacementNormalizer.mjs +52 -0
- package/src/core/altium/PcbComponentPrimitiveIndexer.mjs +109 -0
- package/src/core/altium/PcbEmbeddedFontExtractor.mjs +484 -0
- package/src/core/altium/PcbFillPrimitiveParser.mjs +84 -0
- package/src/core/altium/PcbFontMetricsParser.mjs +308 -0
- package/src/core/altium/PcbGeometryFlipper.mjs +244 -0
- package/src/core/altium/PcbLayerIdCodec.mjs +136 -0
- package/src/core/altium/PcbLibModelParser.mjs +202 -0
- package/src/core/altium/PcbLibStreamExtractor.mjs +968 -0
- package/src/core/altium/PcbModelParser.mjs +618 -66
- package/src/core/altium/PcbOutlineRecovery.mjs +4 -112
- package/src/core/altium/PcbPadPrimitiveParser.mjs +347 -0
- package/src/core/altium/PcbPadShapeCodec.mjs +158 -0
- package/src/core/altium/PcbPadStackParser.mjs +903 -0
- package/src/core/altium/PcbPrimitiveOwnershipIndexParser.mjs +60 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +212 -0
- package/src/core/altium/PcbPrimitiveRecordSlicer.mjs +243 -0
- package/src/core/altium/PcbRawRecordRegistry.mjs +831 -0
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +317 -0
- package/src/core/altium/PcbRuleParser.mjs +587 -0
- package/src/core/altium/PcbStreamExtractor.mjs +127 -4
- package/src/core/altium/PcbTextPrimitiveParser.mjs +537 -0
- package/src/core/altium/PcbTrackPrimitiveParser.mjs +87 -0
- package/src/core/altium/PcbViaPrimitiveParser.mjs +88 -0
- package/src/core/altium/PcbViaStackParser.mjs +548 -0
- package/src/core/altium/PcbWideStringTableParser.mjs +108 -0
- package/src/core/altium/PrjPcbModelParser.mjs +797 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +355 -0
- package/src/parser.mjs +13 -0
- package/src/renderers.mjs +5 -0
- package/src/styles/altium-renderers.css +11 -6
- package/src/ui/PcbCopperPrimitiveSplitter.mjs +113 -0
- package/src/ui/PcbEdgeFacingGlyphNormalizer.mjs +6 -5
- package/src/ui/PcbEmbeddedFontFaceRenderer.mjs +126 -0
- package/src/ui/PcbFootprintPrimitiveSelector.mjs +27 -6
- package/src/ui/PcbRegionPrimitiveRenderer.mjs +243 -0
- package/src/ui/PcbSideResolvedRenderModel.mjs +336 -0
- package/src/ui/PcbSvgRenderer.mjs +101 -109
- package/src/ui/PcbTextPrimitiveRenderer.mjs +252 -0
- package/src/ui/SchematicSheetChromeRenderer.mjs +2 -93
- package/src/ui/SchematicSheetZoneRenderer.mjs +104 -0
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { PcbPrimitiveRecordSlicer } from './PcbPrimitiveRecordSlicer.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Provides a read-only PCB primitive record registry and raw record
|
|
9
|
+
* preservation helpers.
|
|
10
|
+
*/
|
|
11
|
+
export class PcbRawRecordRegistry {
|
|
12
|
+
static #PCB_DOC_DESCRIPTORS = Object.freeze(
|
|
13
|
+
[
|
|
14
|
+
{
|
|
15
|
+
sourceStream: 'Arcs6/Data',
|
|
16
|
+
headerStream: 'Arcs6/Header',
|
|
17
|
+
family: 'arcs',
|
|
18
|
+
collection: 'arcs',
|
|
19
|
+
type: 'arc',
|
|
20
|
+
typeId: 1,
|
|
21
|
+
fixedRecordByteLength: 60,
|
|
22
|
+
minimumPayloadByteLength: 45,
|
|
23
|
+
lengthPrefixedView: 'payload',
|
|
24
|
+
parser: 'PcbArcPrimitiveParser',
|
|
25
|
+
strategy: 'slicer'
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
sourceStream: 'Tracks6/Data',
|
|
29
|
+
headerStream: 'Tracks6/Header',
|
|
30
|
+
family: 'tracks',
|
|
31
|
+
collection: 'tracks',
|
|
32
|
+
type: 'track',
|
|
33
|
+
typeId: 4,
|
|
34
|
+
fixedRecordByteLength: 49,
|
|
35
|
+
minimumPayloadByteLength: 33,
|
|
36
|
+
lengthPrefixedView: 'payload',
|
|
37
|
+
parser: 'PcbTrackPrimitiveParser',
|
|
38
|
+
strategy: 'slicer'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
sourceStream: 'Vias6/Data',
|
|
42
|
+
headerStream: 'Vias6/Header',
|
|
43
|
+
family: 'vias',
|
|
44
|
+
collection: 'vias',
|
|
45
|
+
type: 'via',
|
|
46
|
+
typeId: 3,
|
|
47
|
+
fixedRecordByteLength: 326,
|
|
48
|
+
minimumPayloadByteLength: 321,
|
|
49
|
+
lengthPrefixedView: 'record',
|
|
50
|
+
parser: 'PcbViaPrimitiveParser',
|
|
51
|
+
strategy: 'slicer'
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
sourceStream: 'Fills6/Data',
|
|
55
|
+
headerStream: 'Fills6/Header',
|
|
56
|
+
family: 'fills',
|
|
57
|
+
collection: 'fills',
|
|
58
|
+
type: 'fill',
|
|
59
|
+
typeId: 6,
|
|
60
|
+
fixedRecordByteLength: 55,
|
|
61
|
+
minimumPayloadByteLength: 50,
|
|
62
|
+
lengthPrefixedView: 'record',
|
|
63
|
+
parser: 'PcbFillPrimitiveParser',
|
|
64
|
+
strategy: 'slicer'
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
sourceStream: 'Pads6/Data',
|
|
68
|
+
headerStream: 'Pads6/Header',
|
|
69
|
+
family: 'pads',
|
|
70
|
+
collection: 'pads',
|
|
71
|
+
type: 'pad',
|
|
72
|
+
typeId: 2,
|
|
73
|
+
minimumSubrecordCount: 6,
|
|
74
|
+
validatedSubrecordIndex: 4,
|
|
75
|
+
minimumPayloadByteLength: 61,
|
|
76
|
+
parser: 'PcbPadPrimitiveParser',
|
|
77
|
+
strategy: 'subrecord-list'
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
sourceStream: 'Texts6/Data',
|
|
81
|
+
headerStream: 'Texts6/Header',
|
|
82
|
+
family: 'texts',
|
|
83
|
+
collection: 'texts',
|
|
84
|
+
type: 'text',
|
|
85
|
+
typeId: 5,
|
|
86
|
+
minimumPayloadByteLength: 64,
|
|
87
|
+
maximumPayloadByteLength: 2048,
|
|
88
|
+
parser: 'PcbTextPrimitiveParser',
|
|
89
|
+
strategy: 'text-tail'
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
sourceStream: 'Texts/Data',
|
|
93
|
+
headerStream: 'Texts/Header',
|
|
94
|
+
family: 'texts',
|
|
95
|
+
collection: 'texts',
|
|
96
|
+
type: 'text',
|
|
97
|
+
typeId: 5,
|
|
98
|
+
minimumPayloadByteLength: 64,
|
|
99
|
+
maximumPayloadByteLength: 2048,
|
|
100
|
+
parser: 'PcbTextPrimitiveParser',
|
|
101
|
+
strategy: 'text-tail'
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
sourceStream: 'Regions6/Data',
|
|
105
|
+
headerStream: 'Regions6/Header',
|
|
106
|
+
family: 'regions',
|
|
107
|
+
collection: 'regions',
|
|
108
|
+
type: 'region',
|
|
109
|
+
typeId: 11,
|
|
110
|
+
minimumPayloadByteLength: 18,
|
|
111
|
+
parser: 'PcbRegionPrimitiveParser',
|
|
112
|
+
strategy: 'length-prefixed'
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
sourceStream: 'ShapeBasedRegions6/Data',
|
|
116
|
+
headerStream: 'ShapeBasedRegions6/Header',
|
|
117
|
+
family: 'shapeBasedRegions',
|
|
118
|
+
collection: 'shapeBasedRegions',
|
|
119
|
+
type: 'region',
|
|
120
|
+
typeId: 11,
|
|
121
|
+
minimumPayloadByteLength: 18,
|
|
122
|
+
parser: 'PcbRegionPrimitiveParser',
|
|
123
|
+
strategy: 'length-prefixed'
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
sourceStream: 'BoardRegions/Data',
|
|
127
|
+
headerStream: 'BoardRegions/Header',
|
|
128
|
+
family: 'boardRegions',
|
|
129
|
+
collection: 'boardRegions',
|
|
130
|
+
type: 'region',
|
|
131
|
+
typeId: 11,
|
|
132
|
+
minimumPayloadByteLength: 18,
|
|
133
|
+
parser: 'PcbRegionPrimitiveParser',
|
|
134
|
+
strategy: 'length-prefixed'
|
|
135
|
+
}
|
|
136
|
+
].map((descriptor) => Object.freeze(descriptor))
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Returns immutable copies of the registered PcbDoc primitive descriptors.
|
|
141
|
+
* @returns {object[]}
|
|
142
|
+
*/
|
|
143
|
+
static pcbDocDescriptors() {
|
|
144
|
+
return PcbRawRecordRegistry.#PCB_DOC_DESCRIPTORS.map((descriptor) =>
|
|
145
|
+
Object.freeze({ ...descriptor })
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Returns the descriptor registered for one PcbDoc data stream.
|
|
151
|
+
* @param {string} sourceStream
|
|
152
|
+
* @returns {object | null}
|
|
153
|
+
*/
|
|
154
|
+
static descriptorForPcbDocStream(sourceStream) {
|
|
155
|
+
const descriptor = PcbRawRecordRegistry.#PCB_DOC_DESCRIPTORS.find(
|
|
156
|
+
(candidate) => candidate.sourceStream === sourceStream
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return descriptor ? Object.freeze({ ...descriptor }) : null
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Collects raw records from registered PcbDoc primitive streams.
|
|
164
|
+
* @param {Map<string, Uint8Array>} streams
|
|
165
|
+
* @param {Record<string, object[]>} [binaryPrimitives]
|
|
166
|
+
* @returns {object[]}
|
|
167
|
+
*/
|
|
168
|
+
static collectPcbDocRecords(streams, binaryPrimitives = {}) {
|
|
169
|
+
const rawRecords = []
|
|
170
|
+
|
|
171
|
+
for (const descriptor of PcbRawRecordRegistry.#PCB_DOC_DESCRIPTORS) {
|
|
172
|
+
const headerBytes = streams.get(descriptor.headerStream)
|
|
173
|
+
const dataBytes = streams.get(descriptor.sourceStream)
|
|
174
|
+
|
|
175
|
+
if (!headerBytes || !dataBytes) {
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const slices = PcbRawRecordRegistry.#slicePcbDocRecords(
|
|
180
|
+
descriptor,
|
|
181
|
+
headerBytes,
|
|
182
|
+
dataBytes
|
|
183
|
+
)
|
|
184
|
+
const parsedCount =
|
|
185
|
+
binaryPrimitives?.[descriptor.collection]?.length || 0
|
|
186
|
+
|
|
187
|
+
if (!slices.length) {
|
|
188
|
+
if (
|
|
189
|
+
PcbRawRecordRegistry.#readRecordCount(headerBytes) > 0 &&
|
|
190
|
+
PcbRawRecordRegistry.#toUint8Array(dataBytes).byteLength > 0
|
|
191
|
+
) {
|
|
192
|
+
rawRecords.push(
|
|
193
|
+
PcbRawRecordRegistry.#createUnparsedPcbDocRecord(
|
|
194
|
+
descriptor,
|
|
195
|
+
dataBytes
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
for (const slice of slices) {
|
|
203
|
+
rawRecords.push(
|
|
204
|
+
PcbRawRecordRegistry.#normalizePcbDocRecord(
|
|
205
|
+
descriptor,
|
|
206
|
+
slice,
|
|
207
|
+
parsedCount
|
|
208
|
+
)
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return rawRecords
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Creates one raw PcbLib footprint record descriptor.
|
|
218
|
+
* @param {{ sourceStorage: string, record: { typeId: number, descriptor: object | null, recordBytes: Uint8Array, offset: number, byteLength: number }, recordIndex: number, parsed: boolean }} options
|
|
219
|
+
* @returns {object}
|
|
220
|
+
*/
|
|
221
|
+
static createPcbLibRecord(options) {
|
|
222
|
+
const descriptor = options.record.descriptor
|
|
223
|
+
const sourceStream = options.sourceStorage + '/Data'
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
registryId: 'pcblib:' + sourceStream + ':' + options.recordIndex,
|
|
227
|
+
source: 'pcblib',
|
|
228
|
+
sourceStorage: options.sourceStorage,
|
|
229
|
+
sourceStream,
|
|
230
|
+
family: descriptor?.collection || 'unknown',
|
|
231
|
+
type: descriptor?.type || 'unknown',
|
|
232
|
+
typeId: options.record.typeId,
|
|
233
|
+
recordIndex: options.recordIndex,
|
|
234
|
+
offset: options.record.offset,
|
|
235
|
+
byteLength: options.record.byteLength,
|
|
236
|
+
payloadByteLength: null,
|
|
237
|
+
encoding: 'mixed-footprint',
|
|
238
|
+
supported: Boolean(descriptor),
|
|
239
|
+
parsed: Boolean(options.parsed),
|
|
240
|
+
rawBase64: PcbRawRecordRegistry.#toBase64(
|
|
241
|
+
options.record.recordBytes
|
|
242
|
+
)
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Selects the slicing strategy for one registered PcbDoc stream.
|
|
248
|
+
* @param {object} descriptor
|
|
249
|
+
* @param {Uint8Array | ArrayBuffer} headerBytes
|
|
250
|
+
* @param {Uint8Array | ArrayBuffer} dataBytes
|
|
251
|
+
* @returns {object[]}
|
|
252
|
+
*/
|
|
253
|
+
static #slicePcbDocRecords(descriptor, headerBytes, dataBytes) {
|
|
254
|
+
if (descriptor.strategy === 'slicer') {
|
|
255
|
+
return PcbPrimitiveRecordSlicer.slicePrimitiveRecords({
|
|
256
|
+
headerBytes,
|
|
257
|
+
dataBytes,
|
|
258
|
+
objectId: descriptor.typeId,
|
|
259
|
+
fixedRecordByteLength: descriptor.fixedRecordByteLength,
|
|
260
|
+
minimumPayloadByteLength: descriptor.minimumPayloadByteLength,
|
|
261
|
+
lengthPrefixedView: descriptor.lengthPrefixedView
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (descriptor.strategy === 'subrecord-list') {
|
|
266
|
+
return PcbRawRecordRegistry.#sliceSubrecordListRecords(
|
|
267
|
+
descriptor,
|
|
268
|
+
headerBytes,
|
|
269
|
+
dataBytes
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (descriptor.strategy === 'text-tail') {
|
|
274
|
+
return PcbRawRecordRegistry.#sliceTextTailRecords(
|
|
275
|
+
descriptor,
|
|
276
|
+
headerBytes,
|
|
277
|
+
dataBytes
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return PcbRawRecordRegistry.#sliceLengthPrefixedRecords(
|
|
282
|
+
descriptor,
|
|
283
|
+
headerBytes,
|
|
284
|
+
dataBytes
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Slices exact object-id/payload-length records.
|
|
290
|
+
* @param {object} descriptor
|
|
291
|
+
* @param {Uint8Array | ArrayBuffer} headerBytes
|
|
292
|
+
* @param {Uint8Array | ArrayBuffer} dataBytes
|
|
293
|
+
* @returns {object[]}
|
|
294
|
+
*/
|
|
295
|
+
static #sliceLengthPrefixedRecords(descriptor, headerBytes, dataBytes) {
|
|
296
|
+
const count = PcbRawRecordRegistry.#readRecordCount(headerBytes)
|
|
297
|
+
const bytes = PcbRawRecordRegistry.#toUint8Array(dataBytes)
|
|
298
|
+
const records = []
|
|
299
|
+
let offset = 0
|
|
300
|
+
|
|
301
|
+
for (let index = 0; index < count; index += 1) {
|
|
302
|
+
const record = PcbRawRecordRegistry.#readLengthPrefixedRecordAt(
|
|
303
|
+
bytes,
|
|
304
|
+
offset,
|
|
305
|
+
descriptor
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
if (!record) {
|
|
309
|
+
return []
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
records.push({ ...record, recordIndex: index })
|
|
313
|
+
offset += record.byteLength
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return offset === bytes.byteLength ? records : []
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Reads one object-id/payload-length record at an offset.
|
|
321
|
+
* @param {Uint8Array} bytes
|
|
322
|
+
* @param {number} offset
|
|
323
|
+
* @param {object} descriptor
|
|
324
|
+
* @returns {object | null}
|
|
325
|
+
*/
|
|
326
|
+
static #readLengthPrefixedRecordAt(bytes, offset, descriptor) {
|
|
327
|
+
if (
|
|
328
|
+
offset + 5 > bytes.byteLength ||
|
|
329
|
+
bytes[offset] !== descriptor.typeId
|
|
330
|
+
) {
|
|
331
|
+
return null
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const payloadByteLength = PcbRawRecordRegistry.#readUint32(
|
|
335
|
+
bytes,
|
|
336
|
+
offset + 1
|
|
337
|
+
)
|
|
338
|
+
const byteLength = 5 + payloadByteLength
|
|
339
|
+
|
|
340
|
+
if (
|
|
341
|
+
payloadByteLength < descriptor.minimumPayloadByteLength ||
|
|
342
|
+
offset + byteLength > bytes.byteLength
|
|
343
|
+
) {
|
|
344
|
+
return null
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
recordBytes: bytes.slice(offset, offset + byteLength),
|
|
349
|
+
offset,
|
|
350
|
+
byteLength,
|
|
351
|
+
payloadByteLength,
|
|
352
|
+
encoding: 'length-prefixed',
|
|
353
|
+
objectId: descriptor.typeId,
|
|
354
|
+
recordIndex: 0
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Slices subrecord-list primitives such as PcbDoc pads.
|
|
360
|
+
* @param {object} descriptor
|
|
361
|
+
* @param {Uint8Array | ArrayBuffer} headerBytes
|
|
362
|
+
* @param {Uint8Array | ArrayBuffer} dataBytes
|
|
363
|
+
* @returns {object[]}
|
|
364
|
+
*/
|
|
365
|
+
static #sliceSubrecordListRecords(descriptor, headerBytes, dataBytes) {
|
|
366
|
+
const count = PcbRawRecordRegistry.#readRecordCount(headerBytes)
|
|
367
|
+
const bytes = PcbRawRecordRegistry.#toUint8Array(dataBytes)
|
|
368
|
+
const records = []
|
|
369
|
+
let offset = 0
|
|
370
|
+
|
|
371
|
+
for (let index = 0; index < count; index += 1) {
|
|
372
|
+
const remainingCount = count - index - 1
|
|
373
|
+
const record = PcbRawRecordRegistry.#readSubrecordListRecordAt(
|
|
374
|
+
bytes,
|
|
375
|
+
offset,
|
|
376
|
+
descriptor,
|
|
377
|
+
remainingCount
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
if (!record) {
|
|
381
|
+
return []
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
records.push({ ...record, recordIndex: index })
|
|
385
|
+
offset += record.byteLength
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return records
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Reads one subrecord-list primitive record at an offset.
|
|
393
|
+
* @param {Uint8Array} bytes
|
|
394
|
+
* @param {number} offset
|
|
395
|
+
* @param {object} descriptor
|
|
396
|
+
* @param {number} remainingCount
|
|
397
|
+
* @returns {object | null}
|
|
398
|
+
*/
|
|
399
|
+
static #readSubrecordListRecordAt(
|
|
400
|
+
bytes,
|
|
401
|
+
offset,
|
|
402
|
+
descriptor,
|
|
403
|
+
remainingCount
|
|
404
|
+
) {
|
|
405
|
+
if (
|
|
406
|
+
offset + 1 > bytes.byteLength ||
|
|
407
|
+
bytes[offset] !== descriptor.typeId
|
|
408
|
+
) {
|
|
409
|
+
return null
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const minimumEnd = PcbRawRecordRegistry.#readMinimumSubrecordListEnd(
|
|
413
|
+
bytes,
|
|
414
|
+
offset,
|
|
415
|
+
descriptor
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if (!minimumEnd) {
|
|
419
|
+
return null
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const endOffset = remainingCount
|
|
423
|
+
? PcbRawRecordRegistry.#findNextSubrecordListRecordOffset(
|
|
424
|
+
bytes,
|
|
425
|
+
minimumEnd,
|
|
426
|
+
descriptor,
|
|
427
|
+
remainingCount
|
|
428
|
+
)
|
|
429
|
+
: bytes.byteLength
|
|
430
|
+
|
|
431
|
+
if (!endOffset) {
|
|
432
|
+
return null
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
recordBytes: bytes.slice(offset, endOffset),
|
|
437
|
+
offset,
|
|
438
|
+
byteLength: endOffset - offset,
|
|
439
|
+
payloadByteLength: null,
|
|
440
|
+
encoding: 'subrecord-list',
|
|
441
|
+
objectId: descriptor.typeId,
|
|
442
|
+
recordIndex: 0
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Reads the minimum byte boundary for one subrecord-list record.
|
|
448
|
+
* @param {Uint8Array} bytes
|
|
449
|
+
* @param {number} offset
|
|
450
|
+
* @param {object} descriptor
|
|
451
|
+
* @returns {number | null}
|
|
452
|
+
*/
|
|
453
|
+
static #readMinimumSubrecordListEnd(bytes, offset, descriptor) {
|
|
454
|
+
let cursor = offset + 1
|
|
455
|
+
|
|
456
|
+
for (
|
|
457
|
+
let subrecordIndex = 0;
|
|
458
|
+
subrecordIndex < descriptor.minimumSubrecordCount;
|
|
459
|
+
subrecordIndex += 1
|
|
460
|
+
) {
|
|
461
|
+
const subrecord = PcbRawRecordRegistry.#readSubrecordAt(
|
|
462
|
+
bytes,
|
|
463
|
+
cursor
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
if (!subrecord) {
|
|
467
|
+
return null
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const shouldValidate =
|
|
471
|
+
descriptor.validatedSubrecordIndex === undefined ||
|
|
472
|
+
descriptor.validatedSubrecordIndex === subrecordIndex
|
|
473
|
+
if (
|
|
474
|
+
shouldValidate &&
|
|
475
|
+
subrecord.payloadByteLength <
|
|
476
|
+
descriptor.minimumPayloadByteLength
|
|
477
|
+
) {
|
|
478
|
+
return null
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
cursor = subrecord.nextOffset
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return cursor
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Finds the next known subrecord-list primitive boundary.
|
|
489
|
+
* @param {Uint8Array} bytes
|
|
490
|
+
* @param {number} offset
|
|
491
|
+
* @param {object} descriptor
|
|
492
|
+
* @param {number} remainingCount
|
|
493
|
+
* @returns {number | null}
|
|
494
|
+
*/
|
|
495
|
+
static #findNextSubrecordListRecordOffset(
|
|
496
|
+
bytes,
|
|
497
|
+
offset,
|
|
498
|
+
descriptor,
|
|
499
|
+
remainingCount
|
|
500
|
+
) {
|
|
501
|
+
let cursor = offset
|
|
502
|
+
|
|
503
|
+
while (cursor < bytes.byteLength) {
|
|
504
|
+
if (
|
|
505
|
+
PcbRawRecordRegistry.#canReadSubrecordListSequence(
|
|
506
|
+
bytes,
|
|
507
|
+
cursor,
|
|
508
|
+
descriptor,
|
|
509
|
+
remainingCount
|
|
510
|
+
)
|
|
511
|
+
) {
|
|
512
|
+
return cursor
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const unknownSubrecord = PcbRawRecordRegistry.#readSubrecordAt(
|
|
516
|
+
bytes,
|
|
517
|
+
cursor
|
|
518
|
+
)
|
|
519
|
+
cursor = unknownSubrecord ? unknownSubrecord.nextOffset : cursor + 1
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
return null
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
* Checks whether the remaining subrecord-list records are readable.
|
|
527
|
+
* @param {Uint8Array} bytes
|
|
528
|
+
* @param {number} offset
|
|
529
|
+
* @param {object} descriptor
|
|
530
|
+
* @param {number} remainingCount
|
|
531
|
+
* @returns {boolean}
|
|
532
|
+
*/
|
|
533
|
+
static #canReadSubrecordListSequence(
|
|
534
|
+
bytes,
|
|
535
|
+
offset,
|
|
536
|
+
descriptor,
|
|
537
|
+
remainingCount
|
|
538
|
+
) {
|
|
539
|
+
const minimumEnd = PcbRawRecordRegistry.#readMinimumSubrecordListEnd(
|
|
540
|
+
bytes,
|
|
541
|
+
offset,
|
|
542
|
+
descriptor
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
if (!minimumEnd) {
|
|
546
|
+
return false
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (remainingCount <= 1) {
|
|
550
|
+
return true
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
return (
|
|
554
|
+
PcbRawRecordRegistry.#findNextSubrecordListRecordOffset(
|
|
555
|
+
bytes,
|
|
556
|
+
minimumEnd,
|
|
557
|
+
descriptor,
|
|
558
|
+
remainingCount - 1
|
|
559
|
+
) !== null
|
|
560
|
+
)
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
/**
|
|
564
|
+
* Slices PCB text records including their variable string tails.
|
|
565
|
+
* @param {object} descriptor
|
|
566
|
+
* @param {Uint8Array | ArrayBuffer} headerBytes
|
|
567
|
+
* @param {Uint8Array | ArrayBuffer} dataBytes
|
|
568
|
+
* @returns {object[]}
|
|
569
|
+
*/
|
|
570
|
+
static #sliceTextTailRecords(descriptor, headerBytes, dataBytes) {
|
|
571
|
+
const count = PcbRawRecordRegistry.#readRecordCount(headerBytes)
|
|
572
|
+
const bytes = PcbRawRecordRegistry.#toUint8Array(dataBytes)
|
|
573
|
+
const records = []
|
|
574
|
+
let offset = 0
|
|
575
|
+
|
|
576
|
+
for (let index = 0; index < count; index += 1) {
|
|
577
|
+
const record = PcbRawRecordRegistry.#readTextTailRecordAt(
|
|
578
|
+
bytes,
|
|
579
|
+
offset,
|
|
580
|
+
descriptor,
|
|
581
|
+
index === count - 1
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
if (!record) {
|
|
585
|
+
return []
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
records.push({ ...record, recordIndex: index })
|
|
589
|
+
offset += record.byteLength
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
return records
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Reads one PCB text-tail record.
|
|
597
|
+
* @param {Uint8Array} bytes
|
|
598
|
+
* @param {number} offset
|
|
599
|
+
* @param {object} descriptor
|
|
600
|
+
* @param {boolean} isLastRecord
|
|
601
|
+
* @returns {object | null}
|
|
602
|
+
*/
|
|
603
|
+
static #readTextTailRecordAt(bytes, offset, descriptor, isLastRecord) {
|
|
604
|
+
if (
|
|
605
|
+
!PcbRawRecordRegistry.#isTextTailRecordStart(
|
|
606
|
+
bytes,
|
|
607
|
+
offset,
|
|
608
|
+
descriptor
|
|
609
|
+
)
|
|
610
|
+
) {
|
|
611
|
+
return null
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const payloadByteLength = PcbRawRecordRegistry.#readUint32(
|
|
615
|
+
bytes,
|
|
616
|
+
offset + 1
|
|
617
|
+
)
|
|
618
|
+
const payloadEnd = offset + 5 + payloadByteLength
|
|
619
|
+
const nextOffset = isLastRecord
|
|
620
|
+
? bytes.byteLength
|
|
621
|
+
: PcbRawRecordRegistry.#findNextTextTailRecordOffset(
|
|
622
|
+
bytes,
|
|
623
|
+
payloadEnd,
|
|
624
|
+
descriptor
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
if (!nextOffset) {
|
|
628
|
+
return null
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return {
|
|
632
|
+
recordBytes: bytes.slice(offset, nextOffset),
|
|
633
|
+
offset,
|
|
634
|
+
byteLength: nextOffset - offset,
|
|
635
|
+
payloadByteLength,
|
|
636
|
+
encoding: 'text-tail',
|
|
637
|
+
objectId: descriptor.typeId,
|
|
638
|
+
recordIndex: 0
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Finds the next plausible PCB text-tail record start.
|
|
644
|
+
* @param {Uint8Array} bytes
|
|
645
|
+
* @param {number} offset
|
|
646
|
+
* @param {object} descriptor
|
|
647
|
+
* @returns {number | null}
|
|
648
|
+
*/
|
|
649
|
+
static #findNextTextTailRecordOffset(bytes, offset, descriptor) {
|
|
650
|
+
for (let cursor = offset; cursor < bytes.byteLength - 5; cursor += 1) {
|
|
651
|
+
if (
|
|
652
|
+
PcbRawRecordRegistry.#isTextTailRecordStart(
|
|
653
|
+
bytes,
|
|
654
|
+
cursor,
|
|
655
|
+
descriptor
|
|
656
|
+
)
|
|
657
|
+
) {
|
|
658
|
+
return cursor
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
return null
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Returns true when an offset looks like a PCB text-tail record start.
|
|
667
|
+
* @param {Uint8Array} bytes
|
|
668
|
+
* @param {number} offset
|
|
669
|
+
* @param {object} descriptor
|
|
670
|
+
* @returns {boolean}
|
|
671
|
+
*/
|
|
672
|
+
static #isTextTailRecordStart(bytes, offset, descriptor) {
|
|
673
|
+
if (
|
|
674
|
+
offset + 5 > bytes.byteLength ||
|
|
675
|
+
bytes[offset] !== descriptor.typeId
|
|
676
|
+
) {
|
|
677
|
+
return false
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
const payloadByteLength = PcbRawRecordRegistry.#readUint32(
|
|
681
|
+
bytes,
|
|
682
|
+
offset + 1
|
|
683
|
+
)
|
|
684
|
+
const payloadEnd = offset + 5 + payloadByteLength
|
|
685
|
+
|
|
686
|
+
return (
|
|
687
|
+
payloadByteLength >= descriptor.minimumPayloadByteLength &&
|
|
688
|
+
payloadByteLength <= descriptor.maximumPayloadByteLength &&
|
|
689
|
+
payloadEnd <= bytes.byteLength
|
|
690
|
+
)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Reads one length-prefixed subrecord.
|
|
695
|
+
* @param {Uint8Array} bytes
|
|
696
|
+
* @param {number} offset
|
|
697
|
+
* @returns {{ payloadByteLength: number, nextOffset: number } | null}
|
|
698
|
+
*/
|
|
699
|
+
static #readSubrecordAt(bytes, offset) {
|
|
700
|
+
if (offset + 4 > bytes.byteLength) {
|
|
701
|
+
return null
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const payloadByteLength = PcbRawRecordRegistry.#readUint32(
|
|
705
|
+
bytes,
|
|
706
|
+
offset
|
|
707
|
+
)
|
|
708
|
+
const nextOffset = offset + 4 + payloadByteLength
|
|
709
|
+
|
|
710
|
+
if (nextOffset > bytes.byteLength) {
|
|
711
|
+
return null
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
return { payloadByteLength, nextOffset }
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Creates one normalized PcbDoc raw record.
|
|
719
|
+
* @param {object} descriptor
|
|
720
|
+
* @param {object} slice
|
|
721
|
+
* @param {number} parsedCount
|
|
722
|
+
* @returns {object}
|
|
723
|
+
*/
|
|
724
|
+
static #normalizePcbDocRecord(descriptor, slice, parsedCount) {
|
|
725
|
+
return {
|
|
726
|
+
registryId:
|
|
727
|
+
'pcbdoc:' + descriptor.sourceStream + ':' + slice.recordIndex,
|
|
728
|
+
source: 'pcbdoc',
|
|
729
|
+
sourceStream: descriptor.sourceStream,
|
|
730
|
+
headerStream: descriptor.headerStream,
|
|
731
|
+
family: descriptor.family,
|
|
732
|
+
type: descriptor.type,
|
|
733
|
+
typeId: descriptor.typeId,
|
|
734
|
+
recordIndex: slice.recordIndex,
|
|
735
|
+
offset: slice.offset,
|
|
736
|
+
byteLength: slice.byteLength,
|
|
737
|
+
payloadByteLength: slice.payloadByteLength,
|
|
738
|
+
encoding: slice.encoding,
|
|
739
|
+
supported: true,
|
|
740
|
+
parsed: slice.recordIndex < parsedCount,
|
|
741
|
+
rawBase64: PcbRawRecordRegistry.#toBase64(slice.recordBytes)
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Creates one fallback raw record for a registered but unparsed stream.
|
|
747
|
+
* @param {object} descriptor
|
|
748
|
+
* @param {Uint8Array | ArrayBuffer} dataBytes
|
|
749
|
+
* @returns {object}
|
|
750
|
+
*/
|
|
751
|
+
static #createUnparsedPcbDocRecord(descriptor, dataBytes) {
|
|
752
|
+
const bytes = PcbRawRecordRegistry.#toUint8Array(dataBytes)
|
|
753
|
+
|
|
754
|
+
return {
|
|
755
|
+
registryId: 'pcbdoc:' + descriptor.sourceStream + ':0',
|
|
756
|
+
source: 'pcbdoc',
|
|
757
|
+
sourceStream: descriptor.sourceStream,
|
|
758
|
+
headerStream: descriptor.headerStream,
|
|
759
|
+
family: descriptor.family,
|
|
760
|
+
type: descriptor.type,
|
|
761
|
+
typeId: descriptor.typeId,
|
|
762
|
+
recordIndex: 0,
|
|
763
|
+
offset: 0,
|
|
764
|
+
byteLength: bytes.byteLength,
|
|
765
|
+
payloadByteLength: null,
|
|
766
|
+
encoding: 'unparsed-stream',
|
|
767
|
+
supported: true,
|
|
768
|
+
parsed: false,
|
|
769
|
+
rawBase64: PcbRawRecordRegistry.#toBase64(bytes)
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Reads one little-endian record count.
|
|
775
|
+
* @param {Uint8Array | ArrayBuffer} headerBytes
|
|
776
|
+
* @returns {number}
|
|
777
|
+
*/
|
|
778
|
+
static #readRecordCount(headerBytes) {
|
|
779
|
+
const bytes = PcbRawRecordRegistry.#toUint8Array(headerBytes)
|
|
780
|
+
|
|
781
|
+
if (bytes.byteLength < 4) {
|
|
782
|
+
return 0
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return new DataView(bytes.buffer, bytes.byteOffset, 4).getUint32(
|
|
786
|
+
0,
|
|
787
|
+
true
|
|
788
|
+
)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Reads one little-endian unsigned 32-bit value.
|
|
793
|
+
* @param {Uint8Array} bytes
|
|
794
|
+
* @param {number} offset
|
|
795
|
+
* @returns {number}
|
|
796
|
+
*/
|
|
797
|
+
static #readUint32(bytes, offset) {
|
|
798
|
+
return new DataView(
|
|
799
|
+
bytes.buffer,
|
|
800
|
+
bytes.byteOffset + offset,
|
|
801
|
+
4
|
|
802
|
+
).getUint32(0, true)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Normalizes one byte-like input into a Uint8Array view.
|
|
807
|
+
* @param {Uint8Array | ArrayBuffer} bytes
|
|
808
|
+
* @returns {Uint8Array}
|
|
809
|
+
*/
|
|
810
|
+
static #toUint8Array(bytes) {
|
|
811
|
+
return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
/**
|
|
815
|
+
* Encodes raw bytes as base64 without assuming a Node-only runtime.
|
|
816
|
+
* @param {Uint8Array} bytes
|
|
817
|
+
* @returns {string}
|
|
818
|
+
*/
|
|
819
|
+
static #toBase64(bytes) {
|
|
820
|
+
if (typeof Buffer !== 'undefined') {
|
|
821
|
+
return Buffer.from(bytes).toString('base64')
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
let binary = ''
|
|
825
|
+
for (const byte of bytes) {
|
|
826
|
+
binary += String.fromCharCode(byte)
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
return btoa(binary)
|
|
830
|
+
}
|
|
831
|
+
}
|