kicad-sch-api 0.0.2__py3-none-any.whl → 0.1.0__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,19 +153,30 @@ 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
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
117
180
  schematic_data["title_block"] = {"title": name}
118
181
 
119
182
  logger.info(f"Created new schematic: {name}")
@@ -125,6 +188,16 @@ class Schematic:
125
188
  """Collection of all components in the schematic."""
126
189
  return self._components
127
190
 
191
+ @property
192
+ def wires(self) -> WireCollection:
193
+ """Collection of all wires in the schematic."""
194
+ return self._wires
195
+
196
+ @property
197
+ def junctions(self) -> JunctionCollection:
198
+ """Collection of all junctions in the schematic."""
199
+ return self._junctions
200
+
128
201
  @property
129
202
  def version(self) -> Optional[str]:
130
203
  """KiCAD version string."""
@@ -184,8 +257,10 @@ class Schematic:
184
257
  if errors:
185
258
  raise ValidationError("Cannot save schematic with validation errors", errors)
186
259
 
187
- # Update data structure with current component state
260
+ # Update data structure with current component, wire, and junction state
188
261
  self._sync_components_to_data()
262
+ self._sync_wires_to_data()
263
+ self._sync_junctions_to_data()
189
264
 
190
265
  # Write file
191
266
  if preserve_format and self._original_content:
@@ -258,6 +333,42 @@ class Schematic:
258
333
  issues.extend(component_issues)
259
334
 
260
335
  return issues
336
+
337
+ # Focused helper functions for specific KiCAD sections
338
+ def add_lib_symbols_section(self, lib_symbols: Dict[str, Any]):
339
+ """Add or update lib_symbols section with specific symbol definitions."""
340
+ self._data["lib_symbols"] = lib_symbols
341
+ self._modified = True
342
+
343
+ def add_instances_section(self, instances: Dict[str, Any]):
344
+ """Add instances section for component placement tracking."""
345
+ self._data["instances"] = instances
346
+ self._modified = True
347
+
348
+ def add_sheet_instances_section(self, sheet_instances: List[Dict]):
349
+ """Add sheet_instances section for hierarchical design."""
350
+ self._data["sheet_instances"] = sheet_instances
351
+ self._modified = True
352
+
353
+ def set_paper_size(self, paper: str):
354
+ """Set paper size (A4, A3, etc.)."""
355
+ self._data["paper"] = paper
356
+ self._modified = True
357
+
358
+ def set_version_info(self, version: str, generator: str = "eeschema", generator_version: str = "9.0"):
359
+ """Set version and generator information."""
360
+ self._data["version"] = version
361
+ self._data["generator"] = generator
362
+ self._data["generator_version"] = generator_version
363
+ self._modified = True
364
+
365
+ def copy_metadata_from(self, source_schematic: "Schematic"):
366
+ """Copy all metadata from another schematic (version, generator, paper, etc.)."""
367
+ metadata_fields = ["version", "generator", "generator_version", "paper", "uuid", "title_block"]
368
+ for field in metadata_fields:
369
+ if field in source_schematic._data:
370
+ self._data[field] = source_schematic._data[field]
371
+ self._modified = True
261
372
 
262
373
  def get_summary(self) -> Dict[str, Any]:
263
374
  """Get summary information about the schematic."""
@@ -326,6 +437,382 @@ class Schematic:
326
437
  return True
327
438
  return False
328
439
 
