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