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
|
@@ -0,0 +1,532 @@
|
|
|
1
|
+
"""
|
|
2
|
+
MCP tools for schematic connectivity.
|
|
3
|
+
|
|
4
|
+
Provides MCP-compatible tools for adding wires, labels, junctions, and managing
|
|
5
|
+
circuit connections in KiCAD schematics.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
from typing import TYPE_CHECKING, Optional, Tuple, List
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from fastmcp import Context
|
|
13
|
+
else:
|
|
14
|
+
try:
|
|
15
|
+
from fastmcp import Context
|
|
16
|
+
except ImportError:
|
|
17
|
+
Context = None # type: ignore
|
|
18
|
+
|
|
19
|
+
import kicad_sch_api as ksa
|
|
20
|
+
from kicad_sch_api.core.types import WireType
|
|
21
|
+
from kicad_sch_api.geometry import create_orthogonal_routing, CornerDirection
|
|
22
|
+
from mcp_server.models import PointModel
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Import global schematic state from pin_discovery
|
|
29
|
+
from mcp_server.tools.pin_discovery import get_current_schematic
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
async def add_wire(
|
|
33
|
+
start: Tuple[float, float],
|
|
34
|
+
end: Tuple[float, float],
|
|
35
|
+
ctx: Optional[Context] = None,
|
|
36
|
+
) -> dict:
|
|
37
|
+
"""
|
|
38
|
+
Add a wire between two points.
|
|
39
|
+
|
|
40
|
+
Creates a wire connection between start and end points. Wires are used to
|
|
41
|
+
connect component pins and establish electrical nets.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
start: Start point as (x, y) tuple in mm
|
|
45
|
+
end: End point as (x, y) tuple in mm
|
|
46
|
+
ctx: MCP context for progress reporting (optional)
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Dictionary with success status and wire information
|
|
50
|
+
|
|
51
|
+
Examples:
|
|
52
|
+
>>> # Connect two points horizontally
|
|
53
|
+
>>> result = await add_wire(
|
|
54
|
+
... start=(100.0, 100.0),
|
|
55
|
+
... end=(150.0, 100.0)
|
|
56
|
+
... )
|
|
57
|
+
|
|
58
|
+
>>> # Vertical wire
|
|
59
|
+
>>> result = await add_wire(
|
|
60
|
+
... start=(100.0, 100.0),
|
|
61
|
+
... end=(100.0, 150.0)
|
|
62
|
+
... )
|
|
63
|
+
"""
|
|
64
|
+
logger.info(f"[MCP] add_wire called: start={start}, end={end}")
|
|
65
|
+
|
|
66
|
+
if ctx:
|
|
67
|
+
await ctx.report_progress(0, 100, "Adding wire")
|
|
68
|
+
|
|
69
|
+
# Check if schematic is loaded
|
|
70
|
+
schematic = get_current_schematic()
|
|
71
|
+
if schematic is None:
|
|
72
|
+
logger.error("[MCP] No schematic loaded")
|
|
73
|
+
return {
|
|
74
|
+
"success": False,
|
|
75
|
+
"error": "NO_SCHEMATIC_LOADED",
|
|
76
|
+
"message": "No schematic is currently loaded",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
if ctx:
|
|
81
|
+
await ctx.report_progress(50, 100, "Creating wire connection")
|
|
82
|
+
|
|
83
|
+
# Add wire using library API
|
|
84
|
+
wire_uuid = schematic.wires.add(
|
|
85
|
+
start=start,
|
|
86
|
+
end=end,
|
|
87
|
+
wire_type=WireType.WIRE,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
if ctx:
|
|
91
|
+
await ctx.report_progress(100, 100, "Complete: wire added")
|
|
92
|
+
|
|
93
|
+
logger.info(f"[MCP] Successfully added wire {wire_uuid}")
|
|
94
|
+
return {
|
|
95
|
+
"success": True,
|
|
96
|
+
"uuid": wire_uuid,
|
|
97
|
+
"start": {"x": start[0], "y": start[1]},
|
|
98
|
+
"end": {"x": end[0], "y": end[1]},
|
|
99
|
+
"message": f"Added wire from ({start[0]}, {start[1]}) to ({end[0]}, {end[1]})",
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.error(f"[MCP] Unexpected error: {e}", exc_info=True)
|
|
104
|
+
return {
|
|
105
|
+
"success": False,
|
|
106
|
+
"error": "INTERNAL_ERROR",
|
|
107
|
+
"message": f"Unexpected error adding wire: {str(e)}",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
async def add_label(
|
|
112
|
+
text: str,
|
|
113
|
+
position: Tuple[float, float],
|
|
114
|
+
rotation: float = 0.0,
|
|
115
|
+
size: float = 1.27,
|
|
116
|
+
ctx: Optional[Context] = None,
|
|
117
|
+
) -> dict:
|
|
118
|
+
"""
|
|
119
|
+
Add a net label to the schematic.
|
|
120
|
+
|
|
121
|
+
Net labels are used to name electrical nets and establish connections
|
|
122
|
+
between non-physically connected wires with the same label name.
|
|
123
|
+
|
|
124
|
+
Args:
|
|
125
|
+
text: Label text (net name)
|
|
126
|
+
position: Label position as (x, y) tuple in mm
|
|
127
|
+
rotation: Label rotation in degrees (0, 90, 180, 270), defaults to 0
|
|
128
|
+
size: Text size in mm, defaults to 1.27 (KiCAD standard)
|
|
129
|
+
ctx: MCP context for progress reporting (optional)
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Dictionary with success status and label information
|
|
133
|
+
|
|
134
|
+
Examples:
|
|
135
|
+
>>> # Add VCC label
|
|
136
|
+
>>> result = await add_label(
|
|
137
|
+
... text="VCC",
|
|
138
|
+
... position=(100.0, 100.0)
|
|
139
|
+
... )
|
|
140
|
+
|
|
141
|
+
>>> # Add label with rotation
|
|
142
|
+
>>> result = await add_label(
|
|
143
|
+
... text="GND",
|
|
144
|
+
... position=(150.0, 100.0),
|
|
145
|
+
... rotation=90.0
|
|
146
|
+
... )
|
|
147
|
+
"""
|
|
148
|
+
logger.info(f"[MCP] add_label called: text={text}, position={position}")
|
|
149
|
+
|
|
150
|
+
if ctx:
|
|
151
|
+
await ctx.report_progress(0, 100, f"Adding label {text}")
|
|
152
|
+
|
|
153
|
+
# Check if schematic is loaded
|
|
154
|
+
schematic = get_current_schematic()
|
|
155
|
+
if schematic is None:
|
|
156
|
+
logger.error("[MCP] No schematic loaded")
|
|
157
|
+
return {
|
|
158
|
+
"success": False,
|
|
159
|
+
"error": "NO_SCHEMATIC_LOADED",
|
|
160
|
+
"message": "No schematic is currently loaded",
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
if ctx:
|
|
165
|
+
await ctx.report_progress(25, 100, "Validating label parameters")
|
|
166
|
+
|
|
167
|
+
# Validate rotation (KiCAD supports 0, 90, 180, 270)
|
|
168
|
+
if rotation not in [0.0, 90.0, 180.0, 270.0]:
|
|
169
|
+
logger.warning(f"[MCP] Invalid rotation {rotation}")
|
|
170
|
+
return {
|
|
171
|
+
"success": False,
|
|
172
|
+
"error": "VALIDATION_ERROR",
|
|
173
|
+
"message": f"Rotation must be 0, 90, 180, or 270 degrees, got {rotation}",
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if ctx:
|
|
177
|
+
await ctx.report_progress(50, 100, "Creating label")
|
|
178
|
+
|
|
179
|
+
# Add label using library API
|
|
180
|
+
label = schematic.labels.add(
|
|
181
|
+
text=text,
|
|
182
|
+
position=position,
|
|
183
|
+
rotation=rotation,
|
|
184
|
+
size=size,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if ctx:
|
|
188
|
+
await ctx.report_progress(100, 100, f"Complete: label {text} added")
|
|
189
|
+
|
|
190
|
+
logger.info(f"[MCP] Successfully added label {text}")
|
|
191
|
+
return {
|
|
192
|
+
"success": True,
|
|
193
|
+
"uuid": str(label.uuid),
|
|
194
|
+
"text": text,
|
|
195
|
+
"position": {"x": position[0], "y": position[1]},
|
|
196
|
+
"rotation": rotation,
|
|
197
|
+
"size": size,
|
|
198
|
+
"message": f"Added label '{text}' at ({position[0]}, {position[1]})",
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
except Exception as e:
|
|
202
|
+
logger.error(f"[MCP] Unexpected error: {e}", exc_info=True)
|
|
203
|
+
return {
|
|
204
|
+
"success": False,
|
|
205
|
+
"error": "INTERNAL_ERROR",
|
|
206
|
+
"message": f"Unexpected error adding label: {str(e)}",
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
async def add_junction(
|
|
211
|
+
position: Tuple[float, float],
|
|
212
|
+
diameter: float = 0.0,
|
|
213
|
+
ctx: Optional[Context] = None,
|
|
214
|
+
) -> dict:
|
|
215
|
+
"""
|
|
216
|
+
Add a wire junction at the specified position.
|
|
217
|
+
|
|
218
|
+
Junctions indicate T-connections where three or more wires meet. They are
|
|
219
|
+
required in KiCAD when a wire branches into multiple paths.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
position: Junction position as (x, y) tuple in mm
|
|
223
|
+
diameter: Junction diameter in mm (0 = use KiCAD default)
|
|
224
|
+
ctx: MCP context for progress reporting (optional)
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
Dictionary with success status and junction information
|
|
228
|
+
|
|
229
|
+
Examples:
|
|
230
|
+
>>> # Add junction at T-connection
|
|
231
|
+
>>> result = await add_junction(
|
|
232
|
+
... position=(100.0, 100.0)
|
|
233
|
+
... )
|
|
234
|
+
|
|
235
|
+
>>> # Add junction with custom diameter
|
|
236
|
+
>>> result = await add_junction(
|
|
237
|
+
... position=(150.0, 100.0),
|
|
238
|
+
... diameter=0.8
|
|
239
|
+
... )
|
|
240
|
+
"""
|
|
241
|
+
logger.info(f"[MCP] add_junction called: position={position}")
|
|
242
|
+
|
|
243
|
+
if ctx:
|
|
244
|
+
await ctx.report_progress(0, 100, "Adding junction")
|
|
245
|
+
|
|
246
|
+
# Check if schematic is loaded
|
|
247
|
+
schematic = get_current_schematic()
|
|
248
|
+
if schematic is None:
|
|
249
|
+
logger.error("[MCP] No schematic loaded")
|
|
250
|
+
return {
|
|
251
|
+
"success": False,
|
|
252
|
+
"error": "NO_SCHEMATIC_LOADED",
|
|
253
|
+
"message": "No schematic is currently loaded",
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
try:
|
|
257
|
+
if ctx:
|
|
258
|
+
await ctx.report_progress(50, 100, "Creating junction")
|
|
259
|
+
|
|
260
|
+
# Add junction using library API
|
|
261
|
+
junction_uuid = schematic.junctions.add(
|
|
262
|
+
position=position,
|
|
263
|
+
diameter=diameter,
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
if ctx:
|
|
267
|
+
await ctx.report_progress(100, 100, "Complete: junction added")
|
|
268
|
+
|
|
269
|
+
logger.info(f"[MCP] Successfully added junction {junction_uuid}")
|
|
270
|
+
return {
|
|
271
|
+
"success": True,
|
|
272
|
+
"uuid": junction_uuid,
|
|
273
|
+
"position": {"x": position[0], "y": position[1]},
|
|
274
|
+
"diameter": diameter,
|
|
275
|
+
"message": f"Added junction at ({position[0]}, {position[1]})",
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.error(f"[MCP] Unexpected error: {e}", exc_info=True)
|
|
280
|
+
return {
|
|
281
|
+
"success": False,
|
|
282
|
+
"error": "INTERNAL_ERROR",
|
|
283
|
+
"message": f"Unexpected error adding junction: {str(e)}",
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
async def connect_components(
|
|
288
|
+
from_component: str,
|
|
289
|
+
from_pin: str,
|
|
290
|
+
to_component: str,
|
|
291
|
+
to_pin: str,
|
|
292
|
+
corner_direction: str = "auto",
|
|
293
|
+
add_label: Optional[str] = None,
|
|
294
|
+
add_junction: bool = True,
|
|
295
|
+
ctx: Optional[Context] = None,
|
|
296
|
+
) -> dict:
|
|
297
|
+
"""
|
|
298
|
+
Connect two component pins with automatic orthogonal routing.
|
|
299
|
+
|
|
300
|
+
Uses Manhattan-style (orthogonal) routing to create L-shaped or direct wire
|
|
301
|
+
paths between component pins. Automatically calculates pin positions and
|
|
302
|
+
generates appropriate wire segments with optional junctions and labels.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
from_component: Source component reference (e.g., "R1")
|
|
306
|
+
from_pin: Source pin number (e.g., "2")
|
|
307
|
+
to_component: Destination component reference (e.g., "R2")
|
|
308
|
+
to_pin: Destination pin number (e.g., "1")
|
|
309
|
+
corner_direction: Routing direction preference:
|
|
310
|
+
- "auto": Smart heuristic (horizontal if dx >= dy, else vertical)
|
|
311
|
+
- "horizontal_first": Route horizontally then vertically
|
|
312
|
+
- "vertical_first": Route vertically then horizontally
|
|
313
|
+
add_label: Optional net label text to add at start of routing
|
|
314
|
+
add_junction: Whether to add junction at L-shaped corners (default: True)
|
|
315
|
+
ctx: MCP context for progress reporting (optional)
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Dictionary with success status and connection information including:
|
|
319
|
+
- from/to component and pin details with positions
|
|
320
|
+
- routing type (direct or L-shaped)
|
|
321
|
+
- segments list with start/end positions
|
|
322
|
+
- wire_uuids list
|
|
323
|
+
- junction_uuid (if junction was added)
|
|
324
|
+
- label_uuid (if label was added)
|
|
325
|
+
|
|
326
|
+
Examples:
|
|
327
|
+
>>> # Simple connection with auto routing
|
|
328
|
+
>>> result = await connect_components("R1", "2", "R2", "1")
|
|
329
|
+
|
|
330
|
+
>>> # With label and horizontal-first routing
|
|
331
|
+
>>> result = await connect_components(
|
|
332
|
+
... "R1", "2", "R2", "1",
|
|
333
|
+
... corner_direction="horizontal_first",
|
|
334
|
+
... add_label="VCC"
|
|
335
|
+
... )
|
|
336
|
+
|
|
337
|
+
>>> # Vertical-first without junction
|
|
338
|
+
>>> result = await connect_components(
|
|
339
|
+
... "R1", "1", "C1", "1",
|
|
340
|
+
... corner_direction="vertical_first",
|
|
341
|
+
... add_junction=False
|
|
342
|
+
... )
|
|
343
|
+
"""
|
|
344
|
+
logger.info(
|
|
345
|
+
f"[MCP] connect_components called: {from_component}:{from_pin} -> "
|
|
346
|
+
f"{to_component}:{to_pin}, direction={corner_direction}"
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
if ctx:
|
|
350
|
+
await ctx.report_progress(0, 100, "Connecting components")
|
|
351
|
+
|
|
352
|
+
# Check if schematic is loaded
|
|
353
|
+
schematic = get_current_schematic()
|
|
354
|
+
if schematic is None:
|
|
355
|
+
logger.error("[MCP] No schematic loaded")
|
|
356
|
+
return {
|
|
357
|
+
"success": False,
|
|
358
|
+
"error": "NO_SCHEMATIC_LOADED",
|
|
359
|
+
"message": "No schematic is currently loaded",
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
try:
|
|
363
|
+
if ctx:
|
|
364
|
+
await ctx.report_progress(10, 100, "Looking up components")
|
|
365
|
+
|
|
366
|
+
# Get components
|
|
367
|
+
try:
|
|
368
|
+
from_comp = schematic.components.get(from_component)
|
|
369
|
+
to_comp = schematic.components.get(to_component)
|
|
370
|
+
except KeyError as e:
|
|
371
|
+
logger.error(f"[MCP] Component not found: {e}")
|
|
372
|
+
return {
|
|
373
|
+
"success": False,
|
|
374
|
+
"error": "COMPONENT_NOT_FOUND",
|
|
375
|
+
"message": f"Component not found: {str(e)}",
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if ctx:
|
|
379
|
+
await ctx.report_progress(30, 100, "Getting pin positions")
|
|
380
|
+
|
|
381
|
+
# Get pin positions
|
|
382
|
+
from_pins = schematic.components.get_pins_info(from_component)
|
|
383
|
+
to_pins = schematic.components.get_pins_info(to_component)
|
|
384
|
+
|
|
385
|
+
# Check if pin info was successfully retrieved
|
|
386
|
+
if from_pins is None:
|
|
387
|
+
logger.error(f"[MCP] Could not get pins for {from_component}")
|
|
388
|
+
return {
|
|
389
|
+
"success": False,
|
|
390
|
+
"error": "PIN_INFO_ERROR",
|
|
391
|
+
"message": f"Could not get pin information for component {from_component}",
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
if to_pins is None:
|
|
395
|
+
logger.error(f"[MCP] Could not get pins for {to_component}")
|
|
396
|
+
return {
|
|
397
|
+
"success": False,
|
|
398
|
+
"error": "PIN_INFO_ERROR",
|
|
399
|
+
"message": f"Could not get pin information for component {to_component}",
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
from_pin_obj = next((p for p in from_pins if p.number == from_pin), None)
|
|
403
|
+
to_pin_obj = next((p for p in to_pins if p.number == to_pin), None)
|
|
404
|
+
|
|
405
|
+
if not from_pin_obj:
|
|
406
|
+
logger.error(f"[MCP] Pin {from_pin} not found on {from_component}")
|
|
407
|
+
return {
|
|
408
|
+
"success": False,
|
|
409
|
+
"error": "PIN_NOT_FOUND",
|
|
410
|
+
"message": f"Pin {from_pin} not found on component {from_component}",
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if not to_pin_obj:
|
|
414
|
+
logger.error(f"[MCP] Pin {to_pin} not found on {to_component}")
|
|
415
|
+
return {
|
|
416
|
+
"success": False,
|
|
417
|
+
"error": "PIN_NOT_FOUND",
|
|
418
|
+
"message": f"Pin {to_pin} not found on component {to_component}",
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if ctx:
|
|
422
|
+
await ctx.report_progress(50, 100, "Calculating routing")
|
|
423
|
+
|
|
424
|
+
# Parse corner direction
|
|
425
|
+
try:
|
|
426
|
+
if corner_direction.upper() == "AUTO":
|
|
427
|
+
direction = CornerDirection.AUTO
|
|
428
|
+
elif corner_direction.upper() == "HORIZONTAL_FIRST":
|
|
429
|
+
direction = CornerDirection.HORIZONTAL_FIRST
|
|
430
|
+
elif corner_direction.upper() == "VERTICAL_FIRST":
|
|
431
|
+
direction = CornerDirection.VERTICAL_FIRST
|
|
432
|
+
else:
|
|
433
|
+
logger.warning(f"[MCP] Invalid corner direction: {corner_direction}, using AUTO")
|
|
434
|
+
direction = CornerDirection.AUTO
|
|
435
|
+
except Exception:
|
|
436
|
+
logger.warning(f"[MCP] Error parsing corner direction, using AUTO")
|
|
437
|
+
direction = CornerDirection.AUTO
|
|
438
|
+
|
|
439
|
+
# Create orthogonal routing
|
|
440
|
+
routing_result = create_orthogonal_routing(
|
|
441
|
+
from_pin_obj.position,
|
|
442
|
+
to_pin_obj.position,
|
|
443
|
+
corner_direction=direction
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
logger.info(
|
|
447
|
+
f"[MCP] Routing calculated: {len(routing_result.segments)} segments, "
|
|
448
|
+
f"direct={routing_result.is_direct}, corner={routing_result.corner}"
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
if ctx:
|
|
452
|
+
await ctx.report_progress(70, 100, "Adding wires")
|
|
453
|
+
|
|
454
|
+
# Add wire segments
|
|
455
|
+
wire_uuids = []
|
|
456
|
+
for start, end in routing_result.segments:
|
|
457
|
+
wire_uuid = schematic.wires.add(start=start, end=end)
|
|
458
|
+
wire_uuids.append(wire_uuid)
|
|
459
|
+
|
|
460
|
+
# Add junction at corner if requested and routing is L-shaped
|
|
461
|
+
junction_uuid = None
|
|
462
|
+
if add_junction and routing_result.corner and not routing_result.is_direct:
|
|
463
|
+
if ctx:
|
|
464
|
+
await ctx.report_progress(80, 100, "Adding junction at corner")
|
|
465
|
+
|
|
466
|
+
junction_uuid = schematic.junctions.add(
|
|
467
|
+
position=(routing_result.corner.x, routing_result.corner.y)
|
|
468
|
+
)
|
|
469
|
+
logger.info(f"[MCP] Added junction at corner: {junction_uuid}")
|
|
470
|
+
|
|
471
|
+
# Add label if requested
|
|
472
|
+
label_uuid = None
|
|
473
|
+
if add_label:
|
|
474
|
+
if ctx:
|
|
475
|
+
await ctx.report_progress(90, 100, f"Adding label {add_label}")
|
|
476
|
+
|
|
477
|
+
label = schematic.labels.add(
|
|
478
|
+
text=add_label,
|
|
479
|
+
position=(from_pin_obj.position.x, from_pin_obj.position.y),
|
|
480
|
+
)
|
|
481
|
+
label_uuid = str(label.uuid)
|
|
482
|
+
logger.info(f"[MCP] Added label '{add_label}': {label_uuid}")
|
|
483
|
+
|
|
484
|
+
if ctx:
|
|
485
|
+
await ctx.report_progress(100, 100, "Complete: components connected")
|
|
486
|
+
|
|
487
|
+
logger.info(
|
|
488
|
+
f"[MCP] Successfully connected {from_component}:{from_pin} to "
|
|
489
|
+
f"{to_component}:{to_pin} with {len(wire_uuids)} wires"
|
|
490
|
+
)
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
"success": True,
|
|
494
|
+
"from": {
|
|
495
|
+
"component": from_component,
|
|
496
|
+
"pin": from_pin,
|
|
497
|
+
"position": {"x": from_pin_obj.position.x, "y": from_pin_obj.position.y}
|
|
498
|
+
},
|
|
499
|
+
"to": {
|
|
500
|
+
"component": to_component,
|
|
501
|
+
"pin": to_pin,
|
|
502
|
+
"position": {"x": to_pin_obj.position.x, "y": to_pin_obj.position.y}
|
|
503
|
+
},
|
|
504
|
+
"routing": {
|
|
505
|
+
"type": "direct" if routing_result.is_direct else "l_shaped",
|
|
506
|
+
"segments": len(routing_result.segments),
|
|
507
|
+
"corner": {
|
|
508
|
+
"x": routing_result.corner.x,
|
|
509
|
+
"y": routing_result.corner.y
|
|
510
|
+
} if routing_result.corner else None,
|
|
511
|
+
},
|
|
512
|
+
"segments": [
|
|
513
|
+
{
|
|
514
|
+
"start": {"x": s.x, "y": s.y},
|
|
515
|
+
"end": {"x": e.x, "y": e.y}
|
|
516
|
+
}
|
|
517
|
+
for s, e in routing_result.segments
|
|
518
|
+
],
|
|
519
|
+
"wire_uuids": wire_uuids,
|
|
520
|
+
"junction_uuid": junction_uuid,
|
|
521
|
+
"label_uuid": label_uuid,
|
|
522
|
+
"message": f"Connected {from_component}:{from_pin} to {to_component}:{to_pin} "
|
|
523
|
+
f"with {len(wire_uuids)} wire segment(s)"
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
except Exception as e:
|
|
527
|
+
logger.error(f"[MCP] Unexpected error: {e}", exc_info=True)
|
|
528
|
+
return {
|
|
529
|
+
"success": False,
|
|
530
|
+
"error": "INTERNAL_ERROR",
|
|
531
|
+
"message": f"Unexpected error connecting components: {str(e)}",
|
|
532
|
+
}
|