289 lines
9.3 KiB
Python
289 lines
9.3 KiB
Python
import pcbnew
|
|
import wx
|
|
import traceback
|
|
|
|
from .via_net_assign import Via_Net_Assign_Action
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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):
|
|
"""Direct children of sheet_name only."""
|
|
return [fp for fp in board.GetFootprints() if fp.GetSheetname() == sheet_name]
|
|
|
|
|
|
def get_sheet_prefix(board, sheet_name):
|
|
"""Return the path prefix for sheet_name, e.g. '/uuid_sheet/'.
|
|
Determined from any direct child component of that sheet."""
|
|
for fp in board.GetFootprints():
|
|
if fp.GetSheetname() == sheet_name:
|
|
path = fp.GetPath().AsString()
|
|
parts = [p for p in path.split('/') if p]
|
|
if parts:
|
|
return '/' + '/'.join(parts[:-1]) + '/'
|
|
return None
|
|
|
|
|
|
def get_sheet_fps_hierarchical(board, sheet_prefix):
|
|
"""All components whose path starts with sheet_prefix (includes sub-sheets)."""
|
|
return [fp for fp in board.GetFootprints()
|
|
if fp.GetPath().AsString().startswith(sheet_prefix)]
|
|
|
|
|
|
def local_key(fp, sheet_prefix):
|
|
"""Path relative to the sheet prefix — used to match components across instances.
|
|
For direct children: 'uuid_sym'.
|
|
For sub-sheet children: 'uuid_subsheet/uuid_sym'."""
|
|
path = fp.GetPath().AsString()
|
|
return path[len(sheet_prefix):]
|
|
|
|
|
|
def build_key_map(footprints, sheet_prefix):
|
|
return {local_key(fp, sheet_prefix): fp for fp in footprints}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Transform
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def apply_replication(board, source_sheet, target_sheet, ref_fp, hierarchical=False):
|
|
src_prefix = get_sheet_prefix(board, source_sheet)
|
|
tgt_prefix = get_sheet_prefix(board, target_sheet)
|
|
|
|
if src_prefix is None or tgt_prefix is None:
|
|
raise ValueError('Could not determine sheet path prefix')
|
|
|
|
if hierarchical:
|
|
source_fps = get_sheet_fps_hierarchical(board, src_prefix)
|
|
target_fps = get_sheet_fps_hierarchical(board, tgt_prefix)
|
|
else:
|
|
source_fps = get_sheet_fps(board, source_sheet)
|
|
target_fps = get_sheet_fps(board, target_sheet)
|
|
|
|
target_map = build_key_map(target_fps, tgt_prefix)
|
|
ref_key = local_key(ref_fp, src_prefix)
|
|
|
|
# ref_fp is from the source sheet, so its key uses src_prefix.
|
|
# Find the matching component in target using tgt_prefix.
|
|
target_ref = target_map.get(ref_key)
|
|
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:
|
|
key = local_key(src_fp, src_prefix)
|
|
if key == ref_key:
|
|
continue
|
|
tgt_fp = target_map.get(key)
|
|
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)
|
|
|
|
self.hierarchical_cb = wx.CheckBox(self, label='Include sub-sheets (hierarchical)')
|
|
sizer.Add(self.hierarchical_cb, 0, wx.LEFT | wx.BOTTOM, 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())
|
|
self.hierarchical_cb.Bind(wx.EVT_CHECKBOX, 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)
|
|
if self.hierarchical_cb.IsChecked():
|
|
prefix = get_sheet_prefix(self.board, sheet)
|
|
fps = get_sheet_fps_hierarchical(self.board, prefix) if prefix else []
|
|
else:
|
|
fps = get_sheet_fps(self.board, sheet)
|
|
fps = sorted(fps, 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]
|
|
hierarchical = self.hierarchical_cb.IsChecked()
|
|
|
|
try:
|
|
count = apply_replication(self.board, source_sheet, target_sheet, ref_fp, hierarchical)
|
|
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()
|
|
Via_Net_Assign_Action().register()
|