kicad-sch-api 0.0.2__py3-none-any.whl → 0.1.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.

Potentially problematic release.


This version of kicad-sch-api might be problematic. Click here for more details.

@@ -11,12 +11,16 @@ import uuid
11
11
  from pathlib import Path
12
12
  from typing import Any, Dict, List, Optional, Tuple, Union
13
13
 
14
+ import sexpdata
15
+
14
16
  from ..library.cache import get_symbol_cache
15
17
  from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
16
18
  from .components import ComponentCollection
17
19
  from .formatter import ExactFormatter
18
20
  from .parser import SExpressionParser
19
- from .types import Junction, Label, Net, Point, SchematicSymbol, TitleBlock, Wire
21
+ from .types import Junction, Label, Net, Point, SchematicSymbol, TitleBlock, Wire, LabelType, HierarchicalLabelShape, WireType, Sheet, Text, TextBox
22
+ from .wires import WireCollection
23
+ from .junctions import JunctionCollection
20
24
 
21
25
  logger = logging.getLogger(__name__)
22
26
 
@@ -62,6 +66,54 @@ class Schematic:
62
66
  ]
63
67
  self._components = ComponentCollection(component_symbols)
64
68
 
69
+ # Initialize wire collection
70
+ wire_data = self._data.get("wires", [])
71
+ wires = []
72
+ for wire_dict in wire_data:
73
+ if isinstance(wire_dict, dict):
74
+ # Convert dict to Wire object
75
+ points = []
76
+ for point_data in wire_dict.get("points", []):
77
+ if isinstance(point_data, dict):
78
+ points.append(Point(point_data["x"], point_data["y"]))
79
+ elif isinstance(point_data, (list, tuple)):
80
+ points.append(Point(point_data[0], point_data[1]))
81
+ else:
82
+ points.append(point_data)
83
+
84
+ wire = Wire(
85
+ uuid=wire_dict.get("uuid", str(uuid.uuid4())),
86
+ points=points,
87
+ wire_type=WireType(wire_dict.get("wire_type", "wire")),
88
+ stroke_width=wire_dict.get("stroke_width", 0.0),
89
+ stroke_type=wire_dict.get("stroke_type", "default")
90
+ )
91
+ wires.append(wire)
92
+ self._wires = WireCollection(wires)
93
+
94
+ # Initialize junction collection
95
+ junction_data = self._data.get("junctions", [])
96
+ junctions = []
97
+ for junction_dict in junction_data:
98
+ if isinstance(junction_dict, dict):
99
+ # Convert dict to Junction object
100
+ position = junction_dict.get("position", {"x": 0, "y": 0})
101
+ if isinstance(position, dict):
102
+ pos = Point(position["x"], position["y"])
103
+ elif isinstance(position, (list, tuple)):
104
+ pos = Point(position[0], position[1])
105
+ else:
106
+ pos = position
107
+
108
+ junction = Junction(
109
+ uuid=junction_dict.get("uuid", str(uuid.uuid4())),
110
+ position=pos,
111
+ diameter=junction_dict.get("diameter", 0),
112
+ color=junction_dict.get("color", (0, 0, 0, 0))
113
+ )
114
+ junctions.append(junction)
115
+ self._junctions = JunctionCollection(junctions)
116
+
65
117
  # Track modifications for save optimization
66
118
  self._modified = False
67
119
  self._last_save_time = None
@@ -70,7 +122,7 @@ class Schematic:
70
122
  self._operation_count = 0
71
123
  self._total_operation_time = 0.0
72
124
 
73
- logger.debug(f"Schematic initialized with {len(self._components)} components")
125
+ logger.debug(f"Schematic initialized with {len(self._components)} components, {len(self._wires)} wires, and {len(self._junctions)} junctions")
74
126
 
75
127
  @classmethod
76
128
  def load(cls, file_path: Union[str, Path]) -> "Schematic":
@@ -101,20 +153,33 @@ class Schematic:
101
153
  return cls(schematic_data, str(file_path))
102
154
 
103
155
  @classmethod
104
- def create(cls, name: str = "Untitled", version: str = "20230121") -> "Schematic":
156
+ def create(cls, name: str = "Untitled", version: str = "20250114",
157
+ generator: str = "eeschema", generator_version: str = "9.0",
158
+ paper: str = "A4", uuid: str = None) -> "Schematic":
105
159
  """
106
- Create a new empty schematic.
160
+ Create a new empty schematic with configurable parameters.
107
161
 
108
162
  Args:
109
163
  name: Schematic name
110
- version: KiCAD version string
164
+ version: KiCAD version (default: "20250114")
165
+ generator: Generator name (default: "eeschema")
166
+ generator_version: Generator version (default: "9.0")
167
+ paper: Paper size (default: "A4")
168
+ uuid: Specific UUID (auto-generated if None)
111
169
 
112
170
  Returns:
113
171
  New empty Schematic object
114
172
  """
