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.
- package/README.md +21 -2
- package/docs/api.md +50 -4
- package/docs/model-format.md +6 -3
- package/package.json +1 -1
- package/spec/library-scope.md +4 -1
- package/src/core/CircuitJsonBomBuilder.mjs +146 -0
- package/src/core/CircuitJsonDocument.mjs +46 -13
- package/src/core/CircuitJsonElementValidator.mjs +773 -0
- package/src/core/CircuitJsonIndexer.mjs +268 -4
- package/src/core/CircuitJsonManufacturingBuilder.mjs +426 -0
- package/src/core/CircuitJsonParser.mjs +22 -6
- package/src/core/CircuitJsonSupportMatrixBuilder.mjs +259 -0
- package/src/core/CircuitJsonUnits.mjs +133 -8
- 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 +6 -0
|
@@ -0,0 +1,773 @@
|
|
|
1
|
+
import { CircuitJsonUnits } from './CircuitJsonUnits.mjs'
|
|
2
|
+
|
|
3
|
+
const KNOWN_ELEMENT_TYPES = new Set([
|
|
4
|
+
'cad_component',
|
|
5
|
+
'circuit_json_footprint_load_error',
|
|
6
|
+
'external_footprint_load_error',
|
|
7
|
+
'pcb_autorouting_error',
|
|
8
|
+
'pcb_board',
|
|
9
|
+
'pcb_breakout_point',
|
|
10
|
+
'pcb_component',
|
|
11
|
+
'pcb_component_invalid_layer_error',
|
|
12
|
+
'pcb_component_not_on_board_edge_error',
|
|
13
|
+
'pcb_component_outside_board_error',
|
|
14
|
+
'pcb_connector_not_in_accessible_orientation_warning',
|
|
15
|
+
'pcb_copper_pour',
|
|
16
|
+
'pcb_copper_text',
|
|
17
|
+
'pcb_courtyard',
|
|
18
|
+
'pcb_courtyard_circle',
|
|
19
|
+
'pcb_courtyard_outline',
|
|
20
|
+
'pcb_courtyard_overlap_error',
|
|
21
|
+
'pcb_courtyard_pill',
|
|
22
|
+
'pcb_courtyard_polygon',
|
|
23
|
+
'pcb_courtyard_rect',
|
|
24
|
+
'pcb_cutout',
|
|
25
|
+
'pcb_fabrication_note_dimension',
|
|
26
|
+
'pcb_fabrication_note_path',
|
|
27
|
+
'pcb_fabrication_note_rect',
|
|
28
|
+
'pcb_fabrication_note_text',
|
|
29
|
+
'pcb_footprint_overlap_error',
|
|
30
|
+
'pcb_ground_plane',
|
|
31
|
+
'pcb_ground_plane_region',
|
|
32
|
+
'pcb_group',
|
|
33
|
+
'pcb_hole',
|
|
34
|
+
'pcb_keepout',
|
|
35
|
+
'pcb_manual_edit_conflict_warning',
|
|
36
|
+
'pcb_missing_footprint_error',
|
|
37
|
+
'pcb_net',
|
|
38
|
+
'pcb_note_dimension',
|
|
39
|
+
'pcb_note_line',
|
|
40
|
+
'pcb_note_path',
|
|
41
|
+
'pcb_note_rect',
|
|
42
|
+
'pcb_note_text',
|
|
43
|
+
'pcb_pad_pad_clearance_error',
|
|
44
|
+
'pcb_pad_trace_clearance_error',
|
|
45
|
+
'pcb_panel',
|
|
46
|
+
'pcb_panelization_placement_error',
|
|
47
|
+
'pcb_placement_error',
|
|
48
|
+
'pcb_plated_hole',
|
|
49
|
+
'pcb_port',
|
|
50
|
+
'pcb_port_not_connected_error',
|
|
51
|
+
'pcb_port_not_matched_error',
|
|
52
|
+
'pcb_silkscreen_circle',
|
|
53
|
+
'pcb_silkscreen_graphic',
|
|
54
|
+
'pcb_silkscreen_line',
|
|
55
|
+
'pcb_silkscreen_oval',
|
|
56
|
+
'pcb_silkscreen_path',
|
|
57
|
+
'pcb_silkscreen_pill',
|
|
58
|
+
'pcb_silkscreen_rect',
|
|
59
|
+
'pcb_silkscreen_text',
|
|
60
|
+
'pcb_smtpad',
|
|
61
|
+
'pcb_solder_paste',
|
|
62
|
+
'pcb_text',
|
|
63
|
+
'pcb_thermal_spoke',
|
|
64
|
+
'pcb_trace',
|
|
65
|
+
'pcb_trace_error',
|
|
66
|
+
'pcb_trace_hint',
|
|
67
|
+
'pcb_trace_missing_error',
|
|
68
|
+
'pcb_trace_warning',
|
|
69
|
+
'pcb_via',
|
|
70
|
+
'pcb_via_clearance_error',
|
|
71
|
+
'pcb_via_trace_clearance_error',
|
|
72
|
+
'schematic_arc',
|
|
73
|
+
'schematic_box',
|
|
74
|
+
'schematic_circle',
|
|
75
|
+
'schematic_component',
|
|
76
|
+
'schematic_debug_object',
|
|
77
|
+
'schematic_error',
|
|
78
|
+
'schematic_group',
|
|
79
|
+
'schematic_layout_error',
|
|
80
|
+
'schematic_line',
|
|
81
|
+
'schematic_manual_edit_conflict_warning',
|
|
82
|
+
'schematic_net_label',
|
|
83
|
+
'schematic_path',
|
|
84
|
+
'schematic_port',
|
|
85
|
+
'schematic_rect',
|
|
86
|
+
'schematic_sheet',
|
|
87
|
+
'schematic_symbol',
|
|
88
|
+
'schematic_table',
|
|
89
|
+
'schematic_table_cell',
|
|
90
|
+
'schematic_text',
|
|
91
|
+
'schematic_trace',
|
|
92
|
+
'schematic_voltage_probe',
|
|
93
|
+
'simulation_current_probe',
|
|
94
|
+
'simulation_current_source',
|
|
95
|
+
'simulation_experiment',
|
|
96
|
+
'simulation_op_amp',
|
|
97
|
+
'simulation_oscilloscope_trace',
|
|
98
|
+
'simulation_spice_subcircuit',
|
|
99
|
+
'simulation_switch',
|
|
100
|
+
'simulation_transient_current_graph',
|
|
101
|
+
'simulation_transient_voltage_graph',
|
|
102
|
+
'simulation_unknown_experiment_error',
|
|
103
|
+
'simulation_voltage_probe',
|
|
104
|
+
'simulation_voltage_source',
|
|
105
|
+
'source_ambiguous_port_reference',
|
|
106
|
+
'source_board',
|
|
107
|
+
'source_component',
|
|
108
|
+
'source_component_internal_connection',
|
|
109
|
+
'source_component_misconfigured_error',
|
|
110
|
+
'source_component_pins_underspecified_warning',
|
|
111
|
+
'source_failed_to_create_component_error',
|
|
112
|
+
'source_group',
|
|
113
|
+
'source_i2c_misconfigured_error',
|
|
114
|
+
'source_interconnect',
|
|
115
|
+
'source_invalid_component_property_error',
|
|
116
|
+
'source_manually_placed_via',
|
|
117
|
+
'source_missing_manufacturer_part_number_warning',
|
|
118
|
+
'source_missing_property_error',
|
|
119
|
+
'source_net',
|
|
120
|
+
'source_no_ground_pin_defined_warning',
|
|
121
|
+
'source_no_power_pin_defined_warning',
|
|
122
|
+
'source_pcb_ground_plane',
|
|
123
|
+
'source_pin_missing_trace_warning',
|
|
124
|
+
'source_pin_must_be_connected_error',
|
|
125
|
+
'source_port',
|
|
126
|
+
'source_project_metadata',
|
|
127
|
+
'source_property_ignored_warning',
|
|
128
|
+
'source_trace',
|
|
129
|
+
'source_trace_not_connected_error',
|
|
130
|
+
'supplier_footprint_mismatch_warning',
|
|
131
|
+
'unknown_error_finding_part'
|
|
132
|
+
])
|
|
133
|
+
|
|
134
|
+
const ID_FIELD_EXCEPTIONS = new Set([
|
|
135
|
+
'pcb_autorouting_error',
|
|
136
|
+
'pcb_courtyard_overlap_error',
|
|
137
|
+
'pcb_footprint_overlap_error',
|
|
138
|
+
'pcb_port_not_matched_error',
|
|
139
|
+
'pcb_via_clearance_error',
|
|
140
|
+
'schematic_box',
|
|
141
|
+
'schematic_debug_object',
|
|
142
|
+
'source_project_metadata'
|
|
143
|
+
])
|
|
144
|
+
|
|
145
|
+
const SOURCE_COMPONENT_FTYPES = new Set([
|
|
146
|
+
'simple_ammeter',
|
|
147
|
+
'simple_battery',
|
|
148
|
+
'simple_capacitor',
|
|
149
|
+
'simple_chip',
|
|
150
|
+
'simple_connector',
|
|
151
|
+
'simple_crystal',
|
|
152
|
+
'simple_current_source',
|
|
153
|
+
'simple_diode',
|
|
154
|
+
'simple_fiducial',
|
|
155
|
+
'simple_fuse',
|
|
156
|
+
'simple_ground',
|
|
157
|
+
'simple_inductor',
|
|
158
|
+
'simple_led',
|
|
159
|
+
'simple_mosfet',
|
|
160
|
+
'simple_op_amp',
|
|
161
|
+
'simple_pin_header',
|
|
162
|
+
'simple_pinout',
|
|
163
|
+
'simple_potentiometer',
|
|
164
|
+
'simple_power_source',
|
|
165
|
+
'simple_push_button',
|
|
166
|
+
'simple_resistor',
|
|
167
|
+
'simple_resonator',
|
|
168
|
+
'simple_switch',
|
|
169
|
+
'simple_test_point',
|
|
170
|
+
'simple_transistor',
|
|
171
|
+
'simple_voltage_probe',
|
|
172
|
+
'simple_voltage_source'
|
|
173
|
+
])
|
|
174
|
+
|
|
175
|
+
const LAYERS = new Set([
|
|
176
|
+
'top',
|
|
177
|
+
'bottom',
|
|
178
|
+
'inner1',
|
|
179
|
+
'inner2',
|
|
180
|
+
'inner3',
|
|
181
|
+
'inner4',
|
|
182
|
+
'inner5',
|
|
183
|
+
'inner6'
|
|
184
|
+
])
|
|
185
|
+
|
|
186
|
+
const SMT_PAD_SHAPES = new Set([
|
|
187
|
+
'circle',
|
|
188
|
+
'rect',
|
|
189
|
+
'rotated_rect',
|
|
190
|
+
'rotated_pill',
|
|
191
|
+
'pill',
|
|
192
|
+
'polygon'
|
|
193
|
+
])
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Validates serialized CircuitJSON element objects without external runtime
|
|
197
|
+
* dependencies.
|
|
198
|
+
*/
|
|
199
|
+
export class CircuitJsonElementValidator {
|
|
200
|
+
/**
|
|
201
|
+
* Returns validation errors for a candidate model.
|
|
202
|
+
* @param {unknown} value Candidate model.
|
|
203
|
+
* @returns {string[]}
|
|
204
|
+
*/
|
|
205
|
+
static validateModel(value) {
|
|
206
|
+
if (!Array.isArray(value)) {
|
|
207
|
+
return ['Expected a CircuitJSON element array.']
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return value.flatMap((element, index) =>
|
|
211
|
+
CircuitJsonElementValidator.validateElement(element, index)
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Returns validation errors for one candidate element.
|
|
217
|
+
* @param {unknown} value Candidate element.
|
|
218
|
+
* @param {number} [index] Element index.
|
|
219
|
+
* @returns {string[]}
|
|
220
|
+
*/
|
|
221
|
+
static validateElement(value, index = -1) {
|
|
222
|
+
const location = index >= 0 ? ' at index ' + index : ''
|
|
223
|
+
if (!CircuitJsonElementValidator.#isObject(value)) {
|
|
224
|
+
return ['Expected a CircuitJSON element object' + location + '.']
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const type = String(value.type || '').trim()
|
|
228
|
+
if (!type) {
|
|
229
|
+
return ['CircuitJSON element type is required' + location + '.']
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!KNOWN_ELEMENT_TYPES.has(type)) {
|
|
233
|
+
return ['Unsupported CircuitJSON element type: ' + type + '.']
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const errors = []
|
|
237
|
+
CircuitJsonElementValidator.#validateId(value, type, errors)
|
|
238
|
+
CircuitJsonElementValidator.#validateCoreShape(value, type, errors)
|
|
239
|
+
return errors
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Returns all known serialized element type names.
|
|
244
|
+
* @returns {string[]}
|
|
245
|
+
*/
|
|
246
|
+
static knownElementTypes() {
|
|
247
|
+
return [...KNOWN_ELEMENT_TYPES]
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Returns id convention exceptions from the current schema snapshot.
|
|
252
|
+
* @returns {string[]}
|
|
253
|
+
*/
|
|
254
|
+
static idFieldExceptions() {
|
|
255
|
+
return [...ID_FIELD_EXCEPTIONS]
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Compares current schema metadata against a saved snapshot.
|
|
260
|
+
* @param {{ elementTypes?: string[], idFieldExceptions?: string[] }} snapshot Schema snapshot.
|
|
261
|
+
* @returns {{ matches: boolean, missingElementTypes: string[], unexpectedElementTypes: string[], missingIdFieldExceptions: string[], unexpectedIdFieldExceptions: string[] }}
|
|
262
|
+
*/
|
|
263
|
+
static compareSchemaSnapshot(snapshot = {}) {
|
|
264
|
+
const elementComparison = CircuitJsonElementValidator.#compareSets(
|
|
265
|
+
new Set(snapshot.elementTypes || []),
|
|
266
|
+
KNOWN_ELEMENT_TYPES
|
|
267
|
+
)
|
|
268
|
+
const exceptionComparison = CircuitJsonElementValidator.#compareSets(
|
|
269
|
+
new Set(snapshot.idFieldExceptions || []),
|
|
270
|
+
ID_FIELD_EXCEPTIONS
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
matches:
|
|
275
|
+
elementComparison.missing.length === 0 &&
|
|
276
|
+
elementComparison.unexpected.length === 0 &&
|
|
277
|
+
exceptionComparison.missing.length === 0 &&
|
|
278
|
+
exceptionComparison.unexpected.length === 0,
|
|
279
|
+
missingElementTypes: elementComparison.missing,
|
|
280
|
+
unexpectedElementTypes: elementComparison.unexpected,
|
|
281
|
+
missingIdFieldExceptions: exceptionComparison.missing,
|
|
282
|
+
unexpectedIdFieldExceptions: exceptionComparison.unexpected
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Validates the common id convention.
|
|
288
|
+
* @param {Record<string, unknown>} element Element.
|
|
289
|
+
* @param {string} type Element type.
|
|
290
|
+
* @param {string[]} errors Error sink.
|
|
291
|
+
* @returns {void}
|
|
292
|
+
*/
|
|
293
|
+
static #validateId(element, type, errors) {
|
|
294
|
+
if (ID_FIELD_EXCEPTIONS.has(type)) {
|
|
295
|
+
return
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const idField = type + '_id'
|
|
299
|
+
if (!CircuitJsonElementValidator.#isNonEmptyString(element[idField])) {
|
|
300
|
+
errors.push(type + ' ' + idField + ' is required.')
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* Validates type-specific fields used by core consumers.
|
|
306
|
+
* @param {Record<string, unknown>} element Element.
|
|
307
|
+
* @param {string} type Element type.
|
|
308
|
+
* @param {string[]} errors Error sink.
|
|
309
|
+
* @returns {void}
|
|
310
|
+
*/
|
|
311
|
+
static #validateCoreShape(element, type, errors) {
|
|
312
|
+
if (type === 'source_component') {
|
|
313
|
+
CircuitJsonElementValidator.#validateSourceComponent(
|
|
314
|
+
element,
|
|
315
|
+
errors
|
|
316
|
+
)
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (type === 'source_port') {
|
|
321
|
+
CircuitJsonElementValidator.#validateSourcePort(element, errors)
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (type === 'schematic_component') {
|
|
326
|
+
CircuitJsonElementValidator.#validateSchematicComponent(
|
|
327
|
+
element,
|
|
328
|
+
errors
|
|
329
|
+
)
|
|
330
|
+
return
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (type === 'pcb_board') {
|
|
334
|
+
CircuitJsonElementValidator.#validatePcbBoard(element, errors)
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (type === 'pcb_component') {
|
|
339
|
+
CircuitJsonElementValidator.#validatePcbComponent(element, errors)
|
|
340
|
+
return
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (type === 'pcb_smtpad') {
|
|
344
|
+
CircuitJsonElementValidator.#validatePcbSmtPad(element, errors)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Validates a source component.
|
|
350
|
+
* @param {Record<string, unknown>} element Element.
|
|
351
|
+
* @param {string[]} errors Error sink.
|
|
352
|
+
* @returns {void}
|
|
353
|
+
*/
|
|
354
|
+
static #validateSourceComponent(element, errors) {
|
|
355
|
+
CircuitJsonElementValidator.#requireString(
|
|
356
|
+
element,
|
|
357
|
+
'source_component',
|
|
358
|
+
'name',
|
|
359
|
+
errors
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
if (
|
|
363
|
+
Object.hasOwn(element, 'supplier_part_numbers') &&
|
|
364
|
+
!CircuitJsonElementValidator.#isPlainObject(
|
|
365
|
+
element.supplier_part_numbers
|
|
366
|
+
)
|
|
367
|
+
) {
|
|
368
|
+
errors.push(
|
|
369
|
+
'source_component supplier_part_numbers must be an object.'
|
|
370
|
+
)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (
|
|
374
|
+
Object.hasOwn(element, 'ftype') &&
|
|
375
|
+
!SOURCE_COMPONENT_FTYPES.has(String(element.ftype || ''))
|
|
376
|
+
) {
|
|
377
|
+
errors.push('source_component ftype is not supported.')
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Validates a source port.
|
|
383
|
+
* @param {Record<string, unknown>} element Element.
|
|
384
|
+
* @param {string[]} errors Error sink.
|
|
385
|
+
* @returns {void}
|
|
386
|
+
*/
|
|
387
|
+
static #validateSourcePort(element, errors) {
|
|
388
|
+
if (
|
|
389
|
+
Object.hasOwn(element, 'pin_number') &&
|
|
390
|
+
!Number.isFinite(element.pin_number)
|
|
391
|
+
) {
|
|
392
|
+
errors.push('source_port pin_number must be a number.')
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Validates a schematic component.
|
|
398
|
+
* @param {Record<string, unknown>} element Element.
|
|
399
|
+
* @param {string[]} errors Error sink.
|
|
400
|
+
* @returns {void}
|
|
401
|
+
*/
|
|
402
|
+
static #validateSchematicComponent(element, errors) {
|
|
403
|
+
CircuitJsonElementValidator.#requirePoint(
|
|
404
|
+
element,
|
|
405
|
+
'schematic_component',
|
|
406
|
+
'center',
|
|
407
|
+
errors
|
|
408
|
+
)
|
|
409
|
+
CircuitJsonElementValidator.#requireSize(
|
|
410
|
+
element,
|
|
411
|
+
'schematic_component',
|
|
412
|
+
'size',
|
|
413
|
+
errors
|
|
414
|
+
)
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Validates a PCB board.
|
|
419
|
+
* @param {Record<string, unknown>} element Element.
|
|
420
|
+
* @param {string[]} errors Error sink.
|
|
421
|
+
* @returns {void}
|
|
422
|
+
*/
|
|
423
|
+
static #validatePcbBoard(element, errors) {
|
|
424
|
+
CircuitJsonElementValidator.#requirePoint(
|
|
425
|
+
element,
|
|
426
|
+
'pcb_board',
|
|
427
|
+
'center',
|
|
428
|
+
errors
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Validates a PCB component.
|
|
434
|
+
* @param {Record<string, unknown>} element Element.
|
|
435
|
+
* @param {string[]} errors Error sink.
|
|
436
|
+
* @returns {void}
|
|
437
|
+
*/
|
|
438
|
+
static #validatePcbComponent(element, errors) {
|
|
439
|
+
CircuitJsonElementValidator.#requireString(
|
|
440
|
+
element,
|
|
441
|
+
'pcb_component',
|
|
442
|
+
'source_component_id',
|
|
443
|
+
errors
|
|
444
|
+
)
|
|
445
|
+
CircuitJsonElementValidator.#requirePoint(
|
|
446
|
+
element,
|
|
447
|
+
'pcb_component',
|
|
448
|
+
'center',
|
|
449
|
+
errors
|
|
450
|
+
)
|
|
451
|
+
CircuitJsonElementValidator.#requireLayer(
|
|
452
|
+
element,
|
|
453
|
+
'pcb_component',
|
|
454
|
+
'layer',
|
|
455
|
+
errors
|
|
456
|
+
)
|
|
457
|
+
CircuitJsonElementValidator.#optionalAngle(
|
|
458
|
+
element,
|
|
459
|
+
'pcb_component',
|
|
460
|
+
'rotation',
|
|
461
|
+
errors
|
|
462
|
+
)
|
|
463
|
+
CircuitJsonElementValidator.#optionalLength(
|
|
464
|
+
element,
|
|
465
|
+
'pcb_component',
|
|
466
|
+
'width',
|
|
467
|
+
errors
|
|
468
|
+
)
|
|
469
|
+
CircuitJsonElementValidator.#optionalLength(
|
|
470
|
+
element,
|
|
471
|
+
'pcb_component',
|
|
472
|
+
'height',
|
|
473
|
+
errors
|
|
474
|
+
)
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Validates an SMT pad.
|
|
479
|
+
* @param {Record<string, unknown>} element Element.
|
|
480
|
+
* @param {string[]} errors Error sink.
|
|
481
|
+
* @returns {void}
|
|
482
|
+
*/
|
|
483
|
+
static #validatePcbSmtPad(element, errors) {
|
|
484
|
+
const shape = String(element.shape || '')
|
|
485
|
+
if (!SMT_PAD_SHAPES.has(shape)) {
|
|
486
|
+
errors.push(
|
|
487
|
+
'pcb_smtpad shape must be one of: ' +
|
|
488
|
+
[...SMT_PAD_SHAPES].join(', ') +
|
|
489
|
+
'.'
|
|
490
|
+
)
|
|
491
|
+
return
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
CircuitJsonElementValidator.#requireLayer(
|
|
495
|
+
element,
|
|
496
|
+
'pcb_smtpad',
|
|
497
|
+
'layer',
|
|
498
|
+
errors
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
if (shape === 'polygon') {
|
|
502
|
+
if (!Array.isArray(element.points)) {
|
|
503
|
+
errors.push('pcb_smtpad points is required.')
|
|
504
|
+
}
|
|
505
|
+
return
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
CircuitJsonElementValidator.#requireLength(
|
|
509
|
+
element,
|
|
510
|
+
'pcb_smtpad',
|
|
511
|
+
'x',
|
|
512
|
+
errors
|
|
513
|
+
)
|
|
514
|
+
CircuitJsonElementValidator.#requireLength(
|
|
515
|
+
element,
|
|
516
|
+
'pcb_smtpad',
|
|
517
|
+
'y',
|
|
518
|
+
errors
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
if (shape === 'circle') {
|
|
522
|
+
if (
|
|
523
|
+
!CircuitJsonElementValidator.#hasLength(element, 'radius') &&
|
|
524
|
+
!CircuitJsonElementValidator.#hasLength(element, 'diameter') &&
|
|
525
|
+
!(
|
|
526
|
+
CircuitJsonElementValidator.#hasLength(element, 'width') &&
|
|
527
|
+
CircuitJsonElementValidator.#hasLength(element, 'height')
|
|
528
|
+
)
|
|
529
|
+
) {
|
|
530
|
+
errors.push(
|
|
531
|
+
'pcb_smtpad radius, diameter, or width and height is required.'
|
|
532
|
+
)
|
|
533
|
+
}
|
|
534
|
+
return
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
CircuitJsonElementValidator.#requireLength(
|
|
538
|
+
element,
|
|
539
|
+
'pcb_smtpad',
|
|
540
|
+
'width',
|
|
541
|
+
errors
|
|
542
|
+
)
|
|
543
|
+
CircuitJsonElementValidator.#requireLength(
|
|
544
|
+
element,
|
|
545
|
+
'pcb_smtpad',
|
|
546
|
+
'height',
|
|
547
|
+
errors
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
if (shape.startsWith('rotated_')) {
|
|
551
|
+
CircuitJsonElementValidator.#requireAngle(
|
|
552
|
+
element,
|
|
553
|
+
'pcb_smtpad',
|
|
554
|
+
'ccw_rotation',
|
|
555
|
+
errors
|
|
556
|
+
)
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
if (shape.endsWith('pill')) {
|
|
560
|
+
CircuitJsonElementValidator.#optionalLength(
|
|
561
|
+
element,
|
|
562
|
+
'pcb_smtpad',
|
|
563
|
+
'radius',
|
|
564
|
+
errors
|
|
565
|
+
)
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Requires a non-empty string field.
|
|
571
|
+
* @param {Record<string, unknown>} element Element.
|
|
572
|
+
* @param {string} type Element type.
|
|
573
|
+
* @param {string} field Field name.
|
|
574
|
+
* @param {string[]} errors Error sink.
|
|
575
|
+
* @returns {void}
|
|
576
|
+
*/
|
|
577
|
+
static #requireString(element, type, field, errors) {
|
|
578
|
+
if (!CircuitJsonElementValidator.#isNonEmptyString(element[field])) {
|
|
579
|
+
errors.push(type + ' ' + field + ' is required.')
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Requires a finite number field.
|
|
585
|
+
* @param {Record<string, unknown>} element Element.
|
|
586
|
+
* @param {string} type Element type.
|
|
587
|
+
* @param {string} field Field name.
|
|
588
|
+
* @param {string[]} errors Error sink.
|
|
589
|
+
* @returns {void}
|
|
590
|
+
*/
|
|
591
|
+
static #requireNumber(element, type, field, errors) {
|
|
592
|
+
if (!Number.isFinite(element[field])) {
|
|
593
|
+
errors.push(type + ' ' + field + ' is required.')
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Requires a finite length field.
|
|
599
|
+
* @param {Record<string, unknown>} element Element.
|
|
600
|
+
* @param {string} type Element type.
|
|
601
|
+
* @param {string} field Field name.
|
|
602
|
+
* @param {string[]} errors Error sink.
|
|
603
|
+
* @returns {void}
|
|
604
|
+
*/
|
|
605
|
+
static #requireLength(element, type, field, errors) {
|
|
606
|
+
if (CircuitJsonUnits.optionalLength(element[field]) === null) {
|
|
607
|
+
errors.push(type + ' ' + field + ' is required.')
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* Returns true when a field is a finite length.
|
|
613
|
+
* @param {Record<string, unknown>} element Element.
|
|
614
|
+
* @param {string} field Field name.
|
|
615
|
+
* @returns {boolean}
|
|
616
|
+
*/
|
|
617
|
+
static #hasLength(element, field) {
|
|
618
|
+
return CircuitJsonUnits.optionalLength(element[field]) !== null
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Requires a finite angle field.
|
|
623
|
+
* @param {Record<string, unknown>} element Element.
|
|
624
|
+
* @param {string} type Element type.
|
|
625
|
+
* @param {string} field Field name.
|
|
626
|
+
* @param {string[]} errors Error sink.
|
|
627
|
+
* @returns {void}
|
|
628
|
+
*/
|
|
629
|
+
static #requireAngle(element, type, field, errors) {
|
|
630
|
+
if (CircuitJsonUnits.optionalAngle(element[field]) === null) {
|
|
631
|
+
errors.push(type + ' ' + field + ' is required.')
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
/**
|
|
636
|
+
* Validates an optional finite length field.
|
|
637
|
+
* @param {Record<string, unknown>} element Element.
|
|
638
|
+
* @param {string} type Element type.
|
|
639
|
+
* @param {string} field Field name.
|
|
640
|
+
* @param {string[]} errors Error sink.
|
|
641
|
+
* @returns {void}
|
|
642
|
+
*/
|
|
643
|
+
static #optionalLength(element, type, field, errors) {
|
|
644
|
+
if (
|
|
645
|
+
Object.hasOwn(element, field) &&
|
|
646
|
+
CircuitJsonUnits.optionalLength(element[field]) === null
|
|
647
|
+
) {
|
|
648
|
+
errors.push(type + ' ' + field + ' must be a finite length.')
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Validates an optional finite angle field.
|
|
654
|
+
* @param {Record<string, unknown>} element Element.
|
|
655
|
+
* @param {string} type Element type.
|
|
656
|
+
* @param {string} field Field name.
|
|
657
|
+
* @param {string[]} errors Error sink.
|
|
658
|
+
* @returns {void}
|
|
659
|
+
*/
|
|
660
|
+
static #optionalAngle(element, type, field, errors) {
|
|
661
|
+
if (
|
|
662
|
+
Object.hasOwn(element, field) &&
|
|
663
|
+
CircuitJsonUnits.optionalAngle(element[field]) === null
|
|
664
|
+
) {
|
|
665
|
+
errors.push(type + ' ' + field + ' must be a finite angle.')
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
/**
|
|
670
|
+
* Requires a finite point object field.
|
|
671
|
+
* @param {Record<string, unknown>} element Element.
|
|
672
|
+
* @param {string} type Element type.
|
|
673
|
+
* @param {string} field Field name.
|
|
674
|
+
* @param {string[]} errors Error sink.
|
|
675
|
+
* @returns {void}
|
|
676
|
+
*/
|
|
677
|
+
static #requirePoint(element, type, field, errors) {
|
|
678
|
+
const point = element[field]
|
|
679
|
+
if (!CircuitJsonElementValidator.#isObject(point)) {
|
|
680
|
+
errors.push(type + ' ' + field + ' is required.')
|
|
681
|
+
return
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
if (CircuitJsonUnits.optionalPoint(point) === null) {
|
|
685
|
+
errors.push(type + ' ' + field + ' is required.')
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Requires a finite size object field.
|
|
691
|
+
* @param {Record<string, unknown>} element Element.
|
|
692
|
+
* @param {string} type Element type.
|
|
693
|
+
* @param {string} field Field name.
|
|
694
|
+
* @param {string[]} errors Error sink.
|
|
695
|
+
* @returns {void}
|
|
696
|
+
*/
|
|
697
|
+
static #requireSize(element, type, field, errors) {
|
|
698
|
+
const size = element[field]
|
|
699
|
+
if (!CircuitJsonElementValidator.#isObject(size)) {
|
|
700
|
+
errors.push(type + ' ' + field + ' is required.')
|
|
701
|
+
return
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
if (CircuitJsonUnits.optionalSize(size) === null) {
|
|
705
|
+
errors.push(type + ' ' + field + ' is required.')
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Requires a known layer field.
|
|
711
|
+
* @param {Record<string, unknown>} element Element.
|
|
712
|
+
* @param {string} type Element type.
|
|
713
|
+
* @param {string} field Field name.
|
|
714
|
+
* @param {string[]} errors Error sink.
|
|
715
|
+
* @returns {void}
|
|
716
|
+
*/
|
|
717
|
+
static #requireLayer(element, type, field, errors) {
|
|
718
|
+
const layer =
|
|
719
|
+
typeof element[field] === 'object' && element[field] !== null
|
|
720
|
+
? element[field].name
|
|
721
|
+
: element[field]
|
|
722
|
+
if (!LAYERS.has(String(layer || ''))) {
|
|
723
|
+
errors.push(type + ' ' + field + ' is required.')
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Returns true for non-null objects.
|
|
729
|
+
* @param {unknown} value Candidate.
|
|
730
|
+
* @returns {boolean}
|
|
731
|
+
*/
|
|
732
|
+
static #isObject(value) {
|
|
733
|
+
return Boolean(value) && typeof value === 'object'
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Returns true for plain object values.
|
|
738
|
+
* @param {unknown} value Candidate.
|
|
739
|
+
* @returns {boolean}
|
|
740
|
+
*/
|
|
741
|
+
static #isPlainObject(value) {
|
|
742
|
+
return (
|
|
743
|
+
CircuitJsonElementValidator.#isObject(value) &&
|
|
744
|
+
!Array.isArray(value)
|
|
745
|
+
)
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Returns true for non-empty strings.
|
|
750
|
+
* @param {unknown} value Candidate.
|
|
751
|
+
* @returns {boolean}
|
|
752
|
+
*/
|
|
753
|
+
static #isNonEmptyString(value) {
|
|
754
|
+
return typeof value === 'string' && value.trim().length > 0
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Compares expected and actual string sets.
|
|
759
|
+
* @param {Set<string>} expected Expected values.
|
|
760
|
+
* @param {Set<string>} actual Actual values.
|
|
761
|
+
* @returns {{ missing: string[], unexpected: string[] }}
|
|
762
|
+
*/
|
|
763
|
+
static #compareSets(expected, actual) {
|
|
764
|
+
return {
|
|
765
|
+
missing: [...expected]
|
|
766
|
+
.filter((value) => !actual.has(value))
|
|
767
|
+
.sort((left, right) => left.localeCompare(right)),
|
|
768
|
+
unexpected: [...actual]
|
|
769
|
+
.filter((value) => !expected.has(value))
|
|
770
|
+
.sort((left, right) => left.localeCompare(right))
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|