altium-toolkit 1.1.2 → 1.1.3

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 (27) hide show
  1. package/package.json +1 -1
  2. package/src/core/altium/AltiumLayoutParser.mjs +275 -13
  3. package/src/core/altium/AltiumParser.mjs +240 -8
  4. package/src/core/altium/PcbEmbeddedFontExtractor.mjs +186 -43
  5. package/src/core/altium/PrintableTextDecoder.mjs +133 -13
  6. package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +13 -0
  7. package/src/core/altium/SchematicComponentTextResolver.mjs +40 -1
  8. package/src/core/altium/SchematicImageParser.mjs +291 -6
  9. package/src/core/altium/SchematicMultipartDesignatorNormalizer.mjs +164 -0
  10. package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +2 -0
  11. package/src/core/altium/SchematicPinParser.mjs +175 -4
  12. package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
  13. package/src/core/altium/SchematicTextParser.mjs +125 -11
  14. package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
  15. package/src/ui/SchematicColorResolver.mjs +78 -0
  16. package/src/ui/SchematicContentLayout.mjs +58 -1
  17. package/src/ui/SchematicImageRenderer.mjs +125 -10
  18. package/src/ui/SchematicJunctionRenderer.mjs +1 -1
  19. package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
  20. package/src/ui/SchematicNoteRenderer.mjs +82 -6
  21. package/src/ui/SchematicOwnerPinLabelLayout.mjs +292 -3
  22. package/src/ui/SchematicPinSvgRenderer.mjs +197 -15
  23. package/src/ui/SchematicPowerDiagramImageProcessor.mjs +970 -0
  24. package/src/ui/SchematicPowerDiagramLineMasks.mjs +631 -0
  25. package/src/ui/SchematicPowerPortRenderer.mjs +1 -1
  26. package/src/ui/SchematicShapeRenderer.mjs +82 -23
  27. package/src/ui/SchematicSvgRenderer.mjs +293 -47
