altium-toolkit 1.1.41 → 1.2.0

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 (58) hide show
  1. package/README.md +113 -19
  2. package/docs/api.md +224 -18
  3. package/docs/capabilities.md +65 -0
  4. package/docs/migration/legacy-001.md +309 -0
  5. package/docs/migration/legacy-002.md +309 -0
  6. package/docs/migration/legacy-003.md +309 -0
  7. package/docs/migration/legacy-004.md +309 -0
  8. package/docs/migration/legacy-005.md +111 -0
  9. package/docs/migration.md +20 -0
  10. package/docs/model-format.md +28 -3
  11. package/docs/release-notes-v1.2.0.md +147 -0
  12. package/docs/testing.md +43 -1
  13. package/examples/arduino-uno/PcbThreeSceneRenderer.mjs +1 -1
  14. package/examples/arduino-uno/example.mjs +1 -1
  15. package/examples/cli-utils.mjs +1 -1
  16. package/examples/corpus-smoke.mjs +1 -1
  17. package/examples/inspect-board.mjs +1 -1
  18. package/examples/library-catalog.mjs +1 -1
  19. package/examples/validate-library.mjs +1 -1
  20. package/package.json +20 -5
  21. package/spec/api-baseline-v1.1.41.json +1 -0
  22. package/spec/asset-baseline-v1.1.41.json +1 -0
  23. package/spec/feature-preservation.json +1 -0
  24. package/spec/library-scope.md +14 -4
  25. package/spec/native-source-manifest-v1.1.41.json +1 -0
  26. package/src/capabilities.mjs +4 -0
  27. package/src/convergence/AltiumCircuitJsonProjection.mjs +230 -0
  28. package/src/convergence/AltiumDocumentBuilder.mjs +167 -0
  29. package/src/convergence/AltiumExtensionResolver.mjs +98 -0
  30. package/src/convergence/AltiumProjectDocumentResolver.mjs +324 -0
  31. package/src/convergence/AltiumSchematicCoordinateProjection.mjs +130 -0
  32. package/src/convergence/AltiumWorkerClient.mjs +95 -0
  33. package/src/convergence/Parser.mjs +285 -0
  34. package/src/convergence/ParserInput.mjs +282 -0
  35. package/src/convergence/ProjectLoader.mjs +916 -0
  36. package/src/convergence/SchematicSvgRenderer.mjs +126 -0
  37. package/src/convergence/ToolkitCapabilities.mjs +49 -0
  38. package/src/core/circuit-json/CircuitJsonSchematicDocumentGraphicBuilder.mjs +785 -0
  39. package/src/core/circuit-json/CircuitJsonSchematicGraphicBuilder.mjs +909 -0
  40. package/src/core/circuit-json/CircuitJsonSchematicImageProjection.mjs +277 -0
  41. package/src/core/circuit-json/CircuitJsonSchematicStrokeStyle.mjs +47 -0
  42. package/src/extensions.mjs +11 -0
  43. package/src/index.mjs +20 -4
  44. package/src/interaction.mjs +7 -0
  45. package/src/legacy-netlist-query.mjs +11 -0
  46. package/src/legacy-parser.mjs +141 -0
  47. package/src/legacy-renderers.mjs +25 -0
  48. package/src/legacy-scene3d.mjs +10 -0
  49. package/src/manufacturing.mjs +4 -0
  50. package/src/parser.mjs +12 -138
  51. package/src/project.mjs +11 -0
  52. package/src/query.mjs +4 -0
  53. package/src/renderers.mjs +4 -22
  54. package/src/scene3d.mjs +5 -8
  55. package/src/simulation.mjs +4 -0
  56. package/src/styles/renderers.css +27 -0
  57. package/src/testing.mjs +8 -0
  58. package/src/workers/parser.worker.mjs +37 -0
