altium-toolkit 0.1.23 → 1.0.2

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.
@@ -0,0 +1,826 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { CircuitJsonModelSchema } from './CircuitJsonModelSchema.mjs'
6
+ import { CircuitJsonModelAdapterPrimitives } from './CircuitJsonModelAdapterPrimitives.mjs'
7
+
8
+ const Primitives = CircuitJsonModelAdapterPrimitives
9
+
10
+ /**
11
+ * Converts between legacy renderer models and Circuit JSON element arrays.
12
+ */
13
+ export class CircuitJsonModelAdapter {
14
+ /**
15
+ * Converts a renderer model to a Circuit JSON array.
16
+ * @param {Record<string, unknown> | object[]} rendererModel
17
+ * @returns {object[]}
18
+ */
19
+ static fromRendererModel(rendererModel) {
20
+ if (CircuitJsonModelSchema.isModel(rendererModel)) {
21
+ return CircuitJsonModelSchema.attach(rendererModel)
22
+ }
23
+
24
+ const model = rendererModel || {}
25
+ const circuitJson = []
26
+ const sourceFormat = Primitives.sourceFormat(model)
27
+ const idScope = Primitives.idScope(model, sourceFormat)
28
+
29
+ CircuitJsonModelAdapter.#appendProjectMetadata(
30
+ circuitJson,
31
+ model,
32
+ sourceFormat
33
+ )
34
+
35
+ if (model.schematic) {
36
+ CircuitJsonModelAdapter.#appendSchematic(
37
+ circuitJson,
38
+ model,
39
+ idScope
40
+ )
41
+ }
42
+
43
+ if (model.pcb) {
44
+ CircuitJsonModelAdapter.#appendPcb(circuitJson, model, idScope)
45
+ }
46
+
47
+ if (model.pcbLibrary) {
48
+ CircuitJsonModelAdapter.#appendPcbLibrary(
49
+ circuitJson,
50
+ model,
51
+ idScope
52
+ )
53
+ }
54
+
55
+ CircuitJsonModelAdapter.#appendBom(circuitJson, model, idScope)
56
+ CircuitJsonModelAdapter.#attachCompatibility(circuitJson, model)
57
+
58
+ return CircuitJsonModelSchema.attach(circuitJson)
59
+ }
60
+
61
+ /**
62
+ * Returns a renderer-compatible model for Circuit JSON parser output.
63
+ * @param {object[] | Record<string, unknown>} circuitJson
64
+ * @returns {Record<string, unknown>}
65
+ */
66
+ static toRendererModel(circuitJson) {
67
+ if (
68
+ circuitJson &&
69
+ typeof circuitJson === 'object' &&
70
+ !Array.isArray(circuitJson)
71
+ ) {
72
+ return circuitJson
73
+ }
74
+
75
+ if (
76
+ circuitJson?.kind ||
77
+ circuitJson?.schematic ||
78
+ circuitJson?.pcb ||
79
+ circuitJson?.pcbLibrary ||
80
+ circuitJson?.project
81
+ ) {
82
+ return circuitJson
83
+ }
84
+
85
+ CircuitJsonModelSchema.assertModel(circuitJson)
86
+
87
+ const metadata = circuitJson.find(
88
+ (element) => element.type === 'source_project_metadata'
89
+ )
90
+ const hasPcb = circuitJson.some((element) =>
91
+ String(element.type).startsWith('pcb_')
92
+ )
93
+ const hasSchematic = circuitJson.some((element) =>
94
+ String(element.type).startsWith('schematic_')
95
+ )
96
+
97
+ return {
98
+ schema: CircuitJsonModelSchema.CURRENT_SCHEMA_ID,
99
+ kind: hasPcb ? 'pcb' : hasSchematic ? 'schematic' : 'project',
100
+ fileType: 'CircuitJson',
101
+ fileName: metadata?.name || 'circuit.json',
102
+ summary: {
103
+ title: metadata?.name || 'Circuit JSON',
104
+ elementCount: circuitJson.length
105
+ },
106
+ diagnostics: [],
107
+ circuitJson
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Returns true when a value is a Circuit JSON model array.
113
+ * @param {unknown} value
114
+ * @returns {boolean}
115
+ */
116
+ static isCircuitJson(value) {
117
+ return CircuitJsonModelSchema.isModel(value)
118
+ }
119
+
120
+ /**
121
+ * Appends Circuit JSON project metadata.
122
+ * @param {object[]} circuitJson
123
+ * @param {Record<string, unknown>} model
124
+ * @param {string} sourceFormat
125
+ * @returns {void}
126
+ */
127
+ static #appendProjectMetadata(circuitJson, model, sourceFormat) {
128
+ circuitJson.push({
129
+ type: 'source_project_metadata',
130
+ name:
131
+ String(model.summary?.title || '').trim() ||
132
+ Primitives.stripExtension(model.fileName) ||
133
+ 'Untitled circuit',
134
+ software_used_string: sourceFormat
135
+ })
136
+ }
137
+
138
+ /**
139
+ * Appends schematic elements.
140
+ * @param {object[]} circuitJson
141
+ * @param {Record<string, unknown>} model
142
+ * @param {string} idScope
143
+ * @returns {void}
144
+ */
145
+ static #appendSchematic(circuitJson, model, idScope) {
146
+ const schematic = model.schematic || {}
147
+ const componentIds = new Map()
148
+ const portIds = new Map()
149
+ const netIds = new Map()
150
+
151
+ for (const [componentIndex, component] of Primitives.array(
152
+ schematic.components
153
+ ).entries()) {
154
+ const sourceComponentId = Primitives.id(idScope, [
155
+ 'source_component',
156
+ component.designator || component.name || componentIndex
157
+ ])
158
+ componentIds.set(component, sourceComponentId)
159
+ circuitJson.push(
160
+ CircuitJsonModelAdapter.#sourceComponent(
161
+ sourceComponentId,
162
+ component,
163
+ componentIndex
164
+ )
165
+ )
166
+ circuitJson.push({
167
+ type: 'schematic_component',
168
+ schematic_component_id: Primitives.id(idScope, [
169
+ 'schematic_component',
170
+ component.designator || component.name || componentIndex
171
+ ]),
172
+ source_component_id: sourceComponentId,
173
+ center: Primitives.point(component.x, component.y),
174
+ size: {
175
+ width: Primitives.number(component.width, 0),
176
+ height: Primitives.number(component.height, 0)
177
+ },
178
+ rotation: Primitives.number(component.rotation, 0)
179
+ })
180
+ }
181
+
182
+ for (const [pinIndex, pin] of Primitives.array(
183
+ schematic.pins
184
+ ).entries()) {
185
+ const sourceComponentId =
186
+ CircuitJsonModelAdapter.#sourceComponentIdForPin(
187
+ pin,
188
+ componentIds,
189
+ idScope,
190
+ circuitJson
191
+ )
192
+ const sourcePortId = Primitives.sourcePortId(
193
+ idScope,
194
+ pin,
195
+ pinIndex,
196
+ sourceComponentId
197
+ )
198
+ portIds.set(pin, sourcePortId)
199
+ circuitJson.push({
200
+ type: 'source_port',
201
+ source_port_id: sourcePortId,
202
+ source_component_id: sourceComponentId,
203
+ name: Primitives.string(
204
+ pin.name || pin.designator || pinIndex,
205
+ String(pinIndex + 1)
206
+ ),
207
+ pin_number: Primitives.string(
208
+ pin.pinNumber || pin.designator || pin.name,
209
+ String(pinIndex + 1)
210
+ )
211
+ })
212
+ circuitJson.push({
213
+ type: 'schematic_port',
214
+ schematic_port_id: Primitives.id(idScope, [
215
+ 'schematic_port',
216
+ sourcePortId
217
+ ]),
218
+ source_port_id: sourcePortId,
219
+ center: Primitives.point(pin.x, pin.y),
220
+ facing_direction: Primitives.facingDirection(pin) || 'right'
221
+ })
222
+ }
223
+
224
+ for (const [netIndex, net] of Primitives.array(
225
+ schematic.nets
226
+ ).entries()) {
227
+ const sourceNetId = Primitives.sourceNetId(
228
+ idScope,
229
+ net.name || netIndex
230
+ )
231
+ netIds.set(net.name, sourceNetId)
232
+ circuitJson.push({
233
+ type: 'source_net',
234
+ source_net_id: sourceNetId,
235
+ name: Primitives.string(net.name, `NET_${netIndex + 1}`)
236
+ })
237
+ }
238
+
239
+ for (const [lineIndex, line] of Primitives.array(
240
+ schematic.lines
241
+ ).entries()) {
242
+ CircuitJsonModelAdapter.#appendSchematicLine(
243
+ circuitJson,
244
+ idScope,
245
+ line,
246
+ lineIndex,
247
+ netIds
248
+ )
249
+ }
250
+
251
+ for (const [textIndex, text] of Primitives.array(
252
+ schematic.texts
253
+ ).entries()) {
254
+ CircuitJsonModelAdapter.#appendSchematicText(
255
+ circuitJson,
256
+ idScope,
257
+ text,
258
+ textIndex
259
+ )
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Appends PCB elements.
265
+ * @param {object[]} circuitJson
266
+ * @param {Record<string, unknown>} model
267
+ * @param {string} idScope
268
+ * @returns {void}
269
+ */
270
+ static #appendPcb(circuitJson, model, idScope) {
271
+ const pcb = model.pcb || {}
272
+ const componentIds = new Map()
273
+ const sourceNetIds = new Map()
274
+ const boardId = Primitives.id(idScope, ['pcb_board'])
275
+
276
+ CircuitJsonModelAdapter.#appendPcbBoard(
277
+ circuitJson,
278
+ boardId,
279
+ pcb.boardOutline,
280
+ model
281
+ )
282
+
283
+ for (const [netIndex, net] of Primitives.array(pcb.nets).entries()) {
284
+ const sourceNetId = Primitives.sourceNetId(
285
+ idScope,
286
+ net.name || net.netName || netIndex
287
+ )
288
+ sourceNetIds.set(
289
+ String(net.name || net.netName || netIndex),
290
+ sourceNetId
291
+ )
292
+ circuitJson.push({
293
+ type: 'source_net',
294
+ source_net_id: sourceNetId,
295
+ name: Primitives.string(
296
+ net.name || net.netName,
297
+ `NET_${netIndex + 1}`
298
+ )
299
+ })
300
+ }
301
+
302
+ for (const [componentIndex, component] of Primitives.array(
303
+ pcb.components
304
+ ).entries()) {
305
+ const sourceComponentId = Primitives.id(idScope, [
306
+ 'source_component',
307
+ component.designator || component.name || componentIndex
308
+ ])
309
+ const pcbComponentId = Primitives.id(idScope, [
310
+ 'pcb_component',
311
+ component.designator || component.name || componentIndex
312
+ ])
313
+ componentIds.set(
314
+ Primitives.componentKey(component, componentIndex),
315
+ sourceComponentId
316
+ )
317
+ circuitJson.push(
318
+ CircuitJsonModelAdapter.#sourceComponent(
319
+ sourceComponentId,
320
+ component,
321
+ componentIndex
322
+ )
323
+ )
324
+ circuitJson.push({
325
+ type: 'pcb_component',
326
+ pcb_component_id: pcbComponentId,
327
+ source_component_id: sourceComponentId,
328
+ center: Primitives.milPoint(component.x, component.y),
329
+ layer: Primitives.side(component.layer),
330
+ rotation: Primitives.number(component.rotation, 0),
331
+ width: Primitives.milNumber(
332
+ component.width || component.widthMil,
333
+ 0
334
+ ),
335
+ height: Primitives.milNumber(
336
+ component.height || component.heightMil,
337
+ 0
338
+ )
339
+ })
340
+ }
341
+
342
+ for (const [padIndex, pad] of Primitives.array(pcb.pads).entries()) {
343
+ CircuitJsonModelAdapter.#appendPcbPad(
344
+ circuitJson,
345
+ idScope,
346
+ pad,
347
+ padIndex,
348
+ componentIds,
349
+ sourceNetIds
350
+ )
351
+ }
352
+
353
+ for (const [trackIndex, track] of Primitives.array(
354
+ pcb.tracks
355
+ ).entries()) {
356
+ CircuitJsonModelAdapter.#appendPcbTrace(
357
+ circuitJson,
358
+ idScope,
359
+ track,
360
+ trackIndex,
361
+ sourceNetIds
362
+ )
363
+ }
364
+
365
+ for (const [viaIndex, via] of Primitives.array(pcb.vias).entries()) {
366
+ CircuitJsonModelAdapter.#appendPcbVia(
367
+ circuitJson,
368
+ idScope,
369
+ via,
370
+ viaIndex,
371
+ sourceNetIds
372
+ )
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Appends one PCB board element.
378
+ * @param {object[]} circuitJson
379
+ * @param {string} boardId
380
+ * @param {Record<string, unknown>} boardOutline
381
+ * @param {Record<string, unknown>} model
382
+ * @returns {void}
383
+ */
384
+ static #appendPcbBoard(circuitJson, boardId, boardOutline, model) {
385
+ const widthMil =
386
+ Primitives.number(boardOutline?.widthMil, null) ??
387
+ Primitives.number(model.summary?.boardWidthMil, 0)
388
+ const heightMil =
389
+ Primitives.number(boardOutline?.heightMil, null) ??
390
+ Primitives.number(model.summary?.boardHeightMil, 0)
391
+ const minX = Primitives.number(boardOutline?.minX, 0)
392
+ const minY = Primitives.number(boardOutline?.minY, 0)
393
+ const outline = Primitives.outlinePoints(boardOutline)
394
+
395
+ circuitJson.push({
396
+ type: 'pcb_board',
397
+ pcb_board_id: boardId,
398
+ center: Primitives.milPoint(
399
+ minX + widthMil / 2,
400
+ minY + heightMil / 2
401
+ ),
402
+ width: Primitives.milNumber(widthMil, 0),
403
+ height: Primitives.milNumber(heightMil, 0),
404
+ thickness: 1.6,
405
+ num_layers: Primitives.number(model.summary?.layerCount, 2),
406
+ material: 'fr4',
407
+ outline,
408
+ shape: 'rect'
409
+ })
410
+ }
411
+
412
+ /**
413
+ * Appends one PCB pad and related source port element.
414
+ * @param {object[]} circuitJson
415
+ * @param {string} idScope
416
+ * @param {Record<string, unknown>} pad
417
+ * @param {number} padIndex
418
+ * @param {Map<string, string>} componentIds
419
+ * @param {Map<string, string>} sourceNetIds
420
+ * @returns {void}
421
+ */
422
+ static #appendPcbPad(
423
+ circuitJson,
424
+ idScope,
425
+ pad,
426
+ padIndex,
427
+ componentIds,
428
+ sourceNetIds
429
+ ) {
430
+ const sourceComponentId =
431
+ componentIds.get(String(pad.componentIndex)) ||
432
+ componentIds.get('0') ||
433
+ Primitives.id(idScope, ['source_component', 'unassigned'])
434
+ const sourcePortId = Primitives.sourcePortId(
435
+ idScope,
436
+ pad,
437
+ padIndex,
438
+ sourceComponentId
439
+ )
440
+ const pcbPortId = Primitives.id(idScope, ['pcb_port', sourcePortId])
441
+ const common = {
442
+ source_port_id: sourcePortId,
443
+ pcb_port_id: pcbPortId,
444
+ pcb_component_id: Primitives.id(idScope, [
445
+ 'pcb_component',
446
+ pad.componentIndex ?? 'unassigned'
447
+ ]),
448
+ center: Primitives.milPoint(pad.x, pad.y),
449
+ layer: Primitives.side(pad.layer),
450
+ port_hints: [
451
+ Primitives.string(
452
+ pad.name || pad.pinName || pad.designator,
453
+ String(padIndex + 1)
454
+ )
455
+ ]
456
+ }
457
+ const sourceNetId = Primitives.netIdForPrimitive(
458
+ idScope,
459
+ pad,
460
+ sourceNetIds
461
+ )
462
+
463
+ circuitJson.push({
464
+ type: 'source_port',
465
+ source_port_id: sourcePortId,
466
+ source_component_id: sourceComponentId,
467
+ name: common.port_hints[0],
468
+ pin_number: common.port_hints[0]
469
+ })
470
+ circuitJson.push({
471
+ type: 'pcb_port',
472
+ ...common,
473
+ source_net_id: sourceNetId
474
+ })
475
+
476
+ if (Primitives.isThroughHolePad(pad)) {
477
+ circuitJson.push({
478
+ type: pad.isPlated === false ? 'pcb_hole' : 'pcb_plated_hole',
479
+ ...common,
480
+ outer_diameter: Primitives.milNumber(
481
+ pad.sizeTopX || pad.sizeX || pad.diameter,
482
+ 0
483
+ ),
484
+ hole_diameter: Primitives.milNumber(pad.holeDiameter, 0),
485
+ shape: Primitives.padShape(pad)
486
+ })
487
+ return
488
+ }
489
+
490
+ circuitJson.push({
491
+ type: 'pcb_smtpad',
492
+ ...common,
493
+ shape: Primitives.padShape(pad),
494
+ width: Primitives.milNumber(
495
+ pad.sizeTopX || pad.sizeX || pad.width,
496
+ 0
497
+ ),
498
+ height: Primitives.milNumber(
499
+ pad.sizeTopY || pad.sizeY || pad.height,
500
+ 0
501
+ ),
502
+ rotation: Primitives.number(pad.rotation || pad.holeRotation, 0)
503
+ })
504
+ }
505
+
506
+ /**
507
+ * Appends one PCB copper trace.
508
+ * @param {object[]} circuitJson
509
+ * @param {string} idScope
510
+ * @param {Record<string, unknown>} track
511
+ * @param {number} trackIndex
512
+ * @param {Map<string, string>} sourceNetIds
513
+ * @returns {void}
514
+ */
515
+ static #appendPcbTrace(
516
+ circuitJson,
517
+ idScope,
518
+ track,
519
+ trackIndex,
520
+ sourceNetIds
521
+ ) {
522
+ const sourceTraceId = Primitives.id(idScope, [
523
+ 'source_trace',
524
+ track.netName || track.netIndex || trackIndex
525
+ ])
526
+ circuitJson.push({
527
+ type: 'source_trace',
528
+ source_trace_id: sourceTraceId,
529
+ source_net_id: Primitives.netIdForPrimitive(
530
+ idScope,
531
+ track,
532
+ sourceNetIds
533
+ )
534
+ })
535
+ circuitJson.push({
536
+ type: 'pcb_trace',
537
+ pcb_trace_id: Primitives.id(idScope, ['pcb_trace', trackIndex]),
538
+ source_trace_id: sourceTraceId,
539
+ route: [
540
+ {
541
+ route_type: 'wire',
542
+ x: Primitives.milNumber(track.x1, 0),
543
+ y: Primitives.milNumber(track.y1, 0),
544
+ width: Primitives.milNumber(track.width, 0),
545
+ layer: Primitives.layerName(track)
546
+ },
547
+ {
548
+ route_type: 'wire',
549
+ x: Primitives.milNumber(track.x2, 0),
550
+ y: Primitives.milNumber(track.y2, 0),
551
+ width: Primitives.milNumber(track.width, 0),
552
+ layer: Primitives.layerName(track)
553
+ }
554
+ ]
555
+ })
556
+ }
557
+
558
+ /**
559
+ * Appends one PCB via.
560
+ * @param {object[]} circuitJson
561
+ * @param {string} idScope
562
+ * @param {Record<string, unknown>} via
563
+ * @param {number} viaIndex
564
+ * @param {Map<string, string>} sourceNetIds
565
+ * @returns {void}
566
+ */
567
+ static #appendPcbVia(circuitJson, idScope, via, viaIndex, sourceNetIds) {
568
+ circuitJson.push({
569
+ type: 'pcb_via',
570
+ pcb_via_id: Primitives.id(idScope, ['pcb_via', viaIndex]),
571
+ source_net_id: Primitives.netIdForPrimitive(
572
+ idScope,
573
+ via,
574
+ sourceNetIds
575
+ ),
576
+ x: Primitives.milNumber(via.x, 0),
577
+ y: Primitives.milNumber(via.y, 0),
578
+ outer_diameter: Primitives.milNumber(via.diameter, 0),
579
+ hole_diameter: Primitives.milNumber(via.holeDiameter, 0),
580
+ layers: ['top', 'bottom']
581
+ })
582
+ }
583
+
584
+ /**
585
+ * Appends minimal PCB library elements as metadata.
586
+ * @param {object[]} circuitJson
587
+ * @param {Record<string, unknown>} model
588
+ * @param {string} idScope
589
+ * @returns {void}
590
+ */
591
+ static #appendPcbLibrary(circuitJson, model, idScope) {
592
+ for (const [footprintIndex, footprint] of Primitives.array(
593
+ model.pcbLibrary?.footprints
594
+ ).entries()) {
595
+ circuitJson.push({
596
+ type: 'source_component',
597
+ source_component_id: Primitives.id(idScope, [
598
+ 'library_footprint',
599
+ footprint.name || footprint.pattern || footprintIndex
600
+ ]),
601
+ name: Primitives.string(
602
+ footprint.name || footprint.pattern,
603
+ `FOOTPRINT_${footprintIndex + 1}`
604
+ ),
605
+ ftype: 'simple_chip'
606
+ })
607
+ }
608
+ }
609
+
610
+ /**
611
+ * Appends BOM rows as source components when they are not already present.
612
+ * @param {object[]} circuitJson
613
+ * @param {Record<string, unknown>} model
614
+ * @param {string} idScope
615
+ * @returns {void}
616
+ */
617
+ static #appendBom(circuitJson, model, idScope) {
618
+ const existingComponentIds = new Set(
619
+ circuitJson
620
+ .filter((element) => element.type === 'source_component')
621
+ .map((element) => element.source_component_id)
622
+ )
623
+
624
+ for (const [rowIndex, row] of Primitives.array(model.bom).entries()) {
625
+ for (const designator of Primitives.array(row.designators)) {
626
+ const sourceComponentId = Primitives.id(idScope, [
627
+ 'source_component',
628
+ designator
629
+ ])
630
+ if (existingComponentIds.has(sourceComponentId)) continue
631
+ existingComponentIds.add(sourceComponentId)
632
+ circuitJson.push({
633
+ type: 'source_component',
634
+ source_component_id: sourceComponentId,
635
+ name: Primitives.string(designator, `BOM_${rowIndex + 1}`),
636
+ display_value: Primitives.string(row.value, ''),
637
+ footprint: Primitives.string(row.pattern, ''),
638
+ manufacturer_part_number: Primitives.string(row.source, ''),
639
+ ftype: 'simple_chip'
640
+ })
641
+ }
642
+ }
643
+ }
644
+
645
+ /**
646
+ * Appends one schematic line or trace.
647
+ * @param {object[]} circuitJson
648
+ * @param {string} idScope
649
+ * @param {Record<string, unknown>} line
650
+ * @param {number} lineIndex
651
+ * @param {Map<string, string>} netIds
652
+ * @returns {void}
653
+ */
654
+ static #appendSchematicLine(circuitJson, idScope, line, lineIndex, netIds) {
655
+ const schematicLineId = Primitives.id(idScope, [
656
+ 'schematic_line',
657
+ lineIndex
658
+ ])
659
+ const lineElement = {
660
+ type: 'schematic_line',
661
+ schematic_line_id: schematicLineId,
662
+ x1: Primitives.number(line.x1, 0),
663
+ y1: Primitives.number(line.y1, 0),
664
+ x2: Primitives.number(line.x2, 0),
665
+ y2: Primitives.number(line.y2, 0),
666
+ stroke_width: Primitives.number(line.width, 1),
667
+ is_dashed: line.dashed === true
668
+ }
669
+ circuitJson.push(lineElement)
670
+
671
+ if (line.kind === 'wire' || line.netName || line.netIndex) {
672
+ circuitJson.push({
673
+ type: 'schematic_trace',
674
+ schematic_trace_id: Primitives.id(idScope, [
675
+ 'schematic_trace',
676
+ lineIndex
677
+ ]),
678
+ source_trace_id: Primitives.id(idScope, [
679
+ 'source_trace',
680
+ line.netName || line.netIndex || lineIndex
681
+ ]),
682
+ source_net_id:
683
+ netIds.get(String(line.netName)) ||
684
+ Primitives.sourceNetId(
685
+ idScope,
686
+ line.netName || line.netIndex || lineIndex
687
+ ),
688
+ edges: [
689
+ {
690
+ x1: lineElement.x1,
691
+ y1: lineElement.y1,
692
+ x2: lineElement.x2,
693
+ y2: lineElement.y2
694
+ }
695
+ ]
696
+ })
697
+ }
698
+ }
699
+
700
+ /**
701
+ * Appends one schematic text or net label.
702
+ * @param {object[]} circuitJson
703
+ * @param {string} idScope
704
+ * @param {Record<string, unknown>} text
705
+ * @param {number} textIndex
706
+ * @returns {void}
707
+ */
708
+ static #appendSchematicText(circuitJson, idScope, text, textIndex) {
709
+ const textValue = Primitives.string(
710
+ text.text || text.value || text.name,
711
+ ''
712
+ )
713
+ const base = {
714
+ text: textValue,
715
+ anchor_position: Primitives.point(text.x, text.y),
716
+ anchor_alignment: 'center'
717
+ }
718
+
719
+ if (Primitives.isNetLabel(text)) {
720
+ circuitJson.push({
721
+ type: 'schematic_net_label',
722
+ schematic_net_label_id: Primitives.id(idScope, [
723
+ 'schematic_net_label',
724
+ textIndex
725
+ ]),
726
+ source_net_id: Primitives.sourceNetId(
727
+ idScope,
728
+ textValue || textIndex
729
+ ),
730
+ ...base
731
+ })
732
+ return
733
+ }
734
+
735
+ circuitJson.push({
736
+ type: 'schematic_text',
737
+ schematic_text_id: Primitives.id(idScope, [
738
+ 'schematic_text',
739
+ textIndex
740
+ ]),
741
+ ...base
742
+ })
743
+ }
744
+
745
+ /**
746
+ * Returns a source component id for one schematic pin.
747
+ * @param {Record<string, unknown>} pin
748
+ * @param {Map<object, string>} componentIds
749
+ * @param {string} idScope
750
+ * @param {object[]} circuitJson
751
+ * @returns {string}
752
+ */
753
+ static #sourceComponentIdForPin(pin, componentIds, idScope, circuitJson) {
754
+ const designator = pin.designator || pin.ownerDesignator || pin.owner
755
+ if (designator) {
756
+ const sourceComponentId = Primitives.id(idScope, [
757
+ 'source_component',
758
+ designator
759
+ ])
760
+ if (
761
+ !circuitJson.some(
762
+ (element) =>
763
+ element.type === 'source_component' &&
764
+ element.source_component_id === sourceComponentId
765
+ )
766
+ ) {
767
+ circuitJson.push({
768
+ type: 'source_component',
769
+ source_component_id: sourceComponentId,
770
+ name: Primitives.string(designator, 'U?'),
771
+ ftype: 'simple_chip'
772
+ })
773
+ }
774
+ return sourceComponentId
775
+ }
776
+
777
+ return (
778
+ [...componentIds.values()][0] ||
779
+ Primitives.id(idScope, ['source_component', 'unassigned'])
780
+ )
781
+ }
782
+
783
+ /**
784
+ * Builds a source component element.
785
+ * @param {string} sourceComponentId
786
+ * @param {Record<string, unknown>} component
787
+ * @param {number} componentIndex
788
+ * @returns {object}
789
+ */
790
+ static #sourceComponent(sourceComponentId, component, componentIndex) {
791
+ return {
792
+ type: 'source_component',
793
+ source_component_id: sourceComponentId,
794
+ name: Primitives.string(
795
+ component.designator || component.name,
796
+ `U${componentIndex + 1}`
797
+ ),
798
+ display_value: Primitives.string(
799
+ component.value || component.comment,
800
+ ''
801
+ ),
802
+ footprint: Primitives.string(
803
+ component.pattern || component.footprint,
804
+ ''
805
+ ),
806
+ manufacturer_part_number: Primitives.string(component.source, ''),
807
+ ftype: 'simple_chip'
808
+ }
809
+ }
810
+
811
+ /**
812
+ * Attaches legacy renderer model fields to a Circuit JSON array.
813
+ * @param {object[]} circuitJson
814
+ * @param {Record<string, unknown>} rendererModel
815
+ * @returns {void}
816
+ */
817
+ static #attachCompatibility(circuitJson, rendererModel) {
818
+ Object.assign(circuitJson, rendererModel)
819
+ Object.defineProperty(circuitJson, 'rendererModel', {
820
+ configurable: true,
821
+ enumerable: false,
822
+ value: rendererModel,
823
+ writable: true
824
+ })
825
+ }
826
+ }