altium-toolkit 1.1.22 → 1.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/README.md +34 -5
  2. package/docs/api.md +134 -23
  3. package/docs/model-format.md +174 -20
  4. package/docs/schemas/altium_toolkit/embedded_assets_a1.schema.json +56 -0
  5. package/docs/schemas/altium_toolkit/fixture_coverage_matrix_a1.schema.json +89 -0
  6. package/docs/schemas/altium_toolkit/geometry_bounds_a1.schema.json +86 -0
  7. package/docs/schemas/altium_toolkit/library_catalog_a1.schema.json +65 -0
  8. package/docs/schemas/altium_toolkit/library_diff_a1.schema.json +54 -0
  9. package/docs/schemas/altium_toolkit/library_inspection_a1.schema.json +94 -0
  10. package/docs/schemas/altium_toolkit/library_qa_a1.schema.json +4 -0
  11. package/docs/schemas/altium_toolkit/native_stream_inventory_a1.schema.json +66 -0
  12. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +511 -1
  13. package/docs/schemas/altium_toolkit/parameter_record_inventory_a1.schema.json +84 -0
  14. package/docs/schemas/altium_toolkit/parser_diagnostics_a1.schema.json +63 -0
  15. package/docs/schemas/altium_toolkit/parser_value_verification_a1.schema.json +74 -0
  16. package/docs/schemas/altium_toolkit/pcb_class_report_a1.schema.json +79 -0
  17. package/docs/schemas/altium_toolkit/pcb_inspection_a1.schema.json +65 -0
  18. package/docs/schemas/altium_toolkit/pcb_net_membership_a1.schema.json +98 -0
  19. package/docs/schemas/altium_toolkit/project_bundle_a1.schema.json +3 -0
  20. package/docs/schemas/altium_toolkit/project_hierarchy_a1.schema.json +79 -0
  21. package/docs/schemas/altium_toolkit/unsupported_features_a1.schema.json +212 -0
  22. package/docs/testing.md +2 -0
  23. package/examples/README.md +21 -0
  24. package/examples/cli-utils.mjs +148 -0
  25. package/examples/corpus-smoke.mjs +523 -0
  26. package/examples/extract-bom.mjs +47 -0
  27. package/examples/generate-pnp.mjs +59 -0
  28. package/examples/inspect-board.mjs +70 -0
  29. package/examples/inspect-schematic.mjs +406 -0
  30. package/examples/library-catalog.mjs +115 -0
  31. package/examples/net-report.mjs +61 -0
  32. package/examples/validate-library.mjs +59 -0
  33. package/package.json +1 -1
  34. package/src/core/BinaryReader.mjs +213 -2
  35. package/src/core/altium/AltiumParser.mjs +352 -14
  36. package/src/core/altium/AltiumUnits.mjs +205 -0
  37. package/src/core/altium/AsciiRecordParser.mjs +9 -0
  38. package/src/core/altium/EmbeddedAssetReportBuilder.mjs +383 -0
  39. package/src/core/altium/FixtureCoverageMatrixBuilder.mjs +304 -0
  40. package/src/core/altium/GeometryBoundsReportBuilder.mjs +935 -0
  41. package/src/core/altium/LibraryCatalogArtifactBuilder.mjs +296 -0
  42. package/src/core/altium/LibraryDiffReportBuilder.mjs +260 -0
  43. package/src/core/altium/LibraryInspectionReportBuilder.mjs +156 -0
  44. package/src/core/altium/LibraryQaReportBuilder.mjs +374 -1
  45. package/src/core/altium/NativeStreamInventoryBuilder.mjs +177 -0
  46. package/src/core/altium/NormalizedModelSchema.mjs +3 -31
  47. package/src/core/altium/ParameterCollection.mjs +431 -0
  48. package/src/core/altium/ParameterRecordInventoryBuilder.mjs +274 -0
  49. package/src/core/altium/ParserCompatibilityFuzzer.mjs +106 -2
  50. package/src/core/altium/ParserDiagnosticNormalizer.mjs +213 -0
  51. package/src/core/altium/ParserErrors.mjs +90 -0
  52. package/src/core/altium/ParserFieldCoverageReportBuilder.mjs +656 -0
  53. package/src/core/altium/ParserUtils.mjs +24 -0
  54. package/src/core/altium/ParserValueVerificationReportBuilder.mjs +323 -0
  55. package/src/core/altium/PcbClassReportBuilder.mjs +366 -0
  56. package/src/core/altium/PcbInspectionReportBuilder.mjs +313 -0
  57. package/src/core/altium/PcbLayerGroups.mjs +308 -0
  58. package/src/core/altium/PcbLayerStackCustomDataParser.mjs +183 -0
  59. package/src/core/altium/PcbLayerStackInterchangeParser.mjs +473 -4
  60. package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +83 -15
  61. package/src/core/altium/PcbLayerStackSourceMetadataParser.mjs +74 -4
  62. package/src/core/altium/PcbLibModelParser.mjs +20 -4
  63. package/src/core/altium/PcbLibStreamExtractor.mjs +49 -6
  64. package/src/core/altium/PcbModelParser.mjs +223 -4
  65. package/src/core/altium/PcbNetMembershipReportBuilder.mjs +270 -0
  66. package/src/core/altium/PcbStreamExtractor.mjs +130 -6
  67. package/src/core/altium/PcbTrackPrimitiveParser.mjs +66 -2
  68. package/src/core/altium/ProjectDesignBundleBuilder.mjs +15 -0
  69. package/src/core/altium/ProjectHierarchyReportBuilder.mjs +660 -0
  70. package/src/core/altium/ProjectNetlistExporter.mjs +2 -0
  71. package/src/core/altium/RawDataPreservationReportBuilder.mjs +348 -0
  72. package/src/core/altium/SchLibModelParser.mjs +840 -0
  73. package/src/core/altium/SchLibStreamExtractor.mjs +586 -0
  74. package/src/core/altium/SchematicBusEntryParser.mjs +3 -2
  75. package/src/core/altium/SchematicCodeSymbolParser.mjs +663 -0
  76. package/src/core/altium/SchematicConnectivityQaBuilder.mjs +177 -2
  77. package/src/core/altium/SchematicDisplayModeCatalogParser.mjs +10 -1
  78. package/src/core/altium/SchematicFieldCoverageReportBuilder.mjs +549 -0
  79. package/src/core/altium/SchematicHarnessParser.mjs +9 -3
  80. package/src/core/altium/SchematicHyperlinkParser.mjs +122 -0
  81. package/src/core/altium/SchematicNetlistBuilder.mjs +271 -8
  82. package/src/core/altium/SchematicOwnershipGraphParser.mjs +102 -3
  83. package/src/core/altium/SchematicPinParser.mjs +12 -45
  84. package/src/core/altium/SchematicPrimitiveParser.mjs +9 -14
  85. package/src/core/altium/SchematicQaReportBuilder.mjs +2 -0
  86. package/src/core/altium/SchematicRecordStreamParser.mjs +183 -0
  87. package/src/core/altium/SchematicRecordTypeRegistry.mjs +6 -1
  88. package/src/core/altium/SchematicSheetParser.mjs +8 -2
  89. package/src/core/altium/SchematicStreamExtractor.mjs +64 -25
  90. package/src/core/altium/SchematicTextOrientationResolver.mjs +76 -0
  91. package/src/core/altium/SchematicTextParser.mjs +28 -12
  92. package/src/core/altium/SchematicTextRunParser.mjs +81 -0
  93. package/src/core/altium/SchematicThumbnailParser.mjs +425 -0
  94. package/src/core/altium/UnsupportedFeatureReportBuilder.mjs +380 -0
  95. package/src/parser.mjs +35 -1
  96. package/src/renderers.mjs +1 -0
  97. package/src/ui/SchematicShapeRenderer.mjs +49 -6
  98. package/src/ui/SchematicSvgRenderer.mjs +37 -8
  99. package/src/ui/SchematicTypography.mjs +4 -3
