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
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
import { CircuitJsonElementValidator } from './CircuitJsonElementValidator.mjs'
|
|
2
|
+
|
|
3
|
+
const PCB_RENDERED_TYPES = new Set([
|
|
4
|
+
'pcb_board',
|
|
5
|
+
'pcb_breakout_point',
|
|
6
|
+
'pcb_component',
|
|
7
|
+
'pcb_copper_pour',
|
|
8
|
+
'pcb_copper_text',
|
|
9
|
+
'pcb_courtyard',
|
|
10
|
+
'pcb_courtyard_circle',
|
|
11
|
+
'pcb_courtyard_line',
|
|
12
|
+
'pcb_courtyard_outline',
|
|
13
|
+
'pcb_courtyard_path',
|
|
14
|
+
'pcb_courtyard_pill',
|
|
15
|
+
'pcb_courtyard_polygon',
|
|
16
|
+
'pcb_courtyard_rect',
|
|
17
|
+
'pcb_cutout',
|
|
18
|
+
'pcb_fabrication_note_dimension',
|
|
19
|
+
'pcb_fabrication_note_path',
|
|
20
|
+
'pcb_fabrication_note_rect',
|
|
21
|
+
'pcb_fabrication_note_text',
|
|
22
|
+
'pcb_ground_plane',
|
|
23
|
+
'pcb_ground_plane_region',
|
|
24
|
+
'pcb_group',
|
|
25
|
+
'pcb_hole',
|
|
26
|
+
'pcb_keepout',
|
|
27
|
+
'pcb_note_dimension',
|
|
28
|
+
'pcb_note_line',
|
|
29
|
+
'pcb_note_path',
|
|
30
|
+
'pcb_note_rect',
|
|
31
|
+
'pcb_note_text',
|
|
32
|
+
'pcb_panel',
|
|
33
|
+
'pcb_plated_hole',
|
|
34
|
+
'pcb_port',
|
|
35
|
+
'pcb_silkscreen_circle',
|
|
36
|
+
'pcb_silkscreen_graphic',
|
|
37
|
+
'pcb_silkscreen_line',
|
|
38
|
+
'pcb_silkscreen_oval',
|
|
39
|
+
'pcb_silkscreen_path',
|
|
40
|
+
'pcb_silkscreen_pill',
|
|
41
|
+
'pcb_silkscreen_rect',
|
|
42
|
+
'pcb_silkscreen_text',
|
|
43
|
+
'pcb_smtpad',
|
|
44
|
+
'pcb_solder_paste',
|
|
45
|
+
'pcb_text',
|
|
46
|
+
'pcb_thermal_spoke',
|
|
47
|
+
'pcb_trace',
|
|
48
|
+
'pcb_trace_hint',
|
|
49
|
+
'pcb_via'
|
|
50
|
+
])
|
|
51
|
+
|
|
52
|
+
const SCHEMATIC_RENDERED_TYPES = new Set([
|
|
53
|
+
'schematic_arc',
|
|
54
|
+
'schematic_box',
|
|
55
|
+
'schematic_circle',
|
|
56
|
+
'schematic_component',
|
|
57
|
+
'schematic_debug_object',
|
|
58
|
+
'schematic_group',
|
|
59
|
+
'schematic_line',
|
|
60
|
+
'schematic_net_label',
|
|
61
|
+
'schematic_path',
|
|
62
|
+
'schematic_port',
|
|
63
|
+
'schematic_rect',
|
|
64
|
+
'schematic_sheet',
|
|
65
|
+
'schematic_symbol',
|
|
66
|
+
'schematic_table',
|
|
67
|
+
'schematic_table_cell',
|
|
68
|
+
'schematic_text',
|
|
69
|
+
'schematic_trace',
|
|
70
|
+
'schematic_voltage_probe'
|
|
71
|
+
])
|
|
72
|
+
|
|
73
|
+
const ROUTING_DSN_TYPES = new Set([
|
|
74
|
+
'pcb_board',
|
|
75
|
+
'pcb_component',
|
|
76
|
+
'pcb_smtpad',
|
|
77
|
+
'pcb_trace',
|
|
78
|
+
'pcb_via',
|
|
79
|
+
'pcb_plated_hole',
|
|
80
|
+
'source_net'
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
const VARIANT_ROWS_BY_SET = {
|
|
84
|
+
sourceComponentFtypes: {
|
|
85
|
+
type: 'source_component',
|
|
86
|
+
group: 'ftype',
|
|
87
|
+
capability: 'bom',
|
|
88
|
+
status: 'grouped'
|
|
89
|
+
},
|
|
90
|
+
pcbBoardShapes: {
|
|
91
|
+
type: 'pcb_board',
|
|
92
|
+
group: 'shape',
|
|
93
|
+
capability: 'pcb',
|
|
94
|
+
status: 'rendered'
|
|
95
|
+
},
|
|
96
|
+
pcbSmtPadShapes: {
|
|
97
|
+
type: 'pcb_smtpad',
|
|
98
|
+
group: 'shape',
|
|
99
|
+
capability: 'pcb',
|
|
100
|
+
status: 'rendered'
|
|
101
|
+
},
|
|
102
|
+
pcbHoleShapes: {
|
|
103
|
+
type: 'pcb_hole',
|
|
104
|
+
group: 'hole_shape',
|
|
105
|
+
capability: 'pcb',
|
|
106
|
+
status: 'rendered'
|
|
107
|
+
},
|
|
108
|
+
pcbPlatedHoleShapes: {
|
|
109
|
+
type: 'pcb_plated_hole',
|
|
110
|
+
group: 'shape',
|
|
111
|
+
capability: 'pcb',
|
|
112
|
+
status: 'rendered'
|
|
113
|
+
},
|
|
114
|
+
pcbPlatedHoleHoleShapes: {
|
|
115
|
+
type: 'pcb_plated_hole',
|
|
116
|
+
group: 'hole_shape',
|
|
117
|
+
capability: 'pcb',
|
|
118
|
+
status: 'rendered'
|
|
119
|
+
},
|
|
120
|
+
pcbSolderPasteShapes: {
|
|
121
|
+
type: 'pcb_solder_paste',
|
|
122
|
+
group: 'shape',
|
|
123
|
+
capability: 'pcb',
|
|
124
|
+
status: 'rendered'
|
|
125
|
+
},
|
|
126
|
+
pcbCutoutShapes: {
|
|
127
|
+
type: 'pcb_cutout',
|
|
128
|
+
group: 'shape',
|
|
129
|
+
capability: 'pcb',
|
|
130
|
+
status: 'rendered'
|
|
131
|
+
},
|
|
132
|
+
pcbCopperPourShapes: {
|
|
133
|
+
type: 'pcb_copper_pour',
|
|
134
|
+
group: 'shape',
|
|
135
|
+
capability: 'pcb',
|
|
136
|
+
status: 'rendered'
|
|
137
|
+
},
|
|
138
|
+
simulationSourceKinds: {
|
|
139
|
+
type: 'simulation_voltage_source',
|
|
140
|
+
group: 'kind',
|
|
141
|
+
capability: 'simulation',
|
|
142
|
+
status: 'summarized'
|
|
143
|
+
},
|
|
144
|
+
simulationWaveShapes: {
|
|
145
|
+
type: 'simulation_voltage_source',
|
|
146
|
+
group: 'wave_shape',
|
|
147
|
+
capability: 'simulation',
|
|
148
|
+
status: 'summarized'
|
|
149
|
+
},
|
|
150
|
+
simulationExperimentMethods: {
|
|
151
|
+
type: 'simulation_experiment',
|
|
152
|
+
group: 'spice_options.method',
|
|
153
|
+
capability: 'simulation',
|
|
154
|
+
status: 'summarized'
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Builds document-level support coverage reports from known element metadata.
|
|
160
|
+
*/
|
|
161
|
+
export class CircuitJsonSupportMatrixBuilder {
|
|
162
|
+
/**
|
|
163
|
+
* Builds a support matrix for the known schema snapshot and present rows.
|
|
164
|
+
* @param {object[]} [circuitJson] Parsed element array.
|
|
165
|
+
* @returns {{ sourceFormat: string, totals: object, rows: object[], variantRows: object[], gaps: object[] }}
|
|
166
|
+
*/
|
|
167
|
+
static build(circuitJson = []) {
|
|
168
|
+
const elements = Array.isArray(circuitJson) ? circuitJson : []
|
|
169
|
+
const presentTypes = new Set(
|
|
170
|
+
elements
|
|
171
|
+
.map((element) => String(element?.type || ''))
|
|
172
|
+
.filter(Boolean)
|
|
173
|
+
)
|
|
174
|
+
const rows = CircuitJsonElementValidator.knownElementTypes().map(
|
|
175
|
+
(type) =>
|
|
176
|
+
CircuitJsonSupportMatrixBuilder.#row(
|
|
177
|
+
type,
|
|
178
|
+
presentTypes.has(type)
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
const variantRows =
|
|
182
|
+
CircuitJsonSupportMatrixBuilder.#variantRows(elements)
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
sourceFormat: 'circuitjson',
|
|
186
|
+
totals: CircuitJsonSupportMatrixBuilder.#totals(
|
|
187
|
+
rows,
|
|
188
|
+
presentTypes,
|
|
189
|
+
variantRows
|
|
190
|
+
),
|
|
191
|
+
rows,
|
|
192
|
+
variantRows,
|
|
193
|
+
gaps: rows.flatMap((row) =>
|
|
194
|
+
CircuitJsonSupportMatrixBuilder.#gaps(row)
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Builds one matrix row.
|
|
201
|
+
* @param {string} type Element type.
|
|
202
|
+
* @param {boolean} present Whether the current document contains the type.
|
|
203
|
+
* @returns {object}
|
|
204
|
+
*/
|
|
205
|
+
static #row(type, present) {
|
|
206
|
+
const capabilities = CircuitJsonSupportMatrixBuilder.#capabilities(type)
|
|
207
|
+
return {
|
|
208
|
+
type,
|
|
209
|
+
family: CircuitJsonSupportMatrixBuilder.#family(type),
|
|
210
|
+
present,
|
|
211
|
+
status: CircuitJsonSupportMatrixBuilder.#status(capabilities),
|
|
212
|
+
capabilities
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Resolves capability labels for one type.
|
|
218
|
+
* @param {string} type Element type.
|
|
219
|
+
* @returns {Record<string, string>}
|
|
220
|
+
*/
|
|
221
|
+
static #capabilities(type) {
|
|
222
|
+
const capabilities = {
|
|
223
|
+
validation: 'known',
|
|
224
|
+
parser: 'preserved',
|
|
225
|
+
indexer: 'indexed',
|
|
226
|
+
diagnostics: CircuitJsonSupportMatrixBuilder.#diagnostics(type),
|
|
227
|
+
schematic: SCHEMATIC_RENDERED_TYPES.has(type) ? 'rendered' : 'none',
|
|
228
|
+
pcb: PCB_RENDERED_TYPES.has(type) ? 'rendered' : 'none',
|
|
229
|
+
scene3d: type === 'cad_component' ? 'external-model' : 'none',
|
|
230
|
+
bom: type === 'source_component' ? 'grouped' : 'none',
|
|
231
|
+
manufacturing: CircuitJsonSupportMatrixBuilder.#manufacturing(type),
|
|
232
|
+
simulation: type.startsWith('simulation_') ? 'preserved' : 'none'
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (type === 'pcb_component') {
|
|
236
|
+
capabilities.manufacturing = 'pick-and-place'
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return capabilities
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Resolves diagnostic support for one type.
|
|
244
|
+
* @param {string} type Element type.
|
|
245
|
+
* @returns {string}
|
|
246
|
+
*/
|
|
247
|
+
static #diagnostics(type) {
|
|
248
|
+
return /(?:error|warning)/u.test(type) ? 'normalized' : 'none'
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Resolves manufacturing support for one type.
|
|
253
|
+
* @param {string} type Element type.
|
|
254
|
+
* @returns {string}
|
|
255
|
+
*/
|
|
256
|
+
static #manufacturing(type) {
|
|
257
|
+
if (ROUTING_DSN_TYPES.has(type)) return 'routing-dsn'
|
|
258
|
+
return 'none'
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Resolves an overall support status.
|
|
263
|
+
* @param {Record<string, string>} capabilities Capability labels.
|
|
264
|
+
* @returns {'full' | 'partial' | 'metadata'}
|
|
265
|
+
*/
|
|
266
|
+
static #status(capabilities) {
|
|
267
|
+
if (
|
|
268
|
+
capabilities.pcb === 'rendered' ||
|
|
269
|
+
capabilities.schematic === 'rendered' ||
|
|
270
|
+
capabilities.scene3d === 'external-model' ||
|
|
271
|
+
capabilities.diagnostics === 'normalized'
|
|
272
|
+
) {
|
|
273
|
+
return capabilities.manufacturing === 'routing-dsn'
|
|
274
|
+
? 'partial'
|
|
275
|
+
: 'full'
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return 'metadata'
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Builds gap rows for present partially supported capabilities.
|
|
283
|
+
* @param {object} row Matrix row.
|
|
284
|
+
* @returns {object[]}
|
|
285
|
+
*/
|
|
286
|
+
static #gaps(row) {
|
|
287
|
+
if (!row.present) return []
|
|
288
|
+
if (row.capabilities.manufacturing === 'routing-dsn') {
|
|
289
|
+
return [
|
|
290
|
+
{
|
|
291
|
+
type: row.type,
|
|
292
|
+
capability: 'manufacturing',
|
|
293
|
+
status: 'partial',
|
|
294
|
+
detail: 'Routing exchange metadata is generated without full fabrication packaging.'
|
|
295
|
+
}
|
|
296
|
+
]
|
|
297
|
+
}
|
|
298
|
+
if (row.status === 'metadata') {
|
|
299
|
+
return [
|
|
300
|
+
{
|
|
301
|
+
type: row.type,
|
|
302
|
+
capability: 'rendering',
|
|
303
|
+
status: 'metadata',
|
|
304
|
+
detail: 'The element is preserved for downstream consumers.'
|
|
305
|
+
}
|
|
306
|
+
]
|
|
307
|
+
}
|
|
308
|
+
return []
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Builds variant coverage rows from active schema metadata.
|
|
313
|
+
* @param {object[]} elements Parsed element rows.
|
|
314
|
+
* @returns {object[]}
|
|
315
|
+
*/
|
|
316
|
+
static #variantRows(elements) {
|
|
317
|
+
const schema = CircuitJsonElementValidator.variantSets()
|
|
318
|
+
const present =
|
|
319
|
+
CircuitJsonSupportMatrixBuilder.#presentVariants(elements)
|
|
320
|
+
return Object.entries(schema).flatMap(([setName, values]) => {
|
|
321
|
+
const definition = VARIANT_ROWS_BY_SET[setName]
|
|
322
|
+
if (!definition) return []
|
|
323
|
+
return values.map((value) =>
|
|
324
|
+
CircuitJsonSupportMatrixBuilder.#variantRow(
|
|
325
|
+
setName,
|
|
326
|
+
definition,
|
|
327
|
+
value,
|
|
328
|
+
present
|
|
329
|
+
)
|
|
330
|
+
)
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Builds one variant coverage row.
|
|
336
|
+
* @param {string} setName Variant set name.
|
|
337
|
+
* @param {object} definition Row definition.
|
|
338
|
+
* @param {string} value Variant value.
|
|
339
|
+
* @param {Map<string, Set<string>>} present Present variant values.
|
|
340
|
+
* @returns {object}
|
|
341
|
+
*/
|
|
342
|
+
static #variantRow(setName, definition, value, present) {
|
|
343
|
+
const isPresent = Boolean(present.get(setName)?.has(value))
|
|
344
|
+
return {
|
|
345
|
+
set: setName,
|
|
346
|
+
type: definition.type,
|
|
347
|
+
group: definition.group,
|
|
348
|
+
value,
|
|
349
|
+
present: isPresent,
|
|
350
|
+
capability: definition.capability,
|
|
351
|
+
status: definition.status,
|
|
352
|
+
note: CircuitJsonSupportMatrixBuilder.#variantNote(
|
|
353
|
+
definition,
|
|
354
|
+
value,
|
|
355
|
+
isPresent
|
|
356
|
+
)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Builds a short human-readable variant support note.
|
|
362
|
+
* @param {object} definition Variant row definition.
|
|
363
|
+
* @param {string} value Variant value.
|
|
364
|
+
* @param {boolean} present Whether the variant is present.
|
|
365
|
+
* @returns {string}
|
|
366
|
+
*/
|
|
367
|
+
static #variantNote(definition, value, present) {
|
|
368
|
+
const label =
|
|
369
|
+
definition.type + '.' + definition.group + ' ' + value + ' is '
|
|
370
|
+
if (!present) return label + 'known but not present.'
|
|
371
|
+
if (definition.status === 'rendered') return label + 'rendered.'
|
|
372
|
+
if (definition.status === 'grouped') {
|
|
373
|
+
return label + 'used for BOM grouping.'
|
|
374
|
+
}
|
|
375
|
+
if (definition.status === 'summarized') {
|
|
376
|
+
return label + 'summarized in simulation setup.'
|
|
377
|
+
}
|
|
378
|
+
return label + 'preserved as metadata.'
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Extracts variant values present in the current document.
|
|
383
|
+
* @param {object[]} elements Parsed element rows.
|
|
384
|
+
* @returns {Map<string, Set<string>>}
|
|
385
|
+
*/
|
|
386
|
+
static #presentVariants(elements) {
|
|
387
|
+
const present = new Map()
|
|
388
|
+
const add = (setName, value) => {
|
|
389
|
+
const text = String(value || '').trim()
|
|
390
|
+
if (!text) return
|
|
391
|
+
if (!present.has(setName)) present.set(setName, new Set())
|
|
392
|
+
present.get(setName).add(text)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
for (const element of elements) {
|
|
396
|
+
if (element?.type === 'source_component') {
|
|
397
|
+
add('sourceComponentFtypes', element.ftype)
|
|
398
|
+
}
|
|
399
|
+
if (element?.type === 'pcb_board') {
|
|
400
|
+
add('pcbBoardShapes', element.shape || 'rect')
|
|
401
|
+
}
|
|
402
|
+
if (element?.type === 'pcb_smtpad') {
|
|
403
|
+
add('pcbSmtPadShapes', element.shape)
|
|
404
|
+
}
|
|
405
|
+
if (element?.type === 'pcb_hole') {
|
|
406
|
+
add('pcbHoleShapes', element.hole_shape || element.shape)
|
|
407
|
+
}
|
|
408
|
+
if (element?.type === 'pcb_plated_hole') {
|
|
409
|
+
add('pcbPlatedHoleShapes', element.shape || 'circle')
|
|
410
|
+
add('pcbPlatedHoleHoleShapes', element.hole_shape)
|
|
411
|
+
}
|
|
412
|
+
if (element?.type === 'pcb_solder_paste') {
|
|
413
|
+
add('pcbSolderPasteShapes', element.shape)
|
|
414
|
+
}
|
|
415
|
+
if (element?.type === 'pcb_cutout') {
|
|
416
|
+
add('pcbCutoutShapes', element.shape || 'rect')
|
|
417
|
+
}
|
|
418
|
+
if (element?.type === 'pcb_copper_pour') {
|
|
419
|
+
add('pcbCopperPourShapes', element.shape || 'polygon')
|
|
420
|
+
}
|
|
421
|
+
if (
|
|
422
|
+
element?.type === 'simulation_voltage_source' ||
|
|
423
|
+
element?.type === 'simulation_current_source'
|
|
424
|
+
) {
|
|
425
|
+
add(
|
|
426
|
+
'simulationSourceKinds',
|
|
427
|
+
element.source_type || element.sourceType || element.kind
|
|
428
|
+
)
|
|
429
|
+
add('simulationWaveShapes', element.wave_shape)
|
|
430
|
+
}
|
|
431
|
+
if (element?.type === 'simulation_experiment') {
|
|
432
|
+
add(
|
|
433
|
+
'simulationExperimentMethods',
|
|
434
|
+
element.spice_options?.method
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
return present
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Builds aggregate matrix counts.
|
|
444
|
+
* @param {object[]} rows Matrix rows.
|
|
445
|
+
* @param {Set<string>} presentTypes Present element types.
|
|
446
|
+
* @param {object[]} variantRows Variant coverage rows.
|
|
447
|
+
* @returns {object}
|
|
448
|
+
*/
|
|
449
|
+
static #totals(rows, presentTypes, variantRows) {
|
|
450
|
+
return {
|
|
451
|
+
knownElementTypes: rows.length,
|
|
452
|
+
presentElementTypes: rows.filter((row) => row.present).length,
|
|
453
|
+
renderedElementTypes: rows.filter(
|
|
454
|
+
(row) =>
|
|
455
|
+
row.present &&
|
|
456
|
+
(row.capabilities.pcb === 'rendered' ||
|
|
457
|
+
row.capabilities.schematic === 'rendered' ||
|
|
458
|
+
row.capabilities.scene3d === 'external-model')
|
|
459
|
+
).length,
|
|
460
|
+
diagnosticElementTypes: rows.filter(
|
|
461
|
+
(row) =>
|
|
462
|
+
row.present && row.capabilities.diagnostics === 'normalized'
|
|
463
|
+
).length,
|
|
464
|
+
unknownPresentElementTypes: [...presentTypes].filter(
|
|
465
|
+
(type) => !rows.some((row) => row.type === type)
|
|
466
|
+
).length,
|
|
467
|
+
knownVariantValues: variantRows.length,
|
|
468
|
+
presentVariantValues: variantRows.filter((row) => row.present)
|
|
469
|
+
.length
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Resolves the top-level element family.
|
|
475
|
+
* @param {string} type Element type.
|
|
476
|
+
* @returns {string}
|
|
477
|
+
*/
|
|
478
|
+
static #family(type) {
|
|
479
|
+
return String(type || '').split('_')[0] || 'unknown'
|
|
480
|
+
}
|
|
481
|
+
}
|
|
@@ -1,9 +1,114 @@
|
|
|
1
1
|
const MILS_PER_MM = 39.37007874015748
|
|
2
|
+
const LENGTH_FACTORS_TO_MM = new Map([
|
|
3
|
+
['mm', 1],
|
|
4
|
+
['millimeter', 1],
|
|
5
|
+
['millimeters', 1],
|
|
6
|
+
['cm', 10],
|
|
7
|
+
['centimeter', 10],
|
|
8
|
+
['centimeters', 10],
|
|
9
|
+
['m', 1000],
|
|
10
|
+
['meter', 1000],
|
|
11
|
+
['meters', 1000],
|
|
12
|
+
['in', 25.4],
|
|
13
|
+
['inch', 25.4],
|
|
14
|
+
['inches', 25.4],
|
|
15
|
+
['mil', 0.0254],
|
|
16
|
+
['mils', 0.0254],
|
|
17
|
+
['um', 0.001],
|
|
18
|
+
['micrometer', 0.001],
|
|
19
|
+
['micrometers', 0.001]
|
|
20
|
+
])
|
|
21
|
+
const ANGLE_FACTORS_TO_DEG = new Map([
|
|
22
|
+
['deg', 1],
|
|
23
|
+
['degree', 1],
|
|
24
|
+
['degrees', 1],
|
|
25
|
+
['rad', 180 / Math.PI],
|
|
26
|
+
['radian', 180 / Math.PI],
|
|
27
|
+
['radians', 180 / Math.PI]
|
|
28
|
+
])
|
|
2
29
|
|
|
3
30
|
/**
|
|
4
31
|
* Unit helpers for CircuitJSON's millimeter-based PCB dimensions.
|
|
5
32
|
*/
|
|
6
33
|
export class CircuitJsonUnits {
|
|
34
|
+
/**
|
|
35
|
+
* Converts a length value to millimeters.
|
|
36
|
+
* @param {unknown} value Length candidate.
|
|
37
|
+
* @param {number} [fallback] Fallback millimeter value.
|
|
38
|
+
* @returns {number}
|
|
39
|
+
*/
|
|
40
|
+
static length(value, fallback = 0) {
|
|
41
|
+
return (
|
|
42
|
+
CircuitJsonUnits.optionalLength(value) ??
|
|
43
|
+
CircuitJsonUnits.#round(fallback)
|
|
44
|
+
)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Converts a length value to millimeters, or null when invalid.
|
|
49
|
+
* @param {unknown} value Length candidate.
|
|
50
|
+
* @returns {number | null}
|
|
51
|
+
*/
|
|
52
|
+
static optionalLength(value) {
|
|
53
|
+
return CircuitJsonUnits.#parseUnitValue(value, LENGTH_FACTORS_TO_MM)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Converts an angle value to degrees.
|
|
58
|
+
* @param {unknown} value Angle candidate.
|
|
59
|
+
* @param {number} [fallback] Fallback degree value.
|
|
60
|
+
* @returns {number}
|
|
61
|
+
*/
|
|
62
|
+
static angle(value, fallback = 0) {
|
|
63
|
+
return (
|
|
64
|
+
CircuitJsonUnits.optionalAngle(value) ??
|
|
65
|
+
CircuitJsonUnits.#round(fallback)
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Converts an angle value to degrees, or null when invalid.
|
|
71
|
+
* @param {unknown} value Angle candidate.
|
|
72
|
+
* @returns {number | null}
|
|
73
|
+
*/
|
|
74
|
+
static optionalAngle(value) {
|
|
75
|
+
return CircuitJsonUnits.#parseUnitValue(value, ANGLE_FACTORS_TO_DEG)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Converts a point to normalized millimeter coordinates.
|
|
80
|
+
* @param {{ x?: unknown, y?: unknown } | null | undefined} point Point.
|
|
81
|
+
* @returns {{ x: number, y: number }}
|
|
82
|
+
*/
|
|
83
|
+
static point(point) {
|
|
84
|
+
return {
|
|
85
|
+
x: CircuitJsonUnits.length(point?.x, 0),
|
|
86
|
+
y: CircuitJsonUnits.length(point?.y, 0)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Converts a point to normalized millimeter coordinates when valid.
|
|
92
|
+
* @param {{ x?: unknown, y?: unknown } | null | undefined} point Point.
|
|
93
|
+
* @returns {{ x: number, y: number } | null}
|
|
94
|
+
*/
|
|
95
|
+
static optionalPoint(point) {
|
|
96
|
+
const x = CircuitJsonUnits.optionalLength(point?.x)
|
|
97
|
+
const y = CircuitJsonUnits.optionalLength(point?.y)
|
|
98
|
+
return x === null || y === null ? null : { x, y }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Converts a size to normalized millimeter dimensions.
|
|
103
|
+
* @param {{ width?: unknown, height?: unknown } | null | undefined} size Size.
|
|
104
|
+
* @returns {{ width: number, height: number } | null}
|
|
105
|
+
*/
|
|
106
|
+
static optionalSize(size) {
|
|
107
|
+
const width = CircuitJsonUnits.optionalLength(size?.width)
|
|
108
|
+
const height = CircuitJsonUnits.optionalLength(size?.height)
|
|
109
|
+
return width === null || height === null ? null : { width, height }
|
|
110
|
+
}
|
|
111
|
+
|
|
7
112
|
/**
|
|
8
113
|
* Converts millimeters to mils.
|
|
9
114
|
* @param {unknown} value Millimeter value.
|
|
@@ -12,7 +117,7 @@ export class CircuitJsonUnits {
|
|
|
12
117
|
*/
|
|
13
118
|
static mmToMil(value, fallback = 0) {
|
|
14
119
|
return CircuitJsonUnits.#round(
|
|
15
|
-
CircuitJsonUnits
|
|
120
|
+
CircuitJsonUnits.length(value, fallback) * MILS_PER_MM
|
|
16
121
|
)
|
|
17
122
|
}
|
|
18
123
|
|
|
@@ -29,14 +134,34 @@ export class CircuitJsonUnits {
|
|
|
29
134
|
}
|
|
30
135
|
|
|
31
136
|
/**
|
|
32
|
-
*
|
|
33
|
-
* @param {unknown} value
|
|
34
|
-
* @param {number}
|
|
35
|
-
* @returns {number}
|
|
137
|
+
* Parses one numeric value with an optional unit suffix.
|
|
138
|
+
* @param {unknown} value Value candidate.
|
|
139
|
+
* @param {Map<string, number>} unitFactors Unit factor lookup.
|
|
140
|
+
* @returns {number | null}
|
|
36
141
|
*/
|
|
37
|
-
static #
|
|
38
|
-
|
|
39
|
-
|
|
142
|
+
static #parseUnitValue(value, unitFactors) {
|
|
143
|
+
if (typeof value === 'number') {
|
|
144
|
+
return Number.isFinite(value)
|
|
145
|
+
? CircuitJsonUnits.#round(value)
|
|
146
|
+
: null
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const text = String(value ?? '').trim()
|
|
150
|
+
if (!text) return null
|
|
151
|
+
|
|
152
|
+
const match = text.match(
|
|
153
|
+
/^([+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?)\s*([a-z]+)?$/iu
|
|
154
|
+
)
|
|
155
|
+
if (!match) return null
|
|
156
|
+
|
|
157
|
+
const number = Number(match[1])
|
|
158
|
+
if (!Number.isFinite(number)) return null
|
|
159
|
+
|
|
160
|
+
const unit = String(match[2] || '').toLowerCase()
|
|
161
|
+
const factor = unit ? unitFactors.get(unit) : 1
|
|
162
|
+
if (!Number.isFinite(factor)) return null
|
|
163
|
+
|
|
164
|
+
return CircuitJsonUnits.#round(number * factor)
|
|
40
165
|
}
|
|
41
166
|
|
|
42
167
|
/**
|