kicad-sch-api 0.3.1__py3-none-any.whl → 0.3.4__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.

@@ -187,7 +187,17 @@ class SExpressionParser:
187
187
  "wires": [],
188
188
  "junctions": [],
189
189
  "labels": [],
190
+ "hierarchical_labels": [],
191
+ "no_connects": [],
192
+ "texts": [],
193
+ "text_boxes": [],
194
+ "sheets": [],
195
+ "polylines": [],
196
+ "arcs": [],
197
+ "circles": [],
198
+ "beziers": [],
190
199
  "rectangles": [],
200
+ "images": [],
191
201
  "nets": [],
192
202
  "lib_symbols": {},
193
203
  "sheet_instances": [],
@@ -233,10 +243,50 @@ class SExpressionParser:
233
243
  label = self._parse_label(item)
234
244
  if label:
235
245
  schematic_data["labels"].append(label)
246
+ elif element_type == "hierarchical_label":
247
+ hlabel = self._parse_hierarchical_label(item)
248
+ if hlabel:
249
+ schematic_data["hierarchical_labels"].append(hlabel)
250
+ elif element_type == "no_connect":
251
+ no_connect = self._parse_no_connect(item)
252
+ if no_connect:
253
+ schematic_data["no_connects"].append(no_connect)
254
+ elif element_type == "text":
255
+ text = self._parse_text(item)
256
+ if text:
257
+ schematic_data["texts"].append(text)
258
+ elif element_type == "text_box":
259
+ text_box = self._parse_text_box(item)
260
+ if text_box:
261
+ schematic_data["text_boxes"].append(text_box)
262
+ elif element_type == "sheet":
263
+ sheet = self._parse_sheet(item)
264
+ if sheet:
265
+ schematic_data["sheets"].append(sheet)
266
+ elif element_type == "polyline":
267
+ polyline = self._parse_polyline(item)
268
+ if polyline:
269
+ schematic_data["polylines"].append(polyline)
270
+ elif element_type == "arc":
271
+ arc = self._parse_arc(item)
272
+ if arc:
273
+ schematic_data["arcs"].append(arc)
274
+ elif element_type == "circle":
275
+ circle = self._parse_circle(item)
276
+ if circle:
277
+ schematic_data["circles"].append(circle)
278
+ elif element_type == "bezier":
279
+ bezier = self._parse_bezier(item)
280
+ if bezier:
281
+ schematic_data["beziers"].append(bezier)
236
282
  elif element_type == "rectangle":
237
283
  rectangle = self._parse_rectangle(item)
238
284
  if rectangle:
239
285
  schematic_data["rectangles"].append(rectangle)
286
+ elif element_type == "image":
287
+ image = self._parse_image(item)
288
+ if image:
289
+ schematic_data["images"].append(image)
240
290
  elif element_type == "lib_symbols":
241
291
  schematic_data["lib_symbols"] = self._parse_lib_symbols(item)
242
292
  elif element_type == "sheet_instances":
@@ -297,25 +347,48 @@ class SExpressionParser:
297
347
  for hlabel in schematic_data.get("hierarchical_labels", []):
298
348
  sexp_data.append(self._hierarchical_label_to_sexp(hlabel))
299
349
 
300
- # Add hierarchical sheets
301
- for sheet in schematic_data.get("sheets", []):
302
- sexp_data.append(self._sheet_to_sexp(sheet, schematic_data.get("uuid")))
350
+ # Add no_connects
351
+ for no_connect in schematic_data.get("no_connects", []):
352
+ sexp_data.append(self._no_connect_to_sexp(no_connect))
353
+
354
+ # Add graphical elements (in KiCad element order)
355
+ # Beziers
356
+ for bezier in schematic_data.get("beziers", []):
357
+ sexp_data.append(self._bezier_to_sexp(bezier))
358
+
359
+ # Rectangles (both from API and graphics)
360
+ for rectangle in schematic_data.get("rectangles", []):
361
+ sexp_data.append(self._rectangle_to_sexp(rectangle))
362
+ for graphic in schematic_data.get("graphics", []):
363
+ sexp_data.append(self._graphic_to_sexp(graphic))
364
+
365
+ # Images
366
+ for image in schematic_data.get("images", []):
367
+ sexp_data.append(self._image_to_sexp(image))
368
+
369
+ # Circles
370
+ for circle in schematic_data.get("circles", []):
371
+ sexp_data.append(self._circle_to_sexp(circle))
372
+
373
+ # Arcs
374
+ for arc in schematic_data.get("arcs", []):
375
+ sexp_data.append(self._arc_to_sexp(arc))
376
+
377
+ # Polylines
378
+ for polyline in schematic_data.get("polylines", []):
379
+ sexp_data.append(self._polyline_to_sexp(polyline))
303
380
 
304
- # Add text elements
381
+ # Text elements
305
382
  for text in schematic_data.get("texts", []):
