altium-toolkit 1.1.41 → 1.2.1

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 (59) hide show
  1. package/README.md +118 -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/release-notes-v1.2.1.md +15 -0
  13. package/docs/testing.md +43 -1
  14. package/examples/arduino-uno/PcbThreeSceneRenderer.mjs +1 -1
  15. package/examples/arduino-uno/example.mjs +1 -1
  16. package/examples/cli-utils.mjs +1 -1
  17. package/examples/corpus-smoke.mjs +1 -1
  18. package/examples/inspect-board.mjs +1 -1
  19. package/examples/library-catalog.mjs +1 -1
  20. package/examples/validate-library.mjs +1 -1
  21. package/package.json +23 -8
  22. package/spec/api-baseline-v1.1.41.json +1 -0
  23. package/spec/asset-baseline-v1.1.41.json +1 -0
  24. package/spec/feature-preservation.json +1 -0
  25. package/spec/library-scope.md +14 -4
  26. package/spec/native-source-manifest-v1.1.41.json +1 -0
  27. package/src/capabilities.mjs +4 -0
  28. package/src/convergence/AltiumCircuitJsonProjection.mjs +230 -0
  29. package/src/convergence/AltiumDocumentBuilder.mjs +167 -0
  30. package/src/convergence/AltiumExtensionResolver.mjs +98 -0
  31. package/src/convergence/AltiumProjectDocumentResolver.mjs +324 -0
  32. package/src/convergence/AltiumSchematicCoordinateProjection.mjs +130 -0
  33. package/src/convergence/AltiumWorkerClient.mjs +95 -0
  34. package/src/convergence/Parser.mjs +285 -0
  35. package/src/convergence/ParserInput.mjs +282 -0
  36. package/src/convergence/ProjectLoader.mjs +916 -0
  37. package/src/convergence/SchematicSvgRenderer.mjs +126 -0
  38. package/src/convergence/ToolkitCapabilities.mjs +49 -0
  39. package/src/core/circuit-json/CircuitJsonSchematicDocumentGraphicBuilder.mjs +785 -0
  40. package/src/core/circuit-json/CircuitJsonSchematicGraphicBuilder.mjs +909 -0
  41. package/src/core/circuit-json/CircuitJsonSchematicImageProjection.mjs +277 -0
  42. package/src/core/circuit-json/CircuitJsonSchematicStrokeStyle.mjs +47 -0
  43. package/src/extensions.mjs +11 -0
  44. package/src/index.mjs +20 -4
  45. package/src/interaction.mjs +7 -0
  46. package/src/legacy-netlist-query.mjs +11 -0
  47. package/src/legacy-parser.mjs +141 -0
  48. package/src/legacy-renderers.mjs +25 -0
  49. package/src/legacy-scene3d.mjs +10 -0
  50. package/src/manufacturing.mjs +4 -0
  51. package/src/parser.mjs +12 -138
  52. package/src/project.mjs +11 -0
  53. package/src/query.mjs +4 -0
  54. package/src/renderers.mjs +4 -22
  55. package/src/scene3d.mjs +5 -8
  56. package/src/simulation.mjs +4 -0
  57. package/src/styles/renderers.css +27 -0
  58. package/src/testing.mjs +8 -0
  59. package/src/workers/parser.worker.mjs +37 -0
