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.
- package/docs/api.md +37 -0
- package/docs/model-format.md +18 -0
- package/docs/schemas/altium_toolkit/normalized_model_a1.schema.json +2 -2
- package/docs/testing.md +5 -0
- package/package.json +1 -1
- package/spec/library-scope.md +5 -0
- package/src/core/altium/AltiumLayoutParser.mjs +275 -13
- package/src/core/altium/AltiumLibraryBatchExporter.mjs +206 -0
- package/src/core/altium/AltiumLibraryRecordBuilder.mjs +293 -0
- package/src/core/altium/AltiumParser.mjs +245 -10
- package/src/core/altium/AltiumPcbLibExporter.mjs +101 -0
- package/src/core/altium/AltiumSchLibExporter.mjs +57 -0
- package/src/core/altium/AsciiRecordParser.mjs +43 -11
- package/src/core/altium/PcbComponentKindPolicy.mjs +9 -9
- package/src/core/altium/PcbEmbeddedFontExtractor.mjs +186 -43
- package/src/core/altium/PcbEmbeddedModelExtractor.mjs +22 -3
- package/src/core/altium/PcbOutlineRecovery.mjs +94 -0
- package/src/core/altium/PrintableTextDecoder.mjs +133 -13
- package/src/core/altium/SchematicComponentOwnerTextResolver.mjs +13 -0
- package/src/core/altium/SchematicComponentTextResolver.mjs +40 -1
- package/src/core/altium/SchematicDirectiveParser.mjs +5 -17
- package/src/core/altium/SchematicImageParser.mjs +291 -6
- package/src/core/altium/SchematicMultipartDesignatorNormalizer.mjs +164 -0
- package/src/core/altium/SchematicMultipartOwnerMatcher.mjs +2 -0
- package/src/core/altium/SchematicNoErcSymbolResolver.mjs +36 -0
- package/src/core/altium/SchematicPinParser.mjs +262 -24
- package/src/core/altium/SchematicPrimitiveParser.mjs +116 -8
- package/src/core/altium/SchematicSheetStyleResolver.mjs +38 -0
- package/src/core/altium/SchematicStreamExtractor.mjs +62 -15
- package/src/core/altium/SchematicTextParser.mjs +125 -11
- package/src/core/altium/SchematicTextPostProcessor.mjs +146 -102
- package/src/core/altium/SourceBundleExporter.mjs +156 -0
- package/src/core/altium/SourceComponentBundleNormalizer.mjs +295 -0
- package/src/core/altium/SourceComponentClient.mjs +239 -0
- package/src/core/ole/OleCompoundDocumentWriter.mjs +449 -0
- package/src/parser.mjs +8 -0
- package/src/styles/altium-renderers.css +6 -6
- package/src/ui/PcbArcUtils.mjs +19 -2
- package/src/ui/PcbScene3dBuilder.mjs +202 -20
- package/src/ui/PcbScene3dModelRegistry.mjs +28 -18
- package/src/ui/PcbScene3dPlacementSideResolver.mjs +48 -6
- package/src/ui/SchematicColorResolver.mjs +263 -0
- package/src/ui/SchematicContentLayout.mjs +58 -1
- package/src/ui/SchematicDirectiveRenderer.mjs +133 -22
- package/src/ui/SchematicImageRenderer.mjs +125 -10
- package/src/ui/SchematicJunctionRenderer.mjs +1 -1
- package/src/ui/SchematicLineColorResolver.mjs +88 -0
- package/src/ui/SchematicNativeFooterPartitioner.mjs +275 -0
- package/src/ui/SchematicNoteRenderer.mjs +87 -7
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +560 -10
- package/src/ui/SchematicOwnerPinMarkerLineThemer.mjs +155 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +397 -48
- package/src/ui/SchematicPowerDiagramImageProcessor.mjs +970 -0
- package/src/ui/SchematicPowerDiagramLineMasks.mjs +631 -0
- package/src/ui/SchematicPowerPortRenderer.mjs +1 -1
- package/src/ui/SchematicShapeRenderer.mjs +109 -24
- 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
|
-
|
|
66
|
-
|
|
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(
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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(
|
|
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 (
|
|
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
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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:
|
|
406
|
-
|
|
407
|
-
|
|
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
|
}
|