kicad-sch-api 0.3.5__tar.gz → 0.4.0__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 (136) hide show
  1. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/CHANGELOG.md +65 -0
  2. {kicad_sch_api-0.3.5/kicad_sch_api.egg-info → kicad_sch_api-0.4.0}/PKG-INFO +1 -1
  3. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/collections/__init__.py +2 -2
  4. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/collections/base.py +5 -7
  5. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/collections/components.py +24 -12
  6. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/collections/junctions.py +31 -43
  7. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/collections/labels.py +19 -27
  8. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/collections/wires.py +17 -18
  9. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/core/components.py +5 -0
  10. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/core/formatter.py +3 -1
  11. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/core/labels.py +2 -2
  12. kicad_sch_api-0.4.0/kicad_sch_api/core/managers/__init__.py +26 -0
  13. kicad_sch_api-0.4.0/kicad_sch_api/core/managers/file_io.py +243 -0
  14. kicad_sch_api-0.4.0/kicad_sch_api/core/managers/format_sync.py +501 -0
  15. kicad_sch_api-0.4.0/kicad_sch_api/core/managers/graphics.py +579 -0
  16. kicad_sch_api-0.4.0/kicad_sch_api/core/managers/metadata.py +268 -0
  17. kicad_sch_api-0.4.0/kicad_sch_api/core/managers/sheet.py +454 -0
  18. kicad_sch_api-0.4.0/kicad_sch_api/core/managers/text_elements.py +536 -0
  19. kicad_sch_api-0.4.0/kicad_sch_api/core/managers/validation.py +474 -0
  20. kicad_sch_api-0.4.0/kicad_sch_api/core/managers/wire.py +346 -0
  21. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/core/nets.py +1 -1
  22. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/core/no_connects.py +5 -3
  23. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/core/parser.py +75 -41
  24. kicad_sch_api-0.4.0/kicad_sch_api/core/schematic.py +1584 -0
  25. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/core/texts.py +1 -1
  26. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/core/types.py +1 -4
  27. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/geometry/font_metrics.py +3 -1
  28. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/geometry/symbol_bbox.py +40 -21
  29. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/interfaces/__init__.py +1 -1
  30. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/interfaces/parser.py +1 -1
  31. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/interfaces/repository.py +1 -1
  32. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/interfaces/resolver.py +1 -1
  33. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/parsers/__init__.py +2 -2
  34. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/parsers/base.py +7 -10
  35. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/parsers/label_parser.py +7 -7
  36. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/parsers/registry.py +4 -2
  37. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/parsers/symbol_parser.py +5 -10
  38. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/parsers/wire_parser.py +2 -2
  39. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/symbols/__init__.py +1 -1
  40. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/symbols/cache.py +9 -12
  41. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/symbols/resolver.py +20 -26
  42. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/symbols/validators.py +188 -137
  43. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0/kicad_sch_api.egg-info}/PKG-INFO +1 -1
  44. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api.egg-info/SOURCES.txt +9 -0
  45. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/pyproject.toml +1 -1
  46. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/blank_schematic/blank_schematic.kicad_sch +6 -1
  47. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/test_geometry.py +5 -4
  48. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/test_image_support.py +15 -30
  49. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/test_issue_13_public_properties.py +20 -26
  50. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/test_kicad_validation.py +4 -3
  51. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/test_parse_reference_rectangles.py +5 -3
  52. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/test_rectangle.py +6 -9
  53. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/test_rectangle_roundtrip.py +8 -6
  54. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/test_wire_operations.py +9 -3
  55. kicad_sch_api-0.3.5/kicad_sch_api/core/schematic.py +0 -1888
  56. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/.claude/commands/dev/dead-code-analysis.md +0 -0
  57. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/.claude/commands/dev/publish-pypi.md +0 -0
  58. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/.claude/commands/dev/review-implementation.md +0 -0
  59. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/.claude/commands/dev/run-tests.md +0 -0
  60. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/.claude/commands/dev/update-and-commit.md +0 -0
  61. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/.claude/commands/dev/update-memory-bank.md +0 -0
  62. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/.claude/commands/test/run-reference-tests.md +0 -0
  63. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/LICENSE +0 -0
  64. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/MANIFEST.in +0 -0
  65. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/README.md +0 -0
  66. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/examples/advanced_usage.py +0 -0
  67. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/examples/basic_usage.py +0 -0
  68. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/examples/mcp_basic_example.py +0 -0
  69. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/examples/mcp_integration.py +0 -0
  70. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/examples/parser_demo.py +0 -0
  71. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/examples/pin_to_pin_wiring_demo.py +0 -0
  72. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/examples/simple_circuit_with_pin_wiring.py +0 -0
  73. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/examples/simple_two_resistor_routing.py +0 -0
  74. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/__init__.py +0 -0
  75. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/cli.py +0 -0
  76. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/core/__init__.py +0 -0
  77. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/core/component_bounds.py +0 -0
  78. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/core/config.py +0 -0
  79. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/core/geometry.py +0 -0
  80. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/core/ic_manager.py +0 -0
  81. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/core/junctions.py +0 -0
  82. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/core/manhattan_routing.py +0 -0
  83. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/core/pin_utils.py +0 -0
  84. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/core/simple_manhattan.py +0 -0
  85. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/core/wire_routing.py +0 -0
  86. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/core/wires.py +0 -0
  87. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/discovery/__init__.py +0 -0
  88. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/discovery/search_index.py +0 -0
  89. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/geometry/__init__.py +0 -0
  90. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/library/__init__.py +0 -0
  91. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/library/cache.py +0 -0
  92. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/py.typed +0 -0
  93. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/utils/__init__.py +0 -0
  94. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api/utils/validation.py +0 -0
  95. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api.egg-info/dependency_links.txt +0 -0
  96. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api.egg-info/entry_points.txt +0 -0
  97. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api.egg-info/requires.txt +0 -0
  98. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/kicad_sch_api.egg-info/top_level.txt +0 -0
  99. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/setup.cfg +0 -0
  100. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/README.md +0 -0
  101. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/blank_schematic/blank_schematic.kicad_pro +0 -0
  102. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/multi_unit_7400/multi_unit_7400.kicad_pro +0 -0
  103. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/multi_unit_7400/multi_unit_7400.kicad_sch +0 -0
  104. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/power_symbols/power_symbols.kicad_pro +0 -0
  105. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/power_symbols/power_symbols.kicad_sch +0 -0
  106. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/resistor_divider/resistor_divider.kicad_pro +0 -0
  107. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/resistor_divider/resistor_divider.kicad_sch +0 -0
  108. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/sch_title/sch_title.kicad_pro +0 -0
  109. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/sch_title/sch_title.kicad_sch +0 -0
  110. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/single_extended_component/single_extended_component.kicad_pro +0 -0
  111. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/single_extended_component/single_extended_component.kicad_sch +0 -0
  112. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/single_hierarchical_sheet/single_hierarchical_sheet.kicad_pro +0 -0
  113. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/single_hierarchical_sheet/single_hierarchical_sheet.kicad_sch +0 -0
  114. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/single_hierarchical_sheet/subcircuit1.kicad_sch +0 -0
  115. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/single_label/single_label.kicad_pro +0 -0
  116. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/single_label/single_label.kicad_sch +0 -0
  117. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/single_label_hierarchical/single_label_hierarchical.kicad_pro +0 -0
  118. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/single_label_hierarchical/single_label_hierarchical.kicad_sch +0 -0
  119. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/single_resistor/single_resistor.kicad_pro +0 -0
  120. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/single_resistor/single_resistor.kicad_sch +0 -0
  121. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/single_text/single_text.kicad_pro +0 -0
  122. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/single_text/single_text.kicad_sch +0 -0
  123. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/single_text_box/single_text_box.kicad_pro +0 -0
  124. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/single_text_box/single_text_box.kicad_sch +0 -0
  125. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/single_wire/single_wire.kicad_pro +0 -0
  126. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/single_wire/single_wire.kicad_sch +0 -0
  127. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/two_resistors/two_resistors.kicad_pro +0 -0
  128. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/reference_tests/reference_kicad_projects/two_resistors/two_resistors.kicad_sch +0 -0
  129. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/test_bounding_box_rectangles.py +0 -0
  130. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/test_component_removal.py +0 -0
  131. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/test_element_removal.py +0 -0
  132. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/test_grid_snapping.py +0 -0
  133. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/test_manhattan_routing.py +0 -0
  134. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/test_pin_positioning.py +0 -0
  135. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/test_pin_to_pin_wiring.py +0 -0
  136. {kicad_sch_api-0.3.5 → kicad_sch_api-0.4.0}/tests/test_removal_against_references.py +0 -0
