circuitjson-toolkit 1.0.3 → 1.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/AGENTS.md +5 -3
  2. package/README.md +21 -2
  3. package/docs/api.md +50 -4
  4. package/docs/model-format.md +21 -3
  5. package/package.json +3 -2
  6. package/spec/library-scope.md +4 -1
  7. package/src/core/CircuitJsonBomBuilder.mjs +143 -0
  8. package/src/core/CircuitJsonDocument.mjs +46 -13
  9. package/src/core/CircuitJsonElementValidator.mjs +990 -0
  10. package/src/core/CircuitJsonIndexer.mjs +773 -4
  11. package/src/core/CircuitJsonManufacturingBuilder.mjs +898 -0
  12. package/src/core/CircuitJsonManufacturingDownloadBuilder.mjs +196 -0
  13. package/src/core/CircuitJsonParser.mjs +22 -6
  14. package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +329 -0
  15. package/src/core/CircuitJsonPcbCopperGeometry.mjs +503 -0
  16. package/src/core/CircuitJsonPcbDrawingStyle.mjs +88 -0
  17. package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +172 -0
  18. package/src/core/CircuitJsonPcbNetMetadata.mjs +247 -0
  19. package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +70 -0
  20. package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +992 -0
  21. package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +872 -0
  22. package/src/core/CircuitJsonPcbPrimitiveFields.mjs +233 -0
  23. package/src/core/CircuitJsonPcbPrimitiveGeometry.mjs +142 -0
  24. package/src/core/CircuitJsonPcbPrimitiveGroups.mjs +305 -0
  25. package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +65 -0
  26. package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +895 -0
  27. package/src/core/CircuitJsonPcbTraceLengthModel.mjs +257 -0
  28. package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +683 -0
  29. package/src/core/CircuitJsonSourceMetadata.mjs +233 -0
  30. package/src/core/CircuitJsonSupportMatrixBuilder.mjs +481 -0
  31. package/src/core/CircuitJsonUnits.mjs +133 -8
  32. package/src/core/PcbBoundsSelectionModel.mjs +250 -0
  33. package/src/core/PcbCandidateSelectionModel.mjs +77 -0
  34. package/src/core/PcbDiagnosticFocusModel.mjs +423 -0
  35. package/src/core/PcbInteractionPrimitiveModel.mjs +560 -0
  36. package/src/core/SelectedPartCircuitJsonExportAdapter.mjs +335 -0
  37. package/src/core/spice/SpiceCompatibilityPreprocessor.mjs +139 -0
  38. package/src/core/spice/SpiceDirectiveParser.mjs +231 -0
  39. package/src/core/spice/SpiceFallbackSimulationEngine.mjs +168 -0
  40. package/src/core/spice/SpiceSimulationDiagnostics.mjs +234 -0
  41. package/src/core/spice/SpiceSimulationGraphBuilder.mjs +421 -0
  42. package/src/core/spice/SpiceSimulationGraphSummary.mjs +90 -0
  43. package/src/core/spice/SpiceSimulationService.mjs +92 -0
  44. package/src/core/spice/SpiceTimeSeriesNormalizer.mjs +132 -0
  45. package/src/index.mjs +8 -0
  46. package/src/renderers.mjs +29 -0
  47. package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +128 -0
  48. package/src/ui/CircuitJsonPcbSvgRenderer.mjs +964 -0
  49. package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +168 -0
  50. package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +138 -0
  51. package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +114 -0
  52. package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +130 -0
  53. package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +994 -0
  54. package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +439 -0