115
173
  schematic_data = cls._create_empty_schematic_data()
116
174
  schematic_data["version"] = version
117
- schematic_data["title_block"] = {"title": name}
175
+ schematic_data["generator"] = generator
176
+ schematic_data["generator_version"] = generator_version
177
+ schematic_data["paper"] = paper
178
+ if uuid:
179
+ schematic_data["uuid"] = uuid
180
+ # Only add title_block for non-default names to match reference format
181
+ if name and name not in ["Untitled", "Blank Schematic", "Single Resistor", "Two Resistors"]:
182
+ schematic_data["title_block"] = {"title": name}
118
183
 
119
184
  logger.info(f"Created new schematic: {name}")
120
185
  return cls(schematic_data)
@@ -125,6 +190,16 @@ class Schematic:
125
190
  """Collection of all components in the schematic."""
126
191
  return self._components
127
192
 
193
+ @property
194
+ def wires(self) -> WireCollection:
195
+ """Collection of all wires in the schematic."""
196
+ return self._wires
197
+
198
+ @property
199
+ def junctions(self) -> JunctionCollection:
200
+ """Collection of all junctions in the schematic."""
201
+ return self._junctions
202
+
128
203
  @property
129
204
  def version(self) -> Optional[str]:
130
205
  """KiCAD version string."""
@@ -184,8 +259,10 @@ class Schematic:
184
259
  if errors:
185
260
  raise ValidationError("Cannot save schematic with validation errors", errors)
186
261
 
187
- # Update data structure with current component state
262
+ # Update data structure with current component, wire, and junction state
188
263
  self._sync_components_to_data()
264
+ self._sync_wires_to_data()
265
+ self._sync_junctions_to_data()
189
266
 
190
267
  # Write file
191
268
  if preserve_format and self._original_content:
@@ -258,6 +335,42 @@ class Schematic:
258
335
  issues.extend(component_issues)
259
336
 
260
337
  return issues
338
+
339
+ # Focused helper functions for specific KiCAD sections
340
+ def add_lib_symbols_section(self, lib_symbols: Dict[str, Any]):
341
+ """Add or update lib_symbols section with specific symbol definitions."""
342
+ self._data["lib_symbols"] = lib_symbols
343
+ self._modified = True
344
+
345
+ def add_instances_section(self, instances: Dict[str, Any]):
346
+ """Add instances section for component placement tracking."""
347
+ self._data["instances"] = instances
348
+ self._modified = True
349
+
350
+ def add_sheet_instances_section(self, sheet_instances: List[Dict]):
351
+ """Add sheet_instances section for hierarchical design."""
352
+ self._data["sheet_instances"] = sheet_instances
353
+ self._modified = True
354
+
355
+ def set_paper_size(self, paper: str):
356
+ """Set paper size (A4, A3, etc.)."""
357
+ self._data["paper"] = paper
358
+ self._modified = True
359
+
360
+ def set_version_info(self, version: str, generator: str = "eeschema", generator_version: str = "9.0"):
361
+ """Set version and generator information."""
362
+ self._data["version"] = version
363
+ self._data["generator"] = generator
364
+ self._data["generator_version"] = generator_version
365
+ self._modified = True
366
+
367
+ def copy_metadata_from(self, source_schematic: "Schematic"):
368
+ """Copy all metadata from another schematic (version, generator, paper, etc.)."""
369
+ metadata_fields = ["version", "generator", "generator_version", "paper", "uuid", "title_block"]
370
+ for field in metadata_fields:
371
+ if field in source_schematic._data:
372
+ self._data[field] = source_schematic._data[field]
373
+ self._modified = True
261
374
 
262
375
  def get_summary(self) -> Dict[str, Any]:
263
376
  """Get summary information about the schematic."""
@@ -326,6 +439,414 @@ class Schematic:
326
439
  return True
327
440
  return False
328
441
 
