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
@@ -3,6 +3,7 @@
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later
4
4
 
5
5
  import { ParserUtils } from './ParserUtils.mjs'
6
+ import { SchematicNoErcSymbolResolver } from './SchematicNoErcSymbolResolver.mjs'
6
7
  import { SchematicPinDesignatorInferer } from './SchematicPinDesignatorInferer.mjs'
7
8
 
8
9
  /**
@@ -12,10 +13,15 @@ export class SchematicPinParser {
12
13
  /**
13
14
  * Normalizes schematic pin records into drawable pin primitives.
14
15
  * @param {{ fields: Record<string, string | string[]> }[]} records
16
+ * @param {{ ownerDrawnInternalPinOwners?: Set<string>, numericEndpointLabelOwners?: Set<string> }} [options]
15
17
  * @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
18
  */
17
- static parseSchematicPins(records) {
19
+ static parseSchematicPins(records, options = {}) {
18
20
  const groups = new Map()
21
+ const ownerDrawnInternalPinOwners =
22
+ options.ownerDrawnInternalPinOwners || new Set()
23
+ const numericEndpointLabelOwners =
24
+ options.numericEndpointLabelOwners || new Set()
19
25
 
20
26
  for (const record of records) {
21
27
  const ownerIndex = ParserUtils.getField(record.fields, 'OwnerIndex')
@@ -61,11 +67,9 @@ export class SchematicPinParser {
61
67
  ),
62
68
  designator: ParserUtils.getField(record.fields, 'Designator'),
63
69
  orientation,
64
- electrical:
65
- ParserUtils.parseNumericField(
66
- record.fields,
67
- 'Electrical'
68
- ) || undefined,
70
+ electrical: SchematicPinParser.#parseSchematicPinElectrical(
71
+ record.fields
72
+ ),
69
73
  symbolOuter:
70
74
  ParserUtils.parseNumericField(
71
75
  record.fields,
@@ -81,8 +85,36 @@ export class SchematicPinParser {
81
85
  }
82
86
 
83
87
  return [...groups.values()].flatMap((pins) =>
84
- SchematicPinParser.#normalizeSchematicPinGroup(pins)
88
+ SchematicPinParser.#normalizeSchematicPinGroup(
89
+ pins,
90
+ ownerDrawnInternalPinOwners,
91
+ numericEndpointLabelOwners
92
+ )
93
+ )
94
+ }
95
+
96
+ /**
97
+ * Parses Altium's pin electrical type, including its omitted-field default.
98
+ * Formal schematic pin records omit Electrical for input pins and serialize
99
+ * passive pins explicitly as Electrical=4.
100
+ * @param {Record<string, string | string[]>} fields Pin record fields.
101
+ * @returns {number | undefined}
102
+ */
103
+ static #parseSchematicPinElectrical(fields) {
104
+ const explicitElectrical = ParserUtils.parseNumericField(
105
+ fields,
106
+ 'Electrical'
85
107
  )
108
+
109
+ if (explicitElectrical !== null) {
110
+ return explicitElectrical
111
+ }
112
+
113
+ if (ParserUtils.parseNumericField(fields, 'FormalType') !== null) {
114
+ return 0
115
+ }
116
+
117
+ return undefined
86
118
  }
87
119
 
88
120
  /**
@@ -377,24 +409,36 @@ export class SchematicPinParser {
377
409
  /**
378
410
  * Normalizes no-connect crosses from schematic records.
379
411
  * @param {{ fields: Record<string, string | string[]> }[]} records
380
- * @returns {{ x: number, y: number, size: number, color: string }[]}
412
+ * @returns {{ x: number, y: number, size: number, color: string, symbol: number | null, symbolName: string }[]}
381
413
  */
382
414
  static parseSchematicCrosses(records) {
383
415
  return records
384
- .map((record) => ({
385
- x:
386
- ParserUtils.parseNumericField(
387
- record.fields,
388
- 'Location.X'
389
- ) || 0,
390
- y:
391
- ParserUtils.parseNumericField(
392
- record.fields,
393
- 'Location.Y'
394
- ) || 0,
395
- size: 6,
396
- color: ParserUtils.toColor(record.fields.Color, '#ff0000')
397
- }))
416
+ .map((record) => {
417
+ const rawSymbol = ParserUtils.getField(record.fields, 'Symbol')
418
+ const symbol = ParserUtils.parseNumericField(
419
+ record.fields,
420
+ 'Symbol'
421
+ )
422
+
423
+ return {
424
+ x:
425
+ ParserUtils.parseNumericField(
426
+ record.fields,
427
+ 'Location.X'
428
+ ) || 0,
429
+ y:
430
+ ParserUtils.parseNumericField(
431
+ record.fields,
432
+ 'Location.Y'
433
+ ) || 0,
434
+ size: 6,
435
+ color: ParserUtils.toColor(record.fields.Color, '#ff0000'),
436
+ symbol,
437
+ symbolName: SchematicNoErcSymbolResolver.resolveSymbolName(
438
+ rawSymbol || symbol
439
+ )
440
+ }
441
+ })
398
442
  .filter((cross) => cross.x || cross.y)
399
443
  }
