kicad-sch-api 0.4.0__py3-none-any.whl → 0.4.2__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.
Potentially problematic release.
This version of kicad-sch-api might be problematic. Click here for more details.
- kicad_sch_api/__init__.py +2 -2
- kicad_sch_api/cli/__init__.py +45 -0
- kicad_sch_api/cli/base.py +302 -0
- kicad_sch_api/cli/bom.py +164 -0
- kicad_sch_api/cli/erc.py +229 -0
- kicad_sch_api/cli/export_docs.py +289 -0
- kicad_sch_api/cli/netlist.py +94 -0
- kicad_sch_api/cli/types.py +43 -0
- kicad_sch_api/core/collections/__init__.py +5 -0
- kicad_sch_api/core/collections/base.py +248 -0
- kicad_sch_api/core/component_bounds.py +5 -0
- kicad_sch_api/core/components.py +142 -47
- kicad_sch_api/core/config.py +85 -3
- kicad_sch_api/core/factories/__init__.py +5 -0
- kicad_sch_api/core/factories/element_factory.py +276 -0
- kicad_sch_api/core/formatter.py +22 -5
- kicad_sch_api/core/junctions.py +26 -75
- kicad_sch_api/core/labels.py +28 -52
- kicad_sch_api/core/managers/file_io.py +3 -2
- kicad_sch_api/core/managers/metadata.py +6 -5
- kicad_sch_api/core/managers/validation.py +3 -2
- kicad_sch_api/core/managers/wire.py +7 -1
- kicad_sch_api/core/nets.py +38 -43
- kicad_sch_api/core/no_connects.py +29 -53
- kicad_sch_api/core/parser.py +75 -1765
- kicad_sch_api/core/schematic.py +211 -148
- kicad_sch_api/core/texts.py +28 -55
- kicad_sch_api/core/types.py +59 -18
- kicad_sch_api/core/wires.py +27 -75
- kicad_sch_api/parsers/elements/__init__.py +22 -0
- kicad_sch_api/parsers/elements/graphics_parser.py +564 -0
- kicad_sch_api/parsers/elements/label_parser.py +194 -0
- kicad_sch_api/parsers/elements/library_parser.py +165 -0
- kicad_sch_api/parsers/elements/metadata_parser.py +58 -0
- kicad_sch_api/parsers/elements/sheet_parser.py +352 -0
- kicad_sch_api/parsers/elements/symbol_parser.py +313 -0
- kicad_sch_api/parsers/elements/text_parser.py +250 -0
- kicad_sch_api/parsers/elements/wire_parser.py +242 -0
- kicad_sch_api/parsers/utils.py +80 -0
- kicad_sch_api/validation/__init__.py +25 -0
- kicad_sch_api/validation/erc.py +171 -0
- kicad_sch_api/validation/erc_models.py +203 -0
- kicad_sch_api/validation/pin_matrix.py +243 -0
- kicad_sch_api/validation/validators.py +391 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/METADATA +17 -9
- kicad_sch_api-0.4.2.dist-info/RECORD +87 -0
- kicad_sch_api/core/manhattan_routing.py +0 -430
- kicad_sch_api/core/simple_manhattan.py +0 -228
- kicad_sch_api/core/wire_routing.py +0 -380
- kicad_sch_api/parsers/label_parser.py +0 -254
- kicad_sch_api/parsers/symbol_parser.py +0 -222
- kicad_sch_api/parsers/wire_parser.py +0 -99
- kicad_sch_api-0.4.0.dist-info/RECORD +0 -67
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.4.0.dist-info → kicad_sch_api-0.4.2.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
|