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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. kicad_sch_api/__init__.py +2 -2
  2. kicad_sch_api/cli/__init__.py +45 -0
  3. kicad_sch_api/cli/base.py +302 -0
  4. kicad_sch_api/cli/bom.py +164 -0
  5. kicad_sch_api/cli/erc.py +229 -0
  6. kicad_sch_api/cli/export_docs.py +289 -0
  7. kicad_sch_api/cli/netlist.py +94 -0
  8. kicad_sch_api/cli/types.py +43 -0
  9. kicad_sch_api/core/collections/__init__.py +5 -0
  10. kicad_sch_api/core/collections/base.py +248 -0
  11. kicad_sch_api/core/component_bounds.py +5 -0
  12. kicad_sch_api/core/components.py +62 -45
  13. kicad_sch_api/core/config.py +85 -3
  14. kicad_sch_api/core/factories/__init__.py +5 -0
  15. kicad_sch_api/core/factories/element_factory.py +276 -0
  16. kicad_sch_api/core/junctions.py +26 -75
  17. kicad_sch_api/core/labels.py +28 -52
  18. kicad_sch_api/core/managers/file_io.py +3 -2
  19. kicad_sch_api/core/managers/metadata.py +6 -5
  20. kicad_sch_api/core/managers/validation.py +3 -2
  21. kicad_sch_api/core/managers/wire.py +7 -1
  22. kicad_sch_api/core/nets.py +38 -43
  23. kicad_sch_api/core/no_connects.py +29 -53
  24. kicad_sch_api/core/parser.py +75 -1765
  25. kicad_sch_api/core/schematic.py +211 -148
  26. kicad_sch_api/core/texts.py +28 -55
  27. kicad_sch_api/core/types.py +59 -18
  28. kicad_sch_api/core/wires.py +27 -75
  29. kicad_sch_api/parsers/elements/__init__.py +22 -0
  30. kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
  31. kicad_sch_api/parsers/elements/label_parser.py +194 -0
  32. kicad_sch_api/parsers/elements/library_parser.py +165 -0
  33. kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
  34. kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
  35. kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
  36. kicad_sch_api/parsers/elements/text_parser.py +250 -0
  37. kicad_sch_api/parsers/elements/wire_parser.py +242 -0
  38. kicad_sch_api/parsers/utils.py +80 -0
  39. kicad_sch_api/validation/__init__.py +25 -0
  40. kicad_sch_api/validation/erc.py +171 -0
  41. kicad_sch_api/validation/erc_models.py +203 -0
  42. kicad_sch_api/validation/pin_matrix.py +243 -0
  43. kicad_sch_api/validation/validators.py +391 -0
  44. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.1.dist-info}/METADATA +17 -9
  45. kicad_sch_api-0.4.1.dist-info/RECORD +87 -0
  46. kicad_sch_api/core/manhattan_routing.py +0 -430
  47. kicad_sch_api/core/simple_manhattan.py +0 -228
  48. kicad_sch_api/core/wire_routing.py +0 -380
  49. kicad_sch_api/parsers/label_parser.py +0 -254
  50. kicad_sch_api/parsers/symbol_parser.py +0 -222
  51. kicad_sch_api/parsers/wire_parser.py +0 -99
  52. kicad_sch_api-0.4.0.dist-info/RECORD +0 -67
  53. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.1.dist-info}/WHEEL +0 -0
  54. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.1.dist-info}/entry_points.txt +0 -0
  55. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.1.dist-info}/licenses/LICENSE +0 -0
  56. {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.1.dist-info}/top_level.txt +0 -0