400
444
 
@@ -730,9 +774,15 @@ export class SchematicPinParser {
730
774
  /**
731
775
  * Deduces the visible pins for one schematic symbol owner.
732
776
  * @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
777
+ * @param {Set<string>} ownerDrawnInternalPinOwners
778
+ * @param {Set<string>} numericEndpointLabelOwners
733
779
  * @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
780
  */
735
- static #normalizeSchematicPinGroup(pins) {
781
+ static #normalizeSchematicPinGroup(
782
+ pins,
783
+ ownerDrawnInternalPinOwners,
784
+ numericEndpointLabelOwners
785
+ ) {
736
786
  const deduped = SchematicPinParser.#dedupeSchematicPins(pins)
737
787
  const inferredSequentialDesignators =
738
788
  SchematicPinDesignatorInferer.inferSequentialCompactFourPinDesignators(
@@ -773,6 +823,7 @@ export class SchematicPinParser {
773
823
  /^\d+$/.test(String(pin.designator || '').trim()) &&
774
824
  (!pin.name || /^\d+$/.test(String(pin.name || '').trim()))
775
825
  )
826
+ const ownerIndex = normalizedPins[0]?.ownerIndex || ''
776
827
  let labelMode = 'name-and-number'
777
828
 
778
829
  if (
@@ -795,12 +846,33 @@ export class SchematicPinParser {
795
846
  labelMode = 'number-only'
796
847
  }
797
848
 
798
- if (allPassive && normalizedPins.length <= 2) {
849
+ if (
850
+ numericEndpointLabelOwners.has(ownerIndex) &&
851
+ SchematicPinParser.#isTwoPinNumericEndpointGroup(normalizedPins)
852
+ ) {
853
+ labelMode = 'number-only'
854
+ } else if (
855
+ SchematicPinParser.#isCompactSinglePinMarkerGroup(
856
+ normalizedPins,
857
+ names
858
+ )
859
+ ) {
860
+ labelMode = 'hidden'
861
+ } else if (allPassive && normalizedPins.length <= 2) {
799
862
  labelMode = SchematicPinParser.#isCanonicalPassiveTwoPinGroup(
800
863
  normalizedPins
801
864
  )
802
865
  ? 'hidden'
803
866
  : 'number-only'
867
+ } else if (
868
+ ownerDrawnInternalPinOwners.has(ownerIndex) &&
869
+ SchematicPinParser.#isCompactNumberedFetTerminalGroup(
870
+ normalizedPins,
871
+ semanticNames,
872
+ orientationCount
873
+ )
874
+ ) {
875
+ labelMode = 'number-only'
804
876
  } else if (
805
877
  SchematicPinParser.#isOwnerDrawnTerminalGlyphGroup(
806
878
  normalizedPins,
@@ -809,6 +881,24 @@ export class SchematicPinParser {
809
881
  )
810
882
  ) {
811
883
  labelMode = 'hidden'
884
+ } else if (
885
+ ownerDrawnInternalPinOwners.has(ownerIndex) &&
886
+ SchematicPinParser.#isCompactInternalTerminalGroup(
887
+ normalizedPins,
888
+ names,
889
+ orientationCount
890
+ )
891
+ ) {
892
+ labelMode = 'hidden'
893
+ } else if (
894
+ ownerDrawnInternalPinOwners.has(ownerIndex) &&
895
+ SchematicPinParser.#isCompactTwoPinInternalTerminalGroup(
896
+ normalizedPins,
897
+ names,
898
+ orientationCount
899
+ )
900
+ ) {
901
+ labelMode = 'number-only'
812
902
  } else if (!semanticNames.length && orientationCount <= 2) {
813
903
  labelMode = 'number-only'
814
904
  } else if (
@@ -827,6 +917,58 @@ export class SchematicPinParser {
827
917
  }))
828
918
  }
829
919
 
