altium-toolkit 0.1.1 → 0.1.17

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.
Files changed (54) hide show
  1. package/README.md +25 -6
  2. package/docs/api.md +42 -4
  3. package/docs/model-format.md +95 -5
  4. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +553 -0
  5. package/docs/testing.md +7 -2
  6. package/package.json +6 -2
  7. package/spec/library-scope.md +7 -1
  8. package/src/core/altium/AltiumParser.mjs +22 -325
  9. package/src/core/altium/NormalizedModelSchema.mjs +28 -0
  10. package/src/core/altium/PcbArcPrimitiveParser.mjs +87 -0
  11. package/src/core/altium/PcbBinaryPrimitiveParser.mjs +43 -370
  12. package/src/core/altium/PcbBoardRegionSemanticsParser.mjs +477 -0
  13. package/src/core/altium/PcbComponentAnnotationNormalizer.mjs +290 -0
  14. package/src/core/altium/PcbComponentBodyPlacementNormalizer.mjs +52 -0
  15. package/src/core/altium/PcbComponentPrimitiveIndexer.mjs +109 -0
  16. package/src/core/altium/PcbEmbeddedFontExtractor.mjs +484 -0
  17. package/src/core/altium/PcbFillPrimitiveParser.mjs +84 -0
  18. package/src/core/altium/PcbFontMetricsParser.mjs +308 -0
  19. package/src/core/altium/PcbGeometryFlipper.mjs +244 -0
  20. package/src/core/altium/PcbLayerIdCodec.mjs +136 -0
  21. package/src/core/altium/PcbLibModelParser.mjs +202 -0
  22. package/src/core/altium/PcbLibStreamExtractor.mjs +968 -0
  23. package/src/core/altium/PcbModelParser.mjs +618 -66
  24. package/src/core/altium/PcbOutlineRecovery.mjs +4 -112
  25. package/src/core/altium/PcbPadPrimitiveParser.mjs +347 -0
  26. package/src/core/altium/PcbPadShapeCodec.mjs +158 -0
  27. package/src/core/altium/PcbPadStackParser.mjs +903 -0
  28. package/src/core/altium/PcbPrimitiveOwnershipIndexParser.mjs +60 -0
  29. package/src/core/altium/PcbPrimitiveParameterParser.mjs +212 -0
  30. package/src/core/altium/PcbPrimitiveRecordSlicer.mjs +243 -0
  31. package/src/core/altium/PcbRawRecordRegistry.mjs +831 -0
  32. package/src/core/altium/PcbRegionPrimitiveParser.mjs +317 -0
  33. package/src/core/altium/PcbRuleParser.mjs +587 -0
  34. package/src/core/altium/PcbStreamExtractor.mjs +127 -4
  35. package/src/core/altium/PcbTextPrimitiveParser.mjs +537 -0
  36. package/src/core/altium/PcbTrackPrimitiveParser.mjs +87 -0
  37. package/src/core/altium/PcbViaPrimitiveParser.mjs +88 -0
  38. package/src/core/altium/PcbViaStackParser.mjs +548 -0
  39. package/src/core/altium/PcbWideStringTableParser.mjs +108 -0
  40. package/src/core/altium/PrjPcbModelParser.mjs +797 -0
  41. package/src/core/altium/SchematicComponentTextResolver.mjs +355 -0
  42. package/src/parser.mjs +13 -0
  43. package/src/renderers.mjs +5 -0
  44. package/src/styles/altium-renderers.css +11 -6
  45. package/src/ui/PcbCopperPrimitiveSplitter.mjs +113 -0
  46. package/src/ui/PcbEdgeFacingGlyphNormalizer.mjs +6 -5
  47. package/src/ui/PcbEmbeddedFontFaceRenderer.mjs +126 -0
  48. package/src/ui/PcbFootprintPrimitiveSelector.mjs +27 -6
  49. package/src/ui/PcbRegionPrimitiveRenderer.mjs +243 -0
  50. package/src/ui/PcbSideResolvedRenderModel.mjs +336 -0
  51. package/src/ui/PcbSvgRenderer.mjs +101 -109
  52. package/src/ui/PcbTextPrimitiveRenderer.mjs +252 -0
  53. package/src/ui/SchematicSheetChromeRenderer.mjs +2 -93
  54. package/src/ui/SchematicSheetZoneRenderer.mjs +104 -0
