diff --git a/uv_magic_uv/__init__.py b/uv_magic_uv/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..7d18c1596c033175f2a6592248105ed7ac32aa4f
--- /dev/null
+++ b/uv_magic_uv/__init__.py
@@ -0,0 +1,126 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "4.3"
+__date__ = "1 Apr 2017"
+
+
+bl_info = {
+    "name": "Magic UV",
+    "author": "Nutti, Mifth, Jace Priester, kgeogeo, mem, Keith (Wahooney) Boshoff, McBuff, MaxRobinot",
+    "version": (4, 3),
+    "blender": (2, 77, 0),
+    "location": "See Add-ons Preferences",
+    "description": "UV Manipulator Tools. See Add-ons Preferences for details",
+    "warning": "",
+    "support": "COMMUNITY",
+    "wiki_url": "https://github.com/nutti/Magic-UV/wikil",
+    "tracker_url": "https://github.com/nutti/Copy-And-Paste-UV",
+    "category": "UV"
+}
+
+if "bpy" in locals():
+    import imp
+    imp.reload(muv_preferences)
+    imp.reload(muv_menu)
+    imp.reload(muv_common)
+    imp.reload(muv_props)
+    imp.reload(muv_cpuv_ops)
+    imp.reload(muv_cpuv_selseq_ops)
+    imp.reload(muv_fliprot_ops)
+    imp.reload(muv_transuv_ops)
+    imp.reload(muv_uvbb_ops)
+    imp.reload(muv_mvuv_ops)
+    imp.reload(muv_texproj_ops)
+    imp.reload(muv_packuv_ops)
+    imp.reload(muv_texlock_ops)
+    imp.reload(muv_mirroruv_ops)
+    imp.reload(muv_wsuv_ops)
+    imp.reload(muv_unwrapconst_ops)
+    imp.reload(muv_preserve_uv_aspect)
+else:
+    from . import muv_preferences
+    from . import muv_menu
+    from . import muv_common
+    from . import muv_props
+    from . import muv_cpuv_ops
+    from . import muv_cpuv_selseq_ops
+    from . import muv_fliprot_ops
+    from . import muv_transuv_ops
+    from . import muv_uvbb_ops
+    from . import muv_mvuv_ops
+    from . import muv_texproj_ops
+    from . import muv_packuv_ops
+    from . import muv_texlock_ops
+    from . import muv_mirroruv_ops
+    from . import muv_wsuv_ops
+    from . import muv_unwrapconst_ops
+    from . import muv_preserve_uv_aspect
+
+import bpy
+
+
+def view3d_uvmap_menu_fn(self, context):
+    self.layout.separator()
+    self.layout.menu(muv_menu.MUV_CPUVMenu.bl_idname, icon="PLUGIN")
+    self.layout.operator(muv_fliprot_ops.MUV_FlipRot.bl_idname, icon="PLUGIN")
+    self.layout.menu(muv_menu.MUV_TransUVMenu.bl_idname, icon="PLUGIN")
+    self.layout.operator(muv_mvuv_ops.MUV_MVUV.bl_idname, icon="PLUGIN")
+    self.layout.menu(muv_menu.MUV_TexLockMenu.bl_idname, icon="PLUGIN")
+    self.layout.operator(
+        muv_mirroruv_ops.MUV_MirrorUV.bl_idname, icon="PLUGIN")
+    self.layout.menu(muv_menu.MUV_WSUVMenu.bl_idname, icon="PLUGIN")
+    self.layout.operator(
+        muv_unwrapconst_ops.MUV_UnwrapConstraint.bl_idname, icon='PLUGIN')
+    self.layout.menu(
+        muv_preserve_uv_aspect.MUV_PreserveUVAspectMenu.bl_idname,
+        icon='PLUGIN')
+
+
+def image_uvs_menu_fn(self, context):
+    self.layout.separator()
+    self.layout.operator(muv_packuv_ops.MUV_PackUV.bl_idname, icon="PLUGIN")
+
+
+def view3d_object_menu_fn(self, context):
+    self.layout.separator()
+    self.layout.menu(muv_menu.MUV_CPUVObjMenu.bl_idname, icon="PLUGIN")
+
+
+def register():
+    bpy.utils.register_module(__name__)
+    bpy.types.VIEW3D_MT_uv_map.append(view3d_uvmap_menu_fn)
+    bpy.types.IMAGE_MT_uvs.append(image_uvs_menu_fn)
+    bpy.types.VIEW3D_MT_object.append(view3d_object_menu_fn)
+    muv_props.init_props(bpy.types.Scene)
+
+
+def unregister():
+    bpy.utils.unregister_module(__name__)
+    bpy.types.VIEW3D_MT_uv_map.remove(view3d_uvmap_menu_fn)
+    bpy.types.IMAGE_MT_uvs.remove(image_uvs_menu_fn)
+    bpy.types.VIEW3D_MT_object.remove(view3d_object_menu_fn)
+    muv_props.clear_props(bpy.types.Scene)
+
+
+if __name__ == "__main__":
+    register()
diff --git a/uv_magic_uv/muv_common.py b/uv_magic_uv/muv_common.py
new file mode 100644
index 0000000000000000000000000000000000000000..66f2a54b441353943f76dc38b422e47b20213269
--- /dev/null
+++ b/uv_magic_uv/muv_common.py
@@ -0,0 +1,86 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "4.3"
+__date__ = "1 Apr 2017"
+
+
+import bpy
+from . import muv_props
+
+
+def debug_print(*s):
+    """
+    Print message to console in debugging mode
+    """
+
+    if muv_props.DEBUG:
+        print(s)
+
+
+def check_version(major, minor, _):
+    """
+    Check blender version
+    """
+
+    if bpy.app.version[0] == major and bpy.app.version[1] == minor:
+        return 0
+    if bpy.app.version[0] > major:
+        return 1
+    else:
+        if bpy.app.version[1] > minor:
+            return 1
+        else:
+            return -1
+
+
+def redraw_all_areas():
+    """
+    Redraw all areas
+    """
+
+    for area in bpy.context.screen.areas:
+        area.tag_redraw()
+
+
+def get_space(area_type, region_type, space_type):
+    """
+    Get current area/region/space
+    """
+
+    area = None
+    region = None
+    space = None
+
+    for area in bpy.context.screen.areas:
+        if area.type == area_type:
+            break
+    else:
+        return (None, None, None)
+    for region in area.regions:
+        if region.type == region_type:
+            break
+    for space in area.spaces:
+        if space.type == space_type:
+            break
+
+    return (area, region, space)
diff --git a/uv_magic_uv/muv_cpuv_ops.py b/uv_magic_uv/muv_cpuv_ops.py
new file mode 100644
index 0000000000000000000000000000000000000000..3c5e968ab6dba423c336686c0e2001625e3c96f7
--- /dev/null
+++ b/uv_magic_uv/muv_cpuv_ops.py
@@ -0,0 +1,455 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>, Jace Priester"
+__status__ = "production"
+__version__ = "4.3"
+__date__ = "1 Apr 2017"
+
+
+import bpy
+import bmesh
+from bpy.props import StringProperty, BoolProperty, IntProperty, EnumProperty
+from . import muv_common
+
+
+def memorize_view_3d_mode(fn):
+    def __memorize_view_3d_mode(self, context):
+        mode_orig = bpy.context.object.mode
+        result = fn(self, context)
+        bpy.ops.object.mode_set(mode=mode_orig)
+        return result
+    return __memorize_view_3d_mode
+
+
+class MUV_CPUVCopyUV(bpy.types.Operator):
+    """
+    Operation class: Copy UV coordinate
+    """
+
+    bl_idname = "uv.muv_cpuv_copy_uv"
+    bl_label = "Copy UV (Operation)"
+    bl_description = "Copy UV coordinate (Operation)"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    uv_map = StringProperty(options={'HIDDEN'})
+
+    def execute(self, context):
+        props = context.scene.muv_props.cpuv
+        if self.uv_map == "":
+            self.report({'INFO'}, "Copy UV coordinate")
+        else:
+            self.report(
+                {'INFO'}, "Copy UV coordinate (UV map:%s)" % (self.uv_map))
+        obj = context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.faces.ensure_lookup_table()
+
+        # get UV layer
+        if self.uv_map == "":
+            if not bm.loops.layers.uv:
+                self.report(
+                    {'WARNING'}, "Object must have more than one UV map")
+                return {'CANCELLED'}
+            uv_layer = bm.loops.layers.uv.verify()
+        else:
+            uv_layer = bm.loops.layers.uv[self.uv_map]
+
+        # get selected face
+        props.src_uvs = []
+        props.src_pin_uvs = []
+        for face in bm.faces:
+            if face.select:
+                uvs = [l[uv_layer].uv.copy() for l in face.loops]
+                pin_uvs = [l[uv_layer].pin_uv for l in face.loops]
+                props.src_uvs.append(uvs)
+                props.src_pin_uvs.append(pin_uvs)
+        if len(props.src_uvs) == 0 or len(props.src_pin_uvs) == 0:
+            self.report({'WARNING'}, "No faces are selected")
+            return {'CANCELLED'}
+        self.report({'INFO'}, "%d face(s) are selected" % len(props.src_uvs))
+
+        return {'FINISHED'}
+
+
+class MUV_CPUVCopyUVMenu(bpy.types.Menu):
+    """
+    Menu class: Copy UV coordinate
+    """
+
+    bl_idname = "uv.muv_cpuv_copy_uv_menu"
+    bl_label = "Copy UV"
+    bl_description = "Copy UV coordinate"
+
+    def draw(self, context):
+        layout = self.layout
+        # create sub menu
+        obj = context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+        uv_maps = bm.loops.layers.uv.keys()
+        layout.operator(
+            MUV_CPUVCopyUV.bl_idname,
+            text="[Default]",
+            icon="PLUGIN"
+        ).uv_map = ""
+        for m in uv_maps:
+            layout.operator(
+                MUV_CPUVCopyUV.bl_idname,
+                text=m,
+                icon="PLUGIN"
+            ).uv_map = m
+
+
+class MUV_CPUVPasteUV(bpy.types.Operator):
+    """
+    Operation class: Paste UV coordinate
+    """
+
+    bl_idname = "uv.muv_cpuv_paste_uv"
+    bl_label = "Paste UV (Operation)"
+    bl_description = "Paste UV coordinate (Operation)"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    uv_map = StringProperty(options={'HIDDEN'})
+    strategy = EnumProperty(
+        name="Strategy",
+        description="Paste Strategy",
+        items=[
+            ('N_N', 'N:N', 'Number of faces must be equal to source'),
+            ('N_M', 'N:M', 'Number of faces must not be equal to source')
+        ],
+        default="N_M"
+    )
+    flip_copied_uv = BoolProperty(
+        name="Flip Copied UV",
+        description="Flip Copied UV...",
+        default=False
+    )
+    rotate_copied_uv = IntProperty(
+        default=0,
+        name="Rotate Copied UV",
+        min=0,
+        max=30
+    )
+
+    def execute(self, context):
+        props = context.scene.muv_props.cpuv
+        if len(props.src_uvs) == 0 or len(props.src_pin_uvs) == 0:
+            self.report({'WARNING'}, "Need copy UV at first")
+            return {'CANCELLED'}
+        if self.uv_map == "":
+            self.report({'INFO'}, "Paste UV coordinate")
+        else:
+            self.report(
+                {'INFO'}, "Paste UV coordinate (UV map:%s)" % (self.uv_map))
+        obj = context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.faces.ensure_lookup_table()
+
+        # get UV layer
+        if self.uv_map == "":
+            if not bm.loops.layers.uv:
+                self.report(
+                    {'WARNING'}, "Object must have more than one UV map")
+                return {'CANCELLED'}
+            uv_layer = bm.loops.layers.uv.verify()
+        else:
+            uv_layer = bm.loops.layers.uv[self.uv_map]
+
+        # get selected face
+        dest_uvs = []
+        dest_pin_uvs = []
+        dest_face_indices = []
+        for face in bm.faces:
+            if face.select:
+                dest_face_indices.append(face.index)
+                uvs = [l[uv_layer].uv.copy() for l in face.loops]
+                pin_uvs = [l[uv_layer].pin_uv for l in face.loops]
+                dest_uvs.append(uvs)
+                dest_pin_uvs.append(pin_uvs)
+        if len(dest_uvs) == 0 or len(dest_pin_uvs) == 0:
+            self.report({'WARNING'}, "No faces are selected")
+            return {'CANCELLED'}
+        if self.strategy == 'N_N' and len(props.src_uvs) != len(dest_uvs):
+            self.report(
+                {'WARNING'},
+                "Number of selected faces is different from copied" +
+                "(src:%d, dest:%d)" %
+                (len(props.src_uvs), len(dest_uvs)))
+            return {'CANCELLED'}
+
+        # paste
+        for i, idx in enumerate(dest_face_indices):
+            suv = None
+            spuv = None
+            duv = None
+            if self.strategy == 'N_N':
+                suv = props.src_uvs[i]
+                spuv = props.src_pin_uvs[i]
+                duv = dest_uvs[i]
+            elif self.strategy == 'N_M':
+                suv = props.src_uvs[i % len(props.src_uvs)]
+                spuv = props.src_pin_uvs[i % len(props.src_pin_uvs)]
+                duv = dest_uvs[i]
+            if len(suv) != len(duv):
+                self.report({'WARNING'}, "Some faces are different size")
+                return {'CANCELLED'}
+            suvs_fr = [uv for uv in suv]
+            spuvs_fr = [pin_uv for pin_uv in spuv]
+            # flip UVs
+            if self.flip_copied_uv is True:
+                suvs_fr.reverse()
+                spuvs_fr.reverse()
+            # rotate UVs
+            for _ in range(self.rotate_copied_uv):
+                uv = suvs_fr.pop()
+                pin_uv = spuvs_fr.pop()
+                suvs_fr.insert(0, uv)
+                spuvs_fr.insert(0, pin_uv)
+            # paste UVs
+            for l, suv, spuv in zip(bm.faces[idx].loops, suvs_fr, spuvs_fr):
+                l[uv_layer].uv = suv
+                l[uv_layer].pin_uv = spuv
+        self.report({'INFO'}, "%d face(s) are copied" % len(dest_uvs))
+
+        bmesh.update_edit_mesh(obj.data)
+
+        return {'FINISHED'}
+
+
+class MUV_CPUVPasteUVMenu(bpy.types.Menu):
+    """
+    Menu class: Paste UV coordinate
+    """
+
+    bl_idname = "uv.muv_cpuv_paste_uv_menu"
+    bl_label = "Paste UV"
+    bl_description = "Paste UV coordinate"
+
+    def draw(self, context):
+        layout = self.layout
+        # create sub menu
+        obj = context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+        uv_maps = bm.loops.layers.uv.keys()
+        layout.operator(
+            MUV_CPUVPasteUV.bl_idname,
+            text="[Default]", icon="PLUGIN").uv_map = ""
+        for m in uv_maps:
+            layout.operator(
+                MUV_CPUVPasteUV.bl_idname,
+                text=m, icon="PLUGIN").uv_map = m
+
+
+class MUV_CPUVObjCopyUV(bpy.types.Operator):
+    """
+    Operation class: Copy UV coordinate per object
+    """
+
+    bl_idname = "object.muv_cpuv_obj_copy_uv"
+    bl_label = "Copy UV"
+    bl_description = "Copy UV coordinate"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    uv_map = StringProperty(options={'HIDDEN'})
+
+    @memorize_view_3d_mode
+    def execute(self, context):
+        props = context.scene.muv_props.cpuv_obj
+        if self.uv_map == "":
+            self.report({'INFO'}, "Copy UV coordinate per object")
+        else:
+            self.report(
+                {'INFO'},
+                "Copy UV coordinate per object (UV map:%s)" % (self.uv_map))
+        bpy.ops.object.mode_set(mode='EDIT')
+
+        obj = context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.faces.ensure_lookup_table()
+
+        # get UV layer
+        if self.uv_map == "":
+            if not bm.loops.layers.uv:
+                self.report(
+                    {'WARNING'}, "Object must have more than one UV map")
+                return {'CANCELLED'}
+            uv_layer = bm.loops.layers.uv.verify()
+        else:
+            uv_layer = bm.loops.layers.uv[self.uv_map]
+
+        # get selected face
+        props.src_uvs = []
+        props.src_pin_uvs = []
+        for face in bm.faces:
+            uvs = [l[uv_layer].uv.copy() for l in face.loops]
+            pin_uvs = [l[uv_layer].pin_uv for l in face.loops]
+            props.src_uvs.append(uvs)
+            props.src_pin_uvs.append(pin_uvs)
+
+        self.report({'INFO'}, "%s's UV coordinates are copied" % (obj.name))
+
+        return {'FINISHED'}
+
+
+class MUV_CPUVObjCopyUVMenu(bpy.types.Menu):
+    """
+    Menu class: Copy UV coordinate per object
+    """
+
+    bl_idname = "object.muv_cpuv_obj_copy_uv_menu"
+    bl_label = "Copy UV"
+    bl_description = "Copy UV coordinate per object"
+
+    def draw(self, _):
+        layout = self.layout
+        # create sub menu
+        uv_maps = bpy.context.active_object.data.uv_textures.keys()
+        layout.operator(
+            MUV_CPUVObjCopyUV.bl_idname,
+            text="[Default]", icon="PLUGIN").uv_map = ""
+        for m in uv_maps:
+            layout.operator(
+                MUV_CPUVObjCopyUV.bl_idname,
+                text=m, icon="PLUGIN").uv_map = m
+
+
+class MUV_CPUVObjPasteUV(bpy.types.Operator):
+    """
+    Operation class: Paste UV coordinate per object
+    """
+
+    bl_idname = "object.muv_cpuv_obj_paste_uv"
+    bl_label = "Paste UV"
+    bl_description = "Paste UV coordinate"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    uv_map = StringProperty(options={'HIDDEN'})
+
+    @memorize_view_3d_mode
+    def execute(self, context):
+        props = context.scene.muv_props.cpuv_obj
+        if len(props.src_uvs) == 0 or len(props.src_pin_uvs) == 0:
+            self.report({'WARNING'}, "Need copy UV at first")
+            return {'CANCELLED'}
+
+        for o in bpy.data.objects:
+            if not hasattr(o.data, "uv_textures") or not o.select:
+                continue
+
+            bpy.ops.object.mode_set(mode='OBJECT')
+            bpy.context.scene.objects.active = o
+            bpy.ops.object.mode_set(mode='EDIT')
+
+            obj = context.active_object
+            bm = bmesh.from_edit_mesh(obj.data)
+            if muv_common.check_version(2, 73, 0) >= 0:
+                bm.faces.ensure_lookup_table()
+
+            if (self.uv_map == "" or
+                    self.uv_map not in bm.loops.layers.uv.keys()):
+                self.report({'INFO'}, "Paste UV coordinate per object")
+            else:
+                self.report(
+                    {'INFO'},
+                    "Paste UV coordinate per object (UV map: %s)"
+                    % (self.uv_map))
+
+            # get UV layer
+            if (self.uv_map == "" or
+                    self.uv_map not in bm.loops.layers.uv.keys()):
+                if not bm.loops.layers.uv:
+                    self.report(
+                        {'WARNING'}, "Object must have more than one UV map")
+                    return {'CANCELLED'}
+                uv_layer = bm.loops.layers.uv.verify()
+            else:
+                uv_layer = bm.loops.layers.uv[self.uv_map]
+
+            # get selected face
+            dest_uvs = []
+            dest_pin_uvs = []
+            dest_face_indices = []
+            for face in bm.faces:
+                dest_face_indices.append(face.index)
+                uvs = [l[uv_layer].uv.copy() for l in face.loops]
+                pin_uvs = [l[uv_layer].pin_uv for l in face.loops]
+                dest_uvs.append(uvs)
+                dest_pin_uvs.append(pin_uvs)
+            if len(props.src_uvs) != len(dest_uvs):
+                self.report(
+                    {'WARNING'},
+                    "Number of faces is different from copied "
+                    + "(src:%d, dest:%d)"
+                    % (len(props.src_uvs), len(dest_uvs))
+                )
+                return {'CANCELLED'}
+
+            # paste
+            for i, idx in enumerate(dest_face_indices):
+                suv = props.src_uvs[i]
+                spuv = props.src_pin_uvs[i]
+                duv = dest_uvs[i]
+                if len(suv) != len(duv):
+                    self.report({'WARNING'}, "Some faces are different size")
+                    return {'CANCELLED'}
+                suvs_fr = [uv for uv in suv]
+                spuvs_fr = [pin_uv for pin_uv in spuv]
+                # paste UVs
+                for l, suv, spuv in zip(
+                        bm.faces[idx].loops, suvs_fr, spuvs_fr):
+                    l[uv_layer].uv = suv
+                    l[uv_layer].pin_uv = spuv
+
+            bmesh.update_edit_mesh(obj.data)
+
+            self.report(
+                {'INFO'}, "%s's UV coordinates are pasted" % (obj.name))
+
+        return {'FINISHED'}
+
+
+class MUV_CPUVObjPasteUVMenu(bpy.types.Menu):
+    """
+    Menu class: Paste UV coordinate per object
+    """
+
+    bl_idname = "object.muv_cpuv_obj_paste_uv_menu"
+    bl_label = "Paste UV"
+    bl_description = "Paste UV coordinate per object"
+
+    def draw(self, _):
+        layout = self.layout
+        # create sub menu
+        uv_maps = []
+        for obj in bpy.data.objects:
+            if hasattr(obj.data, "uv_textures") and obj.select:
+                uv_maps.extend(obj.data.uv_textures.keys())
+        uv_maps = list(set(uv_maps))
+        layout.operator(
+            MUV_CPUVObjPasteUV.bl_idname,
+            text="[Default]", icon="PLUGIN").uv_map = ""
+        for m in uv_maps:
+            layout.operator(
+                MUV_CPUVObjPasteUV.bl_idname,
+                text=m, icon="PLUGIN").uv_map = m
diff --git a/uv_magic_uv/muv_cpuv_selseq_ops.py b/uv_magic_uv/muv_cpuv_selseq_ops.py
new file mode 100644
index 0000000000000000000000000000000000000000..f6701b830be1f403d0ed35f47b56b360333972f6
--- /dev/null
+++ b/uv_magic_uv/muv_cpuv_selseq_ops.py
@@ -0,0 +1,249 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "4.3"
+__date__ = "1 Apr 2017"
+
+
+import bpy
+import bmesh
+from bpy.props import StringProperty, BoolProperty, IntProperty, EnumProperty
+from . import muv_common
+
+
+class MUV_CPUVSelSeqCopyUV(bpy.types.Operator):
+    """
+    Operation class: Copy UV coordinate by selection sequence
+    """
+
+    bl_idname = "uv.muv_cpuv_selseq_copy_uv"
+    bl_label = "Copy UV (Selection Sequence) (Operation)"
+    bl_description = "Copy UV data by selection sequence (Operation)"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    uv_map = StringProperty(options={'HIDDEN'})
+
+    def execute(self, context):
+        props = context.scene.muv_props.cpuv_selseq
+        if self.uv_map == "":
+            self.report({'INFO'}, "Copy UV coordinate (selection sequence)")
+        else:
+            self.report(
+                {'INFO'},
+                "Copy UV coordinate (selection sequence) (UV map:%s)"
+                % (self.uv_map))
+        obj = context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.faces.ensure_lookup_table()
+
+        # get UV layer
+        if self.uv_map == "":
+            if not bm.loops.layers.uv:
+                self.report(
+                    {'WARNING'}, "Object must have more than one UV map")
+                return {'CANCELLED'}
+            uv_layer = bm.loops.layers.uv.verify()
+        else:
+            uv_layer = bm.loops.layers.uv[self.uv_map]
+
+        # get selected face
+        props.src_uvs = []
+        props.src_pin_uvs = []
+        for hist in bm.select_history:
+            if isinstance(hist, bmesh.types.BMFace) and hist.select:
+                uvs = [l[uv_layer].uv.copy() for l in hist.loops]
+                pin_uvs = [l[uv_layer].pin_uv for l in hist.loops]
+                props.src_uvs.append(uvs)
+                props.src_pin_uvs.append(pin_uvs)
+        if len(props.src_uvs) == 0 or len(props.src_pin_uvs) == 0:
+            self.report({'WARNING'}, "No faces are selected")
+            return {'CANCELLED'}
+        self.report({'INFO'}, "%d face(s) are selected" % len(props.src_uvs))
+
+        return {'FINISHED'}
+
+
+class MUV_CPUVSelSeqCopyUVMenu(bpy.types.Menu):
+    """
+    Menu class: Copy UV coordinate by selection sequence
+    """
+
+    bl_idname = "uv.muv_cpuv_selseq_copy_uv_menu"
+    bl_label = "Copy UV (Selection Sequence)"
+    bl_description = "Copy UV coordinate by selection sequence"
+
+    def draw(self, context):
+        layout = self.layout
+        obj = context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+        uv_maps = bm.loops.layers.uv.keys()
+        layout.operator(
+            MUV_CPUVSelSeqCopyUV.bl_idname,
+            text="[Default]", icon="PLUGIN").uv_map = ""
+        for m in uv_maps:
+            layout.operator(
+                MUV_CPUVSelSeqCopyUV.bl_idname,
+                text=m, icon="PLUGIN").uv_map = m
+
+
+class MUV_CPUVSelSeqPasteUV(bpy.types.Operator):
+    """
+    Operation class: Paste UV coordinate by selection sequence
+    """
+
+    bl_idname = "uv.muv_cpuv_selseq_paste_uv"
+    bl_label = "Paste UV (Selection Sequence) (Operation)"
+    bl_description = "Paste UV coordinate by selection sequence (Operation)"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    uv_map = StringProperty(options={'HIDDEN'})
+    strategy = EnumProperty(
+        name="Strategy",
+        description="Paste Strategy",
+        items=[
+            ('N_N', 'N:N', 'Number of faces must be equal to source'),
+            ('N_M', 'N:M', 'Number of faces must not be equal to source')
+        ],
+        default="N_M")
+    flip_copied_uv = BoolProperty(
+        name="Flip Copied UV",
+        description="Flip Copied UV...",
+        default=False)
+    rotate_copied_uv = IntProperty(
+        default=0,
+        name="Rotate Copied UV",
+        min=0,
+        max=30)
+
+    def execute(self, context):
+        props = context.scene.muv_props.cpuv_selseq
+        if len(props.src_uvs) == 0 or len(props.src_pin_uvs) == 0:
+            self.report({'WARNING'}, "Need copy UV at first")
+            return {'CANCELLED'}
+        if self.uv_map == "":
+            self.report({'INFO'}, "Paste UV coordinate (selection sequence)")
+        else:
+            self.report(
+                {'INFO'},
+                "Paste UV coordinate (selection sequence) (UV map:%s)"
+                % (self.uv_map))
+
+        obj = context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.faces.ensure_lookup_table()
+
+        # get UV layer
+        if self.uv_map == "":
+            if not bm.loops.layers.uv:
+                self.report(
+                    {'WARNING'}, "Object must have more than one UV map")
+                return {'CANCELLED'}
+            uv_layer = bm.loops.layers.uv.verify()
+        else:
+            uv_layer = bm.loops.layers.uv[self.uv_map]
+
+        # get selected face
+        dest_uvs = []
+        dest_pin_uvs = []
+        dest_face_indices = []
+        for hist in bm.select_history:
+            if isinstance(hist, bmesh.types.BMFace) and hist.select:
+                dest_face_indices.append(hist.index)
+                uvs = [l[uv_layer].uv.copy() for l in hist.loops]
+                pin_uvs = [l[uv_layer].pin_uv for l in hist.loops]
+                dest_uvs.append(uvs)
+                dest_pin_uvs.append(pin_uvs)
+        if len(dest_uvs) == 0 or len(dest_pin_uvs) == 0:
+            self.report({'WARNING'}, "No faces are selected")
+            return {'CANCELLED'}
+        if self.strategy == 'N_N' and len(props.src_uvs) != len(dest_uvs):
+            self.report(
+                {'WARNING'},
+                "Number of selected faces is different from copied faces "
+                + "(src:%d, dest:%d)"
+                % (len(props.src_uvs), len(dest_uvs)))
+            return {'CANCELLED'}
+
+        # paste
+        for i, idx in enumerate(dest_face_indices):
+            suv = None
+            spuv = None
+            duv = None
+            if self.strategy == 'N_N':
+                suv = props.src_uvs[i]
+                spuv = props.src_pin_uvs[i]
+                duv = dest_uvs[i]
+            elif self.strategy == 'N_M':
+                suv = props.src_uvs[i % len(props.src_uvs)]
+                spuv = props.src_pin_uvs[i % len(props.src_pin_uvs)]
+                duv = dest_uvs[i]
+            if len(suv) != len(duv):
+                self.report({'WARNING'}, "Some faces are different size")
+                return {'CANCELLED'}
+            suvs_fr = [uv for uv in suv]
+            spuvs_fr = [pin_uv for pin_uv in spuv]
+            # flip UVs
+            if self.flip_copied_uv is True:
+                suvs_fr.reverse()
+                spuvs_fr.reverse()
+            # rotate UVs
+            for _ in range(self.rotate_copied_uv):
+                uv = suvs_fr.pop()
+                pin_uv = spuvs_fr.pop()
+                suvs_fr.insert(0, uv)
+                spuvs_fr.insert(0, pin_uv)
+            # paste UVs
+            for l, suv, spuv in zip(bm.faces[idx].loops, suvs_fr, spuvs_fr):
+                l[uv_layer].uv = suv
+                l[uv_layer].pin_uv = spuv
+
+        self.report({'INFO'}, "%d face(s) are copied" % len(dest_uvs))
+
+        bmesh.update_edit_mesh(obj.data)
+
+        return {'FINISHED'}
+
+
+class MUV_CPUVSelSeqPasteUVMenu(bpy.types.Menu):
+    """
+    Menu class: Paste UV coordinate by selection sequence
+    """
+
+    bl_idname = "uv.muv_cpuv_selseq_paste_uv_menu"
+    bl_label = "Paste UV (Selection Sequence)"
+    bl_description = "Paste UV coordinate by selection sequence"
+
+    def draw(self, context):
+        layout = self.layout
+        # create sub menu
+        obj = context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+        uv_maps = bm.loops.layers.uv.keys()
+        layout.operator(
+            MUV_CPUVSelSeqPasteUV.bl_idname,
+            text="[Default]", icon="PLUGIN").uv_map = ""
+        for m in uv_maps:
+            layout.operator(
+                MUV_CPUVSelSeqPasteUV.bl_idname,
+                text=m, icon="PLUGIN").uv_map = m
diff --git a/uv_magic_uv/muv_fliprot_ops.py b/uv_magic_uv/muv_fliprot_ops.py
new file mode 100644
index 0000000000000000000000000000000000000000..597ee2a6ef16a7756700ed66625f9332d8877cd7
--- /dev/null
+++ b/uv_magic_uv/muv_fliprot_ops.py
@@ -0,0 +1,107 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "4.3"
+__date__ = "1 Apr 2017"
+
+
+import bpy
+import bmesh
+from bpy.props import BoolProperty, IntProperty
+from . import muv_common
+
+
+class MUV_FlipRot(bpy.types.Operator):
+    """
+    Operation class: Flip and Rotate UV coordinate
+    """
+
+    bl_idname = "uv.muv_fliprot"
+    bl_label = "Flip/Rotate UV"
+    bl_description = "Flip/Rotate UV coordinate"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    flip = BoolProperty(
+        name="Flip UV",
+        description="Flip UV...",
+        default=False
+    )
+    rotate = IntProperty(
+        default=0,
+        name="Rotate UV",
+        min=0,
+        max=30
+    )
+
+    def execute(self, context):
+        self.report({'INFO'}, "Flip/Rotate UV")
+        obj = context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.faces.ensure_lookup_table()
+
+        # get UV layer
+        if not bm.loops.layers.uv:
+            self.report({'WARNING'}, "Object must have more than one UV map")
+            return {'CANCELLED'}
+        uv_layer = bm.loops.layers.uv.verify()
+
+        # get selected face
+        dest_uvs = []
+        dest_pin_uvs = []
+        dest_face_indices = []
+        for face in bm.faces:
+            if face.select:
+                dest_face_indices.append(face.index)
+                uvs = [l[uv_layer].uv.copy() for l in face.loops]
+                pin_uvs = [l[uv_layer].pin_uv for l in face.loops]
+                dest_uvs.append(uvs)
+                dest_pin_uvs.append(pin_uvs)
+        if len(dest_uvs) == 0 or len(dest_pin_uvs) == 0:
+            self.report({'WARNING'}, "No faces are selected")
+            return {'CANCELLED'}
+        self.report({'INFO'}, "%d face(s) are selected" % len(dest_uvs))
+
+        # paste
+        for idx, duvs, dpuvs in zip(dest_face_indices, dest_uvs, dest_pin_uvs):
+            duvs_fr = [uv for uv in duvs]
+            dpuvs_fr = [pin_uv for pin_uv in dpuvs]
+            # flip UVs
+            if self.flip is True:
+                duvs_fr.reverse()
+                dpuvs_fr.reverse()
+            # rotate UVs
+            for _ in range(self.rotate):
+                uv = duvs_fr.pop()
+                pin_uv = dpuvs_fr.pop()
+                duvs_fr.insert(0, uv)
+                dpuvs_fr.insert(0, pin_uv)
+            # paste UVs
+            for l, duv, dpuv in zip(bm.faces[idx].loops, duvs_fr, dpuvs_fr):
+                l[uv_layer].uv = duv
+                l[uv_layer].pin_uv = dpuv
+
+        self.report({'INFO'}, "%d face(s) are flipped/rotated" % len(dest_uvs))
+
+        bmesh.update_edit_mesh(obj.data)
+
+        return {'FINISHED'}
diff --git a/uv_magic_uv/muv_menu.py b/uv_magic_uv/muv_menu.py
new file mode 100644
index 0000000000000000000000000000000000000000..17e77cfb1ce1757393f5978e88d50d2abffc9611
--- /dev/null
+++ b/uv_magic_uv/muv_menu.py
@@ -0,0 +1,122 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "4.3"
+__date__ = "1 Apr 2017"
+
+
+import bpy
+from . import muv_cpuv_ops
+from . import muv_cpuv_selseq_ops
+from . import muv_transuv_ops
+from . import muv_texlock_ops
+from . import muv_wsuv_ops
+
+
+class MUV_CPUVMenu(bpy.types.Menu):
+    """
+    Menu class: Master menu of Copy/Paste UV coordinate
+    """
+
+    bl_idname = "uv.muv_cpuv_menu"
+    bl_label = "Copy/Paste UV"
+    bl_description = "Copy and Paste UV coordinate"
+
+    def draw(self, _):
+        self.layout.menu(
+            muv_cpuv_ops.MUV_CPUVCopyUVMenu.bl_idname, icon="PLUGIN")
+        self.layout.menu(
+            muv_cpuv_ops.MUV_CPUVPasteUVMenu.bl_idname, icon="PLUGIN")
+        self.layout.menu(
+            muv_cpuv_selseq_ops.MUV_CPUVSelSeqCopyUVMenu.bl_idname,
+            icon="PLUGIN")
+        self.layout.menu(
+            muv_cpuv_selseq_ops.MUV_CPUVSelSeqPasteUVMenu.bl_idname,
+            icon="PLUGIN")
+
+
+class MUV_CPUVObjMenu(bpy.types.Menu):
+    """
+    Menu class: Master menu of Copy/Paste UV coordinate per object
+    """
+
+    bl_idname = "object.muv_cpuv_obj_menu"
+    bl_label = "Copy/Paste UV"
+    bl_description = "Copy and Paste UV coordinate per object"
+
+    def draw(self, _):
+        self.layout.menu(
+            muv_cpuv_ops.MUV_CPUVObjCopyUVMenu.bl_idname, icon="PLUGIN")
+        self.layout.menu(
+            muv_cpuv_ops.MUV_CPUVObjPasteUVMenu.bl_idname, icon="PLUGIN")
+
+
+class MUV_TransUVMenu(bpy.types.Menu):
+    """
+    Menu class: Master menu of Transfer UV coordinate
+    """
+
+    bl_idname = "uv.muv_transuv_menu"
+    bl_label = "Transfer UV"
+    bl_description = "Transfer UV coordinate"
+
+    def draw(self, _):
+        self.layout.operator(
+            muv_transuv_ops.MUV_TransUVCopy.bl_idname, icon="PLUGIN")
+        self.layout.operator(
+            muv_transuv_ops.MUV_TransUVPaste.bl_idname, icon="PLUGIN")
+
+
+class MUV_TexLockMenu(bpy.types.Menu):
+    """
+    Menu class: Master menu of Texture Lock
+    """
+
+    bl_idname = "uv.muv_texlock_menu"
+    bl_label = "Texture Lock"
+    bl_description = "Lock texture when vertices of mesh (Preserve UV)"
+
+    def draw(self, _):
+        self.layout.operator(
+            muv_texlock_ops.MUV_TexLockStart.bl_idname, icon="PLUGIN")
+        self.layout.operator(
+            muv_texlock_ops.MUV_TexLockStop.bl_idname, icon="PLUGIN")
+        self.layout.operator(
+            muv_texlock_ops.MUV_TexLockIntrStart.bl_idname, icon="PLUGIN")
+        self.layout.operator(
+            muv_texlock_ops.MUV_TexLockIntrStop.bl_idname, icon="PLUGIN")
+
+
+class MUV_WSUVMenu(bpy.types.Menu):
+    """
+    Menu class: Master menu of world scale UV
+    """
+
+    bl_idname = "uv.muv_wsuv_menu"
+    bl_label = "World Scale UV"
+    bl_description = ""
+
+    def draw(self, _):
+        self.layout.operator(
+            muv_wsuv_ops.MUV_WSUVMeasure.bl_idname, icon="PLUGIN")
+        self.layout.operator(
+            muv_wsuv_ops.MUV_WSUVApply.bl_idname, icon="PLUGIN")
diff --git a/uv_magic_uv/muv_mirroruv_ops.py b/uv_magic_uv/muv_mirroruv_ops.py
new file mode 100644
index 0000000000000000000000000000000000000000..d0a98b87b9b942cbc4aab95a28ff10d8b11f6c66
--- /dev/null
+++ b/uv_magic_uv/muv_mirroruv_ops.py
@@ -0,0 +1,152 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Keith (Wahooney) Boshoff, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "4.3"
+__date__ = "1 Apr 2017"
+
+
+import bpy
+from bpy.props import EnumProperty, FloatProperty
+import bmesh
+from mathutils import Vector
+from . import muv_common
+
+
+class MUV_MirrorUV(bpy.types.Operator):
+    """
+    Operation class: Mirror UV
+    """
+
+    bl_idname = "uv.muv_mirror_uv"
+    bl_label = "Mirror UV"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    axis = EnumProperty(
+        items=(
+            ('X', "X", "Mirror Along X axis"),
+            ('Y', "Y", "Mirror Along Y axis"),
+            ('Z', "Z", "Mirror Along Z axis")
+        ),
+        name="Axis",
+        description="Mirror Axis",
+        default='X'
+    )
+    error = FloatProperty(
+        name="Error",
+        description="Error threshold",
+        default=0.001,
+        min=0.0,
+        max=100.0,
+        soft_min=0.0,
+        soft_max=1.0
+    )
+
+    def __is_vector_similar(self, v1, v2, error):
+        """
+        Check if two vectors are similar, within an error threshold
+        """
+        within_err_x = abs(v2.x - v1.x) < error
+        within_err_y = abs(v2.y - v1.y) < error
+        within_err_z = abs(v2.z - v1.z) < error
+
+        return within_err_x and within_err_y and within_err_z
+
+    def __mirror_uvs(self, uv_layer, src, dst, axis, error):
+        """
+        Copy UV coordinates from one UV face to another
+        """
+        for sl in src.loops:
+            suv = sl[uv_layer].uv.copy()
+            svco = sl.vert.co.copy()
+            for dl in dst.loops:
+                dvco = dl.vert.co.copy()
+                if axis == 'X':
+                    dvco.x = -dvco.x
+                elif axis == 'Y':
+                    dvco.y = -dvco.y
+                elif axis == 'Z':
+                    dvco.z = -dvco.z
+
+                if self.__is_vector_similar(svco, dvco, error):
+                    dl[uv_layer].uv = suv.copy()
+
+    def __get_face_center(self, face):
+        """
+        Get center coordinate of the face
+        """
+        center = Vector((0.0, 0.0, 0.0))
+        for v in face.verts:
+            center = center + v.co
+
+        return center / len(face.verts)
+
+    @classmethod
+    def poll(cls, context):
+        obj = context.active_object
+        return obj and obj.type == 'MESH'
+
+    def execute(self, context):
+        obj = context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+
+        error = self.error
+        axis = self.axis
+
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.faces.ensure_lookup_table()
+        if not bm.loops.layers.uv:
+            self.report({'WARNING'}, "Object must have more than one UV map")
+            return {'CANCELLED'}
+        uv_layer = bm.loops.layers.uv.verify()
+
+        faces = [f for f in bm.faces if f.select]
+        for f_dst in faces:
+            count = len(f_dst.verts)
+            for f_src in bm.faces:
+                # check if this is a candidate to do mirror UV
+                if f_src.index == f_dst.index:
+                    continue
+                if count != len(f_src.verts):
+                    continue
+
+                # test if the vertices x values are the same sign
+                dst = self.__get_face_center(f_dst)
+                src = self.__get_face_center(f_src)
+                if (dst.x > 0 and src.x > 0) or (dst.x < 0 and src.x < 0):
+                    continue
+
+                # invert source axis
+                if axis == 'X':
+                    src.x = -src.x
+                elif axis == 'Y':
+                    src.y = -src.z
+                elif axis == 'Z':
+                    src.z = -src.z
+
+                # do mirror UV
+                if self.__is_vector_similar(dst, src, error):
+                    self.__mirror_uvs(
+                        uv_layer, f_src, f_dst, self.axis, self.error)
+
+        bmesh.update_edit_mesh(obj.data)
+
+        return {'FINISHED'}
diff --git a/uv_magic_uv/muv_mvuv_ops.py b/uv_magic_uv/muv_mvuv_ops.py
new file mode 100644
index 0000000000000000000000000000000000000000..2eb0991f6b757eda8ed64680880b2940cc8abc14
--- /dev/null
+++ b/uv_magic_uv/muv_mvuv_ops.py
@@ -0,0 +1,126 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "kgeogeo, mem, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "4.3"
+__date__ = "1 Apr 2017"
+
+
+import bpy
+import bmesh
+from mathutils import Vector
+
+
+class MUV_MVUV(bpy.types.Operator):
+    """
+    Operator class: Move UV from View3D
+    """
+
+    bl_idname = "view3d.muv_mvuv"
+    bl_label = "Move the UV from View3D"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    def __init__(self):
+        self.__topology_dict = []
+        self.__prev_mouse = Vector((0.0, 0.0))
+        self.__offset_uv = Vector((0.0, 0.0))
+        self.__prev_offset_uv = Vector((0.0, 0.0))
+        self.__first_time = True
+        self.__ini_uvs = []
+        self.__running = False
+
+    def __find_uv(self, context):
+        bm = bmesh.from_edit_mesh(context.object.data)
+        topology_dict = []
+        uvs = []
+        active_uv = bm.loops.layers.uv.active
+        for fidx, f in enumerate(bm.faces):
+            for vidx, v in enumerate(f.verts):
+                if v.select:
+                    uvs.append(f.loops[vidx][active_uv].uv.copy())
+                    topology_dict.append([fidx, vidx])
+
+        return topology_dict, uvs
+
+    @classmethod
+    def poll(cls, context):
+        return context.edit_object
+
+    def modal(self, context, event):
+        if self.__first_time is True:
+            self.__prev_mouse = Vector((
+                event.mouse_region_x, event.mouse_region_y))
+            self.__first_time = False
+            return {'RUNNING_MODAL'}
+
+        # move UV
+        div = 10000
+        self.__offset_uv += Vector((
+            (event.mouse_region_x - self.__prev_mouse.x) / div,
+            (event.mouse_region_y - self.__prev_mouse.y) / div))
+        ouv = self.__offset_uv
+        pouv = self.__prev_offset_uv
+        vec = Vector((ouv.x - ouv.y, ouv.x + ouv.y))
+        dv = vec - pouv
+        self.__prev_offset_uv = vec
+        self.__prev_mouse = Vector((
+            event.mouse_region_x, event.mouse_region_y))
+
+        # check if operation is started
+        if self.__running is True:
+            if event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
+                self.__running = False
+            return {'RUNNING_MODAL'}
+
+        # update UV
+        obj = context.object
+        bm = bmesh.from_edit_mesh(obj.data)
+        active_uv = bm.loops.layers.uv.active
+        for fidx, vidx in self.__topology_dict:
+            l = bm.faces[fidx].loops[vidx]
+            l[active_uv].uv = l[active_uv].uv + dv
+        bmesh.update_edit_mesh(obj.data)
+
+        # check mouse preference
+        if context.user_preferences.inputs.select_mouse == 'RIGHT':
+            confirm_btn = 'LEFTMOUSE'
+            cancel_btn = 'RIGHTMOUSE'
+        else:
+            confirm_btn = 'RIGHTMOUSE'
+            cancel_btn = 'LEFTMOUSE'
+
+        # cancelled
+        if event.type == cancel_btn and event.value == 'PRESS':
+            for (fidx, vidx), uv in zip(self.__topology_dict, self.__ini_uvs):
+                bm.faces[fidx].loops[vidx][active_uv].uv = uv
+            return {'FINISHED'}
+        # confirmed
+        if event.type == confirm_btn and event.value == 'PRESS':
+            return {'FINISHED'}
+
+        return {'RUNNING_MODAL'}
+
+    def execute(self, context):
+        self.__first_time = True
+        self.__running = True
+        context.window_manager.modal_handler_add(self)
+        self.__topology_dict, self.__ini_uvs = self.__find_uv(context)
+        return {'RUNNING_MODAL'}
diff --git a/uv_magic_uv/muv_packuv_ops.py b/uv_magic_uv/muv_packuv_ops.py
new file mode 100644
index 0000000000000000000000000000000000000000..06e79e7a7814c7b7df6a45b78ae27ac68b4f165d
--- /dev/null
+++ b/uv_magic_uv/muv_packuv_ops.py
@@ -0,0 +1,285 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "4.3"
+__date__ = "1 Apr 2017"
+
+
+from math import fabs
+from collections import defaultdict
+
+import bpy
+import bmesh
+import mathutils
+from bpy.props import FloatProperty, FloatVectorProperty, BoolProperty
+from mathutils import Vector
+
+from . import muv_common
+
+
+class MUV_PackUV(bpy.types.Operator):
+    """
+    Operation class: Pack UV with same UV islands are integrated
+    Island matching algorithm
+     - Same center of UV island
+     - Same size of UV island
+     - Same number of UV
+    """
+
+    bl_idname = "uv.muv_packuv"
+    bl_label = "Pack UV"
+    bl_description = "Pack UV (Same UV Islands are integrated)"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    rotate = BoolProperty(
+        name="Rotate",
+        description="Rotate option used by default pack UV function",
+        default=False)
+    margin = FloatProperty(
+        name="Margin",
+        description="Margin used by default pack UV function",
+        min=0,
+        max=1,
+        default=0.001)
+    allowable_center_deviation = FloatVectorProperty(
+        name="Allowable Center Deviation",
+        description="Allowable center deviation to judge same UV island",
+        min=0.000001,
+        max=0.1,
+        default=(0.001, 0.001),
+        size=2)
+    allowable_size_deviation = FloatVectorProperty(
+        name="Allowable Size Deviation",
+        description="Allowable sizse deviation to judge same UV island",
+        min=0.000001,
+        max=0.1,
+        default=(0.001, 0.001),
+        size=2)
+
+    def __init__(self):
+        self.__face_to_verts = defaultdict(set)
+        self.__vert_to_faces = defaultdict(set)
+
+    def execute(self, _):
+        obj = bpy.context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.faces.ensure_lookup_table()
+        if not bm.loops.layers.uv:
+            self.report({'WARNING'}, "Object must have more than one UV map")
+            return {'CANCELLED'}
+        uv_layer = bm.loops.layers.uv.verify()
+
+        selected_faces = [f for f in bm.faces if f.select]
+
+        # create mesh database
+        for f in selected_faces:
+            for l in f.loops:
+                id_ = l[uv_layer].uv.to_tuple(5), l.vert.index
+                self.__face_to_verts[f.index].add(id_)
+                self.__vert_to_faces[id_].add(f.index)
+
+        # Group island
+        uv_island_lists = self.__get_island(bm)
+        island_info = self.__get_island_info(uv_layer, uv_island_lists)
+        num_group = self.__group_island(island_info)
+
+        loop_lists = [l for f in bm.faces for l in f.loops]
+        bpy.ops.mesh.select_all(action='DESELECT')
+
+        # pack UV
+        for gidx in range(num_group):
+            group = list(filter(
+                lambda i, idx=gidx: i['group'] == idx, island_info))
+            for f in group[0]['faces']:
+                f['face'].select = True
+        bmesh.update_edit_mesh(obj.data)
+        bpy.ops.uv.select_all(action='SELECT')
+        bpy.ops.uv.pack_islands(rotate=self.rotate, margin=self.margin)
+
+        # copy/paste UV among same islands
+        for gidx in range(num_group):
+            group = list(filter(
+                lambda i, idx=gidx: i['group'] == idx, island_info))
+            if len(group) <= 1:
+                continue
+            for g in group[1:]:
+                for (src_face, dest_face) in zip(
+                        group[0]['sorted'], g['sorted']):
+                    for (src_loop, dest_loop) in zip(
+                            src_face['face'].loops, dest_face['face'].loops):
+                        loop_lists[dest_loop.index][uv_layer].uv = loop_lists[
+                            src_loop.index][uv_layer].uv
+
+        # restore face/UV selection
+        bpy.ops.uv.select_all(action='DESELECT')
+        bpy.ops.mesh.select_all(action='DESELECT')
+        for f in selected_faces:
+            f.select = True
+        bpy.ops.uv.select_all(action='SELECT')
+
+        bmesh.update_edit_mesh(obj.data)
+
+        return {'FINISHED'}
+
+    def __sort_island_faces(self, kd, uvs, isl1, isl2):
+        """
+        Sort faces in island
+        """
+
+        sorted_faces = []
+        for f in isl1['sorted']:
+            _, idx, _ = kd.find(
+                Vector((f['ave_uv'].x, f['ave_uv'].y, 0.0)))
+            sorted_faces.append(isl2['faces'][uvs[idx]['face_idx']])
+        return sorted_faces
+
+    def __group_island(self, island_info):
+        """
+        Group island
+        """
+
+        num_group = 0
+        while True:
+            # search islands which is not parsed yet
+            isl_1 = None
+            for isl_1 in island_info:
+                if isl_1['group'] == -1:
+                    break
+            else:
+                break   # all faces are parsed
+            if isl_1 is None:
+                break
+            isl_1['group'] = num_group
+            isl_1['sorted'] = isl_1['faces']
+
+            # search same island
+            for isl_2 in island_info:
+                if isl_2['group'] == -1:
+                    dcx = isl_2['center'].x - isl_1['center'].x
+                    dcy = isl_2['center'].y - isl_1['center'].y
+                    dsx = isl_2['size'].x - isl_1['size'].x
+                    dsy = isl_2['size'].y - isl_1['size'].y
+                    center_x_matched = (
+                        fabs(dcx) < self.allowable_center_deviation[0])
+                    center_y_matched = (
+                        fabs(dcy) < self.allowable_center_deviation[1])
+                    size_x_matched = (
+                        fabs(dsx) < self.allowable_size_deviation[0])
+                    size_y_matched = (
+                        fabs(dsy) < self.allowable_size_deviation[1])
+                    center_matched = center_x_matched and center_y_matched
+                    size_matched = size_x_matched and size_y_matched
+                    num_uv_matched = (isl_2['num_uv'] == isl_1['num_uv'])
+                    # are islands have same?
+                    if center_matched and size_matched and num_uv_matched:
+                        isl_2['group'] = num_group
+                        kd = mathutils.kdtree.KDTree(len(isl_2['faces']))
+                        uvs = [
+                            {
+                                'uv': Vector(
+                                    (f['ave_uv'].x, f['ave_uv'].y, 0.0)
+                                ),
+                                'face_idx': fidx
+                            } for fidx, f in enumerate(isl_2['faces'])
+                        ]
+                        for i, uv in enumerate(uvs):
+                            kd.insert(uv['uv'], i)
+                        kd.balance()
+                        # sort faces for copy/paste UV
+                        isl_2['sorted'] = self.__sort_island_faces(
+                            kd, uvs, isl_1, isl_2)
+            num_group = num_group + 1
+
+        return num_group
+
+    def __get_island_info(self, uv_layer, islands):
+        """
+        get information about each island
+        """
+
+        island_info = []
+        for isl in islands:
+            info = {}
+            max_uv = Vector((-10000000.0, -10000000.0))
+            min_uv = Vector((10000000.0, 10000000.0))
+            ave_uv = Vector((0.0, 0.0))
+            num_uv = 0
+            for face in isl:
+                n = 0
+                a = Vector((0.0, 0.0))
+                for l in face['face'].loops:
+                    uv = l[uv_layer].uv
+                    if uv.x > max_uv.x:
+                        max_uv.x = uv.x
+                    if uv.y > max_uv.y:
+                        max_uv.y = uv.y
+                    if uv.x < min_uv.x:
+                        min_uv.x = uv.x
+                    if uv.y < min_uv.y:
+                        min_uv.y = uv.y
+                    a = a + uv
+                    n = n + 1
+                ave_uv = ave_uv + a
+                num_uv = num_uv + n
+                a = a / n
+                face['ave_uv'] = a
+            ave_uv = ave_uv / num_uv
+
+            info['center'] = ave_uv
+            info['size'] = max_uv - min_uv
+            info['num_uv'] = num_uv
+            info['group'] = -1
+            info['faces'] = isl
+
+            island_info.append(info)
+
+        return island_info
+
+    def __parse_island(self, bm, face_idx, faces_left, island):
+        """
+        Parse island
+        """
+
+        if face_idx in faces_left:
+            faces_left.remove(face_idx)
+            island.append({'face': bm.faces[face_idx]})
+            for v in self.__face_to_verts[face_idx]:
+                connected_faces = self.__vert_to_faces[v]
+                if connected_faces:
+                    for cf in connected_faces:
+                        self.__parse_island(bm, cf, faces_left, island)
+
+    def __get_island(self, bm):
+        """
+        Get island list
+        """
+
+        uv_island_lists = []
+        faces_left = set(self.__face_to_verts.keys())
+        while len(faces_left) > 0:
+            current_island = []
+            face_idx = list(faces_left)[0]
+            self.__parse_island(bm, face_idx, faces_left, current_island)
+            uv_island_lists.append(current_island)
+
+        return uv_island_lists
diff --git a/uv_magic_uv/muv_preferences.py b/uv_magic_uv/muv_preferences.py
new file mode 100644
index 0000000000000000000000000000000000000000..066cfbac22b1692ed4f00cae25edb6a8684070b1
--- /dev/null
+++ b/uv_magic_uv/muv_preferences.py
@@ -0,0 +1,141 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "4.3"
+__date__ = "1 Apr 2017"
+
+
+from bpy.props import BoolProperty, FloatProperty, FloatVectorProperty
+from bpy.types import AddonPreferences
+
+
+class MUV_Preferences(AddonPreferences):
+    """Preferences class: Preferences for this add-on"""
+
+    bl_idname = __package__
+
+    # enable/disable switcher
+    enable_texproj = BoolProperty(
+        name="Texture Projection",
+        default=True)
+    enable_uvbb = BoolProperty(
+        name="Bounding Box",
+        default=True)
+
+    # for Texture Projection
+    texproj_canvas_padding = FloatVectorProperty(
+        name="Canvas Padding",
+        description="Canvas Padding.",
+        size=2,
+        max=50.0,
+        min=0.0,
+        default=(20.0, 20.0))
+
+    # for UV Bounding Box
+    uvbb_cp_size = FloatProperty(
+        name="Size",
+        description="Control Point Size",
+        default=6.0,
+        min=3.0,
+        max=100.0)
+    uvbb_cp_react_size = FloatProperty(
+        name="React Size",
+        description="Size event fired",
+        default=10.0,
+        min=3.0,
+        max=100.0)
+
+    def draw(self, _):
+        layout = self.layout
+
+        layout.label("Switch Enable/Disable and Configurate Features:")
+
+        layout.prop(self, "enable_texproj")
+        if self.enable_texproj:
+            sp = layout.split(percentage=0.05)
+            col = sp.column()       # spacer
+            sp = sp.split(percentage=0.3)
+            col = sp.column()
+            col.label("Texture Display: ")
+            col.prop(self, "texproj_canvas_padding")
+
+        layout.prop(self, "enable_uvbb")
+        if self.enable_uvbb:
+            sp = layout.split(percentage=0.05)
+            col = sp.column()       # spacer
+            sp = sp.split(percentage=0.3)
+            col = sp.column()
+            col.label("Control Point: ")
+            col.prop(self, "uvbb_cp_size")
+            col.prop(self, "uvbb_cp_react_size")
+
+        layout.label("Description:")
+        column = layout.column(align=True)
+        column.label("Magic UV is composed of many UV editing features.")
+        column.label("See tutorial page if you know about this add-on.")
+        column.label("https://github.com/nutti/Magic-UV/wiki/Tutorial")
+
+        layout.label("Location:")
+
+        row = layout.row(align=True)
+        sp = row.split(percentage=0.3)
+        sp.label("View3D > U")
+        sp = sp.split(percentage=1.0)
+        col = sp.column(align=True)
+        col.label("Copy/Paste UV Coordinates")
+        col.label("Copy/Paste UV Coordinates (by selection sequence)")
+        col.label("Flip/Rotate UVs")
+        col.label("Transfer UV")
+        col.label("Move UV from 3D View")
+        col.label("Texture Lock")
+        col.label("Mirror UV")
+        col.label("World Scale UV")
+        col.label("Unwrap Constraint")
+        col.label("Preserve UV Aspect")
+
+        row = layout.row(align=True)
+        sp = row.split(percentage=0.3)
+        sp.label("View3D > Object")
+        sp = sp.split(percentage=1.0)
+        col = sp.column(align=True)
+        col.label("Copy/Paste UV Coordinates (Among same objects)")
+
+        row = layout.row(align=True)
+        sp = row.split(percentage=0.3)
+        sp.label("ImageEditor > Property Panel")
+        sp = sp.split(percentage=1.0)
+        col = sp.column(align=True)
+        col.label("Manipulate UV with Bounding Box in UV Editor")
+
+        row = layout.row(align=True)
+        sp = row.split(percentage=0.3)
+        sp.label("View3D > Property Panel")
+        sp = sp.split(percentage=1.0)
+        col = sp.column(align=True)
+        col.label("Texture Projection")
+
+        row = layout.row(align=True)
+        sp = row.split(percentage=0.3)
+        sp.label("ImageEditor > UVs")
+        sp = sp.split(percentage=1.0)
+        col = sp.column(align=True)
+        col.label("Pack UV (with same UV island packing)")
diff --git a/uv_magic_uv/muv_preserve_uv_aspect.py b/uv_magic_uv/muv_preserve_uv_aspect.py
new file mode 100644
index 0000000000000000000000000000000000000000..20be23ad7a947fe302ef80ce77d7f9a9b8804849
--- /dev/null
+++ b/uv_magic_uv/muv_preserve_uv_aspect.py
@@ -0,0 +1,119 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "4.3"
+__date__ = "1 Apr 2017"
+
+
+import bpy
+import bmesh
+from bpy.props import StringProperty
+from mathutils import Vector
+from . import muv_common
+
+
+class MUV_PreserveUVAspect(bpy.types.Operator):
+    """
+    Operation class: Preserve UV Aspect
+    """
+
+    bl_idname = "uv.muv_preserve_uv_aspect"
+    bl_label = "Preserve UV Aspect"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    dest_img_name = StringProperty(options={'HIDDEN'})
+
+    @classmethod
+    def poll(cls, context):
+        obj = context.active_object
+        return obj and obj.type == 'MESH'
+
+    def execute(self, context):
+        obj = context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.faces.ensure_lookup_table()
+
+        if not bm.loops.layers.uv:
+            self.report({'WARNING'}, "Object must have more than one UV map")
+            return {'CANCELLED'}
+        uv_layer = bm.loops.layers.uv.verify()
+        tex_layer = bm.faces.layers.tex.verify()
+
+        sel_faces = [f for f in bm.faces if f.select]
+        dest_img = bpy.data.images[self.dest_img_name]
+
+        info = {}
+
+        for f in sel_faces:
+            if not f[tex_layer].image in info.keys():
+                info[f[tex_layer].image] = {}
+                info[f[tex_layer].image]['faces'] = []
+            info[f[tex_layer].image]['faces'].append(f)
+
+        for img in info:
+            src_img = img
+            ratio = Vector((
+                dest_img.size[0] / src_img.size[0],
+                dest_img.size[1] / src_img.size[1]))
+            origin = Vector((100000.0, 100000.0))
+            for f in info[img]['faces']:
+                for l in f.loops:
+                    uv = l[uv_layer].uv
+                    origin.x = min(uv.x, origin.x)
+                    origin.y = min(uv.y, origin.y)
+            info[img]['ratio'] = ratio
+            info[img]['origin'] = origin
+
+        for img in info:
+            for f in info[img]['faces']:
+                f[tex_layer].image = dest_img
+                for l in f.loops:
+                    uv = l[uv_layer].uv
+                    diff = uv - info[img]['origin']
+                    diff.x = diff.x / info[img]['ratio'].x
+                    diff.y = diff.y / info[img]['ratio'].y
+                    uv.x = origin.x + diff.x
+                    uv.y = origin.y + diff.y
+
+        bmesh.update_edit_mesh(obj.data)
+
+        return {'FINISHED'}
+
+
+class MUV_PreserveUVAspectMenu(bpy.types.Menu):
+    """
+    Menu class: Preserve UV Aspect
+    """
+
+    bl_idname = "uv.muv_preserve_uv_aspect_menu"
+    bl_label = "Preserve UV Aspect"
+    bl_description = "Preserve UV Aspect"
+
+    def draw(self, _):
+        layout = self.layout
+        # create sub menu
+        for key in bpy.data.images.keys():
+            layout.operator(
+                MUV_PreserveUVAspect.bl_idname,
+                text=key, icon="PLUGIN").dest_img_name = key
diff --git a/uv_magic_uv/muv_props.py b/uv_magic_uv/muv_props.py
new file mode 100644
index 0000000000000000000000000000000000000000..10e146147e888e28c64e67b7c0edb95e37dfaec2
--- /dev/null
+++ b/uv_magic_uv/muv_props.py
@@ -0,0 +1,143 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "4.3"
+__date__ = "1 Apr 2017"
+
+
+import bpy
+from bpy.props import FloatProperty, EnumProperty, BoolProperty
+
+
+DEBUG = False
+
+
+def get_loaded_texture_name(_, __):
+    items = [(key, key, "") for key in bpy.data.images.keys()]
+    items.append(("None", "None", ""))
+    return items
+
+
+# Properties used in this add-on.
+class MUV_Properties():
+    cpuv = None
+    cpuv_obj = None
+    cpuv_selseq = None
+    transuv = None
+    uvbb = None
+    texproj = None
+    texlock = None
+    texwrap = None
+    wsuv = None
+
+    def __init__(self):
+        self.cpuv = MUV_CPUVProps()
+        self.cpuv_obj = MUV_CPUVProps()
+        self.cpuv_selseq = MUV_CPUVSelSeqProps()
+        self.transuv = MUV_TransUVProps()
+        self.uvbb = MUV_UVBBProps()
+        self.texproj = MUV_TexProjProps()
+        self.texlock = MUV_TexLockProps()
+        self.texwrap = MUV_TexWrapProps()
+        self.wsuv = MUV_WSUVProps()
+
+
+class MUV_CPUVProps():
+    src_uvs = []
+    src_pin_uvs = []
+
+
+class MUV_CPUVSelSeqProps():
+    src_uvs = []
+    src_pin_uvs = []
+
+
+class MUV_TransUVProps():
+    topology_copied = []
+
+
+class MUV_UVBBProps():
+    uv_info_ini = []
+    ctrl_points_ini = []
+    ctrl_points = []
+    running = False
+
+
+class MUV_TexProjProps():
+    running = False
+
+
+class MUV_TexLockProps():
+    verts_orig = None
+    intr_verts_orig = None
+    intr_running = False
+
+
+class MUV_TexWrapProps():
+    src_face_index = -1
+
+
+class MUV_WSUVProps():
+    ref_sv = None
+    ref_suv = None
+
+
+def init_props(scene):
+    scene.muv_props = MUV_Properties()
+    scene.muv_uvbb_uniform_scaling = BoolProperty(
+        name="Uniform Scaling",
+        description="Enable Uniform Scaling",
+        default=False)
+    scene.muv_texproj_tex_magnitude = FloatProperty(
+        name="Magnitude",
+        description="Texture Magnitude.",
+        default=0.5,
+        min=0.0,
+        max=100.0)
+    scene.muv_texproj_tex_image = EnumProperty(
+        name="Image",
+        description="Texture Image.",
+        items=get_loaded_texture_name)
+    scene.muv_texproj_tex_transparency = FloatProperty(
+        name="Transparency",
+        description="Texture Transparency.",
+        default=0.2,
+        min=0.0,
+        max=1.0)
+    scene.muv_texproj_adjust_window = BoolProperty(
+        name="Adjust Window",
+        description="Size of renderered texture is fitted to window.",
+        default=True)
+    scene.muv_texproj_apply_tex_aspect = BoolProperty(
+        name="Texture Aspect Ratio",
+        description="Apply Texture Aspect ratio to displayed texture.",
+        default=True)
+
+
+def clear_props(scene):
+    del scene.muv_props
+    del scene.muv_uvbb_uniform_scaling
+    del scene.muv_texproj_tex_magnitude
+    del scene.muv_texproj_tex_image
+    del scene.muv_texproj_tex_transparency
+    del scene.muv_texproj_adjust_window
+    del scene.muv_texproj_apply_tex_aspect
diff --git a/uv_magic_uv/muv_texlock_ops.py b/uv_magic_uv/muv_texlock_ops.py
new file mode 100644
index 0000000000000000000000000000000000000000..80ec1c6c4a2833d014f71eb6e75ce71b9cb0d684
--- /dev/null
+++ b/uv_magic_uv/muv_texlock_ops.py
@@ -0,0 +1,431 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "4.3"
+__date__ = "1 Apr 2017"
+
+import math
+from math import atan2, cos, sqrt, sin, fabs
+
+import bpy
+import bmesh
+from mathutils import Vector
+from bpy.props import BoolProperty
+from . import muv_common
+
+
+def get_vco(verts_orig, loop):
+    """
+    Get vertex original coordinate from loop
+    """
+    for vo in verts_orig:
+        if vo["vidx"] == loop.vert.index and vo["moved"] is False:
+            return vo["vco"]
+    return loop.vert.co
+
+
+def get_link_loops(vert):
+    """
+    Get loop linked to vertex
+    """
+    link_loops = []
+    for f in vert.link_faces:
+        adj_loops = []
+        for loop in f.loops:
+            # self loop
+            if loop.vert == vert:
+                l = loop
+            # linked loop
+            else:
+                for e in loop.vert.link_edges:
+                    if e.other_vert(loop.vert) == vert:
+                        adj_loops.append(loop)
+        if len(adj_loops) < 2:
+            return None
+
+        link_loops.append({"l": l, "l0": adj_loops[0], "l1": adj_loops[1]})
+    return link_loops
+
+
+def get_ini_geom(link_loop, uv_layer, verts_orig, v_orig):
+    """
+    Get initial geometory
+    (Get interior angle of face in vertex/UV space)
+    """
+    u = link_loop["l"][uv_layer].uv
+    v0 = get_vco(verts_orig, link_loop["l0"])
+    u0 = link_loop["l0"][uv_layer].uv
+    v1 = get_vco(verts_orig, link_loop["l1"])
+    u1 = link_loop["l1"][uv_layer].uv
+
+    # get interior angle of face in vertex space
+    v0v1 = v1 - v0
+    v0v = v_orig["vco"] - v0
+    v1v = v_orig["vco"] - v1
+    theta0 = v0v1.angle(v0v)
+    theta1 = v0v1.angle(-v1v)
+    if (theta0 + theta1) > math.pi:
+        theta0 = v0v1.angle(-v0v)
+        theta1 = v0v1.angle(v1v)
+
+    # get interior angle of face in UV space
+    u0u1 = u1 - u0
+    u0u = u - u0
+    u1u = u - u1
+    phi0 = u0u1.angle(u0u)
+    phi1 = u0u1.angle(-u1u)
+    if (phi0 + phi1) > math.pi:
+        phi0 = u0u1.angle(-u0u)
+        phi1 = u0u1.angle(u1u)
+
+    # get direction of linked UV coordinate
+    # this will be used to judge whether angle is more or less than 180 degree
+    dir0 = u0u1.cross(u0u) > 0
+    dir1 = u0u1.cross(u1u) > 0
+
+    return {
+        "theta0": theta0,
+        "theta1": theta1,
+        "phi0": phi0,
+        "phi1": phi1,
+        "dir0": dir0,
+        "dir1": dir1}
+
+
+def get_target_uv(link_loop, uv_layer, verts_orig, v, ini_geom):
+    """
+    Get target UV coordinate
+    """
+    v0 = get_vco(verts_orig, link_loop["l0"])
+    lo0 = link_loop["l0"]
+    v1 = get_vco(verts_orig, link_loop["l1"])
+    lo1 = link_loop["l1"]
+
+    # get interior angle of face in vertex space
+    v0v1 = v1 - v0
+    v0v = v.co - v0
+    v1v = v.co - v1
+    theta0 = v0v1.angle(v0v)
+    theta1 = v0v1.angle(-v1v)
+    if (theta0 + theta1) > math.pi:
+        theta0 = v0v1.angle(-v0v)
+        theta1 = v0v1.angle(v1v)
+
+    # calculate target interior angle in UV space
+    phi0 = theta0 * ini_geom["phi0"] / ini_geom["theta0"]
+    phi1 = theta1 * ini_geom["phi1"] / ini_geom["theta1"]
+
+    uv0 = lo0[uv_layer].uv
+    uv1 = lo1[uv_layer].uv
+
+    # calculate target vertex coordinate from target interior angle
+    tuv0, tuv1 = calc_tri_vert(uv0, uv1, phi0, phi1)
+
+    # target UV coordinate depends on direction, so judge using direction of
+    # linked UV coordinate
+    u0u1 = uv1 - uv0
+    u0u = tuv0 - uv0
+    u1u = tuv0 - uv1
+    dir0 = u0u1.cross(u0u) > 0
+    dir1 = u0u1.cross(u1u) > 0
+    if (ini_geom["dir0"] != dir0) or (ini_geom["dir1"] != dir1):
+        return tuv1
+
+    return tuv0
+
+
+def calc_tri_vert(v0, v1, angle0, angle1):
+    """
+    Calculate rest coordinate from other coordinates and angle of end
+    """
+    angle = math.pi - angle0 - angle1
+
+    alpha = atan2(v1.y - v0.y, v1.x - v0.x)
+    d = (v1.x - v0.x) / cos(alpha)
+    a = d * sin(angle0) / sin(angle)
+    b = d * sin(angle1) / sin(angle)
+    s = (a + b + d) / 2.0
+    if fabs(d) < 0.0000001:
+        xd = 0
+        yd = 0
+    else:
+        xd = (b * b - a * a + d * d) / (2 * d)
+        yd = 2 * sqrt(s * (s - a) * (s - b) * (s - d)) / d
+    x1 = xd * cos(alpha) - yd * sin(alpha) + v0.x
+    y1 = xd * sin(alpha) + yd * cos(alpha) + v0.y
+    x2 = xd * cos(alpha) + yd * sin(alpha) + v0.x
+    y2 = xd * sin(alpha) - yd * cos(alpha) + v0.y
+
+    return Vector((x1, y1)), Vector((x2, y2))
+
+
+class MUV_TexLockStart(bpy.types.Operator):
+    """
+    Operation class: Start Texture Lock
+    """
+
+    bl_idname = "uv.muv_texlock_start"
+    bl_label = "Start"
+    bl_description = "Start Texture Lock"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    def execute(self, context):
+        props = context.scene.muv_props.texlock
+        obj = bpy.context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.verts.ensure_lookup_table()
+            bm.edges.ensure_lookup_table()
+            bm.faces.ensure_lookup_table()
+
+        if not bm.loops.layers.uv:
+            self.report(
+                {'WARNING'}, "Object must have more than one UV map")
+            return {'CANCELLED'}
+
+        props.verts_orig = [
+            {"vidx": v.index, "vco": v.co.copy(), "moved": False}
+            for v in bm.verts if v.select]
+
+        return {'FINISHED'}
+
+
+class MUV_TexLockStop(bpy.types.Operator):
+    """
+    Operation class: Stop Texture Lock
+    """
+
+    bl_idname = "uv.muv_texlock_stop"
+    bl_label = "Stop"
+    bl_description = "Start Texture Lock"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    connect = BoolProperty(
+        name="Connect UV",
+        default=True)
+
+    def execute(self, context):
+        props = context.scene.muv_props.texlock
+        obj = bpy.context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.verts.ensure_lookup_table()
+            bm.edges.ensure_lookup_table()
+            bm.faces.ensure_lookup_table()
+
+        if not bm.loops.layers.uv:
+            self.report(
+                {'WARNING'}, "Object must have more than one UV map")
+            return {'CANCELLED'}
+        uv_layer = bm.loops.layers.uv.verify()
+
+        verts = [v.index for v in bm.verts if v.select]
+        verts_orig = props.verts_orig
+
+        # move UV followed by vertex coordinate
+        for vidx, v_orig in zip(verts, verts_orig):
+            if vidx != v_orig["vidx"]:
+                self.report({'ERROR'}, "Internal Error")
+                return {"CANCELLED"}
+
+            v = bm.verts[vidx]
+            link_loops = get_link_loops(v)
+
+            result = []
+
+            for ll in link_loops:
+                ini_geom = get_ini_geom(ll, uv_layer, verts_orig, v_orig)
+                target_uv = get_target_uv(
+                    ll, uv_layer, verts_orig, v, ini_geom)
+                result.append({"l": ll["l"], "uv": target_uv})
+
+            # connect other face's UV
+            if self.connect:
+                ave = Vector((0.0, 0.0))
+                for r in result:
+                    ave = ave + r["uv"]
+                ave = ave / len(result)
+                for r in result:
+                    r["l"][uv_layer].uv = ave
+            else:
+                for r in result:
+                    r["l"][uv_layer].uv = r["uv"]
+            v_orig["moved"] = True
+            bmesh.update_edit_mesh(obj.data)
+
+        return {'FINISHED'}
+
+
+class MUV_TexLockUpdater(bpy.types.Operator):
+    """
+    Operation class: Texture locking updater
+    """
+
+    bl_idname = "uv.muv_texlock_updater"
+    bl_label = "Texture Lock Updater"
+    bl_description = "Texture Lock Updater"
+
+    def __init__(self):
+        self.__timer = None
+
+    def __update_uv(self, context):
+        """
+        Update UV when vertex coordinates are changed
+        """
+        props = context.scene.muv_props.texlock
+        obj = bpy.context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.verts.ensure_lookup_table()
+            bm.edges.ensure_lookup_table()
+            bm.faces.ensure_lookup_table()
+
+        if not bm.loops.layers.uv:
+            self.report({'WARNING'}, "Object must have more than one UV map")
+            return {'CANCELLED'}
+        uv_layer = bm.loops.layers.uv.verify()
+
+        verts = [v.index for v in bm.verts if v.select]
+        verts_orig = props.intr_verts_orig
+
+        for vidx, v_orig in zip(verts, verts_orig):
+            if vidx != v_orig["vidx"]:
+                self.report({'ERROR'}, "Internal Error")
+                return {"CANCELLED"}
+
+            v = bm.verts[vidx]
+            link_loops = get_link_loops(v)
+
+            result = []
+            for ll in link_loops:
+                ini_geom = get_ini_geom(ll, uv_layer, verts_orig, v_orig)
+                target_uv = get_target_uv(
+                    ll, uv_layer, verts_orig, v, ini_geom)
+                result.append({"l": ll["l"], "uv": target_uv})
+
+            # UV connect option is always true, because it raises
+            # unexpected behavior
+            ave = Vector((0.0, 0.0))
+            for r in result:
+                ave = ave + r["uv"]
+            ave = ave / len(result)
+            for r in result:
+                r["l"][uv_layer].uv = ave
+            v_orig["moved"] = True
+            bmesh.update_edit_mesh(obj.data)
+
+        muv_common.redraw_all_areas()
+        props.intr_verts_orig = [
+            {"vidx": v.index, "vco": v.co.copy(), "moved": False}
+            for v in bm.verts if v.select]
+
+    def modal(self, context, event):
+        props = context.scene.muv_props.texlock
+        if context.area:
+            context.area.tag_redraw()
+        if props.intr_running is False:
+            self.__handle_remove(context)
+            return {'FINISHED'}
+        if event.type == 'TIMER':
+            self.__update_uv(context)
+
+        return {'PASS_THROUGH'}
+
+    def __handle_add(self, context):
+        if self.__timer is None:
+            self.__timer = context.window_manager.event_timer_add(
+                0.10, context.window)
+            context.window_manager.modal_handler_add(self)
+
+    def __handle_remove(self, context):
+        if self.__timer is not None:
+            context.window_manager.event_timer_remove(self.__timer)
+            self.__timer = None
+
+    def execute(self, context):
+        props = context.scene.muv_props.texlock
+        if props.intr_running is False:
+            self.__handle_add(context)
+            props.intr_running = True
+            return {'RUNNING_MODAL'}
+        else:
+            props.intr_running = False
+        if context.area:
+            context.area.tag_redraw()
+
+        return {'FINISHED'}
+
+
+class MUV_TexLockIntrStart(bpy.types.Operator):
+    """
+    Operation class: Start texture locking (Interactive mode)
+    """
+
+    bl_idname = "uv.muv_texlock_intr_start"
+    bl_label = "Texture Lock Start (Interactive mode)"
+    bl_description = "Texture Lock Start (Realtime UV update)"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    def execute(self, context):
+        props = context.scene.muv_props.texlock
+        if props.intr_running is True:
+            return {'CANCELLED'}
+
+        obj = bpy.context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.verts.ensure_lookup_table()
+            bm.edges.ensure_lookup_table()
+            bm.faces.ensure_lookup_table()
+
+        if not bm.loops.layers.uv:
+            self.report({'WARNING'}, "Object must have more than one UV map")
+            return {'CANCELLED'}
+
+        props.intr_verts_orig = [
+            {"vidx": v.index, "vco": v.co.copy(), "moved": False}
+            for v in bm.verts if v.select]
+
+        bpy.ops.uv.muv_texlock_updater()
+
+        return {'FINISHED'}
+
+
+# Texture lock (Stop, Interactive mode)
+class MUV_TexLockIntrStop(bpy.types.Operator):
+    """
+    Operation class: Stop texture locking (interactive mode)
+    """
+
+    bl_idname = "uv.muv_texlock_intr_stop"
+    bl_label = "Texture Lock Stop (Interactive mode)"
+    bl_description = "Texture Lock Stop (Realtime UV update)"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    def execute(self, context):
+        props = context.scene.muv_props.texlock
+        if props.intr_running is False:
+            return {'CANCELLED'}
+
+        bpy.ops.uv.muv_texlock_updater()
+
+        return {'FINISHED'}
diff --git a/uv_magic_uv/muv_texproj_ops.py b/uv_magic_uv/muv_texproj_ops.py
new file mode 100644
index 0000000000000000000000000000000000000000..3493b13aba15bc63614a166209ef23ed2a46b504
--- /dev/null
+++ b/uv_magic_uv/muv_texproj_ops.py
@@ -0,0 +1,328 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "4.3"
+__date__ = "1 Apr 2017"
+
+from collections import namedtuple
+
+import bpy
+import bgl
+import bmesh
+import mathutils
+from bpy_extras import view3d_utils
+
+from . import muv_common
+
+
+Rect = namedtuple('Rect', 'x0 y0 x1 y1')
+Rect2 = namedtuple('Rect2', 'x y width height')
+
+
+def get_canvas(context, magnitude):
+    """
+    Get canvas to be renderred texture
+    """
+    sc = context.scene
+    prefs = context.user_preferences.addons["uv_magic_uv"].preferences
+
+    region_w = context.region.width
+    region_h = context.region.height
+    canvas_w = region_w - prefs.texproj_canvas_padding[0] * 2.0
+    canvas_h = region_h - prefs.texproj_canvas_padding[1] * 2.0
+
+    img = bpy.data.images[sc.muv_texproj_tex_image]
+    tex_w = img.size[0]
+    tex_h = img.size[1]
+
+    center_x = region_w * 0.5
+    center_y = region_h * 0.5
+
+    if sc.muv_texproj_adjust_window:
+        ratio_x = canvas_w / tex_w
+        ratio_y = canvas_h / tex_h
+        if sc.muv_texproj_apply_tex_aspect:
+            ratio = ratio_y if ratio_x > ratio_y else ratio_x
+            len_x = ratio * tex_w
+            len_y = ratio * tex_h
+        else:
+            len_x = canvas_w
+            len_y = canvas_h
+    else:
+        if sc.muv_texproj_apply_tex_aspect:
+            len_x = tex_w * magnitude
+            len_y = tex_h * magnitude
+        else:
+            len_x = region_w * magnitude
+            len_y = region_h * magnitude
+
+    x0 = int(center_x - len_x * 0.5)
+    y0 = int(center_y - len_y * 0.5)
+    x1 = int(center_x + len_x * 0.5)
+    y1 = int(center_y + len_y * 0.5)
+
+    return Rect(x0, y0, x1, y1)
+
+
+def rect_to_rect2(rect):
+    """
+    Convert Rect1 to Rect2
+    """
+
+    return Rect2(rect.x0, rect.y0, rect.x1 - rect.x0, rect.y1 - rect.y0)
+
+
+def region_to_canvas(rg_vec, canvas):
+    """
+    Convert screen region to canvas
+    """
+
+    cv_rect = rect_to_rect2(canvas)
+    cv_vec = mathutils.Vector()
+    cv_vec.x = (rg_vec.x - cv_rect.x) / cv_rect.width
+    cv_vec.y = (rg_vec.y - cv_rect.y) / cv_rect.height
+
+    return cv_vec
+
+
+class MUV_TexProjRenderer(bpy.types.Operator):
+    """
+    Operation class: Render selected texture
+    No operation (only rendering texture)
+    """
+
+    bl_idname = "uv.muv_texproj_renderer"
+    bl_description = "Render selected texture"
+    bl_label = "Texture renderer"
+
+    __handle = None
+
+    @staticmethod
+    def handle_add(obj, context):
+        MUV_TexProjRenderer.__handle = bpy.types.SpaceView3D.draw_handler_add(
+            MUV_TexProjRenderer.draw_texture,
+            (obj, context), 'WINDOW', 'POST_PIXEL')
+
+    @staticmethod
+    def handle_remove():
+        if MUV_TexProjRenderer.__handle is not None:
+            bpy.types.SpaceView3D.draw_handler_remove(
+                MUV_TexProjRenderer.__handle, 'WINDOW')
+            MUV_TexProjRenderer.__handle = None
+
+    @staticmethod
+    def draw_texture(_, context):
+        sc = context.scene
+
+        # no textures are selected
+        if sc.muv_texproj_tex_image == "None":
+            return
+
+        # get texture to be renderred
+        img = bpy.data.images[sc.muv_texproj_tex_image]
+
+        # setup rendering region
+        rect = get_canvas(context, sc.muv_texproj_tex_magnitude)
+        positions = [
+            [rect.x0, rect.y0],
+            [rect.x0, rect.y1],
+            [rect.x1, rect.y1],
+            [rect.x1, rect.y0]
+        ]
+        tex_coords = [
+            [0.0, 0.0],
+            [0.0, 1.0],
+            [1.0, 1.0],
+            [1.0, 0.0]
+        ]
+
+        # OpenGL configuration
+        bgl.glEnable(bgl.GL_BLEND)
+        bgl.glEnable(bgl.GL_TEXTURE_2D)
+        if img.bindcode:
+            bind = img.bindcode[0]
+            bgl.glBindTexture(bgl.GL_TEXTURE_2D, bind)
+            bgl.glTexParameteri(
+                bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MIN_FILTER, bgl.GL_LINEAR)
+            bgl.glTexParameteri(
+                bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MAG_FILTER, bgl.GL_LINEAR)
+            bgl.glTexEnvi(
+                bgl.GL_TEXTURE_ENV, bgl.GL_TEXTURE_ENV_MODE, bgl.GL_MODULATE)
+
+        # render texture
+        bgl.glBegin(bgl.GL_QUADS)
+        bgl.glColor4f(1.0, 1.0, 1.0, sc.muv_texproj_tex_transparency)
+        for (v1, v2), (u, v) in zip(positions, tex_coords):
+            bgl.glTexCoord2f(u, v)
+            bgl.glVertex2f(v1, v2)
+        bgl.glEnd()
+
+
+class MUV_TexProjStart(bpy.types.Operator):
+    """
+    Operation class: Start Texture Projection
+    """
+
+    bl_idname = "uv.muv_texproj_start"
+    bl_label = "Start Texture Projection"
+    bl_description = "Start Texture Projection"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    def execute(self, context):
+        props = context.scene.muv_props.texproj
+        if props.running is False:
+            MUV_TexProjRenderer.handle_add(self, context)
+            props.running = True
+        if context.area:
+            context.area.tag_redraw()
+
+        return {'FINISHED'}
+
+
+class MUV_TexProjStop(bpy.types.Operator):
+    """
+    Operation class: Stop Texture Projection
+    """
+
+    bl_idname = "uv.muv_texproj_stop"
+    bl_label = "Stop Texture Projection"
+    bl_description = "Stop Texture Projection"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    def execute(self, context):
+        props = context.scene.muv_props.texproj
+        if props.running is True:
+            MUV_TexProjRenderer.handle_remove()
+            props.running = False
+        if context.area:
+            context.area.tag_redraw()
+
+        return {'FINISHED'}
+
+
+class MUV_TexProjProject(bpy.types.Operator):
+    """
+    Operation class: Project texture
+    """
+
+    bl_idname = "uv.muv_texproj_project"
+    bl_label = "Project Texture"
+    bl_description = "Project Texture"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    def execute(self, context):
+        sc = context.scene
+
+        if sc.muv_texproj_tex_image == "None":
+            self.report({'WARNING'}, "No textures are selected")
+            return {'CANCELLED'}
+        _, region, space = muv_common.get_space(
+            'VIEW_3D', 'WINDOW', 'VIEW_3D')
+
+        # get faces to be texture projected
+        obj = context.active_object
+        world_mat = obj.matrix_world
+        bm = bmesh.from_edit_mesh(obj.data)
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.faces.ensure_lookup_table()
+
+        # get UV and texture layer
+        if not bm.loops.layers.uv:
+            self.report({'WARNING'}, "Object must have more than one UV map")
+            return {'CANCELLED'}
+        uv_layer = bm.loops.layers.uv.verify()
+        tex_layer = bm.faces.layers.tex.verify()
+
+        sel_faces = [f for f in bm.faces if f.select]
+
+        # transform 3d space to screen region
+        v_screen = [
+            view3d_utils.location_3d_to_region_2d(
+                region,
+                space.region_3d,
+                world_mat * l.vert.co)
+            for f in sel_faces for l in f.loops
+        ]
+
+        # transform screen region to canvas
+        v_canvas = [
+            region_to_canvas(
+                v,
+                get_canvas(bpy.context, sc.muv_texproj_tex_magnitude))
+            for v in v_screen
+        ]
+
+        # project texture to object
+        i = 0
+        for f in sel_faces:
+            f[tex_layer].image = bpy.data.images[sc.muv_texproj_tex_image]
+            for l in f.loops:
+                l[uv_layer].uv = v_canvas[i].to_2d()
+                i = i + 1
+
+        muv_common.redraw_all_areas()
+        bmesh.update_edit_mesh(obj.data)
+
+        return {'FINISHED'}
+
+
+class OBJECT_PT_TP(bpy.types.Panel):
+    """
+    Panel class: Texture Projection Menu on Property Panel on View3D
+    """
+
+    bl_label = "Texture Projection"
+    bl_description = "Texture Projection Menu"
+    bl_space_type = 'VIEW_3D'
+    bl_region_type = 'UI'
+    bl_context = 'mesh_edit'
+
+    @classmethod
+    def poll(cls, context):
+        prefs = context.user_preferences.addons["uv_magic_uv"].preferences
+        return prefs.enable_texproj
+
+    def draw_header(self, _):
+        layout = self.layout
+        layout.label(text="", icon='PLUGIN')
+
+    def draw(self, context):
+        sc = context.scene
+        layout = self.layout
+        props = sc.muv_props.texproj
+        if props.running is False:
+            layout.operator(
+                MUV_TexProjStart.bl_idname, text="Start", icon='PLAY')
+        else:
+            layout.operator(
+                MUV_TexProjStop.bl_idname, text="Stop", icon='PAUSE')
+            layout.prop(sc, "muv_texproj_tex_image", text="Image")
+            layout.prop(
+                sc, "muv_texproj_tex_transparency", text="Transparency"
+            )
+            layout.prop(sc, "muv_texproj_adjust_window", text="Adjust Window")
+            if not sc.muv_texproj_adjust_window:
+                layout.prop(sc, "muv_texproj_tex_magnitude", text="Magnitude")
+            layout.prop(
+                sc, "muv_texproj_apply_tex_aspect", text="Texture Aspect Ratio"
+            )
+            layout.operator(MUV_TexProjProject.bl_idname, text="Project")
diff --git a/uv_magic_uv/muv_transuv_ops.py b/uv_magic_uv/muv_transuv_ops.py
new file mode 100644
index 0000000000000000000000000000000000000000..e1083edce80e2f26efb75866fbb30f6d662077f9
--- /dev/null
+++ b/uv_magic_uv/muv_transuv_ops.py
@@ -0,0 +1,345 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>, Mifth, MaxRobinot"
+__status__ = "production"
+__version__ = "4.3"
+__date__ = "1 Apr 2017"
+
+from collections import OrderedDict
+
+import bpy
+import bmesh
+from bpy.props import BoolProperty
+
+from . import muv_props
+from . import muv_common
+
+
+class MUV_TransUVCopy(bpy.types.Operator):
+    """
+        Operation class: Transfer UV copy
+        Topological based copy
+    """
+
+    bl_idname = "uv.muv_transuv_copy"
+    bl_label = "Transfer UV Copy"
+    bl_description = "Transfer UV Copy (Topological based copy)"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    def execute(self, context):
+        props = context.scene.muv_props.transuv
+        active_obj = context.scene.objects.active
+        bm = bmesh.from_edit_mesh(active_obj.data)
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.faces.ensure_lookup_table()
+
+        # get UV layer
+        if not bm.loops.layers.uv:
+            self.report({'WARNING'}, "Object must have more than one UV map")
+            return {'CANCELLED'}
+        uv_layer = bm.loops.layers.uv.verify()
+
+        props.topology_copied.clear()
+
+        # get selected faces
+        active_face = bm.faces.active
+        sel_faces = [face for face in bm.faces if face.select]
+        if len(sel_faces) != 2:
+            self.report({'WARNING'}, "Two faces must be selected")
+            return {'CANCELLED'}
+        if not active_face or active_face not in sel_faces:
+            self.report({'WARNING'}, "Two faces must be active")
+            return {'CANCELLED'}
+
+        # parse all faces according to selection
+        active_face_nor = active_face.normal.copy()
+        all_sorted_faces = main_parse(
+            self, uv_layer, sel_faces, active_face,
+            active_face_nor)
+
+        if all_sorted_faces:
+            for face_data in all_sorted_faces.values():
+                uv_loops = face_data[2]
+                uvs = [l.uv.copy() for l in uv_loops]
+                pin_uvs = [l.pin_uv for l in uv_loops]
+                props.topology_copied.append([uvs, pin_uvs])
+
+        bmesh.update_edit_mesh(active_obj.data)
+
+        return {'FINISHED'}
+
+
+class MUV_TransUVPaste(bpy.types.Operator):
+    """
+        Operation class: Transfer UV paste
+        Topological based paste
+    """
+
+    bl_idname = "uv.muv_transuv_paste"
+    bl_label = "Transfer UV Paste"
+    bl_description = "Transfer UV Paste (Topological based paste)"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    invert_normals = BoolProperty(
+        name="Invert Normals",
+        description="Invert Normals",
+        default=False)
+
+    def execute(self, context):
+        props = context.scene.muv_props.transuv
+        active_obj = context.scene.objects.active
+        bm = bmesh.from_edit_mesh(active_obj.data)
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.faces.ensure_lookup_table()
+
+        # get UV layer
+        if not bm.loops.layers.uv:
+            self.report({'WARNING'}, "Object must have more than one UV map")
+            return {'CANCELLED'}
+        uv_layer = bm.loops.layers.uv.verify()
+
+        # get selection history
+        all_sel_faces = [
+            e for e in bm.select_history
+            if isinstance(e, bmesh.types.BMFace) and e.select]
+        if len(all_sel_faces) % 2 != 0:
+            self.report({'WARNING'}, "Two faces must be selected")
+            return {'CANCELLED'}
+
+        # parse selection history
+        for i, _ in enumerate(all_sel_faces):
+            if i > 0 and i % 2 != 0:
+                sel_faces = [all_sel_faces[i - 1], all_sel_faces[i]]
+                active_face = all_sel_faces[i]
+
+                # parse all faces according to selection history
+                active_face_nor = active_face.normal.copy()
+                if self.invert_normals:
+                    active_face_nor.negate()
+                all_sorted_faces = main_parse(
+                    self, uv_layer, sel_faces, active_face,
+                    active_face_nor)
+
+                if all_sorted_faces:
+                    # check amount of copied/pasted faces
+                    if len(all_sorted_faces) != len(props.topology_copied):
+                        self.report(
+                            {'WARNING'},
+                            "Mesh has different amount of faces"
+                        )
+                        return {'FINISHED'}
+
+                    for i, face_data in enumerate(all_sorted_faces.values()):
+                        copied_data = props.topology_copied[i]
+
+                        # check amount of copied/pasted verts
+                        if len(copied_data[0]) != len(face_data[2]):
+                            bpy.ops.mesh.select_all(action='DESELECT')
+                            # select problematic face
+                            list(all_sorted_faces.keys())[i].select = True
+                            self.report(
+                                {'WARNING'},
+                                "Face have different amount of vertices"
+                            )
+                            return {'FINISHED'}
+
+                        for j, uvloop in enumerate(face_data[2]):
+                            uvloop.uv = copied_data[0][j]
+                            uvloop.pin_uv = copied_data[1][j]
+
+        bmesh.update_edit_mesh(active_obj.data)
+
+        return {'FINISHED'}
+
+
+def main_parse(
+        self, uv_layer, sel_faces,
+        active_face, active_face_nor):
+    all_sorted_faces = OrderedDict()  # This is the main stuff
+
+    used_verts = set()
+    used_edges = set()
+
+    faces_to_parse = []
+
+    # get shared edge of two faces
+    cross_edges = []
+    for edge in active_face.edges:
+        if edge in sel_faces[0].edges and edge in sel_faces[1].edges:
+            cross_edges.append(edge)
+
+    # parse two selected faces
+    if cross_edges and len(cross_edges) == 1:
+        shared_edge = cross_edges[0]
+        vert1 = None
+        vert2 = None
+
+        dot_n = active_face_nor.normalized()
+        edge_vec_1 = (shared_edge.verts[1].co - shared_edge.verts[0].co)
+        edge_vec_len = edge_vec_1.length
+        edge_vec_1 = edge_vec_1.normalized()
+
+        af_center = active_face.calc_center_median()
+        af_vec = shared_edge.verts[0].co + (edge_vec_1 * (edge_vec_len * 0.5))
+        af_vec = (af_vec - af_center).normalized()
+
+        if af_vec.cross(edge_vec_1).dot(dot_n) > 0:
+            vert1 = shared_edge.verts[0]
+            vert2 = shared_edge.verts[1]
+        else:
+            vert1 = shared_edge.verts[1]
+            vert2 = shared_edge.verts[0]
+
+        # get active face stuff and uvs
+        face_stuff = get_other_verts_edges(
+            active_face, vert1, vert2, shared_edge, uv_layer)
+        all_sorted_faces[active_face] = face_stuff
+        used_verts.update(active_face.verts)
+        used_edges.update(active_face.edges)
+
+        # get first selected face stuff and uvs as they share shared_edge
+        second_face = sel_faces[0]
+        if second_face is active_face:
+            second_face = sel_faces[1]
+        face_stuff = get_other_verts_edges(
+            second_face, vert1, vert2, shared_edge, uv_layer)
+        all_sorted_faces[second_face] = face_stuff
+        used_verts.update(second_face.verts)
+        used_edges.update(second_face.edges)
+
+        # first Grow
+        faces_to_parse.append(active_face)
+        faces_to_parse.append(second_face)
+
+    else:
+        self.report({'WARNING'}, "Two faces should share one edge")
+        return None
+
+    # parse all faces
+    while True:
+        new_parsed_faces = []
+        if not faces_to_parse:
+            break
+        for face in faces_to_parse:
+            face_stuff = all_sorted_faces.get(face)
+            new_faces = parse_faces(
+                face, face_stuff, used_verts, used_edges, all_sorted_faces,
+                uv_layer)
+            if new_faces == 'CANCELLED':
+                self.report({'WARNING'}, "More than 2 faces share edge")
+                return None
+
+            new_parsed_faces += new_faces
+        faces_to_parse = new_parsed_faces
+
+    return all_sorted_faces
+
+
+def parse_faces(
+        check_face, face_stuff, used_verts, used_edges, all_sorted_faces,
+        uv_layer):
+    """recurse faces around the new_grow only"""
+
+    new_shared_faces = []
+    for sorted_edge in face_stuff[1]:
+        shared_faces = sorted_edge.link_faces
+        if shared_faces:
+            if len(shared_faces) > 2:
+                bpy.ops.mesh.select_all(action='DESELECT')
+                for face_sel in shared_faces:
+                    face_sel.select = True
+                shared_faces = []
+                return 'CANCELLED'
+
+            clear_shared_faces = get_new_shared_faces(
+                check_face, sorted_edge, shared_faces, all_sorted_faces.keys())
+            if clear_shared_faces:
+                shared_face = clear_shared_faces[0]
+                # get vertices of the edge
+                vert1 = sorted_edge.verts[0]
+                vert2 = sorted_edge.verts[1]
+
+                muv_common.debug_print(face_stuff[0], vert1, vert2)
+                if face_stuff[0].index(vert1) > face_stuff[0].index(vert2):
+                    vert1 = sorted_edge.verts[1]
+                    vert2 = sorted_edge.verts[0]
+
+                muv_common.debug_print(shared_face.verts, vert1, vert2)
+                new_face_stuff = get_other_verts_edges(
+                    shared_face, vert1, vert2, sorted_edge, uv_layer)
+                all_sorted_faces[shared_face] = new_face_stuff
+                used_verts.update(shared_face.verts)
+                used_edges.update(shared_face.edges)
+
+                if muv_props.DEBUG:
+                    shared_face.select = True  # test which faces are parsed
+
+                new_shared_faces.append(shared_face)
+
+    return new_shared_faces
+
+
+def get_new_shared_faces(orig_face, shared_edge, check_faces, used_faces):
+    shared_faces = []
+
+    for face in check_faces:
+        is_shared_edge = shared_edge in face.edges
+        not_used = face not in used_faces
+        not_orig = face is not orig_face
+        not_hide = face.hide is False
+        if is_shared_edge and not_used and not_orig and not_hide:
+            shared_faces.append(face)
+
+    return shared_faces
+
+
+def get_other_verts_edges(face, vert1, vert2, first_edge, uv_layer):
+    face_edges = [first_edge]
+    face_verts = [vert1, vert2]
+    face_loops = []
+
+    other_edges = [edge for edge in face.edges if edge not in face_edges]
+
+    for _ in range(len(other_edges)):
+        found_edge = None
+        # get sorted verts and edges
+        for edge in other_edges:
+            if face_verts[-1] in edge.verts:
+                other_vert = edge.other_vert(face_verts[-1])
+
+                if other_vert not in face_verts:
+                    face_verts.append(other_vert)
+
+                found_edge = edge
+                if found_edge not in face_edges:
+                    face_edges.append(edge)
+                break
+
+        other_edges.remove(found_edge)
+
+    # get sorted uvs
+    for vert in face_verts:
+        for loop in face.loops:
+            if loop.vert is vert:
+                face_loops.append(loop[uv_layer])
+                break
+
+    return [face_verts, face_edges, face_loops]
diff --git a/uv_magic_uv/muv_unwrapconst_ops.py b/uv_magic_uv/muv_unwrapconst_ops.py
new file mode 100644
index 0000000000000000000000000000000000000000..d18634cd5c38d5a800392c412cde884bcaee5b58
--- /dev/null
+++ b/uv_magic_uv/muv_unwrapconst_ops.py
@@ -0,0 +1,117 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "4.3"
+__date__ = "1 Apr 2017"
+
+
+import bpy
+import bmesh
+from bpy.props import BoolProperty, EnumProperty, FloatProperty
+from . import muv_common
+
+
+class MUV_UnwrapConstraint(bpy.types.Operator):
+    """
+    Operation class: Unwrap with constrain UV coordinate
+    """
+
+    bl_idname = "uv.muv_unwrap_constraint"
+    bl_label = "Unwrap Constraint"
+    bl_description = "Unwrap while keeping uv coordinate"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    # property for original unwrap
+    method = EnumProperty(
+        name="Method",
+        description="Unwrapping method",
+        items=[
+            ('ANGLE_BASED', 'Angle Based', 'Angle Based'),
+            ('CONFORMAL', 'Conformal', 'Conformal')
+        ],
+        default='ANGLE_BASED')
+    fill_holes = BoolProperty(
+        name="Fill Holes",
+        description="Virtual fill holes in meshes before unwrapping",
+        default=True)
+    correct_aspect = BoolProperty(
+        name="Correct Aspect",
+        description="Map UVs taking image aspect ratio into account",
+        default=True)
+    use_subsurf_data = BoolProperty(
+        name="Use Subsurf Modifier",
+        description="""Map UVs taking vertex position after subsurf
+                       into account""",
+        default=False)
+    margin = FloatProperty(
+        name="Margin",
+        description="Space between islands",
+        max=1.0,
+        min=0.0,
+        default=0.001)
+
+    # property for this operation
+    u_const = BoolProperty(
+        name="U-Constraint",
+        description="Keep UV U-axis coordinate",
+        default=False)
+    v_const = BoolProperty(
+        name="V-Constraint",
+        description="Keep UV V-axis coordinate",
+        default=False)
+
+    def execute(self, _):
+        obj = bpy.context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.faces.ensure_lookup_table()
+
+        if not bm.loops.layers.uv:
+            self.report({'WARNING'}, "Object must have more than one UV map")
+            return {'CANCELLED'}
+        uv_layer = bm.loops.layers.uv.verify()
+
+        # get original UV coordinate
+        faces = [f for f in bm.faces if f.select]
+        uv_list = []
+        for f in faces:
+            uvs = [l[uv_layer].uv.copy() for l in f.loops]
+            uv_list.append(uvs)
+
+        # unwrap
+        bpy.ops.uv.unwrap(
+            method=self.method,
+            fill_holes=self.fill_holes,
+            correct_aspect=self.correct_aspect,
+            use_subsurf_data=self.use_subsurf_data,
+            margin=self.margin)
+
+        # when U/V-Constraint is checked, revert original coordinate
+        for f, uvs in zip(faces, uv_list):
+            for l, uv in zip(f.loops, uvs):
+                if self.u_const:
+                    l[uv_layer].uv.x = uv.x
+                if self.v_const:
+                    l[uv_layer].uv.y = uv.y
+
+        # update mesh
+        bmesh.update_edit_mesh(obj.data)
+
+        return {'FINISHED'}
diff --git a/uv_magic_uv/muv_uvbb_ops.py b/uv_magic_uv/muv_uvbb_ops.py
new file mode 100644
index 0000000000000000000000000000000000000000..6666ddadf0d68fe8cf9955f5250074f46e6f07c5
--- /dev/null
+++ b/uv_magic_uv/muv_uvbb_ops.py
@@ -0,0 +1,755 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "4.3"
+__date__ = "1 Apr 2017"
+
+
+from enum import IntEnum
+import math
+
+import bpy
+import bgl
+import mathutils
+import bmesh
+
+from . import muv_common
+
+
+MAX_VALUE = 100000.0
+
+
+class MUV_UVBBCmd():
+    """
+    Custom class: Base class of command
+    """
+
+    def __init__(self):
+        self.op = 'NONE'        # operation
+
+    def to_matrix(self):
+        # mat = I
+        mat = mathutils.Matrix()
+        mat.identity()
+        return mat
+
+
+class MUV_UVBBTranslationCmd(MUV_UVBBCmd):
+    """
+    Custom class: Translation operation
+    """
+
+    def __init__(self, ix, iy):
+        super().__init__()
+        self.op = 'TRANSLATION'
+        self.__x = ix       # current x
+        self.__y = iy       # current y
+        self.__ix = ix      # initial x
+        self.__iy = iy      # initial y
+
+    def to_matrix(self):
+        # mat = Mt
+        dx = self.__x - self.__ix
+        dy = self.__y - self.__iy
+        return mathutils.Matrix.Translation((dx, dy, 0))
+
+    def set(self, x, y):
+        self.__x = x
+        self.__y = y
+
+
+class MUV_UVBBRotationCmd(MUV_UVBBCmd):
+    """
+    Custom class: Rotation operation
+    """
+
+    def __init__(self, ix, iy, cx, cy):
+        super().__init__()
+        self.op = 'ROTATION'
+        self.__x = ix       # current x
+        self.__y = iy       # current y
+        self.__cx = cx      # center of rotation x
+        self.__cy = cy      # center of rotation y
+        dx = self.__x - self.__cx
+        dy = self.__y - self.__cy
+        self.__iangle = math.atan2(dy, dx)      # initial rotation angle
+
+    def to_matrix(self):
+        # mat = Mt * Mr * Mt^-1
+        dx = self.__x - self.__cx
+        dy = self.__y - self.__cy
+        angle = math.atan2(dy, dx) - self.__iangle
+        mti = mathutils.Matrix.Translation((-self.__cx, -self.__cy, 0.0))
+        mr = mathutils.Matrix.Rotation(angle, 4, 'Z')
+        mt = mathutils.Matrix.Translation((self.__cx, self.__cy, 0.0))
+        return mt * mr * mti
+
+    def set(self, x, y):
+        self.__x = x
+        self.__y = y
+
+
+class MUV_UVBBScalingCmd(MUV_UVBBCmd):
+    """
+    Custom class: Scaling operation
+    """
+
+    def __init__(self, ix, iy, ox, oy, dir_x, dir_y, mat):
+        super().__init__()
+        self.op = 'SCALING'
+        self.__ix = ix          # initial x
+        self.__iy = iy          # initial y
+        self.__x = ix           # current x
+        self.__y = iy           # current y
+        self.__ox = ox          # origin of scaling x
+        self.__oy = oy          # origin of scaling y
+        self.__dir_x = dir_x    # direction of scaling x
+        self.__dir_y = dir_y    # direction of scaling y
+        self.__mat = mat
+        # initial origin of scaling = M(to original transform) * (ox, oy)
+        iov = mat * mathutils.Vector((ox, oy, 0.0))
+        self.__iox = iov.x      # initial origin of scaling X
+        self.__ioy = iov.y      # initial origin of scaling y
+
+    def to_matrix(self):
+        """
+        mat = M(to original transform)^-1 * Mt(to origin) * Ms *
+              Mt(to origin)^-1 * M(to original transform)
+        """
+        m = self.__mat
+        mi = self.__mat.inverted()
+        mtoi = mathutils.Matrix.Translation((-self.__iox, -self.__ioy, 0.0))
+        mto = mathutils.Matrix.Translation((self.__iox, self.__ioy, 0.0))
+        # every point must be transformed to origin
+        t = m * mathutils.Vector((self.__ix, self.__iy, 0.0))
+        tix, tiy = t.x, t.y
+        t = m * mathutils.Vector((self.__ox, self.__oy, 0.0))
+        tox, toy = t.x, t.y
+        t = m * mathutils.Vector((self.__x, self.__y, 0.0))
+        tx, ty = t.x, t.y
+        ms = mathutils.Matrix()
+        ms.identity()
+        if self.__dir_x == 1:
+            ms[0][0] = (tx - tox) * self.__dir_x / (tix - tox)
+        if self.__dir_y == 1:
+            ms[1][1] = (ty - toy) * self.__dir_y / (tiy - toy)
+        return mi * mto * ms * mtoi * m
+
+    def set(self, x, y):
+        self.__x = x
+        self.__y = y
+
+
+class MUV_UVBBUniformScalingCmd(MUV_UVBBCmd):
+    """
+    Custom class: Uniform Scaling operation
+    """
+
+    def __init__(self, ix, iy, ox, oy, mat):
+        super().__init__()
+        self.op = 'SCALING'
+        self.__ix = ix          # initial x
+        self.__iy = iy          # initial y
+        self.__x = ix           # current x
+        self.__y = iy           # current y
+        self.__ox = ox          # origin of scaling x
+        self.__oy = oy          # origin of scaling y
+        self.__mat = mat
+        # initial origin of scaling = M(to original transform) * (ox, oy)
+        iov = mat * mathutils.Vector((ox, oy, 0.0))
+        self.__iox = iov.x      # initial origin of scaling x
+        self.__ioy = iov.y      # initial origin of scaling y
+        self.__dir_x = 1
+        self.__dir_y = 1
+
+    def to_matrix(self):
+        """
+        mat = M(to original transform)^-1 * Mt(to origin) * Ms *
+              Mt(to origin)^-1 * M(to original transform)
+        """
+        m = self.__mat
+        mi = self.__mat.inverted()
+        mtoi = mathutils.Matrix.Translation((-self.__iox, -self.__ioy, 0.0))
+        mto = mathutils.Matrix.Translation((self.__iox, self.__ioy, 0.0))
+        # every point must be transformed to origin
+        t = m * mathutils.Vector((self.__ix, self.__iy, 0.0))
+        tix, tiy = t.x, t.y
+        t = m * mathutils.Vector((self.__ox, self.__oy, 0.0))
+        tox, toy = t.x, t.y
+        t = m * mathutils.Vector((self.__x, self.__y, 0.0))
+        tx, ty = t.x, t.y
+        ms = mathutils.Matrix()
+        ms.identity()
+        tir = math.sqrt((tix - tox) * (tix - tox) + (tiy - toy) * (tiy - toy))
+        tr = math.sqrt((tx - tox) * (tx - tox) + (ty - toy) * (ty - toy))
+
+        sr = tr / tir
+
+        if ((tx - tox) * (tix - tox)) > 0:
+            self.__dir_x = 1
+        else:
+            self.__dir_x = -1
+        if ((ty - toy) * (tiy - toy)) > 0:
+            self.__dir_y = 1
+        else:
+            self.__dir_y = -1
+
+        ms[0][0] = sr * self.__dir_x
+        ms[1][1] = sr * self.__dir_y
+
+        return mi * mto * ms * mtoi * m
+
+    def set(self, x, y):
+        self.__x = x
+        self.__y = y
+
+
+class MUV_UVBBCmdExecuter():
+    """
+    Custom class: manage command history and execute command
+    """
+
+    def __init__(self):
+        self.__cmd_list = []        # history
+        self.__cmd_list_redo = []   # redo list
+
+    def execute(self, begin=0, end=-1):
+        """
+        create matrix from history
+        """
+        mat = mathutils.Matrix()
+        mat.identity()
+        for i, cmd in enumerate(self.__cmd_list):
+            if begin <= i and (end == -1 or i <= end):
+                mat = cmd.to_matrix() * mat
+        return mat
+
+    def undo_size(self):
+        """
+        get history size
+        """
+        return len(self.__cmd_list)
+
+    def top(self):
+        """
+        get top of history
+        """
+        if len(self.__cmd_list) <= 0:
+            return None
+        return self.__cmd_list[-1]
+
+    def append(self, cmd):
+        """
+        append command
+        """
+        self.__cmd_list.append(cmd)
+        self.__cmd_list_redo = []
+
+    def undo(self):
+        """
+        undo command
+        """
+        if len(self.__cmd_list) <= 0:
+            return
+        self.__cmd_list_redo.append(self.__cmd_list.pop())
+
+    def redo(self):
+        """
+        redo command
+        """
+        if len(self.__cmd_list_redo) <= 0:
+            return
+        self.__cmd_list.append(self.__cmd_list_redo.pop())
+
+    def pop(self):
+        if len(self.__cmd_list) <= 0:
+            return None
+        return self.__cmd_list.pop()
+
+    def push(self, cmd):
+        self.__cmd_list.append(cmd)
+
+
+class MUV_UVBBRenderer(bpy.types.Operator):
+    """
+    Operation class: Render UV bounding box
+    """
+
+    bl_idname = "uv.muv_uvbb_renderer"
+    bl_label = "UV Bounding Box Renderer"
+    bl_description = "Bounding Box Renderer about UV in Image Editor"
+
+    __handle = None
+
+    @staticmethod
+    def handle_add(obj, context):
+        if MUV_UVBBRenderer.__handle is None:
+            sie = bpy.types.SpaceImageEditor
+            MUV_UVBBRenderer.__handle = sie.draw_handler_add(
+                MUV_UVBBRenderer.draw_bb,
+                (obj, context), "WINDOW", "POST_PIXEL")
+
+    @staticmethod
+    def handle_remove():
+        if MUV_UVBBRenderer.__handle is not None:
+            sie = bpy.types.SpaceImageEditor
+            sie.draw_handler_remove(
+                MUV_UVBBRenderer.__handle, "WINDOW")
+            MUV_UVBBRenderer.__handle = None
+
+    @staticmethod
+    def __draw_ctrl_point(context, pos):
+        """
+        Draw control point
+        """
+        prefs = context.user_preferences.addons["uv_magic_uv"].preferences
+        cp_size = prefs.uvbb_cp_size
+        offset = cp_size / 2
+        verts = [
+            [pos.x - offset, pos.y - offset],
+            [pos.x - offset, pos.y + offset],
+            [pos.x + offset, pos.y + offset],
+            [pos.x + offset, pos.y - offset]
+        ]
+        bgl.glEnable(bgl.GL_BLEND)
+        bgl.glBegin(bgl.GL_QUADS)
+        bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
+        for (x, y) in verts:
+            bgl.glVertex2f(x, y)
+        bgl.glEnd()
+
+    @staticmethod
+    def draw_bb(_, context):
+        """
+        Draw bounding box
+        """
+        props = context.scene.muv_props.uvbb
+        for cp in props.ctrl_points:
+            MUV_UVBBRenderer.__draw_ctrl_point(
+                context, mathutils.Vector(
+                    context.region.view2d.view_to_region(cp.x, cp.y)))
+
+
+class MUV_UVBBState(IntEnum):
+    """
+    Enum: State definition used by MUV_UVBBStateMgr
+    """
+    NONE = 0
+    TRANSLATING = 1
+    SCALING_1 = 2
+    SCALING_2 = 3
+    SCALING_3 = 4
+    SCALING_4 = 5
+    SCALING_5 = 6
+    SCALING_6 = 7
+    SCALING_7 = 8
+    SCALING_8 = 9
+    ROTATING = 10
+    UNIFORM_SCALING_1 = 11
+    UNIFORM_SCALING_2 = 12
+    UNIFORM_SCALING_3 = 13
+    UNIFORM_SCALING_4 = 14
+
+
+class MUV_UVBBStateBase():
+    """
+    Custom class: Base class of state
+    """
+
+    def __init__(self):
+        pass
+
+    def update(self, context, event, ctrl_points, mouse_view):
+        raise NotImplementedError
+
+
+class MUV_UVBBStateNone(MUV_UVBBStateBase):
+    """
+    Custom class:
+    No state
+    Wait for event from mouse
+    """
+
+    def __init__(self, cmd_exec):
+        super().__init__()
+        self.__cmd_exec = cmd_exec
+
+    def update(self, context, event, ctrl_points, mouse_view):
+        """
+        Update state
+        """
+        prefs = context.user_preferences.addons["uv_magic_uv"].preferences
+        cp_react_size = prefs.uvbb_cp_react_size
+        is_uscaling = context.scene.muv_uvbb_uniform_scaling
+        if event.type == 'LEFTMOUSE':
+            if event.value == 'PRESS':
+                x, y = context.region.view2d.view_to_region(
+                    mouse_view.x, mouse_view.y)
+                for i, p in enumerate(ctrl_points):
+                    px, py = context.region.view2d.view_to_region(p.x, p.y)
+                    in_cp_x = (px + cp_react_size > x and
+                               px - cp_react_size < x)
+                    in_cp_y = (py + cp_react_size > y and
+                               py - cp_react_size < y)
+                    if in_cp_x and in_cp_y:
+                        if is_uscaling:
+                            arr = [1, 3, 6, 8]
+                            if i in arr:
+                                return (
+                                    MUV_UVBBState.UNIFORM_SCALING_1
+                                    + arr.index(i)
+                                )
+                        else:
+                            return MUV_UVBBState.TRANSLATING + i
+
+        return MUV_UVBBState.NONE
+
+
+class MUV_UVBBStateTranslating(MUV_UVBBStateBase):
+    """
+    Custom class: Translating state
+    """
+
+    def __init__(self, cmd_exec, ctrl_points):
+        super().__init__()
+        self.__cmd_exec = cmd_exec
+        ix, iy = ctrl_points[0].x, ctrl_points[0].y
+        self.__cmd_exec.append(MUV_UVBBTranslationCmd(ix, iy))
+
+    def update(self, context, event, ctrl_points, mouse_view):
+        if event.type == 'LEFTMOUSE':
+            if event.value == 'RELEASE':
+                return MUV_UVBBState.NONE
+        if event.type == 'MOUSEMOVE':
+            x, y = mouse_view.x, mouse_view.y
+            self.__cmd_exec.top().set(x, y)
+        return MUV_UVBBState.TRANSLATING
+
+
+class MUV_UVBBStateScaling(MUV_UVBBStateBase):
+    """
+    Custom class: Scaling state
+    """
+
+    def __init__(self, cmd_exec, state, ctrl_points):
+        super().__init__()
+        self.__state = state
+        self.__cmd_exec = cmd_exec
+        dir_x_list = [1, 1, 1, 0, 0, 1, 1, 1]
+        dir_y_list = [1, 0, 1, 1, 1, 1, 0, 1]
+        idx = state - 2
+        ix, iy = ctrl_points[idx + 1].x, ctrl_points[idx + 1].y
+        ox, oy = ctrl_points[8 - idx].x, ctrl_points[8 - idx].y
+        dir_x, dir_y = dir_x_list[idx], dir_y_list[idx]
+        mat = self.__cmd_exec.execute(end=self.__cmd_exec.undo_size())
+        self.__cmd_exec.append(
+            MUV_UVBBScalingCmd(ix, iy, ox, oy, dir_x, dir_y, mat.inverted()))
+
+    def update(self, context, event, ctrl_points, mouse_view):
+        if event.type == 'LEFTMOUSE':
+            if event.value == 'RELEASE':
+                return MUV_UVBBState.NONE
+        if event.type == 'MOUSEMOVE':
+            x, y = mouse_view.x, mouse_view.y
+            self.__cmd_exec.top().set(x, y)
+        return self.__state
+
+
+class MUV_UVBBStateUniformScaling(MUV_UVBBStateBase):
+    """
+    Custom class: Uniform Scaling state
+    """
+
+    def __init__(self, cmd_exec, state, ctrl_points):
+        super().__init__()
+        self.__state = state
+        self.__cmd_exec = cmd_exec
+        icp_idx = [1, 3, 6, 8]
+        ocp_idx = [8, 6, 3, 1]
+        idx = state - MUV_UVBBState.UNIFORM_SCALING_1
+        ix, iy = ctrl_points[icp_idx[idx]].x, ctrl_points[icp_idx[idx]].y
+        ox, oy = ctrl_points[ocp_idx[idx]].x, ctrl_points[ocp_idx[idx]].y
+        mat = self.__cmd_exec.execute(end=self.__cmd_exec.undo_size())
+        self.__cmd_exec.append(MUV_UVBBUniformScalingCmd(
+            ix, iy, ox, oy, mat.inverted()))
+
+    def update(self, context, event, ctrl_points, mouse_view):
+        if event.type == 'LEFTMOUSE':
+            if event.value == 'RELEASE':
+                return MUV_UVBBState.NONE
+        if event.type == 'MOUSEMOVE':
+            x, y = mouse_view.x, mouse_view.y
+            self.__cmd_exec.top().set(x, y)
+
+        return self.__state
+
+
+class MUV_UVBBStateRotating(MUV_UVBBStateBase):
+    """
+    Custom class: Rotating state
+    """
+
+    def __init__(self, cmd_exec, ctrl_points):
+        super().__init__()
+        self.__cmd_exec = cmd_exec
+        ix, iy = ctrl_points[9].x, ctrl_points[9].y
+        ox, oy = ctrl_points[0].x, ctrl_points[0].y
+        self.__cmd_exec.append(MUV_UVBBRotationCmd(ix, iy, ox, oy))
+
+    def update(self, context, event, ctrl_points, mouse_view):
+        if event.type == 'LEFTMOUSE':
+            if event.value == 'RELEASE':
+                return MUV_UVBBState.NONE
+        if event.type == 'MOUSEMOVE':
+            x, y = mouse_view.x, mouse_view.y
+            self.__cmd_exec.top().set(x, y)
+        return MUV_UVBBState.ROTATING
+
+
+class MUV_UVBBStateMgr():
+    """
+    Custom class: Manage state about this feature
+    """
+
+    def __init__(self, cmd_exec):
+        self.__cmd_exec = cmd_exec          # command executer
+        self.__state = MUV_UVBBState.NONE   # current state
+        self.__state_obj = MUV_UVBBStateNone(self.__cmd_exec)
+
+    def __update_state(self, next_state, ctrl_points):
+        """
+        Update state
+        """
+
+        if next_state == self.__state:
+            return
+        obj = None
+        if next_state == MUV_UVBBState.TRANSLATING:
+            obj = MUV_UVBBStateTranslating(self.__cmd_exec, ctrl_points)
+        elif MUV_UVBBState.SCALING_1 <= next_state <= MUV_UVBBState.SCALING_8:
+            obj = MUV_UVBBStateScaling(
+                self.__cmd_exec, next_state, ctrl_points)
+        elif next_state == MUV_UVBBState.ROTATING:
+            obj = MUV_UVBBStateRotating(self.__cmd_exec, ctrl_points)
+        elif next_state == MUV_UVBBState.NONE:
+            obj = MUV_UVBBStateNone(self.__cmd_exec)
+        elif (MUV_UVBBState.UNIFORM_SCALING_1 <= next_state
+              <= MUV_UVBBState.UNIFORM_SCALING_4):
+            obj = MUV_UVBBStateUniformScaling(
+                self.__cmd_exec, next_state, ctrl_points)
+
+        if obj is not None:
+            self.__state_obj = obj
+
+        self.__state = next_state
+
+    def update(self, context, ctrl_points, event):
+        mouse_region = mathutils.Vector((
+            event.mouse_region_x, event.mouse_region_y))
+        mouse_view = mathutils.Vector((context.region.view2d.region_to_view(
+            mouse_region.x, mouse_region.y)))
+        next_state = self.__state_obj.update(
+            context, event, ctrl_points, mouse_view)
+        self.__update_state(next_state, ctrl_points)
+
+
+class MUV_UVBBUpdater(bpy.types.Operator):
+    """
+    Operation class: Update state and handle event by modal function
+    """
+
+    bl_idname = "uv.muv_uvbb_updater"
+    bl_label = "UV Bounding Box Updater"
+    bl_description = "Update UV Bounding Box"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    def __init__(self):
+        self.__timer = None
+        self.__cmd_exec = MUV_UVBBCmdExecuter()         # Command executer
+        self.__state_mgr = MUV_UVBBStateMgr(self.__cmd_exec)    # State Manager
+
+    def __handle_add(self, context):
+        if self.__timer is None:
+            self.__timer = context.window_manager.event_timer_add(
+                0.1, context.window)
+            context.window_manager.modal_handler_add(self)
+        MUV_UVBBRenderer.handle_add(self, context)
+
+    def __handle_remove(self, context):
+        MUV_UVBBRenderer.handle_remove()
+        if self.__timer is not None:
+            context.window_manager.event_timer_remove(self.__timer)
+            self.__timer = None
+
+    def __get_uv_info(self, context):
+        """
+        Get UV coordinate
+        """
+        obj = context.active_object
+        uv_info = []
+        bm = bmesh.from_edit_mesh(obj.data)
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.faces.ensure_lookup_table()
+        if not bm.loops.layers.uv:
+            return None
+        uv_layer = bm.loops.layers.uv.verify()
+        for f in bm.faces:
+            if f.select:
+                for i, l in enumerate(f.loops):
+                    uv_info.append((f.index, i, l[uv_layer].uv.copy()))
+        if len(uv_info) == 0:
+            return None
+        return uv_info
+
+    def __get_ctrl_point(self, uv_info_ini):
+        """
+        Get control point
+        """
+        left = MAX_VALUE
+        right = -MAX_VALUE
+        top = -MAX_VALUE
+        bottom = MAX_VALUE
+
+        for info in uv_info_ini:
+            uv = info[2]
+            if uv.x < left:
+                left = uv.x
+            if uv.x > right:
+                right = uv.x
+            if uv.y < bottom:
+                bottom = uv.y
+            if uv.y > top:
+                top = uv.y
+
+        points = [
+            mathutils.Vector((
+                (left + right) * 0.5, (top + bottom) * 0.5, 0.0
+            )),
+            mathutils.Vector((left, top, 0.0)),
+            mathutils.Vector((left, (top + bottom) * 0.5, 0.0)),
+            mathutils.Vector((left, bottom, 0.0)),
+            mathutils.Vector(((left + right) * 0.5, top, 0.0)),
+            mathutils.Vector(((left + right) * 0.5, bottom, 0.0)),
+            mathutils.Vector((right, top, 0.0)),
+            mathutils.Vector((right, (top + bottom) * 0.5, 0.0)),
+            mathutils.Vector((right, bottom, 0.0)),
+            mathutils.Vector(((left + right) * 0.5, top + 0.03, 0.0))
+        ]
+
+        return points
+
+    def __update_uvs(self, context, uv_info_ini, trans_mat):
+        """
+        Update UV coordinate
+        """
+        obj = context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.faces.ensure_lookup_table()
+        if not bm.loops.layers.uv:
+            return
+        uv_layer = bm.loops.layers.uv.verify()
+        for info in uv_info_ini:
+            fidx = info[0]
+            lidx = info[1]
+            uv = info[2]
+            v = mathutils.Vector((uv.x, uv.y, 0.0))
+            av = trans_mat * v
+            bm.faces[fidx].loops[lidx][uv_layer].uv = mathutils.Vector(
+                (av.x, av.y))
+
+    def __update_ctrl_point(self, ctrl_points_ini, trans_mat):
+        """
+        Update control point
+        """
+        return [trans_mat * cp for cp in ctrl_points_ini]
+
+    def modal(self, context, event):
+        props = context.scene.muv_props.uvbb
+        muv_common.redraw_all_areas()
+        if props.running is False:
+            self.__handle_remove(context)
+            return {'FINISHED'}
+        if event.type == 'TIMER':
+            trans_mat = self.__cmd_exec.execute()
+            self.__update_uvs(context, props.uv_info_ini, trans_mat)
+            props.ctrl_points = self.__update_ctrl_point(
+                props.ctrl_points_ini, trans_mat)
+
+        self.__state_mgr.update(context, props.ctrl_points, event)
+
+        return {'PASS_THROUGH'}
+
+    def execute(self, context):
+        props = context.scene.muv_props.uvbb
+
+        if props.running is True:
+            props.running = False
+            return {'FINISHED'}
+
+        props.uv_info_ini = self.__get_uv_info(context)
+        if props.uv_info_ini is None:
+            return {'CANCELLED'}
+        props.ctrl_points_ini = self.__get_ctrl_point(props.uv_info_ini)
+        trans_mat = self.__cmd_exec.execute()
+        # Update is needed in order to display control point
+        self.__update_uvs(context, props.uv_info_ini, trans_mat)
+        props.ctrl_points = self.__update_ctrl_point(
+            props.ctrl_points_ini, trans_mat)
+        self.__handle_add(context)
+        props.running = True
+
+        return {'RUNNING_MODAL'}
+
+
+class IMAGE_PT_MUV_UVBB(bpy.types.Panel):
+    """
+    Panel class: UV Bounding Box Menu on Property Panel on UV/ImageEditor
+    """
+
+    bl_space_type = 'IMAGE_EDITOR'
+    bl_region_type = 'UI'
+    bl_label = "UV Bounding Box"
+    bl_context = 'mesh_edit'
+
+    @classmethod
+    def poll(cls, context):
+        prefs = context.user_preferences.addons["uv_magic_uv"].preferences
+        return prefs.enable_uvbb
+
+    def draw_header(self, _):
+        layout = self.layout
+        layout.label(text="", icon='PLUGIN')
+
+    def draw(self, context):
+        sc = context.scene
+        props = sc.muv_props.uvbb
+        layout = self.layout
+        if props.running is False:
+            layout.operator(
+                MUV_UVBBUpdater.bl_idname, text="Display UV Bounding Box",
+                icon='PLAY')
+        else:
+            layout.operator(
+                MUV_UVBBUpdater.bl_idname, text="Hide UV Bounding Box",
+                icon='PAUSE')
+        layout.prop(sc, "muv_uvbb_uniform_scaling", text="Uniform Scaling")
diff --git a/uv_magic_uv/muv_wsuv_ops.py b/uv_magic_uv/muv_wsuv_ops.py
new file mode 100644
index 0000000000000000000000000000000000000000..dce564472d7677ab25dc35ca12dcb770eda6d388
--- /dev/null
+++ b/uv_magic_uv/muv_wsuv_ops.py
@@ -0,0 +1,151 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "McBuff, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "4.3"
+__date__ = "1 Apr 2017"
+
+
+import bpy
+import bmesh
+from . import muv_common
+
+
+def calc_edge_scale(uv_layer, loop0, loop1):
+    v0 = loop0.vert.co
+    v1 = loop1.vert.co
+    uv0 = loop0[uv_layer].uv.copy()
+    uv1 = loop1[uv_layer].uv.copy()
+
+    dv = v1 - v0
+    duv = uv1 - uv0
+
+    scale = 0.0
+    if dv.magnitude > 0.00000001:
+        scale = duv.magnitude / dv.magnitude
+
+    return scale
+
+
+def calc_face_scale(uv_layer, face):
+    es = 0.0
+    for i, l in enumerate(face.loops[1:]):
+        es = es + calc_edge_scale(uv_layer, face.loops[i], l)
+
+    return es
+
+
+class MUV_WSUVMeasure(bpy.types.Operator):
+    """
+    Operation class: Measure face size
+    """
+
+    bl_idname = "uv.muv_wsuv_measure"
+    bl_label = "Measure"
+    bl_description = "Measure face size for scale calculation"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    def execute(self, context):
+        props = context.scene.muv_props.wsuv
+        obj = bpy.context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.verts.ensure_lookup_table()
+            bm.edges.ensure_lookup_table()
+            bm.faces.ensure_lookup_table()
+
+        if not bm.loops.layers.uv:
+            self.report({'WARNING'}, "Object must have more than one UV map")
+            return {'CANCELLED'}
+        uv_layer = bm.loops.layers.uv.verify()
+
+        sel_faces = [f for f in bm.faces if f.select]
+
+        # measure average face size
+        scale = 0.0
+        for f in sel_faces:
+            scale = scale + calc_face_scale(uv_layer, f)
+
+        props.ref_scale = scale / len(sel_faces)
+
+        return {'FINISHED'}
+
+
+class MUV_WSUVApply(bpy.types.Operator):
+    """
+    Operation class: Apply scaled UV
+    """
+
+    bl_idname = "uv.muv_wsuv_apply"
+    bl_label = "Apply"
+    bl_description = "Apply scaled UV based on scale calculation"
+    bl_options = {'REGISTER', 'UNDO'}
+
+    def execute(self, context):
+        props = context.scene.muv_props.wsuv
+        obj = bpy.context.active_object
+        bm = bmesh.from_edit_mesh(obj.data)
+        if muv_common.check_version(2, 73, 0) >= 0:
+            bm.verts.ensure_lookup_table()
+            bm.edges.ensure_lookup_table()
+            bm.faces.ensure_lookup_table()
+
+        if not bm.loops.layers.uv:
+            self.report(
+                {'WARNING'}, "Object must have more than one UV map")
+            return {'CANCELLED'}
+        uv_layer = bm.loops.layers.uv.verify()
+
+        sel_faces = [f for f in bm.faces if f.select]
+
+        # measure average face size
+        scale = 0.0
+        for f in sel_faces:
+            scale = scale + calc_face_scale(uv_layer, f)
+        scale = scale / len(sel_faces)
+
+        ratio = props.ref_scale / scale
+
+        orig_area = bpy.context.area.type
+        bpy.context.area.type = 'IMAGE_EDITOR'
+
+        # apply scaled UV
+        bpy.ops.transform.resize(
+            value=(ratio, ratio, ratio),
+            constraint_axis=(False, False, False),
+            constraint_orientation='GLOBAL',
+            mirror=False,
+            proportional='DISABLED',
+            proportional_edit_falloff='SMOOTH',
+            proportional_size=1,
+            snap=False,
+            snap_target='CLOSEST',
+            snap_point=(0, 0, 0),
+            snap_align=False,
+            snap_normal=(0, 0, 0),
+            texture_space=False,
+            release_confirm=False)
+
+        bpy.context.area.type = orig_area
+
+        bmesh.update_edit_mesh(obj.data)
+
+        return {'FINISHED'}