440
+ # Label management
441
+ def add_hierarchical_label(
442
+ self,
443
+ text: str,
444
+ position: Union[Point, Tuple[float, float]],
445
+ shape: HierarchicalLabelShape = HierarchicalLabelShape.INPUT,
446
+ rotation: float = 0.0,
447
+ size: float = 1.27
448
+ ) -> str:
449
+ """
450
+ Add a hierarchical label.
451
+
452
+ Args:
453
+ text: Label text
454
+ position: Label position
455
+ shape: Label shape/direction
456
+ rotation: Text rotation in degrees
457
+ size: Font size
458
+
459
+ Returns:
460
+ UUID of created hierarchical label
461
+ """
462
+ if isinstance(position, tuple):
463
+ position = Point(position[0], position[1])
464
+
465
+ label = Label(
466
+ uuid=str(uuid.uuid4()),
467
+ position=position,
468
+ text=text,
469
+ label_type=LabelType.HIERARCHICAL,
470
+ rotation=rotation,
471
+ size=size,
472
+ shape=shape
473
+ )
474
+
475
+ if "hierarchical_labels" not in self._data:
476
+ self._data["hierarchical_labels"] = []
477
+
478
+ self._data["hierarchical_labels"].append({
479
+ "uuid": label.uuid,
480
+ "position": {"x": label.position.x, "y": label.position.y},
481
+ "text": label.text,
482
+ "shape": label.shape.value,
483
+ "rotation": label.rotation,
484
+ "size": label.size
485
+ })
486
+ self._modified = True
487
+
488
+ logger.debug(f"Added hierarchical label: {text} at {position}")
489
+ return label.uuid
490
+
491
+ def remove_hierarchical_label(self, label_uuid: str) -> bool:
492
+ """Remove hierarchical label by UUID."""
493
+ labels = self._data.get("hierarchical_labels", [])
494
+ for i, label in enumerate(labels):
495
+ if label.get("uuid") == label_uuid:
496
+ del labels[i]
497
+ self._modified = True
498
+ logger.debug(f"Removed hierarchical label: {label_uuid}")
499
+ return True
500
+ return False
501
+
502
+ def add_label(
503
+ self,
504
+ text: str,
505
+ position: Union[Point, Tuple[float, float]],
506
+ rotation: float = 0.0,
507
+ size: float = 1.27
508
+ ) -> str:
509
+ """
510
+ Add a local label.
511
+
512
+ Args:
513
+ text: Label text
514
+ position: Label position
515
+ rotation: Text rotation in degrees
516
+ size: Font size
517
+
518
+ Returns:
519
+ UUID of created label
520
+ """
521
+ if isinstance(position, tuple):
522
+ position = Point(position[0], position[1])
523
+
524
+ label = Label(
525
+ uuid=str(uuid.uuid4()),
526
+ position=position,
527
+ text=text,
528
+ label_type=LabelType.LOCAL,
529
+ rotation=rotation,
530
+ size=size
531
+ )
532
+
533
+ if "labels" not in self._data:
534
+ self._data["labels"] = []
535
+
536
+ self._data["labels"].append({
537
+ "uuid": label.uuid,
538
+ "position": {"x": label.position.x, "y": label.position.y},
539
+ "text": label.text,
540
+ "rotation": label.rotation,
541
+ "size": label.size
542
+ })
543
+ self._modified = True
544
+
545
+ logger.debug(f"Added local label: {text} at {position}")
546
+ return label.uuid
547
+
548
+ def remove_label(self, label_uuid: str) -> bool:
549
+ """Remove local label by UUID."""
550
+ labels = self._data.get("labels", [])
551
+ for i, label in enumerate(labels):
552
+ if label.get("uuid") == label_uuid:
553
+ del labels[i]
554
+ self._modified = True
555
+ logger.debug(f"Removed local label: {label_uuid}")
556
+ return True
557
+ return False
558
+
559
+ def add_sheet(
560
+ self,
561
+ name: str,
562
+ filename: str,
563
+ position: Union[Point, Tuple[float, float]],
564
+ size: Union[Point, Tuple[float, float]],
565
+ stroke_width: float = 0.1524,
566
+ stroke_type: str = "solid",
567
+ exclude_from_sim: bool = False,
568
+ in_bom: bool = True,
569
+ on_board: bool = True,
570
+ project_name: str = "",
571
+ page_number: str = "2"
572
+ ) -> str:
573
+ """
574
+ Add a hierarchical sheet.
575
+
576
+ Args:
577
+ name: Sheet name (displayed above sheet)
578
+ filename: Sheet filename (.kicad_sch file)
579
+ position: Sheet position (top-left corner)
580
+ size: Sheet size (width, height)
581
+ stroke_width: Border line width
582
+ stroke_type: Border line type
583
+ exclude_from_sim: Exclude from simulation
584
+ in_bom: Include in BOM
585
+ on_board: Include on board
586
+ project_name: Project name for instances
587
+ page_number: Page number for instances
588
+
589
+ Returns:
590
+ UUID of created sheet
591
+ """
592
+ if isinstance(position, tuple):
593
+ position = Point(position[0], position[1])
594
+ if isinstance(size, tuple):
595
+ size = Point(size[0], size[1])
596
+
597
+ sheet = Sheet(
598
+ uuid=str(uuid.uuid4()),
599
+ position=position,
600
+ size=size,
601
+ name=name,
602
+ filename=filename,
603
+ exclude_from_sim=exclude_from_sim,
604
+ in_bom=in_bom,
605
+ on_board=on_board,
606
+ stroke_width=stroke_width,
607
+ stroke_type=stroke_type
608
+ )
609
+
610
+ if "sheets" not in self._data:
611
+ self._data["sheets"] = []
612
+
613
+ self._data["sheets"].append({
614
+ "uuid": sheet.uuid,
615
+ "position": {"x": sheet.position.x, "y": sheet.position.y},
616
+ "size": {"width": sheet.size.x, "height": sheet.size.y},
617
+ "name": sheet.name,
618
+ "filename": sheet.filename,
619
+ "exclude_from_sim": sheet.exclude_from_sim,
620
+ "in_bom": sheet.in_bom,
621
+ "on_board": sheet.on_board,
622
+ "dnp": sheet.dnp,
623
+ "fields_autoplaced": sheet.fields_autoplaced,
624
+ "stroke_width": sheet.stroke_width,
625
+ "stroke_type": sheet.stroke_type,
626
+ "fill_color": sheet.fill_color,
627
+ "pins": [], # Sheet pins added separately
628
+ "project_name": project_name,
629
+ "page_number": page_number
630
+ })
631
+ self._modified = True
632
+
633
+ logger.debug(f"Added hierarchical sheet: {name} ({filename}) at {position}")
634
+ return sheet.uuid
635
+
636
+ def add_sheet_pin(
637
+ self,
638
+ sheet_uuid: str,
639
+ name: str,
640
+ pin_type: str = "input",
641
+ position: Union[Point, Tuple[float, float]] = (0, 0),
642
+ rotation: float = 0,
643
+ size: float = 1.27,
644
+ justify: str = "right"
645
+ ) -> str:
646
+ """
647
+ Add a pin to a hierarchical sheet.
648
+
649
+ Args:
650
+ sheet_uuid: UUID of the sheet to add pin to
651
+ name: Pin name (NET1, NET2, etc.)
652
+ pin_type: Pin type (input, output, bidirectional, etc.)
653
+ position: Pin position relative to sheet
654
+ rotation: Pin rotation in degrees
655
+ size: Font size for pin label
656
+ justify: Text justification (left, right, center)
657
+
658
+ Returns:
659
+ UUID of created sheet pin
660
+ """
661
+ if isinstance(position, tuple):
662
+ position = Point(position[0], position[1])
663
+
664
+ pin_uuid = str(uuid.uuid4())
665
+
666
+ # Find the sheet in the data
667
+ sheets = self._data.get("sheets", [])
668
+ for sheet in sheets:
669
+ if sheet.get("uuid") == sheet_uuid:
670
+ # Add pin to the sheet's pins list
671
+ pin_data = {
672
+ "uuid": pin_uuid,
673
+ "name": name,
674
+ "pin_type": pin_type,
675
+ "position": {"x": position.x, "y": position.y},
676
+ "rotation": rotation,
677
+ "size": size,
678
+ "justify": justify
679
+ }
680
+ sheet["pins"].append(pin_data)
681
+ self._modified = True
682
+
683
+ logger.debug(f"Added sheet pin: {name} ({pin_type}) to sheet {sheet_uuid}")
684
+ return pin_uuid
685
+
686
+ raise ValueError(f"Sheet with UUID '{sheet_uuid}' not found")
687
+
688
+ def add_text(
689
+ self,
690
+ text: str,
691
+ position: Union[Point, Tuple[float, float]],
692
+ rotation: float = 0.0,
693
+ size: float = 1.27,
694
+ exclude_from_sim: bool = False
695
+ ) -> str:
696
+ """
697
+ Add a text element.
698
+
699
+ Args:
700
+ text: Text content
701
+ position: Text position
702
+ rotation: Text rotation in degrees
703
+ size: Font size
704
+ exclude_from_sim: Exclude from simulation
705
+
706
+ Returns:
707
+ UUID of created text element
708
+ """
709
+ if isinstance(position, tuple):
710
+ position = Point(position[0], position[1])
711
+
712
+ text_element = Text(
713
+ uuid=str(uuid.uuid4()),
714
+ position=position,
715
+ text=text,
716
+ rotation=rotation,
717
+ size=size,
718
+ exclude_from_sim=exclude_from_sim
719
+ )
720
+
721
+ if "texts" not in self._data:
722
+ self._data["texts"] = []
723
+
724
+ self._data["texts"].append({
725
+ "uuid": text_element.uuid,
726
+ "position": {"x": text_element.position.x, "y": text_element.position.y},
727
+ "text": text_element.text,
728
+ "rotation": text_element.rotation,
729
+ "size": text_element.size,
730
+ "exclude_from_sim": text_element.exclude_from_sim
731
+ })
732
+ self._modified = True
733
+
734
+ logger.debug(f"Added text: '{text}' at {position}")
735
+ return text_element.uuid
736
+
737
+ def add_text_box(
738
+ self,
739
+ text: str,
740
+ position: Union[Point, Tuple[float, float]],
741
+ size: Union[Point, Tuple[float, float]],
742
+ rotation: float = 0.0,
743
+ font_size: float = 1.27,
744
+ margins: Tuple[float, float, float, float] = (0.9525, 0.9525, 0.9525, 0.9525),
745
+ stroke_width: float = 0.0,
746
+ stroke_type: str = "solid",
747
+ fill_type: str = "none",
748
+ justify_horizontal: str = "left",
749
+ justify_vertical: str = "top",
750
+ exclude_from_sim: bool = False
751
+ ) -> str:
752
+ """
753
+ Add a text box element.
754
+
755
+ Args:
756
+ text: Text content
757
+ position: Text box position (top-left corner)
758
+ size: Text box size (width, height)
759
+ rotation: Text rotation in degrees
760
+ font_size: Font size
761
+ margins: Margins (top, right, bottom, left)
762
+ stroke_width: Border line width
763
+ stroke_type: Border line type
764
+ fill_type: Fill type (none, solid, etc.)
765
+ justify_horizontal: Horizontal text alignment
766
+ justify_vertical: Vertical text alignment
767
+ exclude_from_sim: Exclude from simulation
768
+
769
+ Returns:
770
+ UUID of created text box element
771
+ """
772
+ if isinstance(position, tuple):
773
+ position = Point(position[0], position[1])
774
+ if isinstance(size, tuple):
775
+ size = Point(size[0], size[1])
776
+
777
+ text_box = TextBox(
778
+ uuid=str(uuid.uuid4()),
779
+ position=position,
780
+ size=size,
781
+ text=text,
782
+ rotation=rotation,
783
+ font_size=font_size,
784
+ margins=margins,
785
+ stroke_width=stroke_width,
786
+ stroke_type=stroke_type,
787
+ fill_type=fill_type,
788
+ justify_horizontal=justify_horizontal,
789
+ justify_vertical=justify_vertical,
790
+ exclude_from_sim=exclude_from_sim
791
+ )
792
+
793
+ if "text_boxes" not in self._data:
794
+ self._data["text_boxes"] = []
795
+
796
+ self._data["text_boxes"].append({
797
+ "uuid": text_box.uuid,
798
+ "position": {"x": text_box.position.x, "y": text_box.position.y},
799
+ "size": {"width": text_box.size.x, "height": text_box.size.y},
800
+ "text": text_box.text,
801
+ "rotation": text_box.rotation,
802
+ "font_size": text_box.font_size,
803
+ "margins": text_box.margins,
804
+ "stroke_width": text_box.stroke_width,
805
+ "stroke_type": text_box.stroke_type,
806
+ "fill_type": text_box.fill_type,
807
+ "justify_horizontal": text_box.justify_horizontal,
808
+ "justify_vertical": text_box.justify_vertical,
809
+ "exclude_from_sim": text_box.exclude_from_sim
810
+ })
811
+ self._modified = True
812
+
813
+ logger.debug(f"Added text box: '{text}' at {position} size {size}")
814
+ return text_box.uuid
815
+
329
816
  # Library management
