feat: walk all sub-sheets to build hierarchical pad→net map for sync
Hierarchical KiCad schematics store all components in sub-sheets; the top-level .kicad_sch only contains sheet references. The previous sync_schematic_to_board implementation called generate_netlist on the top-level file only, which has no components, so it always returned 0 pads assigned. Replace with _build_hierarchical_pad_net_map which: - rglobs all .kicad_sch files in the project directory - For each sheet, collects label positions from label, global_label, and hierarchical_label via skip.Schematic - Adds power symbol (#PWR/#FLG) positions using their Value as net name - Builds a wire adjacency graph and BFS-propagates net names through wire segments to reach pins not directly under a label - Calls PinLocator.get_all_symbol_pins to get absolute pin positions, then matches to the propagated net map within 0.5 mm tolerance Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3056,6 +3056,148 @@ class KiCADInterface:
|
||||
logger.error(f"Error generating netlist: {e}")
|
||||
return {"success": False, "message": str(e)}
|
||||
|
||||
def _build_hierarchical_pad_net_map(self, project_sch_path: str):
|
||||
"""Walk all .kicad_sch files in the project and build a {(ref, pin_num): net_name} map.
|
||||
|
||||
Handles hierarchical schematics by scanning every sub-sheet file. Net names
|
||||
from global_label / hierarchical_label / local label / power symbols are all
|
||||
collected. Wire connectivity is traced via BFS so labels not placed directly
|
||||
on a pin endpoint still reach through wire segments.
|
||||
|
||||
Returns: (pad_net_map, net_names_set)
|
||||
"""
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
|
||||
from commands.pin_locator import PinLocator
|
||||
from skip import Schematic
|
||||
|
||||
TOLERANCE = 0.5 # mm; schematic grid is 1.27 mm so 0.5 is safe
|
||||
|
||||
def snap(x, y):
|
||||
"""Round to 2 dp to use exact dict lookup instead of O(n²) scan."""
|
||||
return (round(float(x), 2), round(float(y), 2))
|
||||
|
||||
def nearby_net(pt, point_net, tol=TOLERANCE):
|
||||
"""Return net name for the nearest occupied grid point, or None."""
|
||||
x, y = pt
|
||||
# Try exact snap first (fast path)
|
||||
key = snap(x, y)
|
||||
if key in point_net:
|
||||
return point_net[key]
|
||||
# Slow fallback for off-grid placements
|
||||
for (lx, ly), name in point_net.items():
|
||||
if abs(x - lx) < tol and abs(y - ly) < tol:
|
||||
return name
|
||||
return None
|
||||
|
||||
project_dir = Path(project_sch_path).parent
|
||||
pad_net_map: dict = {}
|
||||
all_net_names: set = set()
|
||||
pin_locator = PinLocator()
|
||||
|
||||
sch_files = sorted(project_dir.rglob("*.kicad_sch"))
|
||||
logger.info(f"_build_hierarchical_pad_net_map: scanning {len(sch_files)} schematic files")
|
||||
|
||||
for sch_path in sch_files:
|
||||
try:
|
||||
sch = Schematic(str(sch_path))
|
||||
except Exception as e:
|
||||
logger.warning(f"Could not load {sch_path}: {e}")
|
||||
continue
|
||||
|
||||
# ── 1. Collect explicit label positions → net name ──────────────
|
||||
point_net: dict = {} # snap(x,y) -> net_name
|
||||
|
||||
for attr in ("label", "global_label", "hierarchical_label"):
|
||||
for lbl in getattr(sch, attr, None) or []:
|
||||
try:
|
||||
pos = lbl.at.value
|
||||
name = lbl.value
|
||||
if name:
|
||||
k = snap(pos[0], pos[1])
|
||||
point_net[k] = name
|
||||
all_net_names.add(name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Power symbols (#PWR / #FLG): value property IS the net name; use pin 1 pos
|
||||
for sym in getattr(sch, "symbol", None) or []:
|
||||
try:
|
||||
ref = sym.property.Reference.value
|
||||
if not (ref.startswith("#PWR") or ref.startswith("#FLG")):
|
||||
continue
|
||||
net_name = sym.property.Value.value
|
||||
if not net_name:
|
||||
continue
|
||||
all_pins = pin_locator.get_all_symbol_pins(sch_path, ref)
|
||||
for _pin_num, (px, py) in all_pins.items():
|
||||
k = snap(px, py)
|
||||
point_net[k] = net_name
|
||||
all_net_names.add(net_name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── 2. Build wire adjacency and BFS-propagate net names ──────────
|
||||
wire_segments = []
|
||||
for wire in getattr(sch, "wire", None) or []:
|
||||
try:
|
||||
pts = []
|
||||
for pt in wire.pts.xy:
|
||||
pts.append(snap(pt.value[0], pt.value[1]))
|
||||
if len(pts) >= 2:
|
||||
wire_segments.append(pts)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Adjacency: connect endpoints of different segments that share a grid point
|
||||
point_adj: dict = defaultdict(set)
|
||||
for seg in wire_segments:
|
||||
# Connect consecutive points within the segment
|
||||
for i in range(len(seg) - 1):
|
||||
point_adj[seg[i]].add(seg[i + 1])
|
||||
point_adj[seg[i + 1]].add(seg[i])
|
||||
|
||||
# All unique wire points
|
||||
all_wire_pts = set()
|
||||
for seg in wire_segments:
|
||||
all_wire_pts.update(seg)
|
||||
|
||||
# BFS: propagate known net names through wire connections
|
||||
queue = [pt for pt in all_wire_pts if pt in point_net]
|
||||
visited = set(queue)
|
||||
while queue:
|
||||
pt = queue.pop()
|
||||
net = point_net[pt]
|
||||
for neighbor in point_adj[pt]:
|
||||
if neighbor not in point_net:
|
||||
point_net[neighbor] = net
|
||||
all_net_names.add(net)
|
||||
if neighbor not in visited:
|
||||
visited.add(neighbor)
|
||||
queue.append(neighbor)
|
||||
|
||||
# ── 3. Match component pin positions to net names ────────────────
|
||||
for sym in getattr(sch, "symbol", None) or []:
|
||||
try:
|
||||
ref = sym.property.Reference.value
|
||||
if ref.startswith("#"):
|
||||
continue
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
pin_positions = pin_locator.get_all_symbol_pins(sch_path, ref)
|
||||
for pin_num, (px, py) in pin_positions.items():
|
||||
net = nearby_net((px, py), point_net)
|
||||
if net:
|
||||
pad_net_map[(ref, pin_num)] = net
|
||||
|
||||
logger.info(
|
||||
f"_build_hierarchical_pad_net_map: {len(pad_net_map)} pin→net assignments, "
|
||||
f"{len(all_net_names)} unique nets"
|
||||
)
|
||||
return pad_net_map, all_net_names
|
||||
|
||||
def _handle_sync_schematic_to_board(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Sync schematic netlist to PCB board (equivalent to KiCAD F8 'Update PCB from Schematic').
|
||||
Reads net connections from the schematic and assigns them to the matching pads in the PCB.
|
||||
@@ -3100,24 +3242,8 @@ class KiCADInterface:
|
||||
"message": f"Schematic not found. Provide schematicPath. Tried: {schematic_path}",
|
||||
}
|
||||
|
||||
# Generate netlist from schematic
|
||||
schematic = SchematicManager.load_schematic(schematic_path)
|
||||
if not schematic:
|
||||
return {"success": False, "message": "Failed to load schematic"}
|
||||
|
||||
netlist = ConnectionManager.generate_netlist(schematic, schematic_path=schematic_path)
|
||||
|
||||
# Build (reference, pad_number) -> net_name map
|
||||
pad_net_map = {} # {(ref, pin_str): net_name}
|
||||
net_names = set()
|
||||
for net_entry in netlist.get("nets", []):
|
||||
net_name = net_entry["name"]
|
||||
net_names.add(net_name)
|
||||
for conn in net_entry.get("connections", []):
|
||||
ref = conn.get("component", "")
|
||||
pin = str(conn.get("pin", ""))
|
||||
if ref and pin and pin != "unknown":
|
||||
pad_net_map[(ref, pin)] = net_name
|
||||
# Build hierarchical pad→net map (walks all sub-sheets)
|
||||
pad_net_map, net_names = self._build_hierarchical_pad_net_map(schematic_path)
|
||||
|
||||
# Add all nets to board
|
||||
netinfo = board.GetNetInfo()
|
||||
|
||||
Reference in New Issue
Block a user