altium-toolkit 0.1.1 → 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 +6 -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,60 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Decodes nullable owner indexes shared by binary PCB primitive records.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbPrimitiveOwnershipIndexParser {
|
|
9
|
+
/**
|
|
10
|
+
* Reads component, net, and polygon ownership indexes from one record.
|
|
11
|
+
* @param {DataView} view
|
|
12
|
+
* @param {{ component: number, net: number, polygon: number }} offsets
|
|
13
|
+
* @returns {{ componentIndex: number | null, netIndex: number | null, polygonIndex: number | null }}
|
|
14
|
+
*/
|
|
15
|
+
static readOwnershipIndexes(view, offsets) {
|
|
16
|
+
return {
|
|
17
|
+
componentIndex: PcbPrimitiveOwnershipIndexParser.readComponentIndex(
|
|
18
|
+
view,
|
|
19
|
+
offsets.component
|
|
20
|
+
),
|
|
21
|
+
netIndex: PcbPrimitiveOwnershipIndexParser.readLinkIndex(
|
|
22
|
+
view,
|
|
23
|
+
offsets.net
|
|
24
|
+
),
|
|
25
|
+
polygonIndex: PcbPrimitiveOwnershipIndexParser.readLinkIndex(
|
|
26
|
+
view,
|
|
27
|
+
offsets.polygon
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Reads one nullable component index.
|
|
34
|
+
* @param {DataView} view
|
|
35
|
+
* @param {number} offset
|
|
36
|
+
* @returns {number | null}
|
|
37
|
+
*/
|
|
38
|
+
static readComponentIndex(view, offset) {
|
|
39
|
+
return PcbPrimitiveOwnershipIndexParser.readLinkIndex(view, offset)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Reads one nullable two-byte Altium object link index.
|
|
44
|
+
* @param {DataView} view
|
|
45
|
+
* @param {number} offset
|
|
46
|
+
* @returns {number | null}
|
|
47
|
+
*/
|
|
48
|
+
static readLinkIndex(view, offset) {
|
|
49
|
+
if (
|
|
50
|
+
!Number.isInteger(offset) ||
|
|
51
|
+
offset < 0 ||
|
|
52
|
+
offset + 2 > view.byteLength
|
|
53
|
+
) {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const value = view.getUint16(offset, true)
|
|
58
|
+
return value === 0xffff ? null : value
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Decodes Altium PrimitiveParameters/Data sidecar records.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbPrimitiveParameterParser {
|
|
9
|
+
/**
|
|
10
|
+
* Parses length-prefixed primitive parameter records.
|
|
11
|
+
* @param {Uint8Array | ArrayBuffer | undefined} dataBytes
|
|
12
|
+
* @returns {{ groups: { primitiveId: string, id: string, appurtenance: string, variantGuid: string, declaredCount: number | null, parameters: Record<string, string>, records: Record<string, string>[] }[], byPrimitiveId: Record<string, Record<string, string>> }}
|
|
13
|
+
*/
|
|
14
|
+
static parse(dataBytes) {
|
|
15
|
+
const bytes = PcbPrimitiveParameterParser.#toUint8Array(dataBytes)
|
|
16
|
+
const groups = []
|
|
17
|
+
const groupsByPrimitiveId = new Map()
|
|
18
|
+
let currentGroup = null
|
|
19
|
+
let offset = 0
|
|
20
|
+
|
|
21
|
+
while (offset + 4 <= bytes.byteLength) {
|
|
22
|
+
const recordLength = PcbPrimitiveParameterParser.#readUint32(
|
|
23
|
+
bytes,
|
|
24
|
+
offset
|
|
25
|
+
)
|
|
26
|
+
offset += 4
|
|
27
|
+
|
|
28
|
+
if (recordLength < 0 || offset + recordLength > bytes.byteLength) {
|
|
29
|
+
break
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const recordBytes = bytes.subarray(offset, offset + recordLength)
|
|
33
|
+
offset += recordLength
|
|
34
|
+
|
|
35
|
+
const fields =
|
|
36
|
+
PcbPrimitiveParameterParser.#parseRecordFields(recordBytes)
|
|
37
|
+
if (!Object.keys(fields).length) {
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const primitiveId = fields.PRIMITIVEID || ''
|
|
42
|
+
if (primitiveId) {
|
|
43
|
+
currentGroup = PcbPrimitiveParameterParser.#groupForPrimitiveId(
|
|
44
|
+
primitiveId,
|
|
45
|
+
fields,
|
|
46
|
+
groups,
|
|
47
|
+
groupsByPrimitiveId
|
|
48
|
+
)
|
|
49
|
+
currentGroup.records.push(fields)
|
|
50
|
+
continue
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (currentGroup) {
|
|
54
|
+
PcbPrimitiveParameterParser.#appendParameterRecord(
|
|
55
|
+
currentGroup,
|
|
56
|
+
fields
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
groups,
|
|
63
|
+
byPrimitiveId:
|
|
64
|
+
PcbPrimitiveParameterParser.#buildPrimitiveParameterLookup(
|
|
65
|
+
groups
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Returns an existing group or creates one for a primitive unique ID.
|
|
72
|
+
* @param {string} primitiveId
|
|
73
|
+
* @param {Record<string, string>} fields
|
|
74
|
+
* @param {{ primitiveId: string, id: string, appurtenance: string, variantGuid: string, declaredCount: number | null, parameters: Record<string, string>, records: Record<string, string>[] }[]} groups
|
|
75
|
+
* @param {Map<string, { primitiveId: string, id: string, appurtenance: string, variantGuid: string, declaredCount: number | null, parameters: Record<string, string>, records: Record<string, string>[] }>} groupsByPrimitiveId
|
|
76
|
+
* @returns {{ primitiveId: string, id: string, appurtenance: string, variantGuid: string, declaredCount: number | null, parameters: Record<string, string>, records: Record<string, string>[] }}
|
|
77
|
+
*/
|
|
78
|
+
static #groupForPrimitiveId(
|
|
79
|
+
primitiveId,
|
|
80
|
+
fields,
|
|
81
|
+
groups,
|
|
82
|
+
groupsByPrimitiveId
|
|
83
|
+
) {
|
|
84
|
+
if (!groupsByPrimitiveId.has(primitiveId)) {
|
|
85
|
+
const group = {
|
|
86
|
+
primitiveId,
|
|
87
|
+
id: '',
|
|
88
|
+
appurtenance: '',
|
|
89
|
+
variantGuid: '',
|
|
90
|
+
declaredCount: null,
|
|
91
|
+
parameters: {},
|
|
92
|
+
records: []
|
|
93
|
+
}
|
|
94
|
+
groupsByPrimitiveId.set(primitiveId, group)
|
|
95
|
+
groups.push(group)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const group = groupsByPrimitiveId.get(primitiveId)
|
|
99
|
+
PcbPrimitiveParameterParser.#mergeGroupFields(group, fields)
|
|
100
|
+
|
|
101
|
+
return group
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Merges group-level metadata fields into one primitive parameter group.
|
|
106
|
+
* @param {{ id: string, appurtenance: string, variantGuid: string, declaredCount: number | null }} group
|
|
107
|
+
* @param {Record<string, string>} fields
|
|
108
|
+
*/
|
|
109
|
+
static #mergeGroupFields(group, fields) {
|
|
110
|
+
group.id = fields.ID || group.id
|
|
111
|
+
group.appurtenance = fields.APPURTENANCE || group.appurtenance
|
|
112
|
+
group.variantGuid = fields.VARIANTGUID || group.variantGuid
|
|
113
|
+
|
|
114
|
+
const declaredCount = Number(fields.COUNT)
|
|
115
|
+
if (Number.isInteger(declaredCount) && declaredCount > 0) {
|
|
116
|
+
group.declaredCount = declaredCount
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Appends one name/value parameter record to a group.
|
|
122
|
+
* @param {{ parameters: Record<string, string>, records: Record<string, string>[] }} group
|
|
123
|
+
* @param {Record<string, string>} fields
|
|
124
|
+
*/
|
|
125
|
+
static #appendParameterRecord(group, fields) {
|
|
126
|
+
const name = fields.NAME || ''
|
|
127
|
+
|
|
128
|
+
if (name) {
|
|
129
|
+
group.parameters[name] = fields.VALUE || ''
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
group.records.push(fields)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Builds a plain-object lookup keyed by primitive unique ID.
|
|
137
|
+
* @param {{ primitiveId: string, parameters: Record<string, string> }[]} groups
|
|
138
|
+
* @returns {Record<string, Record<string, string>>}
|
|
139
|
+
*/
|
|
140
|
+
static #buildPrimitiveParameterLookup(groups) {
|
|
141
|
+
const byPrimitiveId = {}
|
|
142
|
+
|
|
143
|
+
for (const group of groups) {
|
|
144
|
+
byPrimitiveId[group.primitiveId] = { ...group.parameters }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return byPrimitiveId
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Parses one pipe-delimited primitive parameter record.
|
|
152
|
+
* @param {Uint8Array} bytes
|
|
153
|
+
* @returns {Record<string, string>}
|
|
154
|
+
*/
|
|
155
|
+
static #parseRecordFields(bytes) {
|
|
156
|
+
const text = new TextDecoder()
|
|
157
|
+
.decode(bytes)
|
|
158
|
+
.replace(/\u0000/gu, '')
|
|
159
|
+
.replace(/\r\n?/gu, '\n')
|
|
160
|
+
.trim()
|
|
161
|
+
const fields = {}
|
|
162
|
+
|
|
163
|
+
for (const segment of text.split('|')) {
|
|
164
|
+
const candidate = segment.trim()
|
|
165
|
+
const separatorIndex = candidate.indexOf('=')
|
|
166
|
+
|
|
167
|
+
if (separatorIndex <= 0) {
|
|
168
|
+
continue
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const key = candidate.slice(0, separatorIndex).trim()
|
|
172
|
+
if (!key) {
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
fields[key] = candidate.slice(separatorIndex + 1).trim()
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return fields
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Reads one little-endian unsigned integer from a byte view.
|
|
184
|
+
* @param {Uint8Array} bytes
|
|
185
|
+
* @param {number} offset
|
|
186
|
+
* @returns {number}
|
|
187
|
+
*/
|
|
188
|
+
static #readUint32(bytes, offset) {
|
|
189
|
+
return new DataView(
|
|
190
|
+
bytes.buffer,
|
|
191
|
+
bytes.byteOffset + offset,
|
|
192
|
+
4
|
|
193
|
+
).getUint32(0, true)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Normalizes one byte-like input into a Uint8Array view.
|
|
198
|
+
* @param {Uint8Array | ArrayBuffer | undefined} bytes
|
|
199
|
+
* @returns {Uint8Array}
|
|
200
|
+
*/
|
|
201
|
+
static #toUint8Array(bytes) {
|
|
202
|
+
if (!bytes) {
|
|
203
|
+
return new Uint8Array(0)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (bytes instanceof Uint8Array) {
|
|
207
|
+
return bytes
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return new Uint8Array(bytes)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Splits mixed-format PCB primitive streams.
|
|
7
|
+
*/
|
|
8
|
+
export class PcbPrimitiveRecordSlicer {
|
|
9
|
+
/**
|
|
10
|
+
* Splits a primitive stream, preferring object-id/length-prefixed records
|
|
11
|
+
* and falling back to legacy fixed-layout records.
|
|
12
|
+
* @param {{ headerBytes: Uint8Array | ArrayBuffer, dataBytes: Uint8Array | ArrayBuffer, objectId: number, fixedRecordByteLength: number, minimumPayloadByteLength: number, lengthPrefixedView?: 'payload' | 'record' }} options
|
|
13
|
+
* @returns {{ view: DataView, viewBytes: Uint8Array, recordBytes: Uint8Array, offset: number, byteLength: number, payloadByteLength: number | null, encoding: 'length-prefixed' | 'fixed', objectId: number | null, recordIndex: number }[]}
|
|
14
|
+
*/
|
|
15
|
+
static slicePrimitiveRecords(options) {
|
|
16
|
+
const lengthPrefixedRecords =
|
|
17
|
+
PcbPrimitiveRecordSlicer.#sliceLengthPrefixedRecords(options)
|
|
18
|
+
|
|
19
|
+
if (lengthPrefixedRecords.length) {
|
|
20
|
+
return lengthPrefixedRecords
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return PcbPrimitiveRecordSlicer.#sliceFixedRecords(
|
|
24
|
+
options.headerBytes,
|
|
25
|
+
options.dataBytes,
|
|
26
|
+
options.fixedRecordByteLength
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Splits object-id/length-prefixed records when the full stream matches
|
|
32
|
+
* the expected object id, count, payload lengths, and byte length exactly.
|
|
33
|
+
* @param {{ headerBytes: Uint8Array | ArrayBuffer, dataBytes: Uint8Array | ArrayBuffer, objectId: number, minimumPayloadByteLength: number, lengthPrefixedView?: 'payload' | 'record' }} options
|
|
34
|
+
* @returns {{ view: DataView, viewBytes: Uint8Array, recordBytes: Uint8Array, offset: number, byteLength: number, payloadByteLength: number | null, encoding: 'length-prefixed' | 'fixed', objectId: number | null, recordIndex: number }[]}
|
|
35
|
+
*/
|
|
36
|
+
static #sliceLengthPrefixedRecords(options) {
|
|
37
|
+
const normalizedData = PcbPrimitiveRecordSlicer.#toUint8Array(
|
|
38
|
+
options.dataBytes
|
|
39
|
+
)
|
|
40
|
+
const count = PcbPrimitiveRecordSlicer.#readRecordCount(
|
|
41
|
+
options.headerBytes
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if (!count) {
|
|
45
|
+
return []
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let offset = 0
|
|
49
|
+
const records = []
|
|
50
|
+
|
|
51
|
+
for (let index = 0; index < count; index += 1) {
|
|
52
|
+
const record = PcbPrimitiveRecordSlicer.#readLengthPrefixedRecordAt(
|
|
53
|
+
normalizedData,
|
|
54
|
+
offset,
|
|
55
|
+
options
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if (!record) {
|
|
59
|
+
return []
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
records.push({ ...record, recordIndex: index })
|
|
63
|
+
offset += record.byteLength
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return offset === normalizedData.byteLength ? records : []
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Reads one length-prefixed record at a byte offset.
|
|
71
|
+
* @param {Uint8Array} bytes
|
|
72
|
+
* @param {number} offset
|
|
73
|
+
* @param {{ objectId: number, minimumPayloadByteLength: number, lengthPrefixedView?: 'payload' | 'record' }} options
|
|
74
|
+
* @returns {{ view: DataView, viewBytes: Uint8Array, recordBytes: Uint8Array, offset: number, byteLength: number, payloadByteLength: number | null, encoding: 'length-prefixed', objectId: number, recordIndex: number } | null}
|
|
75
|
+
*/
|
|
76
|
+
static #readLengthPrefixedRecordAt(bytes, offset, options) {
|
|
77
|
+
if (
|
|
78
|
+
offset + 5 > bytes.byteLength ||
|
|
79
|
+
bytes[offset] !== options.objectId
|
|
80
|
+
) {
|
|
81
|
+
return null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const payloadLength = PcbPrimitiveRecordSlicer.#readUint32FromBytes(
|
|
85
|
+
bytes,
|
|
86
|
+
offset + 1
|
|
87
|
+
)
|
|
88
|
+
const byteLength = 5 + payloadLength
|
|
89
|
+
|
|
90
|
+
if (
|
|
91
|
+
payloadLength < options.minimumPayloadByteLength ||
|
|
92
|
+
offset + byteLength > bytes.byteLength
|
|
93
|
+
) {
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const viewOffset =
|
|
98
|
+
options.lengthPrefixedView === 'record' ? offset : offset + 5
|
|
99
|
+
const viewByteLength =
|
|
100
|
+
options.lengthPrefixedView === 'record' ? byteLength : payloadLength
|
|
101
|
+
|
|
102
|
+
return PcbPrimitiveRecordSlicer.#createRecord(
|
|
103
|
+
bytes,
|
|
104
|
+
offset,
|
|
105
|
+
viewOffset,
|
|
106
|
+
viewByteLength,
|
|
107
|
+
byteLength,
|
|
108
|
+
{
|
|
109
|
+
encoding: 'length-prefixed',
|
|
110
|
+
objectId: options.objectId,
|
|
111
|
+
payloadByteLength: payloadLength
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Splits one legacy fixed-layout primitive stream into record views.
|
|
118
|
+
* @param {Uint8Array | ArrayBuffer} headerBytes
|
|
119
|
+
* @param {Uint8Array | ArrayBuffer} dataBytes
|
|
120
|
+
* @param {number} recordByteLength
|
|
121
|
+
* @returns {{ view: DataView, viewBytes: Uint8Array, recordBytes: Uint8Array, offset: number, byteLength: number, payloadByteLength: number | null, encoding: 'fixed', objectId: null, recordIndex: number }[]}
|
|
122
|
+
*/
|
|
123
|
+
static #sliceFixedRecords(headerBytes, dataBytes, recordByteLength) {
|
|
124
|
+
const normalizedData = PcbPrimitiveRecordSlicer.#toUint8Array(dataBytes)
|
|
125
|
+
const count = PcbPrimitiveRecordSlicer.#readRecordCount(headerBytes)
|
|
126
|
+
|
|
127
|
+
if (!count) {
|
|
128
|
+
return []
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (normalizedData.byteLength < count * recordByteLength) {
|
|
132
|
+
return []
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const records = []
|
|
136
|
+
|
|
137
|
+
for (let index = 0; index < count; index += 1) {
|
|
138
|
+
records.push(
|
|
139
|
+
PcbPrimitiveRecordSlicer.#createRecord(
|
|
140
|
+
normalizedData,
|
|
141
|
+
index * recordByteLength,
|
|
142
|
+
index * recordByteLength,
|
|
143
|
+
recordByteLength,
|
|
144
|
+
recordByteLength,
|
|
145
|
+
{
|
|
146
|
+
encoding: 'fixed',
|
|
147
|
+
objectId: null,
|
|
148
|
+
payloadByteLength: null,
|
|
149
|
+
recordIndex: index
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return records
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Creates one record view tuple over a source byte array.
|
|
160
|
+
* @param {Uint8Array} bytes
|
|
161
|
+
* @param {number} recordOffset
|
|
162
|
+
* @param {number} viewOffset
|
|
163
|
+
* @param {number} viewByteLength
|
|
164
|
+
* @param {number} byteLength
|
|
165
|
+
* @param {{ encoding: 'length-prefixed' | 'fixed', objectId: number | null, payloadByteLength: number | null, recordIndex?: number }} metadata
|
|
166
|
+
* @returns {{ view: DataView, viewBytes: Uint8Array, recordBytes: Uint8Array, offset: number, byteLength: number, payloadByteLength: number | null, encoding: 'length-prefixed' | 'fixed', objectId: number | null, recordIndex: number }}
|
|
167
|
+
*/
|
|
168
|
+
static #createRecord(
|
|
169
|
+
bytes,
|
|
170
|
+
recordOffset,
|
|
171
|
+
viewOffset,
|
|
172
|
+
viewByteLength,
|
|
173
|
+
byteLength,
|
|
174
|
+
metadata
|
|
175
|
+
) {
|
|
176
|
+
return {
|
|
177
|
+
view: new DataView(
|
|
178
|
+
bytes.buffer,
|
|
179
|
+
bytes.byteOffset + viewOffset,
|
|
180
|
+
viewByteLength
|
|
181
|
+
),
|
|
182
|
+
viewBytes: new Uint8Array(
|
|
183
|
+
bytes.buffer,
|
|
184
|
+
bytes.byteOffset + viewOffset,
|
|
185
|
+
viewByteLength
|
|
186
|
+
),
|
|
187
|
+
recordBytes: new Uint8Array(
|
|
188
|
+
bytes.buffer,
|
|
189
|
+
bytes.byteOffset + recordOffset,
|
|
190
|
+
byteLength
|
|
191
|
+
),
|
|
192
|
+
offset: recordOffset,
|
|
193
|
+
byteLength,
|
|
194
|
+
payloadByteLength: metadata.payloadByteLength,
|
|
195
|
+
encoding: metadata.encoding,
|
|
196
|
+
objectId: metadata.objectId,
|
|
197
|
+
recordIndex: metadata.recordIndex ?? 0
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Reads one primitive record count from a stream header.
|
|
203
|
+
* @param {Uint8Array | ArrayBuffer} headerBytes
|
|
204
|
+
* @returns {number}
|
|
205
|
+
*/
|
|
206
|
+
static #readRecordCount(headerBytes) {
|
|
207
|
+
const normalizedHeader =
|
|
208
|
+
PcbPrimitiveRecordSlicer.#toUint8Array(headerBytes)
|
|
209
|
+
|
|
210
|
+
if (normalizedHeader.byteLength < 4) {
|
|
211
|
+
return 0
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return new DataView(
|
|
215
|
+
normalizedHeader.buffer,
|
|
216
|
+
normalizedHeader.byteOffset,
|
|
217
|
+
4
|
|
218
|
+
).getUint32(0, true)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Reads one little-endian unsigned 32-bit value from a byte array.
|
|
223
|
+
* @param {Uint8Array} bytes
|
|
224
|
+
* @param {number} offset
|
|
225
|
+
* @returns {number}
|
|
226
|
+
*/
|
|
227
|
+
static #readUint32FromBytes(bytes, offset) {
|
|
228
|
+
return new DataView(
|
|
229
|
+
bytes.buffer,
|
|
230
|
+
bytes.byteOffset + offset,
|
|
231
|
+
4
|
|
232
|
+
).getUint32(0, true)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Normalizes a byte source to Uint8Array.
|
|
237
|
+
* @param {Uint8Array | ArrayBuffer} bytes
|
|
238
|
+
* @returns {Uint8Array}
|
|
239
|
+
*/
|
|
240
|
+
static #toUint8Array(bytes) {
|
|
241
|
+
return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes)
|
|
242
|
+
}
|
|
243
|
+
}
|