@@ -0,0 +1,484 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { unzlibSync } from 'fflate'
6
+ import { PcbFontMetricsParser } from './PcbFontMetricsParser.mjs'
7
+
8
+ /**
9
+ * Extracts zlib-compressed embedded font payloads from PCB compound streams.
10
+ */
11
+ export class PcbEmbeddedFontExtractor {
12
+ static #CANDIDATE_STREAM_NAMES = [
13
+ 'EmbeddedFonts6/Data',
14
+ 'EmbeddedFonts/Data',
15
+ 'Library/EmbeddedFonts'
16
+ ]
17
+
18
+ /**
19
+ * Extracts embedded fonts from known PcbDoc and PcbLib stream names.
20
+ * @param {Map<string, Uint8Array>} streams
21
+ * @returns {{ fonts: { index: number, name: string, style: string, fileName: string, sourceStream: string, format: string, mimeType: string, byteCount: number, compressedByteCount: number, payloadBase64: string, metrics: Record<string, number | string> }[] }}
22
+ */
23
+ static extractFromStreams(streams) {
24
+ const fonts = []
25
+
26
+ for (const streamName of PcbEmbeddedFontExtractor
27
+ .#CANDIDATE_STREAM_NAMES) {
28
+ const bytes = streams.get(streamName)
29
+ if (!(bytes instanceof Uint8Array) || !bytes.byteLength) {
30
+ continue
31
+ }
32
+
33
+ fonts.push(
34
+ ...PcbEmbeddedFontExtractor.extractFromBytes(bytes, streamName)
35
+ )
36
+ }
37
+
38
+ return {
39
+ fonts: PcbEmbeddedFontExtractor.#dedupeFonts(fonts).map(
40
+ (font, index) => ({
41
+ ...font,
42
+ index
43
+ })
44
+ )
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Extracts embedded fonts from one raw EmbeddedFonts stream.
50
+ * @param {Uint8Array | ArrayBuffer} bytes
51
+ * @param {string} [sourceStream]
52
+ * @returns {{ index: number, name: string, style: string, fileName: string, sourceStream: string, format: string, mimeType: string, byteCount: number, compressedByteCount: number, payloadBase64: string, metrics: Record<string, number | string> }[]}
53
+ */
54
+ static extractFromBytes(bytes, sourceStream = 'EmbeddedFonts6/Data') {
55
+ const normalizedBytes = PcbEmbeddedFontExtractor.#toUint8Array(bytes)
56
+ const fonts = []
57
+ let offset = 0
58
+
59
+ while (offset < normalizedBytes.byteLength) {
60
+ const record = PcbEmbeddedFontExtractor.#readFontRecordAt(
61
+ normalizedBytes,
62
+ offset,
63
+ sourceStream,
64
+ fonts.length
65
+ )
66
+
67
+ if (!record) {
68
+ break
69
+ }
70
+
71
+ fonts.push(record.font)
72
+ offset = record.nextOffset
73
+ }
74
+
75
+ return fonts
76
+ }
77
+
78
+ /**
79
+ * Reads one embedded-font record at the current stream offset.
80
+ * @param {Uint8Array} bytes
81
+ * @param {number} offset
82
+ * @param {string} sourceStream
83
+ * @param {number} index
84
+ * @returns {{ font: { index: number, name: string, style: string, fileName: string, sourceStream: string, format: string, mimeType: string, byteCount: number, compressedByteCount: number, payloadBase64: string, metrics: Record<string, number | string> }, nextOffset: number } | null}
85
+ */
86
+ static #readFontRecordAt(bytes, offset, sourceStream, index) {
87
+ const familyField = PcbEmbeddedFontExtractor.#readUtf16FieldAt(
88
+ bytes,
89
+ offset
90
+ )
91
+ if (!familyField) {
92
+ return null
93
+ }
94
+
95
+ const alternateField = PcbEmbeddedFontExtractor.#readUtf16FieldAt(
96
+ bytes,
97
+ familyField.nextOffset
98
+ )
99
+ const styleField = alternateField
100
+ ? PcbEmbeddedFontExtractor.#readUtf16FieldAt(
101
+ bytes,
102
+ alternateField.nextOffset
103
+ )
104
+ : null
105
+ if (!alternateField || !styleField) {
106
+ return null
107
+ }
108
+
109
+ const zlibOffset = PcbEmbeddedFontExtractor.#findZlibHeaderOffset(
110
+ bytes,
111
+ styleField.nextOffset
112
+ )
113
+ if (zlibOffset < 0) {
114
+ return null
115
+ }
116
+
117
+ const compressedEnd = PcbEmbeddedFontExtractor.#findCompressedEnd(
118
+ bytes,
119
+ zlibOffset
120
+ )
121
+ if (compressedEnd <= zlibOffset) {
122
+ return null
123
+ }
124
+
125
+ const compressedBytes = bytes.subarray(zlibOffset, compressedEnd)
126
+ const payloadBytes = unzlibSync(compressedBytes)
127
+ const metadata = PcbEmbeddedFontExtractor.#normalizeFontMetadata(
128
+ familyField.text,
129
+ alternateField.text,
130
+ styleField.text
131
+ )
132
+ const metrics = PcbFontMetricsParser.parse(payloadBytes)
133
+
134
+ return {
135
+ font: {
136
+ index,
137
+ name: metadata.name,
138
+ style: metadata.style,
139
+ fileName: PcbEmbeddedFontExtractor.#buildFileName(
140
+ metadata.name,
141
+ metadata.style
142
+ ),
143
+ sourceStream,
144
+ format: metrics.format || 'unknown',
145
+ mimeType: PcbEmbeddedFontExtractor.#resolveMimeType(
146
+ metrics.format
147
+ ),
148
+ byteCount: payloadBytes.byteLength,
149
+ compressedByteCount: compressedBytes.byteLength,
150
+ payloadBase64:
151
+ PcbEmbeddedFontExtractor.#bytesToBase64(payloadBytes),
152
+ metrics
153
+ },
154
+ nextOffset: compressedEnd
155
+ }
156
+ }
157
+
158
+ /**
159
+ * Reads one little-endian length-prefixed UTF-16LE string field.
160
+ * @param {Uint8Array} bytes
161
+ * @param {number} offset
162
+ * @returns {{ text: string, nextOffset: number } | null}
163
+ */
164
+ static #readUtf16FieldAt(bytes, offset) {
165
+ if (offset + 4 > bytes.byteLength) {
166
+ return null
167
+ }
168
+
169
+ const byteLength = new DataView(
170
+ bytes.buffer,
171
+ bytes.byteOffset + offset,
172
+ 4
173
+ ).getUint32(0, true)
174
+ const textOffset = offset + 4
175
+ const textEnd = textOffset + byteLength
176
+
177
+ if (byteLength < 0 || textEnd > bytes.byteLength) {
178
+ return null
179
+ }
180
+
181
+ return {
182
+ text: new TextDecoder('utf-16le')
183
+ .decode(bytes.subarray(textOffset, textEnd))
184
+ .replace(/\u0000+$/gu, '')
185
+ .trim(),
186
+ nextOffset: textEnd
187
+ }
188
+ }
189
+
190
+ /**
191
+ * Finds a zlib stream header shortly after the font metadata fields.
192
+ * @param {Uint8Array} bytes
193
+ * @param {number} offset
194
+ * @returns {number}
195
+ */
196
+ static #findZlibHeaderOffset(bytes, offset) {
197
+ const end = Math.min(bytes.byteLength - 1, offset + 256)
198
+
199
+ for (let cursor = offset; cursor < end; cursor += 1) {
200
+ if (
201
+ PcbEmbeddedFontExtractor.#isLikelyZlibHeader(
202
+ bytes[cursor],
203
+ bytes[cursor + 1]
204
+ )
205
+ ) {
206
+ return cursor
207
+ }
208
+ }
209
+
210
+ return -1
211
+ }
212
+
213
+ /**
214
+ * Returns true when two bytes look like a zlib header.
215
+ * @param {number} compressionMethod
216
+ * @param {number} flags
217
+ * @returns {boolean}
218
+ */
219
+ static #isLikelyZlibHeader(compressionMethod, flags) {
220
+ return (
221
+ (Number(compressionMethod) & 0x0f) === 8 &&
222
+ ((Number(compressionMethod) << 8) + Number(flags)) % 31 === 0
223
+ )
224
+ }
225
+
226
+ /**
227
+ * Finds the smallest trailing offset that fully contains a zlib payload.
228
+ * @param {Uint8Array} bytes
229
+ * @param {number} zlibOffset
230
+ * @returns {number}
231
+ */
232
+ static #findCompressedEnd(bytes, zlibOffset) {
233
+ let low = zlibOffset + 2
234
+ let high = bytes.byteLength
235
+
236
+ while (low < high) {
237
+ const midpoint = Math.floor((low + high) / 2)
238
+ if (
239
+ PcbEmbeddedFontExtractor.#canInflate(
240
+ bytes.subarray(zlibOffset, midpoint)
241
+ )
242
+ ) {
243
+ high = midpoint
244
+ } else {
245
+ low = midpoint + 1
246
+ }
247
+ }
248
+
249
+ return PcbEmbeddedFontExtractor.#canInflate(
250
+ bytes.subarray(zlibOffset, low)
251
+ )
252
+ ? low
253
+ : -1
254
+ }
255
+
256
+ /**
257
+ * Returns true when one byte slice can be inflated as a complete zlib
258
+ * stream.
259
+ * @param {Uint8Array} bytes
260
+ * @returns {boolean}
261
+ */
262
+ static #canInflate(bytes) {
263
+ try {
264
+ unzlibSync(bytes)
265
+ return true
266
+ } catch {
267
+ return false
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Normalizes family/style metadata from the embedded-font record fields.
273
+ * @param {string} family
274
+ * @param {string} alternateFamily
275
+ * @param {string} explicitStyle
276
+ * @returns {{ name: string, style: string }}
277
+ */
278
+ static #normalizeFontMetadata(family, alternateFamily, explicitStyle) {
279
+ const inferredStyle =
280
+ PcbEmbeddedFontExtractor.#normalizeStyle(explicitStyle) ||
281
+ PcbEmbeddedFontExtractor.#inferStyle([family, alternateFamily])
282
+ const alternate = String(alternateFamily || '').trim()
283
+ const baseFamily = String(family || alternate || 'Embedded Font').trim()
284
+ const name =
285
+ alternate &&
286
+ inferredStyle !== 'Regular' &&
287
+ PcbEmbeddedFontExtractor.#nameContainsStyle(
288
+ baseFamily,
289
+ inferredStyle
290
+ )
291
+ ? alternate
292
+ : baseFamily
293
+
294
+ return {
295
+ name: PcbEmbeddedFontExtractor.#trimStyleSuffix(
296
+ name,
297
+ inferredStyle
298
+ ),
299
+ style: inferredStyle
300
+ }
301
+ }
302
+
303
+ /**
304
+ * Normalizes a style field into one of the public style labels.
305
+ * @param {string} style
306
+ * @returns {'Regular' | 'Bold' | 'Italic' | 'Bold Italic' | ''}
307
+ */
308
+ static #normalizeStyle(style) {
309
+ const normalized = String(style || '')
310
+ .trim()
311
+ .toLowerCase()
312
+
313
+ if (!normalized || normalized === 'regular') {
314
+ return ''
315
+ }
316
+
317
+ const isBold = normalized.includes('bold')
318
+ const isItalic =
319
+ normalized.includes('italic') || normalized.includes('oblique')
320
+
321
+ if (isBold && isItalic) return 'Bold Italic'
322
+ if (isBold) return 'Bold'
323
+ if (isItalic) return 'Italic'
324
+
325
+ return 'Regular'
326
+ }
327
+
328
+ /**
329
+ * Infers a font style from family-name hints when the style field is empty.
330
+ * @param {string[]} names
331
+ * @returns {'Regular' | 'Bold' | 'Italic' | 'Bold Italic'}
332
+ */
333
+ static #inferStyle(names) {
334
+ const normalized = names.join(' ').toLowerCase()
335
+ const isBold = normalized.includes('bold')
336
+ const isItalic =
337
+ normalized.includes('italic') || normalized.includes('oblique')
338
+
339
+ if (isBold && isItalic) return 'Bold Italic'
340
+ if (isBold) return 'Bold'
341
+ if (isItalic) return 'Italic'
342
+
343
+ return 'Regular'
344
+ }
345
+
346
+ /**
347
+ * Returns true when a family name already carries the style suffix.
348
+ * @param {string} name
349
+ * @param {string} style
350
+ * @returns {boolean}
351
+ */
352
+ static #nameContainsStyle(name, style) {
353
+ const normalizedName = String(name || '').toLowerCase()
354
+ return String(style || '')
355
+ .toLowerCase()
356
+ .split(/\s+/u)
357
+ .every((part) => normalizedName.includes(part))
358
+ }
359
+
360
+ /**
361
+ * Removes an inferred style suffix from a family name.
362
+ * @param {string} name
363
+ * @param {string} style
364
+ * @returns {string}
365
+ */
366
+ static #trimStyleSuffix(name, style) {
367
+ if (style === 'Regular') {
368
+ return name
369
+ }
370
+
371
+ const pattern = new RegExp(
372
+ '\\s+' +
373
+ String(style || '')
374
+ .trim()
375
+ .replace(/\s+/gu, '\\s+') +
376
+ '$',
377
+ 'iu'
378
+ )
379
+
380
+ return String(name || 'Embedded Font')
381
+ .replace(pattern, '')
382
+ .trim()
383
+ }
384
+
385
+ /**
386
+ * Creates a stable public filename for an embedded font payload.
387
+ * @param {string} name
388
+ * @param {string} style
389
+ * @returns {string}
390
+ */
391
+ static #buildFileName(name, style) {
392
+ const suffix =
393
+ style && style !== 'Regular' ? '-' + style.replace(/\s+/gu, '') : ''
394
+ return (
395
+ String(name || 'Embedded Font')
396
+ .replace(/[\\/:*?"<>|]/gu, '_')
397
+ .trim() +
398
+ suffix +
399
+ '.ttf'
400
+ )
401
+ }
402
+
403
+ /**
404
+ * Resolves a browser-safe font MIME type.
405
+ * @param {string | undefined} format
406
+ * @returns {string}
407
+ */
408
+ static #resolveMimeType(format) {
409
+ if (format === 'opentype') {
410
+ return 'font/otf'
411
+ }
412
+
413
+ if (format === 'truetype') {
414
+ return 'font/ttf'
415
+ }
416
+
417
+ return 'application/octet-stream'
418
+ }
419
+
420
+ /**
421
+ * Deduplicates repeated font records.
422
+ * @param {{ name: string, style: string, compressedByteCount: number, payloadBase64: string }[]} fonts
423
+ * @returns {object[]}
424
+ */
425
+ static #dedupeFonts(fonts) {
426
+ const seenKeys = new Set()
427
+ const deduped = []
428
+
429
+ for (const font of fonts) {
430
+ const key = [
431
+ font.name,
432
+ font.style,
433
+ font.compressedByteCount,
434
+ font.payloadBase64
435
+ ].join('\u0000')
436
+
437
+ if (seenKeys.has(key)) {
438
+ continue
439
+ }
440
+
441
+ seenKeys.add(key)
442
+ deduped.push(font)
443
+ }
444
+
445
+ return deduped
446
+ }
447
+
448
+ /**
449
+ * Encodes bytes as base64 in both browser and Node runtimes.
450
+ * @param {Uint8Array} bytes
451
+ * @returns {string}
452
+ */
453
+ static #bytesToBase64(bytes) {
454
+ if (typeof btoa === 'function') {
455
+ let binary = ''
456
+ const chunkSize = 0x8000
457
+ for (
458
+ let offset = 0;
459
+ offset < bytes.byteLength;
460
+ offset += chunkSize
461
+ ) {
462
+ binary += String.fromCharCode(
463
+ ...bytes.subarray(offset, offset + chunkSize)
464
+ )
465
+ }
466
+ return btoa(binary)
467
+ }
468
+
469
+ return Buffer.from(bytes).toString('base64')
470
+ }
471
+
472
+ /**
473
+ * Normalizes byte-like input into a Uint8Array view.
474
+ * @param {Uint8Array | ArrayBuffer} bytes
475
+ * @returns {Uint8Array}
476
+ */
477
+ static #toUint8Array(bytes) {
478
+ if (bytes instanceof Uint8Array) {
479
+ return bytes
480
+ }
481
+
482
+ return new Uint8Array(bytes || new ArrayBuffer(0))
483
+ }
484
+ }
@@ -0,0 +1,84 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { PcbPrimitiveRecordSlicer } from './PcbPrimitiveRecordSlicer.mjs'
6
+ import { PcbPrimitiveOwnershipIndexParser } from './PcbPrimitiveOwnershipIndexParser.mjs'
7
+
8
+ /**
9
+ * Decodes Altium fill primitive streams.
10
+ */
11
+ export class PcbFillPrimitiveParser {
12
+ static #FILL_OBJECT_ID = 6
13
+
14
+ static #FILL_RECORD_BYTE_LENGTH = 55
15
+
16
+ static #FILL_PAYLOAD_MIN_BYTE_LENGTH = 50
17
+
18
+ /**
19
+ * Decodes one fill stream.
20
+ * @param {Uint8Array | ArrayBuffer} headerBytes
21
+ * @param {Uint8Array | ArrayBuffer} dataBytes
22
+ * @returns {{ x1: number, y1: number, x2: number, y2: number, componentIndex: number | null, netIndex: number | null, polygonIndex: number | null, layerCode: number, layerId: number }[]}
23
+ */
24
+ static parseFillStream(headerBytes, dataBytes) {
25
+ return PcbFillPrimitiveParser.#sliceFillRecords(
26
+ headerBytes,
27
+ dataBytes
28
+ ).map((view) => PcbFillPrimitiveParser.#parseFillRecord(view))
29
+ }
30
+
31
+ /**
32
+ * Decodes one fill record view into a normalized primitive.
33
+ * @param {DataView} view
34
+ * @returns {{ x1: number, y1: number, x2: number, y2: number, componentIndex: number | null, netIndex: number | null, polygonIndex: number | null, layerCode: number, layerId: number }}
35
+ */
36
+ static #parseFillRecord(view) {
37
+ const ownershipIndexes =
38
+ PcbPrimitiveOwnershipIndexParser.readOwnershipIndexes(view, {
39
+ component: 12,
40
+ net: 8,
41
+ polygon: 10
42
+ })
43
+
44
+ return {
45
+ x1: PcbFillPrimitiveParser.#readMil(view, 18),
46
+ y1: PcbFillPrimitiveParser.#readMil(view, 22),
47
+ x2: PcbFillPrimitiveParser.#readMil(view, 26),
48
+ y2: PcbFillPrimitiveParser.#readMil(view, 30),
49
+ ...ownershipIndexes,
50
+ layerCode: view.getUint16(46, true),
51
+ layerId: view.getUint8(5)
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Splits a fill stream into record views, preserving variable payload
57
+ * lengths when fills are stored with object-id and payload-length prefixes.
58
+ * @param {Uint8Array | ArrayBuffer} headerBytes
59
+ * @param {Uint8Array | ArrayBuffer} dataBytes
60
+ * @returns {DataView[]}
61
+ */
62
+ static #sliceFillRecords(headerBytes, dataBytes) {
63
+ return PcbPrimitiveRecordSlicer.slicePrimitiveRecords({
64
+ headerBytes,
65
+ dataBytes,
66
+ objectId: PcbFillPrimitiveParser.#FILL_OBJECT_ID,
67
+ fixedRecordByteLength:
68
+ PcbFillPrimitiveParser.#FILL_RECORD_BYTE_LENGTH,
69
+ minimumPayloadByteLength:
70
+ PcbFillPrimitiveParser.#FILL_PAYLOAD_MIN_BYTE_LENGTH,
71
+ lengthPrefixedView: 'record'
72
+ }).map((record) => record.view)
73
+ }
74
+
75
+ /**
76
+ * Reads one standard fixed-point mil value.
77
+ * @param {DataView} view
78
+ * @param {number} offset
79
+ * @returns {number}
80
+ */
81
+ static #readMil(view, offset) {
82
+ return view.getInt32(offset, true) / 10000
83
+ }
84
+ }