@@ -9,8 +9,8 @@
9
9
  export class SchematicNetlistBuilder {
10
10
  /**
11
11
  * Builds normalized nets and connectivity diagnostics.
12
- * @param {{ lines: { x1: number, y1: number, x2: number, y2: number, ownerIndex?: string, isBus?: boolean }[], texts: { x: number, y: number, text: string, recordType?: string }[], pins?: { x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', name: string, designator: string }[], ports?: { x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down', name: string }[], crossSheetConnectors?: { key: string, x: number, y: number, name: string, style?: string }[], junctions?: { x: number, y: number, color: string }[], busEntries?: { x1: number, y1: number, x2: number, y2: number }[], sheetEntries?: { x: number, y: number, name: string }[] }} schematic
13
- * @returns {{ nets: { name: string, segments: { x1: number, y1: number, x2: number, y2: number, ownerIndex?: string, isBus?: boolean }[], labels: { x: number, y: number, text: string, recordType?: string }[], powerPorts: { x: number, y: number, text: string, recordType?: string }[], crossSheetConnectors: { key: string, name: string, x: number, y: number, style?: string }[], pins: { x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', name: string, designator: string }[], ports: { x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down', name: string }[], junctions: { x: number, y: number, color: string }[], busEntries: { x1: number, y1: number, x2: number, y2: number }[], sheetEntries: { x: number, y: number, name: string }[] }[], diagnostics: { severity: 'warning', message: string }[] }}
12
+ * @param {{ lines: { x1: number, y1: number, x2: number, y2: number, ownerIndex?: string, isBus?: boolean }[], texts: { x: number, y: number, text: string, recordType?: string }[], pins?: { x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', name: string, designator: string, ownerIndex?: string, componentDesignator?: string }[], ports?: { x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down', name: string }[], crossSheetConnectors?: { key: string, x: number, y: number, name: string, style?: string }[], junctions?: { x: number, y: number, color: string }[], busEntries?: { x1: number, y1: number, x2: number, y2: number }[], sheetEntries?: { x: number, y: number, name: string }[], componentDesignatorsByOwnerIndex?: Map<string, string> | Record<string, string> }} schematic
13
+ * @returns {{ nets: { name: string, autoName?: string, autoNameSource?: string, aliasCandidates?: string[], segments: { x1: number, y1: number, x2: number, y2: number, ownerIndex?: string, isBus?: boolean }[], labels: { x: number, y: number, text: string, recordType?: string }[], powerPorts: { x: number, y: number, text: string, recordType?: string }[], crossSheetConnectors: { key: string, name: string, x: number, y: number, style?: string }[], pins: { x: number, y: number, length: number, orientation: 'left' | 'right' | 'top' | 'bottom', name: string, designator: string, ownerIndex?: string, componentDesignator?: string }[], ports: { x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down', name: string }[], junctions: { x: number, y: number, color: string }[], busEntries: { x1: number, y1: number, x2: number, y2: number }[], sheetEntries: { x: number, y: number, name: string }[] }[], diagnostics: { severity: 'warning', message: string }[] }}
14
14
  */
15
15
  static build(schematic) {
16
16
  const diagnostics = []
@@ -43,12 +43,19 @@ export class SchematicNetlistBuilder {
43
43
  ).filter((connector) =>
44
44
  SchematicNetlistBuilder.#groupContainsPoint(group, connector)
45
45
  )
46
- const pins = (schematic.pins || []).filter((pin) =>
47
- SchematicNetlistBuilder.#groupContainsPoint(
48
- group,
49
- SchematicNetlistBuilder.#resolvePinConnectionPoint(pin)
46
+ const pins = (schematic.pins || [])
47
+ .filter((pin) =>
48
+ SchematicNetlistBuilder.#groupContainsPoint(
49
+ group,
50
+ SchematicNetlistBuilder.#resolvePinConnectionPoint(pin)
51
+ )
52
+ )
53
+ .map((pin) =>
54
+ SchematicNetlistBuilder.#annotatePinComponentDesignator(
55
+ pin,
56
+ schematic.componentDesignatorsByOwnerIndex
57
+ )
50
58
  )
51
- )
52
59
  const ports = (schematic.ports || []).filter((port) =>
53
60
  SchematicNetlistBuilder.#groupContainsPoint(
54
61
  group,
@@ -89,6 +96,11 @@ export class SchematicNetlistBuilder {
89
96
  ]
90
97
  const name =
91
98
  explicitNames[0] || 'UnknownNet' + String(unknownNetIndex++)
99
+ const implicitNaming =
100
+ SchematicNetlistBuilder.#deriveImplicitNetNaming({
101
+ explicitNames,
102
+ pins
103
+ })
92
104
 