@@ -0,0 +1,909 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { SchematicComponentOwnerTextResolver } from '../altium/SchematicComponentOwnerTextResolver.mjs'
6
+ import { CircuitJsonModelAdapterPrimitives } from './CircuitJsonModelAdapterPrimitives.mjs'
7
+ import { CircuitJsonSchematicDocumentGraphicBuilder } from './CircuitJsonSchematicDocumentGraphicBuilder.mjs'
8
+ import { CircuitJsonSchematicStrokeStyle } from './CircuitJsonSchematicStrokeStyle.mjs'
9
+
10
+ const Primitives = CircuitJsonModelAdapterPrimitives
11
+ const ELLIPSE_SEGMENTS = 48
12
+ const BEZIER_SEGMENTS = 24
13
+ const FAMILY_ORDER = new Map(
14
+ [
15
+ 'line',
16
+ 'rectangle',
17
+ 'rounded_rectangle',
18
+ 'ellipse',
19
+ 'arc',
20
+ 'pie',
21
+ 'bezier',
22
+ 'polygon',
23
+ 'text_box',
24
+ 'table',
25
+ 'sheet',
26
+ 'text'
27
+ ].map((name, index) => [name, index])
28
+ )
29
+
30
+ /**
31
+ * Projects normalized Altium schematic graphics onto shared CircuitJSON rows.
32
+ */
33
+ export class CircuitJsonSchematicGraphicBuilder {
34
+ /**
35
+ * Appends all common schematic graphics in native render order.
36
+ * @param {object[]} circuitJson Destination CircuitJSON model.
37
+ * @param {Record<string, unknown>} schematic Native schematic read model.
38
+ * @param {string} idScope Deterministic document id scope.
39
+ * @param {Map<object, string>} schematicComponentIds Component id lookup.
40
+ * @param {Map<string, string>} netIds Source net id lookup.
41
+ * @returns {void}
42
+ */
43
+ static append(
44
+ circuitJson,
45
+ schematic,
46
+ idScope,
47
+ schematicComponentIds,
48
+ netIds
49
+ ) {
50
+ const groups = []
51
+ const ownerIds = CircuitJsonSchematicGraphicBuilder.#componentOwnerIds(
52
+ schematic,
53
+ schematicComponentIds
54
+ )
55
+
56
+ CircuitJsonSchematicGraphicBuilder.#appendLines(
57
+ groups,
58
+ schematic,
59
+ idScope,
60
+ ownerIds,
61
+ netIds
62
+ )
63
+ CircuitJsonSchematicGraphicBuilder.#appendRectangles(
64
+ groups,
65
+ schematic,
66
+ idScope,
67
+ ownerIds
68
+ )
69
+ CircuitJsonSchematicGraphicBuilder.#appendRoundedRectangles(
70
+ groups,
71
+ schematic,
72
+ idScope,
73
+ ownerIds
74
+ )
75
+ CircuitJsonSchematicGraphicBuilder.#appendEllipses(
76
+ groups,
77
+ schematic,
78
+ idScope,
79
+ ownerIds
80
+ )
81
+ CircuitJsonSchematicGraphicBuilder.#appendArcs(
82
+ groups,
83
+ schematic,
84
+ idScope,
85
+ ownerIds
86
+ )
87
+ CircuitJsonSchematicGraphicBuilder.#appendPies(
88
+ groups,
89
+ schematic,
90
+ idScope,
91
+ ownerIds
92
+ )
93
+ CircuitJsonSchematicGraphicBuilder.#appendBeziers(
94
+ groups,
95
+ schematic,
96
+ idScope,
97
+ ownerIds
98
+ )
99
+ CircuitJsonSchematicGraphicBuilder.#appendPolygons(
100
+ groups,
101
+ schematic,
102
+ idScope,
103
+ ownerIds
104
+ )
105
+ CircuitJsonSchematicDocumentGraphicBuilder.append(
106
+ groups,
107
+ schematic,
108
+ idScope,
109
+ ownerIds
110
+ )
111
+
112
+ groups
113
+ .sort(
114
+ (left, right) =>
115
+ left.order - right.order ||
116
+ left.familyOrder - right.familyOrder ||
117
+ left.index - right.index
118
+ )
119
+ .forEach((group) => circuitJson.push(...group.rows))
120
+ }
121
+
122
+ /**
123
+ * Appends authored line and wire rows.
124
+ * @param {object[]} groups Render groups.
125
+ * @param {Record<string, unknown>} schematic Native schematic.
126
+ * @param {string} idScope Document id scope.
127
+ * @param {Map<string, string>} ownerIds Owner-to-component lookup.
128
+ * @param {Map<string, string>} netIds Source net lookup.
129
+ * @returns {void}
130
+ */
131
+ static #appendLines(groups, schematic, idScope, ownerIds, netIds) {
132
+ for (const [index, line] of Primitives.array(
133
+ schematic.lines
134
+ ).entries()) {
135
+ if (String(line?.recordType || '') === '7') continue
136
+ const rows = []
137
+ const lineId = Primitives.id(idScope, [
138
+ 'schematic_line',
139
+ CircuitJsonSchematicGraphicBuilder.#identity(line, index)
140
+ ])
141
+ rows.push({
142
+ type: 'schematic_line',
143
+ schematic_line_id: lineId,
144
+ x1: Primitives.number(line?.x1, 0),
145
+ y1: Primitives.number(line?.y1, 0),
146
+ x2: Primitives.number(line?.x2, 0),
147
+ y2: Primitives.number(line?.y2, 0),
148
+ stroke_width: Primitives.number(line?.width, 1),
149
+ ...CircuitJsonSchematicStrokeStyle.fields(line, line?.width),
150
+ color: Primitives.string(line?.color, '#000000'),
151
+ ...CircuitJsonSchematicGraphicBuilder.#ownerField(
152
+ line,
153
+ ownerIds
154
+ )
155
+ })
156
+
157
+ if (String(line?.sourceType || '').toLowerCase() === 'wire') {
158
+ const netKey = String(line.netName || '')
159
+ const sourceNetId =
160
+ netIds.get(netKey) ||
161
+ Primitives.sourceNetId(
162
+ idScope,
163
+ line.netName || line.netIndex || index
164
+ )
165
+ const sourceTraceId = Primitives.id(idScope, [
166
+ 'source_trace',
167
+ line.netName || line.netIndex || index,
168
+ index
169
+ ])
170
+ rows.push(
171
+ {
172
+ type: 'source_trace',
173
+ source_trace_id: sourceTraceId,
174
+ connected_source_port_ids: [],
175
+ connected_source_net_ids: sourceNetId
176
+ ? [sourceNetId]
177
+ : []
178
+ },
179
+ {
180
+ type: 'schematic_trace',
181
+ schematic_trace_id: Primitives.id(idScope, [
182
+ 'schematic_trace',
183
+ index
184
+ ]),
185
+ source_trace_id: sourceTraceId,
186
+ junctions: [],
187
+ edges: [
188
+ {
189
+ from: {
190
+ x: Primitives.number(line?.x1, 0),
191
+ y: Primitives.number(line?.y1, 0)
192
+ },
193
+ to: {
194
+ x: Primitives.number(line?.x2, 0),
195
+ y: Primitives.number(line?.y2, 0)
196
+ }
197
+ }
198
+ ]
199
+ }
200
+ )
201
+ }
202
+ CircuitJsonSchematicGraphicBuilder.#group(
203
+ groups,
204
+ line,
205
+ 'line',
206
+ index,
207
+ rows
208
+ )
209
+ }
210
+ }
211
+
212
+ /**
213
+ * Appends rectangular schematic primitives.
214
+ * @param {object[]} groups Render groups.
215
+ * @param {Record<string, unknown>} schematic Native schematic.
216
+ * @param {string} idScope Document id scope.
217
+ * @param {Map<string, string>} ownerIds Owner-to-component lookup.
218
+ * @returns {void}
219
+ */
220
+ static #appendRectangles(groups, schematic, idScope, ownerIds) {
221
+ const families = [
222
+ ...Primitives.array(schematic.rectangles),
223
+ ...Primitives.array(schematic.regions)
224
+ ]
225
+ for (const [index, rectangle] of families.entries()) {
226
+ const width = Math.abs(Primitives.number(rectangle?.width, 0) || 0)
227
+ const height = Math.abs(
228
+ Primitives.number(rectangle?.height, 0) || 0
229
+ )
230
+ const x = Primitives.number(rectangle?.x, 0) || 0
231
+ const y = Primitives.number(rectangle?.y, 0) || 0
232
+ const row = {
233
+ type: 'schematic_rect',
234
+ schematic_rect_id: Primitives.id(idScope, [
235
+ 'schematic_rect',
236
+ CircuitJsonSchematicGraphicBuilder.#identity(
237
+ rectangle,
238
+ index
239
+ )
240
+ ]),
241
+ center: Primitives.point(x + width / 2, y + height / 2),
242
+ width,
243
+ height,
244
+ ...CircuitJsonSchematicGraphicBuilder.#closedStyle(
245
+ rectangle,
246
+ false
247
+ ),
248
+ ...CircuitJsonSchematicGraphicBuilder.#ownerField(
249
+ rectangle,
250
+ ownerIds
251
+ )
252
+ }
253
+ CircuitJsonSchematicGraphicBuilder.#group(
254
+ groups,
255
+ rectangle,
256
+ 'rectangle',
257
+ index,
258
+ [row]
259
+ )
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Appends rounded rectangles as exact sampled paths.
265
+ * @param {object[]} groups Render groups.
266
+ * @param {Record<string, unknown>} schematic Native schematic.
267
+ * @param {string} idScope Document id scope.
268
+ * @param {Map<string, string>} ownerIds Owner-to-component lookup.
269
+ * @returns {void}
270
+ */
271
+ static #appendRoundedRectangles(groups, schematic, idScope, ownerIds) {
272
+ for (const [index, rectangle] of Primitives.array(
273
+ schematic.roundedRectangles
274
+ ).entries()) {
275
+ const row = {
276
+ type: 'schematic_path',
277
+ schematic_path_id: Primitives.id(idScope, [
278
+ 'schematic_path',
279
+ 'rounded_rectangle',
280
+ CircuitJsonSchematicGraphicBuilder.#identity(
281
+ rectangle,
282
+ index
283
+ )
284
+ ]),
285
+ points: CircuitJsonSchematicGraphicBuilder.#roundedRectanglePoints(
286
+ rectangle
287
+ ),
288
+ ...CircuitJsonSchematicGraphicBuilder.#pathStyle(rectangle),
289
+ ...CircuitJsonSchematicGraphicBuilder.#ownerField(
290
+ rectangle,
291
+ ownerIds
292
+ )
293
+ }
294
+ CircuitJsonSchematicGraphicBuilder.#group(
295
+ groups,
296
+ rectangle,
297
+ 'rounded_rectangle',
298
+ index,
299
+ [row]
300
+ )
301
+ }
302
+ }
303
+
304
+ /**
305
+ * Appends circles and sampled unequal ellipses.
306
+ * @param {object[]} groups Render groups.
307
+ * @param {Record<string, unknown>} schematic Native schematic.
308
+ * @param {string} idScope Document id scope.
309
+ * @param {Map<string, string>} ownerIds Owner-to-component lookup.
310
+ * @returns {void}
311
+ */
312
+ static #appendEllipses(groups, schematic, idScope, ownerIds) {
313
+ for (const [index, ellipse] of Primitives.array(
314
+ schematic.ellipses
315
+ ).entries()) {
316
+ const radiusX = Math.abs(
317
+ Primitives.number(ellipse?.radiusX, 0) || 0
318
+ )
319
+ const radiusY = Math.abs(
320
+ Primitives.number(ellipse?.radiusY, 0) || 0
321
+ )
322
+ const identity = CircuitJsonSchematicGraphicBuilder.#identity(
323
+ ellipse,
324
+ index
325
+ )
326
+ const owner = CircuitJsonSchematicGraphicBuilder.#ownerField(
327
+ ellipse,
328
+ ownerIds
329
+ )
330
+ const row =
331
+ Math.abs(radiusX - radiusY) <= 0.000001
332
+ ? {
333
+ type: 'schematic_circle',
334
+ schematic_circle_id: Primitives.id(idScope, [
335
+ 'schematic_circle',
336
+ identity
337
+ ]),
338
+ center: Primitives.point(ellipse?.x, ellipse?.y),
339
+ radius: radiusX,
340
+ ...CircuitJsonSchematicGraphicBuilder.#closedStyle(
341
+ ellipse,
342
+ false
343
+ ),
344
+ ...owner
345
+ }
346
+ : {
347
+ type: 'schematic_path',
348
+ schematic_path_id: Primitives.id(idScope, [
349
+ 'schematic_path',
350
+ 'ellipse',
351
+ identity
352
+ ]),
353
+ points: CircuitJsonSchematicGraphicBuilder.#ellipsePoints(
354
+ ellipse,
355
+ ELLIPSE_SEGMENTS
356
+ ),
357
+ ...CircuitJsonSchematicGraphicBuilder.#pathStyle(
358
+ ellipse
359
+ ),
360
+ ...owner
361
+ }
362
+ CircuitJsonSchematicGraphicBuilder.#group(
363
+ groups,
364
+ ellipse,
365
+ 'ellipse',
366
+ index,
367
+ [row]
368
+ )
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Appends circular arcs and sampled elliptical arcs.
374
+ * @param {object[]} groups Render groups.
375
+ * @param {Record<string, unknown>} schematic Native schematic.
376
+ * @param {string} idScope Document id scope.
377
+ * @param {Map<string, string>} ownerIds Owner-to-component lookup.
378
+ * @returns {void}
379
+ */
380
+ static #appendArcs(groups, schematic, idScope, ownerIds) {
381
+ for (const [index, arc] of Primitives.array(schematic.arcs).entries()) {
382
+ const radius = Math.abs(Primitives.number(arc?.radius, 0) || 0)
383
+ const radiusY = Math.abs(
384
+ Primitives.number(arc?.radiusY, radius) || radius
385
+ )
386
+ const start = Primitives.number(arc?.startAngle, 0) || 0
387
+ const end = Primitives.number(arc?.endAngle, 360) || 0
388
+ const identity = CircuitJsonSchematicGraphicBuilder.#identity(
389
+ arc,
390
+ index
391
+ )
392
+ const owner = CircuitJsonSchematicGraphicBuilder.#ownerField(
393
+ arc,
394
+ ownerIds
395
+ )
396
+ const row =
397
+ Math.abs(radius - radiusY) <= 0.000001
398
+ ? {
399
+ type: 'schematic_arc',
400
+ schematic_arc_id: Primitives.id(idScope, [
401
+ 'schematic_arc',
402
+ identity
403
+ ]),
404
+ center: Primitives.point(arc?.x, arc?.y),
405
+ radius,
406
+ start_angle_degrees: start,
407
+ end_angle_degrees: end,
408
+ direction:
409
+ CircuitJsonSchematicGraphicBuilder.#arcDelta(
410
+ start,
411
+ end
412
+ ) >= 0
413
+ ? 'clockwise'
414
+ : 'counterclockwise',
415
+ stroke_width: Primitives.number(arc?.width, 1),
416
+ color: Primitives.string(arc?.color, '#000000'),
417
+ ...CircuitJsonSchematicStrokeStyle.fields(
418
+ arc,
419
+ arc?.width
420
+ ),
421
+ ...owner
422
+ }
423
+ : {
424
+ type: 'schematic_path',
425
+ schematic_path_id: Primitives.id(idScope, [
426
+ 'schematic_path',
427
+ 'elliptical_arc',
428
+ identity
429
+ ]),
430
+ points: CircuitJsonSchematicGraphicBuilder.#ellipticalArcPoints(
431
+ arc,
432
+ radius,
433
+ radiusY,
434
+ start,
435
+ end
436
+ ),
437
+ stroke_color: Primitives.string(
438
+ arc?.color,
439
+ '#000000'
440
+ ),
441
+ stroke_width: Primitives.number(arc?.width, 1),
442
+ ...CircuitJsonSchematicStrokeStyle.fields(
443
+ arc,
444
+ arc?.width
445
+ ),
446
+ is_filled: false,
447
+ ...owner
448
+ }
449
+ CircuitJsonSchematicGraphicBuilder.#group(
450
+ groups,
451
+ arc,
452
+ 'arc',
453
+ index,
454
+ [row]
455
+ )
456
+ }
457
+ }
458
+
459
+ /**
460
+ * Appends filled pie and wedge primitives as closed common paths.
461
+ * @param {object[]} groups Render groups.
462
+ * @param {Record<string, unknown>} schematic Native schematic.
463
+ * @param {string} idScope Document id scope.
464
+ * @param {Map<string, string>} ownerIds Owner-to-component lookup.
465
+ * @returns {void}
466
+ */
467
+ static #appendPies(groups, schematic, idScope, ownerIds) {
468
+ for (const [index, pie] of Primitives.array(schematic.pies).entries()) {
469
+ const radius = Math.abs(
470
+ Primitives.number(pie?.radius ?? pie?.radiusX, 0) || 0
471
+ )
472
+ const radiusY = Math.abs(
473
+ Primitives.number(pie?.radiusY, radius) || 0
474
+ )
475
+ if (!(radius > 0 && radiusY > 0)) continue
476
+ const start = Primitives.number(pie?.startAngle, 0) || 0
477
+ const end = Primitives.number(pie?.endAngle, 360) || 0
478
+ const row = {
479
+ type: 'schematic_path',
480
+ schematic_path_id: Primitives.id(idScope, [
481
+ 'schematic_path',
482
+ 'pie',
483
+ CircuitJsonSchematicGraphicBuilder.#identity(pie, index)
484
+ ]),
485
+ points: [
486
+ Primitives.point(pie?.x, pie?.y),
487
+ ...CircuitJsonSchematicGraphicBuilder.#ellipticalArcPoints(
488
+ pie,
489
+ radius,
490
+ radiusY,
491
+ start,
492
+ end
493
+ )
494
+ ],
495
+ ...CircuitJsonSchematicGraphicBuilder.#pathStyle(pie),
496
+ ...CircuitJsonSchematicGraphicBuilder.#ownerField(pie, ownerIds)
497
+ }
498
+ CircuitJsonSchematicGraphicBuilder.#group(
499
+ groups,
500
+ pie,
501
+ 'pie',
502
+ index,
503
+ [row]
504
+ )
505
+ }
506
+ }
507
+
508
+ /**
509
+ * Appends deterministic cubic-Bezier samples.
510
+ * @param {object[]} groups Render groups.
511
+ * @param {Record<string, unknown>} schematic Native schematic.
512
+ * @param {string} idScope Document id scope.
513
+ * @param {Map<string, string>} ownerIds Owner-to-component lookup.
514
+ * @returns {void}
515
+ */
516
+ static #appendBeziers(groups, schematic, idScope, ownerIds) {
517
+ for (const [index, bezier] of Primitives.array(
518
+ schematic.beziers
519
+ ).entries()) {
520
+ const points =
521
+ CircuitJsonSchematicGraphicBuilder.#bezierPoints(bezier)
522
+ if (points.length < 2) continue
523
+ const row = {
524
+ type: 'schematic_path',
525
+ schematic_path_id: Primitives.id(idScope, [
526
+ 'schematic_path',
527
+ 'bezier',
528
+ CircuitJsonSchematicGraphicBuilder.#identity(bezier, index)
529
+ ]),
530
+ points,
531
+ stroke_color: Primitives.string(bezier?.color, '#000000'),
532
+ stroke_width: Primitives.number(bezier?.width, 1),
533
+ ...CircuitJsonSchematicStrokeStyle.fields(
534
+ bezier,
535
+ bezier?.width
536
+ ),
537
+ is_filled: false,
538
+ ...CircuitJsonSchematicGraphicBuilder.#ownerField(
539
+ bezier,
540
+ ownerIds
541
+ )
542
+ }
543
+ CircuitJsonSchematicGraphicBuilder.#group(
544
+ groups,
545
+ bezier,
546
+ 'bezier',
547
+ index,
548
+ [row]
549
+ )
550
+ }
551
+ }
552
+
553
+ /**
554
+ * Appends exact polygon point lists.
555
+ * @param {object[]} groups Render groups.
556
+ * @param {Record<string, unknown>} schematic Native schematic.
557
+ * @param {string} idScope Document id scope.
558
+ * @param {Map<string, string>} ownerIds Owner-to-component lookup.
559
+ * @returns {void}
560
+ */
561
+ static #appendPolygons(groups, schematic, idScope, ownerIds) {
562
+ for (const [index, polygon] of Primitives.array(
563
+ schematic.polygons
564
+ ).entries()) {
565
+ const points = Primitives.array(polygon?.points).map((point) =>
566
+ Primitives.point(point?.x, point?.y)
567
+ )
568
+ if (points.length < 2) continue
569
+ const row = {
570
+ type: 'schematic_path',
571
+ schematic_path_id: Primitives.id(idScope, [
572
+ 'schematic_path',
573
+ 'polygon',
574
+ CircuitJsonSchematicGraphicBuilder.#identity(polygon, index)
575
+ ]),
576
+ points,
577
+ ...CircuitJsonSchematicGraphicBuilder.#pathStyle(polygon),
578
+ ...CircuitJsonSchematicGraphicBuilder.#ownerField(
579
+ polygon,
580
+ ownerIds
581
+ )
582
+ }
583
+ CircuitJsonSchematicGraphicBuilder.#group(
584
+ groups,
585
+ polygon,
586
+ 'polygon',
587
+ index,
588
+ [row]
589
+ )
590
+ }
591
+ }
592
+
593
+ /**
594
+ * Resolves native owner ids to canonical schematic component ids.
595
+ * @param {Record<string, unknown>} schematic Native schematic.
596
+ * @param {Map<object, string>} componentIds Component id lookup.
597
+ * @returns {Map<string, string>}
598
+ */
599
+ static #componentOwnerIds(schematic, componentIds) {
600
+ const ownerIds = new Map()
601
+ const components = Primitives.array(schematic.components)
602
+ const records = Primitives.array(schematic.ownership?.records)
603
+ const componentRecords = records.filter((record) =>
604
+ ['1', '45'].includes(String(record?.recordType || ''))
605
+ )
606
+
607
+ for (const [index, component] of components.entries()) {
608
+ const componentId = componentIds.get(component)
609
+ if (!componentId) continue
610
+ for (const ownerIndex of [
611
+ component?.ownerIndex,
612
+ ...Primitives.array(component?.ownerIndexes)
613
+ ]) {
614
+ CircuitJsonSchematicGraphicBuilder.#setOwner(
615
+ ownerIds,
616
+ ownerIndex,
617
+ componentId
618
+ )
619
+ }
620
+ const uniqueId = String(component?.uniqueId || '').trim()
621
+ const componentRecord =
622
+ componentRecords.find(
623
+ (record) =>
624
+ uniqueId &&
625
+ String(record?.uniqueId || '').trim() === uniqueId
626
+ ) || componentRecords[index]
627
+ if (componentRecord) {
628
+ for (const ownerIndex of SchematicComponentOwnerTextResolver.resolveOwnerIndexes(
629
+ componentRecord,
630
+ records
631
+ )) {
632
+ CircuitJsonSchematicGraphicBuilder.#setOwner(
633
+ ownerIds,
634
+ ownerIndex,
635
+ componentId
636
+ )
637
+ }
638
+ }
639
+
640
+ const designator = String(component?.designator || '').trim()
641
+ for (const record of records) {
642
+ if (
643
+ designator &&
644
+ String(record?.name || '').toLowerCase() === 'designator' &&
645
+ String(record?.text || '').trim() === designator
646
+ ) {
647
+ CircuitJsonSchematicGraphicBuilder.#setOwner(
648
+ ownerIds,
649
+ record?.ownerIndex,
650
+ componentId
651
+ )
652
+ }
653
+ }
654
+ }
655
+ return ownerIds
656
+ }
657
+
658
+ /**
659
+ * Builds rounded rectangle perimeter samples.
660
+ * @param {Record<string, unknown>} rectangle Native rounded rectangle.
661
+ * @returns {{ x: number, y: number }[]}
662
+ */
663
+ static #roundedRectanglePoints(rectangle) {
664
+ const x = Primitives.number(rectangle?.x, 0) || 0
665
+ const y = Primitives.number(rectangle?.y, 0) || 0
666
+ const width = Math.abs(Primitives.number(rectangle?.width, 0) || 0)
667
+ const height = Math.abs(Primitives.number(rectangle?.height, 0) || 0)
668
+ const radius = Math.max(
669
+ Math.min(
670
+ Math.abs(Primitives.number(rectangle?.radius, 0) || 0),
671
+ width / 2,
672
+ height / 2
673
+ ),
674
+ 0
675
+ )
676
+ if (radius === 0) {
677
+ return [
678
+ Primitives.point(x, y),
679
+ Primitives.point(x + width, y),
680
+ Primitives.point(x + width, y + height),
681
+ Primitives.point(x, y + height)
682
+ ]
683
+ }
684
+ const points = []
685
+ const corners = [
686
+ { x: x + width - radius, y: y + radius, start: -90 },
687
+ { x: x + width - radius, y: y + height - radius, start: 0 },
688
+ { x: x + radius, y: y + height - radius, start: 90 },
689
+ { x: x + radius, y: y + radius, start: 180 }
690
+ ]
691
+ for (const [cornerIndex, corner] of corners.entries()) {
692
+ for (let step = 0; step <= 8; step += 1) {
693
+ if (cornerIndex > 0 && step === 0) continue
694
+ const angle = ((corner.start + (step / 8) * 90) * Math.PI) / 180
695
+ points.push(
696
+ Primitives.point(
697
+ corner.x + radius * Math.cos(angle),
698
+ corner.y + radius * Math.sin(angle)
699
+ )
700
+ )
701
+ }
702
+ }
703
+ return points
704
+ }
705
+
706
+ /**
707
+ * Builds closed ellipse samples.
708
+ * @param {Record<string, unknown>} ellipse Native ellipse.
709
+ * @param {number} segments Segment count.
710
+ * @returns {{ x: number, y: number }[]}
711
+ */
712
+ static #ellipsePoints(ellipse, segments) {
713
+ const centerX = Primitives.number(ellipse?.x, 0) || 0
714
+ const centerY = Primitives.number(ellipse?.y, 0) || 0
715
+ const radiusX = Math.abs(Primitives.number(ellipse?.radiusX, 0) || 0)
716
+ const radiusY = Math.abs(Primitives.number(ellipse?.radiusY, 0) || 0)
717
+ return Array.from({ length: segments }, (_, index) => {
718
+ const angle = (index / segments) * Math.PI * 2
719
+ return Primitives.point(
720
+ centerX + radiusX * Math.cos(angle),
721
+ centerY + radiusY * Math.sin(angle)
722
+ )
723
+ })
724
+ }
725
+
726
+ /**
727
+ * Builds samples for one elliptical arc.
728
+ * @param {Record<string, unknown>} arc Native arc.
729
+ * @param {number} radiusX Horizontal radius.
730
+ * @param {number} radiusY Vertical radius.
731
+ * @param {number} start Start angle.
732
+ * @param {number} end End angle.
733
+ * @returns {{ x: number, y: number }[]}
734
+ */
735
+ static #ellipticalArcPoints(arc, radiusX, radiusY, start, end) {
736
+ const delta = CircuitJsonSchematicGraphicBuilder.#arcDelta(start, end)
737
+ const segmentCount = Math.max(2, Math.ceil(Math.abs(delta) / 7.5))
738
+ const centerX = Primitives.number(arc?.x, 0) || 0
739
+ const centerY = Primitives.number(arc?.y, 0) || 0
740
+ return Array.from({ length: segmentCount + 1 }, (_, index) => {
741
+ const angle =
742
+ ((start + (delta * index) / segmentCount) * Math.PI) / 180
743
+ return Primitives.point(
744
+ centerX + radiusX * Math.cos(angle),
745
+ centerY + radiusY * Math.sin(angle)
746
+ )
747
+ })
748
+ }
749
+
750
+ /**
751
+ * Builds deterministic samples for every cubic Bezier span.
752
+ * @param {Record<string, unknown>} bezier Native Bezier.
753
+ * @returns {{ x: number, y: number }[]}
754
+ */
755
+ static #bezierPoints(bezier) {
756
+ const points = []
757
+ for (const [segmentIndex, segment] of Primitives.array(
758
+ bezier?.segments
759
+ ).entries()) {
760
+ for (let step = 0; step <= BEZIER_SEGMENTS; step += 1) {
761
+ if (segmentIndex > 0 && step === 0) continue
762
+ const t = step / BEZIER_SEGMENTS
763
+ const inverse = 1 - t
764
+ const x =
765
+ inverse ** 3 *
766
+ (Primitives.number(segment?.start?.x, 0) || 0) +
767
+ 3 *
768
+ inverse ** 2 *
769
+ t *
770
+ (Primitives.number(segment?.control1?.x, 0) || 0) +
771
+ 3 *
772
+ inverse *
773
+ t ** 2 *
774
+ (Primitives.number(segment?.control2?.x, 0) || 0) +
775
+ t ** 3 * (Primitives.number(segment?.end?.x, 0) || 0)
776
+ const y =
777
+ inverse ** 3 *
778
+ (Primitives.number(segment?.start?.y, 0) || 0) +
779
+ 3 *
780
+ inverse ** 2 *
781
+ t *
782
+ (Primitives.number(segment?.control1?.y, 0) || 0) +
783
+ 3 *
784
+ inverse *
785
+ t ** 2 *
786
+ (Primitives.number(segment?.control2?.y, 0) || 0) +
787
+ t ** 3 * (Primitives.number(segment?.end?.y, 0) || 0)
788
+ points.push(Primitives.point(x, y))
789
+ }
790
+ }
791
+ return points
792
+ }
793
+
794
+ /**
795
+ * Builds style fields for one closed primitive.
796
+ * @param {Record<string, unknown>} primitive Native primitive.
797
+ * @param {boolean} path Whether path color naming is required.
798
+ * @returns {Record<string, unknown>}
799
+ */
800
+ static #closedStyle(primitive, path) {
801
+ return {
802
+ stroke_width: Primitives.number(
803
+ primitive?.lineWidth ?? primitive?.width,
804
+ 1
805
+ ),
806
+ ...(path
807
+ ? {
808
+ stroke_color: Primitives.string(
809
+ primitive?.color,
810
+ '#000000'
811
+ )
812
+ }
813
+ : {
814
+ color: Primitives.string(primitive?.color, '#000000')
815
+ }),
816
+ fill_color: Primitives.string(primitive?.fill, '#ffffff'),
817
+ ...CircuitJsonSchematicStrokeStyle.fields(
818
+ primitive,
819
+ primitive?.lineWidth ?? primitive?.width
820
+ ),
821
+ is_filled:
822
+ primitive?.isSolid === true && primitive?.transparent !== true
823
+ }
824
+ }
825
+
826
+ /**
827
+ * Builds style fields for one path primitive.
828
+ * @param {Record<string, unknown>} primitive Native primitive.
829
+ * @returns {Record<string, unknown>}
830
+ */
831
+ static #pathStyle(primitive) {
832
+ return CircuitJsonSchematicGraphicBuilder.#closedStyle(primitive, true)
833
+ }
834
+
835
+ /**
836
+ * Returns optional ownership for one primitive.
837
+ * @param {Record<string, unknown>} primitive Native primitive.
838
+ * @param {Map<string, string>} ownerIds Owner lookup.
839
+ * @returns {{ schematic_component_id?: string }}
840
+ */
841
+ static #ownerField(primitive, ownerIds) {
842
+ const ownerId = ownerIds.get(String(primitive?.ownerIndex || ''))
843
+ return ownerId ? { schematic_component_id: ownerId } : {}
844
+ }
845
+
846
+ /**
847
+ * Adds one stable native owner mapping.
848
+ * @param {Map<string, string>} ownerIds Owner map.
849
+ * @param {unknown} ownerIndex Native owner id.
850
+ * @param {string} componentId Canonical component id.
851
+ * @returns {void}
852
+ */
853
+ static #setOwner(ownerIds, ownerIndex, componentId) {
854
+ const key = String(ownerIndex || '').trim()
855
+ if (key && !ownerIds.has(key)) ownerIds.set(key, componentId)
856
+ }
857
+
858
+ /**
859
+ * Appends one sortable group descriptor.
860
+ * @param {object[]} groups Group list.
861
+ * @param {Record<string, unknown>} source Native source row.
862
+ * @param {string} family Graphic family.
863
+ * @param {number} index Family-local index.
864
+ * @param {object[]} rows Canonical rows.
865
+ * @returns {void}
866
+ */
867
+ static #group(groups, source, family, index, rows) {
868
+ const renderOrder = Primitives.number(source?.renderOrder, index)
869
+ groups.push({
870
+ order: Number.isFinite(renderOrder) ? renderOrder : index,
871
+ familyOrder: FAMILY_ORDER.get(family) ?? FAMILY_ORDER.size,
872
+ index,
873
+ rows
874
+ })
875
+ }
876
+
877
+ /**
878
+ * Returns a deterministic primitive identity.
879
+ * @param {Record<string, unknown>} source Native source row.
880
+ * @param {number} index Family-local index.
881
+ * @returns {unknown}
882
+ */
883
+ static #identity(source, index) {
884
+ return (
885
+ source?.recordId ||
886
+ source?.uniqueId ||
887
+ source?.uuid ||
888
+ source?.id ||
889
+ source?.renderOrder ||
890
+ index
891
+ )
892
+ }
893
+
894
+ /**
895
+ * Normalizes a native arc delta without discarding full turns.
896
+ * @param {number} start Start angle.
897
+ * @param {number} end End angle.
898
+ * @returns {number}
899
+ */
900
+ static #arcDelta(start, end) {
901
+ let delta = Number(end) - Number(start)
902
+ while (delta < -360) delta += 360
903
+ while (delta > 360) delta -= 360
904
+ return delta
905
+ }
906
+ }
907
+
908
+ Object.freeze(CircuitJsonSchematicGraphicBuilder.prototype)
909
+ Object.freeze(CircuitJsonSchematicGraphicBuilder)