JLC2KiCadLib 1.1.0__tar.gz
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.
- jlc2kicadlib-1.1.0/.github/ISSUE_TEMPLATE/bug_report.md +24 -0
- jlc2kicadlib-1.1.0/.github/ISSUE_TEMPLATE/feature_request.md +19 -0
- jlc2kicadlib-1.1.0/.gitignore +20 -0
- jlc2kicadlib-1.1.0/JLC2KiCadLib/JLC2KiCadLib.py +216 -0
- jlc2kicadlib-1.1.0/JLC2KiCadLib/__init__.py +0 -0
- jlc2kicadlib-1.1.0/JLC2KiCadLib/footprint/__init__.py +0 -0
- jlc2kicadlib-1.1.0/JLC2KiCadLib/footprint/footprint.py +181 -0
- jlc2kicadlib-1.1.0/JLC2KiCadLib/footprint/footprint_handlers.py +486 -0
- jlc2kicadlib-1.1.0/JLC2KiCadLib/footprint/model3d.py +251 -0
- jlc2kicadlib-1.1.0/JLC2KiCadLib/helper.py +19 -0
- jlc2kicadlib-1.1.0/JLC2KiCadLib/symbol/__init__.py +0 -0
- jlc2kicadlib-1.1.0/JLC2KiCadLib/symbol/symbol.py +232 -0
- jlc2kicadlib-1.1.0/JLC2KiCadLib/symbol/symbol_handlers.py +356 -0
- jlc2kicadlib-1.1.0/LICENSE +9 -0
- jlc2kicadlib-1.1.0/PKG-INFO +145 -0
- jlc2kicadlib-1.1.0/README.md +128 -0
- jlc2kicadlib-1.1.0/images/JLC_3Dmodel.png +0 -0
- jlc2kicadlib-1.1.0/images/JLC_Footprint_1.png +0 -0
- jlc2kicadlib-1.1.0/images/JLC_Symbol_1.png +0 -0
- jlc2kicadlib-1.1.0/images/KiCad_3Dmodel.png +0 -0
- jlc2kicadlib-1.1.0/images/KiCad_Footprint_1.png +0 -0
- jlc2kicadlib-1.1.0/images/KiCad_Symbol_1.png +0 -0
- jlc2kicadlib-1.1.0/pyproject.toml +52 -0
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Bug report
|
|
3
|
+
about: Create a report to help me improve
|
|
4
|
+
title: ''
|
|
5
|
+
labels: ''
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
**Describe the bug**
|
|
10
|
+
A clear and concise description of what the bug is.
|
|
11
|
+
|
|
12
|
+
**To Reproduce**
|
|
13
|
+
Steps to reproduce the behavior:
|
|
14
|
+
1. LCSC part # that caused the issue
|
|
15
|
+
2. Arguments used for the execution
|
|
16
|
+
|
|
17
|
+
**Expected behavior**
|
|
18
|
+
A clear and concise description of what you expected to happen.
|
|
19
|
+
|
|
20
|
+
**Screenshots**
|
|
21
|
+
If applicable, add screenshots to help explain your problem.
|
|
22
|
+
|
|
23
|
+
**Additional context**
|
|
24
|
+
Add any other context about the problem here.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Feature request
|
|
3
|
+
about: Suggest an idea for this project
|
|
4
|
+
title: ''
|
|
5
|
+
labels: ''
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
**Is your feature request related to a problem? Please describe.**
|
|
10
|
+
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
11
|
+
|
|
12
|
+
**Describe the solution you'd like**
|
|
13
|
+
A clear and concise description of what you want to happen.
|
|
14
|
+
|
|
15
|
+
**Describe alternatives you've considered**
|
|
16
|
+
A clear and concise description of any alternative solutions or features you've considered.
|
|
17
|
+
|
|
18
|
+
**Additional context**
|
|
19
|
+
Add any other context or screenshots about the feature request here.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
.vscode/
|
|
2
|
+
KiCad
|
|
3
|
+
test.py
|
|
4
|
+
__pycache__
|
|
5
|
+
.gitignore
|
|
6
|
+
JLCPCB SMT Parts Library(*).csv
|
|
7
|
+
TestJLC2KiCad.py
|
|
8
|
+
*.log
|
|
9
|
+
*test*
|
|
10
|
+
failedComponents
|
|
11
|
+
build
|
|
12
|
+
JLCPCBsources
|
|
13
|
+
JLC2KiCadLib.egg-info
|
|
14
|
+
JLC2KiCad_lib/
|
|
15
|
+
dist/
|
|
16
|
+
TEST/
|
|
17
|
+
.venv
|
|
18
|
+
gen_lib.py
|
|
19
|
+
kicad-library-utils
|
|
20
|
+
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from importlib.metadata import version as pkg_version
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
from . import helper
|
|
9
|
+
|
|
10
|
+
__version__ = pkg_version("JLC2KiCadLib")
|
|
11
|
+
|
|
12
|
+
from .footprint.footprint import create_footprint, get_footprint_info
|
|
13
|
+
from .symbol.symbol import create_symbol
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def add_component(component_id, args):
|
|
17
|
+
logging.info(f"creating library for component {component_id}")
|
|
18
|
+
data = json.loads(
|
|
19
|
+
requests.get(
|
|
20
|
+
f"https://easyeda.com/api/products/{component_id}/svgs"
|
|
21
|
+
).content.decode()
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
if not data["success"]:
|
|
25
|
+
logging.error(
|
|
26
|
+
f"failed to get component uuid for {component_id}\n"
|
|
27
|
+
"The component # is probably wrong. Check a possible typo and that the "
|
|
28
|
+
"component exists on easyEDA"
|
|
29
|
+
)
|
|
30
|
+
return ()
|
|
31
|
+
|
|
32
|
+
footprint_component_uuid = data["result"][-1]["component_uuid"]
|
|
33
|
+
symbol_component_uuid = [i["component_uuid"] for i in data["result"][:-1]]
|
|
34
|
+
|
|
35
|
+
if args.footprint_creation:
|
|
36
|
+
footprint_name, datasheet_link = create_footprint(
|
|
37
|
+
footprint_component_uuid=footprint_component_uuid,
|
|
38
|
+
component_id=component_id,
|
|
39
|
+
footprint_lib=args.footprint_lib,
|
|
40
|
+
output_dir=args.output_dir,
|
|
41
|
+
model_base_variable=args.model_base_variable,
|
|
42
|
+
model_dir=args.model_dir,
|
|
43
|
+
skip_existing=args.skip_existing,
|
|
44
|
+
models=args.models,
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
_, datasheet_link, _, _ = get_footprint_info(footprint_component_uuid)
|
|
48
|
+
footprint_name = ""
|
|
49
|
+
|
|
50
|
+
if args.symbol_creation:
|
|
51
|
+
create_symbol(
|
|
52
|
+
symbol_component_uuid=symbol_component_uuid,
|
|
53
|
+
footprint_name=footprint_name.replace(
|
|
54
|
+
".pretty", ""
|
|
55
|
+
), # see https://github.com/TousstNicolas/JLC2KiCad_lib/issues/47
|
|
56
|
+
datasheet_link=datasheet_link,
|
|
57
|
+
library_name=args.symbol_lib,
|
|
58
|
+
symbol_path=args.symbol_lib_dir,
|
|
59
|
+
output_dir=args.output_dir,
|
|
60
|
+
component_id=component_id,
|
|
61
|
+
skip_existing=args.skip_existing,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def main():
|
|
66
|
+
parser = argparse.ArgumentParser(
|
|
67
|
+
description=(
|
|
68
|
+
"take a JLCPCB part # and create the according component'skicad's library"
|
|
69
|
+
),
|
|
70
|
+
epilog=(
|
|
71
|
+
"example use : \n"
|
|
72
|
+
" JLC2KiCadLib C1337258 C24112 -dir My_lib "
|
|
73
|
+
"-symbol_lib My_Symbol_lib --no_footprint"
|
|
74
|
+
),
|
|
75
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
parser.add_argument(
|
|
79
|
+
"components",
|
|
80
|
+
metavar="JLCPCB_part_#",
|
|
81
|
+
type=str,
|
|
82
|
+
nargs="+",
|
|
83
|
+
help="List of JLCPCB part # from the components you want to create",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
parser.add_argument(
|
|
87
|
+
"-dir",
|
|
88
|
+
dest="output_dir",
|
|
89
|
+
type=str,
|
|
90
|
+
default="JLC2KiCad_lib",
|
|
91
|
+
help="Base directory for output library files",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"--no_footprint",
|
|
96
|
+
dest="footprint_creation",
|
|
97
|
+
action="store_false",
|
|
98
|
+
help="Use --no_footprint if you do not want to create the footprint",
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
parser.add_argument(
|
|
102
|
+
"--no_symbol",
|
|
103
|
+
dest="symbol_creation",
|
|
104
|
+
action="store_false",
|
|
105
|
+
help="Use --no_symbol if you do not want to create the symbol",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
parser.add_argument(
|
|
109
|
+
"-symbol_lib",
|
|
110
|
+
dest="symbol_lib",
|
|
111
|
+
type=str,
|
|
112
|
+
default=None,
|
|
113
|
+
help='Set symbol library name, default is "default_lib"',
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"-symbol_lib_dir",
|
|
118
|
+
dest="symbol_lib_dir",
|
|
119
|
+
type=str,
|
|
120
|
+
default="symbol",
|
|
121
|
+
help='Set symbol library path, default is "symbol" (relative to OUTPUT_DIR)',
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
parser.add_argument(
|
|
125
|
+
"-footprint_lib",
|
|
126
|
+
dest="footprint_lib",
|
|
127
|
+
type=str,
|
|
128
|
+
default="footprint",
|
|
129
|
+
help='Set footprint library name, default is "footprint"',
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
parser.add_argument(
|
|
133
|
+
"-models",
|
|
134
|
+
dest="models",
|
|
135
|
+
nargs="*",
|
|
136
|
+
choices=["STEP", "WRL"],
|
|
137
|
+
type=str,
|
|
138
|
+
default="STEP",
|
|
139
|
+
help=(
|
|
140
|
+
"Select the 3D model you want to use. Default is STEP. "
|
|
141
|
+
"If both are selected, only the STEP model will be added to the footprint "
|
|
142
|
+
"(the WRL model will still be generated alongside the STEP model). "
|
|
143
|
+
"If you do not want any model to be generated, use the --models "
|
|
144
|
+
"without arguments"
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
parser.add_argument(
|
|
149
|
+
"-model_dir",
|
|
150
|
+
dest="model_dir",
|
|
151
|
+
type=str,
|
|
152
|
+
default="packages3d",
|
|
153
|
+
help=(
|
|
154
|
+
'Set directory for storing 3d models, default is "packages3d" '
|
|
155
|
+
"(relative to FOOTPRINT_LIB)"
|
|
156
|
+
),
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
parser.add_argument( # argument to skip already existing files and symbols
|
|
160
|
+
"--skip_existing",
|
|
161
|
+
dest="skip_existing",
|
|
162
|
+
action="store_true",
|
|
163
|
+
help=(
|
|
164
|
+
"Use --skip_existing if you want do not want to replace already existing "
|
|
165
|
+
"footprints and symbols"
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
parser.add_argument(
|
|
170
|
+
"-model_base_variable",
|
|
171
|
+
dest="model_base_variable",
|
|
172
|
+
type=str,
|
|
173
|
+
default="",
|
|
174
|
+
help=(
|
|
175
|
+
"Use -model_base_variable if you want to specify the base path of the 3D "
|
|
176
|
+
"model using a path variable. If the specified variable starts with '$' it "
|
|
177
|
+
"is used 'as-is', otherwise it is encapsulated: $(MODEL_BASE_VARIABLE)"
|
|
178
|
+
),
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
parser.add_argument(
|
|
182
|
+
"-logging_level",
|
|
183
|
+
dest="logging_level",
|
|
184
|
+
type=str,
|
|
185
|
+
default="INFO",
|
|
186
|
+
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
|
187
|
+
help=(
|
|
188
|
+
"Set logging level. If DEBUG is used, the debug logs are only written in "
|
|
189
|
+
"the log file if the option --log_file is set "
|
|
190
|
+
),
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
parser.add_argument(
|
|
194
|
+
"--log_file",
|
|
195
|
+
dest="log_file",
|
|
196
|
+
action="store_true",
|
|
197
|
+
help="Use --log_file if you want logs to be written in a file",
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
parser.add_argument(
|
|
201
|
+
"--version",
|
|
202
|
+
action="version",
|
|
203
|
+
version=f"%(prog)s {__version__}",
|
|
204
|
+
help="Print versin number and exit",
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
args = parser.parse_args()
|
|
208
|
+
|
|
209
|
+
helper.set_logging(args.logging_level, args.log_file)
|
|
210
|
+
|
|
211
|
+
for component in args.components:
|
|
212
|
+
add_component(component, args)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
main()
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import logging
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from KicadModTree import Footprint, KicadFileHandler, Pad, Text, Translation
|
|
8
|
+
|
|
9
|
+
from .footprint_handlers import handlers, mil2mm
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class FootprintInfo:
|
|
14
|
+
max_X: float = -10000
|
|
15
|
+
max_Y: float = -10000
|
|
16
|
+
min_X: float = 10000
|
|
17
|
+
min_Y: float = 10000
|
|
18
|
+
footprint_name: str = ""
|
|
19
|
+
output_dir: str = ""
|
|
20
|
+
footprint_lib: str = ""
|
|
21
|
+
model_base_variable: str = ""
|
|
22
|
+
model_dir: str = ""
|
|
23
|
+
origin: tuple = (0, 0)
|
|
24
|
+
models: str = ""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def create_footprint(
|
|
28
|
+
footprint_component_uuid,
|
|
29
|
+
component_id,
|
|
30
|
+
footprint_lib,
|
|
31
|
+
output_dir,
|
|
32
|
+
model_base_variable,
|
|
33
|
+
model_dir,
|
|
34
|
+
skip_existing,
|
|
35
|
+
models,
|
|
36
|
+
):
|
|
37
|
+
logging.info("Creating footprint ...")
|
|
38
|
+
|
|
39
|
+
(
|
|
40
|
+
footprint_name,
|
|
41
|
+
datasheet_link,
|
|
42
|
+
footprint_shape,
|
|
43
|
+
translation,
|
|
44
|
+
) = get_footprint_info(footprint_component_uuid)
|
|
45
|
+
|
|
46
|
+
if skip_existing and os.path.isfile(
|
|
47
|
+
os.path.join(output_dir, footprint_lib, footprint_name + ".kicad_mod")
|
|
48
|
+
):
|
|
49
|
+
logging.info(f"Footprint {footprint_name} already exists, skipping.")
|
|
50
|
+
return f"{footprint_lib}:{footprint_name}", datasheet_link
|
|
51
|
+
|
|
52
|
+
# init kicad footprint
|
|
53
|
+
kicad_mod = Footprint(f'"{footprint_name}"')
|
|
54
|
+
kicad_mod.setDescription(f"{footprint_name} footprint") # TODO Set real description
|
|
55
|
+
kicad_mod.setTags(f"{footprint_name} footprint {component_id}")
|
|
56
|
+
|
|
57
|
+
footprint_info = FootprintInfo(
|
|
58
|
+
footprint_name=footprint_name,
|
|
59
|
+
output_dir=output_dir,
|
|
60
|
+
footprint_lib=footprint_lib,
|
|
61
|
+
model_base_variable=model_base_variable,
|
|
62
|
+
model_dir=model_dir,
|
|
63
|
+
origin=translation,
|
|
64
|
+
models=models,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# for each line in data : use the appropriate handler
|
|
68
|
+
for line in footprint_shape:
|
|
69
|
+
args = [
|
|
70
|
+
i for i in line.split("~") if i
|
|
71
|
+
] # split and remove empty string in list
|
|
72
|
+
model = args[0]
|
|
73
|
+
logging.debug(args)
|
|
74
|
+
if model not in handlers:
|
|
75
|
+
logging.warning(f"footprint : model not in handler : {model}")
|
|
76
|
+
else:
|
|
77
|
+
handlers.get(model)(args[1:], kicad_mod, footprint_info)
|
|
78
|
+
|
|
79
|
+
if any(
|
|
80
|
+
isinstance(child, Pad) and child.type == Pad.TYPE_THT
|
|
81
|
+
for child in kicad_mod.getAllChilds()
|
|
82
|
+
):
|
|
83
|
+
kicad_mod.setAttribute("through_hole")
|
|
84
|
+
else:
|
|
85
|
+
kicad_mod.setAttribute("smd")
|
|
86
|
+
|
|
87
|
+
kicad_mod.insert(Translation(-mil2mm(translation[0]), -mil2mm(translation[1])))
|
|
88
|
+
|
|
89
|
+
# Translate the footprint max and min values to the origin
|
|
90
|
+
footprint_info.max_X -= mil2mm(translation[0])
|
|
91
|
+
footprint_info.max_Y -= mil2mm(translation[1])
|
|
92
|
+
footprint_info.min_X -= mil2mm(translation[0])
|
|
93
|
+
footprint_info.min_Y -= mil2mm(translation[1])
|
|
94
|
+
|
|
95
|
+
# set general values
|
|
96
|
+
kicad_mod.append(
|
|
97
|
+
Text(
|
|
98
|
+
type="reference",
|
|
99
|
+
text="REF**",
|
|
100
|
+
at=[
|
|
101
|
+
(footprint_info.min_X + footprint_info.max_X) / 2,
|
|
102
|
+
footprint_info.min_Y - 2,
|
|
103
|
+
],
|
|
104
|
+
layer="F.SilkS",
|
|
105
|
+
)
|
|
106
|
+
)
|
|
107
|
+
kicad_mod.append(
|
|
108
|
+
Text(
|
|
109
|
+
type="user",
|
|
110
|
+
text="${REFERENCE}",
|
|
111
|
+
at=[
|
|
112
|
+
(footprint_info.min_X + footprint_info.max_X) / 2,
|
|
113
|
+
(footprint_info.min_Y + footprint_info.max_Y) / 2,
|
|
114
|
+
],
|
|
115
|
+
layer="F.Fab",
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
kicad_mod.append(
|
|
119
|
+
Text(
|
|
120
|
+
type="value",
|
|
121
|
+
text=footprint_name,
|
|
122
|
+
at=[
|
|
123
|
+
(footprint_info.min_X + footprint_info.max_X) / 2,
|
|
124
|
+
footprint_info.max_Y + 2,
|
|
125
|
+
],
|
|
126
|
+
layer="F.Fab",
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
if not os.path.exists(f"{output_dir}/{footprint_lib}"):
|
|
131
|
+
os.makedirs(f"{output_dir}/{footprint_lib}")
|
|
132
|
+
|
|
133
|
+
# output kicad model
|
|
134
|
+
file_handler = KicadFileHandler(kicad_mod)
|
|
135
|
+
file_handler.writeFile(f"{output_dir}/{footprint_lib}/{footprint_name}.kicad_mod")
|
|
136
|
+
logging.info(f"Created '{output_dir}/{footprint_lib}/{footprint_name}.kicad_mod'")
|
|
137
|
+
|
|
138
|
+
# return the datasheet link and footprint name to be linked with the symbol
|
|
139
|
+
return (f"{footprint_lib}:{footprint_name}", datasheet_link)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_footprint_info(footprint_component_uuid):
|
|
143
|
+
# fetch the component data from easyeda library
|
|
144
|
+
response = requests.get(
|
|
145
|
+
f"https://easyeda.com/api/components/{footprint_component_uuid}"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if response.status_code == requests.codes.ok:
|
|
149
|
+
data = json.loads(response.content.decode())
|
|
150
|
+
else:
|
|
151
|
+
logging.error(
|
|
152
|
+
"create_footprint error. Requests returned with error code "
|
|
153
|
+
f"{response.status_code}"
|
|
154
|
+
)
|
|
155
|
+
return ("", None, "", (0, 0))
|
|
156
|
+
|
|
157
|
+
footprint_shape = data["result"]["dataStr"]["shape"]
|
|
158
|
+
x = data["result"]["dataStr"]["head"]["x"]
|
|
159
|
+
y = data["result"]["dataStr"]["head"]["y"]
|
|
160
|
+
try:
|
|
161
|
+
datasheet_link = data["result"]["dataStr"]["head"]["c_para"]["link"]
|
|
162
|
+
except KeyError:
|
|
163
|
+
datasheet_link = ""
|
|
164
|
+
logging.warning("Could not retrieve datasheet link from EASYEDA")
|
|
165
|
+
|
|
166
|
+
footprint_name = (
|
|
167
|
+
data["result"]["title"]
|
|
168
|
+
.replace(" ", "_")
|
|
169
|
+
.replace("/", "_")
|
|
170
|
+
.replace("(", "_")
|
|
171
|
+
.replace(")", "_")
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
if not footprint_name:
|
|
175
|
+
footprint_name = "NoName"
|
|
176
|
+
logging.warning(
|
|
177
|
+
"Could not retrieve components information from EASYEDA, default name "
|
|
178
|
+
"'NoName'."
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
return (footprint_name, datasheet_link, footprint_shape, (x, y))
|