altium-toolkit 1.1.3 → 1.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/README.md +34 -5
  2. package/docs/api.md +171 -23
  3. package/docs/model-format.md +192 -20
  4. package/docs/schemas/altium_toolkit/embedded_assets_a1.schema.json +56 -0
  5. package/docs/schemas/altium_toolkit/fixture_coverage_matrix_a1.schema.json +89 -0
  6. package/docs/schemas/altium_toolkit/geometry_bounds_a1.schema.json +86 -0
  7. package/docs/schemas/altium_toolkit/library_catalog_a1.schema.json +65 -0
  8. package/docs/schemas/altium_toolkit/library_diff_a1.schema.json +54 -0
  9. package/docs/schemas/altium_toolkit/library_inspection_a1.schema.json +94 -0
  10. package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +4 -0
  11. package/docs/schemas/altium_toolkit/native_stream_inventory_a1.schema.json +66 -0
  12. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +513 -3
  13. package/docs/schemas/altium_toolkit/parameter_record_inventory_a1.schema.json +84 -0
  14. package/docs/schemas/altium_toolkit/parser_diagnostics_a1.schema.json +63 -0
  15. package/docs/schemas/altium_toolkit/parser_value_verification_a1.schema.json +74 -0
  16. package/docs/schemas/altium_toolkit/pcb_class_report_a1.schema.json +79 -0
  17. package/docs/schemas/altium_toolkit/pcb_inspection_a1.schema.json +65 -0
  18. package/docs/schemas/altium_toolkit/pcb_net_membership_a1.schema.json +98 -0
  19. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +3 -0
  20. package/docs/schemas/altium_toolkit/project_hierarchy_a1.schema.json +79 -0
  21. package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +212 -0
  22. package/docs/testing.md +7 -0
  23. package/examples/README.md +21 -0
  24. package/examples/cli-utils.mjs +148 -0
  25. package/examples/corpus-smoke.mjs +523 -0
  26. package/examples/extract-bom.mjs +47 -0
  27. package/examples/generate-pnp.mjs +59 -0
  28. package/examples/inspect-board.mjs +70 -0
  29. package/examples/inspect-schematic.mjs +406 -0
  30. package/examples/library-catalog.mjs +115 -0
  31. package/examples/net-report.mjs +61 -0
  32. package/examples/validate-library.mjs +59 -0
  33. package/package.json +1 -1
  34. package/spec/library-scope.md +5 -0
  35. package/src/core/BinaryReader.mjs +213 -2
  36. package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
  37. package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
  38. package/src/core/altium/AltiumParser.mjs +357 -16
  39. package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
  40. package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
  41. package/src/core/altium/AltiumUnits.mjs +205 -0
  42. package/src/core/altium/AsciiRecordParser.mjs +52 -11
  43. package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
  44. package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
  45. package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
  46. package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
  47. package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
  48. package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
  49. package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
  50. package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
  51. package/src/core/altium/NormalizedModelSchema.mjs +3 -31
  52. package/src/core/altium/ParameterCollection.mjs +431 -0
  53. package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
  54. package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
  55. package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
  56. package/src/core/altium/ParserErrors.mjs +90 -0
  57. package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
  58. package/src/core/altium/ParserUtils.mjs +24 -0
  59. package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
  60. package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
  61. package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
  62. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
  63. package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
  64. package/src/core/altium/PcbLayerGroups.mjs +308 -0
  65. package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
  66. package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
  67. package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
  68. package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
  69. package/src/core/altium/PcbLibModelParser.mjs +20 -4
  70. package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
  71. package/src/core/altium/PcbModelParser.mjs +223 -4
  72. package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
  73. package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
  74. package/src/core/altium/PcbStreamExtractor.mjs +130 -6
  75. package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
  76. package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
  77. package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
  78. package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
  79. package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
  80. package/src/core/altium/SchLibModelParser.mjs +840 -0
  81. package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
  82. package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
  83. package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
  84. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
  85. package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
  86. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
  87. package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
  88. package/src/core/altium/SchematicHarnessParser.mjs +9 -3
  89. package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
  90. package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
  91. package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
  92. package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
  93. package/src/core/altium/SchematicPinParser.mjs +99 -65
  94. package/src/core/altium/SchematicPrimitiveParser.mjs +125 -22
  95. package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
  96. package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
  97. package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
  98. package/src/core/altium/SchematicSheetParser.mjs +8 -2
  99. package/src/core/altium/SchematicStreamExtractor.mjs +107 -21
  100. package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
  101. package/src/core/altium/SchematicTextParser.mjs +28 -12
  102. package/src/core/altium/SchematicTextRunParser.mjs +81 -0
  103. package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
  104. package/src/core/altium/SourceBundleExporter.mjs +156 -0
  105. package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
  106. package/src/core/altium/SourceComponentClient.mjs +239 -0
  107. package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
  108. package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
  109. package/src/parser.mjs +43 -1
  110. package/src/renderers.mjs +1 -0
  111. package/src/styles/altium-renderers.css +6 -6
  112. package/src/ui/PcbArcUtils.mjs +19 -2
  113. package/src/ui/PcbScene3dBuilder.mjs +202 -20
  114. package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
  115. package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
  116. package/src/ui/SchematicColorResolver.mjs +185 -0
  117. package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
  118. package/src/ui/SchematicLineColorResolver.mjs +88 -0
  119. package/src/ui/SchematicNoteRenderer.mjs +5 -1
  120. package/src/ui/SchematicOwnerPinLabelLayout.mjs +269 -8
  121. package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
  122. package/src/ui/SchematicPinSvgRenderer.mjs +229 -62
  123. package/src/ui/SchematicShapeRenderer.mjs +86 -17
  124. package/src/ui/SchematicSvgRenderer.mjs +980 -58
  125. package/src/ui/SchematicTypography.mjs +4 -3
