Copy and Paste UV
authorNutti <nutti.metro@gmail.com>
Wed, 31 Dec 2014 05:30:25 +0000 (14:30 +0900)
committerNutti <nutti.metro@gmail.com>
Wed, 31 Dec 2014 05:33:00 +0000 (14:33 +0900)
===================
- Upload script
  Support new feature, and some optimization about this script.
    1. Support copy/paste between UV maps.
2. Support copy/paste by selection sequence.
- Wiki update

The wiki page is ready, see here:
http://wiki.blender.org/index.php/Extensions:2.6/Py/Scripts/UV/Copy_Paste_UV

Modified Paths:
-------------------
blender-addons-contrib/uv_copy_and_paste_uv.py

uv_copy_and_paste_uv.py

index 84a74bf..9cb41cd 100644 (file)
 # ##### END GPL LICENSE BLOCK #####
 
 import bpy
+import bmesh
+import math
+from collections import namedtuple
+from bpy.props import *
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "2.0"
+__date__ = "20 December 2014"
 
 bl_info = {
     "name" : "Copy and Paste UV",
     "author" : "Nutti",
-    "version" : (1,1),
-    "blender" : (2, 6, 5),
+    "version" : (2,0),
+    "blender" : (2, 7, 2),
     "location" : "UV Mapping > Copy and Paste UV",
     "description" : "Copy and Paste UV data",
     "warning" : "",
@@ -33,9 +42,26 @@ bl_info = {
     "category" : "UV"
 }
 
-src_indices = None           # source indices
-dest_indices = None          # destination indices
+SelectedFaceInfo = namedtuple('SelectedFaceInfo', 'normal indices')
+
+src_uv_map = None            # source uv map
 src_obj = None               # source object
+src_sel_face_info = None     # source selected faces information
+dest_sel_face_info = None    # destination selected faces information
+
+# master menu
+class CopyAndPasteUVMenu(bpy.types.Menu):
+    bl_idname = "uv.copy_and_paste_uv_menu"
+    bl_label = "Copy/Paste UV"
+    bl_description = "Copy and Paste UV Menu"
+
+    def draw(self, context):
+        self.layout.operator(CopyAndPasteUVCopyUV.bl_idname)
+        self.layout.operator(CopyAndPasteUVPasteUV.bl_idname)
+        self.layout.operator(CopyAndPasteUVCopyUVBySelSeq.bl_idname)
+        self.layout.operator(CopyAndPasteUVPasteUVBySelSeq.bl_idname)
+        self.layout.menu(CopyAndPasteUVCopyUVMap.bl_idname)
+        self.layout.menu(CopyAndPasteUVPasteUVMap.bl_idname)
 
 
 # copy UV
@@ -48,40 +74,25 @@ class CopyAndPasteUVCopyUV(bpy.types.Operator):
     bl_options = {'REGISTER', 'UNDO'}
 
     def execute(self, context):
-        
-        # global variables
-        global src_indices
-        global dest_indices
+        global src_sel_face_info
+        global src_uv_map
         global src_obj
+    
+        self.report({'INFO'}, "Copy UV coordinate.")
         
-        # get active (source) object to be copied from
-        active_obj = bpy.context.active_object;
-
-        # change to 'OBJECT' mode, in order to access internal data
-        mode_orig = bpy.context.object.mode
-        bpy.ops.object.mode_set(mode='OBJECT')
-
-        # create source indices list
-        src_indices = []
-        for i in range(len(active_obj.data.polygons)):
-            # get selected faces
-            poly = active_obj.data.polygons[i]
-            if poly.select:
-               src_indices.extend(poly.loop_indices)
-        
-        # check if any faces are selected
-        if len(src_indices) == 0:
-            self.report({'WARNING'}, "No faces are not selected.")
-            bpy.ops.object.mode_set(mode=mode_orig)
+        # prepare for coping
+        ret, src_obj, mode_orig = prep_copy()
+        if ret != 0:
             return {'CANCELLED'}
-        else:
-            self.report(
-                {'INFO'},
-                 "%d indices are selected." % len(src_indices))
-            src_obj = active_obj
         
-        # revert to original mode
-        bpy.ops.object.mode_set(mode=mode_orig)
+        # copy
+        src_sel_face_info = get_selected_faces(src_obj)
+        ret, src_uv_map = copy_opt(self, "", src_obj, src_sel_face_info)
+        
+        # finish coping
+        fini_copy(mode_orig)
+        if ret != 0:
+            return {'CANCELLED'}
         
         return {'FINISHED'}
 
@@ -96,65 +107,420 @@ class CopyAndPasteUVPasteUV(bpy.types.Operator):
     bl_options = {'REGISTER', 'UNDO'}
 
     def execute(self, context):
+        global src_sel_face_info
+        global dest_sel_face_info
+        global src_uv_map
+        global src_obj
+    
+        self.report({'INFO'}, "Paste UV coordinate.")
+
+        # prepare for pasting
+        ret, dest_obj, mode_orig = prep_paste(src_obj, src_sel_face_info)
+        if ret != 0:
+            return {'CANCELLED'}
+        
+        # paste
+        dest_sel_face_info = get_selected_faces(dest_obj)
+        ret = paste_opt(
+            self, "", src_obj, src_sel_face_info,
+            src_uv_map, dest_obj, dest_sel_face_info)
+        
+        # finish pasting
+        fini_paste(mode_orig)
+        if ret != 0:
+            return {'CANCELLED'}
         
-        # global variables
-        global src_indices
-        global dest_indices
+        return {'FINISHED'}
+
+
+# copy UV (by selection sequence)
+class CopyAndPasteUVCopyUVBySelSeq(bpy.types.Operator):
+    """Copying UV coordinate on selected object by selection sequence."""
+    
+    bl_idname = "uv.copy_uv_sel_seq"
+    bl_label = "Copy UV (Selection Sequence)"
+    bl_description = "Copy UV data by selection sequence."
+    bl_options = {'REGISTER', 'UNDO'}
+
+    def execute(self, context):
+        global src_sel_face_info
+        global src_uv_map
         global src_obj
+
+        self.report({'INFO'}, "Copy UV coordinate. (sequence)")
         
-        # check if copying operation was executed
-        if src_indices is None or src_obj is None:
-               self.report({'WARNING'}, "Do copy operation at first.")
-               return {'CANCELLED'}
-        
-        # get active (source) object to be pasted to
-        active_obj = bpy.context.active_object
-
-        # change to 'OBJECT' mode, in order to access internal data
-        mode_orig = bpy.context.object.mode
-        bpy.ops.object.mode_set(mode='OBJECT')
-
-        # create source indices list
-        dest_indices = []
-        for i in range(len(active_obj.data.polygons)):
-            # get selected faces
-            poly = active_obj.data.polygons[i]
-            if poly.select:
-               dest_indices.extend(poly.loop_indices)
-        
-        if len(dest_indices) != len(src_indices):
-            self.report(
-                {'WARNING'},
-                "Number of selected faces is different from copied faces." +
-                "(src:%d, dest:%d)" % (len(src_indices), len(dest_indices)))
-            bpy.ops.object.mode_set(mode=mode_orig)
+        # prepare for coping
+        ret, src_obj, mode_orig = prep_copy()
+        if ret != 0:
             return {'CANCELLED'}
-        else:
-            dest_obj = active_obj
-
-        # update UV data
-        src_uv = src_obj.data.uv_layers.active         # source UV data
-        dest_uv = dest_obj.data.uv_layers.active       # destination UV data
-        for i in range(len(dest_indices)):
-            dest_data = dest_uv.data[dest_indices[i]]
-            src_data = src_uv.data[src_indices[i]]
-            dest_data.uv = src_data.uv
 
+        # copy
+        src_sel_face_info = get_selected_faces_by_sel_seq(src_obj)
+        ret, src_uv_map = copy_opt(self, "", src_obj, src_sel_face_info)
+
+        # finish coping
+        fini_copy(mode_orig)
+        if ret != 0:
+            return {'CANCELLED'}
+
+        return {'FINISHED'}
+
+
+# paste UV (by selection sequence)
+class CopyAndPasteUVPasteUVBySelSeq(bpy.types.Operator):
+    """Paste UV coordinate which is copied by selection sequence."""
+    
+    bl_idname = "uv.paste_uv_sel_seq"
+    bl_label = "Paste UV (Selection Sequence)"
+    bl_description = "Paste UV data by selection sequence."
+    bl_options = {'REGISTER', 'UNDO'}
+
+    flip_copied_uv = BoolProperty(
+        name = "flip_copied_uv",
+        description = "flip_copied_uv...",
+        default = False )
+
+    rotate_copied_uv = IntProperty(
+        default = 0,
+        min = 0,
+        max = 30 )
+
+    def execute(self, context):
+        global src_sel_face_info
+        global dest_sel_face_info
+        global src_uv_map
+        global src_obj
+
+        self.report({'INFO'}, "Paste UV coordinate. (sequence)")
+
+        # prepare for pasting
+        ret, dest_obj, mode_orig = prep_paste(src_obj, src_sel_face_info)
+        if ret != 0:
+            return {'CANCELLED'}
+
+        # paste
+        dest_sel_face_info = get_selected_faces_by_sel_seq(dest_obj)
+        ret = paste_opt(
+            self, "", src_obj, src_sel_face_info,
+            src_uv_map, dest_obj, dest_sel_face_info)
+            
+        # finish pasting
+        fini_paste(mode_orig)
+        if ret != 0:
+            return {'CANCELLED'}
+
+        return {'FINISHED'}
+
+
+# copy UV map (sub menu operator)
+class CopyAndPasteUVCopyUVMapSubOpt(bpy.types.Operator):
+    bl_idname = "uv.copy_uv_map_sub_opt"
+    bl_label = "Copy UV Map (Sub Menu Operator)"
+    uv_map = bpy.props.StringProperty()
+    
+    def execute(self, context):
+        global src_sel_face_info
+        global src_uv_map
+        global src_obj
+        
         self.report(
             {'INFO'},
-            "%d indices are copied." % len(dest_indices))
+            "Copy UV coordinate. (UV map:" + self.uv_map + ")")
+
+        # prepare for coping
+        ret, src_obj, mode_orig = prep_copy()
+        if ret != 0:
+            return {'CANCELLED'}
+        
+        # copy
+        src_sel_face_info = get_selected_faces(src_obj)
+        ret, src_uv_map = copy_opt(
+            self, self.uv_map, src_obj,
+            src_sel_face_info)
+        
+        # finish coping
+        fini_copy(mode_orig)
+        if ret != 0:
+            return {'CANCELLED'}
+        
+        return {'FINISHED'}
+
+
+# copy UV map
+class CopyAndPasteUVCopyUVMap(bpy.types.Menu):
+    """Copying UV map coordinate on selected object."""
+    
+    bl_idname = "uv.copy_uv_map"
+    bl_label = "Copy UV Map"
+    bl_description = "Copy UV map data"
+    bl_options = {'REGISTER', 'UNDO'}
 