@@ -5,6 +5,71 @@ 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.4.0] - 2025-10-26
9
+
10
+ ### Added
11
+ - **Phase 4 Manager Architecture**: Complete refactoring of schematic data management
12
+ - Introduced `ComponentManager` for component operations and lib_symbols synchronization
13
+ - Introduced `WireManager` for wire and bus operations
14
+ - Introduced `JunctionManager` for junction operations
15
+ - Introduced `SheetManager` for hierarchical sheet management
16
+ - Introduced `TextElementManager` for label and text operations
17
+ - Introduced `GraphicsManager` for graphical elements (rectangles, circles, arcs, polylines)
18
+ - Composition-based architecture replacing monolithic schematic class
19
+
20
+ - **Text Box Support**: Full text box functionality with complete KiCAD compatibility
21
+ - `add_text_box()` method with rotation, font size, margins, and justification
22
+ - Support for stroke styling and fill options
23
+ - Proper serialization matching KiCAD format exactly
24
+
25
+ - **Enhanced Hierarchical Sheet Support**: Complete sheet management functionality
26
+ - Sheet creation with proper position, size, and styling
27
+ - Sheet pin management with multiple pin types (input, output, bidirectional, tri_state, passive)
28
+ - Sheet hierarchy validation and traversal
29
+ - Proper data structure matching KiCAD parser expectations
30
+
31
+ - **Rectangle Color Support**: Full color support for graphical rectangles
32
+ - Stroke color customization (RGBA)
33
+ - Fill color customization (RGBA)
34
+ - Proper color serialization in S-expression output
35
+
36
+ ### Fixed
37
+ - **Hierarchical Sheet Data Structure**: Fixed sheet serialization format
38
+ - Changed storage key from "sheet" (singular) to "sheets" (plural)
39
+ - Fixed position format from lists to dictionaries: `{"x": x, "y": y}`
40
+ - Fixed size format from lists to dictionaries: `{"width": w, "height": h}`
41
+ - Fixed fill_color default from white (255,255,255,0.0) to transparent black (0,0,0,0.0)
42
+ - Fixed sheet pin structure: "pin" → "pins", "shape" → "pin_type"
43
+
44
+ - **Rectangle Color Flow**: Fixed color parameter propagation through managers and parser
45
+ - GraphicsManager now extracts and stores stroke_color and fill_color from stroke/fill dicts
46
+ - Parser now serializes colors in stroke and fill S-expression sections
47
+ - Complete color support for bounding box visualization
48
+
49
+ - **Text Box Data Structure**: Fixed text box format to match parser expectations
50
+ - Changed storage key from "text_box" to "text_boxes" (plural)
51
+ - Implemented complete parameter set (rotation, font_size, margins, stroke, fill, justification)
52
+ - Fixed position/size format to use dictionaries instead of lists
53
+
54
+ ### Changed
55
+ - **Schematic Class Refactoring**: Delegated operations to specialized managers
56
+ - Schematic class now composes managers instead of implementing all operations
57
+ - Cleaner separation of concerns and improved maintainability
58
+ - All existing APIs maintained for backward compatibility
59
+
60
+ - **Test Suite**: All 302 tests passing (295 run, 7 skipped)
61
+ - Added tests for hierarchical sheets with proper serialization
62
+ - Added tests for rectangle colors with stroke and fill
63
+ - Added tests for text boxes with full parameter support
64
+ - Format preservation tests validate exact KiCAD compatibility
65
+
66
+ ### Technical Notes
67
+ - Manager architecture enables easier feature additions and maintenance
68
+ - All data structures now properly synchronized between managers and parser
69
+ - Singular/plural key consistency enforced throughout codebase
70
+ - Position and size formats standardized to dictionary format
71
+ - 100% backward compatibility maintained - no breaking changes
72
+
8
73
  ## [0.3.0] - 2025-10-12