93
105
  if (explicitNames.length > 1) {
94
106
  diagnostics.push({
@@ -102,6 +114,7 @@ export class SchematicNetlistBuilder {
102
114
 
103
115
  return {
104
116
  name,
117
+ ...implicitNaming,
105
118
  segments: group,
106
119
  labels,
107
120
  powerPorts,
@@ -120,7 +133,257 @@ export class SchematicNetlistBuilder {
120
133
  }
121
134
  })
122
135
 
123
- return { nets, diagnostics }
136
+ return {
137
+ nets: SchematicNetlistBuilder.#mergeMatchingPowerPortNets(nets),
138
+ diagnostics
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Adds a component designator to a net pin when owner metadata resolves it.
144
+ * @param {object} pin Net pin.
145
+ * @param {Map<string, string> | Record<string, string> | undefined} componentDesignatorsByOwnerIndex Component designators by native owner id.
146
+ * @returns {object}
147
+ */
148
+ static #annotatePinComponentDesignator(
149
+ pin,
150
+ componentDesignatorsByOwnerIndex
151
+ ) {
152
+ const componentDesignator =
153
+ pin?.componentDesignator ||
154
+ SchematicNetlistBuilder.#lookupComponentDesignator(
155
+ componentDesignatorsByOwnerIndex,
156
+ pin?.ownerIndex
157
+ )
158
+
159
+ if (!componentDesignator) {
160
+ return pin
161
+ }
162
+
163
+ return {
164
+ ...pin,
165
+ componentDesignator
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Derives non-breaking names for unnamed nets from attached pins.
171
+ * @param {{ explicitNames: string[], pins: object[] }} options Naming inputs.
172
+ * @returns {{ autoName?: string, autoNameSource?: string, aliasCandidates?: string[] }}
173
+ */
174
+ static #deriveImplicitNetNaming(options) {
175
+ if ((options.explicitNames || []).length) {
176
+ return {}
177
+ }
178
+
179
+ const candidates = SchematicNetlistBuilder.#pinAliasCandidates(
180
+ options.pins || []
181
+ )
182
+
183
+ if (!candidates.length) {
184
+ return {}
185
+ }
186
+
187
+ return {
188
+ autoName: candidates[0].name,
189
+ autoNameSource: candidates[0].source,
190
+ aliasCandidates: candidates.map((candidate) => candidate.name)
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Builds deterministic alias candidates from connected pins.
196
+ * @param {object[]} pins Connected pins.
197
+ * @returns {{ name: string, source: string }[]}
198
+ */
199
+ static #pinAliasCandidates(pins) {
200
+ const candidates = []
201
+
202
+ for (const pin of pins || []) {
203
+ const componentDesignator = String(
204
+ pin?.componentDesignator || ''
205
+ ).trim()
206
+ const ownerIndex = String(pin?.ownerIndex || '').trim()
207
+ const pinDesignator = String(pin?.designator || '').trim()
208
+ const pinName = String(pin?.name || '').trim()
209
+
210
+ if (componentDesignator) {
211
+ SchematicNetlistBuilder.#pushPinAliasCandidate(
212
+ candidates,
213
+ componentDesignator,
214
+ pinDesignator,
215
+ 'component-pin'
216
+ )
217
+ SchematicNetlistBuilder.#pushPinAliasCandidate(
218
+ candidates,
219
+ componentDesignator,
220
+ pinName,
221
+ 'component-pin-name'
222
+ )
223
+ continue
224
+ }
225
+
226
+ if (ownerIndex) {
227
+ SchematicNetlistBuilder.#pushPinAliasCandidate(
228
+ candidates,
229
+ 'owner-' + ownerIndex,
230
+ pinDesignator,
231
+ 'owner-pin'
232
+ )
233
+ SchematicNetlistBuilder.#pushPinAliasCandidate(
234
+ candidates,
235
+ 'owner-' + ownerIndex,
236
+ pinName,
237
+ 'owner-pin-name'
238
+ )
239
+ continue
240
+ }
241
+
242
+ if (pinName) {
243
+ candidates.push({
244
+ name: 'pin.' + pinName,
245
+ source: 'pin-name'
246
+ })
247
+ }
248
+ }
249
+
250
+ return SchematicNetlistBuilder.#dedupeAliasCandidates(candidates)
251
+ }
252
+
253
+ /**
254
+ * Adds one scoped pin alias candidate.
255
+ * @param {{ name: string, source: string }[]} candidates Alias candidates.
256
+ * @param {string} scope Alias scope.
257
+ * @param {string} pinLabel Pin designator or name.
258
+ * @param {string} source Alias source label.
259
+ * @returns {void}
260
+ */
261
+ static #pushPinAliasCandidate(candidates, scope, pinLabel, source) {
262
+ const trimmedScope = String(scope || '').trim()
263
+ const trimmedPinLabel = String(pinLabel || '').trim()
264
+
265
+ if (!trimmedScope || !trimmedPinLabel) {
266
+ return
267
+ }
268
+
269
+ candidates.push({
270
+ name: trimmedScope + '.' + trimmedPinLabel,
271
+ source
272
+ })
273
+ }
274
+
275
+ /**
276
+ * Deduplicates alias candidates by visible alias name.
277
+ * @param {{ name: string, source: string }[]} candidates Alias candidates.
278
+ * @returns {{ name: string, source: string }[]}
279
+ */
280
+ static #dedupeAliasCandidates(candidates) {
281
+ const seen = new Set()
282
+ const uniqueCandidates = []
283
+
284
+ for (const candidate of candidates || []) {
285
+ if (!candidate.name || seen.has(candidate.name)) {
286
+ continue
287
+ }
288
+
289
+ seen.add(candidate.name)
290
+ uniqueCandidates.push(candidate)
291
+ }
292
+
293
+ return uniqueCandidates
294
+ }
295
+
296
+ /**
297
+ * Looks up a component designator by native owner id.
298
+ * @param {Map<string, string> | Record<string, string> | undefined} componentDesignatorsByOwnerIndex Component designators by native owner id.
299
+ * @param {string | undefined} ownerIndex Native owner id.
300
+ * @returns {string}
301
+ */
302
+ static #lookupComponentDesignator(
303
+ componentDesignatorsByOwnerIndex,
304
+ ownerIndex
305
+ ) {
306
+ const key = String(ownerIndex || '').trim()
307
+ if (!key || !componentDesignatorsByOwnerIndex) {
308
+ return ''
309
+ }
310
+
311
+ if (componentDesignatorsByOwnerIndex instanceof Map) {
312
+ return String(componentDesignatorsByOwnerIndex.get(key) || '')
313
+ }
314
+
315
+ return String(componentDesignatorsByOwnerIndex[key] || '')
316
+ }
317
+
318
+ /**
319
+ * Merges physically separate net groups that share one power-port label.
320
+ * @param {{ name: string, powerPorts: { text?: string }[] }[]} nets Source nets.
321
+ * @returns {object[]}
322
+ */
323
+ static #mergeMatchingPowerPortNets(nets) {
324
+ const mergedNets = []
325
+ const netsByPowerName = new Map()
326
+
327
+ for (const net of nets) {
328
+ const powerName =
329
+ SchematicNetlistBuilder.#singlePowerPortNetName(net)
330
+
331
+ if (!powerName) {
332
+ mergedNets.push(net)
333
+ continue
334
+ }
335
+
336
+ const existingNet = netsByPowerName.get(powerName)
337
+ if (!existingNet) {
338
+ netsByPowerName.set(powerName, net)
339
+ mergedNets.push(net)
340
+ continue
341
+ }
342
+
343
+ SchematicNetlistBuilder.#appendNet(existingNet, net)
344
+ }
345
+
346
+ return mergedNets
347
+ }
348
+
349
+ /**
350
+ * Returns one normalized power-port name when every port agrees.
351
+ * @param {{ powerPorts?: { text?: string }[] }} net Source net.
352
+ * @returns {string}
353
+ */
354
+ static #singlePowerPortNetName(net) {
355
+ const names = [
356
+ ...new Set(
357
+ (net.powerPorts || [])
358
+ .map((powerPort) => String(powerPort.text || '').trim())
359
+ .filter(Boolean)
360
+ .map((name) => name.toUpperCase())
361
+ )
362
+ ]
363
+
364
+ return names.length === 1 ? names[0] : ''
365
+ }
366
+
367
+ /**
368
+ * Appends one net's observable members to an existing logical net.
369
+ * @param {object} target Existing logical net.
370
+ * @param {object} source Source net to append.
371
+ * @returns {void}
372
+ */
373
+ static #appendNet(target, source) {
374
+ for (const key of [
375
+ 'segments',
376
+ 'labels',
377
+ 'powerPorts',
378
+ 'crossSheetConnectors',
379
+ 'pins',
380
+ 'ports',
381
+ 'junctions',
382
+ 'busEntries',
383
+ 'sheetEntries'
384
+ ]) {
385
+ target[key].push(...(source[key] || []))
386
+ }
124
387
  }