@@ -1,430 +0,0 @@
1
- """
2
- Manhattan routing with obstacle avoidance for KiCAD schematics.
3
-
4
- This module implements grid-based pathfinding algorithms for routing wires
5
- around component obstacles while maintaining Manhattan (orthogonal) constraints
6
- and perfect KiCAD grid alignment.
7
-
8
- Based on research into EDA routing algorithms and adapted for schematic capture.
9
- """
10
-
11
- import heapq
12
- import logging
13
- from dataclasses import dataclass, field
14
- from enum import Enum
15
- from typing import Dict, List, Optional, Set, Tuple
16
-
17
- from .component_bounds import BoundingBox, check_path_collision, get_component_bounding_box
18
- from .types import Point, SchematicSymbol
19
- from .wire_routing import snap_to_kicad_grid
20
-
21
- logger = logging.getLogger(__name__)
22
-
23
-
24
- class RoutingStrategy(Enum):
25
- """Routing strategy options for different optimization goals."""
26
-
27
- SHORTEST = "shortest" # Minimize Manhattan distance
28
- CLEARANCE = "clearance" # Maximize obstacle clearance
29
- AESTHETIC = "aesthetic" # Balance distance, turns, and clearance
30
- DIRECT = "direct" # Fallback to direct routing
31
-
32
-
33
- @dataclass
34
- class GridPoint:
35
- """A point on the routing grid."""
36
-
37
- x: int
38
- y: int
39
-
40
- def __hash__(self) -> int:
41
- return hash((self.x, self.y))
42
-
43
- def __eq__(self, other) -> bool:
44
- return isinstance(other, GridPoint) and self.x == other.x and self.y == other.y
45
-
46
- def to_world_point(self, grid_size: float = 1.27) -> Point:
47
- """Convert grid coordinates to world coordinates."""
48
- return Point(self.x * grid_size, self.y * grid_size)
49
-
50
- @classmethod
51
- def from_world_point(cls, point: Point, grid_size: float = 1.27) -> "GridPoint":
52
- """Convert world coordinates to grid coordinates."""
53
- return cls(x=round(point.x / grid_size), y=round(point.y / grid_size))
54
-
55
- def manhattan_distance(self, other: "GridPoint") -> int:
56
- """Calculate Manhattan distance to another grid point."""
57
- return abs(self.x - other.x) + abs(self.y - other.y)
58
-
59
-
60
- @dataclass
61
- class PathNode:
62
- """Node for A* pathfinding algorithm."""
63
-
64
- position: GridPoint
65
- g_cost: float # Cost from start
66
- h_cost: float # Heuristic cost to goal
67
- parent: Optional["PathNode"] = None
68
-
69
- @property
70
- def f_cost(self) -> float:
71
- """Total cost for A* (g + h)."""
72
- return self.g_cost + self.h_cost
73
-
74
- def __lt__(self, other: "PathNode") -> bool:
75
- """For priority queue ordering."""
76
- return self.f_cost < other.f_cost
77
-
78
-
79
- @dataclass
80
- class RoutingGrid:
81
- """Grid representation for Manhattan routing."""
82
-
83
- grid_size: float = 1.27 # KiCAD standard grid in mm
84
- obstacles: Set[GridPoint] = field(default_factory=set)
85
- boundaries: Optional[BoundingBox] = None
86
- clearance_map: Dict[GridPoint, float] = field(default_factory=dict)
87
-
88
- def is_valid_point(self, point: GridPoint) -> bool:
89
- """Check if a grid point is valid for routing."""
90
- # Check if point is an obstacle
91
- if point in self.obstacles:
92
- return False
93
-
94
- # Check boundaries if defined
95
- if self.boundaries:
96
- world_point = point.to_world_point(self.grid_size)
97
- if not self.boundaries.contains_point(world_point):
98
- return False
99
-
100
- return True
101
-
102
- def get_neighbors(self, point: GridPoint) -> List[GridPoint]:
103
- """Get valid neighboring points (4-connected Manhattan)."""
104
- neighbors = []
105
- for dx, dy in [(0, 1), (1, 0), (0, -1), (-1, 0)]: # Up, Right, Down, Left
106
- neighbor = GridPoint(point.x + dx, point.y + dy)
107
- if self.is_valid_point(neighbor):
108
- neighbors.append(neighbor)
109
- return neighbors
110
-
111
- def get_clearance_cost(self, point: GridPoint) -> float:
112
- """Get clearance-based cost for routing strategies."""
113
- return self.clearance_map.get(point, 0.0)
114
-
115
-
116
- class ManhattanRouter:
117
- """Manhattan routing engine with obstacle avoidance."""
118
-
119
- def __init__(self, grid_size: float = 1.27, default_clearance: float = 1.27):
120
- """
121
- Initialize Manhattan router.
122
-
123
- Args:
124
- grid_size: Grid spacing in mm (KiCAD standard: 1.27mm)
125
- default_clearance: Default clearance from obstacles in mm
126
- """
127
- self.grid_size = grid_size
128
- self.default_clearance = default_clearance
129
-
130
- def route_between_points(
131
- self,
132
- start: Point,
133
- end: Point,
134
- obstacles: List[BoundingBox],
135
- strategy: RoutingStrategy = RoutingStrategy.AESTHETIC,
136
- clearance: Optional[float] = None,
137
- ) -> Optional[List[Point]]:
138
- """
139
- Route between two points avoiding obstacles.
140
-
141
- Args:
142
- start: Starting point in world coordinates
143
- end: Ending point in world coordinates
144
- obstacles: List of obstacle bounding boxes
145
- strategy: Routing strategy to use
146
- clearance: Obstacle clearance (uses default if None)
147
-
148
- Returns:
149
- List of waypoints for the route, or None if no route found
150
- """
151
- if clearance is None:
152
- clearance = self.default_clearance
153
-
154
- logger.debug(f"Routing from {start} to {end} with strategy {strategy.value}")
155
-
156
- # Check if direct path is clear
157
- if not check_path_collision(start, end, obstacles, clearance):
158
- logger.debug("Direct path is clear, using direct routing")
159
- return [start, end]
160
-
161
- # Grid-snap the start and end points
162
- start_snapped = snap_to_kicad_grid(start, self.grid_size)
163
- end_snapped = snap_to_kicad_grid(end, self.grid_size)
164
-
165
- # Build routing grid
166
- routing_grid = self._build_routing_grid(obstacles, clearance, start_snapped, end_snapped)
167
-
168
- # Convert to grid coordinates
169
- start_grid = GridPoint.from_world_point(start_snapped, self.grid_size)
170
- end_grid = GridPoint.from_world_point(end_snapped, self.grid_size)
171
-
172
- # Find path using A*
173
- path_grid = self._find_path_astar(start_grid, end_grid, routing_grid, strategy)
174
-
175
- if not path_grid:
176
- logger.warning(f"No path found from {start} to {end}")
177
- return None
178
-
179
- # Convert back to world coordinates
180
- path_world = [point.to_world_point(self.grid_size) for point in path_grid]
181
-
182
- # Optimize path
183
- optimized_path = self._optimize_path(path_world)
184
-
185
- logger.debug(f"Found path with {len(optimized_path)} waypoints")
186
- return optimized_path
187
-
188
- def _build_routing_grid(
189
- self, obstacles: List[BoundingBox], clearance: float, start: Point, end: Point
190
- ) -> RoutingGrid:
191
- """Build routing grid with obstacles and clearance information."""
192
- # Calculate grid bounds that encompass all obstacles plus start/end
193
- min_x = min(start.x, end.x)
194
- max_x = max(start.x, end.x)
195
- min_y = min(start.y, end.y)
196
- max_y = max(start.y, end.y)
197
-
198
- for obstacle in obstacles:
199
- min_x = min(min_x, obstacle.min_x - clearance)
200
- max_x = max(max_x, obstacle.max_x + clearance)
201
- min_y = min(min_y, obstacle.min_y - clearance)
202
- max_y = max(max_y, obstacle.max_y + clearance)
203
-
204
- # Add margin for routing
205
- margin = clearance * 2
206
- grid_bounds = BoundingBox(min_x - margin, min_y - margin, max_x + margin, max_y + margin)
207
-
208
- # Create grid and mark obstacles
209
- grid = RoutingGrid(self.grid_size, boundaries=grid_bounds)
210
-
211
- for obstacle in obstacles:
212
- self._mark_obstacle_on_grid(grid, obstacle, clearance)
213
-
214
- return grid
215
-
216
- def _mark_obstacle_on_grid(self, grid: RoutingGrid, obstacle: BoundingBox, clearance: float):
217
- """Mark obstacle area on routing grid."""
218
- # Expand obstacle by clearance
219
- expanded_obstacle = obstacle.expand(clearance)
220
-
221
- # Convert to grid coordinates
222
- min_grid_x = int((expanded_obstacle.min_x) // self.grid_size) - 1
223
- max_grid_x = int((expanded_obstacle.max_x) // self.grid_size) + 1
224
- min_grid_y = int((expanded_obstacle.min_y) // self.grid_size) - 1
225
- max_grid_y = int((expanded_obstacle.max_y) // self.grid_size) + 1
226
-
227
- # Mark obstacle points
228
- for gx in range(min_grid_x, max_grid_x + 1):
229
- for gy in range(min_grid_y, max_grid_y + 1):
230
- grid_point = GridPoint(gx, gy)
231
- world_point = grid_point.to_world_point(self.grid_size)
232
-
233
- if expanded_obstacle.contains_point(world_point):
234
- grid.obstacles.add(grid_point)
235
- else:
236
- # Calculate clearance cost for points near obstacles
237
- distance_to_obstacle = self._distance_to_bbox(world_point, obstacle)
238
- if distance_to_obstacle < clearance * 2:
239
- # Inverse cost - closer to obstacle = higher cost
240
- cost = max(0, clearance * 2 - distance_to_obstacle)
241
- grid.clearance_map[grid_point] = cost
242
-
243
- def _distance_to_bbox(self, point: Point, bbox: BoundingBox) -> float:
244
- """Calculate minimum distance from point to bounding box."""
245
- dx = max(bbox.min_x - point.x, 0, point.x - bbox.max_x)
246
- dy = max(bbox.min_y - point.y, 0, point.y - bbox.max_y)
247
- return (dx * dx + dy * dy) ** 0.5
248
-
249
- def _find_path_astar(
250
- self, start: GridPoint, goal: GridPoint, grid: RoutingGrid, strategy: RoutingStrategy
251
- ) -> Optional[List[GridPoint]]:
252
- """Find path using A* algorithm with strategy-specific costs."""
253
- if start == goal:
254
- return [start]
255
-
256
- # Priority queue for open set
257
- open_set = []
258
- open_dict: Dict[GridPoint, PathNode] = {}
259
- closed_set: Set[GridPoint] = set()
260
-
261
- # Initialize start node
262
- start_node = PathNode(
263
- position=start, g_cost=0, h_cost=self._heuristic_cost(start, goal, strategy)
264
- )
265
-
266
- heapq.heappush(open_set, start_node)
267
- open_dict[start] = start_node
268
-
269
- while open_set:
270
- # Get node with lowest f_cost
271
- current = heapq.heappop(open_set)
272
- del open_dict[current.position]
273
- closed_set.add(current.position)
274
-
275
- # Check if we reached the goal
276
- if current.position == goal:
277
- return self._reconstruct_path(current)
278
-
279
- # Check neighbors
280
- for neighbor_pos in grid.get_neighbors(current.position):
281
- if neighbor_pos in closed_set:
282
- continue
283
-
284
- # Calculate costs
285
- move_cost = self._movement_cost(current.position, neighbor_pos, grid, strategy)
286
- tentative_g = current.g_cost + move_cost
287
-
288
- # Check if this path to neighbor is better
289
- if neighbor_pos in open_dict:
290
- neighbor_node = open_dict[neighbor_pos]
291
- if tentative_g < neighbor_node.g_cost:
292
- neighbor_node.g_cost = tentative_g
293
- neighbor_node.parent = current
294
- else:
295
- # Add new node to open set
296
- neighbor_node = PathNode(
297
- position=neighbor_pos,
298
- g_cost=tentative_g,
299
- h_cost=self._heuristic_cost(neighbor_pos, goal, strategy),
300
- parent=current,
301
- )
302
- heapq.heappush(open_set, neighbor_node)
303
- open_dict[neighbor_pos] = neighbor_node
304
-
305
- # No path found
306
- return None
307
-
308
- def _heuristic_cost(
309
- self, point: GridPoint, goal: GridPoint, strategy: RoutingStrategy
310
- ) -> float:
311
- """Calculate heuristic cost based on strategy."""
312
- manhattan_dist = point.manhattan_distance(goal)
313
-
314
- if strategy == RoutingStrategy.SHORTEST:
315
- return float(manhattan_dist)
316
- elif strategy == RoutingStrategy.CLEARANCE:
317
- return float(manhattan_dist) * 0.5 # Lower weight on distance
318
- elif strategy == RoutingStrategy.AESTHETIC:
319
- return float(manhattan_dist) * 0.8 # Balanced weight
320
- else: # DIRECT fallback
321
- return float(manhattan_dist)
322
-
323
- def _movement_cost(
324
- self,
325
- from_point: GridPoint,
326
- to_point: GridPoint,
327
- grid: RoutingGrid,
328
- strategy: RoutingStrategy,
329
- ) -> float:
330
- """Calculate cost of moving between two adjacent grid points."""
331
- base_cost = 1.0 # Base movement cost
332
-
333
- if strategy == RoutingStrategy.SHORTEST:
334
- return base_cost
335
-
336
- elif strategy == RoutingStrategy.CLEARANCE:
337
- # Higher cost for points near obstacles
338
- clearance_cost = grid.get_clearance_cost(to_point)
339
- return base_cost + clearance_cost * 2.0
340
-
341
- elif strategy == RoutingStrategy.AESTHETIC:
342
- # Balance clearance with reasonable cost
343
- clearance_cost = grid.get_clearance_cost(to_point)
344
- return base_cost + clearance_cost * 0.5
345
-
346
- else: # DIRECT fallback
347
- return base_cost
348
-
349
- def _reconstruct_path(self, goal_node: PathNode) -> List[GridPoint]:
350
- """Reconstruct path from goal node back to start."""
351
- path = []
352
- current = goal_node
353
-
354
- while current:
355
- path.append(current.position)
356
- current = current.parent
357
-
358
- path.reverse()
359
- return path
360
-
361
- def _optimize_path(self, path: List[Point]) -> List[Point]:
362
- """Optimize path by removing unnecessary waypoints."""
363
- if len(path) < 3:
364
- return path
365
-
366
- optimized = [path[0]] # Always keep start point
367
-
368
- for i in range(1, len(path) - 1):
369
- prev_point = optimized[-1]
370
- current_point = path[i]
371
- next_point = path[i + 1]
372
-
373
- # Check if we can skip current point (collinear points)
374
- if not self._are_collinear(prev_point, current_point, next_point):
375
- optimized.append(current_point)
376
-
377
- optimized.append(path[-1]) # Always keep end point
378
- return optimized
379
-
380
- def _are_collinear(self, p1: Point, p2: Point, p3: Point, tolerance: float = 0.01) -> bool:
381
- """Check if three points are collinear (on the same line)."""
382
- # For Manhattan routing, points are collinear if they form a straight horizontal or vertical line
383
- horizontal = abs(p1.y - p2.y) < tolerance and abs(p2.y - p3.y) < tolerance
384
- vertical = abs(p1.x - p2.x) < tolerance and abs(p2.x - p3.x) < tolerance
385
- return horizontal or vertical
386
-
387
-
388
- def route_around_obstacles(
389
- start: Point,
390
- end: Point,
391
- components: List[SchematicSymbol],
392
- strategy: RoutingStrategy = RoutingStrategy.AESTHETIC,
393
- clearance: float = 1.27,
394
- grid_size: float = 1.27,
395
- ) -> Optional[List[Point]]:
396
- """
397
- High-level function to route between two points avoiding component obstacles.
398
-
399
- Args:
400
- start: Starting point in world coordinates
401
- end: Ending point in world coordinates
402
- components: List of components to avoid
403
- strategy: Routing strategy to use
404
- clearance: Minimum clearance from components
405
- grid_size: Grid spacing for routing
406
-
407
- Returns:
408
- List of waypoints for the route, or None if no route found
409
- """
410
- logger.debug(f"🛣️ Manhattan routing: {start} → {end}, strategy={strategy.value}")
411
-
412
- # Get component bounding boxes
413
- obstacles = []
414
- for component in components:
415
- bbox = get_component_bounding_box(component, include_properties=False)
416
- obstacles.append(bbox)
417
- logger.debug(f" Obstacle: {component.reference} at {bbox}")
418
-
419
- # Create router and find path
420
- router = ManhattanRouter(grid_size, clearance)
421
- path = router.route_between_points(start, end, obstacles, strategy, clearance)
422
-
423
- if path:
424
- logger.debug(f" ✅ Route found with {len(path)} waypoints")
425
- for i, point in enumerate(path):
426
- logger.debug(f" {i}: {point}")
427
- else:
428
- logger.warning(f" ❌ No route found")
429
-
430
- return path
@@ -1,228 +0,0 @@
1
- """
2
- Simple Manhattan routing with basic obstacle avoidance.
3
-
4
- This module provides a simple, working implementation of Manhattan routing
5
- that can be integrated with the existing auto_route_pins API.
6
- """
7
-
8
- import logging
9
- from typing import List, Optional, Tuple
10
-
11
- from .component_bounds import BoundingBox, get_component_bounding_box
12
- from .types import Point, SchematicSymbol
13
-
14
- logger = logging.getLogger(__name__)
15
-
16
-
17
- def simple_manhattan_route(start: Point, end: Point) -> List[Point]:
18
- """
19
- Create a simple L-shaped Manhattan route between two points.
20
-
21
- Args:
22
- start: Starting point
23
- end: Ending point
24
-
25
- Returns:
26
- List of waypoints for L-shaped route
27
- """
28
- # Simple L-route: horizontal first, then vertical
29
- if abs(start.x - end.x) > 0.1: # Need horizontal segment
30
- if abs(start.y - end.y) > 0.1: # Need vertical segment too
31
- # L-shaped route: start -> corner -> end
32
- corner = Point(end.x, start.y)
33
- return [start, corner, end]
34
- else:
35
- # Pure horizontal
36
- return [start, end]
37
- else:
38
- # Pure vertical or same point
39
- return [start, end]
40
-
41
-
42
- def check_horizontal_line_collision(start: Point, end: Point, bbox: BoundingBox) -> bool:
43
- """Check if a horizontal line collides with a bounding box."""
44
- line_y = start.y
45
- line_min_x = min(start.x, end.x)
46
- line_max_x = max(start.x, end.x)
47
-
48
- # Check Y range collision
49
- if not (bbox.min_y <= line_y <= bbox.max_y):
50
- return False
51
-
52
- # Check X range collision
53
- if line_max_x < bbox.min_x or line_min_x > bbox.max_x:
54
- return False
55
-
56
- return True
57
-
58
-
59
- def simple_obstacle_avoidance_route(
60
- start: Point, end: Point, obstacles: List[BoundingBox], clearance: float = 2.54
61
- ) -> List[Point]:
62
- """
63
- Route around obstacles with simple above/below strategy.
64
-
65
- Args:
66
- start: Starting point
67
- end: Ending point
68
- obstacles: List of obstacle bounding boxes
69
- clearance: Clearance distance from obstacles
70
-
71
- Returns:
72
- List of waypoints avoiding obstacles
73
- """
74
- # Try direct L-shaped route first
75
- direct_route = simple_manhattan_route(start, end)
76
-
77
- # Check if any segment collides with obstacles
78
- collision_detected = False
79
- for i in range(len(direct_route) - 1):
80
- seg_start = direct_route[i]
81
- seg_end = direct_route[i + 1]
82
-
83
- for obstacle in obstacles:
84
- if check_horizontal_line_collision(seg_start, seg_end, obstacle):
85
- collision_detected = True
86
- logger.debug(
87
- f"Collision detected between {seg_start} -> {seg_end} and obstacle {obstacle}"
88
- )
89
- break
90
- if collision_detected:
91
- break
92
-
93
- if not collision_detected:
94
- logger.debug("Direct route is clear")
95
- return direct_route
96
-
97
- # Find obstacles that block the path
98
- blocking_obstacles = []
99
- for obstacle in obstacles:
100
- # Check if obstacle is roughly between start and end points
101
- if min(start.x, end.x) < obstacle.max_x and max(start.x, end.x) > obstacle.min_x:
102
- blocking_obstacles.append(obstacle)
103
-
104
- if not blocking_obstacles:
105
- logger.debug("No blocking obstacles found, using direct route")
106
- return direct_route
107
-
108
- # Find the combined bounding area of all blocking obstacles
109
- combined_min_y = min(obs.min_y for obs in blocking_obstacles)
110
- combined_max_y = max(obs.max_y for obs in blocking_obstacles)
111
-
112
- # Calculate routing options
113
- above_y = combined_max_y + clearance
114
- below_y = combined_min_y - clearance
115
-
116
- # Route above obstacles
117
- route_above = [
118
- start,
119
- Point(start.x, above_y), # Go up
120
- Point(end.x, above_y), # Go across above obstacles
121
- end, # Go down to destination
122
- ]
123
-
124
- # Route below obstacles
125
- route_below = [
126
- start,
127
- Point(start.x, below_y), # Go down
128
- Point(end.x, below_y), # Go across below obstacles
129
- end, # Go up to destination
130
- ]
131
-
132
- # Calculate Manhattan distances
133
- def manhattan_distance(route):
134
- total = 0
135
- for i in range(len(route) - 1):
136
- total += abs(route[i + 1].x - route[i].x) + abs(route[i + 1].y - route[i].y)
137
- return total
138
-
139
- above_distance = manhattan_distance(route_above)
140
- below_distance = manhattan_distance(route_below)
141
-
142
- # Choose shorter route
143
- if above_distance <= below_distance:
144
- chosen_route = route_above
145
- choice = "above"
146
- else:
147
- chosen_route = route_below
148
- choice = "below"
149
-
150
- logger.debug(
151
- f"Obstacle avoidance: routing {choice} obstacles (distance: {min(above_distance, below_distance):.1f}mm)"
152
- )
153
- return chosen_route
154
-
155
-
156
- def auto_route_with_manhattan(
157
- schematic,
158
- start_component: SchematicSymbol,
159
- start_pin: str,
160
- end_component: SchematicSymbol,
161
- end_pin: str,
162
- avoid_components: Optional[List[SchematicSymbol]] = None,
163
- clearance: float = 2.54,
164
- ) -> Optional[str]:
165
- """
166
- Auto route between pins using Manhattan routing with obstacle avoidance.
167
-
168
- Args:
169
- schematic: The schematic object
170
- start_component: Starting component
171
- start_pin: Starting pin number
172
- end_component: Ending component
173
- end_pin: Ending pin number
174
- avoid_components: Components to avoid (if None, avoid all components)
175
- clearance: Clearance distance from obstacles
176
-
177
- Returns:
178
- Wire UUID if successful, None if failed
179
- """
180
- # Get pin positions
181
- from .pin_utils import get_component_pin_position
182
-
183
- start_pos = get_component_pin_position(start_component, start_pin)
184
- end_pos = get_component_pin_position(end_component, end_pin)
185
-
186
- if not start_pos or not end_pos:
187
- logger.warning("Could not determine pin positions")
188
- return None
189
-
190
- logger.debug(
191
- f"Manhattan routing: {start_component.reference} pin {start_pin} -> {end_component.reference} pin {end_pin}"
192
- )
193
- logger.debug(f" From: {start_pos}")
194
- logger.debug(f" To: {end_pos}")
195
-
196
- # Get obstacle bounding boxes
197
- obstacles = []
198
- if avoid_components is None:
199
- # Avoid all components in schematic except start and end
200
- avoid_components = [
201
- comp
202
- for comp in schematic.components
203
- if comp.reference != start_component.reference
204
- and comp.reference != end_component.reference
205
- ]
206
-
207
- for component in avoid_components:
208
- bbox = get_component_bounding_box(component, include_properties=False)
209
- obstacles.append(bbox)
210
- logger.debug(f" Obstacle: {component.reference} at {bbox}")
211
-
212
- # Calculate route
213
- route = simple_obstacle_avoidance_route(start_pos, end_pos, obstacles, clearance)
214
-
215
- logger.debug(f" Route: {len(route)} waypoints")
216
- for i, point in enumerate(route):
217
- logger.debug(f" {i}: {point}")
218
-
219
- # Add wires to schematic
220
- wire_uuids = []
221
- for i in range(len(route) - 1):
222
- wire_uuid = schematic.add_wire(route[i], route[i + 1])
223
- wire_uuids.append(wire_uuid)
224
-
225
- logger.debug(f"Added {len(wire_uuids)} wire segments")
226
-
227
- # Return first wire UUID (for compatibility)
228
- return wire_uuids[0] if wire_uuids else None