Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNutti <nutti.metro@gmail.com>2017-04-02 05:21:28 +0300
committerNutti <nutti.metro@gmail.com>2017-04-02 05:21:28 +0300
commit8a17c01627bae97a66a237d5ab3cfd19c8f7adc1 (patch)
tree77d38ef0d3284b7bf7d7350f88b185d08c20261e /uv_magic_uv
parentd9c25b43907a2620a77b6fd889af9302f859c9d2 (diff)
uv_magic_uv commit to addons release: T51064
Initial commit to addons release. Task: T51064
Diffstat (limited to 'uv_magic_uv')
-rw-r--r--uv_magic_uv/__init__.py126
-rw-r--r--uv_magic_uv/muv_common.py86
-rw-r--r--uv_magic_uv/muv_cpuv_ops.py455
-rw-r--r--uv_magic_uv/muv_cpuv_selseq_ops.py249
-rw-r--r--uv_magic_uv/muv_fliprot_ops.py107
-rw-r--r--uv_magic_uv/muv_menu.py122
-rw-r--r--uv_magic_uv/muv_mirroruv_ops.py152
-rw-r--r--uv_magic_uv/muv_mvuv_ops.py126
-rw-r--r--uv_magic_uv/muv_packuv_ops.py285
-rw-r--r--uv_magic_uv/muv_preferences.py141
-rw-r--r--uv_magic_uv/muv_preserve_uv_aspect.py119
-rw-r--r--uv_magic_uv/muv_props.py143
-rw-r--r--uv_magic_uv/muv_texlock_ops.py431
-rw-r--r--uv_magic_uv/muv_texproj_ops.py328
-rw-r--r--uv_magic_uv/muv_transuv_ops.py345
-rw-r--r--uv_magic_uv/muv_unwrapconst_ops.py117
-rw-r--r--uv_magic_uv/muv_uvbb_ops.py755
-rw-r--r--uv_magic_uv/muv_wsuv_ops.py151
18 files changed, 4238 insertions, 0 deletions
diff --git a/uv_magic_uv/__init__.py b/uv_magic_uv/__init__.py
new file mode 100644
index 00000000..7d18c159
--- /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 00000000..66f2a54b
--- /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 00000000..3c5e968a
--- /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 00000000..f6701b83
--- /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 00000000..597ee2a6
--- /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 00000000..17e77cfb
--- /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 00000000..d0a98b87
--- /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 00000000..2eb0991f
--- /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 00000000..06e79e7a
--- /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 00000000..066cfbac
--- /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 00000000..20be23ad
--- /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 00000000..10e14614
--- /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 00000000..80ec1c6c
--- /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 00000000..3493b13a
--- /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 00000000..e1083edc
--- /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 00000000..d18634cd
--- /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 00000000..6666ddad
--- /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 00000000..dce56447
--- /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'}