From fdc914d653d4cf9e8c6ad5ea3d71fdefb9529491 Mon Sep 17 00:00:00 2001 From: Nutti <nutti.metro@gmail.com> Date: Fri, 16 Feb 2018 21:04:36 +0900 Subject: [PATCH] Magic UV: Release v5.0 * Add features - Align UV Cursor - UV Cursor Location - Align UV - Smooth UV - UV Inspection - Select UV - Texture Wrap - UV Sculpt * Improve features - Copy/Paste UV: Add menu to UV/Image Editor - World Scale UV: Add information about Texel Density - UV Bounding Box: Add option "Bound" - Texture Projection: Add option "Assign UVMap" - UVW: Add option "Assign UVMap" * Improve UI * Fixed bugs * Optimization/Refactoring --- uv_magic_uv/__init__.py | 103 +-- uv_magic_uv/common.py | 592 +++++++++++++ uv_magic_uv/muv_common.py | 83 -- uv_magic_uv/muv_cpuv_selseq_ops.py | 279 ------- uv_magic_uv/muv_menu.py | 138 --- uv_magic_uv/muv_preferences.py | 144 ---- uv_magic_uv/muv_props.py | 148 ---- uv_magic_uv/op/__init__.py | 72 ++ uv_magic_uv/op/align_uv.py | 784 ++++++++++++++++++ uv_magic_uv/op/align_uv_cursor.py | 154 ++++ .../{muv_cpuv_ops.py => op/copy_paste_uv.py} | 435 ++++++---- uv_magic_uv/op/copy_paste_uv_object.py | 252 ++++++ uv_magic_uv/op/copy_paste_uv_uvedit.py | 144 ++++ .../flip_rotate_uv.py} | 9 +- .../{muv_mirroruv_ops.py => op/mirror_uv.py} | 9 +- .../{muv_mvuv_ops.py => op/move_uv.py} | 13 +- .../{muv_packuv_ops.py => op/pack_uv.py} | 119 +-- .../preserve_uv_aspect.py} | 28 +- uv_magic_uv/op/smooth_uv.py | 215 +++++ .../texture_lock.py} | 32 +- .../texture_projection.py} | 67 +- uv_magic_uv/op/texture_wrap.py | 212 +++++ .../{muv_transuv_ops.py => op/transfer_uv.py} | 17 +- .../unwrap_constraint.py} | 16 +- .../uv_bounding_box.py} | 56 +- uv_magic_uv/op/uv_inspection.py | 623 ++++++++++++++ uv_magic_uv/op/uv_sculpt.py | 355 ++++++++ uv_magic_uv/{muv_uvw_ops.py => op/uvw.py} | 43 +- .../{muv_wsuv_ops.py => op/world_scale_uv.py} | 130 ++- uv_magic_uv/preferences.py | 216 +++++ uv_magic_uv/properites.py | 755 +++++++++++++++++ uv_magic_uv/ui/__init__.py | 44 + uv_magic_uv/ui/uvedit_copy_paste_uv.py | 54 ++ uv_magic_uv/ui/uvedit_editor_enhance.py | 136 +++ uv_magic_uv/ui/uvedit_uv_manipulation.py | 117 +++ .../ui/view3d_copy_paste_uv_editmode.py | 81 ++ .../ui/view3d_copy_paste_uv_objectmode.py | 56 ++ uv_magic_uv/ui/view3d_uv_manipulation.py | 180 ++++ uv_magic_uv/ui/view3d_uv_mapping.py | 99 +++ 39 files changed, 5629 insertions(+), 1381 deletions(-) create mode 100644 uv_magic_uv/common.py delete mode 100644 uv_magic_uv/muv_common.py delete mode 100644 uv_magic_uv/muv_cpuv_selseq_ops.py delete mode 100644 uv_magic_uv/muv_menu.py delete mode 100644 uv_magic_uv/muv_preferences.py delete mode 100644 uv_magic_uv/muv_props.py create mode 100644 uv_magic_uv/op/__init__.py create mode 100644 uv_magic_uv/op/align_uv.py create mode 100644 uv_magic_uv/op/align_uv_cursor.py rename uv_magic_uv/{muv_cpuv_ops.py => op/copy_paste_uv.py} (50%) create mode 100644 uv_magic_uv/op/copy_paste_uv_object.py create mode 100644 uv_magic_uv/op/copy_paste_uv_uvedit.py rename uv_magic_uv/{muv_fliprot_ops.py => op/flip_rotate_uv.py} (97%) rename uv_magic_uv/{muv_mirroruv_ops.py => op/mirror_uv.py} (97%) rename uv_magic_uv/{muv_mvuv_ops.py => op/move_uv.py} (94%) rename uv_magic_uv/{muv_packuv_ops.py => op/pack_uv.py} (69%) rename uv_magic_uv/{muv_preserve_uv_aspect.py => op/preserve_uv_aspect.py} (92%) create mode 100644 uv_magic_uv/op/smooth_uv.py rename uv_magic_uv/{muv_texlock_ops.py => op/texture_lock.py} (96%) rename uv_magic_uv/{muv_texproj_ops.py => op/texture_projection.py} (80%) create mode 100644 uv_magic_uv/op/texture_wrap.py rename uv_magic_uv/{muv_transuv_ops.py => op/transfer_uv.py} (97%) rename uv_magic_uv/{muv_unwrapconst_ops.py => op/unwrap_constraint.py} (94%) rename uv_magic_uv/{muv_uvbb_ops.py => op/uv_bounding_box.py} (94%) create mode 100644 uv_magic_uv/op/uv_inspection.py create mode 100644 uv_magic_uv/op/uv_sculpt.py rename uv_magic_uv/{muv_uvw_ops.py => op/uvw.py} (86%) rename uv_magic_uv/{muv_wsuv_ops.py => op/world_scale_uv.py} (70%) create mode 100644 uv_magic_uv/preferences.py create mode 100644 uv_magic_uv/properites.py create mode 100644 uv_magic_uv/ui/__init__.py create mode 100644 uv_magic_uv/ui/uvedit_copy_paste_uv.py create mode 100644 uv_magic_uv/ui/uvedit_editor_enhance.py create mode 100644 uv_magic_uv/ui/uvedit_uv_manipulation.py create mode 100644 uv_magic_uv/ui/view3d_copy_paste_uv_editmode.py create mode 100644 uv_magic_uv/ui/view3d_copy_paste_uv_objectmode.py create mode 100644 uv_magic_uv/ui/view3d_uv_manipulation.py create mode 100644 uv_magic_uv/ui/view3d_uv_mapping.py diff --git a/uv_magic_uv/__init__.py b/uv_magic_uv/__init__.py index 171a5ac4f..97f8bb791 100644 --- a/uv_magic_uv/__init__.py +++ b/uv_magic_uv/__init__.py @@ -20,18 +20,18 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "4.5" -__date__ = "19 Nov 2017" +__version__ = "5.0" +__date__ = "16 Feb 2018" bl_info = { "name": "Magic UV", - "author": "Nutti, Mifth, Jace Priester, kgeogeo, mem, " + "author": "Nutti, Mifth, Jace Priester, kgeogeo, mem, imdjs" "Keith (Wahooney) Boshoff, McBuff, MaxRobinot, Alexander Milovsky", - "version": (4, 5, 0), + "version": (5, 0, 0), "blender": (2, 79, 0), "location": "See Add-ons Preferences", - "description": "UV Manipulator Tools. See Add-ons Preferences for details", + "description": "UV Toolset. See Add-ons Preferences for details", "warning": "", "support": "COMMUNITY", "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/" @@ -42,98 +42,29 @@ bl_info = { if "bpy" in locals(): import importlib - importlib.reload(muv_preferences) - importlib.reload(muv_menu) - importlib.reload(muv_common) - importlib.reload(muv_props) - importlib.reload(muv_cpuv_ops) - importlib.reload(muv_cpuv_selseq_ops) - importlib.reload(muv_fliprot_ops) - importlib.reload(muv_transuv_ops) - importlib.reload(muv_uvbb_ops) - importlib.reload(muv_mvuv_ops) - importlib.reload(muv_texproj_ops) - importlib.reload(muv_packuv_ops) - importlib.reload(muv_texlock_ops) - importlib.reload(muv_mirroruv_ops) - importlib.reload(muv_wsuv_ops) - importlib.reload(muv_unwrapconst_ops) - importlib.reload(muv_preserve_uv_aspect) - importlib.reload(muv_uvw_ops) + importlib.reload(op) + importlib.reload(ui) + importlib.reload(common) + importlib.reload(preferences) + importlib.reload(properites) 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 - from . import muv_uvw_ops + from . import op + from . import ui + from . import common + from . import preferences + from . import properites import bpy -def view3d_uvmap_menu_fn(self, context): - self.layout.separator() - self.layout.menu(muv_menu.MUV_CPUVMenu.bl_idname, icon="IMAGE_COL") - self.layout.operator( - muv_fliprot_ops.MUV_FlipRot.bl_idname, icon="IMAGE_COL") - self.layout.menu(muv_menu.MUV_TransUVMenu.bl_idname, icon="IMAGE_COL") - self.layout.operator(muv_mvuv_ops.MUV_MVUV.bl_idname, icon="IMAGE_COL") - self.layout.menu(muv_menu.MUV_TexLockMenu.bl_idname, icon="IMAGE_COL") - self.layout.operator( - muv_mirroruv_ops.MUV_MirrorUV.bl_idname, icon="IMAGE_COL") - self.layout.menu(muv_menu.MUV_WSUVMenu.bl_idname, icon="IMAGE_COL") - self.layout.operator( - muv_unwrapconst_ops.MUV_UnwrapConstraint.bl_idname, icon='IMAGE_COL') - self.layout.menu( - muv_preserve_uv_aspect.MUV_PreserveUVAspectMenu.bl_idname, - icon='IMAGE_COL') - self.layout.menu(muv_menu.MUV_UVWMenu.bl_idname, icon="IMAGE_COL") - - -def image_uvs_menu_fn(self, context): - self.layout.separator() - self.layout.operator(muv_packuv_ops.MUV_PackUV.bl_idname, icon="IMAGE_COL") - - -def view3d_object_menu_fn(self, context): - self.layout.separator() - self.layout.menu(muv_menu.MUV_CPUVObjMenu.bl_idname, icon="IMAGE_COL") - - 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) - try: - bpy.types.VIEW3D_MT_Object.append(view3d_object_menu_fn) - except: - pass - muv_props.init_props(bpy.types.Scene) + properites.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) - try: - bpy.types.VIEW3D_MT_Object.remove(view3d_object_menu_fn) - except: - pass - muv_props.clear_props(bpy.types.Scene) + properites.clear_props(bpy.types.Scene) if __name__ == "__main__": diff --git a/uv_magic_uv/common.py b/uv_magic_uv/common.py new file mode 100644 index 000000000..dc8876a05 --- /dev/null +++ b/uv_magic_uv/common.py @@ -0,0 +1,592 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +from collections import defaultdict +from pprint import pprint +from math import fabs, sqrt + +import bpy +from mathutils import Vector +import bmesh + + +DEBUG = False + + +def debug_print(*s): + """ + Print message to console in debugging mode + """ + + if DEBUG: + pprint(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 + if bpy.app.version[1] > minor: + return 1 + 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) + + +def __get_island_info(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)) + ma = Vector((-10000000.0, -10000000.0)) + mi = Vector((10000000.0, 10000000.0)) + for l in face['face'].loops: + uv = l[uv_layer].uv + ma.x = max(uv.x, ma.x) + ma.y = max(uv.y, ma.y) + mi.x = min(uv.x, mi.x) + mi.y = min(uv.y, mi.y) + a = a + uv + n = n + 1 + ave_uv = ave_uv + a + num_uv = num_uv + n + a = a / n + max_uv.x = max(ma.x, max_uv.x) + max_uv.y = max(ma.y, max_uv.y) + min_uv.x = min(mi.x, min_uv.x) + min_uv.y = min(mi.y, min_uv.y) + face['max_uv'] = ma + face['min_uv'] = mi + 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 + info['max'] = max_uv + info['min'] = min_uv + + island_info.append(info) + + return island_info + + +def __parse_island(bm, face_idx, faces_left, island, + face_to_verts, vert_to_faces): + """ + Parse island + """ + + if face_idx in faces_left: + faces_left.remove(face_idx) + island.append({'face': bm.faces[face_idx]}) + for v in face_to_verts[face_idx]: + connected_faces = vert_to_faces[v] + if connected_faces: + for cf in connected_faces: + __parse_island(bm, cf, faces_left, island, face_to_verts, + vert_to_faces) + + +def __get_island(bm, face_to_verts, vert_to_faces): + """ + Get island list + """ + + uv_island_lists = [] + faces_left = set(face_to_verts.keys()) + while faces_left: + current_island = [] + face_idx = list(faces_left)[0] + __parse_island(bm, face_idx, faces_left, current_island, + face_to_verts, vert_to_faces) + uv_island_lists.append(current_island) + + return uv_island_lists + + +def __create_vert_face_db(faces, uv_layer): + # create mesh database for all faces + face_to_verts = defaultdict(set) + vert_to_faces = defaultdict(set) + for f in faces: + for l in f.loops: + id_ = l[uv_layer].uv.to_tuple(5), l.vert.index + face_to_verts[f.index].add(id_) + vert_to_faces[id_].add(f.index) + + return (face_to_verts, vert_to_faces) + + +def get_island_info(obj, only_selected=True): + bm = bmesh.from_edit_mesh(obj.data) + if check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + return get_island_info_from_bmesh(bm, only_selected) + + +def get_island_info_from_bmesh(bm, only_selected=True): + if not bm.loops.layers.uv: + return None + uv_layer = bm.loops.layers.uv.verify() + + # create database + if only_selected: + selected_faces = [f for f in bm.faces if f.select] + else: + selected_faces = [f for f in bm.faces] + + return get_island_info_from_faces(bm, selected_faces, uv_layer) + + +def get_island_info_from_faces(bm, faces, uv_layer): + ftv, vtf = __create_vert_face_db(faces, uv_layer) + + # Get island information + uv_island_lists = __get_island(bm, ftv, vtf) + island_info = __get_island_info(uv_layer, uv_island_lists) + + return island_info + + +def get_uvimg_editor_board_size(area): + if area.spaces.active.image: + return area.spaces.active.image.size + + return (255.0, 255.0) + + +def calc_polygon_2d_area(points): + area = 0.0 + for i, p1 in enumerate(points): + p2 = points[(i + 1) % len(points)] + v1 = p1 - points[0] + v2 = p2 - points[0] + a = v1.x * v2.y - v1.y * v2.x + area = area + a + + return fabs(0.5 * area) + + +def calc_polygon_3d_area(points): + area = 0.0 + for i, p1 in enumerate(points): + p2 = points[(i + 1) % len(points)] + v1 = p1 - points[0] + v2 = p2 - points[0] + cx = v1.y * v2.z - v1.z * v2.y + cy = v1.z * v2.x - v1.x * v2.z + cz = v1.x * v2.y - v1.y * v2.x + a = sqrt(cx * cx + cy * cy + cz * cz) + area = area + a + + return 0.5 * area + + +def measure_mesh_area(obj): + bm = bmesh.from_edit_mesh(obj.data) + if check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + sel_faces = [f for f in bm.faces if f.select] + + # measure + mesh_area = 0.0 + for f in sel_faces: + verts = [l.vert.co for l in f.loops] + f_mesh_area = calc_polygon_3d_area(verts) + mesh_area = mesh_area + f_mesh_area + + return mesh_area + + +def measure_uv_area(obj): + bm = bmesh.from_edit_mesh(obj.data) + if 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: + return None + uv_layer = bm.loops.layers.uv.verify() + + if not bm.faces.layers.tex: + return None + tex_layer = bm.faces.layers.tex.verify() + + sel_faces = [f for f in bm.faces if f.select] + + # measure + uv_area = 0.0 + for f in sel_faces: + uvs = [l[uv_layer].uv for l in f.loops] + f_uv_area = calc_polygon_2d_area(uvs) + if tex_layer: + img = f[tex_layer].image + if not img: + return None + uv_area = uv_area + f_uv_area * img.size[0] * img.size[1] + + return uv_area + + +def diff_point_to_segment(a, b, p): + ab = b - a + normal_ab = ab.normalized() + + ap = p - a + dist_ax = normal_ab.dot(ap) + + # cross point + x = a + normal_ab * dist_ax + + # difference between cross point and point + xp = p - x + + return xp, x + + +# get selected loop pair whose loops are connected each other +def __get_loop_pairs(l, uv_layer): + + def __get_loop_pairs_internal(l_, pairs_, uv_layer_, parsed_): + parsed_.append(l_) + for ll in l_.vert.link_loops: + # forward direction + lln = ll.link_loop_next + # if there is same pair, skip it + found = False + for p in pairs_: + if (ll in p) and (lln in p): + found = True + break + # two loops must be selected + if ll[uv_layer_].select and lln[uv_layer_].select: + if not found: + pairs_.append([ll, lln]) + if lln not in parsed_: + __get_loop_pairs_internal(lln, pairs_, uv_layer_, parsed_) + + # backward direction + llp = ll.link_loop_prev + # if there is same pair, skip it + found = False + for p in pairs_: + if (ll in p) and (llp in p): + found = True + break + # two loops must be selected + if ll[uv_layer_].select and llp[uv_layer_].select: + if not found: + pairs_.append([ll, llp]) + if llp not in parsed_: + __get_loop_pairs_internal(llp, pairs_, uv_layer_, parsed_) + + pairs = [] + parsed = [] + __get_loop_pairs_internal(l, pairs, uv_layer, parsed) + + return pairs + + +# sort pair by vertex +# (v0, v1) - (v1, v2) - (v2, v3) .... +def __sort_loop_pairs(uv_layer, pairs, closed): + rest = pairs + sorted_pairs = [rest[0]] + rest.remove(rest[0]) + + # prepend + while True: + p1 = sorted_pairs[0] + for p2 in rest: + if p1[0].vert == p2[0].vert: + sorted_pairs.insert(0, [p2[1], p2[0]]) + rest.remove(p2) + break + elif p1[0].vert == p2[1].vert: + sorted_pairs.insert(0, [p2[0], p2[1]]) + rest.remove(p2) + break + else: + break + + # append + while True: + p1 = sorted_pairs[-1] + for p2 in rest: + if p1[1].vert == p2[0].vert: + sorted_pairs.append([p2[0], p2[1]]) + rest.remove(p2) + break + elif p1[1].vert == p2[1].vert: + sorted_pairs.append([p2[1], p2[0]]) + rest.remove(p2) + break + else: + break + + begin_vert = sorted_pairs[0][0].vert + end_vert = sorted_pairs[-1][-1].vert + if begin_vert != end_vert: + return sorted_pairs, "" + if closed and (begin_vert == end_vert): + # if the sequence of UV is circular, it is ok + return sorted_pairs, "" + + # if the begin vertex and the end vertex are same, search the UVs which + # are separated each other + tmp_pairs = sorted_pairs + for i, (p1, p2) in enumerate(zip(tmp_pairs[:-1], tmp_pairs[1:])): + diff = p2[0][uv_layer].uv - p1[-1][uv_layer].uv + if diff.length > 0.000000001: + # UVs are separated + sorted_pairs = tmp_pairs[i + 1:] + sorted_pairs.extend(tmp_pairs[:i + 1]) + break + else: + p1 = tmp_pairs[0] + p2 = tmp_pairs[-1] + diff = p2[-1][uv_layer].uv - p1[0][uv_layer].uv + if diff.length < 0.000000001: + # all UVs are not separated + return None, "All UVs are not separted" + + return sorted_pairs, "" + + +# get index of the island group which includes loop +def __get_island_group_include_loop(loop, island_info): + for i, isl in enumerate(island_info): + for f in isl['faces']: + for l in f['face'].loops: + if l == loop: + return i # found + + return -1 # not found + + +# get index of the island group which includes pair. +# if island group is not same between loops, it will be invalid +def __get_island_group_include_pair(pair, island_info): + l1_grp = __get_island_group_include_loop(pair[0], island_info) + if l1_grp == -1: + return -1 # not found + + for p in pair[1:]: + l2_grp = __get_island_group_include_loop(p, island_info) + if (l2_grp == -1) or (l1_grp != l2_grp): + return -1 # not found or invalid + + return l1_grp + + +# x ---- x <- next_loop_pair +# | | +# o ---- o <- pair +def __get_next_loop_pair(pair): + lp = pair[0].link_loop_prev + if lp.vert == pair[1].vert: + lp = pair[0].link_loop_next + if lp.vert == pair[1].vert: + # no loop is found + return None + + ln = pair[1].link_loop_next + if ln.vert == pair[0].vert: + ln = pair[1].link_loop_prev + if ln.vert == pair[0].vert: + # no loop is found + return None + + # tri-face + if lp == ln: + return [lp] + + # quad-face + return [lp, ln] + + +# | ---- | +# % ---- % <- next_poly_loop_pair +# x ---- x <- next_loop_pair +# | | +# o ---- o <- pair +def __get_next_poly_loop_pair(pair): + v1 = pair[0].vert + v2 = pair[1].vert + for l1 in v1.link_loops: + if l1 == pair[0]: + continue + for l2 in v2.link_loops: + if l2 == pair[1]: + continue + if l1.link_loop_next == l2: + return [l1, l2] + elif l1.link_loop_prev == l2: + return [l1, l2] + + # no next poly loop is found + return None + + +# get loop sequence in the same island +def __get_loop_sequence_internal(uv_layer, pairs, island_info, closed): + loop_sequences = [] + for pair in pairs: + seqs = [pair] + p = pair + isl_grp = __get_island_group_include_pair(pair, island_info) + if isl_grp == -1: + return None, "Can not find the island or invalid island" + + while True: + nlp = __get_next_loop_pair(p) + if not nlp: + break # no more loop pair + nlp_isl_grp = __get_island_group_include_pair(nlp, island_info) + if nlp_isl_grp != isl_grp: + break # another island + for nlpl in nlp: + if nlpl[uv_layer].select: + return None, "Do not select UV which does not belong to " \ + "the end edge" + + seqs.append(nlp) + + # when face is triangle, it indicates CLOSED + if (len(nlp) == 1) and closed: + break + + nplp = __get_next_poly_loop_pair(nlp) + if not nplp: + break # no more loop pair + nplp_isl_grp = __get_island_group_include_pair(nplp, island_info) + if nplp_isl_grp != isl_grp: + break # another island + + # check if the UVs are already parsed. + # this check is needed for the mesh which has the circular + # sequence of the verticies + matched = False + for p1 in seqs: + p2 = nplp + if ((p1[0] == p2[0]) and (p1[1] == p2[1])) or \ + ((p1[0] == p2[1]) and (p1[1] == p2[0])): + matched = True + if matched: + debug_print("This is a circular sequence") + break + + for nlpl in nplp: + if nlpl[uv_layer].select: + return None, "Do not select UV which does not belong to " \ + "the end edge" + + seqs.append(nplp) + + p = nplp + + loop_sequences.append(seqs) + return loop_sequences, "" + + +def get_loop_sequences(bm, uv_layer): + sel_faces = [f for f in bm.faces if f.select] + + # get candidate loops + cand_loops = [] + for f in sel_faces: + for l in f.loops: + if l[uv_layer].select: + cand_loops.append(l) + + if len(cand_loops) < 2: + return None, "More than 2 UVs must be selected" + + first_loop = cand_loops[0] + isl_info = get_island_info_from_bmesh(bm, False) + loop_pairs = __get_loop_pairs(first_loop, uv_layer) + loop_pairs, err = __sort_loop_pairs(uv_layer, loop_pairs, False) + if not loop_pairs: + return None, err + loop_seqs, err = __get_loop_sequence_internal(uv_layer, loop_pairs, + isl_info, False) + if not loop_seqs: + return None, err + + return loop_seqs, "" diff --git a/uv_magic_uv/muv_common.py b/uv_magic_uv/muv_common.py deleted file mode 100644 index b52971ec3..000000000 --- a/uv_magic_uv/muv_common.py +++ /dev/null @@ -1,83 +0,0 @@ -# <pep8-80 compliant> - -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -__author__ = "Nutti <nutti.metro@gmail.com>" -__status__ = "production" -__version__ = "4.5" -__date__ = "19 Nov 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 - if bpy.app.version[1] > minor: - return 1 - 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_selseq_ops.py b/uv_magic_uv/muv_cpuv_selseq_ops.py deleted file mode 100644 index 3cf69ff76..000000000 --- a/uv_magic_uv/muv_cpuv_selseq_ops.py +++ /dev/null @@ -1,279 +0,0 @@ -# <pep8-80 compliant> - -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -__author__ = "Nutti <nutti.metro@gmail.com>" -__status__ = "production" -__version__ = "4.5" -__date__ = "19 Nov 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 = [] - props.src_seams = [] - 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] - seams = [l.edge.seam for l in hist.loops] - props.src_uvs.append(uvs) - props.src_pin_uvs.append(pin_uvs) - props.src_seams.append(seams) - if not props.src_uvs or not props.src_pin_uvs: - 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="IMAGE_COL").uv_map = "" - for m in uv_maps: - layout.operator( - MUV_CPUVSelSeqCopyUV.bl_idname, - text=m, icon="IMAGE_COL").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 - ) - copy_seams = BoolProperty( - name="Copy Seams", - description="Copy Seams", - default=True - ) - - def execute(self, context): - props = context.scene.muv_props.cpuv_selseq - if not props.src_uvs or not props.src_pin_uvs: - 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_seams = [] - 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] - seams = [l.edge.seam for l in hist.loops] - dest_uvs.append(uvs) - dest_pin_uvs.append(pin_uvs) - dest_seams.append(seams) - if not dest_uvs or not dest_pin_uvs: - 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 - ss = None - duv = None - if self.strategy == 'N_N': - suv = props.src_uvs[i] - spuv = props.src_pin_uvs[i] - ss = props.src_seams[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)] - ss = props.src_seams[i % len(props.src_seams)] - 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] - ss_fr = [s for s in ss] - # flip UVs - if self.flip_copied_uv is True: - suvs_fr.reverse() - spuvs_fr.reverse() - ss_fr.reverse() - # rotate UVs - for _ in range(self.rotate_copied_uv): - uv = suvs_fr.pop() - pin_uv = spuvs_fr.pop() - s = ss_fr.pop() - suvs_fr.insert(0, uv) - spuvs_fr.insert(0, pin_uv) - ss_fr.insert(0, s) - # paste UVs - for l, suv, spuv, ss in zip(bm.faces[idx].loops, suvs_fr, - spuvs_fr, ss_fr): - l[uv_layer].uv = suv - l[uv_layer].pin_uv = spuv - if self.copy_seams is True: - l.edge.seam = ss - - self.report({'INFO'}, "%d face(s) are copied" % len(dest_uvs)) - - bmesh.update_edit_mesh(obj.data) - if self.copy_seams is True: - obj.data.show_edge_seams = True - - 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="IMAGE_COL").uv_map = "" - for m in uv_maps: - layout.operator( - MUV_CPUVSelSeqPasteUV.bl_idname, - text=m, icon="IMAGE_COL").uv_map = m diff --git a/uv_magic_uv/muv_menu.py b/uv_magic_uv/muv_menu.py deleted file mode 100644 index 47c79bbd1..000000000 --- a/uv_magic_uv/muv_menu.py +++ /dev/null @@ -1,138 +0,0 @@ -# <pep8-80 compliant> - -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -__author__ = "Nutti <nutti.metro@gmail.com>" -__status__ = "production" -__version__ = "4.5" -__date__ = "19 Nov 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 -from . import muv_uvw_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="IMAGE_COL") - self.layout.menu( - muv_cpuv_ops.MUV_CPUVPasteUVMenu.bl_idname, icon="IMAGE_COL") - self.layout.menu( - muv_cpuv_selseq_ops.MUV_CPUVSelSeqCopyUVMenu.bl_idname, - icon="IMAGE_COL") - self.layout.menu( - muv_cpuv_selseq_ops.MUV_CPUVSelSeqPasteUVMenu.bl_idname, - icon="IMAGE_COL") - - -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="IMAGE_COL") - self.layout.menu( - muv_cpuv_ops.MUV_CPUVObjPasteUVMenu.bl_idname, icon="IMAGE_COL") - - -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="IMAGE_COL") - self.layout.operator( - muv_transuv_ops.MUV_TransUVPaste.bl_idname, icon="IMAGE_COL") - - -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="IMAGE_COL") - self.layout.operator( - muv_texlock_ops.MUV_TexLockStop.bl_idname, icon="IMAGE_COL") - self.layout.operator( - muv_texlock_ops.MUV_TexLockIntrStart.bl_idname, icon="IMAGE_COL") - self.layout.operator( - muv_texlock_ops.MUV_TexLockIntrStop.bl_idname, icon="IMAGE_COL") - - -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="IMAGE_COL") - self.layout.operator( - muv_wsuv_ops.MUV_WSUVApply.bl_idname, icon="IMAGE_COL") - - -class MUV_UVWMenu(bpy.types.Menu): - """ - Menu class: Master menu of UVW - """ - - bl_idname = "uv.muv_uvw_menu" - bl_label = "UVW" - bl_description = "" - - def draw(self, _): - self.layout.operator( - muv_uvw_ops.MUV_UVWBoxMap.bl_idname, icon="IMAGE_COL") - self.layout.operator( - muv_uvw_ops.MUV_UVWBestPlanerMap.bl_idname, icon="IMAGE_COL") diff --git a/uv_magic_uv/muv_preferences.py b/uv_magic_uv/muv_preferences.py deleted file mode 100644 index e14ce99bd..000000000 --- a/uv_magic_uv/muv_preferences.py +++ /dev/null @@ -1,144 +0,0 @@ -# <pep8-80 compliant> - -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -__author__ = "Nutti <nutti.metro@gmail.com>" -__status__ = "production" -__version__ = "4.5" -__date__ = "19 Nov 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 are new to 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_props.py b/uv_magic_uv/muv_props.py deleted file mode 100644 index c0a7d961f..000000000 --- a/uv_magic_uv/muv_props.py +++ /dev/null @@ -1,148 +0,0 @@ -# <pep8-80 compliant> - -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -__author__ = "Nutti <nutti.metro@gmail.com>" -__status__ = "production" -__version__ = "4.5" -__date__ = "19 Nov 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 = [] - src_seams = [] - - -class MUV_CPUVSelSeqProps(): - src_uvs = [] - src_pin_uvs = [] - src_seams = [] - - -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/op/__init__.py b/uv_magic_uv/op/__init__.py new file mode 100644 index 000000000..0b93c96ab --- /dev/null +++ b/uv_magic_uv/op/__init__.py @@ -0,0 +1,72 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +if "bpy" in locals(): + import importlib + importlib.reload(align_uv) + importlib.reload(align_uv_cursor) + importlib.reload(copy_paste_uv) + importlib.reload(copy_paste_uv_object) + importlib.reload(copy_paste_uv_uvedit) + importlib.reload(flip_rotate_uv) + importlib.reload(mirror_uv) + importlib.reload(move_uv) + importlib.reload(pack_uv) + importlib.reload(preserve_uv_aspect) + importlib.reload(smooth_uv) + importlib.reload(texture_lock) + importlib.reload(texture_projection) + importlib.reload(texture_wrap) + importlib.reload(transfer_uv) + importlib.reload(unwrap_constraint) + importlib.reload(uv_bounding_box) + importlib.reload(uv_inspection) + importlib.reload(uv_sculpt) + importlib.reload(uvw) + importlib.reload(world_scale_uv) +else: + from . import align_uv + from . import align_uv_cursor + from . import copy_paste_uv + from . import copy_paste_uv_object + from . import copy_paste_uv_uvedit + from . import flip_rotate_uv + from . import mirror_uv + from . import move_uv + from . import pack_uv + from . import preserve_uv_aspect + from . import smooth_uv + from . import texture_lock + from . import texture_projection + from . import texture_wrap + from . import transfer_uv + from . import unwrap_constraint + from . import uv_bounding_box + from . import uv_inspection + from . import uv_sculpt + from . import uvw + from . import world_scale_uv + +import bpy diff --git a/uv_magic_uv/op/align_uv.py b/uv_magic_uv/op/align_uv.py new file mode 100644 index 000000000..f90f02ffa --- /dev/null +++ b/uv_magic_uv/op/align_uv.py @@ -0,0 +1,784 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "imdjs, Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +import math +from math import atan2, tan, sin, cos + +import bpy +import bmesh +from mathutils import Vector +from bpy.props import EnumProperty, BoolProperty + +from .. import common + + +def get_closed_loop_sequences(bm, uv_layer): + sel_faces = [f for f in bm.faces if f.select] + + # get candidate loops + cand_loops = [] + for f in sel_faces: + for l in f.loops: + if l[uv_layer].select: + cand_loops.append(l) + + if len(cand_loops) < 2: + return None, "More than 2 UVs must be selected" + + first_loop = cand_loops[0] + isl_info = common.get_island_info_from_bmesh(bm, False) + loop_pairs = common.get_loop_pairs(first_loop, uv_layer) + loop_pairs, err = common.sort_loop_pairs(uv_layer, loop_pairs, True) + if not loop_pairs: + return None, err + loop_seqs, err = common.get_loop_sequence_internal(uv_layer, loop_pairs, + isl_info, True) + if not loop_seqs: + return None, err + + return loop_seqs, "" + + +# get sum vertex length of loop sequences +def get_loop_vert_len(loops): + length = 0 + for l1, l2 in zip(loops[:-1], loops[1:]): + diff = l2.vert.co - l1.vert.co + length = length + abs(diff.length) + + return length + + +# get sum uv length of loop sequences +def get_loop_uv_len(loops, uv_layer): + length = 0 + for l1, l2 in zip(loops[:-1], loops[1:]): + diff = l2[uv_layer].uv - l1[uv_layer].uv + length = length + abs(diff.length) + + return length + + +# get center/radius of circle by 3 vertices +def get_circle(v): + alpha = atan2((v[0].y - v[1].y), (v[0].x - v[1].x)) + math.pi / 2 + beta = atan2((v[1].y - v[2].y), (v[1].x - v[2].x)) + math.pi / 2 + ex = (v[0].x + v[1].x) / 2.0 + ey = (v[0].y + v[1].y) / 2.0 + fx = (v[1].x + v[2].x) / 2.0 + fy = (v[1].y + v[2].y) / 2.0 + cx = (ey - fy - ex * tan(alpha) + fx * tan(beta)) / \ + (tan(beta) - tan(alpha)) + cy = ey - (ex - cx) * tan(alpha) + center = Vector((cx, cy)) + + r = v[0] - center + radian = r.length + + return center, radian + + +# get position on circle with same arc length +def calc_v_on_circle(v, center, radius): + base = v[0] + theta = atan2(base.y - center.y, base.x - center.x) + new_v = [] + for i in range(len(v)): + angle = theta + i * 2 * math.pi / len(v) + new_v.append(Vector((center.x + radius * sin(angle), + center.y + radius * cos(angle)))) + + return new_v + + +class MUV_AUVCircle(bpy.types.Operator): + + bl_idname = "uv.muv_auv_circle" + bl_label = "Circle" + bl_description = "Align UV coordinates to Circle" + bl_options = {'REGISTER', 'UNDO'} + + transmission = BoolProperty( + name="Transmission", + description="Align linked UVs", + default=False + ) + select = BoolProperty( + name="Select", + description="Select UVs which are aligned", + default=False + ) + + @classmethod + def poll(cls, context): + return context.mode == 'EDIT_MESH' + + def execute(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.verify() + + # loop_seqs[horizontal][vertical][loop] + loop_seqs, error = get_closed_loop_sequences(bm, uv_layer) + if not loop_seqs: + self.report({'WARNING'}, error) + return {'CANCELLED'} + + # get circle and new UVs + uvs = [hseq[0][0][uv_layer].uv.copy() for hseq in loop_seqs] + c, r = get_circle(uvs[0:3]) + new_uvs = calc_v_on_circle(uvs, c, r) + + # check center UV of circle + center = loop_seqs[0][-1][0].vert + for hseq in loop_seqs[1:]: + if len(hseq[-1]) != 1: + self.report({'WARNING'}, "Last face must be triangle") + return {'CANCELLED'} + if hseq[-1][0].vert != center: + self.report({'WARNING'}, "Center must be identical") + return {'CANCELLED'} + + # align to circle + if self.transmission: + for hidx, hseq in enumerate(loop_seqs): + for vidx, pair in enumerate(hseq): + all_ = int((len(hseq) + 1) / 2) + r = (all_ - int((vidx + 1) / 2)) / all_ + pair[0][uv_layer].uv = c + (new_uvs[hidx] - c) * r + if self.select: + pair[0][uv_layer].select = True + + if len(pair) < 2: + continue + # for quad polygon + next_hidx = (hidx + 1) % len(loop_seqs) + pair[1][uv_layer].uv = c + ((new_uvs[next_hidx]) - c) * r + if self.select: + pair[1][uv_layer].select = True + else: + for hidx, hseq in enumerate(loop_seqs): + pair = hseq[0] + pair[0][uv_layer].uv = new_uvs[hidx] + pair[1][uv_layer].uv = new_uvs[(hidx + 1) % len(loop_seqs)] + if self.select: + pair[0][uv_layer].select = True + pair[1][uv_layer].select = True + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} + + +# get horizontal differential of UV influenced by mesh vertex +def get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pair_idx): + common.debug_print( + "vidx={0}, hidx={1}, pair_idx={2}".format(vidx, hidx, pair_idx)) + + # get total vertex length + hloops = [] + for s in loop_seqs: + hloops.extend([s[vidx][0], s[vidx][1]]) + vert_total_hlen = get_loop_vert_len(hloops) + common.debug_print(vert_total_hlen) + + # target vertex length + hloops = [] + for s in loop_seqs[:hidx]: + hloops.extend([s[vidx][0], s[vidx][1]]) + for pidx, l in enumerate(loop_seqs[hidx][vidx]): + if pidx > pair_idx: + break + hloops.append(l) + vert_hlen = get_loop_vert_len(hloops) + common.debug_print(vert_hlen) + + # get total UV length + # uv_all_hdiff = loop_seqs[-1][0][-1][uv_layer].uv - + # loop_seqs[0][0][0][uv_layer].uv + uv_total_hlen = loop_seqs[-1][vidx][-1][uv_layer].uv -\ + loop_seqs[0][vidx][0][uv_layer].uv + common.debug_print(uv_total_hlen) + + return uv_total_hlen * vert_hlen / vert_total_hlen + + +# get vertical differential of UV influenced by mesh vertex +def get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pair_idx): + common.debug_print( + "vidx={0}, hidx={1}, pair_idx={2}".format(vidx, hidx, pair_idx)) + + # get total vertex length + hloops = [] + for s in loop_seqs[hidx]: + hloops.append(s[pair_idx]) + vert_total_hlen = get_loop_vert_len(hloops) + common.debug_print(vert_total_hlen) + + # target vertex length + hloops = [] + for s in loop_seqs[hidx][:vidx + 1]: + hloops.append(s[pair_idx]) + vert_hlen = get_loop_vert_len(hloops) + common.debug_print(vert_hlen) + + # get total UV length + # uv_all_hdiff = loop_seqs[0][-1][pair_idx][uv_layer].uv - \ + # loop_seqs[0][0][pair_idx][uv_layer].uv + uv_total_hlen = loop_seqs[hidx][-1][pair_idx][uv_layer].uv -\ + loop_seqs[hidx][0][pair_idx][uv_layer].uv + common.debug_print(uv_total_hlen) + + return uv_total_hlen * vert_hlen / vert_total_hlen + + +# get horizontal differential of UV no influenced +def get_hdiff_uv(uv_layer, loop_seqs, hidx): + base_uv = loop_seqs[0][0][0][uv_layer].uv.copy() + h_uv = loop_seqs[-1][0][1][uv_layer].uv.copy() - base_uv + + return hidx * h_uv / len(loop_seqs) + + +# get vertical differential of UV no influenced +def get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx): + base_uv = loop_seqs[0][0][0][uv_layer].uv.copy() + v_uv = loop_seqs[0][-1][0][uv_layer].uv.copy() - base_uv + + hseq = loop_seqs[hidx] + return int((vidx + 1) / 2) * v_uv / (len(hseq) / 2) + + +class MUV_AUVStraighten(bpy.types.Operator): + + bl_idname = "uv.muv_auv_straighten" + bl_label = "Straighten" + bl_description = "Straighten UV coordinates" + bl_options = {'REGISTER', 'UNDO'} + + transmission = BoolProperty( + name="Transmission", + description="Align linked UVs", + default=False + ) + select = BoolProperty( + name="Select", + description="Select UVs which are aligned", + default=False + ) + vertical = BoolProperty( + name="Vert-Infl (Vertical)", + description="Align vertical direction influenced " + "by mesh vertex proportion", + default=False + ) + horizontal = BoolProperty( + name="Vert-Infl (Horizontal)", + description="Align horizontal direction influenced " + "by mesh vertex proportion", + default=False + ) + + @classmethod + def poll(cls, context): + return context.mode == 'EDIT_MESH' + + # selected and paralleled UV loop sequence will be aligned + def __align_w_transmission(self, loop_seqs, uv_layer): + base_uv = loop_seqs[0][0][0][uv_layer].uv.copy() + + # calculate diff UVs + diff_uvs = [] + # hseq[vertical][loop] + for hidx, hseq in enumerate(loop_seqs): + # pair[loop] + diffs = [] + for vidx in range(0, len(hseq), 2): + if self.horizontal: + hdiff_uvs = [ + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 0), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 1), + ] + else: + hdiff_uvs = [ + get_hdiff_uv(uv_layer, loop_seqs, hidx), + get_hdiff_uv(uv_layer, loop_seqs, hidx + 1), + get_hdiff_uv(uv_layer, loop_seqs, hidx), + get_hdiff_uv(uv_layer, loop_seqs, hidx + 1) + ] + if self.vertical: + vdiff_uvs = [ + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 0), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 1), + ] + else: + vdiff_uvs = [ + get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx), + get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx), + get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx), + get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx) + ] + diffs.append([hdiff_uvs, vdiff_uvs]) + diff_uvs.append(diffs) + + # update UV + for hseq, diffs in zip(loop_seqs, diff_uvs): + for vidx in range(0, len(hseq), 2): + loops = [ + hseq[vidx][0], hseq[vidx][1], + hseq[vidx + 1][0], hseq[vidx + 1][1] + ] + for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0], + diffs[int(vidx / 2)][1]): + l[uv_layer].uv = base_uv + hdiff + vdiff + if self.select: + l[uv_layer].select = True + + # only selected UV loop sequence will be aligned + def __align_wo_transmission(self, loop_seqs, uv_layer): + base_uv = loop_seqs[0][0][0][uv_layer].uv.copy() + + h_uv = loop_seqs[-1][0][1][uv_layer].uv.copy() - base_uv + for hidx, hseq in enumerate(loop_seqs): + # only selected loop pair is targeted + pair = hseq[0] + hdiff_uv_0 = hidx * h_uv / len(loop_seqs) + hdiff_uv_1 = (hidx + 1) * h_uv / len(loop_seqs) + pair[0][uv_layer].uv = base_uv + hdiff_uv_0 + pair[1][uv_layer].uv = base_uv + hdiff_uv_1 + if self.select: + pair[0][uv_layer].select = True + pair[1][uv_layer].select = True + + def __align(self, loop_seqs, uv_layer): + if self.transmission: + self.__align_w_transmission(loop_seqs, uv_layer) + else: + self.__align_wo_transmission(loop_seqs, uv_layer) + + def execute(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.verify() + + # loop_seqs[horizontal][vertical][loop] + loop_seqs, error = common.get_loop_sequences(bm, uv_layer) + if not loop_seqs: + self.report({'WARNING'}, error) + return {'CANCELLED'} + + # align + self.__align(loop_seqs, uv_layer) + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} + + +class MUV_AUVAxis(bpy.types.Operator): + + bl_idname = "uv.muv_auv_axis" + bl_label = "XY-Axis" + bl_description = "Align UV to XY-axis" + bl_options = {'REGISTER', 'UNDO'} + + transmission = BoolProperty( + name="Transmission", + description="Align linked UVs", + default=False + ) + select = BoolProperty( + name="Select", + description="Select UVs which are aligned", + default=False + ) + vertical = BoolProperty( + name="Vert-Infl (Vertical)", + description="Align vertical direction influenced " + "by mesh vertex proportion", + default=False + ) + horizontal = BoolProperty( + name="Vert-Infl (Horizontal)", + description="Align horizontal direction influenced " + "by mesh vertex proportion", + default=False + ) + location = EnumProperty( + name="Location", + description="Align location", + items=[ + ('LEFT_TOP', "Left/Top", "Align to Left or Top"), + ('MIDDLE', "Middle", "Align to middle"), + ('RIGHT_BOTTOM', "Right/Bottom", "Align to Right or Bottom") + ], + default='MIDDLE' + ) + + @classmethod + def poll(cls, context): + return context.mode == 'EDIT_MESH' + + # get min/max of UV + def __get_uv_max_min(self, loop_seqs, uv_layer): + uv_max = Vector((-1000000.0, -1000000.0)) + uv_min = Vector((1000000.0, 1000000.0)) + for hseq in loop_seqs: + for l in hseq[0]: + uv = l[uv_layer].uv + uv_max.x = max(uv.x, uv_max.x) + uv_max.y = max(uv.y, uv_max.y) + uv_min.x = min(uv.x, uv_min.x) + uv_min.y = min(uv.y, uv_min.y) + + return uv_max, uv_min + + # get UV differentiation when UVs are aligned to X-axis + def __get_x_axis_align_diff_uvs(self, loop_seqs, uv_layer, uv_min, + width, height): + diff_uvs = [] + for hidx, hseq in enumerate(loop_seqs): + pair = hseq[0] + luv0 = pair[0][uv_layer] + luv1 = pair[1][uv_layer] + target_uv0 = Vector((0.0, 0.0)) + target_uv1 = Vector((0.0, 0.0)) + if self.location == 'RIGHT_BOTTOM': + target_uv0.y = target_uv1.y = uv_min.y + elif self.location == 'MIDDLE': + target_uv0.y = target_uv1.y = uv_min.y + height * 0.5 + elif self.location == 'LEFT_TOP': + target_uv0.y = target_uv1.y = uv_min.y + height + if luv0.uv.x < luv1.uv.x: + target_uv0.x = uv_min.x + hidx * width / len(loop_seqs) + target_uv1.x = uv_min.x + (hidx + 1) * width / len(loop_seqs) + else: + target_uv0.x = uv_min.x + (hidx + 1) * width / len(loop_seqs) + target_uv1.x = uv_min.x + hidx * width / len(loop_seqs) + diff_uvs.append([target_uv0 - luv0.uv, target_uv1 - luv1.uv]) + + return diff_uvs + + # get UV differentiation when UVs are aligned to Y-axis + def __get_y_axis_align_diff_uvs(self, loop_seqs, uv_layer, uv_min, + width, height): + diff_uvs = [] + for hidx, hseq in enumerate(loop_seqs): + pair = hseq[0] + luv0 = pair[0][uv_layer] + luv1 = pair[1][uv_layer] + target_uv0 = Vector((0.0, 0.0)) + target_uv1 = Vector((0.0, 0.0)) + if self.location == 'RIGHT_BOTTOM': + target_uv0.x = target_uv1.x = uv_min.x + width + elif self.location == 'MIDDLE': + target_uv0.x = target_uv1.x = uv_min.x + width * 0.5 + elif self.location == 'LEFT_TOP': + target_uv0.x = target_uv1.x = uv_min.x + if luv0.uv.y < luv1.uv.y: + target_uv0.y = uv_min.y + hidx * height / len(loop_seqs) + target_uv1.y = uv_min.y + (hidx + 1) * height / len(loop_seqs) + else: + target_uv0.y = uv_min.y + (hidx + 1) * height / len(loop_seqs) + target_uv1.y = uv_min.y + hidx * height / len(loop_seqs) + diff_uvs.append([target_uv0 - luv0.uv, target_uv1 - luv1.uv]) + + return diff_uvs + + # only selected UV loop sequence will be aligned along to X-axis + def __align_to_x_axis_wo_transmission(self, loop_seqs, uv_layer, + uv_min, width, height): + # reverse if the UV coordinate is not sorted by position + need_revese = loop_seqs[0][0][0][uv_layer].uv.x > \ + loop_seqs[-1][0][0][uv_layer].uv.x + if need_revese: + loop_seqs.reverse() + for hidx, hseq in enumerate(loop_seqs): + for vidx, pair in enumerate(hseq): + tmp = loop_seqs[hidx][vidx][0] + loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1] + loop_seqs[hidx][vidx][1] = tmp + + # get UV differential + diff_uvs = self.__get_x_axis_align_diff_uvs(loop_seqs, uv_layer, + uv_min, width, height) + + # update UV + for hseq, duv in zip(loop_seqs, diff_uvs): + pair = hseq[0] + luv0 = pair[0][uv_layer] + luv1 = pair[1][uv_layer] + luv0.uv = luv0.uv + duv[0] + luv1.uv = luv1.uv + duv[1] + + # only selected UV loop sequence will be aligned along to Y-axis + def __align_to_y_axis_wo_transmission(self, loop_seqs, uv_layer, + uv_min, width, height): + # reverse if the UV coordinate is not sorted by position + need_revese = loop_seqs[0][0][0][uv_layer].uv.y > \ + loop_seqs[-1][0][0][uv_layer].uv.y + if need_revese: + loop_seqs.reverse() + for hidx, hseq in enumerate(loop_seqs): + for vidx, pair in enumerate(hseq): + tmp = loop_seqs[hidx][vidx][0] + loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1] + loop_seqs[hidx][vidx][1] = tmp + + # get UV differential + diff_uvs = self.__get_y_axis_align_diff_uvs(loop_seqs, uv_layer, + uv_min, width, height) + + # update UV + for hseq, duv in zip(loop_seqs, diff_uvs): + pair = hseq[0] + luv0 = pair[0][uv_layer] + luv1 = pair[1][uv_layer] + luv0.uv = luv0.uv + duv[0] + luv1.uv = luv1.uv + duv[1] + + # selected and paralleled UV loop sequence will be aligned along to X-axis + def __align_to_x_axis_w_transmission(self, loop_seqs, uv_layer, + uv_min, width, height): + # reverse if the UV coordinate is not sorted by position + need_revese = loop_seqs[0][0][0][uv_layer].uv.x > \ + loop_seqs[-1][0][0][uv_layer].uv.x + if need_revese: + loop_seqs.reverse() + for hidx, hseq in enumerate(loop_seqs): + for vidx in range(len(hseq)): + tmp = loop_seqs[hidx][vidx][0] + loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1] + loop_seqs[hidx][vidx][1] = tmp + + # get offset UVs when the UVs are aligned to X-axis + align_diff_uvs = self.__get_x_axis_align_diff_uvs(loop_seqs, uv_layer, + uv_min, width, + height) + base_uv = loop_seqs[0][0][0][uv_layer].uv.copy() + offset_uvs = [] + for hseq, aduv in zip(loop_seqs, align_diff_uvs): + luv0 = hseq[0][0][uv_layer] + luv1 = hseq[0][1][uv_layer] + offset_uvs.append([luv0.uv + aduv[0] - base_uv, + luv1.uv + aduv[1] - base_uv]) + + # get UV differential + diff_uvs = [] + # hseq[vertical][loop] + for hidx, hseq in enumerate(loop_seqs): + # pair[loop] + diffs = [] + for vidx in range(0, len(hseq), 2): + if self.horizontal: + hdiff_uvs = [ + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 0), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 1), + ] + hdiff_uvs[0].y = hdiff_uvs[0].y + offset_uvs[hidx][0].y + hdiff_uvs[1].y = hdiff_uvs[1].y + offset_uvs[hidx][1].y + hdiff_uvs[2].y = hdiff_uvs[2].y + offset_uvs[hidx][0].y + hdiff_uvs[3].y = hdiff_uvs[3].y + offset_uvs[hidx][1].y + else: + hdiff_uvs = [ + offset_uvs[hidx][0], + offset_uvs[hidx][1], + offset_uvs[hidx][0], + offset_uvs[hidx][1], + ] + if self.vertical: + vdiff_uvs = [ + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 0), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 1), + ] + else: + vdiff_uvs = [ + get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx), + get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx), + get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx), + get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx) + ] + diffs.append([hdiff_uvs, vdiff_uvs]) + diff_uvs.append(diffs) + + # update UV + for hseq, diffs in zip(loop_seqs, diff_uvs): + for vidx in range(0, len(hseq), 2): + loops = [ + hseq[vidx][0], hseq[vidx][1], + hseq[vidx + 1][0], hseq[vidx + 1][1] + ] + for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0], + diffs[int(vidx / 2)][1]): + l[uv_layer].uv = base_uv + hdiff + vdiff + if self.select: + l[uv_layer].select = True + + # selected and paralleled UV loop sequence will be aligned along to Y-axis + def __align_to_y_axis_w_transmission(self, loop_seqs, uv_layer, + uv_min, width, height): + # reverse if the UV coordinate is not sorted by position + need_revese = loop_seqs[0][0][0][uv_layer].uv.y > \ + loop_seqs[-1][0][-1][uv_layer].uv.y + if need_revese: + loop_seqs.reverse() + for hidx, hseq in enumerate(loop_seqs): + for vidx in range(len(hseq)): + tmp = loop_seqs[hidx][vidx][0] + loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1] + loop_seqs[hidx][vidx][1] = tmp + + # get offset UVs when the UVs are aligned to Y-axis + align_diff_uvs = self.__get_y_axis_align_diff_uvs(loop_seqs, uv_layer, + uv_min, width, + height) + base_uv = loop_seqs[0][0][0][uv_layer].uv.copy() + offset_uvs = [] + for hseq, aduv in zip(loop_seqs, align_diff_uvs): + luv0 = hseq[0][0][uv_layer] + luv1 = hseq[0][1][uv_layer] + offset_uvs.append([luv0.uv + aduv[0] - base_uv, + luv1.uv + aduv[1] - base_uv]) + + # get UV differential + diff_uvs = [] + # hseq[vertical][loop] + for hidx, hseq in enumerate(loop_seqs): + # pair[loop] + diffs = [] + for vidx in range(0, len(hseq), 2): + if self.horizontal: + hdiff_uvs = [ + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 0), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 1), + ] + hdiff_uvs[0].x = hdiff_uvs[0].x + offset_uvs[hidx][0].x + hdiff_uvs[1].x = hdiff_uvs[1].x + offset_uvs[hidx][1].x + hdiff_uvs[2].x = hdiff_uvs[2].x + offset_uvs[hidx][0].x + hdiff_uvs[3].x = hdiff_uvs[3].x + offset_uvs[hidx][1].x + else: + hdiff_uvs = [ + offset_uvs[hidx][0], + offset_uvs[hidx][1], + offset_uvs[hidx][0], + offset_uvs[hidx][1], + ] + if self.vertical: + vdiff_uvs = [ + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 0), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 1), + ] + else: + vdiff_uvs = [ + get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx), + get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx), + get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx), + get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx) + ] + diffs.append([hdiff_uvs, vdiff_uvs]) + diff_uvs.append(diffs) + + # update UV + for hseq, diffs in zip(loop_seqs, diff_uvs): + for vidx in range(0, len(hseq), 2): + loops = [ + hseq[vidx][0], hseq[vidx][1], + hseq[vidx + 1][0], hseq[vidx + 1][1] + ] + for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0], + diffs[int(vidx / 2)][1]): + l[uv_layer].uv = base_uv + hdiff + vdiff + if self.select: + l[uv_layer].select = True + + def __align(self, loop_seqs, uv_layer, uv_min, width, height): + # align along to x-axis + if width > height: + if self.transmission: + self.__align_to_x_axis_w_transmission(loop_seqs, uv_layer, + uv_min, width, height) + else: + self.__align_to_x_axis_wo_transmission(loop_seqs, uv_layer, + uv_min, width, height) + # align along to y-axis + else: + if self.transmission: + self.__align_to_y_axis_w_transmission(loop_seqs, uv_layer, + uv_min, width, height) + else: + self.__align_to_y_axis_wo_transmission(loop_seqs, uv_layer, + uv_min, width, height) + + def execute(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.verify() + + # loop_seqs[horizontal][vertical][loop] + loop_seqs, error = common.get_loop_sequences(bm, uv_layer) + if not loop_seqs: + self.report({'WARNING'}, error) + return {'CANCELLED'} + + # get height and width + uv_max, uv_min = self.__get_uv_max_min(loop_seqs, uv_layer) + width = uv_max.x - uv_min.x + height = uv_max.y - uv_min.y + + self.__align(loop_seqs, uv_layer, uv_min, width, height) + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/uv_magic_uv/op/align_uv_cursor.py b/uv_magic_uv/op/align_uv_cursor.py new file mode 100644 index 000000000..b33dc68ee --- /dev/null +++ b/uv_magic_uv/op/align_uv_cursor.py @@ -0,0 +1,154 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +import bpy +from mathutils import Vector +from bpy.props import EnumProperty +import bmesh + +from .. import common + + +class MUV_AUVCAlignOps(bpy.types.Operator): + + bl_idname = "uv.muv_auvc_align" + bl_label = "Align" + bl_description = "Align cursor to the center of UV island" + bl_options = {'REGISTER', 'UNDO'} + + position = EnumProperty( + items=( + ('CENTER', "Center", "Align to Center"), + ('LEFT_TOP', "Left Top", "Align to Left Top"), + ('LEFT_MIDDLE', "Left Middle", "Align to Left Middle"), + ('LEFT_BOTTOM', "Left Bottom", "Align to Left Bottom"), + ('MIDDLE_TOP', "Middle Top", "Align to Middle Top"), + ('MIDDLE_BOTTOM', "Middle Bottom", "Align to Middle Bottom"), + ('RIGHT_TOP', "Right Top", "Align to Right Top"), + ('RIGHT_MIDDLE', "Right Middle", "Align to Right Middle"), + ('RIGHT_BOTTOM', "Right Bottom", "Align to Right Bottom") + ), + name="Position", + description="Align position", + default='CENTER' + ) + base = EnumProperty( + items=( + ('TEXTURE', "Texture", "Align based on Texture"), + ('UV', "UV", "Align to UV"), + ('UV_SEL', "UV (Selected)", "Align to Selected UV") + ), + name="Base", + description="Align base", + default='TEXTURE' + ) + + def execute(self, context): + area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW', + 'IMAGE_EDITOR') + bd_size = common.get_uvimg_editor_board_size(area) + + if self.base == 'UV': + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if not bm.loops.layers.uv: + return None + uv_layer = bm.loops.layers.uv.verify() + + max_ = Vector((-10000000.0, -10000000.0)) + min_ = Vector((10000000.0, 10000000.0)) + for f in bm.faces: + if not f.select: + continue + for l in f.loops: + uv = l[uv_layer].uv + max_.x = max(max_.x, uv.x) + max_.y = max(max_.y, uv.y) + min_.x = min(min_.x, uv.x) + min_.y = min(min_.y, uv.y) + center = Vector(((max_.x + min_.x) / 2.0, (max_.y + min_.y) / 2.0)) + + elif self.base == 'UV_SEL': + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if not bm.loops.layers.uv: + return None + uv_layer = bm.loops.layers.uv.verify() + + max_ = Vector((-10000000.0, -10000000.0)) + min_ = Vector((10000000.0, 10000000.0)) + for f in bm.faces: + if not f.select: + continue + for l in f.loops: + if not l[uv_layer].select: + continue + uv = l[uv_layer].uv + max_.x = max(max_.x, uv.x) + max_.y = max(max_.y, uv.y) + min_.x = min(min_.x, uv.x) + min_.y = min(min_.y, uv.y) + center = Vector(((max_.x + min_.x) / 2.0, (max_.y + min_.y) / 2.0)) + + elif self.base == 'TEXTURE': + min_ = Vector((0.0, 0.0)) + max_ = Vector((1.0, 1.0)) + center = Vector((0.5, 0.5)) + else: + self.report({'ERROR'}, "Unknown Operation") + + if self.position == 'CENTER': + cx = center.x * bd_size[0] + cy = center.y * bd_size[1] + elif self.position == 'LEFT_TOP': + cx = min_.x * bd_size[0] + cy = max_.y * bd_size[1] + elif self.position == 'LEFT_MIDDLE': + cx = min_.x * bd_size[0] + cy = center.y * bd_size[1] + elif self.position == 'LEFT_BOTTOM': + cx = min_.x * bd_size[0] + cy = min_.y * bd_size[1] + elif self.position == 'MIDDLE_TOP': + cx = center.x * bd_size[0] + cy = max_.y * bd_size[1] + elif self.position == 'MIDDLE_BOTTOM': + cx = center.x * bd_size[0] + cy = min_.y * bd_size[1] + elif self.position == 'RIGHT_TOP': + cx = max_.x * bd_size[0] + cy = max_.y * bd_size[1] + elif self.position == 'RIGHT_MIDDLE': + cx = max_.x * bd_size[0] + cy = center.y * bd_size[1] + elif self.position == 'RIGHT_BOTTOM': + cx = max_.x * bd_size[0] + cy = min_.y * bd_size[1] + else: + self.report({'ERROR'}, "Unknown Operation") + + space.cursor_location = Vector((cx, cy)) + + return {'FINISHED'} diff --git a/uv_magic_uv/muv_cpuv_ops.py b/uv_magic_uv/op/copy_paste_uv.py similarity index 50% rename from uv_magic_uv/muv_cpuv_ops.py rename to uv_magic_uv/op/copy_paste_uv.py index 82f043c62..3aac3f6de 100644 --- a/uv_magic_uv/muv_cpuv_ops.py +++ b/uv_magic_uv/op/copy_paste_uv.py @@ -18,10 +18,13 @@ # # ##### END GPL LICENSE BLOCK ##### -__author__ = "Nutti <nutti.metro@gmail.com>, Jace Priester" +__author__ = "imdjs, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "4.5" -__date__ = "19 Nov 2017" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +import math +from math import atan2, sin, cos import bpy import bmesh @@ -31,16 +34,9 @@ from bpy.props import ( IntProperty, EnumProperty, ) -from . import muv_common - +from mathutils import Vector -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 +from .. import common class MUV_CPUVCopyUV(bpy.types.Operator): @@ -64,7 +60,7 @@ class MUV_CPUVCopyUV(bpy.types.Operator): {'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: + if common.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() # get UV layer @@ -174,7 +170,7 @@ class MUV_CPUVPasteUV(bpy.types.Operator): {'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: + if common.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() # get UV layer @@ -273,46 +269,158 @@ class MUV_CPUVPasteUVMenu(bpy.types.Menu): bl_description = "Paste UV coordinate" def draw(self, context): + sc = context.scene 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="IMAGE_COL").uv_map = "" + ops = layout.operator(MUV_CPUVPasteUV.bl_idname, text="[Default]") + ops.uv_map = "" + ops.copy_seams = sc.muv_cpuv_copy_seams + ops.strategy = sc.muv_cpuv_strategy for m in uv_maps: - layout.operator( - MUV_CPUVPasteUV.bl_idname, - text=m, icon="IMAGE_COL").uv_map = m + ops = layout.operator(MUV_CPUVPasteUV.bl_idname, text=m) + ops.uv_map = m + ops.copy_seams = sc.muv_cpuv_copy_seams + ops.strategy = sc.muv_cpuv_strategy -class MUV_CPUVObjCopyUV(bpy.types.Operator): +class MUV_CPUVIECopyUV(bpy.types.Operator): """ - Operation class: Copy UV coordinate per object + Operation class: Copy UV coordinate on UV/Image Editor """ - bl_idname = "object.muv_cpuv_obj_copy_uv" + bl_idname = "uv.muv_cpuv_ie_copy_uv" bl_label = "Copy UV" - bl_description = "Copy UV coordinate" + bl_description = "Copy UV coordinate (only selected in UV/Image Editor)" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return context.mode == 'EDIT_MESH' + + def execute(self, context): + props = context.scene.muv_props.cpuv + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + uv_layer = bm.loops.layers.uv.verify() + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + for face in bm.faces: + if not face.select: + continue + skip = False + for l in face.loops: + if not l[uv_layer].select: + skip = True + break + if skip: + continue + props.src_uvs.append([l[uv_layer].uv.copy() for l in face.loops]) + + return {'FINISHED'} + + +class MUV_CPUVIEPasteUV(bpy.types.Operator): + """ + Operation class: Paste UV coordinate on UV/Image Editor + """ + + bl_idname = "uv.muv_cpuv_ie_paste_uv" + bl_label = "Paste UV" + bl_description = "Paste UV coordinate (only selected in UV/Image Editor)" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return context.mode == 'EDIT_MESH' + + def execute(self, context): + props = context.scene.muv_props.cpuv + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + uv_layer = bm.loops.layers.uv.verify() + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + dest_uvs = [] + dest_face_indices = [] + for face in bm.faces: + if not face.select: + continue + skip = False + for l in face.loops: + if not l[uv_layer].select: + skip = True + break + if skip: + continue + dest_face_indices.append(face.index) + uvs = [l[uv_layer].uv.copy() for l in face.loops] + dest_uvs.append(uvs) + + for suvs, duvs in zip(props.src_uvs, dest_uvs): + src_diff = suvs[1] - suvs[0] + dest_diff = duvs[1] - duvs[0] + + src_base = suvs[0] + dest_base = duvs[0] + + src_rad = atan2(src_diff.y, src_diff.x) + dest_rad = atan2(dest_diff.y, dest_diff.x) + if src_rad < dest_rad: + radian = dest_rad - src_rad + elif src_rad > dest_rad: + radian = math.pi * 2 - (src_rad - dest_rad) + else: # src_rad == dest_rad + radian = 0.0 + + ratio = dest_diff.length / src_diff.length + break + + for suvs, fidx in zip(props.src_uvs, dest_face_indices): + for l, suv in zip(bm.faces[fidx].loops, suvs): + base = suv - src_base + radian_ref = atan2(base.y, base.x) + radian_fin = (radian + radian_ref) + length = base.length + turn = Vector((length * cos(radian_fin), + length * sin(radian_fin))) + target_uv = Vector((turn.x * ratio, turn.y * ratio)) + \ + dest_base + l[uv_layer].uv = target_uv + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} + + +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'}) - @memorize_view_3d_mode def execute(self, context): - props = context.scene.muv_props.cpuv_obj + props = context.scene.muv_props.cpuv_selseq if self.uv_map == "": - self.report({'INFO'}, "Copy UV coordinate per object") + self.report({'INFO'}, "Copy UV coordinate (selection sequence)") else: self.report( {'INFO'}, - "Copy UV coordinate per object (UV map:%s)" % (self.uv_map)) - bpy.ops.object.mode_set(mode='EDIT') - + "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: + if common.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() # get UV layer @@ -329,171 +437,210 @@ class MUV_CPUVObjCopyUV(bpy.types.Operator): props.src_uvs = [] props.src_pin_uvs = [] props.src_seams = [] - 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] - seams = [l.edge.seam for l in face.loops] - props.src_uvs.append(uvs) - props.src_pin_uvs.append(pin_uvs) - props.src_seams.append(seams) - - self.report({'INFO'}, "%s's UV coordinates are copied" % (obj.name)) + 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] + seams = [l.edge.seam for l in hist.loops] + props.src_uvs.append(uvs) + props.src_pin_uvs.append(pin_uvs) + props.src_seams.append(seams) + if not props.src_uvs or not props.src_pin_uvs: + 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_CPUVObjCopyUVMenu(bpy.types.Menu): +class MUV_CPUVSelSeqCopyUVMenu(bpy.types.Menu): """ - Menu class: Copy UV coordinate per object + Menu class: Copy UV coordinate by selection sequence """ - bl_idname = "object.muv_cpuv_obj_copy_uv_menu" - bl_label = "Copy UV" - bl_description = "Copy UV coordinate per object" + 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, _): + def draw(self, context): layout = self.layout - # create sub menu - uv_maps = bpy.context.active_object.data.uv_textures.keys() + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + uv_maps = bm.loops.layers.uv.keys() layout.operator( - MUV_CPUVObjCopyUV.bl_idname, + MUV_CPUVSelSeqCopyUV.bl_idname, text="[Default]", icon="IMAGE_COL").uv_map = "" for m in uv_maps: layout.operator( - MUV_CPUVObjCopyUV.bl_idname, + MUV_CPUVSelSeqCopyUV.bl_idname, text=m, icon="IMAGE_COL").uv_map = m -class MUV_CPUVObjPasteUV(bpy.types.Operator): +class MUV_CPUVSelSeqPasteUV(bpy.types.Operator): """ - Operation class: Paste UV coordinate per object + Operation class: Paste UV coordinate by selection sequence """ - bl_idname = "object.muv_cpuv_obj_paste_uv" - bl_label = "Paste UV" - bl_description = "Paste UV coordinate" + 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 + ) copy_seams = BoolProperty( name="Copy Seams", description="Copy Seams", default=True ) - @memorize_view_3d_mode def execute(self, context): - props = context.scene.muv_props.cpuv_obj + props = context.scene.muv_props.cpuv_selseq if not props.src_uvs or not props.src_pin_uvs: 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)) - 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() + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if 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: + # get UV layer + if self.uv_map == "": + if not bm.loops.layers.uv: 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_seams = [] - 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] - seams = [l.edge.seam for l in face.loops] + {'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_seams = [] + 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] + seams = [l.edge.seam for l in hist.loops] dest_uvs.append(uvs) dest_pin_uvs.append(pin_uvs) dest_seams.append(seams) - 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'} + if not dest_uvs or not dest_pin_uvs: + 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): + # paste + for i, idx in enumerate(dest_face_indices): + suv = None + spuv = None + ss = None + duv = None + if self.strategy == 'N_N': suv = props.src_uvs[i] spuv = props.src_pin_uvs[i] ss = props.src_seams[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] - ss_fr = [s for s in ss] - # paste UVs - for l, suv, spuv, ss in zip( - bm.faces[idx].loops, suvs_fr, spuvs_fr, ss_fr): - l[uv_layer].uv = suv - l[uv_layer].pin_uv = spuv - if self.copy_seams is True: - l.edge.seam = ss - - bmesh.update_edit_mesh(obj.data) - if self.copy_seams is True: - obj.data.show_edge_seams = True + 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)] + ss = props.src_seams[i % len(props.src_seams)] + 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] + ss_fr = [s for s in ss] + # flip UVs + if self.flip_copied_uv is True: + suvs_fr.reverse() + spuvs_fr.reverse() + ss_fr.reverse() + # rotate UVs + for _ in range(self.rotate_copied_uv): + uv = suvs_fr.pop() + pin_uv = spuvs_fr.pop() + s = ss_fr.pop() + suvs_fr.insert(0, uv) + spuvs_fr.insert(0, pin_uv) + ss_fr.insert(0, s) + # paste UVs + for l, suv, spuv, ss in zip(bm.faces[idx].loops, suvs_fr, + spuvs_fr, ss_fr): + l[uv_layer].uv = suv + l[uv_layer].pin_uv = spuv + if self.copy_seams is True: + l.edge.seam = ss - self.report( - {'INFO'}, "%s's UV coordinates are pasted" % (obj.name)) + self.report({'INFO'}, "%d face(s) are copied" % len(dest_uvs)) + + bmesh.update_edit_mesh(obj.data) + if self.copy_seams is True: + obj.data.show_edge_seams = True return {'FINISHED'} -class MUV_CPUVObjPasteUVMenu(bpy.types.Menu): +class MUV_CPUVSelSeqPasteUVMenu(bpy.types.Menu): """ - Menu class: Paste UV coordinate per object + Menu class: Paste UV coordinate by selection sequence """ - bl_idname = "object.muv_cpuv_obj_paste_uv_menu" - bl_label = "Paste UV" - bl_description = "Paste UV coordinate per object" + 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, _): + def draw(self, context): + sc = context.scene 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="IMAGE_COL").uv_map = "" + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + uv_maps = bm.loops.layers.uv.keys() + ops = layout.operator(MUV_CPUVSelSeqPasteUV.bl_idname, + text="[Default]") + ops.uv_map = "" + ops.copy_seams = sc.muv_cpuv_copy_seams + ops.strategy = sc.muv_cpuv_strategy for m in uv_maps: - layout.operator( - MUV_CPUVObjPasteUV.bl_idname, - text=m, icon="IMAGE_COL").uv_map = m + ops = layout.operator(MUV_CPUVSelSeqPasteUV.bl_idname, text=m) + ops.uv_map = m + ops.copy_seams = sc.muv_cpuv_copy_seams + ops.strategy = sc.muv_cpuv_strategy diff --git a/uv_magic_uv/op/copy_paste_uv_object.py b/uv_magic_uv/op/copy_paste_uv_object.py new file mode 100644 index 000000000..eb42d99ae --- /dev/null +++ b/uv_magic_uv/op/copy_paste_uv_object.py @@ -0,0 +1,252 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +import bpy +import bmesh +from bpy.props import ( + StringProperty, + BoolProperty, +) + +from .. import 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_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 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 = [] + props.src_seams = [] + 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] + seams = [l.edge.seam for l in face.loops] + props.src_uvs.append(uvs) + props.src_pin_uvs.append(pin_uvs) + props.src_seams.append(seams) + + 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]")\ + .uv_map = "" + for m in uv_maps: + layout.operator(MUV_CPUVObjCopyUV.bl_idname, text=m).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'}) + copy_seams = BoolProperty( + name="Copy Seams", + description="Copy Seams", + default=True + ) + + @memorize_view_3d_mode + def execute(self, context): + props = context.scene.muv_props.cpuv_obj + if not props.src_uvs or not props.src_pin_uvs: + 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 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_seams = [] + 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] + seams = [l.edge.seam for l in face.loops] + dest_uvs.append(uvs) + dest_pin_uvs.append(pin_uvs) + dest_seams.append(seams) + 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] + ss = props.src_seams[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] + ss_fr = [s for s in ss] + # paste UVs + for l, suv, spuv, ss in zip( + bm.faces[idx].loops, suvs_fr, spuvs_fr, ss_fr): + l[uv_layer].uv = suv + l[uv_layer].pin_uv = spuv + if self.copy_seams is True: + l.edge.seam = ss + + bmesh.update_edit_mesh(obj.data) + if self.copy_seams is True: + obj.data.show_edge_seams = True + + 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, context): + sc = context.scene + 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)) + ops = layout.operator(MUV_CPUVObjPasteUV.bl_idname, text="[Default]") + ops.uv_map = "" + ops.copy_seams = sc.muv_cpuv_copy_seams + for m in uv_maps: + ops = layout.operator(MUV_CPUVObjPasteUV.bl_idname, text=m) + ops.uv_map = m + ops.copy_seams = sc.muv_cpuv_copy_seams diff --git a/uv_magic_uv/op/copy_paste_uv_uvedit.py b/uv_magic_uv/op/copy_paste_uv_uvedit.py new file mode 100644 index 000000000..5b64505ea --- /dev/null +++ b/uv_magic_uv/op/copy_paste_uv_uvedit.py @@ -0,0 +1,144 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>, Jace Priester" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +import math +from math import atan2, sin, cos + +import bpy +import bmesh +from mathutils import Vector + +from .. import common + + +class MUV_CPUVIECopyUV(bpy.types.Operator): + """ + Operation class: Copy UV coordinate on UV/Image Editor + """ + + bl_idname = "uv.muv_cpuv_ie_copy_uv" + bl_label = "Copy UV" + bl_description = "Copy UV coordinate (only selected in UV/Image Editor)" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return context.mode == 'EDIT_MESH' + + def execute(self, context): + props = context.scene.muv_props.cpuv + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + uv_layer = bm.loops.layers.uv.verify() + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + for face in bm.faces: + if not face.select: + continue + skip = False + for l in face.loops: + if not l[uv_layer].select: + skip = True + break + if skip: + continue + props.src_uvs.append([l[uv_layer].uv.copy() for l in face.loops]) + + return {'FINISHED'} + + +class MUV_CPUVIEPasteUV(bpy.types.Operator): + """ + Operation class: Paste UV coordinate on UV/Image Editor + """ + + bl_idname = "uv.muv_cpuv_ie_paste_uv" + bl_label = "Paste UV" + bl_description = "Paste UV coordinate (only selected in UV/Image Editor)" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + return context.mode == 'EDIT_MESH' + + def execute(self, context): + props = context.scene.muv_props.cpuv + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + uv_layer = bm.loops.layers.uv.verify() + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + dest_uvs = [] + dest_face_indices = [] + for face in bm.faces: + if not face.select: + continue + skip = False + for l in face.loops: + if not l[uv_layer].select: + skip = True + break + if skip: + continue + dest_face_indices.append(face.index) + uvs = [l[uv_layer].uv.copy() for l in face.loops] + dest_uvs.append(uvs) + + for suvs, duvs in zip(props.src_uvs, dest_uvs): + src_diff = suvs[1] - suvs[0] + dest_diff = duvs[1] - duvs[0] + + src_base = suvs[0] + dest_base = duvs[0] + + src_rad = atan2(src_diff.y, src_diff.x) + dest_rad = atan2(dest_diff.y, dest_diff.x) + if src_rad < dest_rad: + radian = dest_rad - src_rad + elif src_rad > dest_rad: + radian = math.pi * 2 - (src_rad - dest_rad) + else: # src_rad == dest_rad + radian = 0.0 + + ratio = dest_diff.length / src_diff.length + break + + for suvs, fidx in zip(props.src_uvs, dest_face_indices): + for l, suv in zip(bm.faces[fidx].loops, suvs): + base = suv - src_base + radian_ref = atan2(base.y, base.x) + radian_fin = (radian + radian_ref) + length = base.length + turn = Vector((length * cos(radian_fin), + length * sin(radian_fin))) + target_uv = Vector((turn.x * ratio, turn.y * ratio)) + \ + dest_base + l[uv_layer].uv = target_uv + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/uv_magic_uv/muv_fliprot_ops.py b/uv_magic_uv/op/flip_rotate_uv.py similarity index 97% rename from uv_magic_uv/muv_fliprot_ops.py rename to uv_magic_uv/op/flip_rotate_uv.py index 334eb14c5..907c77c48 100644 --- a/uv_magic_uv/muv_fliprot_ops.py +++ b/uv_magic_uv/op/flip_rotate_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "4.5" -__date__ = "19 Nov 2017" +__version__ = "5.0" +__date__ = "16 Feb 2018" import bpy import bmesh @@ -29,7 +29,8 @@ from bpy.props import ( BoolProperty, IntProperty, ) -from . import muv_common + +from .. import common class MUV_FlipRot(bpy.types.Operator): @@ -63,7 +64,7 @@ class MUV_FlipRot(bpy.types.Operator): 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: + if common.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() # get UV layer diff --git a/uv_magic_uv/muv_mirroruv_ops.py b/uv_magic_uv/op/mirror_uv.py similarity index 97% rename from uv_magic_uv/muv_mirroruv_ops.py rename to uv_magic_uv/op/mirror_uv.py index 63eb9bd54..d1014c734 100644 --- a/uv_magic_uv/muv_mirroruv_ops.py +++ b/uv_magic_uv/op/mirror_uv.py @@ -20,8 +20,8 @@ __author__ = "Keith (Wahooney) Boshoff, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "4.5" -__date__ = "19 Nov 2017" +__version__ = "5.0" +__date__ = "16 Feb 2018" import bpy from bpy.props import ( @@ -30,7 +30,8 @@ from bpy.props import ( ) import bmesh from mathutils import Vector -from . import muv_common + +from .. import common class MUV_MirrorUV(bpy.types.Operator): @@ -113,7 +114,7 @@ class MUV_MirrorUV(bpy.types.Operator): error = self.error axis = self.axis - if muv_common.check_version(2, 73, 0) >= 0: + if 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") diff --git a/uv_magic_uv/muv_mvuv_ops.py b/uv_magic_uv/op/move_uv.py similarity index 94% rename from uv_magic_uv/muv_mvuv_ops.py rename to uv_magic_uv/op/move_uv.py index 28346270d..e0ac418f7 100644 --- a/uv_magic_uv/muv_mvuv_ops.py +++ b/uv_magic_uv/op/move_uv.py @@ -20,8 +20,8 @@ __author__ = "kgeogeo, mem, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "4.5" -__date__ = "19 Nov 2017" +__version__ = "5.0" +__date__ = "16 Feb 2018" import bpy import bmesh @@ -64,6 +64,7 @@ class MUV_MVUV(bpy.types.Operator): return context.edit_object def modal(self, context, event): + props = context.scene.muv_props.mvuv if self.__first_time is True: self.__prev_mouse = Vector(( event.mouse_region_x, event.mouse_region_y)) @@ -84,7 +85,7 @@ class MUV_MVUV(bpy.types.Operator): event.mouse_region_x, event.mouse_region_y)) # check if operation is started - if self.__running is True: + if self.__running: if event.type == 'LEFTMOUSE' and event.value == 'RELEASE': self.__running = False return {'RUNNING_MODAL'} @@ -110,16 +111,20 @@ class MUV_MVUV(bpy.types.Operator): 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 + props.running = False return {'FINISHED'} # confirmed if event.type == confirm_btn and event.value == 'PRESS': + props.running = False return {'FINISHED'} return {'RUNNING_MODAL'} def execute(self, context): - self.__first_time = True + props = context.scene.muv_props.mvuv + props.running = True self.__running = True + self.__first_time = 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/op/pack_uv.py similarity index 69% rename from uv_magic_uv/muv_packuv_ops.py rename to uv_magic_uv/op/pack_uv.py index f663e6627..692fa93e6 100644 --- a/uv_magic_uv/muv_packuv_ops.py +++ b/uv_magic_uv/op/pack_uv.py @@ -20,11 +20,10 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "4.5" -__date__ = "19 Nov 2017" +__version__ = "5.0" +__date__ = "16 Feb 2018" from math import fabs -from collections import defaultdict import bpy import bmesh @@ -36,7 +35,7 @@ from bpy.props import ( ) from mathutils import Vector -from . import muv_common +from .. import common class MUV_PackUV(bpy.types.Operator): @@ -69,23 +68,21 @@ class MUV_PackUV(bpy.types.Operator): min=0.000001, max=0.1, default=(0.001, 0.001), - size=2) + 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) + 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 + def execute(self, context): + obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) - if muv_common.check_version(2, 73, 0) >= 0: + if 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") @@ -93,17 +90,7 @@ class MUV_PackUV(bpy.types.Operator): 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) + island_info = common.get_island_info(obj) num_group = self.__group_island(island_info) loop_lists = [l for f in bm.faces for l in f.loops] @@ -183,13 +170,17 @@ class MUV_PackUV(bpy.types.Operator): 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]) + fabs(dcx) < self.allowable_center_deviation[0] + ) center_y_matched = ( - fabs(dcy) < self.allowable_center_deviation[1]) + fabs(dcy) < self.allowable_center_deviation[1] + ) size_x_matched = ( - fabs(dsx) < self.allowable_size_deviation[0]) + fabs(dsx) < self.allowable_size_deviation[0] + ) size_y_matched = ( - fabs(dsy) < self.allowable_size_deviation[1]) + 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']) @@ -214,75 +205,3 @@ class MUV_PackUV(bpy.types.Operator): 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 faces_left: - 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_preserve_uv_aspect.py b/uv_magic_uv/op/preserve_uv_aspect.py similarity index 92% rename from uv_magic_uv/muv_preserve_uv_aspect.py rename to uv_magic_uv/op/preserve_uv_aspect.py index 68e75f74a..9838aec60 100644 --- a/uv_magic_uv/muv_preserve_uv_aspect.py +++ b/uv_magic_uv/op/preserve_uv_aspect.py @@ -20,14 +20,15 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "4.5" -__date__ = "19 Nov 2017" +__version__ = "5.0" +__date__ = "16 Feb 2018" import bpy import bmesh from bpy.props import StringProperty, EnumProperty from mathutils import Vector -from . import muv_common + +from .. import common class MUV_PreserveUVAspect(bpy.types.Operator): @@ -71,7 +72,7 @@ class MUV_PreserveUVAspect(bpy.types.Operator): obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) - if muv_common.check_version(2, 73, 0) >= 0: + if common.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() if not bm.loops.layers.uv: @@ -202,22 +203,3 @@ class MUV_PreserveUVAspect(bpy.types.Operator): 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="IMAGE_COL").dest_img_name = key diff --git a/uv_magic_uv/op/smooth_uv.py b/uv_magic_uv/op/smooth_uv.py new file mode 100644 index 000000000..6a120d087 --- /dev/null +++ b/uv_magic_uv/op/smooth_uv.py @@ -0,0 +1,215 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "imdjs, Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +import bpy +import bmesh +from bpy.props import BoolProperty, FloatProperty + +from .. import common + + +class MUV_AUVSmooth(bpy.types.Operator): + + bl_idname = "uv.muv_auv_smooth" + bl_label = "Smooth" + bl_description = "Smooth UV coordinates" + bl_options = {'REGISTER', 'UNDO'} + + transmission = BoolProperty( + name="Transmission", + description="Smooth linked UVs", + default=False + ) + mesh_infl = FloatProperty( + name="Mesh Influence", + description="Influence rate of mesh vertex", + min=0.0, + max=1.0, + default=0.0 + ) + select = BoolProperty( + name="Select", + description="Select UVs which are smoothed", + default=False + ) + + @classmethod + def poll(cls, context): + return context.mode == 'EDIT_MESH' + + def __smooth_wo_transmission(self, loop_seqs, uv_layer): + # calculate path length + loops = [] + for hseq in loop_seqs: + loops.extend([hseq[0][0], hseq[0][1]]) + full_vlen = 0 + accm_vlens = [0.0] + full_uvlen = 0 + accm_uvlens = [0.0] + orig_uvs = [loop_seqs[0][0][0][uv_layer].uv.copy()] + for l1, l2 in zip(loops[:-1], loops[1:]): + diff_v = l2.vert.co - l1.vert.co + full_vlen = full_vlen + diff_v.length + accm_vlens.append(full_vlen) + diff_uv = l2[uv_layer].uv - l1[uv_layer].uv + full_uvlen = full_uvlen + diff_uv.length + accm_uvlens.append(full_uvlen) + orig_uvs.append(l2[uv_layer].uv.copy()) + + for hidx, hseq in enumerate(loop_seqs): + pair = hseq[0] + for pidx, l in enumerate(pair): + if self.select: + l[uv_layer].select = True + + # ignore start/end loop + if (hidx == 0 and pidx == 0) or\ + ((hidx == len(loop_seqs) - 1) and (pidx == len(pair) - 1)): + continue + + # calculate target path length + # target = no influenced * (1 - infl) + influenced * infl + tgt_noinfl = full_uvlen * (hidx + pidx) / (len(loop_seqs)) + tgt_infl = full_uvlen * accm_vlens[hidx * 2 + pidx] / full_vlen + target_length = tgt_noinfl * (1 - self.mesh_infl) + \ + tgt_infl * self.mesh_infl + + # get target UV + for i in range(len(accm_uvlens[:-1])): + # get line segment which UV will be placed + if ((accm_uvlens[i] <= target_length) and + (accm_uvlens[i + 1] > target_length)): + tgt_seg_len = target_length - accm_uvlens[i] + seg_len = accm_uvlens[i + 1] - accm_uvlens[i] + uv1 = orig_uvs[i] + uv2 = orig_uvs[i + 1] + target_uv = uv1 + (uv2 - uv1) * tgt_seg_len / seg_len + break + else: + self.report({'ERROR'}, "Failed to get target UV") + return {'CANCELLED'} + + # update UV + l[uv_layer].uv = target_uv + + def __smooth_w_transmission(self, loop_seqs, uv_layer): + # calculate path length + loops = [] + for vidx in range(len(loop_seqs[0])): + ls = [] + for hseq in loop_seqs: + ls.extend(hseq[vidx]) + loops.append(ls) + + orig_uvs = [] + accm_vlens = [] + full_vlens = [] + accm_uvlens = [] + full_uvlens = [] + for ls in loops: + full_v = 0.0 + accm_v = [0.0] + full_uv = 0.0 + accm_uv = [0.0] + uvs = [ls[0][uv_layer].uv.copy()] + for l1, l2 in zip(ls[:-1], ls[1:]): + diff_v = l2.vert.co - l1.vert.co + full_v = full_v + diff_v.length + accm_v.append(full_v) + diff_uv = l2[uv_layer].uv - l1[uv_layer].uv + full_uv = full_uv + diff_uv.length + accm_uv.append(full_uv) + uvs.append(l2[uv_layer].uv.copy()) + accm_vlens.append(accm_v) + full_vlens.append(full_v) + accm_uvlens.append(accm_uv) + full_uvlens.append(full_uv) + orig_uvs.append(uvs) + + for hidx, hseq in enumerate(loop_seqs): + for vidx, (pair, uvs, accm_v, full_v, accm_uv, full_uv)\ + in enumerate(zip(hseq, orig_uvs, accm_vlens, full_vlens, + accm_uvlens, full_uvlens)): + for pidx, l in enumerate(pair): + if self.select: + l[uv_layer].select = True + + # ignore start/end loop + if hidx == 0 and pidx == 0: + continue + if hidx == len(loop_seqs) - 1 and pidx == len(pair) - 1: + continue + + # calculate target path length + # target = no influenced * (1 - infl) + influenced * infl + tgt_noinfl = full_uv * (hidx + pidx) / (len(loop_seqs)) + tgt_infl = full_uv * accm_v[hidx * 2 + pidx] / full_v + target_length = tgt_noinfl * (1 - self.mesh_infl) + \ + tgt_infl * self.mesh_infl + + # get target UV + for i in range(len(accm_uv[:-1])): + # get line segment to be placed + if ((accm_uv[i] <= target_length) and + (accm_uv[i + 1] > target_length)): + tgt_seg_len = target_length - accm_uv[i] + seg_len = accm_uv[i + 1] - accm_uv[i] + uv1 = uvs[i] + uv2 = uvs[i + 1] + target_uv = uv1 +\ + (uv2 - uv1) * tgt_seg_len / seg_len + break + else: + self.report({'ERROR'}, "Failed to get target UV") + return {'CANCELLED'} + + # update UV + l[uv_layer].uv = target_uv + + def __smooth(self, loop_seqs, uv_layer): + if self.transmission: + self.__smooth_w_transmission(loop_seqs, uv_layer) + else: + self.__smooth_wo_transmission(loop_seqs, uv_layer) + + def execute(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.verify() + + # loop_seqs[horizontal][vertical][loop] + loop_seqs, error = common.get_loop_sequences(bm, uv_layer) + if not loop_seqs: + self.report({'WARNING'}, error) + return {'CANCELLED'} + + # smooth + self.__smooth(loop_seqs, uv_layer) + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/uv_magic_uv/muv_texlock_ops.py b/uv_magic_uv/op/texture_lock.py similarity index 96% rename from uv_magic_uv/muv_texlock_ops.py rename to uv_magic_uv/op/texture_lock.py index bfc951299..b0be35346 100644 --- a/uv_magic_uv/muv_texlock_ops.py +++ b/uv_magic_uv/op/texture_lock.py @@ -20,20 +20,18 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "4.5" -__date__ = "19 Nov 2017" +__version__ = "5.0" +__date__ = "16 Feb 2018" import math -from math import ( - atan2, cos, - sqrt, sin, fabs, -) +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 + +from .. import common def get_vco(verts_orig, loop): @@ -195,7 +193,7 @@ class MUV_TexLockStart(bpy.types.Operator): 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: + if common.check_version(2, 73, 0) >= 0: bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() bm.faces.ensure_lookup_table() @@ -224,13 +222,15 @@ class MUV_TexLockStop(bpy.types.Operator): connect = BoolProperty( name="Connect UV", - default=True) + default=True + ) def execute(self, context): - props = context.scene.muv_props.texlock + sc = context.scene + props = sc.muv_props.texlock obj = bpy.context.active_object bm = bmesh.from_edit_mesh(obj.data) - if muv_common.check_version(2, 73, 0) >= 0: + if common.check_version(2, 73, 0) >= 0: bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() bm.faces.ensure_lookup_table() @@ -297,14 +297,14 @@ class MUV_TexLockUpdater(bpy.types.Operator): 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: + if 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'} + return uv_layer = bm.loops.layers.uv.verify() verts = [v.index for v in bm.verts if v.select] @@ -313,7 +313,7 @@ class MUV_TexLockUpdater(bpy.types.Operator): for vidx, v_orig in zip(verts, verts_orig): if vidx != v_orig["vidx"]: self.report({'ERROR'}, "Internal Error") - return {"CANCELLED"} + return v = bm.verts[vidx] link_loops = get_link_loops(v) @@ -336,7 +336,7 @@ class MUV_TexLockUpdater(bpy.types.Operator): v_orig["moved"] = True bmesh.update_edit_mesh(obj.data) - muv_common.redraw_all_areas() + 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] @@ -395,7 +395,7 @@ class MUV_TexLockIntrStart(bpy.types.Operator): obj = bpy.context.active_object bm = bmesh.from_edit_mesh(obj.data) - if muv_common.check_version(2, 73, 0) >= 0: + if common.check_version(2, 73, 0) >= 0: bm.verts.ensure_lookup_table() bm.edges.ensure_lookup_table() bm.faces.ensure_lookup_table() diff --git a/uv_magic_uv/muv_texproj_ops.py b/uv_magic_uv/op/texture_projection.py similarity index 80% rename from uv_magic_uv/muv_texproj_ops.py rename to uv_magic_uv/op/texture_projection.py index ffa4e789e..9c2dc521c 100644 --- a/uv_magic_uv/muv_texproj_ops.py +++ b/uv_magic_uv/op/texture_projection.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "4.5" -__date__ = "19 Nov 2017" +__version__ = "5.0" +__date__ = "16 Feb 2018" from collections import namedtuple @@ -31,7 +31,7 @@ import bmesh import mathutils from bpy_extras import view3d_utils -from . import muv_common +from .. import common Rect = namedtuple('Rect', 'x0 y0 x1 y1') @@ -237,28 +237,28 @@ class MUV_TexProjProject(bpy.types.Operator): def execute(self, context): sc = context.scene - if context.mode != "EDIT_MESH": - self.report({'WARNING'}, "Mesh must be in Edit mode") - return {'CANCELLED'} - if sc.muv_texproj_tex_image == "None": self.report({'WARNING'}, "No textures are selected") return {'CANCELLED'} - _, region, space = muv_common.get_space( + _, region, space = 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: + if 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'} + if sc.muv_texproj_assign_uvmap: + bm.loops.layers.uv.new() + else: + 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() @@ -290,50 +290,7 @@ class MUV_TexProjProject(bpy.types.Operator): l[uv_layer].uv = v_canvas[i].to_2d() i = i + 1 - muv_common.redraw_all_areas() + 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='IMAGE_COL') - - 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/op/texture_wrap.py b/uv_magic_uv/op/texture_wrap.py new file mode 100644 index 000000000..91b067041 --- /dev/null +++ b/uv_magic_uv/op/texture_wrap.py @@ -0,0 +1,212 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +import bpy +import bmesh + +from .. import common + + +class MUV_TexWrapRefer(bpy.types.Operator): + """ + Operation class: Refer UV + """ + + bl_idname = "uv.muv_texwrap_refer" + bl_label = "Refer" + bl_description = "Refer UV" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + props = context.scene.muv_props.texwrap + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if 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'} + + sel_faces = [f for f in bm.faces if f.select] + if len(sel_faces) != 1: + self.report({'WARNING'}, "Must select only one face") + return {'CANCELLED'} + + props.ref_face_index = sel_faces[0].index + props.ref_obj = obj + + return {'FINISHED'} + + +class MUV_TexWrapSet(bpy.types.Operator): + """ + Operation class: Set UV + """ + + bl_idname = "uv.muv_texwrap_set" + bl_label = "Set" + bl_description = "Set UV" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + sc = context.scene + props = sc.muv_props.texwrap + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if 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() + + if sc.muv_texwrap_selseq: + sel_faces = [] + for hist in bm.select_history: + if isinstance(hist, bmesh.types.BMFace) and hist.select: + sel_faces.append(hist) + if not sel_faces: + self.report({'WARNING'}, "Must select more than one face") + return {'CANCELLED'} + else: + sel_faces = [f for f in bm.faces if f.select] + if len(sel_faces) != 1: + self.report({'WARNING'}, "Must select only one face") + return {'CANCELLED'} + + ref_face_index = props.ref_face_index + for face in sel_faces: + tgt_face_index = face.index + if ref_face_index == tgt_face_index: + self.report({'WARNING'}, "Must select different face") + return {'CANCELLED'} + + if props.ref_obj != obj: + self.report({'WARNING'}, "Object must be same") + return {'CANCELLED'} + + ref_face = bm.faces[ref_face_index] + tgt_face = bm.faces[tgt_face_index] + + # get common vertices info + common_verts = [] + for sl in ref_face.loops: + for dl in tgt_face.loops: + if sl.vert == dl.vert: + info = {"vert": sl.vert, "ref_loop": sl, + "tgt_loop": dl} + common_verts.append(info) + break + + if len(common_verts) != 2: + self.report({'WARNING'}, + "2 verticies must be shared among faces") + return {'CANCELLED'} + + # get reference other vertices info + ref_other_verts = [] + for sl in ref_face.loops: + for ci in common_verts: + if sl.vert == ci["vert"]: + break + else: + info = {"vert": sl.vert, "loop": sl} + ref_other_verts.append(info) + + if not ref_other_verts: + self.report({'WARNING'}, "More than 1 vertex must be unshared") + return {'CANCELLED'} + + # get reference info + ref_info = {} + cv0 = common_verts[0]["vert"].co + cv1 = common_verts[1]["vert"].co + cuv0 = common_verts[0]["ref_loop"][uv_layer].uv + cuv1 = common_verts[1]["ref_loop"][uv_layer].uv + ov0 = ref_other_verts[0]["vert"].co + ouv0 = ref_other_verts[0]["loop"][uv_layer].uv + ref_info["vert_vdiff"] = cv1 - cv0 + ref_info["uv_vdiff"] = cuv1 - cuv0 + ref_info["vert_hdiff"], _ = common.diff_point_to_segment( + cv0, cv1, ov0) + ref_info["uv_hdiff"], _ = common.diff_point_to_segment( + cuv0, cuv1, ouv0) + + # get target other vertices info + tgt_other_verts = [] + for dl in tgt_face.loops: + for ci in common_verts: + if dl.vert == ci["vert"]: + break + else: + info = {"vert": dl.vert, "loop": dl} + tgt_other_verts.append(info) + + if not tgt_other_verts: + self.report({'WARNING'}, "More than 1 vertex must be unshared") + return {'CANCELLED'} + + # get target info + for info in tgt_other_verts: + cv0 = common_verts[0]["vert"].co + cv1 = common_verts[1]["vert"].co + cuv0 = common_verts[0]["ref_loop"][uv_layer].uv + ov = info["vert"].co + info["vert_hdiff"], x = common.diff_point_to_segment( + cv0, cv1, ov) + info["vert_vdiff"] = x - common_verts[0]["vert"].co + + # calclulate factor + fact_h = -info["vert_hdiff"].length / \ + ref_info["vert_hdiff"].length + fact_v = info["vert_vdiff"].length / \ + ref_info["vert_vdiff"].length + duv_h = ref_info["uv_hdiff"] * fact_h + duv_v = ref_info["uv_vdiff"] * fact_v + + # get target UV + info["target_uv"] = cuv0 + duv_h + duv_v + + # apply to common UVs + for info in common_verts: + info["tgt_loop"][uv_layer].uv = \ + info["ref_loop"][uv_layer].uv.copy() + # apply to other UVs + for info in tgt_other_verts: + info["loop"][uv_layer].uv = info["target_uv"] + + common.debug_print("===== Target Other Verticies =====") + common.debug_print(tgt_other_verts) + + bmesh.update_edit_mesh(obj.data) + + ref_face_index = tgt_face_index + + if sc.muv_texwrap_set_and_refer: + props.ref_face_index = tgt_face_index + + return {'FINISHED'} diff --git a/uv_magic_uv/muv_transuv_ops.py b/uv_magic_uv/op/transfer_uv.py similarity index 97% rename from uv_magic_uv/muv_transuv_ops.py rename to uv_magic_uv/op/transfer_uv.py index ed0a3c462..fd1b45e46 100644 --- a/uv_magic_uv/muv_transuv_ops.py +++ b/uv_magic_uv/op/transfer_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>, Mifth, MaxRobinot" __status__ = "production" -__version__ = "4.5" -__date__ = "19 Nov 2017" +__version__ = "5.0" +__date__ = "16 Feb 2018" from collections import OrderedDict @@ -29,8 +29,7 @@ import bpy import bmesh from bpy.props import BoolProperty -from . import muv_props -from . import muv_common +from .. import common class MUV_TransUVCopy(bpy.types.Operator): @@ -48,7 +47,7 @@ class MUV_TransUVCopy(bpy.types.Operator): 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: + if common.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() # get UV layer @@ -115,7 +114,7 @@ class MUV_TransUVPaste(bpy.types.Operator): 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: + if common.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() # get UV layer @@ -291,19 +290,19 @@ def parse_faces( vert1 = sorted_edge.verts[0] vert2 = sorted_edge.verts[1] - muv_common.debug_print(face_stuff[0], vert1, vert2) + 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) + 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: + if common.DEBUG: shared_face.select = True # test which faces are parsed new_shared_faces.append(shared_face) diff --git a/uv_magic_uv/muv_unwrapconst_ops.py b/uv_magic_uv/op/unwrap_constraint.py similarity index 94% rename from uv_magic_uv/muv_unwrapconst_ops.py rename to uv_magic_uv/op/unwrap_constraint.py index 1a6911199..311b2c35d 100644 --- a/uv_magic_uv/muv_unwrapconst_ops.py +++ b/uv_magic_uv/op/unwrap_constraint.py @@ -18,8 +18,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "4.5" -__date__ = "19 Nov 2017" +__version__ = "5.0" +__date__ = "16 Feb 2018" import bpy import bmesh @@ -28,7 +28,8 @@ from bpy.props import ( EnumProperty, FloatProperty, ) -from . import muv_common + +from .. import common class MUV_UnwrapConstraint(bpy.types.Operator): @@ -74,18 +75,21 @@ class MUV_UnwrapConstraint(bpy.types.Operator): u_const = BoolProperty( name="U-Constraint", description="Keep UV U-axis coordinate", - default=False) + default=False + ) v_const = BoolProperty( name="V-Constraint", description="Keep UV V-axis coordinate", - default=False) + 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: + if common.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() + # bpy.ops.uv.unwrap() makes one UV map at least if not bm.loops.layers.uv: self.report({'WARNING'}, "Object must have more than one UV map") return {'CANCELLED'} diff --git a/uv_magic_uv/muv_uvbb_ops.py b/uv_magic_uv/op/uv_bounding_box.py similarity index 94% rename from uv_magic_uv/muv_uvbb_ops.py rename to uv_magic_uv/op/uv_bounding_box.py index 4f7b0631b..04aa61106 100644 --- a/uv_magic_uv/muv_uvbb_ops.py +++ b/uv_magic_uv/op/uv_bounding_box.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "4.5" -__date__ = "19 Nov 2017" +__version__ = "5.0" +__date__ = "16 Feb 2018" from enum import IntEnum import math @@ -31,7 +31,7 @@ import bgl import mathutils import bmesh -from . import muv_common +from .. import common MAX_VALUE = 100000.0 @@ -602,17 +602,23 @@ class MUV_UVBBUpdater(bpy.types.Operator): """ Get UV coordinate """ + sc = context.scene obj = context.active_object uv_info = [] bm = bmesh.from_edit_mesh(obj.data) - if muv_common.check_version(2, 73, 0) >= 0: + if 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): + if not f.select: + continue + for i, l in enumerate(f.loops): + if sc.muv_uvbb_boundary == 'UV_SEL': + if l[uv_layer].select: + uv_info.append((f.index, i, l[uv_layer].uv.copy())) + elif sc.muv_uvbb_boundary == 'UV': uv_info.append((f.index, i, l[uv_layer].uv.copy())) if not uv_info: return None @@ -661,7 +667,7 @@ class MUV_UVBBUpdater(bpy.types.Operator): """ obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) - if muv_common.check_version(2, 73, 0) >= 0: + if common.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() if not bm.loops.layers.uv: return @@ -683,7 +689,7 @@ class MUV_UVBBUpdater(bpy.types.Operator): def modal(self, context, event): props = context.scene.muv_props.uvbb - muv_common.redraw_all_areas() + common.redraw_all_areas() if props.running is False: self.__handle_remove(context) return {'FINISHED'} @@ -717,37 +723,3 @@ class MUV_UVBBUpdater(bpy.types.Operator): 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='IMAGE_COL') - - 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/op/uv_inspection.py b/uv_magic_uv/op/uv_inspection.py new file mode 100644 index 000000000..0e8778f33 --- /dev/null +++ b/uv_magic_uv/op/uv_inspection.py @@ -0,0 +1,623 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +import bpy +import bmesh +import bgl +from mathutils import Vector + +from .. import common + + +def is_polygon_same(points1, points2): + if len(points1) != len(points2): + return False + + pts1 = points1.as_list() + pts2 = points2.as_list() + + for p1 in pts1: + for p2 in pts2: + diff = p2 - p1 + if diff.length < 0.0000001: + pts2.remove(p2) + break + else: + return False + + return True + + +def is_segment_intersect(start1, end1, start2, end2): + seg1 = end1 - start1 + seg2 = end2 - start2 + + a1 = -seg1.y + b1 = seg1.x + d1 = -(a1 * start1.x + b1 * start1.y) + + a2 = -seg2.y + b2 = seg2.x + d2 = -(a2 * start2.x + b2 * start2.y) + + seg1_line2_start = a2 * start1.x + b2 * start1.y + d2 + seg1_line2_end = a2 * end1.x + b2 * end1.y + d2 + + seg2_line1_start = a1 * start2.x + b1 * start2.y + d1 + seg2_line1_end = a1 * end2.x + b1 * end2.y + d1 + + if (seg1_line2_start * seg1_line2_end >= 0) or \ + (seg2_line1_start * seg2_line1_end >= 0): + return False, None + + u = seg1_line2_start / (seg1_line2_start - seg1_line2_end) + out = start1 + u * seg1 + + return True, out + + +class RingBuffer: + def __init__(self, arr): + self.__buffer = arr.copy() + self.__pointer = 0 + + def __repr__(self): + return repr(self.__buffer) + + def __len__(self): + return len(self.__buffer) + + def insert(self, val, offset=0): + self.__buffer.insert(self.__pointer + offset, val) + + def head(self): + return self.__buffer[0] + + def tail(self): + return self.__buffer[-1] + + def get(self, offset=0): + size = len(self.__buffer) + val = self.__buffer[(self.__pointer + offset) % size] + return val + + def next(self): + size = len(self.__buffer) + self.__pointer = (self.__pointer + 1) % size + + def reset(self): + self.__pointer = 0 + + def find(self, obj): + try: + idx = self.__buffer.index(obj) + except ValueError: + return None + return self.__buffer[idx] + + def find_and_next(self, obj): + size = len(self.__buffer) + idx = self.__buffer.index(obj) + self.__pointer = (idx + 1) % size + + def find_and_set(self, obj): + idx = self.__buffer.index(obj) + self.__pointer = idx + + def as_list(self): + return self.__buffer.copy() + + def reverse(self): + self.__buffer.reverse() + self.reset() + + +# clip: reference polygon +# subject: tested polygon +def do_weiler_atherton_cliping(clip, subject, uv_layer, mode): + + clip_uvs = RingBuffer([l[uv_layer].uv.copy() for l in clip.loops]) + if is_polygon_flipped(clip_uvs): + clip_uvs.reverse() + subject_uvs = RingBuffer([l[uv_layer].uv.copy() for l in subject.loops]) + if is_polygon_flipped(subject_uvs): + subject_uvs.reverse() + + common.debug_print("===== Clip UV List =====") + common.debug_print(clip_uvs) + common.debug_print("===== Subject UV List =====") + common.debug_print(subject_uvs) + + # check if clip and subject is overlapped completely + if is_polygon_same(clip_uvs, subject_uvs): + polygons = [subject_uvs.as_list()] + common.debug_print("===== Polygons Overlapped Completely =====") + common.debug_print(polygons) + return True, polygons + + # check if subject is in clip + if is_points_in_polygon(subject_uvs, clip_uvs): + polygons = [subject_uvs.as_list()] + return True, polygons + + # check if clip is in subject + if is_points_in_polygon(clip_uvs, subject_uvs): + polygons = [subject_uvs.as_list()] + return True, polygons + + # check if clip and subject is overlapped partially + intersections = [] + while True: + subject_uvs.reset() + while True: + uv_start1 = clip_uvs.get() + uv_end1 = clip_uvs.get(1) + uv_start2 = subject_uvs.get() + uv_end2 = subject_uvs.get(1) + intersected, point = is_segment_intersect(uv_start1, uv_end1, + uv_start2, uv_end2) + if intersected: + clip_uvs.insert(point, 1) + subject_uvs.insert(point, 1) + intersections.append([point, + [clip_uvs.get(), clip_uvs.get(1)]]) + subject_uvs.next() + if subject_uvs.get() == subject_uvs.head(): + break + clip_uvs.next() + if clip_uvs.get() == clip_uvs.head(): + break + + common.debug_print("===== Intersection List =====") + common.debug_print(intersections) + + # no intersection, so subject and clip is not overlapped + if not intersections: + return False, None + + def get_intersection_pair(intersections, key): + for sect in intersections: + if sect[0] == key: + return sect[1] + + return None + + # make enter/exit pair + subject_uvs.reset() + subject_entering = [] + subject_exiting = [] + clip_entering = [] + clip_exiting = [] + intersect_uv_list = [] + while True: + pair = get_intersection_pair(intersections, subject_uvs.get()) + if pair: + sub = subject_uvs.get(1) - subject_uvs.get(-1) + inter = pair[1] - pair[0] + cross = sub.x * inter.y - inter.x * sub.y + if cross < 0: + subject_entering.append(subject_uvs.get()) + clip_exiting.append(subject_uvs.get()) + else: + subject_exiting.append(subject_uvs.get()) + clip_entering.append(subject_uvs.get()) + intersect_uv_list.append(subject_uvs.get()) + + subject_uvs.next() + if subject_uvs.get() == subject_uvs.head(): + break + + common.debug_print("===== Enter List =====") + common.debug_print(clip_entering) + common.debug_print(subject_entering) + common.debug_print("===== Exit List =====") + common.debug_print(clip_exiting) + common.debug_print(subject_exiting) + + # for now, can't handle the situation when fulfill all below conditions + # * two faces have common edge + # * each face is intersected + # * Show Mode is "Part" + # so for now, ignore this situation + if len(subject_entering) != len(subject_exiting): + if mode == 'FACE': + polygons = [subject_uvs.as_list()] + return True, polygons + return False, None + + def traverse(current_list, entering, exiting, poly, current, other_list): + result = current_list.find(current) + if not result: + return None + if result != current: + print("Internal Error") + return None + + # enter + if entering.count(current) >= 1: + entering.remove(current) + + current_list.find_and_next(current) + current = current_list.get() + + while exiting.count(current) == 0: + poly.append(current.copy()) + current_list.find_and_next(current) + current = current_list.get() + + # exit + poly.append(current.copy()) + exiting.remove(current) + + other_list.find_and_set(current) + return other_list.get() + + # Traverse + polygons = [] + current_uv_list = subject_uvs + other_uv_list = clip_uvs + current_entering = subject_entering + current_exiting = subject_exiting + + poly = [] + current_uv = current_entering[0] + + while True: + current_uv = traverse(current_uv_list, current_entering, + current_exiting, poly, current_uv, other_uv_list) + + if current_uv_list == subject_uvs: + current_uv_list = clip_uvs + other_uv_list = subject_uvs + current_entering = clip_entering + current_exiting = clip_exiting + common.debug_print("-- Next: Clip --") + else: + current_uv_list = subject_uvs + other_uv_list = clip_uvs + current_entering = subject_entering + current_exiting = subject_exiting + common.debug_print("-- Next: Subject --") + + common.debug_print(clip_entering) + common.debug_print(clip_exiting) + common.debug_print(subject_entering) + common.debug_print(subject_exiting) + + if not clip_entering and not clip_exiting \ + and not subject_entering and not subject_exiting: + break + + polygons.append(poly) + + common.debug_print("===== Polygons Overlapped Partially =====") + common.debug_print(polygons) + + return True, polygons + + +class MUV_UVInspRenderer(bpy.types.Operator): + """ + Operation class: Render UV Inspection + No operation (only rendering) + """ + + bl_idname = "uv.muv_uvinsp_renderer" + bl_description = "Render overlapped/flipped UVs" + bl_label = "Overlapped/Flipped UV renderer" + + __handle = None + + @staticmethod + def handle_add(obj, context): + sie = bpy.types.SpaceImageEditor + MUV_UVInspRenderer.__handle = sie.draw_handler_add( + MUV_UVInspRenderer.draw, (obj, context), 'WINDOW', 'POST_PIXEL') + + @staticmethod + def handle_remove(): + if MUV_UVInspRenderer.__handle is not None: + bpy.types.SpaceImageEditor.draw_handler_remove( + MUV_UVInspRenderer.__handle, 'WINDOW') + MUV_UVInspRenderer.__handle = None + + @staticmethod + def draw(_, context): + sc = context.scene + props = sc.muv_props.uvinsp + prefs = context.user_preferences.addons["uv_magic_uv"].preferences + + # OpenGL configuration + bgl.glEnable(bgl.GL_BLEND) + + # render overlapped UV + if sc.muv_uvinsp_show_overlapped: + color = prefs.uvinsp_overlapped_color + for info in props.overlapped_info: + if sc.muv_uvinsp_show_mode == 'PART': + for poly in info["polygons"]: + bgl.glBegin(bgl.GL_TRIANGLE_FAN) + bgl.glColor4f(color[0], color[1], color[2], color[3]) + for uv in poly: + x, y = context.region.view2d.view_to_region( + uv.x, uv.y) + bgl.glVertex2f(x, y) + bgl.glEnd() + elif sc.muv_uvinsp_show_mode == 'FACE': + bgl.glBegin(bgl.GL_TRIANGLE_FAN) + bgl.glColor4f(color[0], color[1], color[2], color[3]) + for uv in info["subject_uvs"]: + x, y = context.region.view2d.view_to_region(uv.x, uv.y) + bgl.glVertex2f(x, y) + bgl.glEnd() + + # render flipped UV + if sc.muv_uvinsp_show_flipped: + color = prefs.uvinsp_flipped_color + for info in props.flipped_info: + if sc.muv_uvinsp_show_mode == 'PART': + for poly in info["polygons"]: + bgl.glBegin(bgl.GL_TRIANGLE_FAN) + bgl.glColor4f(color[0], color[1], color[2], color[3]) + for uv in poly: + x, y = context.region.view2d.view_to_region( + uv.x, uv.y) + bgl.glVertex2f(x, y) + bgl.glEnd() + elif sc.muv_uvinsp_show_mode == 'FACE': + bgl.glBegin(bgl.GL_TRIANGLE_FAN) + bgl.glColor4f(color[0], color[1], color[2], color[3]) + for uv in info["uvs"]: + x, y = context.region.view2d.view_to_region(uv.x, uv.y) + bgl.glVertex2f(x, y) + bgl.glEnd() + + +def is_polygon_flipped(points): + area = 0.0 + for i in range(len(points)): + uv1 = points.get(i) + uv2 = points.get(i + 1) + a = uv1.x * uv2.y - uv1.y * uv2.x + area = area + a + if area < 0: + # clock-wise + return True + return False + + +def is_point_in_polygon(point, subject_points): + count = 0 + for i in range(len(subject_points)): + uv_start1 = subject_points.get(i) + uv_end1 = subject_points.get(i + 1) + uv_start2 = point + uv_end2 = Vector((1000000.0, point.y)) + intersected, _ = is_segment_intersect(uv_start1, uv_end1, + uv_start2, uv_end2) + if intersected: + count = count + 1 + + return count % 2 + + +def is_points_in_polygon(points, subject_points): + for i in range(len(points)): + internal = is_point_in_polygon(points.get(i), subject_points) + if not internal: + return False + + return True + + +def get_overlapped_uv_info(bm, faces, uv_layer, mode): + # at first, check island overlapped + isl = common.get_island_info_from_faces(bm, faces, uv_layer) + overlapped_isl_pairs = [] + for i, i1 in enumerate(isl): + for i2 in isl[i + 1:]: + if (i1["max"].x < i2["min"].x) or (i2["max"].x < i1["min"].x) or \ + (i1["max"].y < i2["min"].y) or (i2["max"].y < i1["min"].y): + continue + overlapped_isl_pairs.append([i1, i2]) + + # next, check polygon overlapped + overlapped_uvs = [] + for oip in overlapped_isl_pairs: + for clip in oip[0]["faces"]: + f_clip = clip["face"] + for subject in oip[1]["faces"]: + f_subject = subject["face"] + + # fast operation, apply bounding box algorithm + if (clip["max_uv"].x < subject["min_uv"].x) or \ + (subject["max_uv"].x < clip["min_uv"].x) or \ + (clip["max_uv"].y < subject["min_uv"].y) or \ + (subject["max_uv"].y < clip["min_uv"].y): + continue + + # slow operation, apply Weiler-Atherton cliping algorithm + result, polygons = do_weiler_atherton_cliping(f_clip, + f_subject, + uv_layer, mode) + if result: + subject_uvs = [l[uv_layer].uv.copy() + for l in f_subject.loops] + overlapped_uvs.append({"clip_face": f_clip, + "subject_face": f_subject, + "subject_uvs": subject_uvs, + "polygons": polygons}) + + return overlapped_uvs + + +def get_flipped_uv_info(faces, uv_layer): + flipped_uvs = [] + for f in faces: + polygon = RingBuffer([l[uv_layer].uv.copy() for l in f.loops]) + if is_polygon_flipped(polygon): + uvs = [l[uv_layer].uv.copy() for l in f.loops] + flipped_uvs.append({"face": f, "uvs": uvs, + "polygons": [polygon.as_list()]}) + + return flipped_uvs + + +def update_uvinsp_info(context): + sc = context.scene + props = sc.muv_props.uvinsp + + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.verify() + + if context.tool_settings.use_uv_select_sync: + sel_faces = [f for f in bm.faces] + else: + sel_faces = [f for f in bm.faces if f.select] + props.overlapped_info = get_overlapped_uv_info(bm, sel_faces, uv_layer, + sc.muv_uvinsp_show_mode) + props.flipped_info = get_flipped_uv_info(sel_faces, uv_layer) + + +class MUV_UVInspUpdate(bpy.types.Operator): + """ + Operation class: Update + """ + + bl_idname = "uv.muv_uvinsp_update" + bl_label = "Update" + bl_description = "Update Overlapped/Flipped UV" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + update_uvinsp_info(context) + + if context.area: + context.area.tag_redraw() + + return {'FINISHED'} + + +class MUV_UVInspDisplay(bpy.types.Operator): + """ + Operation class: Display + """ + + bl_idname = "uv.muv_uvinsp_display" + bl_label = "Display" + bl_description = "Display Overlapped/Flipped UV" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + sc = context.scene + props = sc.muv_props.uvinsp + if not props.display_running: + update_uvinsp_info(context) + MUV_UVInspRenderer.handle_add(self, context) + props.display_running = True + else: + MUV_UVInspRenderer.handle_remove() + props.display_running = False + + if context.area: + context.area.tag_redraw() + + return {'FINISHED'} + + +class MUV_UVInspSelectOverlapped(bpy.types.Operator): + """ + Operation class: Select faces which have overlapped UVs + """ + + bl_idname = "uv.muv_uvinsp_select_overlapped" + bl_label = "Overlapped" + bl_description = "Select faces which have overlapped UVs" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.verify() + + if context.tool_settings.use_uv_select_sync: + sel_faces = [f for f in bm.faces] + else: + sel_faces = [f for f in bm.faces if f.select] + + overlapped_info = get_overlapped_uv_info(bm, sel_faces, uv_layer, + 'FACE') + + for info in overlapped_info: + if context.tool_settings.use_uv_select_sync: + info["subject_face"].select = True + else: + for l in info["subject_face"].loops: + l[uv_layer].select = True + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} + + +class MUV_UVInspSelectFlipped(bpy.types.Operator): + """ + Operation class: Select faces which have flipped UVs + """ + + bl_idname = "uv.muv_uvinsp_select_flipped" + bl_label = "Flipped" + bl_description = "Select faces which have flipped UVs" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.verify() + + if context.tool_settings.use_uv_select_sync: + sel_faces = [f for f in bm.faces] + else: + sel_faces = [f for f in bm.faces if f.select] + + flipped_info = get_flipped_uv_info(sel_faces, uv_layer) + + for info in flipped_info: + if context.tool_settings.use_uv_select_sync: + info["face"].select = True + else: + for l in info["face"].loops: + l[uv_layer].select = True + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/uv_magic_uv/op/uv_sculpt.py b/uv_magic_uv/op/uv_sculpt.py new file mode 100644 index 000000000..6133b2a2c --- /dev/null +++ b/uv_magic_uv/op/uv_sculpt.py @@ -0,0 +1,355 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +from math import pi, cos, tan, sin + +import bpy +import bmesh +import bgl +from mathutils import Vector +from bpy_extras import view3d_utils +from mathutils.bvhtree import BVHTree +from mathutils.geometry import barycentric_transform + +from .. import common + + +class MUV_UVSculptRenderer(bpy.types.Operator): + """ + Operation class: Render Brush + """ + + bl_idname = "uv.muv_uvsculpt_renderer" + bl_label = "Brush Renderer" + bl_description = "Brush Renderer in View3D" + + __handle = None + + @staticmethod + def handle_add(obj, context): + if MUV_UVSculptRenderer.__handle is None: + sv = bpy.types.SpaceView3D + MUV_UVSculptRenderer.__handle = sv.draw_handler_add( + MUV_UVSculptRenderer.draw_brush, + (obj, context), "WINDOW", "POST_PIXEL") + + @staticmethod + def handle_remove(): + if MUV_UVSculptRenderer.__handle is not None: + sv = bpy.types.SpaceView3D + sv.draw_handler_remove( + MUV_UVSculptRenderer.__handle, "WINDOW") + MUV_UVSculptRenderer.__handle = None + + @staticmethod + def draw_brush(obj, context): + sc = context.scene + prefs = context.user_preferences.addons["uv_magic_uv"].preferences + + num_segment = 180 + theta = 2 * pi / num_segment + fact_t = tan(theta) + fact_r = cos(theta) + color = prefs.uvsculpt_brush_color + + bgl.glBegin(bgl.GL_LINE_STRIP) + bgl.glColor4f(color[0], color[1], color[2], color[3]) + x = sc.muv_uvsculpt_radius * cos(0.0) + y = sc.muv_uvsculpt_radius * sin(0.0) + for _ in range(num_segment): + bgl.glVertex2f(x + obj.current_mco.x, y + obj.current_mco.y) + tx = -y + ty = x + x = x + tx * fact_t + y = y + ty * fact_t + x = x * fact_r + y = y * fact_r + bgl.glEnd() + + +class MUV_UVSculptOps(bpy.types.Operator): + """ + Operation class: UV Sculpt in View3D + """ + + bl_idname = "uv.muv_uvsculpt_ops" + bl_label = "UV Sculpt" + bl_description = "UV Sculpt in View3D" + bl_options = {'REGISTER'} + + def __init__(self): + self.__timer = None + self.__loop_info = [] + self.__stroking = False + self.current_mco = Vector((0.0, 0.0)) + self.__initial_mco = Vector((0.0, 0.0)) + + def __get_strength(self, p, len_, factor): + f = factor + + if p > len_: + return 0.0 + + if p < 0.0: + return f + + return (len_ - p) * f / len_ + + def __stroke_init(self, context, _): + sc = context.scene + + self.__initial_mco = self.current_mco + + # get influenced UV + obj = context.active_object + world_mat = obj.matrix_world + bm = bmesh.from_edit_mesh(obj.data) + uv_layer = bm.loops.layers.uv.verify() + _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') + + self.__loop_info = [] + for f in bm.faces: + if not f.select: + continue + for i, l in enumerate(f.loops): + loc_2d = view3d_utils.location_3d_to_region_2d( + region, space.region_3d, world_mat * l.vert.co) + diff = loc_2d - self.__initial_mco + if diff.length < sc.muv_uvsculpt_radius: + info = { + "face_idx": f.index, + "loop_idx": i, + "initial_vco": l.vert.co.copy(), + "initial_vco_2d": loc_2d, + "initial_uv": l[uv_layer].uv.copy(), + "strength": self.__get_strength( + diff.length, sc.muv_uvsculpt_radius, + sc.muv_uvsculpt_strength) + } + self.__loop_info.append(info) + + def __stroke_apply(self, context, _): + sc = context.scene + obj = context.active_object + world_mat = obj.matrix_world + bm = bmesh.from_edit_mesh(obj.data) + uv_layer = bm.loops.layers.uv.verify() + mco = self.current_mco + + if sc.muv_uvsculpt_tools == 'GRAB': + for info in self.__loop_info: + diff_uv = (mco - self.__initial_mco) * info["strength"] + l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] + l[uv_layer].uv = info["initial_uv"] + diff_uv / 100.0 + + elif sc.muv_uvsculpt_tools == 'PINCH': + _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') + loop_info = [] + for f in bm.faces: + if not f.select: + continue + for i, l in enumerate(f.loops): + loc_2d = view3d_utils.location_3d_to_region_2d( + region, space.region_3d, world_mat * l.vert.co) + diff = loc_2d - self.__initial_mco + if diff.length < sc.muv_uvsculpt_radius: + info = { + "face_idx": f.index, + "loop_idx": i, + "initial_vco": l.vert.co.copy(), + "initial_vco_2d": loc_2d, + "initial_uv": l[uv_layer].uv.copy(), + "strength": self.__get_strength( + diff.length, sc.muv_uvsculpt_radius, + sc.muv_uvsculpt_strength) + } + loop_info.append(info) + + # mouse coordinate to UV coordinate + ray_vec = view3d_utils.region_2d_to_vector_3d(region, + space.region_3d, mco) + ray_vec.normalize() + ray_orig = view3d_utils.region_2d_to_origin_3d(region, + space.region_3d, + mco) + ray_tgt = ray_orig + ray_vec * 1000000.0 + mwi = world_mat.inverted() + ray_orig_obj = mwi * ray_orig + ray_tgt_obj = mwi * ray_tgt + ray_dir_obj = ray_tgt_obj - ray_orig_obj + ray_dir_obj.normalize() + tree = BVHTree.FromBMesh(bm) + loc, _, fidx, _ = tree.ray_cast(ray_orig_obj, ray_dir_obj) + if not loc: + return + loops = [l for l in bm.faces[fidx].loops] + uvs = [Vector((l[uv_layer].uv.x, l[uv_layer].uv.y, 0.0)) + for l in loops] + target_uv = barycentric_transform( + loc, loops[0].vert.co, loops[1].vert.co, loops[2].vert.co, + uvs[0], uvs[1], uvs[2]) + target_uv = Vector((target_uv.x, target_uv.y)) + + # move to target UV coordinate + for info in loop_info: + l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] + if sc.muv_uvsculpt_pinch_invert: + diff_uv = (l[uv_layer].uv - target_uv) * info["strength"] + else: + diff_uv = (target_uv - l[uv_layer].uv) * info["strength"] + l[uv_layer].uv = l[uv_layer].uv + diff_uv / 10.0 + + elif sc.muv_uvsculpt_tools == 'RELAX': + _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') + + # get vertex and loop relation + vert_db = {} + for f in bm.faces: + for l in f.loops: + if l.vert in vert_db: + vert_db[l.vert]["loops"].append(l) + else: + vert_db[l.vert] = {"loops": [l]} + + # get relaxation information + for k in vert_db.keys(): + d = vert_db[k] + d["uv_sum"] = Vector((0.0, 0.0)) + d["uv_count"] = 0 + + for l in d["loops"]: + ln = l.link_loop_next + lp = l.link_loop_prev + d["uv_sum"] = d["uv_sum"] + ln[uv_layer].uv + d["uv_sum"] = d["uv_sum"] + lp[uv_layer].uv + d["uv_count"] = d["uv_count"] + 2 + d["uv_p"] = d["uv_sum"] / d["uv_count"] + d["uv_b"] = d["uv_p"] - d["loops"][0][uv_layer].uv + for k in vert_db.keys(): + d = vert_db[k] + d["uv_sum_b"] = Vector((0.0, 0.0)) + for l in d["loops"]: + ln = l.link_loop_next + lp = l.link_loop_prev + dn = vert_db[ln.vert] + dp = vert_db[lp.vert] + d["uv_sum_b"] = d["uv_sum_b"] + dn["uv_b"] + dp["uv_b"] + + # apply + for f in bm.faces: + if not f.select: + continue + for i, l in enumerate(f.loops): + loc_2d = view3d_utils.location_3d_to_region_2d( + region, space.region_3d, world_mat * l.vert.co) + diff = loc_2d - self.__initial_mco + if diff.length >= sc.muv_uvsculpt_radius: + continue + db = vert_db[l.vert] + strength = self.__get_strength(diff.length, + sc.muv_uvsculpt_radius, + sc.muv_uvsculpt_strength) + + base = (1.0 - strength) * l[uv_layer].uv + if sc.muv_uvsculpt_relax_method == 'HC': + t = 0.5 * (db["uv_b"] + db["uv_sum_b"] / d["uv_count"]) + diff = strength * (db["uv_p"] - t) + target_uv = base + diff + elif sc.muv_uvsculpt_relax_method == 'LAPLACIAN': + diff = strength * db["uv_p"] + target_uv = base + diff + else: + continue + + l[uv_layer].uv = target_uv + + bmesh.update_edit_mesh(obj.data) + + def __stroke_exit(self, context, _): + sc = context.scene + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + uv_layer = bm.loops.layers.uv.verify() + mco = self.current_mco + + if sc.muv_uvsculpt_tools == 'GRAB': + for info in self.__loop_info: + diff_uv = (mco - self.__initial_mco) * info["strength"] + l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] + l[uv_layer].uv = info["initial_uv"] + diff_uv / 100.0 + + bmesh.update_edit_mesh(obj.data) + + def modal(self, context, event): + props = context.scene.muv_props.uvsculpt + + if context.area: + context.area.tag_redraw() + + if not props.running: + if self.__timer is not None: + MUV_UVSculptRenderer.handle_remove() + context.window_manager.event_timer_remove(self.__timer) + self.__timer = None + return {'FINISHED'} + + self.current_mco = Vector((event.mouse_region_x, event.mouse_region_y)) + + if event.type == 'LEFTMOUSE': + if event.value == 'PRESS': + if not self.__stroking: + self.__stroke_init(context, event) + self.__stroking = True + elif event.value == 'RELEASE': + if self.__stroking: + self.__stroke_exit(context, event) + self.__stroking = False + elif event.type == 'MOUSEMOVE': + if self.__stroking: + self.__stroke_apply(context, event) + elif event.type == 'TIMER': + if self.__stroking: + self.__stroke_apply(context, event) + + return {'PASS_THROUGH'} + + def invoke(self, context, _): + props = context.scene.muv_props.uvsculpt + + if context.area: + context.area.tag_redraw() + + if props.running: + props.running = False + return {'FINISHED'} + + props.running = True + 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_UVSculptRenderer.handle_add(self, context) + + return {'RUNNING_MODAL'} diff --git a/uv_magic_uv/muv_uvw_ops.py b/uv_magic_uv/op/uvw.py similarity index 86% rename from uv_magic_uv/muv_uvw_ops.py rename to uv_magic_uv/op/uvw.py index eb366e976..37d88a534 100644 --- a/uv_magic_uv/muv_uvw_ops.py +++ b/uv_magic_uv/op/uvw.py @@ -20,9 +20,8 @@ __author__ = "Alexander Milovsky, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "4.5" -__date__ = "19 Nov 2017" - +__version__ = "5.0" +__date__ = "16 Feb 2018" from math import sin, cos, pi @@ -30,11 +29,12 @@ import bpy import bmesh from bpy.props import ( FloatProperty, - FloatVectorProperty + FloatVectorProperty, + BoolProperty ) from mathutils import Vector -from . import muv_common +from .. import common class MUV_UVWBoxMap(bpy.types.Operator): @@ -62,6 +62,11 @@ class MUV_UVWBoxMap(bpy.types.Operator): default=1.0, precision=4 ) + assign_uvmap = BoolProperty( + name="Assign UVMap", + description="Assign UVMap when no UVmaps are available", + default=True + ) @classmethod def poll(cls, context): @@ -71,15 +76,17 @@ class MUV_UVWBoxMap(bpy.types.Operator): def execute(self, context): obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) - if muv_common.check_version(2, 73, 0) >= 0: + if 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'} - + if self.assign_uvmap: + bm.loops.layers.uv.new() + else: + self.report( + {'WARNING'}, "Object must have more than one UV map") + return {'CANCELLED'} uv_layer = bm.loops.layers.uv.verify() scale = 1.0 / self.size @@ -168,6 +175,11 @@ class MUV_UVWBestPlanerMap(bpy.types.Operator): default=1.0, precision=4 ) + assign_uvmap = BoolProperty( + name="Assign UVMap", + description="Assign UVMap when no UVmaps are available", + default=True + ) @classmethod def poll(cls, context): @@ -177,14 +189,17 @@ class MUV_UVWBestPlanerMap(bpy.types.Operator): def execute(self, context): obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) - if muv_common.check_version(2, 73, 0) >= 0: + if 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'} + if self.assign_uvmap: + bm.loops.layers.uv.new() + else: + self.report( + {'WARNING'}, "Object must have more than one UV map") + return {'CANCELLED'} uv_layer = bm.loops.layers.uv.verify() diff --git a/uv_magic_uv/muv_wsuv_ops.py b/uv_magic_uv/op/world_scale_uv.py similarity index 70% rename from uv_magic_uv/muv_wsuv_ops.py rename to uv_magic_uv/op/world_scale_uv.py index 4ee8b4f93..f1539ddbb 100644 --- a/uv_magic_uv/muv_wsuv_ops.py +++ b/uv_magic_uv/op/world_scale_uv.py @@ -20,43 +20,32 @@ __author__ = "McBuff, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "4.5" -__date__ = "19 Nov 2017" +__version__ = "5.0" +__date__ = "16 Feb 2018" +from math import sqrt import bpy import bmesh from mathutils import Vector -from bpy.props import ( - FloatProperty, - BoolProperty, - EnumProperty -) -from . import muv_common +from bpy.props import EnumProperty +from .. import 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 +def measure_wsuv_info(obj): + mesh_area = common.measure_mesh_area(obj) + uv_area = common.measure_uv_area(obj) - scale = 0.0 - if dv.magnitude > 0.00000001: - scale = duv.magnitude / dv.magnitude + if not uv_area: + return None, None, None - return scale + if mesh_area == 0.0: + density = 0.0 + else: + density = sqrt(uv_area) / sqrt(mesh_area) - -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 + return uv_area, mesh_area, density class MUV_WSUVMeasure(bpy.types.Operator): @@ -70,30 +59,22 @@ class MUV_WSUVMeasure(bpy.types.Operator): 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() + sc = context.scene + obj = context.active_object - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") + uv_area, mesh_area, density = measure_wsuv_info(obj) + if not uv_area: + self.report({'WARNING'}, + "Object must have more than one UV map and texture") 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) + sc.muv_wsuv_src_uv_area = uv_area + sc.muv_wsuv_src_mesh_area = mesh_area + sc.muv_wsuv_src_density = density - props.ref_scale = scale / len(sel_faces) - - self.report( - {'INFO'}, "Average face size: {0}".format(props.ref_scale)) + self.report({'INFO'}, + "UV Area: {0}, Mesh Area: {1}, Texel Density: {2}" + .format(uv_area, mesh_area, density)) return {'FINISHED'} @@ -108,16 +89,6 @@ class MUV_WSUVApply(bpy.types.Operator): bl_description = "Apply scaled UV based on scale calculation" bl_options = {'REGISTER', 'UNDO'} - proportional_scaling = BoolProperty( - name="Proportional Scaling", - default=True - ) - scaling_factor = FloatProperty( - name="Scaling Factor", - default=1.0, - max=1000.0, - min=0.00001 - ) origin = EnumProperty( name="Origin", description="Aspect Origin", @@ -139,43 +110,38 @@ class MUV_WSUVApply(bpy.types.Operator): def draw(self, _): layout = self.layout - row = layout.row() - row.prop(self, "proportional_scaling") - row = layout.row() - row.prop(self, "scaling_factor") - if self.proportional_scaling: - row.enabled = False + layout.prop(self, "origin") def execute(self, context): - props = context.scene.muv_props.wsuv - obj = bpy.context.active_object + sc = context.scene + obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) - if muv_common.check_version(2, 73, 0) >= 0: + if 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) + uv_area, mesh_area, density = measure_wsuv_info(obj) + if not uv_area: + self.report({'WARNING'}, + "Object must have more than one UV map and texture") + return {'CANCELLED'} - self.report( - {'INFO'}, "Average face size: {0}".format(scale)) + uv_layer = bm.loops.layers.uv.verify() - if self.proportional_scaling: - factor = props.ref_scale / scale - else: - factor = self.scaling_factor + if sc.muv_wsuv_mode == 'PROPORTIONAL': + tgt_density = sc.muv_wsuv_src_density * sqrt(mesh_area) / \ + sqrt(sc.muv_wsuv_src_mesh_area) + elif sc.muv_wsuv_mode == 'SCALING': + tgt_density = sc.muv_wsuv_src_density * sc.muv_wsuv_scaling_factor + elif sc.muv_wsuv_mode == 'USER': + tgt_density = sc.muv_wsuv_tgt_density + elif sc.muv_wsuv_mode == 'CONSTANT': + tgt_density = sc.muv_wsuv_src_density + + factor = tgt_density / density # calculate origin if self.origin == 'CENTER': diff --git a/uv_magic_uv/preferences.py b/uv_magic_uv/preferences.py new file mode 100644 index 000000000..eb86804e0 --- /dev/null +++ b/uv_magic_uv/preferences.py @@ -0,0 +1,216 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +from bpy.props import ( + FloatProperty, + FloatVectorProperty, +) +from bpy.types import AddonPreferences + + +class MUV_Preferences(AddonPreferences): + """Preferences class: Preferences for this add-on""" + + bl_idname = __package__ + + # for UV Sculpt + uvsculpt_brush_color = FloatVectorProperty( + name="Color", + description="Color", + default=(1.0, 0.4, 0.4, 1.0), + min=0.0, + max=1.0, + size=4, + subtype='COLOR' + ) + + # for Overlapped UV + uvinsp_overlapped_color = FloatVectorProperty( + name="Color", + description="Color", + default=(0.0, 0.0, 1.0, 0.3), + min=0.0, + max=1.0, + size=4, + subtype='COLOR' + ) + + # for Flipped UV + uvinsp_flipped_color = FloatVectorProperty( + name="Color", + description="Color", + default=(1.0, 0.0, 0.0, 0.3), + min=0.0, + max=1.0, + size=4, + subtype='COLOR' + ) + + # 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("[Configuration]") + + layout.label("UV Sculpt:") + sp = layout.split(percentage=0.05) + col = sp.column() # spacer + sp = sp.split(percentage=0.3) + col = sp.column() + col.label("Brush Color:") + col.prop(self, "uvsculpt_brush_color", text="") + + layout.separator() + + layout.label("UV Inspection:") + sp = layout.split(percentage=0.05) + col = sp.column() # spacer + sp = sp.split(percentage=0.3) + col = sp.column() + col.label("Overlapped UV Color:") + col.prop(self, "uvinsp_overlapped_color", text="") + sp = sp.split(percentage=0.45) + col = sp.column() + col.label("Flipped UV Color:") + col.prop(self, "uvinsp_flipped_color", text="") + + layout.separator() + + layout.label("Texture Projection:") + sp = layout.split(percentage=0.05) + col = sp.column() # spacer + sp = sp.split(percentage=0.3) + col = sp.column() + col.prop(self, "texproj_canvas_padding") + + layout.separator() + + layout.label("UV Bounding Box:") + 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("--------------------------------------") + + 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 are new to this add-on.") + column.label("https://github.com/nutti/Magic-UV/wiki/Tutorial") + + layout.label("--------------------------------------") + + layout.label("[Location]") + + row = layout.row(align=True) + sp = row.split(percentage=0.5) + sp.label("3D View > Tool shelf > Copy/Paste UV (Object mode)") + sp = sp.split(percentage=1.0) + col = sp.column(align=True) + col.label("Copy/Paste UV (Among objects)") + + row = layout.row(align=True) + sp = row.split(percentage=0.5) + sp.label("3D View > Tool shelf > Copy/Paste UV (Edit mode)") + sp = sp.split(percentage=1.0) + col = sp.column(align=True) + col.label("Copy/Paste UV (Among faces in 3D View)") + col.label("Transfer UV") + + row = layout.row(align=True) + sp = row.split(percentage=0.5) + sp.label("3D View > Tool shelf > UV Manipulation (Edit mode)") + sp = sp.split(percentage=1.0) + col = sp.column(align=True) + col.label("Flip/Rotate UV") + col.label("Mirror UV") + col.label("Move UV") + col.label("World Scale UV") + col.label("Preserve UV Aspect") + col.label("Texture Lock") + col.label("Texture Wrap") + col.label("UV Sculpt") + + row = layout.row(align=True) + sp = row.split(percentage=0.5) + sp.label("3D View > Tool shelf > UV Manipulation (Edit mode)") + sp = sp.split(percentage=1.0) + col = sp.column(align=True) + col.label("Unwrap Constraint") + col.label("Texture Projection") + col.label("UVW") + + row = layout.row(align=True) + sp = row.split(percentage=0.5) + sp.label("UV/Image Editor > Tool shelf > Copy/Paste UV") + sp = sp.split(percentage=1.0) + col = sp.column(align=True) + col.label("Copy/Paste UV (Among faces in UV/Image Editor)") + + row = layout.row(align=True) + sp = row.split(percentage=0.5) + sp.label("UV/Image Editor > Tool shelf > UV Manipulation") + sp = sp.split(percentage=1.0) + col = sp.column(align=True) + col.label("Align UV") + col.label("Smooth UV") + col.label("Select UV") + col.label("Pack UV (Extension)") + + row = layout.row(align=True) + sp = row.split(percentage=0.5) + sp.label("UV/Image Editor > Tool shelf > Editor Enhancement") + sp = sp.split(percentage=1.0) + col = sp.column(align=True) + col.label("Align UV Cursor") + col.label("UV Cursor Location") + col.label("UV Bounding Box") + col.label("UV Inspection") diff --git a/uv_magic_uv/properites.py b/uv_magic_uv/properites.py new file mode 100644 index 000000000..f40e9f1fb --- /dev/null +++ b/uv_magic_uv/properites.py @@ -0,0 +1,755 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +import bpy +from bpy.props import ( + FloatProperty, + EnumProperty, + BoolProperty, + FloatVectorProperty, + IntProperty +) +from mathutils import Vector + +from . import common + + +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 + texlock = None + texproj = None + texwrap = None + mvuv = None + uvinsp = None + uvsculpt = 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.texlock = MUV_TexLockProps() + self.texproj = MUV_TexProjProps() + self.texwrap = MUV_TexWrapProps() + self.mvuv = MUV_MVUVProps() + self.uvinsp = MUV_UVInspProps() + self.uvsculpt = MUV_UVSculptProps() + + +class MUV_CPUVProps(): + src_uvs = [] + src_pin_uvs = [] + src_seams = [] + + +class MUV_CPUVSelSeqProps(): + src_uvs = [] + src_pin_uvs = [] + src_seams = [] + + +class MUV_TransUVProps(): + topology_copied = [] + + +class MUV_TexProjProps(): + running = False + + +class MUV_UVBBProps(): + uv_info_ini = [] + ctrl_points_ini = [] + ctrl_points = [] + running = False + + +class MUV_TexLockProps(): + verts_orig = None + intr_verts_orig = None + intr_running = False + + +class MUV_TexWrapProps(): + ref_face_index = -1 + ref_obj = None + + +class MUV_MVUVProps(): + running = False + + +class MUV_UVInspProps(): + display_running = False + overlapped_info = [] + flipped_info = [] + + +class MUV_UVSculptProps(): + running = False + + +def init_props(scene): + scene.muv_props = MUV_Properties() + + # UV Sculpt + scene.muv_uvsculpt_enabled = BoolProperty( + name="UV Sculpt", + description="UV Sculpt is enabled", + default=False + ) + scene.muv_uvsculpt_radius = IntProperty( + name="Radius", + description="Radius of the brush", + min=1, + max=500, + default=30 + ) + scene.muv_uvsculpt_strength = FloatProperty( + name="Strength", + description="How powerful the effect of the brush when applied", + min=0.0, + max=1.0, + default=0.03, + ) + scene.muv_uvsculpt_tools = EnumProperty( + name="Tools", + description="Select Tools for the UV sculpt brushes", + items=[ + ('GRAB', "Grab", "Grab UVs"), + ('RELAX', "Relax", "Relax UVs"), + ('PINCH', "Pinch", "Pinch UVs") + ], + default='GRAB' + ) + scene.muv_uvsculpt_show_brush = BoolProperty( + name="Show Brush", + description="Show Brush", + default=True + ) + scene.muv_uvsculpt_pinch_invert = BoolProperty( + name="Invert", + description="Pinch UV to invert direction", + default=False + ) + scene.muv_uvsculpt_relax_method = EnumProperty( + name="Method", + description="Algorithm used for relaxation", + items=[ + ('HC', "HC", "Use HC method for relaxation"), + ('LAPLACIAN', "Laplacian", "Use laplacian method for relaxation") + ], + default='HC' + ) + + # Texture Wrap + scene.muv_texwrap_enabled = BoolProperty( + name="Texture Wrap", + description="Texture Wrap is enabled", + default=False + ) + scene.muv_texwrap_set_and_refer = BoolProperty( + name="Set and Refer", + description="Refer and set UV", + default=True + ) + scene.muv_texwrap_selseq = BoolProperty( + name="Selection Sequence", + description="Set UV sequentially", + default=False + ) + + # UV inspection + scene.muv_seluv_enabled = BoolProperty( + name="Select UV Enabled", + description="Select UV is enabled", + default=False + ) + scene.muv_uvinsp_enabled = BoolProperty( + name="UV Inspection Enabled", + description="UV Inspection is enabled", + default=False + ) + scene.muv_uvinsp_show_overlapped = BoolProperty( + name="Overlapped", + description="Show overlapped UVs", + default=False + ) + scene.muv_uvinsp_show_flipped = BoolProperty( + name="Flipped", + description="Show flipped UVs", + default=False + ) + scene.muv_uvinsp_show_mode = EnumProperty( + name="Mode", + description="Show mode", + items=[ + ('PART', "Part", "Show only overlapped/flipped part"), + ('FACE', "Face", "Show overlapped/flipped face") + ], + default='PART' + ) + + # Align UV + scene.muv_auv_enabled = BoolProperty( + name="Aline UV Enabled", + description="Align UV is enabled", + default=False + ) + scene.muv_auv_transmission = BoolProperty( + name="Transmission", + description="Align linked UVs", + default=False + ) + scene.muv_auv_select = BoolProperty( + name="Select", + description="Select UVs which are aligned", + default=False + ) + scene.muv_auv_vertical = BoolProperty( + name="Vert-Infl (Vertical)", + description="Align vertical direction influenced " + "by mesh vertex proportion", + default=False + ) + scene.muv_auv_horizontal = BoolProperty( + name="Vert-Infl (Horizontal)", + description="Align horizontal direction influenced " + "by mesh vertex proportion", + default=False + ) + scene.muv_auv_location = EnumProperty( + name="Location", + description="Align location", + items=[ + ('LEFT_TOP', "Left/Top", "Align to Left or Top"), + ('MIDDLE', "Middle", "Align to middle"), + ('RIGHT_BOTTOM', "Right/Bottom", "Align to Right or Bottom") + ], + default='MIDDLE' + ) + + # Smooth UV + scene.muv_smuv_enabled = BoolProperty( + name="Smooth UV Enabled", + description="Smooth UV is enabled", + default=False + ) + scene.muv_smuv_transmission = BoolProperty( + name="Transmission", + description="Smooth linked UVs", + default=False + ) + scene.muv_smuv_mesh_infl = FloatProperty( + name="Mesh Influence", + description="Influence rate of mesh vertex", + min=0.0, + max=1.0, + default=0.0 + ) + scene.muv_smuv_select = BoolProperty( + name="Select", + description="Select UVs which are smoothed", + default=False + ) + + # UV Bounding Box + scene.muv_uvbb_enabled = BoolProperty( + name="UV Bounding Box Enabled", + description="UV Bounding Box is enabled", + default=False + ) + scene.muv_uvbb_uniform_scaling = BoolProperty( + name="Uniform Scaling", + description="Enable Uniform Scaling", + default=False + ) + scene.muv_uvbb_boundary = EnumProperty( + name="Boundary", + description="Boundary", + default='UV_SEL', + items=[ + ('UV', "UV", "Boundary is decided by UV"), + ('UV_SEL', "UV (Selected)", "Boundary is decided by Selected UV") + ] + ) + + # Pack UV + scene.muv_packuv_enabled = BoolProperty( + name="Pack UV Enabled", + description="Pack UV is enabled", + default=False + ) + scene.muv_packuv_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 + ) + scene.muv_packuv_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 + ) + + # Move UV + scene.muv_mvuv_enabled = BoolProperty( + name="Move UV Enabled", + description="Move UV is enabled", + default=False + ) + + # UVW + scene.muv_uvw_enabled = BoolProperty( + name="UVW Enabled", + description="UVW is enabled", + default=False + ) + scene.muv_uvw_assign_uvmap = BoolProperty( + name="Assign UVMap", + description="Assign UVMap when no UVmaps are available", + default=True + ) + + # Texture Projection + scene.muv_texproj_enabled = BoolProperty( + name="Texture Projection Enabled", + description="Texture Projection is enabled", + 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 + ) + scene.muv_texproj_assign_uvmap = BoolProperty( + name="Assign UVMap", + description="Assign UVMap when no UVmaps are available", + default=True + ) + + # Texture Lock + scene.muv_texlock_enabled = BoolProperty( + name="Texture Lock Enabled", + description="Texture Lock is enabled", + default=False + ) + scene.muv_texlock_connect = BoolProperty( + name="Connect UV", + default=True + ) + + # World Scale UV + scene.muv_wsuv_enabled = BoolProperty( + name="World Scale UV Enabled", + description="World Scale UV is enabled", + default=False + ) + scene.muv_wsuv_src_mesh_area = FloatProperty( + name="Mesh Area", + description="Source Mesh Area", + default=0.0, + min=0.0 + ) + scene.muv_wsuv_src_uv_area = FloatProperty( + name="UV Area", + description="Source UV Area", + default=0.0, + min=0.0 + ) + scene.muv_wsuv_src_density = FloatProperty( + name="Density", + description="Source Texel Density", + default=0.0, + min=0.0 + ) + scene.muv_wsuv_tgt_density = FloatProperty( + name="Density", + description="Target Texel Density", + default=0.0, + min=0.0 + ) + scene.muv_wsuv_mode = EnumProperty( + name="Mode", + description="Density calculation mode", + items=[ + ('PROPORTIONAL', 'Proportional', 'Scale proportionally by mesh'), + ('SCALING', 'Scaling', 'Specify scale factor'), + ('USER', 'User', 'Specify density'), + ('CONSTANT', 'Constant', 'Constant density') + ], + default='CONSTANT' + ) + scene.muv_wsuv_scaling_factor = FloatProperty( + name="Scaling Factor", + default=1.0, + max=1000.0, + min=0.00001 + ) + scene.muv_wsuv_origin = EnumProperty( + name="Origin", + description="Aspect Origin", + items=[ + ('CENTER', 'Center', 'Center'), + ('LEFT_TOP', 'Left Top', 'Left Bottom'), + ('LEFT_CENTER', 'Left Center', 'Left Center'), + ('LEFT_BOTTOM', 'Left Bottom', 'Left Bottom'), + ('CENTER_TOP', 'Center Top', 'Center Top'), + ('CENTER_BOTTOM', 'Center Bottom', 'Center Bottom'), + ('RIGHT_TOP', 'Right Top', 'Right Top'), + ('RIGHT_CENTER', 'Right Center', 'Right Center'), + ('RIGHT_BOTTOM', 'Right Bottom', 'Right Bottom') + + ], + default='CENTER' + ) + + # Unwrap Constraint + scene.muv_unwrapconst_enabled = BoolProperty( + name="Unwrap Constraint Enabled", + description="Unwrap Constraint is enabled", + default=False + ) + scene.muv_unwrapconst_u_const = BoolProperty( + name="U-Constraint", + description="Keep UV U-axis coordinate", + default=False + ) + scene.muv_unwrapconst_v_const = BoolProperty( + name="V-Constraint", + description="Keep UV V-axis coordinate", + default=False + ) + + # Preserve UV Aspect + scene.muv_preserve_uv_enabled = BoolProperty( + name="Preserve UV Aspect Enabled", + description="Preserve UV Aspect is enabled", + default=False + ) + scene.muv_preserve_uv_tex_image = EnumProperty( + name="Image", + description="Texture Image", + items=get_loaded_texture_name + ) + scene.muv_preserve_uv_origin = EnumProperty( + name="Origin", + description="Aspect Origin", + items=[ + ('CENTER', 'Center', 'Center'), + ('LEFT_TOP', 'Left Top', 'Left Bottom'), + ('LEFT_CENTER', 'Left Center', 'Left Center'), + ('LEFT_BOTTOM', 'Left Bottom', 'Left Bottom'), + ('CENTER_TOP', 'Center Top', 'Center Top'), + ('CENTER_BOTTOM', 'Center Bottom', 'Center Bottom'), + ('RIGHT_TOP', 'Right Top', 'Right Top'), + ('RIGHT_CENTER', 'Right Center', 'Right Center'), + ('RIGHT_BOTTOM', 'Right Bottom', 'Right Bottom') + + ], + default="CENTER" + ) + + # Flip/Rotate UV + scene.muv_fliprot_enabled = BoolProperty( + name="Flip/Rotate UV Enabled", + description="Flip/Rotate UV is enabled", + default=False + ) + scene.muv_fliprot_seams = BoolProperty( + name="Seams", + description="Seams", + default=True + ) + + # Mirror UV + scene.muv_mirroruv_enabled = BoolProperty( + name="Mirror UV Enabled", + description="Mirror UV is enabled", + default=False + ) + scene.muv_mirroruv_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' + ) + + # Copy/Paste UV + scene.muv_cpuv_enabled = BoolProperty( + name="Copy/Paste UV Enabled", + description="Copy/Paste UV is enabled", + default=False + ) + scene.muv_cpuv_copy_seams = BoolProperty( + name="Copy Seams", + description="Copy Seams", + default=True + ) + scene.muv_cpuv_mode = EnumProperty( + items=[ + ('DEFAULT', "Default", "Default Mode"), + ('SEL_SEQ', "Selection Sequence", "Selection Sequence Mode") + ], + name="Copy/Paste UV Mode", + description="Copy/Paste UV Mode", + default='DEFAULT' + ) + scene.muv_cpuv_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' + ) + + # Transfer UV + scene.muv_transuv_enabled = BoolProperty( + name="Transfer UV Enabled", + description="Transfer UV is enabled", + default=False + ) + scene.muv_transuv_invert_normals = BoolProperty( + name="Invert Normals", + description="Invert Normals", + default=False + ) + scene.muv_transuv_copy_seams = BoolProperty( + name="Copy Seams", + description="Copy Seams", + default=True + ) + + # Align UV Cursor + def auvc_get_cursor_loc(self): + area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW', + 'IMAGE_EDITOR') + bd_size = common.get_uvimg_editor_board_size(area) + loc = space.cursor_location + if bd_size[0] < 0.000001: + cx = 0.0 + else: + cx = loc[0] / bd_size[0] + if bd_size[1] < 0.000001: + cy = 0.0 + else: + cy = loc[1] / bd_size[1] + self['muv_auvc_cursor_loc'] = Vector((cx, cy)) + return self.get('muv_auvc_cursor_loc', (0.0, 0.0)) + + def auvc_set_cursor_loc(self, value): + self['muv_auvc_cursor_loc'] = value + area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW', + 'IMAGE_EDITOR') + bd_size = common.get_uvimg_editor_board_size(area) + cx = bd_size[0] * value[0] + cy = bd_size[1] * value[1] + space.cursor_location = Vector((cx, cy)) + + scene.muv_auvc_enabled = BoolProperty( + name="Align UV Cursor Enabled", + description="Align UV Cursor is enabled", + default=False + ) + scene.muv_auvc_cursor_loc = FloatVectorProperty( + name="UV Cursor Location", + size=2, + precision=4, + soft_min=-1.0, + soft_max=1.0, + step=1, + default=(0.000, 0.000), + get=auvc_get_cursor_loc, + set=auvc_set_cursor_loc + ) + scene.muv_auvc_align_menu = EnumProperty( + name="Align Method", + description="Align Method", + default='TEXTURE', + items=[ + ('TEXTURE', "Texture", "Align to texture"), + ('UV', "UV", "Align to UV"), + ('UV_SEL', "UV (Selected)", "Align to Selected UV") + ] + ) + + +def clear_props(scene): + del scene.muv_props + + # UV Sculpt + del scene.muv_uvsculpt_enabled + del scene.muv_uvsculpt_radius + del scene.muv_uvsculpt_strength + del scene.muv_uvsculpt_tools + del scene.muv_uvsculpt_show_brush + del scene.muv_uvsculpt_pinch_invert + del scene.muv_uvsculpt_relax_method + + # Texture Wrap + del scene.muv_texwrap_enabled + del scene.muv_texwrap_set_and_refer + del scene.muv_texwrap_selseq + + # UV Inspection + del scene.muv_seluv_enabled + del scene.muv_uvinsp_enabled + del scene.muv_uvinsp_show_overlapped + del scene.muv_uvinsp_show_flipped + del scene.muv_uvinsp_show_mode + + # Align UV + del scene.muv_auv_enabled + del scene.muv_auv_transmission + del scene.muv_auv_select + del scene.muv_auv_vertical + del scene.muv_auv_horizontal + del scene.muv_auv_location + + # Smooth UV + del scene.muv_smuv_enabled + del scene.muv_smuv_transmission + del scene.muv_smuv_mesh_infl + del scene.muv_smuv_select + + # UV Bounding Box + del scene.muv_uvbb_enabled + del scene.muv_uvbb_uniform_scaling + del scene.muv_uvbb_boundary + + # Pack UV + del scene.muv_packuv_enabled + del scene.muv_packuv_allowable_center_deviation + del scene.muv_packuv_allowable_size_deviation + + # Move UV + del scene.muv_mvuv_enabled + + # UVW + del scene.muv_uvw_enabled + del scene.muv_uvw_assign_uvmap + + # Texture Projection + del scene.muv_texproj_enabled + 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 + del scene.muv_texproj_assign_uvmap + + # Texture Lock + del scene.muv_texlock_enabled + del scene.muv_texlock_connect + + # World Scale UV + del scene.muv_wsuv_enabled + del scene.muv_wsuv_src_mesh_area + del scene.muv_wsuv_src_uv_area + del scene.muv_wsuv_src_density + del scene.muv_wsuv_tgt_density + del scene.muv_wsuv_mode + del scene.muv_wsuv_scaling_factor + del scene.muv_wsuv_origin + + # Unwrap Constraint + del scene.muv_unwrapconst_enabled + del scene.muv_unwrapconst_u_const + del scene.muv_unwrapconst_v_const + + # Preserve UV Aspect + del scene.muv_preserve_uv_enabled + del scene.muv_preserve_uv_tex_image + del scene.muv_preserve_uv_origin + + # Flip/Rotate UV + del scene.muv_fliprot_enabled + del scene.muv_fliprot_seams + + # Mirror UV + del scene.muv_mirroruv_enabled + del scene.muv_mirroruv_axis + + # Copy/Paste UV + del scene.muv_cpuv_enabled + del scene.muv_cpuv_copy_seams + del scene.muv_cpuv_mode + del scene.muv_cpuv_strategy + + # Transfer UV + del scene.muv_transuv_enabled + del scene.muv_transuv_invert_normals + del scene.muv_transuv_copy_seams + + # Align UV Cursor + del scene.muv_auvc_enabled + del scene.muv_auvc_cursor_loc + del scene.muv_auvc_align_menu diff --git a/uv_magic_uv/ui/__init__.py b/uv_magic_uv/ui/__init__.py new file mode 100644 index 000000000..00af3e062 --- /dev/null +++ b/uv_magic_uv/ui/__init__.py @@ -0,0 +1,44 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +if "bpy" in locals(): + import importlib + importlib.reload(view3d_copy_paste_uv_objectmode) + importlib.reload(view3d_copy_paste_uv_editmode) + importlib.reload(view3d_uv_manipulation) + importlib.reload(view3d_uv_mapping) + importlib.reload(uvedit_copy_paste_uv) + importlib.reload(uvedit_uv_manipulation) + importlib.reload(uvedit_editor_enhance) +else: + from . import view3d_copy_paste_uv_objectmode + from . import view3d_copy_paste_uv_editmode + from . import view3d_uv_manipulation + from . import view3d_uv_mapping + from . import uvedit_copy_paste_uv + from . import uvedit_uv_manipulation + from . import uvedit_editor_enhance + +import bpy diff --git a/uv_magic_uv/ui/uvedit_copy_paste_uv.py b/uv_magic_uv/ui/uvedit_copy_paste_uv.py new file mode 100644 index 000000000..87b23fed7 --- /dev/null +++ b/uv_magic_uv/ui/uvedit_copy_paste_uv.py @@ -0,0 +1,54 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +import bpy + +from ..op import copy_paste_uv_uvedit + + +class IMAGE_PT_MUV_CPUV(bpy.types.Panel): + """ + Panel class: Copy/Paste UV on Property Panel on UV/ImageEditor + """ + + bl_space_type = 'IMAGE_EDITOR' + bl_region_type = 'TOOLS' + bl_label = "Copy/Paste UV" + bl_category = "Magic UV" + bl_context = 'mesh_edit' + bl_options = {'DEFAULT_CLOSED'} + + def draw_header(self, _): + layout = self.layout + layout.label(text="", icon='IMAGE_COL') + + def draw(self, _): + layout = self.layout + + row = layout.row(align=True) + row.operator(copy_paste_uv_uvedit.MUV_CPUVIECopyUV.bl_idname, + text="Copy") + row.operator(copy_paste_uv_uvedit.MUV_CPUVIEPasteUV.bl_idname, + text="Paste") diff --git a/uv_magic_uv/ui/uvedit_editor_enhance.py b/uv_magic_uv/ui/uvedit_editor_enhance.py new file mode 100644 index 000000000..dfe309782 --- /dev/null +++ b/uv_magic_uv/ui/uvedit_editor_enhance.py @@ -0,0 +1,136 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +import bpy + +from ..op import align_uv_cursor +from ..op import uv_bounding_box +from ..op import uv_inspection + + +class IMAGE_PT_MUV_EE(bpy.types.Panel): + """ + Panel class: UV/Image Editor Enhancement + """ + + bl_space_type = 'IMAGE_EDITOR' + bl_region_type = 'TOOLS' + bl_label = "Editor Enhancement" + bl_category = "Magic UV" + bl_context = 'mesh_edit' + bl_options = {'DEFAULT_CLOSED'} + + def draw_header(self, _): + layout = self.layout + layout.label(text="", icon='IMAGE_COL') + + def draw(self, context): + layout = self.layout + sc = context.scene + props = sc.muv_props + + box = layout.box() + box.prop(sc, "muv_auvc_enabled", text="Align UV Cursor") + if sc.muv_auvc_enabled: + box.prop(sc, "muv_auvc_align_menu", expand=True) + + col = box.column(align=True) + + row = col.row(align=True) + ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname, + text="Left Top") + ops.position = 'LEFT_TOP' + ops.base = sc.muv_auvc_align_menu + ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname, + text="Middle Top") + ops.position = 'MIDDLE_TOP' + ops.base = sc.muv_auvc_align_menu + ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname, + text="Right Top") + ops.position = 'RIGHT_TOP' + ops.base = sc.muv_auvc_align_menu + + row = col.row(align=True) + ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname, + text="Left Middle") + ops.position = 'LEFT_MIDDLE' + ops.base = sc.muv_auvc_align_menu + ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname, + text="Center") + ops.position = 'CENTER' + ops.base = sc.muv_auvc_align_menu + ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname, + text="Right Middle") + ops.position = 'RIGHT_MIDDLE' + ops.base = sc.muv_auvc_align_menu + + row = col.row(align=True) + ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname, + text="Left Bottom") + ops.position = 'LEFT_BOTTOM' + ops.base = sc.muv_auvc_align_menu + ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname, + text="Middle Bottom") + ops.position = 'MIDDLE_BOTTOM' + ops.base = sc.muv_auvc_align_menu + ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname, + text="Right Bottom") + ops.position = 'RIGHT_BOTTOM' + ops.base = sc.muv_auvc_align_menu + + box = layout.box() + box.prop(sc, "muv_uvcloc_enabled", text="UV Cursor Location") + if sc.muv_uvcloc_enabled: + box.prop(sc, "muv_auvc_cursor_loc", text="") + + box = layout.box() + box.prop(sc, "muv_uvbb_enabled", text="UV Bounding Box") + if sc.muv_uvbb_enabled: + if props.uvbb.running is False: + box.operator(uv_bounding_box.MUV_UVBBUpdater.bl_idname, + text="Display", icon='PLAY') + else: + box.operator(uv_bounding_box.MUV_UVBBUpdater.bl_idname, + text="Hide", icon='PAUSE') + box.prop(sc, "muv_uvbb_uniform_scaling", text="Uniform Scaling") + box.prop(sc, "muv_uvbb_boundary", text="Boundary") + + box = layout.box() + box.prop(sc, "muv_uvinsp_enabled", text="UV Inspection") + if sc.muv_uvinsp_enabled: + row = box.row() + if not sc.muv_props.uvinsp.display_running: + row.operator(uv_inspection.MUV_UVInspDisplay.bl_idname, + text="Display", icon='PLAY') + else: + row.operator(uv_inspection.MUV_UVInspDisplay.bl_idname, + text="Hide", icon='PAUSE') + row.operator(uv_inspection.MUV_UVInspUpdate.bl_idname, + text="Update") + row = box.row() + row.prop(sc, "muv_uvinsp_show_overlapped") + row.prop(sc, "muv_uvinsp_show_flipped") + row = box.row() + row.prop(sc, "muv_uvinsp_show_mode") diff --git a/uv_magic_uv/ui/uvedit_uv_manipulation.py b/uv_magic_uv/ui/uvedit_uv_manipulation.py new file mode 100644 index 000000000..2231cdf4b --- /dev/null +++ b/uv_magic_uv/ui/uvedit_uv_manipulation.py @@ -0,0 +1,117 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +import bpy + +from ..op import uv_inspection +from ..op import align_uv +from ..op import smooth_uv +from ..op import pack_uv + + +class IMAGE_PT_MUV_UVManip(bpy.types.Panel): + """ + Panel class: UV Manipulation on Property Panel on UV/ImageEditor + """ + + bl_space_type = 'IMAGE_EDITOR' + bl_region_type = 'TOOLS' + bl_label = "UV Manipulation" + bl_category = "Magic UV" + bl_context = 'mesh_edit' + bl_options = {'DEFAULT_CLOSED'} + + def draw_header(self, _): + layout = self.layout + layout.label(text="", icon='IMAGE_COL') + + def draw(self, context): + sc = context.scene + layout = self.layout + + box = layout.box() + box.prop(sc, "muv_auv_enabled", text="Align UV") + if sc.muv_auv_enabled: + col = box.column() + row = col.row(align=True) + ops = row.operator(align_uv.MUV_AUVCircle.bl_idname, text="Circle") + ops.transmission = sc.muv_auv_transmission + ops.select = sc.muv_auv_select + ops = row.operator(align_uv.MUV_AUVStraighten.bl_idname, + text="Straighten") + ops.transmission = sc.muv_auv_transmission + ops.select = sc.muv_auv_select + ops.vertical = sc.muv_auv_vertical + ops.horizontal = sc.muv_auv_horizontal + row = col.row() + ops = row.operator(align_uv.MUV_AUVAxis.bl_idname, text="XY-axis") + ops.transmission = sc.muv_auv_transmission + ops.select = sc.muv_auv_select + ops.vertical = sc.muv_auv_vertical + ops.horizontal = sc.muv_auv_horizontal + ops.location = sc.muv_auv_location + row.prop(sc, "muv_auv_location", text="") + + col = box.column(align=True) + row = col.row(align=True) + row.prop(sc, "muv_auv_transmission", text="Transmission") + row.prop(sc, "muv_auv_select", text="Select") + row = col.row(align=True) + row.prop(sc, "muv_auv_vertical", text="Vertical") + row.prop(sc, "muv_auv_horizontal", text="Horizontal") + + box = layout.box() + box.prop(sc, "muv_smuv_enabled", text="Smooth UV") + if sc.muv_smuv_enabled: + ops = box.operator(smooth_uv.MUV_AUVSmooth.bl_idname, + text="Smooth") + ops.transmission = sc.muv_smuv_transmission + ops.select = sc.muv_smuv_select + ops.mesh_infl = sc.muv_smuv_mesh_infl + col = box.column(align=True) + row = col.row(align=True) + row.prop(sc, "muv_smuv_transmission", text="Transmission") + row.prop(sc, "muv_smuv_select", text="Select") + col.prop(sc, "muv_smuv_mesh_infl", text="Mesh Influence") + + box = layout.box() + box.prop(sc, "muv_seluv_enabled", text="Select UV") + if sc.muv_seluv_enabled: + row = box.row(align=True) + row.operator(uv_inspection.MUV_UVInspSelectOverlapped.bl_idname) + row.operator(uv_inspection.MUV_UVInspSelectFlipped.bl_idname) + + box = layout.box() + box.prop(sc, "muv_packuv_enabled", text="Pack UV (Extension)") + if sc.muv_packuv_enabled: + ops = box.operator(pack_uv.MUV_PackUV.bl_idname, text="Pack UV") + ops.allowable_center_deviation = \ + sc.muv_packuv_allowable_center_deviation + ops.allowable_size_deviation = \ + sc.muv_packuv_allowable_size_deviation + box.label("Allowable Center Deviation:") + box.prop(sc, "muv_packuv_allowable_center_deviation", text="") + box.label("Allowable Size Deviation:") + box.prop(sc, "muv_packuv_allowable_size_deviation", text="") diff --git a/uv_magic_uv/ui/view3d_copy_paste_uv_editmode.py b/uv_magic_uv/ui/view3d_copy_paste_uv_editmode.py new file mode 100644 index 000000000..530b17977 --- /dev/null +++ b/uv_magic_uv/ui/view3d_copy_paste_uv_editmode.py @@ -0,0 +1,81 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +import bpy + +from ..op import copy_paste_uv +from ..op import transfer_uv + + +class OBJECT_PT_MUV_CPUV(bpy.types.Panel): + """ + Panel class: Copy/Paste UV on Property Panel on View3D + """ + + bl_space_type = 'VIEW_3D' + bl_region_type = 'TOOLS' + bl_label = "Copy/Paste UV" + bl_category = "Magic UV" + bl_context = 'mesh_edit' + bl_options = {'DEFAULT_CLOSED'} + + def draw_header(self, _): + layout = self.layout + layout.label(text="", icon='IMAGE_COL') + + def draw(self, context): + sc = context.scene + layout = self.layout + + box = layout.box() + box.prop(sc, "muv_cpuv_enabled", text="Copy/Paste UV") + if sc.muv_cpuv_enabled: + row = box.row(align=True) + if sc.muv_cpuv_mode == 'DEFAULT': + row.menu(copy_paste_uv.MUV_CPUVCopyUVMenu.bl_idname, + text="Copy") + row.menu(copy_paste_uv.MUV_CPUVPasteUVMenu.bl_idname, + text="Paste") + elif sc.muv_cpuv_mode == 'SEL_SEQ': + row.menu(copy_paste_uv.MUV_CPUVSelSeqCopyUVMenu.bl_idname, + text="Copy") + row.menu(copy_paste_uv.MUV_CPUVSelSeqPasteUVMenu.bl_idname, + text="Paste") + box.prop(sc, "muv_cpuv_mode", expand=True) + box.prop(sc, "muv_cpuv_copy_seams", text="Seams") + box.prop(sc, "muv_cpuv_strategy", text="Strategy") + + box = layout.box() + box.prop(sc, "muv_transuv_enabled", text="Transfer UV") + if sc.muv_transuv_enabled: + row = box.row(align=True) + row.operator(transfer_uv.MUV_TransUVCopy.bl_idname, text="Copy") + ops = row.operator(transfer_uv.MUV_TransUVPaste.bl_idname, + text="Paste") + ops.invert_normals = sc.muv_transuv_invert_normals + ops.copy_seams = sc.muv_transuv_copy_seams + row = box.row() + row.prop(sc, "muv_transuv_invert_normals", text="Invert Normals") + row.prop(sc, "muv_transuv_copy_seams", text="Seams") diff --git a/uv_magic_uv/ui/view3d_copy_paste_uv_objectmode.py b/uv_magic_uv/ui/view3d_copy_paste_uv_objectmode.py new file mode 100644 index 000000000..5aa968f27 --- /dev/null +++ b/uv_magic_uv/ui/view3d_copy_paste_uv_objectmode.py @@ -0,0 +1,56 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +import bpy + +from ..op import copy_paste_uv_object + + +class OBJECT_PT_MUV_CPUVObj(bpy.types.Panel): + """ + Panel class: Copy/Paste UV on Property Panel on View3D + """ + + bl_space_type = 'VIEW_3D' + bl_region_type = 'TOOLS' + bl_label = "Copy/Paste UV" + bl_category = "Magic UV" + bl_context = 'objectmode' + bl_options = {'DEFAULT_CLOSED'} + + def draw_header(self, _): + layout = self.layout + layout.label(text="", icon='IMAGE_COL') + + def draw(self, context): + sc = context.scene + layout = self.layout + + row = layout.row(align=True) + row.menu(copy_paste_uv_object.MUV_CPUVObjCopyUVMenu.bl_idname, + text="Copy") + row.menu(copy_paste_uv_object.MUV_CPUVObjPasteUVMenu.bl_idname, + text="Paste") + layout.prop(sc, "muv_cpuv_copy_seams", text="Copy Seams") diff --git a/uv_magic_uv/ui/view3d_uv_manipulation.py b/uv_magic_uv/ui/view3d_uv_manipulation.py new file mode 100644 index 000000000..76e0d3aac --- /dev/null +++ b/uv_magic_uv/ui/view3d_uv_manipulation.py @@ -0,0 +1,180 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +import bpy + +from ..op import flip_rotate_uv +from ..op import mirror_uv +from ..op import move_uv +from ..op import preserve_uv_aspect +from ..op import texture_lock +from ..op import texture_wrap +from ..op import uv_sculpt +from ..op import world_scale_uv + + +class OBJECT_PT_MUV_UVManip(bpy.types.Panel): + """ + Panel class: UV Manipulation on Property Panel on View3D + """ + + bl_space_type = 'VIEW_3D' + bl_region_type = 'TOOLS' + bl_label = "UV Manipulation" + bl_category = "Magic UV" + bl_context = 'mesh_edit' + bl_options = {'DEFAULT_CLOSED'} + + def draw_header(self, _): + layout = self.layout + layout.label(text="", icon='IMAGE_COL') + + def draw(self, context): + sc = context.scene + props = sc.muv_props + layout = self.layout + + box = layout.box() + box.prop(sc, "muv_fliprot_enabled", text="Flip/Rotate UV") + if sc.muv_fliprot_enabled: + row = box.row() + ops = row.operator(flip_rotate_uv.MUV_FlipRot.bl_idname, + text="Flip/Rotate") + ops.seams = sc.muv_fliprot_seams + row.prop(sc, "muv_fliprot_seams", text="Seams") + + box = layout.box() + box.prop(sc, "muv_mirroruv_enabled", text="Mirror UV") + if sc.muv_mirroruv_enabled: + row = box.row() + ops = row.operator(mirror_uv.MUV_MirrorUV.bl_idname, text="Mirror") + ops.axis = sc.muv_mirroruv_axis + row.prop(sc, "muv_mirroruv_axis", text="") + + box = layout.box() + box.prop(sc, "muv_mvuv_enabled", text="Move UV") + if sc.muv_mvuv_enabled: + col = box.column() + col.operator(move_uv.MUV_MVUV.bl_idname, icon='PLAY', text="Start") + if props.mvuv.running: + col.enabled = False + else: + col.enabled = True + + box = layout.box() + box.prop(sc, "muv_wsuv_enabled", text="World Scale UV") + if sc.muv_wsuv_enabled: + row = box.row(align=True) + row.operator(world_scale_uv.MUV_WSUVMeasure.bl_idname, + text="Measure") + ops = row.operator(world_scale_uv.MUV_WSUVApply.bl_idname, + text="Apply") + ops.origin = sc.muv_wsuv_origin + box.label("Source:") + sp = box.split(percentage=0.7) + col = sp.column(align=True) + col.prop(sc, "muv_wsuv_src_mesh_area", text="Mesh Area") + col.prop(sc, "muv_wsuv_src_uv_area", text="UV Area") + col.prop(sc, "muv_wsuv_src_density", text="Density") + col.enabled = False + sp = sp.split(percentage=1.0) + col = sp.column(align=True) + col.label("cm x cm") + col.label("px x px") + col.label("px/cm") + col.enabled = False + sp = box.split(percentage=0.3) + sp.label("Mode:") + sp = sp.split(percentage=1.0) + col = sp.column() + col.prop(sc, "muv_wsuv_mode", text="") + if sc.muv_wsuv_mode == 'USER': + col.prop(sc, "muv_wsuv_tgt_density", text="Density") + if sc.muv_wsuv_mode == 'SCALING': + col.prop(sc, "muv_wsuv_scaling_factor", text="Scaling Factor") + box.prop(sc, "muv_wsuv_origin", text="Origin") + + box = layout.box() + box.prop(sc, "muv_preserve_uv_enabled", text="Preserve UV Aspect") + if sc.muv_preserve_uv_enabled: + row = box.row() + ops = row.operator( + preserve_uv_aspect.MUV_PreserveUVAspect.bl_idname, + text="Change Image") + ops.dest_img_name = sc.muv_preserve_uv_tex_image + ops.origin = sc.muv_preserve_uv_origin + row.prop(sc, "muv_preserve_uv_tex_image", text="") + box.prop(sc, "muv_preserve_uv_origin", text="Origin") + + box = layout.box() + box.prop(sc, "muv_texlock_enabled", text="Texture Lock") + if sc.muv_texlock_enabled: + row = box.row(align=True) + col = row.column(align=True) + col.label("Normal Mode:") + col = row.column(align=True) + col.operator(texture_lock.MUV_TexLockStart.bl_idname, text="Lock") + ops = col.operator(texture_lock.MUV_TexLockStop.bl_idname, + text="Unlock") + ops.connect = sc.muv_texlock_connect + col.prop(sc, "muv_texlock_connect", text="Connect") + + row = box.row(align=True) + row.label("Interactive Mode:") + if not props.texlock.intr_running: + row.operator(texture_lock.MUV_TexLockIntrStart.bl_idname, + icon='PLAY', text="Start") + else: + row.operator(texture_lock.MUV_TexLockIntrStop.bl_idname, + icon="PAUSE", text="Stop") + + box = layout.box() + box.prop(sc, "muv_texwrap_enabled", text="Texture Wrap") + if sc.muv_texwrap_enabled: + row = box.row(align=True) + row.operator(texture_wrap.MUV_TexWrapRefer.bl_idname, text="Refer") + row.operator(texture_wrap.MUV_TexWrapSet.bl_idname, text="Set") + box.prop(sc, "muv_texwrap_set_and_refer") + box.prop(sc, "muv_texwrap_selseq") + + box = layout.box() + box.prop(sc, "muv_uvsculpt_enabled", text="UV Sculpt") + if sc.muv_uvsculpt_enabled: + if not props.uvsculpt.running: + box.operator(uv_sculpt.MUV_UVSculptOps.bl_idname, + icon='PLAY', text="Start") + else: + box.operator(uv_sculpt.MUV_UVSculptOps.bl_idname, + icon='PAUSE', text="Stop") + col = box.column() + col.label("Brush:") + col.prop(sc, "muv_uvsculpt_radius") + col.prop(sc, "muv_uvsculpt_strength") + box.prop(sc, "muv_uvsculpt_tools") + if sc.muv_uvsculpt_tools == 'PINCH': + box.prop(sc, "muv_uvsculpt_pinch_invert") + elif sc.muv_uvsculpt_tools == 'RELAX': + box.prop(sc, "muv_uvsculpt_relax_method") + box.prop(sc, "muv_uvsculpt_show_brush") diff --git a/uv_magic_uv/ui/view3d_uv_mapping.py b/uv_magic_uv/ui/view3d_uv_mapping.py new file mode 100644 index 000000000..77c60c9ee --- /dev/null +++ b/uv_magic_uv/ui/view3d_uv_mapping.py @@ -0,0 +1,99 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.0" +__date__ = "16 Feb 2018" + +import bpy + +from ..op import texture_projection +from ..op import unwrap_constraint +from ..op import uvw + + +class OBJECT_PT_MUV_UVMapping(bpy.types.Panel): + """ + Panel class: UV Mapping on Property Panel on View3D + """ + + bl_space_type = 'VIEW_3D' + bl_region_type = 'TOOLS' + bl_label = "UV Mapping" + bl_category = "Magic UV" + bl_context = 'mesh_edit' + bl_options = {'DEFAULT_CLOSED'} + + def draw_header(self, _): + layout = self.layout + layout.label(text="", icon='IMAGE_COL') + + def draw(self, context): + sc = context.scene + props = sc.muv_props + layout = self.layout + + box = layout.box() + box.prop(sc, "muv_unwrapconst_enabled", text="Unwrap Constraint") + if sc.muv_unwrapconst_enabled: + ops = box.operator( + unwrap_constraint.MUV_UnwrapConstraint.bl_idname, + text="Unwrap") + ops.u_const = sc.muv_unwrapconst_u_const + ops.v_const = sc.muv_unwrapconst_v_const + row = box.row(align=True) + row.prop(sc, "muv_unwrapconst_u_const", text="U-Constraint") + row.prop(sc, "muv_unwrapconst_v_const", text="V-Constraint") + + box = layout.box() + box.prop(sc, "muv_texproj_enabled", text="Texture Projection") + if sc.muv_texproj_enabled: + row = box.row() + if not props.texproj.running: + row.operator(texture_projection.MUV_TexProjStart.bl_idname, + text="Start", icon='PLAY') + else: + row.operator(texture_projection.MUV_TexProjStop.bl_idname, + text="Stop", icon='PAUSE') + row.prop(sc, "muv_texproj_tex_image", text="") + box.prop(sc, "muv_texproj_tex_transparency", text="Transparency") + col = box.column(align=True) + row = col.row() + row.prop(sc, "muv_texproj_adjust_window", text="Adjust Window") + if not sc.muv_texproj_adjust_window: + row.prop(sc, "muv_texproj_tex_magnitude", text="Magnitude") + col.prop(sc, "muv_texproj_apply_tex_aspect", + text="Texture Aspect Ratio") + col.prop(sc, "muv_texproj_assign_uvmap", text="Assign UVMap") + if props.texproj.running: + box.operator(texture_projection.MUV_TexProjProject.bl_idname, + text="Project") + + box = layout.box() + box.prop(sc, "muv_uvw_enabled", text="UVW") + if sc.muv_uvw_enabled: + row = box.row(align=True) + ops = row.operator(uvw.MUV_UVWBoxMap.bl_idname, text="Box") + ops.assign_uvmap = sc.muv_uvw_assign_uvmap + ops = row.operator(uvw.MUV_UVWBestPlanerMap.bl_idname, + text="Best Planner") + ops.assign_uvmap = sc.muv_uvw_assign_uvmap + box.prop(sc, "muv_uvw_assign_uvmap", text="Assign UVMap") -- GitLab