altium-toolkit 1.1.2 → 1.1.22

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 (57) hide show
  1. package/docs/api.md +37 -0
  2. package/docs/model-format.md +18 -0
  3. package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +2 -2
  4. package/docs/testing.md +5 -0
  5. package/package.json +1 -1
  6. package/spec/library-scope.md +5 -0
  7. package/src/core/altium/AltiumLayoutParser.mjs +275 -13
  8. package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
  9. package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
  10. package/src/core/altium/AltiumParser.mjs +245 -10
  11. package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
  12. package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
  13. package/src/core/altium/AsciiRecordParser.mjs +43 -11
  14. package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
  15. package/src/core/altium/PcbEmbeddedFontExtractor.mjs +186 -43
  16. package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
  17. package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
  18. package/src/core/altium/PrintableTextDecoder.mjs +133 -13
  19. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +13 -0
  20. package/src/core/altium/SchematicComponentTextResolver.mjs +40 -1
  21. package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
  22. package/src/core/altium/SchematicImageParser.mjs +291 -6
  23. package/src/core/altium/SchematicMultipartDesignatorNormalizer.mjs +164 -0
  24. package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +2 -0
  25. package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
  26. package/src/core/altium/SchematicPinParser.mjs +262 -24
  27. package/src/core/altium/SchematicPrimitiveParser.mjs +116 -8
  28. package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
  29. package/src/core/altium/SchematicStreamExtractor.mjs +62 -15
  30. package/src/core/altium/SchematicTextParser.mjs +125 -11
  31. package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
  32. package/src/core/altium/SourceBundleExporter.mjs +156 -0
  33. package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
  34. package/src/core/altium/SourceComponentClient.mjs +239 -0
  35. package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
  36. package/src/parser.mjs +8 -0
  37. package/src/styles/altium-renderers.css +6 -6
  38. package/src/ui/PcbArcUtils.mjs +19 -2
  39. package/src/ui/PcbScene3dBuilder.mjs +202 -20
  40. package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
  41. package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
  42. package/src/ui/SchematicColorResolver.mjs +263 -0
  43. package/src/ui/SchematicContentLayout.mjs +58 -1
  44. package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
  45. package/src/ui/SchematicImageRenderer.mjs +125 -10
  46. package/src/ui/SchematicJunctionRenderer.mjs +1 -1
  47. package/src/ui/SchematicLineColorResolver.mjs +88 -0
  48. package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
  49. package/src/ui/SchematicNoteRenderer.mjs +87 -7
  50. package/src/ui/SchematicOwnerPinLabelLayout.mjs +560 -10
  51. package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
  52. package/src/ui/SchematicPinSvgRenderer.mjs +397 -48
  53. package/src/ui/SchematicPowerDiagramImageProcessor.mjs +970 -0
  54. package/src/ui/SchematicPowerDiagramLineMasks.mjs +631 -0
  55. package/src/ui/SchematicPowerPortRenderer.mjs +1 -1
  56. package/src/ui/SchematicShapeRenderer.mjs +109 -24
  57. package/src/ui/SchematicSvgRenderer.mjs +1210 -71
@@ -12,6 +12,8 @@ import { OleConstants } from '../ole/OleConstants.mjs'
12
12
  * containers.
13
13
  */
