altium-toolkit 0.1.20 → 0.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.
@@ -51,11 +51,23 @@ export class SchematicNoteRenderer {
51
51
  '--schematic-note-border-color'
52
52
  )
53
53
  const noteStroke = text.showBorder ? borderColor : 'none'
54
+ const noteSourceLines = text.noteLines || []
55
+ const verticalTextMargin =
56
+ SchematicNoteRenderer.#resolveVerticalTextMargin(
57
+ textMargin,
58
+ height,
59
+ requestedTextSize
60
+ )
54
61
  const layout = SchematicNoteRenderer.#resolveTextLayout(
55
- text.noteLines || [],
62
+ noteSourceLines,
56
63
  Math.max(width - textMargin * 2, requestedTextSize),
57
- Math.max(height - textMargin * 2, requestedTextSize),
58
- requestedTextSize
64
+ Math.max(height - verticalTextMargin * 2, requestedTextSize),
65
+ requestedTextSize,
66
+ SchematicNoteRenderer.#isCompactSingleLineNote(
67
+ noteSourceLines,
68
+ height,
69
+ requestedTextSize
70
+ )
59
71
  )
60
72
  const noteLines = layout.noteLines
61
73
  const textSize = layout.textSize
@@ -69,6 +81,7 @@ export class SchematicNoteRenderer {
69
81
  right,
70
82
  top,
71
83
  textMargin,
84
+ verticalTextMargin,
72
85
  lineHeight,
73
86
  textSize,
74
87
  text
@@ -102,14 +115,33 @@ export class SchematicNoteRenderer {
102
115
  * @param {number} maxWidth
103
116
  * @param {number} maxHeight
104
117
  * @param {number} requestedTextSize
118
+ * @param {boolean} keepSingleLineSize
105
119
  * @returns {{ noteLines: string[], textSize: number, lineHeight: number }}
106
120
  */
107
121
  static #resolveTextLayout(
108
122
  noteLines,
109
123
  maxWidth,
110
124
  maxHeight,
111
- requestedTextSize
125
+ requestedTextSize,
126
+ keepSingleLineSize = false
112
127
  ) {
128
+ if (keepSingleLineSize) {
129
+ const visibleLines = noteLines.filter((line) =>
130
+ String(line || '').trim()
131
+ )
132
+
133
+ return {
134
+ noteLines: visibleLines,
135
+ textSize: requestedTextSize,
136
+ lineHeight: SchematicNoteRenderer.#resolveLineHeight(
137
+ requestedTextSize,
138
+ maxHeight,
139
+ 0,
140
+ visibleLines.length
141
+ )
142
+ }
143
+ }
144
+
113
145
  let textSize = requestedTextSize
114
146
  let wrappedLines = []
115
147
 
@@ -262,6 +294,36 @@ export class SchematicNoteRenderer {
262
294
  return wrappedLines
263
295
  }
264
296
 
297
+ /**
298
+ * Checks if a note is a tight one-line callout where Altium preserves the
299
+ * text size even when the note rectangle has little vertical padding.
300
+ * @param {string[]} noteLines
301
+ * @param {number} height
302
+ * @param {number} requestedTextSize
303
+ * @returns {boolean}
304
+ */
305
+ static #isCompactSingleLineNote(noteLines, height, requestedTextSize) {
306
+ const visibleLineCount = noteLines.filter((line) =>
307
+ String(line || '').trim()
308
+ ).length
309
+
310
+ return visibleLineCount === 1 && height <= requestedTextSize * 1.5
311
+ }
312
+
313
+ /**
314
+ * Reduces vertical padding for short note rectangles so readable text is
315
+ * centered instead of scaled down to satisfy the default margin.
316
+ * @param {number} textMargin
317
+ * @param {number} height
318
+ * @param {number} requestedTextSize
319
+ * @returns {number}
320
+ */
321
+ static #resolveVerticalTextMargin(textMargin, height, requestedTextSize) {
322
+ const centeredMargin = Math.max((height - requestedTextSize) / 2, 0)
323
+
324
+ return Math.min(textMargin, centeredMargin)
325
+ }
326
+
265
327
  /**
266
328
  * Splits one oversized token into smaller width-safe fragments.
267
329
  * @param {string} token
@@ -360,7 +422,8 @@ export class SchematicNoteRenderer {
360
422
  * @param {number} left
361
423
  * @param {number} right
362
424
  * @param {number} top
363
- * @param {number} textMargin
425
+ * @param {number} horizontalTextMargin
426
+ * @param {number} verticalTextMargin
364
427
  * @param {number} lineHeight
365
428
  * @param {number} textSize
366
429
  * @param {{ color: string, fontFamily?: string, fontWeight?: number }} text
@@ -372,13 +435,14 @@ export class SchematicNoteRenderer {
372
435
  left,
373
436
  right,
374
437
  top,
375
- textMargin,
438
+ horizontalTextMargin,
439
+ verticalTextMargin,
376
440
  lineHeight,
377
441
  textSize,
378
442
  text
379
443
  ) {
380
- const x = left + textMargin
381
- const y = top + textMargin + textSize + index * lineHeight
444
+ const x = left + horizontalTextMargin
445
+ const y = top + verticalTextMargin + textSize + index * lineHeight
382
446
 
383
447
  if (/^_+$/.test(String(line || '').trim())) {
384
448
  return (
@@ -387,7 +451,7 @@ export class SchematicNoteRenderer {
387
451
  '" y1="' +
388
452
  formatNumber(y - textSize * 0.35) +
389
453
  '" x2="' +
390
- formatNumber(right - textMargin) +
454
+ formatNumber(right - horizontalTextMargin) +
391
455
  '" y2="' +
392
456
  formatNumber(y - textSize * 0.35) +
393
457
  '" stroke="' +
@@ -49,7 +49,6 @@ export class SchematicPinSvgRenderer {
49
49
  const labelMode = pin.labelMode || 'name-and-number'
50
50
  const outerMarkerStyle =
51
51
  SchematicPinSvgRenderer.#resolveSchematicOuterPinMarkerStyle(pin)
52
- const usesOuterMarker = outerMarkerStyle !== null
53
52
  const rotateTopNumber =
54
53
  pin.orientation === 'top' &&
55
54
  rotatedVerticalNumberOwners.has(String(pin.ownerIndex || ''))
@@ -65,7 +64,10 @@ export class SchematicPinSvgRenderer {
65
64
  if (pin.orientation === 'left') {
66
65
  if (labelMode !== 'hidden' && labelMode !== 'name-only') {
67
66
  const defaultNumberX =
68
- geometry.bodyX - (usesOuterMarker ? 8 : 2)
67
+ geometry.bodyX -
68
+ SchematicPinSvgRenderer.#resolveHorizontalPinNumberClearance(
69
+ outerMarkerStyle
70
+ )
69
71
  const numberX = hasExplicitOwnerPinName
70
72
  ? SchematicOwnerPinLabelLayout.resolveExplicitOwnerPinNumberX(
71
73
  pin,
@@ -110,7 +112,10 @@ export class SchematicPinSvgRenderer {
110
112
  if (pin.orientation === 'right') {
111
113
  if (labelMode !== 'hidden' && labelMode !== 'name-only') {
112
114
  const defaultNumberX =
113
- geometry.bodyX + (usesOuterMarker ? 8 : 2)
115
+ geometry.bodyX +
116
+ SchematicPinSvgRenderer.#resolveHorizontalPinNumberClearance(
117
+ outerMarkerStyle
118
+ )
114
119
  const numberX = hasExplicitOwnerPinName
115
120
  ? SchematicOwnerPinLabelLayout.resolveExplicitOwnerPinNumberX(
116
121
  pin,
@@ -357,6 +362,23 @@ export class SchematicPinSvgRenderer {
357
362
  )
358
363
  }
359
364
 
365
+ /**
366
+ * Returns the horizontal pin-number clearance needed by an authored marker.
367
+ * @param {'single-in' | 'single-out' | 'double' | null} markerStyle
368
+ * @returns {number}
369
+ */
370
+ static #resolveHorizontalPinNumberClearance(markerStyle) {
371
+ switch (markerStyle) {
372
+ case 'double':
373
+ return 17
374
+ case 'single-in':
375
+ case 'single-out':
376
+ return 8
377
+ default:
378
+ return 2
379
+ }
380
+ }
381
+
360
382
  /**
361
383
  * Builds one or two authored outer-marker polygons for one horizontal pin.
362
384
  * @param {number} bodyX
@@ -56,7 +56,9 @@ export class SchematicPowerPortRenderer {
56
56
  y,
57
57
  fontSize,
58
58
  labelOptions,
59
- resolvedColor
59
+ resolvedColor,
60
+ lines,
61
+ sheetHeight
60
62
  ) +
61
63
  '</g>'
62
64
  )
@@ -77,7 +79,9 @@ export class SchematicPowerPortRenderer {
77
79
  y,
78
80
  fontSize,
79
81
  labelOptions,
80
- resolvedColor
82
+ resolvedColor,
83
+ lines,
84
+ sheetHeight
81
85
  ) +
82
86
  '</g>'
83
87
  )
@@ -514,6 +518,8 @@ export class SchematicPowerPortRenderer {
514
518
  * @param {number} fontSize
515
519
  * @param {{ fontSize?: number, fontFamily?: string, fontWeight?: number }} labelOptions
516
520
  * @param {string} color
521
+ * @param {{ x1: number, y1: number, x2: number, y2: number }[]} lines
522
+ * @param {number} sheetHeight
517
523
  * @returns {string}
518
524
  */
