kicad-sch-api 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of kicad-sch-api might be problematic. Click here for more details.

Files changed (93) hide show
  1. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/CHANGELOG.md +31 -0
  2. {kicad_sch_api-0.2.1/kicad_sch_api.egg-info → kicad_sch_api-0.2.2}/PKG-INFO +1 -1
  3. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/core/formatter.py +7 -1
  4. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/core/parser.py +102 -11
  5. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/core/schematic.py +53 -0
  6. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/core/types.py +35 -0
  7. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2/kicad_sch_api.egg-info}/PKG-INFO +1 -1
  8. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api.egg-info/SOURCES.txt +3 -0
  9. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/pyproject.toml +1 -1
  10. kicad_sch_api-0.2.2/tests/test_parse_reference_rectangles.py +48 -0
  11. kicad_sch_api-0.2.2/tests/test_rectangle.py +88 -0
  12. kicad_sch_api-0.2.2/tests/test_rectangle_roundtrip.py +111 -0
  13. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/.claude/commands/dev/dead-code-analysis.md +0 -0
  14. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/.claude/commands/dev/publish-pypi.md +0 -0
  15. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/.claude/commands/dev/review-implementation.md +0 -0
  16. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/.claude/commands/dev/run-tests.md +0 -0
  17. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/.claude/commands/dev/update-and-commit.md +0 -0
  18. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/.claude/commands/dev/update-memory-bank.md +0 -0
  19. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/.claude/commands/test/run-reference-tests.md +0 -0
  20. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/LICENSE +0 -0
  21. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/MANIFEST.in +0 -0
  22. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/README.md +0 -0
  23. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/examples/advanced_usage.py +0 -0
  24. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/examples/basic_usage.py +0 -0
  25. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/examples/mcp_basic_example.py +0 -0
  26. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/examples/mcp_integration.py +0 -0
  27. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/examples/pin_to_pin_wiring_demo.py +0 -0
  28. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/examples/simple_circuit_with_pin_wiring.py +0 -0
  29. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/examples/simple_two_resistor_routing.py +0 -0
  30. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/__init__.py +0 -0
  31. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/cli.py +0 -0
  32. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/core/__init__.py +0 -0
  33. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/core/component_bounds.py +0 -0
  34. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/core/components.py +0 -0
  35. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/core/config.py +0 -0
  36. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/core/geometry.py +0 -0
  37. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/core/ic_manager.py +0 -0
  38. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/core/junctions.py +0 -0
  39. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/core/manhattan_routing.py +0 -0
  40. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/core/pin_utils.py +0 -0
  41. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/core/simple_manhattan.py +0 -0
  42. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/core/wire_routing.py +0 -0
  43. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/core/wires.py +0 -0
  44. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/discovery/__init__.py +0 -0
  45. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/discovery/search_index.py +0 -0
  46. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/library/__init__.py +0 -0
  47. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/library/cache.py +0 -0
  48. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/py.typed +0 -0
  49. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/utils/__init__.py +0 -0
  50. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api/utils/validation.py +0 -0
  51. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api.egg-info/dependency_links.txt +0 -0
  52. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api.egg-info/entry_points.txt +0 -0
  53. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api.egg-info/requires.txt +0 -0
  54. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/kicad_sch_api.egg-info/top_level.txt +0 -0
  55. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/setup.cfg +0 -0
  56. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/README.md +0 -0
  57. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/blank_schematic/blank_schematic.kicad_pro +0 -0
  58. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/blank_schematic/blank_schematic.kicad_sch +0 -0
  59. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/multi_unit_7400/multi_unit_7400.kicad_pro +0 -0
  60. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/multi_unit_7400/multi_unit_7400.kicad_sch +0 -0
  61. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/power_symbols/power_symbols.kicad_pro +0 -0
  62. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/power_symbols/power_symbols.kicad_sch +0 -0
  63. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/resistor_divider/resistor_divider.kicad_pro +0 -0
  64. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/resistor_divider/resistor_divider.kicad_sch +0 -0
  65. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/sch_title/sch_title.kicad_pro +0 -0
  66. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/sch_title/sch_title.kicad_sch +0 -0
  67. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/single_extended_component/single_extended_component.kicad_pro +0 -0
  68. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/single_extended_component/single_extended_component.kicad_sch +0 -0
  69. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/single_hierarchical_sheet/single_hierarchical_sheet.kicad_pro +0 -0
  70. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/single_hierarchical_sheet/single_hierarchical_sheet.kicad_sch +0 -0
  71. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/single_hierarchical_sheet/subcircuit1.kicad_sch +0 -0
  72. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/single_label/single_label.kicad_pro +0 -0
  73. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/single_label/single_label.kicad_sch +0 -0
  74. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/single_label_hierarchical/single_label_hierarchical.kicad_pro +0 -0
  75. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/single_label_hierarchical/single_label_hierarchical.kicad_sch +0 -0
  76. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/single_resistor/single_resistor.kicad_pro +0 -0
  77. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/single_resistor/single_resistor.kicad_sch +0 -0
  78. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/single_text/single_text.kicad_pro +0 -0
  79. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/single_text/single_text.kicad_sch +0 -0
  80. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/single_text_box/single_text_box.kicad_pro +0 -0
  81. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/single_text_box/single_text_box.kicad_sch +0 -0
  82. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/single_wire/single_wire.kicad_pro +0 -0
  83. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/single_wire/single_wire.kicad_sch +0 -0
  84. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/two_resistors/two_resistors.kicad_pro +0 -0
  85. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/reference_tests/reference_kicad_projects/two_resistors/two_resistors.kicad_sch +0 -0
  86. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/test_bounding_box_rectangles.py +0 -0
  87. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/test_component_removal.py +0 -0
  88. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/test_element_removal.py +0 -0
  89. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/test_grid_snapping.py +0 -0
  90. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/test_manhattan_routing.py +0 -0
  91. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/test_pin_positioning.py +0 -0
  92. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/test_pin_to_pin_wiring.py +0 -0
  93. {kicad_sch_api-0.2.1 → kicad_sch_api-0.2.2}/tests/test_removal_against_references.py +0 -0