9
74
 
10
75
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kicad-sch-api
3
- Version: 0.3.5
3
+ Version: 0.4.0
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>
@@ -8,9 +8,9 @@ indexing, performance optimization, and management capabilities.
8
8
 
9
9
  from .base import IndexedCollection
10
10
  from .components import ComponentCollection
11
- from .wires import WireCollection
12
11
  from .junctions import JunctionCollection
13
12
  from .labels import LabelCollection
13
+ from .wires import WireCollection
14
14
 
15
15
  __all__ = [
16
16
  "IndexedCollection",
@@ -18,4 +18,4 @@ __all__ = [
18
18
  "WireCollection",
19
19
  "JunctionCollection",
20
20
  "LabelCollection",
21
- ]
21
+ ]
@@ -11,7 +11,7 @@ from typing import Any, Callable, Dict, Generic, Iterator, List, Optional, TypeV
11
11
 
12
12
  logger = logging.getLogger(__name__)
13
13
 
14
- T = TypeVar('T') # Type variable for collection items
14
+ T = TypeVar("T") # Type variable for collection items
15
15
 
16
16
 
17
17
  class IndexedCollection(Generic[T], ABC):
@@ -183,6 +183,7 @@ class IndexedCollection(Generic[T], ABC):
183
183
  Returns:
184
184
  List of matching items
185
185
  """
186
+
186
187
  def matches_criteria(item: T) -> bool:
187
188
  for attr, value in criteria.items():
188
189
  if not hasattr(item, attr) or getattr(item, attr) != value:
@@ -257,10 +258,7 @@ class IndexedCollection(Generic[T], ABC):
257
258
  def _rebuild_indexes(self) -> None:
258
259
  """Rebuild all indexes."""
259
260
  # Rebuild UUID index
260
- self._uuid_index = {
261
- self._get_item_uuid(item): i
262
- for i, item in enumerate(self._items)
263
- }
261
+ self._uuid_index = {self._get_item_uuid(item): i for i, item in enumerate(self._items)}
264
262
 
265
263
  # Let subclasses rebuild their additional indexes
266
264
  self._build_additional_indexes()
@@ -282,7 +280,7 @@ class IndexedCollection(Generic[T], ABC):
282
280
  "uuid_index_size": len(self._uuid_index),
283
281
  "modified": self._modified,
284
282
  "indexes_dirty": self._dirty_indexes,
285
- "collection_type": self.__class__.__name__
283
+ "collection_type": self.__class__.__name__,
286
284
  }
287
285
 
288
286
  @property
@@ -293,4 +291,4 @@ class IndexedCollection(Generic[T], ABC):
293
291
  def mark_clean(self) -> None:
294
292
  """Mark collection as clean (not modified)."""
295
293
  self._modified = False
296
- logger.debug(f"Marked {self.__class__.__name__} as clean")
294
+ logger.debug(f"Marked {self.__class__.__name__} as clean")
@@ -250,6 +250,7 @@ class ComponentCollection(IndexedCollection[Component]):
250
250
 
251
251
  # Always snap component position to KiCAD grid (1.27mm = 50mil)
252
252
  from ..core.geometry import snap_to_grid
253
+
253
254
  snapped_pos = snap_to_grid((position.x, position.y), grid_size=1.27)
254
255
  position = Point(snapped_pos[0], snapped_pos[1])
255
256
 
@@ -269,7 +270,7 @@ class ComponentCollection(IndexedCollection[Component]):
269
270
  in_bom=True,
270
271
  on_board=True,
271
272
  footprint=footprint,
272
- properties=properties.copy()
273
+ properties=properties.copy(),
273
274
  )
274
275
 
275
276
  # Create component wrapper
@@ -335,16 +336,27 @@ class ComponentCollection(IndexedCollection[Component]):
335
336
 
336
337
  # Map common component types to standard prefixes
337
338
  ref_prefixes = {
338
- "R": "R", "Resistor": "R",
339
- "C": "C", "Capacitor": "C",
340
- "L": "L", "Inductor": "L",
341
- "D": "D", "Diode": "D",
342
- "Q": "Q", "Transistor": "Q",
343
- "U": "U", "IC": "U", "Amplifier": "U",
344
- "J": "J", "Connector": "J",
345
- "SW": "SW", "Switch": "SW",
346
- "F": "F", "Fuse": "F",
347
- "TP": "TP", "TestPoint": "TP",
339
+ "R": "R",
340
+ "Resistor": "R",
341
+ "C": "C",
342
+ "Capacitor": "C",
343
+ "L": "L",
344
+ "Inductor": "L",
345
+ "D": "D",
346
+ "Diode": "D",
347
+ "Q": "Q",
348
+ "Transistor": "Q",
349
+ "U": "U",
350
+ "IC": "U",
351
+ "Amplifier": "U",
352
+ "J": "J",
353
+ "Connector": "J",
354
+ "SW": "SW",
355
+ "Switch": "SW",
356
+ "F": "F",
357
+ "Fuse": "F",
358
+ "TP": "TP",
359
+ "TestPoint": "TP",
348
360
  }
349
361
 
350
362
  prefix = ref_prefixes.get(base_ref, "U")
@@ -419,4 +431,4 @@ class ComponentCollection(IndexedCollection[Component]):
419
431
  self._mark_indexes_dirty()
420
432
 
421
433
  logger.info(f"Bulk updated {len(matching_components)} components")
422
- return len(matching_components)
434
+ return len(matching_components)
@@ -89,26 +89,22 @@ class JunctionCollection(IndexedCollection[Junction]):
89
89
  pos_key = (position.x, position.y)
90
90
  if pos_key in self._position_index:
91
91
  existing = self._position_index[pos_key]
92
- raise ValueError(f"Junction already exists at position {position} (UUID: {existing.uuid})")
92
+ raise ValueError(
93
+ f"Junction already exists at position {position} (UUID: {existing.uuid})"
94
+ )
93
95
 
94
96
  # Generate UUID if not provided
95
97
  if junction_uuid is None:
96
98
  junction_uuid = str(uuid_module.uuid4())
97
99
 
98
100
  # Create junction
99
- junction = Junction(
100
- uuid=junction_uuid,
101
- position=position,
102
- diameter=diameter
103
- )
101
+ junction = Junction(uuid=junction_uuid, position=position, diameter=diameter)
104
102
 
105
103
  # Add to collection using base class method
106
104
  return super().add(junction)
107
105
 
108
106
  def get_junction_at_position(
109
- self,
110
- position: Union[Point, Tuple[float, float]],
111
- tolerance: float = 0.0
107
+ self, position: Union[Point, Tuple[float, float]], tolerance: float = 0.0
112
108
  ) -> Optional[Junction]:
113
109
  """
