From 8a17c01627bae97a66a237d5ab3cfd19c8f7adc1 Mon Sep 17 00:00:00 2001 From: Nutti Date: Sun, 2 Apr 2017 11:21:28 +0900 Subject: uv_magic_uv commit to addons release: T51064 Initial commit to addons release. Task: T51064 --- uv_magic_uv/__init__.py | 126 ++++++ uv_magic_uv/muv_common.py | 86 ++++ uv_magic_uv/muv_cpuv_ops.py | 455 ++++++++++++++++++++ uv_magic_uv/muv_cpuv_selseq_ops.py | 249 +++++++++++ uv_magic_uv/muv_fliprot_ops.py | 107 +++++ uv_magic_uv/muv_menu.py | 122 ++++++ uv_magic_uv/muv_mirroruv_ops.py | 152 +++++++ uv_magic_uv/muv_mvuv_ops.py | 126 ++++++ uv_magic_uv/muv_packuv_ops.py | 285 +++++++++++++ uv_magic_uv/muv_preferences.py | 141 +++++++ uv_magic_uv/muv_preserve_uv_aspect.py | 119 ++++++ uv_magic_uv/muv_props.py | 143 +++++++ uv_magic_uv/muv_texlock_ops.py | 431 +++++++++++++++++++ uv_magic_uv/muv_texproj_ops.py | 328 +++++++++++++++ uv_magic_uv/muv_transuv_ops.py | 345 ++++++++++++++++ uv_magic_uv/muv_unwrapconst_ops.py | 117 ++++++ uv_magic_uv/muv_uvbb_ops.py | 755 ++++++++++++++++++++++++++++++++++ uv_magic_uv/muv_wsuv_ops.py | 151 +++++++ 18 files changed, 4238 insertions(+) create mode 100644 uv_magic_uv/__init__.py create mode 100644 uv_magic_uv/muv_common.py create mode 100644 uv_magic_uv/muv_cpuv_ops.py create mode 100644 uv_magic_uv/muv_cpuv_selseq_ops.py create mode 100644 uv_magic_uv/muv_fliprot_ops.py create mode 100644 uv_magic_uv/muv_menu.py create mode 100644 uv_magic_uv/muv_mirroruv_ops.py create mode 100644 uv_magic_uv/muv_mvuv_ops.py create mode 100644 uv_magic_uv/muv_packuv_ops.py create mode 100644 uv_magic_uv/muv_preferences.py create mode 100644 uv_magic_uv/muv_preserve_uv_aspect.py create mode 100644 uv_magic_uv/muv_props.py create mode 100644 uv_magic_uv/muv_texlock_ops.py create mode 100644 uv_magic_uv/muv_texproj_ops.py create mode 100644 uv_magic_uv/muv_transuv_ops.py create mode 100644 uv_magic_uv/muv_unwrapconst_ops.py create mode 100644 uv_magic_uv/muv_uvbb_ops.py create mode 100644 uv_magic_uv/muv_wsuv_ops.py (limited to 'uv_magic_uv') 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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 , 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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 , 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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'} -- cgit v1.2.3