-        # revert to original mode
-        bpy.ops.object.mode_set(mode=mode_orig)
+    def draw(self, context):
+        global menu_count
+    
+        layout = self.layout
+        
+        # create sub menu
+        uv_maps = bpy.context.active_object.data.uv_textures.keys()
+        for m in uv_maps:
+            layout.operator(
+                CopyAndPasteUVCopyUVMapSubOpt.bl_idname,
+                text=m).uv_map = m
+            
 
+# paste UV map (sub menu operator)
+class CopyAndPasteUVPasteUVMapSubOpt(bpy.types.Operator):
+    bl_idname = "uv.paste_uv_map_sub_opt"
+    bl_label = "Paste UV Map (Sub Menu Operator)"
+    uv_map = bpy.props.StringProperty()
+    
+    def execute(self, context):
+        global src_sel_face_info
+        global dest_sel_face_info
+        global src_uv_map
+        global src_obj
+        
+        self.report(
+            {'INFO'}, "Paste UV coordinate. (UV map:" + self.uv_map + ")")
+
+        # prepare for pasting
+        ret, dest_obj, mode_orig = prep_paste(src_obj, src_sel_face_info)
+        if ret != 0:
+            return {'CANCELLED'}
+        
+        # paste
+        dest_sel_face_info = get_selected_faces(dest_obj)
+        ret = paste_opt(
+            self, self.uv_map, src_obj, src_sel_face_info, src_uv_map,
+            dest_obj, dest_sel_face_info)
+        
+        # finish pasting
+        fini_paste(mode_orig)
+        if ret != 0:
+            return {'CANCELLED'}
+        
         return {'FINISHED'}
 
 
