#
tokens: 16797/50000 2/81 files (page 4/4)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 4 of 4. Use http://codebase.md/mixelpixx/kicad-mcp-server?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .github
│   └── workflows
│       └── ci.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CHANGELOG_2025-10-26.md
├── config
│   ├── claude-desktop-config.json
│   ├── default-config.json
│   ├── linux-config.example.json
│   ├── macos-config.example.json
│   └── windows-config.example.json
├── CONTRIBUTING.md
├── docs
│   ├── BUILD_AND_TEST_SESSION.md
│   ├── CLIENT_CONFIGURATION.md
│   ├── IPC_API_MIGRATION_PLAN.md
│   ├── JLCPCB_INTEGRATION_PLAN.md
│   ├── KNOWN_ISSUES.md
│   ├── LIBRARY_INTEGRATION.md
│   ├── LINUX_COMPATIBILITY_AUDIT.md
│   ├── REALTIME_WORKFLOW.md
│   ├── ROADMAP.md
│   ├── STATUS_SUMMARY.md
│   ├── UI_AUTO_LAUNCH.md
│   ├── VISUAL_FEEDBACK.md
│   ├── WEEK1_SESSION1_SUMMARY.md
│   └── WEEK1_SESSION2_SUMMARY.md
├── LICENSE
├── package-json.json
├── package-lock.json
├── package.json
├── pytest.ini
├── python
│   ├── commands
│   │   ├── __init__.py
│   │   ├── board
│   │   │   ├── __init__.py
│   │   │   ├── layers.py
│   │   │   ├── outline.py
│   │   │   ├── size.py
│   │   │   └── view.py
│   │   ├── board.py
│   │   ├── component_schematic.py
│   │   ├── component.py
│   │   ├── connection_schematic.py
│   │   ├── design_rules.py
│   │   ├── export.py
│   │   ├── library_schematic.py
│   │   ├── project.py
│   │   ├── routing.py
│   │   └── schematic.py
│   ├── kicad_api
│   │   ├── __init__.py
│   │   ├── base.py
│   │   ├── factory.py
│   │   ├── ipc_backend.py
│   │   └── swig_backend.py
│   ├── kicad_interface.py
│   ├── requirements.txt
│   └── utils
│       ├── __init__.py
│       ├── kicad_process.py
│       └── platform_helper.py
├── README.md
├── requirements-dev.txt
├── requirements.txt
├── scripts
│   ├── auto_refresh_kicad.sh
│   └── install-linux.sh
├── src
│   ├── config.ts
│   ├── index.ts
│   ├── kicad-server.ts
│   ├── logger.ts
│   ├── prompts
│   │   ├── component.ts
│   │   ├── design.ts
│   │   ├── index.ts
│   │   └── routing.ts
│   ├── resources
│   │   ├── board.ts
│   │   ├── component.ts
│   │   ├── index.ts
│   │   ├── library.ts
│   │   └── project.ts
│   ├── server.ts
│   ├── tools
│   │   ├── board.ts
│   │   ├── component.ts
│   │   ├── component.txt
│   │   ├── design-rules.ts
│   │   ├── export.ts
│   │   ├── index.ts
│   │   ├── project.ts
│   │   ├── routing.ts
│   │   ├── schematic.ts
│   │   └── ui.ts
│   └── utils
│       └── resource-helpers.ts
├── tests
│   ├── __init__.py
│   └── test_platform_helper.py
├── tsconfig-json.json
└── tsconfig.json
```

# Files

--------------------------------------------------------------------------------
/python/commands/routing.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Routing-related command implementations for KiCAD interface
  3 | """
  4 | 
  5 | import os
  6 | import pcbnew
  7 | import logging
  8 | import math
  9 | from typing import Dict, Any, Optional, List, Tuple
 10 | 
 11 | logger = logging.getLogger('kicad_interface')
 12 | 
 13 | class RoutingCommands:
 14 |     """Handles routing-related KiCAD operations"""
 15 | 
 16 |     def __init__(self, board: Optional[pcbnew.BOARD] = None):
 17 |         """Initialize with optional board instance"""
 18 |         self.board = board
 19 | 
 20 |     def add_net(self, params: Dict[str, Any]) -> Dict[str, Any]:
 21 |         """Add a new net to the PCB"""
 22 |         try:
 23 |             if not self.board:
 24 |                 return {
 25 |                     "success": False,
 26 |                     "message": "No board is loaded",
 27 |                     "errorDetails": "Load or create a board first"
 28 |                 }
 29 | 
 30 |             name = params.get("name")
 31 |             net_class = params.get("class")
 32 | 
 33 |             if not name:
 34 |                 return {
 35 |                     "success": False,
 36 |                     "message": "Missing net name",
 37 |                     "errorDetails": "name parameter is required"
 38 |                 }
 39 | 
 40 |             # Create new net
 41 |             netinfo = self.board.GetNetInfo()
 42 |             net = netinfo.FindNet(name)
 43 |             if not net:
 44 |                 net = netinfo.AddNet(name)
 45 | 
 46 |             # Set net class if provided
 47 |             if net_class:
 48 |                 net_classes = self.board.GetNetClasses()
 49 |                 if net_classes.Find(net_class):
 50 |                     net.SetClass(net_classes.Find(net_class))
 51 | 
 52 |             return {
 53 |                 "success": True,
 54 |                 "message": f"Added net: {name}",
 55 |                 "net": {
 56 |                     "name": name,
 57 |                     "class": net_class if net_class else "Default",
 58 |                     "netcode": net.GetNetCode()
 59 |                 }
 60 |             }
 61 | 
 62 |         except Exception as e:
 63 |             logger.error(f"Error adding net: {str(e)}")
 64 |             return {
 65 |                 "success": False,
 66 |                 "message": "Failed to add net",
 67 |                 "errorDetails": str(e)
 68 |             }
 69 | 
 70 |     def route_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
 71 |         """Route a trace between two points or pads"""
 72 |         try:
 73 |             if not self.board:
 74 |                 return {
 75 |                     "success": False,
 76 |                     "message": "No board is loaded",
 77 |                     "errorDetails": "Load or create a board first"
 78 |                 }
 79 | 
 80 |             start = params.get("start")
 81 |             end = params.get("end")
 82 |             layer = params.get("layer", "F.Cu")
 83 |             width = params.get("width")
 84 |             net = params.get("net")
 85 |             via = params.get("via", False)
 86 | 
 87 |             if not start or not end:
 88 |                 return {
 89 |                     "success": False,
 90 |                     "message": "Missing parameters",
 91 |                     "errorDetails": "start and end points are required"
 92 |                 }
 93 | 
 94 |             # Get layer ID
 95 |             layer_id = self.board.GetLayerID(layer)
 96 |             if layer_id < 0:
 97 |                 return {
 98 |                     "success": False,
 99 |                     "message": "Invalid layer",
100 |                     "errorDetails": f"Layer '{layer}' does not exist"
101 |                 }
102 | 
103 |             # Get start point
104 |             start_point = self._get_point(start)
105 |             end_point = self._get_point(end)
106 | 
107 |             # Create track segment
108 |             track = pcbnew.PCB_TRACK(self.board)
109 |             track.SetStart(start_point)
110 |             track.SetEnd(end_point)
111 |             track.SetLayer(layer_id)
112 | 
113 |             # Set width (default to board's current track width)
114 |             if width:
115 |                 track.SetWidth(int(width * 1000000))  # Convert mm to nm
116 |             else:
117 |                 track.SetWidth(self.board.GetDesignSettings().GetCurrentTrackWidth())
118 | 
119 |             # Set net if provided
120 |             if net:
121 |                 netinfo = self.board.GetNetInfo()
122 |                 net_obj = netinfo.FindNet(net)
123 |                 if net_obj:
124 |                     track.SetNet(net_obj)
125 | 
126 |             # Add track to board
127 |             self.board.Add(track)
128 | 
129 |             # Add via if requested and net is specified
130 |             if via and net:
131 |                 via_point = end_point
132 |                 self.add_via({
133 |                     "position": {
134 |                         "x": via_point.x / 1000000,
135 |                         "y": via_point.y / 1000000,
136 |                         "unit": "mm"
137 |                     },
138 |                     "net": net
139 |                 })
140 | 
141 |             return {
142 |                 "success": True,
143 |                 "message": "Added trace",
144 |                 "trace": {
145 |                     "start": {
146 |                         "x": start_point.x / 1000000,
147 |                         "y": start_point.y / 1000000,
148 |                         "unit": "mm"
149 |                     },
150 |                     "end": {
151 |                         "x": end_point.x / 1000000,
152 |                         "y": end_point.y / 1000000,
153 |                         "unit": "mm"
154 |                     },
155 |                     "layer": layer,
156 |                     "width": track.GetWidth() / 1000000,
157 |                     "net": net
158 |                 }
159 |             }
160 | 
161 |         except Exception as e:
162 |             logger.error(f"Error routing trace: {str(e)}")
163 |             return {
164 |                 "success": False,
165 |                 "message": "Failed to route trace",
166 |                 "errorDetails": str(e)
167 |             }
168 | 
169 |     def add_via(self, params: Dict[str, Any]) -> Dict[str, Any]:
170 |         """Add a via at the specified location"""
171 |         try:
172 |             if not self.board:
173 |                 return {
174 |                     "success": False,
175 |                     "message": "No board is loaded",
176 |                     "errorDetails": "Load or create a board first"
177 |                 }
178 | 
179 |             position = params.get("position")
180 |             size = params.get("size")
181 |             drill = params.get("drill")
182 |             net = params.get("net")
183 |             from_layer = params.get("from_layer", "F.Cu")
184 |             to_layer = params.get("to_layer", "B.Cu")
185 | 
186 |             if not position:
187 |                 return {
188 |                     "success": False,
189 |                     "message": "Missing position",
190 |                     "errorDetails": "position parameter is required"
191 |                 }
192 | 
193 |             # Create via
194 |             via = pcbnew.PCB_VIA(self.board)
195 |             
196 |             # Set position
197 |             scale = 1000000 if position["unit"] == "mm" else 25400000  # mm or inch to nm
198 |             x_nm = int(position["x"] * scale)
199 |             y_nm = int(position["y"] * scale)
200 |             via.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
201 | 
202 |             # Set size and drill (default to board's current via settings)
203 |             design_settings = self.board.GetDesignSettings()
204 |             via.SetWidth(int(size * 1000000) if size else design_settings.GetCurrentViaSize())
205 |             via.SetDrill(int(drill * 1000000) if drill else design_settings.GetCurrentViaDrill())
206 | 
207 |             # Set layers
208 |             from_id = self.board.GetLayerID(from_layer)
209 |             to_id = self.board.GetLayerID(to_layer)
210 |             if from_id < 0 or to_id < 0:
211 |                 return {
212 |                     "success": False,
213 |                     "message": "Invalid layer",
214 |                     "errorDetails": "Specified layers do not exist"
215 |                 }
216 |             via.SetLayerPair(from_id, to_id)
217 | 
218 |             # Set net if provided
219 |             if net:
220 |                 netinfo = self.board.GetNetInfo()
221 |                 net_obj = netinfo.FindNet(net)
222 |                 if net_obj:
223 |                     via.SetNet(net_obj)
224 | 
225 |             # Add via to board
226 |             self.board.Add(via)
227 | 
228 |             return {
229 |                 "success": True,
230 |                 "message": "Added via",
231 |                 "via": {
232 |                     "position": {
233 |                         "x": position["x"],
234 |                         "y": position["y"],
235 |                         "unit": position["unit"]
236 |                     },
237 |                     "size": via.GetWidth() / 1000000,
238 |                     "drill": via.GetDrill() / 1000000,
239 |                     "from_layer": from_layer,
240 |                     "to_layer": to_layer,
241 |                     "net": net
242 |                 }
243 |             }
244 | 
245 |         except Exception as e:
246 |             logger.error(f"Error adding via: {str(e)}")
247 |             return {
248 |                 "success": False,
249 |                 "message": "Failed to add via",
250 |                 "errorDetails": str(e)
251 |             }
252 | 
253 |     def delete_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
254 |         """Delete a trace from the PCB"""
255 |         try:
256 |             if not self.board:
257 |                 return {
258 |                     "success": False,
259 |                     "message": "No board is loaded",
260 |                     "errorDetails": "Load or create a board first"
261 |                 }
262 | 
263 |             trace_uuid = params.get("traceUuid")
264 |             position = params.get("position")
265 | 
266 |             if not trace_uuid and not position:
267 |                 return {
268 |                     "success": False,
269 |                     "message": "Missing parameters",
270 |                     "errorDetails": "Either traceUuid or position must be provided"
271 |                 }
272 | 
273 |             # Find track by UUID
274 |             if trace_uuid:
275 |                 track = None
276 |                 for item in self.board.Tracks():
277 |                     if str(item.m_Uuid) == trace_uuid:
278 |                         track = item
279 |                         break
280 | 
281 |                 if not track:
282 |                     return {
283 |                         "success": False,
284 |                         "message": "Track not found",
285 |                         "errorDetails": f"Could not find track with UUID: {trace_uuid}"
286 |                     }
287 | 
288 |                 self.board.Remove(track)
289 |                 return {
290 |                     "success": True,
291 |                     "message": f"Deleted track: {trace_uuid}"
292 |                 }
293 | 
294 |             # Find track by position
295 |             if position:
296 |                 scale = 1000000 if position["unit"] == "mm" else 25400000  # mm or inch to nm
297 |                 x_nm = int(position["x"] * scale)
298 |                 y_nm = int(position["y"] * scale)
299 |                 point = pcbnew.VECTOR2I(x_nm, y_nm)
300 | 
301 |                 # Find closest track
302 |                 closest_track = None
303 |                 min_distance = float('inf')
304 |                 for track in self.board.Tracks():
305 |                     dist = self._point_to_track_distance(point, track)
306 |                     if dist < min_distance:
307 |                         min_distance = dist
308 |                         closest_track = track
309 | 
310 |                 if closest_track and min_distance < 1000000:  # Within 1mm
311 |                     self.board.Remove(closest_track)
312 |                     return {
313 |                         "success": True,
314 |                         "message": "Deleted track at specified position"
315 |                     }
316 |                 else:
317 |                     return {
318 |                         "success": False,
319 |                         "message": "No track found",
320 |                         "errorDetails": "No track found near specified position"
321 |                     }
322 | 
323 |         except Exception as e:
324 |             logger.error(f"Error deleting trace: {str(e)}")
325 |             return {
326 |                 "success": False,
327 |                 "message": "Failed to delete trace",
328 |                 "errorDetails": str(e)
329 |             }
330 | 
331 |     def get_nets_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
332 |         """Get a list of all nets in the PCB"""
333 |         try:
334 |             if not self.board:
335 |                 return {
336 |                     "success": False,
337 |                     "message": "No board is loaded",
338 |                     "errorDetails": "Load or create a board first"
339 |                 }
340 | 
341 |             nets = []
342 |             netinfo = self.board.GetNetInfo()
343 |             for net_code in range(netinfo.GetNetCount()):
344 |                 net = netinfo.GetNetItem(net_code)
345 |                 if net:
346 |                     nets.append({
347 |                         "name": net.GetNetname(),
348 |                         "code": net.GetNetCode(),
349 |                         "class": net.GetClassName()
350 |                     })
351 | 
352 |             return {
353 |                 "success": True,
354 |                 "nets": nets
355 |             }
356 | 
357 |         except Exception as e:
358 |             logger.error(f"Error getting nets list: {str(e)}")
359 |             return {
360 |                 "success": False,
361 |                 "message": "Failed to get nets list",
362 |                 "errorDetails": str(e)
363 |             }
364 |             
365 |     def create_netclass(self, params: Dict[str, Any]) -> Dict[str, Any]:
366 |         """Create a new net class with specified properties"""
367 |         try:
368 |             if not self.board:
369 |                 return {
370 |                     "success": False,
371 |                     "message": "No board is loaded",
372 |                     "errorDetails": "Load or create a board first"
373 |                 }
374 | 
375 |             name = params.get("name")
376 |             clearance = params.get("clearance")
377 |             track_width = params.get("trackWidth")
378 |             via_diameter = params.get("viaDiameter")
379 |             via_drill = params.get("viaDrill")
380 |             uvia_diameter = params.get("uviaDiameter")
381 |             uvia_drill = params.get("uviaDrill")
382 |             diff_pair_width = params.get("diffPairWidth")
383 |             diff_pair_gap = params.get("diffPairGap")
384 |             nets = params.get("nets", [])
385 | 
386 |             if not name:
387 |                 return {
388 |                     "success": False,
389 |                     "message": "Missing netclass name",
390 |                     "errorDetails": "name parameter is required"
391 |                 }
392 | 
393 |             # Get net classes
394 |             net_classes = self.board.GetNetClasses()
395 |             
396 |             # Create new net class if it doesn't exist
397 |             if not net_classes.Find(name):
398 |                 netclass = pcbnew.NETCLASS(name)
399 |                 net_classes.Add(netclass)
400 |             else:
401 |                 netclass = net_classes.Find(name)
402 | 
403 |             # Set properties
404 |             scale = 1000000  # mm to nm
405 |             if clearance is not None:
406 |                 netclass.SetClearance(int(clearance * scale))
407 |             if track_width is not None:
408 |                 netclass.SetTrackWidth(int(track_width * scale))
409 |             if via_diameter is not None:
410 |                 netclass.SetViaDiameter(int(via_diameter * scale))
411 |             if via_drill is not None:
412 |                 netclass.SetViaDrill(int(via_drill * scale))
413 |             if uvia_diameter is not None:
414 |                 netclass.SetMicroViaDiameter(int(uvia_diameter * scale))
415 |             if uvia_drill is not None:
416 |                 netclass.SetMicroViaDrill(int(uvia_drill * scale))
417 |             if diff_pair_width is not None:
418 |                 netclass.SetDiffPairWidth(int(diff_pair_width * scale))
419 |             if diff_pair_gap is not None:
420 |                 netclass.SetDiffPairGap(int(diff_pair_gap * scale))
421 | 
422 |             # Add nets to net class
423 |             netinfo = self.board.GetNetInfo()
424 |             for net_name in nets:
425 |                 net = netinfo.FindNet(net_name)
426 |                 if net:
427 |                     net.SetClass(netclass)
428 | 
429 |             return {
430 |                 "success": True,
431 |                 "message": f"Created net class: {name}",
432 |                 "netClass": {
433 |                     "name": name,
434 |                     "clearance": netclass.GetClearance() / scale,
435 |                     "trackWidth": netclass.GetTrackWidth() / scale,
436 |                     "viaDiameter": netclass.GetViaDiameter() / scale,
437 |                     "viaDrill": netclass.GetViaDrill() / scale,
438 |                     "uviaDiameter": netclass.GetMicroViaDiameter() / scale,
439 |                     "uviaDrill": netclass.GetMicroViaDrill() / scale,
440 |                     "diffPairWidth": netclass.GetDiffPairWidth() / scale,
441 |                     "diffPairGap": netclass.GetDiffPairGap() / scale,
442 |                     "nets": nets
443 |                 }
444 |             }
445 | 
446 |         except Exception as e:
447 |             logger.error(f"Error creating net class: {str(e)}")
448 |             return {
449 |                 "success": False,
450 |                 "message": "Failed to create net class",
451 |                 "errorDetails": str(e)
452 |             }
453 |             
454 |     def add_copper_pour(self, params: Dict[str, Any]) -> Dict[str, Any]:
455 |         """Add a copper pour (zone) to the PCB"""
456 |         try:
457 |             if not self.board:
458 |                 return {
459 |                     "success": False,
460 |                     "message": "No board is loaded",
461 |                     "errorDetails": "Load or create a board first"
462 |                 }
463 | 
464 |             layer = params.get("layer", "F.Cu")
465 |             net = params.get("net")
466 |             clearance = params.get("clearance")
467 |             min_width = params.get("minWidth", 0.2)
468 |             points = params.get("points", [])
469 |             priority = params.get("priority", 0)
470 |             fill_type = params.get("fillType", "solid")  # solid or hatched
471 |             
472 |             if not points or len(points) < 3:
473 |                 return {
474 |                     "success": False,
475 |                     "message": "Missing points",
476 |                     "errorDetails": "At least 3 points are required for copper pour outline"
477 |                 }
478 | 
479 |             # Get layer ID
480 |             layer_id = self.board.GetLayerID(layer)
481 |             if layer_id < 0:
482 |                 return {
483 |                     "success": False,
484 |                     "message": "Invalid layer",
485 |                     "errorDetails": f"Layer '{layer}' does not exist"
486 |                 }
487 | 
488 |             # Create zone
489 |             zone = pcbnew.ZONE(self.board)
490 |             zone.SetLayer(layer_id)
491 |             
492 |             # Set net if provided
493 |             if net:
494 |                 netinfo = self.board.GetNetInfo()
495 |                 net_obj = netinfo.FindNet(net)
496 |                 if net_obj:
497 |                     zone.SetNet(net_obj)
498 |             
499 |             # Set zone properties
500 |             scale = 1000000  # mm to nm
501 |             zone.SetPriority(priority)
502 |             
503 |             if clearance is not None:
504 |                 zone.SetLocalClearance(int(clearance * scale))
505 |             
506 |             zone.SetMinThickness(int(min_width * scale))
507 |             
508 |             # Set fill type
509 |             if fill_type == "hatched":
510 |                 zone.SetFillMode(pcbnew.ZONE_FILL_MODE_HATCH_PATTERN)
511 |             else:
512 |                 zone.SetFillMode(pcbnew.ZONE_FILL_MODE_POLYGON)
513 |             
514 |             # Create outline
515 |             outline = zone.Outline()
516 |             
517 |             # Add points to outline
518 |             for point in points:
519 |                 scale = 1000000 if point.get("unit", "mm") == "mm" else 25400000
520 |                 x_nm = int(point["x"] * scale)
521 |                 y_nm = int(point["y"] * scale)
522 |                 outline.Append(pcbnew.VECTOR2I(x_nm, y_nm))
523 |             
524 |             # Add zone to board
525 |             self.board.Add(zone)
526 |             
527 |             # Fill zone
528 |             filler = pcbnew.ZONE_FILLER(self.board)
529 |             filler.Fill(self.board.Zones())
530 | 
531 |             return {
532 |                 "success": True,
533 |                 "message": "Added copper pour",
534 |                 "pour": {
535 |                     "layer": layer,
536 |                     "net": net,
537 |                     "clearance": clearance,
538 |                     "minWidth": min_width,
539 |                     "priority": priority,
540 |                     "fillType": fill_type,
541 |                     "pointCount": len(points)
542 |                 }
543 |             }
544 | 
545 |         except Exception as e:
546 |             logger.error(f"Error adding copper pour: {str(e)}")
547 |             return {
548 |                 "success": False,
549 |                 "message": "Failed to add copper pour",
550 |                 "errorDetails": str(e)
551 |             }
552 |             
553 |     def route_differential_pair(self, params: Dict[str, Any]) -> Dict[str, Any]:
554 |         """Route a differential pair between two sets of points or pads"""
555 |         try:
556 |             if not self.board:
557 |                 return {
558 |                     "success": False,
559 |                     "message": "No board is loaded",
560 |                     "errorDetails": "Load or create a board first"
561 |                 }
562 | 
563 |             start_pos = params.get("startPos")
564 |             end_pos = params.get("endPos")
565 |             net_pos = params.get("netPos")
566 |             net_neg = params.get("netNeg")
567 |             layer = params.get("layer", "F.Cu")
568 |             width = params.get("width")
569 |             gap = params.get("gap")
570 | 
571 |             if not start_pos or not end_pos or not net_pos or not net_neg:
572 |                 return {
573 |                     "success": False,
574 |                     "message": "Missing parameters",
575 |                     "errorDetails": "startPos, endPos, netPos, and netNeg are required"
576 |                 }
577 | 
578 |             # Get layer ID
579 |             layer_id = self.board.GetLayerID(layer)
580 |             if layer_id < 0:
581 |                 return {
582 |                     "success": False,
583 |                     "message": "Invalid layer",
584 |                     "errorDetails": f"Layer '{layer}' does not exist"
585 |                 }
586 | 
587 |             # Get nets
588 |             netinfo = self.board.GetNetInfo()
589 |             net_pos_obj = netinfo.FindNet(net_pos)
590 |             net_neg_obj = netinfo.FindNet(net_neg)
591 |             
592 |             if not net_pos_obj or not net_neg_obj:
593 |                 return {
594 |                     "success": False,
595 |                     "message": "Nets not found",
596 |                     "errorDetails": "One or both nets specified for the differential pair do not exist"
597 |                 }
598 | 
599 |             # Get start and end points
600 |             start_point = self._get_point(start_pos)
601 |             end_point = self._get_point(end_pos)
602 |             
603 |             # Calculate offset vectors for the two traces
604 |             # First, get the direction vector from start to end
605 |             dx = end_point.x - start_point.x
606 |             dy = end_point.y - start_point.y
607 |             length = math.sqrt(dx * dx + dy * dy)
608 |             
609 |             if length <= 0:
610 |                 return {
611 |                     "success": False,
612 |                     "message": "Invalid points",
613 |                     "errorDetails": "Start and end points must be different"
614 |                 }
615 |                 
616 |             # Normalize direction vector
617 |             dx /= length
618 |             dy /= length
619 |             
620 |             # Get perpendicular vector
621 |             px = -dy
622 |             py = dx
623 |             
624 |             # Set default gap if not provided
625 |             if gap is None:
626 |                 gap = 0.2  # mm
627 |                 
628 |             # Convert to nm
629 |             gap_nm = int(gap * 1000000)
630 |             
631 |             # Calculate offsets
632 |             offset_x = int(px * gap_nm / 2)
633 |             offset_y = int(py * gap_nm / 2)
634 |             
635 |             # Create positive and negative trace points
636 |             pos_start = pcbnew.VECTOR2I(int(start_point.x + offset_x), int(start_point.y + offset_y))
637 |             pos_end = pcbnew.VECTOR2I(int(end_point.x + offset_x), int(end_point.y + offset_y))
638 |             neg_start = pcbnew.VECTOR2I(int(start_point.x - offset_x), int(start_point.y - offset_y))
639 |             neg_end = pcbnew.VECTOR2I(int(end_point.x - offset_x), int(end_point.y - offset_y))
640 |             
641 |             # Create positive trace
642 |             pos_track = pcbnew.PCB_TRACK(self.board)
643 |             pos_track.SetStart(pos_start)
644 |             pos_track.SetEnd(pos_end)
645 |             pos_track.SetLayer(layer_id)
646 |             pos_track.SetNet(net_pos_obj)
647 |             
648 |             # Create negative trace
649 |             neg_track = pcbnew.PCB_TRACK(self.board)
650 |             neg_track.SetStart(neg_start)
651 |             neg_track.SetEnd(neg_end)
652 |             neg_track.SetLayer(layer_id)
653 |             neg_track.SetNet(net_neg_obj)
654 |             
655 |             # Set width
656 |             if width:
657 |                 trace_width_nm = int(width * 1000000)
658 |                 pos_track.SetWidth(trace_width_nm)
659 |                 neg_track.SetWidth(trace_width_nm)
660 |             else:
661 |                 # Get default width from design rules or net class
662 |                 trace_width = self.board.GetDesignSettings().GetCurrentTrackWidth()
663 |                 pos_track.SetWidth(trace_width)
664 |                 neg_track.SetWidth(trace_width)
665 |             
666 |             # Add tracks to board
667 |             self.board.Add(pos_track)
668 |             self.board.Add(neg_track)
669 | 
670 |             return {
671 |                 "success": True,
672 |                 "message": "Added differential pair traces",
673 |                 "diffPair": {
674 |                     "posNet": net_pos,
675 |                     "negNet": net_neg,
676 |                     "layer": layer,
677 |                     "width": pos_track.GetWidth() / 1000000,
678 |                     "gap": gap,
679 |                     "length": length / 1000000
680 |                 }
681 |             }
682 | 
683 |         except Exception as e:
684 |             logger.error(f"Error routing differential pair: {str(e)}")
685 |             return {
686 |                 "success": False,
687 |                 "message": "Failed to route differential pair",
688 |                 "errorDetails": str(e)
689 |             }
690 | 
691 |     def _get_point(self, point_spec: Dict[str, Any]) -> pcbnew.VECTOR2I:
692 |         """Convert point specification to KiCAD point"""
693 |         if "x" in point_spec and "y" in point_spec:
694 |             scale = 1000000 if point_spec.get("unit", "mm") == "mm" else 25400000
695 |             x_nm = int(point_spec["x"] * scale)
696 |             y_nm = int(point_spec["y"] * scale)
697 |             return pcbnew.VECTOR2I(x_nm, y_nm)
698 |         elif "pad" in point_spec and "componentRef" in point_spec:
699 |             module = self.board.FindFootprintByReference(point_spec["componentRef"])
700 |             if module:
701 |                 pad = module.FindPadByName(point_spec["pad"])
702 |                 if pad:
703 |                     return pad.GetPosition()
704 |         raise ValueError("Invalid point specification")
705 | 
706 |     def _point_to_track_distance(self, point: pcbnew.VECTOR2I, track: pcbnew.PCB_TRACK) -> float:
707 |         """Calculate distance from point to track segment"""
708 |         start = track.GetStart()
709 |         end = track.GetEnd()
710 |         
711 |         # Vector from start to end
712 |         v = pcbnew.VECTOR2I(end.x - start.x, end.y - start.y)
713 |         # Vector from start to point
714 |         w = pcbnew.VECTOR2I(point.x - start.x, point.y - start.y)
715 |         
716 |         # Length of track squared
717 |         c1 = v.x * v.x + v.y * v.y
718 |         if c1 == 0:
719 |             return self._point_distance(point, start)
720 |             
721 |         # Projection coefficient
722 |         c2 = float(w.x * v.x + w.y * v.y) / c1
723 |         
724 |         if c2 < 0:
725 |             return self._point_distance(point, start)
726 |         elif c2 > 1:
727 |             return self._point_distance(point, end)
728 |             
729 |         # Point on line
730 |         proj = pcbnew.VECTOR2I(
731 |             int(start.x + c2 * v.x),
732 |             int(start.y + c2 * v.y)
733 |         )
734 |         return self._point_distance(point, proj)
735 | 
736 |     def _point_distance(self, p1: pcbnew.VECTOR2I, p2: pcbnew.VECTOR2I) -> float:
737 |         """Calculate distance between two points"""
738 |         dx = p1.x - p2.x
739 |         dy = p1.y - p2.y
740 |         return (dx * dx + dy * dy) ** 0.5
741 | 
```

