circuitjson-toolkit 1.0.10 → 1.0.17

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 (40) hide show
  1. package/AGENTS.md +20 -3
  2. package/docs/model-format.md +15 -0
  3. package/package.json +3 -2
  4. package/src/core/CircuitJsonBomBuilder.mjs +22 -25
  5. package/src/core/CircuitJsonElementValidator.mjs +233 -16
  6. package/src/core/CircuitJsonIndexer.mjs +510 -5
  7. package/src/core/CircuitJsonManufacturingBuilder.mjs +488 -16
  8. package/src/core/CircuitJsonManufacturingDownloadBuilder.mjs +196 -0
  9. package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +329 -0
  10. package/src/core/CircuitJsonPcbCopperGeometry.mjs +503 -0
  11. package/src/core/CircuitJsonPcbDrawingStyle.mjs +88 -0
  12. package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +172 -0
  13. package/src/core/CircuitJsonPcbNetMetadata.mjs +247 -0
  14. package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +70 -0
  15. package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +992 -0
  16. package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +872 -0
  17. package/src/core/CircuitJsonPcbPrimitiveFields.mjs +233 -0
  18. package/src/core/CircuitJsonPcbPrimitiveGeometry.mjs +142 -0
  19. package/src/core/CircuitJsonPcbPrimitiveGroups.mjs +305 -0
  20. package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +65 -0
  21. package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +895 -0
  22. package/src/core/CircuitJsonPcbTraceLengthModel.mjs +257 -0
  23. package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +683 -0
  24. package/src/core/CircuitJsonSourceMetadata.mjs +233 -0
  25. package/src/core/CircuitJsonSupportMatrixBuilder.mjs +227 -5
  26. package/src/core/PcbBoundsSelectionModel.mjs +250 -0
  27. package/src/core/PcbCandidateSelectionModel.mjs +77 -0
  28. package/src/core/PcbDiagnosticFocusModel.mjs +423 -0
  29. package/src/core/PcbInteractionPrimitiveModel.mjs +560 -0
  30. package/src/core/SelectedPartCircuitJsonExportAdapter.mjs +335 -0
  31. package/src/index.mjs +2 -0
  32. package/src/renderers.mjs +29 -0
  33. package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +128 -0
  34. package/src/ui/CircuitJsonPcbSvgRenderer.mjs +964 -0
  35. package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +168 -0
  36. package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +138 -0
  37. package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +114 -0
  38. package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +130 -0
  39. package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +994 -0
  40. package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +439 -0
@@ -19,6 +19,20 @@ const ID_FIELDS_BY_TYPE = {
19
19
  }
20
20
 
21
21
  const GROUP_TYPES = new Set(['source_group', 'pcb_group', 'schematic_group'])
22
+ const SCHEMATIC_PRIMITIVE_TYPES = new Set([
23
+ 'schematic_arc',
24
+ 'schematic_box',
25
+ 'schematic_circle',
26
+ 'schematic_line',
27
+ 'schematic_net_label',
28
+ 'schematic_path',
29
+ 'schematic_rect',
30
+ 'schematic_table',
31
+ 'schematic_table_cell',
32
+ 'schematic_text',
33
+ 'schematic_trace',
34
+ 'schematic_voltage_probe'
35
+ ])
22
36
 
23
37
  /**
24
38
  * Builds lookup maps for CircuitJSON element arrays.
@@ -27,7 +41,7 @@ export class CircuitJsonIndexer {
27
41
  /**
28
42
  * Indexes a CircuitJSON model.
29
43
  * @param {object[]} circuitJson CircuitJSON model.
30
- * @returns {{ elements: object[], elementsByType: Map<string, object[]>, elementsById: Map<string, object>, relationsByField: Map<string, Map<string, object[]>>, sourceComponentById: Map<string, object>, pcbComponentById: Map<string, object>, componentsBySourceId: Map<string, object>, groupsById: Map<string, object>, elementsByGroupId: Map<string, object[]>, elementsBySubcircuitId: Map<string, object[]>, diagnostics: object[] }}
44
+ * @returns {{ elements: object[], elementsByType: Map<string, object[]>, elementsById: Map<string, object>, relationsByField: Map<string, Map<string, object[]>>, sourceComponentById: Map<string, object>, pcbComponentById: Map<string, object>, sourceTraceById: Map<string, object>, sourceTraceConnectivity: Map<string, object>, componentsBySourceId: Map<string, object>, groupsById: Map<string, object>, elementsByGroupId: Map<string, object[]>, elementsBySubcircuitId: Map<string, object[]>, diagnostics: object[] }}
31
45
  */
