Files
kicad-layout-replicator/plugins/layout_replicator/via_net_assign.py
mikael-lovqvists-claude-agent d8eeb3425a Add Via Net Assign feature
Iterates all vias in the active selection and assigns each one to the
net of the nearest copper item (track or pad) on the board. Intended
for re-using via layouts after copying traces to a new net.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-27 19:16:36 +00:00

122 lines
3.5 KiB
Python

import pcbnew
import wx
import math
import traceback
# ---------------------------------------------------------------------------
# Geometry
# ---------------------------------------------------------------------------
def _point_to_segment_dist(px, py, ax, ay, bx, by):
dx, dy = bx - ax, by - ay
if dx == 0 and dy == 0:
return math.hypot(px - ax, py - ay)
t = max(0.0, min(1.0, ((px - ax) * dx + (py - ay) * dy) / (dx * dx + dy * dy)))
return math.hypot(px - (ax + t * dx), py - (ay + t * dy))
# ---------------------------------------------------------------------------
# Logic
# ---------------------------------------------------------------------------
def _build_candidates(board):
"""Collect all non-via tracks and pads that have a real net."""
candidates = []
for track in board.GetTracks():
if track.GetClass() == 'PCB_VIA':
continue
net = track.GetNet()
if net and net.GetNetCode() != 0:
candidates.append(('track', track, net))
for pad in board.GetPads():
net = pad.GetNet()
if net and net.GetNetCode() != 0:
candidates.append(('pad', pad, net))
return candidates
def _nearest_net(via, candidates):
vx = via.GetX()
vy = via.GetY()
vr = via.GetWidth() / 2
best_net = None
best_dist = None
for kind, item, net in candidates:
if kind == 'track':
ax, ay = item.GetStart().x, item.GetStart().y
bx, by = item.GetEnd().x, item.GetEnd().y
hw = item.GetWidth() / 2
dist = max(0.0, _point_to_segment_dist(vx, vy, ax, ay, bx, by) - hw - vr)
else: # pad
dist = max(0.0, math.hypot(vx - item.GetX(), vy - item.GetY()) - vr)
if best_dist is None or dist < best_dist:
best_dist = dist
best_net = net
return best_net, best_dist
def assign_via_nets(board):
vias = [t for t in board.GetTracks()
if t.GetClass() == 'PCB_VIA' and t.IsSelected()]
if not vias:
return 0, 'No vias in selection.'
candidates = _build_candidates(board)
if not candidates:
return 0, 'No routed copper found on board.'
count = 0
for via in vias:
net, _ = _nearest_net(via, candidates)
if net is not None:
via.SetNet(net)
count += 1
pcbnew.Refresh()
return count, None
# ---------------------------------------------------------------------------
# Plugin
# ---------------------------------------------------------------------------
class Via_Net_Assign_Action(pcbnew.ActionPlugin):
def defaults(self):
self.name = 'Assign Via Nets from Nearest Copper'
self.category = 'Placement'
self.description = 'Update net assignment of all selected vias to match the nearest copper item'
self.show_toolbar_button = True
self.icon_file_name = ''
def Run(self):
try:
board = pcbnew.GetBoard()
count, err = assign_via_nets(board)
if err:
wx.MessageBox(err, 'Via Net Assign', wx.OK | wx.ICON_INFORMATION)
else:
wx.MessageBox(f'{count} via(s) updated.', 'Via Net Assign', wx.OK | wx.ICON_INFORMATION)
except Exception:
msg = traceback.format_exc()
dlg = wx.Dialog(None, title='Via Net Assign — Error', size=(520, 320),
style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER)
s = wx.BoxSizer(wx.VERTICAL)
txt = wx.TextCtrl(dlg, value=msg, style=wx.TE_MULTILINE | wx.TE_READONLY | wx.HSCROLL)
txt.SetFont(wx.Font(9, wx.FONTFAMILY_TELETYPE, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_NORMAL))
s.Add(txt, 1, wx.EXPAND | wx.ALL, 6)
btn = wx.Button(dlg, wx.ID_CLOSE, 'Close')
btn.Bind(wx.EVT_BUTTON, lambda e: dlg.Close())
s.Add(btn, 0, wx.ALIGN_RIGHT | wx.ALL, 6)
dlg.SetSizer(s)
dlg.ShowModal()
dlg.Destroy()