920
+ /**
921
+ * Returns true when a single owner pin belongs to compact marker artwork
922
+ * rather than to a visibly numbered electrical contact.
923
+ * @param {{ designator: string, name: string, length: number, electrical?: number }[]} pins
924
+ * @param {string[]} names
925
+ * @returns {boolean}
926
+ */
927
+ static #isCompactSinglePinMarkerGroup(pins, names) {
928
+ if (pins.length !== 1 || names.length !== 0) {
929
+ return false
930
+ }
931
+
932
+ const pin = pins[0]
933
+ const designator = String(pin.designator || '').trim()
934
+ const length = Math.abs(Number(pin.length || 0))
935
+ const electrical = Number(pin.electrical)
936
+
937
+ return (
938
+ /^\d+$/.test(designator) &&
939
+ length > 0 &&
940
+ length <= 15 &&
941
+ (!Number.isFinite(electrical) || electrical === 4)
942
+ )
943
+ }
944
+
945
+ /**
946
+ * Returns true when a compact owner-drawn FET body uses semantic terminal
947
+ * names internally but still exposes external numeric contact labels.
948
+ * @param {{ designator: string, name: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
949
+ * @param {string[]} semanticNames
950
+ * @param {number} orientationCount
951
+ * @returns {boolean}
952
+ */
953
+ static #isCompactNumberedFetTerminalGroup(
954
+ pins,
955
+ semanticNames,
956
+ orientationCount
957
+ ) {
958
+ if (
959
+ pins.length !== 4 ||
960
+ orientationCount < 3 ||
961
+ semanticNames.length !== pins.length ||
962
+ !pins.every((pin) => /^\d+$/.test(String(pin.designator || '')))
963
+ ) {
964
+ return false
965
+ }
966
+
967
+ return semanticNames.every((name) =>
968
+ SchematicPinParser.#isFetTerminalName(name)
969
+ )
970
+ }
971
+
830
972
  /**
831
973
  * Returns true when a compact multi-side owner has transistor-like terminal
832
974
  * letters that are part of the drawn symbol body, not external pin labels.
@@ -858,6 +1000,60 @@ export class SchematicPinParser {
858
1000
  )
859
1001
  }
860
1002
 
1003
+ /**
1004
+ * Returns true when a compact owner-drawn body carries repeated internal
1005
+ * terminal names that belong to the symbol body, not external labels.
1006
+ * @param {{ designator: string, name: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
1007
+ * @param {string[]} names
1008
+ * @param {number} orientationCount
1009
+ * @returns {boolean}
1010
+ */
1011
+ static #isCompactInternalTerminalGroup(pins, names, orientationCount) {
1012
+ if (
1013
+ pins.length < 3 ||
1014
+ pins.length > 4 ||
1015
+ orientationCount < 3 ||
1016
+ names.length >= pins.length ||
1017
+ !SchematicPinParser.#hasOptionalNumericPinDesignators(pins)
1018
+ ) {
1019
+ return false
1020
+ }
1021
+
1022
+ return (
1023
+ names.length > 0 &&
1024
+ names.every((name) =>
1025
+ SchematicPinParser.#isInternalTerminalName(name)
1026
+ )
1027
+ )
1028
+ }
1029
+
1030
+ /**
1031
+ * Returns true when a compact two-pin owner-drawn symbol stores internal
1032
+ * placeholder terminal names that should not be rendered as labels.
1033
+ * @param {{ designator: string, name: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
1034
+ * @param {string[]} names
1035
+ * @param {number} orientationCount
1036
+ * @returns {boolean}
1037
+ */
1038
+ static #isCompactTwoPinInternalTerminalGroup(
1039
+ pins,
1040
+ names,
1041
+ orientationCount
1042
+ ) {
1043
+ if (
1044
+ pins.length !== 2 ||
1045
+ orientationCount < 2 ||
1046
+ names.length !== pins.length ||
1047
+ !SchematicPinParser.#hasOptionalNumericPinDesignators(pins)
1048
+ ) {
1049
+ return false
1050
+ }
1051
+
1052
+ return names.every((name) =>
1053
+ SchematicPinParser.#isInternalTerminalName(name)
1054
+ )
1055
+ }
1056
+
861
1057
  /**
862
1058
  * Returns true when compact owner-drawn terminal glyph pins have either no
863
1059
  * external designators or ordinary numeric pin numbers.
@@ -881,6 +1077,28 @@ export class SchematicPinParser {
881
1077
  return /^[BCDEGS]$/i.test(String(name || '').trim())
882
1078
  }
883
1079
 
1080
+ /**
1081
+ * Returns true for FET terminal names, including numbered gate/source
1082
+ * variants used by dual-gate symbols.
1083
+ * @param {string} name
1084
+ * @returns {boolean}
1085
+ */
1086
+ static #isFetTerminalName(name) {
1087
+ return /^(?:[DS]|[GS]\d*)$/i.test(String(name || '').trim())
1088
+ }
1089
+
1090
+ /**
1091
+ * Returns true for compact internal terminal labels usually drawn inside
1092
+ * owner-authored symbol bodies.
1093
+ * @param {string} name
1094
+ * @returns {boolean}
1095
+ */
1096
+ static #isInternalTerminalName(name) {
1097
+ return /^(x|y|gnd|agnd|dgnd|pgnd|vcc|vdd|vee|vss|nc)$/i.test(
1098
+ String(name || '').trim()
1099
+ )
1100
+ }
1101
+
884
1102
  /**
885
1103
  * Returns true when one passive two-pin symbol uses the ordinary 1/2 pin
886
1104
  * numbering that should stay hidden for simple resistor-like parts.
@@ -899,6 +1117,24 @@ export class SchematicPinParser {
899
1117
  return designators[0] === '1' && designators[1] === '2'
900
1118
  }
901
1119
 
1120
+ /**
1121
+ * Returns true when one owner exposes exactly two numeric endpoints.
1122
+ * @param {{ designator: string, name: string }[]} pins
1123
+ * @returns {boolean}
1124
+ */
1125
+ static #isTwoPinNumericEndpointGroup(pins) {
1126
+ if (pins.length !== 2) {
1127
+ return false
1128
+ }
1129
+
1130
+ return pins.every((pin) => {
1131
+ const designator = String(pin.designator || '').trim()
1132
+ const name = String(pin.name || '').trim()
1133
+
1134
+ return /^\d+$/.test(designator) && (!name || /^\d+$/.test(name))
1135
+ })
1136
+ }
1137
+
902
1138
  /**
903
1139
  * Returns true when one owner uses the dense two-sided horizontal 48/50
904
1140
  * pin family whose semantic names belong to the owner-drawn symbol body
@@ -1049,10 +1285,12 @@ export class SchematicPinParser {
1049
1285
  static #inferSchematicPinOrientation(conglomerate) {
1050
1286
  switch (conglomerate) {
1051
1287
  case 34:
1288
+ case 42:
1052
1289
  case 50:
1053
1290
  case 58:
1054
1291
  return 'left'
1055
1292
  case 32:
1293
+ case 40:
1056
1294
  case 48:
1057
1295
  case 56:
1058
1296
  return 'right'
@@ -98,6 +98,18 @@ export class SchematicPrimitiveParser {
98
98
  )
99
99
  }
100
100
 
101
+ /**
102
+ * Returns true when one point-listed record carries only an axis-aligned
103
+ * rectangular point list without authored `Location` and `Corner` fields.
104
+ * @param {Record<string, string | string[]>} fields
105
+ * @returns {boolean}
106
+ */
107
+ static isPointListedRectangleRecord(fields) {
108
+ return Boolean(
109
+ SchematicPrimitiveParser.#pointListedRectangleBounds(fields)
110
+ )
111
+ }
112
+
101
113
  /**
102
114
  * Normalizes record-7 polygon primitives into fill-capable polygons.
103
115
  * @param {{ fields: Record<string, string | string[]> }[]} records
@@ -352,16 +364,32 @@ export class SchematicPrimitiveParser {
352
364
  static parseSchematicRectangles(records) {
353
365
  return records
354
366
  .map((record, index) => {
355
- const x1 = parseNumericField(record.fields, 'Location.X')
356
- const y1 = parseNumericField(record.fields, 'Location.Y')
357
- const x2 = parseNumericField(record.fields, 'Corner.X')
358
- const y2 = parseNumericField(record.fields, 'Corner.Y')
367
+ const pointListedBounds =
368
+ SchematicPrimitiveParser.#pointListedRectangleBounds(
369
+ record.fields
370
+ )
371
+ const x1 =
372
+ parseNumericField(record.fields, 'Location.X') ??
373
+ pointListedBounds?.minX ??
374
+ null
375
+ const y1 =
376
+ parseNumericField(record.fields, 'Location.Y') ??
377
+ pointListedBounds?.minY ??
378
+ null
379
+ const x2 =
380
+ parseNumericField(record.fields, 'Corner.X') ??
381
+ pointListedBounds?.maxX ??
382
+ null
383
+ const y2 =
384
+ parseNumericField(record.fields, 'Corner.Y') ??
385
+ pointListedBounds?.maxY ??
386
+ null
359
387
  const isRectangleRecord =
360
388
  SchematicPrimitiveParser.isRectangleRecord(record.fields)
361
389
  const isListedRectangle =
362
390
  SchematicPrimitiveParser.isListedRectangleRecord(
363
391
  record.fields
364
- )
392
+ ) || Boolean(pointListedBounds)
365
393
  const usesFrameFallback =
366
394
  SchematicPrimitiveParser.#shouldUseFrameFallback(
367
395
  record.fields,
@@ -402,9 +430,15 @@ export class SchematicPrimitiveParser {
402
430
  : parseBoolean(record.fields.Transparent),
403
431
  lineWidth:
404
432
  parseNumericField(record.fields, 'LineWidth') || 1,
405
- lineStyle: usesFrameFallback
406
- ? 1
407
- : parseNumericField(record.fields, 'LineStyle') || 0,
433
+ lineStyle:
434
+ usesFrameFallback ||
435
+ SchematicPrimitiveParser.#shouldDefaultBlanketDash(
436
+ record.fields,
437
+ pointListedBounds
438
+ )
439
+ ? 1
440
+ : parseNumericField(record.fields, 'LineStyle') ||
441
+ 0,
408
442
  renderOrder: SchematicPrimitiveParser.#resolveRenderOrder(
409
443
  record.fields,
410
444
  index
@@ -989,6 +1023,80 @@ export class SchematicPrimitiveParser {
989
1023
  )
990
1024
  }
991
1025
 
1026
+ /**
1027
+ * Returns true when a point-listed blanket should use Altium's dashed frame
1028
+ * style even when the printable record omits `LineStyle`.
1029
+ * @param {Record<string, string | string[]>} fields Record fields.
1030
+ * @param {{ minX: number, minY: number, maxX: number, maxY: number } | null} pointListedBounds Point-listed bounds.
1031
+ * @returns {boolean}
1032
+ */
1033
+ static #shouldDefaultBlanketDash(fields, pointListedBounds) {
1034
+ return (
1035
+ Boolean(pointListedBounds) &&
1036
+ getField(fields, 'RECORD') === '225' &&
1037
+ getField(fields, 'LineStyle') === ''
1038
+ )
1039
+ }
1040
+
1041
+ /**
1042
+ * Resolves rectangle bounds from a point-listed axis-aligned frame.
1043
+ * @param {Record<string, string | string[]>} fields Record fields.
1044
+ * @returns {{ minX: number, minY: number, maxX: number, maxY: number } | null}
1045
+ */
1046
+ static #pointListedRectangleBounds(fields) {
1047
+ const points = SchematicPrimitiveParser.#normalizeClosedPointList(
1048
+ SchematicPrimitiveParser.#collectPolygonPoints(fields)
1049
+ )
1050
+
1051
+ if (points.length !== 4) {
1052
+ return null
1053
+ }
1054
+
1055
+ const xs = [...new Set(points.map((point) => point.x))]
1056
+ const ys = [...new Set(points.map((point) => point.y))]
1057
+
1058
+ if (xs.length !== 2 || ys.length !== 2) {
1059
+ return null
1060
+ }
1061
+
1062
+ const minX = Math.min(...xs)
1063
+ const maxX = Math.max(...xs)
1064
+ const minY = Math.min(...ys)
1065
+ const maxY = Math.max(...ys)
1066
+ const corners = new Set([
1067
+ minX + ':' + minY,
1068
+ minX + ':' + maxY,
1069
+ maxX + ':' + minY,
1070
+ maxX + ':' + maxY
1071
+ ])
1072
+
1073
+ if (!points.every((point) => corners.has(point.x + ':' + point.y))) {
1074
+ return null
1075
+ }
1076
+
1077
+ return { minX, minY, maxX, maxY }
1078
+ }
1079
+
1080
+ /**
1081
+ * Removes an optional repeated closing point from one source point list.
1082
+ * @param {{ x: number, y: number }[]} points Source points.
1083
+ * @returns {{ x: number, y: number }[]}
1084
+ */
1085
+ static #normalizeClosedPointList(points) {
1086
+ if (points.length < 2) {
1087
+ return points
1088
+ }
1089
+
1090
+ const first = points[0]
1091
+ const last = points.at(-1)
1092
+
1093
+ if (first.x === last.x && first.y === last.y) {
1094
+ return points.slice(0, -1)
1095
+ }
1096
+
1097
+ return points
1098
+ }
1099
+
992
1100
  /**
993
1101
  * Collects one record-7 polygon point list in source order.
994
1102
  * @param {Record<string, string | string[]>} fields
@@ -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
  }