feat(component): add check_courtyard_overlaps MCP tool (#189)
Detects courtyard overlaps between footprints and flags courtyards that
extend past the board outline. Returns overlap pairs with intersection
extents (mm), per-component boundary violations, and a placement summary.
The killer feature for AI-driven workflows is the `positions` parameter,
which accepts hypothetical placements `{ref: [x, y]}` or
`{ref: [x, y, rotation_degrees]}`. The tool evaluates the proposed
placement WITHOUT writing to the board file — so an AI agent can validate
a move_component / place_component before committing it, instead of the
current loop of write -> run DRC -> parse violations -> revert.
## Implementation
- Uses the real courtyard polygons from pcbnew (`fp.GetCourtyard(F_CrtYd)`
or B_CrtYd) for accurate AABBs even on custom and rotated footprints.
- Falls back to `fp.GetBoundingBox()` when no F/B.Courtyard polygon is
present.
- For virtual rotation, rotates the four AABB corners and re-axis-aligns.
Conservative: the rotated-AABB is always >= the rotated-polygon, so
overlap reports are never false-negatives (may be marginally
over-cautious on diagonal rectangles, which is the right error bias
for a placement validator).
- Optional `margin` parameter expands every courtyard by N mm — useful
for enforcing a manufacturing keepout wider than the symbol's
declared courtyard.
## Attribution
The approach is ported from morningfire-pcb-automation
(https://github.com/NiNjA-CodE/morningfire-pcb-automation), specifically
`scripts/placement/check_overlaps.py`. The upstream uses a static
per-footprint-type courtyard lookup table; this implementation reads
the real polygons from pcbnew so it works on any footprint without
maintaining a table. Attribution is in the function docstring, the
TypeScript wrapper, the tool's description (visible to MCP clients),
and the CHANGELOG entry.
## Tests
12 pytest cases in tests/test_check_courtyard_overlaps.py, all passing:
- No overlaps when spaced; overlap detected on intersect
- Margin pushes borderline pairs into overlap
- `refs` filter restricts the check
- Boundary violations are flagged; `include_boundary=false` suppresses
- Virtual position does not mutate the footprint (asserts
`SetPosition` is never called)
- Virtual rotation swaps a tall-narrow courtyard's x/y extents
- No-board-loaded returns clean error payload
- Bad position spec (wrong arity) returns clean error payload
- GetCourtyard() OutlineCount=0 -> fallback to GetBoundingBox()
- `board_outline` override replaces the Edge.Cuts bbox
Tests use mocked pcbnew objects so they run under both the conftest stub
and a real pcbnew install. Real-board smoke test on a 44-footprint
production board succeeds: 1 known overlap detected (SW1<->SW2), 0
boundary violations, virtual placement test reports 6 expected overlaps.
## Files touched
- python/commands/component.py (impl + helpers)
- python/kicad_interface.py (tool registration)
- python/schemas/tool_schemas.py (MCP schema entry)
- src/tools/component.ts (TypeScript surface, builds clean)
- tests/test_check_courtyard_overlaps.py (12 cases)
- CHANGELOG.md (Unreleased -> New MCP Tools)
This commit is contained in:
@@ -541,6 +541,70 @@ export function registerComponentTools(server: McpServer, callKicadScript: Comma
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Check Courtyard Overlaps Tool
|
||||
//
|
||||
// Lets the caller validate a placement plan before committing it. The
|
||||
// `positions` parameter accepts hypothetical {ref: [x, y]} or
|
||||
// [x, y, rotation_degrees] entries; the board file is not modified.
|
||||
//
|
||||
// Approach ported from morningfire-pcb-automation
|
||||
// https://github.com/NiNjA-CodE/morningfire-pcb-automation
|
||||
// (scripts/placement/check_overlaps.py)
|
||||
// ------------------------------------------------------
|
||||
server.tool(
|
||||
"check_courtyard_overlaps",
|
||||
"Detect courtyard overlaps between footprints and (optionally) flag courtyards that extend past the board outline. Accepts a `positions` dict of hypothetical placements so an AI can validate a proposed move_component / place_component before committing it. Returns overlap pairs with intersection extents (mm) and per-component boundary violations.",
|
||||
{
|
||||
positions: z
|
||||
.record(z.string(), z.array(z.number()).min(2).max(3))
|
||||
.optional()
|
||||
.describe(
|
||||
"Virtual placements: map of reference designator to [x, y] or [x, y, rotation_degrees] in mm. Each listed ref is checked AS IF it were at the given coordinates. Unspecified refs use their current board position.",
|
||||
),
|
||||
refs: z
|
||||
.array(z.string())
|
||||
.optional()
|
||||
.describe("Limit the check to these refs (default: every footprint on the board)."),
|
||||
margin: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe(
|
||||
"Extra clearance in mm added around every courtyard (default 0). Useful to enforce a manufacturing keepout wider than the symbol's declared courtyard.",
|
||||
),
|
||||
include_boundary: z
|
||||
.boolean()
|
||||
.optional()
|
||||
.describe("Also flag courtyards that extend past the board outline (default true)."),
|
||||
board_outline: z
|
||||
.object({
|
||||
x1: z.number(),
|
||||
y1: z.number(),
|
||||
x2: z.number(),
|
||||
y2: z.number(),
|
||||
unit: z.enum(["mm", "inch"]).optional(),
|
||||
})
|
||||
.optional()
|
||||
.describe("Optional board outline bbox override. Default: derived from Edge.Cuts."),
|
||||
},
|
||||
async (args) => {
|
||||
logger.debug(
|
||||
`Checking courtyard overlaps (virtual=${
|
||||
args.positions ? Object.keys(args.positions).length : 0
|
||||
})`,
|
||||
);
|
||||
const result = await callKicadScript("check_courtyard_overlaps", args);
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: JSON.stringify(result),
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// ------------------------------------------------------
|
||||
// Duplicate Component Tool
|
||||
// ------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user