altium-toolkit 1.1.38 → 1.1.40
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/package.json +1 -1
- package/src/core/altium/SchematicDirectiveParser.mjs +14 -2
- package/src/core/altium/SchematicPinParser.mjs +151 -13
- package/src/core/altium/SchematicTextOrientationResolver.mjs +31 -2
- package/src/core/altium/SchematicTextParser.mjs +1 -1
- package/src/ui/SchematicColorResolver.mjs +30 -0
- package/src/ui/SchematicDirectiveRenderer.mjs +69 -3
- package/src/ui/SchematicLineColorResolver.mjs +4 -2
- package/src/ui/SchematicOwnerPinLabelLayout.mjs +97 -0
- package/src/ui/SchematicPinSvgRenderer.mjs +10 -6
- package/src/ui/SchematicSvgRenderer.mjs +22 -6
package/package.json
CHANGED
|
@@ -12,7 +12,7 @@ export class SchematicDirectiveParser {
|
|
|
12
12
|
/**
|
|
13
13
|
* Normalizes schematic directive records into drawable directive metadata.
|
|
14
14
|
* @param {{ fields: Record<string, string | string[]> }[]} records
|
|
15
|
-
* @returns {{ x: number, y: number, color: string, name: string, orientation: number }[]}
|
|
15
|
+
* @returns {{ x: number, y: number, color: string, name: string, orientation: number, style?: number }[]}
|
|
16
16
|
*/
|
|
17
17
|
static parseSchematicDirectives(records) {
|
|
18
18
|
return records
|
|
@@ -31,7 +31,11 @@ export class SchematicDirectiveParser {
|
|
|
31
31
|
return null
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
|
|
34
|
+
const style = ParserUtils.parseNumericField(
|
|
35
|
+
record.fields,
|
|
36
|
+
'Style'
|
|
37
|
+
)
|
|
38
|
+
const directive = {
|
|
35
39
|
x,
|
|
36
40
|
y,
|
|
37
41
|
color: ParserUtils.toColor(record.fields.Color, '#ff0000'),
|
|
@@ -42,6 +46,12 @@ export class SchematicDirectiveParser {
|
|
|
42
46
|
'Orientation'
|
|
43
47
|
) || 0
|
|
44
48
|
}
|
|
49
|
+
|
|
50
|
+
if (style !== null) {
|
|
51
|
+
directive.style = style
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return directive
|
|
45
55
|
})
|
|
46
56
|
.filter(Boolean)
|
|
47
57
|
}
|
|
@@ -210,6 +220,8 @@ export class SchematicDirectiveParser {
|
|
|
210
220
|
orientation:
|
|
211
221
|
ParserUtils.parseNumericField(record.fields, 'Orientation') ||
|
|
212
222
|
0,
|
|
223
|
+
style:
|
|
224
|
+
ParserUtils.parseNumericField(record.fields, 'Style') ?? null,
|
|
213
225
|
parameters,
|
|
214
226
|
parameterMap,
|
|
215
227
|
isDifferentialPair,
|
|
@@ -873,11 +873,18 @@ export class SchematicPinParser {
|
|
|
873
873
|
ownerDrawnInternalPinOwners.has(ownerIndex) &&
|
|
874
874
|
SchematicPinParser.#isCompactNumberedFetTerminalGroup(
|
|
875
875
|
normalizedPins,
|
|
876
|
-
semanticNames,
|
|
877
876
|
orientationCount
|
|
878
877
|
)
|
|
879
878
|
) {
|
|
880
879
|
labelMode = 'number-only'
|
|
880
|
+
} else if (
|
|
881
|
+
ownerDrawnInternalPinOwners.has(ownerIndex) &&
|
|
882
|
+
SchematicPinParser.#isOwnerDrawnAmplifierTerminalGroup(
|
|
883
|
+
normalizedPins,
|
|
884
|
+
semanticNames
|
|
885
|
+
)
|
|
886
|
+
) {
|
|
887
|
+
labelMode = 'number-only'
|
|
881
888
|
} else if (
|
|
882
889
|
SchematicPinParser.#isOwnerDrawnTerminalGlyphGroup(
|
|
883
890
|
normalizedPins,
|
|
@@ -904,6 +911,15 @@ export class SchematicPinParser {
|
|
|
904
911
|
)
|
|
905
912
|
) {
|
|
906
913
|
labelMode = 'number-only'
|
|
914
|
+
} else if (
|
|
915
|
+
ownerDrawnInternalPinOwners.has(ownerIndex) &&
|
|
916
|
+
SchematicPinParser.#isCompactCommonTerminalDiodeGroup(
|
|
917
|
+
normalizedPins,
|
|
918
|
+
names,
|
|
919
|
+
orientationCount
|
|
920
|
+
)
|
|
921
|
+
) {
|
|
922
|
+
labelMode = 'number-only'
|
|
907
923
|
} else if (!semanticNames.length && orientationCount <= 2) {
|
|
908
924
|
labelMode = 'number-only'
|
|
909
925
|
} else if (
|
|
@@ -948,32 +964,94 @@ export class SchematicPinParser {
|
|
|
948
964
|
}
|
|
949
965
|
|
|
950
966
|
/**
|
|
951
|
-
* Returns true when a compact owner-drawn FET body
|
|
952
|
-
* names internally but still exposes external numeric
|
|
967
|
+
* Returns true when a compact owner-drawn FET body or pin array uses
|
|
968
|
+
* semantic terminal names internally but still exposes external numeric
|
|
969
|
+
* contact labels.
|
|
953
970
|
* @param {{ designator: string, name: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
|
|
954
|
-
* @param {string[]} semanticNames
|
|
955
971
|
* @param {number} orientationCount
|
|
956
972
|
* @returns {boolean}
|
|
957
973
|
*/
|
|
958
|
-
static #isCompactNumberedFetTerminalGroup(
|
|
959
|
-
pins,
|
|
960
|
-
semanticNames,
|
|
961
|
-
orientationCount
|
|
962
|
-
) {
|
|
974
|
+
static #isCompactNumberedFetTerminalGroup(pins, orientationCount) {
|
|
963
975
|
if (
|
|
964
|
-
pins.length
|
|
965
|
-
|
|
976
|
+
pins.length < 4 ||
|
|
977
|
+
pins.length > 12 ||
|
|
978
|
+
orientationCount < 2 ||
|
|
979
|
+
!pins.every((pin) => /^\d+$/.test(String(pin.designator || '')))
|
|
980
|
+
) {
|
|
981
|
+
return false
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
const normalizedNames = pins.map((pin) =>
|
|
985
|
+
String(pin.name || '')
|
|
986
|
+
.trim()
|
|
987
|
+
.toUpperCase()
|
|
988
|
+
)
|
|
989
|
+
|
|
990
|
+
return (
|
|
991
|
+
new Set(normalizedNames).size >= 2 &&
|
|
992
|
+
normalizedNames.every((name) =>
|
|
993
|
+
SchematicPinParser.#isFetTerminalName(name)
|
|
994
|
+
)
|
|
995
|
+
)
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
/**
|
|
999
|
+
* Returns true when an owner-drawn amplifier body already represents its
|
|
1000
|
+
* input, output, and supply terminal names in the drawn symbol artwork.
|
|
1001
|
+
* @param {{ designator: string, name: string }[]} pins
|
|
1002
|
+
* @param {string[]} semanticNames
|
|
1003
|
+
* @returns {boolean}
|
|
1004
|
+
*/
|
|
1005
|
+
static #isOwnerDrawnAmplifierTerminalGroup(pins, semanticNames) {
|
|
1006
|
+
if (
|
|
1007
|
+
pins.length < 3 ||
|
|
1008
|
+
pins.length > 8 ||
|
|
966
1009
|
semanticNames.length !== pins.length ||
|
|
967
1010
|
!pins.every((pin) => /^\d+$/.test(String(pin.designator || '')))
|
|
968
1011
|
) {
|
|
969
1012
|
return false
|
|
970
1013
|
}
|
|
971
1014
|
|
|
972
|
-
|
|
973
|
-
SchematicPinParser.#
|
|
1015
|
+
const normalizedNames = semanticNames.map((name) =>
|
|
1016
|
+
SchematicPinParser.#normalizeAmplifierTerminalName(name)
|
|
1017
|
+
)
|
|
1018
|
+
|
|
1019
|
+
return (
|
|
1020
|
+
normalizedNames.every(Boolean) &&
|
|
1021
|
+
normalizedNames.some((name) => name === 'IN+' || name === 'IN-') &&
|
|
1022
|
+
normalizedNames.includes('OUT')
|
|
974
1023
|
)
|
|
975
1024
|
}
|
|
976
1025
|
|
|
1026
|
+
/**
|
|
1027
|
+
* Normalizes amplifier terminal names that are normally drawn inside the
|
|
1028
|
+
* owner-authored symbol body.
|
|
1029
|
+
* @param {string} name Raw pin name.
|
|
1030
|
+
* @returns {string}
|
|
1031
|
+
*/
|
|
1032
|
+
static #normalizeAmplifierTerminalName(name) {
|
|
1033
|
+
const normalized = String(name || '')
|
|
1034
|
+
.trim()
|
|
1035
|
+
.toUpperCase()
|
|
1036
|
+
|
|
1037
|
+
switch (normalized) {
|
|
1038
|
+
case 'IN+':
|
|
1039
|
+
case '+IN':
|
|
1040
|
+
case '+':
|
|
1041
|
+
return 'IN+'
|
|
1042
|
+
case 'IN-':
|
|
1043
|
+
case '-IN':
|
|
1044
|
+
case '-':
|
|
1045
|
+
return 'IN-'
|
|
1046
|
+
case 'OUT':
|
|
1047
|
+
case 'V+':
|
|
1048
|
+
case 'V-':
|
|
1049
|
+
return normalized
|
|
1050
|
+
default:
|
|
1051
|
+
return ''
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
|
|
977
1055
|
/**
|
|
978
1056
|
* Returns true when a compact multi-side owner has transistor-like terminal
|
|
979
1057
|
* letters that are part of the drawn symbol body, not external pin labels.
|
|
@@ -1059,6 +1137,48 @@ export class SchematicPinParser {
|
|
|
1059
1137
|
)
|
|
1060
1138
|
}
|
|
1061
1139
|
|
|
1140
|
+
/**
|
|
1141
|
+
* Returns true when a compact owner-drawn diode-like section exposes one
|
|
1142
|
+
* ordinary anode/cathode terminal and one common terminal name internally.
|
|
1143
|
+
* @param {{ designator: string, name: string, length: number, electrical?: number, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
|
|
1144
|
+
* @param {string[]} names
|
|
1145
|
+
* @param {number} orientationCount
|
|
1146
|
+
* @returns {boolean}
|
|
1147
|
+
*/
|
|
1148
|
+
static #isCompactCommonTerminalDiodeGroup(pins, names, orientationCount) {
|
|
1149
|
+
if (
|
|
1150
|
+
pins.length !== 2 ||
|
|
1151
|
+
orientationCount !== 2 ||
|
|
1152
|
+
names.length !== pins.length ||
|
|
1153
|
+
!SchematicPinParser.#hasOptionalNumericPinDesignators(pins)
|
|
1154
|
+
) {
|
|
1155
|
+
return false
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
if (
|
|
1159
|
+
pins.some((pin) => {
|
|
1160
|
+
const length = Math.abs(Number(pin.length || 0))
|
|
1161
|
+
const electrical = Number(pin.electrical)
|
|
1162
|
+
return (
|
|
1163
|
+
length <= 0 ||
|
|
1164
|
+
length > 15 ||
|
|
1165
|
+
(Number.isFinite(electrical) && electrical !== 4)
|
|
1166
|
+
)
|
|
1167
|
+
})
|
|
1168
|
+
) {
|
|
1169
|
+
return false
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
const hasDiodeTerminal = names.some((name) =>
|
|
1173
|
+
SchematicPinParser.#isDiodeTerminalName(name)
|
|
1174
|
+
)
|
|
1175
|
+
const hasCommonTerminal = names.some((name) =>
|
|
1176
|
+
SchematicPinParser.#isCommonDiodeTerminalName(name)
|
|
1177
|
+
)
|
|
1178
|
+
|
|
1179
|
+
return hasDiodeTerminal && hasCommonTerminal
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1062
1182
|
/**
|
|
1063
1183
|
* Returns true when compact owner-drawn terminal glyph pins have either no
|
|
1064
1184
|
* external designators or ordinary numeric pin numbers.
|
|
@@ -1104,6 +1224,24 @@ export class SchematicPinParser {
|
|
|
1104
1224
|
)
|
|
1105
1225
|
}
|
|
1106
1226
|
|
|
1227
|
+
/**
|
|
1228
|
+
* Returns true for one-letter terminals used inside diode-style symbols.
|
|
1229
|
+
* @param {string} name
|
|
1230
|
+
* @returns {boolean}
|
|
1231
|
+
*/
|
|
1232
|
+
static #isDiodeTerminalName(name) {
|
|
1233
|
+
return /^[AKC]$/i.test(String(name || '').trim())
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* Returns true for common-terminal names used by multipart diode symbols.
|
|
1238
|
+
* @param {string} name
|
|
1239
|
+
* @returns {boolean}
|
|
1240
|
+
*/
|
|
1241
|
+
static #isCommonDiodeTerminalName(name) {
|
|
1242
|
+
return /^COM[AC]$/i.test(String(name || '').trim())
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1107
1245
|
/**
|
|
1108
1246
|
* Returns true when one passive two-pin symbol uses the ordinary 1/2 pin
|
|
1109
1247
|
* numbering that should stay hidden for simple resistor-like parts.
|
|
@@ -29,12 +29,19 @@ export class SchematicTextOrientationResolver {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
|
-
* Resolves
|
|
32
|
+
* Resolves text vertical anchoring from Altium's justification grid and
|
|
33
|
+
* component text orientation bits.
|
|
33
34
|
* @param {Record<string, string | string[]>} fields Text record fields.
|
|
34
35
|
* @param {string} recordType Native text record type.
|
|
35
|
-
* @returns {'top' | null}
|
|
36
|
+
* @returns {'middle' | 'top' | null}
|
|
36
37
|
*/
|
|
37
38
|
static resolveVerticalAnchor(fields, recordType) {
|
|
39
|
+
const justificationAnchor =
|
|
40
|
+
SchematicTextOrientationResolver.#resolveJustificationVerticalAnchor(
|
|
41
|
+
fields
|
|
42
|
+
)
|
|
43
|
+
if (justificationAnchor) return justificationAnchor
|
|
44
|
+
|
|
38
45
|
return SchematicTextOrientationResolver.resolveHorizontalAnchor(
|
|
39
46
|
fields,
|
|
40
47
|
recordType
|
|
@@ -65,6 +72,28 @@ export class SchematicTextOrientationResolver {
|
|
|
65
72
|
return orientation === 1
|
|
66
73
|
}
|
|
67
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Resolves the vertical row from Altium's 3x3 justification grid.
|
|
77
|
+
* @param {Record<string, string | string[]>} fields Text record fields.
|
|
78
|
+
* @returns {'middle' | 'top' | null}
|
|
79
|
+
*/
|
|
80
|
+
static #resolveJustificationVerticalAnchor(fields) {
|
|
81
|
+
const justification =
|
|
82
|
+
ParserUtils.parseNumericField(fields, 'Justification') ??
|
|
83
|
+
ParserUtils.parseNumericField(fields, 'Alignment')
|
|
84
|
+
|
|
85
|
+
if (!Number.isInteger(justification)) {
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const normalizedJustification = ((justification % 9) + 9) % 9
|
|
90
|
+
const verticalRow = Math.floor(normalizedJustification / 3)
|
|
91
|
+
|
|
92
|
+
if (verticalRow === 1) return 'middle'
|
|
93
|
+
if (verticalRow === 2) return 'top'
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
|
|
68
97
|
/**
|
|
69
98
|
* Returns true for component designator and parameter text records.
|
|
70
99
|
* @param {string} recordType Native text record type.
|
|
@@ -137,7 +137,7 @@ export class SchematicTextParser {
|
|
|
137
137
|
* @param {{ width: number, marginWidth: number, titleBlockOn?: boolean }} sheet
|
|
138
138
|
* @param {Record<string, { size: number, family: string, bold: boolean, italic?: boolean, rotation: number }>} fonts
|
|
139
139
|
* @param {Map<string, Record<string, string>>} [ownerMetadata]
|
|
140
|
-
* @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', verticalAnchor?: 'top', powerPortDirection?: 'up' | 'down' | 'left' | 'right', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] } | null}
|
|
140
|
+
* @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', verticalAnchor?: 'middle' | 'top', powerPortDirection?: 'up' | 'down' | 'left' | 'right', cornerX?: number, cornerY?: number, fill?: string, borderColor?: string, isSolid?: boolean, showBorder?: boolean, textMargin?: number, noteLines?: string[] } | null}
|
|
141
141
|
*/
|
|
142
142
|
static normalizeSchematicTextRecord(
|
|
143
143
|
fields,
|
|
@@ -49,6 +49,12 @@ export class SchematicColorResolver {
|
|
|
49
49
|
return normalized
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
+
if (SchematicColorResolver.#isDefaultInkSourceColor(normalized)) {
|
|
53
|
+
return SchematicColorResolver.#toVariable(
|
|
54
|
+
'--schematic-default-ink-color'
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
|
|
52
58
|
const token = COLOR_TOKEN_BY_VALUE.get(normalized)
|
|
53
59
|
|
|
54
60
|
if (token) {
|
|
@@ -316,6 +322,30 @@ export class SchematicColorResolver {
|
|
|
316
322
|
return Math.max(rgb.r, rgb.g, rgb.b) <= 32
|
|
317
323
|
}
|
|
318
324
|
|
|
325
|
+
/**
|
|
326
|
+
* Returns true for dark saturated blue source colors that represent
|
|
327
|
+
* Altium's default schematic ink family rather than custom artwork color.
|
|
328
|
+
* @param {string} color Normalized color.
|
|
329
|
+
* @returns {boolean}
|
|
330
|
+
*/
|
|
331
|
+
static #isDefaultInkSourceColor(color) {
|
|
332
|
+
const rgb = SchematicColorResolver.#parseHexColor(color)
|
|
333
|
+
if (!rgb) {
|
|
334
|
+
return false
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const hsl = SchematicColorResolver.#rgbToHsl(rgb)
|
|
338
|
+
const hueDegrees = hsl.h * 360
|
|
339
|
+
|
|
340
|
+
return (
|
|
341
|
+
hueDegrees >= 220 &&
|
|
342
|
+
hueDegrees <= 270 &&
|
|
343
|
+
hsl.s >= 45 &&
|
|
344
|
+
hsl.l >= 12 &&
|
|
345
|
+
hsl.l <= 38
|
|
346
|
+
)
|
|
347
|
+
}
|
|
348
|
+
|
|
319
349
|
/**
|
|
320
350
|
* Converts RGB channels to HSL.
|
|
321
351
|
* @param {{ r: number, g: number, b: number }} color
|
|
@@ -15,7 +15,7 @@ const { createSvgText, escapeHtml, formatNumber, projectSchematicY } =
|
|
|
15
15
|
export class SchematicDirectiveRenderer {
|
|
16
16
|
/**
|
|
17
17
|
* Builds directive markup for supported schematic directive primitives.
|
|
18
|
-
* @param {{ x: number, y: number, color: string, name: string, orientation?: number }[]} directives
|
|
18
|
+
* @param {{ x: number, y: number, color: string, name: string, orientation?: number, style?: number }[]} directives
|
|
19
19
|
* @param {number} sheetHeight
|
|
20
20
|
* @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean }> }} sheet
|
|
21
21
|
* @returns {string}
|
|
@@ -34,7 +34,7 @@ export class SchematicDirectiveRenderer {
|
|
|
34
34
|
|
|
35
35
|
/**
|
|
36
36
|
* Builds one supported directive glyph.
|
|
37
|
-
* @param {{ x: number, y: number, color: string, name: string, orientation?: number }} directive
|
|
37
|
+
* @param {{ x: number, y: number, color: string, name: string, orientation?: number, style?: number }} directive
|
|
38
38
|
* @param {number} sheetHeight
|
|
39
39
|
* @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean }> }} sheet
|
|
40
40
|
* @returns {string}
|
|
@@ -67,13 +67,24 @@ export class SchematicDirectiveRenderer {
|
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
69
|
* Builds the labeled info-callout marker for one parameter-set directive.
|
|
70
|
-
* @param {{ x: number, y: number, color: string, name: string, orientation?: number }} directive
|
|
70
|
+
* @param {{ x: number, y: number, color: string, name: string, orientation?: number, style?: number }} directive
|
|
71
71
|
* @param {number} sheetHeight
|
|
72
72
|
* @param {{ fonts?: Record<string, { size: number, family: string, bold: boolean }> }} sheet
|
|
73
73
|
* @param {string} classModifier
|
|
74
74
|
* @returns {string}
|
|
75
75
|
*/
|
|
76
76
|
static #buildRouteMarkup(directive, sheetHeight, sheet, classModifier) {
|
|
77
|
+
if (
|
|
78
|
+
classModifier === 'parameter-set' &&
|
|
79
|
+
Number(directive?.style) === 1
|
|
80
|
+
) {
|
|
81
|
+
return SchematicDirectiveRenderer.#buildCompactParameterSetMarkup(
|
|
82
|
+
directive,
|
|
83
|
+
sheetHeight,
|
|
84
|
+
classModifier
|
|
85
|
+
)
|
|
86
|
+
}
|
|
87
|
+
|
|
77
88
|
const color = SchematicColorResolver.resolveColor(
|
|
78
89
|
directive.color,
|
|
79
90
|
'--schematic-alert-color'
|
|
@@ -154,6 +165,61 @@ export class SchematicDirectiveRenderer {
|
|
|
154
165
|
)
|
|
155
166
|
}
|
|
156
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Builds the compact style-1 parameter-set marker used for connection
|
|
170
|
+
* adornments whose class name is carried as metadata rather than text.
|
|
171
|
+
* @param {{ x: number, y: number, color: string, orientation?: number }} directive
|
|
172
|
+
* @param {number} sheetHeight
|
|
173
|
+
* @param {string} classModifier
|
|
174
|
+
* @returns {string}
|
|
175
|
+
*/
|
|
176
|
+
static #buildCompactParameterSetMarkup(
|
|
177
|
+
directive,
|
|
178
|
+
sheetHeight,
|
|
179
|
+
classModifier
|
|
180
|
+
) {
|
|
181
|
+
const color = SchematicColorResolver.resolveColor(
|
|
182
|
+
directive.color,
|
|
183
|
+
'--schematic-alert-color'
|
|
184
|
+
)
|
|
185
|
+
const projectedY = projectSchematicY(sheetHeight, directive.y)
|
|
186
|
+
const direction = SchematicDirectiveRenderer.#resolveCalloutDirection(
|
|
187
|
+
directive.orientation
|
|
188
|
+
)
|
|
189
|
+
const circleRadius = 3
|
|
190
|
+
const circleCenterX = directive.x + direction.x * 6
|
|
191
|
+
const circleCenterY = projectedY + direction.y * 6
|
|
192
|
+
const leaderEndX = circleCenterX - direction.x * circleRadius
|
|
193
|
+
const leaderEndY = circleCenterY - direction.y * circleRadius
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
'<g class="schematic-directive schematic-directive--' +
|
|
197
|
+
escapeHtml(classModifier) +
|
|
198
|
+
'">' +
|
|
199
|
+
'<line x1="' +
|
|
200
|
+
formatNumber(directive.x) +
|
|
201
|
+
'" y1="' +
|
|
202
|
+
formatNumber(projectedY) +
|
|
203
|
+
'" x2="' +
|
|
204
|
+
formatNumber(leaderEndX) +
|
|
205
|
+
'" y2="' +
|
|
206
|
+
formatNumber(leaderEndY) +
|
|
207
|
+
'" stroke="' +
|
|
208
|
+
escapeHtml(color) +
|
|
209
|
+
'" stroke-width="1" />' +
|
|
210
|
+
'<circle cx="' +
|
|
211
|
+
formatNumber(circleCenterX) +
|
|
212
|
+
'" cy="' +
|
|
213
|
+
formatNumber(circleCenterY) +
|
|
214
|
+
'" r="' +
|
|
215
|
+
formatNumber(circleRadius) +
|
|
216
|
+
'" fill="none" stroke="' +
|
|
217
|
+
escapeHtml(color) +
|
|
218
|
+
'" stroke-width="1" />' +
|
|
219
|
+
'</g>'
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
157
223
|
/**
|
|
158
224
|
* Resolves Altium's four-way callout orientation into an outward vector.
|
|
159
225
|
* @param {number | undefined} orientation
|
|
@@ -17,7 +17,8 @@ export class SchematicLineColorResolver {
|
|
|
17
17
|
if (SchematicLineColorResolver.isElectricalLine(line)) {
|
|
18
18
|
return SchematicColorResolver.resolveNonTextColor(
|
|
19
19
|
line?.color,
|
|
20
|
-
'--schematic-default-ink-color'
|
|
20
|
+
'--schematic-default-ink-color',
|
|
21
|
+
true
|
|
21
22
|
)
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -37,7 +38,8 @@ export class SchematicLineColorResolver {
|
|
|
37
38
|
|
|
38
39
|
return SchematicColorResolver.resolveNonTextColor(
|
|
39
40
|
line?.color,
|
|
40
|
-
'--schematic-default-ink-color'
|
|
41
|
+
'--schematic-default-ink-color',
|
|
42
|
+
true
|
|
41
43
|
)
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -143,6 +143,56 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
143
143
|
return offsets
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
+
/**
|
|
147
|
+
* Collects pins whose owner already draws a visible text label for the pin
|
|
148
|
+
* designator, avoiding a second synthetic pin-number label at the contact.
|
|
149
|
+
* @param {{ ownerIndex?: string, text?: string, x?: number, y?: number, recordType?: string, hidden?: boolean }[]} texts
|
|
150
|
+
* @param {{ ownerIndex?: string, designator?: string, x: number, y: number, length?: number, orientation: 'left' | 'right' | 'top' | 'bottom', labelMode?: 'hidden' | 'number-only' | 'name-only' | 'name-and-number' }[]} pins
|
|
151
|
+
* @returns {Set<string>}
|
|
152
|
+
*/
|
|
153
|
+
static collectExplicitOwnerPinNumberLabelKeys(texts, pins) {
|
|
154
|
+
const ownerTexts = SchematicOwnerPinLabelLayout.#groupByOwnerIndex(
|
|
155
|
+
(texts || []).filter(
|
|
156
|
+
(text) => text.recordType === '4' && !text.hidden
|
|
157
|
+
)
|
|
158
|
+
)
|
|
159
|
+
const keys = new Set()
|
|
160
|
+
|
|
161
|
+
for (const pin of pins || []) {
|
|
162
|
+
const ownerIndex = String(pin.ownerIndex || '').trim()
|
|
163
|
+
const designator = String(pin.designator || '').trim()
|
|
164
|
+
|
|
165
|
+
if (
|
|
166
|
+
!ownerIndex ||
|
|
167
|
+
!designator ||
|
|
168
|
+
(pin.labelMode || 'name-and-number') === 'hidden' ||
|
|
169
|
+
(pin.labelMode || 'name-and-number') === 'name-only'
|
|
170
|
+
) {
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const nativeNumberLabels = ownerTexts.get(ownerIndex) || []
|
|
175
|
+
const hasNativeNumberLabel = nativeNumberLabels.some((text) =>
|
|
176
|
+
SchematicOwnerPinLabelLayout.#isExplicitOwnerPinNumberLabel(
|
|
177
|
+
text,
|
|
178
|
+
pin,
|
|
179
|
+
designator
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
if (hasNativeNumberLabel) {
|
|
184
|
+
keys.add(
|
|
185
|
+
SchematicOwnerPinLabelLayout.buildOwnerPinLabelKey(
|
|
186
|
+
ownerIndex,
|
|
187
|
+
designator
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return keys
|
|
194
|
+
}
|
|
195
|
+
|
|
146
196
|
/**
|
|
147
197
|
* Collects compact FET-like owner groups whose numeric contact labels need
|
|
148
198
|
* to stay outside the owner-drawn device body.
|
|
@@ -481,6 +531,53 @@ export class SchematicOwnerPinLabelLayout {
|
|
|
481
531
|
)
|
|
482
532
|
}
|
|
483
533
|
|
|
534
|
+
/**
|
|
535
|
+
* Returns true when an owner text is a native pin-number label for a pin.
|
|
536
|
+
* @param {{ text?: string, x?: number, y?: number }} text
|
|
537
|
+
* @param {{ x: number, y: number, length?: number, orientation: 'left' | 'right' | 'top' | 'bottom' }} pin
|
|
538
|
+
* @param {string} designator
|
|
539
|
+
* @returns {boolean}
|
|
540
|
+
*/
|
|
541
|
+
static #isExplicitOwnerPinNumberLabel(text, pin, designator) {
|
|
542
|
+
if (String(text?.text || '').trim() !== designator) {
|
|
543
|
+
return false
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const textX = Number(text?.x)
|
|
547
|
+
const textY = Number(text?.y)
|
|
548
|
+
const pinX = Number(pin?.x)
|
|
549
|
+
const pinY = Number(pin?.y)
|
|
550
|
+
|
|
551
|
+
if (
|
|
552
|
+
!Number.isFinite(textX) ||
|
|
553
|
+
!Number.isFinite(textY) ||
|
|
554
|
+
!Number.isFinite(pinX) ||
|
|
555
|
+
!Number.isFinite(pinY)
|
|
556
|
+
) {
|
|
557
|
+
return false
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const laneTolerance = 6
|
|
561
|
+
const axisTolerance =
|
|
562
|
+
Math.max(Math.abs(Number(pin.length || 0)), 10) + 6
|
|
563
|
+
|
|
564
|
+
if (pin.orientation === 'left' || pin.orientation === 'right') {
|
|
565
|
+
return (
|
|
566
|
+
Math.abs(textY - pinY) <= laneTolerance &&
|
|
567
|
+
Math.abs(textX - pinX) <= axisTolerance
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (pin.orientation === 'top' || pin.orientation === 'bottom') {
|
|
572
|
+
return (
|
|
573
|
+
Math.abs(textX - pinX) <= laneTolerance &&
|
|
574
|
+
Math.abs(textY - pinY) <= axisTolerance
|
|
575
|
+
)
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return false
|
|
579
|
+
}
|
|
580
|
+
|
|
484
581
|
/**
|
|
485
582
|
* Groups schematic owner-local primitives by owner index.
|
|
486
583
|
* @template T
|
|
@@ -24,6 +24,7 @@ export class SchematicPinSvgRenderer {
|
|
|
24
24
|
* @param {Map<string, number>} explicitOwnerPinLabelOffsets
|
|
25
25
|
* @param {Map<string, 'left' | 'right'>} compactExternalNumberLabelSides
|
|
26
26
|
* @param {Map<string, { left: number, right: number }>} internalNumberLabelBoxes
|
|
27
|
+
* @param {Set<string>} explicitOwnerPinNumberLabelKeys
|
|
27
28
|
* @param {Set<string>} overlappingExternalNumberLabelKeys
|
|
28
29
|
* @returns {string}
|
|
29
30
|
*/
|
|
@@ -36,6 +37,7 @@ export class SchematicPinSvgRenderer {
|
|
|
36
37
|
explicitOwnerPinLabelOffsets,
|
|
37
38
|
compactExternalNumberLabelSides = new Map(),
|
|
38
39
|
internalNumberLabelBoxes = new Map(),
|
|
40
|
+
explicitOwnerPinNumberLabelKeys = new Set(),
|
|
39
41
|
overlappingExternalNumberLabelKeys = new Set()
|
|
40
42
|
) {
|
|
41
43
|
const geometry =
|
|
@@ -76,13 +78,14 @@ export class SchematicPinSvgRenderer {
|
|
|
76
78
|
null
|
|
77
79
|
const internalNumberLabelBox =
|
|
78
80
|
internalNumberLabelBoxes.get(String(pin.ownerIndex || '')) || null
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
pin.designator
|
|
84
|
-
)
|
|
81
|
+
const ownerPinNumberLabelKey =
|
|
82
|
+
SchematicOwnerPinLabelLayout.buildOwnerPinLabelKey(
|
|
83
|
+
pin.ownerIndex,
|
|
84
|
+
pin.designator
|
|
85
85
|
)
|
|
86
|
+
const shouldRenderExternalNumber =
|
|
87
|
+
!explicitOwnerPinNumberLabelKeys.has(ownerPinNumberLabelKey) &&
|
|
88
|
+
!overlappingExternalNumberLabelKeys.has(ownerPinNumberLabelKey)
|
|
86
89
|
|
|
87
90
|
if (pin.orientation === 'left') {
|
|
88
91
|
if (labelMode !== 'hidden' && labelMode !== 'name-only') {
|
|
@@ -247,6 +250,7 @@ export class SchematicPinSvgRenderer {
|
|
|
247
250
|
if (
|
|
248
251
|
labelMode !== 'hidden' &&
|
|
249
252
|
labelMode !== 'name-only' &&
|
|
253
|
+
shouldRenderExternalNumber &&
|
|
250
254
|
(pin.orientation === 'top' || pin.orientation === 'bottom')
|
|
251
255
|
) {
|
|
252
256
|
texts.push(
|
|
@@ -34,6 +34,7 @@ const SECTION_HEADING_LINE_X_PADDING = 15
|
|
|
34
34
|
const BODY_TEXT_ASCENT_RATIO = 0.72
|
|
35
35
|
const BODY_TEXT_DESCENT_RATIO = 0.14
|
|
36
36
|
const BODY_TEXT_FLAT_DESCENT_RATIO = 0
|
|
37
|
+
const TEXT_VERTICAL_MIDDLE_BASELINE_RATIO = 0.36
|
|
37
38
|
const BODY_TEXT_MIN_SEPARATOR_SPAN_RATIO = 0.3
|
|
38
39
|
const BODY_TEXT_DESCENDER_PATTERN = /[gjpqyQ_,;]/
|
|
39
40
|
|
|
@@ -45,7 +46,7 @@ export class SchematicSvgRenderer {
|
|
|
45
46
|
|
|
46
47
|
/**
|
|
47
48
|
* Renders a normalized schematic model into SVG markup.
|
|
48
|
-
* @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, textSegments?: { text: string, overline: boolean }[], color: 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[] }[], 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
|
|
49
|
+
* @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, style?: number }[], texts: { x: number, y: number, text: string, textSegments?: { text: string, overline: boolean }[], color: 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[] }[], 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
|
|
49
50
|
* @param {{ projectParameters?: Record<string, string | number | boolean | null | undefined>, colorizeImages?: boolean, colorize_images?: boolean }} options Render options.
|
|
50
51
|
* @returns {string}
|
|
51
52
|
*/
|
|
@@ -634,6 +635,11 @@ export class SchematicSvgRenderer {
|
|
|
634
635
|
pins,
|
|
635
636
|
contentRectangles
|
|
636
637
|
)
|
|
638
|
+
const explicitOwnerPinNumberLabelKeys =
|
|
639
|
+
SchematicOwnerPinLabelLayout.collectExplicitOwnerPinNumberLabelKeys(
|
|
640
|
+
texts,
|
|
641
|
+
pins
|
|
642
|
+
)
|
|
637
643
|
const overlappingExternalNumberLabelKeys =
|
|
638
644
|
SchematicOwnerPinLabelLayout.collectOverlappingExternalNumberLabelKeys(
|
|
639
645
|
pins,
|
|
@@ -652,6 +658,7 @@ export class SchematicSvgRenderer {
|
|
|
652
658
|
explicitOwnerPinLabelOffsets,
|
|
653
659
|
compactExternalNumberLabelSides,
|
|
654
660
|
internalNumberLabelBoxes,
|
|
661
|
+
explicitOwnerPinNumberLabelKeys,
|
|
655
662
|
overlappingExternalNumberLabelKeys
|
|
656
663
|
),
|
|
657
664
|
SchematicSvgRenderer.#semanticAttributes(
|
|
@@ -2702,7 +2709,7 @@ export class SchematicSvgRenderer {
|
|
|
2702
2709
|
|
|
2703
2710
|
/**
|
|
2704
2711
|
* Resolves final text placement for schematic free-text annotations.
|
|
2705
|
-
* @param {{ x: number, y: number, text: string, ownerIndex?: string, recordType?: string, fontSize?: number, rotation?: number, anchor?: 'start' | 'middle' | 'end', verticalAnchor?: 'top' }} text
|
|
2712
|
+
* @param {{ x: number, y: number, text: string, ownerIndex?: string, recordType?: string, fontSize?: number, rotation?: number, anchor?: 'start' | 'middle' | 'end', verticalAnchor?: 'middle' | 'top' }} text
|
|
2706
2713
|
* @param {number} sheetHeight
|
|
2707
2714
|
* @param {{ x1: number, y1: number, x2: number, y2: number, lineStyle?: number }[]} lines
|
|
2708
2715
|
* @param {{ x: number, y: number, name?: string, ownerIndex?: string, orientation: 'left' | 'right' | 'top' | 'bottom' } | null} matchedOwnerPin
|
|
@@ -2822,17 +2829,26 @@ export class SchematicSvgRenderer {
|
|
|
2822
2829
|
}
|
|
2823
2830
|
|
|
2824
2831
|
/**
|
|
2825
|
-
* Converts
|
|
2826
|
-
*
|
|
2832
|
+
* Converts vertically anchored text coordinates into SVG baseline
|
|
2833
|
+
* coordinates.
|
|
2834
|
+
* @param {{ verticalAnchor?: 'middle' | 'top' }} text Text primitive.
|
|
2827
2835
|
* @param {number} fontSize Viewer font size.
|
|
2828
2836
|
* @returns {number}
|
|
2829
2837
|
*/
|
|
2830
2838
|
static #resolveVerticalAnchorBaselineOffset(text, fontSize) {
|
|
2831
|
-
if (
|
|
2839
|
+
if (!Number.isFinite(fontSize) || fontSize <= 0) {
|
|
2832
2840
|
return 0
|
|
2833
2841
|
}
|
|
2834
2842
|
|
|
2835
|
-
|
|
2843
|
+
if (text?.verticalAnchor === 'middle') {
|
|
2844
|
+
return fontSize * TEXT_VERTICAL_MIDDLE_BASELINE_RATIO
|
|
2845
|
+
}
|
|
2846
|
+
|
|
2847
|
+
if (text?.verticalAnchor === 'top') {
|
|
2848
|
+
return fontSize
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2851
|
+
return 0
|
|
2836
2852
|
}
|
|
2837
2853
|
|
|
2838
2854
|
/**
|