@@ -0,0 +1,785 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ import { CircuitJsonModelAdapterPrimitives } from './CircuitJsonModelAdapterPrimitives.mjs'
6
+ import { CircuitJsonSchematicImageProjection } from './CircuitJsonSchematicImageProjection.mjs'
7
+ import { CircuitJsonSchematicStrokeStyle } from './CircuitJsonSchematicStrokeStyle.mjs'
8
+
9
+ const Primitives = CircuitJsonModelAdapterPrimitives
10
+ const FAMILY_ORDER = Object.freeze({
11
+ textBox: 7,
12
+ table: 8,
13
+ sheet: 9,
14
+ image: 10,
15
+ text: 11
16
+ })
17
+
18
+ /**
19
+ * Projects document-level schematic graphics onto shared CircuitJSON rows.
20
+ */
21
+ export class CircuitJsonSchematicDocumentGraphicBuilder {
22
+ /**
23
+ * Appends text boxes, tables, hierarchical sheets, and ordinary text.
24
+ * @param {object[]} groups Destination render groups.
25
+ * @param {Record<string, unknown>} schematic Native schematic read model.
26
+ * @param {string} idScope Deterministic document id scope.
27
+ * @param {Map<string, string>} ownerIds Owner-to-component lookup.
28
+ * @returns {void}
29
+ */
30
+ static append(groups, schematic, idScope, ownerIds) {
31
+ CircuitJsonSchematicDocumentGraphicBuilder.#appendTextFrames(
32
+ groups,
33
+ schematic,
34
+ idScope,
35
+ ownerIds
36
+ )
37
+ CircuitJsonSchematicDocumentGraphicBuilder.#appendTables(
38
+ groups,
39
+ schematic,
40
+ idScope,
41
+ ownerIds
42
+ )
43
+ CircuitJsonSchematicDocumentGraphicBuilder.#appendSheets(
44
+ groups,
45
+ schematic,
46
+ idScope,
47
+ ownerIds
48
+ )
49
+ CircuitJsonSchematicDocumentGraphicBuilder.#appendImages(
50
+ groups,
51
+ schematic,
52
+ idScope,
53
+ ownerIds
54
+ )
55
+ CircuitJsonSchematicDocumentGraphicBuilder.#appendTexts(
56
+ groups,
57
+ schematic,
58
+ idScope,
59
+ ownerIds
60
+ )
61
+ }
62
+
63
+ /**
64
+ * Appends boxed notes as one outline and one positioned text row.
65
+ * @param {object[]} groups Render groups.
66
+ * @param {Record<string, unknown>} schematic Native schematic.
67
+ * @param {string} idScope Document id scope.
68
+ * @param {Map<string, string>} ownerIds Owner-to-component lookup.
69
+ * @returns {void}
70
+ */
71
+ static #appendTextFrames(groups, schematic, idScope, ownerIds) {
72
+ for (const [index, frame] of Primitives.array(
73
+ schematic.textFrames
74
+ ).entries()) {
75
+ const left = Math.min(
76
+ Primitives.number(frame?.x, 0) || 0,
77
+ Primitives.number(frame?.cornerX, frame?.x) || 0
78
+ )
79
+ const firstY = Primitives.number(frame?.y, 0) || 0
80
+ const secondY =
81
+ Primitives.number(frame?.cornerY, frame?.y) || firstY
82
+ const bottom = Math.min(firstY, secondY)
83
+ const top = Math.max(firstY, secondY)
84
+ const width = Math.abs(
85
+ Primitives.number(
86
+ frame?.width,
87
+ (Primitives.number(frame?.cornerX, 0) || 0) - left
88
+ ) || 0
89
+ )
90
+ const height = Math.abs(
91
+ Primitives.number(frame?.height, top - bottom) || 0
92
+ )
93
+ const margin = Math.max(
94
+ Primitives.number(frame?.textMargin, 0) || 0,
95
+ 0
96
+ )
97
+ const identity =
98
+ CircuitJsonSchematicDocumentGraphicBuilder.#identity(
99
+ frame,
100
+ index
101
+ )
102
+ const owner =
103
+ CircuitJsonSchematicDocumentGraphicBuilder.#ownerField(
104
+ frame,
105
+ ownerIds
106
+ )
107
+ CircuitJsonSchematicDocumentGraphicBuilder.#group(
108
+ groups,
109
+ frame,
110
+ FAMILY_ORDER.textBox,
111
+ index,
112
+ [
113
+ {
114
+ type: 'schematic_rect',
115
+ schematic_rect_id: Primitives.id(idScope, [
116
+ 'schematic_rect',
117
+ 'text_box',
118
+ identity
119
+ ]),
120
+ center: Primitives.point(
121
+ left + width / 2,
122
+ bottom + height / 2
123
+ ),
124
+ width,
125
+ height,
126
+ stroke_width:
127
+ frame?.showBorder === false
128
+ ? 0
129
+ : Primitives.number(frame?.borderWidth, 1),
130
+ color: Primitives.string(
131
+ frame?.borderColor || frame?.color,
132
+ '#000000'
133
+ ),
134
+ fill_color: Primitives.string(frame?.fill, '#ffffff'),
135
+ ...CircuitJsonSchematicStrokeStyle.fields(
136
+ frame,
137
+ frame?.borderWidth ?? frame?.lineWidth
138
+ ),
139
+ is_filled: frame?.isSolid === true,
140
+ ...owner
141
+ },
142
+ {
143
+ type: 'schematic_text',
144
+ schematic_text_id: Primitives.id(idScope, [
145
+ 'schematic_text',
146
+ 'text_box',
147
+ identity
148
+ ]),
149
+ text: Primitives.string(frame?.text, ''),
150
+ position: Primitives.point(left + margin, top - margin),
151
+ font_size: Primitives.number(frame?.font?.size, 0.18),
152
+ rotation:
153
+ CircuitJsonSchematicDocumentGraphicBuilder.#textRotation(
154
+ frame
155
+ ),
156
+ anchor: 'top_left',
157
+ color: Primitives.string(frame?.color, '#000000'),
158
+ ...owner
159
+ }
160
+ ]
161
+ )
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Appends generic native table read models when supplied by a parser.
167
+ * @param {object[]} groups Render groups.
168
+ * @param {Record<string, unknown>} schematic Native schematic.
169
+ * @param {string} idScope Document id scope.
170
+ * @param {Map<string, string>} ownerIds Owner-to-component lookup.
171
+ * @returns {void}
172
+ */
173
+ static #appendTables(groups, schematic, idScope, ownerIds) {
174
+ for (const [index, table] of Primitives.array(
175
+ schematic.tables
176
+ ).entries()) {
177
+ const tableId = Primitives.id(idScope, [
178
+ 'schematic_table',
179
+ CircuitJsonSchematicDocumentGraphicBuilder.#identity(
180
+ table,
181
+ index
182
+ )
183
+ ])
184
+ const rows = [
185
+ {
186
+ type: 'schematic_table',
187
+ schematic_table_id: tableId,
188
+ anchor_position: Primitives.point(
189
+ table?.anchorPosition?.x ?? table?.x,
190
+ table?.anchorPosition?.y ?? table?.y
191
+ ),
192
+ anchor: CircuitJsonSchematicDocumentGraphicBuilder.#tableAnchor(
193
+ table?.anchor
194
+ ),
195
+ column_widths: Primitives.array(
196
+ table?.columnWidths ?? table?.column_widths
197
+ ).map((width) => Primitives.number(width, 0)),
198
+ row_heights: Primitives.array(
199
+ table?.rowHeights ?? table?.row_heights
200
+ ).map((height) => Primitives.number(height, 0)),
201
+ border_width: Primitives.number(
202
+ table?.borderWidth ?? table?.border_width,
203
+ 0
204
+ ),
205
+ ...CircuitJsonSchematicDocumentGraphicBuilder.#ownerField(
206
+ table,
207
+ ownerIds
208
+ )
209
+ }
210
+ ]
211
+ for (const [cellIndex, cell] of Primitives.array(
212
+ table?.cells
213
+ ).entries()) {
214
+ rows.push(
215
+ CircuitJsonSchematicDocumentGraphicBuilder.#tableCell(
216
+ cell,
217
+ cellIndex,
218
+ table,
219
+ tableId,
220
+ idScope
221
+ )
222
+ )
223
+ }
224
+ CircuitJsonSchematicDocumentGraphicBuilder.#group(
225
+ groups,
226
+ table,
227
+ FAMILY_ORDER.table,
228
+ index,
229
+ rows
230
+ )
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Appends hierarchical sheet boxes and entry ports.
236
+ * @param {object[]} groups Render groups.
237
+ * @param {Record<string, unknown>} schematic Native schematic.
238
+ * @param {string} idScope Document id scope.
239
+ * @param {Map<string, string>} ownerIds Owner-to-component lookup.
240
+ * @returns {void}
241
+ */
242
+ static #appendSheets(groups, schematic, idScope, ownerIds) {
243
+ const sheets = Primitives.array(schematic.sheetSymbols)
244
+ const sheetRows = new Map()
245
+ for (const [index, sheet] of sheets.entries()) {
246
+ const identity =
247
+ CircuitJsonSchematicDocumentGraphicBuilder.#identity(
248
+ sheet,
249
+ index
250
+ )
251
+ const sheetSymbolId = Primitives.id(idScope, [
252
+ 'schematic_sheet_symbol',
253
+ identity
254
+ ])
255
+ const sourceComponentId = Primitives.id(idScope, [
256
+ 'source_component',
257
+ 'sheet',
258
+ identity
259
+ ])
260
+ const height = Math.abs(Primitives.number(sheet?.height, 0) || 0)
261
+ const y = Primitives.number(sheet?.y, 0) || 0
262
+ const width = Math.abs(Primitives.number(sheet?.width, 0) || 0)
263
+ const x = Primitives.number(sheet?.x, 0) || 0
264
+ sheetRows.set(sheet, { sheetSymbolId, sourceComponentId })
265
+ CircuitJsonSchematicDocumentGraphicBuilder.#group(
266
+ groups,
267
+ sheet,
268
+ FAMILY_ORDER.sheet,
269
+ index,
270
+ [
271
+ {
272
+ type: 'source_component',
273
+ source_component_id: sourceComponentId,
274
+ name: Primitives.string(
275
+ sheet?.name || sheet?.fileName,
276
+ `Sheet ${index + 1}`
277
+ ),
278
+ ftype: 'simple_chip'
279
+ },
280
+ {
281
+ type: 'schematic_sheet_symbol',
282
+ schematic_sheet_symbol_id: sheetSymbolId,
283
+ name: Primitives.string(
284
+ sheet?.name || sheet?.fileName,
285
+ `Sheet ${index + 1}`
286
+ ),
287
+ ...(sheet?.fileName
288
+ ? { source_file_name: String(sheet.fileName) }
289
+ : {}),
290
+ center: Primitives.point(x + width / 2, y - height / 2),
291
+ width,
292
+ height,
293
+ stroke_width: Primitives.number(sheet?.lineWidth, 1),
294
+ color: Primitives.string(sheet?.color, '#000000'),
295
+ fill_color: Primitives.string(sheet?.fill, '#ffffff'),
296
+ ...CircuitJsonSchematicStrokeStyle.fields(
297
+ sheet,
298
+ sheet?.lineWidth
299
+ ),
300
+ is_filled:
301
+ sheet?.isSolid === true &&
302
+ sheet?.transparent !== true,
303
+ render_order:
304
+ CircuitJsonSchematicDocumentGraphicBuilder.#renderOrder(
305
+ sheet,
306
+ index
307
+ ),
308
+ ...CircuitJsonSchematicDocumentGraphicBuilder.#ownerField(
309
+ sheet,
310
+ ownerIds
311
+ )
312
+ }
313
+ ]
314
+ )
315
+ }
316
+
317
+ for (const [index, entry] of Primitives.array(
318
+ schematic.sheetEntries
319
+ ).entries()) {
320
+ const sheet =
321
+ CircuitJsonSchematicDocumentGraphicBuilder.#sheetForEntry(
322
+ entry,
323
+ sheets
324
+ )
325
+ const parent = sheetRows.get(sheet)
326
+ if (!parent) continue
327
+ const identity =
328
+ CircuitJsonSchematicDocumentGraphicBuilder.#identity(
329
+ entry,
330
+ index
331
+ )
332
+ const sourcePortId = Primitives.id(idScope, [
333
+ 'source_port',
334
+ 'sheet',
335
+ parent.sheetSymbolId,
336
+ identity
337
+ ])
338
+ const side = CircuitJsonSchematicDocumentGraphicBuilder.#sheetSide(
339
+ entry?.side
340
+ )
341
+ CircuitJsonSchematicDocumentGraphicBuilder.#group(
342
+ groups,
343
+ entry,
344
+ FAMILY_ORDER.sheet,
345
+ sheets.length + index,
346
+ [
347
+ {
348
+ type: 'source_port',
349
+ source_port_id: sourcePortId,
350
+ source_component_id: parent.sourceComponentId,
351
+ name: Primitives.string(
352
+ entry?.name,
353
+ `PORT_${index + 1}`
354
+ )
355
+ },
356
+ {
357
+ type: 'schematic_port',
358
+ schematic_port_id: Primitives.id(idScope, [
359
+ 'schematic_port',
360
+ 'sheet',
361
+ parent.sheetSymbolId,
362
+ identity
363
+ ]),
364
+ source_port_id: sourcePortId,
365
+ schematic_sheet_symbol_id: parent.sheetSymbolId,
366
+ display_pin_label: Primitives.string(entry?.name, ''),
367
+ center: Primitives.point(entry?.x, entry?.y),
368
+ side_of_component: side,
369
+ facing_direction:
370
+ side === 'top'
371
+ ? 'up'
372
+ : side === 'bottom'
373
+ ? 'down'
374
+ : side,
375
+ has_input_arrow:
376
+ entry?.direction === 'input' ||
377
+ entry?.direction === 'bidirectional',
378
+ has_output_arrow:
379
+ entry?.direction === 'output' ||
380
+ entry?.direction === 'bidirectional'
381
+ }
382
+ ]
383
+ )
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Appends asset-backed schematic image placements.
389
+ * @param {object[]} groups Render groups.
390
+ * @param {Record<string, unknown>} schematic Native schematic.
391
+ * @param {string} idScope Document id scope.
392
+ * @param {Map<string, string>} ownerIds Owner-to-component lookup.
393
+ * @returns {void}
394
+ */
395
+ static #appendImages(groups, schematic, idScope, ownerIds) {
396
+ for (const [index, image] of Primitives.array(
397
+ schematic.images
398
+ ).entries()) {
399
+ const row = CircuitJsonSchematicImageProjection.element(
400
+ image,
401
+ index,
402
+ idScope,
403
+ ownerIds
404
+ )
405
+ if (!row) continue
406
+ CircuitJsonSchematicDocumentGraphicBuilder.#group(
407
+ groups,
408
+ image,
409
+ FAMILY_ORDER.image,
410
+ index,
411
+ [row]
412
+ )
413
+ }
414
+ }
415
+
416
+ /**
417
+ * Appends non-frame schematic text and net labels.
418
+ * @param {object[]} groups Render groups.
419
+ * @param {Record<string, unknown>} schematic Native schematic.
420
+ * @param {string} idScope Document id scope.
421
+ * @param {Map<string, string>} ownerIds Owner-to-component lookup.
422
+ * @returns {void}
423
+ */
424
+ static #appendTexts(groups, schematic, idScope, ownerIds) {
425
+ const hasTextFrames = Primitives.array(schematic.textFrames).length > 0
426
+ for (const [index, text] of Primitives.array(
427
+ schematic.texts
428
+ ).entries()) {
429
+ if (hasTextFrames && String(text?.recordType || '') === '28') {
430
+ continue
431
+ }
432
+ const textValue = Primitives.string(
433
+ text?.text || text?.value || text?.name,
434
+ ''
435
+ )
436
+ const center = Primitives.point(text?.x, text?.y)
437
+ const row = Primitives.isNetLabel(text)
438
+ ? {
439
+ type: 'schematic_net_label',
440
+ schematic_net_label_id: Primitives.id(idScope, [
441
+ 'schematic_net_label',
442
+ CircuitJsonSchematicDocumentGraphicBuilder.#identity(
443
+ text,
444
+ index
445
+ )
446
+ ]),
447
+ source_net_id: Primitives.sourceNetId(
448
+ idScope,
449
+ textValue || index
450
+ ),
451
+ text: textValue,
452
+ center,
453
+ anchor_position: center,
454
+ anchor_side: 'top',
455
+ rotation:
456
+ CircuitJsonSchematicDocumentGraphicBuilder.#textRotation(
457
+ text
458
+ )
459
+ }
460
+ : {
461
+ type: 'schematic_text',
462
+ schematic_text_id: Primitives.id(idScope, [
463
+ 'schematic_text',
464
+ CircuitJsonSchematicDocumentGraphicBuilder.#identity(
465
+ text,
466
+ index
467
+ )
468
+ ]),
469
+ text: textValue,
470
+ position: center,
471
+ font_size: Primitives.number(
472
+ text?.fontSize || text?.size,
473
+ 0.18
474
+ ),
475
+ rotation:
476
+ CircuitJsonSchematicDocumentGraphicBuilder.#textRotation(
477
+ text
478
+ ),
479
+ anchor: CircuitJsonSchematicDocumentGraphicBuilder.#textAnchor(
480
+ text
481
+ ),
482
+ color: Primitives.string(text?.color, '#000000'),
483
+ ...CircuitJsonSchematicDocumentGraphicBuilder.#ownerField(
484
+ text,
485
+ ownerIds
486
+ )
487
+ }
488
+ CircuitJsonSchematicDocumentGraphicBuilder.#group(
489
+ groups,
490
+ text,
491
+ FAMILY_ORDER.text,
492
+ index,
493
+ [row]
494
+ )
495
+ }
496
+ }
497
+
498
+ /**
499
+ * Builds one table cell from explicit or derived geometry.
500
+ * @param {Record<string, unknown>} cell Native table cell.
501
+ * @param {number} index Cell index.
502
+ * @param {Record<string, unknown>} table Native table.
503
+ * @param {string} tableId Canonical table id.
504
+ * @param {string} idScope Document id scope.
505
+ * @returns {object}
506
+ */
507
+ static #tableCell(cell, index, table, tableId, idScope) {
508
+ const startRow = Math.max(
509
+ Math.trunc(
510
+ Primitives.number(
511
+ cell?.startRowIndex ?? cell?.row ?? cell?.start_row_index,
512
+ 0
513
+ ) || 0
514
+ ),
515
+ 0
516
+ )
517
+ const endRow = Math.max(
518
+ Math.trunc(
519
+ Primitives.number(
520
+ cell?.endRowIndex ?? cell?.end_row_index,
521
+ startRow
522
+ ) || startRow
523
+ ),
524
+ startRow
525
+ )
526
+ const startColumn = Math.max(
527
+ Math.trunc(
528
+ Primitives.number(
529
+ cell?.startColumnIndex ??
530
+ cell?.column ??
531
+ cell?.start_column_index,
532
+ 0
533
+ ) || 0
534
+ ),
535
+ 0
536
+ )
537
+ const endColumn = Math.max(
538
+ Math.trunc(
539
+ Primitives.number(
540
+ cell?.endColumnIndex ?? cell?.end_column_index,
541
+ startColumn
542
+ ) || startColumn
543
+ ),
544
+ startColumn
545
+ )
546
+ const columnWidths = Primitives.array(
547
+ table?.columnWidths ?? table?.column_widths
548
+ )
549
+ const rowHeights = Primitives.array(
550
+ table?.rowHeights ?? table?.row_heights
551
+ )
552
+ const anchor = Primitives.point(
553
+ table?.anchorPosition?.x ?? table?.x,
554
+ table?.anchorPosition?.y ?? table?.y
555
+ )
556
+ const beforeX = CircuitJsonSchematicDocumentGraphicBuilder.#sum(
557
+ columnWidths.slice(0, startColumn)
558
+ )
559
+ const width = CircuitJsonSchematicDocumentGraphicBuilder.#sum(
560
+ columnWidths.slice(startColumn, endColumn + 1)
561
+ )
562
+ const beforeY = CircuitJsonSchematicDocumentGraphicBuilder.#sum(
563
+ rowHeights.slice(0, startRow)
564
+ )
565
+ const height = CircuitJsonSchematicDocumentGraphicBuilder.#sum(
566
+ rowHeights.slice(startRow, endRow + 1)
567
+ )
568
+ return {
569
+ type: 'schematic_table_cell',
570
+ schematic_table_cell_id: Primitives.id(idScope, [
571
+ 'schematic_table_cell',
572
+ tableId,
573
+ CircuitJsonSchematicDocumentGraphicBuilder.#identity(
574
+ cell,
575
+ index
576
+ )
577
+ ]),
578
+ schematic_table_id: tableId,
579
+ text: Primitives.string(cell?.text, ''),
580
+ center: Primitives.point(
581
+ cell?.center?.x ?? anchor.x + beforeX + width / 2,
582
+ cell?.center?.y ?? anchor.y + beforeY + height / 2
583
+ ),
584
+ width: Primitives.number(cell?.width, width),
585
+ height: Primitives.number(cell?.height, height),
586
+ start_column_index: startColumn,
587
+ end_column_index: endColumn,
588
+ start_row_index: startRow,
589
+ end_row_index: endRow,
590
+ horizontal_align:
591
+ CircuitJsonSchematicDocumentGraphicBuilder.#horizontalAlign(
592
+ cell?.horizontalAlign ?? cell?.alignment
593
+ ),
594
+ vertical_align:
595
+ CircuitJsonSchematicDocumentGraphicBuilder.#verticalAlign(
596
+ cell?.verticalAlign
597
+ )
598
+ }
599
+ }
600
+
601
+ /**
602
+ * Sums numeric size values.
603
+ * @param {unknown[]} values Size values.
604
+ * @returns {number}
605
+ */
606
+ static #sum(values) {
607
+ return values.reduce(
608
+ (sum, value) => sum + (Primitives.number(value, 0) || 0),
609
+ 0
610
+ )
611
+ }
612
+
613
+ /**
614
+ * Adds one sortable group descriptor.
615
+ * @param {object[]} groups Group list.
616
+ * @param {Record<string, unknown>} source Native source row.
617
+ * @param {number} familyOrder Graphic family order.
618
+ * @param {number} index Family-local index.
619
+ * @param {object[]} rows Canonical rows.
620
+ * @returns {void}
621
+ */
622
+ static #group(groups, source, familyOrder, index, rows) {
623
+ const renderOrder = Primitives.number(source?.renderOrder, index)
624
+ groups.push({
625
+ order: Number.isFinite(renderOrder) ? renderOrder : index,
626
+ familyOrder,
627
+ index,
628
+ rows
629
+ })
630
+ }
631
+
632
+ /**
633
+ * Returns a deterministic native identity.
634
+ * @param {Record<string, unknown>} source Native row.
635
+ * @param {number} index Family-local index.
636
+ * @returns {unknown}
637
+ */
638
+ static #identity(source, index) {
639
+ return (
640
+ source?.recordId ||
641
+ source?.uniqueId ||
642
+ source?.uuid ||
643
+ source?.id ||
644
+ source?.renderOrder ||
645
+ index
646
+ )
647
+ }
648
+
649
+ /**
650
+ * Resolves one safe authored render order.
651
+ * @param {Record<string, unknown>} source Native row.
652
+ * @param {number} fallback Fallback order.
653
+ * @returns {number}
654
+ */
655
+ static #renderOrder(source, fallback) {
656
+ const order = Primitives.number(source?.renderOrder, fallback)
657
+ return Number.isSafeInteger(order) ? order : fallback
658
+ }
659
+
660
+ /**
661
+ * Returns optional component ownership.
662
+ * @param {Record<string, unknown>} source Native row.
663
+ * @param {Map<string, string>} ownerIds Owner lookup.
664
+ * @returns {{ schematic_component_id?: string }}
665
+ */
666
+ static #ownerField(source, ownerIds) {
667
+ const ownerId = ownerIds.get(String(source?.ownerIndex || ''))
668
+ return ownerId ? { schematic_component_id: ownerId } : {}
669
+ }
670
+
671
+ /**
672
+ * Finds the parent sheet whose perimeter contains an entry.
673
+ * @param {Record<string, unknown>} entry Native sheet entry.
674
+ * @param {Record<string, unknown>[]} sheets Native sheets.
675
+ * @returns {Record<string, unknown> | undefined}
676
+ */
677
+ static #sheetForEntry(entry, sheets) {
678
+ const x = Primitives.number(entry?.x, 0) || 0
679
+ const y = Primitives.number(entry?.y, 0) || 0
680
+ const epsilon = 0.000001
681
+ return sheets.find((sheet) => {
682
+ const left = Primitives.number(sheet?.x, 0) || 0
683
+ const top = Primitives.number(sheet?.y, 0) || 0
684
+ const right = left + (Primitives.number(sheet?.width, 0) || 0)
685
+ const bottom = top - (Primitives.number(sheet?.height, 0) || 0)
686
+ const withinX = x >= left - epsilon && x <= right + epsilon
687
+ const withinY = y >= bottom - epsilon && y <= top + epsilon
688
+ const onVertical =
689
+ Math.abs(x - left) <= epsilon || Math.abs(x - right) <= epsilon
690
+ const onHorizontal =
691
+ Math.abs(y - top) <= epsilon || Math.abs(y - bottom) <= epsilon
692
+ return withinX && withinY && (onVertical || onHorizontal)
693
+ })
694
+ }
695
+
696
+ /**
697
+ * Normalizes a sheet-entry side.
698
+ * @param {unknown} side Native side.
699
+ * @returns {'left' | 'right' | 'top' | 'bottom'}
700
+ */
701
+ static #sheetSide(side) {
702
+ const normalized = String(side || '').toLowerCase()
703
+ return ['left', 'right', 'top', 'bottom'].includes(normalized)
704
+ ? normalized
705
+ : 'left'
706
+ }
707
+
708
+ /**
709
+ * Normalizes one text anchor.
710
+ * @param {Record<string, unknown>} text Native text.
711
+ * @returns {string}
712
+ */
713
+ static #textAnchor(text) {
714
+ if (text?.verticalAnchor === 'top') {
715
+ if (text?.anchor === 'end') return 'top_right'
716
+ if (text?.anchor === 'middle') return 'top_center'
717
+ return 'top_left'
718
+ }
719
+ if (text?.anchor === 'end') return 'right'
720
+ if (text?.anchor === 'start') return 'left'
721
+ return 'center'
722
+ }
723
+
724
+ /**
725
+ * Preserves the historical signed SVG rotation for Altium text.
726
+ * @param {Record<string, unknown>} text Native text.
727
+ * @returns {number} Signed canonical rotation.
728
+ */
729
+ static #textRotation(text) {
730
+ const rotation = Primitives.number(text?.rotation, 0) || 0
731
+ if (!rotation) return 0
732
+ const signed =
733
+ Number(text?.sourceOrientation || 0) === 3 ? rotation : -rotation
734
+ return text?.isMirrored === true ? -signed : signed
735
+ }
736
+
737
+ /**
738
+ * Normalizes one table anchor.
739
+ * @param {unknown} anchor Native anchor.
740
+ * @returns {string}
741
+ */
742
+ static #tableAnchor(anchor) {
743
+ const normalized = String(anchor || '').toLowerCase()
744
+ return [
745
+ 'top_left',
746
+ 'top_center',
747
+ 'top_right',
748
+ 'center_left',
749
+ 'center',
750
+ 'center_right',
751
+ 'bottom_left',
752
+ 'bottom_center',
753
+ 'bottom_right'
754
+ ].includes(normalized)
755
+ ? normalized
756
+ : 'top_left'
757
+ }
758
+
759
+ /**
760
+ * Normalizes horizontal table-cell alignment.
761
+ * @param {unknown} alignment Native alignment.
762
+ * @returns {'left' | 'center' | 'right'}
763
+ */
764
+ static #horizontalAlign(alignment) {
765
+ const normalized = String(alignment || '').toLowerCase()
766
+ return ['left', 'center', 'right'].includes(normalized)
767
+ ? normalized
768
+ : 'left'
769
+ }
770
+
771
+ /**
772
+ * Normalizes vertical table-cell alignment.
773
+ * @param {unknown} alignment Native alignment.
774
+ * @returns {'top' | 'middle' | 'bottom'}
775
+ */
776
+ static #verticalAlign(alignment) {
777
+ const normalized = String(alignment || '').toLowerCase()
778
+ return ['top', 'middle', 'bottom'].includes(normalized)
779
+ ? normalized
780
+ : 'middle'
781
+ }
782
+ }
783
+
784
+ Object.freeze(CircuitJsonSchematicDocumentGraphicBuilder.prototype)
785
+ Object.freeze(CircuitJsonSchematicDocumentGraphicBuilder)