@@ -0,0 +1,275 @@
1
+ // SPDX-FileCopyrightText: 2026 André Fiedler
2
+ //
3
+ // SPDX-License-Identifier: GPL-3.0-or-later
4
+
5
+ /**
6
+ * Splits native Altium footer/template primitives from scalable schematic
7
+ * content when a standard template page is larger than the stored custom size.
8
+ */
9
+ export class SchematicNativeFooterPartitioner {
10
+ /**
11
+ * Partitions render primitives into scalable content and fixed footer
12
+ * chrome.
13
+ * @param {{ lines?: object[], polygons?: object[], rectangles?: object[], roundedRectangles?: object[], ellipses?: object[], arcs?: object[], beziers?: object[], pies?: object[], ieeeSymbols?: object[], texts?: object[], images?: object[] }} primitives Primitive families.
14
+ * @param {{ width?: number, height?: number, sourceWidth?: number, sourceHeight?: number, marginWidth?: number, paperSize?: string, borderOn?: boolean }} sheet Sheet metadata.
15
+ * @returns {{ content: Record<string, object[]>, footer: Record<string, object[]>, footerBounds: { minX: number, minY: number, maxX: number, maxY: number } | null }}
16
+ */
17
+ static partitionPrimitives(primitives, sheet) {
18
+ const families = [
19
+ 'lines',
20
+ 'polygons',
21
+ 'rectangles',
22
+ 'roundedRectangles',
23
+ 'ellipses',
24
+ 'arcs',
25
+ 'beziers',
26
+ 'pies',
27
+ 'ieeeSymbols',
28
+ 'texts',
29
+ 'images'
30
+ ]
31
+ const content = {}
32
+ const footer = {}
33
+
34
+ for (const family of families) {
35
+ const values = primitives?.[family] || []
36
+ content[family] = values.filter(
37
+ (primitive) =>
38
+ !SchematicNativeFooterPartitioner.#isNativeFooterPrimitive(
39
+ primitive,
40
+ sheet
41
+ )
42
+ )
43
+ footer[family] = values.filter((primitive) =>
44
+ SchematicNativeFooterPartitioner.#isNativeFooterPrimitive(
45
+ primitive,
46
+ sheet
47
+ )
48
+ )
49
+ }
50
+
51
+ return {
52
+ content,
53
+ footer,
54
+ footerBounds:
55
+ SchematicNativeFooterPartitioner.#resolvePrimitiveSetBounds(
56
+ Object.values(footer).flat()
57
+ )
58
+ }
59
+ }
60
+
61
+ /**
62
+ * Returns true when one primitive is native lower-page footer chrome.
63
+ * @param {object} primitive Primitive candidate.
64
+ * @param {{ width?: number, height?: number, sourceWidth?: number, sourceHeight?: number, marginWidth?: number, paperSize?: string, borderOn?: boolean }} sheet Sheet metadata.
65
+ * @returns {boolean}
66
+ */
67
+ static #isNativeFooterPrimitive(primitive, sheet) {
68
+ if (!SchematicNativeFooterPartitioner.#shouldSplitNativeFooter(sheet)) {
69
+ return false
70
+ }
71
+ if (!String(primitive?.ownerIndex || '').trim()) {
72
+ return false
73
+ }
74
+
75
+ const bounds =
76
+ SchematicNativeFooterPartitioner.#resolvePrimitiveBounds(primitive)
77
+ if (!bounds) {
78
+ return false
79
+ }
80
+
81
+ const margin = Math.max(Number(sheet?.marginWidth || 20), 10)
82
+ const footerLimit = Math.max(margin * 6, 120)
83
+ const footerStartX = Math.max(Number(sheet?.width || 0), 0) * 0.5
84
+
85
+ return bounds.maxY <= footerLimit && bounds.maxX >= footerStartX
86
+ }
87
+
88
+ /**
89
+ * Returns true when promoted standard-sheet content needs fixed native
90
+ * footer handling.
91
+ * @param {{ width?: number, height?: number, sourceWidth?: number, sourceHeight?: number, paperSize?: string, borderOn?: boolean }} sheet Sheet metadata.
92
+ * @returns {boolean}
93
+ */
94
+ static #shouldSplitNativeFooter(sheet) {
95
+ const width = Number(sheet?.width || 0)
96
+ const height = Number(sheet?.height || 0)
97
+ const sourceWidth = Number(sheet?.sourceWidth || 0)
98
+ const sourceHeight = Number(sheet?.sourceHeight || 0)
99
+
100
+ return Boolean(
101
+ sheet?.borderOn &&
102
+ sheet?.paperSize &&
103
+ sourceWidth > 0 &&
104
+ sourceHeight > 0 &&
105
+ (width > sourceWidth || height > sourceHeight)
106
+ )
107
+ }
108
+
109
+ /**
110
+ * Resolves one primitive's document-space bounds.
111
+ * @param {object} primitive Primitive candidate.
112
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
113
+ */
114
+ static #resolvePrimitiveBounds(primitive) {
115
+ const points =
116
+ SchematicNativeFooterPartitioner.#collectPrimitivePoints(primitive)
117
+ if (!points.length) {
118
+ return null
119
+ }
120
+
121
+ return {
122
+ minX: Math.min(...points.map((point) => point.x)),
123
+ minY: Math.min(...points.map((point) => point.y)),
124
+ maxX: Math.max(...points.map((point) => point.x)),
125
+ maxY: Math.max(...points.map((point) => point.y))
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Resolves combined bounds for a set of primitives.
131
+ * @param {object[]} primitives Primitive candidates.
132
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
133
+ */
134
+ static #resolvePrimitiveSetBounds(primitives) {
135
+ const bounds = primitives
136
+ .map((primitive) =>
137
+ SchematicNativeFooterPartitioner.#resolvePrimitiveBounds(
138
+ primitive
139
+ )
140
+ )
141
+ .filter(Boolean)
142
+
143
+ if (!bounds.length) {
144
+ return null
145
+ }
146
+
147
+ return {
148
+ minX: Math.min(...bounds.map((bound) => bound.minX)),
149
+ minY: Math.min(...bounds.map((bound) => bound.minY)),
150
+ maxX: Math.max(...bounds.map((bound) => bound.maxX)),
151
+ maxY: Math.max(...bounds.map((bound) => bound.maxY))
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Collects point coordinates from one normalized primitive.
157
+ * @param {object} primitive Primitive candidate.
158
+ * @returns {{ x: number, y: number }[]}
159
+ */
160
+ static #collectPrimitivePoints(primitive) {
161
+ if (Array.isArray(primitive?.points)) {
162
+ return primitive.points
163
+ .map((point) =>
164
+ SchematicNativeFooterPartitioner.#point(point?.x, point?.y)
165
+ )
166
+ .filter(Boolean)
167
+ }
168
+ if (Array.isArray(primitive?.segments)) {
169
+ return primitive.segments.flatMap((segment) =>
170
+ [
171
+ SchematicNativeFooterPartitioner.#point(
172
+ segment?.x1,
173
+ segment?.y1
174
+ ),
175
+ SchematicNativeFooterPartitioner.#point(
176
+ segment?.x2,
177
+ segment?.y2
178
+ )
179
+ ].filter(Boolean)
180
+ )
181
+ }
182
+ if (
183
+ Number.isFinite(Number(primitive?.x1)) &&
184
+ Number.isFinite(Number(primitive?.y1)) &&
185
+ Number.isFinite(Number(primitive?.x2)) &&
186
+ Number.isFinite(Number(primitive?.y2))
187
+ ) {
188
+ return [
189
+ SchematicNativeFooterPartitioner.#point(
190
+ primitive.x1,
191
+ primitive.y1
192
+ ),
193
+ SchematicNativeFooterPartitioner.#point(
194
+ primitive.x2,
195
+ primitive.y2
196
+ )
197
+ ]
198
+ }
199
+ if (
200
+ Number.isFinite(Number(primitive?.cornerX)) &&
201
+ Number.isFinite(Number(primitive?.cornerY))
202
+ ) {
203
+ return [
204
+ SchematicNativeFooterPartitioner.#point(
205
+ primitive.x,
206
+ primitive.y
207
+ ),
208
+ SchematicNativeFooterPartitioner.#point(
209
+ primitive.cornerX,
210
+ primitive.cornerY
211
+ )
212
+ ].filter(Boolean)
213
+ }
214
+ if (
215
+ Number.isFinite(Number(primitive?.width)) &&
216
+ Number.isFinite(Number(primitive?.height))
217
+ ) {
218
+ return [
219
+ SchematicNativeFooterPartitioner.#point(
220
+ primitive.x,
221
+ primitive.y
222
+ ),
223
+ SchematicNativeFooterPartitioner.#point(
224
+ Number(primitive.x || 0) + Number(primitive.width || 0),
225
+ Number(primitive.y || 0) + Number(primitive.height || 0)
226
+ )
227
+ ].filter(Boolean)
228
+ }
229
+ if (
230
+ Number.isFinite(Number(primitive?.radius)) ||
231
+ Number.isFinite(Number(primitive?.radiusX))
232
+ ) {
233
+ const radiusX = Math.max(
234
+ Number(primitive?.radiusX || primitive?.radius || 0),
235
+ 0
236
+ )
237
+ const radiusY = Math.max(
238
+ Number(primitive?.radiusY || primitive?.radius || 0),
239
+ 0
240
+ )
241
+
242
+ return [
243
+ SchematicNativeFooterPartitioner.#point(
244
+ Number(primitive.x || 0) - radiusX,
245
+ Number(primitive.y || 0) - radiusY
246
+ ),
247
+ SchematicNativeFooterPartitioner.#point(
248
+ Number(primitive.x || 0) + radiusX,
249
+ Number(primitive.y || 0) + radiusY
250
+ )
251
+ ]
252
+ }
253
+
254
+ return [
255
+ SchematicNativeFooterPartitioner.#point(primitive?.x, primitive?.y)
256
+ ].filter(Boolean)
257
+ }
258
+
259
+ /**
260
+ * Builds one finite point.
261
+ * @param {number | string | undefined} x Raw x coordinate.
262
+ * @param {number | string | undefined} y Raw y coordinate.
263
+ * @returns {{ x: number, y: number } | null}
264
+ */
265
+ static #point(x, y) {
266
+ const normalizedX = Number(x)
267
+ const normalizedY = Number(y)
268
+
269
+ if (!Number.isFinite(normalizedX) || !Number.isFinite(normalizedY)) {
270
+ return null
271
+ }
272
+
273
+ return { x: normalizedX, y: normalizedY }
274
+ }
275
+ }
@@ -15,7 +15,7 @@ const MINIMUM_NOTE_TEXT_SIZE = 4
15
15
  export class SchematicNoteRenderer {
16
16
  /**
17
17
  * Builds one boxed schematic note/callout with wrapped text rows.
18
- * @param {{ x: number, y: number, color: string, fontSize?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string, cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, lineWidth?: number, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] }} text
18
+ * @param {{ x: number, y: number, color: string, fontSize?: number, fontFamily?: string, fontWeight?: number, fontStyle?: string, anchor?: 'start' | 'middle' | 'end', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, lineWidth?: number, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] }} text
19
19
  * @param {number} sheetHeight
20
20
  * @returns {string}
21
21
  */
@@ -59,6 +59,10 @@ export class SchematicNoteRenderer {
59
59
  height,
60
60
  requestedTextSize
61
61
  )
62
+ const compactMarkerNote = SchematicNoteRenderer.#isCompactMarkerNote(
63
+ noteSourceLines,
64
+ compactSingleLineNote
65
+ )
62
66
  const horizontalTextMargin =