442
+ # Label management
443
+ def add_hierarchical_label(
444
+ self,
445
+ text: str,
446
+ position: Union[Point, Tuple[float, float]],
447
+ shape: HierarchicalLabelShape = HierarchicalLabelShape.INPUT,
448
+ rotation: float = 0.0,
449
+ size: float = 1.27
450
+ ) -> str:
451
+ """
452
+ Add a hierarchical label.
453
+
454
+ Args:
455
+ text: Label text
456
+ position: Label position
457
+ shape: Label shape/direction
458
+ rotation: Text rotation in degrees
459
+ size: Font size
460
+
461
+ Returns:
462
+ UUID of created hierarchical label
463
+ """
464
+ if isinstance(position, tuple):
465
+ position = Point(position[0], position[1])
466
+
467
+ label = Label(
468
+ uuid=str(uuid.uuid4()),
469
+ position=position,
470
+ text=text,
471
+ label_type=LabelType.HIERARCHICAL,
472
+ rotation=rotation,
473
+ size=size,
474
+ shape=shape
475
+ )
476
+
477
+ if "hierarchical_labels" not in self._data:
478
+ self._data["hierarchical_labels"] = []
479
+
480
+ self._data["hierarchical_labels"].append({
481
+ "uuid": label.uuid,
482
+ "position": {"x": label.position.x, "y": label.position.y},
483
+ "text": label.text,
484
+ "shape": label.shape.value,
485
+ "rotation": label.rotation,
486
+ "size": label.size
487
+ })
488
+ self._modified = True
489
+
490
+ logger.debug(f"Added hierarchical label: {text} at {position}")
491
+ return label.uuid
492
+
493
+ def remove_hierarchical_label(self, label_uuid: str) -> bool:
494
+ """Remove hierarchical label by UUID."""
495
+ labels = self._data.get("hierarchical_labels", [])
496
+ for i, label in enumerate(labels):
497
+ if label.get("uuid") == label_uuid:
498
+ del labels[i]
499
+ self._modified = True
500
+ logger.debug(f"Removed hierarchical label: {label_uuid}")
501
+ return True
502
+ return False
503
+
504
+ def add_label(
505
+ self,
506
+ text: str,
507
+ position: Union[Point, Tuple[float, float]],
508
+ rotation: float = 0.0,
509
+ size: float = 1.27
510
+ ) -> str:
511
+ """
512
+ Add a local label.
513
+
514
+ Args:
515
+ text: Label text
516
+ position: Label position
517
+ rotation: Text rotation in degrees
518
+ size: Font size
519
+
520
+ Returns:
521
+ UUID of created label
522
+ """
523
+ if isinstance(position, tuple):
524
+ position = Point(position[0], position[1])
525
+
526
+ label = Label(
527
+ uuid=str(uuid.uuid4()),
528
+ position=position,
529
+ text=text,
530
+ label_type=LabelType.LOCAL,
531
+ rotation=rotation,
532
+ size=size
533
+ )
534
+
535
+ if "labels" not in self._data:
536
+ self._data["labels"] = []
537
+
538
+ self._data["labels"].append({
539
+ "uuid": label.uuid,
540
+ "position": {"x": label.position.x, "y": label.position.y},
541
+ "text": label.text,
542
+ "rotation": label.rotation,
543
+ "size": label.size
544
+ })
545
+ self._modified = True
546
+
547
+ logger.debug(f"Added local label: {text} at {position}")
548
+ return label.uuid
549
+
550
+ def remove_label(self, label_uuid: str) -> bool:
551
+ """Remove local label by UUID."""
552
+ labels = self._data.get("labels", [])
553
+ for i, label in enumerate(labels):
554
+ if label.get("uuid") == label_uuid:
555
+ del labels[i]
556
+ self._modified = True
557
+ logger.debug(f"Removed local label: {label_uuid}")
558
+ return True
559
+ return False
560
+
561
+ def add_sheet(
562
+ self,
563
+ name: str,
564
+ filename: str,
565
+ position: Union[Point, Tuple[float, float]],
566
+ size: Union[Point, Tuple[float, float]],
567
+ stroke_width: float = 0.1524,
568
+ stroke_type: str = "solid",
569
+ exclude_from_sim: bool = False,
570
+ in_bom: bool = True,
571
+ on_board: bool = True,
572
+ project_name: str = "",
573
+ page_number: str = "2"
574
+ ) -> str:
575
+ """
576
+ Add a hierarchical sheet.
577
+
578
+ Args:
579
+ name: Sheet name (displayed above sheet)
580
+ filename: Sheet filename (.kicad_sch file)
581
+ position: Sheet position (top-left corner)
582
+ size: Sheet size (width, height)
583
+ stroke_width: Border line width
584
+ stroke_type: Border line type
585
+ exclude_from_sim: Exclude from simulation
586
+ in_bom: Include in BOM
587
+ on_board: Include on board
588
+ project_name: Project name for instances
589
+ page_number: Page number for instances
590
+
591
+ Returns:
592
+ UUID of created sheet
593
+ """
594
+ if isinstance(position, tuple):
595
+ position = Point(position[0], position[1])
596
+ if isinstance(size, tuple):
597
+ size = Point(size[0], size[1])
598
+
599
+ sheet = Sheet(
600
+ uuid=str(uuid.uuid4()),
601
+ position=position,
602
+ size=size,
603
+ name=name,
604
+ filename=filename,
605
+ exclude_from_sim=exclude_from_sim,
606
+ in_bom=in_bom,
607
+ on_board=on_board,
608
+ stroke_width=stroke_width,
609
+ stroke_type=stroke_type
610
+ )
611
+
612
+ if "sheets" not in self._data:
613
+ self._data["sheets"] = []
614
+
615
+ self._data["sheets"].append({
616
+ "uuid": sheet.uuid,
617
+ "position": {"x": sheet.position.x, "y": sheet.position.y},
618
+ "size": {"width": sheet.size.x, "height": sheet.size.y},
619
+ "name": sheet.name,
620
+ "filename": sheet.filename,
621
+ "exclude_from_sim": sheet.exclude_from_sim,
622
+ "in_bom": sheet.in_bom,
623
+ "on_board": sheet.on_board,
624
+ "dnp": sheet.dnp,
625
+ "fields_autoplaced": sheet.fields_autoplaced,
626
+ "stroke_width": sheet.stroke_width,
627
+ "stroke_type": sheet.stroke_type,
628
+ "fill_color": sheet.fill_color,
629
+ "pins": [], # Sheet pins added separately
630
+ "project_name": project_name,
631
+ "page_number": page_number
632
+ })
633
+ self._modified = True
634
+
635
+ logger.debug(f"Added hierarchical sheet: {name} ({filename}) at {position}")
636
+ return sheet.uuid
637
+
638
+ def add_sheet_pin(
639
+ self,
640
+ sheet_uuid: str,
641
+ name: str,
642
+ pin_type: str = "input",
643
+ position: Union[Point, Tuple[float, float]] = (0, 0),
644
+ rotation: float = 0,
645
+ size: float = 1.27,
646
+ justify: str = "right"
647
+ ) -> str:
648
+ """
649
+ Add a pin to a hierarchical sheet.
650
+
651
+ Args:
652
+ sheet_uuid: UUID of the sheet to add pin to
653
+ name: Pin name (NET1, NET2, etc.)
654
+ pin_type: Pin type (input, output, bidirectional, etc.)
655
+ position: Pin position relative to sheet
656
+ rotation: Pin rotation in degrees
657
+ size: Font size for pin label
658
+ justify: Text justification (left, right, center)
659
+
660
+ Returns:
661
+ UUID of created sheet pin
662
+ """
663
+ if isinstance(position, tuple):
664
+ position = Point(position[0], position[1])
665
+
666
+ pin_uuid = str(uuid.uuid4())
667
+
668
+ # Find the sheet in the data
669
+ sheets = self._data.get("sheets", [])
670
+ for sheet in sheets:
671
+ if sheet.get("uuid") == sheet_uuid:
672
+ # Add pin to the sheet's pins list
673
+ pin_data = {
674
+ "uuid": pin_uuid,
675
+ "name": name,
676
+ "pin_type": pin_type,
677
+ "position": {"x": position.x, "y": position.y},
678
+ "rotation": rotation,
679
+ "size": size,
680
+ "justify": justify
681
+ }
682
+ sheet["pins"].append(pin_data)
683
+ self._modified = True
684
+
685
+ logger.debug(f"Added sheet pin: {name} ({pin_type}) to sheet {sheet_uuid}")
686
+ return pin_uuid
687
+
688
+ raise ValueError(f"Sheet with UUID '{sheet_uuid}' not found")
689
+
690
+ def add_text(
691
+ self,
692
+ text: str,
693
+ position: Union[Point, Tuple[float, float]],
694
+ rotation: float = 0.0,
695
+ size: float = 1.27,
696
+ exclude_from_sim: bool = False
697
+ ) -> str:
698
+ """
699
+ Add a text element.
700
+
701
+ Args:
702
+ text: Text content
703
+ position: Text position
704
+ rotation: Text rotation in degrees
705
+ size: Font size
706
+ exclude_from_sim: Exclude from simulation
707
+
708
+ Returns:
709
+ UUID of created text element
710
+ """
711
+ if isinstance(position, tuple):
712
+ position = Point(position[0], position[1])
713
+
714
+ text_element = Text(
715
+ uuid=str(uuid.uuid4()),
716
+ position=position,
717
+ text=text,
718
+ rotation=rotation,
719
+ size=size,
720
+ exclude_from_sim=exclude_from_sim
721
+ )
722
+
723
+ if "texts" not in self._data:
724
+ self._data["texts"] = []
725
+
726
+ self._data["texts"].append({
727
+ "uuid": text_element.uuid,
728
+ "position": {"x": text_element.position.x, "y": text_element.position.y},
729
+ "text": text_element.text,
730
+ "rotation": text_element.rotation,
731
+ "size": text_element.size,
732
+ "exclude_from_sim": text_element.exclude_from_sim
733
+ })
734
+ self._modified = True
735
+
736
+ logger.debug(f"Added text: '{text}' at {position}")
737
+ return text_element.uuid
738
+
739
+ def add_text_box(
740
+ self,
741
+ text: str,
742
+ position: Union[Point, Tuple[float, float]],
743
+ size: Union[Point, Tuple[float, float]],
744
+ rotation: float = 0.0,
745
+ font_size: float = 1.27,
746
+ margins: Tuple[float, float, float, float] = (0.9525, 0.9525, 0.9525, 0.9525),
747
+ stroke_width: float = 0.0,
748
+ stroke_type: str = "solid",
749
+ fill_type: str = "none",
750
+ justify_horizontal: str = "left",
751
+ justify_vertical: str = "top",
752
+ exclude_from_sim: bool = False
753
+ ) -> str:
754
+ """
755
+ Add a text box element.
756
+
757
+ Args:
758
+ text: Text content
759
+ position: Text box position (top-left corner)
760
+ size: Text box size (width, height)
761
+ rotation: Text rotation in degrees
762
+ font_size: Font size
763
+ margins: Margins (top, right, bottom, left)
764
+ stroke_width: Border line width
765
+ stroke_type: Border line type
766
+ fill_type: Fill type (none, solid, etc.)
767
+ justify_horizontal: Horizontal text alignment
768
+ justify_vertical: Vertical text alignment
769
+ exclude_from_sim: Exclude from simulation
770
+
771
+ Returns:
772
+ UUID of created text box element
773
+ """
774
+ if isinstance(position, tuple):
775
+ position = Point(position[0], position[1])
776
+ if isinstance(size, tuple):
777
+ size = Point(size[0], size[1])
778
+
779
+ text_box = TextBox(
780
+ uuid=str(uuid.uuid4()),
781
+ position=position,
782
+ size=size,
783
+ text=text,
784
+ rotation=rotation,
785
+ font_size=font_size,
786
+ margins=margins,
787
+ stroke_width=stroke_width,
788
+ stroke_type=stroke_type,
789
+ fill_type=fill_type,
790
+ justify_horizontal=justify_horizontal,
791
+ justify_vertical=justify_vertical,
792
+ exclude_from_sim=exclude_from_sim
793
+ )
794
+
795
+ if "text_boxes" not in self._data:
796
+ self._data["text_boxes"] = []
797
+
798
+ self._data["text_boxes"].append({
799
+ "uuid": text_box.uuid,
800
+ "position": {"x": text_box.position.x, "y": text_box.position.y},
801
+ "size": {"width": text_box.size.x, "height": text_box.size.y},
802
+ "text": text_box.text,
803
+ "rotation": text_box.rotation,
804
+ "font_size": text_box.font_size,
805
+ "margins": text_box.margins,
806
+ "stroke_width": text_box.stroke_width,
807
+ "stroke_type": text_box.stroke_type,
808
+ "fill_type": text_box.fill_type,
809
+ "justify_horizontal": text_box.justify_horizontal,
810
+ "justify_vertical": text_box.justify_vertical,
811
+ "exclude_from_sim": text_box.exclude_from_sim
812
+ })
813
+ self._modified = True
814
+
815
+ logger.debug(f"Added text box: '{text}' at {position} size {size}")
816
+ return text_box.uuid
817
+
818
+ def set_title_block(
819
+ self,
820
+ title: str = "",
821
+ date: str = "",
822
+ rev: str = "",
823
+ company: str = "",
824
+ comments: Optional[Dict[int, str]] = None
825
+ ):
826
+ """
827
+ Set title block information.
828
+
829
+ Args:
830
+ title: Schematic title
831
+ date: Creation/revision date
832
+ rev: Revision number
833
+ company: Company name
834
+ comments: Numbered comments (1, 2, 3, etc.)
835
+ """
836
+ if comments is None:
837
+ comments = {}
838
+
839
+ self._data["title_block"] = {
840
+ "title": title,
841
+ "date": date,
842
+ "rev": rev,
843
+ "company": company,
844
+ "comments": comments
845
+ }
846
+ self._modified = True
847
+
848
+ logger.debug(f"Set title block: {title} rev {rev}")
849
+
329
850
  # Library management