125
388
 
126
389
  /**
@@ -15,7 +15,7 @@ export class SchematicOwnershipGraphParser {
15
15
  /**
16
16
  * Parses record parent/child links from raw schematic records.
17
17
  * @param {{ fields: Record<string, string | string[]>, recordIndex?: number }[]} records Schematic records.
18
- * @returns {{ schema: string, records: object[], recordsByIndexInSheet: Record<string, object>, childrenByParentKey: Record<string, object[]>, parentsByChildKey: Record<string, object> }}
18
+ * @returns {{ schema: string, records: object[], hierarchy: object[], recordsByRecordIndex: Record<string, object>, recordsByIndexInSheet: Record<string, object>, childrenByParentKey: Record<string, object[]>, parentsByChildKey: Record<string, object> }}
19
19
  */
20
20
  static parse(records) {
21
21
  const entries = (records || []).map((record, fallbackIndex) =>
@@ -54,6 +54,12 @@ export class SchematicOwnershipGraphParser {
54
54
  records: entries.map((entry) =>
55
55
  SchematicOwnershipGraphParser.#publicRecord(entry)
56
56
  ),
57
+ hierarchy: SchematicOwnershipGraphParser.#hierarchy(
58
+ entries,
59
+ parentsByChildKey
60
+ ),
61
+ recordsByRecordIndex:
62
+ SchematicOwnershipGraphParser.#recordsByRecordIndex(entries),
57
63
  recordsByIndexInSheet:
58
64
  SchematicOwnershipGraphParser.#recordsByIndexInSheet(entries),
59
65
  childrenByParentKey,
@@ -61,6 +67,63 @@ export class SchematicOwnershipGraphParser {
61
67
  }
62
68
  }
63
69
 
70
+ /**
71
+ * Builds a nested public hierarchy from resolved parent links.
72
+ * @param {object[]} entries Record sidecar rows.
73
+ * @param {Record<string, { parentKey: string }>} parentsByChildKey Resolved parent links.
74
+ * @returns {object[]}
75
+ */
76
+ static #hierarchy(entries, parentsByChildKey) {
77
+ const entriesByKey = new Map(entries.map((entry) => [entry.key, entry]))
78
+ const childrenByParentKey = new Map()
79
+ const childKeys = new Set()
80
+
81
+ for (const [childKey, parentLink] of Object.entries(
82
+ parentsByChildKey
83
+ )) {
84
+ const child = entriesByKey.get(childKey)
85
+ const parent = entriesByKey.get(parentLink.parentKey)
86
+
87
+ if (!child || !parent) {
88
+ continue
89
+ }
90
+
91
+ childKeys.add(childKey)
92
+ if (!childrenByParentKey.has(parent.key)) {
93
+ childrenByParentKey.set(parent.key, [])
94
+ }
95
+
96
+ childrenByParentKey.get(parent.key).push(child)
97
+ }
98
+
99
+ return entries
100
+ .filter((entry) => !childKeys.has(entry.key))
101
+ .map((entry) =>
102
+ SchematicOwnershipGraphParser.#hierarchyNode(
103
+ entry,
104
+ childrenByParentKey
105
+ )
106
+ )
107
+ }
108
+
109
+ /**
110
+ * Builds one nested hierarchy node.
111
+ * @param {object} entry Record sidecar row.
112
+ * @param {Map<string, object[]>} childrenByParentKey Child rows by parent key.
113
+ * @returns {object}
114
+ */
115
+ static #hierarchyNode(entry, childrenByParentKey) {
116
+ return {
117
+ ...SchematicOwnershipGraphParser.#publicRecord(entry),
118
+ children: (childrenByParentKey.get(entry.key) || []).map((child) =>
119
+ SchematicOwnershipGraphParser.#hierarchyNode(
120
+ child,
121
+ childrenByParentKey
122
+ )
123
+ )
124
+ }
125
+ }
126
+
64
127
  /**
65
128
  * Normalizes one raw schematic record into a sidecar row.
66
129
  * @param {{ fields: Record<string, string | string[]>, recordIndex?: number }} record Source record.
@@ -83,7 +146,8 @@ export class SchematicOwnershipGraphParser {
83
146
  getField(record?.fields, 'UniqueID') ||
84
147
  getField(record?.fields, 'UniqueId'),
85
148
  name: getField(record?.fields, 'Name'),
86
- text: getField(record?.fields, 'Text')
149
+ text: getField(record?.fields, 'Text'),
150
+ fields: SchematicOwnershipGraphParser.#publicFields(record?.fields)
87
151
  }
88
152
  }
89
153
 
@@ -139,7 +203,8 @@ export class SchematicOwnershipGraphParser {
139
203
  ownerIndex: entry.ownerIndex,
140
204
  uniqueId: entry.uniqueId,
141
205
  name: entry.name,
142
- text: entry.text
206
+ text: entry.text,
207
+ fields: entry.fields
143
208
  })
144
209
  }
145
210
 
@@ -159,6 +224,22 @@ export class SchematicOwnershipGraphParser {
159
224
  })
160
225
  }
161
226
 
227
+ /**
228
+ * Indexes records by parser record index.
229
+ * @param {object[]} entries Record sidecar rows.
230
+ * @returns {Record<string, object>}
231
+ */
232
+ static #recordsByRecordIndex(entries) {
233
+ const index = {}
234
+
235
+ for (const entry of entries) {
236
+ index[String(entry.recordIndex)] =
237
+ SchematicOwnershipGraphParser.#publicRecord(entry)
238
+ }
239
+
240
+ return index
241
+ }
242
+
162
243
  /**
163
244
  * Indexes records by native IndexInSheet values.
164
245
  * @param {object[]} entries Record sidecar rows.
@@ -177,6 +258,24 @@ export class SchematicOwnershipGraphParser {
177
258
  return index
178
259
  }
179
260
 
261
+ /**
262
+ * Copies source fields into a stable plain object for read-only consumers.
263
+ * @param {Record<string, string | string[]> | undefined} fields Source fields.
264
+ * @returns {Record<string, string | string[]>}
265
+ */
266
+ static #publicFields(fields) {
267
+ if (!fields || typeof fields !== 'object') {
268
+ return {}
269
+ }
270
+
271
+ return Object.fromEntries(
272
+ Object.entries(fields).map(([key, value]) => [
273
+ key,
274
+ Array.isArray(value) ? [...value] : value
275
+ ])
276
+ )
277
+ }
278
+
180
279
  /**
181
280
  * Drops empty object fields while preserving numeric zero.
182
281
  * @param {Record<string, unknown>} value Candidate object.
@@ -5,6 +5,7 @@
5
5
  import { ParserUtils } from './ParserUtils.mjs'
6
6
  import { SchematicNoErcSymbolResolver } from './SchematicNoErcSymbolResolver.mjs'
7
7
  import { SchematicPinDesignatorInferer } from './SchematicPinDesignatorInferer.mjs'
8
+ import { SchematicTextRunParser } from './SchematicTextRunParser.mjs'
8
9
 
9
10
  /**
10
11
  * Helpers for normalized schematic pins, ports, and crosses.
@@ -480,7 +481,7 @@ export class SchematicPinParser {
480
481
  options.recordType
481
482
  )
482
483
  ),
483
- width: ParserUtils.parseNumericField(fields, 'LineWidth') || 1,
484
+ width: ParserUtils.parseSchematicLineWidth(fields),
484
485
  lineStyle,
485
486
  isBus: options.isBus === true ? true : undefined,
486
487
  recordType: options.recordType || undefined,
@@ -524,7 +525,7 @@ export class SchematicPinParser {
524
525
  fields.Color,
525
526
  SchematicPinParser.#resolveDefaultPolylineColor(fields, '7')
526
527
  ),
527
- width: ParserUtils.parseNumericField(fields, 'LineWidth') || 1,
528
+ width: ParserUtils.parseSchematicLineWidth(fields),
528
529
  lineStyle
529
530
  })
530
531
  }
@@ -541,7 +542,7 @@ export class SchematicPinParser {
541
542
  fields.Color,
542
543
  SchematicPinParser.#resolveDefaultPolylineColor(fields, '7')
543
544
  ),
544
- width: ParserUtils.parseNumericField(fields, 'LineWidth') || 1,
545
+ width: ParserUtils.parseSchematicLineWidth(fields),
545
546
  lineStyle
546
547
  })
547
548
 
@@ -644,8 +645,12 @@ export class SchematicPinParser {
644
645
  case 2:
645
646
  return 'filled-arrow'
646
647
  case 3:
647
- return 'circle'
648
+ return 'tail'
648
649
  case 4:
650
+ return 'filled-tail'
651
+ case 5:
652
+ return 'circle'
653
+ case 6:
649
654
  return 'square'
650
655
  default:
651
656
  return 'marker-' + shape
@@ -1209,49 +1214,11 @@ export class SchematicPinParser {
1209
1214
  * @returns {{ name: string, nameSegments?: { text: string, overline: boolean }[] }}
1210
1215
  */
