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>
This commit is contained in:
2026-03-27 19:16:36 +00:00
parent 70cb9d1a93
commit d8eeb3425a
2 changed files with 124 additions and 0 deletions

View File

@@ -2,6 +2,8 @@ import pcbnew
import wx
import traceback
from .via_net_assign import Via_Net_Assign_Action
# ---------------------------------------------------------------------------
# Board helpers
@@ -241,3 +243,4 @@ class Layout_Replicator_Action(pcbnew.ActionPlugin):
dlg.Destroy()
Layout_Replicator_Action().register()
Via_Net_Assign_Action().register()

View File

@@ -0,0 +1,121 @@
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()