kicad-sch-api 0.3.1__py3-none-any.whl → 0.3.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 CHANGED
@@ -42,7 +42,7 @@ Advanced Usage:
42
42
  print(f"Found {len(issues)} validation issues")
43
43
  """
44
44
 
45
- __version__ = "0.3.1"
45
+ __version__ = "0.3.2"
46
46
  __author__ = "Circuit-Synth"
47
47
  __email__ = "info@circuit-synth.com"
48
48
 
@@ -55,7 +55,7 @@ from .library.cache import SymbolLibraryCache, get_symbol_cache
55
55
  from .utils.validation import ValidationError, ValidationIssue
56
56
 
57
57
  # Version info
58
- VERSION_INFO = (0, 3, 1)
58
+ VERSION_INFO = (0, 3, 2)
59
59
 
60
60
  # Public API
61
61
  __all__ = [
@@ -189,7 +189,8 @@ class ExactFormatter:
189
189
  elif isinstance(element, str):
190
190
  # Quote strings that need quoting
191
191
  if self._needs_quoting(element):
192
- return f'"{element}"'
192
+ escaped = self._escape_string(element)
193
+ return f'"{escaped}"'
193
194
  return element
194
195
  elif isinstance(element, float):
195
196
  # Custom float formatting for KiCAD compatibility
@@ -187,6 +187,15 @@ 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": [],
191
200
  "nets": [],
192
201
  "lib_symbols": {},
@@ -233,6 +242,42 @@ class SExpressionParser:
233
242
  label = self._parse_label(item)
234
243
  if label:
235
244
  schematic_data["labels"].append(label)
245
+ elif element_type == "hierarchical_label":
246
+ hlabel = self._parse_hierarchical_label(item)
247
+ if hlabel:
248
+ schematic_data["hierarchical_labels"].append(hlabel)
249
+ elif element_type == "no_connect":
250
+ no_connect = self._parse_no_connect(item)
251
+ if no_connect:
252
+ schematic_data["no_connects"].append(no_connect)
253
+ elif element_type == "text":
254
+ text = self._parse_text(item)
255
+ if text:
256
+ schematic_data["texts"].append(text)
257
+ elif element_type == "text_box":
258
+ text_box = self._parse_text_box(item)
259
+ if text_box:
260
+ schematic_data["text_boxes"].append(text_box)
261
+ elif element_type == "sheet":
262
+ sheet = self._parse_sheet(item)
263
+ if sheet:
264
+ schematic_data["sheets"].append(sheet)
265
+ elif element_type == "polyline":
266
+ polyline = self._parse_polyline(item)
267
+ if polyline:
268
+ schematic_data["polylines"].append(polyline)
269
+ elif element_type == "arc":
270
+ arc = self._parse_arc(item)
271
+ if arc:
272
+ schematic_data["arcs"].append(arc)
273
+ elif element_type == "circle":
274
+ circle = self._parse_circle(item)
275
+ if circle:
276
+ schematic_data["circles"].append(circle)
277
+ elif element_type == "bezier":
278
+ bezier = self._parse_bezier(item)
279
+ if bezier:
280
+ schematic_data["beziers"].append(bezier)
236
281
  elif element_type == "rectangle":
237
282
  rectangle = self._parse_rectangle(item)
238
283
  if rectangle:
@@ -297,25 +342,44 @@ class SExpressionParser:
297
342
  for hlabel in schematic_data.get("hierarchical_labels", []):
298
343
  sexp_data.append(self._hierarchical_label_to_sexp(hlabel))
299
344
 
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")))
345
+ # Add no_connects
346
+ for no_connect in schematic_data.get("no_connects", []):
347
+ sexp_data.append(self._no_connect_to_sexp(no_connect))
348
+
349
+ # Add graphical elements (in KiCad element order)
350
+ # Beziers
351
+ for bezier in schematic_data.get("beziers", []):
352
+ sexp_data.append(self._bezier_to_sexp(bezier))
353
+
354
+ # Rectangles (both from API and graphics)
355
+ for rectangle in schematic_data.get("rectangles", []):
356
+ sexp_data.append(self._rectangle_to_sexp(rectangle))
357
+ for graphic in schematic_data.get("graphics", []):
358
+ sexp_data.append(self._graphic_to_sexp(graphic))
359
+
360
+ # Circles
361
+ for circle in schematic_data.get("circles", []):
362
+ sexp_data.append(self._circle_to_sexp(circle))
303
363
 
304
- # Add text elements
364
+ # Arcs
365
+ for arc in schematic_data.get("arcs", []):
366
+ sexp_data.append(self._arc_to_sexp(arc))
367
+
368
+ # Polylines
369
+ for polyline in schematic_data.get("polylines", []):
370
+ sexp_data.append(self._polyline_to_sexp(polyline))
371
+
372
+ # Text elements
305
373
  for text in schematic_data.get("texts", []):
306
374
  sexp_data.append(self._text_to_sexp(text))
307
375
 
308
- # Add text boxes
376
+ # Text boxes
309
377
  for text_box in schematic_data.get("text_boxes", []):
310
378
  sexp_data.append(self._text_box_to_sexp(text_box))
311
379
 
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))
380
+ # Hierarchical sheets
381
+ for sheet in schematic_data.get("sheets", []):
382
+ sexp_data.append(self._sheet_to_sexp(sheet, schematic_data.get("uuid")))
319
383
 
320
384
  # Add sheet_instances (required by KiCAD)
321
385
  sheet_instances = schematic_data.get("sheet_instances", [])
@@ -538,6 +602,481 @@ class SExpressionParser:
538
602
 
539
603
  return label_data
540
604
 
605
+ def _parse_hierarchical_label(self, item: List[Any]) -> Optional[Dict[str, Any]]:
606
+ """Parse a hierarchical label definition."""
607
+ # Format: (hierarchical_label "text" (shape input) (at x y rotation) (effects ...) (uuid ...))
608
+ if len(item) < 2:
609
+ return None
610
+
611
+ hlabel_data = {
612
+ "text": str(item[1]), # Hierarchical label text is second element
613
+ "shape": "input", # input/output/bidirectional/tri_state/passive
614
+ "position": {"x": 0, "y": 0},
615
+ "rotation": 0,
616
+ "size": 1.27,
617
+ "justify": "left",
618
+ "uuid": None
619
+ }
620
+
621
+ for elem in item[2:]: # Skip hierarchical_label keyword and text
622
+ if not isinstance(elem, list):
623
+ continue
624
+
625
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
626
+
627
+ if elem_type == "shape":
628
+ # Parse shape: (shape input)
629
+ if len(elem) >= 2:
630
+ hlabel_data["shape"] = str(elem[1])
631
+
632
+ elif elem_type == "at":
633
+ # Parse position: (at x y rotation)
634
+ if len(elem) >= 3:
635
+ hlabel_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
636
+ if len(elem) >= 4:
637
+ hlabel_data["rotation"] = float(elem[3])
638
+
639
+ elif elem_type == "effects":
640
+ # Parse effects for font size and justification: (effects (font (size x y)) (justify left))
641
+ for effect_elem in elem[1:]:
642
+ if isinstance(effect_elem, list):
643
+ effect_type = str(effect_elem[0]) if isinstance(effect_elem[0], sexpdata.Symbol) else None
644
+
645
+ if effect_type == "font":
646
+ # Parse font size
647
+ for font_elem in effect_elem[1:]:
648
+ if isinstance(font_elem, list) and str(font_elem[0]) == "size":
649
+ if len(font_elem) >= 2:
650
+ hlabel_data["size"] = float(font_elem[1])
651
+
652
+ elif effect_type == "justify":
653
+ # Parse justification (e.g., "left", "right")
654
+ if len(effect_elem) >= 2:
655
+ hlabel_data["justify"] = str(effect_elem[1])
656
+
657
+ elif elem_type == "uuid":
658
+ hlabel_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
659
+
660
+ return hlabel_data
661
+
662
+ def _parse_no_connect(self, item: List[Any]) -> Optional[Dict[str, Any]]:
663
+ """Parse a no_connect symbol."""
664
+ # Format: (no_connect (at x y) (uuid ...))
665
+ no_connect_data = {
666
+ "position": {"x": 0, "y": 0},
667
+ "uuid": None
668
+ }
669
+
670
+ for elem in item[1:]:
671
+ if not isinstance(elem, list):
672
+ continue
673
+
674
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
675
+
676
+ if elem_type == "at":
677
+ if len(elem) >= 3:
678
+ no_connect_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
679
+ elif elem_type == "uuid":
680
+ no_connect_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
681
+
682
+ return no_connect_data
683
+
684
+ def _parse_text(self, item: List[Any]) -> Optional[Dict[str, Any]]:
685
+ """Parse a text element."""
686
+ # Format: (text "text" (exclude_from_sim no) (at x y rotation) (effects ...) (uuid ...))
687
+ if len(item) < 2:
688
+ return None
689
+
690
+ text_data = {
691
+ "text": str(item[1]),
692
+ "exclude_from_sim": False,
693
+ "position": {"x": 0, "y": 0},
694
+ "rotation": 0,
695
+ "size": 1.27,
696
+ "uuid": None
697
+ }
698
+
699
+ for elem in item[2:]:
700
+ if not isinstance(elem, list):
701
+ continue
702
+
703
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
704
+
705
+ if elem_type == "exclude_from_sim":
706
+ if len(elem) >= 2:
707
+ text_data["exclude_from_sim"] = str(elem[1]) == "yes"
708
+ elif elem_type == "at":
709
+ if len(elem) >= 3:
710
+ text_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
711
+ if len(elem) >= 4:
712
+ text_data["rotation"] = float(elem[3])
713
+ elif elem_type == "effects":
714
+ for effect_elem in elem[1:]:
715
+ if isinstance(effect_elem, list) and str(effect_elem[0]) == "font":
716
+ for font_elem in effect_elem[1:]:
717
+ if isinstance(font_elem, list) and str(font_elem[0]) == "size":
718
+ if len(font_elem) >= 2:
719
+ text_data["size"] = float(font_elem[1])
720
+ elif elem_type == "uuid":
721
+ text_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
722
+
723
+ return text_data
724
+
725
+ def _parse_text_box(self, item: List[Any]) -> Optional[Dict[str, Any]]:
726
+ """Parse a text_box element."""
727
+ # Format: (text_box "text" (exclude_from_sim no) (at x y rotation) (size w h) (margins ...) (stroke ...) (fill ...) (effects ...) (uuid ...))
728
+ if len(item) < 2:
729
+ return None
730
+
731
+ text_box_data = {
732
+ "text": str(item[1]),
733
+ "exclude_from_sim": False,
734
+ "position": {"x": 0, "y": 0},
735
+ "rotation": 0,
736
+ "size": {"width": 0, "height": 0},
737
+ "margins": (0.9525, 0.9525, 0.9525, 0.9525),
738
+ "stroke_width": 0,
739
+ "stroke_type": "solid",
740
+ "fill_type": "none",
741
+ "font_size": 1.27,
742
+ "justify_horizontal": "left",
743
+ "justify_vertical": "top",
744
+ "uuid": None
745
+ }
746
+
747
+ for elem in item[2:]:
748
+ if not isinstance(elem, list):
749
+ continue
750
+
751
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
752
+
753
+ if elem_type == "exclude_from_sim":
754
+ if len(elem) >= 2:
755
+ text_box_data["exclude_from_sim"] = str(elem[1]) == "yes"
756
+ elif elem_type == "at":
757
+ if len(elem) >= 3:
758
+ text_box_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
759
+ if len(elem) >= 4:
760
+ text_box_data["rotation"] = float(elem[3])
761
+ elif elem_type == "size":
762
+ if len(elem) >= 3:
763
+ text_box_data["size"] = {"width": float(elem[1]), "height": float(elem[2])}
764
+ elif elem_type == "margins":
765
+ if len(elem) >= 5:
766
+ text_box_data["margins"] = (float(elem[1]), float(elem[2]), float(elem[3]), float(elem[4]))
767
+ elif elem_type == "stroke":
768
+ for stroke_elem in elem[1:]:
769
+ if isinstance(stroke_elem, list):
770
+ stroke_type = str(stroke_elem[0])
771
+ if stroke_type == "width" and len(stroke_elem) >= 2:
772
+ text_box_data["stroke_width"] = float(stroke_elem[1])
773
+ elif stroke_type == "type" and len(stroke_elem) >= 2:
774
+ text_box_data["stroke_type"] = str(stroke_elem[1])
775
+ elif elem_type == "fill":
776
+ for fill_elem in elem[1:]:
777
+ if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
778
+ text_box_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
779
+ elif elem_type == "effects":
780
+ for effect_elem in elem[1:]:
781
+ if isinstance(effect_elem, list):
782
+ effect_type = str(effect_elem[0])
783
+ if effect_type == "font":
784
+ for font_elem in effect_elem[1:]:
785
+ if isinstance(font_elem, list) and str(font_elem[0]) == "size":
786
+ if len(font_elem) >= 2:
787
+ text_box_data["font_size"] = float(font_elem[1])
788
+ elif effect_type == "justify":
789
+ if len(effect_elem) >= 2:
790
+ text_box_data["justify_horizontal"] = str(effect_elem[1])
791
+ if len(effect_elem) >= 3:
792
+ text_box_data["justify_vertical"] = str(effect_elem[2])
793
+ elif elem_type == "uuid":
794
+ text_box_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
795
+
796
+ return text_box_data
797
+
798
+ def _parse_sheet(self, item: List[Any]) -> Optional[Dict[str, Any]]:
799
+ """Parse a hierarchical sheet."""
800
+ # Complex format with position, size, properties, pins, instances
801
+ sheet_data = {
802
+ "position": {"x": 0, "y": 0},
803
+ "size": {"width": 0, "height": 0},
804
+ "exclude_from_sim": False,
805
+ "in_bom": True,
806
+ "on_board": True,
807
+ "dnp": False,
808
+ "fields_autoplaced": True,
809
+ "stroke_width": 0.1524,
810
+ "stroke_type": "solid",
811
+ "fill_color": (0, 0, 0, 0.0),
812
+ "uuid": None,
813
+ "name": "Sheet",
814
+ "filename": "sheet.kicad_sch",
815
+ "pins": [],
816
+ "project_name": "",
817
+ "page_number": "2"
818
+ }
819
+
820
+ for elem in item[1:]:
821
+ if not isinstance(elem, list):
822
+ continue
823
+
824
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
825
+
826
+ if elem_type == "at":
827
+ if len(elem) >= 3:
828
+ sheet_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
829
+ elif elem_type == "size":
830
+ if len(elem) >= 3:
831
+ sheet_data["size"] = {"width": float(elem[1]), "height": float(elem[2])}
832
+ elif elem_type == "exclude_from_sim":
833
+ sheet_data["exclude_from_sim"] = str(elem[1]) == "yes" if len(elem) > 1 else False
834
+ elif elem_type == "in_bom":
835
+ sheet_data["in_bom"] = str(elem[1]) == "yes" if len(elem) > 1 else True
836
+ elif elem_type == "on_board":
837
+ sheet_data["on_board"] = str(elem[1]) == "yes" if len(elem) > 1 else True
838
+ elif elem_type == "dnp":
839
+ sheet_data["dnp"] = str(elem[1]) == "yes" if len(elem) > 1 else False
840
+ elif elem_type == "fields_autoplaced":
841
+ sheet_data["fields_autoplaced"] = str(elem[1]) == "yes" if len(elem) > 1 else True
842
+ elif elem_type == "stroke":
843
+ for stroke_elem in elem[1:]:
844
+ if isinstance(stroke_elem, list):
845
+ stroke_type = str(stroke_elem[0])
846
+ if stroke_type == "width" and len(stroke_elem) >= 2:
847
+ sheet_data["stroke_width"] = float(stroke_elem[1])
848
+ elif stroke_type == "type" and len(stroke_elem) >= 2:
849
+ sheet_data["stroke_type"] = str(stroke_elem[1])
850
+ elif elem_type == "fill":
851
+ for fill_elem in elem[1:]:
852
+ if isinstance(fill_elem, list) and str(fill_elem[0]) == "color":
853
+ if len(fill_elem) >= 5:
854
+ sheet_data["fill_color"] = (int(fill_elem[1]), int(fill_elem[2]), int(fill_elem[3]), float(fill_elem[4]))
855
+ elif elem_type == "uuid":
856
+ sheet_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
857
+ elif elem_type == "property":
858
+ if len(elem) >= 3:
859
+ prop_name = str(elem[1])
860
+ prop_value = str(elem[2])
861
+ if prop_name == "Sheetname":
862
+ sheet_data["name"] = prop_value
863
+ elif prop_name == "Sheetfile":
864
+ sheet_data["filename"] = prop_value
865
+ elif elem_type == "pin":
866
+ # Parse sheet pin - reuse existing _parse_sheet_pin helper
867
+ pin_data = self._parse_sheet_pin_for_read(elem)
868
+ if pin_data:
869
+ sheet_data["pins"].append(pin_data)
870
+ elif elem_type == "instances":
871
+ # Parse instances for project name and page number
872
+ for inst_elem in elem[1:]:
873
+ if isinstance(inst_elem, list) and str(inst_elem[0]) == "project":
874
+ if len(inst_elem) >= 2:
875
+ sheet_data["project_name"] = str(inst_elem[1])
876
+ for path_elem in inst_elem[2:]:
877
+ if isinstance(path_elem, list) and str(path_elem[0]) == "path":
878
+ for page_elem in path_elem[1:]:
879
+ if isinstance(page_elem, list) and str(page_elem[0]) == "page":
880
+ sheet_data["page_number"] = str(page_elem[1]) if len(page_elem) > 1 else "2"
881
+
882
+ return sheet_data
883
+
884
+ def _parse_sheet_pin_for_read(self, item: List[Any]) -> Optional[Dict[str, Any]]:
885
+ """Parse a sheet pin (for reading during sheet parsing)."""
886
+ # Format: (pin "name" type (at x y rotation) (uuid ...) (effects ...))
887
+ if len(item) < 3:
888
+ return None
889
+
890
+ pin_data = {
891
+ "name": str(item[1]),
892
+ "pin_type": str(item[2]) if len(item) > 2 else "input",
893
+ "position": {"x": 0, "y": 0},
894
+ "rotation": 0,
895
+ "size": 1.27,
896
+ "justify": "right",
897
+ "uuid": None
898
+ }
899
+
900
+ for elem in item[3:]:
901
+ if not isinstance(elem, list):
902
+ continue
903
+
904
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
905
+
906
+ if elem_type == "at":
907
+ if len(elem) >= 3:
908
+ pin_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
909
+ if len(elem) >= 4:
910
+ pin_data["rotation"] = float(elem[3])
911
+ elif elem_type == "uuid":
912
+ pin_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
913
+ elif elem_type == "effects":
914
+ for effect_elem in elem[1:]:
915
+ if isinstance(effect_elem, list):
916
+ effect_type = str(effect_elem[0])
917
+ if effect_type == "font":
918
+ for font_elem in effect_elem[1:]:
919
+ if isinstance(font_elem, list) and str(font_elem[0]) == "size":
920
+ if len(font_elem) >= 2:
921
+ pin_data["size"] = float(font_elem[1])
922
+ elif effect_type == "justify":
923
+ if len(effect_elem) >= 2:
924
+ pin_data["justify"] = str(effect_elem[1])
925
+
926
+ return pin_data
927
+
928
+ def _parse_polyline(self, item: List[Any]) -> Optional[Dict[str, Any]]:
929
+ """Parse a polyline graphical element."""
930
+ # Format: (polyline (pts (xy x1 y1) (xy x2 y2) ...) (stroke ...) (uuid ...))
931
+ polyline_data = {
932
+ "points": [],
933
+ "stroke_width": 0,
934
+ "stroke_type": "default",
935
+ "uuid": None
936
+ }
937
+
938
+ for elem in item[1:]:
939
+ if not isinstance(elem, list):
940
+ continue
941
+
942
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
943
+
944
+ if elem_type == "pts":
945
+ for pt in elem[1:]:
946
+ if isinstance(pt, list) and len(pt) >= 3 and str(pt[0]) == "xy":
947
+ polyline_data["points"].append({"x": float(pt[1]), "y": float(pt[2])})
948
+ elif elem_type == "stroke":
949
+ for stroke_elem in elem[1:]:
950
+ if isinstance(stroke_elem, list):
951
+ stroke_type = str(stroke_elem[0])
952
+ if stroke_type == "width" and len(stroke_elem) >= 2:
953
+ polyline_data["stroke_width"] = float(stroke_elem[1])
954
+ elif stroke_type == "type" and len(stroke_elem) >= 2:
955
+ polyline_data["stroke_type"] = str(stroke_elem[1])
956
+ elif elem_type == "uuid":
957
+ polyline_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
958
+
959
+ return polyline_data if polyline_data["points"] else None
960
+
961
+ def _parse_arc(self, item: List[Any]) -> Optional[Dict[str, Any]]:
962
+ """Parse an arc graphical element."""
963
+ # Format: (arc (start x y) (mid x y) (end x y) (stroke ...) (fill ...) (uuid ...))
964
+ arc_data = {
965
+ "start": {"x": 0, "y": 0},
966
+ "mid": {"x": 0, "y": 0},
967
+ "end": {"x": 0, "y": 0},
968
+ "stroke_width": 0,
969
+ "stroke_type": "default",
970
+ "fill_type": "none",
971
+ "uuid": None
972
+ }
973
+
974
+ for elem in item[1:]:
975
+ if not isinstance(elem, list):
976
+ continue
977
+
978
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
979
+
980
+ if elem_type == "start" and len(elem) >= 3:
981
+ arc_data["start"] = {"x": float(elem[1]), "y": float(elem[2])}
982
+ elif elem_type == "mid" and len(elem) >= 3:
983
+ arc_data["mid"] = {"x": float(elem[1]), "y": float(elem[2])}
984
+ elif elem_type == "end" and len(elem) >= 3:
985
+ arc_data["end"] = {"x": float(elem[1]), "y": float(elem[2])}
986
+ elif elem_type == "stroke":
987
+ for stroke_elem in elem[1:]:
988
+ if isinstance(stroke_elem, list):
989
+ stroke_type = str(stroke_elem[0])
990
+ if stroke_type == "width" and len(stroke_elem) >= 2:
991
+ arc_data["stroke_width"] = float(stroke_elem[1])
992
+ elif stroke_type == "type" and len(stroke_elem) >= 2:
993
+ arc_data["stroke_type"] = str(stroke_elem[1])
994
+ elif elem_type == "fill":
995
+ for fill_elem in elem[1:]:
996
+ if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
997
+ arc_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
998
+ elif elem_type == "uuid":
999
+ arc_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
1000
+
1001
+ return arc_data
1002
+
1003
+ def _parse_circle(self, item: List[Any]) -> Optional[Dict[str, Any]]:
1004
+ """Parse a circle graphical element."""
1005
+ # Format: (circle (center x y) (radius r) (stroke ...) (fill ...) (uuid ...))
1006
+ circle_data = {
1007
+ "center": {"x": 0, "y": 0},
1008
+ "radius": 0,
1009
+ "stroke_width": 0,
1010
+ "stroke_type": "default",
1011
+ "fill_type": "none",
1012
+ "uuid": None
1013
+ }
1014
+
1015
+ for elem in item[1:]:
1016
+ if not isinstance(elem, list):
1017
+ continue
1018
+
1019
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
1020
+
1021
+ if elem_type == "center" and len(elem) >= 3:
1022
+ circle_data["center"] = {"x": float(elem[1]), "y": float(elem[2])}
1023
+ elif elem_type == "radius" and len(elem) >= 2:
1024
+ circle_data["radius"] = float(elem[1])
1025
+ elif elem_type == "stroke":
1026
+ for stroke_elem in elem[1:]:
1027
+ if isinstance(stroke_elem, list):
1028
+ stroke_type = str(stroke_elem[0])
1029
+ if stroke_type == "width" and len(stroke_elem) >= 2:
1030
+ circle_data["stroke_width"] = float(stroke_elem[1])
1031
+ elif stroke_type == "type" and len(stroke_elem) >= 2:
1032
+ circle_data["stroke_type"] = str(stroke_elem[1])
1033
+ elif elem_type == "fill":
1034
+ for fill_elem in elem[1:]:
1035
+ if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
1036
+ circle_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
1037
+ elif elem_type == "uuid":
1038
+ circle_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
1039
+
1040
+ return circle_data
1041
+
1042
+ def _parse_bezier(self, item: List[Any]) -> Optional[Dict[str, Any]]:
1043
+ """Parse a bezier curve graphical element."""
1044
+ # Format: (bezier (pts (xy x1 y1) (xy x2 y2) ...) (stroke ...) (fill ...) (uuid ...))
1045
+ bezier_data = {
1046
+ "points": [],
1047
+ "stroke_width": 0,
1048
+ "stroke_type": "default",
1049
+ "fill_type": "none",
1050
+ "uuid": None
1051
+ }
1052
+
1053
+ for elem in item[1:]:
1054
+ if not isinstance(elem, list):
1055
+ continue
1056
+
1057
+ elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
1058
+
1059
+ if elem_type == "pts":
1060
+ for pt in elem[1:]:
1061
+ if isinstance(pt, list) and len(pt) >= 3 and str(pt[0]) == "xy":
1062
+ bezier_data["points"].append({"x": float(pt[1]), "y": float(pt[2])})
1063
+ elif elem_type == "stroke":
1064
+ for stroke_elem in elem[1:]:
1065
+ if isinstance(stroke_elem, list):
1066
+ stroke_type = str(stroke_elem[0])
1067
+ if stroke_type == "width" and len(stroke_elem) >= 2:
1068
+ bezier_data["stroke_width"] = float(stroke_elem[1])
1069
+ elif stroke_type == "type" and len(stroke_elem) >= 2:
1070
+ bezier_data["stroke_type"] = str(stroke_elem[1])
1071
+ elif elem_type == "fill":
1072
+ for fill_elem in elem[1:]:
1073
+ if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
1074
+ bezier_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
1075
+ elif elem_type == "uuid":
1076
+ bezier_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
1077
+
1078
+ return bezier_data if bezier_data["points"] else None
1079
+
541
1080
  def _parse_rectangle(self, item: List[Any]) -> Optional[Dict[str, Any]]:
542
1081
  """Parse a rectangle graphical element."""
543
1082
  rectangle = {}
@@ -942,6 +1481,171 @@ class SExpressionParser:
942
1481
 
943
1482
  return sexp
944
1483
 
1484
+ def _no_connect_to_sexp(self, no_connect_data: Dict[str, Any]) -> List[Any]:
1485
+ """Convert no_connect to S-expression."""
1486
+ sexp = [sexpdata.Symbol("no_connect")]
1487
+
1488
+ # Add position
1489
+ pos = no_connect_data["position"]
1490
+ x, y = pos["x"], pos["y"]
1491
+
1492
+ # Format coordinates properly
1493
+ if isinstance(x, float) and x.is_integer():
1494
+ x = int(x)
1495
+ if isinstance(y, float) and y.is_integer():
1496
+ y = int(y)
1497
+
1498
+ sexp.append([sexpdata.Symbol("at"), x, y])
1499
+
1500
+ # Add UUID
1501
+ if "uuid" in no_connect_data:
1502
+ sexp.append([sexpdata.Symbol("uuid"), no_connect_data["uuid"]])
1503
+
1504
+ return sexp
1505
+
1506
+ def _polyline_to_sexp(self, polyline_data: Dict[str, Any]) -> List[Any]:
1507
+ """Convert polyline to S-expression."""
1508
+ sexp = [sexpdata.Symbol("polyline")]
1509
+
1510
+ # Add points
1511
+ points = polyline_data.get("points", [])
1512
+ if points:
1513
+ pts_sexp = [sexpdata.Symbol("pts")]
1514
+ for point in points:
1515
+ x, y = point["x"], point["y"]
1516
+ # Format coordinates properly
1517
+ if isinstance(x, float) and x.is_integer():
1518
+ x = int(x)
1519
+ if isinstance(y, float) and y.is_integer():
1520
+ y = int(y)
1521
+ pts_sexp.append([sexpdata.Symbol("xy"), x, y])
1522
+ sexp.append(pts_sexp)
1523
+
1524
+ # Add stroke
1525
+ stroke_width = polyline_data.get("stroke_width", 0)
1526
+ stroke_type = polyline_data.get("stroke_type", "default")
1527
+ stroke_sexp = [sexpdata.Symbol("stroke")]
1528
+ stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
1529
+ stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
1530
+ sexp.append(stroke_sexp)
1531
+
1532
+ # Add UUID
1533
+ if "uuid" in polyline_data:
1534
+ sexp.append([sexpdata.Symbol("uuid"), polyline_data["uuid"]])
1535
+
1536
+ return sexp
1537
+
1538
+ def _arc_to_sexp(self, arc_data: Dict[str, Any]) -> List[Any]:
1539
+ """Convert arc to S-expression."""
1540
+ sexp = [sexpdata.Symbol("arc")]
1541
+
1542
+ # Add start, mid, end points
1543
+ for point_name in ["start", "mid", "end"]:
1544
+ point = arc_data.get(point_name, {"x": 0, "y": 0})
1545
+ x, y = point["x"], point["y"]
1546
+ # Format coordinates properly
1547
+ if isinstance(x, float) and x.is_integer():
1548
+ x = int(x)
1549
+ if isinstance(y, float) and y.is_integer():
1550
+ y = int(y)
1551
+ sexp.append([sexpdata.Symbol(point_name), x, y])
1552
+
1553
+ # Add stroke
1554
+ stroke_width = arc_data.get("stroke_width", 0)
1555
+ stroke_type = arc_data.get("stroke_type", "default")
1556
+ stroke_sexp = [sexpdata.Symbol("stroke")]
1557
+ stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
1558
+ stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
1559
+ sexp.append(stroke_sexp)
1560
+
1561
+ # Add fill
1562
+ fill_type = arc_data.get("fill_type", "none")
1563
+ fill_sexp = [sexpdata.Symbol("fill")]
1564
+ fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
1565
+ sexp.append(fill_sexp)
1566
+
1567
+ # Add UUID
1568
+ if "uuid" in arc_data:
1569
+ sexp.append([sexpdata.Symbol("uuid"), arc_data["uuid"]])
1570
+
1571
+ return sexp
1572
+
1573
+ def _circle_to_sexp(self, circle_data: Dict[str, Any]) -> List[Any]:
1574
+ """Convert circle to S-expression."""
1575
+ sexp = [sexpdata.Symbol("circle")]
1576
+
1577
+ # Add center
1578
+ center = circle_data.get("center", {"x": 0, "y": 0})
1579
+ x, y = center["x"], center["y"]
1580
+ # Format coordinates properly
1581
+ if isinstance(x, float) and x.is_integer():
1582
+ x = int(x)
1583
+ if isinstance(y, float) and y.is_integer():
1584
+ y = int(y)
1585
+ sexp.append([sexpdata.Symbol("center"), x, y])
1586
+
1587
+ # Add radius
1588
+ radius = circle_data.get("radius", 0)
1589
+ sexp.append([sexpdata.Symbol("radius"), radius])
1590
+
1591
+ # Add stroke
1592
+ stroke_width = circle_data.get("stroke_width", 0)
1593
+ stroke_type = circle_data.get("stroke_type", "default")
1594
+ stroke_sexp = [sexpdata.Symbol("stroke")]
1595
+ stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
1596
+ stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
1597
+ sexp.append(stroke_sexp)
1598
+
1599
+ # Add fill
1600
+ fill_type = circle_data.get("fill_type", "none")
1601
+ fill_sexp = [sexpdata.Symbol("fill")]
1602
+ fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
1603
+ sexp.append(fill_sexp)
1604
+
1605
+ # Add UUID
1606
+ if "uuid" in circle_data:
1607
+ sexp.append([sexpdata.Symbol("uuid"), circle_data["uuid"]])
1608
+
1609
+ return sexp
1610
+
1611
+ def _bezier_to_sexp(self, bezier_data: Dict[str, Any]) -> List[Any]:
1612
+ """Convert bezier curve to S-expression."""
1613
+ sexp = [sexpdata.Symbol("bezier")]
1614
+
1615
+ # Add points
1616
+ points = bezier_data.get("points", [])
1617
+ if points:
1618
+ pts_sexp = [sexpdata.Symbol("pts")]
1619
+ for point in points:
1620
+ x, y = point["x"], point["y"]
1621
+ # Format coordinates properly
1622
+ if isinstance(x, float) and x.is_integer():
1623
+ x = int(x)
1624
+ if isinstance(y, float) and y.is_integer():
1625
+ y = int(y)
1626
+ pts_sexp.append([sexpdata.Symbol("xy"), x, y])
1627
+ sexp.append(pts_sexp)
1628
+
1629
+ # Add stroke
1630
+ stroke_width = bezier_data.get("stroke_width", 0)
1631
+ stroke_type = bezier_data.get("stroke_type", "default")
1632
+ stroke_sexp = [sexpdata.Symbol("stroke")]
1633
+ stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
1634
+ stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
1635
+ sexp.append(stroke_sexp)
1636
+
1637
+ # Add fill
1638
+ fill_type = bezier_data.get("fill_type", "none")
1639
+ fill_sexp = [sexpdata.Symbol("fill")]
1640
+ fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
1641
+ sexp.append(fill_sexp)
1642
+
1643
+ # Add UUID
1644
+ if "uuid" in bezier_data:
1645
+ sexp.append([sexpdata.Symbol("uuid"), bezier_data["uuid"]])
1646
+
1647
+ return sexp
1648
+
945
1649
  def _sheet_to_sexp(self, sheet_data: Dict[str, Any], schematic_uuid: str) -> List[Any]:
946
1650
  """Convert hierarchical sheet to S-expression."""
947
1651
  sexp = [sexpdata.Symbol("sheet")]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kicad-sch-api
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: Professional KiCAD schematic manipulation library with exact format preservation
5
5
  Author-email: Circuit-Synth <shane@circuit-synth.com>
6
6
  Maintainer-email: Circuit-Synth <shane@circuit-synth.com>
@@ -1,16 +1,16 @@
1
- kicad_sch_api/__init__.py,sha256=J2JWDT18vzJzY1Qo59oRkz8gQu2LdEPYmyzcCFYLRZ8,2919
1
+ kicad_sch_api/__init__.py,sha256=6yDjQeaoOdZatCgDaIULVxwXqT1fjLSNjYU0yKRh9f4,2919
2
2
  kicad_sch_api/cli.py,sha256=ZzmwzfHEvPgGfCiQBU4G2LBAyRtMNiBRoY21pivJSYc,7621
3
3
  kicad_sch_api/py.typed,sha256=e4ldqxwpY7pNDG1olbvj4HSKr8sZ9vxgA_2ek8xXn-Q,70
4
4
  kicad_sch_api/core/__init__.py,sha256=ur_KeYBlGKl-e1hLpLdxAhGV2A-PCCGkcqd0r6KSeBA,566
5
5
  kicad_sch_api/core/component_bounds.py,sha256=BFYJYULyzs5it2hN7bHTimyS9Vet4dxsMklRStob-F4,17509
6
6
  kicad_sch_api/core/components.py,sha256=tXRL18GObl2u94wl5jP-1ID56s_UD9F1gQ_iRIyZ_Kw,25290
7
7
  kicad_sch_api/core/config.py,sha256=itw0j3DeIEHaFVf8p3mfAS1SP6jclBwvMv7NPdkThE4,4309
8
- kicad_sch_api/core/formatter.py,sha256=cTvQ9kNIYr86viPtBnMlJLAn627ciwYznJduTgsBR18,20807
8
+ kicad_sch_api/core/formatter.py,sha256=7kwM7WdbVjT8biirWSSFO44OMjeByvzYYr_-mpGiEA4,20862
9
9
  kicad_sch_api/core/geometry.py,sha256=27SgN0padLbQuTi8MV6UUCp6Pyaiv8V9gmYDOhfwny8,2947
10
10
  kicad_sch_api/core/ic_manager.py,sha256=Kg0HIOMU-TGXiIkrnwcHFQ1Kfv_3rW2U1cwBKJsKopc,7219
11
11
  kicad_sch_api/core/junctions.py,sha256=Ay6BsWX_DLs-wB0eMA2CytKKq0N8Ja41ZubJWpAqNgM,6122
12
12
  kicad_sch_api/core/manhattan_routing.py,sha256=t_T2u0zsQB-a8dTijFmY-qFq-oDt2qDebYyXzD_pBWI,15989
13
- kicad_sch_api/core/parser.py,sha256=eX70sATlGMeZP-TM9Izhx0gGQ7DGAYBDVF3aHjRSpTM,61489
13
+ kicad_sch_api/core/parser.py,sha256=UMTpwrxtyFM5cjAkgLWts6vBJ254gc4Hvx-puZydkVQ,92209
14
14
  kicad_sch_api/core/pin_utils.py,sha256=XGEow3HzBTyT8a0B_ZC8foMvwzYaENSaqTUwDW1rz24,5417
15
15
  kicad_sch_api/core/schematic.py,sha256=U9-wrhuGtgRqZJfc76Dj-g1_ZTjrT8R9LmfX-BIBH8w,61201
16
16
  kicad_sch_api/core/simple_manhattan.py,sha256=CvIHvwmfABPF-COzhblYxEgRoR_R_eD-lmBFHHjDuMI,7241
@@ -23,9 +23,9 @@ kicad_sch_api/library/__init__.py,sha256=NG9UTdcpn25Bl9tPsYs9ED7bvpaVPVdtLMbnxkQ
23
23
  kicad_sch_api/library/cache.py,sha256=7na88grl465WHwUOGuOzYrrWwjsMBXhXVtxhnaJ9GBY,33208
24
24
  kicad_sch_api/utils/__init__.py,sha256=1V_yGgI7jro6MUc4Pviux_WIeJ1wmiYFID186SZwWLQ,277
25
25
  kicad_sch_api/utils/validation.py,sha256=XlWGRZJb3cOPYpU9sLQQgC_NASwbi6W-LCN7PzUmaPY,15626
26
- kicad_sch_api-0.3.1.dist-info/licenses/LICENSE,sha256=Em65Nvte1G9MHc0rHqtYuGkCPcshD588itTa358J6gs,1070
27
- kicad_sch_api-0.3.1.dist-info/METADATA,sha256=xFLLtl5pIDqCTvvSyWz8Y1hFMdev4NpEKkq6IPmFMzo,17183
28
- kicad_sch_api-0.3.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
29
- kicad_sch_api-0.3.1.dist-info/entry_points.txt,sha256=VWKsFi2Jv7G_tmio3cNVhhIBfv_OZFaKa-T_ED84lc8,57
30
- kicad_sch_api-0.3.1.dist-info/top_level.txt,sha256=n0ex4gOJ1b_fARowcGqRzyOGZcHRhc5LZa6_vVgGxcI,14
31
- kicad_sch_api-0.3.1.dist-info/RECORD,,
26
+ kicad_sch_api-0.3.2.dist-info/licenses/LICENSE,sha256=Em65Nvte1G9MHc0rHqtYuGkCPcshD588itTa358J6gs,1070
27
+ kicad_sch_api-0.3.2.dist-info/METADATA,sha256=m8NfvoWy9m4AfShTSlHkn2AMUzy1cZkxiQXPnF3e0a0,17183
28
+ kicad_sch_api-0.3.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
29
+ kicad_sch_api-0.3.2.dist-info/entry_points.txt,sha256=VWKsFi2Jv7G_tmio3cNVhhIBfv_OZFaKa-T_ED84lc8,57
30
+ kicad_sch_api-0.3.2.dist-info/top_level.txt,sha256=n0ex4gOJ1b_fARowcGqRzyOGZcHRhc5LZa6_vVgGxcI,14
31
+ kicad_sch_api-0.3.2.dist-info/RECORD,,