@@ -0,0 +1,101 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { OleCompoundDocumentWriter } from '../ole/OleCompoundDocumentWriter.mjs'
6
+ import { AltiumLibraryRecordBuilder } from './AltiumLibraryRecordBuilder.mjs'
7
+ import { SourceComponentBundleNormalizer } from './SourceComponentBundleNormalizer.mjs'
8
+
9
+ /**
10
+ * Exports normalized bundles into a compact OLE-backed footprint library.
11
+ */
12
+ export class AltiumPcbLibExporter {
13
+ /**
14
+ * Exports one or more component bundles as `.PcbLib` bytes.
15
+ * @param {object[] | object} bundles Component bundles.
16
+ * @returns {Uint8Array}
17
+ */
18
+ static export(bundles) {
19
+ const normalizedBundles =
20
+ AltiumPcbLibExporter.#normalizeBundles(bundles)
21
+ const streams = new Map()
22
+ const modelRows = AltiumPcbLibExporter.#collectModels(normalizedBundles)
23
+
24
+ streams.set(
25
+ 'Library/Data',
26
+ AltiumLibraryRecordBuilder.buildPcbLibraryData(normalizedBundles)
27
+ )
28
+ streams.set(
29
+ 'Library/ComponentParamsTOC/Data',
30
+ AltiumLibraryRecordBuilder.buildComponentParamsToc(
31
+ normalizedBundles
32
+ )
33
+ )
34
+ streams.set(
35
+ 'SectionKeys',
36
+ AltiumLibraryRecordBuilder.buildSectionKeys(normalizedBundles)
37
+ )
38
+
39
+ for (const bundle of normalizedBundles) {
40
+ const storageName = AltiumLibraryRecordBuilder.sanitizeStorageName(
41
+ bundle.footprint.name
42
+ )
43
+ streams.set(
44
+ storageName + '/Header',
45
+ AltiumLibraryRecordBuilder.createCountHeader(0)
46
+ )
47
+ streams.set(
48
+ storageName + '/Parameters',
49
+ AltiumLibraryRecordBuilder.buildFootprintParameters(bundle)
50
+ )
51
+ streams.set(
52
+ storageName + '/Data',
53
+ AltiumLibraryRecordBuilder.buildFootprintData(bundle)
54
+ )
55
+ streams.set(
56
+ storageName + '/SourceRecord',
57
+ new TextEncoder().encode(
58
+ AltiumLibraryRecordBuilder.buildPcbFootprintRecord(bundle)
59
+ )
60
+ )
61
+ }
62
+
63
+ if (modelRows.length) {
64
+ streams.set(
65
+ 'Models/Data',
66
+ AltiumLibraryRecordBuilder.buildModelsData(modelRows)
67
+ )
68
+ modelRows.forEach((row, index) => {
69
+ streams.set('Models/' + index, row.model.bytes)
70
+ })
71
+ }
72
+
73
+ return OleCompoundDocumentWriter.write({ streams })
74
+ }
75
+
76
+ /**
77
+ * Collects model rows with deterministic generated ids.
78
+ * @param {object[]} bundles Normalized bundles.
79
+ * @returns {{ model: object, id: string, checksum: number }[]}
80
+ */
81
+ static #collectModels(bundles) {
82
+ return bundles.flatMap((bundle, bundleIndex) =>
83
+ bundle.models.map((model, modelIndex) => ({
84
+ model,
85
+ id: 'model-' + bundleIndex + '-' + modelIndex,
86
+ checksum: AltiumLibraryRecordBuilder.checksumBytes(model.bytes)
87
+ }))
88
+ )
89
+ }
90
+
91
+ /**
92
+ * Normalizes one or more bundles.
93
+ * @param {object[] | object} bundles Bundle input.
94
+ * @returns {object[]}
95
+ */
96
+ static #normalizeBundles(bundles) {
97
+ return (Array.isArray(bundles) ? bundles : [bundles]).map((bundle) =>
98
+ SourceComponentBundleNormalizer.normalize(bundle)
99
+ )
100
+ }
101
+ }
@@ -0,0 +1,57 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { OleCompoundDocumentWriter } from '../ole/OleCompoundDocumentWriter.mjs'
6
+ import { AltiumLibraryRecordBuilder } from './AltiumLibraryRecordBuilder.mjs'
7
+ import { SourceComponentBundleNormalizer } from './SourceComponentBundleNormalizer.mjs'
8
+
9
+ /**
10
+ * Exports normalized bundles into a compact OLE-backed schematic library.
11
+ */
12
+ export class AltiumSchLibExporter {
13
+ /**
14
+ * Exports one or more component bundles as `.SchLib` bytes.
15
+ * @param {object[] | object} bundles Component bundles.
16
+ * @returns {Uint8Array}
17
+ */
18
+ static export(bundles) {
19
+ const normalizedBundles =
20
+ AltiumSchLibExporter.#normalizeBundles(bundles)
21
+ const streams = new Map()
22
+ const libraryRecord = normalizedBundles
23
+ .map((bundle) =>
24
+ AltiumLibraryRecordBuilder.buildSchematicComponentRecord(bundle)
25
+ )
26
+ .join('\n')
27
+
28
+ streams.set('Library/Data', new TextEncoder().encode(libraryRecord))
29
+ for (const bundle of normalizedBundles) {
30
+ streams.set(
31
+ 'Components/' +
32
+ AltiumLibraryRecordBuilder.sanitizeStorageName(
33
+ bundle.symbol.name
34
+ ) +
35
+ '/Data',
36
+ new TextEncoder().encode(
37
+ AltiumLibraryRecordBuilder.buildSchematicComponentRecord(
38
+ bundle
39
+ )
40
+ )
41
+ )
42
+ }
43
+
44
+ return OleCompoundDocumentWriter.write({ streams })
45
+ }
46
+
47
+ /**
48
+ * Normalizes one or more bundles.
49
+ * @param {object[] | object} bundles Bundle input.
50
+ * @returns {object[]}
51
+ */
52
+ static #normalizeBundles(bundles) {
53
+ return (Array.isArray(bundles) ? bundles : [bundles]).map((bundle) =>
54
+ SourceComponentBundleNormalizer.normalize(bundle)
55
+ )
56
+ }
57
+ }
@@ -0,0 +1,205 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Provides deterministic conversions for common Altium length units.
7
+ */
8
+ export class AltiumUnits {
9
+ static RAW_UNITS_PER_MIL = 10000
10
+
11
+ static MM_PER_MIL = 0.0254
12
+
13
+ static MIL_PER_INCH = 1000
14
+
15
+ /**
16
+ * Converts millimeters to mils.
17
+ * @param {number} value Length in millimeters.
18
+ * @returns {number}
19
+ */
20
+ static mmToMil(value) {
21
+ return AltiumUnits.#round(Number(value) / AltiumUnits.MM_PER_MIL)
22
+ }
23
+
24
+ /**
25
+ * Converts mils to millimeters.
26
+ * @param {number} value Length in mils.
27
+ * @returns {number}
28
+ */
29
+ static milToMm(value) {
30
+ return AltiumUnits.#round(Number(value) * AltiumUnits.MM_PER_MIL)
31
+ }
32
+
33
+ /**
34
+ * Converts inches to mils.
35
+ * @param {number} value Length in inches.
36
+ * @returns {number}
37
+ */
38
+ static inchToMil(value) {
39
+ return AltiumUnits.#round(Number(value) * AltiumUnits.MIL_PER_INCH)
40
+ }
41
+
42
+ /**
43
+ * Converts mils to inches.
44
+ * @param {number} value Length in mils.
45
+ * @returns {number}
46
+ */
47
+ static milToInch(value) {
48
+ return AltiumUnits.#round(Number(value) / AltiumUnits.MIL_PER_INCH)
49
+ }
50
+
51
+ /**
52
+ * Converts raw Altium coordinate units to mils.
53
+ * @param {number} value Raw coordinate units.
54
+ * @returns {number}
55
+ */
56
+ static rawToMil(value) {
57
+ return AltiumUnits.#round(Number(value) / AltiumUnits.RAW_UNITS_PER_MIL)
58
+ }
59
+
60
+ /**
61
+ * Converts mils to raw Altium coordinate units.
62
+ * @param {number} value Length in mils.
63
+ * @returns {number}
64
+ */
65
+ static milToRaw(value) {
66
+ return AltiumUnits.#round(Number(value) * AltiumUnits.RAW_UNITS_PER_MIL)
67
+ }
68
+
69
+ /**
70
+ * Parses one numeric or textual length into a deterministic unit bundle.
71
+ * @param {unknown} value Length value.
72
+ * @param {{ defaultUnit?: 'mil' | 'mm' | 'in' | 'raw' }} [options]
73
+ * @returns {{ value: number, unit: string, mil: number, mm: number, inch: number, raw: number } | null}
74
+ */
75
+ static parseLength(value, options = {}) {
76
+ const parsed = AltiumUnits.#parseNumericAndUnit(value, options)
77
+ if (!parsed) return null
78
+
79
+ const mil = AltiumUnits.#toMil(parsed.value, parsed.unit)
80
+ if (!Number.isFinite(mil)) return null
81
+
82
+ return {
83
+ value: AltiumUnits.#round(parsed.value),
84
+ unit: parsed.unit,
85
+ mil: AltiumUnits.#round(mil),
86
+ mm: AltiumUnits.milToMm(mil),
87
+ inch: AltiumUnits.milToInch(mil),
88
+ raw: AltiumUnits.milToRaw(mil)
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Formats a mil value in the requested target unit.
94
+ * @param {number} value Length in mils.
95
+ * @param {'mil' | 'mm' | 'in' | 'raw'} [unit]
96
+ * @returns {string}
97
+ */
98
+ static formatMil(value, unit = 'mil') {
99
+ const mil = Number(value)
100
+ if (!Number.isFinite(mil)) return ''
101
+
102
+ const normalizedUnit = AltiumUnits.#normalizeUnit(unit)
103
+ if (normalizedUnit === 'mm') {
104
+ return AltiumUnits.#formatNumber(AltiumUnits.milToMm(mil)) + 'mm'
105
+ }
106
+ if (normalizedUnit === 'in') {
107
+ return AltiumUnits.#formatNumber(AltiumUnits.milToInch(mil)) + 'in'
108
+ }
109
+ if (normalizedUnit === 'raw') {
110
+ return AltiumUnits.#formatNumber(AltiumUnits.milToRaw(mil)) + 'raw'
111
+ }
112
+ return AltiumUnits.#formatNumber(mil) + 'mil'
113
+ }
114
+
115
+ /**
116
+ * Parses an input value into a number and normalized unit token.
117
+ * @param {unknown} value Length input.
118
+ * @param {{ defaultUnit?: string }} options Parse options.
119
+ * @returns {{ value: number, unit: 'mil' | 'mm' | 'in' | 'raw' } | null}
120
+ */
121
+ static #parseNumericAndUnit(value, options) {
122
+ const defaultUnit = AltiumUnits.#normalizeUnit(
123
+ options?.defaultUnit || 'mil'
124
+ )
125
+
126
+ if (typeof value === 'number') {
127
+ return Number.isFinite(value) ? { value, unit: defaultUnit } : null
128
+ }
129
+
130
+ const text = String(value ?? '').trim()
131
+ const match = text.match(
132
+ /^([-+]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[-+]?\d+)?)\s*([A-Za-z]*)$/u
133
+ )
134
+ if (!match) return null
135
+
136
+ const numericValue = Number(match[1])
137
+ if (!Number.isFinite(numericValue)) return null
138
+
139
+ return {
140
+ value: numericValue,
141
+ unit: AltiumUnits.#normalizeUnit(match[2] || defaultUnit)
142
+ }
143
+ }
144
+
145
+ /**
146
+ * Converts one normalized length value to mils.
147
+ * @param {number} value Numeric length.
148
+ * @param {'mil' | 'mm' | 'in' | 'raw'} unit Length unit.
149
+ * @returns {number}
150
+ */
151
+ static #toMil(value, unit) {
152
+ if (unit === 'mm') return AltiumUnits.mmToMil(value)
153
+ if (unit === 'in') return AltiumUnits.inchToMil(value)
154
+ if (unit === 'raw') return AltiumUnits.rawToMil(value)
155
+ return Number(value)
156
+ }
157
+
158
+ /**
159
+ * Normalizes one unit label.
160
+ * @param {unknown} unit Unit label.
161
+ * @returns {'mil' | 'mm' | 'in' | 'raw'}
162
+ */
163
+ static #normalizeUnit(unit) {
164
+ const normalized = String(unit || '')
165
+ .trim()
166
+ .toLowerCase()
167
+ if (
168
+ normalized === 'mm' ||
169
+ normalized === 'millimeter' ||
170
+ normalized === 'millimeters'
171
+ ) {
172
+ return 'mm'
173
+ }
174
+ if (
175
+ normalized === 'in' ||
176
+ normalized === 'inch' ||
177
+ normalized === 'inches'
178
+ ) {
179
+ return 'in'
180
+ }
181
+ if (normalized === 'raw') {
182
+ return 'raw'
183
+ }
184
+ return 'mil'
185
+ }
186
+
187
+ /**
188
+ * Formats a number without unstable floating-point tails.
189
+ * @param {number} value Numeric value.
190
+ * @returns {string}
191
+ */
192
+ static #formatNumber(value) {
193
+ return String(AltiumUnits.#round(value))
194
+ }
195
+
196
+ /**
197
+ * Rounds a numeric value to stable JSON/report precision.
198
+ * @param {number} value Numeric value.
199
+ * @returns {number}
200
+ */
201
+ static #round(value) {
202
+ const numeric = Number(value)
203
+ return Number.isFinite(numeric) ? Number(numeric.toFixed(9)) : numeric
204
+ }
205
+ }
@@ -63,6 +63,15 @@ export class AsciiRecordParser {
63
63
  return records
64
64
  }