@@ -5,6 +5,37 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.2.1] - 2025-01-20
9
+
10
+ ### Added
11
+ - **Professional PyPI Release**: First official release to Python Package Index
12
+ - **Enhanced Bounding Box Visualization**:
13
+ - Colored rectangle support with all KiCAD stroke types
14
+ - Support for solid, dash, dot, dash_dot, dash_dot_dot line styles
15
+ - Component bounding box visualization with color customization
16
+ - **Improved Manhattan Routing**:
17
+ - Enhanced obstacle avoidance algorithms
18
+ - Perfect KiCAD grid alignment (1.27mm grid)
19
+ - Multiple routing strategies and clearance options
20
+ - **Code Quality Improvements**:
21
+ - Formatted with black for consistent style
22
+ - Import sorting with isort
23
+ - Enhanced type checking coverage
24
+ - **Comprehensive Testing**:
25
+ - 71 passing tests with 6 intentionally skipped
26
+ - Enhanced test coverage for new features
27
+ - Format preservation validation
28
+
29
+ ### Enhanced
30
+ - **Exact Format Preservation**: Improved KiCAD format compatibility
31
+ - **Parser & Formatter**: Enhanced S-expression handling
32
+ - **Performance**: Optimized for professional use cases
33
+
34
+ ### Technical
35
+ - **Dependencies**: Updated build system and packaging
36
+ - **Documentation**: Enhanced API documentation
37
+ - **CI/CD**: Professional package validation and testing
38
+
8
39
  ## [0.3.1] - 2025-01-20
9
40
 
10
41
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kicad-sch-api
3
- Version: 0.2.1
3
+ Version: 0.2.2
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>
@@ -55,7 +55,7 @@ class ExactFormatter:
55
55
  self.rules["generator"] = FormatRule(inline=True, quote_indices={1})
56
56
  self.rules["generator_version"] = FormatRule(inline=True, quote_indices={1})
57
57
  self.rules["uuid"] = FormatRule(inline=True, quote_indices={1})
