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,994 @@
1
+ import { CircuitJsonIndexer } from '../core/CircuitJsonIndexer.mjs'
2
+ import { CircuitJsonUnits } from '../core/CircuitJsonUnits.mjs'
3
+ import { CircuitJsonSchematicSvgArcPath } from './CircuitJsonSchematicSvgArcPath.mjs'
4
+ import { CircuitJsonSchematicSvgPortMetadata } from './CircuitJsonSchematicSvgPortMetadata.mjs'
5
+ import { CircuitJsonSchematicSvgPrimitiveAttributes } from './CircuitJsonSchematicSvgPrimitiveAttributes.mjs'
6
+ import { CircuitJsonSchematicTableSvgRenderer } from './CircuitJsonSchematicTableSvgRenderer.mjs'
7
+
8
+ /**
9
+ * Renders standards-shaped schematic element arrays into app-compatible SVG.
10
+ */
11
+ export class CircuitJsonSchematicSvgRenderer {
12
+ /**
13
+ * Renders one schematic document into SVG markup.
14
+ * @param {object | object[]} documentModel Parsed document model.
15
+ * @returns {string}
16
+ */
17
+ static render(documentModel) {
18
+ const elements =
19
+ CircuitJsonSchematicSvgRenderer.#elements(documentModel)
20
+ const index = CircuitJsonIndexer.index(elements)
21
+ const bounds = CircuitJsonSchematicSvgRenderer.#bounds(index)
22
+ const viewBox = CircuitJsonSchematicSvgRenderer.#viewBox(bounds)
23
+
24
+ return (
25
+ '<svg class="schematic-svg schematic-svg--circuitjson" xmlns="http://www.w3.org/2000/svg" role="img" viewBox="' +
26
+ CircuitJsonSchematicSvgRenderer.#formatViewBox(viewBox) +
27
+ '">' +
28
+ CircuitJsonSchematicSvgRenderer.#renderSheet(bounds) +
29
+ CircuitJsonSchematicSvgRenderer.#renderComponents(index) +
30
+ CircuitJsonSchematicSvgRenderer.#renderSymbols(index) +
31
+ CircuitJsonSchematicSvgRenderer.#renderGroups(index) +
32
+ CircuitJsonSchematicSvgRenderer.#renderLines(index) +
33
+ CircuitJsonSchematicSvgRenderer.#renderShapes(index) +
34
+ CircuitJsonSchematicSvgRenderer.#renderPorts(index) +
35
+ CircuitJsonSchematicSvgRenderer.#renderTables(index) +
36
+ CircuitJsonSchematicSvgRenderer.#renderTexts(index) +
37
+ CircuitJsonSchematicSvgRenderer.#renderProbes(index) +
38
+ CircuitJsonSchematicSvgRenderer.#renderDebugObjects(index) +
39
+ CircuitJsonSchematicSvgRenderer.#renderDiagnostics(index) +
40
+ '</svg>'
41
+ )
42
+ }
43
+
44
+ /**
45
+ * Returns element rows from an array or wrapper object.
46
+ * @param {object | object[]} documentModel Parsed document model.
47
+ * @returns {object[]}
48
+ */
49
+ static #elements(documentModel) {
50
+ if (Array.isArray(documentModel)) return documentModel
51
+ if (Array.isArray(documentModel?.elements))
52
+ return documentModel.elements
53
+ if (Array.isArray(documentModel?.circuitJson)) {
54
+ return documentModel.circuitJson
55
+ }
56
+ return []
57
+ }
58
+
59
+ /**
60
+ * Renders the schematic sheet backdrop.
61
+ * @param {object} bounds Sheet bounds.
62
+ * @returns {string}
63
+ */
64
+ static #renderSheet(bounds) {
65
+ return (
66
+ '<rect class="sheet-backdrop schematic-sheet" x="' +
67
+ CircuitJsonSchematicSvgRenderer.#formatNumber(bounds.minX) +
68
+ '" y="' +
69
+ CircuitJsonSchematicSvgRenderer.#formatNumber(bounds.minY) +
70
+ '" width="' +
71
+ CircuitJsonSchematicSvgRenderer.#formatNumber(bounds.width) +
72
+ '" height="' +
73
+ CircuitJsonSchematicSvgRenderer.#formatNumber(bounds.height) +
74
+ '"></rect>'
75
+ )
76
+ }
77
+
78
+ /**
79
+ * Renders schematic component boxes.
80
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
81
+ * @returns {string}
82
+ */
83
+ static #renderComponents(index) {
84
+ const sourceNames = CircuitJsonSchematicSvgRenderer.#sourceNames(index)
85
+ const components = CircuitJsonSchematicSvgRenderer.#all(
86
+ index,
87
+ 'schematic_component'
88
+ ).map((component, componentIndex) => {
89
+ const center = CircuitJsonSchematicSvgRenderer.#center(
90
+ component
91
+ ) || {
92
+ x: 0,
93
+ y: 0
94
+ }
95
+ const size = CircuitJsonUnits.optionalSize(component.size) || {
96
+ width: 4,
97
+ height: 3
98
+ }
99
+ const key =
100
+ sourceNames.get(String(component.source_component_id || '')) ||
101
+ String(component.name || 'U' + (componentIndex + 1))
102
+ const x = center.x - size.width / 2
103
+ const y = center.y - size.height / 2
104
+
105
+ return (
106
+ '<g class="schematic-component" data-component-key="' +
107
+ CircuitJsonSchematicSvgRenderer.#escapeHtml(key) +
108
+ '" data-schematic-component-id="' +
109
+ CircuitJsonSchematicSvgRenderer.#escapeHtml(
110
+ component.schematic_component_id || ''
111
+ ) +
112
+ '">' +
113
+ '<rect class="schematic-component__body" x="' +
114
+ CircuitJsonSchematicSvgRenderer.#formatNumber(x) +
115
+ '" y="' +
116
+ CircuitJsonSchematicSvgRenderer.#formatNumber(y) +
117
+ '" width="' +
118
+ CircuitJsonSchematicSvgRenderer.#formatNumber(size.width) +
119
+ '" height="' +
120
+ CircuitJsonSchematicSvgRenderer.#formatNumber(size.height) +
121
+ '"></rect><text class="schematic-component__label" x="' +
122
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.x) +
123
+ '" y="' +
124
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.y) +
125
+ '" text-anchor="middle" dominant-baseline="central">' +
126
+ CircuitJsonSchematicSvgRenderer.#escapeHtml(key) +
127
+ '</text></g>'
128
+ )
129
+ })
130
+
131
+ return components.length
132
+ ? '<g class="schematic-components">' + components.join('') + '</g>'
133
+ : ''
134
+ }
135
+
136
+ /**
137
+ * Renders reusable schematic symbol rows.
138
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
139
+ * @returns {string}
140
+ */
141
+ static #renderSymbols(index) {
142
+ const sourceNames = CircuitJsonSchematicSvgRenderer.#sourceNames(index)
143
+ const symbols = CircuitJsonSchematicSvgRenderer.#all(
144
+ index,
145
+ 'schematic_symbol'
146
+ )
147
+ .map((symbol, symbolIndex) =>
148
+ CircuitJsonSchematicSvgRenderer.#symbolElement(
149
+ symbol,
150
+ symbolIndex,
151
+ sourceNames
152
+ )
153
+ )
154
+ .filter(Boolean)
155
+ return symbols.length
156
+ ? '<g class="schematic-symbols">' + symbols.join('') + '</g>'
157
+ : ''
158
+ }
159
+
160
+ /**
161
+ * Renders one schematic symbol.
162
+ * @param {object} symbol Symbol row.
163
+ * @param {number} symbolIndex Symbol index.
164
+ * @param {Map<string, string>} sourceNames Source display names.
165
+ * @returns {string}
166
+ */
167
+ static #symbolElement(symbol, symbolIndex, sourceNames) {
168
+ const center = CircuitJsonSchematicSvgRenderer.#center(symbol)
169
+ const size = CircuitJsonSchematicSvgRenderer.#size(symbol)
170
+ if (!center || !size) return ''
171
+ const key =
172
+ sourceNames.get(String(symbol.source_component_id || '')) ||
173
+ String(symbol.name || 'SYM' + (symbolIndex + 1))
174
+ const x = center.x - size.width / 2
175
+ const y = center.y - size.height / 2
176
+
177
+ return (
178
+ '<g class="schematic-symbol" data-component-key="' +
179
+ CircuitJsonSchematicSvgRenderer.#escapeHtml(key) +
180
+ '" data-schematic-symbol-id="' +
181
+ CircuitJsonSchematicSvgRenderer.#escapeHtml(
182
+ symbol.schematic_symbol_id || ''
183
+ ) +
184
+ '"><rect class="schematic-symbol__body" x="' +
185
+ CircuitJsonSchematicSvgRenderer.#formatNumber(x) +
186
+ '" y="' +
187
+ CircuitJsonSchematicSvgRenderer.#formatNumber(y) +
188
+ '" width="' +
189
+ CircuitJsonSchematicSvgRenderer.#formatNumber(size.width) +
190
+ '" height="' +
191
+ CircuitJsonSchematicSvgRenderer.#formatNumber(size.height) +
192
+ '"></rect><text class="schematic-symbol__label" x="' +
193
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.x) +
194
+ '" y="' +
195
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.y) +
196
+ '" text-anchor="middle" dominant-baseline="central">' +
197
+ CircuitJsonSchematicSvgRenderer.#escapeHtml(key) +
198
+ '</text></g>'
199
+ )
200
+ }
201
+
202
+ /**
203
+ * Renders schematic grouping bounds.
204
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
205
+ * @returns {string}
206
+ */
207
+ static #renderGroups(index) {
208
+ const groups = CircuitJsonSchematicSvgRenderer.#all(
209
+ index,
210
+ 'schematic_group'
211
+ )
212
+ .map((element) =>
213
+ CircuitJsonSchematicSvgRenderer.#groupElement(element)
214
+ )
215
+ .filter(Boolean)
216
+ return groups.length
217
+ ? '<g class="schematic-groups">' + groups.join('') + '</g>'
218
+ : ''
219
+ }
220
+
221
+ /**
222
+ * Renders one schematic group.
223
+ * @param {object} element Group element.
224
+ * @returns {string}
225
+ */
226
+ static #groupElement(element) {
227
+ const rect = CircuitJsonSchematicSvgRenderer.#rectAttributes(element)
228
+ const center = CircuitJsonSchematicSvgRenderer.#center(element)
229
+ if (!rect || !center) return ''
230
+ const label = String(element.name || element.schematic_group_id || '')
231
+
232
+ return (
233
+ '<g class="schematic-group" data-schematic-group-id="' +
234
+ CircuitJsonSchematicSvgRenderer.#escapeHtml(
235
+ element.schematic_group_id || ''
236
+ ) +
237
+ '"><rect class="schematic-group__bounds" ' +
238
+ rect +
239
+ '></rect><text class="schematic-group__label" x="' +
240
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.x) +
241
+ '" y="' +
242
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.y) +
243
+ '" text-anchor="middle" dominant-baseline="central">' +
244
+ CircuitJsonSchematicSvgRenderer.#escapeHtml(label) +
245
+ '</text></g>'
246
+ )
247
+ }
248
+
249
+ /**
250
+ * Renders schematic line and trace elements.
251
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
252
+ * @returns {string}
253
+ */
254
+ static #renderLines(index) {
255
+ const lines = ['schematic_line', 'schematic_trace']
256
+ .flatMap((type) =>
257
+ CircuitJsonSchematicSvgRenderer.#all(index, type)
258
+ )
259
+ .map((element) =>
260
+ CircuitJsonSchematicSvgRenderer.#lineElement(element)
261
+ )
262
+ .filter(Boolean)
263
+ return lines.length
264
+ ? '<g class="schematic-wires">' + lines.join('') + '</g>'
265
+ : ''
266
+ }
267
+
268
+ /**
269
+ * Renders one schematic line.
270
+ * @param {object} element Source element.
271
+ * @returns {string}
272
+ */
273
+ static #lineElement(element) {
274
+ const start = CircuitJsonSchematicSvgRenderer.#point({
275
+ x: element.x1 ?? element.start?.x,
276
+ y: element.y1 ?? element.start?.y
277
+ })
278
+ const end = CircuitJsonSchematicSvgRenderer.#point({
279
+ x: element.x2 ?? element.end?.x,
280
+ y: element.y2 ?? element.end?.y
281
+ })
282
+ if (!start || !end) return ''
283
+ return (
284
+ '<line class="schematic-wire" x1="' +
285
+ CircuitJsonSchematicSvgRenderer.#formatNumber(start.x) +
286
+ '" y1="' +
287
+ CircuitJsonSchematicSvgRenderer.#formatNumber(start.y) +
288
+ '" x2="' +
289
+ CircuitJsonSchematicSvgRenderer.#formatNumber(end.x) +
290
+ '" y2="' +
291
+ CircuitJsonSchematicSvgRenderer.#formatNumber(end.y) +
292
+ '"' +
293
+ CircuitJsonSchematicSvgPrimitiveAttributes.attributes(element, {
294
+ fill: false
295
+ }) +
296
+ '></line>'
297
+ )
298
+ }
299
+
300
+ /**
301
+ * Renders schematic rectangle and circle shapes.
302
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
303
+ * @returns {string}
304
+ */
305
+ static #renderShapes(index) {
306
+ const rects = ['schematic_rect', 'schematic_box']
307
+ .flatMap((type) =>
308
+ CircuitJsonSchematicSvgRenderer.#all(index, type)
309
+ )
310
+ .map((element) =>
311
+ CircuitJsonSchematicSvgRenderer.#rectElement(element)
312
+ )
313
+ .filter(Boolean)
314
+ const circles = CircuitJsonSchematicSvgRenderer.#all(
315
+ index,
316
+ 'schematic_circle'
317
+ )
318
+ .map((element) =>
319
+ CircuitJsonSchematicSvgRenderer.#circleElement(element)
320
+ )
321
+ .filter(Boolean)
322
+ const arcs = CircuitJsonSchematicSvgRenderer.#all(
323
+ index,
324
+ 'schematic_arc'
325
+ )
326
+ .map((element) =>
327
+ CircuitJsonSchematicSvgRenderer.#arcElement(element)
328
+ )
329
+ .filter(Boolean)
330
+ const paths = CircuitJsonSchematicSvgRenderer.#all(
331
+ index,
332
+ 'schematic_path'
333
+ )
334
+ .map((element) =>
335
+ CircuitJsonSchematicSvgRenderer.#pathElement(element)
336
+ )
337
+ .filter(Boolean)
338
+ const markup = [...rects, ...circles, ...arcs, ...paths]
339
+ return markup.length
340
+ ? '<g class="schematic-shapes">' + markup.join('') + '</g>'
341
+ : ''
342
+ }
343
+
344
+ /**
345
+ * Renders one schematic rectangle.
346
+ * @param {object} element Rectangle element.
347
+ * @returns {string}
348
+ */
349
+ static #rectElement(element) {
350
+ const center = CircuitJsonSchematicSvgRenderer.#center(element)
351
+ const size = CircuitJsonUnits.optionalSize(element.size || element)
352
+ if (!center || !size) return ''
353
+ return (
354
+ '<rect class="schematic-shape schematic-shape--rect" x="' +
355
+ CircuitJsonSchematicSvgRenderer.#formatNumber(
356
+ center.x - size.width / 2
357
+ ) +
358
+ '" y="' +
359
+ CircuitJsonSchematicSvgRenderer.#formatNumber(
360
+ center.y - size.height / 2
361
+ ) +
362
+ '" width="' +
363
+ CircuitJsonSchematicSvgRenderer.#formatNumber(size.width) +
364
+ '" height="' +
365
+ CircuitJsonSchematicSvgRenderer.#formatNumber(size.height) +
366
+ '"' +
367
+ CircuitJsonSchematicSvgPrimitiveAttributes.attributes(element) +
368
+ '></rect>'
369
+ )
370
+ }
371
+
372
+ /**
373
+ * Renders one schematic circle.
374
+ * @param {object} element Circle element.
375
+ * @returns {string}
376
+ */
377
+ static #circleElement(element) {
378
+ const center = CircuitJsonSchematicSvgRenderer.#center(element)
379
+ const radius = CircuitJsonUnits.optionalLength(element.radius)
380
+ if (!center || radius === null) return ''
381
+ return (
382
+ '<circle class="schematic-shape schematic-shape--circle" cx="' +
383
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.x) +
384
+ '" cy="' +
385
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.y) +
386
+ '" r="' +
387
+ CircuitJsonSchematicSvgRenderer.#formatNumber(radius) +
388
+ '"' +
389
+ CircuitJsonSchematicSvgPrimitiveAttributes.attributes(element) +
390
+ '></circle>'
391
+ )
392
+ }
393
+
394
+ /**
395
+ * Renders one schematic arc.
396
+ * @param {object} element Arc element.
397
+ * @returns {string}
398
+ */
399
+ static #arcElement(element) {
400
+ const threePointPath = CircuitJsonSchematicSvgArcPath.fromThreePoints(
401
+ CircuitJsonSchematicSvgRenderer.#point(element.start),
402
+ CircuitJsonSchematicSvgRenderer.#point(element.mid),
403
+ CircuitJsonSchematicSvgRenderer.#point(element.end)
404
+ )
405
+ if (threePointPath) {
406
+ return (
407
+ '<path class="schematic-shape schematic-shape--arc" d="' +
408
+ CircuitJsonSchematicSvgRenderer.#escapeHtml(threePointPath) +
409
+ '"' +
410
+ CircuitJsonSchematicSvgPrimitiveAttributes.attributes(element, {
411
+ fill: false
412
+ }) +
413
+ '></path>'
414
+ )
415
+ }
416
+
417
+ const center = CircuitJsonSchematicSvgRenderer.#center(element)
418
+ const radius = CircuitJsonUnits.optionalLength(element.radius)
419
+ if (!center || radius === null) return ''
420
+ const startAngle = CircuitJsonUnits.angle(
421
+ element.start_angle ?? element.startAngle,
422
+ 0
423
+ )
424
+ const endAngle = CircuitJsonUnits.angle(
425
+ element.end_angle ?? element.endAngle,
426
+ 360
427
+ )
428
+ const start = CircuitJsonSchematicSvgRenderer.#polarPoint(
429
+ center,
430
+ radius,
431
+ startAngle
432
+ )
433
+ const end = CircuitJsonSchematicSvgRenderer.#polarPoint(
434
+ center,
435
+ radius,
436
+ endAngle
437
+ )
438
+ const largeArc = Math.abs(endAngle - startAngle) > 180 ? 1 : 0
439
+
440
+ return (
441
+ '<path class="schematic-shape schematic-shape--arc" d="M ' +
442
+ CircuitJsonSchematicSvgRenderer.#formatNumber(start.x) +
443
+ ' ' +
444
+ CircuitJsonSchematicSvgRenderer.#formatNumber(start.y) +
445
+ ' A ' +
446
+ CircuitJsonSchematicSvgRenderer.#formatNumber(radius) +
447
+ ' ' +
448
+ CircuitJsonSchematicSvgRenderer.#formatNumber(radius) +
449
+ ' 0 ' +
450
+ largeArc +
451
+ ' 1 ' +
452
+ CircuitJsonSchematicSvgRenderer.#formatNumber(end.x) +
453
+ ' ' +
454
+ CircuitJsonSchematicSvgRenderer.#formatNumber(end.y) +
455
+ '"' +
456
+ CircuitJsonSchematicSvgPrimitiveAttributes.attributes(element, {
457
+ fill: false
458
+ }) +
459
+ '></path>'
460
+ )
461
+ }
462
+
463
+ /**
464
+ * Renders one schematic polyline path.
465
+ * @param {object} element Path element.
466
+ * @returns {string}
467
+ */
468
+ static #pathElement(element) {
469
+ const points = CircuitJsonSchematicSvgRenderer.#points(element)
470
+ if (points.length < 2) return ''
471
+ const tag = element?.is_filled === true ? 'polygon' : 'polyline'
472
+ return (
473
+ '<' +
474
+ tag +
475
+ ' class="schematic-shape schematic-shape--path" points="' +
476
+ CircuitJsonSchematicSvgRenderer.#escapeHtml(
477
+ CircuitJsonSchematicSvgRenderer.#pointsAttribute(points)
478
+ ) +
479
+ '"' +
480
+ CircuitJsonSchematicSvgPrimitiveAttributes.attributes(element) +
481
+ '></' +
482
+ tag +
483
+ '>'
484
+ )
485
+ }
486
+
487
+ /**
488
+ * Renders schematic ports.
489
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
490
+ * @returns {string}
491
+ */
492
+ static #renderPorts(index) {
493
+ const sourcePorts = CircuitJsonSchematicSvgPortMetadata.sourcePorts(
494
+ CircuitJsonSchematicSvgRenderer.#all(index, 'source_port')
495
+ )
496
+ const ports = CircuitJsonSchematicSvgRenderer.#all(
497
+ index,
498
+ 'schematic_port'
499
+ )
500
+ .map((element) =>
501
+ CircuitJsonSchematicSvgRenderer.#portElement(
502
+ element,
503
+ sourcePorts
504
+ )
505
+ )
506
+ .filter(Boolean)
507
+ return ports.length
508
+ ? '<g class="schematic-ports">' + ports.join('') + '</g>'
509
+ : ''
510
+ }
511
+
512
+ /**
513
+ * Renders one schematic port marker.
514
+ * @param {object} element Port element.
515
+ * @param {Map<string, object>} sourcePorts Source port lookup.
516
+ * @returns {string}
517
+ */
518
+ static #portElement(element, sourcePorts) {
519
+ const center = CircuitJsonSchematicSvgRenderer.#center(element)
520
+ if (!center) return ''
521
+ const sourcePortId = String(element.source_port_id || '').trim()
522
+ const sourcePort = sourcePorts.get(sourcePortId) || null
523
+ const label = CircuitJsonSchematicSvgPortMetadata.label(
524
+ element,
525
+ sourcePort
526
+ )
527
+
528
+ return (
529
+ '<g ' +
530
+ CircuitJsonSchematicSvgPortMetadata.attributes(
531
+ element,
532
+ sourcePort
533
+ ) +
534
+ '><circle class="schematic-port__terminal" cx="' +
535
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.x) +
536
+ '" cy="' +
537
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.y) +
538
+ '" r="0.35"></circle><text class="schematic-port__label" x="' +
539
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.x + 0.7) +
540
+ '" y="' +
541
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.y) +
542
+ '" dominant-baseline="central">' +
543
+ CircuitJsonSchematicSvgRenderer.#escapeHtml(label) +
544
+ '</text></g>'
545
+ )
546
+ }
547
+
548
+ /**
549
+ * Renders schematic tables and cells.
550
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
551
+ * @returns {string}
552
+ */
553
+ static #renderTables(index) {
554
+ return CircuitJsonSchematicTableSvgRenderer.render(index, {
555
+ escapeHtml: CircuitJsonSchematicSvgRenderer.#escapeHtml,
556
+ formatNumber: CircuitJsonSchematicSvgRenderer.#formatNumber
557
+ })
558
+ }
559
+
560
+ /**
561
+ * Renders schematic text and net labels.
562
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
563
+ * @returns {string}
564
+ */
565
+ static #renderTexts(index) {
566
+ const texts = ['schematic_text', 'schematic_net_label']
567
+ .flatMap((type) =>
568
+ CircuitJsonSchematicSvgRenderer.#all(index, type)
569
+ )
570
+ .map((element) =>
571
+ CircuitJsonSchematicSvgRenderer.#textElement(element)
572
+ )
573
+ .filter(Boolean)
574
+ return texts.length
575
+ ? '<g class="schematic-texts">' + texts.join('') + '</g>'
576
+ : ''
577
+ }
578
+
579
+ /**
580
+ * Renders one schematic text row.
581
+ * @param {object} element Text element.
582
+ * @returns {string}
583
+ */
584
+ static #textElement(element) {
585
+ const point =
586
+ CircuitJsonSchematicSvgRenderer.#point(element.anchor_position) ||
587
+ CircuitJsonSchematicSvgRenderer.#point(element.position) ||
588
+ CircuitJsonSchematicSvgRenderer.#center(element)
589
+ if (!point) return ''
590
+ const fontSize = CircuitJsonUnits.optionalLength(element.font_size)
591
+ const rotation = CircuitJsonUnits.angle(
592
+ element.ccw_rotation ?? element.rotation,
593
+ 0
594
+ )
595
+ const attributes = [
596
+ 'class="schematic-text"',
597
+ 'x="' +
598
+ CircuitJsonSchematicSvgRenderer.#formatNumber(point.x) +
599
+ '"',
600
+ 'y="' + CircuitJsonSchematicSvgRenderer.#formatNumber(point.y) + '"'
601
+ ]
602
+
603
+ if (fontSize !== null) {
604
+ attributes.push(
605
+ 'font-size="' +
606
+ CircuitJsonSchematicSvgRenderer.#formatNumber(fontSize) +
607
+ '"'
608
+ )
609
+ }
610
+ if (rotation) {
611
+ attributes.push(
612
+ 'transform="rotate(' +
613
+ CircuitJsonSchematicSvgRenderer.#formatNumber(rotation) +
614
+ ' ' +
615
+ CircuitJsonSchematicSvgRenderer.#formatNumber(point.x) +
616
+ ' ' +
617
+ CircuitJsonSchematicSvgRenderer.#formatNumber(point.y) +
618
+ ')"'
619
+ )
620
+ }
621
+
622
+ return (
623
+ '<text ' +
624
+ attributes.join(' ') +
625
+ '>' +
626
+ CircuitJsonSchematicSvgRenderer.#escapeHtml(
627
+ element.text || element.name || ''
628
+ ) +
629
+ '</text>'
630
+ )
631
+ }
632
+
633
+ /**
634
+ * Renders schematic probe markers.
635
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
636
+ * @returns {string}
637
+ */
638
+ static #renderProbes(index) {
639
+ const probes = ['schematic_voltage_probe']
640
+ .flatMap((type) =>
641
+ CircuitJsonSchematicSvgRenderer.#all(index, type)
642
+ )
643
+ .map((element) =>
644
+ CircuitJsonSchematicSvgRenderer.#probeElement(element)
645
+ )
646
+ .filter(Boolean)
647
+ return probes.length
648
+ ? '<g class="schematic-probes">' + probes.join('') + '</g>'
649
+ : ''
650
+ }
651
+
652
+ /**
653
+ * Renders one schematic probe.
654
+ * @param {object} element Probe element.
655
+ * @returns {string}
656
+ */
657
+ static #probeElement(element) {
658
+ const center = CircuitJsonSchematicSvgRenderer.#center(element)
659
+ if (!center) return ''
660
+ const label = String(element.name || element.label || '').trim()
661
+ return (
662
+ '<g class="schematic-probe" data-schematic-probe-id="' +
663
+ CircuitJsonSchematicSvgRenderer.#escapeHtml(
664
+ CircuitJsonIndexer.getElementId(element)
665
+ ) +
666
+ '"><circle cx="' +
667
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.x) +
668
+ '" cy="' +
669
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.y) +
670
+ '" r="0.45"></circle><text x="' +
671
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.x + 0.65) +
672
+ '" y="' +
673
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.y) +
674
+ '" dominant-baseline="central">' +
675
+ CircuitJsonSchematicSvgRenderer.#escapeHtml(label) +
676
+ '</text></g>'
677
+ )
678
+ }
679
+
680
+ /**
681
+ * Renders schematic debug objects.
682
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
683
+ * @returns {string}
684
+ */
685
+ static #renderDebugObjects(index) {
686
+ const objects = CircuitJsonSchematicSvgRenderer.#all(
687
+ index,
688
+ 'schematic_debug_object'
689
+ )
690
+ .map((element) =>
691
+ CircuitJsonSchematicSvgRenderer.#debugObjectElement(element)
692
+ )
693
+ .filter(Boolean)
694
+ return objects.length
695
+ ? '<g class="schematic-debug-objects">' + objects.join('') + '</g>'
696
+ : ''
697
+ }
698
+
699
+ /**
700
+ * Renders one schematic debug object.
701
+ * @param {object} element Debug element.
702
+ * @returns {string}
703
+ */
704
+ static #debugObjectElement(element) {
705
+ const rect = CircuitJsonSchematicSvgRenderer.#rectAttributes(element)
706
+ const center = CircuitJsonSchematicSvgRenderer.#center(element)
707
+ if (!rect || !center) return ''
708
+ const message = String(element.message || '').trim()
709
+
710
+ return (
711
+ '<g class="schematic-debug-object" data-schematic-debug-object-id="' +
712
+ CircuitJsonSchematicSvgRenderer.#escapeHtml(
713
+ element.schematic_debug_object_id || ''
714
+ ) +
715
+ '"><title>' +
716
+ CircuitJsonSchematicSvgRenderer.#escapeHtml(message) +
717
+ '</title><rect ' +
718
+ rect +
719
+ '></rect><text x="' +
720
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.x) +
721
+ '" y="' +
722
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.y) +
723
+ '" text-anchor="middle" dominant-baseline="central">' +
724
+ CircuitJsonSchematicSvgRenderer.#escapeHtml(message) +
725
+ '</text></g>'
726
+ )
727
+ }
728
+
729
+ /**
730
+ * Renders schematic warning and error markers.
731
+ * @param {{ elements: object[] }} index Element index.
732
+ * @returns {string}
733
+ */
734
+ static #renderDiagnostics(index) {
735
+ const diagnostics = (index.elements || [])
736
+ .filter((element) =>
737
+ /(?:error|warning)/iu.test(String(element?.type || ''))
738
+ )
739
+ .map((element) =>
740
+ CircuitJsonSchematicSvgRenderer.#diagnosticElement(element)
741
+ )
742
+ .filter(Boolean)
743
+ return diagnostics.length
744
+ ? '<g class="schematic-diagnostics">' +
745
+ diagnostics.join('') +
746
+ '</g>'
747
+ : ''
748
+ }
749
+
750
+ /**
751
+ * Renders one schematic diagnostic marker.
752
+ * @param {object} element Diagnostic row.
753
+ * @returns {string}
754
+ */
755
+ static #diagnosticElement(element) {
756
+ const center = CircuitJsonSchematicSvgRenderer.#center(element) || {
757
+ x: 0,
758
+ y: 0
759
+ }
760
+ const severity = String(element.warning_type ? 'warning' : 'error')
761
+ return (
762
+ '<g class="schematic-diagnostic-marker schematic-diagnostic-marker--' +
763
+ severity +
764
+ '" data-schematic-diagnostic-id="' +
765
+ CircuitJsonSchematicSvgRenderer.#escapeHtml(
766
+ CircuitJsonIndexer.getElementId(element)
767
+ ) +
768
+ '"><title>' +
769
+ CircuitJsonSchematicSvgRenderer.#escapeHtml(
770
+ element.message ||
771
+ element.error_type ||
772
+ element.warning_type ||
773
+ ''
774
+ ) +
775
+ '</title><circle cx="' +
776
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.x) +
777
+ '" cy="' +
778
+ CircuitJsonSchematicSvgRenderer.#formatNumber(center.y) +
779
+ '" r="0.45"></circle></g>'
780
+ )
781
+ }
782
+
783
+ /**
784
+ * Builds source component display names.
785
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
786
+ * @returns {Map<string, string>}
787
+ */
788
+ static #sourceNames(index) {
789
+ return new Map(
790
+ CircuitJsonSchematicSvgRenderer.#all(index, 'source_component').map(
791
+ (element) => [
792
+ String(element.source_component_id || '').trim(),
793
+ String(element.name || element.source_component_id || '')
794
+ ]
795
+ )
796
+ )
797
+ }
798
+
799
+ /**
800
+ * Resolves schematic bounds.
801
+ * @param {{ elements: object[], elementsByType: Map<string, object[]> }} index Element index.
802
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number, width: number, height: number }}
803
+ */
804
+ static #bounds(index) {
805
+ const sheet = CircuitJsonSchematicSvgRenderer.#all(
806
+ index,
807
+ 'schematic_sheet'
808
+ )[0]
809
+ const width = CircuitJsonUnits.optionalLength(sheet?.width)
810
+ const height = CircuitJsonUnits.optionalLength(sheet?.height)
811
+ if (width !== null && height !== null) {
812
+ return {
813
+ minX: 0,
814
+ minY: 0,
815
+ maxX: width,
816
+ maxY: height,
817
+ width,
818
+ height
819
+ }
820
+ }
821
+ return {
822
+ minX: -10,
823
+ minY: -10,
824
+ maxX: 10,
825
+ maxY: 10,
826
+ width: 20,
827
+ height: 20
828
+ }
829
+ }
830
+
831
+ /**
832
+ * Builds a padded SVG viewBox.
833
+ * @param {object} bounds Bounds.
834
+ * @returns {object}
835
+ */
836
+ static #viewBox(bounds) {
837
+ const padding = Math.max(bounds.width, bounds.height, 1) * 0.06
838
+ return {
839
+ minX: bounds.minX - padding,
840
+ minY: bounds.minY - padding,
841
+ width: bounds.width + padding * 2,
842
+ height: bounds.height + padding * 2
843
+ }
844
+ }
845
+
846
+ /**
847
+ * Returns indexed elements by type.
848
+ * @param {{ elementsByType: Map<string, object[]> }} index Element index.
849
+ * @param {string} type Element type.
850
+ * @returns {object[]}
851
+ */
852
+ static #all(index, type) {
853
+ return index.elementsByType.get(type) || []
854
+ }
855
+
856
+ /**
857
+ * Resolves an element center.
858
+ * @param {object} element Element row.
859
+ * @returns {{ x: number, y: number } | null}
860
+ */
861
+ static #center(element) {
862
+ return CircuitJsonSchematicSvgRenderer.#point(
863
+ element?.center || element
864
+ )
865
+ }
866
+
867
+ /**
868
+ * Resolves an element size.
869
+ * @param {object} element Element row.
870
+ * @returns {{ width: number, height: number } | null}
871
+ */
872
+ static #size(element) {
873
+ return CircuitJsonUnits.optionalSize(element?.size || element)
874
+ }
875
+
876
+ /**
877
+ * Resolves a point.
878
+ * @param {object | null | undefined} value Point candidate.
879
+ * @returns {{ x: number, y: number } | null}
880
+ */
881
+ static #point(value) {
882
+ return CircuitJsonUnits.optionalPoint(value)
883
+ }
884
+
885
+ /**
886
+ * Resolves path points from common element fields.
887
+ * @param {object} element Element row.
888
+ * @returns {{ x: number, y: number }[]}
889
+ */
890
+ static #points(element) {
891
+ const points = Array.isArray(element?.points)
892
+ ? element.points
893
+ : Array.isArray(element?.path)
894
+ ? element.path
895
+ : []
896
+ return points
897
+ .map((point) => CircuitJsonSchematicSvgRenderer.#point(point))
898
+ .filter(Boolean)
899
+ }
900
+
901
+ /**
902
+ * Builds SVG rect attributes for a center/size element.
903
+ * @param {object} element Element row.
904
+ * @returns {string}
905
+ */
906
+ static #rectAttributes(element) {
907
+ const center = CircuitJsonSchematicSvgRenderer.#center(element)
908
+ const size = CircuitJsonSchematicSvgRenderer.#size(element)
909
+ if (!center || !size) return ''
910
+ return (
911
+ 'x="' +
912
+ CircuitJsonSchematicSvgRenderer.#formatNumber(
913
+ center.x - size.width / 2
914
+ ) +
915
+ '" y="' +
916
+ CircuitJsonSchematicSvgRenderer.#formatNumber(
917
+ center.y - size.height / 2
918
+ ) +
919
+ '" width="' +
920
+ CircuitJsonSchematicSvgRenderer.#formatNumber(size.width) +
921
+ '" height="' +
922
+ CircuitJsonSchematicSvgRenderer.#formatNumber(size.height) +
923
+ '"'
924
+ )
925
+ }
926
+
927
+ /**
928
+ * Resolves one point on a circle.
929
+ * @param {{ x: number, y: number }} center Circle center.
930
+ * @param {number} radius Circle radius.
931
+ * @param {number} angle Angle in degrees.
932
+ * @returns {{ x: number, y: number }}
933
+ */
934
+ static #polarPoint(center, radius, angle) {
935
+ const radians = (angle * Math.PI) / 180
936
+ return {
937
+ x: center.x + Math.cos(radians) * radius,
938
+ y: center.y + Math.sin(radians) * radius
939
+ }
940
+ }
941
+
942
+ /**
943
+ * Builds a polyline point string.
944
+ * @param {{ x: number, y: number }[]} points Points.
945
+ * @returns {string}
946
+ */
947
+ static #pointsAttribute(points) {
948
+ return points
949
+ .map(
950
+ (point) =>
951
+ CircuitJsonSchematicSvgRenderer.#formatNumber(point.x) +
952
+ ',' +
953
+ CircuitJsonSchematicSvgRenderer.#formatNumber(point.y)
954
+ )
955
+ .join(' ')
956
+ }
957
+
958
+ /**
959
+ * Formats a viewBox record.
960
+ * @param {{ minX: number, minY: number, width: number, height: number }} viewBox ViewBox record.
961
+ * @returns {string}
962
+ */
963
+ static #formatViewBox(viewBox) {
964
+ return [viewBox.minX, viewBox.minY, viewBox.width, viewBox.height]
965
+ .map((value) =>
966
+ CircuitJsonSchematicSvgRenderer.#formatNumber(value)
967
+ )
968
+ .join(' ')
969
+ }
970
+
971
+ /**
972
+ * Formats one SVG number.
973
+ * @param {number} value Number.
974
+ * @returns {string}
975
+ */
976
+ static #formatNumber(value) {
977
+ const number = Number(value)
978
+ if (!Number.isFinite(number)) return '0'
979
+ return Number(number.toFixed(6)).toString()
980
+ }
981
+
982
+ /**
983
+ * Escapes markup text.
984
+ * @param {unknown} value Raw value.
985
+ * @returns {string}
986
+ */
987
+ static #escapeHtml(value) {
988
+ return String(value ?? '')
989
+ .replaceAll('&', '&amp;')
990
+ .replaceAll('<', '&lt;')
991
+ .replaceAll('>', '&gt;')
992
+ .replaceAll('"', '&quot;')
993
+ }
994
+ }