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,439 @@
1
+ import { CircuitJsonUnits } from '../core/CircuitJsonUnits.mjs'
2
+
3
+ /**
4
+ * Renders schematic table frames and cells from element-array metadata.
5
+ */
6
+ export class CircuitJsonSchematicTableSvgRenderer {
7
+ /**
8
+ * Renders all schematic table markup.
9
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
10
+ * @param {{ escapeHtml: (value: unknown) => string, formatNumber: (value: number) => string }} formatters Formatting helpers.
11
+ * @returns {string}
12
+ */
13
+ static render(index, formatters) {
14
+ const tables = CircuitJsonSchematicTableSvgRenderer.#tableModels(index)
15
+ const tableMarkup = [...tables.values()]
16
+ .map((table) =>
17
+ CircuitJsonSchematicTableSvgRenderer.#tableElement(
18
+ table,
19
+ formatters
20
+ )
21
+ )
22
+ .filter(Boolean)
23
+ const cellMarkup = CircuitJsonSchematicTableSvgRenderer.#all(
24
+ index,
25
+ 'schematic_table_cell'
26
+ )
27
+ .map((cell) =>
28
+ CircuitJsonSchematicTableSvgRenderer.#tableCellElement(
29
+ cell,
30
+ tables.get(String(cell.schematic_table_id || '').trim()),
31
+ formatters
32
+ )
33
+ )
34
+ .filter(Boolean)
35
+ const markup = [...tableMarkup, ...cellMarkup]
36
+ return markup.length
37
+ ? '<g class="schematic-tables">' + markup.join('') + '</g>'
38
+ : ''
39
+ }
40
+
41
+ /**
42
+ * Builds table models keyed by table id.
43
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
44
+ * @returns {Map<string, object>}
45
+ */
46
+ static #tableModels(index) {
47
+ const tables = new Map()
48
+ for (const table of CircuitJsonSchematicTableSvgRenderer.#all(
49
+ index,
50
+ 'schematic_table'
51
+ )) {
52
+ const model =
53
+ CircuitJsonSchematicTableSvgRenderer.#tableModel(table)
54
+ if (model) tables.set(model.id, model)
55
+ }
56
+ return tables
57
+ }
58
+
59
+ /**
60
+ * Builds one schematic table model.
61
+ * @param {object} element Table element.
62
+ * @returns {object | null}
63
+ */
64
+ static #tableModel(element) {
65
+ const id = String(element.schematic_table_id || '').trim()
66
+ const columns = CircuitJsonSchematicTableSvgRenderer.#lengths(
67
+ element.column_widths || element.columnWidths || element.columns
68
+ )
69
+ const rows = CircuitJsonSchematicTableSvgRenderer.#lengths(
70
+ element.row_heights || element.rowHeights || element.rows
71
+ )
72
+ const gridWidth = CircuitJsonSchematicTableSvgRenderer.#sum(columns)
73
+ const gridHeight = CircuitJsonSchematicTableSvgRenderer.#sum(rows)
74
+ const rect = CircuitJsonSchematicTableSvgRenderer.#rect(
75
+ element,
76
+ gridWidth,
77
+ gridHeight
78
+ )
79
+ if (!id || !rect) return null
80
+
81
+ return {
82
+ id,
83
+ element,
84
+ ...rect,
85
+ columns,
86
+ rows,
87
+ columnOffsets:
88
+ CircuitJsonSchematicTableSvgRenderer.#offsets(columns),
89
+ rowOffsets: CircuitJsonSchematicTableSvgRenderer.#offsets(rows),
90
+ padding: CircuitJsonUnits.length(element.cell_padding, 0),
91
+ borderWidth:
92
+ CircuitJsonUnits.optionalLength(element.border_width) ??
93
+ CircuitJsonUnits.optionalLength(element.borderWidth)
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Renders one table frame.
99
+ * @param {object} table Table model.
100
+ * @param {{ escapeHtml: (value: unknown) => string, formatNumber: (value: number) => string }} formatters Formatting helpers.
101
+ * @returns {string}
102
+ */
103
+ static #tableElement(table, formatters) {
104
+ return (
105
+ '<rect class="schematic-table" data-schematic-table-id="' +
106
+ formatters.escapeHtml(table.id) +
107
+ '" ' +
108
+ CircuitJsonSchematicTableSvgRenderer.#rectAttributes(
109
+ table,
110
+ table.borderWidth,
111
+ formatters
112
+ ) +
113
+ '></rect>'
114
+ )
115
+ }
116
+
117
+ /**
118
+ * Renders one table cell.
119
+ * @param {object} element Cell element.
120
+ * @param {object | undefined} table Parent table model.
121
+ * @param {{ escapeHtml: (value: unknown) => string, formatNumber: (value: number) => string }} formatters Formatting helpers.
122
+ * @returns {string}
123
+ */
124
+ static #tableCellElement(element, table, formatters) {
125
+ const rect = CircuitJsonSchematicTableSvgRenderer.#cellRect(
126
+ element,
127
+ table
128
+ )
129
+ if (!rect) return ''
130
+ const text = CircuitJsonSchematicTableSvgRenderer.#textPosition(
131
+ element,
132
+ table,
133
+ rect
134
+ )
135
+ const fontSize = CircuitJsonUnits.optionalLength(element.font_size)
136
+
137
+ return (
138
+ '<g class="schematic-table-cell" data-schematic-table-id="' +
139
+ formatters.escapeHtml(element.schematic_table_id || '') +
140
+ '" data-schematic-table-cell-id="' +
141
+ formatters.escapeHtml(element.schematic_table_cell_id || '') +
142
+ '"><rect ' +
143
+ CircuitJsonSchematicTableSvgRenderer.#rectAttributes(
144
+ rect,
145
+ table?.borderWidth,
146
+ formatters
147
+ ) +
148
+ '></rect><text x="' +
149
+ formatters.formatNumber(text.x) +
150
+ '" y="' +
151
+ formatters.formatNumber(text.y) +
152
+ '" text-anchor="' +
153
+ text.anchor +
154
+ '" dominant-baseline="' +
155
+ text.baseline +
156
+ '"' +
157
+ CircuitJsonSchematicTableSvgRenderer.#fontSizeAttribute(
158
+ fontSize,
159
+ formatters
160
+ ) +
161
+ '>' +
162
+ formatters.escapeHtml(element.text || '') +
163
+ '</text></g>'
164
+ )
165
+ }
166
+
167
+ /**
168
+ * Resolves one cell rectangle.
169
+ * @param {object} element Cell element.
170
+ * @param {object | undefined} table Parent table model.
171
+ * @returns {object | null}
172
+ */
173
+ static #cellRect(element, table) {
174
+ if (table?.columns?.length && table?.rows?.length) {
175
+ return CircuitJsonSchematicTableSvgRenderer.#gridCellRect(
176
+ element,
177
+ table
178
+ )
179
+ }
180
+ return CircuitJsonSchematicTableSvgRenderer.#rect(element)
181
+ }
182
+
183
+ /**
184
+ * Resolves one grid cell rectangle.
185
+ * @param {object} element Cell element.
186
+ * @param {object} table Parent table model.
187
+ * @returns {object | null}
188
+ */
189
+ static #gridCellRect(element, table) {
190
+ const row = CircuitJsonSchematicTableSvgRenderer.#index(
191
+ element.row ?? element.row_index ?? element.rowIndex
192
+ )
193
+ const column = CircuitJsonSchematicTableSvgRenderer.#index(
194
+ element.column ??
195
+ element.col ??
196
+ element.column_index ??
197
+ element.columnIndex
198
+ )
199
+ if (row === null || column === null) return null
200
+ const rowSpan = CircuitJsonSchematicTableSvgRenderer.#span(
201
+ element.row_span ?? element.rowSpan
202
+ )
203
+ const columnSpan = CircuitJsonSchematicTableSvgRenderer.#span(
204
+ element.col_span ?? element.column_span ?? element.colSpan
205
+ )
206
+ const width = CircuitJsonSchematicTableSvgRenderer.#sum(
207
+ table.columns.slice(column, column + columnSpan)
208
+ )
209
+ const height = CircuitJsonSchematicTableSvgRenderer.#sum(
210
+ table.rows.slice(row, row + rowSpan)
211
+ )
212
+ if (width <= 0 || height <= 0) return null
213
+
214
+ return {
215
+ x: table.x + (table.columnOffsets[column] || 0),
216
+ y: table.y + (table.rowOffsets[row] || 0),
217
+ width,
218
+ height
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Resolves one text position inside a cell.
224
+ * @param {object} element Cell element.
225
+ * @param {object | undefined} table Parent table model.
226
+ * @param {object} rect Cell rectangle.
227
+ * @returns {{ x: number, y: number, anchor: string, baseline: string }}
228
+ */
229
+ static #textPosition(element, table, rect) {
230
+ const padding = Number(table?.padding || 0)
231
+ const horizontal = String(
232
+ element.horizontal_align || element.horizontalAlign || ''
233
+ ).toLowerCase()
234
+ const vertical = String(
235
+ element.vertical_align || element.verticalAlign || ''
236
+ ).toLowerCase()
237
+ const x =
238
+ horizontal === 'left' || horizontal === 'start'
239
+ ? rect.x + padding
240
+ : horizontal === 'right' || horizontal === 'end'
241
+ ? rect.x + rect.width - padding
242
+ : rect.x + rect.width / 2
243
+ const y =
244
+ vertical === 'top'
245
+ ? rect.y + padding
246
+ : vertical === 'bottom'
247
+ ? rect.y + rect.height - padding
248
+ : rect.y + rect.height / 2
249
+ return {
250
+ x,
251
+ y,
252
+ anchor:
253
+ horizontal === 'left' || horizontal === 'start'
254
+ ? 'start'
255
+ : horizontal === 'right' || horizontal === 'end'
256
+ ? 'end'
257
+ : 'middle',
258
+ baseline:
259
+ vertical === 'top'
260
+ ? 'hanging'
261
+ : vertical === 'bottom'
262
+ ? 'text-after-edge'
263
+ : 'central'
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Builds SVG rect attributes.
269
+ * @param {object} rect Rect model.
270
+ * @param {number | null | undefined} borderWidth Border width.
271
+ * @param {{ formatNumber: (value: number) => string }} formatters Formatting helpers.
272
+ * @returns {string}
273
+ */
274
+ static #rectAttributes(rect, borderWidth, formatters) {
275
+ const attributes = [
276
+ ['x', rect.x],
277
+ ['y', rect.y],
278
+ ['width', rect.width],
279
+ ['height', rect.height]
280
+ ].map(
281
+ ([name, value]) =>
282
+ name + '="' + formatters.formatNumber(value) + '"'
283
+ )
284
+ if (borderWidth !== null && borderWidth !== undefined) {
285
+ attributes.push(
286
+ 'stroke-width="' + formatters.formatNumber(borderWidth) + '"'
287
+ )
288
+ }
289
+ return attributes.join(' ')
290
+ }
291
+
292
+ /**
293
+ * Builds an optional font-size attribute.
294
+ * @param {number | null} fontSize Font size.
295
+ * @param {{ formatNumber: (value: number) => string }} formatters Formatting helpers.
296
+ * @returns {string}
297
+ */
298
+ static #fontSizeAttribute(fontSize, formatters) {
299
+ return fontSize === null
300
+ ? ''
301
+ : ' font-size="' + formatters.formatNumber(fontSize) + '"'
302
+ }
303
+
304
+ /**
305
+ * Resolves a rectangle from center/size or anchor metadata.
306
+ * @param {object} element Element row.
307
+ * @param {number} [fallbackWidth] Fallback width.
308
+ * @param {number} [fallbackHeight] Fallback height.
309
+ * @returns {object | null}
310
+ */
311
+ static #rect(element, fallbackWidth = 0, fallbackHeight = 0) {
312
+ const size = CircuitJsonSchematicTableSvgRenderer.#size(
313
+ element,
314
+ fallbackWidth,
315
+ fallbackHeight
316
+ )
317
+ const anchor =
318
+ CircuitJsonUnits.optionalPoint(element.anchor_position) || null
319
+ if (size && anchor) {
320
+ return CircuitJsonSchematicTableSvgRenderer.#anchoredRect(
321
+ anchor,
322
+ size,
323
+ element.anchor
324
+ )
325
+ }
326
+ const center = CircuitJsonUnits.optionalPoint(element.center || element)
327
+ if (!center || !size) return null
328
+ return {
329
+ x: center.x - size.width / 2,
330
+ y: center.y - size.height / 2,
331
+ ...size
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Resolves a width/height pair.
337
+ * @param {object} element Element row.
338
+ * @param {number} fallbackWidth Fallback width.
339
+ * @param {number} fallbackHeight Fallback height.
340
+ * @returns {{ width: number, height: number } | null}
341
+ */
342
+ static #size(element, fallbackWidth, fallbackHeight) {
343
+ const size = CircuitJsonUnits.optionalSize(element.size || element)
344
+ const width = size?.width ?? fallbackWidth
345
+ const height = size?.height ?? fallbackHeight
346
+ if (width <= 0 || height <= 0) return null
347
+ return { width, height }
348
+ }
349
+
350
+ /**
351
+ * Builds an anchored rectangle.
352
+ * @param {{ x: number, y: number }} point Anchor point.
353
+ * @param {{ width: number, height: number }} size Size.
354
+ * @param {unknown} anchor Anchor name.
355
+ * @returns {object}
356
+ */
357
+ static #anchoredRect(point, size, anchor) {
358
+ const text = String(anchor || 'top_left').toLowerCase()
359
+ const x = text.includes('right')
360
+ ? point.x - size.width
361
+ : text.includes('center')
362
+ ? point.x - size.width / 2
363
+ : point.x
364
+ const y = text.includes('bottom')
365
+ ? point.y - size.height
366
+ : text.includes('middle') || text === 'center'
367
+ ? point.y - size.height / 2
368
+ : point.y
369
+ return { x, y, ...size }
370
+ }
371
+
372
+ /**
373
+ * Resolves an array of positive lengths.
374
+ * @param {unknown} value Length array candidate.
375
+ * @returns {number[]}
376
+ */
377
+ static #lengths(value) {
378
+ return (Array.isArray(value) ? value : [])
379
+ .map((entry) =>
380
+ CircuitJsonUnits.optionalLength(
381
+ entry?.width ?? entry?.height ?? entry
382
+ )
383
+ )
384
+ .filter((entry) => entry !== null && entry > 0)
385
+ }
386
+
387
+ /**
388
+ * Builds cumulative offsets for a length list.
389
+ * @param {number[]} values Length values.
390
+ * @returns {number[]}
391
+ */
392
+ static #offsets(values) {
393
+ let total = 0
394
+ return values.map((value) => {
395
+ const offset = total
396
+ total += value
397
+ return offset
398
+ })
399
+ }
400
+
401
+ /**
402
+ * Sums numeric values.
403
+ * @param {number[]} values Values.
404
+ * @returns {number}
405
+ */
406
+ static #sum(values) {
407
+ return values.reduce((total, value) => total + value, 0)
408
+ }
409
+
410
+ /**
411
+ * Resolves a zero-based row or column index.
412
+ * @param {unknown} value Index candidate.
413
+ * @returns {number | null}
414
+ */
415
+ static #index(value) {
416
+ const number = Number(value)
417
+ return Number.isInteger(number) && number >= 0 ? number : null
418
+ }
419
+
420
+ /**
421
+ * Resolves a positive row or column span.
422
+ * @param {unknown} value Span candidate.
423
+ * @returns {number}
424
+ */
425
+ static #span(value) {
426
+ const number = Number(value)
427
+ return Number.isInteger(number) && number > 0 ? number : 1
428
+ }
429
+
430
+ /**
431
+ * Returns indexed element rows.
432
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
433
+ * @param {string} type Element type.
434
+ * @returns {object[]}
435
+ */
436
+ static #all(index, type) {
437
+ return index.elementsByType.get(type) || []
438
+ }
439
+ }