kicad-sch-api 0.2.0__py3-none-any.whl → 0.2.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.
@@ -0,0 +1,228 @@
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
@@ -190,7 +190,7 @@ class Wire:
190
190
  self.wire_type = (
191
191
  WireType(self.wire_type) if isinstance(self.wire_type, str) else self.wire_type
192
192
  )
193
-
193
+
194
194
  # Ensure we have at least 2 points
195
195
  if len(self.points) < 2:
196
196
  raise ValueError("Wire must have at least 2 points")
@@ -259,7 +259,7 @@ class LabelType(Enum):
259
259
 
260
260
  class HierarchicalLabelShape(Enum):
261
261
  """Hierarchical label shapes/directions."""
262
-
262
+
263
263
  INPUT = "input"
264
264
  OUTPUT = "output"
265
265
  BIDIRECTIONAL = "bidirectional"
@@ -287,7 +287,7 @@ class Label:
287
287
  self.label_type = (
288
288
  LabelType(self.label_type) if isinstance(self.label_type, str) else self.label_type
289
289
  )
290
-
290
+
291
291
  if self.shape:
292
292
  self.shape = (
293
293
  HierarchicalLabelShape(self.shape) if isinstance(self.shape, str) else self.shape
@@ -320,7 +320,12 @@ class TextBox:
320
320
  text: str
321
321
  rotation: float = 0.0
322
322
  font_size: float = 1.27
323
- margins: Tuple[float, float, float, float] = (0.9525, 0.9525, 0.9525, 0.9525) # top, right, bottom, left
323
+ margins: Tuple[float, float, float, float] = (
324
+ 0.9525,
325
+ 0.9525,
326
+ 0.9525,
327
+ 0.9525,
328
+ ) # top, right, bottom, left
324
329
  stroke_width: float = 0.0
325
330
  stroke_type: str = "solid"
326
331
  fill_type: str = "none"
@@ -0,0 +1,380 @@
1
+ """
2
+ Simple wire routing and connectivity detection for KiCAD schematics.
3
+
4
+ Provides basic wire routing between component pins and pin connectivity detection.
5
+ All positioning follows KiCAD's 1.27mm grid alignment rules.
6
+ """
7
+
8
+ from typing import List, Optional, Tuple, Union
9
+
10
+ from .types import Point
11
+
12
+
13
+ def snap_to_kicad_grid(
14
+ position: Union[Point, Tuple[float, float]], grid_size: float = 1.27
15
+ ) -> Point:
16
+ """
17
+ Snap position to KiCAD's standard 1.27mm grid.
18
+
19
+ KiCAD uses a 1.27mm (0.05 inch) grid for precise electrical connections.
20
+ ALL components, wires, and labels must be exactly on grid points.
21
+
22
+ Args:
23
+ position: Point or (x, y) tuple to snap
24
+ grid_size: Grid size in mm (default 1.27mm)
25
+
26
+ Returns:
27
+ Point snapped to grid
28
+ """
29
+ if isinstance(position, Point):
30
+ x, y = position.x, position.y
31
+ else:
32
+ x, y = position
33
+
34
+ # Round to nearest grid point
35
+ snapped_x = round(x / grid_size) * grid_size
36
+ snapped_y = round(y / grid_size) * grid_size
37
+
38
+ return Point(snapped_x, snapped_y)
39
+
40
+
41
+ def calculate_component_position_for_grid_pins(
42
+ pin_offsets: List[Tuple[float, float]], target_pin_grid_pos: Point, target_pin_index: int = 0
43
+ ) -> Point:
44
+ """
45
+ Calculate component position so that pins land exactly on grid points.
46
+
47
+ Args:
48
+ pin_offsets: List of (x_offset, y_offset) tuples for each pin relative to component center
49
+ target_pin_grid_pos: Desired grid position for the target pin
50
+ target_pin_index: Index of pin to align to grid (default: pin 0)
51
+
52
+ Returns:
53
+ Component center position that places target pin on grid
54
+ """
55
+ target_offset_x, target_offset_y = pin_offsets[target_pin_index]
56
+
57
+ # Component center = target pin position - pin offset
58
+ comp_center_x = target_pin_grid_pos.x - target_offset_x
59
+ comp_center_y = target_pin_grid_pos.y - target_offset_y
60
+
61
+ return Point(comp_center_x, comp_center_y)
62
+
63
+
64
+ def get_resistor_grid_position(target_pin1_grid: Point) -> Point:
65
+ """
66
+ Get component position for Device:R resistor so pin 1 is at target grid position.
67
+
68
+ Device:R resistor pin offsets: Pin 1 = (0, +3.81), Pin 2 = (0, -3.81)
69
+
70
+ Args:
71
+ target_pin1_grid: Desired grid position for pin 1
72
+
73
+ Returns:
74
+ Component center position
75
+ """
76
+ # Device:R pin offsets (standard KiCAD library values)
77
+ pin_offsets = [(0.0, 3.81), (0.0, -3.81)] # Pin 1 and Pin 2 offsets
78
+ return calculate_component_position_for_grid_pins(pin_offsets, target_pin1_grid, 0)
79
+
80
+
81
+ def route_pins_direct(pin1_position: Point, pin2_position: Point) -> Point:
82
+ """
83
+ Simple direct routing between two pins.
84
+ Just draws a straight wire - ok to go through other components.
85
+
86
+ Args:
87
+ pin1_position: Position of first pin
88
+ pin2_position: Position of second pin
89
+
90
+ Returns:
91
+ End point (pin2_position) for wire creation
92
+ """
93
+ return pin2_position
94
+
95
+
96
+ def are_pins_connected(
97
+ schematic, comp1_ref: str, pin1_num: str, comp2_ref: str, pin2_num: str
98
+ ) -> bool:
99
+ """
100
+ Detect if two component pins are connected via wires or labels (local or hierarchical).
101
+
102
+ Checks for electrical connectivity through:
103
+ 1. Direct wire connections
104
+ 2. Indirect wire network connections
105
+ 3. Local labels on connected nets
106
+ 4. Hierarchical labels for inter-sheet connections
107
+
108
+ Args:
109
+ schematic: The schematic object to analyze
110
+ comp1_ref: Reference of first component (e.g., 'R1')
111
+ pin1_num: Pin number on first component (e.g., '1')
112
+ comp2_ref: Reference of second component (e.g., 'R2')
113
+ pin2_num: Pin number on second component (e.g., '2')
114
+
115
+ Returns:
116
+ True if pins are electrically connected, False otherwise
117
+ """
118
+ # Get pin positions
119
+ pin1_pos = schematic.get_component_pin_position(comp1_ref, pin1_num)
120
+ pin2_pos = schematic.get_component_pin_position(comp2_ref, pin2_num)
121
+
122
+ if not pin1_pos or not pin2_pos:
123
+ return False
124
+
125
+ # 1. Check for direct wire connection between the pins
126
+ for wire in schematic.wires:
127
+ wire_start = wire.points[0]
128
+ wire_end = wire.points[-1] # Last point for multi-segment wires
129
+
130
+ # Check if wire directly connects the two pins
131
+ if (
132
+ wire_start.x == pin1_pos.x
133
+ and wire_start.y == pin1_pos.y
134
+ and wire_end.x == pin2_pos.x
135
+ and wire_end.y == pin2_pos.y
136
+ ) or (
137
+ wire_start.x == pin2_pos.x
138
+ and wire_start.y == pin2_pos.y
139
+ and wire_end.x == pin1_pos.x
140
+ and wire_end.y == pin1_pos.y
141
+ ):
142
+ return True
143
+
144
+ # 2. Check for indirect connection through wire network
145
+ visited_wires = set()
146
+ if _pins_connected_via_wire_network(schematic, pin1_pos, pin2_pos, visited_wires):
147
+ return True
148
+
149
+ # 3. Check for connection via local labels
150
+ if _pins_connected_via_labels(schematic, pin1_pos, pin2_pos):
151
+ return True
152
+
153
+ # 4. Check for connection via hierarchical labels
154
+ if _pins_connected_via_hierarchical_labels(schematic, pin1_pos, pin2_pos):
155
+ return True
156
+
157
+ return False
158
+
159
+
160
+ def _pins_connected_via_wire_network(
161
+ schematic, pin1_pos: Point, pin2_pos: Point, visited_wires: set
162
+ ) -> bool:
163
+ """
164
+ Check if pins are connected through wire network tracing.
165
+
166
+ Args:
167
+ schematic: The schematic object
168
+ pin1_pos: First pin position
169
+ pin2_pos: Second pin position
170
+ visited_wires: Set to track visited wires
171
+
172
+ Returns:
173
+ True if pins connected via wire network
174
+ """
175
+ # Find all wires connected to pin1
176
+ pin1_wires = []
177
+ for wire in schematic.wires:
178
+ for point in wire.points:
179
+ if point.x == pin1_pos.x and point.y == pin1_pos.y:
180
+ pin1_wires.append(wire)
181
+ break
182
+
183
+ # For each wire connected to pin1, trace the network to see if it reaches pin2
184
+ visited_wire_uuids = set() # Use UUIDs instead of Wire objects
185
+ for start_wire in pin1_wires:
186
+ if _trace_wire_network(schematic, start_wire, pin2_pos, visited_wire_uuids):
187
+ return True
188
+
189
+ return False
190
+
191
+
192
+ def _pins_connected_via_labels(schematic, pin1_pos: Point, pin2_pos: Point) -> bool:
193
+ """
194
+ Check if pins are connected via local labels (net names).
195
+
196
+ Two pins are connected if they're on nets with the same label name.
197
+
198
+ Args:
199
+ schematic: The schematic object
200
+ pin1_pos: First pin position
201
+ pin2_pos: Second pin position
202
+
203
+ Returns:
204
+ True if pins connected via same local label
205
+ """
206
+ # Get labels connected to pin1's net
207
+ pin1_labels = _get_net_labels_at_position(schematic, pin1_pos)
208
+ if not pin1_labels:
209
+ return False
210
+
211
+ # Get labels connected to pin2's net
212
+ pin2_labels = _get_net_labels_at_position(schematic, pin2_pos)
213
+ if not pin2_labels:
214
+ return False
215
+
216
+ # Check if any label names match (case-insensitive)
217
+ pin1_names = {label.text.upper() for label in pin1_labels}
218
+ pin2_names = {label.text.upper() for label in pin2_labels}
219
+
220
+ return bool(pin1_names.intersection(pin2_names))
221
+
222
+
223
+ def _pins_connected_via_hierarchical_labels(schematic, pin1_pos: Point, pin2_pos: Point) -> bool:
224
+ """
225
+ Check if pins are connected via hierarchical labels.
226
+
227
+ Hierarchical labels create connections between different sheets in a hierarchy.
228
+
229
+ Args:
230
+ schematic: The schematic object
231
+ pin1_pos: First pin position
232
+ pin2_pos: Second pin position
233
+
234
+ Returns:
235
+ True if pins connected via hierarchical labels
236
+ """
237
+ # Get hierarchical labels connected to pin1's net
238
+ pin1_hier_labels = _get_hierarchical_labels_at_position(schematic, pin1_pos)
239
+ if not pin1_hier_labels:
240
+ return False
241
+
242
+ # Get hierarchical labels connected to pin2's net
243
+ pin2_hier_labels = _get_hierarchical_labels_at_position(schematic, pin2_pos)
244
+ if not pin2_hier_labels:
245
+ return False
246
+
247
+ # Check if any hierarchical label names match (case-insensitive)
248
+ pin1_names = {label.text.upper() for label in pin1_hier_labels}
249
+ pin2_names = {label.text.upper() for label in pin2_hier_labels}
250
+
251
+ return bool(pin1_names.intersection(pin2_names))
252
+
253
+
254
+ def _get_net_labels_at_position(schematic, position: Point) -> List:
255
+ """
256
+ Get all local labels connected to the wire network at the given position.
257
+
258
+ Uses coordinate proximity matching like kicad-skip (0.6mm tolerance).
259
+
260
+ Args:
261
+ schematic: The schematic object
262
+ position: Pin position to check
263
+
264
+ Returns:
265
+ List of labels connected to this position's net
266
+ """
267
+ connected_labels = []
268
+ tolerance = 0.0 # Zero tolerance - KiCAD requires exact coordinate matching
269
+
270
+ # Find all wires connected to this position
271
+ connected_wires = []
272
+ for wire in schematic.wires:
273
+ for point in wire.points:
274
+ if point.x == position.x and point.y == position.y:
275
+ connected_wires.append(wire)
276
+ break
277
+
278
+ # Find labels near any connected wire points
279
+ labels_data = schematic._data.get("labels", [])
280
+ for label_dict in labels_data:
281
+ label_pos_dict = label_dict.get("position", {})
282
+ label_pos = Point(label_pos_dict.get("x", 0), label_pos_dict.get("y", 0))
283
+
284
+ # Check if label is near any connected wire
285
+ for wire in connected_wires:
286
+ for wire_point in wire.points:
287
+ if label_pos.x == wire_point.x and label_pos.y == wire_point.y:
288
+ # Create a simple object with text attribute
289
+ class SimpleLabel:
290
+ def __init__(self, text):
291
+ self.text = text
292
+
293
+ connected_labels.append(SimpleLabel(label_dict.get("text", "")))
294
+ break
295
+
296
+ return connected_labels
297
+
298
+
299
+ def _get_hierarchical_labels_at_position(schematic, position: Point) -> List:
300
+ """
301
+ Get all hierarchical labels connected to the wire network at the given position.
302
+
303
+ Args:
304
+ schematic: The schematic object
305
+ position: Pin position to check
306
+
307
+ Returns:
308
+ List of hierarchical labels connected to this position's net
309
+ """
310
+ connected_labels = []
311
+ tolerance = 0.0 # Zero tolerance - KiCAD requires exact coordinate matching
312
+
313
+ # Find all wires connected to this position
314
+ connected_wires = []
315
+ for wire in schematic.wires:
316
+ for point in wire.points:
317
+ if point.x == position.x and point.y == position.y:
318
+ connected_wires.append(wire)
319
+ break
320
+
321
+ # Find hierarchical labels near any connected wire points
322
+ hier_labels_data = schematic._data.get("hierarchical_labels", [])
323
+ for label_dict in hier_labels_data:
324
+ label_pos_dict = label_dict.get("position", {})
325
+ label_pos = Point(label_pos_dict.get("x", 0), label_pos_dict.get("y", 0))
326
+
327
+ # Check if hierarchical label is near any connected wire
328
+ for wire in connected_wires:
329
+ for wire_point in wire.points:
330
+ if label_pos.x == wire_point.x and label_pos.y == wire_point.y:
331
+ # Create a simple object with text attribute
332
+ class SimpleLabel:
333
+ def __init__(self, text):
334
+ self.text = text
335
+
336
+ connected_labels.append(SimpleLabel(label_dict.get("text", "")))
337
+ break
338
+
339
+ return connected_labels
340
+
341
+
342
+ def _trace_wire_network(
343
+ schematic, current_wire, target_position: Point, visited_uuids: set
344
+ ) -> bool:
345
+ """
346
+ Recursively trace wire network to find if it reaches target position.
347
+
348
+ Args:
349
+ schematic: The schematic object
350
+ current_wire: Current wire being traced
351
+ target_position: Target pin position we're looking for
352
+ visited_uuids: Set of already visited wire UUIDs to prevent infinite loops
353
+
354
+ Returns:
355
+ True if network reaches target position
356
+ """
357
+ wire_uuid = current_wire.uuid
358
+ if wire_uuid in visited_uuids:
359
+ return False
360
+
361
+ visited_uuids.add(wire_uuid)
362
+
363
+ # Check if current wire reaches target
364
+ for point in current_wire.points:
365
+ if point.x == target_position.x and point.y == target_position.y:
366
+ return True
367
+
368
+ # Find other wires connected to this wire's endpoints and trace them
369
+ for wire_point in current_wire.points:
370
+ for other_wire in schematic.wires:
371
+ if other_wire.uuid == wire_uuid or other_wire.uuid in visited_uuids:
372
+ continue
373
+
374
+ # Check if other wire shares an endpoint with current wire
375
+ for other_point in other_wire.points:
376
+ if wire_point.x == other_point.x and wire_point.y == other_point.y:
377
+ if _trace_wire_network(schematic, other_wire, target_position, visited_uuids):
378
+ return True
379
+
380
+ return False