58
- self.rules["paper"] = FormatRule(inline=True, quote_indices={1})
58
+ self.rules["paper"] = FormatRule(inline=True) # No quotes for paper size (A4, A3, etc.)
59
59
 
60
60
  # Title block
61
61
  self.rules["title_block"] = FormatRule(inline=False)
@@ -112,6 +112,12 @@ class ExactFormatter:
112
112
  self.rules["junction"] = FormatRule(inline=False)
113
113
  self.rules["diameter"] = FormatRule(inline=True)
114
114
 
115
+ # Graphical elements
116
+ self.rules["rectangle"] = FormatRule(inline=False)
117
+ self.rules["start"] = FormatRule(inline=True)
118
+ self.rules["end"] = FormatRule(inline=True)
119
+ self.rules["fill"] = FormatRule(inline=False)
120
+
115
121
  # Labels
116
122
  self.rules["label"] = FormatRule(inline=False, quote_indices={1})
117
123
  self.rules["global_label"] = FormatRule(inline=False, quote_indices={1})
@@ -187,6 +187,7 @@ class SExpressionParser:
187
187
  "wires": [],
188
188
  "junctions": [],
189
189
  "labels": [],
190
+ "rectangles": [],
190
191
  "nets": [],
191
192
  "lib_symbols": {},
192
193
  "sheet_instances": [],
@@ -232,6 +233,10 @@ class SExpressionParser:
232
233
  label = self._parse_label(item)
233
234
  if label:
234
235
  schematic_data["labels"].append(label)
236
+ elif element_type == "rectangle":
237
+ rectangle = self._parse_rectangle(item)
238
+ if rectangle:
239
+ schematic_data["rectangles"].append(rectangle)
235
240
  elif element_type == "lib_symbols":
236
241
  schematic_data["lib_symbols"] = self._parse_lib_symbols(item)
237
242
  elif element_type == "sheet_instances":
@@ -304,10 +309,14 @@ class SExpressionParser:
304
309
  for text_box in schematic_data.get("text_boxes", []):
305
310
  sexp_data.append(self._text_box_to_sexp(text_box))
306
311
 
307
- # Add graphics (rectangles, etc.)
312
+ # Add graphics (rectangles from schematic.draw_bounding_box)
308
313
  for graphic in schematic_data.get("graphics", []):
309
314
  sexp_data.append(self._graphic_to_sexp(graphic))
310
315
 
316
+ # Add rectangles (rectangles from add_rectangle API)
317
+ for rectangle in schematic_data.get("rectangles", []):
318
+ sexp_data.append(self._rectangle_to_sexp(rectangle))
319
+
311
320
  # Add sheet_instances (required by KiCAD)
312
321
  sheet_instances = schematic_data.get("sheet_instances", [])
313
322
  if sheet_instances:
@@ -425,6 +434,37 @@ class SExpressionParser:
425
434
  # Implementation for label parsing
426
435
  return {}
427
436
 
437
+ def _parse_rectangle(self, item: List[Any]) -> Optional[Dict[str, Any]]:
438
+ """Parse a rectangle graphical element."""
439
+ rectangle = {}
440
+
441
+ for elem in item[1:]:
442
+ if not isinstance(elem, list):
443
+ continue
444
+
445
+ elem_type = str(elem[0])
446
+
447
+ if elem_type == "start" and len(elem) >= 3:
448
+ rectangle["start"] = {"x": float(elem[1]), "y": float(elem[2])}
449
+ elif elem_type == "end" and len(elem) >= 3:
450
+ rectangle["end"] = {"x": float(elem[1]), "y": float(elem[2])}
451
+ elif elem_type == "stroke":
452
+ for stroke_elem in elem[1:]:
453
+ if isinstance(stroke_elem, list):
454
+ stroke_type = str(stroke_elem[0])
455
+ if stroke_type == "width" and len(stroke_elem) >= 2:
456
+ rectangle["stroke_width"] = float(stroke_elem[1])
457
+ elif stroke_type == "type" and len(stroke_elem) >= 2:
458
+ rectangle["stroke_type"] = str(stroke_elem[1])
459
+ elif elem_type == "fill":
460
+ for fill_elem in elem[1:]:
461
+ if isinstance(fill_elem, list) and str(fill_elem[0]) == "type":
462
+ rectangle["fill_type"] = str(fill_elem[1]) if len(fill_elem) >= 2 else "none"
463
+ elif elem_type == "uuid" and len(elem) >= 2:
464
+ rectangle["uuid"] = str(elem[1])
465
+
466
+ return rectangle if rectangle else None
467
+
428
468
  def _parse_lib_symbols(self, item: List[Any]) -> Dict[str, Any]:
429
469
  """Parse lib_symbols section."""
430
470
  # Implementation for lib_symbols parsing
@@ -529,14 +569,28 @@ class SExpressionParser:
529
569
  # Add instances section (required by KiCAD)
530
570
  from .config import config
531
571
 
532
- project_name = getattr(self, "project_name", config.defaults.project_name)
533
- root_uuid = schematic_uuid or symbol_data.get("root_uuid", str(uuid.uuid4()))
534
- logger.debug(
535
- f"🔧 Using UUID {root_uuid} for component {symbol_data.get('reference', 'unknown')}"
536
- )
537
- logger.debug(
538
- f"🔧 Component properties keys: {list(symbol_data.get('properties', {}).keys())}"
539
- )
572
+ # Get project name from config or properties
573
+ project_name = symbol_data.get("properties", {}).get("project_name")
574
+ if not project_name:
575
+ project_name = getattr(self, "project_name", config.defaults.project_name)
576
+
577
+ # CRITICAL FIX: Use the FULL hierarchy_path from properties if available
578
+ # For hierarchical schematics, this contains the complete path: /root_uuid/sheet_symbol_uuid/...
579
+ # This ensures KiCad can properly annotate components in sub-sheets
580
+ hierarchy_path = symbol_data.get("properties", {}).get("hierarchy_path")
581
+ if hierarchy_path:
582
+ # Use the full hierarchical path (includes root + all sheet symbols)
583
+ instance_path = hierarchy_path
584
+ logger.debug(f"🔧 Using FULL hierarchy_path: {instance_path} for component {symbol_data.get('reference', 'unknown')}")
585
+ else:
586
+ # Fallback: use root_uuid or schematic_uuid for flat designs
587
+ root_uuid = symbol_data.get("properties", {}).get("root_uuid") or schematic_uuid or str(uuid.uuid4())
588
+ instance_path = f"/{root_uuid}"
589
+ logger.debug(f"🔧 Using root UUID path: {instance_path} for component {symbol_data.get('reference', 'unknown')}")
590
+
591
+ logger.debug(f"🔧 Component properties keys: {list(symbol_data.get('properties', {}).keys())}")
592
+ logger.debug(f"🔧 Using project name: '{project_name}'")
593
+
540
594
  sexp.append(
541
595
  [
542
596
  sexpdata.Symbol("instances"),
@@ -545,7 +599,7 @@ class SExpressionParser:
545
599
  project_name,
546
600
  [
547
601
  sexpdata.Symbol("path"),
548
- f"/{root_uuid}",
602
+ instance_path,
549
603
  [sexpdata.Symbol("reference"), symbol_data.get("reference", "U?")],
550
604
  [sexpdata.Symbol("unit"), symbol_data.get("unit", 1)],
551
605
  ],
@@ -772,7 +826,10 @@ class SExpressionParser:
772
826
  effects = [sexpdata.Symbol("effects")]
773
827
  font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
774
828
  effects.append(font)
775
- effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol("left")])
829
+
830
+ # Use justification from data if provided, otherwise default to "left"
831
+ justify = hlabel_data.get("justify", "left")
832
+ effects.append([sexpdata.Symbol("justify"), sexpdata.Symbol(justify)])
776
833
  sexp.append(effects)
777
834
 
778
835
  # Add UUID
@@ -1034,6 +1091,40 @@ class SExpressionParser:
1034
1091
 
1035
1092
  return sexp
1036
1093
 