519
525
  static #buildDirectionalLabel(
@@ -523,52 +529,193 @@ export class SchematicPowerPortRenderer {
523
529
  y,
524
530
  fontSize,
525
531
  labelOptions,
526
- color
532
+ color,
533
+ lines = [],
534
+ sheetHeight = 0
535
+ ) {
536
+ const placement = SchematicPowerPortRenderer.#resolveLabelPlacement(
537
+ text,
538
+ direction,
539
+ x,
540
+ y,
541
+ fontSize,
542
+ lines,
543
+ sheetHeight
544
+ )
545
+
546
+ return createSvgText(
547
+ 'schematic-power-port-label',
548
+ placement.x,
549
+ placement.y,
550
+ text.text,
551
+ color,
552
+ placement.anchor,
553
+ labelOptions
554
+ )
555
+ }
556
+
557
+ /**
558
+ * Resolves label placement and avoids nearby horizontal net-line overlap
559
+ * for upward rail labels in dense schematic areas.
560
+ * @param {{ text: string, style?: number }} text
561
+ * @param {'up' | 'down' | 'left' | 'right'} direction
562
+ * @param {number} x
563
+ * @param {number} y
564
+ * @param {number} fontSize
565
+ * @param {{ x1: number, y1: number, x2: number, y2: number }[]} lines
566
+ * @param {number} sheetHeight
567
+ * @returns {{ x: number, y: number, anchor: 'start' | 'middle' | 'end' }}
568
+ */
569
+ static #resolveLabelPlacement(
570
+ text,
571
+ direction,
572
+ x,
573
+ y,
574
+ fontSize,
575
+ lines,
576
+ sheetHeight
527
577
  ) {
528
578
  if (direction === 'up') {
529
- return createSvgText(
530
- 'schematic-power-port-label',
579
+ return {
531
580
  x,
532
- y - 16,
533
- text.text,
534
- color,
535
- 'middle',
536
- labelOptions
537
- )
581
+ y: SchematicPowerPortRenderer.#resolveUpwardRailLabelY(
582
+ text,
583
+ x,
584
+ y,
585
+ y - 16,
586
+ fontSize,
587
+ lines,
588
+ sheetHeight
589
+ ),
590
+ anchor: 'middle'
591
+ }
538
592
  }
