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
@@ -47,13 +47,18 @@ export class SchematicRecordTypeRegistry {
47
47
  [47, ['map-definer', 'implementation', true]],
48
48
  [48, ['implementation-parameters', 'implementation', true]],
49
49
  [209, ['note', 'annotation', true]],
50
+ [210, ['probe-marker', 'code-symbol', true]],
50
51
  [211, ['compile-mask', 'directive', true]],
51
52
  [215, ['harness-connector', 'harness', true]],
52
53
  [216, ['harness-entry', 'harness', true]],
53
54
  [217, ['harness-type', 'harness', true]],
54
55
  [218, ['signal-harness', 'harness', true]],
56
+ [220, ['code-symbol', 'code-symbol', true]],
57
+ [221, ['code-symbol-entry', 'code-symbol', true]],
58
+ [222, ['code-symbol-title', 'code-symbol', true]],
59
+ [223, ['code-symbol-source', 'code-symbol', true]],
55
60
  [225, ['blanket', 'directive', true]],
56
- [226, ['hyperlink', 'annotation', false]]
61
+ [226, ['hyperlink', 'annotation', true]]
57
62
  ])
58
63
 
59
64
  /**
@@ -13,7 +13,7 @@ export class SchematicSheetParser {
13
13
  /**
14
14
  * Parses sheet symbols and their child entries from schematic records.
15
15
  * @param {{ fields: Record<string, string | string[]>, recordIndex: number }[]} records
16
- * @returns {{ sheetSymbols: { x: number, y: number, width: number, height: number, color: string, fill: string, isSolid: boolean, transparent: boolean, ownerIndex?: string, uniqueId: string, renderOrder: number, sourceRecordIndex: number, indexInSheet: number | null }[], sheetEntries: { ownerIndex: string, name: string, side: 'left' | 'right' | 'top' | 'bottom', direction: 'unspecified' | 'output' | 'input' | 'bidirectional', style: number, x: number, y: number, color: string, fill: string, textColor: string, harnessType: string, renderOrder: number }[] }}
16
+ * @returns {{ sheetSymbols: { x: number, y: number, width: number, height: number, name?: string, fileName?: string, color: string, fill: string, isSolid: boolean, transparent: boolean, ownerIndex?: string, uniqueId: string, renderOrder: number, sourceRecordIndex: number, indexInSheet: number | null }[], sheetEntries: { ownerIndex: string, name: string, side: 'left' | 'right' | 'top' | 'bottom', direction: 'unspecified' | 'output' | 'input' | 'bidirectional', style: number, x: number, y: number, color: string, fill: string, textColor: string, harnessType: string, renderOrder: number }[] }}
17
17
  */