1094
+ def _rectangle_to_sexp(self, rectangle_data: Dict[str, Any]) -> List[Any]:
1095
+ """Convert rectangle element to S-expression."""
1096
+ sexp = [sexpdata.Symbol("rectangle")]
1097
+
1098
+ # Add start point
1099
+ start = rectangle_data["start"]
1100
+ start_x, start_y = start["x"], start["y"]
1101
+ sexp.append([sexpdata.Symbol("start"), start_x, start_y])
1102
+
1103
+ # Add end point
1104
+ end = rectangle_data["end"]
1105
+ end_x, end_y = end["x"], end["y"]
1106
+ sexp.append([sexpdata.Symbol("end"), end_x, end_y])
1107
+
1108
+ # Add stroke
1109
+ stroke_width = rectangle_data.get("stroke_width", 0)
1110
+ stroke_type = rectangle_data.get("stroke_type", "default")
1111
+ stroke_sexp = [sexpdata.Symbol("stroke")]
1112
+ stroke_sexp.append([sexpdata.Symbol("width"), stroke_width])
1113
+ stroke_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(stroke_type)])
1114
+ sexp.append(stroke_sexp)
1115
+
1116
+ # Add fill
1117
+ fill_type = rectangle_data.get("fill_type", "none")
1118
+ fill_sexp = [sexpdata.Symbol("fill")]
1119
+ fill_sexp.append([sexpdata.Symbol("type"), sexpdata.Symbol(fill_type)])
1120
+ sexp.append(fill_sexp)
1121
+
1122
+ # Add UUID
1123
+ if "uuid" in rectangle_data:
1124
+ sexp.append([sexpdata.Symbol("uuid"), rectangle_data["uuid"]])
1125
+
1126
+ return sexp
1127
+
1037
1128
  def _lib_symbols_to_sexp(self, lib_symbols: Dict[str, Any]) -> List[Any]:
1038
1129
  """Convert lib_symbols to S-expression."""
1039
1130
  sexp = [sexpdata.Symbol("lib_symbols")]
@@ -1138,6 +1138,59 @@ class Schematic:
1138
1138
  logger.debug(f"Added text box: '{text}' at {position} size {size}")
1139
1139
  return text_box.uuid
1140
1140
 
