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()
Previous
Previous

Elena Of Avalor S3