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
@@ -12,10 +12,15 @@ export class SchematicPinParser {
12
12
  /**
13
13
  * Normalizes schematic pin records into drawable pin primitives.
14
14
  * @param {{ fields: Record<string, string | string[]> }[]} records
15
+ * @param {{ ownerDrawnInternalPinOwners?: Set<string>, numericEndpointLabelOwners?: Set<string> }} [options]
15
16
  * @returns {{ 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 }[]}
16
17
  */
17
- static parseSchematicPins(records) {
18
+ static parseSchematicPins(records, options = {}) {
18
19
  const groups = new Map()
20
+ const ownerDrawnInternalPinOwners =
21
+ options.ownerDrawnInternalPinOwners || new Set()
22
+ const numericEndpointLabelOwners =
23
+ options.numericEndpointLabelOwners || new Set()
19
24
 
20
25
  for (const record of records) {
21
26
  const ownerIndex = ParserUtils.getField(record.fields, 'OwnerIndex')
@@ -81,7 +86,11 @@ export class SchematicPinParser {
81
86
  }
82
87
 
83
88
  return [...groups.values()].flatMap((pins) =>
84
- SchematicPinParser.#normalizeSchematicPinGroup(pins)
89
+ SchematicPinParser.#normalizeSchematicPinGroup(
90
+ pins,
91
+ ownerDrawnInternalPinOwners,
92
+ numericEndpointLabelOwners
93
+ )
85
94
  )
86
95
  }
87
96
 
@@ -730,9 +739,15 @@ export class SchematicPinParser {
730
739
  /**
731
740
  * Deduces the visible pins for one schematic symbol owner.
732
741
  * @param {{ x: number, y: number, length: number, conglomerate?: number, name: string, nameSegments?: { text: string, overline: boolean }[], designator: string, orientation: 'left' | 'right' | 'top' | 'bottom', electrical?: number, symbolOuter?: number, color?: string, labelColor?: string, ownerIndex: string }[]} pins
742
+ * @param {Set<string>} ownerDrawnInternalPinOwners
743
+ * @param {Set<string>} numericEndpointLabelOwners
733
744
  * @returns {{ 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 }[]}
734
745
  */
735
- static #normalizeSchematicPinGroup(pins) {
746
+ static #normalizeSchematicPinGroup(
747
+ pins,
748
+ ownerDrawnInternalPinOwners,
749
+ numericEndpointLabelOwners
750
+ ) {
736
751
  const deduped = SchematicPinParser.#dedupeSchematicPins(pins)
737
752
  const inferredSequentialDesignators =
738
753
  SchematicPinDesignatorInferer.inferSequentialCompactFourPinDesignators(
@@ -773,6 +788,7 @@ export class SchematicPinParser {
773
788
  /^\d+$/.test(String(pin.designator || '').trim()) &&
774
789
  (!pin.name || /^\d+$/.test(String(pin.name || '').trim()))
775
790
  )
791
+ const ownerIndex = normalizedPins[0]?.ownerIndex || ''
776
792
  let labelMode = 'name-and-number'
777
793
 
778
794
  if (
@@ -795,12 +811,26 @@ export class SchematicPinParser {
795
811
  labelMode = 'number-only'
796
812
  }
797
813
 
798
- if (allPassive && normalizedPins.length <= 2) {
814
+ if (
815
+ numericEndpointLabelOwners.has(ownerIndex) &&
816
+ SchematicPinParser.#isTwoPinNumericEndpointGroup(normalizedPins)
817
+ ) {
818
+ labelMode = 'number-only'
819
+ } else if (allPassive && normalizedPins.length <= 2) {
799
820
  labelMode = SchematicPinParser.#isCanonicalPassiveTwoPinGroup(
800
821
  normalizedPins
801
822
  )
802
823
  ? 'hidden'
803
824
  : 'number-only'
825
+ } else if (
826
+ ownerDrawnInternalPinOwners.has(ownerIndex) &&
827
+ SchematicPinParser.#isCompactNumberedFetTerminalGroup(
828
+ normalizedPins,
829
+ semanticNames,
830
+ orientationCount
831
+ )
832
+ ) {
833
+ labelMode = 'number-only'
804
834
  } else if (
805
835
  SchematicPinParser.#isOwnerDrawnTerminalGlyphGroup(
806
836
  normalizedPins,
@@ -809,6 +839,24 @@ export class SchematicPinParser {
809
839
  )
810
840
  ) {
811
841
  labelMode = 'hidden'
842
+ } else if (
843
+ ownerDrawnInternalPinOwners.has(ownerIndex) &&
844
+ SchematicPinParser.#isCompactInternalTerminalGroup(
845
+ normalizedPins,
846
+ names,
847
+ orientationCount
848
+ )
849
+ ) {
850
+ labelMode = 'hidden'
851
+ } else if (
852
+ ownerDrawnInternalPinOwners.has(ownerIndex) &&
853
+ SchematicPinParser.#isCompactTwoPinInternalTerminalGroup(
854
+ normalizedPins,
855
+ names,
856
+ orientationCount
857
+ )
858
+ ) {
859
+ labelMode = 'number-only'
812
860
  } else if (!semanticNames.length && orientationCount <= 2) {
813
861
  labelMode = 'number-only'
814
862
  } else if (
@@ -827,6 +875,33 @@ export class SchematicPinParser {
827
875
  }))
828
876
  }