-# registration
+# paste UV map
+class CopyAndPasteUVPasteUVMap(bpy.types.Menu):
+    """Copying UV map coordinate on selected object."""
+    
+    bl_idname = "uv.paste_uv_map"
+    bl_label = "Paste UV Map"
+    bl_description = "Paste UV map data"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    def draw(self, context):
+        global menu_count
+    
+        layout = self.layout
+        
+        # create sub menu
+        uv_maps = bpy.context.active_object.data.uv_textures.keys()
+        for m in uv_maps:
+            layout.operator(
+                CopyAndPasteUVPasteUVMapSubOpt.bl_idname,
+                text=m).uv_map = m
+
+
+def prep_copy():
+    """
+    parepare for copy operation.
+    @return tuple(error code, active object, current mode)
+    """
+    # get active (source) object to be copied from
+    obj = bpy.context.active_object;
+
+    # change to 'OBJECT' mode, in order to access internal data
+    mode = bpy.context.object.mode
+    bpy.ops.object.mode_set(mode='OBJECT')
+    
+    return (0, obj, mode)
+
+
+# finish copy operation
+def fini_copy(mode_orig):
+    """
+    finish copy operation.
+    @param  mode_orig orignal mode
+    """
+    # revert to original mode
+    bpy.ops.object.mode_set(mode=mode_orig)
+
+
+# prepare for paste operation
+def prep_paste(src_obj, src_sel_face_info):
+    """
+    prepare for paste operation.
+    @param  src_obj object that is copied from
+    @param  src_sel_face_info information about faces will be copied
+    @return tuple(error code, active object, current mode)
+    """
+     # check if copying operation was executed
+    if src_sel_face_info is None or src_obj is None:
+        self.report({'WARNING'}, "Do copy operation at first.")
+        return (1, None, None)
+    
+    # get active (source) object to be pasted to
+    obj = bpy.context.active_object
+
+    # change to 'OBJECT' mode, in order to access internal data
+    mode = bpy.context.object.mode
+    bpy.ops.object.mode_set(mode='OBJECT')
+    
+    return (0, obj, mode)
+
+
+# finish paste operation
+def fini_paste(mode):
+    """
+    finish paste operation.
+    @param  mode_orig orignal mode
+    """
+    # revert to original mode
+    bpy.ops.object.mode_set(mode=mode)
+
+
+def get_selected_faces(obj):
+    """
+    get information about selected faces.
+    @param  obj object
+    @return information about selected faces (list of SelectedFaceInfo)
+    """
+    out = []
+    for i in range(len(obj.data.polygons)):
+        # get selected faces
+        poly = obj.data.polygons[i]
+        if poly.select:
+            face_info = SelectedFaceInfo(
+                poly.normal.copy(), list(poly.loop_indices))
+            out.append(face_info)
+    return out
+
+
+def get_selected_faces_by_sel_seq(obj):
+    """
+    get information about selected indices.
+    @param  obj object
+    @return information about selected faces (list of SelectedFaceInfo)
+    """
+    out = []
+    faces = []
+    
+    # get selection sequence
+    mode_orig = bpy.context.object.mode
+    bpy.ops.object.mode_set(mode='EDIT')
+    bm = bmesh.from_edit_mesh(obj.data)
+    if bpy.app.version[0] >= 2 and bpy.app.version[1] >= 73:
+        bm.faces.ensure_lookup_table()
+    for e in bm.select_history:
+        if isinstance(e, bmesh.types.BMFace) and e.select:
+            faces.append(e.loops[0].face.index)
+    
+    # get selected faces by selection sequence
+    bpy.ops.object.mode_set(mode='OBJECT')
+    for f in faces:
+        poly = obj.data.polygons[f]
+        face_info = SelectedFaceInfo(
+            poly.normal.copy(), list(poly.loop_indices))
+        out.append(face_info)
+        
+    bpy.ops.object.mode_set(mode=mode_orig)
+    
+    return out
+
+
+def copy_opt(self, uv_map, src_obj, src_sel_face_info):
+    """
+    copy operation.
+    @param  self operation object
+    @param  uv_map UV Map to be copied. (current map when null str)
+    @param  src_obj source object
+    @param  src_sel_face_info source information about selected faces
+    @return tuple(error code, UV map)
+    """
+    # check if any faces are selected
+    if len(src_sel_face_info) == 0:
+        self.report({'WARNING'}, "No faces are not selected.")
+        return (1, None)
+    else:
+        self.report(
+            {'INFO'}, "%d face(s) are selected." % len(src_sel_face_info))
+    
+    if uv_map == "":
+        uv_map = src_obj.data.uv_layers.active.name
+    else:
+        uv_map = uv_map
+    
+    return (0, uv_map)
+
+
+def paste_opt(self, uv_map, src_obj, src_sel_face_info,
+    src_uv_map, dest_obj, dest_sel_face_info):
+    """
+    paste operation.
+    @param  self operation object
+    @param  uv_map UV Map to be pasted. (current map when null str)
+    @param  src_obj source object
+    @param  src_sel_face_info source information about selected faces
+    @param  src_uv_map source UV map
+    @param  dest_obj destination object
+    @param  dest_sel_face_info destination information about selected faces
+    @return error code
+    """
+    if len(dest_sel_face_info) != len(src_sel_face_info):
+        self.report(
+            {'WARNING'},
+            "Number of selected faces is different from copied faces." +
+            "(src:%d, dest:%d)" %
+            (len(src_sel_face_info), len(dest_sel_face_info)))
+        return 1
+    for i in range(len(dest_sel_face_info)):
+        if (len(dest_sel_face_info[i].indices) !=
+            len(src_sel_face_info[i].indices)):
+            self.report({'WARNING'}, "Some faces are different size.")
+            return 1
+    
+    if uv_map == "":
+        dest_uv_map = dest_obj.data.uv_layers.active.name
+    else:
+        dest_uv_map = uv_map
+
+    # update UV data
+    src_uv = src_obj.data.uv_layers[src_uv_map]
+    dest_uv = dest_obj.data.uv_layers[dest_uv_map]
+
+    for i in range(len(dest_sel_face_info)):
+        dest_indices = dest_sel_face_info[i].indices
+        src_indices = src_sel_face_info[i].indices
+
+        # Flip UVs
+        if self.flip_copied_uv is True:
+            dest_indices = list(dest_indices)
+            dest_indices.reverse()
 
+        # Rotate UVs
+        for k in range(self.rotate_copied_uv):
+            item_rotate = dest_indices[-1]
+            dest_indices.remove(item_rotate)
+            dest_indices.insert(0, item_rotate)
+
+        # update
+        for j in range(len(dest_indices)):
+            dest_data = dest_uv.data[dest_indices[j]]
+            src_data = src_uv.data[src_indices[j]]
+            dest_data.uv = src_data.uv
+
+    self.report({'INFO'}, "%d faces are copied." % len(dest_sel_face_info))
+
+    return 0
+
+
+# registration
 def menu_func(self, context):
-    self.layout.operator("uv.copy_uv")
-    self.layout.operator("uv.paste_uv")
+    self.layout.separator()
+    self.layout.menu(CopyAndPasteUVMenu.bl_idname)
 
 
 def register():