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.
- kicad_sch_api/__init__.py +67 -2
- kicad_sch_api/cli/kicad_to_python.py +169 -0
- kicad_sch_api/collections/__init__.py +23 -8
- kicad_sch_api/collections/base.py +369 -59
- kicad_sch_api/collections/components.py +1376 -187
- kicad_sch_api/collections/junctions.py +129 -289
- kicad_sch_api/collections/labels.py +391 -287
- kicad_sch_api/collections/wires.py +202 -316
- kicad_sch_api/core/__init__.py +37 -2
- kicad_sch_api/core/component_bounds.py +34 -12
- kicad_sch_api/core/components.py +146 -7
- kicad_sch_api/core/config.py +25 -12
- kicad_sch_api/core/connectivity.py +692 -0
- kicad_sch_api/core/exceptions.py +175 -0
- kicad_sch_api/core/factories/element_factory.py +3 -1
- kicad_sch_api/core/formatter.py +24 -7
- kicad_sch_api/core/geometry.py +94 -5
- kicad_sch_api/core/managers/__init__.py +4 -0
- kicad_sch_api/core/managers/base.py +76 -0
- kicad_sch_api/core/managers/file_io.py +3 -1
- kicad_sch_api/core/managers/format_sync.py +3 -2
- kicad_sch_api/core/managers/graphics.py +3 -2
- kicad_sch_api/core/managers/hierarchy.py +661 -0
- kicad_sch_api/core/managers/metadata.py +4 -2
- kicad_sch_api/core/managers/sheet.py +52 -14
- kicad_sch_api/core/managers/text_elements.py +3 -2
- kicad_sch_api/core/managers/validation.py +3 -2
- kicad_sch_api/core/managers/wire.py +112 -54
- kicad_sch_api/core/parsing_utils.py +63 -0
- kicad_sch_api/core/pin_utils.py +103 -9
- kicad_sch_api/core/schematic.py +343 -29
- kicad_sch_api/core/types.py +79 -7
- kicad_sch_api/exporters/__init__.py +10 -0
- kicad_sch_api/exporters/python_generator.py +610 -0
- kicad_sch_api/exporters/templates/default.py.jinja2 +65 -0
- kicad_sch_api/geometry/__init__.py +15 -3
- kicad_sch_api/geometry/routing.py +211 -0
- kicad_sch_api/parsers/elements/label_parser.py +30 -8
- kicad_sch_api/parsers/elements/symbol_parser.py +255 -83
- kicad_sch_api/utils/logging.py +555 -0
- kicad_sch_api/utils/logging_decorators.py +587 -0
- kicad_sch_api/utils/validation.py +16 -22
- kicad_sch_api/wrappers/__init__.py +14 -0
- kicad_sch_api/wrappers/base.py +89 -0
- kicad_sch_api/wrappers/wire.py +198 -0
- kicad_sch_api-0.5.1.dist-info/METADATA +540 -0
- kicad_sch_api-0.5.1.dist-info/RECORD +114 -0
- kicad_sch_api-0.5.1.dist-info/entry_points.txt +4 -0
- {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/top_level.txt +1 -0
- mcp_server/__init__.py +34 -0
- mcp_server/example_logging_integration.py +506 -0
- mcp_server/models.py +252 -0
- mcp_server/server.py +357 -0
- mcp_server/tools/__init__.py +32 -0
- mcp_server/tools/component_tools.py +516 -0
- mcp_server/tools/connectivity_tools.py +532 -0
- mcp_server/tools/consolidated_tools.py +1216 -0
- mcp_server/tools/pin_discovery.py +333 -0
- mcp_server/utils/__init__.py +38 -0
- mcp_server/utils/logging.py +127 -0
- mcp_server/utils.py +36 -0
- kicad_sch_api-0.4.1.dist-info/METADATA +0 -491
- kicad_sch_api-0.4.1.dist-info/RECORD +0 -87
- kicad_sch_api-0.4.1.dist-info/entry_points.txt +0 -2
- {kicad_sch_api-0.4.1.dist-info → kicad_sch_api-0.5.1.dist-info}/WHEEL +0 -0
- {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
|
|
2
|
+
Geometry module for KiCad schematic operations.
|
|
3
3
|
|
|
4
|
-
This module provides
|
|
5
|
-
|
|
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)
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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(
|
|
174
|
+
[sexpdata.Symbol("justify"), sexpdata.Symbol(justify_h), sexpdata.Symbol(justify_v)]
|
|
153
175
|
)
|
|
154
176
|
sexp.append(effects)
|
|
155
177
|
|