--------------------------------------------------------------------------------
/python/commands/component.py:
--------------------------------------------------------------------------------

```python
  1 | """
  2 | Component-related command implementations for KiCAD interface
  3 | """
  4 | 
  5 | import os
  6 | import pcbnew
  7 | import logging
  8 | import math
  9 | from typing import Dict, Any, Optional, List, Tuple
 10 | import base64
 11 | 
 12 | logger = logging.getLogger('kicad_interface')
 13 | 
 14 | class ComponentCommands:
 15 |     """Handles component-related KiCAD operations"""
 16 | 
 17 |     def __init__(self, board: Optional[pcbnew.BOARD] = None):
 18 |         """Initialize with optional board instance"""
 19 |         self.board = board
 20 | 
 21 |     def place_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
 22 |         """Place a component on the PCB"""
 23 |         try:
 24 |             if not self.board:
 25 |                 return {
 26 |                     "success": False,
 27 |                     "message": "No board is loaded",
 28 |                     "errorDetails": "Load or create a board first"
 29 |                 }
 30 | 
 31 |             # Get parameters
 32 |             component_id = params.get("componentId")
 33 |             position = params.get("position")
 34 |             reference = params.get("reference")
 35 |             value = params.get("value")
 36 |             footprint = params.get("footprint")
 37 |             rotation = params.get("rotation", 0)
 38 |             layer = params.get("layer", "F.Cu")
 39 | 
 40 |             if not component_id or not position:
 41 |                 return {
 42 |                     "success": False,
 43 |                     "message": "Missing parameters",
 44 |                     "errorDetails": "componentId and position are required"
 45 |                 }
 46 | 
 47 |             # Create new module (footprint)
 48 |             module = pcbnew.FootprintLoad(self.board.GetLibraryPath(), component_id)
 49 |             if not module:
 50 |                 return {
 51 |                     "success": False,
 52 |                     "message": "Component not found",
 53 |                     "errorDetails": f"Could not find component: {component_id}"
 54 |                 }
 55 | 
 56 |             # Set position
 57 |             scale = 1000000 if position["unit"] == "mm" else 25400000  # mm or inch to nm
 58 |             x_nm = int(position["x"] * scale)
 59 |             y_nm = int(position["y"] * scale)
 60 |             module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
 61 | 
 62 |             # Set reference if provided
 63 |             if reference:
 64 |                 module.SetReference(reference)
 65 | 
 66 |             # Set value if provided
 67 |             if value:
 68 |                 module.SetValue(value)
 69 | 
 70 |             # Set footprint if provided
 71 |             if footprint:
 72 |                 module.SetFootprintName(footprint)
 73 | 
 74 |             # Set rotation
 75 |             module.SetOrientation(rotation * 10)  # KiCAD uses decidegrees
 76 | 
 77 |             # Set layer
 78 |             layer_id = self.board.GetLayerID(layer)
 79 |             if layer_id >= 0:
 80 |                 module.SetLayer(layer_id)
 81 | 
 82 |             # Add to board
 83 |             self.board.Add(module)
 84 | 
 85 |             return {
 86 |                 "success": True,
 87 |                 "message": f"Placed component: {component_id}",
 88 |                 "component": {
 89 |                     "reference": module.GetReference(),
 90 |                     "value": module.GetValue(),
 91 |                     "position": {
 92 |                         "x": position["x"],
 93 |                         "y": position["y"],
 94 |                         "unit": position["unit"]
 95 |                     },
 96 |                     "rotation": rotation,
 97 |                     "layer": layer
 98 |                 }
 99 |             }
100 | 
101 |         except Exception as e:
102 |             logger.error(f"Error placing component: {str(e)}")
103 |             return {
104 |                 "success": False,
105 |                 "message": "Failed to place component",
106 |                 "errorDetails": str(e)
107 |             }
108 | 
109 |     def move_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
110 |         """Move an existing component to a new position"""
111 |         try:
112 |             if not self.board:
113 |                 return {
114 |                     "success": False,
115 |                     "message": "No board is loaded",
116 |                     "errorDetails": "Load or create a board first"
117 |                 }
118 | 
119 |             reference = params.get("reference")
120 |             position = params.get("position")
121 |             rotation = params.get("rotation")
122 | 
123 |             if not reference or not position:
124 |                 return {
125 |                     "success": False,
126 |                     "message": "Missing parameters",
127 |                     "errorDetails": "reference and position are required"
128 |                 }
129 | 
130 |             # Find the component
131 |             module = self.board.FindFootprintByReference(reference)
132 |             if not module:
133 |                 return {
134 |                     "success": False,
135 |                     "message": "Component not found",
136 |                     "errorDetails": f"Could not find component: {reference}"
137 |                 }
138 | 
139 |             # Set new position
140 |             scale = 1000000 if position["unit"] == "mm" else 25400000  # mm or inch to nm
141 |             x_nm = int(position["x"] * scale)
142 |             y_nm = int(position["y"] * scale)
143 |             module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
144 | 
145 |             # Set new rotation if provided
146 |             if rotation is not None:
147 |                 module.SetOrientation(rotation * 10)  # KiCAD uses decidegrees
148 | 
149 |             return {
150 |                 "success": True,
151 |                 "message": f"Moved component: {reference}",
152 |                 "component": {
153 |                     "reference": reference,
154 |                     "position": {
155 |                         "x": position["x"],
156 |                         "y": position["y"],
157 |                         "unit": position["unit"]
158 |                     },
159 |                     "rotation": rotation if rotation is not None else module.GetOrientation() / 10
160 |                 }
161 |             }
162 | 
163 |         except Exception as e:
164 |             logger.error(f"Error moving component: {str(e)}")
165 |             return {
166 |                 "success": False,
167 |                 "message": "Failed to move component",
168 |                 "errorDetails": str(e)
169 |             }
170 | 
171 |     def rotate_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
172 |         """Rotate an existing component"""
173 |         try:
174 |             if not self.board:
175 |                 return {
176 |                     "success": False,
177 |                     "message": "No board is loaded",
178 |                     "errorDetails": "Load or create a board first"
179 |                 }
180 | 
181 |             reference = params.get("reference")
182 |             angle = params.get("angle")
183 | 
184 |             if not reference or angle is None:
185 |                 return {
186 |                     "success": False,
187 |                     "message": "Missing parameters",
188 |                     "errorDetails": "reference and angle are required"
189 |                 }
190 | 
191 |             # Find the component
192 |             module = self.board.FindFootprintByReference(reference)
193 |             if not module:
194 |                 return {
195 |                     "success": False,
196 |                     "message": "Component not found",
197 |                     "errorDetails": f"Could not find component: {reference}"
198 |                 }
199 | 
200 |             # Set rotation
201 |             module.SetOrientation(angle * 10)  # KiCAD uses decidegrees
202 | 
203 |             return {
204 |                 "success": True,
205 |                 "message": f"Rotated component: {reference}",
206 |                 "component": {
207 |                     "reference": reference,
208 |                     "rotation": angle
209 |                 }
210 |             }
211 | 
212 |         except Exception as e:
213 |             logger.error(f"Error rotating component: {str(e)}")
214 |             return {
215 |                 "success": False,
216 |                 "message": "Failed to rotate component",
217 |                 "errorDetails": str(e)
218 |             }
219 | 
220 |     def delete_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
221 |         """Delete a component from the PCB"""
222 |         try:
223 |             if not self.board:
224 |                 return {
225 |                     "success": False,
226 |                     "message": "No board is loaded",
227 |                     "errorDetails": "Load or create a board first"
228 |                 }
229 | 
230 |             reference = params.get("reference")
231 |             if not reference:
232 |                 return {
233 |                     "success": False,
234 |                     "message": "Missing reference",
235 |                     "errorDetails": "reference parameter is required"
236 |                 }
237 | 
238 |             # Find the component
239 |             module = self.board.FindFootprintByReference(reference)
240 |             if not module:
241 |                 return {
242 |                     "success": False,
243 |                     "message": "Component not found",
244 |                     "errorDetails": f"Could not find component: {reference}"
245 |                 }
246 | 
247 |             # Remove from board
248 |             self.board.Remove(module)
249 | 
250 |             return {
251 |                 "success": True,
252 |                 "message": f"Deleted component: {reference}"
253 |             }
254 | 
255 |         except Exception as e:
256 |             logger.error(f"Error deleting component: {str(e)}")
257 |             return {
258 |                 "success": False,
259 |                 "message": "Failed to delete component",
260 |                 "errorDetails": str(e)
261 |             }
262 | 
263 |     def edit_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
264 |         """Edit the properties of an existing component"""
265 |         try:
266 |             if not self.board:
267 |                 return {
268 |                     "success": False,
269 |                     "message": "No board is loaded",
270 |                     "errorDetails": "Load or create a board first"
271 |                 }
272 | 
273 |             reference = params.get("reference")
274 |             new_reference = params.get("newReference")
275 |             value = params.get("value")
276 |             footprint = params.get("footprint")
277 | 
278 |             if not reference:
279 |                 return {
280 |                     "success": False,
281 |                     "message": "Missing reference",
282 |                     "errorDetails": "reference parameter is required"
283 |                 }
284 | 
285 |             # Find the component
286 |             module = self.board.FindFootprintByReference(reference)
287 |             if not module:
288 |                 return {
289 |                     "success": False,
290 |                     "message": "Component not found",
291 |                     "errorDetails": f"Could not find component: {reference}"
292 |                 }
293 | 
294 |             # Update properties
295 |             if new_reference:
296 |                 module.SetReference(new_reference)
297 |             if value:
298 |                 module.SetValue(value)
299 |             if footprint:
300 |                 module.SetFootprintName(footprint)
301 | 
302 |             return {
303 |                 "success": True,
304 |                 "message": f"Updated component: {reference}",
305 |                 "component": {
306 |                     "reference": new_reference or reference,
307 |                     "value": value or module.GetValue(),
308 |                     "footprint": footprint or module.GetFootprintName()
309 |                 }
310 |             }
311 | 
312 |         except Exception as e:
313 |             logger.error(f"Error editing component: {str(e)}")
314 |             return {
315 |                 "success": False,
316 |                 "message": "Failed to edit component",
317 |                 "errorDetails": str(e)
318 |             }
319 | 
320 |     def get_component_properties(self, params: Dict[str, Any]) -> Dict[str, Any]:
321 |         """Get detailed properties of a component"""
322 |         try:
323 |             if not self.board:
324 |                 return {
325 |                     "success": False,
326 |                     "message": "No board is loaded",
327 |                     "errorDetails": "Load or create a board first"
328 |                 }
329 | 
330 |             reference = params.get("reference")
331 |             if not reference:
332 |                 return {
333 |                     "success": False,
334 |                     "message": "Missing reference",
335 |                     "errorDetails": "reference parameter is required"
336 |                 }
337 | 
338 |             # Find the component
339 |             module = self.board.FindFootprintByReference(reference)
340 |             if not module:
341 |                 return {
342 |                     "success": False,
343 |                     "message": "Component not found",
344 |                     "errorDetails": f"Could not find component: {reference}"
345 |                 }
346 | 
347 |             # Get position in mm
348 |             pos = module.GetPosition()
349 |             x_mm = pos.x / 1000000
350 |             y_mm = pos.y / 1000000
351 | 
352 |             return {
353 |                 "success": True,
354 |                 "component": {
355 |                     "reference": module.GetReference(),
356 |                     "value": module.GetValue(),
357 |                     "footprint": module.GetFootprintName(),
358 |                     "position": {
359 |                         "x": x_mm,
360 |                         "y": y_mm,
361 |                         "unit": "mm"
362 |                     },
363 |                     "rotation": module.GetOrientation() / 10,
364 |                     "layer": self.board.GetLayerName(module.GetLayer()),
365 |                     "attributes": {
366 |                         "smd": module.GetAttributes() & pcbnew.FP_SMD,
367 |                         "through_hole": module.GetAttributes() & pcbnew.FP_THROUGH_HOLE,
368 |                         "virtual": module.GetAttributes() & pcbnew.FP_VIRTUAL
369 |                     }
370 |                 }
371 |             }
372 | 
373 |         except Exception as e:
374 |             logger.error(f"Error getting component properties: {str(e)}")
375 |             return {
376 |                 "success": False,
377 |                 "message": "Failed to get component properties",
378 |                 "errorDetails": str(e)
379 |             }
380 | 
381 |     def get_component_list(self, params: Dict[str, Any]) -> Dict[str, Any]:
382 |         """Get a list of all components on the board"""
383 |         try:
384 |             if not self.board:
385 |                 return {
386 |                     "success": False,
387 |                     "message": "No board is loaded",
388 |                     "errorDetails": "Load or create a board first"
389 |                 }
390 | 
391 |             components = []
392 |             for module in self.board.GetFootprints():
393 |                 pos = module.GetPosition()
394 |                 x_mm = pos.x / 1000000
395 |                 y_mm = pos.y / 1000000
396 | 
397 |                 components.append({
398 |                     "reference": module.GetReference(),
399 |                     "value": module.GetValue(),
400 |                     "footprint": module.GetFootprintName(),
401 |                     "position": {
402 |                         "x": x_mm,
403 |                         "y": y_mm,
404 |                         "unit": "mm"
405 |                     },
406 |                     "rotation": module.GetOrientation() / 10,
407 |                     "layer": self.board.GetLayerName(module.GetLayer())
408 |                 })
409 | 
410 |             return {
411 |                 "success": True,
412 |                 "components": components
413 |             }
414 | 
415 |         except Exception as e:
416 |             logger.error(f"Error getting component list: {str(e)}")
417 |             return {
418 |                 "success": False,
419 |                 "message": "Failed to get component list",
420 |                 "errorDetails": str(e)
421 |             }
422 |             
423 |     def place_component_array(self, params: Dict[str, Any]) -> Dict[str, Any]:
424 |         """Place an array of components in a grid or circular pattern"""
425 |         try:
426 |             if not self.board:
427 |                 return {
428 |                     "success": False,
429 |                     "message": "No board is loaded",
430 |                     "errorDetails": "Load or create a board first"
431 |                 }
432 | 
433 |             component_id = params.get("componentId")
434 |             pattern = params.get("pattern", "grid")  # grid or circular
435 |             count = params.get("count")
436 |             reference_prefix = params.get("referencePrefix", "U")
437 |             value = params.get("value")
438 |             
439 |             if not component_id or not count:
440 |                 return {
441 |                     "success": False,
442 |                     "message": "Missing parameters",
443 |                     "errorDetails": "componentId and count are required"
444 |                 }
445 |                 
446 |             if pattern == "grid":
447 |                 start_position = params.get("startPosition")
448 |                 rows = params.get("rows")
449 |                 columns = params.get("columns")
450 |                 spacing_x = params.get("spacingX")
451 |                 spacing_y = params.get("spacingY")
452 |                 rotation = params.get("rotation", 0)
453 |                 layer = params.get("layer", "F.Cu")
454 |                 
455 |                 if not start_position or not rows or not columns or not spacing_x or not spacing_y:
456 |                     return {
457 |                         "success": False,
458 |                         "message": "Missing grid parameters",
459 |                         "errorDetails": "For grid pattern, startPosition, rows, columns, spacingX, and spacingY are required"
460 |                     }
461 |                     
462 |                 if rows * columns != count:
463 |                     return {
464 |                         "success": False,
465 |                         "message": "Invalid grid parameters",
466 |                         "errorDetails": "rows * columns must equal count"
467 |                     }
468 |                     
469 |                 placed_components = self._place_grid_array(
470 |                     component_id,
471 |                     start_position,
472 |                     rows,
473 |                     columns,
474 |                     spacing_x,
475 |                     spacing_y,
476 |                     reference_prefix,
477 |                     value,
478 |                     rotation,
479 |                     layer
480 |                 )
481 |                 
482 |             elif pattern == "circular":
483 |                 center = params.get("center")
484 |                 radius = params.get("radius")
485 |                 angle_start = params.get("angleStart", 0)
486 |                 angle_step = params.get("angleStep")
487 |                 rotation_offset = params.get("rotationOffset", 0)
488 |                 layer = params.get("layer", "F.Cu")
489 |                 
490 |                 if not center or not radius or not angle_step:
491 |                     return {
492 |                         "success": False,
493 |                         "message": "Missing circular parameters",
494 |                         "errorDetails": "For circular pattern, center, radius, and angleStep are required"
495 |                     }
496 |                     
497 |                 placed_components = self._place_circular_array(
498 |                     component_id,
499 |                     center,
500 |                     radius,
501 |                     count,
502 |                     angle_start,
503 |                     angle_step,
504 |                     reference_prefix,
505 |                     value,
506 |                     rotation_offset,
507 |                     layer
508 |                 )
509 |                 
510 |             else:
511 |                 return {
512 |                     "success": False,
513 |                     "message": "Invalid pattern",
514 |                     "errorDetails": "Pattern must be 'grid' or 'circular'"
515 |                 }
516 | 
517 |             return {
518 |                 "success": True,
519 |                 "message": f"Placed {count} components in {pattern} pattern",
520 |                 "components": placed_components
521 |             }
522 | 
523 |         except Exception as e:
524 |             logger.error(f"Error placing component array: {str(e)}")
525 |             return {
526 |                 "success": False,
527 |                 "message": "Failed to place component array",
528 |                 "errorDetails": str(e)
529 |             }
530 |             
531 |     def align_components(self, params: Dict[str, Any]) -> Dict[str, Any]:
532 |         """Align multiple components along a line or distribute them evenly"""
533 |         try:
534 |             if not self.board:
535 |                 return {
536 |                     "success": False,
537 |                     "message": "No board is loaded",
538 |                     "errorDetails": "Load or create a board first"
539 |                 }
540 | 
541 |             references = params.get("references", [])
542 |             alignment = params.get("alignment", "horizontal")  # horizontal, vertical, or edge
543 |             distribution = params.get("distribution", "none")  # none, equal, or spacing
544 |             spacing = params.get("spacing")
545 |             
546 |             if not references or len(references) < 2:
547 |                 return {
548 |                     "success": False,
549 |                     "message": "Missing references",
550 |                     "errorDetails": "At least two component references are required"
551 |                 }
552 |                 
553 |             # Find all referenced components
554 |             components = []
555 |             for ref in references:
556 |                 module = self.board.FindFootprintByReference(ref)
557 |                 if not module:
558 |                     return {
559 |                         "success": False,
560 |                         "message": "Component not found",
561 |                         "errorDetails": f"Could not find component: {ref}"
562 |                     }
563 |                 components.append(module)
564 |             
565 |             # Perform alignment based on selected option
566 |             if alignment == "horizontal":
567 |                 self._align_components_horizontally(components, distribution, spacing)
568 |             elif alignment == "vertical":
569 |                 self._align_components_vertically(components, distribution, spacing)
570 |             elif alignment == "edge":
571 |                 edge = params.get("edge")
572 |                 if not edge:
573 |                     return {
574 |                         "success": False,
575 |                         "message": "Missing edge parameter",
576 |                         "errorDetails": "Edge parameter is required for edge alignment"
577 |                     }
578 |                 self._align_components_to_edge(components, edge)
579 |             else:
580 |                 return {
581 |                     "success": False,
582 |                     "message": "Invalid alignment option",
583 |                     "errorDetails": "Alignment must be 'horizontal', 'vertical', or 'edge'"
584 |                 }
585 | 
586 |             # Prepare result data
587 |             aligned_components = []
588 |             for module in components:
589 |                 pos = module.GetPosition()
590 |                 aligned_components.append({
591 |                     "reference": module.GetReference(),
592 |                     "position": {
593 |                         "x": pos.x / 1000000,
594 |                         "y": pos.y / 1000000,
595 |                         "unit": "mm"
596 |                     },
597 |                     "rotation": module.GetOrientation() / 10
598 |                 })
599 | 
600 |             return {
601 |                 "success": True,
602 |                 "message": f"Aligned {len(components)} components",
603 |                 "alignment": alignment,
604 |                 "distribution": distribution,
605 |                 "components": aligned_components
606 |             }
607 | 
608 |         except Exception as e:
609 |             logger.error(f"Error aligning components: {str(e)}")
610 |             return {
611 |                 "success": False,
612 |                 "message": "Failed to align components",
613 |                 "errorDetails": str(e)
614 |             }
615 |             
616 |     def duplicate_component(self, params: Dict[str, Any]) -> Dict[str, Any]:
617 |         """Duplicate an existing component"""
618 |         try:
619 |             if not self.board:
620 |                 return {
621 |                     "success": False,
622 |                     "message": "No board is loaded",
623 |                     "errorDetails": "Load or create a board first"
624 |                 }
625 | 
626 |             reference = params.get("reference")
627 |             new_reference = params.get("newReference")
628 |             position = params.get("position")
629 |             rotation = params.get("rotation")
630 |             
631 |             if not reference or not new_reference:
632 |                 return {
633 |                     "success": False,
634 |                     "message": "Missing parameters",
635 |                     "errorDetails": "reference and newReference are required"
636 |                 }
637 |                 
638 |             # Find the source component
639 |             source = self.board.FindFootprintByReference(reference)
640 |             if not source:
641 |                 return {
642 |                     "success": False,
643 |                     "message": "Component not found",
644 |                     "errorDetails": f"Could not find component: {reference}"
645 |                 }
646 |                 
647 |             # Check if new reference already exists
648 |             if self.board.FindFootprintByReference(new_reference):
649 |                 return {
650 |                     "success": False,
651 |                     "message": "Reference already exists",
652 |                     "errorDetails": f"A component with reference {new_reference} already exists"
653 |                 }
654 |                 
655 |             # Create new footprint with the same properties
656 |             new_module = pcbnew.FOOTPRINT(self.board)
657 |             new_module.SetFootprintName(source.GetFootprintName())
658 |             new_module.SetValue(source.GetValue())
659 |             new_module.SetReference(new_reference)
660 |             new_module.SetLayer(source.GetLayer())
661 |             
662 |             # Copy pads and other items
663 |             for pad in source.Pads():
664 |                 new_pad = pcbnew.PAD(new_module)
665 |                 new_pad.Copy(pad)
666 |                 new_module.Add(new_pad)
667 |                 
668 |             # Set position if provided, otherwise use offset from original
669 |             if position:
670 |                 scale = 1000000 if position.get("unit", "mm") == "mm" else 25400000
671 |                 x_nm = int(position["x"] * scale)
672 |                 y_nm = int(position["y"] * scale)
673 |                 new_module.SetPosition(pcbnew.VECTOR2I(x_nm, y_nm))
674 |             else:
675 |                 # Offset by 5mm
676 |                 source_pos = source.GetPosition()
677 |                 new_module.SetPosition(pcbnew.VECTOR2I(source_pos.x + 5000000, source_pos.y))
678 |                 
679 |             # Set rotation if provided, otherwise use same as original
680 |             if rotation is not None:
681 |                 new_module.SetOrientation(rotation * 10)  # KiCAD uses decidegrees
682 |             else:
683 |                 new_module.SetOrientation(source.GetOrientation())
684 |                 
685 |             # Add to board
686 |             self.board.Add(new_module)
687 |             
688 |             # Get final position in mm
689 |             pos = new_module.GetPosition()
690 | 
691 |             return {
692 |                 "success": True,
693 |                 "message": f"Duplicated component {reference} to {new_reference}",
694 |                 "component": {
695 |                     "reference": new_reference,
696 |                     "value": new_module.GetValue(),
697 |                     "footprint": new_module.GetFootprintName(),
698 |                     "position": {
699 |                         "x": pos.x / 1000000,
700 |                         "y": pos.y / 1000000,
701 |                         "unit": "mm"
702 |                     },
703 |                     "rotation": new_module.GetOrientation() / 10,
704 |                     "layer": self.board.GetLayerName(new_module.GetLayer())
705 |                 }
706 |             }
707 | 
708 |         except Exception as e:
709 |             logger.error(f"Error duplicating component: {str(e)}")
710 |             return {
711 |                 "success": False,
712 |                 "message": "Failed to duplicate component",
713 |                 "errorDetails": str(e)
714 |             }
715 |             
716 |     def _place_grid_array(self, component_id: str, start_position: Dict[str, Any], 
717 |                        rows: int, columns: int, spacing_x: float, spacing_y: float,
718 |                        reference_prefix: str, value: str, rotation: float, layer: str) -> List[Dict[str, Any]]:
719 |         """Place components in a grid pattern and return the list of placed components"""
720 |         placed = []
721 |         
722 |         # Convert spacing to nm
723 |         unit = start_position.get("unit", "mm")
724 |         scale = 1000000 if unit == "mm" else 25400000  # mm or inch to nm
725 |         spacing_x_nm = int(spacing_x * scale)
726 |         spacing_y_nm = int(spacing_y * scale)
727 |         
728 |         # Get layer ID
729 |         layer_id = self.board.GetLayerID(layer)
730 |         
731 |         for row in range(rows):
732 |             for col in range(columns):
733 |                 # Calculate position
734 |                 x = start_position["x"] + (col * spacing_x)
735 |                 y = start_position["y"] + (row * spacing_y)
736 |                 
737 |                 # Generate reference
738 |                 index = row * columns + col + 1
739 |                 component_reference = f"{reference_prefix}{index}"
740 |                 
741 |                 # Place component
742 |                 result = self.place_component({
743 |                     "componentId": component_id,
744 |                     "position": {"x": x, "y": y, "unit": unit},
745 |                     "reference": component_reference,
746 |                     "value": value,
747 |                     "rotation": rotation,
748 |                     "layer": layer
749 |                 })
750 |                 
751 |                 if result["success"]:
752 |                     placed.append(result["component"])
753 |                 
754 |         return placed
755 |         
756 |     def _place_circular_array(self, component_id: str, center: Dict[str, Any], 
757 |                           radius: float, count: int, angle_start: float, 
758 |                           angle_step: float, reference_prefix: str, 
759 |                           value: str, rotation_offset: float, layer: str) -> List[Dict[str, Any]]:
760 |         """Place components in a circular pattern and return the list of placed components"""
761 |         placed = []
762 |         
763 |         # Get unit
764 |         unit = center.get("unit", "mm")
765 |         
766 |         for i in range(count):
767 |             # Calculate angle for this component
768 |             angle = angle_start + (i * angle_step)
769 |             angle_rad = math.radians(angle)
770 |             
771 |             # Calculate position
772 |             x = center["x"] + (radius * math.cos(angle_rad))
773 |             y = center["y"] + (radius * math.sin(angle_rad))
774 |             
775 |             # Generate reference
776 |             component_reference = f"{reference_prefix}{i+1}"
777 |             
778 |             # Calculate rotation (pointing outward from center)
779 |             component_rotation = angle + rotation_offset
780 |             
781 |             # Place component
782 |             result = self.place_component({
783 |                 "componentId": component_id,
784 |                 "position": {"x": x, "y": y, "unit": unit},
785 |                 "reference": component_reference,
786 |                 "value": value,
787 |                 "rotation": component_rotation,
788 |                 "layer": layer
789 |             })
790 |             
791 |             if result["success"]:
792 |                 placed.append(result["component"])
793 |                 
794 |         return placed
795 |         
796 |     def _align_components_horizontally(self, components: List[pcbnew.FOOTPRINT], 
797 |                                    distribution: str, spacing: Optional[float]) -> None:
798 |         """Align components horizontally and optionally distribute them"""
799 |         if not components:
800 |             return
801 |             
802 |         # Find the average Y coordinate
803 |         y_sum = sum(module.GetPosition().y for module in components)
804 |         y_avg = y_sum // len(components)
805 |         
806 |         # Sort components by X position
807 |         components.sort(key=lambda m: m.GetPosition().x)
808 |         
809 |         # Set Y coordinate for all components
810 |         for module in components:
811 |             pos = module.GetPosition()
812 |             module.SetPosition(pcbnew.VECTOR2I(pos.x, y_avg))
813 |             
814 |         # Handle distribution if requested
815 |         if distribution == "equal" and len(components) > 1:
816 |             # Get leftmost and rightmost X coordinates
817 |             x_min = components[0].GetPosition().x
818 |             x_max = components[-1].GetPosition().x
819 |             
820 |             # Calculate equal spacing
821 |             total_space = x_max - x_min
822 |             spacing_nm = total_space // (len(components) - 1)
823 |             
824 |             # Set X positions with equal spacing
825 |             for i in range(1, len(components) - 1):
826 |                 pos = components[i].GetPosition()
827 |                 new_x = x_min + (i * spacing_nm)
828 |                 components[i].SetPosition(pcbnew.VECTOR2I(new_x, pos.y))
829 |                 
830 |         elif distribution == "spacing" and spacing is not None:
831 |             # Convert spacing to nanometers
832 |             spacing_nm = int(spacing * 1000000)  # assuming mm
833 |             
834 |             # Set X positions with the specified spacing
835 |             x_current = components[0].GetPosition().x
836 |             for i in range(1, len(components)):
837 |                 pos = components[i].GetPosition()
838 |                 x_current += spacing_nm
839 |                 components[i].SetPosition(pcbnew.VECTOR2I(x_current, pos.y))
840 |                 
841 |     def _align_components_vertically(self, components: List[pcbnew.FOOTPRINT], 
842 |                                  distribution: str, spacing: Optional[float]) -> None:
843 |         """Align components vertically and optionally distribute them"""
844 |         if not components:
845 |             return
846 |             
847 |         # Find the average X coordinate
848 |         x_sum = sum(module.GetPosition().x for module in components)
849 |         x_avg = x_sum // len(components)
850 |         
851 |         # Sort components by Y position
852 |         components.sort(key=lambda m: m.GetPosition().y)
853 |         
854 |         # Set X coordinate for all components
855 |         for module in components:
856 |             pos = module.GetPosition()
857 |             module.SetPosition(pcbnew.VECTOR2I(x_avg, pos.y))
858 |             
859 |         # Handle distribution if requested
860 |         if distribution == "equal" and len(components) > 1:
861 |             # Get topmost and bottommost Y coordinates
862 |             y_min = components[0].GetPosition().y
863 |             y_max = components[-1].GetPosition().y
864 |             
865 |             # Calculate equal spacing
866 |             total_space = y_max - y_min
867 |             spacing_nm = total_space // (len(components) - 1)
868 |             
869 |             # Set Y positions with equal spacing
870 |             for i in range(1, len(components) - 1):
871 |                 pos = components[i].GetPosition()
872 |                 new_y = y_min + (i * spacing_nm)
873 |                 components[i].SetPosition(pcbnew.VECTOR2I(pos.x, new_y))
874 |                 
875 |         elif distribution == "spacing" and spacing is not None:
876 |             # Convert spacing to nanometers
877 |             spacing_nm = int(spacing * 1000000)  # assuming mm
878 |             
879 |             # Set Y positions with the specified spacing
880 |             y_current = components[0].GetPosition().y
881 |             for i in range(1, len(components)):
882 |                 pos = components[i].GetPosition()
883 |                 y_current += spacing_nm
884 |                 components[i].SetPosition(pcbnew.VECTOR2I(pos.x, y_current))
885 |                 
886 |     def _align_components_to_edge(self, components: List[pcbnew.FOOTPRINT], edge: str) -> None:
887 |         """Align components to the specified edge of the board"""
888 |         if not components:
889 |             return
890 |             
891 |         # Get board bounds
892 |         board_box = self.board.GetBoardEdgesBoundingBox()
893 |         left = board_box.GetLeft()
894 |         right = board_box.GetRight()
895 |         top = board_box.GetTop()
896 |         bottom = board_box.GetBottom()
897 |         
898 |         # Align based on specified edge
899 |         if edge == "left":
900 |             for module in components:
901 |                 pos = module.GetPosition()
902 |                 module.SetPosition(pcbnew.VECTOR2I(left + 2000000, pos.y))  # 2mm offset from edge
903 |         elif edge == "right":
904 |             for module in components:
905 |                 pos = module.GetPosition()
906 |                 module.SetPosition(pcbnew.VECTOR2I(right - 2000000, pos.y))  # 2mm offset from edge
907 |         elif edge == "top":
908 |             for module in components:
909 |                 pos = module.GetPosition()
910 |                 module.SetPosition(pcbnew.VECTOR2I(pos.x, top + 2000000))  # 2mm offset from edge
911 |         elif edge == "bottom":
912 |             for module in components:
913 |                 pos = module.GetPosition()
914 |                 module.SetPosition(pcbnew.VECTOR2I(pos.x, bottom - 2000000))  # 2mm offset from edge
915 |         else:
916 |             logger.warning(f"Unknown edge alignment: {edge}")
917 | 
```
Page 4/4FirstPrevNextLast