altium-toolkit 1.0.8 → 1.0.9

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 (88) hide show
  1. package/README.md +18 -6
  2. package/docs/api.md +78 -16
  3. package/docs/model-format.md +229 -8
  4. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +47 -0
  5. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1661 -104
  6. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +59 -0
  7. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +57 -0
  8. package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
  9. package/docs/testing.md +9 -3
  10. package/package.json +1 -1
  11. package/spec/library-scope.md +7 -1
  12. package/src/core/altium/AltiumLayoutParser.mjs +104 -8
  13. package/src/core/altium/AltiumParser.mjs +191 -45
  14. package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
  15. package/src/core/altium/IntLibModelParser.mjs +240 -0
  16. package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
  17. package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
  18. package/src/core/altium/LibrarySearchIndex.mjs +215 -0
  19. package/src/core/altium/NormalizedModelSchema.mjs +36 -0
  20. package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
  21. package/src/core/altium/PcbDefaultsParser.mjs +171 -0
  22. package/src/core/altium/PcbDimensionParser.mjs +229 -0
  23. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
  24. package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
  25. package/src/core/altium/PcbLibModelParser.mjs +235 -14
  26. package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
  27. package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
  28. package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
  29. package/src/core/altium/PcbModelParser.mjs +466 -28
  30. package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
  31. package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
  32. package/src/core/altium/PcbPadStackParser.mjs +58 -0
  33. package/src/core/altium/PcbPickPlacePositionResolver.mjs +217 -0
  34. package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
  35. package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
  36. package/src/core/altium/PcbRegionPrimitiveParser.mjs +5 -1
  37. package/src/core/altium/PcbRuleParser.mjs +354 -33
  38. package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
  39. package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
  40. package/src/core/altium/PcbStatisticsBuilder.mjs +532 -0
  41. package/src/core/altium/PcbStreamExtractor.mjs +111 -4
  42. package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
  43. package/src/core/altium/PcbUnionParser.mjs +307 -0
  44. package/src/core/altium/PcbViaStackParser.mjs +98 -10
  45. package/src/core/altium/PcbViaStructureParser.mjs +335 -0
  46. package/src/core/altium/PrintableTextDecoder.mjs +53 -3
  47. package/src/core/altium/PrjPcbModelParser.mjs +257 -5
  48. package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
  49. package/src/core/altium/ProjectDesignBundleBuilder.mjs +477 -0
  50. package/src/core/altium/ProjectNetlistExporter.mjs +499 -0
  51. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
  52. package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
  53. package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
  54. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
  55. package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
  56. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
  57. package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
  58. package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
  59. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
  60. package/src/core/altium/SchematicHarnessParser.mjs +302 -0
  61. package/src/core/altium/SchematicImageParser.mjs +474 -3
  62. package/src/core/altium/SchematicImplementationParser.mjs +518 -0
  63. package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
  64. package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
  65. package/src/core/altium/SchematicPinParser.mjs +84 -1
  66. package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
  67. package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
  68. package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
  69. package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
  70. package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
  71. package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
  72. package/src/core/altium/SchematicTemplateParser.mjs +256 -0
  73. package/src/core/altium/SchematicTextParser.mjs +123 -0
  74. package/src/core/ole/OleCompoundDocument.mjs +20 -0
  75. package/src/parser.mjs +29 -0
  76. package/src/styles/altium-renderers.css +19 -0
  77. package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
  78. package/src/ui/PcbInteractionIndex.mjs +9 -4
  79. package/src/ui/PcbScene3dBuilder.mjs +137 -3
  80. package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
  81. package/src/ui/PcbSvgRenderer.mjs +1187 -34
  82. package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
  83. package/src/ui/SchematicNoteRenderer.mjs +9 -2
  84. package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
  85. package/src/ui/SchematicShapeRenderer.mjs +362 -0
  86. package/src/ui/SchematicSvgRenderer.mjs +1442 -92
  87. package/src/ui/SchematicTypography.mjs +48 -5
  88. package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
