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.
- package/AGENTS.md +5 -3
- package/README.md +21 -2
- package/docs/api.md +50 -4
- package/docs/model-format.md +21 -3
- package/package.json +3 -2
- package/spec/library-scope.md +4 -1
- package/src/core/CircuitJsonBomBuilder.mjs +143 -0
- package/src/core/CircuitJsonDocument.mjs +46 -13
- package/src/core/CircuitJsonElementValidator.mjs +990 -0
- package/src/core/CircuitJsonIndexer.mjs +773 -4
- package/src/core/CircuitJsonManufacturingBuilder.mjs +898 -0
- package/src/core/CircuitJsonManufacturingDownloadBuilder.mjs +196 -0
- package/src/core/CircuitJsonParser.mjs +22 -6
- package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +329 -0
- package/src/core/CircuitJsonPcbCopperGeometry.mjs +503 -0
- package/src/core/CircuitJsonPcbDrawingStyle.mjs +88 -0
- package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +172 -0
- package/src/core/CircuitJsonPcbNetMetadata.mjs +247 -0
- package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +70 -0
- package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +992 -0
- package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +872 -0
- package/src/core/CircuitJsonPcbPrimitiveFields.mjs +233 -0
- package/src/core/CircuitJsonPcbPrimitiveGeometry.mjs +142 -0
- package/src/core/CircuitJsonPcbPrimitiveGroups.mjs +305 -0
- package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +65 -0
- package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +895 -0
- package/src/core/CircuitJsonPcbTraceLengthModel.mjs +257 -0
- package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +683 -0
- package/src/core/CircuitJsonSourceMetadata.mjs +233 -0
- package/src/core/CircuitJsonSupportMatrixBuilder.mjs +481 -0
- package/src/core/CircuitJsonUnits.mjs +133 -8
- package/src/core/PcbBoundsSelectionModel.mjs +250 -0
- package/src/core/PcbCandidateSelectionModel.mjs +77 -0
- package/src/core/PcbDiagnosticFocusModel.mjs +423 -0
- package/src/core/PcbInteractionPrimitiveModel.mjs +560 -0
- package/src/core/SelectedPartCircuitJsonExportAdapter.mjs +335 -0
- package/src/core/spice/SpiceCompatibilityPreprocessor.mjs +139 -0
- package/src/core/spice/SpiceDirectiveParser.mjs +231 -0
- package/src/core/spice/SpiceFallbackSimulationEngine.mjs +168 -0
- package/src/core/spice/SpiceSimulationDiagnostics.mjs +234 -0
- package/src/core/spice/SpiceSimulationGraphBuilder.mjs +421 -0
- package/src/core/spice/SpiceSimulationGraphSummary.mjs +90 -0
- package/src/core/spice/SpiceSimulationService.mjs +92 -0
- package/src/core/spice/SpiceTimeSeriesNormalizer.mjs +132 -0
- package/src/index.mjs +8 -0
- package/src/renderers.mjs +29 -0
- package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +128 -0
- package/src/ui/CircuitJsonPcbSvgRenderer.mjs +964 -0
- package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +168 -0
- package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +138 -0
- package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +114 -0
- package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +130 -0
- package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +994 -0
- 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
|
|
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
|
}
|