altium-toolkit 1.1.22 → 1.1.24
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 +134 -23
- package/docs/model-format.md +174 -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 +511 -1
- 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 +2 -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/src/core/BinaryReader.mjs +213 -2
- package/src/core/altium/AltiumParser.mjs +352 -14
- package/src/core/altium/AltiumUnits.mjs +205 -0
- package/src/core/altium/AsciiRecordParser.mjs +9 -0
- 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/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/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/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/SchematicOwnershipGraphParser.mjs +102 -3
- package/src/core/altium/SchematicPinParser.mjs +12 -45
- package/src/core/altium/SchematicPrimitiveParser.mjs +9 -14
- 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 +64 -25
- 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/UnsupportedFeatureReportBuilder.mjs +380 -0
- package/src/parser.mjs +35 -1
- package/src/renderers.mjs +1 -0
- package/src/ui/SchematicShapeRenderer.mjs +49 -6
- package/src/ui/SchematicSvgRenderer.mjs +37 -8
- package/src/ui/SchematicTypography.mjs +4 -3
|
@@ -0,0 +1,663 @@
|
|
|
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
|
+
const { getDisplayText, getField, parseBoolean, parseNumericField, toColor } =
|
|
8
|
+
ParserUtils
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Preserves auxiliary schematic code-symbol records as a read-only sidecar.
|
|
12
|
+
*/
|
|
13
|
+
export class SchematicCodeSymbolParser {
|
|
14
|
+
static SCHEMA_ID = 'altium-toolkit.schematic.code-symbols.a1'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parses code-symbol family records from schematic records.
|
|
18
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex: number }[]} records Parsed records.
|
|
19
|
+
* @returns {{ schema: string, summary: object, symbols: object[], entries: object[], texts: object[], markers: object[] } | null}
|
|
20
|
+
*/
|
|
21
|
+
static parse(records) {
|
|
22
|
+
const symbols = records
|
|
23
|
+
.map((record) =>
|
|
24
|
+
SchematicCodeSymbolParser.#parseSymbolRecord(record)
|
|
25
|
+
)
|
|
26
|
+
.filter(Boolean)
|
|
27
|
+
const symbolLookup =
|
|
28
|
+
SchematicCodeSymbolParser.#buildSymbolLookup(symbols)
|
|
29
|
+
const entries = records
|
|
30
|
+
.map((record) =>
|
|
31
|
+
SchematicCodeSymbolParser.#parseEntryRecord(
|
|
32
|
+
record,
|
|
33
|
+
symbolLookup
|
|
34
|
+
)
|
|
35
|
+
)
|
|
36
|
+
.filter(Boolean)
|
|
37
|
+
const texts = records
|
|
38
|
+
.map((record) =>
|
|
39
|
+
SchematicCodeSymbolParser.#parseTextRecord(record, symbolLookup)
|
|
40
|
+
)
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
const markers = records
|
|
43
|
+
.map((record) =>
|
|
44
|
+
SchematicCodeSymbolParser.#parseMarkerRecord(record)
|
|
45
|
+
)
|
|
46
|
+
.filter(Boolean)
|
|
47
|
+
|
|
48
|
+
if (
|
|
49
|
+
!symbols.length &&
|
|
50
|
+
!entries.length &&
|
|
51
|
+
!texts.length &&
|
|
52
|
+
!markers.length
|
|
53
|
+
) {
|
|
54
|
+
return null
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
SchematicCodeSymbolParser.#attachChildKeys(symbols, entries, texts)
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
schema: SchematicCodeSymbolParser.SCHEMA_ID,
|
|
61
|
+
summary: {
|
|
62
|
+
symbolCount: symbols.length,
|
|
63
|
+
entryCount: entries.length,
|
|
64
|
+
textCount: texts.length,
|
|
65
|
+
markerCount: markers.length
|
|
66
|
+
},
|
|
67
|
+
symbols: symbols.map((symbol) =>
|
|
68
|
+
SchematicCodeSymbolParser.#publicSymbol(symbol)
|
|
69
|
+
),
|
|
70
|
+
entries,
|
|
71
|
+
texts,
|
|
72
|
+
markers
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Parses one `RECORD=220` code symbol.
|
|
78
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex: number }} record Parsed record.
|
|
79
|
+
* @returns {object | null}
|
|
80
|
+
*/
|
|
81
|
+
static #parseSymbolRecord(record) {
|
|
82
|
+
if (getField(record.fields, 'RECORD') !== '220') {
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const indexInSheet = parseNumericField(record.fields, 'IndexInSheet')
|
|
87
|
+
const stableIndex =
|
|
88
|
+
indexInSheet === null ? record.recordIndex : indexInSheet
|
|
89
|
+
const routines = SchematicCodeSymbolParser.#parseRoutineRows(
|
|
90
|
+
record.fields
|
|
91
|
+
)
|
|
92
|
+
const internalMemory = SchematicCodeSymbolParser.#parseInternalMemory(
|
|
93
|
+
record.fields
|
|
94
|
+
)
|
|
95
|
+
const externalMemory =
|
|
96
|
+
SchematicCodeSymbolParser.#parseExternalMemoryRows(record.fields)
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
key: 'schematic-code-symbol-' + stableIndex,
|
|
100
|
+
recordKey: SchematicCodeSymbolParser.#recordKey(record),
|
|
101
|
+
recordId: 'record-' + stableIndex,
|
|
102
|
+
x: parseNumericField(record.fields, 'Location.X') || 0,
|
|
103
|
+
y: parseNumericField(record.fields, 'Location.Y') || 0,
|
|
104
|
+
width: parseNumericField(record.fields, 'XSize') || 0,
|
|
105
|
+
height: parseNumericField(record.fields, 'YSize') || 0,
|
|
106
|
+
color: toColor(record.fields.Color, '#2c3134'),
|
|
107
|
+
fill: toColor(record.fields.AreaColor, '#f6f0d8'),
|
|
108
|
+
isSolid: parseBoolean(record.fields.IsSolid),
|
|
109
|
+
...(getField(record.fields, 'SymbolType')
|
|
110
|
+
? { symbolType: getField(record.fields, 'SymbolType') }
|
|
111
|
+
: {}),
|
|
112
|
+
...(getField(record.fields, 'UniqueID') ||
|
|
113
|
+
getField(record.fields, 'UniqueId')
|
|
114
|
+
? {
|
|
115
|
+
uniqueId:
|
|
116
|
+
getField(record.fields, 'UniqueID') ||
|
|
117
|
+
getField(record.fields, 'UniqueId')
|
|
118
|
+
}
|
|
119
|
+
: {}),
|
|
120
|
+
...(routines.length ? { routines } : {}),
|
|
121
|
+
...(Object.keys(internalMemory).length ? { internalMemory } : {}),
|
|
122
|
+
...(externalMemory.length ? { externalMemory } : {}),
|
|
123
|
+
entryKeys: [],
|
|
124
|
+
textKeys: [],
|
|
125
|
+
sourceRecordIndex: record.recordIndex,
|
|
126
|
+
indexInSheet
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Parses one `RECORD=221` code symbol entry.
|
|
132
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex: number }} record Parsed record.
|
|
133
|
+
* @param {Map<string, object>} symbolLookup Symbol lookup by owner index.
|
|
134
|
+
* @returns {object | null}
|
|
135
|
+
*/
|
|
136
|
+
static #parseEntryRecord(record, symbolLookup) {
|
|
137
|
+
if (getField(record.fields, 'RECORD') !== '221') {
|
|
138
|
+
return null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const indexInSheet = parseNumericField(record.fields, 'IndexInSheet')
|
|
142
|
+
const stableIndex =
|
|
143
|
+
indexInSheet === null ? record.recordIndex : indexInSheet
|
|
144
|
+
const ownerIndex = getField(record.fields, 'OwnerIndex')
|
|
145
|
+
const parentSymbol = symbolLookup.get(ownerIndex)
|
|
146
|
+
const side = SchematicCodeSymbolParser.#resolveSide(
|
|
147
|
+
parseNumericField(record.fields, 'Side')
|
|
148
|
+
)
|
|
149
|
+
const distance = SchematicCodeSymbolParser.#parseEntryDistance(
|
|
150
|
+
record.fields
|
|
151
|
+
)
|
|
152
|
+
const point = parentSymbol
|
|
153
|
+
? SchematicCodeSymbolParser.#resolveEntryPoint(
|
|
154
|
+
parentSymbol,
|
|
155
|
+
side,
|
|
156
|
+
distance
|
|
157
|
+
)
|
|
158
|
+
: null
|
|
159
|
+
const entryType = parseNumericField(record.fields, 'EntryType')
|
|
160
|
+
const style = parseNumericField(record.fields, 'Style')
|
|
161
|
+
const textFontId = parseNumericField(record.fields, 'TextFontID')
|
|
162
|
+
const dataWidth = parseNumericField(record.fields, 'DataWidth')
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
key: 'schematic-code-entry-' + stableIndex,
|
|
166
|
+
recordKey: SchematicCodeSymbolParser.#recordKey(record),
|
|
167
|
+
...(parentSymbol ? { ownerSymbolKey: parentSymbol.key } : {}),
|
|
168
|
+
...(ownerIndex ? { ownerIndex } : {}),
|
|
169
|
+
...(getField(record.fields, 'Name')
|
|
170
|
+
? { name: getField(record.fields, 'Name') }
|
|
171
|
+
: {}),
|
|
172
|
+
...(getField(record.fields, 'DataIdentifier')
|
|
173
|
+
? {
|
|
174
|
+
dataIdentifier: getField(record.fields, 'DataIdentifier')
|
|
175
|
+
}
|
|
176
|
+
: {}),
|
|
177
|
+
...(getField(record.fields, 'DataType')
|
|
178
|
+
? { dataType: getField(record.fields, 'DataType') }
|
|
179
|
+
: {}),
|
|
180
|
+
...(dataWidth !== null ? { dataWidth } : {}),
|
|
181
|
+
side,
|
|
182
|
+
direction: SchematicCodeSymbolParser.#resolveDirection(
|
|
183
|
+
parseNumericField(record.fields, 'IOType')
|
|
184
|
+
),
|
|
185
|
+
...(entryType !== null ? { entryType } : {}),
|
|
186
|
+
...(style !== null ? { style } : {}),
|
|
187
|
+
...(point ? { x: point.x, y: point.y } : {}),
|
|
188
|
+
color: toColor(record.fields.Color, '#2c3134'),
|
|
189
|
+
fill: toColor(record.fields.AreaColor, '#f6f0d8'),
|
|
190
|
+
textColor: toColor(
|
|
191
|
+
record.fields.TextColor || record.fields.Color,
|
|
192
|
+
'#2c3134'
|
|
193
|
+
),
|
|
194
|
+
...(textFontId !== null ? { textFontId } : {}),
|
|
195
|
+
...(getField(record.fields, 'ParentRoutine')
|
|
196
|
+
? { parentRoutine: getField(record.fields, 'ParentRoutine') }
|
|
197
|
+
: {}),
|
|
198
|
+
...(parseBoolean(record.fields.OwnerIndexAdditionalList)
|
|
199
|
+
? { ownerIndexAdditionalList: true }
|
|
200
|
+
: {})
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Parses one `RECORD=222` or `RECORD=223` text row.
|
|
206
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex: number }} record Parsed record.
|
|
207
|
+
* @param {Map<string, object>} symbolLookup Symbol lookup by owner index.
|
|
208
|
+
* @returns {object | null}
|
|
209
|
+
*/
|
|
210
|
+
static #parseTextRecord(record, symbolLookup) {
|
|
211
|
+
const recordType = getField(record.fields, 'RECORD')
|
|
212
|
+
if (recordType !== '222' && recordType !== '223') {
|
|
213
|
+
return null
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const indexInSheet = parseNumericField(record.fields, 'IndexInSheet')
|
|
217
|
+
const stableIndex =
|
|
218
|
+
indexInSheet === null ? record.recordIndex : indexInSheet
|
|
219
|
+
const ownerIndex = getField(record.fields, 'OwnerIndex')
|
|
220
|
+
const parentSymbol = symbolLookup.get(ownerIndex)
|
|
221
|
+
const x = parseNumericField(record.fields, 'Location.X')
|
|
222
|
+
const y = parseNumericField(record.fields, 'Location.Y')
|
|
223
|
+
const fontId = parseNumericField(record.fields, 'FontID')
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
key: 'schematic-code-text-' + stableIndex,
|
|
227
|
+
recordKey: SchematicCodeSymbolParser.#recordKey(record),
|
|
228
|
+
...(parentSymbol ? { ownerSymbolKey: parentSymbol.key } : {}),
|
|
229
|
+
...(ownerIndex ? { ownerIndex } : {}),
|
|
230
|
+
kind: recordType === '222' ? 'title' : 'source',
|
|
231
|
+
text: getDisplayText(record.fields),
|
|
232
|
+
...(x !== null ? { x } : {}),
|
|
233
|
+
...(y !== null ? { y } : {}),
|
|
234
|
+
...(fontId !== null ? { fontId } : {}),
|
|
235
|
+
color: toColor(record.fields.Color, '#2c3134')
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Parses one `RECORD=210` marker row.
|
|
241
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex: number }} record Parsed record.
|
|
242
|
+
* @returns {object | null}
|
|
243
|
+
*/
|
|
244
|
+
static #parseMarkerRecord(record) {
|
|
245
|
+
if (getField(record.fields, 'RECORD') !== '210') {
|
|
246
|
+
return null
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const indexInSheet = parseNumericField(record.fields, 'IndexInSheet')
|
|
250
|
+
const stableIndex =
|
|
251
|
+
indexInSheet === null ? record.recordIndex : indexInSheet
|
|
252
|
+
const x = parseNumericField(record.fields, 'Location.X')
|
|
253
|
+
const y = parseNumericField(record.fields, 'Location.Y')
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
key: 'schematic-code-marker-' + stableIndex,
|
|
257
|
+
recordKey: SchematicCodeSymbolParser.#recordKey(record),
|
|
258
|
+
recordId: 'record-' + stableIndex,
|
|
259
|
+
...(getField(record.fields, 'Name')
|
|
260
|
+
? { name: getField(record.fields, 'Name') }
|
|
261
|
+
: {}),
|
|
262
|
+
...(x !== null ? { x } : {}),
|
|
263
|
+
...(y !== null ? { y } : {}),
|
|
264
|
+
color: toColor(record.fields.Color, '#2c3134'),
|
|
265
|
+
...(getField(record.fields, 'OwnerPartID') ||
|
|
266
|
+
getField(record.fields, 'OwnerPartId')
|
|
267
|
+
? {
|
|
268
|
+
ownerPartId:
|
|
269
|
+
getField(record.fields, 'OwnerPartID') ||
|
|
270
|
+
getField(record.fields, 'OwnerPartId')
|
|
271
|
+
}
|
|
272
|
+
: {})
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Parses indexed exported routine metadata.
|
|
278
|
+
* @param {Record<string, string | string[]>} fields Native fields.
|
|
279
|
+
* @returns {object[]}
|
|
280
|
+
*/
|
|
281
|
+
static #parseRoutineRows(fields) {
|
|
282
|
+
const count = SchematicCodeSymbolParser.#resolveIndexedRowCount(
|
|
283
|
+
fields,
|
|
284
|
+
'ExportedRoutineCount',
|
|
285
|
+
/^(?:RoutineName|InterfaceMode|DataWidth|AddressWidth|Scope|Mau|NoWait|IsLinked)(\d+)$/u
|
|
286
|
+
)
|
|
287
|
+
const rows = []
|
|
288
|
+
|
|
289
|
+
for (let index = 0; index < count; index += 1) {
|
|
290
|
+
const row = {
|
|
291
|
+
index,
|
|
292
|
+
...(getField(fields, 'RoutineName' + index)
|
|
293
|
+
? { name: getField(fields, 'RoutineName' + index) }
|
|
294
|
+
: {})
|
|
295
|
+
}
|
|
296
|
+
SchematicCodeSymbolParser.#assignNumericField(
|
|
297
|
+
row,
|
|
298
|
+
'interfaceMode',
|
|
299
|
+
fields,
|
|
300
|
+
'InterfaceMode' + index
|
|
301
|
+
)
|
|
302
|
+
SchematicCodeSymbolParser.#assignNumericField(
|
|
303
|
+
row,
|
|
304
|
+
'dataWidth',
|
|
305
|
+
fields,
|
|
306
|
+
'DataWidth' + index
|
|
307
|
+
)
|
|
308
|
+
SchematicCodeSymbolParser.#assignNumericField(
|
|
309
|
+
row,
|
|
310
|
+
'addressWidth',
|
|
311
|
+
fields,
|
|
312
|
+
'AddressWidth' + index
|
|
313
|
+
)
|
|
314
|
+
SchematicCodeSymbolParser.#assignNumericField(
|
|
315
|
+
row,
|
|
316
|
+
'scope',
|
|
317
|
+
fields,
|
|
318
|
+
'Scope' + index
|
|
319
|
+
)
|
|
320
|
+
SchematicCodeSymbolParser.#assignNumericField(
|
|
321
|
+
row,
|
|
322
|
+
'mau',
|
|
323
|
+
fields,
|
|
324
|
+
'Mau' + index
|
|
325
|
+
)
|
|
326
|
+
SchematicCodeSymbolParser.#assignBooleanField(
|
|
327
|
+
row,
|
|
328
|
+
'noWait',
|
|
329
|
+
fields,
|
|
330
|
+
'NoWait' + index
|
|
331
|
+
)
|
|
332
|
+
SchematicCodeSymbolParser.#assignBooleanField(
|
|
333
|
+
row,
|
|
334
|
+
'isLinked',
|
|
335
|
+
fields,
|
|
336
|
+
'IsLinked' + index
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
if (Object.keys(row).length > 1) {
|
|
340
|
+
rows.push(row)
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return rows
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* Parses code-symbol internal memory summary fields.
|
|
349
|
+
* @param {Record<string, string | string[]>} fields Native fields.
|
|
350
|
+
* @returns {object}
|
|
351
|
+
*/
|
|
352
|
+
static #parseInternalMemory(fields) {
|
|
353
|
+
const row = {}
|
|
354
|
+
SchematicCodeSymbolParser.#assignNumericField(
|
|
355
|
+
row,
|
|
356
|
+
'count',
|
|
357
|
+
fields,
|
|
358
|
+
'InternalMemoryCount'
|
|
359
|
+
)
|
|
360
|
+
SchematicCodeSymbolParser.#assignNumericField(
|
|
361
|
+
row,
|
|
362
|
+
'size',
|
|
363
|
+
fields,
|
|
364
|
+
'InternalMemorySize'
|
|
365
|
+
)
|
|
366
|
+
SchematicCodeSymbolParser.#assignNumericField(
|
|
367
|
+
row,
|
|
368
|
+
'interfaceMode',
|
|
369
|
+
fields,
|
|
370
|
+
'InternalMemoryInterface'
|
|
371
|
+
)
|
|
372
|
+
SchematicCodeSymbolParser.#assignNumericField(
|
|
373
|
+
row,
|
|
374
|
+
'dataWidth',
|
|
375
|
+
fields,
|
|
376
|
+
'InternalMemoryDataWidth'
|
|
377
|
+
)
|
|
378
|
+
SchematicCodeSymbolParser.#assignNumericField(
|
|
379
|
+
row,
|
|
380
|
+
'addressWidth',
|
|
381
|
+
fields,
|
|
382
|
+
'InternalMemoryAddressWidth'
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
return row
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Parses indexed external memory metadata.
|
|
390
|
+
* @param {Record<string, string | string[]>} fields Native fields.
|
|
391
|
+
* @returns {object[]}
|
|
392
|
+
*/
|
|
393
|
+
static #parseExternalMemoryRows(fields) {
|
|
394
|
+
const count = SchematicCodeSymbolParser.#resolveIndexedRowCount(
|
|
395
|
+
fields,
|
|
396
|
+
'ExternalMemoryCount',
|
|
397
|
+
/^ExternalMemory_(?:Name|Interface|DataWidth|AddressWidth|Scope|Mau|IsReserved)(\d+)$/u
|
|
398
|
+
)
|
|
399
|
+
const rows = []
|
|
400
|
+
|
|
401
|
+
for (let index = 0; index < count; index += 1) {
|
|
402
|
+
const row = {
|
|
403
|
+
index,
|
|
404
|
+
...(getField(fields, 'ExternalMemory_Name' + index)
|
|
405
|
+
? {
|
|
406
|
+
name: getField(fields, 'ExternalMemory_Name' + index)
|
|
407
|
+
}
|
|
408
|
+
: {})
|
|
409
|
+
}
|
|
410
|
+
SchematicCodeSymbolParser.#assignNumericField(
|
|
411
|
+
row,
|
|
412
|
+
'interfaceMode',
|
|
413
|
+
fields,
|
|
414
|
+
'ExternalMemory_Interface' + index
|
|
415
|
+
)
|
|
416
|
+
SchematicCodeSymbolParser.#assignNumericField(
|
|
417
|
+
row,
|
|
418
|
+
'dataWidth',
|
|
419
|
+
fields,
|
|
420
|
+
'ExternalMemory_DataWidth' + index
|
|
421
|
+
)
|
|
422
|
+
SchematicCodeSymbolParser.#assignNumericField(
|
|
423
|
+
row,
|
|
424
|
+
'addressWidth',
|
|
425
|
+
fields,
|
|
426
|
+
'ExternalMemory_AddressWidth' + index
|
|
427
|
+
)
|
|
428
|
+
SchematicCodeSymbolParser.#assignNumericField(
|
|
429
|
+
row,
|
|
430
|
+
'scope',
|
|
431
|
+
fields,
|
|
432
|
+
'ExternalMemory_Scope' + index
|
|
433
|
+
)
|
|
434
|
+
SchematicCodeSymbolParser.#assignNumericField(
|
|
435
|
+
row,
|
|
436
|
+
'mau',
|
|
437
|
+
fields,
|
|
438
|
+
'ExternalMemory_Mau' + index
|
|
439
|
+
)
|
|
440
|
+
SchematicCodeSymbolParser.#assignBooleanField(
|
|
441
|
+
row,
|
|
442
|
+
'isReserved',
|
|
443
|
+
fields,
|
|
444
|
+
'ExternalMemory_IsReserved' + index
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
if (Object.keys(row).length > 1) {
|
|
448
|
+
rows.push(row)
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return rows
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Resolves indexed row count from an explicit count or discovered fields.
|
|
457
|
+
* @param {Record<string, string | string[]>} fields Native fields.
|
|
458
|
+
* @param {string} countKey Explicit count field.
|
|
459
|
+
* @param {RegExp} pattern Indexed field matcher with an index capture.
|
|
460
|
+
* @returns {number}
|
|
461
|
+
*/
|
|
462
|
+
static #resolveIndexedRowCount(fields, countKey, pattern) {
|
|
463
|
+
const explicitCount = parseNumericField(fields, countKey)
|
|
464
|
+
let discoveredCount = 0
|
|
465
|
+
|
|
466
|
+
for (const key of Object.keys(fields || {})) {
|
|
467
|
+
const match = key.match(pattern)
|
|
468
|
+
if (!match) continue
|
|
469
|
+
const index = Number(match[1])
|
|
470
|
+
if (Number.isInteger(index)) {
|
|
471
|
+
discoveredCount = Math.max(discoveredCount, index + 1)
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return Math.max(explicitCount || 0, discoveredCount)
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Adds a numeric field to a row when present.
|
|
480
|
+
* @param {object} row Target row.
|
|
481
|
+
* @param {string} property Public property.
|
|
482
|
+
* @param {Record<string, string | string[]>} fields Native fields.
|
|
483
|
+
* @param {string} fieldName Native field name.
|
|
484
|
+
* @returns {void}
|
|
485
|
+
*/
|
|
486
|
+
static #assignNumericField(row, property, fields, fieldName) {
|
|
487
|
+
const value = parseNumericField(fields, fieldName)
|
|
488
|
+
if (value !== null) {
|
|
489
|
+
row[property] = value
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Adds a boolean field to a row when the source field exists.
|
|
495
|
+
* @param {object} row Target row.
|
|
496
|
+
* @param {string} property Public property.
|
|
497
|
+
* @param {Record<string, string | string[]>} fields Native fields.
|
|
498
|
+
* @param {string} fieldName Native field name.
|
|
499
|
+
* @returns {void}
|
|
500
|
+
*/
|
|
501
|
+
static #assignBooleanField(row, property, fields, fieldName) {
|
|
502
|
+
if (getField(fields, fieldName)) {
|
|
503
|
+
row[property] = parseBoolean(fields[fieldName])
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Builds a lookup that tolerates owner-index variants.
|
|
509
|
+
* @param {{ key: string, sourceRecordIndex: number, indexInSheet: number | null }[]} symbols Parsed symbols.
|
|
510
|
+
* @returns {Map<string, object>}
|
|
511
|
+
*/
|
|
512
|
+
static #buildSymbolLookup(symbols) {
|
|
513
|
+
const lookup = new Map()
|
|
514
|
+
|
|
515
|
+
for (const symbol of symbols) {
|
|
516
|
+
const candidateKeys = new Set([
|
|
517
|
+
String(symbol.sourceRecordIndex),
|
|
518
|
+
String(symbol.sourceRecordIndex + 1)
|
|
519
|
+
])
|
|
520
|
+
|
|
521
|
+
if (symbol.indexInSheet !== null) {
|
|
522
|
+
candidateKeys.add(String(symbol.indexInSheet))
|
|
523
|
+
candidateKeys.add(String(symbol.indexInSheet + 1))
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
for (const key of candidateKeys) {
|
|
527
|
+
lookup.set(key, symbol)
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
return lookup
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Attaches entry and text references to parent symbols.
|
|
536
|
+
* @param {object[]} symbols Parsed symbols.
|
|
537
|
+
* @param {object[]} entries Parsed entries.
|
|
538
|
+
* @param {object[]} texts Parsed texts.
|
|
539
|
+
* @returns {void}
|
|
540
|
+
*/
|
|
541
|
+
static #attachChildKeys(symbols, entries, texts) {
|
|
542
|
+
const symbolsByKey = new Map(
|
|
543
|
+
symbols.map((symbol) => [symbol.key, symbol])
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
for (const entry of entries) {
|
|
547
|
+
const symbol = symbolsByKey.get(entry.ownerSymbolKey)
|
|
548
|
+
if (symbol) {
|
|
549
|
+
symbol.entryKeys.push(entry.key)
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
for (const text of texts) {
|
|
554
|
+
const symbol = symbolsByKey.get(text.ownerSymbolKey)
|
|
555
|
+
if (symbol) {
|
|
556
|
+
symbol.textKeys.push(text.key)
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Removes internal lookup metadata from a public symbol row.
|
|
563
|
+
* @param {object} symbol Internal symbol row.
|
|
564
|
+
* @returns {object}
|
|
565
|
+
*/
|
|
566
|
+
static #publicSymbol(symbol) {
|
|
567
|
+
const { sourceRecordIndex, indexInSheet, ...publicSymbol } = symbol
|
|
568
|
+
return publicSymbol
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Parses an entry distance field and optional fractional companion.
|
|
573
|
+
* @param {Record<string, string | string[]>} fields Native fields.
|
|
574
|
+
* @returns {number}
|
|
575
|
+
*/
|
|
576
|
+
static #parseEntryDistance(fields) {
|
|
577
|
+
const whole = parseNumericField(fields, 'DistanceFromTop') || 0
|
|
578
|
+
const fraction = parseNumericField(fields, 'DistanceFromTop_FRAC1') || 0
|
|
579
|
+
const sign = whole < 0 ? -1 : 1
|
|
580
|
+
|
|
581
|
+
return whole * 10 + (fraction / 100000) * sign
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Resolves an entry point on the parent symbol perimeter.
|
|
586
|
+
* @param {{ x: number, y: number, width: number, height: number }} parentSymbol Parent symbol.
|
|
587
|
+
* @param {'left' | 'right' | 'top' | 'bottom'} side Symbol side.
|
|
588
|
+
* @param {number} distance Distance from the side origin.
|
|
589
|
+
* @returns {{ x: number, y: number }}
|
|
590
|
+
*/
|
|
591
|
+
static #resolveEntryPoint(parentSymbol, side, distance) {
|
|
592
|
+
switch (side) {
|
|
593
|
+
case 'right':
|
|
594
|
+
return {
|
|
595
|
+
x: parentSymbol.x + parentSymbol.width,
|
|
596
|
+
y: parentSymbol.y - distance
|
|
597
|
+
}
|
|
598
|
+
case 'top':
|
|
599
|
+
return {
|
|
600
|
+
x: parentSymbol.x + distance,
|
|
601
|
+
y: parentSymbol.y
|
|
602
|
+
}
|
|
603
|
+
case 'bottom':
|
|
604
|
+
return {
|
|
605
|
+
x: parentSymbol.x + distance,
|
|
606
|
+
y: parentSymbol.y - parentSymbol.height
|
|
607
|
+
}
|
|
608
|
+
case 'left':
|
|
609
|
+
default:
|
|
610
|
+
return {
|
|
611
|
+
x: parentSymbol.x,
|
|
612
|
+
y: parentSymbol.y - distance
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Resolves a side code into a public side label.
|
|
619
|
+
* @param {number | null} side Native side code.
|
|
620
|
+
* @returns {'left' | 'right' | 'top' | 'bottom'}
|
|
621
|
+
*/
|
|
622
|
+
static #resolveSide(side) {
|
|
623
|
+
switch (side) {
|
|
624
|
+
case 1:
|
|
625
|
+
return 'right'
|
|
626
|
+
case 2:
|
|
627
|
+
return 'top'
|
|
628
|
+
case 3:
|
|
629
|
+
return 'bottom'
|
|
630
|
+
case 0:
|
|
631
|
+
default:
|
|
632
|
+
return 'left'
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Resolves an I/O code into a public direction label.
|
|
638
|
+
* @param {number | null} ioType Native I/O code.
|
|
639
|
+
* @returns {'unspecified' | 'output' | 'input' | 'bidirectional'}
|
|
640
|
+
*/
|
|
641
|
+
static #resolveDirection(ioType) {
|
|
642
|
+
switch (ioType) {
|
|
643
|
+
case 1:
|
|
644
|
+
return 'output'
|
|
645
|
+
case 2:
|
|
646
|
+
return 'input'
|
|
647
|
+
case 3:
|
|
648
|
+
return 'bidirectional'
|
|
649
|
+
case 0:
|
|
650
|
+
default:
|
|
651
|
+
return 'unspecified'
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/**
|
|
656
|
+
* Builds a stable schematic record key.
|
|
657
|
+
* @param {{ recordIndex?: number }} record Parsed record.
|
|
658
|
+
* @returns {string}
|
|
659
|
+
*/
|
|
660
|
+
static #recordKey(record) {
|
|
661
|
+
return 'schematic-record-' + String(record?.recordIndex ?? 0)
|
|
662
|
+
}
|
|
663
|
+
}
|