fix: address review issues in get_wire_connections

- Fix critical bug: use pin_data[0]/[1] instead of pin_data["x"]/["y"]
  (get_all_symbol_pins returns List[float], not dict)
- Use index-based wire tracking to avoid fragile float list equality
- Check all polyline points (not just endpoints) during flood-fill
- Add spatial index (0.05mm grid) to replace O(n²) frontier scan
- Skip already-processed refs to avoid redundant calls for multi-unit symbols
- Include wires_out in early return when schematic has no symbols
- Add get_wire_connections schema entry to tool_schemas.py

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Eugene Mikhantyev
2026-03-14 14:10:06 +00:00
parent 6633cd59fd
commit ddbfc62a49
2 changed files with 85 additions and 25 deletions

View File

@@ -2361,39 +2361,72 @@ class KiCADInterface:
# Step 1: Find all seed wires that touch the given point (start or end) # Step 1: Find all seed wires that touch the given point (start or end)
query_point = [x, y] query_point = [x, y]
seed_wires = [ seed_indices = set(
pts for pts in all_wires i for i, pts in enumerate(all_wires)
if points_coincide(pts[0], query_point) or points_coincide(pts[-1], query_point) if any(points_coincide(pt, query_point) for pt in pts)
] )
if not seed_wires: if not seed_indices:
return { return {
"success": False, "success": False,
"message": f"No wire found at ({x},{y}) within {tolerance}mm tolerance", "message": f"No wire found at ({x},{y}) within {tolerance}mm tolerance",
} }
# Step 2: Flood-fill through connected wires # Step 2: Flood-fill through connected wires
connected_wires = list(seed_wires) connected_indices = set(seed_indices)
frontier = set((pt[0], pt[1]) for pts in seed_wires for pt in pts) frontier = set((pt[0], pt[1]) for i in seed_indices for pt in all_wires[i])
remaining = [w for w in all_wires if w not in seed_wires]
# Spatial index: grid-snapped dict for O(1) proximity lookup
import math
GRID = 0.05 # mm, matches KiCAD schematic grid
grid_radius = math.ceil(tolerance / GRID) # cells to check per axis
def _grid_key(x_coord, y_coord):
return (round(x_coord / GRID), round(y_coord / GRID))
# frontier_grid maps grid cell -> list of (x, y) frontier points in that cell
frontier_grid = {}
for fp in frontier:
key = _grid_key(fp[0], fp[1])
frontier_grid.setdefault(key, []).append(fp)
def _frontier_has_neighbour(px, py):
"""Check if any frontier point is within tolerance of (px, py)."""
cx, cy = _grid_key(px, py)
for dx in range(-grid_radius, grid_radius + 1):
for dy in range(-grid_radius, grid_radius + 1):
cell = (cx + dx, cy + dy)
cell_points = frontier_grid.get(cell)
if cell_points:
for fp in cell_points:
if abs(px - fp[0]) < tolerance and abs(py - fp[1]) < tolerance:
return True
return False
def _add_to_frontier_grid(pt):
key = _grid_key(pt[0], pt[1])
frontier_grid.setdefault(key, []).append(pt)
remaining_indices = [i for i in range(len(all_wires)) if i not in seed_indices]
changed = True changed = True
while changed: while changed:
changed = False changed = False
still_remaining = [] still_remaining = []
for pts in remaining: for i in remaining_indices:
wire_endpoints = {(pts[0][0], pts[0][1]), (pts[-1][0], pts[-1][1])} pts = all_wires[i]
if any( wire_points = [(pt[0], pt[1]) for pt in pts]
points_coincide(list(ep), list(fp)) if any(_frontier_has_neighbour(wp[0], wp[1]) for wp in wire_points):
for ep in wire_endpoints connected_indices.add(i)
for fp in frontier
):
connected_wires.append(pts)
for pt in pts: for pt in pts:
frontier.add((pt[0], pt[1])) p = (pt[0], pt[1])
frontier.add(p)
_add_to_frontier_grid(p)
changed = True changed = True
else: else:
still_remaining.append(pts) still_remaining.append(i)
remaining = still_remaining remaining_indices = still_remaining
connected_wires = [all_wires[i] for i in connected_indices]
# Step 3: Collect all points from connected wires # Step 3: Collect all points from connected wires
connected_points = set() connected_points = set()
@@ -2402,12 +2435,17 @@ class KiCADInterface:
connected_points.add((pt[0], pt[1])) connected_points.add((pt[0], pt[1]))
# Step 4: Find component pins at connected points # Step 4: Find component pins at connected points
wires_out = [
{"start": {"x": pts[0][0], "y": pts[0][1]}, "end": {"x": pts[-1][0], "y": pts[-1][1]}}
for pts in connected_wires
]
if not hasattr(schematic, "symbol"): if not hasattr(schematic, "symbol"):
return {"success": True, "pins": []} return {"success": True, "pins": [], "wires": wires_out}
locator = PinLocator() locator = PinLocator()
pins = [] pins = []
seen = set() seen = set()
processed_refs = set()
for symbol in schematic.symbol: for symbol in schematic.symbol:
if not hasattr(symbol.property, "Reference"): if not hasattr(symbol.property, "Reference"):
@@ -2415,13 +2453,16 @@ class KiCADInterface:
ref = symbol.property.Reference.value ref = symbol.property.Reference.value
if ref.startswith("_TEMPLATE"): if ref.startswith("_TEMPLATE"):
continue continue
if ref in processed_refs:
continue
processed_refs.add(ref)
try: try:
all_pins = locator.get_all_symbol_pins(Path(schematic_path), ref) all_pins = locator.get_all_symbol_pins(Path(schematic_path), ref)
if not all_pins: if not all_pins:
continue continue
for pin_num, pin_data in all_pins.items(): for pin_num, pin_data in all_pins.items():
pin_loc = [pin_data["x"], pin_data["y"]] pin_loc = [pin_data[0], pin_data[1]]
for cp in connected_points: for cp in connected_points:
if points_coincide(pin_loc, list(cp)): if points_coincide(pin_loc, list(cp)):
key = (ref, pin_num) key = (ref, pin_num)
@@ -2432,10 +2473,6 @@ class KiCADInterface:
except Exception as e: except Exception as e:
logger.warning(f"Error checking pins for {ref}: {e}") logger.warning(f"Error checking pins for {ref}: {e}")
wires_out = [
{"start": {"x": pts[0][0], "y": pts[0][1]}, "end": {"x": pts[-1][0], "y": pts[-1][1]}}
for pts in connected_wires
]
return {"success": True, "pins": pins, "wires": wires_out} return {"success": True, "pins": pins, "wires": wires_out}
except Exception as e: except Exception as e:

View File

@@ -1471,6 +1471,29 @@ SCHEMATIC_TOOLS = [
"required": ["schematicPath", "netName"], "required": ["schematicPath", "netName"],
}, },
}, },
{
"name": "get_wire_connections",
"title": "Get Wire Connections",
"description": "Returns all wires and component pins connected to the wire at a given point, by flood-filling through touching wires.",
"inputSchema": {
"type": "object",
"properties": {
"schematicPath": {
"type": "string",
"description": "Path to schematic file"
},
"x": {
"type": "number",
"description": "X coordinate of the point on the wire"
},
"y": {
"type": "number",
"description": "Y coordinate of the point on the wire"
}
},
"required": ["schematicPath", "x", "y"]
}
},
{ {
"name": "get_schematic_pin_locations", "name": "get_schematic_pin_locations",
"title": "Get Schematic Pin Locations", "title": "Get Schematic Pin Locations",