@@ -0,0 +1,217 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ const DEFAULT_POSITION_MODE = 'altium-pick-place'
6
+ const COMPONENT_ORIGIN_MODE = 'component-origin'
7
+
8
+ /**
9
+ * Resolves PCB pick-and-place coordinates from component origins and owned pad
10
+ * anchors.
11
+ */
12
+ export class PcbPickPlacePositionResolver {
13
+ /**
14
+ * Builds the public PnP model with the default mode and alternatives.
15
+ * @param {{ componentIndex: number, designator: string, pattern: string, layer: string, rotation: number, x: number, y: number }[]} components
16
+ * @param {{ componentIndex: number, pads?: { x?: number, y?: number }[] }[]} componentPrimitiveGroups
17
+ * @param {{ sourceComponents?: { componentIndex: number, rotation?: number }[] }} [options] Resolver options.
18
+ * @returns {{ positionMode: string, entries: object[], modes: { componentOrigin: { positionMode: string, entries: object[] } } }}
19
+ */
20
+ static buildModel(components, componentPrimitiveGroups, options = {}) {
21
+ return {
22
+ positionMode: DEFAULT_POSITION_MODE,
23
+ entries: PcbPickPlacePositionResolver.buildEntries(
24
+ components,
25
+ componentPrimitiveGroups,
26
+ DEFAULT_POSITION_MODE,
27
+ options
28
+ ),
29
+ modes: {
30
+ componentOrigin: {
31
+ positionMode: COMPONENT_ORIGIN_MODE,
32
+ entries: PcbPickPlacePositionResolver.buildEntries(
33
+ components,
34
+ componentPrimitiveGroups,
35
+ COMPONENT_ORIGIN_MODE,
36
+ options
37
+ )
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Builds PnP entries for one coordinate mode.
45
+ * @param {{ componentIndex: number, designator: string, pattern: string, layer: string, rotation: number, x: number, y: number }[]} components
46
+ * @param {{ componentIndex: number, pads?: { x?: number, y?: number }[] }[]} componentPrimitiveGroups
47
+ * @param {'altium-pick-place' | 'component-origin' | string} mode
48
+ * @param {{ sourceComponents?: { componentIndex: number, rotation?: number }[] }} [options] Resolver options.
49
+ * @returns {{ designator: string, pattern: string, layer: string, rotation: number, x: number, y: number, componentOriginX: number, componentOriginY: number, padAnchorCount: number, positionSource: string }[]}
50
+ */
51
+ static buildEntries(
52
+ components,
53
+ componentPrimitiveGroups,
54
+ mode,
55
+ options = {}
56
+ ) {
57
+ const groupsByIndex = PcbPickPlacePositionResolver.#buildGroupLookup(
58
+ componentPrimitiveGroups
59
+ )
60
+ const sourceComponentsByIndex =
61
+ PcbPickPlacePositionResolver.#buildGroupLookup(
62
+ options.sourceComponents || []
63
+ )
64
+ const normalizedMode =
65
+ PcbPickPlacePositionResolver.normalizePositionMode(mode)
66
+
67
+ return (components || []).map((component) =>
68
+ PcbPickPlacePositionResolver.#buildEntry(
69
+ component,
70
+ groupsByIndex.get(Number(component.componentIndex)),
71
+ normalizedMode,
72
+ sourceComponentsByIndex.get(Number(component.componentIndex))
73
+ )
74
+ )
75
+ }
76
+
77
+ /**
78
+ * Normalizes one public PnP coordinate-mode token.
79
+ * @param {string | null | undefined} mode
80
+ * @returns {'altium-pick-place' | 'component-origin'}
81
+ */
82
+ static normalizePositionMode(mode) {
83
+ const normalized = String(mode || DEFAULT_POSITION_MODE)
84
+ .trim()
85
+ .toLowerCase()
86
+ .replace(/_/gu, '-')
87
+
88
+ if (
89
+ normalized === '' ||
90
+ normalized === 'altium' ||
91
+ normalized === 'altium-pick-place' ||
92
+ normalized === 'pick-place'
93
+ ) {
94
+ return DEFAULT_POSITION_MODE
95
+ }
96
+
97
+ if (
98
+ normalized === 'component-origin' ||
99
+ normalized === 'origin' ||
100
+ normalized === 'part-origin' ||
101
+ normalized === 'footprint-origin'
102
+ ) {
103
+ return COMPONENT_ORIGIN_MODE
104
+ }
105
+
106
+ return DEFAULT_POSITION_MODE
107
+ }
108
+
109
+ /**
110
+ * Builds one PnP entry.
111
+ * @param {{ designator: string, pattern: string, layer: string, rotation: number, x: number, y: number }} component
112
+ * @param {{ pads?: { x?: number, y?: number }[] } | undefined} group
113
+ * @param {'altium-pick-place' | 'component-origin'} mode
114
+ * @param {{ rotation?: number } | undefined} sourceComponent Source component row.
115
+ * @returns {{ designator: string, pattern: string, layer: string, rotation: number, x: number, y: number, componentOriginX: number, componentOriginY: number, padAnchorCount: number, positionSource: string }}
116
+ */
117
+ static #buildEntry(component, group, mode, sourceComponent) {
118
+ const componentOriginX = Number(component.x || 0)
119
+ const componentOriginY = Number(component.y || 0)
120
+ const rotation = Number.isFinite(Number(sourceComponent?.rotation))
121
+ ? Number(sourceComponent.rotation)
122
+ : Number(component.rotation || 0)
123
+ const padAnchors = PcbPickPlacePositionResolver.#padAnchors(group?.pads)
124
+ const padCenter =
125
+ mode === DEFAULT_POSITION_MODE
126
+ ? PcbPickPlacePositionResolver.#padAnchorBoundsCenter(
127
+ padAnchors
128
+ )
129
+ : null
130
+ const position = padCenter || {
131
+ x: componentOriginX,
132
+ y: componentOriginY,
133
+ source: 'component-origin'
134
+ }
135
+
136
+ return {
137
+ designator: component.designator || '',
138
+ pattern: component.pattern || '',
139
+ layer: component.layer || '',
140
+ rotation: PcbPickPlacePositionResolver.#roundCoordinate(rotation),
141
+ x: PcbPickPlacePositionResolver.#roundCoordinate(position.x),
142
+ y: PcbPickPlacePositionResolver.#roundCoordinate(position.y),
143
+ componentOriginX:
144
+ PcbPickPlacePositionResolver.#roundCoordinate(componentOriginX),
145
+ componentOriginY:
146
+ PcbPickPlacePositionResolver.#roundCoordinate(componentOriginY),
147
+ padAnchorCount: padAnchors.length,
148
+ positionSource:
149
+ mode === COMPONENT_ORIGIN_MODE
150
+ ? 'component-origin'
151
+ : position.source
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Builds a lookup of component primitive groups by native component index.
157
+ * @param {{ componentIndex: number }[]} componentPrimitiveGroups
158
+ * @returns {Map<number, object>}
159
+ */
160
+ static #buildGroupLookup(componentPrimitiveGroups) {
161
+ const groupsByIndex = new Map()
162
+
163
+ for (const group of componentPrimitiveGroups || []) {
164
+ const componentIndex = Number(group?.componentIndex)
165
+ if (Number.isInteger(componentIndex)) {
166
+ groupsByIndex.set(componentIndex, group)
167
+ }
168
+ }
169
+
170
+ return groupsByIndex
171
+ }
172
+
173
+ /**
174
+ * Returns pad anchor points with finite coordinates.
175
+ * @param {{ x?: number, y?: number }[] | undefined} pads
176
+ * @returns {{ x: number, y: number }[]}
177
+ */
178
+ static #padAnchors(pads) {
179
+ return (pads || [])
180
+ .map((pad) => ({
181
+ x: Number(pad?.x),
182
+ y: Number(pad?.y)
183
+ }))
184
+ .filter(
185
+ (point) => Number.isFinite(point.x) && Number.isFinite(point.y)
186
+ )
187
+ }
188
+
189
+ /**
190
+ * Resolves the center of the owned pad-anchor bounds.
191
+ * @param {{ x: number, y: number }[]} padAnchors
192
+ * @returns {{ x: number, y: number, source: string } | null}
193
+ */
194
+ static #padAnchorBoundsCenter(padAnchors) {
195
+ if (!padAnchors.length) {
196
+ return null
197
+ }
198
+
199
+ const xs = padAnchors.map((point) => point.x)
200
+ const ys = padAnchors.map((point) => point.y)
201
+
202
+ return {
203
+ x: (Math.min(...xs) + Math.max(...xs)) / 2,
204
+ y: (Math.min(...ys) + Math.max(...ys)) / 2,
205
+ source: 'pad-anchor-bounds'
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Rounds one PnP coordinate for deterministic JSON output.
211
+ * @param {number} value
212
+ * @returns {number}
213
+ */
214
+ static #roundCoordinate(value) {
215
+ return Number(Number(value || 0).toFixed(6))
216
+ }
217
+ }
@@ -2,6 +2,8 @@
2
2
  //
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
+ import { PrintableTextDecoder } from './PrintableTextDecoder.mjs'
6
+
5
7
  /**
6
8
  * Decodes Altium PrimitiveParameters/Data sidecar records.
7
9
  */
