UV Transfer Tool
A simple UV Transfer tool that has helped me a lot, my delusional self, thought it was easier to develop a tool than to manually transfer UVs between 100+ screws back in 2021…
#Gerardo Contreras 2025 August, Version 2.0 from maya import cmds class UVTransferTool: def __init__(self): self.source = [] self.target = [] self.transfer_method = 5 # Default: Topology self.symmetry_axis_scale = (-1, 1, 1) # Default X axis self.win_name = "UVTransferToolWin" self.match_names = True # Default ON for Group tab self.current_tab = 0 # 0 = Duplicates, 1 = Group, 2 = Symmetry self.sample_space_group = None self.sample_space_buttons = {} # ---------------- Utilities ---------------- def delete_history(self, objs=None): if objs is None: objs = cmds.ls(selection=True) if not objs: return for obj in objs: if cmds.objExists(obj): cmds.delete(obj, constructionHistory=True) def notify(self, msg="Operation Completed", success=True): color = "white" if success else "#FF4444" cmds.inViewMessage( amg=f'<span style="color:{color};">{msg}</span>', pos="topCenter", fade=True ) def reset_tool(self, *args): # Reset internal state self.source = [] self.target = [] self.transfer_method = 5 self.symmetry_axis_scale = (-1, 1, 1) self.match_names = True self.current_tab = 0 # Reset Sample Space radio group if self.sample_space_group and "Topology" in self.sample_space_buttons: cmds.radioCollection(self.sample_space_group, edit=True, select=self.sample_space_buttons["Topology"]) # Reset symmetry axis radios if hasattr(self, "axis_radio_collection") and hasattr(self, "axis_radio_buttons"): try: cmds.radioCollection(self.axis_radio_collection, edit=True, select=self.axis_radio_buttons["X"]) except Exception: pass # Reset the Group tab checkbox if it exists if hasattr(self, "match_checkbox"): try: cmds.checkBox(self.match_checkbox, edit=True, value=True) except Exception: pass # Jump back to first tab if hasattr(self, "tab_layout") and cmds.tabLayout(self.tab_layout, exists=True): try: cmds.tabLayout(self.tab_layout, edit=True, selectTabIndex=1) except Exception: pass self.notify("Tool reset to defaults", success=True) def show_help(self, *args): help_text = ( "Instructions:\n" "\n--- Duplicates ---\n" "Select source object first, then one or more targets.\n" "\n--- Group ---\n" "Set Source and Target groups with the buttons in the Group tab.\n" "Match Name will transfer to objects with the exact name ignoring the __pasted suffix created by maya\n" "\n--- Symmetry ---\n" "Select exactly 2 objects: source to mirror first, then target.\n Then make sure your axis matches the direction of the target.\n" "\n--- General ---\n" "After every transfer, the tool will run Delete All by Type > History." ) cmds.confirmDialog( title="UV Transfer Tool - Help", message=help_text, button=["OK"], defaultButton="OK" ) # ---------------- UI Builders ---------------- def build_transfer_mode_ui(self): cmds.text("Sample Space:", align="left") row = cmds.rowLayout(numberOfColumns=5, adjustableColumn=5, columnAlign=(1, "center"), columnAttach5=["both"]*5) self.sample_space_group = cmds.radioCollection() options = ["World", "Local", "UV Space", "Component", "Topology"] sample_spaces = [0, 1, 3, 4, 5] for name, space in zip(options, sample_spaces): self.sample_space_buttons[name] = cmds.radioButton( label=name, onCommand=lambda _, m=space: self.set_transfer_method(m) ) cmds.radioCollection(self.sample_space_group, edit=True, select=self.sample_space_buttons["Topology"]) cmds.setParent("..") def set_transfer_method(self, method): self.transfer_method = method def build_name_match_ui(self): self.match_checkbox = cmds.checkBox( label="Match exact name objects and ignores [__pasted] suffix", value=True, onc=lambda *_: self.set_match_names(True), ofc=lambda *_: self.set_match_names(False) ) def set_match_names(self, state): self.match_names = state def build_symmetry_axis_ui(self): cmds.text("Mirror Axis:", align="left") row = cmds.rowLayout(numberOfColumns=3, adjustableColumn=3, columnAlign=(1, "center")) self.axis_radio_collection = cmds.radioCollection() self.axis_radio_buttons = {} axes = ["X", "Y", "Z"] scales = [(-1, 1, 1), (1, -1, 1), (1, 1, -1)] for name, scale in zip(axes, scales): self.axis_radio_buttons[name] = cmds.radioButton( label=name, onCommand=lambda _, s=scale: self.set_symmetry_axis(s) ) cmds.radioCollection(self.axis_radio_collection, edit=True, select=self.axis_radio_buttons["X"]) cmds.setParent("..") def set_symmetry_axis(self, scale_tuple): self.symmetry_axis_scale = scale_tuple # ---------------- Selection Helpers ---------------- def select_source(self, *args): self.source = cmds.ls(sl=1, dag=1, type=['mesh']) self.notify( f"Source set: {len(self.source)} mesh(es)" if self.source else "No source selected", success=bool(self.source) ) def select_target(self, *args): self.target = cmds.ls(sl=1, dag=1, type=['mesh']) self.notify( f"Target set: {len(self.target)} mesh(es)" if self.target else "No target selected", success=bool(self.target) ) # ---------------- Core Functions ---------------- def copy_uvs_duplicates(self): selected = cmds.ls(sl=True, type='transform') if not selected or len(selected) < 2: raise RuntimeError("Select at least 2 objects (source + target).") driver = selected.pop(0) for obj in selected: print(f"[TRANSFER] source: {driver}, target: {obj}, sampleSpace: {self.transfer_method}") cmds.select([driver, obj], r=True) cmds.transferAttributes( sampleSpace=self.transfer_method, transferUVs=2, transferColors=2 ) cmds.delete(all=True, constructionHistory=True) def copy_uvs_group(self): if not self.source or not self.target: raise RuntimeError("Please select source and target first.") if self.match_names: for src in self.source: src_name = src.split("|")[-1].replace("pasted__", "") for tgt in self.target: tgt_name = tgt.split("|")[-1].replace("pasted__", "") if src_name != tgt_name: continue print(f"[TRANSFER] source: {src}, target: {tgt}, sampleSpace: {self.transfer_method}") cmds.select([src, tgt], r=True) cmds.transferAttributes( transferUVs=2, transferColors=2, sampleSpace=self.transfer_method ) else: for src in self.source: for tgt in self.target: print(f"[TRANSFER] source: {src}, target: {tgt}, sampleSpace: {self.transfer_method}") cmds.select([src, tgt], r=True) cmds.transferAttributes( transferUVs=2, transferColors=2, sampleSpace=self.transfer_method ) cmds.delete(all=True, constructionHistory=True) def symmetrical_copy(self): selected = cmds.ls(sl=True, type='transform') if not selected or len(selected) != 2: raise RuntimeError("Select exactly 2 objects: source to mirror + target.") source_obj = selected[0] target_obj = selected[1] duplicate_list = cmds.duplicate(source_obj, name=f"{source_obj}_symDuplicate") duplicate = duplicate_list[0] cmds.scale(*self.symmetry_axis_scale, duplicate) cmds.makeIdentity(duplicate, apply=True, t=1, r=1, s=1, n=0, pn=1) self.delete_history(duplicate) print(f"[TRANSFER] source: {duplicate}, target: {target_obj}, sampleSpace: {self.transfer_method}") cmds.select([duplicate, target_obj], r=True) cmds.transferAttributes( sampleSpace=self.transfer_method, transferUVs=2, transferColors=2 ) self.delete_history([duplicate, source_obj, target_obj]) cmds.delete(duplicate) cmds.delete(all=True, constructionHistory=True) def run_transfer(self, *args): try: if self.current_tab == 0: self.copy_uvs_duplicates() self.notify("UV Transfer Completed (Duplicates)", success=True) elif self.current_tab == 1: self.copy_uvs_group() self.notify("UV Transfer Completed (Group)", success=True) elif self.current_tab == 2: self.symmetrical_copy() self.notify("UV Transfer Completed (Symmetry)", success=True) except Exception as e: self.notify(f"UV Transfer Failed: {str(e)}", success=False) raise # ---------------- UI ---------------- def build_ui(self): if cmds.window(self.win_name, exists=True): cmds.deleteUI(self.win_name) window = cmds.window(self.win_name, title="UV Transfer Tool", widthHeight=(600, 420), sizeable=True) # Toolbar (menu bar style) cmds.menuBarLayout() cmds.menu(label="Edit") cmds.menuItem(label="Reset Tool", command=self.reset_tool) cmds.menu(label="Help") cmds.menuItem(label="Instructions", command=self.show_help) main_layout = cmds.columnLayout(adjustableColumn=True, rowSpacing=8) # Shared Sample Space self.build_transfer_mode_ui() cmds.separator(h=10, style="in") # Tabs self.tab_layout = cmds.tabLayout(innerMarginWidth=10, innerMarginHeight=10, changeCommand=self.update_tab) tabs = [] # --- Duplicates tab --- tab1 = cmds.columnLayout(adjustableColumn=True, rowSpacing=5) cmds.text(label="Copy UVs to Duplicates:\nSelect source object first, then one or more targets.", align="left") tabs.append(tab1) cmds.setParent("..") # --- Group tab --- tab2 = cmds.columnLayout(adjustableColumn=True, rowSpacing=5) cmds.text(label="Copy UVs between Groups:\nSet Source and Target groups with the buttons below.", align="left") self.build_name_match_ui() cmds.rowLayout(numberOfColumns=2, columnWidth2=(270, 270), adjustableColumn=2, columnAlign2=("center", "center")) cmds.button(label="Select Source Group", command=self.select_source, h=30) cmds.button(label="Select Target Group", command=self.select_target, h=30) cmds.setParent("..") tabs.append(tab2) cmds.setParent("..") # --- Symmetry tab --- tab3 = cmds.columnLayout(adjustableColumn=True, rowSpacing=5) cmds.text(label="Symmetry Transfer:\nSelect exactly 2 objects: source to mirror, then target.", align="left") self.build_symmetry_axis_ui() tabs.append(tab3) cmds.setParent("..") cmds.tabLayout(self.tab_layout, edit=True, tabLabel=[(tabs[0], "Duplicates"), (tabs[1], "Group"), (tabs[2], "Symmetry")]) # Bottom action buttons cmds.setParent("..") cmds.separator(h=15, style="in") cmds.button(label="Transfer", command=self.run_transfer, h=30) cmds.separator(h=10, style="in") cmds.button(label="Close", command=lambda *_: cmds.deleteUI(window, window=True), h=30) cmds.showWindow(window) def update_tab(self, *_): # Update current tab index (1-based -> 0-based) self.current_tab = cmds.tabLayout(self.tab_layout, query=True, selectTabIndex=True) - 1 # Run the tool tool = UVTransferTool() tool.build_ui()