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):
"""Direct children of sheet_name only."""
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."""
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]
return parts[-1] if parts else ''
if parts:
return '/' + '/'.join(parts[:-1]) + '/'
return None
def build_uuid_map(footprints):
return {local_uuid(fp): fp for fp in footprints}
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):
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_uuid_map(target_fps)
ref_uuid = local_uuid(ref_fp)
target_map = build_key_map(target_fps, tgt_prefix)
ref_key = local_key(ref_fp, src_prefix)
target_ref = target_map.get(ref_uuid)
# 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')
@@ -58,10 +90,10 @@ def apply_replication(board, source_sheet, target_sheet, ref_fp):
count = 0
for src_fp in source_fps:
uuid = local_uuid(src_fp)
if uuid == ref_uuid:
key = local_key(src_fp, src_prefix)
if key == ref_key:
continue
tgt_fp = target_map.get(uuid)
tgt_fp = target_map.get(key)
if tgt_fp is None:
continue
@@ -115,6 +147,9 @@ class Layout_Replicator_Dialog(wx.Dialog):
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):'),
@@ -186,9 +221,10 @@ class Layout_Replicator_Dialog(wx.Dialog):
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)
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())