65
65
 
66
+ /**
67
+ * Parses one pipe-delimited record string into a field object.
68
+ * @param {string} raw Raw record text.
69
+ * @returns {{ raw: string, fields: Record<string, string | string[]> }}
70
+ */
71
+ static parseRecord(raw) {
72
+ return AsciiRecordParser.#parseRecord(raw)
73
+ }
74
+
66
75
  /**
67
76
  * Returns true when a printable run looks like an Altium record block.
68
77
  * @param {string} candidate
@@ -146,17 +155,11 @@ export class AsciiRecordParser {
146
155
  const rawKey = AsciiRecordParser.#trimAscii(
147
156
  segment.slice(0, separatorIndex)
148
157
  )
149
- const value = PrintableTextDecoder.decodeBytes(
150
- AsciiRecordParser.#binaryStringToBytes(
151
- AsciiRecordParser.#trimAscii(
152
- segment.slice(separatorIndex + 1)
153
- )
154
- ),
155
- {
156
- encoding: rawKey.startsWith('%UTF8%') ? 'utf-8' : undefined
157
- }
158
- )
159
158
  const isUtf8Field = rawKey.startsWith('%UTF8%')
159
+ const value = AsciiRecordParser.#decodeFieldValue(
160
+ AsciiRecordParser.#trimAscii(segment.slice(separatorIndex + 1)),
161
+ isUtf8Field ? 'utf-8' : ''
162
+ )
160
163
  const key = rawKey.replace(/^%UTF8%/, '')
161
164
  if (!key) continue
162
165
 
@@ -228,13 +231,51 @@ export class AsciiRecordParser {
228
231
  return normalizedKeyIndex
229
232
  }
230
233
 
234
+ /**
235
+ * Decodes one pipe-delimited field value from the byte-preserving run
236
+ * string. Plain ASCII is already decoded by construction and can avoid
237
+ * byte-array allocation plus TextDecoder fallback probing.
238
+ * @param {string} value Byte-preserving field value.
239
+ * @param {string} preferredEncoding Optional preferred decoder encoding.
240
+ * @returns {string}
241
+ */
242
+ static #decodeFieldValue(value, preferredEncoding) {
243
+ if (!AsciiRecordParser.#hasExtendedByte(value)) {
244
+ return value
245
+ }
246
+
247
+ return PrintableTextDecoder.decodeBytes(
248
+ AsciiRecordParser.#binaryStringToBytes(value),
249
+ { encoding: preferredEncoding || undefined }
250
+ )
251
+ }
252
+
231
253
  /**
232
254
  * Converts one binary string into bytes without altering byte values.
233
255
  * @param {string} value
234
256
  * @returns {Uint8Array}
235
257
  */
236
258
  static #binaryStringToBytes(value) {
237
- return Uint8Array.from(value, (character) => character.charCodeAt(0))
259
+ const bytes = new Uint8Array(value.length)
260
+
261
+ for (let index = 0; index < value.length; index += 1) {
262
+ bytes[index] = value.charCodeAt(index) & 0xff
263
+ }
264
+
265
+ return bytes
266
+ }
267
+
268
+ /**
269
+ * Returns true when the byte-preserving string contains non-ASCII bytes.
270
+ * @param {string} value Field value.
271
+ * @returns {boolean}
272
+ */
273
+ static #hasExtendedByte(value) {
274
+ for (let index = 0; index < value.length; index += 1) {
275
+ if (value.charCodeAt(index) > 0x7f) return true
276
+ }
277
+
278
+ return false
238
279
  }
239
280
 
240
281
  /**