@@ -153,8 +155,7 @@ export class PcbPrimitiveParameterParser {
153
155
  * @returns {Record<string, string>}
154
156
  */
155
157
  static #parseRecordFields(bytes) {
156
- const text = new TextDecoder()
157
- .decode(bytes)
158
+ const text = PrintableTextDecoder.decodeBytes(bytes)
158
159
  .replace(/\u0000/gu, '')
159
160
  .replace(/\r\n?/gu, '\n')
160
161
  .trim()
@@ -365,82 +365,149 @@ export class PcbRawRecordRegistry {
365
365
  static #sliceSubrecordListRecords(descriptor, headerBytes, dataBytes) {
366
366
  const count = PcbRawRecordRegistry.#readRecordCount(headerBytes)
367
367
  const bytes = PcbRawRecordRegistry.#toUint8Array(dataBytes)
368
- const records = []
369
- let offset = 0
368
+ const boundaries = PcbRawRecordRegistry.#findSubrecordListBoundaries(
369
+ bytes,
370
+ 0,
371
+ descriptor,
372
+ count
373
+ )
370
374
 
371
- for (let index = 0; index < count; index += 1) {
372
- const remainingCount = count - index - 1
373
- const record = PcbRawRecordRegistry.#readSubrecordListRecordAt(
375
+ if (!boundaries) {
376
+ return []
377
+ }
378
+
379
+ return boundaries.map((boundary, index) => {
380
+ const endOffset = boundaries[index + 1]?.offset ?? bytes.byteLength
381
+
382
+ return {
383
+ recordBytes: bytes.slice(boundary.offset, endOffset),
384
+ offset: boundary.offset,
385
+ byteLength: endOffset - boundary.offset,
386
+ payloadByteLength: null,
387
+ encoding: 'subrecord-list',
388
+ objectId: descriptor.typeId,
389
+ recordIndex: index
390
+ }
391
+ })
392
+ }
393
+
394
+ /**
395
+ * Finds all subrecord-list record boundaries without recursive validation.
396
+ * @param {Uint8Array} bytes
397
+ * @param {number} offset
398
+ * @param {object} descriptor
399
+ * @param {number} count
400
+ * @returns {{ offset: number, minimumEnd: number }[] | null}
401
+ */
402
+ static #findSubrecordListBoundaries(bytes, offset, descriptor, count) {
403
+ if (!count) {
404
+ return []
405
+ }
406
+
407
+ const firstMinimumEnd =
408
+ PcbRawRecordRegistry.#readMinimumSubrecordListEnd(
374
409
  bytes,
375
410
  offset,
376
- descriptor,
377
- remainingCount
411
+ descriptor
378
412
  )
