altium-toolkit 1.0.8 → 1.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/README.md +18 -6
  2. package/docs/api.md +78 -16
  3. package/docs/model-format.md +229 -8
  4. package/docs/schemas/altium_toolkit/ci_artifact_bundle_a1.schema.json +76 -0
  5. package/docs/schemas/altium_toolkit/draftsman_digest_a1.schema.json +35 -0
  6. package/docs/schemas/altium_toolkit/netlist_a1.schema.json +53 -0
  7. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +1826 -110
  8. package/docs/schemas/altium_toolkit/parser_compatibility_fuzz_a1.schema.json +25 -0
  9. package/docs/schemas/altium_toolkit/pcb_svg_semantics_a1.schema.json +86 -0
  10. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +63 -0
  11. package/docs/schemas/altium_toolkit/project_document_graph_a1.schema.json +33 -0
  12. package/docs/schemas/altium_toolkit/schematic_svg_semantics_a1.schema.json +50 -0
  13. package/docs/schemas/altium_toolkit/svg_model_cross_link_a1.schema.json +39 -0
  14. package/docs/testing.md +9 -3
  15. package/package.json +1 -1
  16. package/spec/library-scope.md +7 -1
  17. package/src/core/altium/AltiumLayoutParser.mjs +104 -8
  18. package/src/core/altium/AltiumParser.mjs +196 -45
  19. package/src/core/altium/CiArtifactBundleBuilder.mjs +202 -0
  20. package/src/core/altium/DraftsmanDigestParser.mjs +689 -0
  21. package/src/core/altium/EmbeddedFileInventoryBuilder.mjs +255 -0
  22. package/src/core/altium/IntLibModelParser.mjs +240 -0
  23. package/src/core/altium/IntLibStreamExtractor.mjs +366 -0
  24. package/src/core/altium/LibraryRenderManifestBuilder.mjs +417 -0
  25. package/src/core/altium/LibrarySearchIndex.mjs +215 -0
  26. package/src/core/altium/NormalizedModelSchema.mjs +36 -0
  27. package/src/core/altium/ParserCompatibilityFuzzer.mjs +192 -0
  28. package/src/core/altium/PcbCustomPadShapeParser.mjs +244 -0
  29. package/src/core/altium/PcbDefaultsParser.mjs +171 -0
  30. package/src/core/altium/PcbDimensionParser.mjs +229 -0
  31. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +232 -6
  32. package/src/core/altium/PcbExtendedPrimitiveInformationParser.mjs +256 -0
  33. package/src/core/altium/PcbLibModelParser.mjs +235 -14
  34. package/src/core/altium/PcbLibStreamExtractor.mjs +62 -4
  35. package/src/core/altium/PcbMaskPasteResolver.mjs +354 -0
  36. package/src/core/altium/PcbMechanicalLayerPairParser.mjs +204 -0
  37. package/src/core/altium/PcbModelParser.mjs +495 -32
  38. package/src/core/altium/PcbOwnershipGraphBuilder.mjs +245 -0
  39. package/src/core/altium/PcbPadPrimitiveParser.mjs +78 -65
  40. package/src/core/altium/PcbPadStackParser.mjs +229 -2
  41. package/src/core/altium/PcbPickPlacePositionResolver.mjs +224 -0
  42. package/src/core/altium/PcbPrimitiveParameterParser.mjs +3 -2
  43. package/src/core/altium/PcbRawRecordRegistry.mjs +121 -130
  44. package/src/core/altium/PcbRegionPrimitiveParser.mjs +76 -3
  45. package/src/core/altium/PcbRouteAnalysisBuilder.mjs +730 -0
  46. package/src/core/altium/PcbRuleParser.mjs +354 -33
  47. package/src/core/altium/PcbSidecarRecordParser.mjs +177 -0
  48. package/src/core/altium/PcbSpecialStringResolver.mjs +220 -0
  49. package/src/core/altium/PcbStatisticsBuilder.mjs +541 -0
  50. package/src/core/altium/PcbStreamExtractor.mjs +111 -4
  51. package/src/core/altium/PcbTextPrimitiveParser.mjs +60 -0
  52. package/src/core/altium/PcbUnionParser.mjs +307 -0
  53. package/src/core/altium/PcbViaStackParser.mjs +98 -10
  54. package/src/core/altium/PcbViaStructureParser.mjs +335 -0
  55. package/src/core/altium/PrintableTextDecoder.mjs +53 -3
  56. package/src/core/altium/PrjPcbModelParser.mjs +281 -7
  57. package/src/core/altium/ProjectAnnotationParser.mjs +205 -0
  58. package/src/core/altium/ProjectDesignBundleBuilder.mjs +492 -0
  59. package/src/core/altium/ProjectDocumentGraphBuilder.mjs +280 -0
  60. package/src/core/altium/ProjectNetlistExporter.mjs +503 -0
  61. package/src/core/altium/ProjectOutJobDigestBuilder.mjs +109 -0
  62. package/src/core/altium/ProjectVariantViewBuilder.mjs +334 -0
  63. package/src/core/altium/SchematicBindingProvenanceParser.mjs +223 -0
  64. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +312 -0
  65. package/src/core/altium/SchematicComponentTextResolver.mjs +72 -19
  66. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +271 -0
  67. package/src/core/altium/SchematicCrossSheetConnectorParser.mjs +140 -0
  68. package/src/core/altium/SchematicDirectiveParser.mjs +312 -0
  69. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +231 -0
  70. package/src/core/altium/SchematicHarnessParser.mjs +302 -0
  71. package/src/core/altium/SchematicImageParser.mjs +474 -3
  72. package/src/core/altium/SchematicImplementationParser.mjs +518 -0
  73. package/src/core/altium/SchematicNetlistBuilder.mjs +15 -2
  74. package/src/core/altium/SchematicOwnershipGraphParser.mjs +195 -0
  75. package/src/core/altium/SchematicPinParser.mjs +84 -1
  76. package/src/core/altium/SchematicPrimitiveParser.mjs +301 -0
  77. package/src/core/altium/SchematicProjectParameterResolver.mjs +361 -0
  78. package/src/core/altium/SchematicQaReportBuilder.mjs +284 -0
  79. package/src/core/altium/SchematicRecordTypeRegistry.mjs +137 -0
  80. package/src/core/altium/SchematicRepeatedChannelParser.mjs +229 -0
  81. package/src/core/altium/SchematicStreamExtractor.mjs +10 -1
  82. package/src/core/altium/SchematicTemplateParser.mjs +256 -0
  83. package/src/core/altium/SchematicTextParser.mjs +123 -0
  84. package/src/core/altium/SvgModelCrossLinkValidator.mjs +402 -0
  85. package/src/core/circuit-json/CircuitJsonModelAdapter.mjs +136 -96
  86. package/src/core/circuit-json/CircuitJsonModelAdapterPcbElements.mjs +244 -0
  87. package/src/core/circuit-json/CircuitJsonModelSchema.mjs +1 -1
  88. package/src/core/ole/OleCompoundDocument.mjs +20 -0
  89. package/src/parser.mjs +35 -0
  90. package/src/styles/altium-renderers.css +19 -0
  91. package/src/ui/PcbBarcodeTextRenderer.mjs +436 -0
  92. package/src/ui/PcbInteractionIndex.mjs +9 -4
  93. package/src/ui/PcbScene3dBuilder.mjs +137 -3
  94. package/src/ui/PcbScene3dModelRegistry.mjs +74 -0
  95. package/src/ui/PcbSvgRenderer.mjs +1252 -34
  96. package/src/ui/PcbTextPrimitiveRenderer.mjs +193 -7
  97. package/src/ui/SchematicNoteRenderer.mjs +9 -2
  98. package/src/ui/SchematicOwnerPinLabelLayout.mjs +206 -0
  99. package/src/ui/SchematicShapeRenderer.mjs +362 -0
  100. package/src/ui/SchematicSvgRenderer.mjs +1442 -92
  101. package/src/ui/SchematicTypography.mjs +48 -5
  102. 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
+ }