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 |
```