From b257bef89b400e8dd02fedf701a796933c7b8eea Mon Sep 17 00:00:00 2001 From: Eugene Mikhantyev Date: Sat, 14 Mar 2026 14:20:56 +0000 Subject: [PATCH] fix: harden get_wire_connections against edge cases and improve performance - Add +1 safety margin to grid_radius to handle banker's rounding at cell boundaries in the spatial index - Move symbol property guards inside per-symbol try/except to prevent AttributeError from aborting all pin processing - Replace O(n) connected_points scan for pin matching with spatial index lookup (_frontier_has_neighbour), consistent with flood-fill approach - Wrap float(x)/float(y) conversion with clear user-facing error message Co-Authored-By: Claude Sonnet 4.6 --- python/kicad_interface.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/python/kicad_interface.py b/python/kicad_interface.py index ba3f987..a9f59a0 100644 --- a/python/kicad_interface.py +++ b/python/kicad_interface.py @@ -2335,7 +2335,10 @@ class KiCADInterface: if not all([schematic_path, x is not None, y is not None]): return {"success": False, "message": "Missing required parameters: schematicPath, x, y"} - x, y = float(x), float(y) + try: + x, y = float(x), float(y) + except (TypeError, ValueError): + return {"success": False, "message": "Parameters x and y must be numeric"} tolerance = 0.5 def points_coincide(p1, p2): @@ -2379,7 +2382,7 @@ class KiCADInterface: # 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 + grid_radius = math.ceil(tolerance / GRID) + 1 # cells to check per axis (+1 safety margin for banker's rounding) def _grid_key(x_coord, y_coord): return (round(x_coord / GRID), round(y_coord / GRID)) @@ -2448,28 +2451,25 @@ class KiCADInterface: processed_refs = set() for symbol in schematic.symbol: - if not hasattr(symbol.property, "Reference"): - continue - ref = symbol.property.Reference.value - if ref.startswith("_TEMPLATE"): - continue - if ref in processed_refs: - continue - processed_refs.add(ref) - try: + if not hasattr(symbol, 'property') or not hasattr(symbol.property, "Reference"): + continue + ref = symbol.property.Reference.value + if ref.startswith("_TEMPLATE"): + continue + if ref in processed_refs: + continue + processed_refs.add(ref) all_pins = locator.get_all_symbol_pins(Path(schematic_path), ref) if not all_pins: continue for pin_num, pin_data in all_pins.items(): pin_loc = [pin_data[0], pin_data[1]] - for cp in connected_points: - if points_coincide(pin_loc, list(cp)): - key = (ref, pin_num) - if key not in seen: - seen.add(key) - pins.append({"component": ref, "pin": pin_num}) - break + if _frontier_has_neighbour(pin_loc[0], pin_loc[1]): + key = (ref, pin_num) + if key not in seen: + seen.add(key) + pins.append({"component": ref, "pin": pin_num}) except Exception as e: logger.warning(f"Error checking pins for {ref}: {e}")