539
593
 
540
594
  if (direction === 'right') {
541
- return createSvgText(
542
- 'schematic-power-port-label',
543
- x + 18,
544
- y + fontSize * 0.36,
545
- text.text,
546
- color,
547
- 'start',
548
- labelOptions
549
- )
595
+ return { x: x + 18, y: y + fontSize * 0.36, anchor: 'start' }
550
596
  }
551
597
 
552
598
  if (direction === 'left') {
553
- return createSvgText(
554
- 'schematic-power-port-label',
555
- x - 18,
556
- y + fontSize * 0.36,
557
- text.text,
558
- color,
559
- 'end',
560
- labelOptions
599
+ return { x: x - 18, y: y + fontSize * 0.36, anchor: 'end' }
600
+ }
601
+
602
+ return { x, y: y + 25, anchor: 'middle' }
603
+ }
604
+
605
+ /**
606
+ * Shifts an upward rail label below close parallel net lines when the
607
+ * default placement would draw text through an existing wire.
608
+ * @param {{ text: string, style?: number }} text
609
+ * @param {number} x
610
+ * @param {number} connectionY
611
+ * @param {number} defaultY
612
+ * @param {number} fontSize
613
+ * @param {{ x1: number, y1: number, x2: number, y2: number }[]} lines
614
+ * @param {number} sheetHeight
615
+ * @returns {number}
616
+ */
617
+ static #resolveUpwardRailLabelY(
618
+ text,
619
+ x,
620
+ connectionY,
621
+ defaultY,
622
+ fontSize,
623
+ lines,
624
+ sheetHeight
625
+ ) {
626
+ if (Number(text.style || 0) === 4) {
627
+ return defaultY
628
+ }
629
+
630
+ let labelY = defaultY
631
+
632
+ for (const line of lines) {
633
+ const horizontalLine =
634
+ SchematicPowerPortRenderer.#projectHorizontalLine(
635
+ line,
636
+ sheetHeight
637
+ )
638
+
639
+ if (!horizontalLine) {
640
+ continue
641
+ }
642
+
643
+ if (
644
+ !SchematicPowerPortRenderer.#horizontalLineIntersectsLabel(
645
+ horizontalLine,
646
+ text.text,
647
+ x,
648
+ labelY,
649
+ fontSize
650
+ )
651
+ ) {
652
+ continue
653
+ }
654
+
655
+ labelY = Math.min(
656
+ connectionY - 4,
657
+ Math.max(labelY, horizontalLine.y + fontSize + 4)
561
658
  )