63
67
  SchematicNoteRenderer.#resolveHorizontalTextMargin(
64
68
  textMargin,
@@ -94,6 +98,7 @@ export class SchematicNoteRenderer {
94
98
  lineHeight,
95
99
  textSize,
96
100
  compactSingleLineNote,
101
+ compactMarkerNote,
97
102
  text
98
103
  )
99
104
  )
@@ -462,7 +467,8 @@ export class SchematicNoteRenderer {
462
467
  * @param {number} lineHeight
463
468
  * @param {number} textSize
464
469
  * @param {boolean} compactSingleLineNote
465
- * @param {{ color: string, fontFamily?: string, fontWeight?: number, fontStyle?: string }} text
470
+ * @param {boolean} compactMarkerNote
471
+ * @param {{ color: string, fontFamily?: string, fontWeight?: number, fontStyle?: string, anchor?: 'start' | 'middle' | 'end' }} text
466
472
  * @returns {string}
467
473
  */
468
474
  static #buildNoteLineMarkup(
@@ -476,9 +482,21 @@ export class SchematicNoteRenderer {
476
482
  lineHeight,
477
483
  textSize,
478
484
  compactSingleLineNote,
485
+ compactMarkerNote,
479
486
  text
480
487
  ) {
481
- const x = left + horizontalTextMargin
488
+ const anchor = SchematicNoteRenderer.#resolveTextAnchor(
489
+ text,
490
+ compactMarkerNote
491
+ )
492
+ const x = SchematicNoteRenderer.#resolveLineX(
493
+ left,
494
+ right,
495
+ horizontalTextMargin,
496
+ anchor
497
+ )
498
+ const ruleLeft = left + horizontalTextMargin
499
+ const ruleRight = right - horizontalTextMargin
482
500
  const y =
