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()