14
14
  export class SchematicStreamExtractor {
15
+ static #AUXILIARY_PRINTABLE_STREAMS = new Set(['Additional'])
16
+
15
17
  /**
16
18
  * Returns true when one buffer starts with the OLE compound-document
17
19
  * signature.
@@ -38,9 +40,10 @@ export class SchematicStreamExtractor {
38
40
  }
39
41
 
40
42
  /**
41
- * Extracts schematic records from the logical `FileHeader` stream.
43
+ * Extracts schematic records from the logical `FileHeader` stream and
44
+ * Altium's auxiliary printable schematic streams.
42
45
  * @param {ArrayBuffer} arrayBuffer
43
- * @returns {{ records: Array<{ raw: string, fields: Record<string, string | string[]>, sourceStream: string }>, streamNames: string[] } | null}
46
+ * @returns {{ records: Array<{ raw: string, fields: Record<string, string | string[]>, sourceStream: string }>, streamNames: string[], embeddedFiles?: object } | null}
44
47
  */
45
48
  static extractFromArrayBuffer(arrayBuffer) {
46
49
  if (!SchematicStreamExtractor.isCompoundDocument(arrayBuffer)) {
@@ -55,20 +58,28 @@ export class SchematicStreamExtractor {
55
58
  return null
56
59
  }
57
60
 
58
- let fileHeaderBytes
61
+ let fileHeaderRecords
59
62
 
60
63
  try {
61
- fileHeaderBytes = compoundDocument.getStream('FileHeader')
64
+ fileHeaderRecords = SchematicStreamExtractor.#parseStreamRecords(
65
+ compoundDocument,
66
+ 'FileHeader'
67
+ )
62
68
  } catch {
63
69
  return null
64
70
  }
65
71
 
66
- const records = AsciiRecordParser.parse(
67
- SchematicStreamExtractor.#toArrayBuffer(fileHeaderBytes)
68
- ).map((record) => ({
69
- ...record,
70
- sourceStream: 'FileHeader'
71
- }))
72
+ const streamNames = compoundDocument.listStreams()
73
+ const auxiliaryStreamNames = streamNames.filter((streamName) =>
74
+ SchematicStreamExtractor.#isAuxiliaryPrintableStream(streamName)
75
+ )
76
+ const auxiliaryRecords = auxiliaryStreamNames.flatMap((streamName) =>
77
+ SchematicStreamExtractor.#parseStreamRecords(
78
+ compoundDocument,
79
+ streamName
80
+ )
81
+ )
82
+ const records = [...fileHeaderRecords, ...auxiliaryRecords]
72
83
 
73
84
  if (!records.length) {
74
85
  return null
@@ -76,18 +87,54 @@ export class SchematicStreamExtractor {
76
87
 
77
88
  return {
78
89
  records,
79
- streamNames: compoundDocument.listStreams(),
90
+ streamNames,
80
91
  embeddedFiles: EmbeddedFileInventoryBuilder.buildFromStreams(
81
92
  new Map(
82
- compoundDocument
83
- .listStreams()
84
- .map((name) => [name, compoundDocument.getStream(name)])
93
+ streamNames.map((name) => [
94
+ name,
95
+ compoundDocument.getStream(name)
96
+ ])
85
97
  ),
86
- { skipStreamNames: ['FileHeader'] }
98
+ {
99
+ skipStreamNames: ['FileHeader', ...auxiliaryStreamNames]
100
+ }
87
101
  )
88
102
  }
89
103
  }
90
104
 
105
+ /**
106
+ * Parses printable records from one compound-document stream.
107
+ * @param {OleCompoundDocument} compoundDocument
108
+ * @param {string} streamName
109
+ * @returns {Array<{ raw: string, fields: Record<string, string | string[]>, sourceStream: string }>}
110
+ */
111
+ static #parseStreamRecords(compoundDocument, streamName) {
112
+ return AsciiRecordParser.parse(
113
+ SchematicStreamExtractor.#toArrayBuffer(
114
+ compoundDocument.getStream(streamName)
115
+ )
116
+ ).map((record) => ({
117
+ ...record,
118
+ sourceStream: streamName
119
+ }))
120
+ }
121
+
122
+ /**
123
+ * Returns true for known non-embedded schematic streams with printable
124
+ * primitive records.
125
+ * @param {string} streamName
126
+ * @returns {boolean}
127
+ */
128
+ static #isAuxiliaryPrintableStream(streamName) {
129
+ const leafName = String(streamName || '')
130
+ .split('/')
131
+ .at(-1)
132
+
133
+ return SchematicStreamExtractor.#AUXILIARY_PRINTABLE_STREAMS.has(
134
+ leafName
135
+ )
136
+ }
137
+
91
138
  /**
92
139
  * Returns an ArrayBuffer view over one byte slice.
93
140
  * @param {Uint8Array} bytes
@@ -17,6 +17,10 @@ export class SchematicTextParser {
17
17
  const metadata = {}
18
18
 
19
19
  for (const record of records) {
20
+ if (ParserUtils.getField(record.fields, 'OwnerIndex')) {
21
+ continue
22
+ }
23
+
20
24
  const name = ParserUtils.getField(record.fields, 'Name').trim()
21
25
  const value = ParserUtils.getDisplayText(record.fields)
22
26
 
@@ -30,6 +34,39 @@ export class SchematicTextParser {
30
34
  return metadata
31
35
  }
32
36
 
37
+ /**
38
+ * Extracts owner-local parameter values used by component text templates.
39
+ * @param {{ fields: Record<string, string | string[]> }[]} records
40
+ * @returns {Map<string, Record<string, string>>}
41
+ */
42
+ static extractSchematicOwnerMetadata(records) {
43
+ const ownerMetadata = new Map()
44
+
45
+ for (const record of records) {
46
+ const ownerIndex = ParserUtils.getField(record.fields, 'OwnerIndex')
47
+ const name = ParserUtils.getField(record.fields, 'Name').trim()
48
+ const value = ParserUtils.getDisplayText(record.fields)
49
+
50
+ if (
51
+ !ownerIndex ||
52
+ !name ||
53
+ !value ||
54
+ value === '*' ||
55
+ String(value).trim().startsWith('=')
56
+ ) {
57
+ continue
58
+ }
59
+
60
+ if (!ownerMetadata.has(ownerIndex)) {
61
+ ownerMetadata.set(ownerIndex, {})
62
+ }
63
+
64
+ ownerMetadata.get(ownerIndex)[name.toLowerCase()] = value
65
+ }
66
+
67
+ return ownerMetadata
68
+ }
69
+
33
70
  /**
34
71
  * Builds a font table from the sheet header.
35
72
  * @param {Record<string, string | string[]> | undefined} fields
@@ -96,18 +133,30 @@ export class SchematicTextParser {
96
133
  * @param {Record<string, string>} metadata
97
134
  * @param {{ width: number, marginWidth: number, titleBlockOn?: boolean }} sheet
98
135
  * @param {Record<string, { size: number, family: string, bold: boolean, italic?: boolean, rotation: number }>} fonts
136
+ * @param {Map<string, Record<string, string>>} [ownerMetadata]
99
137
  * @returns {{ x: number, y: number, text: string, color: string, hidden: boolean, name: string, ownerIndex?: string, recordType: string, style: number, fontSize: number, fontFamily: string, fontWeight: number, fontStyle?: string, rotation: number, sourceOrientation?: number, isMirrored?: boolean, anchor: 'start' | 'middle' | 'end', powerPortDirection?: 'up' | 'down' | 'left' | 'right', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] } | null}
100
138
  */