562
659
  }
563
660
 
564
- return createSvgText(
565
- 'schematic-power-port-label',
566
- x,
567
- y + 25,
568
- text.text,
569
- color,
570
- 'middle',
571
- labelOptions
661
+ return labelY
662
+ }
663
+
664
+ /**
665
+ * Projects one source horizontal line into SVG coordinates.
666
+ * @param {{ x1: number, y1: number, x2: number, y2: number }} line
667
+ * @param {number} sheetHeight
668
+ * @returns {{ y: number, minX: number, maxX: number } | null}
669
+ */
670
+ static #projectHorizontalLine(line, sheetHeight) {
671
+ const y1 = projectSchematicY(sheetHeight, line.y1)
672
+ const y2 = projectSchematicY(sheetHeight, line.y2)
673
+
674
+ if (Math.abs(y1 - y2) > 0.01) {
675
+ return null
676
+ }
677
+
678
+ return {
679
+ y: y1,
680
+ minX: Math.min(line.x1, line.x2),
681
+ maxX: Math.max(line.x1, line.x2)
682
+ }
683
+ }
684
+
685
+ /**
686
+ * Checks whether a projected horizontal line crosses an estimated text box.
687
+ * @param {{ y: number, minX: number, maxX: number }} line
688
+ * @param {string} label
689
+ * @param {number} x
690
+ * @param {number} labelY
691
+ * @param {number} fontSize
692
+ * @returns {boolean}
693
+ */
694
+ static #horizontalLineIntersectsLabel(line, label, x, labelY, fontSize) {
695
+ const textWidth = SchematicPowerPortRenderer.#estimateLabelWidth(
696
+ label,
697
+ fontSize
698
+ )
699
+ const textMinX = x - textWidth / 2
700
+ const textMaxX = x + textWidth / 2
701
+ const textTopY = labelY - fontSize
702
+ const textBottomY = labelY + fontSize * 0.25
703
+
704
+ return (
705
+ line.maxX >= textMinX &&
706
+ line.minX <= textMaxX &&
707
+ line.y >= textTopY &&
708
+ line.y <= textBottomY
572
709
  )
573
710
  }
711
+
712
+ /**
713
+ * Estimates one power-port label width for clearance checks.
714
+ * @param {string} label
715
+ * @param {number} fontSize
716
+ * @returns {number}
717
+ */
718
+ static #estimateLabelWidth(label, fontSize) {
719
+ return String(label || '').length * fontSize * 0.56
720
+ }
574
721
  }