483
501
  top +
484
502
  verticalTextMargin +
@@ -491,11 +509,11 @@ export class SchematicNoteRenderer {
491
509
  if (/^_+$/.test(String(line || '').trim())) {
492
510
  return (
493
511
  '<line class="schematic-note-rule" x1="' +
494
- formatNumber(x) +
512
+ formatNumber(ruleLeft) +
495
513
  '" y1="' +
496
514
  formatNumber(y - textSize * 0.35) +
497
515
  '" x2="' +
498
- formatNumber(right - horizontalTextMargin) +
516
+ formatNumber(ruleRight) +
499
517
  '" y2="' +
500
518
  formatNumber(y - textSize * 0.35) +
501
519
  '" stroke="' +
@@ -521,7 +539,9 @@ export class SchematicNoteRenderer {
521
539
  '--schematic-text-color'
522
540
  )
523
541
  ) +
524
- '" text-anchor="start" font-size="' +
542
+ '" text-anchor="' +
543
+ escapeHtml(anchor) +
544
+ '" font-size="' +
525
545
  formatNumber(textSize) +
526
546
  '" font-family="' +
527
547
  escapeHtml(text.fontFamily || 'Times New Roman') +
@@ -534,6 +554,62 @@ export class SchematicNoteRenderer {
534
554
  )
535
555
  }
536
556
 
557
+ /**
558
+ * Resolves the x coordinate for a note line from its text anchor.
559
+ * @param {number} left Note box left edge.
560
+ * @param {number} right Note box right edge.
561
+ * @param {number} horizontalTextMargin Text margin.
562
+ * @param {'start' | 'middle' | 'end'} anchor Text anchor.
563
+ * @returns {number}
564
+ */
565
+ static #resolveLineX(left, right, horizontalTextMargin, anchor) {
566
+ if (anchor === 'middle') return (left + right) / 2
567
+ if (anchor === 'end') return right - horizontalTextMargin
568
+
569
+ return left + horizontalTextMargin
570
+ }
571
+
572
+ /**
573
+ * Returns true for tight single-token marker boxes that Altium centers
574
+ * inside the authored note rectangle.
575
+ * @param {string[]} noteLines Source note rows.
576
+ * @param {boolean} compactSingleLineNote True for tight one-line note boxes.
577
+ * @returns {boolean}
578
+ */
579
+ static #isCompactMarkerNote(noteLines, compactSingleLineNote) {
580
+ if (!compactSingleLineNote) {
581
+ return false
582
+ }
583
+
584
+ const visibleLines = noteLines
585
+ .map((line) => String(line || '').trim())
586
+ .filter(Boolean)
587
+
588
+ if (visibleLines.length !== 1) {
589
+ return false
590
+ }
591
+
592
+ return /^[A-Z0-9._/-]{1,4}$/u.test(visibleLines[0])
593
+ }
594
+
595
+ /**
596
+ * Normalizes note text anchors to SVG text-anchor values.
597
+ * @param {{ anchor?: string }} text Note text.
598
+ * @param {boolean} compactMarkerNote True for centered marker notes.
599
+ * @returns {'start' | 'middle' | 'end'}
600
+ */
601
+ static #resolveTextAnchor(text, compactMarkerNote) {
602
+ if (compactMarkerNote) {
603
+ return 'middle'
604
+ }
605
+
606
+ if (text.anchor === 'middle' || text.anchor === 'end') {
607
+ return text.anchor
608
+ }
609
+
610
+ return 'start'
611
+ }
612
+
537
613
  /**
538
614
  * Builds an optional note font-style attribute.
539
615
  * @param {string | undefined} fontStyle