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.
- kicad_sch_api/__init__.py +67 -2
- kicad_sch_api/cli/kicad_to_python.py +169 -0
- kicad_sch_api/collections/__init__.py +23 -8
- kicad_sch_api/collections/base.py +369 -59
- kicad_sch_api/collections/components.py +1376 -187
- kicad_sch_api/collections/junctions.py +129 -289
- kicad_sch_api/collections/labels.py +391 -287
- kicad_sch_api/collections/wires.py +202 -316
- kicad_sch_api/core/__init__.py +37 -2
- kicad_sch_api/core/component_bounds.py +34 -12
- kicad_sch_api/core/components.py +146 -7
- kicad_sch_api/core/config.py +25 -12
- kicad_sch_api/core/connectivity.py +692 -0
- kicad_sch_api/core/exceptions.py +175 -0
- kicad_sch_api/core/factories/element_factory.py +3 -1
- kicad_sch_api/core/formatter.py +24 -7
- kicad_sch_api/core/geometry.py +94 -5
- kicad_sch_api/core/managers/__init__.py +4 -0
- kicad_sch_api/core/managers/base.py +76 -0
- kicad_sch_api/core/managers/file_io.py +3 -1
- kicad_sch_api/core/managers/format_sync.py +3 -2
- kicad_sch_api/core/managers/graphics.py +3 -2
- kicad_sch_api/core/managers/hierarchy.py +661 -0
- kicad_sch_api/core/managers/metadata.py +4 -2
- kicad_sch_api/core/managers/sheet.py +52 -14
- kicad_sch_api/core/managers/text_elements.py +3 -2
- kicad_sch_api/core/managers/validation.py +3 -2
- kicad_sch_api/core/managers/wire.py +112 -54
- kicad_sch_api/core/parsing_utils.py +63 -0
- kicad_sch_api/core/pin_utils.py +103 -9
- kicad_sch_api/core/schematic.py +343 -29
- kicad_sch_api/core/types.py +79 -7
- kicad_sch_api/exporters/__init__.py +10 -0
- kicad_sch_api/exporters/python_generator.py +610 -0
- kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
- kicad_sch_api/geometry/__init__.py +15 -3
- kicad_sch_api/geometry/routing.py +211 -0
- kicad_sch_api/parsers/elements/label_parser.py +30 -8
- kicad_sch_api/parsers/elements/symbol_parser.py +255 -83
- kicad_sch_api/utils/logging.py +555 -0
- kicad_sch_api/utils/logging_decorators.py +587 -0
- kicad_sch_api/utils/validation.py +16 -22
- kicad_sch_api/wrappers/__init__.py +14 -0
- kicad_sch_api/wrappers/base.py +89 -0
- kicad_sch_api/wrappers/wire.py +198 -0
- kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
- kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
- kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
- {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
- mcp_server/__init__.py +34 -0
- mcp_server/example_logging_integration.py +506 -0
- mcp_server/models.py +252 -0
- mcp_server/server.py +357 -0
- mcp_server/tools/__init__.py +32 -0
- mcp_server/tools/component_tools.py +516 -0
- mcp_server/tools/connectivity_tools.py +532 -0
- mcp_server/tools/consolidated_tools.py +1216 -0
- mcp_server/tools/pin_discovery.py +333 -0
- mcp_server/utils/__init__.py +38 -0
- mcp_server/utils/logging.py +127 -0
- mcp_server/utils.py +36 -0
- kicad_sch_api-0.4.1.dist-info/METADATA +0 -491
- kicad_sch_api-0.4.1.dist-info/RECORD +0 -87
- kicad_sch_api-0.4.1.dist-info/entry_points.txt +0 -2
- {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
kicad_sch_api/core/schematic.py
CHANGED
|
@@ -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.
|
|
318
|
-
or self._wires.
|
|
319
|
-
or self._junctions.
|
|
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.
|
|
322
|
-
or self._hierarchical_labels.
|
|
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.
|
|
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 =
|
|
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(
|
|
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
|
-
|
|
841
|
-
|
|
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,
|
|
852
|
-
|
|
853
|
-
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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
|
|
kicad_sch_api/core/types.py
CHANGED
|
@@ -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
|
-
|
|
203
|
-
#
|
|
204
|
-
#
|
|
205
|
-
#
|
|
206
|
-
|
|
207
|
-
|
|
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']
|