18
18
  static parse(records) {
19
19
  const sheetSymbols = records
@@ -38,7 +38,7 @@ export class SchematicSheetParser {
38
38
  /**
39
39
  * Normalizes one `RECORD=15` sheet symbol.
40
40
  * @param {{ fields: Record<string, string | string[]>, recordIndex: number }} record
41
- * @returns {{ x: number, y: number, width: number, height: number, color: string, fill: string, isSolid: boolean, transparent: boolean, ownerIndex?: string, uniqueId: string, renderOrder: number, sourceRecordIndex: number, indexInSheet: number | null } | null}
41
+ * @returns {{ x: number, y: number, width: number, height: number, name?: string, fileName?: string, color: string, fill: string, isSolid: boolean, transparent: boolean, ownerIndex?: string, uniqueId: string, renderOrder: number, sourceRecordIndex: number, indexInSheet: number | null } | null}
42
42
  */
43
43
  static #parseSheetSymbolRecord(record) {
44
44
  if (getField(record.fields, 'RECORD') !== '15') {
@@ -61,6 +61,12 @@ export class SchematicSheetParser {
61
61
  y,
62
62
  width,
63
63
  height,
64
+ ...(getField(record.fields, 'Name')
65
+ ? { name: getField(record.fields, 'Name') }
66
+ : {}),
67
+ ...(getField(record.fields, 'FileName')
68
+ ? { fileName: getField(record.fields, 'FileName') }
69
+ : {}),
64
70
  color: toColor(record.fields.Color, '#a44a1b'),
65
71
  fill: toColor(record.fields.AreaColor, '#ffe16f'),
66
72
  isSolid: parseBoolean(record.fields.IsSolid),
@@ -2,8 +2,9 @@
2
2
  //
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
- import { AsciiRecordParser } from './AsciiRecordParser.mjs'
6
5
  import { EmbeddedFileInventoryBuilder } from './EmbeddedFileInventoryBuilder.mjs'
6
+ import { NativeStreamInventoryBuilder } from './NativeStreamInventoryBuilder.mjs'
7
+ import { SchematicRecordStreamParser } from './SchematicRecordStreamParser.mjs'
7
8
  import { OleCompoundDocument } from '../ole/OleCompoundDocument.mjs'
8
9
  import { OleConstants } from '../ole/OleConstants.mjs'
9
10
 
@@ -12,6 +13,8 @@ import { OleConstants } from '../ole/OleConstants.mjs'
12
13
  * containers.
13
14
  */
14
15
  export class SchematicStreamExtractor {
16
+ static #AUXILIARY_PRINTABLE_STREAMS = new Set(['Additional'])
17
+
15
18
  /**
16
19
  * Returns true when one buffer starts with the OLE compound-document
17
20
  * signature.
@@ -38,9 +41,10 @@ export class SchematicStreamExtractor {
38
41
  }
39
42
 
40
43
  /**
41
- * Extracts schematic records from the logical `FileHeader` stream.
44
+ * Extracts schematic records from the logical `FileHeader` stream and
45
+ * Altium's auxiliary printable schematic streams.
42
46
  * @param {ArrayBuffer} arrayBuffer
43
- * @returns {{ records: Array<{ raw: string, fields: Record<string, string | string[]>, sourceStream: string }>, streamNames: string[] } | null}
47
+ * @returns {{ records: Array<{ raw: string, fields: Record<string, string | string[]>, sourceStream: string }>, streamNames: string[], embeddedFiles?: object } | null}
44
48
  */
45
49
  static extractFromArrayBuffer(arrayBuffer) {
46
50
  if (!SchematicStreamExtractor.isCompoundDocument(arrayBuffer)) {
@@ -55,39 +59,121 @@ export class SchematicStreamExtractor {
55
59
  return null
56
60
  }
57
61
 
58
- let fileHeaderBytes
62
+ let fileHeaderParse
59
63
 
60
64
  try {
61
- fileHeaderBytes = compoundDocument.getStream('FileHeader')
65
+ fileHeaderParse = SchematicStreamExtractor.#parseStreamRecords(
66
+ compoundDocument,
67
+ 'FileHeader'
68
+ )
62
69
  } catch {
63
70
  return null
64
71
  }
65
72
 
66
- const records = AsciiRecordParser.parse(
67
- SchematicStreamExtractor.#toArrayBuffer(fileHeaderBytes)
68
- ).map((record) => ({
69
- ...record,
70
- sourceStream: 'FileHeader'
71
- }))
73
+ const streamNames = compoundDocument.listStreams()
74
+ const auxiliaryStreamNames = streamNames.filter((streamName) =>
75
+ SchematicStreamExtractor.#isAuxiliaryPrintableStream(streamName)
76
+ )
77
+ const auxiliaryParses = auxiliaryStreamNames.map((streamName) =>
78
+ SchematicStreamExtractor.#parseStreamRecords(
79
+ compoundDocument,
80
+ streamName
81
+ )
82
+ )
83
+ const records = [
84
+ ...fileHeaderParse.records,
85
+ ...auxiliaryParses.flatMap((entry) => entry.records)
86
+ ]
87
+ const opaqueRecords = [
88
+ ...fileHeaderParse.opaqueRecords,
89
+ ...auxiliaryParses.flatMap((entry) => entry.opaqueRecords)
90
+ ]
72
91
 
73
- if (!records.length) {
92
+ if (!records.length && !opaqueRecords.length) {
74
93
  return null
75
94
  }
76
95
 
96
+ const embeddedFiles = EmbeddedFileInventoryBuilder.buildFromStreams(
97
+ new Map(
98
+ streamNames.map((name) => [
99
+ name,
100
+ compoundDocument.getStream(name)
101
+ ])
102
+ ),
103
+ {
104
+ skipStreamNames: ['FileHeader', ...auxiliaryStreamNames]
105
+ }
106
+ )
107
+ const consumedStreamNames = [
108
+ 'FileHeader',
109
+ ...auxiliaryStreamNames,
110
+ ...embeddedFiles.files.map((file) => file.sourceStream),
111
+ ...embeddedFiles.diagnostics.map(
112
+ (diagnostic) => diagnostic.sourceStream
113
+ )
114
+ ]
115
+ const nativeStreams = NativeStreamInventoryBuilder.buildFromStreams(
116
+ new Map(
117
+ streamNames.map((name) => [
118
+ name,
119
+ compoundDocument.getStream(name)
120
+ ])
121
+ ),
122
+ {
123
+ source: 'schdoc',
124
+ consumedStreamNames,
125
+ knownStreamNames: ['FileHeader', ...auxiliaryStreamNames]
126
+ }
127
+ )
128
+
77
129
  return {
78
130
  records,
79
- streamNames: compoundDocument.listStreams(),
80
- embeddedFiles: EmbeddedFileInventoryBuilder.buildFromStreams(
81
- new Map(
82
- compoundDocument
83
- .listStreams()
84
- .map((name) => [name, compoundDocument.getStream(name)])
85
- ),
86
- { skipStreamNames: ['FileHeader'] }
87
- )
131
+ streamNames,
132
+ opaqueRecords,
133
+ nativeStreams,
134
+ embeddedFiles
88
135
  }
89
136
  }
90
137
 
138
+ /**
139
+ * Parses printable records from one compound-document stream.
140
+ * @param {OleCompoundDocument} compoundDocument
141
+ * @param {string} streamName
142
+ * @returns {{ records: Array<{ raw: string, fields: Record<string, string | string[]>, sourceStream: string }>, opaqueRecords: object[] }}
143
+ */
144
+ static #parseStreamRecords(compoundDocument, streamName) {
145
+ const parsed = SchematicRecordStreamParser.parseWithOpaqueRecords(
146
+ SchematicStreamExtractor.#toArrayBuffer(
147
+ compoundDocument.getStream(streamName)
148
+ ),
149
+ { sourceStream: streamName }
150
+ )
151
+
152
+ return {
153
+ records: parsed.records.map((record) => ({
154
+ ...record,
155
+ sourceStream: streamName
156
+ })),
157
+ opaqueRecords: parsed.opaqueRecords
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Returns true for known non-embedded schematic streams with printable
163
+ * primitive records.
164
+ * @param {string} streamName
165
+ * @returns {boolean}
166
+ */
167
+ static #isAuxiliaryPrintableStream(streamName) {
168
+ const leafName = String(streamName || '')
169
+ .split('/')
170
+ .at(-1)
171
+
172
+ return SchematicStreamExtractor.#AUXILIARY_PRINTABLE_STREAMS.has(
173
+ leafName
174
+ )
175
+ }
176
+
91
177
  /**
92
178
  * Returns an ArrayBuffer view over one byte slice.
93
179
  * @param {Uint8Array} bytes
@@ -0,0 +1,76 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { ParserUtils } from './ParserUtils.mjs'
6
+
7
+ /**
8
+ * Resolves native schematic text orientation flags.
9
+ */
10
+ export class SchematicTextOrientationResolver {
11
+ /**
12
+ * Resolves component designator/parameter orientation bit 1 to right
13
+ * anchoring.
14
+ * @param {Record<string, string | string[]>} fields Text record fields.
15
+ * @param {string} recordType Native text record type.
16
+ * @returns {'end' | null}
17
+ */
18
+ static resolveHorizontalAnchor(fields, recordType) {
19
+ if (
20
+ !SchematicTextOrientationResolver.#isComponentTextRecord(recordType)
21
+ ) {
22
+ return null
23
+ }
24
+
25
+ const orientation = ParserUtils.parseNumericField(fields, 'Orientation')
26
+ if (!Number.isInteger(orientation)) return null
27
+
28
+ return (orientation & 2) === 2 ? 'end' : null
29
+ }
30
+
31
+ /**
32
+ * Resolves component text orientation bit 1 to top source anchoring.
33
+ * @param {Record<string, string | string[]>} fields Text record fields.
34
+ * @param {string} recordType Native text record type.
35
+ * @returns {'top' | null}
36
+ */
37
+ static resolveVerticalAnchor(fields, recordType) {
38
+ return SchematicTextOrientationResolver.resolveHorizontalAnchor(
39
+ fields,
40
+ recordType
41
+ )
42
+ ? 'top'
43
+ : null
44
+ }
45
+
46
+ /**
47
+ * Returns true when a text record's orientation encodes a quarter-turn.
48
+ * Component designators and parameters use bit 0; older free labels use
49
+ * the literal orientation-1 value.
50
+ * @param {string} recordType Native text record type.
51
+ * @param {number | null} orientation Native orientation value.
52
+ * @returns {boolean}
53
+ */
54
+ static shouldRotateFromOrientation(recordType, orientation) {
55
+ if (!Number.isInteger(orientation)) {
56
+ return false
57
+ }
58
+
59
+ if (
60
+ SchematicTextOrientationResolver.#isComponentTextRecord(recordType)
61
+ ) {
62
+ return (orientation & 1) === 1
63
+ }
64
+
65
+ return orientation === 1
66
+ }
67
+
68
+ /**
69
+ * Returns true for component designator and parameter text records.
70
+ * @param {string} recordType Native text record type.
71
+ * @returns {boolean}
72
+ */
73
+ static #isComponentTextRecord(recordType) {
74
+ return recordType === '34' || recordType === '41'
75
+ }
76
+ }
@@ -3,6 +3,8 @@
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
5
  import { ParserUtils } from './ParserUtils.mjs'
6
+ import { SchematicTextRunParser } from './SchematicTextRunParser.mjs'
7
+ import { SchematicTextOrientationResolver } from './SchematicTextOrientationResolver.mjs'
6
8
 
7
9
  /**
8
10
  * Helpers for normalized schematic text extraction.
@@ -134,7 +136,7 @@ export class SchematicTextParser {
134
136
  * @param {{ width: number, marginWidth: number, titleBlockOn?: boolean }} sheet
135
137
  * @param {Record<string, { size: number, family: string, bold: boolean, italic?: boolean, rotation: number }>} fonts
136
138
  * @param {Map<string, Record<string, string>>} [ownerMetadata]
137
- * @returns {{ x: number, y: number, text: string, color: string, hidden: boolean, name: string, ownerIndex?: string, recordType: string, style: number, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string, rotation: number, sourceOrientation?: number, isMirrored?: boolean, anchor: 'start' | 'middle' | 'end', powerPortDirection?: 'up' | 'down' | 'left' | 'right', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] } | null}
139
+ * @returns {{ x: number, y: number, text: string, color: string, hidden: boolean, name: string, ownerIndex?: string, recordType: string, style: number, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string, rotation: number, sourceOrientation?: number, isMirrored?: boolean, anchor: 'start' | 'middle' | 'end', verticalAnchor?: 'top', powerPortDirection?: 'up' | 'down' | 'left' | 'right', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] } | null}
138
140
  */
139
141
  static normalizeSchematicTextRecord(
140
142
  fields,
@@ -158,8 +160,9 @@ export class SchematicTextParser {
158
160
  ownerMetadata
159
161
  )
160
162
  )
163
+ const textRuns = SchematicTextRunParser.parseOptionalOverlineRuns(text)
161
164
 
162
- if (hidden || x === null || y === null || !text) {
165
+ if (hidden || x === null || y === null || !textRuns.text) {
163
166
  return null
164
167
  }
165
168
 
@@ -168,7 +171,7 @@ export class SchematicTextParser {
168
171
  fields,
169
172
  name,
170
173
  rawText,
171
- text,
174
+ textRuns.text,
172
175
  sheet
173
176
  )
174
177
  ) {
@@ -191,7 +194,8 @@ export class SchematicTextParser {
191
194
  const textRecord = {
192
195
  x,
193
196
  y,
194
- text,
197
+ text: textRuns.text,
198
+ textSegments: textRuns.segments,
195
199
  color: SchematicTextParser.#resolveSchematicTextColor(
196
200
  fields,
197
201
  recordType
@@ -211,6 +215,11 @@ export class SchematicTextParser {
211
215
  sourceOrientation:
212
216
  sourceOrientation === null ? undefined : sourceOrientation,
213
217
  isMirrored: isMirrored || undefined,
218
+ verticalAnchor:
219
+ SchematicTextOrientationResolver.resolveVerticalAnchor(
220
+ fields,
221
+ recordType
222
+ ) || undefined,
214
223
  powerPortDirection:
215
224
  SchematicTextParser.#resolvePowerPortDirection(
216
225
  fields,
@@ -597,8 +606,14 @@ export class SchematicTextParser {
597
606
  static #inferTextAnchor(fields, recordType, name, text, font, rotation) {
598
607
  const explicitAnchor =
599
608
  SchematicTextParser.#resolveSchematicTextJustificationAnchor(fields)
609
+ const orientationAnchor =
610
+ SchematicTextOrientationResolver.resolveHorizontalAnchor(
611
+ fields,
612
+ recordType
613
+ )
600
614
 
601
615
  if (recordType === '17') return 'middle'
616
+ if (orientationAnchor) return orientationAnchor
602
617
  if (explicitAnchor) return explicitAnchor
603
618
  if (
604
619
  SchematicTextParser.#shouldCenterSchematicNoteByDefault(
@@ -748,12 +763,9 @@ export class SchematicTextParser {
748
763
  */
749
764
  static #resolveTextRotation(fields, font, recordType) {
750
765
  if (recordType === '17') return 0
751
- if (recordType === '25') {
752
- const orientation = ParserUtils.parseNumericField(
753
- fields,
754
- 'Orientation'
755
- )
766
+ const orientation = ParserUtils.parseNumericField(fields, 'Orientation')
756
767
 
768
+ if (recordType === '25') {
757
769
  if (orientation === 1 || orientation === 3) {
758
770
  return 90
759
771
  }
@@ -765,7 +777,12 @@ export class SchematicTextParser {
765
777
  )
766
778
  if (explicitRotation !== null) return explicitRotation
767
779
  if (font.rotation) return font.rotation
768
- if (ParserUtils.parseNumericField(fields, 'Orientation') === 1) {
780
+ if (
781
+ SchematicTextOrientationResolver.shouldRotateFromOrientation(
782
+ recordType,
783
+ orientation
784
+ )
785
+ ) {
769
786
  return 90
770
787
  }
771
788
  return 0
@@ -870,8 +887,7 @@ export class SchematicTextParser {
870
887
  fields.Color || fields.TextColor,
871
888
  '#7b7753'
872
889
  ),
873
- lineWidth:
874
- ParserUtils.parseNumericField(fields, 'LineWidth') ?? undefined,
890
+ lineWidth: ParserUtils.parseSchematicLineWidth(fields),
875
891
  isSolid: ParserUtils.parseBoolean(fields.IsSolid),
876
892
  showBorder: ParserUtils.parseBoolean(fields.ShowBorder),
877
893
  textMargin:
@@ -0,0 +1,81 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Parses schematic text runs that use backslash suffix markers for overlines.
7
+ */
8
+ export class SchematicTextRunParser {
9
+ /**
10
+ * Parses overline suffix markers into display text and contiguous runs.
11
+ * @param {string} value Source text.
12
+ * @returns {{ text: string, hasOverline: boolean, segments: { text: string, overline: boolean }[] }}
13
+ */
14
+ static parseOverlineRuns(value) {
15
+ const characters = []
16
+
17
+ for (const character of String(value || '').trim()) {
18
+ if (character === '\\') {
19
+ const previousCharacter = characters.at(-1)
20
+ if (previousCharacter) {
21
+ previousCharacter.overline = true
22
+ }
23
+ continue
24
+ }
25
+
26
+ characters.push({
27
+ text: character,
28
+ overline: false
29
+ })
30
+ }
31
+
32
+ const segments = SchematicTextRunParser.#segments(characters)
33
+
34
+ return {
35
+ text: characters.map((character) => character.text).join(''),
36
+ hasOverline: segments.some((segment) => segment.overline),
37
+ segments
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Returns only overline segments when a value contains overlined runs.
43
+ * @param {string} value Source text.
44
+ * @returns {{ text: string, segments?: { text: string, overline: boolean }[] }}
45
+ */
46
+ static parseOptionalOverlineRuns(value) {
47
+ const parsed = SchematicTextRunParser.parseOverlineRuns(value)
48
+
49
+ return {
50
+ text: parsed.text,
51
+ segments: parsed.hasOverline ? parsed.segments : undefined
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Merges character flags into contiguous text segments.
57
+ * @param {{ text: string, overline: boolean }[]} characters Character rows.
58
+ * @returns {{ text: string, overline: boolean }[]}
59
+ */
60
+ static #segments(characters) {
61
+ const segments = []
62
+
63
+ for (const character of characters) {
64
+ const previousSegment = segments.at(-1)
65
+ if (
66
+ previousSegment &&
67
+ previousSegment.overline === character.overline
68
+ ) {
69
+ previousSegment.text += character.text
70
+ continue
71
+ }
72
+
73
+ segments.push({
74
+ text: character.text,
75
+ overline: character.overline
76
+ })
77
+ }
78
+
79
+ return segments
80
+ }
81
+ }