altium-toolkit 1.0.8 → 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/styles/altium-renderers.css +19 -0
- package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
- package/src/ui/PcbInteractionIndex.mjs +9 -4
- package/src/ui/PcbScene3dBuilder.mjs +137 -3
- 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
|
@@ -44,4 +44,316 @@ export class SchematicDirectiveParser {
|
|
|
44
44
|
})
|
|
45
45
|
.filter(Boolean)
|
|
46
46
|
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Normalizes directive-like schematic records into first-class semantic
|
|
50
|
+
* groups for project analysis and downstream highlighting.
|
|
51
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex?: number }[]} records
|
|
52
|
+
* @returns {{ noErc: object[], parameterSets: object[], differentialPairs: object[], compileMasks: object[], blankets: object[] }}
|
|
53
|
+
*/
|
|
54
|
+
static parseDirectiveSemantics(records) {
|
|
55
|
+
const childrenByOwner =
|
|
56
|
+
SchematicDirectiveParser.#buildParameterChildren(records)
|
|
57
|
+
const parameterSets = (records || [])
|
|
58
|
+
.filter(
|
|
59
|
+
(record) =>
|
|
60
|
+
ParserUtils.getField(record.fields, 'RECORD') === '43'
|
|
61
|
+
)
|
|
62
|
+
.map((record) =>
|
|
63
|
+
SchematicDirectiveParser.#parseParameterSet(
|
|
64
|
+
record,
|
|
65
|
+
childrenByOwner
|
|
66
|
+
)
|
|
67
|
+
)
|
|
68
|
+
.filter(Boolean)
|
|
69
|
+
|
|
70
|
+
return {
|
|
71
|
+
noErc: (records || [])
|
|
72
|
+
.filter(
|
|
73
|
+
(record) =>
|
|
74
|
+
ParserUtils.getField(record.fields, 'RECORD') === '22'
|
|
75
|
+
)
|
|
76
|
+
.map((record) => SchematicDirectiveParser.#parseNoErc(record))
|
|
77
|
+
.filter(Boolean),
|
|
78
|
+
parameterSets,
|
|
79
|
+
differentialPairs: parameterSets.filter(
|
|
80
|
+
(parameterSet) => parameterSet.isDifferentialPair
|
|
81
|
+
),
|
|
82
|
+
compileMasks: (records || [])
|
|
83
|
+
.filter(
|
|
84
|
+
(record) =>
|
|
85
|
+
ParserUtils.getField(record.fields, 'RECORD') === '211'
|
|
86
|
+
)
|
|
87
|
+
.map((record) =>
|
|
88
|
+
SchematicDirectiveParser.#parseCompileMask(record)
|
|
89
|
+
)
|
|
90
|
+
.filter(Boolean),
|
|
91
|
+
blankets: (records || [])
|
|
92
|
+
.filter(
|
|
93
|
+
(record) =>
|
|
94
|
+
ParserUtils.getField(record.fields, 'RECORD') === '225'
|
|
95
|
+
)
|
|
96
|
+
.map((record) => SchematicDirectiveParser.#parseBlanket(record))
|
|
97
|
+
.filter(Boolean)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Builds child parameter rows keyed by their owning record index.
|
|
103
|
+
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
104
|
+
* @returns {Map<string, object[]>}
|
|
105
|
+
*/
|
|
106
|
+
static #buildParameterChildren(records) {
|
|
107
|
+
const childrenByOwner = new Map()
|
|
108
|
+
for (const record of records || []) {
|
|
109
|
+
if (ParserUtils.getField(record.fields, 'RECORD') !== '41') {
|
|
110
|
+
continue
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const parameter =
|
|
114
|
+
SchematicDirectiveParser.#parseChildParameter(record)
|
|
115
|
+
if (!parameter.ownerIndex) continue
|
|
116
|
+
if (!childrenByOwner.has(parameter.ownerIndex)) {
|
|
117
|
+
childrenByOwner.set(parameter.ownerIndex, [])
|
|
118
|
+
}
|
|
119
|
+
childrenByOwner.get(parameter.ownerIndex).push(parameter)
|
|
120
|
+
}
|
|
121
|
+
return childrenByOwner
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Parses one child parameter record.
|
|
126
|
+
* @param {{ fields: Record<string, string | string[]> }} record Record row.
|
|
127
|
+
* @returns {{ name: string, value: string, isHidden: boolean, ownerIndex: string }}
|
|
128
|
+
*/
|
|
129
|
+
static #parseChildParameter(record) {
|
|
130
|
+
return {
|
|
131
|
+
name: ParserUtils.getField(record.fields, 'Name'),
|
|
132
|
+
value: ParserUtils.getDisplayText(record.fields),
|
|
133
|
+
isHidden: ParserUtils.parseBoolean(record.fields.IsHidden),
|
|
134
|
+
ownerIndex: ParserUtils.getField(record.fields, 'OwnerIndex')
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Parses one No ERC marker.
|
|
140
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex?: number }} record Record row.
|
|
141
|
+
* @returns {object | null}
|
|
142
|
+
*/
|
|
143
|
+
static #parseNoErc(record) {
|
|
144
|
+
const x = ParserUtils.parseNumericField(record.fields, 'Location.X')
|
|
145
|
+
const y = ParserUtils.parseNumericField(record.fields, 'Location.Y')
|
|
146
|
+
if (x === null || y === null) return null
|
|
147
|
+
const symbol = ParserUtils.parseNumericField(record.fields, 'Symbol')
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
recordId: SchematicDirectiveParser.#recordId(record),
|
|
151
|
+
recordType: '22',
|
|
152
|
+
recordIndex: record.recordIndex,
|
|
153
|
+
indexInSheet:
|
|
154
|
+
ParserUtils.parseNumericField(record.fields, 'IndexInSheet') ??
|
|
155
|
+
null,
|
|
156
|
+
x,
|
|
157
|
+
y,
|
|
158
|
+
color: ParserUtils.toColor(record.fields.Color, '#ff0000'),
|
|
159
|
+
orientation:
|
|
160
|
+
ParserUtils.parseNumericField(record.fields, 'Orientation') ||
|
|
161
|
+
0,
|
|
162
|
+
symbol,
|
|
163
|
+
symbolName: SchematicDirectiveParser.#noErcSymbolName(symbol)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Parses one parameter-set directive and its child parameter records.
|
|
169
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex?: number }} record Record row.
|
|
170
|
+
* @param {Map<string, object[]>} childrenByOwner Child parameter map.
|
|
171
|
+
* @returns {object | null}
|
|
172
|
+
*/
|
|
173
|
+
static #parseParameterSet(record, childrenByOwner) {
|
|
174
|
+
const x = ParserUtils.parseNumericField(record.fields, 'Location.X')
|
|
175
|
+
const y = ParserUtils.parseNumericField(record.fields, 'Location.Y')
|
|
176
|
+
const name = ParserUtils.getField(record.fields, 'Name')
|
|
177
|
+
if (x === null || y === null || !name) return null
|
|
178
|
+
const parameters = SchematicDirectiveParser.#childrenForRecord(
|
|
179
|
+
record,
|
|
180
|
+
childrenByOwner
|
|
181
|
+
)
|
|
182
|
+
const parameterMap = {}
|
|
183
|
+
for (const parameter of parameters) {
|
|
184
|
+
if (!parameter.name) continue
|
|
185
|
+
parameterMap[parameter.name] = parameter.value
|
|
186
|
+
}
|
|
187
|
+
const isDifferentialPair =
|
|
188
|
+
/diff(?:erential)?pair/i.test(name) ||
|
|
189
|
+
SchematicDirectiveParser.#booleanText(parameterMap.DifferentialPair)
|
|
190
|
+
const differentialPairClassName =
|
|
191
|
+
parameterMap.DifferentialPairClassName ||
|
|
192
|
+
parameterMap.ClassName ||
|
|
193
|
+
''
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
recordId: SchematicDirectiveParser.#recordId(record),
|
|
197
|
+
recordType: '43',
|
|
198
|
+
recordIndex: record.recordIndex,
|
|
199
|
+
indexInSheet:
|
|
200
|
+
ParserUtils.parseNumericField(record.fields, 'IndexInSheet') ??
|
|
201
|
+
null,
|
|
202
|
+
x,
|
|
203
|
+
y,
|
|
204
|
+
color: ParserUtils.toColor(record.fields.Color, '#ff0000'),
|
|
205
|
+
name,
|
|
206
|
+
orientation:
|
|
207
|
+
ParserUtils.parseNumericField(record.fields, 'Orientation') ||
|
|
208
|
+
0,
|
|
209
|
+
parameters,
|
|
210
|
+
parameterMap,
|
|
211
|
+
isDifferentialPair,
|
|
212
|
+
differentialPairClassName
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Parses one compile-mask rectangle.
|
|
218
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex?: number }} record Record row.
|
|
219
|
+
* @returns {object | null}
|
|
220
|
+
*/
|
|
221
|
+
static #parseCompileMask(record) {
|
|
222
|
+
const x = ParserUtils.parseNumericField(record.fields, 'Location.X')
|
|
223
|
+
const y = ParserUtils.parseNumericField(record.fields, 'Location.Y')
|
|
224
|
+
const cornerX = ParserUtils.parseNumericField(record.fields, 'Corner.X')
|
|
225
|
+
const cornerY = ParserUtils.parseNumericField(record.fields, 'Corner.Y')
|
|
226
|
+
if (x === null || y === null || cornerX === null || cornerY === null) {
|
|
227
|
+
return null
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
recordId: SchematicDirectiveParser.#recordId(record),
|
|
232
|
+
recordType: '211',
|
|
233
|
+
recordIndex: record.recordIndex,
|
|
234
|
+
indexInSheet:
|
|
235
|
+
ParserUtils.parseNumericField(record.fields, 'IndexInSheet') ??
|
|
236
|
+
null,
|
|
237
|
+
x: Math.min(x, cornerX),
|
|
238
|
+
y: Math.min(y, cornerY),
|
|
239
|
+
width: Math.abs(cornerX - x),
|
|
240
|
+
height: Math.abs(cornerY - y),
|
|
241
|
+
color: ParserUtils.toColor(record.fields.Color, '#ff0000'),
|
|
242
|
+
fillColor: ParserUtils.toColor(record.fields.AreaColor, '#ffffff'),
|
|
243
|
+
isSolid: ParserUtils.parseBoolean(record.fields.IsSolid)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Parses one blanket polygon record.
|
|
249
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex?: number }} record Record row.
|
|
250
|
+
* @returns {object | null}
|
|
251
|
+
*/
|
|
252
|
+
static #parseBlanket(record) {
|
|
253
|
+
const points = SchematicDirectiveParser.#parsePoints(record.fields)
|
|
254
|
+
if (!points.length) return null
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
recordId: SchematicDirectiveParser.#recordId(record),
|
|
258
|
+
recordType: '225',
|
|
259
|
+
recordIndex: record.recordIndex,
|
|
260
|
+
indexInSheet:
|
|
261
|
+
ParserUtils.parseNumericField(record.fields, 'IndexInSheet') ??
|
|
262
|
+
null,
|
|
263
|
+
points,
|
|
264
|
+
color: ParserUtils.toColor(record.fields.Color, '#000080'),
|
|
265
|
+
fillColor: ParserUtils.toColor(record.fields.AreaColor, '#ffffff'),
|
|
266
|
+
isSolid: ParserUtils.parseBoolean(record.fields.IsSolid)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Returns child parameters for one owning directive record.
|
|
272
|
+
* @param {{ fields: Record<string, string | string[]> }} record Record row.
|
|
273
|
+
* @param {Map<string, object[]>} childrenByOwner Child parameter map.
|
|
274
|
+
* @returns {object[]}
|
|
275
|
+
*/
|
|
276
|
+
static #childrenForRecord(record, childrenByOwner) {
|
|
277
|
+
for (const key of SchematicDirectiveParser.#ownerKeys(record)) {
|
|
278
|
+
const children = childrenByOwner.get(key)
|
|
279
|
+
if (children?.length) return children
|
|
280
|
+
}
|
|
281
|
+
return []
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Builds possible owner keys for directive child-parameter records.
|
|
286
|
+
* @param {{ fields: Record<string, string | string[]> }} record Record row.
|
|
287
|
+
* @returns {string[]}
|
|
288
|
+
*/
|
|
289
|
+
static #ownerKeys(record) {
|
|
290
|
+
const keys = []
|
|
291
|
+
const indexInSheet = ParserUtils.parseNumericField(
|
|
292
|
+
record.fields,
|
|
293
|
+
'IndexInSheet'
|
|
294
|
+
)
|
|
295
|
+
if (indexInSheet !== null) {
|
|
296
|
+
keys.push(String(indexInSheet), String(indexInSheet + 1))
|
|
297
|
+
}
|
|
298
|
+
const ownerIndex = ParserUtils.getField(record.fields, 'OwnerIndex')
|
|
299
|
+
if (ownerIndex) keys.push(ownerIndex)
|
|
300
|
+
return [...new Set(keys)]
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Parses numbered point fields from a polygon-like record.
|
|
305
|
+
* @param {Record<string, string | string[]>} fields Record fields.
|
|
306
|
+
* @returns {{ x: number, y: number }[]}
|
|
307
|
+
*/
|
|
308
|
+
static #parsePoints(fields) {
|
|
309
|
+
const count =
|
|
310
|
+
ParserUtils.parseNumericField(fields, 'LocationCount') || 0
|
|
311
|
+
const points = []
|
|
312
|
+
for (let index = 1; index <= count; index += 1) {
|
|
313
|
+
const x = ParserUtils.parseNumericField(fields, 'X' + index)
|
|
314
|
+
const y = ParserUtils.parseNumericField(fields, 'Y' + index)
|
|
315
|
+
if (x === null || y === null) continue
|
|
316
|
+
points.push({ x, y })
|
|
317
|
+
}
|
|
318
|
+
return points
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Builds a stable record id.
|
|
323
|
+
* @param {{ fields: Record<string, string | string[]>, recordIndex?: number }} record Record row.
|
|
324
|
+
* @returns {string}
|
|
325
|
+
*/
|
|
326
|
+
static #recordId(record) {
|
|
327
|
+
const indexInSheet = ParserUtils.parseNumericField(
|
|
328
|
+
record.fields,
|
|
329
|
+
'IndexInSheet'
|
|
330
|
+
)
|
|
331
|
+
if (indexInSheet !== null) return 'record-' + indexInSheet
|
|
332
|
+
return 'record-' + String(record.recordIndex ?? 0)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Converts common No ERC symbol ids into public labels.
|
|
337
|
+
* @param {number | null} symbol Symbol id.
|
|
338
|
+
* @returns {string}
|
|
339
|
+
*/
|
|
340
|
+
static #noErcSymbolName(symbol) {
|
|
341
|
+
return (
|
|
342
|
+
{
|
|
343
|
+
0: 'generic',
|
|
344
|
+
1: 'box',
|
|
345
|
+
2: 'cross',
|
|
346
|
+
3: 'triangle'
|
|
347
|
+
}[Number(symbol)] || 'unknown'
|
|
348
|
+
)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Parses one boolean-ish directive parameter value.
|
|
353
|
+
* @param {unknown} value Raw parameter value.
|
|
354
|
+
* @returns {boolean}
|
|
355
|
+
*/
|
|
356
|
+
static #booleanText(value) {
|
|
357
|
+
return /^(1|t|true|yes)$/i.test(String(value || '').trim())
|
|
358
|
+
}
|
|
47
359
|
}
|
|
@@ -0,0 +1,231 @@
|
|
|
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, parseBoolean, parseNumericField } = ParserUtils
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Catalogues all schematic component parts and display modes.
|
|
11
|
+
*/
|
|
12
|
+
export class SchematicDisplayModeCatalogParser {
|
|
13
|
+
static SCHEMA_ID = 'altium-toolkit.schematic.display-modes.a1'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Parses all component display-mode metadata.
|
|
17
|
+
* @param {object[]} records Schematic records.
|
|
18
|
+
* @returns {object | null}
|
|
19
|
+
*/
|
|
20
|
+
static parse(records) {
|
|
21
|
+
const components = (records || [])
|
|
22
|
+
.filter((record) => getField(record.fields, 'RECORD') === '1')
|
|
23
|
+
.map((record) =>
|
|
24
|
+
SchematicDisplayModeCatalogParser.#componentCatalog(
|
|
25
|
+
record,
|
|
26
|
+
records
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
.filter(Boolean)
|
|
30
|
+
|
|
31
|
+
if (!components.length) {
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
schema: SchematicDisplayModeCatalogParser.SCHEMA_ID,
|
|
37
|
+
components
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Builds one component display-mode catalog row.
|
|
43
|
+
* @param {object} componentRecord Component record.
|
|
44
|
+
* @param {object[]} records All schematic records.
|
|
45
|
+
* @returns {object}
|
|
46
|
+
*/
|
|
47
|
+
static #componentCatalog(componentRecord, records) {
|
|
48
|
+
const indexInSheet = parseNumericField(
|
|
49
|
+
componentRecord.fields,
|
|
50
|
+
'IndexInSheet'
|
|
51
|
+
)
|
|
52
|
+
const ownerIndex = String(indexInSheet ?? componentRecord.recordIndex)
|
|
53
|
+
const children = SchematicDisplayModeCatalogParser.#ownerChildren(
|
|
54
|
+
records,
|
|
55
|
+
ownerIndex
|
|
56
|
+
)
|
|
57
|
+
const declaredPartCount =
|
|
58
|
+
parseNumericField(componentRecord.fields, 'PartCount') || 1
|
|
59
|
+
const declaredDisplayModeCount =
|
|
60
|
+
parseNumericField(componentRecord.fields, 'DisplayModeCount') || 1
|
|
61
|
+
const currentPartId =
|
|
62
|
+
parseNumericField(componentRecord.fields, 'CurrentPartId') || 1
|
|
63
|
+
const partIds = SchematicDisplayModeCatalogParser.#collectPartIds(
|
|
64
|
+
children,
|
|
65
|
+
declaredPartCount
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
componentKey: 'schematic-component-' + ownerIndex,
|
|
70
|
+
recordKey:
|
|
71
|
+
'schematic-record-' + String(componentRecord.recordIndex ?? 0),
|
|
72
|
+
uniqueId: getField(componentRecord.fields, 'UniqueID'),
|
|
73
|
+
libReference:
|
|
74
|
+
getField(componentRecord.fields, 'LibReference') ||
|
|
75
|
+
getField(componentRecord.fields, 'DesignItemId'),
|
|
76
|
+
partCount: declaredPartCount,
|
|
77
|
+
displayModeCount: declaredDisplayModeCount,
|
|
78
|
+
currentPartId,
|
|
79
|
+
partIdLocked: parseBoolean(componentRecord.fields.PartIDLocked),
|
|
80
|
+
allPinCount:
|
|
81
|
+
parseNumericField(componentRecord.fields, 'AllPinCount') ||
|
|
82
|
+
children.filter(
|
|
83
|
+
(child) => getField(child.fields, 'RECORD') === '2'
|
|
84
|
+
).length,
|
|
85
|
+
parts: partIds.map((partId) =>
|
|
86
|
+
SchematicDisplayModeCatalogParser.#partCatalog(
|
|
87
|
+
partId,
|
|
88
|
+
currentPartId,
|
|
89
|
+
declaredDisplayModeCount,
|
|
90
|
+
children
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Returns child records with owner part/display metadata.
|
|
98
|
+
* @param {object[]} records All schematic records.
|
|
99
|
+
* @param {string} ownerIndex Component owner index.
|
|
100
|
+
* @returns {object[]}
|
|
101
|
+
*/
|
|
102
|
+
static #ownerChildren(records, ownerIndex) {
|
|
103
|
+
return (records || []).filter(
|
|
104
|
+
(record) =>
|
|
105
|
+
getField(record.fields, 'OwnerIndex') === ownerIndex &&
|
|
106
|
+
parseNumericField(record.fields, 'OwnerPartID') !== null
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Collects declared and observed part ids.
|
|
112
|
+
* @param {object[]} children Owner child records.
|
|
113
|
+
* @param {number} declaredPartCount Declared part count.
|
|
114
|
+
* @returns {number[]}
|
|
115
|
+
*/
|
|
116
|
+
static #collectPartIds(children, declaredPartCount) {
|
|
117
|
+
const ids = new Set()
|
|
118
|
+
|
|
119
|
+
for (let partId = 1; partId <= declaredPartCount; partId += 1) {
|
|
120
|
+
ids.add(partId)
|
|
121
|
+
}
|
|
122
|
+
for (const child of children) {
|
|
123
|
+
const partId = parseNumericField(child.fields, 'OwnerPartID')
|
|
124
|
+
if (partId !== null && partId > 0) ids.add(partId)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return [...ids].sort((left, right) => left - right)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Builds one part catalog row.
|
|
132
|
+
* @param {number} partId Native part id.
|
|
133
|
+
* @param {number} currentPartId Active part id.
|
|
134
|
+
* @param {number} declaredDisplayModeCount Declared display-mode count.
|
|
135
|
+
* @param {object[]} children Owner child records.
|
|
136
|
+
* @returns {object}
|
|
137
|
+
*/
|
|
138
|
+
static #partCatalog(
|
|
139
|
+
partId,
|
|
140
|
+
currentPartId,
|
|
141
|
+
declaredDisplayModeCount,
|
|
142
|
+
children
|
|
143
|
+
) {
|
|
144
|
+
const partChildren = children.filter(
|
|
145
|
+
(child) => parseNumericField(child.fields, 'OwnerPartID') === partId
|
|
146
|
+
)
|
|
147
|
+
const displayModeIds =
|
|
148
|
+
SchematicDisplayModeCatalogParser.#collectDisplayModeIds(
|
|
149
|
+
partChildren,
|
|
150
|
+
declaredDisplayModeCount
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
partId,
|
|
155
|
+
isCurrent: partId === currentPartId,
|
|
156
|
+
primitiveCount: partChildren.length,
|
|
157
|
+
pinCount: SchematicDisplayModeCatalogParser.#pinCount(partChildren),
|
|
158
|
+
displayModes: displayModeIds.map((displayMode) =>
|
|
159
|
+
SchematicDisplayModeCatalogParser.#displayModeCatalog(
|
|
160
|
+
displayMode,
|
|
161
|
+
partId,
|
|
162
|
+
currentPartId,
|
|
163
|
+
partChildren
|
|
164
|
+
)
|
|
165
|
+
)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Collects observed display-mode ids for a part.
|
|
171
|
+
* @param {object[]} partChildren Child records for one part.
|
|
172
|
+
* @param {number} declaredDisplayModeCount Declared display-mode count.
|
|
173
|
+
* @returns {number[]}
|
|
174
|
+
*/
|
|
175
|
+
static #collectDisplayModeIds(partChildren, declaredDisplayModeCount) {
|
|
176
|
+
const ids = new Set()
|
|
177
|
+
|
|
178
|
+
for (const child of partChildren) {
|
|
179
|
+
const displayMode = parseNumericField(
|
|
180
|
+
child.fields,
|
|
181
|
+
'OwnerPartDisplayMode'
|
|
182
|
+
)
|
|
183
|
+
ids.add(displayMode || 1)
|
|
184
|
+
}
|
|
185
|
+
if (!ids.size && declaredDisplayModeCount > 0) {
|
|
186
|
+
ids.add(1)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return [...ids].sort((left, right) => left - right)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Builds one display-mode catalog row.
|
|
194
|
+
* @param {number} displayMode Native display-mode id.
|
|
195
|
+
* @param {number} partId Native part id.
|
|
196
|
+
* @param {number} currentPartId Active part id.
|
|
197
|
+
* @param {object[]} partChildren Child records for one part.
|
|
198
|
+
* @returns {object}
|
|
199
|
+
*/
|
|
200
|
+
static #displayModeCatalog(
|
|
201
|
+
displayMode,
|
|
202
|
+
partId,
|
|
203
|
+
currentPartId,
|
|
204
|
+
partChildren
|
|
205
|
+
) {
|
|
206
|
+
const displayChildren = partChildren.filter(
|
|
207
|
+
(child) =>
|
|
208
|
+
(parseNumericField(child.fields, 'OwnerPartDisplayMode') ||
|
|
209
|
+
1) === displayMode
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
displayMode,
|
|
214
|
+
isActive: partId === currentPartId && displayMode === 1,
|
|
215
|
+
primitiveCount: displayChildren.length,
|
|
216
|
+
pinCount:
|
|
217
|
+
SchematicDisplayModeCatalogParser.#pinCount(displayChildren)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Counts pin records in a child record list.
|
|
223
|
+
* @param {object[]} records Child records.
|
|
224
|
+
* @returns {number}
|
|
225
|
+
*/
|
|
226
|
+
static #pinCount(records) {
|
|
227
|
+
return records.filter(
|
|
228
|
+
(record) => getField(record.fields, 'RECORD') === '2'
|
|
229
|
+
).length
|
|
230
|
+
}
|
|
231
|
+
}
|