circuitjson-toolkit 1.0.2 → 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.
@@ -3,18 +3,23 @@ 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
+
18
23
  /**
19
24
  * Builds lookup maps for CircuitJSON element arrays.
20
25
  */
@@ -22,12 +27,13 @@ export class CircuitJsonIndexer {
22
27
  /**
23
28
  * Indexes a CircuitJSON model.
24
29
  * @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> }}
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[] }}
26
31
  */
27
32
  static index(circuitJson) {
28
33
  CircuitJsonDocument.assertModel(circuitJson)
29
34
  const elementsByType = new Map()
30
35
  const elementsById = new Map()
36
+ const relationsByField = new Map()
31
37
  const sourceComponentById = new Map()
32
38
  const pcbComponentById = new Map()
33
39
 
@@ -38,8 +44,7 @@ export class CircuitJsonIndexer {
38
44
  }
39
45
  elementsByType.get(type).push(element)
40
46
 
41
- const idField = ID_FIELDS_BY_TYPE[type]
42
- const id = idField ? String(element?.[idField] || '') : ''
47
+ const id = CircuitJsonIndexer.getElementId(element)
43
48
  if (id) {
44
49
  elementsById.set(`${type}:${id}`, element)
45
50
  }
@@ -49,14 +54,273 @@ export class CircuitJsonIndexer {
49
54
  if (type === 'pcb_component' && id) {
50
55
  pcbComponentById.set(id, element)
51
56
  }
57
+ CircuitJsonIndexer.#indexRelations(element, relationsByField)
52
58
  })
53
59
 
54
60
  return {
55
61
  elements: circuitJson,
56
62
  elementsByType,
57
63
  elementsById,
64
+ relationsByField,
58
65
  sourceComponentById,
59
- pcbComponentById
66
+ pcbComponentById,
67
+ componentsBySourceId: CircuitJsonIndexer.#componentsBySourceId(
68
+ sourceComponentById,
69
+ relationsByField
70
+ ),
71
+ groupsById: CircuitJsonIndexer.#groupsById(circuitJson),
72
+ elementsByGroupId:
73
+ CircuitJsonIndexer.#elementsByGroupId(circuitJson),
74
+ elementsBySubcircuitId:
75
+ CircuitJsonIndexer.#elementsBySubcircuitId(circuitJson),
76
+ diagnostics: CircuitJsonIndexer.collectDiagnostics(circuitJson)
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Resolves the primary id for one element.
82
+ * @param {object} element Element.
83
+ * @returns {string}
84
+ */
85
+ static getElementId(element) {
86
+ const type = String(element?.type || '')
87
+ const idField = ID_FIELDS_BY_TYPE[type] || type + '_id'
88
+ return String(element?.[idField] || '').trim()
89
+ }
90
+
91
+ /**
92
+ * Collects normalized diagnostic rows from warning and error elements.
93
+ * @param {object[]} circuitJson CircuitJSON model.
94
+ * @returns {object[]}
95
+ */
96
+ static collectDiagnostics(circuitJson) {
97
+ CircuitJsonDocument.assertModel(circuitJson)
98
+ return circuitJson
99
+ .filter((element) => CircuitJsonIndexer.#isDiagnostic(element))
100
+ .map((element) => CircuitJsonIndexer.#diagnostic(element))
101
+ }
102
+
103
+ /**
104
+ * Adds relation fields from one element into lookup maps.
105
+ * @param {object} element Element.
106
+ * @param {Map<string, Map<string, object[]>>} relationsByField Relation map.
107
+ * @returns {void}
108
+ */
109
+ static #indexRelations(element, relationsByField) {
110
+ const primaryIdField =
111
+ ID_FIELDS_BY_TYPE[String(element?.type || '')] ||
112
+ String(element?.type || '') + '_id'
113
+ for (const [field, value] of Object.entries(element || {})) {
114
+ if (!field.endsWith('_id') && !field.endsWith('_ids')) continue
115
+ if (field === primaryIdField) continue
116
+
117
+ if (!relationsByField.has(field)) {
118
+ relationsByField.set(field, new Map())
119
+ }
120
+ const byValue = relationsByField.get(field)
121
+ for (const relationValue of CircuitJsonIndexer.#relationValues(
122
+ value
123
+ )) {
124
+ if (!byValue.has(relationValue)) {
125
+ byValue.set(relationValue, [])
126
+ }
127
+ byValue.get(relationValue).push(element)
128
+ }
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Returns normalized relation values from scalar or array fields.
134
+ * @param {unknown} value Relation value.
135
+ * @returns {string[]}
136
+ */
137
+ static #relationValues(value) {
138
+ const values = Array.isArray(value) ? value : [value]
139
+ return values.map((entry) => String(entry || '').trim()).filter(Boolean)
140
+ }
141
+
142
+ /**
143
+ * Builds component bundles keyed by source component id.
144
+ * @param {Map<string, object>} sourceComponentById Source component lookup.
145
+ * @param {Map<string, Map<string, object[]>>} relationsByField Relation map.
146
+ * @returns {Map<string, object>}
147
+ */
148
+ static #componentsBySourceId(sourceComponentById, relationsByField) {
149
+ const bySource = new Map()
150
+ const sourceRelations =
151
+ relationsByField.get('source_component_id') || new Map()
152
+
153
+ for (const [sourceId, sourceComponent] of sourceComponentById) {
154
+ const linked = sourceRelations.get(sourceId) || []
155
+ bySource.set(sourceId, {
156
+ sourceComponent,
157
+ sourcePorts: linked.filter(
158
+ (element) => element?.type === 'source_port'
159
+ ),
160
+ pcbComponents: linked.filter(
161
+ (element) => element?.type === 'pcb_component'
162
+ ),
163
+ schematicComponents: linked.filter(
164
+ (element) => element?.type === 'schematic_component'
165
+ )
166
+ })
167
+ }
168
+
169
+ return bySource
170
+ }
171
+
172
+ /**
173
+ * Builds group rows keyed by group id.
174
+ * @param {object[]} elements Element rows.
175
+ * @returns {Map<string, object>}
176
+ */
177
+ static #groupsById(elements) {
178
+ const elementsByGroupId =
179
+ CircuitJsonIndexer.#elementsByGroupId(elements)
180
+ const groups = new Map()
181
+ for (const element of elements) {
182
+ if (!GROUP_TYPES.has(String(element?.type || ''))) continue
183
+ const id = CircuitJsonIndexer.getElementId(element)
184
+ if (!id) continue
185
+ groups.set(id, {
186
+ id,
187
+ type: String(element.type || ''),
188
+ name: String(element.name || id),
189
+ group: element,
190
+ members: elementsByGroupId.get(id) || []
191
+ })
192
+ }
193
+ return groups
194
+ }
195
+
196
+ /**
197
+ * Builds element membership by group id.
198
+ * @param {object[]} elements Element rows.
199
+ * @returns {Map<string, object[]>}
200
+ */
201
+ static #elementsByGroupId(elements) {
202
+ const byGroupId = new Map()
203
+ for (const element of elements) {
204
+ if (GROUP_TYPES.has(String(element?.type || ''))) continue
205
+ for (const groupId of CircuitJsonIndexer.#groupIds(element)) {
206
+ if (!byGroupId.has(groupId)) byGroupId.set(groupId, [])
207
+ byGroupId.get(groupId).push(element)
208
+ }
209
+ }
210
+ return byGroupId
211
+ }
212
+
213
+ /**
214
+ * Builds elements by subcircuit id.
215
+ * @param {object[]} elements Element rows.
216
+ * @returns {Map<string, object[]>}
217
+ */
218
+ static #elementsBySubcircuitId(elements) {
219
+ const bySubcircuitId = new Map()
220
+ for (const element of elements) {
221
+ const id = String(element?.subcircuit_id || '').trim()
222
+ if (!id) continue
223
+ if (!bySubcircuitId.has(id)) bySubcircuitId.set(id, [])
224
+ bySubcircuitId.get(id).push(element)
225
+ }
226
+ return bySubcircuitId
227
+ }
228
+
229
+ /**
230
+ * Resolves group ids from common group fields.
231
+ * @param {object} element Element row.
232
+ * @returns {string[]}
233
+ */
234
+ static #groupIds(element) {
235
+ return [
236
+ element?.source_group_id,
237
+ element?.pcb_group_id,
238
+ element?.schematic_group_id,
239
+ element?.group_id,
240
+ ...(Array.isArray(element?.group_ids) ? element.group_ids : [])
241
+ ]
242
+ .map((value) => String(value || '').trim())
243
+ .filter(Boolean)
244
+ }
245
+
246
+ /**
247
+ * Returns true when an element is a warning or error row.
248
+ * @param {object} element Element.
249
+ * @returns {boolean}
250
+ */
251
+ static #isDiagnostic(element) {
252
+ const type = String(element?.type || '')
253
+ return (
254
+ type.endsWith('_error') ||
255
+ type.endsWith('_warning') ||
256
+ Boolean(element?.error_type || element?.warning_type)
257
+ )
258
+ }
259
+
260
+ /**
261
+ * Builds one normalized diagnostic.
262
+ * @param {object} element Diagnostic element.
263
+ * @returns {object}
264
+ */
265
+ static #diagnostic(element) {
266
+ const type = String(
267
+ element?.error_type || element?.warning_type || element?.type || ''
268
+ )
269
+ return {
270
+ severity: element?.warning_type ? 'warning' : 'error',
271
+ sourceFormat: 'circuitjson',
272
+ type,
273
+ category: CircuitJsonIndexer.#diagnosticCategory(type),
274
+ message: String(element?.message || type || 'CircuitJSON issue'),
275
+ elementId: CircuitJsonIndexer.getElementId(element)
276
+ }
277
+ }
278
+
279
+ /**
280
+ * Resolves a broad diagnostic category.
281
+ * @param {string} type Diagnostic type or code.
282
+ * @returns {string}
283
+ */
284
+ static #diagnosticCategory(type) {
285
+ const text = String(type || '').toLowerCase()
286
+ if (text.includes('clearance')) return 'clearance'
287
+ if (text.includes('autorouting') || text.includes('trace_error')) {
288
+ return 'routing'
289
+ }
290
+ if (text.includes('placement') || text.includes('outside_board')) {
291
+ return 'placement'
292
+ }
293
+ if (
294
+ text.includes('trace_missing') ||
295
+ text.includes('not_connected') ||
296
+ text.includes('missing_trace') ||
297
+ text.includes('pin_missing_trace') ||
298
+ text.includes('pin_must_be_connected')
299
+ ) {
300
+ return 'connectivity'
301
+ }
302
+ if (text.includes('layout')) return 'layout'
303
+ if (text.includes('simulation')) return 'simulation'
304
+ if (text.includes('footprint')) return 'footprint'
305
+ if (
306
+ text.includes('pin_defined') ||
307
+ text.includes('pins_underspecified') ||
308
+ text.includes('ground_pin') ||
309
+ text.includes('power_pin')
310
+ ) {
311
+ return 'pin-definition'
312
+ }
313
+ if (
314
+ text.includes('manufacturer_part') ||
315
+ text.includes('missing_property') ||
316
+ text.includes('property_ignored')
317
+ ) {
318
+ return 'metadata'
319
+ }
320
+ if (text.includes('manual_edit_conflict')) return 'edit-conflict'
321
+ if (text.includes('property') || text.includes('misconfigured')) {
322
+ return 'configuration'
60
323
  }
324
+ return 'general'
61
325
  }
62
326
  }