Add query_zones tool for auditing copper pours (#174)
query_traces silently omits PCB_ZONE_T objects, so layer-usage audits miss power planes and GND pours entirely. query_zones complements it by iterating board.Zones() and returning each zone's net, layers, priority, fill state, min thickness, bounding box, and filled area, with the same net/layer/boundingBox filter surface as query_traces.
This commit is contained in:
@@ -791,6 +791,113 @@ class RoutingCommands:
|
||||
"errorDetails": str(e),
|
||||
}
|
||||
|
||||
def query_zones(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Query copper zones (filled pours) by net, layer, or bounding box.
|
||||
|
||||
Returns one entry per zone with its net, layers, priority, fill state,
|
||||
and bounding box. Useful for auditing power planes / GND pours that
|
||||
``query_traces`` does not report (zones are PCB_ZONE_T, not tracks).
|
||||
"""
|
||||
try:
|
||||
if not self.board:
|
||||
return {
|
||||
"success": False,
|
||||
"message": "No board is loaded",
|
||||
"errorDetails": "Load or create a board first",
|
||||
}
|
||||
|
||||
net_name = params.get("net")
|
||||
layer = params.get("layer")
|
||||
bbox = params.get("boundingBox")
|
||||
|
||||
scale = 1000000 # nm -> mm
|
||||
target_layer_id = None
|
||||
if layer:
|
||||
target_layer_id = self.board.GetLayerID(layer)
|
||||
|
||||
bbox_box = None
|
||||
if bbox:
|
||||
bbox_unit = bbox.get("unit", "mm")
|
||||
bbox_scale = scale if bbox_unit == "mm" else 25400000
|
||||
bbox_box = (
|
||||
int(bbox.get("x1", 0) * bbox_scale),
|
||||
int(bbox.get("y1", 0) * bbox_scale),
|
||||
int(bbox.get("x2", 0) * bbox_scale),
|
||||
int(bbox.get("y2", 0) * bbox_scale),
|
||||
)
|
||||
|
||||
zones_out = []
|
||||
for zone in list(self.board.Zones()):
|
||||
try:
|
||||
z_net = zone.GetNetname()
|
||||
if net_name and z_net != net_name:
|
||||
continue
|
||||
|
||||
# A zone can span multiple copper layers; collect them.
|
||||
layer_names = []
|
||||
try:
|
||||
layer_set = zone.GetLayerSet()
|
||||
seq = layer_set.CuStack() if hasattr(layer_set, "CuStack") else layer_set.Seq()
|
||||
for lid in seq:
|
||||
layer_names.append(self.board.GetLayerName(lid))
|
||||
except Exception:
|
||||
layer_names = [self.board.GetLayerName(zone.GetLayer())]
|
||||
|
||||
if target_layer_id is not None:
|
||||
if target_layer_id not in [self.board.GetLayerID(n) for n in layer_names]:
|
||||
continue
|
||||
|
||||
bb = zone.GetBoundingBox()
|
||||
bb_x1, bb_y1 = bb.GetLeft(), bb.GetTop()
|
||||
bb_x2, bb_y2 = bb.GetRight(), bb.GetBottom()
|
||||
|
||||
if bbox_box is not None:
|
||||
x1, y1, x2, y2 = bbox_box
|
||||
# Reject if no overlap with filter bbox.
|
||||
if bb_x2 < x1 or bb_x1 > x2 or bb_y2 < y1 or bb_y1 > y2:
|
||||
continue
|
||||
|
||||
entry = {
|
||||
"uuid": zone.m_Uuid.AsString(),
|
||||
"net": z_net,
|
||||
"netCode": zone.GetNetCode(),
|
||||
"layers": layer_names,
|
||||
"priority": zone.GetAssignedPriority() if hasattr(zone, "GetAssignedPriority") else 0,
|
||||
"isFilled": bool(zone.IsFilled()),
|
||||
"minThickness": zone.GetMinThickness() / scale,
|
||||
"boundingBox": {
|
||||
"x1": bb_x1 / scale,
|
||||
"y1": bb_y1 / scale,
|
||||
"x2": bb_x2 / scale,
|
||||
"y2": bb_y2 / scale,
|
||||
"unit": "mm",
|
||||
},
|
||||
}
|
||||
# Area is only available when zone is filled.
|
||||
try:
|
||||
entry["filledArea"] = zone.GetFilledArea() / (scale * scale)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
zones_out.append(entry)
|
||||
except Exception as zone_err:
|
||||
logger.warning(f"Skipping invalid zone object: {zone_err}")
|
||||
continue
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"zoneCount": len(zones_out),
|
||||
"zones": zones_out,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error querying zones: {str(e)}")
|
||||
return {
|
||||
"success": False,
|
||||
"message": "Failed to query zones",
|
||||
"errorDetails": str(e),
|
||||
}
|
||||
|
||||
def modify_trace(self, params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Modify properties of an existing trace
|
||||
|
||||
|
||||
@@ -368,6 +368,7 @@ class KiCADInterface:
|
||||
"add_via": self.routing_commands.add_via,
|
||||
"delete_trace": self.routing_commands.delete_trace,
|
||||
"query_traces": self.routing_commands.query_traces,
|
||||
"query_zones": self.routing_commands.query_zones,
|
||||
"modify_trace": self.routing_commands.modify_trace,
|
||||
"copy_routing_pattern": self.routing_commands.copy_routing_pattern,
|
||||
"get_nets_list": self.routing_commands.get_nets_list,
|
||||
|
||||
@@ -912,6 +912,39 @@ ROUTING_TOOLS = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "query_zones",
|
||||
"title": "Query Zones",
|
||||
"description": "Queries copper zones (filled pours) on the board with optional filters by net, layer, or bounding box. Returns one entry per zone with net, layers, priority, fill state, and bounding box. Useful for auditing power planes and GND pours, which 'query_traces' does not include.",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"net": {
|
||||
"type": "string",
|
||||
"description": "Filter by net name (e.g., 'GND', '+3V3')",
|
||||
},
|
||||
"layer": {
|
||||
"type": "string",
|
||||
"description": "Filter by layer name (e.g., 'In1.Cu', 'B.Cu'). Matches zones that include this layer in their layer set.",
|
||||
},
|
||||
"boundingBox": {
|
||||
"type": "object",
|
||||
"description": "Filter to zones whose bounding box overlaps this region",
|
||||
"properties": {
|
||||
"x1": {"type": "number", "description": "Left X coordinate"},
|
||||
"y1": {"type": "number", "description": "Top Y coordinate"},
|
||||
"x2": {"type": "number", "description": "Right X coordinate"},
|
||||
"y2": {"type": "number", "description": "Bottom Y coordinate"},
|
||||
"unit": {
|
||||
"type": "string",
|
||||
"enum": ["mm", "inch"],
|
||||
"default": "mm",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "modify_trace",
|
||||
"title": "Modify Trace",
|
||||
|
||||
Reference in New Issue
Block a user