101
- static normalizeSchematicTextRecord(fields, metadata, sheet, fonts) {
139
+ static normalizeSchematicTextRecord(
140
+ fields,
141
+ metadata,
142
+ sheet,
143
+ fonts,
144
+ ownerMetadata = new Map()
145
+ ) {
102
146
  const x = ParserUtils.parseNumericField(fields, 'Location.X')
103
147
  const y = ParserUtils.parseNumericField(fields, 'Location.Y')
104
148
  const hidden = ParserUtils.parseBoolean(fields.IsHidden)
105
149
  const name = ParserUtils.getField(fields, 'Name')
106
150
  const rawText = ParserUtils.getDisplayText(fields)
107
151
  const recordType = ParserUtils.getField(fields, 'RECORD')
152
+ const ownerIndex = ParserUtils.getField(fields, 'OwnerIndex')
108
153
  const text = SchematicTextParser.#resolveSchematicTemplateText(
109
154
  rawText,
110
- metadata
155
+ SchematicTextParser.#resolveSchematicTextMetadata(
156
+ ownerIndex,
157
+ metadata,
158
+ ownerMetadata
159
+ )
111
160
  )
112
161
 
113
162
  if (hidden || x === null || y === null || !text) {
@@ -149,7 +198,7 @@ export class SchematicTextParser {
149
198
  ),
150
199
  hidden,
151
200
  name,
152
- ownerIndex: ParserUtils.getField(fields, 'OwnerIndex') || undefined,
201
+ ownerIndex: ownerIndex || undefined,
153
202
  recordType,
154
203
  style: ParserUtils.parseNumericField(fields, 'Style') || 0,
155
204
  renderOrder:
@@ -476,6 +525,21 @@ export class SchematicTextParser {
476
525
  return replacement ? replacement : normalized
477
526
  }
478
527
 
528
+ /**
529
+ * Chooses the parameter map for one text record.
530
+ * @param {string} ownerIndex
531
+ * @param {Record<string, string>} metadata
532
+ * @param {Map<string, Record<string, string>>} ownerMetadata
533
+ * @returns {Record<string, string>}
534
+ */
535
+ static #resolveSchematicTextMetadata(ownerIndex, metadata, ownerMetadata) {
536
+ if (!ownerIndex) {
537
+ return metadata
538
+ }
539
+
540
+ return ownerMetadata.get(ownerIndex) || {}
541
+ }
542
+
479
543
  /**
480
544
  * Returns true when a text record is metadata rather than sheet content.
481
545
  * @param {Record<string, string | string[]>} fields
@@ -510,7 +574,6 @@ export class SchematicTextParser {
510
574
  if (nonDrawableNames.has(normalizedName)) return true
511
575
  if (/uniqueid$/i.test(normalizedName)) return true
512
576
  if (!normalizedText || normalizedText === '*') return true
513
- if (/^=/.test(normalizedText)) return true
514
577
  if (
515
578
  sheet.titleBlockOn &&
516
579
  SchematicTextParser.isTitleBlockFooterRecord(fields, sheet.width)
@@ -537,10 +600,47 @@ export class SchematicTextParser {
537
600
 
538
601
  if (recordType === '17') return 'middle'
539
602
  if (explicitAnchor) return explicitAnchor
603
+ if (
604
+ SchematicTextParser.#shouldCenterSchematicNoteByDefault(
605
+ fields,
606
+ recordType,
607
+ text
608
+ )
609
+ ) {
610
+ return 'middle'
611
+ }
540
612
 
541
613
  return 'start'
542
614
  }
543
615
 
616
+ /**
617
+ * Returns true for Altium text frames that encode centered one-line labels
618
+ * without carrying an explicit justification field.
619
+ * @param {Record<string, string | string[]>} fields
620
+ * @param {string} recordType
621
+ * @param {string} text
622
+ * @returns {boolean}
623
+ */
624
+ static #shouldCenterSchematicNoteByDefault(fields, recordType, text) {
625
+ if (recordType !== '209' && recordType !== '28') return false
626
+ if (String(text || '').includes('~1')) return false
627
+ if (
628
+ ParserUtils.parseNumericField(fields, 'Corner.X') === null ||
629
+ ParserUtils.parseNumericField(fields, 'Corner.Y') === null
630
+ ) {
631
+ return false
632
+ }
633
+
634
+ if (recordType === '28') {
635
+ return ParserUtils.parseBoolean(fields.WordWrap)
636
+ }
637
+
638
+ return (
639
+ ParserUtils.parseBoolean(fields.WordWrap) &&
640
+ ParserUtils.parseBoolean(fields.ClipToRect)
641
+ )
642
+ }
643
+
544
644
  /**
545
645
  * Decodes Altium's three-column text justification grid into one
546
646
  * horizontal SVG text anchor.
@@ -548,23 +648,37 @@ export class SchematicTextParser {
548
648
  * @returns {'start' | 'middle' | 'end' | null}
549
649
  */
