kicad-sch-api 0.2.0__py3-none-any.whl → 0.2.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 +6 -2
- kicad_sch_api/cli.py +67 -62
- kicad_sch_api/core/component_bounds.py +477 -0
- kicad_sch_api/core/components.py +22 -10
- kicad_sch_api/core/config.py +127 -0
- kicad_sch_api/core/formatter.py +190 -24
- kicad_sch_api/core/geometry.py +111 -0
- kicad_sch_api/core/ic_manager.py +43 -37
- kicad_sch_api/core/junctions.py +17 -22
- kicad_sch_api/core/manhattan_routing.py +430 -0
- kicad_sch_api/core/parser.py +587 -197
- kicad_sch_api/core/pin_utils.py +149 -0
- kicad_sch_api/core/schematic.py +683 -207
- kicad_sch_api/core/simple_manhattan.py +228 -0
- kicad_sch_api/core/types.py +44 -4
- kicad_sch_api/core/wire_routing.py +380 -0
- kicad_sch_api/core/wires.py +29 -25
- kicad_sch_api/discovery/__init__.py +1 -1
- kicad_sch_api/discovery/search_index.py +142 -107
- kicad_sch_api/library/cache.py +70 -62
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.2.dist-info}/METADATA +212 -40
- kicad_sch_api-0.2.2.dist-info/RECORD +31 -0
- kicad_sch_api-0.2.0.dist-info/RECORD +0 -24
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.2.dist-info}/WHEEL +0 -0
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.2.dist-info}/entry_points.txt +0 -0
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.2.dist-info}/licenses/LICENSE +0 -0
- {kicad_sch_api-0.2.0.dist-info → kicad_sch_api-0.2.2.dist-info}/top_level.txt +0 -0
|
@@ -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
|
kicad_sch_api/core/types.py
CHANGED
|
@@ -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] = (
|
|
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"
|
|
@@ -333,6 +338,40 @@ class TextBox:
|
|
|
333
338
|
self.uuid = str(uuid4())
|
|
334
339
|
|
|
335
340
|
|
|
341
|
+
@dataclass
|
|
342
|
+
class SchematicRectangle:
|
|
343
|
+
"""Graphical rectangle element in schematic."""
|
|
344
|
+
|
|
345
|
+
uuid: str
|
|
346
|
+
start: Point
|
|
347
|
+
end: Point
|
|
348
|
+
stroke_width: float = 0.0
|
|
349
|
+
stroke_type: str = "default"
|
|
350
|
+
fill_type: str = "none"
|
|
351
|
+
|
|
352
|
+
def __post_init__(self):
|
|
353
|
+
if not self.uuid:
|
|
354
|
+
self.uuid = str(uuid4())
|
|
355
|
+
|
|
356
|
+
@property
|
|
357
|
+
def width(self) -> float:
|
|
358
|
+
"""Rectangle width."""
|
|
359
|
+
return abs(self.end.x - self.start.x)
|
|
360
|
+
|
|
361
|
+
@property
|
|
362
|
+
def height(self) -> float:
|
|
363
|
+
"""Rectangle height."""
|
|
364
|
+
return abs(self.end.y - self.start.y)
|
|
365
|
+
|
|
366
|
+
@property
|
|
367
|
+
def center(self) -> Point:
|
|
368
|
+
"""Rectangle center point."""
|
|
369
|
+
return Point(
|
|
370
|
+
(self.start.x + self.end.x) / 2,
|
|
371
|
+
(self.start.y + self.end.y) / 2
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
|
|
336
375
|
@dataclass
|
|
337
376
|
class Net:
|
|
338
377
|
"""Electrical net connecting components."""
|
|
@@ -429,6 +468,7 @@ class Schematic:
|
|
|
429
468
|
labels: List[Label] = field(default_factory=list)
|
|
430
469
|
nets: List[Net] = field(default_factory=list)
|
|
431
470
|
sheets: List[Sheet] = field(default_factory=list)
|
|
471
|
+
rectangles: List[SchematicRectangle] = field(default_factory=list)
|
|
432
472
|
lib_symbols: Dict[str, Any] = field(default_factory=dict)
|
|
433
473
|
|
|
434
474
|
def __post_init__(self):
|
|
@@ -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
|