114
110
  Get junction at a specific position.
@@ -137,7 +133,7 @@ class JunctionCollection(IndexedCollection[Junction]):
137
133
  for junction in self._items:
138
134
  dx = abs(junction.position.x - target_x)
139
135
  dy = abs(junction.position.y - target_y)
140
- distance = (dx ** 2 + dy ** 2) ** 0.5
136
+ distance = (dx**2 + dy**2) ** 0.5
141
137
 
142
138
  if distance <= tolerance:
143
139
  return junction
@@ -145,9 +141,7 @@ class JunctionCollection(IndexedCollection[Junction]):
145
141
  return None
146
142
 
147
143
  def has_junction_at_position(
148
- self,
149
- position: Union[Point, Tuple[float, float]],
150
- tolerance: float = 0.0
144
+ self, position: Union[Point, Tuple[float, float]], tolerance: float = 0.0
151
145
  ) -> bool:
152
146
  """
153
147
  Check if a junction exists at a specific position.
@@ -162,11 +156,7 @@ class JunctionCollection(IndexedCollection[Junction]):
162
156
  return self.get_junction_at_position(position, tolerance) is not None
163
157
 
164
158
  def find_junctions_in_region(
165
- self,
166
- min_x: float,
167
- min_y: float,
168
- max_x: float,
169
- max_y: float
159
+ self, min_x: float, min_y: float, max_x: float, max_y: float
170
160
  ) -> List[Junction]:
171
161
  """
172
162
  Find all junctions within a rectangular region.
@@ -183,16 +173,13 @@ class JunctionCollection(IndexedCollection[Junction]):
183
173
  matching_junctions = []
184
174
 
185
175
  for junction in self._items:
186
- if (min_x <= junction.position.x <= max_x and
187
- min_y <= junction.position.y <= max_y):
176
+ if min_x <= junction.position.x <= max_x and min_y <= junction.position.y <= max_y:
188
177
  matching_junctions.append(junction)
189
178
 
190
179
  return matching_junctions
191
180
 