829
877
 
878
+ /**
879
+ * Returns true when a compact owner-drawn FET body uses semantic terminal
880
+ * names internally but still exposes external numeric contact labels.
881
+ * @param {{ designator: string, name: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
882
+ * @param {string[]} semanticNames
883
+ * @param {number} orientationCount
884
+ * @returns {boolean}
885
+ */
886
+ static #isCompactNumberedFetTerminalGroup(
887
+ pins,
888
+ semanticNames,
889
+ orientationCount
890
+ ) {
891
+ if (
892
+ pins.length !== 4 ||
893
+ orientationCount < 3 ||
894
+ semanticNames.length !== pins.length ||
895
+ !pins.every((pin) => /^\d+$/.test(String(pin.designator || '')))
896
+ ) {
897
+ return false
898
+ }
899
+
900
+ return semanticNames.every((name) =>
901
+ SchematicPinParser.#isFetTerminalName(name)
902
+ )
903
+ }
904
+
830
905
  /**
831
906
  * Returns true when a compact multi-side owner has transistor-like terminal
832
907
  * letters that are part of the drawn symbol body, not external pin labels.
@@ -858,6 +933,60 @@ export class SchematicPinParser {
858
933
  )
859
934
  }
860
935
 
936
+ /**
937
+ * Returns true when a compact owner-drawn body carries repeated internal
938
+ * terminal names that belong to the symbol body, not external labels.
939
+ * @param {{ designator: string, name: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
940
+ * @param {string[]} names
941
+ * @param {number} orientationCount
942
+ * @returns {boolean}
943
+ */
944
+ static #isCompactInternalTerminalGroup(pins, names, orientationCount) {
945
+ if (
946
+ pins.length < 3 ||
947
+ pins.length > 4 ||
948
+ orientationCount < 3 ||
949
+ names.length >= pins.length ||
950
+ !SchematicPinParser.#hasOptionalNumericPinDesignators(pins)
951
+ ) {
952
+ return false
953
+ }
954
+
955
+ return (
956
+ names.length > 0 &&
957
+ names.every((name) =>
958
+ SchematicPinParser.#isInternalTerminalName(name)
959
+ )
960
+ )
961
+ }
962
+
963
+ /**
964
+ * Returns true when a compact two-pin owner-drawn symbol stores internal
965
+ * placeholder terminal names that should not be rendered as labels.
966
+ * @param {{ designator: string, name: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
967
+ * @param {string[]} names
968
+ * @param {number} orientationCount
969
+ * @returns {boolean}
970
+ */
971
+ static #isCompactTwoPinInternalTerminalGroup(
972
+ pins,
973
+ names,
974
+ orientationCount
975
+ ) {
976
+ if (
977
+ pins.length !== 2 ||
978
+ orientationCount < 2 ||
979
+ names.length !== pins.length ||
980
+ !SchematicPinParser.#hasOptionalNumericPinDesignators(pins)
981
+ ) {
982
+ return false
983
+ }
984
+
985
+ return names.every((name) =>
986
+ SchematicPinParser.#isInternalTerminalName(name)
987
+ )
988
+ }
989
+
861
990
  /**
862
991
  * Returns true when compact owner-drawn terminal glyph pins have either no
863
992
  * external designators or ordinary numeric pin numbers.
@@ -881,6 +1010,28 @@ export class SchematicPinParser {
881
1010
  return /^[BCDEGS]$/i.test(String(name || '').trim())
882
1011
  }
883
1012
 
1013
+ /**
1014
+ * Returns true for FET terminal names, including numbered gate/source
1015
+ * variants used by dual-gate symbols.
1016
+ * @param {string} name
1017
+ * @returns {boolean}
1018
+ */
1019
+ static #isFetTerminalName(name) {
1020
+ return /^(?:[DS]|[GS]\d*)$/i.test(String(name || '').trim())
1021
+ }
1022
+
1023
+ /**
1024
+ * Returns true for compact internal terminal labels usually drawn inside
1025
+ * owner-authored symbol bodies.
1026
+ * @param {string} name
1027
+ * @returns {boolean}
1028
+ */
1029
+ static #isInternalTerminalName(name) {
1030
+ return /^(x|y|gnd|agnd|dgnd|pgnd|vcc|vdd|vee|vss|nc)$/i.test(
1031
+ String(name || '').trim()
1032
+ )
1033
+ }
1034
+
884
1035
  /**
885
1036
  * Returns true when one passive two-pin symbol uses the ordinary 1/2 pin
886
1037
  * numbering that should stay hidden for simple resistor-like parts.
@@ -899,6 +1050,24 @@ export class SchematicPinParser {
899
1050
  return designators[0] === '1' && designators[1] === '2'
900
1051
  }
901
1052
 
1053
+ /**
1054
+ * Returns true when one owner exposes exactly two numeric endpoints.
1055
+ * @param {{ designator: string, name: string }[]} pins
1056
+ * @returns {boolean}
1057
+ */
1058
+ static #isTwoPinNumericEndpointGroup(pins) {
1059
+ if (pins.length !== 2) {
1060
+ return false
1061
+ }
1062
+
1063
+ return pins.every((pin) => {
1064
+ const designator = String(pin.designator || '').trim()
1065
+ const name = String(pin.name || '').trim()
1066
+
1067
+ return /^\d+$/.test(designator) && (!name || /^\d+$/.test(name))
1068
+ })
1069
+ }
1070
+
902
1071
  /**
903
1072
  * Returns true when one owner uses the dense two-sided horizontal 48/50
904
1073
  * pin family whose semantic names belong to the owner-drawn symbol body
@@ -1049,10 +1218,12 @@ export class SchematicPinParser {
1049
1218
  static #inferSchematicPinOrientation(conglomerate) {
1050
1219
  switch (conglomerate) {
1051
1220
  case 34:
1221
+ case 42:
1052
1222
  case 50:
1053
1223
  case 58:
1054
1224
  return 'left'
1055
1225
  case 32:
1226
+ case 40:
1056
1227
  case 48:
1057
1228
  case 56:
1058
1229
  return 'right'
@@ -43,4 +43,42 @@ export class SchematicSheetStyleResolver {
43
43
 
44
44
  return configuredXZones
45
45
  }
46
+
47
+ /**
48
+ * Resolves the displayed vertical sheet-zone count after the page size
49
+ * has been normalized.
50
+ * @param {{ width: number, height: number, yZones: number, paperSize?: string, sheetStyle?: number }} sheet
51
+ * @returns {number}
52
+ */
53
+ static resolveYZones(sheet) {
54
+ const configuredYZones = Math.max(Number(sheet?.yZones || 0), 1)
55
+ const paperSize = String(sheet?.paperSize || '')
56
+ .trim()
57
+ .toUpperCase()
58
+
59
+ if (Number(sheet?.sheetStyle || 0) !== 1 && !paperSize) {
60
+ return configuredYZones
61
+ }
62
+
63
+ const width = Number(sheet?.width || 0)
64
+ const height = Number(sheet?.height || 0)
65
+ if (height < width) {
66
+ return configuredYZones
67
+ }
68
+
69
+ if (
70
+ paperSize === 'A2' ||
71
+ (width === 1654 && height === 2339) ||
72
+ paperSize === 'A3' ||
73
+ (width === 1169 && height === 1654)
74
+ ) {
75
+ return 8
76
+ }
77
+
78
+ if (paperSize === 'A4' || (width === 827 && height === 1169)) {
79
+ return 4
80
+ }
81
+
82
+ return configuredYZones
83
+ }
46
84
  }
@@ -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
  /**