Feat: add bounding box and courtyard info to component queries (#163)

Components now include boundingBox (min/max X/Y, width, height) in
get_component_list and get_component_properties responses. The SWIG
get_component_properties also includes courtyard dimensions when the
footprint defines a courtyard layer.

SWIG backend uses GetBoundingBox() and GetCourtyard(). IPC backend
tries get_item_bounding_box(), then pad-extent fallback, then falls
back to SWIG backend data when available. This ensures bounding box
data is present regardless of which backend handles the query.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Gavin Colonese
2026-05-19 09:36:33 -04:00
committed by GitHub
parent 95e23c70ff
commit f03a74a93f
4 changed files with 281 additions and 25 deletions

View File

@@ -414,6 +414,38 @@ class ComponentCommands:
x_mm = pos.x / 1000000
y_mm = pos.y / 1000000
# Get bounding box
bbox = module.GetBoundingBox()
bbox_data = {
"min_x": bbox.GetLeft() / 1000000,
"min_y": bbox.GetTop() / 1000000,
"max_x": bbox.GetRight() / 1000000,
"max_y": bbox.GetBottom() / 1000000,
"width": (bbox.GetRight() - bbox.GetLeft()) / 1000000,
"height": (bbox.GetBottom() - bbox.GetTop()) / 1000000,
"unit": "mm",
}
# Try to get courtyard bounds (preferred for placement clearance)
courtyard_data = None
try:
for layer_id in [pcbnew.F_CrtYd, pcbnew.B_CrtYd]:
courtyard = module.GetCourtyard(layer_id)
if courtyard and courtyard.OutlineCount() > 0:
cbox = courtyard.BBox()
courtyard_data = {
"min_x": cbox.GetLeft() / 1000000,
"min_y": cbox.GetTop() / 1000000,
"max_x": cbox.GetRight() / 1000000,
"max_y": cbox.GetBottom() / 1000000,
"width": (cbox.GetRight() - cbox.GetLeft()) / 1000000,
"height": (cbox.GetBottom() - cbox.GetTop()) / 1000000,
"unit": "mm",
}
break
except Exception:
pass # Courtyard may not exist or API may differ
return {
"success": True,
"component": {
@@ -428,6 +460,8 @@ class ComponentCommands:
"through_hole": module.GetAttributes() & pcbnew.FP_THROUGH_HOLE,
"board_only": module.GetAttributes() & pcbnew.FP_BOARD_ONLY,
},
"boundingBox": bbox_data,
"courtyard": courtyard_data,
},
}
@@ -455,6 +489,17 @@ class ComponentCommands:
x_mm = pos.x / 1000000
y_mm = pos.y / 1000000
bbox = module.GetBoundingBox()
bbox_data = {
"min_x": bbox.GetLeft() / 1000000,
"min_y": bbox.GetTop() / 1000000,
"max_x": bbox.GetRight() / 1000000,
"max_y": bbox.GetBottom() / 1000000,
"width": (bbox.GetRight() - bbox.GetLeft()) / 1000000,
"height": (bbox.GetBottom() - bbox.GetTop()) / 1000000,
"unit": "mm",
}
components.append(
{
"reference": module.GetReference(),
@@ -463,6 +508,7 @@ class ComponentCommands:
"position": {"x": x_mm, "y": y_mm, "unit": "mm"},
"rotation": module.GetOrientation().AsDegrees(),
"layer": self.board.GetLayerName(module.GetLayer()),
"boundingBox": bbox_data,
}
)
@@ -1275,7 +1321,7 @@ class ComponentCommands:
"success": False,
"message": "Bad position spec",
"errorDetails": f"positions['{ref}'] must be [x, y] or [x, y, rot]; "
f"got {spec!r}",
f"got {spec!r}",
}
virtual[ref] = spec
@@ -1308,20 +1354,22 @@ class ComponentCommands:
if a[0] < b[2] and a[2] > b[0] and a[1] < b[3] and a[3] > b[1]:
ox = min(a[2], b[2]) - max(a[0], b[0])
oy = min(a[3], b[3]) - max(a[1], b[1])
overlaps.append({
"a": a_ref,
"b": b_ref,
"overlap_x_mm": round(ox, 3),
"overlap_y_mm": round(oy, 3),
"overlap_area_mm2": round(ox * oy, 4),
"bbox": {
"x1": round(max(a[0], b[0]), 3),
"y1": round(max(a[1], b[1]), 3),
"x2": round(min(a[2], b[2]), 3),
"y2": round(min(a[3], b[3]), 3),
"unit": "mm",
},
})
overlaps.append(
{
"a": a_ref,
"b": b_ref,
"overlap_x_mm": round(ox, 3),
"overlap_y_mm": round(oy, 3),
"overlap_area_mm2": round(ox * oy, 4),
"bbox": {
"x1": round(max(a[0], b[0]), 3),
"y1": round(max(a[1], b[1]), 3),
"x2": round(min(a[2], b[2]), 3),
"y2": round(min(a[3], b[3]), 3),
"unit": "mm",
},
}
)
# Boundary violations
boundary_violations = []
@@ -1339,15 +1387,19 @@ class ComponentCommands:
if y2 > oy2 + 1e-6:
exceeds["bottom"] = round(y2 - oy2, 3)
if exceeds:
boundary_violations.append({
"ref": ref,
"bbox": {
"x1": round(x1, 3), "y1": round(y1, 3),
"x2": round(x2, 3), "y2": round(y2, 3),
"unit": "mm",
},
"exceeds": exceeds,
})
boundary_violations.append(
{
"ref": ref,
"bbox": {
"x1": round(x1, 3),
"y1": round(y1, 3),
"x2": round(x2, 3),
"y2": round(y2, 3),
"unit": "mm",
},
"exceeds": exceeds,
}
)
return {
"success": True,
@@ -1360,7 +1412,8 @@ class ComponentCommands:
"margin_mm": margin_mm,
"virtual_placements": len(virtual),
"board_outline_mm": (
None if outline_bbox is None
None
if outline_bbox is None
else {
"x1": round(outline_bbox[0], 3),
"y1": round(outline_bbox[1], 3),
@@ -1475,6 +1528,7 @@ class ComponentCommands:
"""Rotate the four AABB corners around origin and return the new
axis-aligned bounding box. KiCad uses Y-down screen coords."""
import math
rad = math.radians(angle_deg)
c, s = math.cos(rad), math.sin(rad)
# Note: screen Y-down means rotation CCW visually requires the