550
650
  static #resolveSchematicTextJustificationAnchor(fields) {
551
- const justification = ParserUtils.parseNumericField(
552
- fields,
553
- 'Justification'
554
- )
651
+ const justification =
652
+ ParserUtils.parseNumericField(fields, 'Justification') ??
653
+ ParserUtils.parseNumericField(fields, 'Alignment')
555
654
 
556
655
  if (justification === null) {
557
656
  return null
558
657
  }
559
658
 
659
+ let anchor = 'start'
560
660
  switch (((justification % 3) + 3) % 3) {
561
661
  case 1:
562
- return 'middle'
662
+ anchor = 'middle'
663
+ break
563
664
  case 2:
564
- return 'end'
665
+ anchor = 'end'
666
+ break
565
667
  default:
566
- return 'start'
668
+ anchor = 'start'
567
669
  }
670
+
671
+ if (
672
+ anchor !== 'middle' &&
673
+ ParserUtils.getField(fields, 'RECORD') === '4' &&
674
+ ParserUtils.getField(fields, 'OwnerIndex') &&
675
+ ParserUtils.parseBoolean(fields.IsMirrored) &&
676
+ ParserUtils.parseNumericField(fields, 'Orientation') === 2
677
+ ) {
678
+ return anchor === 'start' ? 'end' : 'start'
679
+ }
680
+
681
+ return anchor
568
682
  }
569
683
 
