From d8eeb3425a76396281cd12d1cb47e5840db5e438 Mon Sep 17 00:00:00 2001 From: mikael-lovqvists-claude-agent Date: Fri, 27 Mar 2026 19:16:36 +0000 Subject: [PATCH] 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 --- plugins/layout_replicator/__init__.py | 3 + plugins/layout_replicator/via_net_assign.py | 121 ++++++++++++++++++++ 2 files changed, 124 insertions(+) create mode 100644 plugins/layout_replicator/via_net_assign.py diff --git a/plugins/layout_replicator/__init__.py b/plugins/layout_replicator/__init__.py index ef6d345..9240046 100644 --- a/plugins/layout_replicator/__init__.py +++ b/plugins/layout_replicator/__init__.py @@ -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() diff --git a/plugins/layout_replicator/via_net_assign.py b/plugins/layout_replicator/via_net_assign.py new file mode 100644 index 0000000..98f2208 --- /dev/null +++ b/plugins/layout_replicator/via_net_assign.py @@ -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()