330
851
  @property
331
852
  def libraries(self) -> "LibraryManager":
@@ -390,27 +911,264 @@ class Schematic:
390
911
  def _sync_components_to_data(self):
391
912
  """Sync component collection state back to data structure."""
392
913
  self._data["components"] = [comp._data.__dict__ for comp in self._components]
914
+
915
+ # Populate lib_symbols with actual symbol definitions used by components
916
+ lib_symbols = {}
917
+ cache = get_symbol_cache()
918
+
919
+ for comp in self._components:
920
+ if comp.lib_id and comp.lib_id not in lib_symbols:
921
+ logger.debug(f"🔧 SCHEMATIC: Processing component {comp.lib_id}")
922
+
923
+ # Get the actual symbol definition
924
+ symbol_def = cache.get_symbol(comp.lib_id)
925
+ if symbol_def:
926
+ logger.debug(f"🔧 SCHEMATIC: Loaded symbol {comp.lib_id}")
927
+ lib_symbols[comp.lib_id] = self._convert_symbol_to_kicad_format(symbol_def, comp.lib_id)
928
+
929
+ # Check if this symbol extends another symbol using multiple methods
930
+ extends_parent = None
931
+
932
+ # Method 1: Check raw_kicad_data
933
+ if hasattr(symbol_def, 'raw_kicad_data') and symbol_def.raw_kicad_data:
934
+ extends_parent = self._check_symbol_extends(symbol_def.raw_kicad_data)
935
+ logger.debug(f"🔧 SCHEMATIC: Checked raw_kicad_data for {comp.lib_id}, extends: {extends_parent}")
936
+
937
+ # Method 2: Check raw_data attribute
938
+ if not extends_parent and hasattr(symbol_def, '__dict__'):
939
+ for attr_name, attr_value in symbol_def.__dict__.items():
940
+ if attr_name == 'raw_data':
941
+ logger.debug(f"🔧 SCHEMATIC: Checking raw_data for extends: {type(attr_value)}")
942
+ extends_parent = self._check_symbol_extends(attr_value)
943
+ if extends_parent:
944
+ logger.debug(f"🔧 SCHEMATIC: Found extends in raw_data: {extends_parent}")
945
+
946
+ # Method 3: Check the extends attribute directly
947
+ if not extends_parent and hasattr(symbol_def, 'extends'):
948
+ extends_parent = symbol_def.extends
949
+ logger.debug(f"🔧 SCHEMATIC: Found extends attribute: {extends_parent}")
950
+
951
+ if extends_parent:
952
+ # Load the parent symbol too
953
+ parent_lib_id = f"{comp.lib_id.split(':')[0]}:{extends_parent}"
954
+ logger.debug(f"🔧 SCHEMATIC: Loading parent symbol: {parent_lib_id}")
955
+
956
+ if parent_lib_id not in lib_symbols:
957
+ parent_symbol_def = cache.get_symbol(parent_lib_id)
958
+ if parent_symbol_def:
959
+ lib_symbols[parent_lib_id] = self._convert_symbol_to_kicad_format(parent_symbol_def, parent_lib_id)
960
+ logger.debug(f"🔧 SCHEMATIC: Successfully loaded parent symbol: {parent_lib_id} for {comp.lib_id}")
961
+ else:
962
+ logger.warning(f"🔧 SCHEMATIC: Failed to load parent symbol: {parent_lib_id}")
963
+ else:
964
+ logger.debug(f"🔧 SCHEMATIC: Parent symbol {parent_lib_id} already loaded")
965
+ else:
966
+ logger.debug(f"🔧 SCHEMATIC: No extends found for {comp.lib_id}")
967
+ else:
968
+ # Fallback for unknown symbols
969
+ logger.warning(f"🔧 SCHEMATIC: Failed to load symbol {comp.lib_id}, using fallback")
970
+ lib_symbols[comp.lib_id] = {"definition": "basic"}
971
+
972
+ self._data["lib_symbols"] = lib_symbols
973
+
974
+ # Debug: Log the final lib_symbols structure
975
+ logger.debug(f"🔧 FINAL: lib_symbols contains {len(lib_symbols)} symbols:")
976
+ for sym_id in lib_symbols.keys():
977
+ logger.debug(f"🔧 FINAL: - {sym_id}")
978
+ # Check if this symbol has extends
979
+ sym_data = lib_symbols[sym_id]
980
+ if isinstance(sym_data, list) and len(sym_data) > 2:
981
+ for item in sym_data[1:]:
982
+ if isinstance(item, list) and len(item) >= 2:
983
+ if item[0] == sexpdata.Symbol('extends'):
984
+ logger.debug(f"🔧 FINAL: - {sym_id} extends {item[1]}")
985
+ break
986
+
987
+ def _check_symbol_extends(self, symbol_data: Any) -> Optional[str]:
988
+ """Check if symbol extends another symbol and return parent name."""
989
+ logger.debug(f"🔧 EXTENDS: Checking symbol data type: {type(symbol_data)}")
990
+
991
+ if not isinstance(symbol_data, list):
992
+ logger.debug(f"🔧 EXTENDS: Not a list, returning None")
993
+ return None
994
+
995
+ logger.debug(f"🔧 EXTENDS: Checking {len(symbol_data)} items for extends directive")
996
+
997
+ for i, item in enumerate(symbol_data[1:], 1):
998
+ logger.debug(f"🔧 EXTENDS: Item {i}: {type(item)} - {item if not isinstance(item, list) else f'list[{len(item)}]'}")
999
+ if isinstance(item, list) and len(item) >= 2:
1000
+ if item[0] == sexpdata.Symbol('extends'):
1001
+ parent_name = str(item[1]).strip('"')
1002
+ logger.debug(f"🔧 EXTENDS: Found extends directive: {parent_name}")
1003
+ return parent_name
1004
+
1005
+ logger.debug(f"🔧 EXTENDS: No extends directive found")
1006
+ return None
1007
+
1008
+ def _sync_wires_to_data(self):
1009
+ """Sync wire collection state back to data structure."""
1010
+ wire_data = []
1011
+ for wire in self._wires:
1012
+ wire_dict = {
1013
+ "uuid": wire.uuid,
1014
+ "points": [{"x": p.x, "y": p.y} for p in wire.points],
1015
+ "wire_type": wire.wire_type.value,
1016
+ "stroke_width": wire.stroke_width,
1017
+ "stroke_type": wire.stroke_type
1018
+ }
1019
+ wire_data.append(wire_dict)
1020
+
1021
+ self._data["wires"] = wire_data
1022
+
1023
+ def _sync_junctions_to_data(self):
1024
+ """Sync junction collection state back to data structure."""
1025
+ junction_data = []
1026
+ for junction in self._junctions:
1027
+ junction_dict = {
1028
+ "uuid": junction.uuid,
1029
+ "position": {"x": junction.position.x, "y": junction.position.y},
1030
+ "diameter": junction.diameter,
1031
+ "color": junction.color
1032
+ }
1033
+ junction_data.append(junction_dict)
1034
+
1035
+ self._data["junctions"] = junction_data
1036
+
1037
+ def _convert_symbol_to_kicad_format(self, symbol: "SymbolDefinition", lib_id: str) -> Dict[str, Any]:
1038
+ """Convert SymbolDefinition to KiCAD lib_symbols format using raw parsed data."""
1039
+ # If we have raw KiCAD data from the library file, use it directly
1040
+ if hasattr(symbol, 'raw_kicad_data') and symbol.raw_kicad_data:
1041
+ return self._convert_raw_symbol_data(symbol.raw_kicad_data, lib_id)
1042
+
1043
+ # Fallback: create basic symbol structure
1044
+ return {
1045
+ "pin_numbers": {"hide": "yes"},
1046
+ "pin_names": {"offset": 0},
1047
+ "exclude_from_sim": "no",
1048
+ "in_bom": "yes",
1049
+ "on_board": "yes",
1050
+ "properties": {
1051
+ "Reference": {
1052
+ "value": symbol.reference_prefix,
1053
+ "at": [2.032, 0, 90],
1054
+ "effects": {"font": {"size": [1.27, 1.27]}}
1055
+ },
1056
+ "Value": {
1057
+ "value": symbol.reference_prefix,
1058
+ "at": [0, 0, 90],
1059
+ "effects": {"font": {"size": [1.27, 1.27]}}
1060
+ },
1061
+ "Footprint": {
1062
+ "value": "",
1063
+ "at": [-1.778, 0, 90],
1064
+ "effects": {
1065
+ "font": {"size": [1.27, 1.27]},
1066
+ "hide": "yes"
1067
+ }
1068
+ },
1069
+ "Datasheet": {
1070
+ "value": getattr(symbol, 'Datasheet', None) or getattr(symbol, 'datasheet', None) or "~",
1071
+ "at": [0, 0, 0],
1072
+ "effects": {
1073
+ "font": {"size": [1.27, 1.27]},
1074
+ "hide": "yes"
1075
+ }
1076
+ },
1077
+ "Description": {
1078
+ "value": getattr(symbol, 'Description', None) or getattr(symbol, 'description', None) or "Resistor",
1079
+ "at": [0, 0, 0],
1080
+ "effects": {
1081
+ "font": {"size": [1.27, 1.27]},
1082
+ "hide": "yes"
1083
+ }
1084
+ }
1085
+ },
1086
+ "embedded_fonts": "no"
1087
+ }
1088
+
1089
+ def _convert_raw_symbol_data(self, raw_data: List, lib_id: str) -> Dict[str, Any]:
1090
+ """Convert raw parsed KiCAD symbol data to dictionary format for S-expression generation."""
1091
+ import copy
1092
+ import sexpdata
1093
+
1094
+ # Make a copy and fix symbol name and string/symbol issues
1095
+ modified_data = copy.deepcopy(raw_data)
1096
+
1097
+ # Replace the symbol name with the full lib_id
1098
+ if len(modified_data) >= 2:
1099
+ modified_data[1] = lib_id # Change 'R' to 'Device:R'
1100
+
1101
+ # Fix extends directive to use full lib_id
1102
+ logger.debug(f"🔧 CONVERT: Processing {len(modified_data)} items for {lib_id}")
1103
+ for i, item in enumerate(modified_data[1:], 1):
1104
+ if isinstance(item, list) and len(item) >= 2:
1105
+ logger.debug(f"🔧 CONVERT: Item {i}: {item[0]} = {item[1] if len(item) > 1 else 'N/A'}")
1106
+ if item[0] == sexpdata.Symbol('extends'):
1107
+ # Convert bare symbol name to full lib_id
1108
+ parent_name = str(item[1]).strip('"')
1109
+ parent_lib_id = f"{lib_id.split(':')[0]}:{parent_name}"
1110
+ modified_data[i][1] = parent_lib_id
1111
+ logger.debug(f"🔧 CONVERT: Fixed extends directive: {parent_name} -> {parent_lib_id}")
1112
+ break
1113
+
1114
+ # Fix string/symbol conversion issues in pin definitions
1115
+ print(f"🔧 DEBUG: Before fix - checking for pin definitions...")
1116
+ self._fix_symbol_strings_recursively(modified_data)
1117
+ print(f"🔧 DEBUG: After fix - symbol strings fixed")
1118
+
1119
+ return modified_data
1120
+
1121
+ def _fix_symbol_strings_recursively(self, data):
1122
+ """Recursively fix string/symbol issues in parsed S-expression data."""
1123
+ import sexpdata
1124
+
1125
+ if isinstance(data, list):
1126
+ for i, item in enumerate(data):
1127
+ if isinstance(item, list):
1128
+ # Check for pin definitions that need fixing
1129
+ if (len(item) >= 3 and
1130
+ item[0] == sexpdata.Symbol('pin')):
1131
+ print(f"🔧 DEBUG: Found pin definition: {item[:3]} - types: {[type(x) for x in item[:3]]}")
1132
+ # Fix pin type and shape - ensure they are symbols not strings
1133
+ if isinstance(item[1], str):
1134
+ print(f"🔧 DEBUG: Converting pin type '{item[1]}' to symbol")
1135
+ item[1] = sexpdata.Symbol(item[1]) # pin type: "passive" -> passive
1136
+ if len(item) >= 3 and isinstance(item[2], str):
1137
+ print(f"🔧 DEBUG: Converting pin shape '{item[2]}' to symbol")
1138
+ item[2] = sexpdata.Symbol(item[2]) # pin shape: "line" -> line
1139
+
1140
+ # Recursively process nested lists
1141
+ self._fix_symbol_strings_recursively(item)
1142
+ elif isinstance(item, str):
1143
+ # Fix common KiCAD keywords that should be symbols
1144
+ if item in ['yes', 'no', 'default', 'none', 'left', 'right', 'center']:
1145
+ data[i] = sexpdata.Symbol(item)
1146
+
1147
+ return data
393
1148
 
394
1149
  @staticmethod
395
1150
  def _create_empty_schematic_data() -> Dict[str, Any]:
396
1151
  """Create empty schematic data structure."""
397
1152
  return {
398
- "version": "20230121",
399
- "generator": "kicad-sch-api",
1153
+ "version": "20250114",
1154
+ "generator": "eeschema",
1155
+ "generator_version": "9.0",
400
1156
  "uuid": str(uuid.uuid4()),
401
- "title_block": {
402
- "title": "Untitled",
403
- "date": "",
404
- "revision": "1.0",
405
- "company": "",
406
- "size": "A4",
407
- },
1157
+ "paper": "A4",
408
1158
  "components": [],
409
1159
  "wires": [],
410
1160
  "junctions": [],
411
1161
  "labels": [],
412
1162
  "nets": [],
413
1163
  "lib_symbols": {},
1164
+ "sheet_instances": [
1165
+ {
1166
+ "path": "/",
1167
+ "page": "1"
1168
+ }
1169
+ ],
1170
+ "symbol_instances": [],
1171
+ "embedded_fonts": "no",
414
1172
  }
415
1173
 
416
1174
  # Context manager support for atomic operations