circuitjson-toolkit 1.0.3 → 1.0.16

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 (54) hide show
  1. package/AGENTS.md +5 -3
  2. package/README.md +21 -2
  3. package/docs/api.md +50 -4
  4. package/docs/model-format.md +21 -3
  5. package/package.json +3 -2
  6. package/spec/library-scope.md +4 -1
  7. package/src/core/CircuitJsonBomBuilder.mjs +143 -0
  8. package/src/core/CircuitJsonDocument.mjs +46 -13
  9. package/src/core/CircuitJsonElementValidator.mjs +990 -0
  10. package/src/core/CircuitJsonIndexer.mjs +773 -4
  11. package/src/core/CircuitJsonManufacturingBuilder.mjs +898 -0
  12. package/src/core/CircuitJsonManufacturingDownloadBuilder.mjs +196 -0
  13. package/src/core/CircuitJsonParser.mjs +22 -6
  14. package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +329 -0
  15. package/src/core/CircuitJsonPcbCopperGeometry.mjs +503 -0
  16. package/src/core/CircuitJsonPcbDrawingStyle.mjs +88 -0
  17. package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +172 -0
  18. package/src/core/CircuitJsonPcbNetMetadata.mjs +247 -0
  19. package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +70 -0
  20. package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +992 -0
  21. package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +872 -0
  22. package/src/core/CircuitJsonPcbPrimitiveFields.mjs +233 -0
  23. package/src/core/CircuitJsonPcbPrimitiveGeometry.mjs +142 -0
  24. package/src/core/CircuitJsonPcbPrimitiveGroups.mjs +305 -0
  25. package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +65 -0
  26. package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +895 -0
  27. package/src/core/CircuitJsonPcbTraceLengthModel.mjs +257 -0
  28. package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +683 -0
  29. package/src/core/CircuitJsonSourceMetadata.mjs +233 -0
  30. package/src/core/CircuitJsonSupportMatrixBuilder.mjs +481 -0
  31. package/src/core/CircuitJsonUnits.mjs +133 -8
  32. package/src/core/PcbBoundsSelectionModel.mjs +250 -0
  33. package/src/core/PcbCandidateSelectionModel.mjs +77 -0
  34. package/src/core/PcbDiagnosticFocusModel.mjs +423 -0
  35. package/src/core/PcbInteractionPrimitiveModel.mjs +560 -0
  36. package/src/core/SelectedPartCircuitJsonExportAdapter.mjs +335 -0
  37. package/src/core/spice/SpiceCompatibilityPreprocessor.mjs +139 -0
  38. package/src/core/spice/SpiceDirectiveParser.mjs +231 -0
  39. package/src/core/spice/SpiceFallbackSimulationEngine.mjs +168 -0
  40. package/src/core/spice/SpiceSimulationDiagnostics.mjs +234 -0
  41. package/src/core/spice/SpiceSimulationGraphBuilder.mjs +421 -0
  42. package/src/core/spice/SpiceSimulationGraphSummary.mjs +90 -0
  43. package/src/core/spice/SpiceSimulationService.mjs +92 -0
  44. package/src/core/spice/SpiceTimeSeriesNormalizer.mjs +132 -0
  45. package/src/index.mjs +8 -0
  46. package/src/renderers.mjs +29 -0
  47. package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +128 -0
  48. package/src/ui/CircuitJsonPcbSvgRenderer.mjs +964 -0
  49. package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +168 -0
  50. package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +138 -0
  51. package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +114 -0
  52. package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +130 -0
  53. package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +994 -0
  54. package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +439 -0
@@ -3,18 +3,37 @@ import { CircuitJsonDocument } from './CircuitJsonDocument.mjs'
3
3
  const ID_FIELDS_BY_TYPE = {
4
4
  pcb_board: 'pcb_board_id',
5
5
  pcb_component: 'pcb_component_id',
6
+ pcb_group: 'pcb_group_id',
6
7
  pcb_hole: 'pcb_hole_id',
7
8
  pcb_plated_hole: 'pcb_plated_hole_id',
8
9
  pcb_port: 'pcb_port_id',
9
10
  pcb_smtpad: 'pcb_smtpad_id',
10
11
  pcb_trace: 'pcb_trace_id',
11
12
  pcb_via: 'pcb_via_id',
13
+ schematic_group: 'schematic_group_id',
12
14
  source_component: 'source_component_id',
15
+ source_group: 'source_group_id',
13
16
  source_net: 'source_net_id',
14
17
  source_port: 'source_port_id',
15
18
  source_trace: 'source_trace_id'
16
19
  }