32
46
  static index(circuitJson) {
33
47
  CircuitJsonDocument.assertModel(circuitJson)
@@ -36,6 +50,7 @@ export class CircuitJsonIndexer {
36
50
  const relationsByField = new Map()
37
51
  const sourceComponentById = new Map()
38
52
  const pcbComponentById = new Map()
53
+ const sourceTraceById = new Map()
39
54
 
40
55
  circuitJson.forEach((element) => {
41
56
  const type = String(element?.type || '')
@@ -54,8 +69,13 @@ export class CircuitJsonIndexer {
54
69
  if (type === 'pcb_component' && id) {
55
70
  pcbComponentById.set(id, element)
56
71
  }
72
+ if (type === 'source_trace' && id) {
73
+ sourceTraceById.set(id, element)
74
+ }
57
75
  CircuitJsonIndexer.#indexRelations(element, relationsByField)
58
76
  })
77
+ const sourceTraceConnectivity =
78
+ CircuitJsonIndexer.#sourceTraceConnectivity(sourceTraceById)
59
79
 
60
80
  return {
61
81
  elements: circuitJson,
@@ -64,6 +84,8 @@ export class CircuitJsonIndexer {
64
84
  relationsByField,
65
85
  sourceComponentById,
66
86
  pcbComponentById,
87
+ sourceTraceById,
88
+ sourceTraceConnectivity,
67
89
  componentsBySourceId: CircuitJsonIndexer.#componentsBySourceId(
68
90
  sourceComponentById,
69
91
  relationsByField
@@ -73,7 +95,14 @@ export class CircuitJsonIndexer {
73
95
  CircuitJsonIndexer.#elementsByGroupId(circuitJson),
74
96
  elementsBySubcircuitId:
75
97
  CircuitJsonIndexer.#elementsBySubcircuitId(circuitJson),
76
- diagnostics: CircuitJsonIndexer.collectDiagnostics(circuitJson)
98
+ diagnostics: [
99
+ ...CircuitJsonIndexer.collectDiagnostics(circuitJson),
100
+ ...CircuitJsonIndexer.#referenceDiagnostics(
101
+ elementsByType,
102
+ sourceTraceById,
103
+ sourceTraceConnectivity
104
+ )
105
+ ]
77
106
  }
78
107
  }
79
108
 
@@ -135,7 +164,9 @@ export class CircuitJsonIndexer {
135
164
  * @returns {string[]}
136
165
  */
137
166
  static #relationValues(value) {
138
- const values = Array.isArray(value) ? value : [value]
167
+ const values = Array.isArray(value)
168
+ ? value.flatMap((entry) => (Array.isArray(entry) ? entry : [entry]))
169
+ : [value]
139
170
  return values.map((entry) => String(entry || '').trim()).filter(Boolean)
140
171
  }
141
172
 
@@ -169,6 +200,413 @@ export class CircuitJsonIndexer {
169
200
  return bySource
170
201
  }
171
202
 
203
+ /**
204
+ * Builds connectivity summaries keyed by source trace id.
205
+ * @param {Map<string, object>} sourceTraceById Source trace lookup.
206
+ * @returns {Map<string, object>}
207
+ */
208
+ static #sourceTraceConnectivity(sourceTraceById) {
209
+ const connectivity = new Map()
210
+ for (const [sourceTraceId, sourceTrace] of sourceTraceById) {
211
+ connectivity.set(sourceTraceId, {
212
+ sourceTraceId,
213
+ connectedSourcePortIds: CircuitJsonIndexer.#relationValues([
214
+ sourceTrace.connected_source_port_id,
215
+ sourceTrace.source_port_id,
216
+ sourceTrace.connected_source_port_ids,
217
+ sourceTrace.source_port_ids
218
+ ]),
219
+ connectedSourceNetIds: CircuitJsonIndexer.#relationValues([
220
+ sourceTrace.connected_source_net_id,
221
+ sourceTrace.source_net_id,
222
+ sourceTrace.connected_source_net_ids,
223
+ sourceTrace.source_net_ids
224
+ ])
225
+ })
226
+ }
227
+ return connectivity
228
+ }
229
+
230
+ /**
231
+ * Builds generated diagnostics for broken source-trace references.
232
+ * @param {Map<string, object[]>} elementsByType Element rows by type.
233
+ * @param {Map<string, object>} sourceTraceById Source trace lookup.
234
+ * @param {Map<string, object>} sourceTraceConnectivity Connectivity lookup.
235
+ * @returns {object[]}
236
+ */
237
+ static #referenceDiagnostics(
238
+ elementsByType,
239
+ sourceTraceById,
240
+ sourceTraceConnectivity
241
+ ) {
242
+ const sourcePortIds = new Set(
243
+ CircuitJsonIndexer.#all(elementsByType, 'source_port')
244
+ .map((port) => String(port.source_port_id || '').trim())
245
+ .filter(Boolean)
246
+ )
247
+ const sourceNetIds = new Set(
248
+ CircuitJsonIndexer.#all(elementsByType, 'source_net')
249
+ .map((net) => String(net.source_net_id || '').trim())
250
+ .filter(Boolean)
251
+ )
252
+ const schematicSymbolIds = CircuitJsonIndexer.#elementIds(
253
+ elementsByType,
254
+ 'schematic_symbol'
255
+ )
256
+ const schematicComponentIds = CircuitJsonIndexer.#elementIds(
257
+ elementsByType,
258
+ 'schematic_component'
259
+ )
260
+ return [
261
+ ...CircuitJsonIndexer.#missingSourceTracePortDiagnostics(
262
+ sourceTraceConnectivity,
263
+ sourcePortIds
264
+ ),
265
+ ...CircuitJsonIndexer.#missingSourceTraceNetDiagnostics(
266
+ sourceTraceConnectivity,
267
+ sourceNetIds
268
+ ),
269
+ ...CircuitJsonIndexer.#missingPcbSourceTraceDiagnostics(
270
+ elementsByType,
271
+ sourceTraceById
272
+ ),
273
+ ...CircuitJsonIndexer.#missingSchematicComponentSymbolDiagnostics(
274
+ elementsByType,
275
+ schematicSymbolIds
276
+ ),
277
+ ...CircuitJsonIndexer.#missingSchematicPortComponentDiagnostics(
278
+ elementsByType,
279
+ schematicComponentIds
280
+ ),
281
+ ...CircuitJsonIndexer.#missingSchematicPortSourcePortDiagnostics(
282
+ elementsByType,
283
+ sourcePortIds
284
+ ),
285
+ ...CircuitJsonIndexer.#missingSchematicPrimitiveDiagnostics(
286
+ elementsByType,
287
+ schematicSymbolIds,
288
+ schematicComponentIds
289
+ )
290
+ ]
291
+ }
292
+
293
+ /**
294
+ * Builds source-trace missing source-port diagnostics.
295
+ * @param {Map<string, object>} sourceTraceConnectivity Connectivity lookup.
296
+ * @param {Set<string>} sourcePortIds Known source port ids.
297
+ * @returns {object[]}
298
+ */
299
+ static #missingSourceTracePortDiagnostics(
300
+ sourceTraceConnectivity,
301
+ sourcePortIds
302
+ ) {
303
+ return [...sourceTraceConnectivity.values()].flatMap((trace) =>
304
+ trace.connectedSourcePortIds
305
+ .filter((sourcePortId) => !sourcePortIds.has(sourcePortId))
306
+ .map((sourcePortId) => ({
307
+ isGenerated: true,
308
+ severity: 'warning',
309
+ sourceFormat: 'circuitjson',
310
+ type: 'source_trace_missing_source_port_warning',
311
+ category: 'connectivity',
312
+ message:
313
+ 'Source trace ' +
314
+ trace.sourceTraceId +
315
+ ' references missing source port ' +
316
+ sourcePortId +
317
+ '.',
318
+ elementId:
319
+ trace.sourceTraceId + ':missing-port:' + sourcePortId,
320
+ sourceTraceId: trace.sourceTraceId,
321
+ sourcePortId
322
+ }))
323
+ )
324
+ }
325
+
326
+ /**
327
+ * Builds source-trace missing source-net diagnostics.
328
+ * @param {Map<string, object>} sourceTraceConnectivity Connectivity lookup.
329
+ * @param {Set<string>} sourceNetIds Known source net ids.
330
+ * @returns {object[]}
331
+ */
332
+ static #missingSourceTraceNetDiagnostics(
333
+ sourceTraceConnectivity,
334
+ sourceNetIds
335
+ ) {
336
+ return [...sourceTraceConnectivity.values()].flatMap((trace) =>
337
+ trace.connectedSourceNetIds
338
+ .filter((sourceNetId) => !sourceNetIds.has(sourceNetId))
339
+ .map((sourceNetId) => ({
340
+ isGenerated: true,
341
+ severity: 'warning',
342
+ sourceFormat: 'circuitjson',
343
+ type: 'source_trace_missing_source_net_warning',
344
+ category: 'connectivity',
345
+ message:
346
+ 'Source trace ' +
347
+ trace.sourceTraceId +
348
+ ' references missing source net ' +
349
+ sourceNetId +
350
+ '.',
351
+ elementId:
352
+ trace.sourceTraceId + ':missing-net:' + sourceNetId,
353
+ sourceTraceId: trace.sourceTraceId,
354
+ sourceNetId
355
+ }))
356
+ )
357
+ }
358
+
359
+ /**
360
+ * Builds PCB-trace missing source-trace diagnostics.
361
+ * @param {Map<string, object[]>} elementsByType Element rows by type.
362
+ * @param {Map<string, object>} sourceTraceById Source trace lookup.
363
+ * @returns {object[]}
364
+ */
365
+ static #missingPcbSourceTraceDiagnostics(elementsByType, sourceTraceById) {
366
+ return CircuitJsonIndexer.#all(elementsByType, 'pcb_trace')
367
+ .map((trace) => ({
368
+ pcbTraceId: String(trace.pcb_trace_id || '').trim(),
369
+ sourceTraceId: String(trace.source_trace_id || '').trim()
370
+ }))
371
+ .filter(
372
+ (trace) =>
373
+ trace.sourceTraceId &&
374
+ !sourceTraceById.has(trace.sourceTraceId)
375
+ )
376
+ .map((trace) => ({
377
+ isGenerated: true,
378
+ severity: 'warning',
379
+ sourceFormat: 'circuitjson',
380
+ type: 'pcb_trace_missing_source_trace_warning',
381
+ category: 'connectivity',
382
+ message:
383
+ 'PCB trace ' +
384
+ trace.pcbTraceId +
385
+ ' references missing source trace ' +
386
+ trace.sourceTraceId +
387
+ '.',
388
+ elementId:
389
+ trace.pcbTraceId +
390
+ ':missing-source-trace:' +
391
+ trace.sourceTraceId,
392
+ sourceTraceId: trace.sourceTraceId,
393
+ pcbTraceId: trace.pcbTraceId
394
+ }))
395
+ }
396
+
397
+ /**
398
+ * Builds schematic-component missing symbol diagnostics.
399
+ * @param {Map<string, object[]>} elementsByType Element rows by type.
400
+ * @param {Set<string>} schematicSymbolIds Known schematic symbol ids.
401
+ * @returns {object[]}
402
+ */
403
+ static #missingSchematicComponentSymbolDiagnostics(
404
+ elementsByType,
405
+ schematicSymbolIds
406
+ ) {
407
+ return CircuitJsonIndexer.#all(elementsByType, 'schematic_component')
408
+ .map((component) => ({
409
+ schematicComponentId:
410
+ CircuitJsonIndexer.getElementId(component),
411
+ schematicSymbolId: String(
412
+ component.schematic_symbol_id || ''
413
+ ).trim()
414
+ }))
415
+ .filter(
416
+ (component) =>
417
+ component.schematicSymbolId &&
418
+ !schematicSymbolIds.has(component.schematicSymbolId)
419
+ )
420
+ .map((component) => ({
421
+ isGenerated: true,
422
+ severity: 'warning',
423
+ sourceFormat: 'circuitjson',
424
+ type: 'schematic_component_missing_schematic_symbol_warning',
425
+ category: 'layout',
426
+ message:
427
+ 'Schematic component ' +
428
+ component.schematicComponentId +
429
+ ' references missing schematic symbol ' +
430
+ component.schematicSymbolId +
431
+ '.',
432
+ elementId:
433
+ component.schematicComponentId +
434
+ ':missing-symbol:' +
435
+ component.schematicSymbolId,
436
+ schematicComponentId: component.schematicComponentId,
437
+ schematicSymbolId: component.schematicSymbolId
438
+ }))
439
+ }
440
+
441
+ /**
442
+ * Builds schematic-port missing component diagnostics.
443
+ * @param {Map<string, object[]>} elementsByType Element rows by type.
444
+ * @param {Set<string>} schematicComponentIds Known schematic component ids.
445
+ * @returns {object[]}
446
+ */
447
+ static #missingSchematicPortComponentDiagnostics(
448
+ elementsByType,
449
+ schematicComponentIds
450
+ ) {
451
+ return CircuitJsonIndexer.#all(elementsByType, 'schematic_port')
452
+ .map((port) => ({
453
+ schematicPortId: CircuitJsonIndexer.getElementId(port),
454
+ schematicComponentId: String(
455
+ port.schematic_component_id || ''
456
+ ).trim()
457
+ }))
458
+ .filter(
459
+ (port) =>
460
+ port.schematicComponentId &&
461
+ !schematicComponentIds.has(port.schematicComponentId)
462
+ )
463
+ .map((port) => ({
464
+ isGenerated: true,
465
+ severity: 'warning',
466
+ sourceFormat: 'circuitjson',
467
+ type: 'schematic_port_missing_schematic_component_warning',
468
+ category: 'layout',
469
+ message:
470
+ 'Schematic port ' +
471
+ port.schematicPortId +
472
+ ' references missing schematic component ' +
473
+ port.schematicComponentId +
474
+ '.',
475
+ elementId:
476
+ port.schematicPortId +
477
+ ':missing-component:' +
478
+ port.schematicComponentId,
479
+ schematicComponentId: port.schematicComponentId,
480
+ schematicPortId: port.schematicPortId
481
+ }))
482
+ }
483
+
484
+ /**
485
+ * Builds schematic-port missing source-port diagnostics.
486
+ * @param {Map<string, object[]>} elementsByType Element rows by type.
487
+ * @param {Set<string>} sourcePortIds Known source port ids.
488
+ * @returns {object[]}
489
+ */
490
+ static #missingSchematicPortSourcePortDiagnostics(
491
+ elementsByType,
492
+ sourcePortIds
493
+ ) {
494
+ return CircuitJsonIndexer.#all(elementsByType, 'schematic_port')
495
+ .map((port) => ({
496
+ schematicPortId: CircuitJsonIndexer.getElementId(port),
497
+ sourcePortId: String(port.source_port_id || '').trim()
498
+ }))
499
+ .filter(
500
+ (port) =>
501
+ port.sourcePortId && !sourcePortIds.has(port.sourcePortId)
502
+ )
503
+ .map((port) => ({
504
+ isGenerated: true,
505
+ severity: 'warning',
506
+ sourceFormat: 'circuitjson',
507
+ type: 'schematic_port_missing_source_port_warning',
508
+ category: 'connectivity',
509
+ message:
510
+ 'Schematic port ' +
511
+ port.schematicPortId +
512
+ ' references missing source port ' +
513
+ port.sourcePortId +
514
+ '.',
515
+ elementId:
516
+ port.schematicPortId +
517
+ ':missing-source-port:' +
518
+ port.sourcePortId,
519
+ schematicPortId: port.schematicPortId,
520
+ sourcePortId: port.sourcePortId
521
+ }))
522
+ }
523
+
524
+ /**
525
+ * Builds schematic primitive missing relation diagnostics.
526
+ * @param {Map<string, object[]>} elementsByType Element rows by type.
527
+ * @param {Set<string>} schematicSymbolIds Known schematic symbol ids.
528
+ * @param {Set<string>} schematicComponentIds Known schematic component ids.
529
+ * @returns {object[]}
530
+ */
531
+ static #missingSchematicPrimitiveDiagnostics(
532
+ elementsByType,
533
+ schematicSymbolIds,
534
+ schematicComponentIds
535
+ ) {
536
+ return [...SCHEMATIC_PRIMITIVE_TYPES].flatMap((type) =>
537
+ CircuitJsonIndexer.#all(elementsByType, type).flatMap((element) =>
538
+ CircuitJsonIndexer.#missingSchematicPrimitiveElementDiagnostics(
539
+ element,
540
+ schematicSymbolIds,
541
+ schematicComponentIds
542
+ )
543
+ )
544
+ )
545
+ }
546
+
547
+ /**
548
+ * Builds schematic primitive diagnostics for one element.
549
+ * @param {object} element Schematic primitive row.
550
+ * @param {Set<string>} schematicSymbolIds Known schematic symbol ids.
551
+ * @param {Set<string>} schematicComponentIds Known schematic component ids.
552
+ * @returns {object[]}
553
+ */
554
+ static #missingSchematicPrimitiveElementDiagnostics(
555
+ element,
556
+ schematicSymbolIds,
557
+ schematicComponentIds
558
+ ) {
559
+ const primitiveId = CircuitJsonIndexer.getElementId(element)
560
+ const diagnostics = []
561
+ const schematicSymbolId = String(
562
+ element.schematic_symbol_id || ''
563
+ ).trim()
564
+ const schematicComponentId = String(
565
+ element.schematic_component_id || ''
566
+ ).trim()
567
+
568
+ if (schematicSymbolId && !schematicSymbolIds.has(schematicSymbolId)) {
569
+ diagnostics.push({
570
+ isGenerated: true,
571
+ severity: 'warning',
572
+ sourceFormat: 'circuitjson',
573
+ type: 'schematic_primitive_missing_schematic_symbol_warning',
574
+ category: 'layout',
575
+ message:
576
+ 'Schematic primitive ' +
577
+ primitiveId +
578
+ ' references missing schematic symbol ' +
579
+ schematicSymbolId +
580
+ '.',
581
+ elementId: primitiveId + ':missing-symbol:' + schematicSymbolId,
582
+ schematicSymbolId
583
+ })
584
+ }
585
+ if (
586
+ schematicComponentId &&
587
+ !schematicComponentIds.has(schematicComponentId)
588
+ ) {
589
+ diagnostics.push({
590
+ isGenerated: true,
591
+ severity: 'warning',
592
+ sourceFormat: 'circuitjson',
593
+ type: 'schematic_primitive_missing_schematic_component_warning',
594
+ category: 'layout',
595
+ message:
596
+ 'Schematic primitive ' +
597
+ primitiveId +
598
+ ' references missing schematic component ' +
599
+ schematicComponentId +
600
+ '.',
601
+ elementId:
602
+ primitiveId + ':missing-component:' + schematicComponentId,
603
+ schematicComponentId
604
+ })
605
+ }
606
+
607
+ return diagnostics
608
+ }
609
+
172
610
  /**
173
611
  * Builds group rows keyed by group id.
174
612
  * @param {object[]} elements Element rows.
@@ -226,6 +664,20 @@ export class CircuitJsonIndexer {
226
664
  return bySubcircuitId
227
665
  }
228
666
 
667
+ /**
668
+ * Builds a set of known element ids for one type.
669
+ * @param {Map<string, object[]>} elementsByType Element rows by type.
670
+ * @param {string} type Element type.
671
+ * @returns {Set<string>}
672
+ */
673
+ static #elementIds(elementsByType, type) {
674
+ return new Set(
675
+ CircuitJsonIndexer.#all(elementsByType, type)
676
+ .map((element) => CircuitJsonIndexer.getElementId(element))
677
+ .filter(Boolean)
678
+ )
679
+ }
680
+
229
681
  /**
230
682
  * Resolves group ids from common group fields.
231
683
  * @param {object} element Element row.
@@ -237,12 +689,36 @@ export class CircuitJsonIndexer {
237
689
  element?.pcb_group_id,
238
690
  element?.schematic_group_id,
239
691
  element?.group_id,
240
- ...(Array.isArray(element?.group_ids) ? element.group_ids : [])
692
+ element?.member_source_group_id,
693
+ element?.member_pcb_group_id,
694
+ element?.member_schematic_group_id,
695
+ element?.member_group_id,
696
+ ...CircuitJsonIndexer.#relationValues(element?.group_ids),
697
+ ...CircuitJsonIndexer.#relationValues(
698
+ element?.member_source_group_ids
699
+ ),
700
+ ...CircuitJsonIndexer.#relationValues(
701
+ element?.member_pcb_group_ids
702
+ ),
703
+ ...CircuitJsonIndexer.#relationValues(
704
+ element?.member_schematic_group_ids
705
+ ),
706
+ ...CircuitJsonIndexer.#relationValues(element?.member_group_ids)
241
707
  ]
242
708
  .map((value) => String(value || '').trim())
243
709
  .filter(Boolean)
244
710
  }
245
711
 
712
+ /**
713
+ * Returns indexed element rows by type.
714
+ * @param {Map<string, object[]>} elementsByType Element rows by type.
715
+ * @param {string} type Element type.
716
+ * @returns {object[]}
717
+ */
718
+ static #all(elementsByType, type) {
719
+ return elementsByType.get(type) || []
720
+ }
721
+
246
722
  /**
247
723
  * Returns true when an element is a warning or error row.
248
724
  * @param {object} element Element.
@@ -272,10 +748,39 @@ export class CircuitJsonIndexer {
272
748
  type,
273
749
  category: CircuitJsonIndexer.#diagnosticCategory(type),
274
750
  message: String(element?.message || type || 'CircuitJSON issue'),
275
- elementId: CircuitJsonIndexer.getElementId(element)
751
+ elementId: CircuitJsonIndexer.getElementId(element),
752
+ ...CircuitJsonIndexer.#diagnosticRelations(element)
276
753
  }
277
754
  }
278
755
 
756
+ /**
757
+ * Extracts optional relation ids from one diagnostic element.
758
+ * @param {object} element Diagnostic element.
759
+ * @returns {object}
760
+ */
761
+ static #diagnosticRelations(element) {
762
+ return Object.fromEntries(
763
+ [
764
+ ['sourceComponentId', element?.source_component_id],
765
+ ['sourcePortId', element?.source_port_id],
766
+ ['sourceNetId', element?.source_net_id],
767
+ ['sourceTraceId', element?.source_trace_id],
768
+ ['pcbComponentId', element?.pcb_component_id],
769
+ ['pcbPortId', element?.pcb_port_id],
770
+ ['pcbTraceId', element?.pcb_trace_id],
771
+ ['pcbSmtpadId', element?.pcb_smtpad_id],
772
+ ['pcbViaId', element?.pcb_via_id],
773
+ ['pcbPlatedHoleId', element?.pcb_plated_hole_id],
774
+ ['pcbHoleId', element?.pcb_hole_id],
775
+ ['schematicComponentId', element?.schematic_component_id],
776
+ ['schematicSymbolId', element?.schematic_symbol_id],
777
+ ['schematicPortId', element?.schematic_port_id]
778
+ ]
779
+ .map(([key, value]) => [key, String(value || '').trim()])
780
+ .filter(([_key, value]) => value)
781
+ )
782
+ }
783
+
279
784
  /**
280
785
  * Resolves a broad diagnostic category.
281
786
  * @param {string} type Diagnostic type or code.