570
684
  /**
@@ -2,6 +2,8 @@
2
2
  //
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
+ import { SchematicMultipartDesignatorNormalizer } from './SchematicMultipartDesignatorNormalizer.mjs'
6
+
5
7
  /**
6
8
  * Applies placement-oriented cleanup passes to normalized schematic text.
7
9
  */
@@ -47,84 +49,17 @@ export class SchematicTextPostProcessor {
47
49
  }
48
50
 
49
51
  /**
50
- * Adds multipart section suffixes like A/B/J to visible designator texts
51
- * when the active Altium part id is stored separately from the base
52
+ * Normalizes multipart section suffixes like A/B/J on visible designator
53
+ * texts when the active Altium part id is stored separately from the
52
54
  * designator string.
53
55
  * @param {{ text: string, name?: string, ownerIndex?: string, recordType?: string }[]} texts
54
56
  * @param {Map<string, string>} activeMultipartOwnerParts
55
57
  * @returns {{ text: string, name?: string, ownerIndex?: string, recordType?: string }[]}
56
58
  */
57
59
  static decorateMultipartDesignators(texts, activeMultipartOwnerParts) {
58
- const duplicatedActiveBaseDesignators =
59
- SchematicTextPostProcessor.#collectDuplicatedActiveMultipartDesignators(
60
- texts,
61
- activeMultipartOwnerParts
62
- )
63
-
64
- return texts.map((text) => {
65
- const ownerIndex = String(text.ownerIndex || '')
66
- const suffix =
67
- SchematicTextPostProcessor.#formatMultipartPartSuffix(
68
- activeMultipartOwnerParts.get(ownerIndex)
69
- )
70
-
71
- if (
72
- !suffix ||
73
- text.recordType !== '34' ||
74
- String(text.name || '')
75
- .trim()
76
- .toLowerCase() !== 'designator' ||
77
- !duplicatedActiveBaseDesignators.has(text.text)
78
- ) {
79
- return text
80
- }
81
-
82
- if (!/\d$/i.test(text.text) || text.text.endsWith(suffix)) {
83
- return text
84
- }
85
-
86
- return {
87
- ...text,
88
- text: text.text + suffix
89
- }
90
- })
91
- }
92
-
93
- /**
94
- * Collects base multipart designators that belong to more than one active
95
- * visible owner on the current sheet.
96
- * @param {{ text: string, name?: string, ownerIndex?: string, recordType?: string }[]} texts
97
- * @param {Map<string, string>} activeMultipartOwnerParts
98
- * @returns {Set<string>}
99
- */
100
- static #collectDuplicatedActiveMultipartDesignators(
101
- texts,
102
- activeMultipartOwnerParts
103
- ) {
104
- const counts = new Map()
105
-
106
- for (const text of texts) {
107
- const ownerIndex = String(text.ownerIndex || '')
108
-
109
- if (
110
- !activeMultipartOwnerParts.has(ownerIndex) ||
111
- text.recordType !== '34' ||
112
- String(text.name || '')
113
- .trim()
114
- .toLowerCase() !== 'designator' ||
115
- !/\d$/i.test(String(text.text || '').trim())
116
- ) {
117
- continue
118
- }
119
-
120
- const baseDesignator = String(text.text || '').trim()
121
- counts.set(baseDesignator, (counts.get(baseDesignator) || 0) + 1)
122
- }
123
-
124
- return new Set(
125
- [...counts.entries()]
126
- .filter(([, count]) => count > 1)
127
- .map(([baseDesignator]) => baseDesignator)
60
+ return SchematicMultipartDesignatorNormalizer.normalize(
61
+ texts,
62
+ activeMultipartOwnerParts
128
63
  )
129
64
  }
130
65
 
@@ -136,7 +71,7 @@ export class SchematicTextPostProcessor {
136
71
  * @param {{ x1: number, y1: number, x2: number, y2: number, ownerIndex?: string }[]} lines
137
72
  * @param {{ x: number, y: number, ownerIndex: string, length: number, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
138
73
  * @param {{ x: number, y: number, width: number, direction?: 'left' | 'right' | 'up' | 'down' }[]} ports
139
- * @param {{ x: number, y: number, width: number, height: number, ownerIndex?: string }[]} rectangles
74
+ * @param {{ x: number, y: number, width: number, height: number, ownerIndex?: string }[] | { rectangles?: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], roundedRectangles?: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], ellipses?: { x: number, y: number, radiusX: number, radiusY: number, ownerIndex?: string }[], arcs?: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[], pies?: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[] }} bodyPrimitives
140
75
  * @returns {{ x: number, y: number, text: string, name?: string, ownerIndex?: string, recordType?: string, rotation?: number, anchor?: 'start' | 'middle' | 'end' }[]}
141
76
  */
142
77
  static anchorComponentTextsFromOwnerBounds(
@@ -144,17 +79,22 @@ export class SchematicTextPostProcessor {
144
79
  lines,
145
80
  pins,
146
81
  ports = [],
147
- rectangles = []
82
+ bodyPrimitives = []
148
83
  ) {
84
+ const normalizedBodyPrimitives =
85
+ SchematicTextPostProcessor.#normalizeOwnerBodyPrimitives(
86
+ bodyPrimitives
87
+ )
149
88
  const ownerBounds = SchematicTextPostProcessor.#buildOwnerBounds(
150
89
  lines,
151
90
  pins
152
91
  )
153
- const ownerBodyBounds = SchematicTextPostProcessor.#buildOwnerBounds(
154
- lines,
155
- pins,
156
- rectangles
157
- )
92
+ const ownerBodyBounds =
93
+ SchematicTextPostProcessor.#buildOwnerBodyBounds(
94
+ lines,
95
+ pins,
96
+ normalizedBodyPrimitives
97
+ )
158
98
  const ownerPinCounts =