379
413
 
380
- if (!record) {
381
- return []
414
+ if (!firstMinimumEnd) {
415
+ return null
416
+ }
417
+
418
+ const boundaries = [{ offset, minimumEnd: firstMinimumEnd }]
419
+ const alternativeScanOffsets = [null]
420
+ let depth = 1
421
+ let scanOffset = firstMinimumEnd
422
+
423
+ while (depth < count) {
424
+ const candidate =
425
+ PcbRawRecordRegistry.#findNextSubrecordListCandidate(
426
+ bytes,
427
+ scanOffset,
428
+ descriptor
429
+ )
430
+
431
+ if (!candidate) {
432
+ let foundAlternative = false
433
+
434
+ while (depth > 1 && !foundAlternative) {
435
+ depth -= 1
436
+ boundaries.length = depth
437
+
438
+ const alternativeOffset = alternativeScanOffsets[depth]
439
+ alternativeScanOffsets.length = depth
440
+
441
+ if (alternativeOffset !== null) {
442
+ scanOffset = alternativeOffset
443
+ foundAlternative = true
444
+ }
445
+ }
446
+
447
+ if (!foundAlternative) {
448
+ return null
449
+ }
450
+
451
+ continue
382
452
  }
383
453
 
384
- records.push({ ...record, recordIndex: index })
385
- offset += record.byteLength
454
+ boundaries[depth] = {
455
+ offset: candidate.offset,
456
+ minimumEnd: candidate.minimumEnd
457
+ }
458
+ alternativeScanOffsets[depth] = candidate.alternativeOffset
459
+ depth += 1
460
+ scanOffset = candidate.minimumEnd
386
461
  }