1211
1216
  static #parseSchematicPinName(name) {
1212
- const characters = []
1213
-
1214
- for (const character of String(name || '').trim()) {
1215
- if (character === '\\') {
1216
- const previousCharacter = characters.at(-1)
1217
- if (previousCharacter) {
1218
- previousCharacter.overline = true
1219
- }
1220
- continue
1221
- }
1222
-
1223
- characters.push({
1224
- text: character,
1225
- overline: false
1226
- })
1227
- }
1228
-
1229
- const normalizedName = characters
1230
- .map((character) => character.text)
1231
- .join('')
1232
- const nameSegments = []
1233
-
1234
- for (const character of characters) {
1235
- const previousSegment = nameSegments.at(-1)
1236
- if (
1237
- previousSegment &&
1238
- previousSegment.overline === character.overline
1239
- ) {
1240
- previousSegment.text += character.text
1241
- continue
1242
- }
1243
-
1244
- nameSegments.push({
1245
- text: character.text,
1246
- overline: character.overline
1247
- })
1248
- }
1217
+ const parsed = SchematicTextRunParser.parseOptionalOverlineRuns(name)
1249
1218
 
1250
1219
  return {
1251
- name: normalizedName,
1252
- nameSegments: nameSegments.some((segment) => segment.overline)
1253
- ? nameSegments
1254
- : undefined
1220
+ name: parsed.text,
1221
+ nameSegments: parsed.segments
1255
1222
  }