17
20
 
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
+ ])
36
+
18
37
  /**
19
38
  * Builds lookup maps for CircuitJSON element arrays.
20
39
  */
@@ -22,14 +41,16 @@ export class CircuitJsonIndexer {
22
41
  /**
23
42
  * Indexes a CircuitJSON model.
24
43
  * @param {object[]} circuitJson CircuitJSON model.
25
- * @returns {{ elements: object[], elementsByType: Map<string, object[]>, elementsById: Map<string, object>, sourceComponentById: Map<string, object>, pcbComponentById: Map<string, 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[] }}
26
45
  */
27
46
  static index(circuitJson) {
28
47
  CircuitJsonDocument.assertModel(circuitJson)
29
48
  const elementsByType = new Map()
30
49
  const elementsById = new Map()
50
+ const relationsByField = new Map()
31
51
  const sourceComponentById = new Map()
32
52
  const pcbComponentById = new Map()
53
+ const sourceTraceById = new Map()
33
54
 
34
55
  circuitJson.forEach((element) => {
35
56
  const type = String(element?.type || '')
@@ -38,8 +59,7 @@ export class CircuitJsonIndexer {
38
59
  }
39
60
  elementsByType.get(type).push(element)
40
61
 
41
- const idField = ID_FIELDS_BY_TYPE[type]
42
- const id = idField ? String(element?.[idField] || '') : ''
62
+ const id = CircuitJsonIndexer.getElementId(element)
43
63
  if (id) {
44
64
  elementsById.set(`${type}:${id}`, element)
45
65
  }
@@ -49,14 +69,763 @@ export class CircuitJsonIndexer {
49
69
  if (type === 'pcb_component' && id) {
50
70
  pcbComponentById.set(id, element)
51
71
  }
72
+ if (type === 'source_trace' && id) {
73
+ sourceTraceById.set(id, element)
74
+ }
75
+ CircuitJsonIndexer.#indexRelations(element, relationsByField)
52
76
  })
77
+ const sourceTraceConnectivity =
78
+ CircuitJsonIndexer.#sourceTraceConnectivity(sourceTraceById)
53
79
 
54
80
  return {
55
81
  elements: circuitJson,
56
82
  elementsByType,
57
83
  elementsById,
84
+ relationsByField,
58
85
  sourceComponentById,
59
- pcbComponentById
86
+ pcbComponentById,
87
+ sourceTraceById,
88
+ sourceTraceConnectivity,
89
+ componentsBySourceId: CircuitJsonIndexer.#componentsBySourceId(
90
+ sourceComponentById,
91
+ relationsByField
92
+ ),
93
+ groupsById: CircuitJsonIndexer.#groupsById(circuitJson),
94
+ elementsByGroupId:
95
+ CircuitJsonIndexer.#elementsByGroupId(circuitJson),
96
+ elementsBySubcircuitId:
97
+ CircuitJsonIndexer.#elementsBySubcircuitId(circuitJson),
98
+ diagnostics: [
99
+ ...CircuitJsonIndexer.collectDiagnostics(circuitJson),
100
+ ...CircuitJsonIndexer.#referenceDiagnostics(
101
+ elementsByType,
102
+ sourceTraceById,
103
+ sourceTraceConnectivity
104
+ )
105
+ ]
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Resolves the primary id for one element.
111
+ * @param {object} element Element.
112
+ * @returns {string}
113
+ */
114
+ static getElementId(element) {
115
+ const type = String(element?.type || '')
116
+ const idField = ID_FIELDS_BY_TYPE[type] || type + '_id'
117
+ return String(element?.[idField] || '').trim()
118
+ }
119
+
120
+ /**
121
+ * Collects normalized diagnostic rows from warning and error elements.
122
+ * @param {object[]} circuitJson CircuitJSON model.
123
+ * @returns {object[]}
124
+ */
125
+ static collectDiagnostics(circuitJson) {
126
+ CircuitJsonDocument.assertModel(circuitJson)
127
+ return circuitJson
128
+ .filter((element) => CircuitJsonIndexer.#isDiagnostic(element))
129
+ .map((element) => CircuitJsonIndexer.#diagnostic(element))
130
+ }
131
+
132
+ /**
133
+ * Adds relation fields from one element into lookup maps.
134
+ * @param {object} element Element.
135
+ * @param {Map<string, Map<string, object[]>>} relationsByField Relation map.
136
+ * @returns {void}
137
+ */
138
+ static #indexRelations(element, relationsByField) {
139
+ const primaryIdField =
140
+ ID_FIELDS_BY_TYPE[String(element?.type || '')] ||
141
+ String(element?.type || '') + '_id'
142
+ for (const [field, value] of Object.entries(element || {})) {
143
+ if (!field.endsWith('_id') && !field.endsWith('_ids')) continue
144
+ if (field === primaryIdField) continue
145
+
146
+ if (!relationsByField.has(field)) {
147
+ relationsByField.set(field, new Map())
148
+ }
149
+ const byValue = relationsByField.get(field)
150
+ for (const relationValue of CircuitJsonIndexer.#relationValues(
151
+ value
152
+ )) {
153
+ if (!byValue.has(relationValue)) {
154
+ byValue.set(relationValue, [])
155
+ }
156
+ byValue.get(relationValue).push(element)
157
+ }
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Returns normalized relation values from scalar or array fields.
163
+ * @param {unknown} value Relation value.
164
+ * @returns {string[]}
165
+ */
166
+ static #relationValues(value) {
167
+ const values = Array.isArray(value)
168
+ ? value.flatMap((entry) => (Array.isArray(entry) ? entry : [entry]))
169
+ : [value]
170
+ return values.map((entry) => String(entry || '').trim()).filter(Boolean)
171
+ }
172
+
173
+ /**
174
+ * Builds component bundles keyed by source component id.
175
+ * @param {Map<string, object>} sourceComponentById Source component lookup.
176
+ * @param {Map<string, Map<string, object[]>>} relationsByField Relation map.
177
+ * @returns {Map<string, object>}
178
+ */
179
+ static #componentsBySourceId(sourceComponentById, relationsByField) {
180
+ const bySource = new Map()
181
+ const sourceRelations =
182
+ relationsByField.get('source_component_id') || new Map()
183
+
184
+ for (const [sourceId, sourceComponent] of sourceComponentById) {
185
+ const linked = sourceRelations.get(sourceId) || []
186
+ bySource.set(sourceId, {
187
+ sourceComponent,
188
+ sourcePorts: linked.filter(
189
+ (element) => element?.type === 'source_port'
190
+ ),
191
+ pcbComponents: linked.filter(
192
+ (element) => element?.type === 'pcb_component'
193
+ ),
194
+ schematicComponents: linked.filter(
195
+ (element) => element?.type === 'schematic_component'
196
+ )
197
+ })
198
+ }
199
+
200
+ return bySource
201
+ }
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
+
610
+ /**
611
+ * Builds group rows keyed by group id.
612
+ * @param {object[]} elements Element rows.
613
+ * @returns {Map<string, object>}
614
+ */
615
+ static #groupsById(elements) {
616
+ const elementsByGroupId =
617
+ CircuitJsonIndexer.#elementsByGroupId(elements)
618
+ const groups = new Map()
619
+ for (const element of elements) {
620
+ if (!GROUP_TYPES.has(String(element?.type || ''))) continue
621
+ const id = CircuitJsonIndexer.getElementId(element)
622
+ if (!id) continue
623
+ groups.set(id, {
624
+ id,
625
+ type: String(element.type || ''),
626
+ name: String(element.name || id),
627
+ group: element,
628
+ members: elementsByGroupId.get(id) || []
629
+ })
630
+ }
631
+ return groups
632
+ }
633
+
634
+ /**
635
+ * Builds element membership by group id.
636
+ * @param {object[]} elements Element rows.
637
+ * @returns {Map<string, object[]>}
638
+ */
639
+ static #elementsByGroupId(elements) {
640
+ const byGroupId = new Map()
641
+ for (const element of elements) {
642
+ if (GROUP_TYPES.has(String(element?.type || ''))) continue
643
+ for (const groupId of CircuitJsonIndexer.#groupIds(element)) {
644
+ if (!byGroupId.has(groupId)) byGroupId.set(groupId, [])
645
+ byGroupId.get(groupId).push(element)
646
+ }
647
+ }
648
+ return byGroupId
649
+ }
650
+
651
+ /**
652
+ * Builds elements by subcircuit id.
653
+ * @param {object[]} elements Element rows.
654
+ * @returns {Map<string, object[]>}
655
+ */
656
+ static #elementsBySubcircuitId(elements) {
657
+ const bySubcircuitId = new Map()
658
+ for (const element of elements) {
659
+ const id = String(element?.subcircuit_id || '').trim()
660
+ if (!id) continue
661
+ if (!bySubcircuitId.has(id)) bySubcircuitId.set(id, [])
662
+ bySubcircuitId.get(id).push(element)
663
+ }
664
+ return bySubcircuitId
665
+ }
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
+
681
+ /**
682
+ * Resolves group ids from common group fields.
683
+ * @param {object} element Element row.
684
+ * @returns {string[]}
685
+ */
686
+ static #groupIds(element) {
687
+ return [
688
+ element?.source_group_id,
689
+ element?.pcb_group_id,
690
+ element?.schematic_group_id,
691
+ element?.group_id,
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)
707
+ ]
708
+ .map((value) => String(value || '').trim())
709
+ .filter(Boolean)
710
+ }
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
+
722
+ /**
723
+ * Returns true when an element is a warning or error row.
724
+ * @param {object} element Element.
725
+ * @returns {boolean}
726
+ */
727
+ static #isDiagnostic(element) {
728
+ const type = String(element?.type || '')
729
+ return (
730
+ type.endsWith('_error') ||
731
+ type.endsWith('_warning') ||
732
+ Boolean(element?.error_type || element?.warning_type)
733
+ )
734
+ }
735
+
736
+ /**
737
+ * Builds one normalized diagnostic.
738
+ * @param {object} element Diagnostic element.
739
+ * @returns {object}
740
+ */
741
+ static #diagnostic(element) {
742
+ const type = String(
743
+ element?.error_type || element?.warning_type || element?.type || ''
744
+ )
745
+ return {
746
+ severity: element?.warning_type ? 'warning' : 'error',
747
+ sourceFormat: 'circuitjson',
748
+ type,
749
+ category: CircuitJsonIndexer.#diagnosticCategory(type),
750
+ message: String(element?.message || type || 'CircuitJSON issue'),
751
+ elementId: CircuitJsonIndexer.getElementId(element),
752
+ ...CircuitJsonIndexer.#diagnosticRelations(element)
753
+ }
754
+ }
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
+
784
+ /**
785
+ * Resolves a broad diagnostic category.
786
+ * @param {string} type Diagnostic type or code.
787
+ * @returns {string}
788
+ */
789
+ static #diagnosticCategory(type) {
790
+ const text = String(type || '').toLowerCase()
791
+ if (text.includes('clearance')) return 'clearance'
792
+ if (text.includes('autorouting') || text.includes('trace_error')) {
793
+ return 'routing'
794
+ }
795
+ if (text.includes('placement') || text.includes('outside_board')) {
796
+ return 'placement'
797
+ }
798
+ if (
799
+ text.includes('trace_missing') ||
800
+ text.includes('not_connected') ||
801
+ text.includes('missing_trace') ||
802
+ text.includes('pin_missing_trace') ||
803
+ text.includes('pin_must_be_connected')
804
+ ) {
805
+ return 'connectivity'
806
+ }
807
+ if (text.includes('layout')) return 'layout'
808
+ if (text.includes('simulation')) return 'simulation'
809
+ if (text.includes('footprint')) return 'footprint'
810
+ if (
811
+ text.includes('pin_defined') ||
812
+ text.includes('pins_underspecified') ||
813
+ text.includes('ground_pin') ||
814
+ text.includes('power_pin')
815
+ ) {
816
+ return 'pin-definition'
817
+ }
818
+ if (
819
+ text.includes('manufacturer_part') ||
820
+ text.includes('missing_property') ||
821
+ text.includes('property_ignored')
822
+ ) {
823
+ return 'metadata'
824
+ }
825
+ if (text.includes('manual_edit_conflict')) return 'edit-conflict'
826
+ if (text.includes('property') || text.includes('misconfigured')) {
827
+ return 'configuration'
60
828
  }
829
+ return 'general'
61
830
  }
62
831
  }