fix(add_mounting_hole): set FPID and restrict NPTH pad to mask layers (#154)

`BoardOutlineCommands.add_mounting_hole` produced footprints with an empty
library:name id (`(footprint "" ...)` in the .kicad_pcb), which KiCad's GUI
Move tool refuses to select — users couldn't drag the resulting MHs in the
editor. It also emitted the pad with the default `*.Cu` + `*.Mask` LSET on
NPTHs; with `padDiameter > diameter` that creates phantom copper annular
rings on every Cu layer and trips clearance DRC against neighbouring nets.

Repro: call `add_mounting_hole` with `position={x:117,y:84.5,unit:"mm"},
diameter:3.2, padDiameter:3.5`. The resulting MH is unselectable and DRC
reports phantom F.Cu pad shorts to neighbouring component pads.

Changes:

- Set a real FPID via `module.SetFPID(pcbnew.LIB_ID(lib, name))`. Default
  is `MountingHole:MountingHole_<diameter:g>mm` (e.g. 3.2 → 3.2mm); a new
  optional `footprintLibId` parameter overrides.
- For NPTH (the default `plated:false`), restrict the pad's LSET to
  `F.Mask` + `B.Mask` only. PTH path is unchanged — the default Cu+Mask
  LSET is correct there.
- Update the schema in `tool_schemas.py`: previously advertised
  `x`/`y`/`diameter` at the top level, but the impl reads
  `position={x,y,unit}`, `padDiameter`, `plated`. Schema now matches the
  implementation and exposes the new `footprintLibId` param.
- New `tests/test_add_mounting_hole.py` regression suite (7 tests) asserting
  SetFPID is invoked with non-empty lib:name (default + override forms),
  NPTH SetLayerSet excludes any Cu layer, and PTH does not call
  SetLayerSet (preserves default Cu+Mask).
This commit is contained in:
Matthew Runo
2026-05-18 11:05:10 -07:00
committed by GitHub
parent ff6ae63b8c
commit 4740013d24
3 changed files with 249 additions and 5 deletions

View File

@@ -216,6 +216,7 @@ class BoardOutlineCommands:
diameter = params.get("diameter")
pad_diameter = params.get("padDiameter")
plated = params.get("plated", False)
footprint_lib_id = params.get("footprintLibId")
if not position or not diameter:
return {
@@ -247,6 +248,19 @@ class BoardOutlineCommands:
module.SetReference(f"MH{next_num}")
module.SetValue(f"MountingHole_{diameter}mm")
# Set a real library:name FPID. Without this, the footprint is
# written as `(footprint "" ...)` and KiCad's GUI Move tool refuses
# to select it (no library link → not draggable in the editor).
if not footprint_lib_id:
# Strip trailing zeros so 3.2 → "3.2" not "3.20"
footprint_lib_id = f"MountingHole:MountingHole_{diameter:g}mm"
if ":" in footprint_lib_id:
lib_name, fp_name = footprint_lib_id.split(":", 1)
else:
lib_name = "MountingHole"
fp_name = footprint_lib_id
module.SetFPID(pcbnew.LIB_ID(lib_name, fp_name))
# Create the pad for the hole
pad = pcbnew.PAD(module)
pad.SetNumber(1)
@@ -255,6 +269,18 @@ class BoardOutlineCommands:
pad.SetSize(pcbnew.VECTOR2I(pad_diameter_nm, pad_diameter_nm))
pad.SetDrillSize(pcbnew.VECTOR2I(diameter_nm, diameter_nm))
pad.SetPosition(pcbnew.VECTOR2I(0, 0)) # Position relative to module
if not plated:
# NPTH must not include *.Cu in pad layers. The default LSET
# for a circular pad is *.Cu + *.Mask; on a NPTH with
# padDiameter > diameter that produces phantom copper annular
# rings on every Cu layer, which trip clearance DRC against
# neighbouring nets.
mask_only = pcbnew.LSET()
mask_only.AddLayer(pcbnew.F_Mask)
mask_only.AddLayer(pcbnew.B_Mask)
pad.SetLayerSet(mask_only)
module.Add(pad)
# Position the mounting hole
@@ -271,6 +297,7 @@ class BoardOutlineCommands:
"diameter": diameter,
"padDiameter": pad_diameter or diameter + 1,
"plated": plated,
"footprintLibId": f"{lib_name}:{fp_name}",
},
}