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.
- package/README.md +34 -5
- package/docs/api.md +171 -23
- package/docs/model-format.md +192 -20
- package/docs/schemas/altium_toolkit/embedded_assets_a1.schema.json +56 -0
- package/docs/schemas/altium_toolkit/fixture_coverage_matrix_a1.schema.json +89 -0
- package/docs/schemas/altium_toolkit/geometry_bounds_a1.schema.json +86 -0
- package/docs/schemas/altium_toolkit/library_catalog_a1.schema.json +65 -0
- package/docs/schemas/altium_toolkit/library_diff_a1.schema.json +54 -0
- package/docs/schemas/altium_toolkit/library_inspection_a1.schema.json +94 -0
- package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +4 -0
- package/docs/schemas/altium_toolkit/native_stream_inventory_a1.schema.json +66 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +513 -3
- package/docs/schemas/altium_toolkit/parameter_record_inventory_a1.schema.json +84 -0
- package/docs/schemas/altium_toolkit/parser_diagnostics_a1.schema.json +63 -0
- package/docs/schemas/altium_toolkit/parser_value_verification_a1.schema.json +74 -0
- package/docs/schemas/altium_toolkit/pcb_class_report_a1.schema.json +79 -0
- package/docs/schemas/altium_toolkit/pcb_inspection_a1.schema.json +65 -0
- package/docs/schemas/altium_toolkit/pcb_net_membership_a1.schema.json +98 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +3 -0
- package/docs/schemas/altium_toolkit/project_hierarchy_a1.schema.json +79 -0
- package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +212 -0
- package/docs/testing.md +7 -0
- package/examples/README.md +21 -0
- package/examples/cli-utils.mjs +148 -0
- package/examples/corpus-smoke.mjs +523 -0
- package/examples/extract-bom.mjs +47 -0
- package/examples/generate-pnp.mjs +59 -0
- package/examples/inspect-board.mjs +70 -0
- package/examples/inspect-schematic.mjs +406 -0
- package/examples/library-catalog.mjs +115 -0
- package/examples/net-report.mjs +61 -0
- package/examples/validate-library.mjs +59 -0
- package/package.json +1 -1
- package/spec/library-scope.md +5 -0
- package/src/core/BinaryReader.mjs +213 -2
- package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
- package/src/core/altium/AltiumParser.mjs +357 -16
- package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
- package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
- package/src/core/altium/AltiumUnits.mjs +205 -0
- package/src/core/altium/AsciiRecordParser.mjs +52 -11
- package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
- package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
- package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
- package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
- package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
- package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
- package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
- package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
- package/src/core/altium/NormalizedModelSchema.mjs +3 -31
- package/src/core/altium/ParameterCollection.mjs +431 -0
- package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
- package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
- package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
- package/src/core/altium/ParserErrors.mjs +90 -0
- package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
- package/src/core/altium/ParserUtils.mjs +24 -0
- package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
- package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
- package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
- package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
- package/src/core/altium/PcbLayerGroups.mjs +308 -0
- package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
- package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
- package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
- package/src/core/altium/PcbLibModelParser.mjs +20 -4
- package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
- package/src/core/altium/PcbModelParser.mjs +223 -4
- package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
- package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
- package/src/core/altium/PcbStreamExtractor.mjs +130 -6
- package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
- package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
- package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
- package/src/core/altium/SchLibModelParser.mjs +840 -0
- package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
- package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
- package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
- package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
- package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
- package/src/core/altium/SchematicHarnessParser.mjs +9 -3
- package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
- package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
- package/src/core/altium/SchematicPinParser.mjs +99 -65
- package/src/core/altium/SchematicPrimitiveParser.mjs +125 -22
- package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
- package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
- package/src/core/altium/SchematicSheetParser.mjs +8 -2
- package/src/core/altium/SchematicStreamExtractor.mjs +107 -21
- package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
- package/src/core/altium/SchematicTextParser.mjs +28 -12
- package/src/core/altium/SchematicTextRunParser.mjs +81 -0
- package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
- package/src/core/altium/SourceBundleExporter.mjs +156 -0
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
- package/src/core/altium/SourceComponentClient.mjs +239 -0
- package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
- package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
- package/src/parser.mjs +43 -1
- package/src/renderers.mjs +1 -0
- package/src/styles/altium-renderers.css +6 -6
- package/src/ui/PcbArcUtils.mjs +19 -2
- package/src/ui/PcbScene3dBuilder.mjs +202 -20
- package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
- package/src/ui/SchematicColorResolver.mjs +185 -0
- package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
- package/src/ui/SchematicLineColorResolver.mjs +88 -0
- package/src/ui/SchematicNoteRenderer.mjs +5 -1
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +269 -8
- package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +229 -62
- package/src/ui/SchematicShapeRenderer.mjs +86 -17
- package/src/ui/SchematicSvgRenderer.mjs +980 -58
- 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',
|
|
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
|
|
62
|
+
let fileHeaderParse
|
|
59
63
|
|
|
60
64
|
try {
|
|
61
|
-
|
|
65
|
+
fileHeaderParse = SchematicStreamExtractor.#parseStreamRecords(
|
|
66
|
+
compoundDocument,
|
|
67
|
+
'FileHeader'
|
|
68
|
+
)
|
|
62
69
|
} catch {
|
|
63
70
|
return null
|
|
64
71
|
}
|
|
65
72
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
+
}
|