1256
1223
  }
1257
1224
 
@@ -9,6 +9,7 @@ const {
9
9
  parseBoolean,
10
10
  parseNumericField,
11
11
  parseNumericFieldWithFraction,
12
+ parseSchematicLineWidth,
12
13
  toColor
13
14
  } = ParserUtils
14
15
 
@@ -134,8 +135,7 @@ export class SchematicPrimitiveParser {
134
135
  fill: toColor(record.fields.AreaColor, color),
135
136
  isSolid: parseBoolean(record.fields.IsSolid),
136
137
  transparent: parseBoolean(record.fields.Transparent),
137
- lineWidth:
138
- parseNumericField(record.fields, 'LineWidth') || 1,
138
+ lineWidth: parseSchematicLineWidth(record.fields),
139
139
  renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
140
140
  record.fields,
141
141
  index
@@ -169,7 +169,7 @@ export class SchematicPrimitiveParser {
169
169
  points,
170
170
  segments,
171
171
  color: toColor(record.fields.Color, '#000000'),
172
- width: parseNumericField(record.fields, 'LineWidth') || 1,
172
+ width: parseSchematicLineWidth(record.fields),
173
173
  lineStyle:
174
174
  parseNumericField(record.fields, 'LineStyle') || 0,
175
175
  renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
@@ -233,8 +233,7 @@ export class SchematicPrimitiveParser {
233
233
  fill: toColor(record.fields.AreaColor, '#ffffff'),
234
234
  isSolid: parseBoolean(record.fields.IsSolid),
235
235
  transparent: parseBoolean(record.fields.Transparent),
236
- lineWidth:
237
- parseNumericField(record.fields, 'LineWidth') || 1,
236
+ lineWidth: parseSchematicLineWidth(record.fields),
238
237
  renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
239
238
  record.fields,
240
239
  index
@@ -280,8 +279,7 @@ export class SchematicPrimitiveParser {
280
279
  fill: toColor(record.fields.AreaColor, '#ffffff'),
281
280
  isSolid: parseBoolean(record.fields.IsSolid),
282
281
  transparent: parseBoolean(record.fields.Transparent),
283
- lineWidth:
284
- parseNumericField(record.fields, 'LineWidth') || 1,
282
+ lineWidth: parseSchematicLineWidth(record.fields),
285
283
  lineStyle:
286
284
  parseNumericField(record.fields, 'LineStyle') || 0,
287
285
  renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
@@ -339,8 +337,7 @@ export class SchematicPrimitiveParser {
339
337
  1
340
338
  ),
341
339
  color: toColor(record.fields.Color, '#000000'),
342
- lineWidth:
343
- parseNumericField(record.fields, 'LineWidth') || 1,
340
+ lineWidth: parseSchematicLineWidth(record.fields),
344
341
  renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
345
342
  record.fields,
346
343
  index
@@ -428,8 +425,7 @@ export class SchematicPrimitiveParser {
428
425
  transparent: usesFrameFallback
429
426
  ? false
430
427
  : parseBoolean(record.fields.Transparent),
431
- lineWidth:
432
- parseNumericField(record.fields, 'LineWidth') || 1,
428
+ lineWidth: parseSchematicLineWidth(record.fields),
433
429
  lineStyle:
434
430
  usesFrameFallback ||
435
431
  SchematicPrimitiveParser.#shouldDefaultBlanketDash(
@@ -604,7 +600,7 @@ export class SchematicPrimitiveParser {
604
600
  startAngle: startAngle === null ? 0 : startAngle,
605
601
  endAngle: endAngle === null ? 360 : endAngle,
606
602
  color: toColor(record.fields.Color, '#a44a1b'),
607
- width: parseNumericField(record.fields, 'LineWidth') || 1,
603
+ width: parseSchematicLineWidth(record.fields),
608
604
  renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
609
605
  record.fields,
610
606
  index
@@ -661,8 +657,7 @@ export class SchematicPrimitiveParser {
661
657
  fill: toColor(record.fields.AreaColor, '#ffffff'),
662
658
  isSolid: parseBoolean(record.fields.IsSolid),
663
659
  transparent: parseBoolean(record.fields.Transparent),
664
- lineWidth:
665
- parseNumericField(record.fields, 'LineWidth') || 1,
660
+ lineWidth: parseSchematicLineWidth(record.fields),
666
661
  renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
667
662
  record.fields,
668
663
  index
@@ -3,6 +3,7 @@
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
5
  import { ParserUtils } from './ParserUtils.mjs'
6
+ import { SchematicFieldCoverageReportBuilder } from './SchematicFieldCoverageReportBuilder.mjs'
6
7
 
7
8
  const { parseBoolean, parseNumericField, toColor } = ParserUtils
8
9
 
@@ -55,6 +56,7 @@ export class SchematicQaReportBuilder {
55
56
  geometryFallbacks: SchematicQaReportBuilder.#geometryFallbacks(
56
57
  input?.texts
57
58
  ),
59
+ fieldCoverage: SchematicFieldCoverageReportBuilder.build(records),
58
60
  findings
59
61
  }
60
62
  }