Remove component table, rename package to layout_replicator

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 17:26:17 +00:00
parent 9902348c07
commit aeca2616ef
3 changed files with 3 additions and 119 deletions

View File

@@ -0,0 +1,243 @@
import pcbnew
import wx
import traceback
# ---------------------------------------------------------------------------
# Board helpers
# ---------------------------------------------------------------------------
def get_sheets(board):
sheets = set()
for fp in board.GetFootprints():
name = fp.GetSheetname()
if name:
sheets.add(name)
return sorted(sheets)
def get_sheet_fps(board, sheet_name):
return [fp for fp in board.GetFootprints() if fp.GetSheetname() == sheet_name]
def local_uuid(fp):
"""Last segment of the hierarchical path — same across all instances of a sheet."""
path = fp.GetPath().AsString()
parts = [p for p in path.split('/') if p]
return parts[-1] if parts else ''
def build_uuid_map(footprints):
return {local_uuid(fp): fp for fp in footprints}
# ---------------------------------------------------------------------------
# Transform
# ---------------------------------------------------------------------------
def apply_replication(board, source_sheet, target_sheet, ref_fp):
source_fps = get_sheet_fps(board, source_sheet)
target_fps = get_sheet_fps(board, target_sheet)
target_map = build_uuid_map(target_fps)
ref_uuid = local_uuid(ref_fp)
target_ref = target_map.get(ref_uuid)
if target_ref is None:
raise ValueError('Could not find matching reference component in target sheet')
src_ref_pos = ref_fp.GetPosition()
tgt_ref_pos = target_ref.GetPosition()
delta_rot = target_ref.GetOrientationDegrees() - ref_fp.GetOrientationDegrees()
delta_pos = pcbnew.VECTOR2I(
tgt_ref_pos.x - src_ref_pos.x,
tgt_ref_pos.y - src_ref_pos.y,
)
count = 0
for src_fp in source_fps:
uuid = local_uuid(src_fp)
if uuid == ref_uuid:
continue
tgt_fp = target_map.get(uuid)
if tgt_fp is None:
continue
# Place on top of source counterpart
tgt_fp.SetPosition(src_fp.GetPosition())
tgt_fp.SetOrientationDegrees(src_fp.GetOrientationDegrees())
# Rotate around source ref position (keeps relative layout intact)
if delta_rot != 0.0:
tgt_fp.Rotate(src_ref_pos, pcbnew.EDA_ANGLE(delta_rot, pcbnew.DEGREES_T))
# Translate to target
tgt_fp.Move(delta_pos)
count += 1
pcbnew.Refresh()
return count
# ---------------------------------------------------------------------------
# Dialog
# ---------------------------------------------------------------------------
class Layout_Replicator_Dialog(wx.Dialog):
def __init__(self, parent, board):
super().__init__(
parent,
title='Layout Replicator',
size=(480, 500),
style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER,
)
self.board = board
self._ref_fps = []
sizer = wx.BoxSizer(wx.VERTICAL)
# Sheet selectors
grid = wx.FlexGridSizer(2, 2, 6, 8)
grid.AddGrowableCol(1)
sheets = get_sheets(board)
grid.Add(wx.StaticText(self, label='Source sheet:'), 0, wx.ALIGN_CENTER_VERTICAL)
self.source_choice = wx.Choice(self, choices=sheets)
grid.Add(self.source_choice, 1, wx.EXPAND)
grid.Add(wx.StaticText(self, label='Target sheet:'), 0, wx.ALIGN_CENTER_VERTICAL)
self.target_choice = wx.Choice(self, choices=sheets)
grid.Add(self.target_choice, 1, wx.EXPAND)
sizer.Add(grid, 0, wx.EXPAND | wx.ALL, 8)
# Reference component list
sizer.Add(
wx.StaticText(self, label='Reference component (from source sheet):'),
0, wx.LEFT | wx.TOP, 8,
)
self.ref_list = wx.ListCtrl(
self,
style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.LC_HRULES | wx.LC_VRULES,
)
self.ref_list.InsertColumn(0, 'Designation', width=100)
self.ref_list.InsertColumn(1, 'Value', width=130)
self.ref_list.InsertColumn(2, 'Package', width=190)
sizer.Add(self.ref_list, 1, wx.EXPAND | wx.ALL, 6)
# Status label
self.status = wx.StaticText(self, label='')
sizer.Add(self.status, 0, wx.LEFT | wx.BOTTOM, 8)
# Buttons
btn_row = wx.BoxSizer(wx.HORIZONTAL)
self.btn_apply = wx.Button(self, label='Apply')
btn_close = wx.Button(self, wx.ID_CLOSE, 'Close')
self.btn_apply.Bind(wx.EVT_BUTTON, self._on_apply)
btn_close.Bind(wx.EVT_BUTTON, lambda e: self.Close())
btn_row.Add(self.btn_apply, 0, wx.RIGHT, 6)
btn_row.Add(btn_close)
sizer.Add(btn_row, 0, wx.ALIGN_RIGHT | wx.ALL, 8)
self.SetSizer(sizer)
self.Centre()
self.source_choice.Bind(wx.EVT_CHOICE, lambda e: self._refresh_ref_list())
if sheets:
self.source_choice.SetSelection(0)
self.target_choice.SetSelection(1 if len(sheets) > 1 else 0)
self._refresh_ref_list()
def _refresh_ref_list(self):
self.ref_list.DeleteAllItems()
idx = self.source_choice.GetSelection()
if idx == wx.NOT_FOUND:
return
sheet = self.source_choice.GetString(idx)
fps = sorted(get_sheet_fps(self.board, sheet), key=lambda f: f.GetReference())
self._ref_fps = fps
for i, fp in enumerate(fps):
self.ref_list.InsertItem(i, fp.GetReference())
self.ref_list.SetItem(i, 1, fp.GetValue())
self.ref_list.SetItem(i, 2, str(fp.GetFPID().GetLibItemName()))
def _on_apply(self, event):
src_idx = self.source_choice.GetSelection()
tgt_idx = self.target_choice.GetSelection()
ref_idx = self.ref_list.GetFirstSelected()
if src_idx == wx.NOT_FOUND or tgt_idx == wx.NOT_FOUND:
self.status.SetLabel('Select source and target sheets.')
return
if ref_idx == -1:
self.status.SetLabel('Select a reference component.')
return
source_sheet = self.source_choice.GetString(src_idx)
target_sheet = self.target_choice.GetString(tgt_idx)
if source_sheet == target_sheet:
self.status.SetLabel('Source and target must be different sheets.')
return
ref_fp = self._ref_fps[ref_idx]
try:
count = apply_replication(self.board, source_sheet, target_sheet, ref_fp)
self.status.SetLabel(f'Done — {count} component(s) placed.')
except Exception:
self._show_error(traceback.format_exc())
def _show_error(self, msg):
self.status.SetLabel('Error — see details.')
dlg = wx.Dialog(self, title='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()
# ---------------------------------------------------------------------------
# Plugin
# ---------------------------------------------------------------------------
class Layout_Replicator_Action(pcbnew.ActionPlugin):
def defaults(self):
self.name = 'Layout Replicator'
self.category = 'Placement'
self.description = 'Replicate component placement from one hierarchical sheet instance to another'
self.show_toolbar_button = True
self.icon_file_name = ''
def Run(self):
try:
board = pcbnew.GetBoard()
dlg = Layout_Replicator_Dialog(None, board)
dlg.ShowModal()
dlg.Destroy()
except Exception:
msg = traceback.format_exc()
dlg = wx.Dialog(None, title='Layout Replicator — 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()
Layout_Replicator_Action().register()