192
181
  def update_junction_position(
193
- self,
194
- junction_uuid: str,
195
- new_position: Union[Point, Tuple[float, float]]
182
+ self, junction_uuid: str, new_position: Union[Point, Tuple[float, float]]
196
183
  ) -> bool:
197
184
  """
198
185
  Update the position of an existing junction.
@@ -286,8 +273,9 @@ class JunctionCollection(IndexedCollection[Junction]):
286
273
 
287
274
  # Allow small tolerance for floating point precision
288
275
  tolerance = grid_size * 0.01
289
- if (x_remainder > tolerance and x_remainder < grid_size - tolerance) or \
290
- (y_remainder > tolerance and y_remainder < grid_size - tolerance):
276
+ if (x_remainder > tolerance and x_remainder < grid_size - tolerance) or (
277
+ y_remainder > tolerance and y_remainder < grid_size - tolerance
278
+ ):
291
279
  misaligned.append(junction)
292
280
 
293
281
  return misaligned
@@ -310,8 +298,10 @@ class JunctionCollection(IndexedCollection[Junction]):
310
298
  aligned_y = round(junction.position.y / grid_size) * grid_size
311
299
 
312
300
  # Check if position needs to change
313
- if (abs(junction.position.x - aligned_x) > 0.001 or
314
- abs(junction.position.y - aligned_y) > 0.001):
301
+ if (
302
+ abs(junction.position.x - aligned_x) > 0.001
303
+ or abs(junction.position.y - aligned_y) > 0.001
304
+ ):
315
305
 
316
306
  # Update position
317
307
  junction.position = Point(aligned_x, aligned_y)
@@ -326,11 +316,7 @@ class JunctionCollection(IndexedCollection[Junction]):
326
316
 
327
317
  # Bulk operations
328
318
  def remove_junctions_in_region(
329
- self,
330
- min_x: float,
331
- min_y: float,
332
- max_x: float,
333
- max_y: float
319
+ self, min_x: float, min_y: float, max_x: float, max_y: float
334
320
  ) -> int:
335
321
  """
336
322
  Remove all junctions within a rectangular region.
@@ -365,14 +351,16 @@ class JunctionCollection(IndexedCollection[Junction]):
365
351
  # Calculate diameter statistics
366
352
  diameters = [junction.diameter for junction in self._items]
367
353
  if diameters:
368
- stats.update({
369
- "diameter_stats": {
370
- "min": min(diameters),
371
- "max": max(diameters),
372
- "average": sum(diameters) / len(diameters)
373
- },
374
- "grid_aligned": len(self._items) - len(self.validate_grid_alignment()),
375
- "misaligned": len(self.validate_grid_alignment())
376
- })
377
-
378
- return stats
354
+ stats.update(
355
+ {
356
+ "diameter_stats": {
357
+ "min": min(diameters),
358
+ "max": max(diameters),
359
+ "average": sum(diameters) / len(diameters),
360
+ },
361
+ "grid_aligned": len(self._items) - len(self.validate_grid_alignment()),
362
+ "misaligned": len(self.validate_grid_alignment()),
363
+ }
364
+ )
365
+
366
+ return stats
@@ -25,7 +25,7 @@ class Label:
25
25
  position: Point,
26
26
  rotation: float = 0.0,
27
27
  label_type: str = "label",
28
- effects: Optional[Dict[str, Any]] = None
28
+ effects: Optional[Dict[str, Any]] = None,
29
29
  ):
30
30
  self.uuid = uuid
31
31
  self.text = text
@@ -150,7 +150,7 @@ class LabelCollection(IndexedCollection[Label]):
150
150
  position=position,
151
151
  rotation=rotation,
152
152
  label_type=label_type,
153
- effects=effects or {}
153
+ effects=effects or {},
154
154
  )
155
155
 
156
156
  # Add to collection using base class method
@@ -176,9 +176,7 @@ class LabelCollection(IndexedCollection[Label]):
176
176
  return self._text_index.get(text_key, []).copy()
177
177
 
178
178
  def get_labels_at_position(
179
- self,
180
- position: Union[Point, Tuple[float, float]],
181
- tolerance: float = 0.0
179
+ self, position: Union[Point, Tuple[float, float]], tolerance: float = 0.0
182
180
  ) -> List[Label]:
183
181
  """
184
182
  Get all labels at a specific position.
@@ -208,7 +206,7 @@ class LabelCollection(IndexedCollection[Label]):
208
206
  for label in self._items:
209
207
  dx = abs(label.position.x - target_x)
210
208
  dy = abs(label.position.y - target_y)
211
- distance = (dx ** 2 + dy ** 2) ** 0.5
209
+ distance = (dx**2 + dy**2) ** 0.5
212
210
 
213
211
  if distance <= tolerance:
214
212
  matching_labels.append(label)
@@ -251,11 +249,7 @@ class LabelCollection(IndexedCollection[Label]):
251
249
  return self.get_labels_by_text(net_name, case_sensitive)
252
250
 
253
251
  def find_labels_in_region(
254
- self,
255
- min_x: float,
256
- min_y: float,
257
- max_x: float,
258
- max_y: float
252
+ self, min_x: float, min_y: float, max_x: float, max_y: float
259
253
  ) -> List[Label]:
260
254
  """
261
255
  Find all labels within a rectangular region.
@@ -272,8 +266,7 @@ class LabelCollection(IndexedCollection[Label]):
272
266
  matching_labels = []
273
267
 
274
268
  for label in self._items:
275
- if (min_x <= label.position.x <= max_x and
276
- min_y <= label.position.y <= max_y):
269
+ if min_x <= label.position.x <= max_x and min_y <= label.position.y <= max_y:
277
270
  matching_labels.append(label)
278
271
 
279
272
  return matching_labels
@@ -308,9 +301,7 @@ class LabelCollection(IndexedCollection[Label]):
308
301
  return True
309
302
 
310
303
  def update_label_position(
311
- self,
312
- label_uuid: str,
313
- new_position: Union[Point, Tuple[float, float]]
304
+ self, label_uuid: str, new_position: Union[Point, Tuple[float, float]]
314
305
  ) -> bool:
315
306
  """
316
307
  Update the position of an existing label.
@@ -399,14 +390,15 @@ class LabelCollection(IndexedCollection[Label]):
399
390
  stats = super().get_statistics()
400
391
 
401
392
  # Add label-specific statistics
402
- stats.update({
403
- "unique_texts": len(self._text_index),
404
- "unique_positions": len(self._position_index),
405
- "label_types": {
406
- label_type: len(labels)
407
- for label_type, labels in self._type_index.items()
408
- },
409
- "net_count": len(self.get_net_names())
410
- })
411
-
412
- return stats
393
+ stats.update(
394
+ {
395
+ "unique_texts": len(self._text_index),
396
+ "unique_positions": len(self._position_index),
397
+ "label_types": {
398
+ label_type: len(labels) for label_type, labels in self._type_index.items()
399
+ },
400
+ "net_count": len(self.get_net_names()),
401
+ }
402
+ )
403
+
404
+ return stats
@@ -65,7 +65,7 @@ class WireCollection(IndexedCollection[Wire]):
65
65
  self._endpoint_index[endpoint].append(wire)
66
66
 
67
67
  # Type index
68
- wire_type = getattr(wire, 'wire_type', WireType.WIRE)
68
+ wire_type = getattr(wire, "wire_type", WireType.WIRE)
69
69
  if wire_type not in self._type_index:
70
70
  self._type_index[wire_type] = []
71
71
  self._type_index[wire_type].append(wire)
@@ -114,7 +114,7 @@ class WireCollection(IndexedCollection[Wire]):
114
114
  points=[start, end],
115
115
  wire_type=wire_type,
116
116
  stroke_width=stroke_width,
117
- stroke_type=stroke_type
117
+ stroke_type=stroke_type,
118
118
  )
119
119
 
120
120
  # Add to collection using base class method
@@ -165,7 +165,7 @@ class WireCollection(IndexedCollection[Wire]):
165
165
  points=converted_points,
166
166
  wire_type=wire_type,
167
167
  stroke_width=stroke_width,
168
- stroke_type=stroke_type
168
+ stroke_type=stroke_type,
169
169
  )
170
170
 
171
171
  # Add to collection using base class method
@@ -262,9 +262,7 @@ class WireCollection(IndexedCollection[Wire]):
262
262
  return networks
263
263
 
264
264
  def modify_wire_path(
265
- self,
266
- wire_uuid: str,
267
- new_points: List[Union[Point, Tuple[float, float]]]
265
+ self, wire_uuid: str, new_points: List[Union[Point, Tuple[float, float]]]
268
266
  ) -> bool:
269
267
  """
