altium-toolkit 1.1.39 → 1.1.41
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/AltiumLayoutParser.mjs +9 -4
- package/src/core/altium/PcbLayerStackReadModelBuilder.mjs +1 -0
- package/src/core/altium/SchematicPinParser.mjs +82 -13
- package/src/styles/altium-renderers.css +67 -1
- package/src/ui/PcbInteractionLayerModel.mjs +6 -2
- package/src/ui/PcbPadMaskApertureRenderer.mjs +288 -0
- package/src/ui/PcbSvgRenderer.mjs +674 -38
- package/src/ui/SchematicColorResolver.mjs +30 -0
package/package.json
CHANGED
|
@@ -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 { PcbLayerIdCodec } from './PcbLayerIdCodec.mjs'
|
|
6
7
|
import { SchematicTextParser } from './SchematicTextParser.mjs'
|
|
7
8
|
|
|
8
9
|
const { getField, parseNumericField } = ParserUtils
|
|
@@ -114,14 +115,18 @@ export class AltiumLayoutParser {
|
|
|
114
115
|
if (!match) continue
|
|
115
116
|
|
|
116
117
|
const index = Number.parseInt(match[1], 10)
|
|
118
|
+
const layerId = parseNumericField(
|
|
119
|
+
fields,
|
|
120
|
+
'V9_STACK_LAYER' + index + '_LAYERID'
|
|
121
|
+
)
|
|
122
|
+
const legacyLayerId =
|
|
123
|
+
PcbLayerIdCodec.legacyLayerIdFromV7SaveId(layerId) ?? undefined
|
|
117
124
|
layers.push(
|
|
118
125
|
AltiumLayoutParser.#stripUndefined({
|
|
119
126
|
index,
|
|
120
127
|
name: getField(fields, key),
|
|
121
|
-
layerId
|
|
122
|
-
|
|
123
|
-
'V9_STACK_LAYER' + index + '_LAYERID'
|
|
124
|
-
),
|
|
128
|
+
layerId,
|
|
129
|
+
legacyLayerId,
|
|
125
130
|
kind: AltiumLayoutParser.#firstLayerStackTextField(
|
|
126
131
|
fields,
|
|
127
132
|
index,
|
|
@@ -180,6 +180,7 @@ export class PcbLayerStackReadModelBuilder {
|
|
|
180
180
|
PcbLayerStackReadModelBuilder.#stripUndefined({
|
|
181
181
|
index: layer.index,
|
|
182
182
|
layerId: layer.layerId,
|
|
183
|
+
legacyLayerId: layer.legacyLayerId,
|
|
183
184
|
layerKey: Number.isFinite(layer.layerId)
|
|
184
185
|
? 'L' + layer.layerId
|
|
185
186
|
: undefined,
|
|
@@ -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,
|
|
@@ -957,32 +964,94 @@ export class SchematicPinParser {
|
|
|
957
964
|
}
|
|
958
965
|
|
|
959
966
|
/**
|
|
960
|
-
* Returns true when a compact owner-drawn FET body
|
|
961
|
-
* 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.
|
|
962
970
|
* @param {{ designator: string, name: string, orientation: 'left' | 'right' | 'top' | 'bottom' }[]} pins
|
|
963
|
-
* @param {string[]} semanticNames
|
|
964
971
|
* @param {number} orientationCount
|
|
965
972
|
* @returns {boolean}
|
|
966
973
|
*/
|
|
967
|
-
static #isCompactNumberedFetTerminalGroup(
|
|
968
|
-
pins,
|
|
969
|
-
semanticNames,
|
|
970
|
-
orientationCount
|
|
971
|
-
) {
|
|
974
|
+
static #isCompactNumberedFetTerminalGroup(pins, orientationCount) {
|
|
972
975
|
if (
|
|
973
|
-
pins.length
|
|
974
|
-
|
|
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 ||
|
|
975
1009
|
semanticNames.length !== pins.length ||
|
|
976
1010
|
!pins.every((pin) => /^\d+$/.test(String(pin.designator || '')))
|
|
977
1011
|
) {
|
|
978
1012
|
return false
|
|
979
1013
|
}
|
|
980
1014
|
|
|
981
|
-
|
|
982
|
-
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')
|
|
983
1023
|
)
|
|
984
1024
|
}
|
|
985
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
|
+
|
|
986
1055
|
/**
|
|
987
1056
|
* Returns true when a compact multi-side owner has transistor-like terminal
|
|
988
1057
|
* letters that are part of the drawn symbol body, not external pin labels.
|
|
@@ -142,6 +142,16 @@
|
|
|
142
142
|
--pcb-via-hole-fill: #0f746c;
|
|
143
143
|
--pcb-footprint-fill: rgba(247, 230, 117, 0.14);
|
|
144
144
|
--pcb-footprint-track-color: rgba(237, 172, 36, 1);
|
|
145
|
+
--pcb-detail-paste-fill: rgba(146, 146, 146, 0.94);
|
|
146
|
+
--pcb-detail-paste-stroke: rgba(154, 154, 154, 0.96);
|
|
147
|
+
--pcb-detail-mask-fill: rgba(137, 34, 132, 0.4);
|
|
148
|
+
--pcb-detail-mask-stroke: rgba(137, 34, 132, 0.82);
|
|
149
|
+
--pcb-detail-mechanical-fill: rgba(148, 160, 166, 0.32);
|
|
150
|
+
--pcb-detail-mechanical-stroke: rgba(96, 110, 118, 0.86);
|
|
151
|
+
--pcb-detail-drill-fill: rgba(79, 87, 93, 0.46);
|
|
152
|
+
--pcb-detail-drill-stroke: rgba(42, 50, 56, 0.88);
|
|
153
|
+
--pcb-detail-fill: rgba(150, 160, 166, 0.38);
|
|
154
|
+
--pcb-detail-stroke: rgba(80, 92, 100, 0.82);
|
|
145
155
|
--pcb-dimension-color: rgba(39, 88, 111, 0.78);
|
|
146
156
|
--pcb-dimension-text: #22343b;
|
|
147
157
|
--pcb-text-knockout-fill: rgba(248, 246, 239, 0.96);
|
|
@@ -200,7 +210,9 @@
|
|
|
200
210
|
.pcb-track,
|
|
201
211
|
.pcb-arc,
|
|
202
212
|
.pcb-footprint-track,
|
|
203
|
-
.pcb-footprint-arc
|
|
213
|
+
.pcb-footprint-arc,
|
|
214
|
+
.pcb-detail-track,
|
|
215
|
+
.pcb-detail-arc {
|
|
204
216
|
fill: none;
|
|
205
217
|
stroke-linecap: round;
|
|
206
218
|
stroke-linejoin: round;
|
|
@@ -271,6 +283,60 @@
|
|
|
271
283
|
stroke: var(--pcb-footprint-track-color);
|
|
272
284
|
}
|
|
273
285
|
|
|
286
|
+
.pcb-detail-fill,
|
|
287
|
+
.pcb-detail-region {
|
|
288
|
+
fill: var(--pcb-detail-fill);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.pcb-detail-track,
|
|
292
|
+
.pcb-detail-arc {
|
|
293
|
+
stroke: var(--pcb-detail-stroke);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.pcb-detail-fill--paste,
|
|
297
|
+
.pcb-detail-region--paste {
|
|
298
|
+
fill: var(--pcb-detail-paste-fill);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.pcb-detail-track--paste,
|
|
302
|
+
.pcb-detail-arc--paste {
|
|
303
|
+
stroke: var(--pcb-detail-paste-stroke);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
.pcb-detail-fill--mask,
|
|
307
|
+
.pcb-detail-region--mask,
|
|
308
|
+
.pcb-detail-fill--solder-mask,
|
|
309
|
+
.pcb-detail-region--solder-mask {
|
|
310
|
+
fill: var(--pcb-detail-mask-fill);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.pcb-detail-track--mask,
|
|
314
|
+
.pcb-detail-arc--mask,
|
|
315
|
+
.pcb-detail-track--solder-mask,
|
|
316
|
+
.pcb-detail-arc--solder-mask {
|
|
317
|
+
stroke: var(--pcb-detail-mask-stroke);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.pcb-detail-fill--mechanical,
|
|
321
|
+
.pcb-detail-region--mechanical {
|
|
322
|
+
fill: var(--pcb-detail-mechanical-fill);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.pcb-detail-track--mechanical,
|
|
326
|
+
.pcb-detail-arc--mechanical {
|
|
327
|
+
stroke: var(--pcb-detail-mechanical-stroke);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
.pcb-detail-fill--drill,
|
|
331
|
+
.pcb-detail-region--drill {
|
|
332
|
+
fill: var(--pcb-detail-drill-fill);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.pcb-detail-track--drill,
|
|
336
|
+
.pcb-detail-arc--drill {
|
|
337
|
+
stroke: var(--pcb-detail-drill-stroke);
|
|
338
|
+
}
|
|
339
|
+
|
|
274
340
|
.pcb-dimensions {
|
|
275
341
|
pointer-events: none;
|
|
276
342
|
}
|
|
@@ -56,13 +56,17 @@ export class PcbInteractionLayerModel {
|
|
|
56
56
|
const key = String(layer?.name || layer?.layer || '').trim()
|
|
57
57
|
if (!key || seen.has(key)) continue
|
|
58
58
|
seen.add(key)
|
|
59
|
-
|
|
59
|
+
const row = {
|
|
60
60
|
key,
|
|
61
61
|
label: key,
|
|
62
62
|
layerId: Number.isFinite(Number(layer?.layerId))
|
|
63
63
|
? Number(layer.layerId)
|
|
64
64
|
: null
|
|
65
|
-
}
|
|
65
|
+
}
|
|
66
|
+
if (Number.isFinite(Number(layer?.legacyLayerId))) {
|
|
67
|
+
row.legacyLayerId = Number(layer.legacyLayerId)
|
|
68
|
+
}
|
|
69
|
+
layers.push(row)
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
return layers
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 André Fiedler
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
4
|
+
|
|
5
|
+
import { SchematicSvgUtils } from './SchematicSvgUtils.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Renders pad-derived paste and solder-mask apertures as independent SVG layers.
|
|
9
|
+
*/
|
|
10
|
+
export class PcbPadMaskApertureRenderer {
|
|
11
|
+
static #PAD_SHAPE_RECTANGULAR = 2
|
|
12
|
+
static #SPECS = [
|
|
13
|
+
{
|
|
14
|
+
flag: 'hasTopPasteMaskOpening',
|
|
15
|
+
kind: 'paste',
|
|
16
|
+
primitiveKind: 'pad-paste',
|
|
17
|
+
side: 'top',
|
|
18
|
+
layerId: 35,
|
|
19
|
+
layerName: 'Top Paste',
|
|
20
|
+
expansionKey: 'effectivePasteMaskExpansion'
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
flag: 'hasBottomPasteMaskOpening',
|
|
24
|
+
kind: 'paste',
|
|
25
|
+
primitiveKind: 'pad-paste',
|
|
26
|
+
side: 'bottom',
|
|
27
|
+
layerId: 36,
|
|
28
|
+
layerName: 'Bottom Paste',
|
|
29
|
+
expansionKey: 'effectivePasteMaskExpansion'
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
flag: 'hasTopSolderMaskOpening',
|
|
33
|
+
kind: 'solder-mask',
|
|
34
|
+
primitiveKind: 'pad-solder-mask',
|
|
35
|
+
side: 'top',
|
|
36
|
+
layerId: 37,
|
|
37
|
+
layerName: 'Top Solder',
|
|
38
|
+
expansionKey: 'effectiveSolderMaskExpansion'
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
flag: 'hasBottomSolderMaskOpening',
|
|
42
|
+
kind: 'solder-mask',
|
|
43
|
+
primitiveKind: 'pad-solder-mask',
|
|
44
|
+
side: 'bottom',
|
|
45
|
+
layerId: 38,
|
|
46
|
+
layerName: 'Bottom Solder',
|
|
47
|
+
expansionKey: 'effectiveSolderMaskExpansion'
|
|
48
|
+
}
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Builds one SVG group containing all derived pad mask apertures.
|
|
53
|
+
* @param {object[]} pads Normalized pad records.
|
|
54
|
+
* @param {{ attributes?: (aperture: object) => string, padIndex?: (pad: object, index: number) => number }} [options]
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
static render(pads, options = {}) {
|
|
58
|
+
const apertures = []
|
|
59
|
+
|
|
60
|
+
for (const [fallbackIndex, pad] of (pads || []).entries()) {
|
|
61
|
+
const padIndex = PcbPadMaskApertureRenderer.#padIndex(
|
|
62
|
+
pad,
|
|
63
|
+
fallbackIndex,
|
|
64
|
+
options
|
|
65
|
+
)
|
|
66
|
+
for (const [specIndex, spec] of this.#SPECS.entries()) {
|
|
67
|
+
if (!pad?.[spec.flag]) continue
|
|
68
|
+
const aperture = this.#apertureForPad(
|
|
69
|
+
pad,
|
|
70
|
+
padIndex,
|
|
71
|
+
specIndex,
|
|
72
|
+
spec
|
|
73
|
+
)
|
|
74
|
+
if (!aperture) continue
|
|
75
|
+
apertures.push(
|
|
76
|
+
this.#renderAperture(aperture, options.attributes)
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!apertures.length) return ''
|
|
82
|
+
|
|
83
|
+
return '<g class="pcb-pad-mask-layers">' + apertures.join('') + '</g>'
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Resolves one stable pad index.
|
|
88
|
+
* @param {object} pad Pad record.
|
|
89
|
+
* @param {number} fallbackIndex Array index.
|
|
90
|
+
* @param {{ padIndex?: (pad: object, index: number) => number }} options
|
|
91
|
+
* @returns {number}
|
|
92
|
+
*/
|
|
93
|
+
static #padIndex(pad, fallbackIndex, options) {
|
|
94
|
+
const resolved = options.padIndex?.(pad, fallbackIndex)
|
|
95
|
+
return Number.isInteger(resolved) ? resolved : fallbackIndex
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Builds one normalized aperture descriptor for a pad/spec pair.
|
|
100
|
+
* @param {object} pad Pad record.
|
|
101
|
+
* @param {number} padIndex Stable pad index.
|
|
102
|
+
* @param {number} specIndex Spec index.
|
|
103
|
+
* @param {object} spec Aperture layer spec.
|
|
104
|
+
* @returns {object | null}
|
|
105
|
+
*/
|
|
106
|
+
static #apertureForPad(pad, padIndex, specIndex, spec) {
|
|
107
|
+
const size = this.#sizeForPad(pad, spec)
|
|
108
|
+
if (size.width <= 0 || size.height <= 0) return null
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
pad,
|
|
112
|
+
padIndex,
|
|
113
|
+
index: padIndex * this.#SPECS.length + specIndex,
|
|
114
|
+
spec,
|
|
115
|
+
size,
|
|
116
|
+
primitive: {
|
|
117
|
+
...pad,
|
|
118
|
+
layerId: spec.layerId,
|
|
119
|
+
layerCode: spec.layerId,
|
|
120
|
+
legacyLayerId: spec.layerId,
|
|
121
|
+
layerName: spec.layerName
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Resolves aperture dimensions after the parsed mask expansion.
|
|
128
|
+
* @param {object} pad Pad record.
|
|
129
|
+
* @param {object} spec Aperture layer spec.
|
|
130
|
+
* @returns {{ width: number, height: number }}
|
|
131
|
+
*/
|
|
132
|
+
static #sizeForPad(pad, spec) {
|
|
133
|
+
const side = spec.side === 'bottom' ? 'Bottom' : 'Top'
|
|
134
|
+
const width = Number(pad?.['size' + side + 'X'] || pad?.sizeMidX || 0)
|
|
135
|
+
const height = Number(pad?.['size' + side + 'Y'] || pad?.sizeMidY || 0)
|
|
136
|
+
const expansion = Number(pad?.[spec.expansionKey] || 0)
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
width: Math.max(width + expansion * 2, 0),
|
|
140
|
+
height: Math.max(height + expansion * 2, 0)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Renders one aperture shape.
|
|
146
|
+
* @param {object} aperture Aperture descriptor.
|
|
147
|
+
* @param {(aperture: object) => string | undefined} attributes Attribute callback.
|
|
148
|
+
* @returns {string}
|
|
149
|
+
*/
|
|
150
|
+
static #renderAperture(aperture, attributes) {
|
|
151
|
+
const className =
|
|
152
|
+
'pcb-detail-fill pcb-detail-fill--' +
|
|
153
|
+
(aperture.spec.kind === 'solder-mask' ? 'mask' : 'paste') +
|
|
154
|
+
' pcb-pad-mask-aperture pcb-pad-mask-aperture--' +
|
|
155
|
+
aperture.spec.kind
|
|
156
|
+
const renderedAttributes = attributes?.(aperture) || ''
|
|
157
|
+
|
|
158
|
+
return PcbPadMaskApertureRenderer.#isRoundAperture(aperture)
|
|
159
|
+
? PcbPadMaskApertureRenderer.#renderCircle(
|
|
160
|
+
aperture,
|
|
161
|
+
className,
|
|
162
|
+
renderedAttributes
|
|
163
|
+
)
|
|
164
|
+
: PcbPadMaskApertureRenderer.#renderRect(
|
|
165
|
+
aperture,
|
|
166
|
+
className,
|
|
167
|
+
renderedAttributes
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Renders one circular aperture.
|
|
173
|
+
* @param {object} aperture Aperture descriptor.
|
|
174
|
+
* @param {string} className SVG class list.
|
|
175
|
+
* @param {string} attributes SVG attributes.
|
|
176
|
+
* @returns {string}
|
|
177
|
+
*/
|
|
178
|
+
static #renderCircle(aperture, className, attributes) {
|
|
179
|
+
const { pad, size } = aperture
|
|
180
|
+
const radius = Math.max(size.width, size.height) / 2
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
'<circle class="' +
|
|
184
|
+
className +
|
|
185
|
+
'" cx="' +
|
|
186
|
+
SchematicSvgUtils.formatNumber(Number(pad.x || 0)) +
|
|
187
|
+
'" cy="' +
|
|
188
|
+
SchematicSvgUtils.formatNumber(Number(pad.y || 0)) +
|
|
189
|
+
'" r="' +
|
|
190
|
+
SchematicSvgUtils.formatNumber(radius) +
|
|
191
|
+
'"' +
|
|
192
|
+
attributes +
|
|
193
|
+
' />'
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Renders one rectangular aperture.
|
|
199
|
+
* @param {object} aperture Aperture descriptor.
|
|
200
|
+
* @param {string} className SVG class list.
|
|
201
|
+
* @param {string} attributes SVG attributes.
|
|
202
|
+
* @returns {string}
|
|
203
|
+
*/
|
|
204
|
+
static #renderRect(aperture, className, attributes) {
|
|
205
|
+
const { pad, size } = aperture
|
|
206
|
+
const x = Number(pad.x || 0) - size.width / 2
|
|
207
|
+
const y = Number(pad.y || 0) - size.height / 2
|
|
208
|
+
|
|
209
|
+
return (
|
|
210
|
+
'<rect class="' +
|
|
211
|
+
className +
|
|
212
|
+
'" x="' +
|
|
213
|
+
SchematicSvgUtils.formatNumber(x) +
|
|
214
|
+
'" y="' +
|
|
215
|
+
SchematicSvgUtils.formatNumber(y) +
|
|
216
|
+
'" width="' +
|
|
217
|
+
SchematicSvgUtils.formatNumber(size.width) +
|
|
218
|
+
'" height="' +
|
|
219
|
+
SchematicSvgUtils.formatNumber(size.height) +
|
|
220
|
+
'" rx="' +
|
|
221
|
+
SchematicSvgUtils.formatNumber(
|
|
222
|
+
PcbPadMaskApertureRenderer.#cornerRadius(aperture)
|
|
223
|
+
) +
|
|
224
|
+
'" transform="rotate(' +
|
|
225
|
+
SchematicSvgUtils.formatNumber(Number(pad.rotation || 0)) +
|
|
226
|
+
' ' +
|
|
227
|
+
SchematicSvgUtils.formatNumber(Number(pad.x || 0)) +
|
|
228
|
+
' ' +
|
|
229
|
+
SchematicSvgUtils.formatNumber(Number(pad.y || 0)) +
|
|
230
|
+
')"' +
|
|
231
|
+
attributes +
|
|
232
|
+
' />'
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Returns true when one aperture should be rendered as a circle.
|
|
238
|
+
* @param {object} aperture Aperture descriptor.
|
|
239
|
+
* @returns {boolean}
|
|
240
|
+
*/
|
|
241
|
+
static #isRoundAperture(aperture) {
|
|
242
|
+
const shape = PcbPadMaskApertureRenderer.#shapeForAperture(aperture)
|
|
243
|
+
return (
|
|
244
|
+
shape !== PcbPadMaskApertureRenderer.#PAD_SHAPE_RECTANGULAR &&
|
|
245
|
+
Math.abs(aperture.size.width - aperture.size.height) < 0.001
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Resolves the side-specific pad shape code.
|
|
251
|
+
* @param {object} aperture Aperture descriptor.
|
|
252
|
+
* @returns {number}
|
|
253
|
+
*/
|
|
254
|
+
static #shapeForAperture(aperture) {
|
|
255
|
+
const side = aperture.spec.side === 'bottom' ? 'Bottom' : 'Top'
|
|
256
|
+
if (
|
|
257
|
+
side === 'Top' &&
|
|
258
|
+
aperture.pad?.hasRoundedRect &&
|
|
259
|
+
Number.isInteger(aperture.pad.roundedRectShapeTop)
|
|
260
|
+
) {
|
|
261
|
+
return Number(aperture.pad.roundedRectShapeTop)
|
|
262
|
+
}
|
|
263
|
+
return Number(aperture.pad?.['shape' + side] || 0)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Resolves rectangle corner radius for rounded pad shapes.
|
|
268
|
+
* @param {object} aperture Aperture descriptor.
|
|
269
|
+
* @returns {number}
|
|
270
|
+
*/
|
|
271
|
+
static #cornerRadius(aperture) {
|
|
272
|
+
if (
|
|
273
|
+
aperture.spec.side === 'top' &&
|
|
274
|
+
aperture.pad?.hasRoundedRect &&
|
|
275
|
+
Number.isFinite(aperture.pad.cornerRadiusTop)
|
|
276
|
+
) {
|
|
277
|
+
return (
|
|
278
|
+
Math.min(aperture.size.width, aperture.size.height) *
|
|
279
|
+
(Number(aperture.pad.cornerRadiusTop) / 100)
|
|
280
|
+
)
|
|
281
|
+
}
|
|
282
|
+
if (PcbPadMaskApertureRenderer.#shapeForAperture(aperture) === 1) {
|
|
283
|
+
return Math.min(aperture.size.width, aperture.size.height) / 2
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return 0
|
|
287
|
+
}
|
|
288
|
+
}
|