306
383
  sexp_data.append(self._text_to_sexp(text))
307
384
 
308
- # Add text boxes
385
+ # Text boxes
309
386
  for text_box in schematic_data.get("text_boxes", []):
310
387
  sexp_data.append(self._text_box_to_sexp(text_box))
311
388
 
312
- # Add graphics (rectangles from schematic.draw_bounding_box)
313
- for graphic in schematic_data.get("graphics", []):
314
- sexp_data.append(self._graphic_to_sexp(graphic))
315
-
316
- # Add rectangles (rectangles from add_rectangle API)
317
- for rectangle in schematic_data.get("rectangles", []):
318
- sexp_data.append(self._rectangle_to_sexp(rectangle))
389
+ # Hierarchical sheets
390
+ for sheet in schematic_data.get("sheets", []):
391
+ sexp_data.append(self._sheet_to_sexp(sheet, schematic_data.get("uuid")))
319
392
 
320
393
  # Add sheet_instances (required by KiCAD)
321
394
  sheet_instances = schematic_data.get("sheet_instances", [])
@@ -538,6 +611,481 @@ class SExpressionParser:
538
611
 
539
612
  return label_data
540
613
 
614
+ def _parse_hierarchical_label(self, item: List[Any]) -> Optional[Dict[str, Any]]:
615
+ """Parse a hierarchical label definition."""
616
+ # Format: (hierarchical_label "text" (shape input) (at x y rotation) (effects ...) (uuid ...))
617
+ if len(item) < 2:
618
+ return None
619
+
620
+ hlabel_data = {
621
+ "text": str(item[1]), # Hierarchical label text is second element
622
+ "shape": "input", # input/output/bidirectional/tri_state/passive
623
+ "position": {"x": 0, "y": 0},
624
+ "rotation": 0,
625
+ "size": 1.27,
626
+ "justify": "left",
627
+ "uuid": None
628
+ }
629
+
630
+ for elem in item[2:]: # Skip hierarchical_label keyword and text
631
+ if not isinstance(elem, list):
632
+ continue
633
+
634
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
635
+
636
+ if elem_type == "shape":
637
+ # Parse shape: (shape input)
638
+ if len(elem) >= 2:
639
+ hlabel_data["shape"] = str(elem[1])
640
+
641
+ elif elem_type == "at":
642
+ # Parse position: (at x y rotation)
643
+ if len(elem) >= 3:
644
+ hlabel_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
645
+ if len(elem) >= 4:
646
+ hlabel_data["rotation"] = float(elem[3])
647
+
648
+ elif elem_type == "effects":
649
+ # Parse effects for font size and justification: (effects (font (size x y)) (justify left))
650
+ for effect_elem in elem[1:]:
651
+ if isinstance(effect_elem, list):
652
+ effect_type = str(effect_elem[0]) if isinstance(effect_elem[0], sexpdata.Symbol) else None
653
+
654
+ if effect_type == "font":
655
+ # Parse font size
656
+ for font_elem in effect_elem[1:]:
657
+ if isinstance(font_elem, list) and str(font_elem[0]) == "size":
658
+ if len(font_elem) >= 2:
659
+ hlabel_data["size"] = float(font_elem[1])
660
+
661
+ elif effect_type == "justify":
662
+ # Parse justification (e.g., "left", "right")
663
+ if len(effect_elem) >= 2:
664
+ hlabel_data["justify"] = str(effect_elem[1])
665
+
666
+ elif elem_type == "uuid":
667
+ hlabel_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
668
+
669
+ return hlabel_data
670
+
671
+ def _parse_no_connect(self, item: List[Any]) -> Optional[Dict[str, Any]]:
672
+ """Parse a no_connect symbol."""
673
+ # Format: (no_connect (at x y) (uuid ...))
674
+ no_connect_data = {
675
+ "position": {"x": 0, "y": 0},
676
+ "uuid": None
677
+ }
678
+
679
+ for elem in item[1:]:
680
+ if not isinstance(elem, list):
681
+ continue
682
+
683
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
684
+
685
+ if elem_type == "at":
686
+ if len(elem) >= 3:
687
+ no_connect_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
688
+ elif elem_type == "uuid":
689
+ no_connect_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
690
+
691
+ return no_connect_data
692
+
693
+ def _parse_text(self, item: List[Any]) -> Optional[Dict[str, Any]]:
694
+ """Parse a text element."""
695
+ # Format: (text "text" (exclude_from_sim no) (at x y rotation) (effects ...) (uuid ...))
696
+ if len(item) < 2:
697
+ return None
698
+
699
+ text_data = {
700
+ "text": str(item[1]),
701
+ "exclude_from_sim": False,
702
+ "position": {"x": 0, "y": 0},
703
+ "rotation": 0,
704
+ "size": 1.27,
705
+ "uuid": None
706
+ }
707
+
708
+ for elem in item[2:]:
709
+ if not isinstance(elem, list):
710
+ continue
711
+
712
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
713
+
714
+ if elem_type == "exclude_from_sim":
715
+ if len(elem) >= 2:
716
+ text_data["exclude_from_sim"] = str(elem[1]) == "yes"
717
+ elif elem_type == "at":
718
+ if len(elem) >= 3:
719
+ text_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
720
+ if len(elem) >= 4:
721
+ text_data["rotation"] = float(elem[3])
722
+ elif elem_type == "effects":
723
+ for effect_elem in elem[1:]:
724
+ if isinstance(effect_elem, list) and str(effect_elem[0]) == "font":
725
+ for font_elem in effect_elem[1:]:
726
+ if isinstance(font_elem, list) and str(font_elem[0]) == "size":
727
+ if len(font_elem) >= 2:
728
+ text_data["size"] = float(font_elem[1])
729
+ elif elem_type == "uuid":
730
+ text_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
731
+
732
+ return text_data
733
+
734
+ def _parse_text_box(self, item: List[Any]) -> Optional[Dict[str, Any]]:
735
+ """Parse a text_box element."""
736
+ # Format: (text_box "text" (exclude_from_sim no) (at x y rotation) (size w h) (margins ...) (stroke ...) (fill ...) (effects ...) (uuid ...))
737
+ if len(item) < 2:
738
+ return None
739
+
740
+ text_box_data = {
741
+ "text": str(item[1]),
742
+ "exclude_from_sim": False,
743
+ "position": {"x": 0, "y": 0},
744
+ "rotation": 0,
745
+ "size": {"width": 0, "height": 0},
746
+ "margins": (0.9525, 0.9525, 0.9525, 0.9525),
747
+ "stroke_width": 0,
748
+ "stroke_type": "solid",
749
+ "fill_type": "none",
750
+ "font_size": 1.27,
751
+ "justify_horizontal": "left",
752
+ "justify_vertical": "top",
753
+ "uuid": None
754
+ }
755
+
756
+ for elem in item[2:]:
757
+ if not isinstance(elem, list):
758
+ continue
759
+
760
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
761
+
762
+ if elem_type == "exclude_from_sim":
763
+ if len(elem) >= 2:
764
+ text_box_data["exclude_from_sim"] = str(elem[1]) == "yes"
765
+ elif elem_type == "at":
766
+ if len(elem) >= 3:
767
+ text_box_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
768
+ if len(elem) >= 4:
769
+ text_box_data["rotation"] = float(elem[3])
770
+ elif elem_type == "size":
771
+ if len(elem) >= 3:
772
+ text_box_data["size"] = {"width": float(elem[1]), "height": float(elem[2])}
773
+ elif elem_type == "margins":
774
+ if len(elem) >= 5:
775
+ text_box_data["margins"] = (float(elem[1]), float(elem[2]), float(elem[3]), float(elem[4]))
776
+ elif elem_type == "stroke":
777
+ for stroke_elem in elem[1:]:
778
+ if isinstance(stroke_elem, list):
779
+ stroke_type = str(stroke_elem[0])
780
+ if stroke_type == "width" and len(stroke_elem) >= 2:
781
+ text_box_data["stroke_width"] = float(stroke_elem[1])
782
+ elif stroke_type == "type" and len(stroke_elem) >= 2:
783
+ text_box_data["stroke_type"] = str(stroke_elem[1])
784
+ elif elem_type == "fill":
785
+ for fill_elem in elem[1:]:
786
+ if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
787
+ text_box_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
788
+ elif elem_type == "effects":
789
+ for effect_elem in elem[1:]:
790
+ if isinstance(effect_elem, list):
791
+ effect_type = str(effect_elem[0])
792
+ if effect_type == "font":
793
+ for font_elem in effect_elem[1:]:
794
+ if isinstance(font_elem, list) and str(font_elem[0]) == "size":
795
+ if len(font_elem) >= 2:
796
+ text_box_data["font_size"] = float(font_elem[1])
797
+ elif effect_type == "justify":
798
+ if len(effect_elem) >= 2:
799
+ text_box_data["justify_horizontal"] = str(effect_elem[1])
800
+ if len(effect_elem) >= 3:
801
+ text_box_data["justify_vertical"] = str(effect_elem[2])
802
+ elif elem_type == "uuid":
803
+ text_box_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
804
+
805
+ return text_box_data
806
+
807
+ def _parse_sheet(self, item: List[Any]) -> Optional[Dict[str, Any]]:
808
+ """Parse a hierarchical sheet."""
809
+ # Complex format with position, size, properties, pins, instances
810
+ sheet_data = {
811
+ "position": {"x": 0, "y": 0},
812
+ "size": {"width": 0, "height": 0},
813
+ "exclude_from_sim": False,
814
+ "in_bom": True,
815
+ "on_board": True,
816
+ "dnp": False,
817
+ "fields_autoplaced": True,
818
+ "stroke_width": 0.1524,
819
+ "stroke_type": "solid",
820
+ "fill_color": (0, 0, 0, 0.0),
821
+ "uuid": None,
822
+ "name": "Sheet",
823
+ "filename": "sheet.kicad_sch",
824
+ "pins": [],
825
+ "project_name": "",
826
+ "page_number": "2"
827
+ }
828
+
829
+ for elem in item[1:]:
830
+ if not isinstance(elem, list):
831
+ continue
832
+
833
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
834
+
835
+ if elem_type == "at":
836
+ if len(elem) >= 3:
837
+ sheet_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
838
+ elif elem_type == "size":
839
+ if len(elem) >= 3:
840
+ sheet_data["size"] = {"width": float(elem[1]), "height": float(elem[2])}
841
+ elif elem_type == "exclude_from_sim":
842
+ sheet_data["exclude_from_sim"] = str(elem[1]) == "yes" if len(elem) > 1 else False
843
+ elif elem_type == "in_bom":
844
+ sheet_data["in_bom"] = str(elem[1]) == "yes" if len(elem) > 1 else True
845
+ elif elem_type == "on_board":
846
+ sheet_data["on_board"] = str(elem[1]) == "yes" if len(elem) > 1 else True
847
+ elif elem_type == "dnp":
848
+ sheet_data["dnp"] = str(elem[1]) == "yes" if len(elem) > 1 else False
849
+ elif elem_type == "fields_autoplaced":
850
+ sheet_data["fields_autoplaced"] = str(elem[1]) == "yes" if len(elem) > 1 else True
851
+ elif elem_type == "stroke":
852
+ for stroke_elem in elem[1:]:
853
+ if isinstance(stroke_elem, list):
854
+ stroke_type = str(stroke_elem[0])
855
+ if stroke_type == "width" and len(stroke_elem) >= 2:
856
+ sheet_data["stroke_width"] = float(stroke_elem[1])
857
+ elif stroke_type == "type" and len(stroke_elem) >= 2:
858
+ sheet_data["stroke_type"] = str(stroke_elem[1])
859
+ elif elem_type == "fill":
860
+ for fill_elem in elem[1:]:
861
+ if isinstance(fill_elem, list) and str(fill_elem[0]) == "color":
862
+ if len(fill_elem) >= 5:
863
+ sheet_data["fill_color"] = (int(fill_elem[1]), int(fill_elem[2]), int(fill_elem[3]), float(fill_elem[4]))
864
+ elif elem_type == "uuid":
865
+ sheet_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
866
+ elif elem_type == "property":
867
+ if len(elem) >= 3:
868
+ prop_name = str(elem[1])
869
+ prop_value = str(elem[2])
870
+ if prop_name == "Sheetname":
871
+ sheet_data["name"] = prop_value
872
+ elif prop_name == "Sheetfile":
873
+ sheet_data["filename"] = prop_value
874
+ elif elem_type == "pin":
875
+ # Parse sheet pin - reuse existing _parse_sheet_pin helper
876
+ pin_data = self._parse_sheet_pin_for_read(elem)
877
+ if pin_data:
878
+ sheet_data["pins"].append(pin_data)
879
+ elif elem_type == "instances":
880
+ # Parse instances for project name and page number
881
+ for inst_elem in elem[1:]:
882
+ if isinstance(inst_elem, list) and str(inst_elem[0]) == "project":
883
+ if len(inst_elem) >= 2:
884
+ sheet_data["project_name"] = str(inst_elem[1])
885
+ for path_elem in inst_elem[2:]:
886
+ if isinstance(path_elem, list) and str(path_elem[0]) == "path":
887
+ for page_elem in path_elem[1:]:
888
+ if isinstance(page_elem, list) and str(page_elem[0]) == "page":
889
+ sheet_data["page_number"] = str(page_elem[1]) if len(page_elem) > 1 else "2"
890
+
891
+ return sheet_data
892
+
893
+ def _parse_sheet_pin_for_read(self, item: List[Any]) -> Optional[Dict[str, Any]]:
894
+ """Parse a sheet pin (for reading during sheet parsing)."""
895
+ # Format: (pin "name" type (at x y rotation) (uuid ...) (effects ...))
896
+ if len(item) < 3:
897
+ return None
898
+
899
+ pin_data = {
900
+ "name": str(item[1]),
901
+ "pin_type": str(item[2]) if len(item) > 2 else "input",
902
+ "position": {"x": 0, "y": 0},
903
+ "rotation": 0,
904
+ "size": 1.27,
905
+ "justify": "right",
906
+ "uuid": None
907
+ }
908
+
909
+ for elem in item[3:]:
910
+ if not isinstance(elem, list):
911
+ continue
912
+
913
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
914
+
915
+ if elem_type == "at":
916
+ if len(elem) >= 3:
917
+ pin_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
918
+ if len(elem) >= 4:
919
+ pin_data["rotation"] = float(elem[3])
920
+ elif elem_type == "uuid":
921
+ pin_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
922
+ elif elem_type == "effects":
923
+ for effect_elem in elem[1:]:
924
+ if isinstance(effect_elem, list):
925
+ effect_type = str(effect_elem[0])
926
+ if effect_type == "font":
927
+ for font_elem in effect_elem[1:]:
928
+ if isinstance(font_elem, list) and str(font_elem[0]) == "size":
929
+ if len(font_elem) >= 2:
930
+ pin_data["size"] = float(font_elem[1])
931
+ elif effect_type == "justify":
932
+ if len(effect_elem) >= 2:
933
+ pin_data["justify"] = str(effect_elem[1])
934
+
935
+ return pin_data
936
+
937
+ def _parse_polyline(self, item: List[Any]) -> Optional[Dict[str, Any]]:
938
+ """Parse a polyline graphical element."""
939
+ # Format: (polyline (pts (xy x1 y1) (xy x2 y2) ...) (stroke ...) (uuid ...))
940
+ polyline_data = {
941
+ "points": [],
942
+ "stroke_width": 0,
943
+ "stroke_type": "default",
944
+ "uuid": None
945
+ }
946
+
947
+ for elem in item[1:]:
948
+ if not isinstance(elem, list):
949
+ continue
950
+
951
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
952
+
953
+ if elem_type == "pts":
954
+ for pt in elem[1:]:
955
+ if isinstance(pt, list) and len(pt) >= 3 and str(pt[0]) == "xy":
956
+ polyline_data["points"].append({"x": float(pt[1]), "y": float(pt[2])})
957
+ elif elem_type == "stroke":
958
+ for stroke_elem in elem[1:]:
959
+ if isinstance(stroke_elem, list):
960
+ stroke_type = str(stroke_elem[0])
961
+ if stroke_type == "width" and len(stroke_elem) >= 2:
962
+ polyline_data["stroke_width"] = float(stroke_elem[1])
963
+ elif stroke_type == "type" and len(stroke_elem) >= 2:
964
+ polyline_data["stroke_type"] = str(stroke_elem[1])
965
+ elif elem_type == "uuid":
966
+ polyline_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
967
+
968
+ return polyline_data if polyline_data["points"] else None
969
+
970
+ def _parse_arc(self, item: List[Any]) -> Optional[Dict[str, Any]]:
971
+ """Parse an arc graphical element."""
972
+ # Format: (arc (start x y) (mid x y) (end x y) (stroke ...) (fill ...) (uuid ...))
973
+ arc_data = {
974
+ "start": {"x": 0, "y": 0},
975
+ "mid": {"x": 0, "y": 0},
976
+ "end": {"x": 0, "y": 0},
977
+ "stroke_width": 0,
978
+ "stroke_type": "default",
979
+ "fill_type": "none",
980
+ "uuid": None
981
+ }
982
+
983
+ for elem in item[1:]:
984
+ if not isinstance(elem, list):
985
+ continue
986
+
987
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
988
+
989
+ if elem_type == "start" and len(elem) >= 3:
990
+ arc_data["start"] = {"x": float(elem[1]), "y": float(elem[2])}
991
+ elif elem_type == "mid" and len(elem) >= 3:
992
+ arc_data["mid"] = {"x": float(elem[1]), "y": float(elem[2])}
993
+ elif elem_type == "end" and len(elem) >= 3:
994
+ arc_data["end"] = {"x": float(elem[1]), "y": float(elem[2])}
995
+ elif elem_type == "stroke":
996
+ for stroke_elem in elem[1:]:
997
+ if isinstance(stroke_elem, list):
998
+ stroke_type = str(stroke_elem[0])
999
+ if stroke_type == "width" and len(stroke_elem) >= 2:
1000
+ arc_data["stroke_width"] = float(stroke_elem[1])
1001
+ elif stroke_type == "type" and len(stroke_elem) >= 2:
1002
+ arc_data["stroke_type"] = str(stroke_elem[1])
1003
+ elif elem_type == "fill":
1004
+ for fill_elem in elem[1:]:
1005
+ if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
1006
+ arc_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
1007
+ elif elem_type == "uuid":
1008
+ arc_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
1009
+
1010
+ return arc_data
1011
+
1012
+ def _parse_circle(self, item: List[Any]) -> Optional[Dict[str, Any]]:
1013
+ """Parse a circle graphical element."""
1014
+ # Format: (circle (center x y) (radius r) (stroke ...) (fill ...) (uuid ...))
1015
+ circle_data = {
1016
+ "center": {"x": 0, "y": 0},
1017
+ "radius": 0,
1018
+ "stroke_width": 0,
1019
+ "stroke_type": "default",
1020
+ "fill_type": "none",
1021
+ "uuid": None
1022
+ }
1023
+
1024
+ for elem in item[1:]:
1025
+ if not isinstance(elem, list):
1026
+ continue
1027
+
1028
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
1029
+
1030
+ if elem_type == "center" and len(elem) >= 3:
1031
+ circle_data["center"] = {"x": float(elem[1]), "y": float(elem[2])}
1032
+ elif elem_type == "radius" and len(elem) >= 2:
1033
+ circle_data["radius"] = float(elem[1])
1034
+ elif elem_type == "stroke":
1035
+ for stroke_elem in elem[1:]:
1036
+ if isinstance(stroke_elem, list):
1037
+ stroke_type = str(stroke_elem[0])
1038
+ if stroke_type == "width" and len(stroke_elem) >= 2:
1039
+ circle_data["stroke_width"] = float(stroke_elem[1])
1040
+ elif stroke_type == "type" and len(stroke_elem) >= 2:
1041
+ circle_data["stroke_type"] = str(stroke_elem[1])
1042
+ elif elem_type == "fill":
1043
+ for fill_elem in elem[1:]:
1044
+ if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
1045
+ circle_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
1046
+ elif elem_type == "uuid":
1047
+ circle_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
1048
+
1049
+ return circle_data
1050
+
1051
+ def _parse_bezier(self, item: List[Any]) -> Optional[Dict[str, Any]]:
1052
+ """Parse a bezier curve graphical element."""
1053
+ # Format: (bezier (pts (xy x1 y1) (xy x2 y2) ...) (stroke ...) (fill ...) (uuid ...))
1054
+ bezier_data = {
1055
+ "points": [],
1056
+ "stroke_width": 0,
1057
+ "stroke_type": "default",
1058
+ "fill_type": "none",
1059
+ "uuid": None
1060
+ }
1061
+
1062
+ for elem in item[1:]:
1063
+ if not isinstance(elem, list):
1064
+ continue
1065
+
1066
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
1067
+
1068
+ if elem_type == "pts":
1069
+ for pt in elem[1:]:
1070
+ if isinstance(pt, list) and len(pt) >= 3 and str(pt[0]) == "xy":
1071
+ bezier_data["points"].append({"x": float(pt[1]), "y": float(pt[2])})
1072
+ elif elem_type == "stroke":
1073
+ for stroke_elem in elem[1:]:
1074
+ if isinstance(stroke_elem, list):
1075
+ stroke_type = str(stroke_elem[0])
1076
+ if stroke_type == "width" and len(stroke_elem) >= 2:
1077
+ bezier_data["stroke_width"] = float(stroke_elem[1])
1078
+ elif stroke_type == "type" and len(stroke_elem) >= 2:
1079
+ bezier_data["stroke_type"] = str(stroke_elem[1])
1080
+ elif elem_type == "fill":
1081
+ for fill_elem in elem[1:]:
1082
+ if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
1083
+ bezier_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
1084
+ elif elem_type == "uuid":
1085
+ bezier_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
1086
+
1087
+ return bezier_data if bezier_data["points"] else None
1088
+
541
1089
  def _parse_rectangle(self, item: List[Any]) -> Optional[Dict[str, Any]]:
542
1090
  """Parse a rectangle graphical element."""
543
1091
  rectangle = {}
@@ -569,6 +1117,37 @@ class SExpressionParser:
569
1117
 
570
1118
  return rectangle if rectangle else None
571
1119
 
1120
+ def _parse_image(self, item: List[Any]) -> Optional[Dict[str, Any]]:
1121
+ """Parse an image element."""
1122
+ # Format: (image (at x y) (uuid "...") (data "base64..."))
1123
+ image = {
1124
+ "position": {"x": 0, "y": 0},
1125
+ "data": "",
1126
+ "scale": 1.0,
1127
+ "uuid": None
1128
+ }
1129
+
1130
+ for elem in item[1:]:
1131
+ if not isinstance(elem, list):
1132
+ continue
1133
+
1134
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
1135
+
1136
+ if elem_type == "at" and len(elem) >= 3:
1137
+ image["position"] = {"x": float(elem[1]), "y": float(elem[2])}
1138
+ elif elem_type == "scale" and len(elem) >= 2:
1139
+ image["scale"] = float(elem[1])
1140
+ elif elem_type == "data" and len(elem) >= 2:
1141
+ # The data can be spread across multiple string elements
1142
+ data_parts = []
1143
+ for data_elem in elem[1:]:
1144
+ data_parts.append(str(data_elem).strip('"'))
1145
+ image["data"] = "".join(data_parts)
1146
+ elif elem_type == "uuid" and len(elem) >= 2:
1147
+ image["uuid"] = str(elem[1]).strip('"')
1148
+
1149
+ return image if image.get("uuid") and image.get("data") else None
1150
+
572
1151
  def _parse_lib_symbols(self, item: List[Any]) -> Dict[str, Any]:
573
1152
  """Parse lib_symbols section."""
574
1153
  # Implementation for lib_symbols parsing
@@ -942,6 +1521,171 @@ class SExpressionParser:
942
1521
 
943
1522
  return sexp
944
1523
 
1524
+ def _no_connect_to_sexp(self, no_connect_data: Dict[str, Any]) -> List[Any]:
1525
+ """Convert no_connect to S-expression."""
1526
+ sexp = [sexpdata.Symbol("no_connect")]
1527
+
1528
+ # Add position
1529
+ pos = no_connect_data["position"]
1530
+ x, y = pos["x"], pos["y"]
1531
+
1532
+ # Format coordinates properly
1533
+ if isinstance(x, float) and x.is_integer():
1534
+ x = int(x)
1535
+ if isinstance(y, float) and y.is_integer():
1536
+ y = int(y)
1537
+
1538
+ sexp.append([sexpdata.Symbol("at"), x, y])
1539
+
1540
+ # Add UUID
1541
+ if "uuid" in no_connect_data:
1542
+ sexp.append([sexpdata.Symbol("uuid"), no_connect_data["uuid"]])
1543
+
1544
+ return sexp
1545
+
1546
+ def _polyline_to_sexp(self, polyline_data: Dict[str, Any]) -> List[Any]:
1547
+ """Convert polyline to S-expression."""
1548
+ sexp = [sexpdata.Symbol("polyline")]
1549
+
1550
+ # Add points
1551
+ points = polyline_data.get("points", [])
1552
+ if points:
1553
+ pts_sexp = [sexpdata.Symbol("pts")]
1554
+ for point in points:
1555
+ x, y = point["x"], point["y"]
1556
+ # Format coordinates properly
1557
+ if isinstance(x, float) and x.is_integer():
1558
+ x = int(x)
1559
+ if isinstance(y, float) and y.is_integer():
1560
+ y = int(y)
1561
+ pts_sexp.append([sexpdata.Symbol("xy"), x, y])
1562
+ sexp.append(pts_sexp)
1563
+
1564
+ # Add stroke
1565
+ stroke_width = polyline_data.get("stroke_width", 0)
1566
+ stroke_type = polyline_data.get("stroke_type", "default")
1567
+ stroke_sexp = [sexpdata.Symbol("stroke")]
1568
+ stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
1569
+ stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
1570
+ sexp.append(stroke_sexp)
1571
+
1572
+ # Add UUID
1573
+ if "uuid" in polyline_data:
1574
+ sexp.append([sexpdata.Symbol("uuid"), polyline_data["uuid"]])
1575
+
1576
+ return sexp
1577
+
1578
+ def _arc_to_sexp(self, arc_data: Dict[str, Any]) -> List[Any]:
1579
+ """Convert arc to S-expression."""
1580
+ sexp = [sexpdata.Symbol("arc")]
1581
+
1582
+ # Add start, mid, end points
1583
+ for point_name in ["start", "mid", "end"]:
1584
+ point = arc_data.get(point_name, {"x": 0, "y": 0})
1585
+ x, y = point["x"], point["y"]
1586
+ # Format coordinates properly
1587
+ if isinstance(x, float) and x.is_integer():
1588
+ x = int(x)
1589
+ if isinstance(y, float) and y.is_integer():
1590
+ y = int(y)
1591
+ sexp.append([sexpdata.Symbol(point_name), x, y])
1592
+
1593
+ # Add stroke
1594
+ stroke_width = arc_data.get("stroke_width", 0)
1595
+ stroke_type = arc_data.get("stroke_type", "default")
1596
+ stroke_sexp = [sexpdata.Symbol("stroke")]
1597
+ stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
1598
+ stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
1599
+ sexp.append(stroke_sexp)
1600
+
1601
+ # Add fill
1602
+ fill_type = arc_data.get("fill_type", "none")
1603
+ fill_sexp = [sexpdata.Symbol("fill")]
1604
+ fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
1605
+ sexp.append(fill_sexp)
1606
+
1607
+ # Add UUID
1608
+ if "uuid" in arc_data:
1609
+ sexp.append([sexpdata.Symbol("uuid"), arc_data["uuid"]])
1610
+
1611
+ return sexp
1612
+
1613
+ def _circle_to_sexp(self, circle_data: Dict[str, Any]) -> List[Any]:
1614
+ """Convert circle to S-expression."""
1615
+ sexp = [sexpdata.Symbol("circle")]
1616
+
1617
+ # Add center
1618
+ center = circle_data.get("center", {"x": 0, "y": 0})
1619
+ x, y = center["x"], center["y"]
1620
+ # Format coordinates properly
1621
+ if isinstance(x, float) and x.is_integer():
1622
+ x = int(x)
1623
+ if isinstance(y, float) and y.is_integer():
1624
+ y = int(y)
1625
+ sexp.append([sexpdata.Symbol("center"), x, y])
1626
+
1627
+ # Add radius
1628
+ radius = circle_data.get("radius", 0)
1629
+ sexp.append([sexpdata.Symbol("radius"), radius])
1630
+
1631
+ # Add stroke
1632
+ stroke_width = circle_data.get("stroke_width", 0)
1633
+ stroke_type = circle_data.get("stroke_type", "default")
1634
+ stroke_sexp = [sexpdata.Symbol("stroke")]
1635
+ stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
1636
+ stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
1637
+ sexp.append(stroke_sexp)
1638
+
1639
+ # Add fill
1640
+ fill_type = circle_data.get("fill_type", "none")
1641
+ fill_sexp = [sexpdata.Symbol("fill")]
1642
+ fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
1643
+ sexp.append(fill_sexp)
1644
+
1645
+ # Add UUID
1646
+ if "uuid" in circle_data:
1647
+ sexp.append([sexpdata.Symbol("uuid"), circle_data["uuid"]])
1648
+
1649
+ return sexp
1650
+
1651
+ def _bezier_to_sexp(self, bezier_data: Dict[str, Any]) -> List[Any]:
1652
+ """Convert bezier curve to S-expression."""
1653
+ sexp = [sexpdata.Symbol("bezier")]
1654
+
1655
+ # Add points
1656
+ points = bezier_data.get("points", [])
1657
+ if points:
1658
+ pts_sexp = [sexpdata.Symbol("pts")]
1659
+ for point in points:
1660
+ x, y = point["x"], point["y"]
1661
+ # Format coordinates properly
1662
+ if isinstance(x, float) and x.is_integer():
1663
+ x = int(x)
1664
+ if isinstance(y, float) and y.is_integer():
1665
+ y = int(y)
1666
+ pts_sexp.append([sexpdata.Symbol("xy"), x, y])
1667
+ sexp.append(pts_sexp)
1668
+
1669
+ # Add stroke
1670
+ stroke_width = bezier_data.get("stroke_width", 0)
1671
+ stroke_type = bezier_data.get("stroke_type", "default")
1672
+ stroke_sexp = [sexpdata.Symbol("stroke")]
1673
+ stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
1674
+ stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
1675
+ sexp.append(stroke_sexp)
1676
+
1677
+ # Add fill
1678
+ fill_type = bezier_data.get("fill_type", "none")
1679
+ fill_sexp = [sexpdata.Symbol("fill")]
1680
+ fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
1681
+ sexp.append(fill_sexp)
1682
+
1683
+ # Add UUID
1684
+ if "uuid" in bezier_data:
1685
+ sexp.append([sexpdata.Symbol("uuid"), bezier_data["uuid"]])
1686
+
1687
+ return sexp
1688
+
945
1689
  def _sheet_to_sexp(self, sheet_data: Dict[str, Any], schematic_uuid: str) -> List[Any]:
946
1690
  """Convert hierarchical sheet to S-expression."""
947
1691
  sexp = [sexpdata.Symbol("sheet")]
@@ -1229,6 +1973,38 @@ class SExpressionParser:
1229
1973
 
1230
1974
  return sexp
1231
1975
 
1976
+ def _image_to_sexp(self, image_data: Dict[str, Any]) -> List[Any]:
1977
+ """Convert image element to S-expression."""
1978
+ sexp = [sexpdata.Symbol("image")]
1979
+
1980
+ # Add position
1981
+ position = image_data.get("position", {"x": 0, "y": 0})
1982
+ pos_x, pos_y = position["x"], position["y"]
1983
+ sexp.append([sexpdata.Symbol("at"), pos_x, pos_y])
1984
+
1985
+ # Add UUID
1986
+ if "uuid" in image_data:
1987
+ sexp.append([sexpdata.Symbol("uuid"), image_data["uuid"]])
1988
+
1989
+ # Add scale if not default
1990
+ scale = image_data.get("scale", 1.0)
1991
+ if scale != 1.0:
1992
+ sexp.append([sexpdata.Symbol("scale"), scale])
1993
+
1994
+ # Add image data
1995
+ # KiCad splits base64 data into multiple lines for readability
1996
+ # Each line is roughly 76 characters (standard base64 line length)
1997
+ data = image_data.get("data", "")
1998
+ if data:
1999
+ data_sexp = [sexpdata.Symbol("data")]
2000
+ # Split the data into 76-character chunks
2001
+ chunk_size = 76
2002
+ for i in range(0, len(data), chunk_size):
2003
+ data_sexp.append(data[i:i+chunk_size])
2004
+ sexp.append(data_sexp)
2005
+
2006
+ return sexp
2007
+
1232
2008
  def _lib_symbols_to_sexp(self, lib_symbols: Dict[str, Any]) -> List[Any]:
1233
2009
  """Convert lib_symbols to S-expression."""
1234
2010
  sexp = [sexpdata.Symbol("lib_symbols")]