@@ -0,0 +1,423 @@
1
+ import { CircuitJsonIndexer } from './CircuitJsonIndexer.mjs'
2
+ import { CircuitJsonUnits } from './CircuitJsonUnits.mjs'
3
+ import { PcbInteractionPrimitiveModel } from './PcbInteractionPrimitiveModel.mjs'
4
+
5
+ /**
6
+ * Resolves viewport focus targets for PCB diagnostic rows.
7
+ */
8
+ export class PcbDiagnosticFocusModel {
9
+ /**
10
+ * Builds a diagnostic-id to focus-target map.
11
+ * @param {object | object[]} documentModel Parsed PCB document.
12
+ * @returns {Map<string, object>}
13
+ */
14
+ static build(documentModel) {
15
+ const model = PcbInteractionPrimitiveModel.build(documentModel)
16
+ const context = PcbDiagnosticFocusModel.#context(documentModel, model)
17
+ const rows = new Map()
18
+
19
+ for (const diagnostic of model.diagnostics || []) {
20
+ const row = PcbDiagnosticFocusModel.#focusRow(diagnostic, context)
21
+ if (row) rows.set(row.id, row)
22
+ }
23
+
24
+ return rows
25
+ }
26
+
27
+ /**
28
+ * Builds a compact viewport target around a diagnostic center.
29
+ * @param {{ point?: object, bounds?: object } | null | undefined} focus Diagnostic focus row.
30
+ * @returns {{ x: number, y: number, width: number, height: number } | null}
31
+ */
32
+ static viewportBounds(focus) {
33
+ const bounds = PcbDiagnosticFocusModel.#normalizeBounds(focus?.bounds)
34
+ if (!bounds) return null
35
+ const center =
36
+ CircuitJsonUnits.optionalPoint(focus?.point) ||
37
+ PcbDiagnosticFocusModel.#boundsCenter(bounds)
38
+ const width = Math.min(Math.max(bounds.width, 0.4), 1)
39
+ const height = Math.min(Math.max(bounds.height, 0.4), 0.6)
40
+
41
+ return {
42
+ x: PcbDiagnosticFocusModel.#rounded(center.x - width / 2),
43
+ y: PcbDiagnosticFocusModel.#rounded(center.y - height / 2),
44
+ width: PcbDiagnosticFocusModel.#rounded(width),
45
+ height: PcbDiagnosticFocusModel.#rounded(height)
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Builds lookup data for focus resolution.
51
+ * @param {object | object[]} documentModel Parsed document.
52
+ * @param {object} model Primitive model.
53
+ * @returns {object}
54
+ */
55
+ static #context(documentModel, model) {
56
+ const elements = PcbDiagnosticFocusModel.#elements(documentModel)
57
+ const elementsById = new Map()
58
+ for (const element of elements) {
59
+ const id = CircuitJsonIndexer.getElementId(element)
60
+ if (id) elementsById.set(id, element)
61
+ }
62
+
63
+ return {
64
+ model,
65
+ elements,
66
+ elementsById,
67
+ sourcePortToPcbPortIds:
68
+ PcbDiagnosticFocusModel.#sourcePortToPcbPortIds(elements),
69
+ primitivesById: new Map(
70
+ (model.primitives || [])
71
+ .filter((primitive) => String(primitive.id || '').trim())
72
+ .map((primitive) => [String(primitive.id), primitive])
73
+ )
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Builds one diagnostic focus row.
79
+ * @param {object} diagnostic Diagnostic row.
80
+ * @param {object} context Focus context.
81
+ * @returns {object | null}
82
+ */
83
+ static #focusRow(diagnostic, context) {
84
+ const id = String(diagnostic.id || '').trim()
85
+ if (!id) return null
86
+
87
+ const element = context.elementsById.get(id) || null
88
+ const relatedPrimitives = PcbDiagnosticFocusModel.#relatedPrimitives(
89
+ diagnostic,
90
+ element,
91
+ context
92
+ )
93
+ const relatedBounds =
94
+ PcbDiagnosticFocusModel.#mergeBounds(
95
+ relatedPrimitives.map((primitive) => primitive.bounds)
96
+ ) ||
97
+ PcbDiagnosticFocusModel.#componentBounds(element, context.model)
98
+ const bounds =
99
+ relatedBounds ||
100
+ PcbDiagnosticFocusModel.#normalizeBounds(diagnostic.bounds) ||
101
+ PcbDiagnosticFocusModel.#pointBounds(diagnostic.point)
102
+ if (!bounds) return null
103
+
104
+ return {
105
+ id,
106
+ point: PcbDiagnosticFocusModel.#roundPoint(
107
+ PcbDiagnosticFocusModel.#boundsCenter(bounds)
108
+ ),
109
+ bounds: PcbDiagnosticFocusModel.#viewportBounds(bounds),
110
+ relatedPrimitiveIds: relatedPrimitives
111
+ .map((primitive) => String(primitive.id || '').trim())
112
+ .filter(Boolean)
113
+ .sort()
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Resolves primitives related to one diagnostic.
119
+ * @param {object} diagnostic Diagnostic row.
120
+ * @param {object | null} element Source diagnostic element.
121
+ * @param {object} context Focus context.
122
+ * @returns {object[]}
123
+ */
124
+ static #relatedPrimitives(diagnostic, element, context) {
125
+ const ids = Array.isArray(diagnostic.relatedPrimitiveIds)
126
+ ? diagnostic.relatedPrimitiveIds
127
+ : []
128
+ const direct = ids
129
+ .map((id) => context.primitivesById.get(String(id || '').trim()))
130
+ .filter(Boolean)
131
+ if (direct.length) return direct
132
+
133
+ const fields = PcbDiagnosticFocusModel.#relatedFields(element)
134
+ const fieldPrimitives = (context.model.primitives || []).filter(
135
+ (primitive) =>
136
+ fields.some(([field, value]) =>
137
+ PcbDiagnosticFocusModel.#primitiveMatches(
138
+ primitive,
139
+ field,
140
+ value
141
+ )
142
+ )
143
+ )
144
+ if (fieldPrimitives.length) return fieldPrimitives
145
+
146
+ const sourcePortId = String(element?.source_port_id || '').trim()
147
+ const pcbPortIds =
148
+ context.sourcePortToPcbPortIds.get(sourcePortId) || []
149
+ const sourcePortPrimitives = (context.model.primitives || []).filter(
150
+ (primitive) =>
151
+ pcbPortIds.some(
152
+ (id) =>
153
+ String(primitive.source?.pcb_port_id || '').trim() ===
154
+ id
155
+ )
156
+ )
157
+ if (sourcePortPrimitives.length) return sourcePortPrimitives
158
+
159
+ const sourceComponentId = String(
160
+ element?.source_component_id || ''
161
+ ).trim()
162
+ if (!sourceComponentId) return []
163
+
164
+ return (context.model.primitives || []).filter(
165
+ (primitive) =>
166
+ String(primitive.sourceComponentId || '').trim() ===
167
+ sourceComponentId
168
+ )
169
+ }
170
+
171
+ /**
172
+ * Builds source element fields that can identify related primitives.
173
+ * @param {object | null} element Source diagnostic element.
174
+ * @returns {Array<[string, string]>}
175
+ */
176
+ static #relatedFields(element) {
177
+ return [
178
+ ['pcb_trace_id', [element?.pcb_trace_id, element?.pcb_trace_ids]],
179
+ [
180
+ 'pcb_smtpad_id',
181
+ [element?.pcb_smtpad_id, element?.pcb_smtpad_ids]
182
+ ],
183
+ ['pcb_via_id', [element?.pcb_via_id, element?.pcb_via_ids]],
184
+ [
185
+ 'pcb_plated_hole_id',
186
+ [element?.pcb_plated_hole_id, element?.pcb_plated_hole_ids]
187
+ ],
188
+ ['pcb_hole_id', [element?.pcb_hole_id, element?.pcb_hole_ids]],
189
+ ['pcb_port_id', [element?.pcb_port_id, element?.pcb_port_ids]],
190
+ [
191
+ 'pcb_component_id',
192
+ [element?.pcb_component_id, element?.pcb_component_ids]
193
+ ]
194
+ ].flatMap(([field, values]) =>
195
+ PcbDiagnosticFocusModel.#idValues(values).map((value) => [
196
+ field,
197
+ value
198
+ ])
199
+ )
200
+ }
201
+
202
+ /**
203
+ * Normalizes scalar or array ID values.
204
+ * @param {unknown[]} values Candidate values.
205
+ * @returns {string[]}
206
+ */
207
+ static #idValues(values) {
208
+ return [
209
+ ...new Set(
210
+ values
211
+ .flatMap((value) =>
212
+ Array.isArray(value) ? value : [value]
213
+ )
214
+ .map((value) => String(value || '').trim())
215
+ .filter(Boolean)
216
+ )
217
+ ]
218
+ }
219
+
220
+ /**
221
+ * Returns true when a primitive is associated with a source field.
222
+ * @param {object} primitive Primitive row.
223
+ * @param {string} field Source id field.
224
+ * @param {string} value Source id value.
225
+ * @returns {boolean}
226
+ */
227
+ static #primitiveMatches(primitive, field, value) {
228
+ if (field === 'pcb_trace_id') {
229
+ return String(primitive.source?.pcb_trace_id || '').trim() === value
230
+ }
231
+ if (field === 'pcb_component_id') {
232
+ return String(primitive.componentId || '').trim() === value
233
+ }
234
+ return String(primitive.source?.[field] || '').trim() === value
235
+ }
236
+
237
+ /**
238
+ * Builds source-port to PCB-port id lookup data.
239
+ * @param {object[]} elements Element rows.
240
+ * @returns {Map<string, string[]>}
241
+ */
242
+ static #sourcePortToPcbPortIds(elements) {
243
+ const map = new Map()
244
+ for (const element of elements) {
245
+ if (element?.type !== 'pcb_port') continue
246
+ const sourcePortId = String(element.source_port_id || '').trim()
247
+ const pcbPortId = String(element.pcb_port_id || '').trim()
248
+ if (!sourcePortId || !pcbPortId) continue
249
+ if (!map.has(sourcePortId)) map.set(sourcePortId, [])
250
+ map.get(sourcePortId).push(pcbPortId)
251
+ }
252
+ return map
253
+ }
254
+
255
+ /**
256
+ * Resolves component bounds when no primitive bounds are available.
257
+ * @param {object | null} element Source diagnostic element.
258
+ * @param {object} model Primitive model.
259
+ * @returns {object | null}
260
+ */
261
+ static #componentBounds(element, model) {
262
+ const componentId = String(element?.pcb_component_id || '').trim()
263
+ if (!componentId) return null
264
+
265
+ const component = (model.components || []).find(
266
+ (row) => String(row.pcbComponentId || '').trim() === componentId
267
+ )
268
+ if (!component) return null
269
+
270
+ return PcbDiagnosticFocusModel.#centerBounds(
271
+ { x: component.x, y: component.y },
272
+ CircuitJsonUnits.length(component.width, 0.8),
273
+ CircuitJsonUnits.length(component.height, 0.8)
274
+ )
275
+ }
276
+
277
+ /**
278
+ * Reads element rows from an array or wrapper object.
279
+ * @param {object | object[]} documentModel Parsed document.
280
+ * @returns {object[]}
281
+ */
282
+ static #elements(documentModel) {
283
+ if (Array.isArray(documentModel)) return documentModel
284
+ if (Array.isArray(documentModel?.elements))
285
+ return documentModel.elements
286
+ if (Array.isArray(documentModel?.circuitJson)) {
287
+ return documentModel.circuitJson
288
+ }
289
+ return []
290
+ }
291
+
292
+ /**
293
+ * Normalizes min/max bounds.
294
+ * @param {object | null | undefined} bounds Bounds candidate.
295
+ * @returns {object | null}
296
+ */
297
+ static #normalizeBounds(bounds) {
298
+ if (!bounds) return null
299
+ const minX = Number(bounds.minX ?? bounds.x)
300
+ const minY = Number(bounds.minY ?? bounds.y)
301
+ const width = Number(bounds.width)
302
+ const height = Number(bounds.height)
303
+ const maxX = Number(bounds.maxX ?? minX + width)
304
+ const maxY = Number(bounds.maxY ?? minY + height)
305
+ if (
306
+ ![minX, minY, maxX, maxY].every((value) => Number.isFinite(value))
307
+ ) {
308
+ return null
309
+ }
310
+ return {
311
+ minX,
312
+ minY,
313
+ maxX,
314
+ maxY,
315
+ width: maxX - minX,
316
+ height: maxY - minY
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Builds compact bounds around a diagnostic point.
322
+ * @param {object | null | undefined} point Point candidate.
323
+ * @returns {object | null}
324
+ */
325
+ static #pointBounds(point) {
326
+ const normalized = CircuitJsonUnits.optionalPoint(point)
327
+ return normalized
328
+ ? PcbDiagnosticFocusModel.#centerBounds(normalized, 0.8, 0.8)
329
+ : null
330
+ }
331
+
332
+ /**
333
+ * Builds center-size bounds.
334
+ * @param {{ x: number, y: number }} center Center point.
335
+ * @param {number} width Width.
336
+ * @param {number} height Height.
337
+ * @returns {object}
338
+ */
339
+ static #centerBounds(center, width, height) {
340
+ const minX = Number(center.x) - width / 2
341
+ const minY = Number(center.y) - height / 2
342
+ return {
343
+ minX,
344
+ minY,
345
+ maxX: minX + width,
346
+ maxY: minY + height,
347
+ width,
348
+ height
349
+ }
350
+ }
351
+
352
+ /**
353
+ * Merges min/max bounds rows.
354
+ * @param {object[]} boundsRows Bounds rows.
355
+ * @returns {object | null}
356
+ */
357
+ static #mergeBounds(boundsRows) {
358
+ const rows = boundsRows
359
+ .map((bounds) => PcbDiagnosticFocusModel.#normalizeBounds(bounds))
360
+ .filter(Boolean)
361
+ if (!rows.length) return null
362
+
363
+ const minX = Math.min(...rows.map((bounds) => bounds.minX))
364
+ const minY = Math.min(...rows.map((bounds) => bounds.minY))
365
+ const maxX = Math.max(...rows.map((bounds) => bounds.maxX))
366
+ const maxY = Math.max(...rows.map((bounds) => bounds.maxY))
367
+ return {
368
+ minX,
369
+ minY,
370
+ maxX,
371
+ maxY,
372
+ width: maxX - minX,
373
+ height: maxY - minY
374
+ }
375
+ }
376
+
377
+ /**
378
+ * Resolves a bounds center.
379
+ * @param {object} bounds Bounds row.
380
+ * @returns {{ x: number, y: number }}
381
+ */
382
+ static #boundsCenter(bounds) {
383
+ return {
384
+ x: bounds.minX + bounds.width / 2,
385
+ y: bounds.minY + bounds.height / 2
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Formats bounds for viewport controllers.
391
+ * @param {object} bounds Bounds row.
392
+ * @returns {{ x: number, y: number, width: number, height: number }}
393
+ */
394
+ static #viewportBounds(bounds) {
395
+ return {
396
+ x: PcbDiagnosticFocusModel.#rounded(bounds.minX),
397
+ y: PcbDiagnosticFocusModel.#rounded(bounds.minY),
398
+ width: PcbDiagnosticFocusModel.#rounded(bounds.width),
399
+ height: PcbDiagnosticFocusModel.#rounded(bounds.height)
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Rounds one point.
405
+ * @param {{ x: number, y: number }} point Point row.
406
+ * @returns {{ x: number, y: number }}
407
+ */
408
+ static #roundPoint(point) {
409
+ return {
410
+ x: PcbDiagnosticFocusModel.#rounded(point.x),
411
+ y: PcbDiagnosticFocusModel.#rounded(point.y)
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Rounds a numeric value for stable comparisons.
417
+ * @param {number} value Numeric value.
418
+ * @returns {number}
419
+ */
420
+ static #rounded(value) {
421
+ return Number(Number(value).toFixed(6))
422
+ }
423
+ }