270
268
  Modify the path of an existing wire.
@@ -325,7 +323,7 @@ class WireCollection(IndexedCollection[Wire]):
325
323
  self,
326
324
  wire_type: Optional[WireType] = None,
327
325
  stroke_width: Optional[float] = None,
328
- stroke_type: Optional[str] = None
326
+ stroke_type: Optional[str] = None,
329
327
  ) -> int:
330
328
  """
331
329
  Bulk update stroke properties for wires.
@@ -368,15 +366,16 @@ class WireCollection(IndexedCollection[Wire]):
368
366
  stats = super().get_statistics()
369
367
 
370
368
  # Add wire-specific statistics
371
- stats.update({
372
- "endpoint_count": len(self._endpoint_index),
373
- "wire_types": {
374
- wire_type.value: len(wires)
375
- for wire_type, wires in self._type_index.items()
376
- },
377
- "networks": len(self.find_wire_networks()),
378
- "total_length": sum(self._calculate_wire_length(wire) for wire in self._items)
379
- })
369
+ stats.update(
370
+ {
371
+ "endpoint_count": len(self._endpoint_index),
372
+ "wire_types": {
373
+ wire_type.value: len(wires) for wire_type, wires in self._type_index.items()
374
+ },
375
+ "networks": len(self.find_wire_networks()),
376
+ "total_length": sum(self._calculate_wire_length(wire) for wire in self._items),
377
+ }
378
+ )
380
379
 
381
380
  return stats
382
381
 
@@ -400,8 +399,8 @@ class WireCollection(IndexedCollection[Wire]):
400
399
 
401
400
  dx = end_point.x - start_point.x
402
401
  dy = end_point.y - start_point.y
403
- segment_length = (dx ** 2 + dy ** 2) ** 0.5
402
+ segment_length = (dx**2 + dy**2) ** 0.5
404
403
 
405
404
  total_length += segment_length
406
405
 
407
- return total_length
406
+ return total_length
@@ -38,6 +38,11 @@ class Component:
38
38
  self._validator = SchematicValidator()
39
39
 
40
40
  # Core properties with validation
41
+ @property
42
+ def uuid(self) -> str:
43
+ """Component UUID."""
44
+ return self._data.uuid
45
+
41
46
  @property
42
47
  def reference(self) -> str:
43
48
  """Component reference (e.g., 'R1')."""
@@ -55,7 +55,9 @@ 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}) # Paper size should be quoted per KiCad format
58
+ self.rules["paper"] = FormatRule(
59
+ inline=True, quote_indices={1}
60
+ ) # Paper size should be quoted per KiCad format
59
61
 
60
62
  # Title block
61
63
  self.rules["title_block"] = FormatRule(inline=False)
@@ -10,7 +10,7 @@ import uuid
10
10
  from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Union
11
11
 
12
12
  from ..utils.validation import SchematicValidator, ValidationError, ValidationIssue
13
- from .types import Point, Label
13
+ from .types import Label, Point
14
14
 
15
15
  logger = logging.getLogger(__name__)
16
16
 
@@ -345,4 +345,4 @@ class LabelCollection:
345
345
 
346
346
  def __bool__(self) -> bool:
347
347
  """Return True if collection has labels."""
348
- return len(self._labels) > 0
348
+ return len(self._labels) > 0
@@ -0,0 +1,26 @@
1
+ """
2
+ Schematic management modules for separating responsibilities.
3
+
4
+ This package contains specialized managers for different aspects of schematic
5
+ manipulation, enabling clean separation of concerns and better maintainability.
6
+ """
7
+
8
+ from .file_io import FileIOManager
9
+ from .format_sync import FormatSyncManager
10
+ from .graphics import GraphicsManager
11
+ from .metadata import MetadataManager
12
+ from .sheet import SheetManager
13
+ from .text_elements import TextElementManager
14
+ from .validation import ValidationManager
15
+ from .wire import WireManager
16
+
17
+ __all__ = [
18
+ "FileIOManager",
19
+ "FormatSyncManager",
20
+ "GraphicsManager",
21
+ "MetadataManager",
22
+ "SheetManager",
23
+ "TextElementManager",
24
+ "ValidationManager",
25
+ "WireManager",
26
+ ]