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.

Files changed (57) hide show
  1. kicad_sch_api/__init__.py +2 -2
  2. kicad_sch_api/cli/__init__.py +45 -0
  3. kicad_sch_api/cli/base.py +302 -0
  4. kicad_sch_api/cli/bom.py +164 -0
  5. kicad_sch_api/cli/erc.py +229 -0
  6. kicad_sch_api/cli/export_docs.py +289 -0
  7. kicad_sch_api/cli/netlist.py +94 -0
  8. kicad_sch_api/cli/types.py +43 -0
  9. kicad_sch_api/core/collections/__init__.py +5 -0
  10. kicad_sch_api/core/collections/base.py +248 -0
  11. kicad_sch_api/core/component_bounds.py +5 -0
  12. kicad_sch_api/core/components.py +142 -47
  13. kicad_sch_api/core/config.py +85 -3
  14. kicad_sch_api/core/factories/__init__.py +5 -0
  15. kicad_sch_api/core/factories/element_factory.py +276 -0
  16. kicad_sch_api/core/formatter.py +22 -5
  17. kicad_sch_api/core/junctions.py +26 -75
  18. kicad_sch_api/core/labels.py +28 -52
  19. kicad_sch_api/core/managers/file_io.py +3 -2
  20. kicad_sch_api/core/managers/metadata.py +6 -5
  21. kicad_sch_api/core/managers/validation.py +3 -2
  22. kicad_sch_api/core/managers/wire.py +7 -1
  23. kicad_sch_api/core/nets.py +38 -43
  24. kicad_sch_api/core/no_connects.py +29 -53
  25. kicad_sch_api/core/parser.py +75 -1765
  26. kicad_sch_api/core/schematic.py +211 -148
  27. kicad_sch_api/core/texts.py +28 -55
  28. kicad_sch_api/core/types.py +59 -18
  29. kicad_sch_api/core/wires.py +27 -75
  30. kicad_sch_api/parsers/elements/__init__.py +22 -0
  31. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  32. kicad_sch_api/parsers/elements/label_parser.py +194 -0
  33. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  34. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  35. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  36. kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
  37. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  38. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  39. kicad_sch_api/parsers/utils.py +80 -0
  40. kicad_sch_api/validation/__init__.py +25 -0
  41. kicad_sch_api/validation/erc.py +171 -0
  42. kicad_sch_api/validation/erc_models.py +203 -0
  43. kicad_sch_api/validation/pin_matrix.py +243 -0
  44. kicad_sch_api/validation/validators.py +391 -0
  45. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/METADATA +17 -9
  46. kicad_sch_api-0.4.2.dist-info/RECORD +87 -0
  47. kicad_sch_api/core/manhattan_routing.py +0 -430
  48. kicad_sch_api/core/simple_manhattan.py +0 -228
  49. kicad_sch_api/core/wire_routing.py +0 -380
  50. kicad_sch_api/parsers/label_parser.py +0 -254
  51. kicad_sch_api/parsers/symbol_parser.py +0 -222
  52. kicad_sch_api/parsers/wire_parser.py +0 -99
  53. kicad_sch_api-0.4.0.dist-info/RECORD +0 -67
  54. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/WHEEL +0 -0
  55. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/entry_points.txt +0 -0
  56. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/licenses/LICENSE +0 -0
  57. {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
+