1141
+ def add_rectangle(
1142
+ self,
1143
+ start: Union[Point, Tuple[float, float]],
1144
+ end: Union[Point, Tuple[float, float]],
1145
+ stroke_width: float = 0.0,
1146
+ stroke_type: str = "default",
1147
+ fill_type: str = "none"
1148
+ ) -> str:
1149
+ """
1150
+ Add a graphical rectangle element.
1151
+
1152
+ Args:
1153
+ start: Rectangle start point (top-left)
1154
+ end: Rectangle end point (bottom-right)
1155
+ stroke_width: Border line width
1156
+ stroke_type: Border line type (default, solid, dash, dot, etc.)
1157
+ fill_type: Fill type (none, solid, etc.)
1158
+
1159
+ Returns:
1160
+ UUID of created rectangle element
1161
+ """
1162
+ if isinstance(start, tuple):
1163
+ start = Point(start[0], start[1])
1164
+ if isinstance(end, tuple):
1165
+ end = Point(end[0], end[1])
1166
+
1167
+ from .types import SchematicRectangle
1168
+
1169
+ rectangle = SchematicRectangle(
1170
+ uuid=str(uuid.uuid4()),
1171
+ start=start,
1172
+ end=end,
1173
+ stroke_width=stroke_width,
1174
+ stroke_type=stroke_type,
1175
+ fill_type=fill_type
1176
+ )
1177
+
1178
+ if "rectangles" not in self._data:
1179
+ self._data["rectangles"] = []
1180
+
1181
+ self._data["rectangles"].append({
1182
+ "uuid": rectangle.uuid,
1183
+ "start": {"x": rectangle.start.x, "y": rectangle.start.y},
1184
+ "end": {"x": rectangle.end.x, "y": rectangle.end.y},
1185
+ "stroke_width": rectangle.stroke_width,
1186
+ "stroke_type": rectangle.stroke_type,
1187
+ "fill_type": rectangle.fill_type
1188
+ })
1189
+ self._modified = True
1190
+
1191
+ logger.debug(f"Added rectangle: {start} to {end}")
1192
+ return rectangle.uuid
1193
+
1141
1194
  def set_title_block(
1142
1195
  self,
1143
1196
  title: str = "",
@@ -338,6 +338,40 @@ class TextBox:
338
338
  self.uuid = str(uuid4())
339
339
 
340
340
 
341
+ @dataclass
342
+ class SchematicRectangle:
343
+ """Graphical rectangle element in schematic."""
344
+
345
+ uuid: str
346
+ start: Point
347
+ end: Point
348
+ stroke_width: float = 0.0
349
+ stroke_type: str = "default"
350
+ fill_type: str = "none"
351
+
352
+ def __post_init__(self):
353
+ if not self.uuid:
354
+ self.uuid = str(uuid4())
355
+
356
+ @property
357
+ def width(self) -> float:
358
+ """Rectangle width."""
359
+ return abs(self.end.x - self.start.x)
360
+
361
+ @property
362
+ def height(self) -> float:
363
+ """Rectangle height."""
364
+ return abs(self.end.y - self.start.y)
365
+
366
+ @property
367
+ def center(self) -> Point:
368
+ """Rectangle center point."""
369
+ return Point(
370
+ (self.start.x + self.end.x) / 2,
371
+ (self.start.y + self.end.y) / 2
372
+ )
373
+
374
+
341
375
  @dataclass
342
376
  class Net:
343
377
  """Electrical net connecting components."""
@@ -434,6 +468,7 @@ class Schematic:
434
468
  labels: List[Label] = field(default_factory=list)
435
469
  nets: List[Net] = field(default_factory=list)
436
470
  sheets: List[Sheet] = field(default_factory=list)
471
+ rectangles: List[SchematicRectangle] = field(default_factory=list)
437
472
  lib_symbols: Dict[str, Any] = field(default_factory=dict)
438
473
 
439
474
  def __post_init__(self):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kicad-sch-api
3
- Version: 0.2.1
3
+ Version: 0.2.2
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>
@@ -53,8 +53,11 @@ tests/test_component_removal.py
53
53
  tests/test_element_removal.py
54
54
  tests/test_grid_snapping.py
55
55
  tests/test_manhattan_routing.py
56
+ tests/test_parse_reference_rectangles.py
56
57
  tests/test_pin_positioning.py
57
58
  tests/test_pin_to_pin_wiring.py
59
+ tests/test_rectangle.py
60
+ tests/test_rectangle_roundtrip.py
58
61
  tests/test_removal_against_references.py
59
62
  tests/reference_tests/reference_kicad_projects/README.md
60
63
  tests/reference_tests/reference_kicad_projects/blank_schematic/blank_schematic.kicad_pro
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "kicad-sch-api"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "Professional KiCAD schematic manipulation library with exact format preservation"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1,48 @@
1
+ """
2
+ Test parsing rectangles from the reference RP2040 schematic.
3
+ """
4
+
5
+ import pytest
6
+ from kicad_sch_api import load_schematic
7
+
8
+
9
+ REFERENCE_SCHEMATIC = "/Users/shanemattner/Desktop/circuit-synth-examples/pcbs/rp2040-minimal/RP2040_Minimal/RP2040_Minimal.kicad_sch"
10
+
11
+
12
+ def test_parse_reference_schematic_rectangles():
13
+ """Test that we can parse rectangles from the reference schematic."""
14
+ try:
15
+ sch = load_schematic(REFERENCE_SCHEMATIC)
16
+
17
+ # The reference schematic should have rectangles (bounding boxes)
18
+ assert "rectangles" in sch._data
19
+ rectangles = sch._data["rectangles"]
20
+
21
+ # Should have at least one rectangle
22
+ assert len(rectangles) > 0, "Expected to find rectangles in reference schematic"
23
+
24
+ # Verify rectangle structure
25
+ rect = rectangles[0]
26
+ assert "start" in rect
27
+ assert "end" in rect
28
+ assert "x" in rect["start"]
29
+ assert "y" in rect["start"]
30
+ assert "x" in rect["end"]
31
+ assert "y" in rect["end"]
32
+ assert "uuid" in rect
33
+
34
+ # Verify stroke and fill
35
+ assert "stroke_width" in rect
36
+ assert "stroke_type" in rect
37
+ assert "fill_type" in rect
38
+
39
+ print(f"Successfully parsed {len(rectangles)} rectangles from reference schematic")
40
+ print(f"First rectangle: start=({rect['start']['x']}, {rect['start']['y']}), "
41
+ f"end=({rect['end']['x']}, {rect['end']['y']})")
42
+
43
+ except FileNotFoundError:
44
+ pytest.skip(f"Reference schematic not found: {REFERENCE_SCHEMATIC}")
45
+
46
+
47
+ if __name__ == "__main__":
48
+ pytest.main([__file__, "-v", "-s"])
@@ -0,0 +1,88 @@
1
+ """
2
+ Test rectangle functionality in kicad-sch-api.
3
+ """
4
+
5
+ import pytest
6
+ from kicad_sch_api.core.schematic import Schematic
7
+ from kicad_sch_api.core.types import Point
8
+
9
+
10
+ def test_add_rectangle():
11
+ """Test adding a rectangle to a schematic."""
12
+ # Create a new schematic
13
+ sch = Schematic()
14
+
15
+ # Add a rectangle
16
+ rect_uuid = sch.add_rectangle(
17
+ start=(10.0, 20.0),
18
+ end=(50.0, 60.0),
19
+ stroke_width=0.127,
20
+ stroke_type="solid",
21
+ fill_type="none"
22
+ )
23
+
24
+ # Verify UUID was returned
25
+ assert rect_uuid is not None
26
+ assert isinstance(rect_uuid, str)
27
+
28
+ # Verify rectangle was added to internal data
29
+ assert "rectangles" in sch._data
30
+ assert len(sch._data["rectangles"]) == 1
31
+
32
+ # Verify rectangle properties
33
+ rect = sch._data["rectangles"][0]
34
+ assert rect["uuid"] == rect_uuid
35
+ assert rect["start"]["x"] == 10.0
36
+ assert rect["start"]["y"] == 20.0
37
+ assert rect["end"]["x"] == 50.0
38
+ assert rect["end"]["y"] == 60.0
39
+ assert rect["stroke_width"] == 0.127
40
+ assert rect["stroke_type"] == "solid"
41
+ assert rect["fill_type"] == "none"
42
+
43
+
44
+ def test_add_rectangle_with_point_objects():
45
+ """Test adding rectangle using Point objects."""
46
+ sch = Schematic()
47
+
48
+ start = Point(100.0, 200.0)
49
+ end = Point(150.0, 250.0)
50
+
51
+ rect_uuid = sch.add_rectangle(
52
+ start=start,
53
+ end=end,
54
+ stroke_width=0.254
55
+ )
56
+
57
+ assert rect_uuid is not None
58
+ rect = sch._data["rectangles"][0]
59
+ assert rect["start"]["x"] == 100.0
60
+ assert rect["start"]["y"] == 200.0
61
+
62
+
63
+ def test_add_multiple_rectangles():
64
+ """Test adding multiple rectangles."""
65
+ sch = Schematic()
66
+
67
+ rect1_uuid = sch.add_rectangle((0, 0), (10, 10))
68
+ rect2_uuid = sch.add_rectangle((20, 20), (30, 30))
69
+ rect3_uuid = sch.add_rectangle((40, 40), (50, 50))
70
+
71
+ assert len(sch._data["rectangles"]) == 3
72
+ assert rect1_uuid != rect2_uuid != rect3_uuid
73
+
74
+
75
+ def test_rectangle_default_values():
76
+ """Test rectangle default parameter values."""
77
+ sch = Schematic()
78
+
79
+ rect_uuid = sch.add_rectangle((0, 0), (10, 10))
80
+
81
+ rect = sch._data["rectangles"][0]
82
+ assert rect["stroke_width"] == 0.0
83
+ assert rect["stroke_type"] == "default"
84
+ assert rect["fill_type"] == "none"
85
+
86
+
87
+ if __name__ == "__main__":
88
+ pytest.main([__file__, "-v"])
@@ -0,0 +1,111 @@
1
+ """
2
+ Test rectangle round-trip: create, save, load, verify.
3
+ """
4
+
5
+ import os
6
+ import tempfile
7
+ import pytest
8
+ from kicad_sch_api import create_schematic, load_schematic
9
+
10
+
11
+ def test_rectangle_roundtrip():
12
+ """Test creating, saving, and loading a schematic with rectangles."""
13
+ # Create a schematic with rectangles
14
+ sch = create_schematic("Rectangle Test")
15
+
16
+ # Add multiple rectangles
17
+ rect1_uuid = sch.add_rectangle(
18
+ start=(10.0, 20.0),
19
+ end=(50.0, 60.0),
20
+ stroke_width=0.127,
21
+ stroke_type="solid",
22
+ fill_type="none"
23
+ )
24
+
25
+ rect2_uuid = sch.add_rectangle(
26
+ start=(100.0, 100.0),
27
+ end=(200.0, 150.0),
28
+ stroke_width=0.254,
29
+ stroke_type="default",
30
+ fill_type="none"
31
+ )
32
+
33
+ # Save to temporary file
34
+ with tempfile.NamedTemporaryFile(suffix=".kicad_sch", delete=False, mode='w') as f:
35
+ temp_path = f.name
36
+
37
+ try:
38
+ sch.save(temp_path)
39
+
40
+ # Load the schematic back
41
+ sch_loaded = load_schematic(temp_path)
42
+
43
+ # Verify rectangles were preserved
44
+ assert "rectangles" in sch_loaded._data
45
+ assert len(sch_loaded._data["rectangles"]) == 2
46
+
47
+ # Verify first rectangle
48
+ rect1 = sch_loaded._data["rectangles"][0]
49
+ assert rect1["uuid"] == rect1_uuid
50
+ assert rect1["start"]["x"] == 10.0
51
+ assert rect1["start"]["y"] == 20.0
52
+ assert rect1["end"]["x"] == 50.0
53
+ assert rect1["end"]["y"] == 60.0
54
+ assert rect1["stroke_width"] == 0.127
55
+ assert rect1["stroke_type"] == "solid"
56
+
57
+ # Verify second rectangle
58
+ rect2 = sch_loaded._data["rectangles"][1]
59
+ assert rect2["uuid"] == rect2_uuid
60
+ assert rect2["start"]["x"] == 100.0
61
+ assert rect2["start"]["y"] == 100.0
62
+ assert rect2["stroke_width"] == 0.254
63
+
64
+ finally:
65
+ # Cleanup
66
+ if os.path.exists(temp_path):
67
+ os.remove(temp_path)
68
+
69
+
70
+ def test_rectangle_format_preservation():
71
+ """Test that rectangle S-expression format matches KiCAD."""
72
+ sch = create_schematic("Format Test")
73
+
74
+ # Add rectangle with specific parameters
75
+ sch.add_rectangle(
76
+ start=(91.821, 32.211),
77
+ end=(155.829, 148.049),
78
+ stroke_width=0.127,
79
+ stroke_type="solid",
80
+ fill_type="none"
81
+ )
82
+
83
+ # Save to string
84
+ with tempfile.NamedTemporaryFile(suffix=".kicad_sch", delete=False, mode='w') as f:
85
+ temp_path = f.name
86
+
87
+ try:
88
+ sch.save(temp_path)
89
+
90
+ # Read file content
91
+ with open(temp_path, 'r') as f:
92
+ content = f.read()
93
+
94
+ # Verify S-expression structure
95
+ assert "(rectangle" in content
96
+ assert "(start 91.821 32.211)" in content
97
+ assert "(end 155.829 148.049)" in content
98
+ assert "(stroke" in content
99
+ assert "(width 0.127)" in content
100
+ assert "(type solid)" in content
101
+ assert "(fill" in content
102
+ assert "(type none)" in content
103
+ assert "(uuid" in content
104
+
105
+ finally:
106
+ if os.path.exists(temp_path):
107
+ os.remove(temp_path)
108
+
109
+
110
+ if __name__ == "__main__":
111
+ pytest.main([__file__, "-v"])
File without changes
File without changes
File without changes
File without changes