@@ -28,7 +28,7 @@ const { createSvgText, escapeHtml, formatNumber, projectSchematicY } =
28
28
  export class SchematicSvgRenderer {
29
29
  /**
30
30
  * Renders a normalized schematic model into SVG markup.
31
- * @param {{ fileName?: string, summary: { title?: string }, schematic?: { sheet: { width: number, height: number, sourceWidth?: number, sourceHeight?: number, paperSize?: string, borderOn?: boolean, titleBlockOn?: boolean, marginWidth?: number, xZones?: number, yZones?: number, titleBlock?: { title?: string, revision?: string, documentNumber?: string, sheetNumber?: string, sheetTotal?: string, date?: string, drawnBy?: string } }, lines: { x1: number, y1: number, x2: number, y2: number, color: string, width: number, lineStyle?: number, isBus?: boolean, ownerIndex?: string, renderOrder?: number }[], polygons?: { points: { x: number, y: number }[], color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, ownerIndex?: string, renderOrder?: number }[], rectangles?: { x: number, y: number, width: number, height: number, color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, ownerIndex?: string, renderOrder?: number }[], regions?: { x: number, y: number, width: number, height: number, color: string, fill: string, renderOrder?: number }[], ellipses?: { x: number, y: number, radiusX: number, radiusY: number, color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, ownerIndex?: string, renderOrder?: number }[], arcs?: { x: number, y: number, radius: number, startAngle: number, endAngle: number, color: string, width: number, ownerIndex?: string, renderOrder?: number }[], directives?: { x: number, y: number, color: string, name: string, orientation?: number }[], texts: { x: number, y: number, text: string, color: string, recordType?: string, style?: number, fontSize?: number, fontFamily?: string, fontWeight?: number, 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[] }[], components: { x: number, y: number, designator: string }[], pins?: { x: number, y: number, length: number, name: string, nameSegments?: { text: string, overline: boolean }[], designator: string, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number, symbolOuter?: number, color: string, labelColor?: string, labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number', ownerIndex?: string }[], ports?: { x: number, y: number, width: number, height: number, name: string, fill: string, color: string, direction?: 'left' | 'right' | 'up' | 'down', shape?: 'single' | 'double' | 'plain' }[], crosses?: { x: number, y: number, size: number, color: string }[] } }} documentModel
31
+ * @param {{ fileName?: string, summary: { title?: string }, schematic?: { sheet: { width: number, height: number, sourceWidth?: number, sourceHeight?: number, paperSize?: string, borderOn?: boolean, titleBlockOn?: boolean, marginWidth?: number, xZones?: number, yZones?: number, titleBlock?: { title?: string, revision?: string, documentNumber?: string, sheetNumber?: string, sheetTotal?: string, date?: string, drawnBy?: string } }, lines: { x1: number, y1: number, x2: number, y2: number, color: string, width: number, lineStyle?: number, isBus?: boolean, ownerIndex?: string, renderOrder?: number, recordType?: string }[], polygons?: { points: { x: number, y: number }[], color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, ownerIndex?: string, renderOrder?: number }[], rectangles?: { x: number, y: number, width: number, height: number, color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, ownerIndex?: string, renderOrder?: number }[], regions?: { x: number, y: number, width: number, height: number, color: string, fill: string, renderOrder?: number }[], ellipses?: { x: number, y: number, radiusX: number, radiusY: number, color: string, fill: string, isSolid: boolean, transparent: boolean, lineWidth: number, ownerIndex?: string, renderOrder?: number }[], arcs?: { x: number, y: number, radius: number, startAngle: number, endAngle: number, color: string, width: number, ownerIndex?: string, renderOrder?: number }[], directives?: { x: number, y: number, color: string, name: string, orientation?: number }[], texts: { x: number, y: number, text: string, color: string, recordType?: string, style?: number, fontSize?: number, fontFamily?: string, fontWeight?: number, 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[] }[], components: { x: number, y: number, designator: string }[], pins?: { x: number, y: number, length: number, name: string, nameSegments?: { text: string, overline: boolean }[], designator: string, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number, symbolOuter?: number, color: string, labelColor?: string, labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number', ownerIndex?: string }[], ports?: { x: number, y: number, width: number, height: number, name: string, fill: string, color: string, direction?: 'left' | 'right' | 'up' | 'down', shape?: 'single' | 'double' | 'plain' }[], crosses?: { x: number, y: number, size: number, color: string }[] } }} documentModel
32
32
  * @returns {string}
33
33
  */
34
34
  static render(documentModel) {
@@ -461,7 +461,7 @@ export class SchematicSvgRenderer {
461
461
  /**
462
462
  * Builds one schematic line segment, preserving dashed line styles when
463
463
  * the source primitive requests them.
464
- * @param {{ x1: number, y1: number, x2: number, y2: number, color: string, width: number, lineStyle?: number, isBus?: boolean }} line
464
+ * @param {{ x1: number, y1: number, x2: number, y2: number, color: string, width: number, lineStyle?: number, isBus?: boolean, recordType?: string }} line
465
465
  * @param {number} sheetHeight
466
466
  * @returns {string}
467
467
  */