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