kicad-sch-api 0.4.1__py3-none-any.whl → 0.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. kicad_sch_api/__init__.py +67 -2
  2. kicad_sch_api/cli/kicad_to_python.py +169 -0
  3. kicad_sch_api/collections/__init__.py +23 -8
  4. kicad_sch_api/collections/base.py +369 -59
  5. kicad_sch_api/collections/components.py +1376 -187
  6. kicad_sch_api/collections/junctions.py +129 -289
  7. kicad_sch_api/collections/labels.py +391 -287
  8. kicad_sch_api/collections/wires.py +202 -316
  9. kicad_sch_api/core/__init__.py +37 -2
  10. kicad_sch_api/core/component_bounds.py +34 -12
  11. kicad_sch_api/core/components.py +146 -7
  12. kicad_sch_api/core/config.py +25 -12
  13. kicad_sch_api/core/connectivity.py +692 -0
  14. kicad_sch_api/core/exceptions.py +175 -0
  15. kicad_sch_api/core/factories/element_factory.py +3 -1
  16. kicad_sch_api/core/formatter.py +24 -7
  17. kicad_sch_api/core/geometry.py +94 -5
  18. kicad_sch_api/core/managers/__init__.py +4 -0
  19. kicad_sch_api/core/managers/base.py +76 -0
  20. kicad_sch_api/core/managers/file_io.py +3 -1
  21. kicad_sch_api/core/managers/format_sync.py +3 -2
  22. kicad_sch_api/core/managers/graphics.py +3 -2
  23. kicad_sch_api/core/managers/hierarchy.py +661 -0
  24. kicad_sch_api/core/managers/metadata.py +4 -2
  25. kicad_sch_api/core/managers/sheet.py +52 -14
  26. kicad_sch_api/core/managers/text_elements.py +3 -2
  27. kicad_sch_api/core/managers/validation.py +3 -2
  28. kicad_sch_api/core/managers/wire.py +112 -54
  29. kicad_sch_api/core/parsing_utils.py +63 -0
  30. kicad_sch_api/core/pin_utils.py +103 -9
  31. kicad_sch_api/core/schematic.py +343 -29
  32. kicad_sch_api/core/types.py +79 -7
  33. kicad_sch_api/exporters/__init__.py +10 -0
  34. kicad_sch_api/exporters/python_generator.py +610 -0
  35. kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
  36. kicad_sch_api/geometry/__init__.py +15 -3
  37. kicad_sch_api/geometry/routing.py +211 -0
  38. kicad_sch_api/parsers/elements/label_parser.py +30 -8
  39. kicad_sch_api/parsers/elements/symbol_parser.py +255 -83
  40. kicad_sch_api/utils/logging.py +555 -0
  41. kicad_sch_api/utils/logging_decorators.py +587 -0
  42. kicad_sch_api/utils/validation.py +16 -22
  43. kicad_sch_api/wrappers/__init__.py +14 -0
  44. kicad_sch_api/wrappers/base.py +89 -0
  45. kicad_sch_api/wrappers/wire.py +198 -0
  46. kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
  47. kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
  48. kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
  49. {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
  50. mcp_server/__init__.py +34 -0
  51. mcp_server/example_logging_integration.py +506 -0
  52. mcp_server/models.py +252 -0
  53. mcp_server/server.py +357 -0
  54. mcp_server/tools/__init__.py +32 -0
  55. mcp_server/tools/component_tools.py +516 -0
  56. mcp_server/tools/connectivity_tools.py +532 -0
  57. mcp_server/tools/consolidated_tools.py +1216 -0
  58. mcp_server/tools/pin_discovery.py +333 -0
  59. mcp_server/utils/__init__.py +38 -0
  60. mcp_server/utils/logging.py +127 -0
  61. mcp_server/utils.py +36 -0
  62. kicad_sch_api-0.4.1.dist-info/METADATA +0 -491
  63. kicad_sch_api-0.4.1.dist-info/RECORD +0 -87
  64. kicad_sch_api-0.4.1.dist-info/entry_points.txt +0 -2
  65. {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
  66. {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/licenses/LICENSE +0 -0
@@ -1,8 +1,10 @@
1
1
  """
2
- Geometry module for KiCad schematic symbol bounding box calculations.
2
+ Geometry module for KiCad schematic operations.
3
3
 
4
- This module provides accurate bounding box calculations for KiCad symbols,
5
- including font metrics and symbol geometry analysis.
4
+ This module provides:
5
+ - Accurate bounding box calculations for KiCad symbols
6
+ - Orthogonal (Manhattan) routing for wire connections
7
+ - Font metrics and symbol geometry analysis
6
8
 
7
9
  Migrated from circuit-synth to kicad-sch-api for better architectural separation.
8
10
  """
@@ -14,6 +16,12 @@ from .font_metrics import (
14
16
  DEFAULT_PIN_TEXT_WIDTH_RATIO,
15
17
  DEFAULT_TEXT_HEIGHT,
16
18
  )
19
+ from .routing import (
20
+ CornerDirection,
21
+ RoutingResult,
22
+ create_orthogonal_routing,
23
+ validate_routing_result,
24
+ )
17
25
  from .symbol_bbox import SymbolBoundingBoxCalculator
18
26
 
19
27
  __all__ = [
@@ -23,4 +31,8 @@ __all__ = [
23
31
  "DEFAULT_PIN_NAME_OFFSET",
24
32
  "DEFAULT_PIN_NUMBER_SIZE",
25
33
  "DEFAULT_PIN_TEXT_WIDTH_RATIO",
34
+ "CornerDirection",
35
+ "RoutingResult",
36
+ "create_orthogonal_routing",
37
+ "validate_routing_result",
26
38
  ]
@@ -0,0 +1,211 @@
1
+ """
2
+ Orthogonal routing algorithms for automatic wire routing between points.
3
+
4
+ This module provides functions for creating orthogonal (Manhattan) wire routes
5
+ between component pins, with support for direct routing when points are aligned
6
+ and L-shaped routing when they are not.
7
+
8
+ CRITICAL: KiCAD Y-axis is INVERTED (+Y is DOWN)
9
+ - Lower Y values = visually HIGHER on screen (top)
10
+ - Higher Y values = visually LOWER on screen (bottom)
11
+ """
12
+
13
+ from dataclasses import dataclass
14
+ from enum import Enum
15
+ from typing import List, Optional, Tuple
16
+
17
+ from kicad_sch_api.core.types import Point
18
+
19
+
20
+ class CornerDirection(Enum):
21
+ """Direction preference for L-shaped routing corner."""
22
+
23
+ AUTO = "auto" # Automatic selection based on distance heuristic
24
+ HORIZONTAL_FIRST = "horizontal_first" # Route horizontally, then vertically
25
+ VERTICAL_FIRST = "vertical_first" # Route vertically, then horizontally
26
+
27
+
28
+ @dataclass
29
+ class RoutingResult:
30
+ """
31
+ Result of orthogonal routing calculation.
32
+
33
+ Attributes:
34
+ segments: List of wire segments as (start, end) point tuples
35
+ corner: Corner junction point (None if direct routing)
36
+ is_direct: True if routing is a single straight line
37
+ """
38
+
39
+ segments: List[Tuple[Point, Point]]
40
+ corner: Optional[Point]
41
+ is_direct: bool
42
+
43
+
44
+ def create_orthogonal_routing(
45
+ from_pos: Point,
46
+ to_pos: Point,
47
+ corner_direction: CornerDirection = CornerDirection.AUTO
48
+ ) -> RoutingResult:
49
+ """
50
+ Create orthogonal (Manhattan) routing between two points.
51
+
52
+ Generates either direct routing (when points are aligned on same axis)
53
+ or L-shaped routing (when points require a corner).
54
+
55
+ CRITICAL: Remember KiCAD Y-axis is INVERTED:
56
+ - Lower Y values = visually HIGHER (top of screen)
57
+ - Higher Y values = visually LOWER (bottom of screen)
58
+
59
+ Args:
60
+ from_pos: Starting point
61
+ to_pos: Ending point
62
+ corner_direction: Direction preference for L-shaped corner
63
+ - AUTO: Choose based on distance heuristic (horizontal if dx >= dy)
64
+ - HORIZONTAL_FIRST: Route horizontally, then vertically
65
+ - VERTICAL_FIRST: Route vertically, then horizontally
66
+
67
+ Returns:
68
+ RoutingResult with segments list, corner point, and direct flag
69
+
70
+ Examples:
71
+ >>> # Direct horizontal routing (aligned on Y axis)
72
+ >>> result = create_orthogonal_routing(
73
+ ... Point(100, 100),
74
+ ... Point(150, 100)
75
+ ... )
76
+ >>> result.is_direct
77
+ True
78
+ >>> len(result.segments)
79
+ 1
80
+
81
+ >>> # L-shaped routing (not aligned)
82
+ >>> result = create_orthogonal_routing(
83
+ ... Point(100, 100),
84
+ ... Point(150, 125),
85
+ ... corner_direction=CornerDirection.HORIZONTAL_FIRST
86
+ ... )
87
+ >>> result.is_direct
88
+ False
89
+ >>> len(result.segments)
90
+ 2
91
+ >>> result.corner
92
+ Point(x=150.0, y=100.0)
93
+ """
94
+ # Check if points are aligned on same axis (direct routing possible)
95
+ if from_pos.x == to_pos.x or from_pos.y == to_pos.y:
96
+ # Direct line - no corner needed
97
+ return RoutingResult(
98
+ segments=[(from_pos, to_pos)],
99
+ corner=None,
100
+ is_direct=True
101
+ )
102
+
103
+ # Points are not aligned - need L-shaped routing with corner
104
+ corner = _calculate_corner_point(from_pos, to_pos, corner_direction)
105
+
106
+ return RoutingResult(
107
+ segments=[
108
+ (from_pos, corner),
109
+ (corner, to_pos)
110
+ ],
111
+ corner=corner,
112
+ is_direct=False
113
+ )
114
+
115
+
116
+ def _calculate_corner_point(
117
+ from_pos: Point,
118
+ to_pos: Point,
119
+ corner_direction: CornerDirection
120
+ ) -> Point:
121
+ """
122
+ Calculate the corner point for L-shaped routing.
123
+
124
+ Args:
125
+ from_pos: Starting point
126
+ to_pos: Ending point
127
+ corner_direction: Direction preference for corner
128
+
129
+ Returns:
130
+ Corner point position
131
+ """
132
+ if corner_direction == CornerDirection.HORIZONTAL_FIRST:
133
+ # Route horizontally first, then vertically
134
+ # Corner is at destination X, source Y
135
+ return Point(to_pos.x, from_pos.y)
136
+
137
+ elif corner_direction == CornerDirection.VERTICAL_FIRST:
138
+ # Route vertically first, then horizontally
139
+ # Corner is at source X, destination Y
140
+ return Point(from_pos.x, to_pos.y)
141
+
142
+ else: # AUTO
143
+ # Heuristic: prefer horizontal first if horizontal distance >= vertical distance
144
+ dx = abs(to_pos.x - from_pos.x)
145
+ dy = abs(to_pos.y - from_pos.y)
146
+
147
+ if dx >= dy:
148
+ # Horizontal distance is greater - route horizontally first
149
+ return Point(to_pos.x, from_pos.y)
150
+ else:
151
+ # Vertical distance is greater - route vertically first
152
+ return Point(from_pos.x, to_pos.y)
153
+
154
+
155
+ def validate_routing_result(result: RoutingResult) -> bool:
156
+ """
157
+ Validate that routing result is correct.
158
+
159
+ Checks:
160
+ - All segments are orthogonal (horizontal or vertical)
161
+ - Segments connect end-to-end
162
+ - Corner point matches segment endpoints if present
163
+
164
+ Args:
165
+ result: Routing result to validate
166
+
167
+ Returns:
168
+ True if routing is valid
169
+
170
+ Raises:
171
+ ValueError: If routing is invalid
172
+ """
173
+ if not result.segments:
174
+ raise ValueError("Routing must have at least one segment")
175
+
176
+ for start, end in result.segments:
177
+ # Check orthogonality - each segment must be horizontal OR vertical
178
+ if start.x != end.x and start.y != end.y:
179
+ raise ValueError(
180
+ f"Segment ({start}, {end}) is not orthogonal - "
181
+ f"must be horizontal (same Y) or vertical (same X)"
182
+ )
183
+
184
+ # Check segment connectivity - segments must connect end-to-end
185
+ for i in range(len(result.segments) - 1):
186
+ current_end = result.segments[i][1]
187
+ next_start = result.segments[i + 1][0]
188
+
189
+ if current_end.x != next_start.x or current_end.y != next_start.y:
190
+ raise ValueError(
191
+ f"Segments not connected: segment {i} ends at {current_end}, "
192
+ f"segment {i+1} starts at {next_start}"
193
+ )
194
+
195
+ # Check corner consistency
196
+ if result.corner is not None:
197
+ if len(result.segments) < 2:
198
+ raise ValueError("Corner specified but less than 2 segments present")
199
+
200
+ # Corner should be the endpoint of first segment and startpoint of second
201
+ first_end = result.segments[0][1]
202
+ second_start = result.segments[1][0]
203
+
204
+ if (result.corner.x != first_end.x or result.corner.y != first_end.y or
205
+ result.corner.x != second_start.x or result.corner.y != second_start.y):
206
+ raise ValueError(
207
+ f"Corner point {result.corner} does not match segment endpoints: "
208
+ f"first segment ends at {first_end}, second starts at {second_start}"
209
+ )
210
+
211
+ return True
@@ -33,6 +33,8 @@ class LabelParser(BaseElementParser):
33
33
  "position": {"x": 0, "y": 0},
34
34
  "rotation": 0,
35
35
  "size": config.defaults.font_size,
36
+ "justify_h": "left",
37
+ "justify_v": "bottom",
36
38
  "uuid": None,
37
39
  }
38
40
 
@@ -50,13 +52,29 @@ class LabelParser(BaseElementParser):
50
52
  label_data["rotation"] = float(elem[3])
51
53
 
52
54
  elif elem_type == "effects":
53
- # Parse effects for font size: (effects (font (size x y)) ...)
55
+ # Parse effects for font size and justification: (effects (font (size x y)) (justify left bottom))
54
56
  for effect_elem in elem[1:]:
55
- if isinstance(effect_elem, list) and str(effect_elem[0]) == "font":
56
- for font_elem in effect_elem[1:]:
57
- if isinstance(font_elem, list) and str(font_elem[0]) == "size":
58
- if len(font_elem) >= 2:
59
- label_data["size"] = float(font_elem[1])
57
+ if isinstance(effect_elem, list):
58
+ effect_type = (
59
+ str(effect_elem[0])
60
+ if isinstance(effect_elem[0], sexpdata.Symbol)
61
+ else None
62
+ )
63
+
64
+ if effect_type == "font":
65
+ # Parse font size
66
+ for font_elem in effect_elem[1:]:
67
+ if isinstance(font_elem, list) and str(font_elem[0]) == "size":
68
+ if len(font_elem) >= 2:
69
+ label_data["size"] = float(font_elem[1])
70
+
71
+ elif effect_type == "justify":
72
+ # Parse justification (e.g., "left bottom", "right top")
73
+ # Format: (justify left bottom) or (justify right)
74
+ if len(effect_elem) >= 2:
75
+ label_data["justify_h"] = str(effect_elem[1])
76
+ if len(effect_elem) >= 3:
77
+ label_data["justify_v"] = str(effect_elem[2])
60
78
 
61
79
  elif elem_type == "uuid":
62
80
  label_data["uuid"] = str(elem[1]) if len(elem) > 1 else None
@@ -143,13 +161,17 @@ class LabelParser(BaseElementParser):
143
161
 
144
162
  sexp.append([sexpdata.Symbol("at"), x, y, rotation])
145
163
 
146
- # Add effects (font properties)
164
+ # Add effects (font properties and justification)
147
165
  size = label_data.get("size", config.defaults.font_size)
148
166
  effects = [sexpdata.Symbol("effects")]
149
167
  font = [sexpdata.Symbol("font"), [sexpdata.Symbol("size"), size, size]]
150
168
  effects.append(font)
169
+
170
+ # Use justification from data, defaulting to "left bottom"
171
+ justify_h = label_data.get("justify_h", "left")
172
+ justify_v = label_data.get("justify_v", "bottom")
151
173
  effects.append(
152
- [sexpdata.Symbol("justify"), sexpdata.Symbol("left"), sexpdata.Symbol("bottom")]
174
+ [sexpdata.Symbol("justify"), sexpdata.Symbol(justify_h), sexpdata.Symbol(justify_v)]
153
175
  )
154
176
  sexp.append(effects)
155
177