kicad-sch-api 0.2.0__py3-none-any.whl → 0.2.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 +6 -2
- kicad_sch_api/cli.py +67 -62
- kicad_sch_api/core/component_bounds.py +477 -0
- kicad_sch_api/core/components.py +22 -10
- kicad_sch_api/core/config.py +127 -0
- kicad_sch_api/core/formatter.py +183 -23
- kicad_sch_api/core/geometry.py +111 -0
- kicad_sch_api/core/ic_manager.py +43 -37
- kicad_sch_api/core/junctions.py +17 -22
- kicad_sch_api/core/manhattan_routing.py +430 -0
- kicad_sch_api/core/parser.py +495 -196
- kicad_sch_api/core/pin_utils.py +149 -0
- kicad_sch_api/core/schematic.py +630 -207
- kicad_sch_api/core/simple_manhattan.py +228 -0
- kicad_sch_api/core/types.py +9 -4
- kicad_sch_api/core/wire_routing.py +380 -0
- kicad_sch_api/core/wires.py +29 -25
- kicad_sch_api/discovery/__init__.py +1 -1
- kicad_sch_api/discovery/search_index.py +142 -107
- kicad_sch_api/library/cache.py +70 -62
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.1.dist-info}/METADATA +212 -40
- kicad_sch_api-0.2.1.dist-info/RECORD +31 -0
- kicad_sch_api-0.2.0.dist-info/RECORD +0 -24
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.1.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.1.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.1.dist-info}/top_level.txt +0 -0
kicad_sch_api/library/cache.py
CHANGED
|
@@ -41,7 +41,7 @@ class SymbolDefinition:
|
|
|
41
41
|
|
|
42
42
|
# Raw KiCAD data for exact format preservation
|
|
43
43
|
raw_kicad_data: Any = None
|
|
44
|
-
|
|
44
|
+
|
|
45
45
|
# Symbol inheritance
|
|
46
46
|
extends: Optional[str] = None # Parent symbol name if this symbol extends another
|
|
47
47
|
|
|
@@ -263,7 +263,7 @@ class SymbolLibraryCache:
|
|
|
263
263
|
Symbol definition if found, None otherwise
|
|
264
264
|
"""
|
|
265
265
|
logger.debug(f"🔧 CACHE: Requesting symbol: {lib_id}")
|
|
266
|
-
|
|
266
|
+
|
|
267
267
|
# Check cache first
|
|
268
268
|
if lib_id in self._symbols:
|
|
269
269
|
self._cache_hits += 1
|
|
@@ -357,7 +357,7 @@ class SymbolLibraryCache:
|
|
|
357
357
|
def _load_symbol(self, lib_id: str) -> Optional[SymbolDefinition]:
|
|
358
358
|
"""Load a single symbol from its library."""
|
|
359
359
|
logger.debug(f"🔧 LOAD: Loading symbol {lib_id}")
|
|
360
|
-
|
|
360
|
+
|
|
361
361
|
if ":" not in lib_id:
|
|
362
362
|
logger.warning(f"🔧 LOAD: Invalid lib_id format: {lib_id}")
|
|
363
363
|
return None
|
|
@@ -423,29 +423,29 @@ class SymbolLibraryCache:
|
|
|
423
423
|
try:
|
|
424
424
|
# Extract symbol name from lib_id
|
|
425
425
|
library_name, symbol_name = lib_id.split(":", 1)
|
|
426
|
-
|
|
427
|
-
with open(library_path,
|
|
426
|
+
|
|
427
|
+
with open(library_path, "r", encoding="utf-8") as f:
|
|
428
428
|
content = f.read()
|
|
429
429
|
|
|
430
430
|
# Parse the S-expression with symbol preservation
|
|
431
431
|
parsed = sexpdata.loads(content, true=None, false=None, nil=None)
|
|
432
432
|
logger.debug(f"🔧 PARSE: Parsed library file with {len(parsed)} top-level items")
|
|
433
|
-
|
|
433
|
+
|
|
434
434
|
# Find the symbol we're looking for
|
|
435
435
|
symbol_data = self._find_symbol_in_parsed_data(parsed, symbol_name)
|
|
436
436
|
if not symbol_data:
|
|
437
437
|
logger.debug(f"🔧 PARSE: Symbol {symbol_name} not found in {library_path}")
|
|
438
438
|
return None
|
|
439
|
-
|
|
439
|
+
|
|
440
440
|
logger.debug(f"🔧 PARSE: Found symbol {symbol_name} in library")
|
|
441
441
|
|
|
442
442
|
# Extract the library name and symbol name for resolution
|
|
443
443
|
library_name, symbol_name = lib_id.split(":", 1)
|
|
444
|
-
|
|
444
|
+
|
|
445
445
|
# Check if this symbol extends another symbol
|
|
446
446
|
extends_symbol = self._check_extends_directive(symbol_data)
|
|
447
447
|
logger.debug(f"🔧 CACHE: Symbol {lib_id} extends: {extends_symbol}")
|
|
448
|
-
|
|
448
|
+
|
|
449
449
|
# If this symbol extends another, we need to resolve it
|
|
450
450
|
if extends_symbol:
|
|
451
451
|
resolved_symbol_data = self._resolve_extends_relationship(
|
|
@@ -455,7 +455,7 @@ class SymbolLibraryCache:
|
|
|
455
455
|
symbol_data = resolved_symbol_data
|
|
456
456
|
extends_symbol = None # Clear extends after resolution
|
|
457
457
|
logger.debug(f"🔧 CACHE: Resolved extends for {lib_id}")
|
|
458
|
-
|
|
458
|
+
|
|
459
459
|
# Extract symbol information
|
|
460
460
|
result = {
|
|
461
461
|
"raw_data": symbol_data, # Store the raw parsed data
|
|
@@ -464,26 +464,26 @@ class SymbolLibraryCache:
|
|
|
464
464
|
"keywords": "",
|
|
465
465
|
"datasheet": "~",
|
|
466
466
|
"pins": [],
|
|
467
|
-
"extends": extends_symbol # Should be None after resolution
|
|
467
|
+
"extends": extends_symbol, # Should be None after resolution
|
|
468
468
|
}
|
|
469
469
|
|
|
470
470
|
# Extract properties from the symbol
|
|
471
471
|
for item in symbol_data[1:]:
|
|
472
472
|
if isinstance(item, list) and len(item) > 0:
|
|
473
|
-
if item[0] == sexpdata.Symbol(
|
|
473
|
+
if item[0] == sexpdata.Symbol("property"):
|
|
474
474
|
prop_name = item[1]
|
|
475
475
|
prop_value = item[2]
|
|
476
|
-
|
|
476
|
+
|
|
477
477
|
logger.debug(f"🔧 Processing property: {prop_name} = {prop_value}")
|
|
478
|
-
if prop_name == sexpdata.Symbol(
|
|
478
|
+
if prop_name == sexpdata.Symbol("Reference"):
|
|
479
479
|
result["reference_prefix"] = str(prop_value)
|
|
480
480
|
logger.debug(f"🔧 Set reference_prefix: {str(prop_value)}")
|
|
481
|
-
elif prop_name == sexpdata.Symbol(
|
|
481
|
+
elif prop_name == sexpdata.Symbol("Description"):
|
|
482
482
|
result["Description"] = str(prop_value) # Keep original case
|
|
483
483
|
logger.debug(f"🔧 Set Description: {str(prop_value)}")
|
|
484
|
-
elif prop_name == sexpdata.Symbol(
|
|
484
|
+
elif prop_name == sexpdata.Symbol("ki_keywords"):
|
|
485
485
|
result["keywords"] = str(prop_value)
|
|
486
|
-
elif prop_name == sexpdata.Symbol(
|
|
486
|
+
elif prop_name == sexpdata.Symbol("Datasheet"):
|
|
487
487
|
result["Datasheet"] = str(prop_value) # Keep original case
|
|
488
488
|
logger.debug(f"🔧 Set Datasheet: {str(prop_value)}")
|
|
489
489
|
|
|
@@ -500,7 +500,7 @@ class SymbolLibraryCache:
|
|
|
500
500
|
def _find_symbol_in_parsed_data(self, parsed_data: List, symbol_name: str) -> Optional[List]:
|
|
501
501
|
"""Find a specific symbol in parsed KiCAD library data."""
|
|
502
502
|
logger.debug(f"🔧 FIND: Looking for symbol '{symbol_name}' in parsed data")
|
|
503
|
-
|
|
503
|
+
|
|
504
504
|
if not isinstance(parsed_data, list):
|
|
505
505
|
logger.debug(f"🔧 FIND: Parsed data is not a list: {type(parsed_data)}")
|
|
506
506
|
return None
|
|
@@ -509,61 +509,66 @@ class SymbolLibraryCache:
|
|
|
509
509
|
available_symbols = []
|
|
510
510
|
for item in parsed_data:
|
|
511
511
|
if isinstance(item, list) and len(item) >= 2:
|
|
512
|
-
if item[0] == sexpdata.Symbol(
|
|
512
|
+
if item[0] == sexpdata.Symbol("symbol"):
|
|
513
513
|
available_symbols.append(str(item[1]).strip('"'))
|
|
514
|
-
|
|
515
|
-
logger.debug(
|
|
514
|
+
|
|
515
|
+
logger.debug(
|
|
516
|
+
f"🔧 FIND: Available symbols in library: {available_symbols[:10]}..."
|
|
517
|
+
) # Show first 10
|
|
516
518
|
|
|
517
519
|
# Search through the parsed data for the symbol
|
|
518
520
|
for item in parsed_data:
|
|
519
521
|
if isinstance(item, list) and len(item) >= 2:
|
|
520
|
-
if (
|
|
521
|
-
|
|
522
|
-
|
|
522
|
+
if (
|
|
523
|
+
item[0] == sexpdata.Symbol("symbol")
|
|
524
|
+
and len(item) > 1
|
|
525
|
+
and str(item[1]).strip('"') == symbol_name
|
|
526
|
+
):
|
|
523
527
|
logger.debug(f"🔧 FIND: Found symbol '{symbol_name}'")
|
|
524
528
|
return item
|
|
525
|
-
|
|
526
|
-
logger.debug(f"🔧 FIND: Symbol '{symbol_name}' not found in library")
|
|
529
|
+
|
|
530
|
+
logger.debug(f"🔧 FIND: Symbol '{symbol_name}' not found in library")
|
|
527
531
|
return None
|
|
528
532
|
|
|
529
533
|
def _check_extends_directive(self, symbol_data: List) -> Optional[str]:
|
|
530
534
|
"""Check if symbol has extends directive and return parent symbol name."""
|
|
531
535
|
if not isinstance(symbol_data, list):
|
|
532
536
|
return None
|
|
533
|
-
|
|
537
|
+
|
|
534
538
|
for item in symbol_data[1:]:
|
|
535
539
|
if isinstance(item, list) and len(item) >= 2:
|
|
536
|
-
if item[0] == sexpdata.Symbol(
|
|
540
|
+
if item[0] == sexpdata.Symbol("extends"):
|
|
537
541
|
parent_name = str(item[1]).strip('"')
|
|
538
542
|
logger.debug(f"Found extends directive: {parent_name}")
|
|
539
543
|
return parent_name
|
|
540
544
|
return None
|
|
541
545
|
|
|
542
|
-
def _resolve_extends_relationship(
|
|
543
|
-
|
|
546
|
+
def _resolve_extends_relationship(
|
|
547
|
+
self, child_symbol_data: List, parent_name: str, library_path: Path, library_name: str
|
|
548
|
+
) -> Optional[List]:
|
|
544
549
|
"""Resolve extends relationship by merging parent symbol into child."""
|
|
545
550
|
logger.debug(f"🔧 RESOLVE: Resolving extends {parent_name} for child symbol")
|
|
546
|
-
|
|
551
|
+
|
|
547
552
|
try:
|
|
548
553
|
# Load the parent symbol from the same library
|
|
549
|
-
with open(library_path,
|
|
554
|
+
with open(library_path, "r", encoding="utf-8") as f:
|
|
550
555
|
content = f.read()
|
|
551
|
-
|
|
556
|
+
|
|
552
557
|
parsed = sexpdata.loads(content, true=None, false=None, nil=None)
|
|
553
558
|
parent_symbol_data = self._find_symbol_in_parsed_data(parsed, parent_name)
|
|
554
|
-
|
|
559
|
+
|
|
555
560
|
if not parent_symbol_data:
|
|
556
561
|
logger.warning(f"🔧 RESOLVE: Parent symbol {parent_name} not found in library")
|
|
557
562
|
return None
|
|
558
|
-
|
|
563
|
+
|
|
559
564
|
logger.debug(f"🔧 RESOLVE: Found parent symbol {parent_name}")
|
|
560
|
-
|
|
565
|
+
|
|
561
566
|
# Merge parent into child (adapt from circuit-synth logic)
|
|
562
567
|
merged_symbol = self._merge_parent_into_child(child_symbol_data, parent_symbol_data)
|
|
563
568
|
logger.debug(f"🔧 RESOLVE: Merged parent into child symbol")
|
|
564
|
-
|
|
569
|
+
|
|
565
570
|
return merged_symbol
|
|
566
|
-
|
|
571
|
+
|
|
567
572
|
except Exception as e:
|
|
568
573
|
logger.error(f"🔧 RESOLVE: Error resolving extends: {e}")
|
|
569
574
|
return None
|
|
@@ -571,27 +576,30 @@ class SymbolLibraryCache:
|
|
|
571
576
|
def _merge_parent_into_child(self, child_data: List, parent_data: List) -> List:
|
|
572
577
|
"""Merge parent symbol graphics and pins into child symbol."""
|
|
573
578
|
import copy
|
|
574
|
-
|
|
579
|
+
|
|
575
580
|
# Get child and parent symbol names for unit renaming
|
|
576
581
|
child_name = str(child_data[1]).strip('"') if len(child_data) > 1 else "Child"
|
|
577
582
|
parent_name = str(parent_data[1]).strip('"') if len(parent_data) > 1 else "Parent"
|
|
578
|
-
|
|
583
|
+
|
|
579
584
|
logger.debug(f"🔧 MERGE: Merging {parent_name} into {child_name}")
|
|
580
|
-
|
|
585
|
+
|
|
581
586
|
# Start with child symbol structure
|
|
582
587
|
merged = copy.deepcopy(child_data)
|
|
583
|
-
|
|
588
|
+
|
|
584
589
|
# Remove the extends directive from child
|
|
585
|
-
merged = [
|
|
586
|
-
|
|
587
|
-
item
|
|
588
|
-
|
|
589
|
-
|
|
590
|
+
merged = [
|
|
591
|
+
item
|
|
592
|
+
for item in merged
|
|
593
|
+
if not (
|
|
594
|
+
isinstance(item, list) and len(item) >= 2 and item[0] == sexpdata.Symbol("extends")
|
|
595
|
+
)
|
|
596
|
+
]
|
|
597
|
+
|
|
590
598
|
# Copy all graphics and unit definitions from parent
|
|
591
599
|
for item in parent_data[1:]:
|
|
592
600
|
if isinstance(item, list) and len(item) > 0:
|
|
593
601
|
# Copy symbol unit definitions (contain graphics and pins)
|
|
594
|
-
if item[0] == sexpdata.Symbol(
|
|
602
|
+
if item[0] == sexpdata.Symbol("symbol"):
|
|
595
603
|
# Rename unit from parent name to child name
|
|
596
604
|
unit_item = copy.deepcopy(item)
|
|
597
605
|
if len(unit_item) > 1:
|
|
@@ -602,36 +610,36 @@ class SymbolLibraryCache:
|
|
|
602
610
|
logger.debug(f"🔧 MERGE: Renamed unit {old_unit_name} -> {new_unit_name}")
|
|
603
611
|
merged.append(unit_item)
|
|
604
612
|
# Copy other non-property elements (child properties override parent)
|
|
605
|
-
elif item[0] not in [sexpdata.Symbol(
|
|
613
|
+
elif item[0] not in [sexpdata.Symbol("property")]:
|
|
606
614
|
merged.append(copy.deepcopy(item))
|
|
607
|
-
|
|
615
|
+
|
|
608
616
|
logger.debug(f"🔧 MERGE: Merged symbol has {len(merged)} elements")
|
|
609
617
|
return merged
|
|
610
618
|
|
|
611
619
|
def _extract_pins_from_symbol(self, symbol_data: List) -> List[SchematicPin]:
|
|
612
620
|
"""Extract pins from symbol data."""
|
|
613
621
|
pins = []
|
|
614
|
-
|
|
622
|
+
|
|
615
623
|
# Look for symbol sub-definitions like "R_1_1" that contain pins
|
|
616
624
|
for item in symbol_data[1:]:
|
|
617
625
|
if isinstance(item, list) and len(item) > 0:
|
|
618
|
-
if item[0] == sexpdata.Symbol(
|
|
626
|
+
if item[0] == sexpdata.Symbol("symbol"):
|
|
619
627
|
# This is a symbol unit definition, look for pins
|
|
620
628
|
pins.extend(self._extract_pins_from_unit(item))
|
|
621
|
-
|
|
629
|
+
|
|
622
630
|
return pins
|
|
623
631
|
|
|
624
632
|
def _extract_pins_from_unit(self, unit_data: List) -> List[SchematicPin]:
|
|
625
633
|
"""Extract pins from a symbol unit definition."""
|
|
626
634
|
pins = []
|
|
627
|
-
|
|
635
|
+
|
|
628
636
|
for item in unit_data[1:]:
|
|
629
637
|
if isinstance(item, list) and len(item) > 0:
|
|
630
|
-
if item[0] == sexpdata.Symbol(
|
|
638
|
+
if item[0] == sexpdata.Symbol("pin"):
|
|
631
639
|
pin = self._parse_pin_definition(item)
|
|
632
640
|
if pin:
|
|
633
641
|
pins.append(pin)
|
|
634
|
-
|
|
642
|
+
|
|
635
643
|
return pins
|
|
636
644
|
|
|
637
645
|
def _parse_pin_definition(self, pin_data: List) -> Optional[SchematicPin]:
|
|
@@ -640,27 +648,27 @@ class SymbolLibraryCache:
|
|
|
640
648
|
# pin_data format: (pin passive line (at 0 3.81 270) (length 1.27) ...)
|
|
641
649
|
pin_type_str = str(pin_data[1]) if len(pin_data) > 1 else "passive"
|
|
642
650
|
pin_shape_str = str(pin_data[2]) if len(pin_data) > 2 else "line"
|
|
643
|
-
|
|
651
|
+
|
|
644
652
|
position = Point(0, 0)
|
|
645
653
|
length = 2.54
|
|
646
654
|
rotation = 0
|
|
647
655
|
name = "~"
|
|
648
656
|
number = "1"
|
|
649
|
-
|
|
657
|
+
|
|
650
658
|
# Parse pin attributes
|
|
651
659
|
for item in pin_data[3:]:
|
|
652
660
|
if isinstance(item, list) and len(item) > 0:
|
|
653
|
-
if item[0] == sexpdata.Symbol(
|
|
661
|
+
if item[0] == sexpdata.Symbol("at"):
|
|
654
662
|
# (at x y rotation)
|
|
655
663
|
if len(item) >= 3:
|
|
656
664
|
position = Point(float(item[1]), float(item[2]))
|
|
657
665
|
if len(item) >= 4:
|
|
658
666
|
rotation = float(item[3])
|
|
659
|
-
elif item[0] == sexpdata.Symbol(
|
|
667
|
+
elif item[0] == sexpdata.Symbol("length"):
|
|
660
668
|
length = float(item[1])
|
|
661
|
-
elif item[0] == sexpdata.Symbol(
|
|
669
|
+
elif item[0] == sexpdata.Symbol("name"):
|
|
662
670
|
name = str(item[1]).strip('"')
|
|
663
|
-
elif item[0] == sexpdata.Symbol(
|
|
671
|
+
elif item[0] == sexpdata.Symbol("number"):
|
|
664
672
|
number = str(item[1]).strip('"')
|
|
665
673
|
|
|
666
674
|
# Map pin type
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kicad-sch-api
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Professional KiCAD schematic manipulation library with exact format preservation
|
|
5
5
|
Author-email: Circuit-Synth <shane@circuit-synth.com>
|
|
6
6
|
Maintainer-email: Circuit-Synth <shane@circuit-synth.com>
|
|
@@ -56,6 +56,9 @@ Create and manipulate KiCAD schematic files programmatically with guaranteed exa
|
|
|
56
56
|
- **🏗️ Professional Component Management**: Object-oriented collections with search and validation
|
|
57
57
|
- **⚡ High Performance**: Optimized for large schematics with intelligent caching
|
|
58
58
|
- **🔍 Real KiCAD Library Integration**: Access to actual KiCAD symbol libraries and validation
|
|
59
|
+
- **📐 Component Bounding Boxes**: Precise component boundary calculation and visualization
|
|
60
|
+
- **🎨 Colored Rectangle Graphics**: KiCAD-compatible rectangles with all stroke types and colors
|
|
61
|
+
- **🛣️ Manhattan Routing**: Intelligent wire routing with obstacle avoidance
|
|
59
62
|
- **🤖 AI Agent Ready**: MCP server for seamless integration with AI development tools
|
|
60
63
|
- **📚 Hierarchical Design**: Complete support for multi-sheet schematic projects
|
|
61
64
|
|
|
@@ -100,6 +103,16 @@ capacitor = sch.components.add(
|
|
|
100
103
|
footprint="Capacitor_SMD:C_0603_1608Metric"
|
|
101
104
|
)
|
|
102
105
|
|
|
106
|
+
# Add wires for connectivity
|
|
107
|
+
sch.wires.add(start=(100, 110), end=(150, 110))
|
|
108
|
+
|
|
109
|
+
# Pin-to-pin wiring (NEW in v0.3.1)
|
|
110
|
+
wire_uuid = sch.add_wire_between_pins("R1", "2", "C1", "1") # Connect R1 pin 2 to C1 pin 1
|
|
111
|
+
external_wire = sch.add_wire_to_pin((50, 100), "R1", "1") # Connect external point to R1 pin 1
|
|
112
|
+
|
|
113
|
+
# Add labels for nets
|
|
114
|
+
sch.add_label("VCC", position=(125, 110))
|
|
115
|
+
|
|
103
116
|
# Save with exact format preservation
|
|
104
117
|
sch.save("my_circuit.kicad_sch")
|
|
105
118
|
```
|
|
@@ -134,6 +147,131 @@ power_sch.save("power.kicad_sch")
|
|
|
134
147
|
|
|
135
148
|
## 🔧 Advanced Features
|
|
136
149
|
|
|
150
|
+
### Component Bounding Boxes and Colored Graphics (NEW in v0.3.1)
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
from kicad_sch_api.core.component_bounds import get_component_bounding_box
|
|
154
|
+
|
|
155
|
+
# Add components
|
|
156
|
+
resistor = sch.components.add("Device:R", "R1", "10k", (100, 100))
|
|
157
|
+
opamp = sch.components.add("Amplifier_Operational:LM358", "U1", "LM358", (150, 100))
|
|
158
|
+
|
|
159
|
+
# Get component bounding boxes
|
|
160
|
+
bbox_body = get_component_bounding_box(resistor, include_properties=False)
|
|
161
|
+
bbox_full = get_component_bounding_box(resistor, include_properties=True)
|
|
162
|
+
|
|
163
|
+
# Draw colored bounding box rectangles
|
|
164
|
+
sch.draw_bounding_box(bbox_body, stroke_width=0.5, stroke_color="blue", stroke_type="solid")
|
|
165
|
+
sch.draw_bounding_box(bbox_full, stroke_width=0.3, stroke_color="red", stroke_type="dash")
|
|
166
|
+
|
|
167
|
+
# Draw bounding boxes for all components at once
|
|
168
|
+
bbox_uuids = sch.draw_component_bounding_boxes(
|
|
169
|
+
include_properties=True,
|
|
170
|
+
stroke_width=0.4,
|
|
171
|
+
stroke_color="green",
|
|
172
|
+
stroke_type="dot"
|
|
173
|
+
)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Manhattan Routing with Obstacle Avoidance (NEW in v0.3.1)
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
from kicad_sch_api.core.manhattan_routing import ManhattanRouter
|
|
180
|
+
from kicad_sch_api.core.types import Point
|
|
181
|
+
|
|
182
|
+
# Create router
|
|
183
|
+
router = ManhattanRouter()
|
|
184
|
+
|
|
185
|
+
# Add components that act as obstacles
|
|
186
|
+
r1 = sch.components.add("Device:R", "R1", "1k", (50, 50))
|
|
187
|
+
r2 = sch.components.add("Device:R", "R2", "2k", (150, 150))
|
|
188
|
+
obstacle = sch.components.add("Device:C", "C1", "100nF", (100, 100))
|
|
189
|
+
|
|
190
|
+
# Get obstacle bounding boxes
|
|
191
|
+
obstacle_bbox = get_component_bounding_box(obstacle, include_properties=False)
|
|
192
|
+
|
|
193
|
+
# Route around obstacles
|
|
194
|
+
start_point = Point(r1.position.x, r1.position.y)
|
|
195
|
+
end_point = Point(r2.position.x, r2.position.y)
|
|
196
|
+
path = router.route_between_points(start_point, end_point, [obstacle_bbox], clearance=2.0)
|
|
197
|
+
|
|
198
|
+
# Add wires along the path
|
|
199
|
+
for i in range(len(path) - 1):
|
|
200
|
+
sch.wires.add(path[i], path[i + 1])
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Pin-to-Pin Wiring
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
# Connect component pins directly - automatically calculates pin positions
|
|
207
|
+
wire_uuid = sch.add_wire_between_pins("R1", "2", "R2", "1") # R1 pin 2 to R2 pin 1
|
|
208
|
+
|
|
209
|
+
# Connect arbitrary point to component pin
|
|
210
|
+
external_wire = sch.add_wire_to_pin((75, 125), "R1", "1") # External point to R1 pin 1
|
|
211
|
+
tuple_wire = sch.add_wire_to_pin(Point(100, 150), "C1", "2") # Using Point object
|
|
212
|
+
|
|
213
|
+
# Get component pin positions for advanced operations
|
|
214
|
+
pin_position = sch.get_component_pin_position("R1", "1")
|
|
215
|
+
if pin_position:
|
|
216
|
+
print(f"R1 pin 1 is at ({pin_position.x:.2f}, {pin_position.y:.2f})")
|
|
217
|
+
|
|
218
|
+
# Error handling - returns None for invalid components/pins
|
|
219
|
+
invalid_wire = sch.add_wire_between_pins("R999", "1", "R1", "1") # Returns None
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Component Bounding Box Visualization (NEW in v0.3.1)
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from kicad_sch_api.core.component_bounds import get_component_bounding_box
|
|
226
|
+
|
|
227
|
+
# Get component bounding box (body only)
|
|
228
|
+
resistor = sch.components.get("R1")
|
|
229
|
+
bbox = get_component_bounding_box(resistor, include_properties=False)
|
|
230
|
+
print(f"R1 body size: {bbox.width:.2f}×{bbox.height:.2f}mm")
|
|
231
|
+
|
|
232
|
+
# Get bounding box including properties (reference, value, etc.)
|
|
233
|
+
bbox_with_props = get_component_bounding_box(resistor, include_properties=True)
|
|
234
|
+
print(f"R1 with labels: {bbox_with_props.width:.2f}×{bbox_with_props.height:.2f}mm")
|
|
235
|
+
|
|
236
|
+
# Draw bounding box as rectangle graphics (for visualization/debugging)
|
|
237
|
+
rect_uuid = sch.draw_bounding_box(bbox)
|
|
238
|
+
print(f"Drew bounding box rectangle: {rect_uuid}")
|
|
239
|
+
|
|
240
|
+
# Draw bounding boxes for all components
|
|
241
|
+
bbox_uuids = sch.draw_component_bounding_boxes(
|
|
242
|
+
include_properties=False # True to include reference/value labels
|
|
243
|
+
)
|
|
244
|
+
print(f"Drew {len(bbox_uuids)} component bounding boxes")
|
|
245
|
+
|
|
246
|
+
# Expand bounding box for clearance analysis
|
|
247
|
+
expanded_bbox = bbox.expand(2.54) # Expand by 2.54mm (0.1 inch)
|
|
248
|
+
clearance_rect = sch.draw_bounding_box(expanded_bbox)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Manhattan Routing with Obstacle Avoidance (NEW in v0.3.1)
|
|
252
|
+
|
|
253
|
+
```python
|
|
254
|
+
# Automatic Manhattan routing between component pins
|
|
255
|
+
wire_segments = sch.auto_route_pins(
|
|
256
|
+
"R1", "2", # From component R1, pin 2
|
|
257
|
+
"R2", "1", # To component R2, pin 1
|
|
258
|
+
routing_mode="manhattan", # Manhattan (L-shaped) routing
|
|
259
|
+
avoid_components=True # Avoid component bounding boxes
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Direct routing (straight line)
|
|
263
|
+
direct_wire = sch.auto_route_pins("C1", "1", "C2", "2", routing_mode="direct")
|
|
264
|
+
|
|
265
|
+
# Manual obstacle avoidance using bounding boxes
|
|
266
|
+
bbox_r1 = get_component_bounding_box(sch.components.get("R1"))
|
|
267
|
+
bbox_r2 = get_component_bounding_box(sch.components.get("R2"))
|
|
268
|
+
|
|
269
|
+
# Check if routing path intersects with component
|
|
270
|
+
def path_clear(start, end, obstacles):
|
|
271
|
+
# Custom collision detection logic
|
|
272
|
+
return not any(bbox.intersects_line(start, end) for bbox in obstacles)
|
|
273
|
+
```
|
|
274
|
+
|
|
137
275
|
### Component Search and Management
|
|
138
276
|
|
|
139
277
|
```python
|
|
@@ -154,6 +292,53 @@ validation_result = sch.components.validate_component(
|
|
|
154
292
|
)
|
|
155
293
|
```
|
|
156
294
|
|
|
295
|
+
### Component and Element Removal
|
|
296
|
+
|
|
297
|
+
```python
|
|
298
|
+
# Remove components by reference
|
|
299
|
+
removed = sch.components.remove("R1") # Returns True if removed
|
|
300
|
+
|
|
301
|
+
# Remove wires, labels, and other elements
|
|
302
|
+
sch.remove_wire(wire_uuid)
|
|
303
|
+
sch.remove_label(label_uuid)
|
|
304
|
+
sch.remove_hierarchical_label(label_uuid)
|
|
305
|
+
|
|
306
|
+
# Remove from collections
|
|
307
|
+
sch.wires.remove(wire_uuid)
|
|
308
|
+
sch.junctions.remove(junction_uuid)
|
|
309
|
+
|
|
310
|
+
# lib_symbols are automatically cleaned up when last component of type is removed
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### Configuration and Customization
|
|
314
|
+
|
|
315
|
+
```python
|
|
316
|
+
import kicad_sch_api as ksa
|
|
317
|
+
|
|
318
|
+
# Access global configuration
|
|
319
|
+
config = ksa.config
|
|
320
|
+
|
|
321
|
+
# Customize property positioning
|
|
322
|
+
config.properties.reference_y = -2.0 # Move reference labels higher
|
|
323
|
+
config.properties.value_y = 2.0 # Move value labels lower
|
|
324
|
+
|
|
325
|
+
# Customize tolerances and precision
|
|
326
|
+
config.tolerance.position_tolerance = 0.05 # Tighter position matching
|
|
327
|
+
config.tolerance.wire_segment_min = 0.005 # Different wire segment threshold
|
|
328
|
+
|
|
329
|
+
# Customize defaults
|
|
330
|
+
config.defaults.project_name = "my_company_project"
|
|
331
|
+
config.defaults.stroke_width = 0.1
|
|
332
|
+
|
|
333
|
+
# Grid and spacing customization
|
|
334
|
+
config.grid.unit_spacing = 10.0 # Tighter multi-unit IC spacing
|
|
335
|
+
config.grid.component_spacing = 5.0 # Closer component placement
|
|
336
|
+
|
|
337
|
+
# Sheet settings for hierarchical designs
|
|
338
|
+
config.sheet.name_offset_y = -1.0 # Different sheet label position
|
|
339
|
+
config.sheet.file_offset_y = 1.0 # Different file label position
|
|
340
|
+
```
|
|
341
|
+
|
|
157
342
|
### KiCAD Integration
|
|
158
343
|
|
|
159
344
|
```python
|
|
@@ -170,48 +355,23 @@ net_info = netlist.analyze_net("VCC")
|
|
|
170
355
|
|
|
171
356
|
## 🤖 AI Agent Integration
|
|
172
357
|
|
|
173
|
-
This library serves as the
|
|
174
|
-
|
|
175
|
-
### MCP Server Integration
|
|
176
|
-
|
|
177
|
-
```bash
|
|
178
|
-
# Install the dedicated MCP server (separate package)
|
|
179
|
-
pip install mcp-kicad-sch-api
|
|
180
|
-
|
|
181
|
-
# Configure for Claude Code
|
|
182
|
-
code mcp install mcp-kicad-sch-api
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
### Library Design for MCP Compatibility
|
|
186
|
-
|
|
187
|
-
This library is specifically designed to provide:
|
|
188
|
-
- **Stable API**: Consistent interface for MCP servers to build upon
|
|
189
|
-
- **Format Preservation**: Guaranteed exact KiCAD output for reliable automation
|
|
190
|
-
- **Professional Validation**: Component and library validation for quality assurance
|
|
191
|
-
- **Performance**: Optimized for AI agent workloads with caching and bulk operations
|
|
192
|
-
|
|
193
|
-
### Usage with AI Agents
|
|
194
|
-
|
|
195
|
-
```
|
|
196
|
-
# Natural language commands to your AI agent:
|
|
197
|
-
"Create a voltage divider with two 10kΩ resistors"
|
|
198
|
-
"Add an ESP32 microcontroller with USB connector"
|
|
199
|
-
"Generate a hierarchical schematic with power supply subcircuit"
|
|
200
|
-
```
|
|
201
|
-
|
|
202
|
-
**Related MCP Servers:**
|
|
203
|
-
- **[mcp-kicad-sch-api](https://github.com/circuit-synth/mcp-kicad-sch-api)**: Full-featured MCP server built on this library
|
|
358
|
+
This library serves as the foundation for AI agent integration. For Claude Code or other AI agents, use the **[mcp-kicad-sch-api](https://github.com/circuit-synth/mcp-kicad-sch-api)** MCP server (included as a submodule in `submodules/mcp-kicad-sch-api/`).
|
|
204
359
|
|
|
205
360
|
## 🏗️ Architecture
|
|
206
361
|
|
|
207
362
|
### Library Structure
|
|
208
363
|
|
|
209
364
|
```
|
|
210
|
-
|
|
211
|
-
├──
|
|
212
|
-
├──
|
|
213
|
-
├──
|
|
214
|
-
|
|
365
|
+
kicad-sch-api/
|
|
366
|
+
├── kicad_sch_api/ # Core Python library
|
|
367
|
+
│ ├── core/ # Core schematic manipulation
|
|
368
|
+
│ ├── library/ # KiCAD library integration
|
|
369
|
+
│ ├── discovery/ # Component search and indexing
|
|
370
|
+
│ └── utils/ # Validation and utilities
|
|
371
|
+
├── submodules/ # Related projects as submodules
|
|
372
|
+
│ └── mcp-kicad-sch-api/ # MCP server for AI agents
|
|
373
|
+
├── tests/ # Comprehensive test suite
|
|
374
|
+
└── examples/ # Usage examples and tutorials
|
|
215
375
|
```
|
|
216
376
|
|
|
217
377
|
### Design Principles
|
|
@@ -225,18 +385,30 @@ kicad_sch_api/
|
|
|
225
385
|
## 🧪 Testing & Quality
|
|
226
386
|
|
|
227
387
|
```bash
|
|
228
|
-
# Run all tests
|
|
388
|
+
# Run all tests (29 tests covering all functionality)
|
|
229
389
|
uv run pytest tests/ -v
|
|
230
390
|
|
|
231
|
-
# Format preservation tests (critical)
|
|
391
|
+
# Format preservation tests (critical - exact KiCAD output matching)
|
|
232
392
|
uv run pytest tests/reference_tests/ -v
|
|
233
393
|
|
|
394
|
+
# Component removal tests (comprehensive removal functionality)
|
|
395
|
+
uv run pytest tests/test_*_removal.py -v
|
|
396
|
+
|
|
234
397
|
# Code quality checks
|
|
235
398
|
uv run black kicad_sch_api/ tests/
|
|
236
399
|
uv run mypy kicad_sch_api/
|
|
237
400
|
uv run flake8 kicad_sch_api/ tests/
|
|
238
401
|
```
|
|
239
402
|
|
|
403
|
+
### Test Categories
|
|
404
|
+
|
|
405
|
+
- **Format Preservation**: Byte-for-byte compatibility with KiCAD native files
|
|
406
|
+
- **Component Management**: Creation, modification, and removal of components
|
|
407
|
+
- **Element Operations**: Wires, labels, junctions, hierarchical sheets
|
|
408
|
+
- **Configuration**: Customizable settings and behavior
|
|
409
|
+
- **Performance**: Large schematic handling and optimization
|
|
410
|
+
- **Integration**: Real KiCAD library compatibility
|
|
411
|
+
|
|
240
412
|
## 🆚 Why This Library?
|
|
241
413
|
|
|
242
414
|
### vs. Direct KiCAD File Editing
|
|
@@ -301,7 +473,7 @@ MIT License - see [LICENSE](LICENSE) for details.
|
|
|
301
473
|
|
|
302
474
|
## 🔗 Related Projects
|
|
303
475
|
|
|
304
|
-
- **[mcp-kicad-sch-api](https://github.com/circuit-synth/mcp-kicad-sch-api)**: MCP server for AI agents built on this library
|
|
476
|
+
- **[mcp-kicad-sch-api](https://github.com/circuit-synth/mcp-kicad-sch-api)**: MCP server for AI agents built on this library (included as submodule)
|
|
305
477
|
- **[circuit-synth](https://github.com/circuit-synth/circuit-synth)**: High-level circuit design automation using this library
|
|
306
478
|
- **[Claude Code](https://claude.ai/code)**: AI development environment with MCP support
|
|
307
479
|
- **[KiCAD](https://kicad.org/)**: Open source electronics design automation suite
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
kicad_sch_api/__init__.py,sha256=pxSnH_vkk5oJSRMp55RsitpoxRjum84Jw5JpQrGcSPc,2919
|
|
2
|
+
kicad_sch_api/cli.py,sha256=ZzmwzfHEvPgGfCiQBU4G2LBAyRtMNiBRoY21pivJSYc,7621
|
|
3
|
+
kicad_sch_api/py.typed,sha256=e4ldqxwpY7pNDG1olbvj4HSKr8sZ9vxgA_2ek8xXn-Q,70
|
|
4
|
+
kicad_sch_api/core/__init__.py,sha256=ur_KeYBlGKl-e1hLpLdxAhGV2A-PCCGkcqd0r6KSeBA,566
|
|
5
|
+
kicad_sch_api/core/component_bounds.py,sha256=BFYJYULyzs5it2hN7bHTimyS9Vet4dxsMklRStob-F4,17509
|
|
6
|
+
kicad_sch_api/core/components.py,sha256=tXRL18GObl2u94wl5jP-1ID56s_UD9F1gQ_iRIyZ_Kw,25290
|
|
7
|
+
kicad_sch_api/core/config.py,sha256=itw0j3DeIEHaFVf8p3mfAS1SP6jclBwvMv7NPdkThE4,4309
|
|
8
|
+
kicad_sch_api/core/formatter.py,sha256=ggOc1PZZAGkpdMMNxm1ukCCz9riYnd_sM93nB0fBYuw,20390
|
|
9
|
+
kicad_sch_api/core/geometry.py,sha256=27SgN0padLbQuTi8MV6UUCp6Pyaiv8V9gmYDOhfwny8,2947
|
|
10
|
+
kicad_sch_api/core/ic_manager.py,sha256=Kg0HIOMU-TGXiIkrnwcHFQ1Kfv_3rW2U1cwBKJsKopc,7219
|
|
11
|
+
kicad_sch_api/core/junctions.py,sha256=Ay6BsWX_DLs-wB0eMA2CytKKq0N8Ja41ZubJWpAqNgM,6122
|
|
12
|
+
kicad_sch_api/core/manhattan_routing.py,sha256=t_T2u0zsQB-a8dTijFmY-qFq-oDt2qDebYyXzD_pBWI,15989
|
|
13
|
+
kicad_sch_api/core/parser.py,sha256=raQdKrJZgw3v2ZKyECK7SbzqLMD762H-lQWwxUMDwks,52992
|
|
14
|
+
kicad_sch_api/core/pin_utils.py,sha256=XGEow3HzBTyT8a0B_ZC8foMvwzYaENSaqTUwDW1rz24,5417
|
|
15
|
+
kicad_sch_api/core/schematic.py,sha256=5BSPVWHgj9uwzlwCBT6JSg7sjx9jlADd1aEprO_yOd0,59498
|
|
16
|
+
kicad_sch_api/core/simple_manhattan.py,sha256=CvIHvwmfABPF-COzhblYxEgRoR_R_eD-lmBFHHjDuMI,7241
|
|
17
|
+
kicad_sch_api/core/types.py,sha256=exIJ0cA1puYj09uxiq6inom2YVAs6JTDjf1ka0bKN8g,12872
|
|
18
|
+
kicad_sch_api/core/wire_routing.py,sha256=G-C7S-ntQxwuu1z3OaaYlkURXwKE4r4xmhbbi6cvvaI,12830
|
|
19
|
+
kicad_sch_api/core/wires.py,sha256=608t9oH4UzppdGgNgUd-ABK6T-ahyETZwhO_-CuKFO8,8319
|
|
20
|
+
kicad_sch_api/discovery/__init__.py,sha256=qSuCsnC-hVtaLYE8fwd-Gea6JKwEVGPQ-hSNDNJYsIU,329
|
|
21
|
+
kicad_sch_api/discovery/search_index.py,sha256=KgQT8ipT9OU6ktUwhDZ37Mao0Cba0fJOsxUk9m8ZKbY,15856
|
|
22
|
+
kicad_sch_api/library/__init__.py,sha256=NG9UTdcpn25Bl9tPsYs9ED7bvpaVPVdtLMbnxkQkOnU,250
|
|
23
|
+
kicad_sch_api/library/cache.py,sha256=7na88grl465WHwUOGuOzYrrWwjsMBXhXVtxhnaJ9GBY,33208
|
|
24
|
+
kicad_sch_api/utils/__init__.py,sha256=1V_yGgI7jro6MUc4Pviux_WIeJ1wmiYFID186SZwWLQ,277
|
|
25
|
+
kicad_sch_api/utils/validation.py,sha256=XlWGRZJb3cOPYpU9sLQQgC_NASwbi6W-LCN7PzUmaPY,15626
|
|
26
|
+
kicad_sch_api-0.2.1.dist-info/licenses/LICENSE,sha256=Em65Nvte1G9MHc0rHqtYuGkCPcshD588itTa358J6gs,1070
|
|
27
|
+
kicad_sch_api-0.2.1.dist-info/METADATA,sha256=UJaikwtrjEvTIYzOZpeYA1-nP6LlQpBQf_Fc9wzHO2M,17183
|
|
28
|
+
kicad_sch_api-0.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
29
|
+
kicad_sch_api-0.2.1.dist-info/entry_points.txt,sha256=VWKsFi2Jv7G_tmio3cNVhhIBfv_OZFaKa-T_ED84lc8,57
|
|
30
|
+
kicad_sch_api-0.2.1.dist-info/top_level.txt,sha256=n0ex4gOJ1b_fARowcGqRzyOGZcHRhc5LZa6_vVgGxcI,14
|
|
31
|
+
kicad_sch_api-0.2.1.dist-info/RECORD,,
|