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.
@@ -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, 'r', encoding='utf-8') as f:
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('property'):
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('Reference'):
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('Description'):
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('ki_keywords'):
484
+ elif prop_name == sexpdata.Symbol("ki_keywords"):
485
485
  result["keywords"] = str(prop_value)
486
- elif prop_name == sexpdata.Symbol('Datasheet'):
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('symbol'):
512
+ if item[0] == sexpdata.Symbol("symbol"):
513
513
  available_symbols.append(str(item[1]).strip('"'))
514
-
515
- logger.debug(f"🔧 FIND: Available symbols in library: {available_symbols[:10]}...") # Show first 10
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 (item[0] == sexpdata.Symbol('symbol') and
521
- len(item) > 1 and
522
- str(item[1]).strip('"') == symbol_name):
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('extends'):
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(self, child_symbol_data: List, parent_name: str,
543
- library_path: Path, library_name: str) -> Optional[List]:
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, 'r', encoding='utf-8') as f:
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 = [item for item in merged if not (
586
- isinstance(item, list) and len(item) >= 2 and
587
- item[0] == sexpdata.Symbol('extends')
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('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('property')]:
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('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('pin'):
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('at'):
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('length'):
667
+ elif item[0] == sexpdata.Symbol("length"):
660
668
  length = float(item[1])
661
- elif item[0] == sexpdata.Symbol('name'):
669
+ elif item[0] == sexpdata.Symbol("name"):
662
670
  name = str(item[1]).strip('"')
663
- elif item[0] == sexpdata.Symbol('number'):
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.0
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 foundational layer for AI agent integration through dedicated MCP (Model Context Protocol) servers.
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
- kicad_sch_api/
211
- ├── core/ # Core schematic manipulation
212
- ├── library/ # KiCAD library integration
213
- ├── integration/ # KiCAD CLI and tool integration
214
- └── utils/ # Validation and utilities
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,,