330
817
  @property
331
818
  def libraries(self) -> "LibraryManager":
@@ -390,14 +877,250 @@ class Schematic:
390
877
  def _sync_components_to_data(self):
391
878
  """Sync component collection state back to data structure."""
392
879
  self._data["components"] = [comp._data.__dict__ for comp in self._components]
880
+
881
+ # Populate lib_symbols with actual symbol definitions used by components
882
+ lib_symbols = {}
883
+ cache = get_symbol_cache()
884
+
885
+ for comp in self._components:
886
+ if comp.lib_id and comp.lib_id not in lib_symbols:
887
+ logger.debug(f"🔧 SCHEMATIC: Processing component {comp.lib_id}")
888
+
889
+ # Get the actual symbol definition
890
+ symbol_def = cache.get_symbol(comp.lib_id)
891
+ if symbol_def:
892
+ logger.debug(f"🔧 SCHEMATIC: Loaded symbol {comp.lib_id}")
893
+ lib_symbols[comp.lib_id] = self._convert_symbol_to_kicad_format(symbol_def, comp.lib_id)
894
+
895
+ # Check if this symbol extends another symbol using multiple methods
896
+ extends_parent = None
897
+
898
+ # Method 1: Check raw_kicad_data
899
+ if hasattr(symbol_def, 'raw_kicad_data') and symbol_def.raw_kicad_data:
900
+ extends_parent = self._check_symbol_extends(symbol_def.raw_kicad_data)
901
+ logger.debug(f"🔧 SCHEMATIC: Checked raw_kicad_data for {comp.lib_id}, extends: {extends_parent}")
902
+
903
+ # Method 2: Check raw_data attribute
904
+ if not extends_parent and hasattr(symbol_def, '__dict__'):
905
+ for attr_name, attr_value in symbol_def.__dict__.items():
906
+ if attr_name == 'raw_data':
907
+ logger.debug(f"🔧 SCHEMATIC: Checking raw_data for extends: {type(attr_value)}")
908
+ extends_parent = self._check_symbol_extends(attr_value)
909
+ if extends_parent:
910
+ logger.debug(f"🔧 SCHEMATIC: Found extends in raw_data: {extends_parent}")
911
+
912
+ # Method 3: Check the extends attribute directly
913
+ if not extends_parent and hasattr(symbol_def, 'extends'):
914
+ extends_parent = symbol_def.extends
915
+ logger.debug(f"🔧 SCHEMATIC: Found extends attribute: {extends_parent}")
916
+
917
+ if extends_parent:
918
+ # Load the parent symbol too
919
+ parent_lib_id = f"{comp.lib_id.split(':')[0]}:{extends_parent}"
920
+ logger.debug(f"🔧 SCHEMATIC: Loading parent symbol: {parent_lib_id}")
921
+
922
+ if parent_lib_id not in lib_symbols:
923
+ parent_symbol_def = cache.get_symbol(parent_lib_id)
924
+ if parent_symbol_def:
925
+ lib_symbols[parent_lib_id] = self._convert_symbol_to_kicad_format(parent_symbol_def, parent_lib_id)
926
+ logger.debug(f"🔧 SCHEMATIC: Successfully loaded parent symbol: {parent_lib_id} for {comp.lib_id}")
927
+ else:
928
+ logger.warning(f"🔧 SCHEMATIC: Failed to load parent symbol: {parent_lib_id}")
929
+ else:
930
+ logger.debug(f"🔧 SCHEMATIC: Parent symbol {parent_lib_id} already loaded")
931
+ else:
932
+ logger.debug(f"🔧 SCHEMATIC: No extends found for {comp.lib_id}")
933
+ else:
934
+ # Fallback for unknown symbols
935
+ logger.warning(f"🔧 SCHEMATIC: Failed to load symbol {comp.lib_id}, using fallback")
936
+ lib_symbols[comp.lib_id] = {"definition": "basic"}
937
+
938
+ self._data["lib_symbols"] = lib_symbols
939
+
940
+ # Debug: Log the final lib_symbols structure
941
+ logger.debug(f"🔧 FINAL: lib_symbols contains {len(lib_symbols)} symbols:")
942
+ for sym_id in lib_symbols.keys():
943
+ logger.debug(f"🔧 FINAL: - {sym_id}")
944
+ # Check if this symbol has extends
945
+ sym_data = lib_symbols[sym_id]
946
+ if isinstance(sym_data, list) and len(sym_data) > 2:
947
+ for item in sym_data[1:]:
948
+ if isinstance(item, list) and len(item) >= 2:
949
+ if item[0] == sexpdata.Symbol('extends'):
950
+ logger.debug(f"🔧 FINAL: - {sym_id} extends {item[1]}")
951
+ break
952
+
953
+ def _check_symbol_extends(self, symbol_data: Any) -> Optional[str]:
954
+ """Check if symbol extends another symbol and return parent name."""
955
+ logger.debug(f"🔧 EXTENDS: Checking symbol data type: {type(symbol_data)}")
956
+
957
+ if not isinstance(symbol_data, list):
958
+ logger.debug(f"🔧 EXTENDS: Not a list, returning None")
959
+ return None
960
+
961
+ logger.debug(f"🔧 EXTENDS: Checking {len(symbol_data)} items for extends directive")
962
+
963
+ for i, item in enumerate(symbol_data[1:], 1):
964
+ logger.debug(f"🔧 EXTENDS: Item {i}: {type(item)} - {item if not isinstance(item, list) else f'list[{len(item)}]'}")
965
+ if isinstance(item, list) and len(item) >= 2:
966
+ if item[0] == sexpdata.Symbol('extends'):
967
+ parent_name = str(item[1]).strip('"')
968
+ logger.debug(f"🔧 EXTENDS: Found extends directive: {parent_name}")
969
+ return parent_name
970
+
971
+ logger.debug(f"🔧 EXTENDS: No extends directive found")
972
+ return None
973
+
974
+ def _sync_wires_to_data(self):
975
+ """Sync wire collection state back to data structure."""
976
+ wire_data = []
977
+ for wire in self._wires:
978
+ wire_dict = {
979
+ "uuid": wire.uuid,
980
+ "points": [{"x": p.x, "y": p.y} for p in wire.points],
981
+ "wire_type": wire.wire_type.value,
982
+ "stroke_width": wire.stroke_width,
983
+ "stroke_type": wire.stroke_type
984
+ }
985
+ wire_data.append(wire_dict)
986
+
987
+ self._data["wires"] = wire_data
988
+
989
+ def _sync_junctions_to_data(self):
990
+ """Sync junction collection state back to data structure."""
991
+ junction_data = []
992
+ for junction in self._junctions:
993
+ junction_dict = {
994
+ "uuid": junction.uuid,
995
+ "position": {"x": junction.position.x, "y": junction.position.y},
996
+ "diameter": junction.diameter,
997
+ "color": junction.color
998
+ }
999
+ junction_data.append(junction_dict)
1000
+
1001
+ self._data["junctions"] = junction_data
1002
+
1003
+ def _convert_symbol_to_kicad_format(self, symbol: "SymbolDefinition", lib_id: str) -> Dict[str, Any]:
1004
+ """Convert SymbolDefinition to KiCAD lib_symbols format using raw parsed data."""
1005
+ # If we have raw KiCAD data from the library file, use it directly
1006
+ if hasattr(symbol, 'raw_kicad_data') and symbol.raw_kicad_data:
1007
+ return self._convert_raw_symbol_data(symbol.raw_kicad_data, lib_id)
1008
+
1009
+ # Fallback: create basic symbol structure
1010
+ return {
1011
+ "pin_numbers": {"hide": "yes"},
1012
+ "pin_names": {"offset": 0},
1013
+ "exclude_from_sim": "no",
1014
+ "in_bom": "yes",
1015
+ "on_board": "yes",
1016
+ "properties": {
1017
+ "Reference": {
1018
+ "value": symbol.reference_prefix,
1019
+ "at": [2.032, 0, 90],
1020
+ "effects": {"font": {"size": [1.27, 1.27]}}
1021
+ },
1022
+ "Value": {
1023
+ "value": symbol.reference_prefix,
1024
+ "at": [0, 0, 90],
1025
+ "effects": {"font": {"size": [1.27, 1.27]}}
1026
+ },
1027
+ "Footprint": {
1028
+ "value": "",
1029
+ "at": [-1.778, 0, 90],
1030
+ "effects": {
1031
+ "font": {"size": [1.27, 1.27]},
1032
+ "hide": "yes"
1033
+ }
1034
+ },
1035
+ "Datasheet": {
1036
+ "value": getattr(symbol, 'Datasheet', None) or getattr(symbol, 'datasheet', None) or "~",
1037
+ "at": [0, 0, 0],
1038
+ "effects": {
1039
+ "font": {"size": [1.27, 1.27]},
1040
+ "hide": "yes"
1041
+ }
1042
+ },
1043
+ "Description": {
1044
+ "value": getattr(symbol, 'Description', None) or getattr(symbol, 'description', None) or "Resistor",
1045
+ "at": [0, 0, 0],
1046
+ "effects": {
1047
+ "font": {"size": [1.27, 1.27]},
1048
+ "hide": "yes"
1049
+ }
1050
+ }
1051
+ },
1052
+ "embedded_fonts": "no"
1053
+ }
1054
+
1055
+ def _convert_raw_symbol_data(self, raw_data: List, lib_id: str) -> Dict[str, Any]:
1056
+ """Convert raw parsed KiCAD symbol data to dictionary format for S-expression generation."""
1057
+ import copy
1058
+ import sexpdata
1059
+
1060
+ # Make a copy and fix symbol name and string/symbol issues
1061
+ modified_data = copy.deepcopy(raw_data)
1062
+
1063
+ # Replace the symbol name with the full lib_id
1064
+ if len(modified_data) >= 2:
1065
+ modified_data[1] = lib_id # Change 'R' to 'Device:R'
1066
+
1067
+ # Fix extends directive to use full lib_id
1068
+ logger.debug(f"🔧 CONVERT: Processing {len(modified_data)} items for {lib_id}")
1069
+ for i, item in enumerate(modified_data[1:], 1):
1070
+ if isinstance(item, list) and len(item) >= 2:
1071
+ logger.debug(f"🔧 CONVERT: Item {i}: {item[0]} = {item[1] if len(item) > 1 else 'N/A'}")
1072
+ if item[0] == sexpdata.Symbol('extends'):
1073
+ # Convert bare symbol name to full lib_id
1074
+ parent_name = str(item[1]).strip('"')
1075
+ parent_lib_id = f"{lib_id.split(':')[0]}:{parent_name}"
1076
+ modified_data[i][1] = parent_lib_id
1077
+ logger.debug(f"🔧 CONVERT: Fixed extends directive: {parent_name} -> {parent_lib_id}")
1078
+ break
1079
+
1080
+ # Fix string/symbol conversion issues in pin definitions
1081
+ print(f"🔧 DEBUG: Before fix - checking for pin definitions...")
1082
+ self._fix_symbol_strings_recursively(modified_data)
1083
+ print(f"🔧 DEBUG: After fix - symbol strings fixed")
1084
+
1085
+ return modified_data
1086
+
1087
+ def _fix_symbol_strings_recursively(self, data):
1088
+ """Recursively fix string/symbol issues in parsed S-expression data."""
1089
+ import sexpdata
1090
+
1091
+ if isinstance(data, list):
1092
+ for i, item in enumerate(data):
1093
+ if isinstance(item, list):
1094
+ # Check for pin definitions that need fixing
1095
+ if (len(item) >= 3 and
1096
+ item[0] == sexpdata.Symbol('pin')):
1097
+ print(f"🔧 DEBUG: Found pin definition: {item[:3]} - types: {[type(x) for x in item[:3]]}")
1098
+ # Fix pin type and shape - ensure they are symbols not strings
1099
+ if isinstance(item[1], str):
1100
+ print(f"🔧 DEBUG: Converting pin type '{item[1]}' to symbol")
1101
+ item[1] = sexpdata.Symbol(item[1]) # pin type: "passive" -> passive
1102
+ if len(item) >= 3 and isinstance(item[2], str):
1103
+ print(f"🔧 DEBUG: Converting pin shape '{item[2]}' to symbol")
1104
+ item[2] = sexpdata.Symbol(item[2]) # pin shape: "line" -> line
1105
+
1106
+ # Recursively process nested lists
1107
+ self._fix_symbol_strings_recursively(item)
1108
+ elif isinstance(item, str):
1109
+ # Fix common KiCAD keywords that should be symbols
1110
+ if item in ['yes', 'no', 'default', 'none', 'left', 'right', 'center']:
1111
+ data[i] = sexpdata.Symbol(item)
1112
+
1113
+ return data
393
1114
 
394
1115
  @staticmethod
395
1116
  def _create_empty_schematic_data() -> Dict[str, Any]:
396
1117
  """Create empty schematic data structure."""
397
1118
  return {
398
- "version": "20230121",
399
- "generator": "kicad-sch-api",
1119
+ "version": "20250114",
1120
+ "generator": "eeschema",
1121
+ "generator_version": "9.0",
400
1122
  "uuid": str(uuid.uuid4()),
1123
+ "paper": "A4",
401
1124
  "title_block": {
402
1125
  "title": "Untitled",
403
1126
  "date": "",
@@ -411,6 +1134,14 @@ class Schematic:
411
1134
  "labels": [],
412
1135
  "nets": [],
413
1136
  "lib_symbols": {},
1137
+ "sheet_instances": [
1138
+ {
1139
+ "path": "/",
1140
+ "page": "1"
1141
+ }
1142
+ ],
1143
+ "symbol_instances": [],
1144
+ "embedded_fonts": "no",
414
1145
  }
415
1146
 
416
1147
  # Context manager support for atomic operations