159
99
  SchematicTextPostProcessor.#buildOwnerPinCounts(pins)
160
100
  const ownerPinOrientations =
@@ -236,6 +176,131 @@ export class SchematicTextPostProcessor {
236
176
  })
237
177
  }
238
178
 
179
+ /**
180
+ * Normalizes owner body primitive arguments while preserving the older
181
+ * rectangle-array call shape.
182
+ * @param {{ x: number, y: number, width: number, height: number, ownerIndex?: string }[] | { rectangles?: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], roundedRectangles?: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], ellipses?: { x: number, y: number, radiusX: number, radiusY: number, ownerIndex?: string }[], arcs?: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[], pies?: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[] }} bodyPrimitives
183
+ * @returns {{ rectangles: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], roundedRectangles: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], ellipses: { x: number, y: number, radiusX: number, radiusY: number, ownerIndex?: string }[], arcs: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[], pies: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[] }}
184
+ */
185
+ static #normalizeOwnerBodyPrimitives(bodyPrimitives) {
186
+ if (Array.isArray(bodyPrimitives)) {
187
+ return {
188
+ rectangles: bodyPrimitives,
189
+ roundedRectangles: [],
190
+ ellipses: [],
191
+ arcs: [],
192
+ pies: []
193
+ }
194
+ }
195
+
196
+ return {
197
+ rectangles: bodyPrimitives?.rectangles || [],
198
+ roundedRectangles: bodyPrimitives?.roundedRectangles || [],
199
+ ellipses: bodyPrimitives?.ellipses || [],
200
+ arcs: bodyPrimitives?.arcs || [],
201
+ pies: bodyPrimitives?.pies || []
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Builds per-owner body bounds from straight and curved owner primitives.
207
+ * @param {{ x1: number, y1: number, x2: number, y2: number, ownerIndex?: string }[]} lines
208
+ * @param {{ x: number, y: number, ownerIndex: string }[]} pins
209
+ * @param {{ rectangles: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], roundedRectangles: { x: number, y: number, width: number, height: number, ownerIndex?: string }[], ellipses: { x: number, y: number, radiusX: number, radiusY: number, ownerIndex?: string }[], arcs: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[], pies: { x: number, y: number, radius: number, radiusY?: number, ownerIndex?: string }[] }} bodyPrimitives
210
+ * @returns {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>}
211
+ */
212
+ static #buildOwnerBodyBounds(lines, pins, bodyPrimitives) {
213
+ const ownerBounds = SchematicTextPostProcessor.#buildOwnerBounds(
214
+ lines,
215
+ pins,
216
+ [...bodyPrimitives.rectangles, ...bodyPrimitives.roundedRectangles]
217
+ )
218
+
219
+ SchematicTextPostProcessor.#extendCenteredOwnerBounds(
220
+ ownerBounds,
221
+ bodyPrimitives.ellipses
222
+ )
223
+ SchematicTextPostProcessor.#extendCenteredOwnerBounds(
224
+ ownerBounds,
225
+ bodyPrimitives.arcs
226
+ )
227
+ SchematicTextPostProcessor.#extendCenteredOwnerBounds(
228
+ ownerBounds,
229
+ bodyPrimitives.pies
230
+ )
231
+
232
+ return ownerBounds
233
+ }
234
+
235
+ /**
236
+ * Extends owner bounds by primitives expressed as a center and radii.
237
+ * @param {Map<string, { minX: number, minY: number, maxX: number, maxY: number }>} ownerBounds
238
+ * @param {{ x: number, y: number, radius?: number, radiusX?: number, radiusY?: number, ownerIndex?: string }[]} primitives
239
+ * @returns {void}
240
+ */
241
+ static #extendCenteredOwnerBounds(ownerBounds, primitives) {
242
+ for (const primitive of primitives) {
243
+ if (!primitive.ownerIndex) {
244
+ continue
245
+ }
246
+
247
+ const radiusX =
248
+ SchematicTextPostProcessor.#resolveHorizontalRadius(primitive)
249
+ const radiusY =
250
+ SchematicTextPostProcessor.#resolveVerticalRadius(primitive)
251
+
252
+ if (radiusX <= 0 || radiusY <= 0) {
253
+ continue
254
+ }
255
+
256
+ SchematicTextPostProcessor.#extendBounds(
257
+ ownerBounds,
258
+ primitive.ownerIndex,
259
+ [
260
+ { x: primitive.x - radiusX, y: primitive.y - radiusY },
261
+ { x: primitive.x + radiusX, y: primitive.y + radiusY }
262
+ ]
263
+ )
264
+ }
265
+ }
266
+
267
+ /**
268
+ * Resolves the horizontal radius of a center-defined primitive.
269
+ * @param {{ radius?: number, radiusX?: number }} primitive
270
+ * @returns {number}
271
+ */
272
+ static #resolveHorizontalRadius(primitive) {
273
+ return SchematicTextPostProcessor.#coercePositiveNumber(
274
+ primitive.radiusX ?? primitive.radius
275
+ )
276
+ }
277
+
278
+ /**
279
+ * Resolves the vertical radius of a center-defined primitive.
280
+ * @param {{ radius?: number, radiusY?: number }} primitive
281
+ * @returns {number}
282
+ */
283
+ static #resolveVerticalRadius(primitive) {
284
+ return SchematicTextPostProcessor.#coercePositiveNumber(
285
+ primitive.radiusY ?? primitive.radius
286
+ )
287
+ }
288
+
289
+ /**
290
+ * Converts finite positive numeric values, preserving zero for invalids.
291
+ * @param {number | string | undefined | null} value
292
+ * @returns {number}
293
+ */
294
+ static #coercePositiveNumber(value) {
295
+ const numericValue = Number(value)
296
+
297
+ if (!Number.isFinite(numericValue) || numericValue <= 0) {
298
+ return 0
299
+ }
300
+
301
+ return numericValue
302
+ }
303
+
239
304
  /**
240
305
  * Right-aligns wire labels that precede a same-row component designator so
241
306
  * they stay clear of the symbol body.
@@ -401,6 +466,8 @@ export class SchematicTextPostProcessor {
401
466
  case 2:
402
467
  case 33:
403
468
  return 8
469
+ case 6:
470
+ return 12
404
471
  default:
405
472
  return 2
406
473
  }
@@ -744,29 +811,6 @@ export class SchematicTextPostProcessor {
744
811
  )
745
812
  }
746
813
 
747
- /**
748
- * Converts one numeric multipart section id into an alphabetic suffix.
749
- * @param {string | undefined} partId
750
- * @returns {string}
751
- */
752
- static #formatMultipartPartSuffix(partId) {
753
- const numericPartId = Number.parseInt(String(partId || ''), 10)
754
- if (!Number.isInteger(numericPartId) || numericPartId <= 0) {
755
- return ''
756
- }
757
-
758
- let suffix = ''
759
- let remaining = numericPartId
760
-
761
- while (remaining > 0) {
762
- remaining -= 1
763
- suffix = String.fromCharCode(65 + (remaining % 26)) + suffix
764
- remaining = Math.floor(remaining / 26)
765
- }
766
-
767
- return suffix
768
- }
769
-
770
814
  /**
771
815
  * Returns true when the horizontal wire segment under the label starts at a
772
816
  * pin endpoint, which means the label should continue reading rightward.