Add hierarchical mode to layout replicator

A checkbox enables hierarchical operation: when checked, components in
sub-sheets are included in the replication. Matching uses the full local
path (relative to the sheet prefix) instead of just the last UUID, so
sub-sheet components match correctly across instances.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-27 19:47:18 +00:00
parent 0157e773b5
commit 843d88d0c4

View File

@@ -19,32 +19,64 @@ def get_sheets(board):
def get_sheet_fps(board, sheet_name): 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] return [fp for fp in board.GetFootprints() if fp.GetSheetname() == sheet_name]
def local_uuid(fp): def get_sheet_prefix(board, sheet_name):
"""Last segment of the hierarchical path — same across all instances of a sheet.""" """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() path = fp.GetPath().AsString()
parts = [p for p in path.split('/') if p] return path[len(sheet_prefix):]
return parts[-1] if parts else ''
def build_uuid_map(footprints): def build_key_map(footprints, sheet_prefix):
return {local_uuid(fp): fp for fp in footprints} return {local_key(fp, sheet_prefix): fp for fp in footprints}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Transform # Transform
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def apply_replication(board, source_sheet, target_sheet, ref_fp): def apply_replication(board, source_sheet, target_sheet, ref_fp, hierarchical=False):
source_fps = get_sheet_fps(board, source_sheet) src_prefix = get_sheet_prefix(board, source_sheet)
target_fps = get_sheet_fps(board, target_sheet) tgt_prefix = get_sheet_prefix(board, target_sheet)
target_map = build_uuid_map(target_fps) if src_prefix is None or tgt_prefix is None:
ref_uuid = local_uuid(ref_fp) raise ValueError('Could not determine sheet path prefix')
target_ref = target_map.get(ref_uuid) 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: if target_ref is None:
raise ValueError('Could not find matching reference component in target sheet') raise ValueError('Could not find matching reference component in target sheet')
@@ -58,10 +90,10 @@ def apply_replication(board, source_sheet, target_sheet, ref_fp):
count = 0 count = 0
for src_fp in source_fps: for src_fp in source_fps:
uuid = local_uuid(src_fp) key = local_key(src_fp, src_prefix)
if uuid == ref_uuid: if key == ref_key:
continue continue
tgt_fp = target_map.get(uuid) tgt_fp = target_map.get(key)
if tgt_fp is None: if tgt_fp is None:
continue continue
@@ -115,6 +147,9 @@ class Layout_Replicator_Dialog(wx.Dialog):
sizer.Add(grid, 0, wx.EXPAND | wx.ALL, 8) 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 # Reference component list
sizer.Add( sizer.Add(
wx.StaticText(self, label='Reference component (from source sheet):'), wx.StaticText(self, label='Reference component (from source sheet):'),
@@ -186,9 +221,10 @@ class Layout_Replicator_Dialog(wx.Dialog):
return return
ref_fp = self._ref_fps[ref_idx] ref_fp = self._ref_fps[ref_idx]
hierarchical = self.hierarchical_cb.IsChecked()
try: try:
count = apply_replication(self.board, source_sheet, target_sheet, ref_fp) count = apply_replication(self.board, source_sheet, target_sheet, ref_fp, hierarchical)
self.status.SetLabel(f'Done — {count} component(s) placed.') self.status.SetLabel(f'Done — {count} component(s) placed.')
except Exception: except Exception:
self._show_error(traceback.format_exc()) self._show_error(traceback.format_exc())