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
@@ -12,6 +12,15 @@ from typing import Any, Dict, List, Optional, Tuple, Union
12
12
 
13
13
  import sexpdata
14
14
 
15
+ from ..parsers.elements.graphics_parser import GraphicsParser
16
+ from ..parsers.elements.label_parser import LabelParser
17
+ from ..parsers.elements.library_parser import LibraryParser
18
+ from ..parsers.elements.metadata_parser import MetadataParser
19
+ from ..parsers.elements.sheet_parser import SheetParser
20
+ from ..parsers.elements.symbol_parser import SymbolParser
21
+ from ..parsers.elements.text_parser import TextParser
22
+ from ..parsers.elements.wire_parser import WireParser
23
+ from ..parsers.utils import color_to_rgb255, color_to_rgba
15
24
  from ..utils.validation import ValidationError, ValidationIssue
16
25
  from .formatter import ExactFormatter
17
26
  from .types import Junction, Label, Net, Point, SchematicSymbol, Wire
@@ -40,8 +49,30 @@ class SExpressionParser:
40
49
  self.preserve_format = preserve_format
41
50
  self._formatter = ExactFormatter() if preserve_format else None
42
51
  self._validation_issues = []
52
+ self._graphics_parser = GraphicsParser()
53
+ self._wire_parser = WireParser()
54
+ self._label_parser = LabelParser()
55
+ self._text_parser = TextParser()
56
+ self._sheet_parser = SheetParser()
57
+ self._library_parser = LibraryParser()
58
+ self._symbol_parser = SymbolParser()
59
+ self._metadata_parser = MetadataParser()
60
+ self._project_name = None
43
61
  logger.info(f"S-expression parser initialized (format preservation: {preserve_format})")
44
62
 
63
+ @property
64
+ def project_name(self):
65
+ """Get project name."""
66
+ return self._project_name
67
+
68
+ @project_name.setter
69
+ def project_name(self, value):
70
+ """Set project name on parser and propagate to sub-parsers."""
71
+ self._project_name = value
72
+ # Propagate to symbol parser which needs it for instances
73
+ if hasattr(self, '_symbol_parser'):
74
+ self._symbol_parser.project_name = value
75
+
45
76
  def parse_file(self, filepath: Union[str, Path]) -> Dict[str, Any]:
46
77
  """
47
78
  Parse a KiCAD schematic file with comprehensive validation.
@@ -412,912 +443,67 @@ class SExpressionParser:
412
443
 
413
444
  def _parse_title_block(self, item: List[Any]) -> Dict[str, Any]:
414
445
  """Parse title block information."""
415
- title_block = {}
416
- for sub_item in item[1:]:
417
- if isinstance(sub_item, list) and len(sub_item) >= 2:
418
- key = str(sub_item[0]) if isinstance(sub_item[0], sexpdata.Symbol) else None
419
- if key:
420
- title_block[key] = sub_item[1] if len(sub_item) > 1 else None
421
- return title_block
422
-
446
+ return self._metadata_parser._parse_title_block(item)
423
447
  def _parse_symbol(self, item: List[Any]) -> Optional[Dict[str, Any]]:
424
448
  """Parse a symbol (component) definition."""
425
- try:
426
- symbol_data = {
427
- "lib_id": None,
428
- "position": Point(0, 0),
429
- "rotation": 0,
430
- "uuid": None,
431
- "reference": None,
432
- "value": None,
433
- "footprint": None,
434
- "properties": {},
435
- "pins": [],
436
- "in_bom": True,
437
- "on_board": True,
438
- }
439
-
440
- for sub_item in item[1:]:
441
- if not isinstance(sub_item, list) or len(sub_item) == 0:
442
- continue
443
-
444
- element_type = (
445
- str(sub_item[0]) if isinstance(sub_item[0], sexpdata.Symbol) else None
446
- )
447
-
448
- if element_type == "lib_id":
449
- symbol_data["lib_id"] = sub_item[1] if len(sub_item) > 1 else None
450
- elif element_type == "at":
451
- if len(sub_item) >= 3:
452
- symbol_data["position"] = Point(float(sub_item[1]), float(sub_item[2]))
453
- if len(sub_item) > 3:
454
- symbol_data["rotation"] = float(sub_item[3])
455
- elif element_type == "uuid":
456
- symbol_data["uuid"] = sub_item[1] if len(sub_item) > 1 else None
457
- elif element_type == "property":
458
- prop_data = self._parse_property(sub_item)
459
- if prop_data:
460
- prop_name = prop_data.get("name")
461
- if prop_name == "Reference":
462
- symbol_data["reference"] = prop_data.get("value")
463
- elif prop_name == "Value":
464
- symbol_data["value"] = prop_data.get("value")
465
- elif prop_name == "Footprint":
466
- symbol_data["footprint"] = prop_data.get("value")
467
- else:
468
- # Unescape quotes in property values when loading
469
- prop_value = prop_data.get("value")
470
- if prop_value:
471
- prop_value = str(prop_value).replace('\\"', '"')
472
- symbol_data["properties"][prop_name] = prop_value
473
- elif element_type == "in_bom":
474
- symbol_data["in_bom"] = sub_item[1] == "yes" if len(sub_item) > 1 else True
475
- elif element_type == "on_board":
476
- symbol_data["on_board"] = sub_item[1] == "yes" if len(sub_item) > 1 else True
477
-
478
- return symbol_data
479
-
480
- except Exception as e:
481
- logger.warning(f"Error parsing symbol: {e}")
482
- return None
483
-
449
+ return self._symbol_parser._parse_symbol(item)
484
450
  def _parse_property(self, item: List[Any]) -> Optional[Dict[str, Any]]:
485
451
  """Parse a property definition."""
486
- if len(item) < 3:
487
- return None
488
-
489
- return {
490
- "name": item[1] if len(item) > 1 else None,
491
- "value": item[2] if len(item) > 2 else None,
492
- }
493
-
452
+ return self._symbol_parser._parse_property(item)
494
453
  def _parse_wire(self, item: List[Any]) -> Optional[Dict[str, Any]]:
495
454
  """Parse a wire definition."""
496
- wire_data = {
497
- "points": [],
498
- "stroke_width": 0.0,
499
- "stroke_type": "default",
500
- "uuid": None,
501
- "wire_type": "wire", # Default to wire (vs bus)
502
- }
503
-
504
- for elem in item[1:]:
505
- if not isinstance(elem, list):
506
- continue
507
-
508
- elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
509
-
510
- if elem_type == "pts":
511
- # Parse points: (pts (xy x1 y1) (xy x2 y2) ...)
512
- for pt in elem[1:]:
513
- if isinstance(pt, list) and len(pt) >= 3:
514
- if str(pt[0]) == "xy":
515
- x, y = float(pt[1]), float(pt[2])
516
- wire_data["points"].append({"x": x, "y": y})
517
-
518
- elif elem_type == "stroke":
519
- # Parse stroke: (stroke (width 0) (type default))
520
- for stroke_elem in elem[1:]:
521
- if isinstance(stroke_elem, list) and len(stroke_elem) >= 2:
522
- stroke_type = str(stroke_elem[0])
523
- if stroke_type == "width":
524
- wire_data["stroke_width"] = float(stroke_elem[1])
525
- elif stroke_type == "type":
526
- wire_data["stroke_type"] = str(stroke_elem[1])
527
-
528
- elif elem_type == "uuid":
529
- wire_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
530
-
531
- # Only return wire if it has at least 2 points
532
- if len(wire_data["points"]) >= 2:
533
- return wire_data
534
- else:
535
- logger.warning(f"Wire has insufficient points: {len(wire_data['points'])}")
536
- return None
537
-
455
+ return self._wire_parser._parse_wire(item)
538
456
  def _parse_junction(self, item: List[Any]) -> Optional[Dict[str, Any]]:
539
457
  """Parse a junction definition."""
540
- junction_data = {
541
- "position": {"x": 0, "y": 0},
542
- "diameter": 0,
543
- "color": (0, 0, 0, 0),
544
- "uuid": None,
545
- }
546
-
547
- for elem in item[1:]:
548
- if not isinstance(elem, list):
549
- continue
550
-
551
- elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
552
-
553
- if elem_type == "at":
554
- # Parse position: (at x y)
555
- if len(elem) >= 3:
556
- junction_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
557
-
558
- elif elem_type == "diameter":
559
- # Parse diameter: (diameter value)
560
- if len(elem) >= 2:
561
- junction_data["diameter"] = float(elem[1])
562
-
563
- elif elem_type == "color":
564
- # Parse color: (color r g b a)
565
- if len(elem) >= 5:
566
- junction_data["color"] = (
567
- int(elem[1]),
568
- int(elem[2]),
569
- int(elem[3]),
570
- int(elem[4]),
571
- )
572
-
573
- elif elem_type == "uuid":
574
- junction_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
575
-
576
- return junction_data
577
-
458
+ return self._wire_parser._parse_junction(item)
578
459
  def _parse_label(self, item: List[Any]) -> Optional[Dict[str, Any]]:
579
460
  """Parse a label definition."""
580
- # Label format: (label "text" (at x y rotation) (effects ...) (uuid ...))
581
- if len(item) < 2:
582
- return None
583
-
584
- label_data = {
585
- "text": str(item[1]), # Label text is second element
586
- "position": {"x": 0, "y": 0},
587
- "rotation": 0,
588
- "size": 1.27,
589
- "uuid": None,
590
- }
591
-
592
- for elem in item[2:]: # Skip label keyword and text
593
- if not isinstance(elem, list):
594
- continue
595
-
596
- elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
597
-
598
- if elem_type == "at":
599
- # Parse position: (at x y rotation)
600
- if len(elem) >= 3:
601
- label_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
602
- if len(elem) >= 4:
603
- label_data["rotation"] = float(elem[3])
604
-
605
- elif elem_type == "effects":
606
- # Parse effects for font size: (effects (font (size x y)) ...)
607
- for effect_elem in elem[1:]:
608
- if isinstance(effect_elem, list) and str(effect_elem[0]) == "font":
609
- for font_elem in effect_elem[1:]:
610
- if isinstance(font_elem, list) and str(font_elem[0]) == "size":
611
- if len(font_elem) >= 2:
612
- label_data["size"] = float(font_elem[1])
613
-
614
- elif elem_type == "uuid":
615
- label_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
616
-
617
- return label_data
618
-
461
+ return self._label_parser._parse_label(item)
619
462
  def _parse_hierarchical_label(self, item: List[Any]) -> Optional[Dict[str, Any]]:
620
463
  """Parse a hierarchical label definition."""
621
- # Format: (hierarchical_label "text" (shape input) (at x y rotation) (effects ...) (uuid ...))
622
- if len(item) < 2:
623
- return None
624
-
625
- hlabel_data = {
626
- "text": str(item[1]), # Hierarchical label text is second element
627
- "shape": "input", # input/output/bidirectional/tri_state/passive
628
- "position": {"x": 0, "y": 0},
629
- "rotation": 0,
630
- "size": 1.27,
631
- "justify": "left",
632
- "uuid": None,
633
- }
634
-
635
- for elem in item[2:]: # Skip hierarchical_label keyword and text
636
- if not isinstance(elem, list):
637
- continue
638
-
639
- elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
640
-
641
- if elem_type == "shape":
642
- # Parse shape: (shape input)
643
- if len(elem) >= 2:
644
- hlabel_data["shape"] = str(elem[1])
645
-
646
- elif elem_type == "at":
647
- # Parse position: (at x y rotation)
648
- if len(elem) >= 3:
649
- hlabel_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
650
- if len(elem) >= 4:
651
- hlabel_data["rotation"] = float(elem[3])
652
-
653
- elif elem_type == "effects":
654
- # Parse effects for font size and justification: (effects (font (size x y)) (justify left))
655
- for effect_elem in elem[1:]:
656
- if isinstance(effect_elem, list):
657
- effect_type = (
658
- str(effect_elem[0])
659
- if isinstance(effect_elem[0], sexpdata.Symbol)
660
- else None
661
- )
662
-
663
- if effect_type == "font":
664
- # Parse font size
665
- for font_elem in effect_elem[1:]:
666
- if isinstance(font_elem, list) and str(font_elem[0]) == "size":
667
- if len(font_elem) >= 2:
668
- hlabel_data["size"] = float(font_elem[1])
669
-
670
- elif effect_type == "justify":
671
- # Parse justification (e.g., "left", "right")
672
- if len(effect_elem) >= 2:
673
- hlabel_data["justify"] = str(effect_elem[1])
674
-
675
- elif elem_type == "uuid":
676
- hlabel_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
677
-
678
- return hlabel_data
679
-
464
+ return self._label_parser._parse_hierarchical_label(item)
680
465
  def _parse_no_connect(self, item: List[Any]) -> Optional[Dict[str, Any]]:
681
466
  """Parse a no_connect symbol."""
682
- # Format: (no_connect (at x y) (uuid ...))
683
- no_connect_data = {"position": {"x": 0, "y": 0}, "uuid": None}
684
-
685
- for elem in item[1:]:
686
- if not isinstance(elem, list):
687
- continue
688
-
689
- elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
690
-
691
- if elem_type == "at":
692
- if len(elem) >= 3:
693
- no_connect_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
694
- elif elem_type == "uuid":
695
- no_connect_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
696
-
697
- return no_connect_data
698
-
467
+ return self._wire_parser._parse_no_connect(item)
699
468
  def _parse_text(self, item: List[Any]) -> Optional[Dict[str, Any]]:
700
469
  """Parse a text element."""
701
- # Format: (text "text" (exclude_from_sim no) (at x y rotation) (effects ...) (uuid ...))
702
- if len(item) < 2:
703
- return None
704
-
705
- text_data = {
706
- "text": str(item[1]),
707
- "exclude_from_sim": False,
708
- "position": {"x": 0, "y": 0},
709
- "rotation": 0,
710
- "size": 1.27,
711
- "uuid": None,
712
- }
713
-
714
- for elem in item[2:]:
715
- if not isinstance(elem, list):
716
- continue
717
-
718
- elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
719
-
720
- if elem_type == "exclude_from_sim":
721
- if len(elem) >= 2:
722
- text_data["exclude_from_sim"] = str(elem[1]) == "yes"
723
- elif elem_type == "at":
724
- if len(elem) >= 3:
725
- text_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
726
- if len(elem) >= 4:
727
- text_data["rotation"] = float(elem[3])
728
- elif elem_type == "effects":
729
- for effect_elem in elem[1:]:
730
- if isinstance(effect_elem, list) and str(effect_elem[0]) == "font":
731
- for font_elem in effect_elem[1:]:
732
- if isinstance(font_elem, list) and str(font_elem[0]) == "size":
733
- if len(font_elem) >= 2:
734
- text_data["size"] = float(font_elem[1])
735
- elif elem_type == "uuid":
736
- text_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
737
-
738
- return text_data
739
-
470
+ return self._text_parser._parse_text(item)
740
471
  def _parse_text_box(self, item: List[Any]) -> Optional[Dict[str, Any]]:
741
472
  """Parse a text_box element."""
742
- # Format: (text_box "text" (exclude_from_sim no) (at x y rotation) (size w h) (margins ...) (stroke ...) (fill ...) (effects ...) (uuid ...))
743
- if len(item) < 2:
744
- return None
745
-
746
- text_box_data = {
747
- "text": str(item[1]),
748
- "exclude_from_sim": False,
749
- "position": {"x": 0, "y": 0},
750
- "rotation": 0,
751
- "size": {"width": 0, "height": 0},
752
- "margins": (0.9525, 0.9525, 0.9525, 0.9525),
753
- "stroke_width": 0,
754
- "stroke_type": "solid",
755
- "fill_type": "none",
756
- "font_size": 1.27,
757
- "justify_horizontal": "left",
758
- "justify_vertical": "top",
759
- "uuid": None,
760
- }
761
-
762
- for elem in item[2:]:
763
- if not isinstance(elem, list):
764
- continue
765
-
766
- elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
767
-
768
- if elem_type == "exclude_from_sim":
769
- if len(elem) >= 2:
770
- text_box_data["exclude_from_sim"] = str(elem[1]) == "yes"
771
- elif elem_type == "at":
772
- if len(elem) >= 3:
773
- text_box_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
774
- if len(elem) >= 4:
775
- text_box_data["rotation"] = float(elem[3])
776
- elif elem_type == "size":
777
- if len(elem) >= 3:
778
- text_box_data["size"] = {"width": float(elem[1]), "height": float(elem[2])}
779
- elif elem_type == "margins":
780
- if len(elem) >= 5:
781
- text_box_data["margins"] = (
782
- float(elem[1]),
783
- float(elem[2]),
784
- float(elem[3]),
785
- float(elem[4]),
786
- )
787
- elif elem_type == "stroke":
788
- for stroke_elem in elem[1:]:
789
- if isinstance(stroke_elem, list):
790
- stroke_type = str(stroke_elem[0])
791
- if stroke_type == "width" and len(stroke_elem) >= 2:
792
- text_box_data["stroke_width"] = float(stroke_elem[1])
793
- elif stroke_type == "type" and len(stroke_elem) >= 2:
794
- text_box_data["stroke_type"] = str(stroke_elem[1])
795
- elif elem_type == "fill":
796
- for fill_elem in elem[1:]:
797
- if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
798
- text_box_data["fill_type"] = (
799
- str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
800
- )
801
- elif elem_type == "effects":
802
- for effect_elem in elem[1:]:
803
- if isinstance(effect_elem, list):
804
- effect_type = str(effect_elem[0])
805
- if effect_type == "font":
806
- for font_elem in effect_elem[1:]:
807
- if isinstance(font_elem, list) and str(font_elem[0]) == "size":
808
- if len(font_elem) >= 2:
809
- text_box_data["font_size"] = float(font_elem[1])
810
- elif effect_type == "justify":
811
- if len(effect_elem) >= 2:
812
- text_box_data["justify_horizontal"] = str(effect_elem[1])
813
- if len(effect_elem) >= 3:
814
- text_box_data["justify_vertical"] = str(effect_elem[2])
815
- elif elem_type == "uuid":
816
- text_box_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
817
-
818
- return text_box_data
819
-
473
+ return self._text_parser._parse_text_box(item)
820
474
  def _parse_sheet(self, item: List[Any]) -> Optional[Dict[str, Any]]:
821
475
  """Parse a hierarchical sheet."""
822
- # Complex format with position, size, properties, pins, instances
823
- sheet_data = {
824
- "position": {"x": 0, "y": 0},
825
- "size": {"width": 0, "height": 0},
826
- "exclude_from_sim": False,
827
- "in_bom": True,
828
- "on_board": True,
829
- "dnp": False,
830
- "fields_autoplaced": True,
831
- "stroke_width": 0.1524,
832
- "stroke_type": "solid",
833
- "fill_color": (0, 0, 0, 0.0),
834
- "uuid": None,
835
- "name": "Sheet",
836
- "filename": "sheet.kicad_sch",
837
- "pins": [],
838
- "project_name": "",
839
- "page_number": "2",
840
- }
841
-
842
- for elem in item[1:]:
843
- if not isinstance(elem, list):
844
- continue
845
-
846
- elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
847
-
848
- if elem_type == "at":
849
- if len(elem) >= 3:
850
- sheet_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
851
- elif elem_type == "size":
852
- if len(elem) >= 3:
853
- sheet_data["size"] = {"width": float(elem[1]), "height": float(elem[2])}
854
- elif elem_type == "exclude_from_sim":
855
- sheet_data["exclude_from_sim"] = str(elem[1]) == "yes" if len(elem) > 1 else False
856
- elif elem_type == "in_bom":
857
- sheet_data["in_bom"] = str(elem[1]) == "yes" if len(elem) > 1 else True
858
- elif elem_type == "on_board":
859
- sheet_data["on_board"] = str(elem[1]) == "yes" if len(elem) > 1 else True
860
- elif elem_type == "dnp":
861
- sheet_data["dnp"] = str(elem[1]) == "yes" if len(elem) > 1 else False
862
- elif elem_type == "fields_autoplaced":
863
- sheet_data["fields_autoplaced"] = str(elem[1]) == "yes" if len(elem) > 1 else True
864
- elif elem_type == "stroke":
865
- for stroke_elem in elem[1:]:
866
- if isinstance(stroke_elem, list):
867
- stroke_type = str(stroke_elem[0])
868
- if stroke_type == "width" and len(stroke_elem) >= 2:
869
- sheet_data["stroke_width"] = float(stroke_elem[1])
870
- elif stroke_type == "type" and len(stroke_elem) >= 2:
871
- sheet_data["stroke_type"] = str(stroke_elem[1])
872
- elif elem_type == "fill":
873
- for fill_elem in elem[1:]:
874
- if isinstance(fill_elem, list) and str(fill_elem[0]) == "color":
875
- if len(fill_elem) >= 5:
876
- sheet_data["fill_color"] = (
877
- int(fill_elem[1]),
878
- int(fill_elem[2]),
879
- int(fill_elem[3]),
880
- float(fill_elem[4]),
881
- )
882
- elif elem_type == "uuid":
883
- sheet_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
884
- elif elem_type == "property":
885
- if len(elem) >= 3:
886
- prop_name = str(elem[1])
887
- prop_value = str(elem[2])
888
- if prop_name == "Sheetname":
889
- sheet_data["name"] = prop_value
890
- elif prop_name == "Sheetfile":
891
- sheet_data["filename"] = prop_value
892
- elif elem_type == "pin":
893
- # Parse sheet pin - reuse existing _parse_sheet_pin helper
894
- pin_data = self._parse_sheet_pin_for_read(elem)
895
- if pin_data:
896
- sheet_data["pins"].append(pin_data)
897
- elif elem_type == "instances":
898
- # Parse instances for project name and page number
899
- for inst_elem in elem[1:]:
900
- if isinstance(inst_elem, list) and str(inst_elem[0]) == "project":
901
- if len(inst_elem) >= 2:
902
- sheet_data["project_name"] = str(inst_elem[1])
903
- for path_elem in inst_elem[2:]:
904
- if isinstance(path_elem, list) and str(path_elem[0]) == "path":
905
- for page_elem in path_elem[1:]:
906
- if isinstance(page_elem, list) and str(page_elem[0]) == "page":
907
- sheet_data["page_number"] = (
908
- str(page_elem[1]) if len(page_elem) > 1 else "2"
909
- )
910
-
911
- return sheet_data
912
-
476
+ return self._sheet_parser._parse_sheet(item)
913
477
  def _parse_sheet_pin_for_read(self, item: List[Any]) -> Optional[Dict[str, Any]]:
914
478
  """Parse a sheet pin (for reading during sheet parsing)."""
915
- # Format: (pin "name" type (at x y rotation) (uuid ...) (effects ...))
916
- if len(item) < 3:
917
- return None
918
-
919
- pin_data = {
920
- "name": str(item[1]),
921
- "pin_type": str(item[2]) if len(item) > 2 else "input",
922
- "position": {"x": 0, "y": 0},
923
- "rotation": 0,
924
- "size": 1.27,
925
- "justify": "right",
926
- "uuid": None,
927
- }
928
-
929
- for elem in item[3:]:
930
- if not isinstance(elem, list):
931
- continue
932
-
933
- elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
934
-
935
- if elem_type == "at":
936
- if len(elem) >= 3:
937
- pin_data["position"] = {"x": float(elem[1]), "y": float(elem[2])}
938
- if len(elem) >= 4:
939
- pin_data["rotation"] = float(elem[3])
940
- elif elem_type == "uuid":
941
- pin_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
942
- elif elem_type == "effects":
943
- for effect_elem in elem[1:]:
944
- if isinstance(effect_elem, list):
945
- effect_type = str(effect_elem[0])
946
- if effect_type == "font":
947
- for font_elem in effect_elem[1:]:
948
- if isinstance(font_elem, list) and str(font_elem[0]) == "size":
949
- if len(font_elem) >= 2:
950
- pin_data["size"] = float(font_elem[1])
951
- elif effect_type == "justify":
952
- if len(effect_elem) >= 2:
953
- pin_data["justify"] = str(effect_elem[1])
954
-
955
- return pin_data
956
-
479
+ return self._sheet_parser._parse_sheet_pin_for_read(item)
957
480
  def _parse_polyline(self, item: List[Any]) -> Optional[Dict[str, Any]]:
958
481
  """Parse a polyline graphical element."""
959
- # Format: (polyline (pts (xy x1 y1) (xy x2 y2) ...) (stroke ...) (uuid ...))
960
- polyline_data = {"points": [], "stroke_width": 0, "stroke_type": "default", "uuid": None}
961
-
962
- for elem in item[1:]:
963
- if not isinstance(elem, list):
964
- continue
965
-
966
- elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
967
-
968
- if elem_type == "pts":
969
- for pt in elem[1:]:
970
- if isinstance(pt, list) and len(pt) >= 3 and str(pt[0]) == "xy":
971
- polyline_data["points"].append({"x": float(pt[1]), "y": float(pt[2])})
972
- elif elem_type == "stroke":
973
- for stroke_elem in elem[1:]:
974
- if isinstance(stroke_elem, list):
975
- stroke_type = str(stroke_elem[0])
976
- if stroke_type == "width" and len(stroke_elem) >= 2:
977
- polyline_data["stroke_width"] = float(stroke_elem[1])
978
- elif stroke_type == "type" and len(stroke_elem) >= 2:
979
- polyline_data["stroke_type"] = str(stroke_elem[1])
980
- elif elem_type == "uuid":
981
- polyline_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
982
-
983
- return polyline_data if polyline_data["points"] else None
984
-
482
+ return self._graphics_parser._parse_polyline(item)
985
483
  def _parse_arc(self, item: List[Any]) -> Optional[Dict[str, Any]]:
986
484
  """Parse an arc graphical element."""
987
- # Format: (arc (start x y) (mid x y) (end x y) (stroke ...) (fill ...) (uuid ...))
988
- arc_data = {
989
- "start": {"x": 0, "y": 0},
990
- "mid": {"x": 0, "y": 0},
991
- "end": {"x": 0, "y": 0},
992
- "stroke_width": 0,
993
- "stroke_type": "default",
994
- "fill_type": "none",
995
- "uuid": None,
996
- }
997
-
998
- for elem in item[1:]:
999
- if not isinstance(elem, list):
1000
- continue
1001
-
1002
- elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
1003
-
1004
- if elem_type == "start" and len(elem) >= 3:
1005
- arc_data["start"] = {"x": float(elem[1]), "y": float(elem[2])}
1006
- elif elem_type == "mid" and len(elem) >= 3:
1007
- arc_data["mid"] = {"x": float(elem[1]), "y": float(elem[2])}
1008
- elif elem_type == "end" and len(elem) >= 3:
1009
- arc_data["end"] = {"x": float(elem[1]), "y": float(elem[2])}
1010
- elif elem_type == "stroke":
1011
- for stroke_elem in elem[1:]:
1012
- if isinstance(stroke_elem, list):
1013
- stroke_type = str(stroke_elem[0])
1014
- if stroke_type == "width" and len(stroke_elem) >= 2:
1015
- arc_data["stroke_width"] = float(stroke_elem[1])
1016
- elif stroke_type == "type" and len(stroke_elem) >= 2:
1017
- arc_data["stroke_type"] = str(stroke_elem[1])
1018
- elif elem_type == "fill":
1019
- for fill_elem in elem[1:]:
1020
- if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
1021
- arc_data["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
1022
- elif elem_type == "uuid":
1023
- arc_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
1024
-
1025
- return arc_data
1026
-
485
+ return self._graphics_parser._parse_arc(item)
1027
486
  def _parse_circle(self, item: List[Any]) -> Optional[Dict[str, Any]]:
1028
487
  """Parse a circle graphical element."""
1029
- # Format: (circle (center x y) (radius r) (stroke ...) (fill ...) (uuid ...))
1030
- circle_data = {
1031
- "center": {"x": 0, "y": 0},
1032
- "radius": 0,
1033
- "stroke_width": 0,
1034
- "stroke_type": "default",
1035
- "fill_type": "none",
1036
- "uuid": None,
1037
- }
1038
-
1039
- for elem in item[1:]:
1040
- if not isinstance(elem, list):
1041
- continue
1042
-
1043
- elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
1044
-
1045
- if elem_type == "center" and len(elem) >= 3:
1046
- circle_data["center"] = {"x": float(elem[1]), "y": float(elem[2])}
1047
- elif elem_type == "radius" and len(elem) >= 2:
1048
- circle_data["radius"] = float(elem[1])
1049
- elif elem_type == "stroke":
1050
- for stroke_elem in elem[1:]:
1051
- if isinstance(stroke_elem, list):
1052
- stroke_type = str(stroke_elem[0])
1053
- if stroke_type == "width" and len(stroke_elem) >= 2:
1054
- circle_data["stroke_width"] = float(stroke_elem[1])
1055
- elif stroke_type == "type" and len(stroke_elem) >= 2:
1056
- circle_data["stroke_type"] = str(stroke_elem[1])
1057
- elif elem_type == "fill":
1058
- for fill_elem in elem[1:]:
1059
- if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
1060
- circle_data["fill_type"] = (
1061
- str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
1062
- )
1063
- elif elem_type == "uuid":
1064
- circle_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
1065
-
1066
- return circle_data
1067
-
488
+ return self._graphics_parser._parse_circle(item)
1068
489
  def _parse_bezier(self, item: List[Any]) -> Optional[Dict[str, Any]]:
1069
490
  """Parse a bezier curve graphical element."""
1070
- # Format: (bezier (pts (xy x1 y1) (xy x2 y2) ...) (stroke ...) (fill ...) (uuid ...))
1071
- bezier_data = {
1072
- "points": [],
1073
- "stroke_width": 0,
1074
- "stroke_type": "default",
1075
- "fill_type": "none",
1076
- "uuid": None,
1077
- }
1078
-
1079
- for elem in item[1:]:
1080
- if not isinstance(elem, list):
1081
- continue
1082
-
1083
- elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
1084
-
1085
- if elem_type == "pts":
1086
- for pt in elem[1:]:
1087
- if isinstance(pt, list) and len(pt) >= 3 and str(pt[0]) == "xy":
1088
- bezier_data["points"].append({"x": float(pt[1]), "y": float(pt[2])})
1089
- elif elem_type == "stroke":
1090
- for stroke_elem in elem[1:]:
1091
- if isinstance(stroke_elem, list):
1092
- stroke_type = str(stroke_elem[0])
1093
- if stroke_type == "width" and len(stroke_elem) >= 2:
1094
- bezier_data["stroke_width"] = float(stroke_elem[1])
1095
- elif stroke_type == "type" and len(stroke_elem) >= 2:
1096
- bezier_data["stroke_type"] = str(stroke_elem[1])
1097
- elif elem_type == "fill":
1098
- for fill_elem in elem[1:]:
1099
- if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
1100
- bezier_data["fill_type"] = (
1101
- str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
1102
- )
1103
- elif elem_type == "uuid":
1104
- bezier_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
1105
-
1106
- return bezier_data if bezier_data["points"] else None
1107
-
491
+ return self._graphics_parser._parse_bezier(item)
1108
492
  def _parse_rectangle(self, item: List[Any]) -> Optional[Dict[str, Any]]:
1109
493
  """Parse a rectangle graphical element."""
1110
- rectangle = {}
1111
-
1112
- for elem in item[1:]:
1113
- if not isinstance(elem, list):
1114
- continue
1115
-
1116
- elem_type = str(elem[0])
1117
-
1118
- if elem_type == "start" and len(elem) >= 3:
1119
- rectangle["start"] = {"x": float(elem[1]), "y": float(elem[2])}
1120
- elif elem_type == "end" and len(elem) >= 3:
1121
- rectangle["end"] = {"x": float(elem[1]), "y": float(elem[2])}
1122
- elif elem_type == "stroke":
1123
- for stroke_elem in elem[1:]:
1124
- if isinstance(stroke_elem, list):
1125
- stroke_type = str(stroke_elem[0])
1126
- if stroke_type == "width" and len(stroke_elem) >= 2:
1127
- rectangle["stroke_width"] = float(stroke_elem[1])
1128
- elif stroke_type == "type" and len(stroke_elem) >= 2:
1129
- rectangle["stroke_type"] = str(stroke_elem[1])
1130
- elif elem_type == "fill":
1131
- for fill_elem in elem[1:]:
1132
- if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
1133
- rectangle["fill_type"] = (
1134
- str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
1135
- )
1136
- elif elem_type == "uuid" and len(elem) >= 2:
1137
- rectangle["uuid"] = str(elem[1])
1138
-
1139
- return rectangle if rectangle else None
1140
-
494
+ return self._graphics_parser._parse_rectangle(item)
1141
495
  def _parse_image(self, item: List[Any]) -> Optional[Dict[str, Any]]:
1142
496
  """Parse an image element."""
1143
- # Format: (image (at x y) (uuid "...") (data "base64..."))
1144
- image = {"position": {"x": 0, "y": 0}, "data": "", "scale": 1.0, "uuid": None}
1145
-
1146
- for elem in item[1:]:
1147
- if not isinstance(elem, list):
1148
- continue
1149
-
1150
- elem_type = str(elem[0]) if isinstance(elem[0], sexpdata.Symbol) else None
1151
-
1152
- if elem_type == "at" and len(elem) >= 3:
1153
- image["position"] = {"x": float(elem[1]), "y": float(elem[2])}
1154
- elif elem_type == "scale" and len(elem) >= 2:
1155
- image["scale"] = float(elem[1])
1156
- elif elem_type == "data" and len(elem) >= 2:
1157
- # The data can be spread across multiple string elements
1158
- data_parts = []
1159
- for data_elem in elem[1:]:
1160
- data_parts.append(str(data_elem).strip('"'))
1161
- image["data"] = "".join(data_parts)
1162
- elif elem_type == "uuid" and len(elem) >= 2:
1163
- image["uuid"] = str(elem[1]).strip('"')
1164
-
1165
- return image if image.get("uuid") and image.get("data") else None
1166
-
497
+ return self._graphics_parser._parse_image(item)
1167
498
  def _parse_lib_symbols(self, item: List[Any]) -> Dict[str, Any]:
1168
499
  """Parse lib_symbols section."""
1169
- # Implementation for lib_symbols parsing
1170
- return {}
1171
-
1172
- # Conversion methods from internal format to S-expression
500
+ return self._library_parser._parse_lib_symbols(item)
1173
501
  def _title_block_to_sexp(self, title_block: Dict[str, Any]) -> List[Any]:
1174
502
  """Convert title block to S-expression."""
1175
- sexp = [sexpdata.Symbol("title_block")]
1176
-
1177
- # Add standard fields
1178
- for key in ["title", "date", "rev", "company"]:
1179
- if key in title_block and title_block[key]:
1180
- sexp.append([sexpdata.Symbol(key), title_block[key]])
1181
-
1182
- # Add comments with special formatting
1183
- comments = title_block.get("comments", {})
1184
- if isinstance(comments, dict):
1185
- for comment_num, comment_text in comments.items():
1186
- sexp.append([sexpdata.Symbol("comment"), comment_num, comment_text])
1187
-
1188
- return sexp
1189
-
503
+ return self._metadata_parser._title_block_to_sexp(title_block)
1190
504
  def _symbol_to_sexp(self, symbol_data: Dict[str, Any], schematic_uuid: str = None) -> List[Any]:
1191
505
  """Convert symbol to S-expression."""
1192
- sexp = [sexpdata.Symbol("symbol")]
1193
-
1194
- if symbol_data.get("lib_id"):
1195
- sexp.append([sexpdata.Symbol("lib_id"), symbol_data["lib_id"]])
1196
-
1197
- # Add position and rotation (preserve original format)
1198
- pos = symbol_data.get("position", Point(0, 0))
1199
- rotation = symbol_data.get("rotation", 0)
1200
- # Format numbers as integers if they are whole numbers
1201
- x = int(pos.x) if pos.x == int(pos.x) else pos.x
1202
- y = int(pos.y) if pos.y == int(pos.y) else pos.y
1203
- r = int(rotation) if rotation == int(rotation) else rotation
1204
- # Always include rotation for format consistency with KiCAD
1205
- sexp.append([sexpdata.Symbol("at"), x, y, r])
1206
-
1207
- # Add unit (required by KiCAD)
1208
- unit = symbol_data.get("unit", 1)
1209
- sexp.append([sexpdata.Symbol("unit"), unit])
1210
-
1211
- # Add simulation and board settings (required by KiCAD)
1212
- sexp.append([sexpdata.Symbol("exclude_from_sim"), "no"])
1213
- sexp.append([sexpdata.Symbol("in_bom"), "yes" if symbol_data.get("in_bom", True) else "no"])
1214
- sexp.append(
1215
- [sexpdata.Symbol("on_board"), "yes" if symbol_data.get("on_board", True) else "no"]
1216
- )
1217
- sexp.append([sexpdata.Symbol("dnp"), "no"])
1218
- sexp.append([sexpdata.Symbol("fields_autoplaced"), "yes"])
1219
-
1220
- if symbol_data.get("uuid"):
1221
- sexp.append([sexpdata.Symbol("uuid"), symbol_data["uuid"]])
1222
-
1223
- # Add properties with proper positioning and effects
1224
- lib_id = symbol_data.get("lib_id", "")
1225
- is_power_symbol = "power:" in lib_id
1226
-
1227
- if symbol_data.get("reference"):
1228
- # Power symbol references should be hidden by default
1229
- ref_hide = is_power_symbol
1230
- ref_prop = self._create_property_with_positioning(
1231
- "Reference", symbol_data["reference"], pos, 0, "left", hide=ref_hide
1232
- )
1233
- sexp.append(ref_prop)
1234
-
1235
- if symbol_data.get("value"):
1236
- # Power symbol values need different positioning
1237
- if is_power_symbol:
1238
- val_prop = self._create_power_symbol_value_property(
1239
- symbol_data["value"], pos, lib_id
1240
- )
1241
- else:
1242
- val_prop = self._create_property_with_positioning(
1243
- "Value", symbol_data["value"], pos, 1, "left"
1244
- )
1245
- sexp.append(val_prop)
1246
-
1247
- footprint = symbol_data.get("footprint")
1248
- if footprint is not None: # Include empty strings but not None
1249
- fp_prop = self._create_property_with_positioning(
1250
- "Footprint", footprint, pos, 2, "left", hide=True
1251
- )
1252
- sexp.append(fp_prop)
1253
-
1254
- for prop_name, prop_value in symbol_data.get("properties", {}).items():
1255
- escaped_value = str(prop_value).replace('"', '\\"')
1256
- prop = self._create_property_with_positioning(
1257
- prop_name, escaped_value, pos, 3, "left", hide=True
1258
- )
1259
- sexp.append(prop)
1260
-
1261
- # Add pin UUID assignments (required by KiCAD)
1262
- for pin in symbol_data.get("pins", []):
1263
- pin_uuid = str(uuid.uuid4())
1264
- # Ensure pin number is a string for proper quoting
1265
- pin_number = str(pin.number)
1266
- sexp.append([sexpdata.Symbol("pin"), pin_number, [sexpdata.Symbol("uuid"), pin_uuid]])
1267
-
1268
- # Add instances section (required by KiCAD)
1269
- from .config import config
1270
-
1271
- # Get project name from config or properties
1272
- project_name = symbol_data.get("properties", {}).get("project_name")
1273
- if not project_name:
1274
- project_name = getattr(self, "project_name", config.defaults.project_name)
1275
-
1276
- # CRITICAL FIX: Use the FULL hierarchy_path from properties if available
1277
- # For hierarchical schematics, this contains the complete path: /root_uuid/sheet_symbol_uuid/...
1278
- # This ensures KiCad can properly annotate components in sub-sheets
1279
- hierarchy_path = symbol_data.get("properties", {}).get("hierarchy_path")
1280
- if hierarchy_path:
1281
- # Use the full hierarchical path (includes root + all sheet symbols)
1282
- instance_path = hierarchy_path
1283
- logger.debug(
1284
- f"🔧 Using FULL hierarchy_path: {instance_path} for component {symbol_data.get('reference', 'unknown')}"
1285
- )
1286
- else:
1287
- # Fallback: use root_uuid or schematic_uuid for flat designs
1288
- root_uuid = (
1289
- symbol_data.get("properties", {}).get("root_uuid")
1290
- or schematic_uuid
1291
- or str(uuid.uuid4())
1292
- )
1293
- instance_path = f"/{root_uuid}"
1294
- logger.debug(
1295
- f"🔧 Using root UUID path: {instance_path} for component {symbol_data.get('reference', 'unknown')}"
1296
- )
1297
-
1298
- logger.debug(
1299
- f"🔧 Component properties keys: {list(symbol_data.get('properties', {}).keys())}"
1300
- )
1301
- logger.debug(f"🔧 Using project name: '{project_name}'")
1302
-
1303
- sexp.append(
1304
- [
1305
- sexpdata.Symbol("instances"),
1306
- [
1307
- sexpdata.Symbol("project"),
1308
- project_name,
1309
- [
1310
- sexpdata.Symbol("path"),
1311
- instance_path,
1312
- [sexpdata.Symbol("reference"), symbol_data.get("reference", "U?")],
1313
- [sexpdata.Symbol("unit"), symbol_data.get("unit", 1)],
1314
- ],
1315
- ],
1316
- ]
1317
- )
1318
-
1319
- return sexp
1320
-
506
+ return self._symbol_parser._symbol_to_sexp(symbol_data, schematic_uuid)
1321
507
  def _create_property_with_positioning(
1322
508
  self,
1323
509
  prop_name: str,
@@ -1401,950 +587,74 @@ class SExpressionParser:
1401
587
 
1402
588
  def _wire_to_sexp(self, wire_data: Dict[str, Any]) -> List[Any]:
1403
589
  """Convert wire to S-expression."""
1404
- sexp = [sexpdata.Symbol("wire")]
1405
-
1406
- # Add points (pts section)
1407
- points = wire_data.get("points", [])
1408
- if len(points) >= 2:
1409
- pts_sexp = [sexpdata.Symbol("pts")]
1410
- for point in points:
1411
- if isinstance(point, dict):
1412
- x, y = point["x"], point["y"]
1413
- elif isinstance(point, (list, tuple)) and len(point) >= 2:
1414
- x, y = point[0], point[1]
1415
- else:
1416
- # Assume it's a Point object
1417
- x, y = point.x, point.y
1418
-
1419
- # Format coordinates properly (avoid unnecessary .0 for integers)
1420
- if isinstance(x, float) and x.is_integer():
1421
- x = int(x)
1422
- if isinstance(y, float) and y.is_integer():
1423
- y = int(y)
1424
-
1425
- pts_sexp.append([sexpdata.Symbol("xy"), x, y])
1426
- sexp.append(pts_sexp)
1427
-
1428
- # Add stroke information
1429
- stroke_width = wire_data.get("stroke_width", 0)
1430
- stroke_type = wire_data.get("stroke_type", "default")
1431
- stroke_sexp = [sexpdata.Symbol("stroke")]
1432
-
1433
- # Format stroke width (use int for 0, preserve float for others)
1434
- if isinstance(stroke_width, float) and stroke_width == 0.0:
1435
- stroke_width = 0
1436
-
1437
- stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
1438
- stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
1439
- sexp.append(stroke_sexp)
1440
-
1441
- # Add UUID
1442
- if "uuid" in wire_data:
1443
- sexp.append([sexpdata.Symbol("uuid"), wire_data["uuid"]])
1444
-
1445
- return sexp
1446
-
590
+ return self._wire_parser._wire_to_sexp(wire_data)
1447
591
  def _junction_to_sexp(self, junction_data: Dict[str, Any]) -> List[Any]:
1448
592
  """Convert junction to S-expression."""
1449
- sexp = [sexpdata.Symbol("junction")]
1450
-
1451
- # Add position
1452
- pos = junction_data["position"]
1453
- if isinstance(pos, dict):
1454
- x, y = pos["x"], pos["y"]
1455
- elif isinstance(pos, (list, tuple)) and len(pos) >= 2:
1456
- x, y = pos[0], pos[1]
1457
- else:
1458
- # Assume it's a Point object
1459
- x, y = pos.x, pos.y
1460
-
1461
- # Format coordinates properly
1462
- if isinstance(x, float) and x.is_integer():
1463
- x = int(x)
1464
- if isinstance(y, float) and y.is_integer():
1465
- y = int(y)
1466
-
1467
- sexp.append([sexpdata.Symbol("at"), x, y])
1468
-
1469
- # Add diameter
1470
- diameter = junction_data.get("diameter", 0)
1471
- sexp.append([sexpdata.Symbol("diameter"), diameter])
1472
-
1473
- # Add color (RGBA)
1474
- color = junction_data.get("color", (0, 0, 0, 0))
1475
- if isinstance(color, (list, tuple)) and len(color) >= 4:
1476
- sexp.append([sexpdata.Symbol("color"), color[0], color[1], color[2], color[3]])
1477
- else:
1478
- sexp.append([sexpdata.Symbol("color"), 0, 0, 0, 0])
1479
-
1480
- # Add UUID
1481
- if "uuid" in junction_data:
1482
- sexp.append([sexpdata.Symbol("uuid"), junction_data["uuid"]])
1483
-
1484
- return sexp
1485
-
593
+ return self._wire_parser._junction_to_sexp(junction_data)
1486
594
  def _label_to_sexp(self, label_data: Dict[str, Any]) -> List[Any]:
1487
595
  """Convert local label to S-expression."""
1488
- sexp = [sexpdata.Symbol("label"), label_data["text"]]
1489
-
1490
- # Add position
1491
- pos = label_data["position"]
1492
- x, y = pos["x"], pos["y"]
1493
- rotation = label_data.get("rotation", 0)
1494
-
1495
- # Format coordinates properly
1496
- if isinstance(x, float) and x.is_integer():
1497
- x = int(x)
1498
- if isinstance(y, float) and y.is_integer():
1499
- y = int(y)
1500
-
1501
- sexp.append([sexpdata.Symbol("at"), x, y, rotation])
1502
-
1503
- # Add effects (font properties)
1504
- size = label_data.get("size", 1.27)
1505
- effects = [sexpdata.Symbol("effects")]
1506
- font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
1507
- effects.append(font)
1508
- effects.append(
1509
- [sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("bottom")]
1510
- )
1511
- sexp.append(effects)
1512
-
1513
- # Add UUID
1514
- if "uuid" in label_data:
1515
- sexp.append([sexpdata.Symbol("uuid"), label_data["uuid"]])
1516
-
1517
- return sexp
1518
-
596
+ return self._label_parser._label_to_sexp(label_data)
1519
597
  def _hierarchical_label_to_sexp(self, hlabel_data: Dict[str, Any]) -> List[Any]:
1520
598
  """Convert hierarchical label to S-expression."""
1521
- sexp = [sexpdata.Symbol("hierarchical_label"), hlabel_data["text"]]
1522
-
1523
- # Add shape
1524
- shape = hlabel_data.get("shape", "input")
1525
- sexp.append([sexpdata.Symbol("shape"), sexpdata.Symbol(shape)])
1526
-
1527
- # Add position
1528
- pos = hlabel_data["position"]
1529
- x, y = pos["x"], pos["y"]
1530
- rotation = hlabel_data.get("rotation", 0)
1531
- sexp.append([sexpdata.Symbol("at"), x, y, rotation])
1532
-
1533
- # Add effects (font properties)
1534
- size = hlabel_data.get("size", 1.27)
1535
- effects = [sexpdata.Symbol("effects")]
1536
- font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
1537
- effects.append(font)
1538
-
1539
- # Use justification from data if provided, otherwise default to "left"
1540
- justify = hlabel_data.get("justify", "left")
1541
- effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol(justify)])
1542
- sexp.append(effects)
1543
-
1544
- # Add UUID
1545
- if "uuid" in hlabel_data:
1546
- sexp.append([sexpdata.Symbol("uuid"), hlabel_data["uuid"]])
1547
-
1548
- return sexp
1549
-
599
+ return self._label_parser._hierarchical_label_to_sexp(hlabel_data)
1550
600
  def _no_connect_to_sexp(self, no_connect_data: Dict[str, Any]) -> List[Any]:
1551
601
  """Convert no_connect to S-expression."""
1552
- sexp = [sexpdata.Symbol("no_connect")]
1553
-
1554
- # Add position
1555
- pos = no_connect_data["position"]
1556
- x, y = pos["x"], pos["y"]
1557
-
1558
- # Format coordinates properly
1559
- if isinstance(x, float) and x.is_integer():
1560
- x = int(x)
1561
- if isinstance(y, float) and y.is_integer():
1562
- y = int(y)
1563
-
1564
- sexp.append([sexpdata.Symbol("at"), x, y])
1565
-
1566
- # Add UUID
1567
- if "uuid" in no_connect_data:
1568
- sexp.append([sexpdata.Symbol("uuid"), no_connect_data["uuid"]])
1569
-
1570
- return sexp
1571
-
602
+ return self._wire_parser._no_connect_to_sexp(no_connect_data)
1572
603
  def _polyline_to_sexp(self, polyline_data: Dict[str, Any]) -> List[Any]:
1573
604
  """Convert polyline to S-expression."""
1574
- sexp = [sexpdata.Symbol("polyline")]
1575
-
1576
- # Add points
1577
- points = polyline_data.get("points", [])
1578
- if points:
1579
- pts_sexp = [sexpdata.Symbol("pts")]
1580
- for point in points:
1581
- x, y = point["x"], point["y"]
1582
- # Format coordinates properly
1583
- if isinstance(x, float) and x.is_integer():
1584
- x = int(x)
1585
- if isinstance(y, float) and y.is_integer():
1586
- y = int(y)
1587
- pts_sexp.append([sexpdata.Symbol("xy"), x, y])
1588
- sexp.append(pts_sexp)
1589
-
1590
- # Add stroke
1591
- stroke_width = polyline_data.get("stroke_width", 0)
1592
- stroke_type = polyline_data.get("stroke_type", "default")
1593
- stroke_sexp = [sexpdata.Symbol("stroke")]
1594
- stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
1595
- stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
1596
- sexp.append(stroke_sexp)
1597
-
1598
- # Add UUID
1599
- if "uuid" in polyline_data:
1600
- sexp.append([sexpdata.Symbol("uuid"), polyline_data["uuid"]])
1601
-
1602
- return sexp
1603
-
605
+ return self._graphics_parser._polyline_to_sexp(polyline_data)
1604
606
  def _arc_to_sexp(self, arc_data: Dict[str, Any]) -> List[Any]:
1605
607
  """Convert arc to S-expression."""
1606
- sexp = [sexpdata.Symbol("arc")]
1607
-
1608
- # Add start, mid, end points
1609
- for point_name in ["start", "mid", "end"]:
1610
- point = arc_data.get(point_name, {"x": 0, "y": 0})
1611
- x, y = point["x"], point["y"]
1612
- # Format coordinates properly
1613
- if isinstance(x, float) and x.is_integer():
1614
- x = int(x)
1615
- if isinstance(y, float) and y.is_integer():
1616
- y = int(y)
1617
- sexp.append([sexpdata.Symbol(point_name), x, y])
1618
-
1619
- # Add stroke
1620
- stroke_width = arc_data.get("stroke_width", 0)
1621
- stroke_type = arc_data.get("stroke_type", "default")
1622
- stroke_sexp = [sexpdata.Symbol("stroke")]
1623
- stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
1624
- stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
1625
- sexp.append(stroke_sexp)
1626
-
1627
- # Add fill
1628
- fill_type = arc_data.get("fill_type", "none")
1629
- fill_sexp = [sexpdata.Symbol("fill")]
1630
- fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
1631
- sexp.append(fill_sexp)
1632
-
1633
- # Add UUID
1634
- if "uuid" in arc_data:
1635
- sexp.append([sexpdata.Symbol("uuid"), arc_data["uuid"]])
1636
-
1637
- return sexp
1638
-
608
+ return self._graphics_parser._arc_to_sexp(arc_data)
1639
609
  def _circle_to_sexp(self, circle_data: Dict[str, Any]) -> List[Any]:
1640
610
  """Convert circle to S-expression."""
1641
- sexp = [sexpdata.Symbol("circle")]
1642
-
1643
- # Add center
1644
- center = circle_data.get("center", {"x": 0, "y": 0})
1645
- x, y = center["x"], center["y"]
1646
- # Format coordinates properly
1647
- if isinstance(x, float) and x.is_integer():
1648
- x = int(x)
1649
- if isinstance(y, float) and y.is_integer():
1650
- y = int(y)
1651
- sexp.append([sexpdata.Symbol("center"), x, y])
1652
-
1653
- # Add radius
1654
- radius = circle_data.get("radius", 0)
1655
- sexp.append([sexpdata.Symbol("radius"), radius])
1656
-
1657
- # Add stroke
1658
- stroke_width = circle_data.get("stroke_width", 0)
1659
- stroke_type = circle_data.get("stroke_type", "default")
1660
- stroke_sexp = [sexpdata.Symbol("stroke")]
1661
- stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
1662
- stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
1663
- sexp.append(stroke_sexp)
1664
-
1665
- # Add fill
1666
- fill_type = circle_data.get("fill_type", "none")
1667
- fill_sexp = [sexpdata.Symbol("fill")]
1668
- fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
1669
- sexp.append(fill_sexp)
1670
-
1671
- # Add UUID
1672
- if "uuid" in circle_data:
1673
- sexp.append([sexpdata.Symbol("uuid"), circle_data["uuid"]])
1674
-
1675
- return sexp
1676
-
611
+ return self._graphics_parser._circle_to_sexp(circle_data)
1677
612
  def _bezier_to_sexp(self, bezier_data: Dict[str, Any]) -> List[Any]:
1678
613
  """Convert bezier curve to S-expression."""
1679
- sexp = [sexpdata.Symbol("bezier")]
1680
-
1681
- # Add points
1682
- points = bezier_data.get("points", [])
1683
- if points:
1684
- pts_sexp = [sexpdata.Symbol("pts")]
1685
- for point in points:
1686
- x, y = point["x"], point["y"]
1687
- # Format coordinates properly
1688
- if isinstance(x, float) and x.is_integer():
1689
- x = int(x)
1690
- if isinstance(y, float) and y.is_integer():
1691
- y = int(y)
1692
- pts_sexp.append([sexpdata.Symbol("xy"), x, y])
1693
- sexp.append(pts_sexp)
1694
-
1695
- # Add stroke
1696
- stroke_width = bezier_data.get("stroke_width", 0)
1697
- stroke_type = bezier_data.get("stroke_type", "default")
1698
- stroke_sexp = [sexpdata.Symbol("stroke")]
1699
- stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
1700
- stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
1701
- sexp.append(stroke_sexp)
1702
-
1703
- # Add fill
1704
- fill_type = bezier_data.get("fill_type", "none")
1705
- fill_sexp = [sexpdata.Symbol("fill")]
1706
- fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
1707
- sexp.append(fill_sexp)
1708
-
1709
- # Add UUID
1710
- if "uuid" in bezier_data:
1711
- sexp.append([sexpdata.Symbol("uuid"), bezier_data["uuid"]])
1712
-
1713
- return sexp
1714
-
614
+ return self._graphics_parser._bezier_to_sexp(bezier_data)
1715
615
  def _sheet_to_sexp(self, sheet_data: Dict[str, Any], schematic_uuid: str) -> List[Any]:
1716
616
  """Convert hierarchical sheet to S-expression."""
1717
- sexp = [sexpdata.Symbol("sheet")]
1718
-
1719
- # Add position
1720
- pos = sheet_data["position"]
1721
- x, y = pos["x"], pos["y"]
1722
- if isinstance(x, float) and x.is_integer():
1723
- x = int(x)
1724
- if isinstance(y, float) and y.is_integer():
1725
- y = int(y)
1726
- sexp.append([sexpdata.Symbol("at"), x, y])
1727
-
1728
- # Add size
1729
- size = sheet_data["size"]
1730
- w, h = size["width"], size["height"]
1731
- sexp.append([sexpdata.Symbol("size"), w, h])
1732
-
1733
- # Add basic properties
1734
- sexp.append(
1735
- [
1736
- sexpdata.Symbol("exclude_from_sim"),
1737
- sexpdata.Symbol("yes" if sheet_data.get("exclude_from_sim", False) else "no"),
1738
- ]
1739
- )
1740
- sexp.append(
1741
- [
1742
- sexpdata.Symbol("in_bom"),
1743
- sexpdata.Symbol("yes" if sheet_data.get("in_bom", True) else "no"),
1744
- ]
1745
- )
1746
- sexp.append(
1747
- [
1748
- sexpdata.Symbol("on_board"),
1749
- sexpdata.Symbol("yes" if sheet_data.get("on_board", True) else "no"),
1750
- ]
1751
- )
1752
- sexp.append(
1753
- [
1754
- sexpdata.Symbol("dnp"),
1755
- sexpdata.Symbol("yes" if sheet_data.get("dnp", False) else "no"),
1756
- ]
1757
- )
1758
- sexp.append(
1759
- [
1760
- sexpdata.Symbol("fields_autoplaced"),
1761
- sexpdata.Symbol("yes" if sheet_data.get("fields_autoplaced", True) else "no"),
1762
- ]
1763
- )
1764
-
1765
- # Add stroke
1766
- stroke_width = sheet_data.get("stroke_width", 0.1524)
1767
- stroke_type = sheet_data.get("stroke_type", "solid")
1768
- stroke_sexp = [sexpdata.Symbol("stroke")]
1769
- stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
1770
- stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
1771
- sexp.append(stroke_sexp)
1772
-
1773
- # Add fill
1774
- fill_color = sheet_data.get("fill_color", (0, 0, 0, 0.0))
1775
- fill_sexp = [sexpdata.Symbol("fill")]
1776
- fill_sexp.append(
1777
- [sexpdata.Symbol("color"), fill_color[0], fill_color[1], fill_color[2], fill_color[3]]
1778
- )
1779
- sexp.append(fill_sexp)
1780
-
1781
- # Add UUID
1782
- if "uuid" in sheet_data:
1783
- sexp.append([sexpdata.Symbol("uuid"), sheet_data["uuid"]])
1784
-
1785
- # Add sheet properties (name and filename)
1786
- name = sheet_data.get("name", "Sheet")
1787
- filename = sheet_data.get("filename", "sheet.kicad_sch")
1788
-
1789
- # Sheetname property
1790
- from .config import config
1791
-
1792
- name_prop = [sexpdata.Symbol("property"), "Sheetname", name]
1793
- name_prop.append(
1794
- [sexpdata.Symbol("at"), x, round(y + config.sheet.name_offset_y, 4), 0]
1795
- ) # Above sheet
1796
- name_prop.append(
1797
- [
1798
- sexpdata.Symbol("effects"),
1799
- [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
1800
- [sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("bottom")],
1801
- ]
1802
- )
1803
- sexp.append(name_prop)
1804
-
1805
- # Sheetfile property
1806
- file_prop = [sexpdata.Symbol("property"), "Sheetfile", filename]
1807
- file_prop.append(
1808
- [sexpdata.Symbol("at"), x, round(y + h + config.sheet.file_offset_y, 4), 0]
1809
- ) # Below sheet
1810
- file_prop.append(
1811
- [
1812
- sexpdata.Symbol("effects"),
1813
- [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
1814
- [sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("top")],
1815
- ]
1816
- )
1817
- sexp.append(file_prop)
1818
-
1819
- # Add sheet pins if any
1820
- for pin in sheet_data.get("pins", []):
1821
- pin_sexp = self._sheet_pin_to_sexp(pin)
1822
- sexp.append(pin_sexp)
1823
-
1824
- # Add instances
1825
- if schematic_uuid:
1826
- instances_sexp = [sexpdata.Symbol("instances")]
1827
- project_name = sheet_data.get("project_name", "")
1828
- page_number = sheet_data.get("page_number", "2")
1829
- project_sexp = [sexpdata.Symbol("project"), project_name]
1830
- path_sexp = [sexpdata.Symbol("path"), f"/{schematic_uuid}"]
1831
- path_sexp.append([sexpdata.Symbol("page"), page_number])
1832
- project_sexp.append(path_sexp)
1833
- instances_sexp.append(project_sexp)
1834
- sexp.append(instances_sexp)
1835
-
1836
- return sexp
1837
-
617
+ return self._sheet_parser._sheet_to_sexp(sheet_data, schematic_uuid)
1838
618
  def _sheet_pin_to_sexp(self, pin_data: Dict[str, Any]) -> List[Any]:
1839
619
  """Convert sheet pin to S-expression."""
1840
- pin_sexp = [
1841
- sexpdata.Symbol("pin"),
1842
- pin_data["name"],
1843
- sexpdata.Symbol(pin_data.get("pin_type", "input")),
1844
- ]
1845
-
1846
- # Add position
1847
- pos = pin_data["position"]
1848
- x, y = pos["x"], pos["y"]
1849
- rotation = pin_data.get("rotation", 0)
1850
- pin_sexp.append([sexpdata.Symbol("at"), x, y, rotation])
1851
-
1852
- # Add UUID
1853
- if "uuid" in pin_data:
1854
- pin_sexp.append([sexpdata.Symbol("uuid"), pin_data["uuid"]])
1855
-
1856
- # Add effects
1857
- size = pin_data.get("size", 1.27)
1858
- effects = [sexpdata.Symbol("effects")]
1859
- font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
1860
- effects.append(font)
1861
- justify = pin_data.get("justify", "right")
1862
- effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol(justify)])
1863
- pin_sexp.append(effects)
1864
-
1865
- return pin_sexp
1866
-
620
+ return self._sheet_parser._sheet_pin_to_sexp(pin_data)
1867
621
  def _text_to_sexp(self, text_data: Dict[str, Any]) -> List[Any]:
1868
622
  """Convert text element to S-expression."""
1869
- sexp = [sexpdata.Symbol("text"), text_data["text"]]
1870
-
1871
- # Add exclude_from_sim
1872
- exclude_sim = text_data.get("exclude_from_sim", False)
1873
- sexp.append(
1874
- [sexpdata.Symbol("exclude_from_sim"), sexpdata.Symbol("yes" if exclude_sim else "no")]
1875
- )
1876
-
1877
- # Add position
1878
- pos = text_data["position"]
1879
- x, y = pos["x"], pos["y"]
1880
- rotation = text_data.get("rotation", 0)
1881
-
1882
- # Format coordinates properly
1883
- if isinstance(x, float) and x.is_integer():
1884
- x = int(x)
1885
- if isinstance(y, float) and y.is_integer():
1886
- y = int(y)
1887
-
1888
- sexp.append([sexpdata.Symbol("at"), x, y, rotation])
1889
-
1890
- # Add effects (font properties)
1891
- size = text_data.get("size", 1.27)
1892
- effects = [sexpdata.Symbol("effects")]
1893
- font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
1894
- effects.append(font)
1895
- sexp.append(effects)
1896
-
1897
- # Add UUID
1898
- if "uuid" in text_data:
1899
- sexp.append([sexpdata.Symbol("uuid"), text_data["uuid"]])
1900
-
1901
- return sexp
1902
-
623
+ return self._text_parser._text_to_sexp(text_data)
1903
624
  def _text_box_to_sexp(self, text_box_data: Dict[str, Any]) -> List[Any]:
1904
625
  """Convert text box element to S-expression."""
1905
- sexp = [sexpdata.Symbol("text_box"), text_box_data["text"]]
1906
-
1907
- # Add exclude_from_sim
1908
- exclude_sim = text_box_data.get("exclude_from_sim", False)
1909
- sexp.append(
1910
- [sexpdata.Symbol("exclude_from_sim"), sexpdata.Symbol("yes" if exclude_sim else "no")]
1911
- )
1912
-
1913
- # Add position
1914
- pos = text_box_data["position"]
1915
- x, y = pos["x"], pos["y"]
1916
- rotation = text_box_data.get("rotation", 0)
1917
-
1918
- # Format coordinates properly
1919
- if isinstance(x, float) and x.is_integer():
1920
- x = int(x)
1921
- if isinstance(y, float) and y.is_integer():
1922
- y = int(y)
1923
-
1924
- sexp.append([sexpdata.Symbol("at"), x, y, rotation])
1925
-
1926
- # Add size
1927
- size = text_box_data["size"]
1928
- w, h = size["width"], size["height"]
1929
- sexp.append([sexpdata.Symbol("size"), w, h])
1930
-
1931
- # Add margins
1932
- margins = text_box_data.get("margins", (0.9525, 0.9525, 0.9525, 0.9525))
1933
- sexp.append([sexpdata.Symbol("margins"), margins[0], margins[1], margins[2], margins[3]])
1934
-
1935
- # Add stroke
1936
- stroke_width = text_box_data.get("stroke_width", 0)
1937
- stroke_type = text_box_data.get("stroke_type", "solid")
1938
- stroke_sexp = [sexpdata.Symbol("stroke")]
1939
- stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
1940
- stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
1941
- sexp.append(stroke_sexp)
1942
-
1943
- # Add fill
1944
- fill_type = text_box_data.get("fill_type", "none")
1945
- fill_sexp = [sexpdata.Symbol("fill")]
1946
- fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
1947
- sexp.append(fill_sexp)
1948
-
1949
- # Add effects (font properties and justification)
1950
- font_size = text_box_data.get("font_size", 1.27)
1951
- justify_h = text_box_data.get("justify_horizontal", "left")
1952
- justify_v = text_box_data.get("justify_vertical", "top")
1953
-
1954
- effects = [sexpdata.Symbol("effects")]
1955
- font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), font_size, font_size]]
1956
- effects.append(font)
1957
- effects.append(
1958
- [sexpdata.Symbol("justify"), sexpdata.Symbol(justify_h), sexpdata.Symbol(justify_v)]
1959
- )
1960
- sexp.append(effects)
1961
-
1962
- # Add UUID
1963
- if "uuid" in text_box_data:
1964
- sexp.append([sexpdata.Symbol("uuid"), text_box_data["uuid"]])
1965
-
1966
- return sexp
1967
-
626
+ return self._text_parser._text_box_to_sexp(text_box_data)
1968
627
  def _rectangle_to_sexp(self, rectangle_data: Dict[str, Any]) -> List[Any]:
1969
628
  """Convert rectangle element to S-expression."""
1970
- sexp = [sexpdata.Symbol("rectangle")]
1971
-
1972
- # Add start point
1973
- start = rectangle_data["start"]
1974
- start_x, start_y = start["x"], start["y"]
1975
- sexp.append([sexpdata.Symbol("start"), start_x, start_y])
1976
-
1977
- # Add end point
1978
- end = rectangle_data["end"]
1979
- end_x, end_y = end["x"], end["y"]
1980
- sexp.append([sexpdata.Symbol("end"), end_x, end_y])
1981
-
1982
- # Add stroke
1983
- stroke_width = rectangle_data.get("stroke_width", 0)
1984
- stroke_type = rectangle_data.get("stroke_type", "default")
1985
- stroke_sexp = [sexpdata.Symbol("stroke")]
1986
- stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
1987
- stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
1988
- # Add stroke color if present
1989
- if "stroke_color" in rectangle_data:
1990
- r, g, b, a = rectangle_data["stroke_color"]
1991
- stroke_sexp.append([sexpdata.Symbol("color"), r, g, b, a])
1992
- sexp.append(stroke_sexp)
1993
-
1994
- # Add fill
1995
- fill_type = rectangle_data.get("fill_type", "none")
1996
- fill_sexp = [sexpdata.Symbol("fill")]
1997
- fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
1998
- # Add fill color if present
1999
- if "fill_color" in rectangle_data:
2000
- r, g, b, a = rectangle_data["fill_color"]
2001
- fill_sexp.append([sexpdata.Symbol("color"), r, g, b, a])
2002
- sexp.append(fill_sexp)
2003
-
2004
- # Add UUID
2005
- if "uuid" in rectangle_data:
2006
- sexp.append([sexpdata.Symbol("uuid"), rectangle_data["uuid"]])
2007
-
2008
- return sexp
2009
-
629
+ return self._graphics_parser._rectangle_to_sexp(rectangle_data)
2010
630
  def _image_to_sexp(self, image_data: Dict[str, Any]) -> List[Any]:
2011
631
  """Convert image element to S-expression."""
2012
- sexp = [sexpdata.Symbol("image")]
2013
-
2014
- # Add position
2015
- position = image_data.get("position", {"x": 0, "y": 0})
2016
- pos_x, pos_y = position["x"], position["y"]
2017
- sexp.append([sexpdata.Symbol("at"), pos_x, pos_y])
2018
-
2019
- # Add UUID
2020
- if "uuid" in image_data:
2021
- sexp.append([sexpdata.Symbol("uuid"), image_data["uuid"]])
2022
-
2023
- # Add scale if not default
2024
- scale = image_data.get("scale", 1.0)
2025
- if scale != 1.0:
2026
- sexp.append([sexpdata.Symbol("scale"), scale])
2027
-
2028
- # Add image data
2029
- # KiCad splits base64 data into multiple lines for readability
2030
- # Each line is roughly 76 characters (standard base64 line length)
2031
- data = image_data.get("data", "")
2032
- if data:
2033
- data_sexp = [sexpdata.Symbol("data")]
2034
- # Split the data into 76-character chunks
2035
- chunk_size = 76
2036
- for i in range(0, len(data), chunk_size):
2037
- data_sexp.append(data[i : i + chunk_size])
2038
- sexp.append(data_sexp)
2039
-
2040
- return sexp
2041
-
632
+ return self._graphics_parser._image_to_sexp(image_data)
2042
633
  def _lib_symbols_to_sexp(self, lib_symbols: Dict[str, Any]) -> List[Any]:
2043
634
  """Convert lib_symbols to S-expression."""
2044
- sexp = [sexpdata.Symbol("lib_symbols")]
2045
-
2046
- # Add each symbol definition
2047
- for symbol_name, symbol_def in lib_symbols.items():
2048
- if isinstance(symbol_def, list):
2049
- # Raw S-expression data from parsed library file - use directly
2050
- sexp.append(symbol_def)
2051
- elif isinstance(symbol_def, dict):
2052
- # Dictionary format - convert to S-expression
2053
- symbol_sexp = self._create_basic_symbol_definition(symbol_name)
2054
- sexp.append(symbol_sexp)
2055
-
2056
- return sexp
2057
-
635
+ return self._library_parser._lib_symbols_to_sexp(lib_symbols)
2058
636
  def _create_basic_symbol_definition(self, lib_id: str) -> List[Any]:
2059
637
  """Create a basic symbol definition for KiCAD compatibility."""
2060
- symbol_sexp = [sexpdata.Symbol("symbol"), lib_id]
2061
-
2062
- # Add basic symbol properties
2063
- symbol_sexp.extend(
2064
- [
2065
- [sexpdata.Symbol("pin_numbers"), [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")]],
2066
- [sexpdata.Symbol("pin_names"), [sexpdata.Symbol("offset"), 0]],
2067
- [sexpdata.Symbol("exclude_from_sim"), sexpdata.Symbol("no")],
2068
- [sexpdata.Symbol("in_bom"), sexpdata.Symbol("yes")],
2069
- [sexpdata.Symbol("on_board"), sexpdata.Symbol("yes")],
2070
- ]
2071
- )
2072
-
2073
- # Add basic properties for the symbol
2074
- if "R" in lib_id: # Resistor
2075
- symbol_sexp.extend(
2076
- [
2077
- [
2078
- sexpdata.Symbol("property"),
2079
- "Reference",
2080
- "R",
2081
- [sexpdata.Symbol("at"), 2.032, 0, 90],
2082
- [
2083
- sexpdata.Symbol("effects"),
2084
- [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
2085
- ],
2086
- ],
2087
- [
2088
- sexpdata.Symbol("property"),
2089
- "Value",
2090
- "R",
2091
- [sexpdata.Symbol("at"), 0, 0, 90],
2092
- [
2093
- sexpdata.Symbol("effects"),
2094
- [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
2095
- ],
2096
- ],
2097
- [
2098
- sexpdata.Symbol("property"),
2099
- "Footprint",
2100
- "",
2101
- [sexpdata.Symbol("at"), -1.778, 0, 90],
2102
- [
2103
- sexpdata.Symbol("effects"),
2104
- [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
2105
- [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
2106
- ],
2107
- ],
2108
- [
2109
- sexpdata.Symbol("property"),
2110
- "Datasheet",
2111
- "~",
2112
- [sexpdata.Symbol("at"), 0, 0, 0],
2113
- [
2114
- sexpdata.Symbol("effects"),
2115
- [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
2116
- [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
2117
- ],
2118
- ],
2119
- ]
2120
- )
2121
-
2122
- elif "C" in lib_id: # Capacitor
2123
- symbol_sexp.extend(
2124
- [
2125
- [
2126
- sexpdata.Symbol("property"),
2127
- "Reference",
2128
- "C",
2129
- [sexpdata.Symbol("at"), 0.635, 2.54, 0],
2130
- [
2131
- sexpdata.Symbol("effects"),
2132
- [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
2133
- ],
2134
- ],
2135
- [
2136
- sexpdata.Symbol("property"),
2137
- "Value",
2138
- "C",
2139
- [sexpdata.Symbol("at"), 0.635, -2.54, 0],
2140
- [
2141
- sexpdata.Symbol("effects"),
2142
- [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
2143
- ],
2144
- ],
2145
- [
2146
- sexpdata.Symbol("property"),
2147
- "Footprint",
2148
- "",
2149
- [sexpdata.Symbol("at"), 0, -1.27, 0],
2150
- [
2151
- sexpdata.Symbol("effects"),
2152
- [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
2153
- [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
2154
- ],
2155
- ],
2156
- [
2157
- sexpdata.Symbol("property"),
2158
- "Datasheet",
2159
- "~",
2160
- [sexpdata.Symbol("at"), 0, 0, 0],
2161
- [
2162
- sexpdata.Symbol("effects"),
2163
- [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), 1.27, 1.27]],
2164
- [sexpdata.Symbol("hide"), sexpdata.Symbol("yes")],
2165
- ],
2166
- ],
2167
- ]
2168
- )
2169
-
2170
- # Add basic graphics and pins (minimal for now)
2171
- symbol_sexp.append([sexpdata.Symbol("embedded_fonts"), sexpdata.Symbol("no")])
2172
-
2173
- return symbol_sexp
2174
-
638
+ return self._library_parser._create_basic_symbol_definition(lib_id)
2175
639
  def _parse_sheet_instances(self, item: List[Any]) -> List[Dict[str, Any]]:
2176
640
  """Parse sheet_instances section."""
2177
- sheet_instances = []
2178
- for sheet_item in item[1:]: # Skip 'sheet_instances' header
2179
- if isinstance(sheet_item, list) and len(sheet_item) > 0:
2180
- sheet_data = {"path": "/", "page": "1"}
2181
- for element in sheet_item[1:]: # Skip element header
2182
- if isinstance(element, list) and len(element) >= 2:
2183
- key = (
2184
- str(element[0])
2185
- if isinstance(element[0], sexpdata.Symbol)
2186
- else str(element[0])
2187
- )
2188
- if key == "path":
2189
- sheet_data["path"] = element[1]
2190
- elif key == "page":
2191
- sheet_data["page"] = element[1]
2192
- sheet_instances.append(sheet_data)
2193
- return sheet_instances
2194
-
641
+ return self._sheet_parser._parse_sheet_instances(item)
2195
642
  def _parse_symbol_instances(self, item: List[Any]) -> List[Any]:
2196
643
  """Parse symbol_instances section."""
2197
- # For now, just return the raw structure minus the header
2198
- return item[1:] if len(item) > 1 else []
2199
-
644
+ return self._metadata_parser._parse_symbol_instances(item)
2200
645
  def _sheet_instances_to_sexp(self, sheet_instances: List[Dict[str, Any]]) -> List[Any]:
2201
646
  """Convert sheet_instances to S-expression."""
2202
- sexp = [sexpdata.Symbol("sheet_instances")]
2203
- for sheet in sheet_instances:
2204
- # Create: (path "/" (page "1"))
2205
- sheet_sexp = [
2206
- sexpdata.Symbol("path"),
2207
- sheet.get("path", "/"),
2208
- [sexpdata.Symbol("page"), str(sheet.get("page", "1"))],
2209
- ]
2210
- sexp.append(sheet_sexp)
2211
- return sexp
2212
-
647
+ return self._sheet_parser._sheet_instances_to_sexp(sheet_instances)
2213
648
  def _graphic_to_sexp(self, graphic_data: Dict[str, Any]) -> List[Any]:
2214
649
  """Convert graphics (rectangles, etc.) to S-expression."""
2215
- # For now, we only support rectangles - this is the main graphics element we create
2216
- sexp = [sexpdata.Symbol("rectangle")]
2217
-
2218
- # Add start position
2219
- start = graphic_data.get("start", {})
2220
- start_x = start.get("x", 0)
2221
- start_y = start.get("y", 0)
2222
-
2223
- # Format coordinates properly (avoid unnecessary .0 for integers)
2224
- if isinstance(start_x, float) and start_x.is_integer():
2225
- start_x = int(start_x)
2226
- if isinstance(start_y, float) and start_y.is_integer():
2227
- start_y = int(start_y)
2228
-
2229
- sexp.append([sexpdata.Symbol("start"), start_x, start_y])
2230
-
2231
- # Add end position
2232
- end = graphic_data.get("end", {})
2233
- end_x = end.get("x", 0)
2234
- end_y = end.get("y", 0)
2235
-
2236
- # Format coordinates properly (avoid unnecessary .0 for integers)
2237
- if isinstance(end_x, float) and end_x.is_integer():
2238
- end_x = int(end_x)
2239
- if isinstance(end_y, float) and end_y.is_integer():
2240
- end_y = int(end_y)
2241
-
2242
- sexp.append([sexpdata.Symbol("end"), end_x, end_y])
2243
-
2244
- # Add stroke information (KiCAD format: width, type, and optionally color)
2245
- stroke = graphic_data.get("stroke", {})
2246
- stroke_sexp = [sexpdata.Symbol("stroke")]
2247
-
2248
- # Stroke width - default to 0 to match KiCAD behavior
2249
- stroke_width = stroke.get("width", 0)
2250
- if isinstance(stroke_width, float) and stroke_width == 0.0:
2251
- stroke_width = 0
2252
- stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
2253
-
2254
- # Stroke type - normalize to KiCAD format and validate
2255
- stroke_type = stroke.get("type", "default")
2256
-
2257
- # KiCAD only supports these exact stroke types
2258
- valid_kicad_types = {"solid", "dash", "dash_dot", "dash_dot_dot", "dot", "default"}
2259
-
2260
- # Map common variations to KiCAD format
2261
- stroke_type_map = {
2262
- "dashdot": "dash_dot",
2263
- "dash-dot": "dash_dot",
2264
- "dashdotdot": "dash_dot_dot",
2265
- "dash-dot-dot": "dash_dot_dot",
2266
- "solid": "solid",
2267
- "dash": "dash",
2268
- "dot": "dot",
2269
- "default": "default",
2270
- }
2271
-
2272
- # Normalize and validate
2273
- normalized_stroke_type = stroke_type_map.get(stroke_type.lower(), stroke_type)
2274
- if normalized_stroke_type not in valid_kicad_types:
2275
- normalized_stroke_type = "default" # Fallback to default for invalid types
2276
-
2277
- stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(normalized_stroke_type)])
2278
-
2279
- # Stroke color (if specified) - KiCAD format uses RGB 0-255 values plus alpha
2280
- stroke_color = stroke.get("color")
2281
- if stroke_color:
2282
- if isinstance(stroke_color, str):
2283
- # Convert string color names to RGB 0-255 values
2284
- color_rgb = self._color_to_rgb255(stroke_color)
2285
- stroke_sexp.append([sexpdata.Symbol("color")] + color_rgb + [1]) # Add alpha=1
2286
- elif isinstance(stroke_color, (list, tuple)) and len(stroke_color) >= 3:
2287
- # Use provided RGB values directly
2288
- stroke_sexp.append([sexpdata.Symbol("color")] + list(stroke_color))
2289
-
2290
- sexp.append(stroke_sexp)
2291
-
2292
- # Add fill information
2293
- fill = graphic_data.get("fill", {"type": "none"})
2294
- fill_type = fill.get("type", "none")
2295
- fill_sexp = [sexpdata.Symbol("fill"), [sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)]]
2296
- sexp.append(fill_sexp)
2297
-
2298
- # Add UUID (no quotes around UUID in KiCAD format)
2299
- if "uuid" in graphic_data:
2300
- uuid_str = graphic_data["uuid"]
2301
- # Remove quotes and convert to Symbol to match KiCAD format
2302
- uuid_clean = uuid_str.replace('"', "")
2303
- sexp.append([sexpdata.Symbol("uuid"), sexpdata.Symbol(uuid_clean)])
2304
-
2305
- return sexp
2306
-
650
+ return self._graphics_parser._graphic_to_sexp(graphic_data)
2307
651
  def _color_to_rgba(self, color_name: str) -> List[float]:
2308
652
  """Convert color name to RGBA values (0.0-1.0) for KiCAD compatibility."""
2309
- # Basic color mapping for common colors (0.0-1.0 range)
2310
- color_map = {
2311
- "red": [1.0, 0.0, 0.0, 1.0],
2312
- "blue": [0.0, 0.0, 1.0, 1.0],
2313
- "green": [0.0, 1.0, 0.0, 1.0],
2314
- "yellow": [1.0, 1.0, 0.0, 1.0],
2315
- "magenta": [1.0, 0.0, 1.0, 1.0],
2316
- "cyan": [0.0, 1.0, 1.0, 1.0],
2317
- "black": [0.0, 0.0, 0.0, 1.0],
2318
- "white": [1.0, 1.0, 1.0, 1.0],
2319
- "gray": [0.5, 0.5, 0.5, 1.0],
2320
- "grey": [0.5, 0.5, 0.5, 1.0],
2321
- "orange": [1.0, 0.5, 0.0, 1.0],
2322
- "purple": [0.5, 0.0, 0.5, 1.0],
2323
- }
2324
-
2325
- # Return RGBA values, default to black if color not found
2326
- return color_map.get(color_name.lower(), [0.0, 0.0, 0.0, 1.0])
653
+ return color_to_rgba(color_name)
2327
654
 
2328
655
  def _color_to_rgb255(self, color_name: str) -> List[int]:
2329
656
  """Convert color name to RGB values (0-255) for KiCAD rectangle graphics."""
2330
- # Basic color mapping for common colors (0-255 range)
2331
- color_map = {
2332
- "red": [255, 0, 0],
2333
- "blue": [0, 0, 255],
2334
- "green": [0, 255, 0],
2335
- "yellow": [255, 255, 0],
2336
- "magenta": [255, 0, 255],
2337
- "cyan": [0, 255, 255],
2338
- "black": [0, 0, 0],
2339
- "white": [255, 255, 255],
2340
- "gray": [128, 128, 128],
2341
- "grey": [128, 128, 128],
2342
- "orange": [255, 128, 0],
2343
- "purple": [128, 0, 128],
2344
- }
2345
-
2346
- # Return RGB values, default to black if color not found
2347
- return color_map.get(color_name.lower(), [0, 0, 0])
657
+ return color_to_rgb255(color_name)
2348
658
 
2349
659
  def get_validation_issues(self) -> List[ValidationIssue]:
2350
660
  """Get list of validation issues from last parse operation."""