kicad-sch-api 0.4.1__py3-none-any.whl → 0.5.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 (66) hide show
  1. kicad_sch_api/__init__.py +67 -2
  2. kicad_sch_api/cli/kicad_to_python.py +169 -0
  3. kicad_sch_api/collections/__init__.py +23 -8
  4. kicad_sch_api/collections/base.py +369 -59
  5. kicad_sch_api/collections/components.py +1376 -187
  6. kicad_sch_api/collections/junctions.py +129 -289
  7. kicad_sch_api/collections/labels.py +391 -287
  8. kicad_sch_api/collections/wires.py +202 -316
  9. kicad_sch_api/core/__init__.py +37 -2
  10. kicad_sch_api/core/component_bounds.py +34 -12
  11. kicad_sch_api/core/components.py +146 -7
  12. kicad_sch_api/core/config.py +25 -12
  13. kicad_sch_api/core/connectivity.py +692 -0
  14. kicad_sch_api/core/exceptions.py +175 -0
  15. kicad_sch_api/core/factories/element_factory.py +3 -1
  16. kicad_sch_api/core/formatter.py +24 -7
  17. kicad_sch_api/core/geometry.py +94 -5
  18. kicad_sch_api/core/managers/__init__.py +4 -0
  19. kicad_sch_api/core/managers/base.py +76 -0
  20. kicad_sch_api/core/managers/file_io.py +3 -1
  21. kicad_sch_api/core/managers/format_sync.py +3 -2
  22. kicad_sch_api/core/managers/graphics.py +3 -2
  23. kicad_sch_api/core/managers/hierarchy.py +661 -0
  24. kicad_sch_api/core/managers/metadata.py +4 -2
  25. kicad_sch_api/core/managers/sheet.py +52 -14
  26. kicad_sch_api/core/managers/text_elements.py +3 -2
  27. kicad_sch_api/core/managers/validation.py +3 -2
  28. kicad_sch_api/core/managers/wire.py +112 -54
  29. kicad_sch_api/core/parsing_utils.py +63 -0
  30. kicad_sch_api/core/pin_utils.py +103 -9
  31. kicad_sch_api/core/schematic.py +343 -29
  32. kicad_sch_api/core/types.py +79 -7
  33. kicad_sch_api/exporters/__init__.py +10 -0
  34. kicad_sch_api/exporters/python_generator.py +610 -0
  35. kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
  36. kicad_sch_api/geometry/__init__.py +15 -3
  37. kicad_sch_api/geometry/routing.py +211 -0
  38. kicad_sch_api/parsers/elements/label_parser.py +30 -8
  39. kicad_sch_api/parsers/elements/symbol_parser.py +255 -83
  40. kicad_sch_api/utils/logging.py +555 -0
  41. kicad_sch_api/utils/logging_decorators.py +587 -0
  42. kicad_sch_api/utils/validation.py +16 -22
  43. kicad_sch_api/wrappers/__init__.py +14 -0
  44. kicad_sch_api/wrappers/base.py +89 -0
  45. kicad_sch_api/wrappers/wire.py +198 -0
  46. kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
  47. kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
  48. kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
  49. {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
  50. mcp_server/__init__.py +34 -0
  51. mcp_server/example_logging_integration.py +506 -0
  52. mcp_server/models.py +252 -0
  53. mcp_server/server.py +357 -0
  54. mcp_server/tools/__init__.py +32 -0
  55. mcp_server/tools/component_tools.py +516 -0
  56. mcp_server/tools/connectivity_tools.py +532 -0
  57. mcp_server/tools/consolidated_tools.py +1216 -0
  58. mcp_server/tools/pin_discovery.py +333 -0
  59. mcp_server/utils/__init__.py +38 -0
  60. mcp_server/utils/logging.py +127 -0
  61. mcp_server/utils.py +36 -0
  62. kicad_sch_api-0.4.1.dist-info/METADATA +0 -491
  63. kicad_sch_api-0.4.1.dist-info/RECORD +0 -87
  64. kicad_sch_api-0.4.1.dist-info/entry_points.txt +0 -2
  65. {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
  66. {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -14,17 +14,22 @@ from typing import Any, Dict, List, Optional, Tuple, Union
14
14
 
15
15
  import sexpdata
16
16
 
17
+ from ..collections import (
18
+ ComponentCollection,
19
+ JunctionCollection,
20
+ LabelCollection,
21
+ LabelElement,
22
+ WireCollection,
23
+ )
17
24
  from ..library.cache import get_symbol_cache
18
25
  from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
19
- from .components import ComponentCollection
20
26
  from .factories import ElementFactory
21
27
  from .formatter import ExactFormatter
22
- from .junctions import JunctionCollection
23
- from .labels import LabelCollection
24
28
  from .managers import (
25
29
  FileIOManager,
26
30
  FormatSyncManager,
27
31
  GraphicsManager,
32
+ HierarchyManager,
28
33
  MetadataManager,
29
34
  SheetManager,
30
35
  TextElementManager,
@@ -52,7 +57,6 @@ from .types import (
52
57
  WireType,
53
58
  point_from_dict_or_tuple,
54
59
  )
55
- from .wires import WireCollection
56
60
 
57
61
  logger = logging.getLogger(__name__)
58
62
 
@@ -105,7 +109,7 @@ class Schematic:
105
109
  SchematicSymbol(**comp) if isinstance(comp, dict) else comp
106
110
  for comp in self._data.get("components", [])
107
111
  ]
108
- self._components = ComponentCollection(component_symbols)
112
+ self._components = ComponentCollection(component_symbols, parent_schematic=self)
109
113
 
110
114
  # Initialize wire collection
111
115
  wire_data = self._data.get("wires", [])
@@ -152,10 +156,11 @@ class Schematic:
152
156
  self._file_io_manager = FileIOManager()
153
157
  self._format_sync_manager = FormatSyncManager(self._data)
154
158
  self._graphics_manager = GraphicsManager(self._data)
159
+ self._hierarchy_manager = HierarchyManager(self._data)
155
160
  self._metadata_manager = MetadataManager(self._data)
156
161
  self._sheet_manager = SheetManager(self._data)
157
162
  self._text_element_manager = TextElementManager(self._data)
158
- self._wire_manager = WireManager(self._data, self._wires, self._components)
163
+ self._wire_manager = WireManager(self._data, self._wires, self._components, self)
159
164
  self._validation_manager = ValidationManager(self._data, self._components, self._wires)
160
165
 
161
166
  # Track modifications for save optimization
@@ -166,6 +171,11 @@ class Schematic:
166
171
  self._operation_count = 0
167
172
  self._total_operation_time = 0.0
168
173
 
174
+ # Hierarchical design context (for child schematics)
175
+ self._parent_uuid: Optional[str] = None
176
+ self._sheet_uuid: Optional[str] = None
177
+ self._hierarchy_path: Optional[str] = None
178
+
169
179
  logger.debug(
170
180
  f"Schematic initialized with {len(self._components)} components, {len(self._wires)} wires, "
171
181
  f"{len(self._junctions)} junctions, {len(self._texts)} texts, {len(self._labels)} labels, "
@@ -314,12 +324,12 @@ class Schematic:
314
324
  """Whether schematic has been modified since last save."""
315
325
  return (
316
326
  self._modified
317
- or self._components._modified
318
- or self._wires._modified
319
- or self._junctions._modified
327
+ or self._components.modified
328
+ or self._wires.modified
329
+ or self._junctions.modified
320
330
  or self._texts._modified
321
- or self._labels._modified
322
- or self._hierarchical_labels._modified
331
+ or self._labels.modified
332
+ or self._hierarchical_labels.modified
323
333
  or self._no_connects._modified
324
334
  or self._nets._modified
325
335
  or self._format_sync_manager.is_dirty()
@@ -350,6 +360,71 @@ class Schematic:
350
360
  """Collection of all electrical nets in the schematic."""
351
361
  return self._nets
352
362
 
363
+ @property
364
+ def sheets(self):
365
+ """Sheet manager for hierarchical sheet operations."""
366
+ return self._sheet_manager
367
+
368
+ @property
369
+ def hierarchy(self):
370
+ """
371
+ Advanced hierarchy manager for complex hierarchical designs.
372
+
373
+ Provides features for:
374
+ - Sheet reuse tracking (sheets used multiple times)
375
+ - Cross-sheet signal tracking
376
+ - Sheet pin validation
377
+ - Hierarchy flattening
378
+ - Signal tracing through hierarchy
379
+ """
380
+ return self._hierarchy_manager
381
+
382
+ def set_hierarchy_context(self, parent_uuid: str, sheet_uuid: str) -> None:
383
+ """
384
+ Set hierarchical context for this schematic (for child schematics in hierarchical designs).
385
+
386
+ This method configures a child schematic to be part of a hierarchical design.
387
+ Components added after this call will automatically have the correct hierarchical
388
+ instance path for proper annotation in KiCad.
389
+
390
+ Args:
391
+ parent_uuid: UUID of the parent schematic
392
+ sheet_uuid: UUID of the sheet instance in the parent schematic
393
+
394
+ Example:
395
+ >>> # Create parent schematic
396
+ >>> main = ksa.create_schematic("MyProject")
397
+ >>> parent_uuid = main.uuid
398
+ >>>
399
+ >>> # Add sheet to parent and get its UUID
400
+ >>> sheet_uuid = main.sheets.add_sheet(
401
+ ... name="Power Supply",
402
+ ... filename="power.kicad_sch",
403
+ ... position=(50, 50),
404
+ ... size=(100, 100),
405
+ ... project_name="MyProject"
406
+ ... )
407
+ >>>
408
+ >>> # Create child schematic with hierarchy context
409
+ >>> power = ksa.create_schematic("MyProject")
410
+ >>> power.set_hierarchy_context(parent_uuid, sheet_uuid)
411
+ >>>
412
+ >>> # Components added now will have correct hierarchical path
413
+ >>> vreg = power.components.add('Device:R', 'U1', 'AMS1117-3.3')
414
+
415
+ Note:
416
+ - This must be called BEFORE adding components to the child schematic
417
+ - Both parent and child schematics must use the same project name
418
+ - The hierarchical path will be: /{parent_uuid}/{sheet_uuid}
419
+ """
420
+ self._parent_uuid = parent_uuid
421
+ self._sheet_uuid = sheet_uuid
422
+ self._hierarchy_path = f"/{parent_uuid}/{sheet_uuid}"
423
+
424
+ logger.info(
425
+ f"Set hierarchy context: parent={parent_uuid}, sheet={sheet_uuid}, path={self._hierarchy_path}"
426
+ )
427
+
353
428
  # Pin positioning methods (delegated to WireManager)
354
429
  def get_component_pin_position(self, reference: str, pin_number: str) -> Optional[Point]:
355
430
  """
@@ -376,6 +451,59 @@ class Schematic:
376
451
  """
377
452
  return self._wire_manager.list_component_pins(reference)
378
453
 
454
+ # Connectivity methods (delegated to WireManager)
455
+ def are_pins_connected(
456
+ self, component1_ref: str, pin1_number: str, component2_ref: str, pin2_number: str
457
+ ) -> bool:
458
+ """
459
+ Check if two pins are electrically connected.
460
+
461
+ Performs full connectivity analysis including connections through:
462
+ - Direct wires
463
+ - Junctions
464
+ - Labels (local/global/hierarchical)
465
+ - Power symbols
466
+ - Hierarchical sheets
467
+
468
+ Args:
469
+ component1_ref: First component reference (e.g., "R1")
470
+ pin1_number: First pin number
471
+ component2_ref: Second component reference (e.g., "R2")
472
+ pin2_number: Second pin number
473
+
474
+ Returns:
475
+ True if pins are electrically connected, False otherwise
476
+ """
477
+ return self._wire_manager.are_pins_connected(
478
+ component1_ref, pin1_number, component2_ref, pin2_number
479
+ )
480
+
481
+ def get_net_for_pin(self, component_ref: str, pin_number: str):
482
+ """
483
+ Get the electrical net connected to a specific pin.
484
+
485
+ Args:
486
+ component_ref: Component reference (e.g., "R1")
487
+ pin_number: Pin number
488
+
489
+ Returns:
490
+ Net object if pin is connected, None otherwise
491
+ """
492
+ return self._wire_manager.get_net_for_pin(component_ref, pin_number)
493
+
494
+ def get_connected_pins(self, component_ref: str, pin_number: str) -> List[Tuple[str, str]]:
495
+ """
496
+ Get all pins electrically connected to a specific pin.
497
+
498
+ Args:
499
+ component_ref: Component reference (e.g., "R1")
500
+ pin_number: Pin number
501
+
502
+ Returns:
503
+ List of (reference, pin_number) tuples for all connected pins
504
+ """
505
+ return self._wire_manager.get_connected_pins(component_ref, pin_number)
506
+
379
507
  # File operations (delegated to FileIOManager)
380
508
  def save(self, file_path: Optional[Union[str, Path]] = None, preserve_format: bool = True):
381
509
  """
@@ -423,7 +551,11 @@ class Schematic:
423
551
 
424
552
  # Update state
425
553
  self._modified = False
426
- self._components._modified = False
554
+ self._components.mark_saved()
555
+ self._wires.mark_saved()
556
+ self._junctions.mark_saved()
557
+ self._labels.mark_saved()
558
+ self._hierarchical_labels.mark_saved()
427
559
  self._format_sync_manager.clear_dirty_flags()
428
560
  self._last_save_time = time.time()
429
561
 
@@ -449,6 +581,59 @@ class Schematic:
449
581
 
450
582
  return self._file_io_manager.create_backup(self._file_path, suffix)
451
583
 
584
+ def export_to_python(
585
+ self,
586
+ output_path: Union[str, Path],
587
+ template: str = 'default',
588
+ include_hierarchy: bool = True,
589
+ format_code: bool = True,
590
+ add_comments: bool = True
591
+ ) -> Path:
592
+ """
593
+ Export schematic to executable Python code.
594
+
595
+ Generates Python code that uses kicad-sch-api to recreate this
596
+ schematic programmatically.
597
+
598
+ Args:
599
+ output_path: Output .py file path
600
+ template: Code template style ('minimal', 'default', 'verbose', 'documented')
601
+ include_hierarchy: Include hierarchical sheets
602
+ format_code: Format code with Black
603
+ add_comments: Add explanatory comments
604
+
605
+ Returns:
606
+ Path to generated Python file
607
+
608
+ Raises:
609
+ CodeGenerationError: If code generation fails
610
+
611
+ Example:
612
+ >>> sch = Schematic.load('circuit.kicad_sch')
613
+ >>> sch.export_to_python('circuit.py')
614
+ PosixPath('circuit.py')
615
+
616
+ >>> sch.export_to_python('circuit.py',
617
+ ... template='verbose',
618
+ ... add_comments=True)
619
+ PosixPath('circuit.py')
620
+ """
621
+ from ..exporters.python_generator import PythonCodeGenerator
622
+
623
+ generator = PythonCodeGenerator(
624
+ template=template,
625
+ format_code=format_code,
626
+ add_comments=add_comments
627
+ )
628
+
629
+ generator.generate(
630
+ schematic=self,
631
+ include_hierarchy=include_hierarchy,
632
+ output_path=Path(output_path)
633
+ )
634
+
635
+ return Path(output_path)
636
+
452
637
  # Wire operations (delegated to WireManager)
453
638
  def add_wire(
454
639
  self, start: Union[Point, Tuple[float, float]], end: Union[Point, Tuple[float, float]]
@@ -577,9 +762,10 @@ class Schematic:
577
762
  def add_label(
578
763
  self,
579
764
  text: str,
580
- position: Union[Point, Tuple[float, float]],
765
+ position: Optional[Union[Point, Tuple[float, float]]] = None,
766
+ pin: Optional[Tuple[str, str]] = None,
581
767
  effects: Optional[Dict[str, Any]] = None,
582
- rotation: float = 0,
768
+ rotation: Optional[float] = None,
583
769
  size: Optional[float] = None,
584
770
  uuid: Optional[str] = None,
585
771
  ) -> str:
@@ -588,19 +774,84 @@ class Schematic:
588
774
 
589
775
  Args:
590
776
  text: Label text content
591
- position: Label position
777
+ position: Label position (required if pin not provided)
778
+ pin: Pin to attach label to as (component_ref, pin_number) tuple (alternative to position)
592
779
  effects: Text effects (size, font, etc.)
593
- rotation: Label rotation in degrees (default 0)
780
+ rotation: Label rotation in degrees (default 0, or auto-calculated if pin provided)
594
781
  size: Text size override (default from effects)
595
782
  uuid: Specific UUID for label (auto-generated if None)
596
783
 
597
784
  Returns:
598
785
  UUID of created label
599
- """
786
+
787
+ Raises:
788
+ ValueError: If neither position nor pin is provided, or if pin is not found
789
+ """
790
+ from .pin_utils import get_component_pin_info
791
+
792
+ # Validate arguments
793
+ if position is None and pin is None:
794
+ raise ValueError("Either position or pin must be provided")
795
+ if position is not None and pin is not None:
796
+ raise ValueError("Cannot provide both position and pin")
797
+
798
+ # Handle pin-based placement
799
+ justify_h = "left"
800
+ justify_v = "bottom"
801
+
802
+ if pin is not None:
803
+ component_ref, pin_number = pin
804
+
805
+ # Get component
806
+ component = self._components.get(component_ref)
807
+ if component is None:
808
+ raise ValueError(f"Component {component_ref} not found")
809
+
810
+ # Get pin position and rotation
811
+ pin_info = get_component_pin_info(component, pin_number)
812
+ if pin_info is None:
813
+ raise ValueError(f"Pin {pin_number} not found on component {component_ref}")
814
+
815
+ pin_position, pin_rotation = pin_info
816
+ position = pin_position
817
+
818
+ # Calculate label rotation if not explicitly provided
819
+ if rotation is None:
820
+ # Label should face away from component:
821
+ # Pin rotation indicates where pin points INTO the component
822
+ # Label should face OPPOSITE direction
823
+ rotation = (pin_rotation + 180) % 360
824
+ logger.info(
825
+ f"Auto-calculated label rotation: {rotation}° (pin rotation: {pin_rotation}°)"
826
+ )
827
+
828
+ # Calculate justification based on pin angle
829
+ # This determines which corner of the text is anchored to the pin position
830
+ if pin_rotation == 0: # Pin points right into component
831
+ justify_h = "left"
832
+ justify_v = "bottom"
833
+ elif pin_rotation == 90: # Pin points up into component
834
+ justify_h = "right"
835
+ justify_v = "bottom"
836
+ elif pin_rotation == 180: # Pin points left into component
837
+ justify_h = "right"
838
+ justify_v = "bottom"
839
+ elif pin_rotation == 270: # Pin points down into component
840
+ justify_h = "left"
841
+ justify_v = "bottom"
842
+ logger.info(f"Auto-calculated justification: {justify_h} {justify_v} (pin angle: {pin_rotation}°)")
843
+
844
+ # Use default rotation if still not set
845
+ if rotation is None:
846
+ rotation = 0
847
+
600
848
  # Use the new labels collection instead of manager
601
849
  if size is None:
602
850
  size = 1.27 # Default size
603
- label = self._labels.add(text, position, rotation=rotation, size=size, label_uuid=uuid)
851
+ label = self._labels.add(
852
+ text, position, rotation=rotation, size=size,
853
+ justify_h=justify_h, justify_v=justify_v, uuid=uuid
854
+ )
604
855
  self._sync_labels_to_data() # Sync immediately
605
856
  self._format_sync_manager.mark_dirty("label", "add", {"uuid": label.uuid})
606
857
  self._modified = True
@@ -837,28 +1088,42 @@ class Schematic:
837
1088
  sheet_uuid: str,
838
1089
  name: str,
839
1090
  pin_type: str,
840
- position: Union[Point, Tuple[float, float]],
841
- rotation: float = 0,
842
- justify: str = "left",
1091
+ edge: str,
1092
+ position_along_edge: float,
843
1093
  uuid: Optional[str] = None,
844
1094
  ) -> str:
845
1095
  """
846
- Add a pin to a hierarchical sheet.
1096
+ Add a pin to a hierarchical sheet using edge-based positioning.
847
1097
 
848
1098
  Args:
849
1099
  sheet_uuid: UUID of the sheet to add pin to
850
1100
  name: Pin name
851
- pin_type: Pin type (input, output, bidirectional, etc.)
852
- position: Pin position
853
- rotation: Pin rotation in degrees
854
- justify: Text justification
1101
+ pin_type: Pin type (input, output, bidirectional, tri_state, passive)
1102
+ edge: Edge to place pin on ("right", "bottom", "left", "top")
1103
+ position_along_edge: Distance along edge from reference corner (mm)
855
1104
  uuid: Optional UUID for the pin
856
1105
 
857
1106
  Returns:
858
1107
  UUID of created sheet pin
1108
+
1109
+ Edge positioning (clockwise from right):
1110
+ - "right": Pins face right (0°), position measured from top edge
1111
+ - "bottom": Pins face down (270°), position measured from left edge
1112
+ - "left": Pins face left (180°), position measured from bottom edge
1113
+ - "top": Pins face up (90°), position measured from left edge
1114
+
1115
+ Example:
1116
+ >>> # Sheet at (100, 100) with size (50, 40)
1117
+ >>> sch.add_sheet_pin(
1118
+ ... sheet_uuid=sheet_id,
1119
+ ... name="DATA_IN",
1120
+ ... pin_type="input",
1121
+ ... edge="left",
1122
+ ... position_along_edge=20 # 20mm from top on left edge
1123
+ ... )
859
1124
  """
860
1125
  pin_uuid = self._sheet_manager.add_sheet_pin(
861
- sheet_uuid, name, pin_type, position, rotation, justify, uuid_str=uuid
1126
+ sheet_uuid, name, pin_type, edge, position_along_edge, uuid_str=uuid
862
1127
  )
863
1128
  self._format_sync_manager.mark_dirty("sheet", "modify", {"uuid": sheet_uuid})
864
1129
  self._modified = True
@@ -898,7 +1163,7 @@ class Schematic:
898
1163
  start: Top-left corner position
899
1164
  end: Bottom-right corner position
900
1165
  stroke_width: Line width
901
- stroke_type: Line type (solid, dashed, etc.)
1166
+ stroke_type: Line type (solid, dash, dash_dot, dash_dot_dot, dot, or default)
902
1167
  fill_type: Fill type (none, background, etc.)
903
1168
  stroke_color: Stroke color as (r, g, b, a)
904
1169
  fill_color: Fill color as (r, g, b, a)
@@ -906,6 +1171,14 @@ class Schematic:
906
1171
  Returns:
907
1172
  UUID of created rectangle
908
1173
  """
1174
+ # Validate stroke_type
1175
+ valid_stroke_types = ["solid", "dash", "dash_dot", "dash_dot_dot", "dot", "default"]
1176
+ if stroke_type not in valid_stroke_types:
1177
+ raise ValueError(
1178
+ f"Invalid stroke_type '{stroke_type}'. "
1179
+ f"Must be one of: {', '.join(valid_stroke_types)}"
1180
+ )
1181
+
909
1182
  # Convert individual parameters to stroke/fill dicts
910
1183
  stroke = {"width": stroke_width, "type": stroke_type}
911
1184
  if stroke_color:
@@ -1159,10 +1432,13 @@ class Schematic:
1159
1432
  @staticmethod
1160
1433
  def _create_empty_schematic_data() -> Dict[str, Any]:
1161
1434
  """Create empty schematic data structure."""
1435
+ from uuid import uuid4
1436
+
1162
1437
  return {
1163
1438
  "version": "20250114",
1164
1439
  "generator": "eeschema",
1165
1440
  "generator_version": "9.0",
1441
+ "uuid": str(uuid4()),
1166
1442
  "paper": "A4",
1167
1443
  "lib_symbols": {},
1168
1444
  "symbol": [],
@@ -1216,7 +1492,43 @@ class Schematic:
1216
1492
  # Internal sync methods (migrated from original implementation)
1217
1493
  def _sync_components_to_data(self):
1218
1494
  """Sync component collection state back to data structure."""
1219
- self._data["components"] = [comp._data.__dict__ for comp in self._components]
1495
+ logger.debug("🔍 _sync_components_to_data: Syncing components to _data")
1496
+
1497
+ components_data = []
1498
+ for comp in self._components:
1499
+ # Start with base component data
1500
+ comp_dict = {k: v for k, v in comp._data.__dict__.items() if not k.startswith("_")}
1501
+
1502
+ # CRITICAL FIX: Explicitly preserve instances if user set them
1503
+ if hasattr(comp._data, "instances") and comp._data.instances:
1504
+ logger.debug(
1505
+ f" Component {comp._data.reference} has {len(comp._data.instances)} instance(s)"
1506
+ )
1507
+ comp_dict["instances"] = [
1508
+ {
1509
+ "project": (
1510
+ getattr(inst, "project", self.name)
1511
+ if hasattr(inst, "project")
1512
+ else self.name
1513
+ ),
1514
+ "path": inst.path, # PRESERVE exact path user set!
1515
+ "reference": inst.reference,
1516
+ "unit": inst.unit,
1517
+ }
1518
+ for inst in comp._data.instances
1519
+ ]
1520
+ logger.debug(
1521
+ f" Instance paths: {[inst.path for inst in comp._data.instances]}"
1522
+ )
1523
+ else:
1524
+ logger.debug(
1525
+ f" Component {comp._data.reference} has NO instances (will be generated by parser)"
1526
+ )
1527
+
1528
+ components_data.append(comp_dict)
1529
+
1530
+ self._data["components"] = components_data
1531
+ logger.debug(f" Synced {len(components_data)} components to _data")
1220
1532
 
1221
1533
  # Populate lib_symbols with actual symbol definitions used by components
1222
1534
  lib_symbols = {}
@@ -1297,6 +1609,8 @@ class Schematic:
1297
1609
  "position": {"x": label_element.position.x, "y": label_element.position.y},
1298
1610
  "rotation": label_element.rotation,
1299
1611
  "size": label_element.size,
1612
+ "justify_h": label_element._data.justify_h,
1613
+ "justify_v": label_element._data.justify_v,
1300
1614
  }
1301
1615
  label_data.append(label_dict)
1302
1616
 
@@ -155,6 +155,54 @@ class SchematicPin:
155
155
  )
156
156
 
157
157
 
158
+ @dataclass
159
+ class PinInfo:
160
+ """
161
+ Complete pin information for a component pin.
162
+
163
+ This dataclass provides comprehensive pin metadata including position,
164
+ electrical properties, and graphical representation. Positions are in
165
+ schematic coordinates (absolute positions accounting for component
166
+ rotation and mirroring).
167
+ """
168
+
169
+ number: str # Pin number (e.g., "1", "2", "A1")
170
+ name: str # Pin name (e.g., "VCC", "GND", "CLK")
171
+ position: Point # Absolute position in schematic coordinates (mm)
172
+ electrical_type: PinType = PinType.PASSIVE # Electrical type (input, output, passive, etc.)
173
+ shape: PinShape = PinShape.LINE # Graphical shape (line, inverted, clock, etc.)
174
+ length: float = 2.54 # Pin length in mm
175
+ orientation: float = 0.0 # Pin orientation in degrees (0, 90, 180, 270)
176
+ uuid: str = "" # Unique identifier for this pin instance
177
+
178
+ def __post_init__(self) -> None:
179
+ """Validate and normalize pin information."""
180
+ # Ensure types are correct
181
+ self.electrical_type = (
182
+ PinType(self.electrical_type)
183
+ if isinstance(self.electrical_type, str)
184
+ else self.electrical_type
185
+ )
186
+ self.shape = PinShape(self.shape) if isinstance(self.shape, str) else self.shape
187
+
188
+ # Generate UUID if not provided
189
+ if not self.uuid:
190
+ self.uuid = str(uuid4())
191
+
192
+ def to_dict(self) -> Dict[str, Any]:
193
+ """Convert pin info to dictionary representation."""
194
+ return {
195
+ "number": self.number,
196
+ "name": self.name,
197
+ "position": {"x": self.position.x, "y": self.position.y},
198
+ "electrical_type": self.electrical_type.value,
199
+ "shape": self.shape.value,
200
+ "length": self.length,
201
+ "orientation": self.orientation,
202
+ "uuid": self.uuid,
203
+ }
204
+
205
+
158
206
  @dataclass
159
207
  class SchematicSymbol:
160
208
  """Component symbol in a schematic."""
@@ -171,6 +219,7 @@ class SchematicSymbol:
171
219
  in_bom: bool = True
172
220
  on_board: bool = True
173
221
  unit: int = 1
222
+ instances: List["SymbolInstance"] = field(default_factory=list) # FIX: Add instances field for hierarchical support
174
223
 
175
224
  def __post_init__(self) -> None:
176
225
  # Generate UUID if not provided
@@ -195,16 +244,37 @@ class SchematicSymbol:
195
244
  return None
196
245
 
197
246
  def get_pin_position(self, pin_number: str) -> Optional[Point]:
198
- """Get absolute position of a pin."""
247
+ """Get absolute position of a pin with rotation transformation.
248
+
249
+ Args:
250
+ pin_number: Pin number to get position for
251
+
252
+ Returns:
253
+ Absolute position of the pin in schematic coordinates, or None if pin not found
254
+
255
+ Note:
256
+ Applies standard 2D rotation matrix to transform pin position from
257
+ symbol's local coordinate system to schematic's global coordinate system.
258
+ """
259
+ import math
260
+
199
261
  pin = self.get_pin(pin_number)
200
262
  if not pin:
201
263
  return None
202
- # TODO: Apply rotation and symbol position transformation
203
- # NOTE: Currently assumes rotation. For rotated components, pin positions
204
- # would need to be transformed using rotation matrix before adding to component position.
205
- # This affects pin-to-pin wiring accuracy for rotated components.
206
- # Priority: MEDIUM - Would improve wiring accuracy for rotated components
207
- return Point(self.position.x + pin.position.x, self.position.y + pin.position.y)
264
+
265
+ # Apply rotation transformation using standard 2D rotation matrix
266
+ # [x'] = [cos(θ) -sin(θ)] [x]
267
+ # [y'] [sin(θ) cos(θ)] [y]
268
+ angle_rad = math.radians(self.rotation)
269
+ cos_a = math.cos(angle_rad)
270
+ sin_a = math.sin(angle_rad)
271
+
272
+ # Rotate pin position from symbol's local coordinates
273
+ rotated_x = pin.position.x * cos_a - pin.position.y * sin_a
274
+ rotated_y = pin.position.x * sin_a + pin.position.y * cos_a
275
+
276
+ # Add to component position to get absolute position
277
+ return Point(self.position.x + rotated_x, self.position.y + rotated_y)
208
278
 
209
279
 
210
280
  class WireType(Enum):
@@ -320,6 +390,8 @@ class Label:
320
390
  rotation: float = 0.0
321
391
  size: float = 1.27
322
392
  shape: Optional[HierarchicalLabelShape] = None # Only for hierarchical labels
393
+ justify_h: str = "left" # Horizontal justification: "left", "right", "center"
394
+ justify_v: str = "bottom" # Vertical justification: "top", "bottom", "center"
323
395
 
324
396
  def __post_init__(self) -> None:
325
397
  if not self.uuid:
@@ -0,0 +1,10 @@
1
+ """
2
+ Exporters module for kicad-sch-api.
3
+
4
+ This module provides functionality to export KiCad schematics to various formats,
5
+ starting with Python code export.
6
+ """
7
+
8
+ from .python_generator import PythonCodeGenerator
9
+
10
+ __all__ = ['PythonCodeGenerator']