kicad-sch-api 0.4.0__py3-none-any.whl → 0.4.2__py3-none-any.whl
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.
Potentially problematic release.
This version of kicad-sch-api might be problematic. Click here for more details.
- kicad_sch_api/__init__.py +2 -2
- kicad_sch_api/cli/__init__.py +45 -0
- kicad_sch_api/cli/base.py +302 -0
- kicad_sch_api/cli/bom.py +164 -0
- kicad_sch_api/cli/erc.py +229 -0
- kicad_sch_api/cli/export_docs.py +289 -0
- kicad_sch_api/cli/netlist.py +94 -0
- kicad_sch_api/cli/types.py +43 -0
- kicad_sch_api/core/collections/__init__.py +5 -0
- kicad_sch_api/core/collections/base.py +248 -0
- kicad_sch_api/core/component_bounds.py +5 -0
- kicad_sch_api/core/components.py +142 -47
- kicad_sch_api/core/config.py +85 -3
- kicad_sch_api/core/factories/__init__.py +5 -0
- kicad_sch_api/core/factories/element_factory.py +276 -0
- kicad_sch_api/core/formatter.py +22 -5
- kicad_sch_api/core/junctions.py +26 -75
- kicad_sch_api/core/labels.py +28 -52
- kicad_sch_api/core/managers/file_io.py +3 -2
- kicad_sch_api/core/managers/metadata.py +6 -5
- kicad_sch_api/core/managers/validation.py +3 -2
- kicad_sch_api/core/managers/wire.py +7 -1
- kicad_sch_api/core/nets.py +38 -43
- kicad_sch_api/core/no_connects.py +29 -53
- kicad_sch_api/core/parser.py +75 -1765
- kicad_sch_api/core/schematic.py +211 -148
- kicad_sch_api/core/texts.py +28 -55
- kicad_sch_api/core/types.py +59 -18
- kicad_sch_api/core/wires.py +27 -75
- kicad_sch_api/parsers/elements/__init__.py +22 -0
- kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
- kicad_sch_api/parsers/elements/label_parser.py +194 -0
- kicad_sch_api/parsers/elements/library_parser.py +165 -0
- kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
- kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
- kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
- kicad_sch_api/parsers/elements/text_parser.py +250 -0
- kicad_sch_api/parsers/elements/wire_parser.py +242 -0
- kicad_sch_api/parsers/utils.py +80 -0
- kicad_sch_api/validation/__init__.py +25 -0
- kicad_sch_api/validation/erc.py +171 -0
- kicad_sch_api/validation/erc_models.py +203 -0
- kicad_sch_api/validation/pin_matrix.py +243 -0
- kicad_sch_api/validation/validators.py +391 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/METADATA +17 -9
- kicad_sch_api-0.4.2.dist-info/RECORD +87 -0
- kicad_sch_api/core/manhattan_routing.py +0 -430
- kicad_sch_api/core/simple_manhattan.py +0 -228
- kicad_sch_api/core/wire_routing.py +0 -380
- kicad_sch_api/parsers/label_parser.py +0 -254
- kicad_sch_api/parsers/symbol_parser.py +0 -222
- kicad_sch_api/parsers/wire_parser.py +0 -99
- kicad_sch_api-0.4.0.dist-info/RECORD +0 -67
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Graphics element parsers for KiCAD schematics.
|
|
3
|
+
|
|
4
|
+
Handles parsing and serialization of graphical elements:
|
|
5
|
+
- Polyline
|
|
6
|
+
- Arc
|
|
7
|
+
- Circle
|
|
8
|
+
- Bezier curves
|
|
9
|
+
- Rectangle
|
|
10
|
+
- Image
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import logging
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
import sexpdata
|
|
17
|
+
|
|
18
|
+
from ...core.config import config
|
|
19
|
+
from ..base import BaseElementParser
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class GraphicsParser(BaseElementParser):
|
|
25
|
+
"""Parser for graphical schematic elements."""
|
|
26
|
+
|
|
27
|
+
def __init__(self):
|
|
28
|
+
"""Initialize graphics parser."""
|
|
29
|
+
super().__init__("graphics")
|
|
30
|
+
|
|
31
|
+
def _parse_polyline(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
32
|
+
"""Parse a polyline graphical element."""
|
|
33
|
+
# Format: (polyline (pts (xy x1 y1) (xy x2 y2) ...) (stroke ...) (uuid ...))
|
|
34
|
+
polyline_data = {"points": [], "stroke_width": config.defaults.stroke_width, "stroke_type": config.defaults.stroke_type, "uuid": None}
|
|
35
|
+
|
|
36
|
+
for elem in item[1:]:
|
|
37
|
+
if not isinstance(elem, list):
|
|
38
|
+
continue
|
|
39
|
+
|
|
40
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
41
|
+
|
|
42
|
+
if elem_type == "pts":
|
|
43
|
+
for pt in elem[1:]:
|
|
44
|
+
if isinstance(pt, list) and len(pt) >= 3 and str(pt[0]) == "xy":
|
|
45
|
+
polyline_data["points"].append({"x": float(pt[1]), "y": float(pt[2])})
|
|
46
|
+
elif elem_type == "stroke":
|
|
47
|
+
for stroke_elem in elem[1:]:
|
|
48
|
+
if isinstance(stroke_elem, list):
|
|
49
|
+
stroke_type = str(stroke_elem[0])
|
|
50
|
+
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
51
|
+
polyline_data["stroke_width"] = float(stroke_elem[1])
|
|
52
|
+
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
53
|
+
polyline_data["stroke_type"] = str(stroke_elem[1])
|
|
54
|
+
elif elem_type == "uuid":
|
|
55
|
+
polyline_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
56
|
+
|
|
57
|
+
return polyline_data if polyline_data["points"] else None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _parse_arc(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
61
|
+
"""Parse an arc graphical element."""
|
|
62
|
+
# Format: (arc (start x y) (mid x y) (end x y) (stroke ...) (fill ...) (uuid ...))
|
|
63
|
+
arc_data = {
|
|
64
|
+
"start": {"x": 0, "y": 0},
|
|
65
|
+
"mid": {"x": 0, "y": 0},
|
|
66
|
+
"end": {"x": 0, "y": 0},
|
|
67
|
+
"stroke_width": config.defaults.stroke_width,
|
|
68
|
+
"stroke_type": config.defaults.stroke_type,
|
|
69
|
+
"fill_type": config.defaults.fill_type,
|
|
70
|
+
"uuid": None,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for elem in item[1:]:
|
|
74
|
+
if not isinstance(elem, list):
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
78
|
+
|
|
79
|
+
if elem_type == "start" and len(elem) >= 3:
|
|
80
|
+
arc_data["start"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
81
|
+
elif elem_type == "mid" and len(elem) >= 3:
|
|
82
|
+
arc_data["mid"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
83
|
+
elif elem_type == "end" and len(elem) >= 3:
|
|
84
|
+
arc_data["end"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
85
|
+
elif elem_type == "stroke":
|
|
86
|
+
for stroke_elem in elem[1:]:
|
|
87
|
+
if isinstance(stroke_elem, list):
|
|
88
|
+
stroke_type = str(stroke_elem[0])
|
|
89
|
+
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
90
|
+
arc_data["stroke_width"] = float(stroke_elem[1])
|
|
91
|
+
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
92
|
+
arc_data["stroke_type"] = str(stroke_elem[1])
|
|
93
|
+
elif elem_type == "fill":
|
|
94
|
+
for fill_elem in elem[1:]:
|
|
95
|
+
if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
|
|
96
|
+
arc_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
|
|
97
|
+
elif elem_type == "uuid":
|
|
98
|
+
arc_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
99
|
+
|
|
100
|
+
return arc_data
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _parse_circle(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
104
|
+
"""Parse a circle graphical element."""
|
|
105
|
+
# Format: (circle (center x y) (radius r) (stroke ...) (fill ...) (uuid ...))
|
|
106
|
+
circle_data = {
|
|
107
|
+
"center": {"x": 0, "y": 0},
|
|
108
|
+
"radius": 0,
|
|
109
|
+
"stroke_width": config.defaults.stroke_width,
|
|
110
|
+
"stroke_type": config.defaults.stroke_type,
|
|
111
|
+
"fill_type": config.defaults.fill_type,
|
|
112
|
+
"uuid": None,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
for elem in item[1:]:
|
|
116
|
+
if not isinstance(elem, list):
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
120
|
+
|
|
121
|
+
if elem_type == "center" and len(elem) >= 3:
|
|
122
|
+
circle_data["center"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
123
|
+
elif elem_type == "radius" and len(elem) >= 2:
|
|
124
|
+
circle_data["radius"] = float(elem[1])
|
|
125
|
+
elif elem_type == "stroke":
|
|
126
|
+
for stroke_elem in elem[1:]:
|
|
127
|
+
if isinstance(stroke_elem, list):
|
|
128
|
+
stroke_type = str(stroke_elem[0])
|
|
129
|
+
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
130
|
+
circle_data["stroke_width"] = float(stroke_elem[1])
|
|
131
|
+
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
132
|
+
circle_data["stroke_type"] = str(stroke_elem[1])
|
|
133
|
+
elif elem_type == "fill":
|
|
134
|
+
for fill_elem in elem[1:]:
|
|
135
|
+
if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
|
|
136
|
+
circle_data["fill_type"] = (
|
|
137
|
+
str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
|
|
138
|
+
)
|
|
139
|
+
elif elem_type == "uuid":
|
|
140
|
+
circle_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
141
|
+
|
|
142
|
+
return circle_data
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _parse_bezier(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
146
|
+
"""Parse a bezier curve graphical element."""
|
|
147
|
+
# Format: (bezier (pts (xy x1 y1) (xy x2 y2) ...) (stroke ...) (fill ...) (uuid ...))
|
|
148
|
+
bezier_data = {
|
|
149
|
+
"points": [],
|
|
150
|
+
"stroke_width": config.defaults.stroke_width,
|
|
151
|
+
"stroke_type": config.defaults.stroke_type,
|
|
152
|
+
"fill_type": config.defaults.fill_type,
|
|
153
|
+
"uuid": None,
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for elem in item[1:]:
|
|
157
|
+
if not isinstance(elem, list):
|
|
158
|
+
continue
|
|
159
|
+
|
|
160
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
161
|
+
|
|
162
|
+
if elem_type == "pts":
|
|
163
|
+
for pt in elem[1:]:
|
|
164
|
+
if isinstance(pt, list) and len(pt) >= 3 and str(pt[0]) == "xy":
|
|
165
|
+
bezier_data["points"].append({"x": float(pt[1]), "y": float(pt[2])})
|
|
166
|
+
elif elem_type == "stroke":
|
|
167
|
+
for stroke_elem in elem[1:]:
|
|
168
|
+
if isinstance(stroke_elem, list):
|
|
169
|
+
stroke_type = str(stroke_elem[0])
|
|
170
|
+
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
171
|
+
bezier_data["stroke_width"] = float(stroke_elem[1])
|
|
172
|
+
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
173
|
+
bezier_data["stroke_type"] = str(stroke_elem[1])
|
|
174
|
+
elif elem_type == "fill":
|
|
175
|
+
for fill_elem in elem[1:]:
|
|
176
|
+
if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
|
|
177
|
+
bezier_data["fill_type"] = (
|
|
178
|
+
str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
|
|
179
|
+
)
|
|
180
|
+
elif elem_type == "uuid":
|
|
181
|
+
bezier_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
|
|
182
|
+
|
|
183
|
+
return bezier_data if bezier_data["points"] else None
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _parse_rectangle(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
187
|
+
"""Parse a rectangle graphical element."""
|
|
188
|
+
rectangle = {}
|
|
189
|
+
|
|
190
|
+
for elem in item[1:]:
|
|
191
|
+
if not isinstance(elem, list):
|
|
192
|
+
continue
|
|
193
|
+
|
|
194
|
+
elem_type = str(elem[0])
|
|
195
|
+
|
|
196
|
+
if elem_type == "start" and len(elem) >= 3:
|
|
197
|
+
rectangle["start"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
198
|
+
elif elem_type == "end" and len(elem) >= 3:
|
|
199
|
+
rectangle["end"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
200
|
+
elif elem_type == "stroke":
|
|
201
|
+
for stroke_elem in elem[1:]:
|
|
202
|
+
if isinstance(stroke_elem, list):
|
|
203
|
+
stroke_type = str(stroke_elem[0])
|
|
204
|
+
if stroke_type == "width" and len(stroke_elem) >= 2:
|
|
205
|
+
rectangle["stroke_width"] = float(stroke_elem[1])
|
|
206
|
+
elif stroke_type == "type" and len(stroke_elem) >= 2:
|
|
207
|
+
rectangle["stroke_type"] = str(stroke_elem[1])
|
|
208
|
+
elif elem_type == "fill":
|
|
209
|
+
for fill_elem in elem[1:]:
|
|
210
|
+
if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
|
|
211
|
+
rectangle["fill_type"] = (
|
|
212
|
+
str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
|
|
213
|
+
)
|
|
214
|
+
elif elem_type == "uuid" and len(elem) >= 2:
|
|
215
|
+
rectangle["uuid"] = str(elem[1])
|
|
216
|
+
|
|
217
|
+
return rectangle if rectangle else None
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _parse_image(self, item: List[Any]) -> Optional[Dict[str, Any]]:
|
|
221
|
+
"""Parse an image element."""
|
|
222
|
+
# Format: (image (at x y) (uuid "...") (data "base64..."))
|
|
223
|
+
image = {"position": {"x": 0, "y": 0}, "data": "", "scale": 1.0, "uuid": None}
|
|
224
|
+
|
|
225
|
+
for elem in item[1:]:
|
|
226
|
+
if not isinstance(elem, list):
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
|
|
230
|
+
|
|
231
|
+
if elem_type == "at" and len(elem) >= 3:
|
|
232
|
+
image["position"] = {"x": float(elem[1]), "y": float(elem[2])}
|
|
233
|
+
elif elem_type == "scale" and len(elem) >= 2:
|
|
234
|
+
image["scale"] = float(elem[1])
|
|
235
|
+
elif elem_type == "data" and len(elem) >= 2:
|
|
236
|
+
# The data can be spread across multiple string elements
|
|
237
|
+
data_parts = []
|
|
238
|
+
for data_elem in elem[1:]:
|
|
239
|
+
data_parts.append(str(data_elem).strip('"'))
|
|
240
|
+
image["data"] = "".join(data_parts)
|
|
241
|
+
elif elem_type == "uuid" and len(elem) >= 2:
|
|
242
|
+
image["uuid"] = str(elem[1]).strip('"')
|
|
243
|
+
|
|
244
|
+
return image if image.get("uuid") and image.get("data") else None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _polyline_to_sexp(self, polyline_data: Dict[str, Any]) -> List[Any]:
|
|
248
|
+
"""Convert polyline to S-expression."""
|
|
249
|
+
sexp = [sexpdata.Symbol("polyline")]
|
|
250
|
+
|
|
251
|
+
# Add points
|
|
252
|
+
points = polyline_data.get("points", [])
|
|
253
|
+
if points:
|
|
254
|
+
pts_sexp = [sexpdata.Symbol("pts")]
|
|
255
|
+
for point in points:
|
|
256
|
+
x, y = point["x"], point["y"]
|
|
257
|
+
# Format coordinates properly
|
|
258
|
+
if isinstance(x, float) and x.is_integer():
|
|
259
|
+
x = int(x)
|
|
260
|
+
if isinstance(y, float) and y.is_integer():
|
|
261
|
+
y = int(y)
|
|
262
|
+
pts_sexp.append([sexpdata.Symbol("xy"), x, y])
|
|
263
|
+
sexp.append(pts_sexp)
|
|
264
|
+
|
|
265
|
+
# Add stroke
|
|
266
|
+
stroke_width = polyline_data.get("stroke_width", config.defaults.stroke_width)
|
|
267
|
+
stroke_type = polyline_data.get("stroke_type", config.defaults.stroke_type)
|
|
268
|
+
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
269
|
+
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
270
|
+
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
271
|
+
sexp.append(stroke_sexp)
|
|
272
|
+
|
|
273
|
+
# Add UUID
|
|
274
|
+
if "uuid" in polyline_data:
|
|
275
|
+
sexp.append([sexpdata.Symbol("uuid"), polyline_data["uuid"]])
|
|
276
|
+
|
|
277
|
+
return sexp
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _arc_to_sexp(self, arc_data: Dict[str, Any]) -> List[Any]:
|
|
281
|
+
"""Convert arc to S-expression."""
|
|
282
|
+
sexp = [sexpdata.Symbol("arc")]
|
|
283
|
+
|
|
284
|
+
# Add start, mid, end points
|
|
285
|
+
for point_name in ["start", "mid", "end"]:
|
|
286
|
+
point = arc_data.get(point_name, {"x": 0, "y": 0})
|
|
287
|
+
x, y = point["x"], point["y"]
|
|
288
|
+
# Format coordinates properly
|
|
289
|
+
if isinstance(x, float) and x.is_integer():
|
|
290
|
+
x = int(x)
|
|
291
|
+
if isinstance(y, float) and y.is_integer():
|
|
292
|
+
y = int(y)
|
|
293
|
+
sexp.append([sexpdata.Symbol(point_name), x, y])
|
|
294
|
+
|
|
295
|
+
# Add stroke
|
|
296
|
+
stroke_width = arc_data.get("stroke_width", config.defaults.stroke_width)
|
|
297
|
+
stroke_type = arc_data.get("stroke_type", config.defaults.stroke_type)
|
|
298
|
+
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
299
|
+
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
300
|
+
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
301
|
+
sexp.append(stroke_sexp)
|
|
302
|
+
|
|
303
|
+
# Add fill
|
|
304
|
+
fill_type = arc_data.get("fill_type", config.defaults.fill_type)
|
|
305
|
+
fill_sexp = [sexpdata.Symbol("fill")]
|
|
306
|
+
fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
|
|
307
|
+
sexp.append(fill_sexp)
|
|
308
|
+
|
|
309
|
+
# Add UUID
|
|
310
|
+
if "uuid" in arc_data:
|
|
311
|
+
sexp.append([sexpdata.Symbol("uuid"), arc_data["uuid"]])
|
|
312
|
+
|
|
313
|
+
return sexp
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _circle_to_sexp(self, circle_data: Dict[str, Any]) -> List[Any]:
|
|
317
|
+
"""Convert circle to S-expression."""
|
|
318
|
+
sexp = [sexpdata.Symbol("circle")]
|
|
319
|
+
|
|
320
|
+
# Add center
|
|
321
|
+
center = circle_data.get("center", {"x": 0, "y": 0})
|
|
322
|
+
x, y = center["x"], center["y"]
|
|
323
|
+
# Format coordinates properly
|
|
324
|
+
if isinstance(x, float) and x.is_integer():
|
|
325
|
+
x = int(x)
|
|
326
|
+
if isinstance(y, float) and y.is_integer():
|
|
327
|
+
y = int(y)
|
|
328
|
+
sexp.append([sexpdata.Symbol("center"), x, y])
|
|
329
|
+
|
|
330
|
+
# Add radius
|
|
331
|
+
radius = circle_data.get("radius", 0)
|
|
332
|
+
sexp.append([sexpdata.Symbol("radius"), radius])
|
|
333
|
+
|
|
334
|
+
# Add stroke
|
|
335
|
+
stroke_width = circle_data.get("stroke_width", config.defaults.stroke_width)
|
|
336
|
+
stroke_type = circle_data.get("stroke_type", config.defaults.stroke_type)
|
|
337
|
+
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
338
|
+
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
339
|
+
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
340
|
+
sexp.append(stroke_sexp)
|
|
341
|
+
|
|
342
|
+
# Add fill
|
|
343
|
+
fill_type = circle_data.get("fill_type", config.defaults.fill_type)
|
|
344
|
+
fill_sexp = [sexpdata.Symbol("fill")]
|
|
345
|
+
fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
|
|
346
|
+
sexp.append(fill_sexp)
|
|
347
|
+
|
|
348
|
+
# Add UUID
|
|
349
|
+
if "uuid" in circle_data:
|
|
350
|
+
sexp.append([sexpdata.Symbol("uuid"), circle_data["uuid"]])
|
|
351
|
+
|
|
352
|
+
return sexp
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _bezier_to_sexp(self, bezier_data: Dict[str, Any]) -> List[Any]:
|
|
356
|
+
"""Convert bezier curve to S-expression."""
|
|
357
|
+
sexp = [sexpdata.Symbol("bezier")]
|
|
358
|
+
|
|
359
|
+
# Add points
|
|
360
|
+
points = bezier_data.get("points", [])
|
|
361
|
+
if points:
|
|
362
|
+
pts_sexp = [sexpdata.Symbol("pts")]
|
|
363
|
+
for point in points:
|
|
364
|
+
x, y = point["x"], point["y"]
|
|
365
|
+
# Format coordinates properly
|
|
366
|
+
if isinstance(x, float) and x.is_integer():
|
|
367
|
+
x = int(x)
|
|
368
|
+
if isinstance(y, float) and y.is_integer():
|
|
369
|
+
y = int(y)
|
|
370
|
+
pts_sexp.append([sexpdata.Symbol("xy"), x, y])
|
|
371
|
+
sexp.append(pts_sexp)
|
|
372
|
+
|
|
373
|
+
# Add stroke
|
|
374
|
+
stroke_width = bezier_data.get("stroke_width", config.defaults.stroke_width)
|
|
375
|
+
stroke_type = bezier_data.get("stroke_type", config.defaults.stroke_type)
|
|
376
|
+
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
377
|
+
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
378
|
+
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
379
|
+
sexp.append(stroke_sexp)
|
|
380
|
+
|
|
381
|
+
# Add fill
|
|
382
|
+
fill_type = bezier_data.get("fill_type", config.defaults.fill_type)
|
|
383
|
+
fill_sexp = [sexpdata.Symbol("fill")]
|
|
384
|
+
fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
|
|
385
|
+
sexp.append(fill_sexp)
|
|
386
|
+
|
|
387
|
+
# Add UUID
|
|
388
|
+
if "uuid" in bezier_data:
|
|
389
|
+
sexp.append([sexpdata.Symbol("uuid"), bezier_data["uuid"]])
|
|
390
|
+
|
|
391
|
+
return sexp
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def _rectangle_to_sexp(self, rectangle_data: Dict[str, Any]) -> List[Any]:
|
|
395
|
+
"""Convert rectangle element to S-expression."""
|
|
396
|
+
sexp = [sexpdata.Symbol("rectangle")]
|
|
397
|
+
|
|
398
|
+
# Add start point
|
|
399
|
+
start = rectangle_data["start"]
|
|
400
|
+
start_x, start_y = start["x"], start["y"]
|
|
401
|
+
sexp.append([sexpdata.Symbol("start"), start_x, start_y])
|
|
402
|
+
|
|
403
|
+
# Add end point
|
|
404
|
+
end = rectangle_data["end"]
|
|
405
|
+
end_x, end_y = end["x"], end["y"]
|
|
406
|
+
sexp.append([sexpdata.Symbol("end"), end_x, end_y])
|
|
407
|
+
|
|
408
|
+
# Add stroke
|
|
409
|
+
stroke_width = rectangle_data.get("stroke_width", config.defaults.stroke_width)
|
|
410
|
+
stroke_type = rectangle_data.get("stroke_type", config.defaults.stroke_type)
|
|
411
|
+
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
412
|
+
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
413
|
+
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
|
|
414
|
+
# Add stroke color if present
|
|
415
|
+
if "stroke_color" in rectangle_data:
|
|
416
|
+
r, g, b, a = rectangle_data["stroke_color"]
|
|
417
|
+
stroke_sexp.append([sexpdata.Symbol("color"), r, g, b, a])
|
|
418
|
+
sexp.append(stroke_sexp)
|
|
419
|
+
|
|
420
|
+
# Add fill
|
|
421
|
+
fill_type = rectangle_data.get("fill_type", config.defaults.fill_type)
|
|
422
|
+
fill_sexp = [sexpdata.Symbol("fill")]
|
|
423
|
+
fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
|
|
424
|
+
# Add fill color if present
|
|
425
|
+
if "fill_color" in rectangle_data:
|
|
426
|
+
r, g, b, a = rectangle_data["fill_color"]
|
|
427
|
+
fill_sexp.append([sexpdata.Symbol("color"), r, g, b, a])
|
|
428
|
+
sexp.append(fill_sexp)
|
|
429
|
+
|
|
430
|
+
# Add UUID
|
|
431
|
+
if "uuid" in rectangle_data:
|
|
432
|
+
sexp.append([sexpdata.Symbol("uuid"), rectangle_data["uuid"]])
|
|
433
|
+
|
|
434
|
+
return sexp
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def _image_to_sexp(self, image_data: Dict[str, Any]) -> List[Any]:
|
|
438
|
+
"""Convert image element to S-expression."""
|
|
439
|
+
sexp = [sexpdata.Symbol("image")]
|
|
440
|
+
|
|
441
|
+
# Add position
|
|
442
|
+
position = image_data.get("position", {"x": 0, "y": 0})
|
|
443
|
+
pos_x, pos_y = position["x"], position["y"]
|
|
444
|
+
sexp.append([sexpdata.Symbol("at"), pos_x, pos_y])
|
|
445
|
+
|
|
446
|
+
# Add UUID
|
|
447
|
+
if "uuid" in image_data:
|
|
448
|
+
sexp.append([sexpdata.Symbol("uuid"), image_data["uuid"]])
|
|
449
|
+
|
|
450
|
+
# Add scale if not default
|
|
451
|
+
scale = image_data.get("scale", 1.0)
|
|
452
|
+
if scale != 1.0:
|
|
453
|
+
sexp.append([sexpdata.Symbol("scale"), scale])
|
|
454
|
+
|
|
455
|
+
# Add image data
|
|
456
|
+
# KiCad splits base64 data into multiple lines for readability
|
|
457
|
+
# Each line is roughly 76 characters (standard base64 line length)
|
|
458
|
+
data = image_data.get("data", "")
|
|
459
|
+
if data:
|
|
460
|
+
data_sexp = [sexpdata.Symbol("data")]
|
|
461
|
+
# Split the data into 76-character chunks
|
|
462
|
+
chunk_size = 76
|
|
463
|
+
for i in range(0, len(data), chunk_size):
|
|
464
|
+
data_sexp.append(data[i : i + chunk_size])
|
|
465
|
+
sexp.append(data_sexp)
|
|
466
|
+
|
|
467
|
+
return sexp
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _graphic_to_sexp(self, graphic_data: Dict[str, Any]) -> List[Any]:
|
|
471
|
+
"""Convert graphics (rectangles, etc.) to S-expression."""
|
|
472
|
+
# For now, we only support rectangles - this is the main graphics element we create
|
|
473
|
+
sexp = [sexpdata.Symbol("rectangle")]
|
|
474
|
+
|
|
475
|
+
# Add start position
|
|
476
|
+
start = graphic_data.get("start", {})
|
|
477
|
+
start_x = start.get("x", 0)
|
|
478
|
+
start_y = start.get("y", 0)
|
|
479
|
+
|
|
480
|
+
# Format coordinates properly (avoid unnecessary .0 for integers)
|
|
481
|
+
if isinstance(start_x, float) and start_x.is_integer():
|
|
482
|
+
start_x = int(start_x)
|
|
483
|
+
if isinstance(start_y, float) and start_y.is_integer():
|
|
484
|
+
start_y = int(start_y)
|
|
485
|
+
|
|
486
|
+
sexp.append([sexpdata.Symbol("start"), start_x, start_y])
|
|
487
|
+
|
|
488
|
+
# Add end position
|
|
489
|
+
end = graphic_data.get("end", {})
|
|
490
|
+
end_x = end.get("x", 0)
|
|
491
|
+
end_y = end.get("y", 0)
|
|
492
|
+
|
|
493
|
+
# Format coordinates properly (avoid unnecessary .0 for integers)
|
|
494
|
+
if isinstance(end_x, float) and end_x.is_integer():
|
|
495
|
+
end_x = int(end_x)
|
|
496
|
+
if isinstance(end_y, float) and end_y.is_integer():
|
|
497
|
+
end_y = int(end_y)
|
|
498
|
+
|
|
499
|
+
sexp.append([sexpdata.Symbol("end"), end_x, end_y])
|
|
500
|
+
|
|
501
|
+
# Add stroke information (KiCAD format: width, type, and optionally color)
|
|
502
|
+
stroke = graphic_data.get("stroke", {})
|
|
503
|
+
stroke_sexp = [sexpdata.Symbol("stroke")]
|
|
504
|
+
|
|
505
|
+
# Stroke width - default to 0 to match KiCAD behavior
|
|
506
|
+
stroke_width = stroke.get("width", 0)
|
|
507
|
+
if isinstance(stroke_width, float) and stroke_width == 0.0:
|
|
508
|
+
stroke_width = 0
|
|
509
|
+
stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
|
|
510
|
+
|
|
511
|
+
# Stroke type - normalize to KiCAD format and validate
|
|
512
|
+
stroke_type = stroke.get("type", "default")
|
|
513
|
+
|
|
514
|
+
# KiCAD only supports these exact stroke types
|
|
515
|
+
valid_kicad_types = {"solid", "dash", "dash_dot", "dash_dot_dot", "dot", "default"}
|
|
516
|
+
|
|
517
|
+
# Map common variations to KiCAD format
|
|
518
|
+
stroke_type_map = {
|
|
519
|
+
"dashdot": "dash_dot",
|
|
520
|
+
"dash-dot": "dash_dot",
|
|
521
|
+
"dashdotdot": "dash_dot_dot",
|
|
522
|
+
"dash-dot-dot": "dash_dot_dot",
|
|
523
|
+
"solid": "solid",
|
|
524
|
+
"dash": "dash",
|
|
525
|
+
"dot": "dot",
|
|
526
|
+
"default": "default",
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
# Normalize and validate
|
|
530
|
+
normalized_stroke_type = stroke_type_map.get(stroke_type.lower(), stroke_type)
|
|
531
|
+
if normalized_stroke_type not in valid_kicad_types:
|
|
532
|
+
normalized_stroke_type = "default" # Fallback to default for invalid types
|
|
533
|
+
|
|
534
|
+
stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(normalized_stroke_type)])
|
|
535
|
+
|
|
536
|
+
# Stroke color (if specified) - KiCAD format uses RGB 0-255 values plus alpha
|
|
537
|
+
stroke_color = stroke.get("color")
|
|
538
|
+
if stroke_color:
|
|
539
|
+
if isinstance(stroke_color, str):
|
|
540
|
+
# Convert string color names to RGB 0-255 values
|
|
541
|
+
color_rgb = self._color_to_rgb255(stroke_color)
|
|
542
|
+
stroke_sexp.append([sexpdata.Symbol("color")] + color_rgb + [1]) # Add alpha=1
|
|
543
|
+
elif isinstance(stroke_color, (list, tuple)) and len(stroke_color) >= 3:
|
|
544
|
+
# Use provided RGB values directly
|
|
545
|
+
stroke_sexp.append([sexpdata.Symbol("color")] + list(stroke_color))
|
|
546
|
+
|
|
547
|
+
sexp.append(stroke_sexp)
|
|
548
|
+
|
|
549
|
+
# Add fill information
|
|
550
|
+
fill = graphic_data.get("fill", {"type": "none"})
|
|
551
|
+
fill_type = fill.get("type", "none")
|
|
552
|
+
fill_sexp = [sexpdata.Symbol("fill"), [sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)]]
|
|
553
|
+
sexp.append(fill_sexp)
|
|
554
|
+
|
|
555
|
+
# Add UUID (no quotes around UUID in KiCAD format)
|
|
556
|
+
if "uuid" in graphic_data:
|
|
557
|
+
uuid_str = graphic_data["uuid"]
|
|
558
|
+
# Remove quotes and convert to Symbol to match KiCAD format
|
|
559
|
+
uuid_clean = uuid_str.replace('"', "")
|
|
560
|
+
sexp.append([sexpdata.Symbol("uuid"), sexpdata.Symbol(uuid_clean)])
|
|
561
|
+
|
|
562
|
+
return sexp
|
|
563
|
+
|
|
564
|
+
|