feat(routing): add add_gnd_stitching_vias MCP tool with all-layer collision (#191)
Drop GND stitching vias across the board with collision checking
against every non-GND segment, via, and pad on every copper layer.
PTH vias penetrate the full stackup, so an F.Cu-only check (the most
common shortcut) silently creates shorts on inner / B.Cu copper —
this implementation explicitly walks all layers.
grid Regular grid across the board interior. Default
spacing 5mm.
around_refs Densify around specified footprints (e.g. MCUs,
switching regulators, RF parts). Configurable
density via densifyRadius.
in_zones Restrict placements to candidates inside the filled
polygons of GND copper zones, so each new via lands
on copper that's already a GND equipotential.
Recommended on boards where the GND zone is fragmented:
these vias actually stitch real polygons rather than
floating on silkscreen.
All three strategies use the same collision check + intra-call
clump-prevention, so passing `["grid", "around_refs", "in_zones"]`
is a safe kitchen-sink configuration.
- Auto-detect GND net (tries GND / GROUND / VSS / /GND in order)
OR explicit `gndNet` parameter.
- Per-via geometry control: viaSize, viaDrill, clearance.
- edgeMargin: keep-out distance from board edge.
- maxVias: cap on total placements (useful for incremental work).
- dryRun: return placements without modifying the board — for
previewing before committing.
- Validates viaDrill < viaSize, rejects unknown strategy names,
surfaces clear errors when GND net can't be resolved or the
board outline is missing.
Approach ported from morningfire-pcb-automation
(https://github.com/NiNjA-CodE/morningfire-pcb-automation,
scripts/ground/add_gnd_vias.py). The original parses the PCB text
with regex and writes vias by string concatenation; this port reads
obstacles via the pcbnew API (handles rotated footprints, integrates
with the live in-memory board so two sequential calls see each
other's placements, picks up net codes from the loaded board) and
adds the in_zones strategy, the maxVias cap, and dry-run mode.
Credit is in the docstring, the TypeScript wrapper comment, the MCP
tool description (visible to clients), and the CHANGELOG entry.
tests/test_add_gnd_stitching_vias.py — 18 cases, all passing.
Uses mocked pcbnew objects so the suite runs under both the conftest
stub and a real pcbnew install.
- grid strategy fills empty board with correct count
- collision blocks via near a signal track (with extent assertion)
- GND-net obstacles are correctly ignored
- around_refs densifies near footprints with bounded extent
- in_zones rejects candidates outside HitTestFilledArea
- dryRun does NOT call board.Add
- actual run calls board.Add per placement
- maxVias caps total placements
- intra-call clump prevention (asserts pairwise distance)
- viaDrill >= viaSize is rejected
- unknown strategy name is rejected
- missing GND net returns clear error payload
- no board loaded returns clear error
- named GND net (e.g. VSS) is honoured even when GND also exists
- direct unit tests for _point_to_segment_distance_nm helper
Real-board smoke test on TuneForge_TF001 (4-layer, 44 footprints):
- GND net auto-detected
- grid spacing 4mm: 141 placements, 129 blocked by collision
- grid + in_zones: 140 placed, 15 rejected by zone membership,
115 blocked by collision
python/commands/routing.py (+impl, ~370 LOC)
python/kicad_interface.py (+handler registration)
python/schemas/tool_schemas.py (+MCP schema)
src/tools/routing.ts (+TypeScript surface, builds clean)
tests/test_add_gnd_stitching_vias.py (+18 tests)
CHANGELOG.md (+Unreleased -> New MCP Tools)
This commit is contained in:
@@ -1027,6 +1027,122 @@ ROUTING_TOOLS = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "add_gnd_stitching_vias",
|
||||
"title": "Add GND Stitching Vias",
|
||||
"description": (
|
||||
"Drop GND stitching vias across the board with collision "
|
||||
"checking against every non-GND segment, via, and pad on "
|
||||
"every copper layer (PTH vias penetrate the full stackup, "
|
||||
"so missing one layer is the classic silent-short failure "
|
||||
"mode that other GND-stitching tools have). Combines three "
|
||||
"strategies: a regular `grid` across the interior, "
|
||||
"`around_refs` (densify around named ICs like an MCU or "
|
||||
"switching regulator), and `in_zones` (only place vias "
|
||||
"where they actually land on a GND copper zone so they "
|
||||
"stitch real polygons together rather than floating on "
|
||||
"silkscreen). Supports `dryRun` to preview placements "
|
||||
"without writing to the board. "
|
||||
"Approach ported from morningfire-pcb-automation "
|
||||
"(https://github.com/NiNjA-CodE/morningfire-pcb-automation, "
|
||||
"scripts/ground/add_gnd_vias.py); this version reads "
|
||||
"obstacles via the pcbnew API (handles rotation, picks up "
|
||||
"net classes, integrates with the live in-memory board) "
|
||||
"and adds the in-zones strategy + maxVias cap + dry-run."
|
||||
),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"gndNet": {
|
||||
"type": "string",
|
||||
"description": (
|
||||
"Name of the ground net (default: auto-detect "
|
||||
"GND / GROUND / VSS / /GND)."
|
||||
),
|
||||
},
|
||||
"strategies": {
|
||||
"type": "array",
|
||||
"description": (
|
||||
"Which placement strategies to combine (default: "
|
||||
"['grid']). Pass ['grid', 'around_refs', "
|
||||
"'in_zones'] for full coverage."
|
||||
),
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["grid", "around_refs", "in_zones"],
|
||||
},
|
||||
},
|
||||
"viaSize": {
|
||||
"type": "number",
|
||||
"description": "Via pad diameter in mm (default 0.6).",
|
||||
"default": 0.6,
|
||||
},
|
||||
"viaDrill": {
|
||||
"type": "number",
|
||||
"description": (
|
||||
"Via drill diameter in mm (default 0.3). "
|
||||
"Must be smaller than viaSize."
|
||||
),
|
||||
"default": 0.3,
|
||||
},
|
||||
"clearance": {
|
||||
"type": "number",
|
||||
"description": (
|
||||
"Extra clearance beyond required between each new "
|
||||
"via and existing copper, in mm. Default 0.2."
|
||||
),
|
||||
"default": 0.2,
|
||||
},
|
||||
"spacing": {
|
||||
"type": "number",
|
||||
"description": (
|
||||
"Grid spacing in mm for the `grid` and "
|
||||
"`around_refs` strategies. Default 5.0."
|
||||
),
|
||||
"default": 5.0,
|
||||
},
|
||||
"densifyRefs": {
|
||||
"type": "array",
|
||||
"description": (
|
||||
"Reference designators to densify ground around "
|
||||
"(used by `around_refs` strategy). Good targets: "
|
||||
"MCUs, switching regulators, RF parts."
|
||||
),
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
"densifyRadius": {
|
||||
"type": "integer",
|
||||
"description": (
|
||||
"How many grid cells around each ref to try "
|
||||
"(default 2 = 5x5 candidate field per ref)."
|
||||
),
|
||||
"default": 2,
|
||||
},
|
||||
"edgeMargin": {
|
||||
"type": "number",
|
||||
"description": (
|
||||
"Keep-out from the board edge in mm. Default 0.5."
|
||||
),
|
||||
"default": 0.5,
|
||||
},
|
||||
"maxVias": {
|
||||
"type": "integer",
|
||||
"description": (
|
||||
"Cap on total placements across all strategies "
|
||||
"(default unlimited). Useful when iterating."
|
||||
),
|
||||
},
|
||||
"dryRun": {
|
||||
"type": "boolean",
|
||||
"description": (
|
||||
"If true, return the placements that would be "
|
||||
"made but don't modify the board. Default false."
|
||||
),
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "modify_trace",
|
||||
"title": "Modify Trace",
|
||||
|
||||
Reference in New Issue
Block a user