altium-toolkit 1.0.7 → 1.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -6
- package/docs/api.md +78 -16
- package/docs/model-format.md +229 -8
- package/docs/schemas/altium_toolkit/netlist_a1.schema.json +47 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1661 -104
- package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +59 -0
- package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +57 -0
- package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
- package/docs/testing.md +9 -3
- package/package.json +1 -1
- package/spec/library-scope.md +7 -1
- package/src/core/altium/AltiumLayoutParser.mjs +104 -8
- package/src/core/altium/AltiumParser.mjs +191 -45
- package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
- package/src/core/altium/IntLibModelParser.mjs +240 -0
- package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
- package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
- package/src/core/altium/LibrarySearchIndex.mjs +215 -0
- package/src/core/altium/NormalizedModelSchema.mjs +36 -0
- package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
- package/src/core/altium/PcbDefaultsParser.mjs +171 -0
- package/src/core/altium/PcbDimensionParser.mjs +229 -0
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
- package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
- package/src/core/altium/PcbLibModelParser.mjs +235 -14
- package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
- package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
- package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
- package/src/core/altium/PcbModelParser.mjs +466 -28
- package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
- package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
- package/src/core/altium/PcbPadStackParser.mjs +58 -0
- package/src/core/altium/PcbPickPlacePositionResolver.mjs +217 -0
- package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
- package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
- package/src/core/altium/PcbRegionPrimitiveParser.mjs +5 -1
- package/src/core/altium/PcbRuleParser.mjs +354 -33
- package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
- package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
- package/src/core/altium/PcbStatisticsBuilder.mjs +532 -0
- package/src/core/altium/PcbStreamExtractor.mjs +111 -4
- package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
- package/src/core/altium/PcbUnionParser.mjs +307 -0
- package/src/core/altium/PcbViaStackParser.mjs +98 -10
- package/src/core/altium/PcbViaStructureParser.mjs +335 -0
- package/src/core/altium/PrintableTextDecoder.mjs +53 -3
- package/src/core/altium/PrjPcbModelParser.mjs +257 -5
- package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
- package/src/core/altium/ProjectDesignBundleBuilder.mjs +477 -0
- package/src/core/altium/ProjectNetlistExporter.mjs +499 -0
- package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
- package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
- package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
- package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
- package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
- package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
- package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
- package/src/core/altium/SchematicHarnessParser.mjs +302 -0
- package/src/core/altium/SchematicImageParser.mjs +474 -3
- package/src/core/altium/SchematicImplementationParser.mjs +518 -0
- package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
- package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
- package/src/core/altium/SchematicPinParser.mjs +84 -1
- package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
- package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
- package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
- package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
- package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
- package/src/core/altium/SchematicTemplateParser.mjs +256 -0
- package/src/core/altium/SchematicTextParser.mjs +123 -0
- package/src/core/ole/OleCompoundDocument.mjs +20 -0
- package/src/parser.mjs +29 -0
- package/src/renderers.mjs +3 -0
- package/src/styles/altium-renderers.css +25 -0
- package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
- package/src/ui/PcbInteractionGeometry.mjs +350 -0
- package/src/ui/PcbInteractionIndex.mjs +593 -0
- package/src/ui/PcbInteractionItemRegistry.mjs +66 -0
- package/src/ui/PcbInteractionLayerModel.mjs +99 -0
- package/src/ui/PcbScene3dBoardOutlineRefiner.mjs +74 -9
- package/src/ui/PcbScene3dBuilder.mjs +169 -7
- package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
- package/src/ui/PcbSvgRenderer.mjs +1187 -34
- package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
- package/src/ui/SchematicNoteRenderer.mjs +9 -2
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
- package/src/ui/SchematicShapeRenderer.mjs +362 -0
- package/src/ui/SchematicSvgRenderer.mjs +1442 -92
- package/src/ui/SchematicTypography.mjs +48 -5
- package/src/ui/TextGeometrySidecarBuilder.mjs +147 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Provides stable schematic record type descriptors for parser consumers.
|
|
7
|
+
*/
|
|
8
|
+
export class SchematicRecordTypeRegistry {
|
|
9
|
+
static #RECORD_TYPES = new Map([
|
|
10
|
+
[0, ['file-header', 'metadata', true]],
|
|
11
|
+
[1, ['component', 'component', true]],
|
|
12
|
+
[2, ['pin', 'component', true]],
|
|
13
|
+
[3, ['ieee-symbol', 'symbol', true]],
|
|
14
|
+
[4, ['label', 'annotation', true]],
|
|
15
|
+
[5, ['bezier', 'graphic', true]],
|
|
16
|
+
[6, ['polyline', 'graphic', true]],
|
|
17
|
+
[7, ['polygon', 'graphic', true]],
|
|
18
|
+
[8, ['ellipse', 'graphic', true]],
|
|
19
|
+
[9, ['pie-chart', 'graphic', true]],
|
|
20
|
+
[10, ['rounded-rectangle', 'graphic', true]],
|
|
21
|
+
[11, ['elliptical-arc', 'graphic', true]],
|
|
22
|
+
[12, ['arc', 'graphic', true]],
|
|
23
|
+
[13, ['line', 'graphic', true]],
|
|
24
|
+
[14, ['rectangle', 'graphic', true]],
|
|
25
|
+
[15, ['sheet-symbol', 'sheet', true]],
|
|
26
|
+
[16, ['sheet-entry', 'sheet', true]],
|
|
27
|
+
[17, ['power-port', 'connectivity', true]],
|
|
28
|
+
[18, ['port', 'connectivity', true]],
|
|
29
|
+
[22, ['no-erc', 'directive', true]],
|
|
30
|
+
[25, ['net-label', 'connectivity', true]],
|
|
31
|
+
[26, ['bus', 'connectivity', true]],
|
|
32
|
+
[27, ['wire', 'connectivity', true]],
|
|
33
|
+
[28, ['text-frame', 'annotation', true]],
|
|
34
|
+
[29, ['junction', 'connectivity', true]],
|
|
35
|
+
[30, ['image', 'graphic', true]],
|
|
36
|
+
[31, ['sheet', 'sheet', true]],
|
|
37
|
+
[32, ['sheet-name', 'sheet', true]],
|
|
38
|
+
[33, ['file-name', 'sheet', true]],
|
|
39
|
+
[34, ['designator', 'component', true]],
|
|
40
|
+
[37, ['bus-entry', 'connectivity', true]],
|
|
41
|
+
[39, ['template', 'metadata', true]],
|
|
42
|
+
[41, ['parameter', 'component', true]],
|
|
43
|
+
[43, ['parameter-set', 'directive', true]],
|
|
44
|
+
[44, ['implementation-list', 'implementation', true]],
|
|
45
|
+
[45, ['implementation', 'implementation', true]],
|
|
46
|
+
[46, ['map-definer-list', 'implementation', true]],
|
|
47
|
+
[47, ['map-definer', 'implementation', true]],
|
|
48
|
+
[48, ['implementation-parameters', 'implementation', true]],
|
|
49
|
+
[209, ['note', 'annotation', true]],
|
|
50
|
+
[211, ['compile-mask', 'directive', true]],
|
|
51
|
+
[215, ['harness-connector', 'harness', true]],
|
|
52
|
+
[216, ['harness-entry', 'harness', true]],
|
|
53
|
+
[217, ['harness-type', 'harness', true]],
|
|
54
|
+
[218, ['signal-harness', 'harness', true]],
|
|
55
|
+
[225, ['blanket', 'directive', true]],
|
|
56
|
+
[226, ['hyperlink', 'annotation', false]]
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Lists every known schematic record descriptor.
|
|
61
|
+
* @returns {{ recordType: number, name: string, family: string, supported: boolean }[]}
|
|
62
|
+
*/
|
|
63
|
+
static list() {
|
|
64
|
+
return [...SchematicRecordTypeRegistry.#RECORD_TYPES.keys()]
|
|
65
|
+
.sort((left, right) => left - right)
|
|
66
|
+
.map((recordType) => SchematicRecordTypeRegistry.get(recordType))
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Returns the descriptor for one record type.
|
|
71
|
+
* @param {number | string} recordType
|
|
72
|
+
* @returns {{ recordType: number, name: string, family: string, supported: boolean }}
|
|
73
|
+
*/
|
|
74
|
+
static get(recordType) {
|
|
75
|
+
const normalizedRecordType =
|
|
76
|
+
SchematicRecordTypeRegistry.#normalizeRecordType(recordType)
|
|
77
|
+
const descriptor =
|
|
78
|
+
SchematicRecordTypeRegistry.#RECORD_TYPES.get(normalizedRecordType)
|
|
79
|
+
|
|
80
|
+
if (!descriptor) {
|
|
81
|
+
return {
|
|
82
|
+
recordType: normalizedRecordType,
|
|
83
|
+
name: 'unknown-' + normalizedRecordType,
|
|
84
|
+
family: 'unknown',
|
|
85
|
+
supported: false
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
recordType: normalizedRecordType,
|
|
91
|
+
name: descriptor[0],
|
|
92
|
+
family: descriptor[1],
|
|
93
|
+
supported: descriptor[2]
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Summarizes record counts by known type descriptor.
|
|
99
|
+
* @param {{ fields?: Record<string, string | string[]> }[]} records
|
|
100
|
+
* @returns {{ recordType: number, name: string, family: string, supported: boolean, count: number }[]}
|
|
101
|
+
*/
|
|
102
|
+
static summarize(records) {
|
|
103
|
+
const counts = new Map()
|
|
104
|
+
|
|
105
|
+
for (const record of records || []) {
|
|
106
|
+
const recordType = SchematicRecordTypeRegistry.#normalizeRecordType(
|
|
107
|
+
record?.fields?.RECORD
|
|
108
|
+
)
|
|
109
|
+
if (recordType < 0) {
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
counts.set(recordType, (counts.get(recordType) || 0) + 1)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return [...counts.entries()]
|
|
117
|
+
.sort(([left], [right]) => left - right)
|
|
118
|
+
.map(([recordType, count]) => ({
|
|
119
|
+
...SchematicRecordTypeRegistry.get(recordType),
|
|
120
|
+
count
|
|
121
|
+
}))
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Normalizes one record type field value.
|
|
126
|
+
* @param {number | string | undefined | string[]} recordType
|
|
127
|
+
* @returns {number}
|
|
128
|
+
*/
|
|
129
|
+
static #normalizeRecordType(recordType) {
|
|
130
|
+
const rawValue = Array.isArray(recordType) ? recordType[0] : recordType
|
|
131
|
+
const normalizedRecordType = Number(rawValue)
|
|
132
|
+
|
|
133
|
+
return Number.isInteger(normalizedRecordType)
|
|
134
|
+
? normalizedRecordType
|
|
135
|
+
: -1
|
|
136
|
+
}
|
|
137
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
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 { getField, parseNumericField } = ParserUtils
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Parses schematic repeated-channel declarations and derived net aliases.
|
|
11
|
+
*/
|
|
12
|
+
export class SchematicRepeatedChannelParser {
|
|
13
|
+
static SCHEMA_ID = 'altium-toolkit.schematic.repeated-channels.a1'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parses repeated-channel rooms and sheet-entry aliases.
|
|
17
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex?: number }[]} records Schematic records.
|
|
18
|
+
* @returns {object | null}
|
|
19
|
+
*/
|
|
20
|
+
static parse(records) {
|
|
21
|
+
const sheetSymbols = (records || []).filter(
|
|
22
|
+
(record) => getField(record.fields, 'RECORD') === '15'
|
|
23
|
+
)
|
|
24
|
+
const sheetEntries = (records || []).filter(
|
|
25
|
+
(record) => getField(record.fields, 'RECORD') === '16'
|
|
26
|
+
)
|
|
27
|
+
const rooms = sheetSymbols
|
|
28
|
+
.map((record) => SchematicRepeatedChannelParser.#room(record))
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
const netAliases = rooms.flatMap((room) =>
|
|
31
|
+
SchematicRepeatedChannelParser.#netAliases(
|
|
32
|
+
room,
|
|
33
|
+
sheetEntries.filter(
|
|
34
|
+
(entry) =>
|
|
35
|
+
getField(entry.fields, 'OwnerIndex') === room.ownerIndex
|
|
36
|
+
)
|
|
37
|
+
)
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if (!rooms.length && !netAliases.length) {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
schema: SchematicRepeatedChannelParser.SCHEMA_ID,
|
|
46
|
+
rooms: rooms.map(({ ownerIndex, ...room }) => room),
|
|
47
|
+
netAliases
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Parses one repeated sheet-symbol declaration.
|
|
53
|
+
* @param {object} record Sheet-symbol record.
|
|
54
|
+
* @returns {object | null}
|
|
55
|
+
*/
|
|
56
|
+
static #room(record) {
|
|
57
|
+
const repeat = SchematicRepeatedChannelParser.#parseRepeatRange(
|
|
58
|
+
getField(record.fields, 'Name') ||
|
|
59
|
+
getField(record.fields, 'SheetName') ||
|
|
60
|
+
getField(record.fields, 'Designator')
|
|
61
|
+
)
|
|
62
|
+
if (!repeat) {
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const indexInSheet =
|
|
67
|
+
parseNumericField(record.fields, 'IndexInSheet') ??
|
|
68
|
+
record.recordIndex ??
|
|
69
|
+
0
|
|
70
|
+
const designatorTemplate =
|
|
71
|
+
getField(record.fields, 'Designator') ||
|
|
72
|
+
'$ChannelPrefix$ChannelIndex'
|
|
73
|
+
const instances = []
|
|
74
|
+
|
|
75
|
+
for (
|
|
76
|
+
let index = repeat.startIndex;
|
|
77
|
+
index <= repeat.endIndex;
|
|
78
|
+
index += 1
|
|
79
|
+
) {
|
|
80
|
+
const alpha = SchematicRepeatedChannelParser.#alpha(index)
|
|
81
|
+
const channelPrefix = repeat.channelName
|
|
82
|
+
const designator = SchematicRepeatedChannelParser.#formatDesignator(
|
|
83
|
+
designatorTemplate,
|
|
84
|
+
{
|
|
85
|
+
channelPrefix,
|
|
86
|
+
channelIndex: index,
|
|
87
|
+
channelAlpha: alpha
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
instances.push({
|
|
92
|
+
index,
|
|
93
|
+
alpha,
|
|
94
|
+
channelPrefix,
|
|
95
|
+
designator,
|
|
96
|
+
hierarchyPath: designator
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
key: 'repeated-channel-' + String(indexInSheet),
|
|
102
|
+
sheetSymbolKey: SchematicRepeatedChannelParser.#recordKey(record),
|
|
103
|
+
ownerIndex: String(indexInSheet),
|
|
104
|
+
channelName: repeat.channelName,
|
|
105
|
+
startIndex: repeat.startIndex,
|
|
106
|
+
endIndex: repeat.endIndex,
|
|
107
|
+
designatorTemplate,
|
|
108
|
+
instances
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Builds aliases for repeated sheet entries.
|
|
114
|
+
* @param {object} room Repeated-channel room.
|
|
115
|
+
* @param {object[]} sheetEntries Child sheet-entry records.
|
|
116
|
+
* @returns {object[]}
|
|
117
|
+
*/
|
|
118
|
+
static #netAliases(room, sheetEntries) {
|
|
119
|
+
return (sheetEntries || [])
|
|
120
|
+
.map((entry) => {
|
|
121
|
+
const repeatEntry =
|
|
122
|
+
SchematicRepeatedChannelParser.#parseRepeatEntryName(
|
|
123
|
+
getField(entry.fields, 'Name')
|
|
124
|
+
)
|
|
125
|
+
if (!repeatEntry) {
|
|
126
|
+
return null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return {
|
|
130
|
+
key:
|
|
131
|
+
'repeated-channel-net-' +
|
|
132
|
+
String(entry.recordIndex ?? 0),
|
|
133
|
+
sheetEntryKey:
|
|
134
|
+
SchematicRepeatedChannelParser.#recordKey(entry),
|
|
135
|
+
sheetSymbolKey: room.sheetSymbolKey,
|
|
136
|
+
baseName: repeatEntry,
|
|
137
|
+
aliases: room.instances.map(
|
|
138
|
+
(instance) => instance.hierarchyPath + '/' + repeatEntry
|
|
139
|
+
),
|
|
140
|
+
hierarchyPaths: room.instances.map(
|
|
141
|
+
(instance) => instance.hierarchyPath
|
|
142
|
+
)
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
.filter(Boolean)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Parses `REPEAT(name, start, end)`.
|
|
150
|
+
* @param {string} value Raw repeat string.
|
|
151
|
+
* @returns {{ channelName: string, startIndex: number, endIndex: number } | null}
|
|
152
|
+
*/
|
|
153
|
+
static #parseRepeatRange(value) {
|
|
154
|
+
const match = String(value || '').match(
|
|
155
|
+
/^REPEAT\s*\(\s*([^,]+)\s*,\s*(-?\d+)\s*,\s*(-?\d+)\s*\)$/i
|
|
156
|
+
)
|
|
157
|
+
if (!match) {
|
|
158
|
+
return null
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const startIndex = Number.parseInt(match[2], 10)
|
|
162
|
+
const endIndex = Number.parseInt(match[3], 10)
|
|
163
|
+
if (
|
|
164
|
+
!Number.isInteger(startIndex) ||
|
|
165
|
+
!Number.isInteger(endIndex) ||
|
|
166
|
+
endIndex < startIndex
|
|
167
|
+
) {
|
|
168
|
+
return null
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
channelName: match[1].trim(),
|
|
173
|
+
startIndex,
|
|
174
|
+
endIndex
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Parses `REPEAT(name)` sheet-entry aliases.
|
|
180
|
+
* @param {string} value Raw sheet-entry name.
|
|
181
|
+
* @returns {string}
|
|
182
|
+
*/
|
|
183
|
+
static #parseRepeatEntryName(value) {
|
|
184
|
+
const match = String(value || '').match(
|
|
185
|
+
/^REPEAT\s*\(\s*([^)]+?)\s*\)$/i
|
|
186
|
+
)
|
|
187
|
+
return match ? match[1].trim() : ''
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Resolves a channel designator template.
|
|
192
|
+
* @param {string} template Designator template.
|
|
193
|
+
* @param {{ channelPrefix: string, channelIndex: number, channelAlpha: string }} tokens Channel tokens.
|
|
194
|
+
* @returns {string}
|
|
195
|
+
*/
|
|
196
|
+
static #formatDesignator(template, tokens) {
|
|
197
|
+
return String(template || '')
|
|
198
|
+
.replaceAll('$ChannelPrefix', tokens.channelPrefix)
|
|
199
|
+
.replaceAll('$ChannelIndex', String(tokens.channelIndex))
|
|
200
|
+
.replaceAll('$ChannelAlpha', tokens.channelAlpha)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Converts one-based channel indexes into spreadsheet-style letters.
|
|
205
|
+
* @param {number} index One-based index.
|
|
206
|
+
* @returns {string}
|
|
207
|
+
*/
|
|
208
|
+
static #alpha(index) {
|
|
209
|
+
let value = Math.max(Number(index) || 1, 1)
|
|
210
|
+
let output = ''
|
|
211
|
+
|
|
212
|
+
while (value > 0) {
|
|
213
|
+
value -= 1
|
|
214
|
+
output = String.fromCharCode(65 + (value % 26)) + output
|
|
215
|
+
value = Math.floor(value / 26)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return output
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Builds a stable schematic record key.
|
|
223
|
+
* @param {object} record Schematic record.
|
|
224
|
+
* @returns {string}
|
|
225
|
+
*/
|
|
226
|
+
static #recordKey(record) {
|
|
227
|
+
return 'schematic-record-' + String(record?.recordIndex ?? 0)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
4
|
|
|
5
5
|
import { AsciiRecordParser } from './AsciiRecordParser.mjs'
|
|
6
|
+
import { EmbeddedFileInventoryBuilder } from './EmbeddedFileInventoryBuilder.mjs'
|
|
6
7
|
import { OleCompoundDocument } from '../ole/OleCompoundDocument.mjs'
|
|
7
8
|
import { OleConstants } from '../ole/OleConstants.mjs'
|
|
8
9
|
|
|
@@ -75,7 +76,15 @@ export class SchematicStreamExtractor {
|
|
|
75
76
|
|
|
76
77
|
return {
|
|
77
78
|
records,
|
|
78
|
-
streamNames: compoundDocument.listStreams()
|
|
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
|
+
)
|
|
79
88
|
}
|
|
80
89
|
}
|
|
81
90
|
|
|
@@ -0,0 +1,256 @@
|
|
|
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 } =
|
|
8
|
+
ParserUtils
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Builds a read-only sidecar for schematic template metadata.
|
|
12
|
+
*/
|
|
13
|
+
export class SchematicTemplateParser {
|
|
14
|
+
static SCHEMA_ID = 'altium-toolkit.schematic.template.a1'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Parses template identity and owned schematic records.
|
|
18
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex?: number }[]} records Schematic records.
|
|
19
|
+
* @param {{ fields?: Record<string, string | string[]> } | undefined} sheetRecord Sheet record.
|
|
20
|
+
* @param {{ fonts?: object, titleBlock?: object }} sheet Normalized sheet.
|
|
21
|
+
* @returns {object | null}
|
|
22
|
+
*/
|
|
23
|
+
static parse(records, sheetRecord, sheet) {
|
|
24
|
+
const templateRecord =
|
|
25
|
+
(records || []).find(
|
|
26
|
+
(record) => getField(record.fields, 'RECORD') === '39'
|
|
27
|
+
) || null
|
|
28
|
+
const identity = SchematicTemplateParser.#identity(
|
|
29
|
+
sheetRecord?.fields,
|
|
30
|
+
templateRecord
|
|
31
|
+
)
|
|
32
|
+
const ownedRecords = templateRecord
|
|
33
|
+
? SchematicTemplateParser.#ownedRecords(records, templateRecord)
|
|
34
|
+
: []
|
|
35
|
+
|
|
36
|
+
if (
|
|
37
|
+
!identity.fileName &&
|
|
38
|
+
!identity.recordId &&
|
|
39
|
+
ownedRecords.length === 0
|
|
40
|
+
) {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
schema: SchematicTemplateParser.SCHEMA_ID,
|
|
46
|
+
identity,
|
|
47
|
+
ownedRecordKeys: ownedRecords.map((record) =>
|
|
48
|
+
SchematicTemplateParser.#recordKey(record)
|
|
49
|
+
),
|
|
50
|
+
ownedGraphics: SchematicTemplateParser.#ownedGraphics(ownedRecords),
|
|
51
|
+
fonts: sheet?.fonts || {},
|
|
52
|
+
missingParameters:
|
|
53
|
+
SchematicTemplateParser.#missingParameters(records),
|
|
54
|
+
titleBlock: sheet?.titleBlock || {}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Builds template identity fields from sheet and template records.
|
|
60
|
+
* @param {Record<string, string | string[]> | undefined} fields Sheet fields.
|
|
61
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex?: number } | null} templateRecord Template record.
|
|
62
|
+
* @returns {object}
|
|
63
|
+
*/
|
|
64
|
+
static #identity(fields, templateRecord) {
|
|
65
|
+
return SchematicTemplateParser.#stripEmpty({
|
|
66
|
+
showGraphics: parseBoolean(fields?.ShowTemplateGraphics),
|
|
67
|
+
fileName: getField(fields, 'TemplateFileName'),
|
|
68
|
+
vaultGuid: getField(fields, 'TemplateVaultGUID'),
|
|
69
|
+
itemGuid: getField(fields, 'TemplateItemGUID'),
|
|
70
|
+
revisionGuid: getField(fields, 'TemplateRevisionGUID'),
|
|
71
|
+
vaultHrid: getField(fields, 'TemplateVaultHRID'),
|
|
72
|
+
revisionHrid: getField(fields, 'TemplateRevisionHRID'),
|
|
73
|
+
recordId: templateRecord
|
|
74
|
+
? SchematicTemplateParser.#semanticRecordId(templateRecord)
|
|
75
|
+
: '',
|
|
76
|
+
name: getField(templateRecord?.fields, 'Name'),
|
|
77
|
+
uniqueId:
|
|
78
|
+
getField(templateRecord?.fields, 'UniqueID') ||
|
|
79
|
+
getField(templateRecord?.fields, 'UniqueId')
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Returns records owned by the template record.
|
|
85
|
+
* @param {object[]} records Schematic records.
|
|
86
|
+
* @param {object} templateRecord Template owner record.
|
|
87
|
+
* @returns {object[]}
|
|
88
|
+
*/
|
|
89
|
+
static #ownedRecords(records, templateRecord) {
|
|
90
|
+
const ownerKeys = new Set(
|
|
91
|
+
SchematicTemplateParser.#ownerKeys(templateRecord)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
return (records || []).filter(
|
|
95
|
+
(record) =>
|
|
96
|
+
record !== templateRecord &&
|
|
97
|
+
ownerKeys.has(getField(record.fields, 'OwnerIndex'))
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Builds grouped owned record keys by primitive family.
|
|
103
|
+
* @param {object[]} ownedRecords Template-owned records.
|
|
104
|
+
* @returns {object}
|
|
105
|
+
*/
|
|
106
|
+
static #ownedGraphics(ownedRecords) {
|
|
107
|
+
const groups = {
|
|
108
|
+
lines: [],
|
|
109
|
+
polygons: [],
|
|
110
|
+
rectangles: [],
|
|
111
|
+
ellipses: [],
|
|
112
|
+
arcs: [],
|
|
113
|
+
texts: [],
|
|
114
|
+
images: []
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const record of ownedRecords) {
|
|
118
|
+
const key = SchematicTemplateParser.#recordKey(record)
|
|
119
|
+
switch (getField(record.fields, 'RECORD')) {
|
|
120
|
+
case '4':
|
|
121
|
+
case '28':
|
|
122
|
+
case '41':
|
|
123
|
+
groups.texts.push(key)
|
|
124
|
+
break
|
|
125
|
+
case '7':
|
|
126
|
+
groups.polygons.push(key)
|
|
127
|
+
break
|
|
128
|
+
case '8':
|
|
129
|
+
groups.ellipses.push(key)
|
|
130
|
+
break
|
|
131
|
+
case '11':
|
|
132
|
+
case '12':
|
|
133
|
+
groups.arcs.push(key)
|
|
134
|
+
break
|
|
135
|
+
case '14':
|
|
136
|
+
case '225':
|
|
137
|
+
groups.rectangles.push(key)
|
|
138
|
+
break
|
|
139
|
+
case '30':
|
|
140
|
+
groups.images.push(key)
|
|
141
|
+
break
|
|
142
|
+
default:
|
|
143
|
+
groups.lines.push(key)
|
|
144
|
+
break
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return groups
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Collects unresolved equals-prefixed placeholders.
|
|
153
|
+
* @param {object[]} records Schematic records.
|
|
154
|
+
* @returns {string[]}
|
|
155
|
+
*/
|
|
156
|
+
static #missingParameters(records) {
|
|
157
|
+
const metadata = SchematicTemplateParser.#metadata(records)
|
|
158
|
+
const missing = []
|
|
159
|
+
|
|
160
|
+
for (const record of records || []) {
|
|
161
|
+
const text = getDisplayText(record.fields).trim()
|
|
162
|
+
const match = text.match(/^=([A-Za-z0-9_.-]+)$/u)
|
|
163
|
+
if (!match) {
|
|
164
|
+
continue
|
|
165
|
+
}
|
|
166
|
+
const parameterName = match[1]
|
|
167
|
+
if (!metadata.has(parameterName.toLowerCase())) {
|
|
168
|
+
missing.push(parameterName)
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return [...new Set(missing)].sort((left, right) =>
|
|
173
|
+
left.localeCompare(right)
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Builds a lowercase metadata-name set.
|
|
179
|
+
* @param {object[]} records Schematic records.
|
|
180
|
+
* @returns {Set<string>}
|
|
181
|
+
*/
|
|
182
|
+
static #metadata(records) {
|
|
183
|
+
const metadata = new Set()
|
|
184
|
+
|
|
185
|
+
for (const record of records || []) {
|
|
186
|
+
const name = getField(record.fields, 'Name').trim()
|
|
187
|
+
const value = getDisplayText(record.fields).trim()
|
|
188
|
+
if (name && value && value !== '*') {
|
|
189
|
+
metadata.add(name.toLowerCase())
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return metadata
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Builds owner lookup keys for one record.
|
|
198
|
+
* @param {object} record Schematic record.
|
|
199
|
+
* @returns {string[]}
|
|
200
|
+
*/
|
|
201
|
+
static #ownerKeys(record) {
|
|
202
|
+
const recordIndex = Number(record?.recordIndex)
|
|
203
|
+
const indexInSheet = parseNumericField(record?.fields, 'IndexInSheet')
|
|
204
|
+
const keys = new Set()
|
|
205
|
+
|
|
206
|
+
if (Number.isInteger(recordIndex)) {
|
|
207
|
+
keys.add(String(recordIndex))
|
|
208
|
+
keys.add(String(recordIndex + 1))
|
|
209
|
+
}
|
|
210
|
+
if (Number.isInteger(indexInSheet)) {
|
|
211
|
+
keys.add(String(indexInSheet))
|
|
212
|
+
keys.add(String(indexInSheet + 1))
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return [...keys]
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Builds the public semantic record id.
|
|
220
|
+
* @param {object} record Schematic record.
|
|
221
|
+
* @returns {string}
|
|
222
|
+
*/
|
|
223
|
+
static #semanticRecordId(record) {
|
|
224
|
+
const indexInSheet = parseNumericField(record?.fields, 'IndexInSheet')
|
|
225
|
+
return 'record-' + String(indexInSheet ?? record?.recordIndex ?? 0)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Builds a stable internal record key.
|
|
230
|
+
* @param {object} record Schematic record.
|
|
231
|
+
* @returns {string}
|
|
232
|
+
*/
|
|
233
|
+
static #recordKey(record) {
|
|
234
|
+
return 'schematic-record-' + String(record?.recordIndex ?? 0)
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Removes empty fields while preserving false and zero.
|
|
239
|
+
* @param {Record<string, unknown>} value Candidate object.
|
|
240
|
+
* @returns {Record<string, unknown>}
|
|
241
|
+
*/
|
|
242
|
+
static #stripEmpty(value) {
|
|
243
|
+
return Object.fromEntries(
|
|
244
|
+
Object.entries(value || {}).filter(([, entryValue]) => {
|
|
245
|
+
if (Array.isArray(entryValue)) {
|
|
246
|
+
return entryValue.length > 0
|
|
247
|
+
}
|
|
248
|
+
return (
|
|
249
|
+
entryValue !== null &&
|
|
250
|
+
entryValue !== undefined &&
|
|
251
|
+
entryValue !== ''
|
|
252
|
+
)
|
|
253
|
+
})
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
}
|