387
462
 
388
- return records
463
+ return boundaries
389
464
  }
390
465
 
391
466
  /**
392
- * Reads one subrecord-list primitive record at an offset.
467
+ * Finds the next plausible subrecord-list record boundary.
393
468
  * @param {Uint8Array} bytes
394
469
  * @param {number} offset
395
470
  * @param {object} descriptor
396
- * @param {number} remainingCount
397
- * @returns {object | null}
471
+ * @returns {{ offset: number, minimumEnd: number, alternativeOffset: number | null } | null}
398
472
  */
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
- }
473
+ static #findNextSubrecordListCandidate(bytes, offset, descriptor) {
474
+ let cursor = offset
411
475
 
412
- const minimumEnd = PcbRawRecordRegistry.#readMinimumSubrecordListEnd(
413
- bytes,
414
- offset,
415
- descriptor
416
- )
476
+ while (cursor < bytes.byteLength) {
477
+ const minimumEnd =
478
+ PcbRawRecordRegistry.#readMinimumSubrecordListEnd(
479
+ bytes,
480
+ cursor,
481
+ descriptor
482
+ )
483
+ const unknownSubrecord = PcbRawRecordRegistry.#readSubrecordAt(
484
+ bytes,
485
+ cursor
486
+ )
417
487
 
418
- if (!minimumEnd) {
419
- return null
420
- }
488
+ if (minimumEnd) {
489
+ const alternativeOffset =
490
+ unknownSubrecord?.nextOffset ?? cursor + 1
491
+
492
+ return {
493
+ offset: cursor,
494
+ minimumEnd,
495
+ alternativeOffset:
496
+ alternativeOffset < bytes.byteLength
497
+ ? alternativeOffset
498
+ : null
499
+ }
500
+ }
421
501
 
422
- const endOffset = remainingCount
423
- ? PcbRawRecordRegistry.#findNextSubrecordListRecordOffset(
424
- bytes,
425
- minimumEnd,
426
- descriptor,
427
- remainingCount
428
- )
429
- : bytes.byteLength
502
+ if (!unknownSubrecord) {
503
+ cursor += 1
504
+ continue
505
+ }
430
506
 
431
- if (!endOffset) {
432
- return null
507
+ cursor = unknownSubrecord.nextOffset
433
508
  }
434
509
 
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
- }
510
+ return null
444
511
  }
445
512
 
446
513
  /**
@@ -484,82 +551,6 @@ export class PcbRawRecordRegistry {
484
551
  return cursor
485
552
  }
486
553
 
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
554
  /**
564
555
  * Slices PCB text records including their variable string tails.
565
556
  * @param {object} descriptor
@@ -3,6 +3,7 @@
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
5
  import { PcbPrimitiveOwnershipIndexParser } from './PcbPrimitiveOwnershipIndexParser.mjs'
6
+ import { PrintableTextDecoder } from './PrintableTextDecoder.mjs'
6
7
 
7
8
  /**
8
9
  * Decodes Altium PCB region primitive streams.
@@ -254,7 +255,10 @@ export class PcbRegionPrimitiveParser {
254
255
  * @returns {Record<string, string>}
255
256
  */
256
257
  static #parsePropertyBytes(bytes) {
257
- const text = new TextDecoder().decode(bytes).replace(/\u0000+$/u, '')
258
+ const text = PrintableTextDecoder.decodeBytes(bytes).replace(
259
+ /\u0000+$/u,
260
+ ''
261
+ )
258
262
  const properties = {}
259
263
 
260
264
  for (const part of text.split('|')) {