diff options
author | Nutti <nutti.metro@gmail.com> | 2019-01-26 05:22:38 +0300 |
---|---|---|
committer | Nutti <nutti.metro@gmail.com> | 2019-01-26 05:22:38 +0300 |
commit | c034e1968465acb939efc089e63c5c51302947f5 (patch) | |
tree | 84f04a329e082ff4d7f860a926f8848cadb63428 /magic_uv | |
parent | 2eb519ceca77a4fe2fd5f8d071767db06aa01aa5 (diff) |
Magic UV: Release v6.0
Support Blender 2.8.
Diffstat (limited to 'magic_uv')
46 files changed, 13065 insertions, 0 deletions
diff --git a/magic_uv/__init__.py b/magic_uv/__init__.py new file mode 100644 index 00000000..5bac5fd2 --- /dev/null +++ b/magic_uv/__init__.py @@ -0,0 +1,87 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + + +bl_info = { + "name": "Magic UV", + "author": "Nutti, Mifth, Jace Priester, kgeogeo, mem, imdjs" + "Keith (Wahooney) Boshoff, McBuff, MaxRobinot, " + "Alexander Milovsky", + "version": (6, 0, 0), + "blender": (2, 80, 0), + "location": "See Add-ons Preferences", + "description": "UV Toolset. See Add-ons Preferences for details", + "warning": "", + "support": "COMMUNITY", + "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/" + "Py/Scripts/UV/Magic_UV", + "tracker_url": "https://github.com/nutti/Magic-UV", + "category": "UV" +} + + +if "bpy" in locals(): + import importlib + importlib.reload(common) + importlib.reload(utils) + utils.bl_class_registry.BlClassRegistry.cleanup() + importlib.reload(op) + importlib.reload(ui) + importlib.reload(properites) + importlib.reload(preferences) + importlib.reload(updater) +else: + import bpy + from . import common + from . import utils + from . import op + from . import ui + from . import properites + from . import preferences + from . import updater + +import bpy + + +def register(): + updater.register_updater(bl_info) + + utils.bl_class_registry.BlClassRegistry.register() + properites.init_props(bpy.types.Scene) + user_prefs = utils.compatibility.get_user_preferences(bpy.context) + if user_prefs.addons['magic_uv'].preferences.enable_builtin_menu: + preferences.add_builtin_menu() + + +def unregister(): + user_prefs = utils.compatibility.get_user_preferences(bpy.context) + if user_prefs.addons['magic_uv'].preferences.enable_builtin_menu: + preferences.remove_builtin_menu() + properites.clear_props(bpy.types.Scene) + utils.bl_class_registry.BlClassRegistry.unregister() + + +if __name__ == "__main__": + register() diff --git a/magic_uv/common.py b/magic_uv/common.py new file mode 100644 index 00000000..78a88308 --- /dev/null +++ b/magic_uv/common.py @@ -0,0 +1,1134 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +from collections import defaultdict +from pprint import pprint +from math import fabs, sqrt +import os + +import bpy +from mathutils import Vector +import bmesh + +from .utils import compatibility as compat + + +__DEBUG_MODE = False + + +def is_console_mode(): + if "MUV_CONSOLE_MODE" not in os.environ: + return False + return os.environ["MUV_CONSOLE_MODE"] == "True" + + +def is_debug_mode(): + return __DEBUG_MODE + + +def enable_debugg_mode(): + # pylint: disable=W0603 + global __DEBUG_MODE + __DEBUG_MODE = True + + +def disable_debug_mode(): + # pylint: disable=W0603 + global __DEBUG_MODE + __DEBUG_MODE = False + + +def debug_print(*s): + """ + Print message to console in debugging mode + """ + + if is_debug_mode(): + 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: + if compat.check_version(2, 80, 0) >= 0: + if region.width <= 1 or region.height <= 1: + continue + break + else: + return (area, None, None) + for space in area.spaces: + if space.type == space_type: + break + else: + return (area, region, None) + + return (area, region, space) + + +def mouse_on_region(event, area_type, region_type): + pos = Vector((event.mouse_x, event.mouse_y)) + + _, region, _ = get_space(area_type, region_type, "") + if region is None: + return False + + if (pos.x > region.x) and (pos.x < region.x + region.width) and \ + (pos.y > region.y) and (pos.y < region.y + region.height): + return True + + return False + + +def mouse_on_area(event, area_type): + pos = Vector((event.mouse_x, event.mouse_y)) + + area, _, _ = get_space(area_type, "", "") + if area is None: + return False + + if (pos.x > area.x) and (pos.x < area.x + area.width) and \ + (pos.y > area.y) and (pos.y < area.y + area.height): + return True + + return False + + +def mouse_on_regions(event, area_type, regions): + if not mouse_on_area(event, area_type): + return False + + for region in regions: + result = mouse_on_region(event, area_type, region) + if result: + return True + + return False + + +def create_bmesh(obj): + bm = bmesh.from_edit_mesh(obj.data) + if check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + return bm + + +def create_new_uv_map(obj, name=None): + uv_maps_old = {l.name for l in obj.data.uv_layers} + bpy.ops.mesh.uv_texture_add() + uv_maps_new = {l.name for l in obj.data.uv_layers} + diff = uv_maps_new - uv_maps_old + + if not list(diff): + return None # no more UV maps can not be created + + # rename UV map + new = obj.data.uv_layers[list(diff)[0]] + if name: + new.name = name + + return new + + +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 find_texture_layer(bm): + if check_version(2, 80, 0) >= 0: + return None + if bm.faces.layers.tex is None: + return None + + return bm.faces.layers.tex.verify() + + +def find_texture_nodes(obj): + nodes = [] + for mat in obj.material_slots: + if not mat.material.node_tree: + continue + for node in mat.material.node_tree.nodes: + tex_node_types = [ + 'TEX_ENVIRONMENT', + 'TEX_IMAGE', + ] + if node.type not in tex_node_types: + continue + if not node.image: + continue + nodes.append(node) + + return nodes + + +def find_image(obj, face=None, tex_layer=None): + # try to find from texture_layer + img = None + if tex_layer and face: + img = face[tex_layer].image + + # not found, then try to search from node + if not img: + nodes = find_texture_nodes(obj) + if len(nodes) >= 2: + raise RuntimeError("Find more than 2 texture nodes") + if len(nodes) == 1: + img = nodes[0].image + + return img + + +def measure_uv_area(obj, tex_size=None): + 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() + + tex_layer = find_texture_layer(bm) + + 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) + + # user specified + if tex_size: + uv_area = uv_area + f_uv_area * tex_size[0] * tex_size[1] + continue + + img = find_image(obj, f, tex_layer) + + # can not find from node, so we can not get texture size + if not img: + return None + + img_size = img.size + 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 separated" + + 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 vertices + 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, closed=False): + 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, closed) + if not loop_pairs: + return None, err + loop_seqs, err = __get_loop_sequence_internal(uv_layer, loop_pairs, + isl_info, closed) + if not loop_seqs: + return None, err + + return loop_seqs, "" + + +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() + + debug_print("===== Clip UV List =====") + debug_print(clip_uvs) + debug_print("===== Subject UV List =====") + 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()] + debug_print("===== Polygons Overlapped Completely =====") + 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 + + debug_print("===== Intersection List =====") + debug_print(intersections) + + # no intersection, so subject and clip is not overlapped + if not intersections: + return False, None + + def get_intersection_pair(intersects, key): + for sect in intersects: + 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 + + debug_print("===== Enter List =====") + debug_print(clip_entering) + debug_print(subject_entering) + debug_print("===== Exit List =====") + debug_print(clip_exiting) + 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, p, current, other_list): + result = current_list.find(current) + if not result: + return None + if result != current: + print("Internal Error") + return None + if not exiting: + print("Internal Error: No exiting UV") + return None + + # enter + if entering.count(current) >= 1: + entering.remove(current) + + current_list.find_and_next(current) + current = current_list.get() + + prev = None + error = False + while exiting.count(current) == 0: + p.append(current.copy()) + current_list.find_and_next(current) + current = current_list.get() + if prev == current: + error = True + break + prev = current + + if error: + print("Internal Error: Infinite loop") + return None + + # exit + p.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 is None: + break + + if current_uv_list == subject_uvs: + current_uv_list = clip_uvs + other_uv_list = subject_uvs + current_entering = clip_entering + current_exiting = clip_exiting + debug_print("-- Next: Clip --") + else: + current_uv_list = subject_uvs + other_uv_list = clip_uvs + current_entering = subject_entering + current_exiting = subject_exiting + debug_print("-- Next: Subject --") + + debug_print(clip_entering) + debug_print(clip_exiting) + debug_print(subject_entering) + debug_print(subject_exiting) + + if not clip_entering and not clip_exiting \ + and not subject_entering and not subject_exiting: + break + + polygons.append(poly) + + debug_print("===== Polygons Overlapped Partially =====") + debug_print(polygons) + + return True, polygons + + +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 = 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 __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 diff --git a/magic_uv/lib/__init__.py b/magic_uv/lib/__init__.py new file mode 100644 index 00000000..db6f9df9 --- /dev/null +++ b/magic_uv/lib/__init__.py @@ -0,0 +1,32 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +if "bpy" in locals(): + import importlib + importlib.reload(bglx) +else: + from . import bglx + +import bpy diff --git a/magic_uv/lib/bglx.py b/magic_uv/lib/bglx.py new file mode 100644 index 00000000..5abde12c --- /dev/null +++ b/magic_uv/lib/bglx.py @@ -0,0 +1,275 @@ +from threading import Lock + +import bgl +from bgl import Buffer as Buffer +import gpu +from gpu_extras.batch import batch_for_shader + +GL_LINES = 0 +GL_LINE_STRIP = 1 +GL_LINE_LOOP = 2 +GL_TRIANGLES = 5 +GL_TRIANGLE_FAN = 6 +GL_QUADS = 4 + +class InternalData: + __inst = None + __lock = Lock() + + def __init__(self): + raise NotImplementedError("Not allowed to call constructor") + + @classmethod + def __internal_new(cls): + inst = super().__new__(cls) + inst.color = [1.0, 1.0, 1.0, 1.0] + inst.line_width = 1.0 + + return inst + + @classmethod + def get_instance(cls): + if not cls.__inst: + with cls.__lock: + if not cls.__inst: + cls.__inst = cls.__internal_new() + + return cls.__inst + + def init(self): + self.clear() + + def set_prim_mode(self, mode): + self.prim_mode = mode + + def set_dims(self, dims): + self.dims = dims + + def add_vert(self, v): + self.verts.append(v) + + def add_tex_coord(self, uv): + self.tex_coords.append(uv) + + def set_color(self, c): + self.color = c + + def set_line_width(self, width): + self.line_width = width + + def clear(self): + self.prim_mode = None + self.verts = [] + self.dims = None + self.tex_coords = [] + + def get_verts(self): + return self.verts + + def get_dims(self): + return self.dims + + def get_prim_mode(self): + return self.prim_mode + + def get_color(self): + return self.color + + def get_line_width(self): + return self.line_width + + def get_tex_coords(self): + return self.tex_coords + + +def glLineWidth(width): + inst = InternalData.get_instance() + inst.set_line_width(width) + + +def glColor3f(r, g, b): + inst = InternalData.get_instance() + inst.set_color([r, g, b, 1.0]) + + +def glColor4f(r, g, b, a): + inst = InternalData.get_instance() + inst.set_color([r, g, b, a]) + + +def glRecti(x0, y0, x1, y1): + glBegin(GL_QUADS) + glVertex2f(x0, y0) + glVertex2f(x0, y1) + glVertex2f(x1, y1) + glVertex2f(x1, y0) + glEnd() + + +def glBegin(mode): + inst = InternalData.get_instance() + inst.init() + inst.set_prim_mode(mode) + + +def _get_transparency_shader(): + vertex_shader = ''' + uniform mat4 modelViewMatrix; + uniform mat4 projectionMatrix; + + in vec2 pos; + in vec2 texCoord; + out vec2 uvInterp; + + void main() + { + uvInterp = texCoord; + gl_Position = projectionMatrix * modelViewMatrix * vec4(pos.xy, 0.0, 1.0); + gl_Position.z = 1.0; + } + ''' + + fragment_shader = ''' + uniform sampler2D image; + uniform vec4 color; + + in vec2 uvInterp; + out vec4 fragColor; + + void main() + { + fragColor = texture(image, uvInterp); + fragColor.a = color.a; + } + ''' + + return vertex_shader, fragment_shader + + +def glEnd(): + inst = InternalData.get_instance() + + color = inst.get_color() + coords = inst.get_verts() + tex_coords = inst.get_tex_coords() + if inst.get_dims() == 2: + if len(tex_coords) == 0: + shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR') + else: + #shader = gpu.shader.from_builtin('2D_IMAGE') + vert_shader, frag_shader = _get_transparency_shader() + shader = gpu.types.GPUShader(vert_shader, frag_shader) + else: + raise NotImplemented("get_dims() != 2") + + if len(tex_coords) == 0: + data = { + "pos": coords, + } + else: + data = { + "pos": coords, + "texCoord": tex_coords + } + + if inst.get_prim_mode() == GL_LINES: + indices = [] + for i in range(0, len(coords), 2): + indices.append([i, i + 1]) + batch = batch_for_shader(shader, 'LINES', data, indices=indices) + + elif inst.get_prim_mode() == GL_LINE_STRIP: + batch = batch_for_shader(shader, 'LINE_STRIP', data) + + + elif inst.get_prim_mode() == GL_LINE_LOOP: + data["pos"].append(data["pos"][0]) + batch = batch_for_shader(shader, 'LINE_STRIP', data) + + elif inst.get_prim_mode() == GL_TRIANGLES: + indices = [] + for i in range(0, len(coords), 3): + indices.append([i, i + 1, i + 2]) + batch = batch_for_shader(shader, 'TRIS', data, indices=indices) + + elif inst.get_prim_mode() == GL_TRIANGLE_FAN: + indices = [] + for i in range(1, len(coords) - 1): + indices.append([0, i, i + 1]) + batch = batch_for_shader(shader, 'TRIS', data, indices=indices) + + elif inst.get_prim_mode() == GL_QUADS: + indices = [] + for i in range(0, len(coords), 4): + indices.extend([[i, i + 1, i + 2], [i + 2, i + 3, i]]) + batch = batch_for_shader(shader, 'TRIS', data, indices=indices) + else: + raise NotImplemented("get_prim_mode() != (GL_LINES|GL_TRIANGLES|GL_QUADS)") + + shader.bind() + if len(tex_coords) != 0: + shader.uniform_float("modelViewMatrix", gpu.matrix.get_model_view_matrix()) + shader.uniform_float("projectionMatrix", gpu.matrix.get_projection_matrix()) + shader.uniform_int("image", 0) + shader.uniform_float("color", color) + batch.draw(shader) + + inst.clear() + + +def glVertex2f(x, y): + inst = InternalData.get_instance() + inst.add_vert([x, y]) + inst.set_dims(2) + + +def glTexCoord2f(u, v): + inst = InternalData.get_instance() + inst.add_tex_coord([u, v]) + + +GL_BLEND = bgl.GL_BLEND +GL_LINE_SMOOTH = bgl.GL_LINE_SMOOTH +GL_INT = bgl.GL_INT +GL_SCISSOR_BOX = bgl.GL_SCISSOR_BOX +GL_TEXTURE_2D = bgl.GL_TEXTURE_2D +GL_TEXTURE0 = bgl.GL_TEXTURE0 + +GL_TEXTURE_MIN_FILTER = 0 +GL_TEXTURE_MAG_FILTER = 0 +GL_LINEAR = 0 +GL_TEXTURE_ENV = 0 +GL_TEXTURE_ENV_MODE = 0 +GL_MODULATE = 0 + +def glEnable(cap): + bgl.glEnable(cap) + + +def glDisable(cap): + bgl.glDisable(cap) + + +def glScissor(x, y, width, height): + bgl.glScissor(x, y, width, height) + + +def glGetIntegerv(pname, params): + bgl.glGetIntegerv(pname, params) + + +def glActiveTexture(texture): + bgl.glActiveTexture(texture) + + +def glBindTexture(target, texture): + bgl.glBindTexture(target, texture) + + +def glTexParameteri(target, pname, param): + pass + + +def glTexEnvi(target, pname, param): + pass + diff --git a/magic_uv/op/__init__.py b/magic_uv/op/__init__.py new file mode 100644 index 00000000..d637e78a --- /dev/null +++ b/magic_uv/op/__init__.py @@ -0,0 +1,74 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +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(select_uv) + 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 select_uv + 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/magic_uv/op/align_uv.py b/magic_uv/op/align_uv.py new file mode 100644 index 00000000..f8ea4176 --- /dev/null +++ b/magic_uv/op/align_uv.py @@ -0,0 +1,991 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import math +from math import atan2, tan, sin, cos + +import bpy +from bpy.props import EnumProperty, BoolProperty, FloatProperty +import bmesh +from mathutils import Vector + +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat + +from .. import common + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +# 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 + + +# get accumulate vertex lengths of loop sequences +def _get_loop_vert_accum_len(loops): + accum_lengths = [0.0] + length = 0 + for l1, l2 in zip(loops[:-1], loops[1:]): + diff = l2.vert.co - l1.vert.co + length = length + abs(diff.length) + accum_lengths.extend([length]) + + return accum_lengths + + +# get sum uv length of loop sequences +def _get_loop_uv_accum_len(loops, uv_layer): + accum_lengths = [0.0] + 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) + accum_lengths.extend([length]) + + return accum_lengths + + +# get horizontal differential of UV influenced by mesh vertex +def _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl): + common.debug_print( + "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx)) + + base_uv = loop_seqs[0][vidx][0][uv_layer].uv.copy() + + # calculate original length + hloops = [] + for s in loop_seqs: + hloops.extend([s[vidx][0], s[vidx][1]]) + total_vlen = _get_loop_vert_len(hloops) + accum_vlens = _get_loop_vert_accum_len(hloops) + total_uvlen = _get_loop_uv_len(hloops, uv_layer) + accum_uvlens = _get_loop_uv_accum_len(hloops, uv_layer) + orig_uvs = [l[uv_layer].uv.copy() for l in hloops] + + # calculate target length + tgt_noinfl = total_uvlen * (hidx + pidx) / len(loop_seqs) + tgt_infl = total_uvlen * accum_vlens[hidx * 2 + pidx] / total_vlen + target_length = tgt_noinfl * (1 - infl) + tgt_infl * infl + common.debug_print(target_length) + common.debug_print(accum_uvlens) + + # calculate target UV + for i in range(len(accum_uvlens[:-1])): + # get line segment which UV will be placed + if ((accum_uvlens[i] <= target_length) and + (accum_uvlens[i + 1] > target_length)): + tgt_seg_len = target_length - accum_uvlens[i] + seg_len = accum_uvlens[i + 1] - accum_uvlens[i] + uv1 = orig_uvs[i] + uv2 = orig_uvs[i + 1] + target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len + break + elif i == (len(accum_uvlens[:-1]) - 1): + if abs(accum_uvlens[i + 1] - target_length) > 0.000001: + raise Exception( + "Internal Error: horizontal_target_length={}" + " is not equal to {}" + .format(target_length, accum_uvlens[-1])) + tgt_seg_len = target_length - accum_uvlens[i] + seg_len = accum_uvlens[i + 1] - accum_uvlens[i] + uv1 = orig_uvs[i] + uv2 = orig_uvs[i + 1] + target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len + break + else: + raise Exception("Internal Error: horizontal_target_length={}" + " is not in range {} to {}" + .format(target_length, accum_uvlens[0], + accum_uvlens[-1])) + + return target_uv + + +# --------------------- LOOP STRUCTURE ---------------------- +# +# loops[hidx][vidx][pidx] +# hidx: horizontal index +# vidx: vertical index +# pidx: pair index +# +# <----- horizontal -----> +# +# (hidx, vidx, pidx) = (0, 3, 0) +# | (hidx, vidx, pidx) = (1, 3, 0) +# v v +# ^ o --- oo --- o +# | | || | +# vertical | o --- oo --- o <- (hidx, vidx, pidx) +# | o --- oo --- o = (1, 2, 1) +# | | || | +# v o --- oo --- o +# ^ ^ +# | (hidx, vidx, pidx) = (1, 0, 1) +# (hidx, vidx, pidx) = (0, 0, 0) +# +# ----------------------------------------------------------- + + +# get vertical differential of UV influenced by mesh vertex +def _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl): + common.debug_print( + "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx)) + + base_uv = loop_seqs[hidx][0][pidx][uv_layer].uv.copy() + + # calculate original length + vloops = [] + for s in loop_seqs[hidx]: + vloops.append(s[pidx]) + total_vlen = _get_loop_vert_len(vloops) + accum_vlens = _get_loop_vert_accum_len(vloops) + total_uvlen = _get_loop_uv_len(vloops, uv_layer) + accum_uvlens = _get_loop_uv_accum_len(vloops, uv_layer) + orig_uvs = [l[uv_layer].uv.copy() for l in vloops] + + # calculate target length + tgt_noinfl = total_uvlen * int((vidx + 1) / 2) * 2 / len(loop_seqs[hidx]) + tgt_infl = total_uvlen * accum_vlens[vidx] / total_vlen + target_length = tgt_noinfl * (1 - infl) + tgt_infl * infl + common.debug_print(target_length) + common.debug_print(accum_uvlens) + + # calculate target UV + for i in range(len(accum_uvlens[:-1])): + # get line segment which UV will be placed + if ((accum_uvlens[i] <= target_length) and + (accum_uvlens[i + 1] > target_length)): + tgt_seg_len = target_length - accum_uvlens[i] + seg_len = accum_uvlens[i + 1] - accum_uvlens[i] + uv1 = orig_uvs[i] + uv2 = orig_uvs[i + 1] + target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len + break + elif i == (len(accum_uvlens[:-1]) - 1): + if abs(accum_uvlens[i + 1] - target_length) > 0.000001: + raise Exception("Internal Error: horizontal_target_length={}" + " is not equal to {}" + .format(target_length, accum_uvlens[-1])) + tgt_seg_len = target_length - accum_uvlens[i] + seg_len = accum_uvlens[i + 1] - accum_uvlens[i] + uv1 = orig_uvs[i] + uv2 = orig_uvs[i + 1] + target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len + break + else: + raise Exception("Internal Error: horizontal_target_length={}" + " is not in range {} to {}" + .format(target_length, accum_uvlens[0], + accum_uvlens[-1])) + + return target_uv + + +# 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) + + +@PropertyClassRegistry() +class _Properties: + idname = "align_uv" + + @classmethod + def init_props(cls, scene): + scene.muv_align_uv_enabled = BoolProperty( + name="Align UV Enabled", + description="Align UV is enabled", + default=False + ) + scene.muv_align_uv_transmission = BoolProperty( + name="Transmission", + description="Align linked UVs", + default=False + ) + scene.muv_align_uv_select = BoolProperty( + name="Select", + description="Select UVs which are aligned", + default=False + ) + scene.muv_align_uv_vertical = BoolProperty( + name="Vert-Infl (Vertical)", + description="Align vertical direction influenced " + "by mesh vertex proportion", + default=False + ) + scene.muv_align_uv_horizontal = BoolProperty( + name="Vert-Infl (Horizontal)", + description="Align horizontal direction influenced " + "by mesh vertex proportion", + default=False + ) + scene.muv_align_uv_mesh_infl = FloatProperty( + name="Mesh Influence", + description="Influence rate of mesh vertex", + min=0.0, + max=1.0, + default=0.0 + ) + scene.muv_align_uv_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 del_props(cls, scene): + del scene.muv_align_uv_enabled + del scene.muv_align_uv_transmission + del scene.muv_align_uv_select + del scene.muv_align_uv_vertical + del scene.muv_align_uv_horizontal + del scene.muv_align_uv_mesh_infl + del scene.muv_align_uv_location + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_AlignUV_Circle(bpy.types.Operator): + + bl_idname = "uv.muv_ot_align_uv_circle" + bl_label = "Align UV (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): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + 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, True) + 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'} + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_AlignUV_Straighten(bpy.types.Operator): + + bl_idname = "uv.muv_ot_align_uv_straighten" + bl_label = "Align UV (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 + ) + mesh_infl = FloatProperty( + name="Mesh Influence", + description="Influence rate of mesh vertex", + min=0.0, + max=1.0, + default=0.0 + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + # 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, + self.mesh_infl), + _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1, + self.mesh_infl), + _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 0, self.mesh_infl), + _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 1, self.mesh_infl), + ] + 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, + self.mesh_infl), + _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1, + self.mesh_infl), + _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 0, self.mesh_infl), + _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 1, self.mesh_infl), + ] + 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'} + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_AlignUV_Axis(bpy.types.Operator): + + bl_idname = "uv.muv_ot_align_uv_axis" + bl_label = "Align UV (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' + ) + mesh_infl = FloatProperty( + name="Mesh Influence", + description="Influence rate of mesh vertex", + min=0.0, + max=1.0, + default=0.0 + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + # 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, + self.mesh_infl), + _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1, + self.mesh_infl), + _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 0, self.mesh_infl), + _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 1, self.mesh_infl), + ] + 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, + self.mesh_infl), + _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1, + self.mesh_infl), + _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 0, self.mesh_infl), + _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 1, self.mesh_infl), + ] + 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, + self.mesh_infl), + _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1, + self.mesh_infl), + _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 0, self.mesh_infl), + _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 1, self.mesh_infl), + ] + 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, + self.mesh_infl), + _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1, + self.mesh_infl), + _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 0, self.mesh_infl), + _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 1, self.mesh_infl), + ] + 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/magic_uv/op/align_uv_cursor.py b/magic_uv/op/align_uv_cursor.py new file mode 100644 index 00000000..86d13179 --- /dev/null +++ b/magic_uv/op/align_uv_cursor.py @@ -0,0 +1,269 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy +from mathutils import Vector +from bpy.props import EnumProperty, BoolProperty, FloatVectorProperty +import bmesh + +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat + + +def _is_valid_context(context): + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +@PropertyClassRegistry() +class _Properties: + idname = "align_uv_cursor" + + @classmethod + def init_props(cls, scene): + def auvc_get_cursor_loc(self): + area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW', + 'IMAGE_EDITOR') + if compat.check_version(2, 80, 0) < 0: + bd_size = common.get_uvimg_editor_board_size(area) + else: + bd_size = [1.0, 1.0] + 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_align_uv_cursor_cursor_loc'] = Vector((cx, cy)) + return self.get('muv_align_uv_cursor_cursor_loc', (0.0, 0.0)) + + def auvc_set_cursor_loc(self, value): + self['muv_align_uv_cursor_cursor_loc'] = value + area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW', + 'IMAGE_EDITOR') + if compat.check_version(2, 80, 0) < 0: + bd_size = common.get_uvimg_editor_board_size(area) + else: + bd_size = [1.0, 1.0] + cx = bd_size[0] * value[0] + cy = bd_size[1] * value[1] + space.cursor_location = Vector((cx, cy)) + + scene.muv_align_uv_cursor_enabled = BoolProperty( + name="Align UV Cursor Enabled", + description="Align UV Cursor is enabled", + default=False + ) + + scene.muv_align_uv_cursor_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_align_uv_cursor_align_method = 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") + ] + ) + + scene.muv_uv_cursor_location_enabled = BoolProperty( + name="UV Cursor Location Enabled", + description="UV Cursor Location is enabled", + default=False + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_align_uv_cursor_enabled + del scene.muv_align_uv_cursor_cursor_loc + del scene.muv_align_uv_cursor_align_method + + del scene.muv_uv_cursor_location_enabled + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_AlignUVCursor(bpy.types.Operator): + + bl_idname = "uv.muv_ot_align_uv_cursor" + bl_label = "Align UV Cursor" + 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' + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + def execute(self, context): + area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW', + 'IMAGE_EDITOR') + if compat.check_version(2, 80, 0) < 0: + bd_size = common.get_uvimg_editor_board_size(area) + else: + bd_size = [1.0, 1.0] + + 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") + return {'CANCELLED'} + + if self.position == 'CENTER': + cx = center.x + cy = center.y + elif self.position == 'LEFT_TOP': + cx = min_.x + cy = max_.y + elif self.position == 'LEFT_MIDDLE': + cx = min_.x + cy = center.y + elif self.position == 'LEFT_BOTTOM': + cx = min_.x + cy = min_.y + elif self.position == 'MIDDLE_TOP': + cx = center.x + cy = max_.y + elif self.position == 'MIDDLE_BOTTOM': + cx = center.x + cy = min_.y + elif self.position == 'RIGHT_TOP': + cx = max_.x + cy = max_.y + elif self.position == 'RIGHT_MIDDLE': + cx = max_.x + cy = center.y + elif self.position == 'RIGHT_BOTTOM': + cx = max_.x + cy = min_.y + else: + self.report({'ERROR'}, "Unknown Operation") + return {'CANCELLED'} + + cx = cx * bd_size[0] + cy = cy * bd_size[1] + + space.cursor_location = Vector((cx, cy)) + + return {'FINISHED'} diff --git a/magic_uv/op/copy_paste_uv.py b/magic_uv/op/copy_paste_uv.py new file mode 100644 index 00000000..fca412ad --- /dev/null +++ b/magic_uv/op/copy_paste_uv.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>, Jace Priester" +__status__ = "production" +__version__ = "6.0" +__date__ = "26 Jan 2019" + +import bmesh +import bpy.utils +from bpy.props import ( + StringProperty, + BoolProperty, + IntProperty, + EnumProperty, +) + +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +def get_copy_uv_layers(ops_obj, bm, uv_map): + uv_layers = [] + if uv_map == "__default": + if not bm.loops.layers.uv: + ops_obj.report( + {'WARNING'}, "Object must have more than one UV map") + return None + uv_layers.append(bm.loops.layers.uv.verify()) + ops_obj.report({'INFO'}, "Copy UV coordinate") + elif uv_map == "__all": + for uv in bm.loops.layers.uv.keys(): + uv_layers.append(bm.loops.layers.uv[uv]) + ops_obj.report({'INFO'}, "Copy UV coordinate (UV map: ALL)") + else: + uv_layers.append(bm.loops.layers.uv[uv_map]) + ops_obj.report( + {'INFO'}, "Copy UV coordinate (UV map:{})".format(uv_map)) + + return uv_layers + + +def get_paste_uv_layers(ops_obj, obj, bm, src_info, uv_map): + uv_layers = [] + if uv_map == "__default": + if not bm.loops.layers.uv: + ops_obj.report( + {'WARNING'}, "Object must have more than one UV map") + return None + uv_layers.append(bm.loops.layers.uv.verify()) + ops_obj.report({'INFO'}, "Paste UV coordinate") + elif uv_map == "__new": + new_uv_map = common.create_new_uv_map(obj) + if not new_uv_map: + ops_obj.report({'WARNING'}, + "Reached to the maximum number of UV map") + return None + uv_layers.append(bm.loops.layers.uv[new_uv_map.name]) + ops_obj.report( + {'INFO'}, "Paste UV coordinate (UV map:{})".format(new_uv_map)) + elif uv_map == "__all": + for src_layer in src_info.keys(): + if src_layer not in bm.loops.layers.uv.keys(): + new_uv_map = common.create_new_uv_map(obj, src_layer) + if not new_uv_map: + ops_obj.report({'WARNING'}, + "Reached to the maximum number of UV map") + return None + uv_layers.append(bm.loops.layers.uv[src_layer]) + ops_obj.report({'INFO'}, "Paste UV coordinate (UV map: ALL)") + else: + uv_layers.append(bm.loops.layers.uv[uv_map]) + ops_obj.report( + {'INFO'}, "Paste UV coordinate (UV map:{})".format(uv_map)) + + return uv_layers + + +def get_src_face_info(ops_obj, bm, uv_layers, only_select=False): + src_info = {} + for layer in uv_layers: + face_info = [] + for face in bm.faces: + if not only_select or face.select: + info = { + "index": face.index, + "uvs": [l[layer].uv.copy() for l in face.loops], + "pin_uvs": [l[layer].pin_uv for l in face.loops], + "seams": [l.edge.seam for l in face.loops], + } + face_info.append(info) + if not face_info: + ops_obj.report({'WARNING'}, "No faces are selected") + return None + src_info[layer.name] = face_info + + return src_info + + +def get_dest_face_info(ops_obj, bm, uv_layers, src_info, strategy, + only_select=False): + dest_info = {} + for layer in uv_layers: + face_info = [] + for face in bm.faces: + if not only_select or face.select: + info = { + "index": face.index, + "uvs": [l[layer].uv.copy() for l in face.loops], + } + face_info.append(info) + if not face_info: + ops_obj.report({'WARNING'}, "No faces are selected") + return None + key = list(src_info.keys())[0] + src_face_count = len(src_info[key]) + dest_face_count = len(face_info) + if strategy == 'N_N' and src_face_count != dest_face_count: + ops_obj.report( + {'WARNING'}, + "Number of selected faces is different from copied" + + "(src:{}, dest:{})" + .format(src_face_count, dest_face_count)) + return None + dest_info[layer.name] = face_info + + return dest_info + + +def _get_select_history_src_face_info(ops_obj, bm, uv_layers): + src_info = {} + for layer in uv_layers: + face_info = [] + for hist in bm.select_history: + if isinstance(hist, bmesh.types.BMFace) and hist.select: + info = { + "index": hist.index, + "uvs": [l[layer].uv.copy() for l in hist.loops], + "pin_uvs": [l[layer].pin_uv for l in hist.loops], + "seams": [l.edge.seam for l in hist.loops], + } + face_info.append(info) + if not face_info: + ops_obj.report({'WARNING'}, "No faces are selected") + return None + src_info[layer.name] = face_info + + return src_info + + +def _get_select_history_dest_face_info(ops_obj, bm, uv_layers, src_info, + strategy): + dest_info = {} + for layer in uv_layers: + face_info = [] + for hist in bm.select_history: + if isinstance(hist, bmesh.types.BMFace) and hist.select: + info = { + "index": hist.index, + "uvs": [l[layer].uv.copy() for l in hist.loops], + } + face_info.append(info) + if not face_info: + ops_obj.report({'WARNING'}, "No faces are selected") + return None + key = list(src_info.keys())[0] + src_face_count = len(src_info[key]) + dest_face_count = len(face_info) + if strategy == 'N_N' and src_face_count != dest_face_count: + ops_obj.report( + {'WARNING'}, + "Number of selected faces is different from copied" + + "(src:{}, dest:{})" + .format(src_face_count, dest_face_count)) + return None + dest_info[layer.name] = face_info + + return dest_info + + +def paste_uv(ops_obj, bm, src_info, dest_info, uv_layers, strategy, flip, + rotate, copy_seams): + for slayer_name, dlayer in zip(src_info.keys(), uv_layers): + src_faces = src_info[slayer_name] + dest_faces = dest_info[dlayer.name] + + for idx, dinfo in enumerate(dest_faces): + sinfo = None + if strategy == 'N_N': + sinfo = src_faces[idx] + elif strategy == 'N_M': + sinfo = src_faces[idx % len(src_faces)] + + suv = sinfo["uvs"] + spuv = sinfo["pin_uvs"] + ss = sinfo["seams"] + if len(sinfo["uvs"]) != len(dinfo["uvs"]): + ops_obj.report({'WARNING'}, "Some faces are different size") + return -1 + + 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 flip is True: + suvs_fr.reverse() + spuvs_fr.reverse() + ss_fr.reverse() + + # rotate UVs + for _ in range(rotate): + 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[dinfo["index"]].loops, + suvs_fr, spuvs_fr, ss_fr): + l[dlayer].uv = suv + l[dlayer].pin_uv = spuv + if copy_seams is True: + l.edge.seam = ss + + return 0 + + +@PropertyClassRegistry() +class _Properties: + idname = "copy_paste_uv" + + @classmethod + def init_props(cls, scene): + class Props(): + src_info = None + + scene.muv_props.copy_paste_uv = Props() + scene.muv_props.copy_paste_uv_selseq = Props() + + scene.muv_copy_paste_uv_enabled = BoolProperty( + name="Copy/Paste UV Enabled", + description="Copy/Paste UV is enabled", + default=False + ) + scene.muv_copy_paste_uv_copy_seams = BoolProperty( + name="Seams", + description="Copy Seams", + default=True + ) + scene.muv_copy_paste_uv_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_copy_paste_uv_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' + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_props.copy_paste_uv + del scene.muv_props.copy_paste_uv_selseq + del scene.muv_copy_paste_uv_enabled + del scene.muv_copy_paste_uv_copy_seams + del scene.muv_copy_paste_uv_mode + del scene.muv_copy_paste_uv_strategy + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_CopyPasteUV_CopyUV(bpy.types.Operator): + """ + Operation class: Copy UV coordinate + """ + + bl_idname = "uv.muv_ot_copy_paste_uv_copy_uv" + bl_label = "Copy UV" + bl_description = "Copy UV coordinate" + bl_options = {'REGISTER', 'UNDO'} + + uv_map = StringProperty(default="__default", options={'HIDDEN'}) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + def execute(self, context): + props = context.scene.muv_props.copy_paste_uv + obj = context.active_object + bm = common.create_bmesh(obj) + + # get UV layer + uv_layers = get_copy_uv_layers(self, bm, self.uv_map) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + src_info = get_src_face_info(self, bm, uv_layers, True) + if src_info is None: + return {'CANCELLED'} + props.src_info = src_info + + face_count = len(props.src_info[list(props.src_info.keys())[0]]) + self.report({'INFO'}, "{} face(s) are copied".format(face_count)) + + return {'FINISHED'} + + +@BlClassRegistry() +class MUV_MT_CopyPasteUV_CopyUV(bpy.types.Menu): + """ + Menu class: Copy UV coordinate + """ + + bl_idname = "uv.muv_mt_copy_paste_uv_copy_uv" + bl_label = "Copy UV (Menu)" + bl_description = "Menu of Copy UV coordinate" + + @classmethod + def poll(cls, context): + return _is_valid_context(context) + + def draw(self, context): + layout = self.layout + # create sub menu + obj = context.active_object + bm = common.create_bmesh(obj) + uv_maps = bm.loops.layers.uv.keys() + + ops = layout.operator(MUV_OT_CopyPasteUV_CopyUV.bl_idname, + text="[Default]") + ops.uv_map = "__default" + + ops = layout.operator(MUV_OT_CopyPasteUV_CopyUV.bl_idname, + text="[All]") + ops.uv_map = "__all" + + for m in uv_maps: + ops = layout.operator(MUV_OT_CopyPasteUV_CopyUV.bl_idname, text=m) + ops.uv_map = m + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_CopyPasteUV_PasteUV(bpy.types.Operator): + """ + Operation class: Paste UV coordinate + """ + + bl_idname = "uv.muv_ot_copy_paste_uv_paste_uv" + bl_label = "Paste UV" + bl_description = "Paste UV coordinate" + bl_options = {'REGISTER', 'UNDO'} + + uv_map = StringProperty(default="__default", 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="Seams", + description="Copy Seams", + default=True + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.copy_paste_uv + if not props.src_info: + return False + return _is_valid_context(context) + + def execute(self, context): + props = context.scene.muv_props.copy_paste_uv + if not props.src_info: + self.report({'WARNING'}, "Need copy UV at first") + return {'CANCELLED'} + obj = context.active_object + bm = common.create_bmesh(obj) + + # get UV layer + uv_layers = get_paste_uv_layers(self, obj, bm, props.src_info, + self.uv_map) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + dest_info = get_dest_face_info(self, bm, uv_layers, + props.src_info, self.strategy, True) + if dest_info is None: + return {'CANCELLED'} + + # paste + ret = paste_uv(self, bm, props.src_info, dest_info, uv_layers, + self.strategy, self.flip_copied_uv, + self.rotate_copied_uv, self.copy_seams) + if ret: + return {'CANCELLED'} + + face_count = len(props.src_info[list(dest_info.keys())[0]]) + self.report({'INFO'}, "{} face(s) are pasted".format(face_count)) + + bmesh.update_edit_mesh(obj.data) + + if compat.check_version(2, 80, 0) < 0: + if self.copy_seams is True: + obj.data.show_edge_seams = True + + return {'FINISHED'} + + +@BlClassRegistry() +class MUV_MT_CopyPasteUV_PasteUV(bpy.types.Menu): + """ + Menu class: Paste UV coordinate + """ + + bl_idname = "uv.muv_mt_copy_paste_uv_paste_uv" + bl_label = "Paste UV (Menu)" + bl_description = "Menu of Paste UV coordinate" + + @classmethod + def poll(cls, context): + sc = context.scene + props = sc.muv_props.copy_paste_uv + if not props.src_info: + return False + return _is_valid_context(context) + + def draw(self, context): + sc = context.scene + layout = self.layout + # create sub menu + obj = context.active_object + bm = common.create_bmesh(obj) + uv_maps = bm.loops.layers.uv.keys() + + ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname, + text="[Default]") + ops.uv_map = "__default" + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy + + ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname, + text="[New]") + ops.uv_map = "__new" + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy + + ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname, + text="[All]") + ops.uv_map = "__all" + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy + + for m in uv_maps: + ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname, text=m) + ops.uv_map = m + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_CopyPasteUV_SelSeqCopyUV(bpy.types.Operator): + """ + Operation class: Copy UV coordinate by selection sequence + """ + + bl_idname = "uv.muv_ot_copy_paste_uv_selseq_copy_uv" + bl_label = "Copy UV (Selection Sequence)" + bl_description = "Copy UV data by selection sequence" + bl_options = {'REGISTER', 'UNDO'} + + uv_map = StringProperty(default="__default", options={'HIDDEN'}) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + def execute(self, context): + props = context.scene.muv_props.copy_paste_uv_selseq + obj = context.active_object + bm = common.create_bmesh(obj) + + # get UV layer + uv_layers = get_copy_uv_layers(self, bm, self.uv_map) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + src_info = _get_select_history_src_face_info(self, bm, uv_layers) + if src_info is None: + return {'CANCELLED'} + props.src_info = src_info + + face_count = len(props.src_info[list(props.src_info.keys())[0]]) + self.report({'INFO'}, "{} face(s) are selected".format(face_count)) + + return {'FINISHED'} + + +@BlClassRegistry() +class MUV_MT_CopyPasteUV_SelSeqCopyUV(bpy.types.Menu): + """ + Menu class: Copy UV coordinate by selection sequence + """ + + bl_idname = "uv.muv_mt_copy_paste_uv_selseq_copy_uv" + bl_label = "Copy UV (Selection Sequence) (Menu)" + bl_description = "Menu of Copy UV coordinate by selection sequence" + + @classmethod + def poll(cls, context): + return _is_valid_context(context) + + def draw(self, context): + layout = self.layout + obj = context.active_object + bm = common.create_bmesh(obj) + uv_maps = bm.loops.layers.uv.keys() + + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV.bl_idname, + text="[Default]") + ops.uv_map = "__default" + + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV.bl_idname, + text="[All]") + ops.uv_map = "__all" + + for m in uv_maps: + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV.bl_idname, + text=m) + ops.uv_map = m + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_CopyPasteUV_SelSeqPasteUV(bpy.types.Operator): + """ + Operation class: Paste UV coordinate by selection sequence + """ + + bl_idname = "uv.muv_ot_copy_paste_uv_selseq_paste_uv" + bl_label = "Paste UV (Selection Sequence)" + bl_description = "Paste UV coordinate by selection sequence" + bl_options = {'REGISTER', 'UNDO'} + + uv_map = StringProperty(default="__default", 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="Seams", + description="Copy Seams", + default=True + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.copy_paste_uv_selseq + if not props.src_info: + return False + return _is_valid_context(context) + + def execute(self, context): + props = context.scene.muv_props.copy_paste_uv_selseq + if not props.src_info: + self.report({'WARNING'}, "Need copy UV at first") + return {'CANCELLED'} + obj = context.active_object + bm = common.create_bmesh(obj) + + # get UV layer + uv_layers = get_paste_uv_layers(self, obj, bm, props.src_info, + self.uv_map) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + dest_info = _get_select_history_dest_face_info(self, bm, uv_layers, + props.src_info, + self.strategy) + if dest_info is None: + return {'CANCELLED'} + + # paste + ret = paste_uv(self, bm, props.src_info, dest_info, uv_layers, + self.strategy, self.flip_copied_uv, + self.rotate_copied_uv, self.copy_seams) + if ret: + return {'CANCELLED'} + + face_count = len(props.src_info[list(dest_info.keys())[0]]) + self.report({'INFO'}, "{} face(s) are pasted".format(face_count)) + + bmesh.update_edit_mesh(obj.data) + + if compat.check_version(2, 80, 0) < 0: + if self.copy_seams is True: + obj.data.show_edge_seams = True + + return {'FINISHED'} + + +@BlClassRegistry() +class MUV_MT_CopyPasteUV_SelSeqPasteUV(bpy.types.Menu): + """ + Menu class: Paste UV coordinate by selection sequence + """ + + bl_idname = "uv.muv_mt_copy_paste_uv_selseq_paste_uv" + bl_label = "Paste UV (Selection Sequence) (Menu)" + bl_description = "Menu of Paste UV coordinate by selection sequence" + + @classmethod + def poll(cls, context): + sc = context.scene + props = sc.muv_props.copy_paste_uv_selseq + if not props.src_uvs or not props.src_pin_uvs: + return False + return _is_valid_context(context) + + def draw(self, context): + sc = context.scene + layout = self.layout + # create sub menu + obj = context.active_object + bm = common.create_bmesh(obj) + uv_maps = bm.loops.layers.uv.keys() + + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname, + text="[Default]") + ops.uv_map = "__default" + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy + + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname, + text="[New]") + ops.uv_map = "__new" + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy + + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname, + text="[All]") + ops.uv_map = "__all" + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy + + for m in uv_maps: + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname, + text=m) + ops.uv_map = m + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy diff --git a/magic_uv/op/copy_paste_uv_object.py b/magic_uv/op/copy_paste_uv_object.py new file mode 100644 index 00000000..23ff412b --- /dev/null +++ b/magic_uv/op/copy_paste_uv_object.py @@ -0,0 +1,306 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bmesh +import bpy +from bpy.props import ( + StringProperty, + BoolProperty, +) + +from .copy_paste_uv import ( + get_copy_uv_layers, + get_src_face_info, + get_paste_uv_layers, + get_dest_face_info, + paste_uv, +) +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat + + +def _is_valid_context(context): + obj = context.object + + # only object mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'OBJECT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +@PropertyClassRegistry() +class _Properties: + idname = "copy_paste_uv_object" + + @classmethod + def init_props(cls, scene): + class Props(): + src_info = None + + scene.muv_props.copy_paste_uv_object = Props() + + scene.muv_copy_paste_uv_object_copy_seams = BoolProperty( + name="Seams", + description="Copy Seams", + default=True + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_props.copy_paste_uv_object + del scene.muv_copy_paste_uv_object_copy_seams + + +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 + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_CopyPasteUVObject_CopyUV(bpy.types.Operator): + """ + Operation class: Copy UV coordinate among objects + """ + + bl_idname = "object.muv_ot_copy_paste_uv_object_copy_uv" + bl_label = "Copy UV (Among Objects)" + bl_description = "Copy UV coordinate (Among Objects)" + bl_options = {'REGISTER', 'UNDO'} + + uv_map = StringProperty(default="__default", options={'HIDDEN'}) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + @memorize_view_3d_mode + def execute(self, context): + props = context.scene.muv_props.copy_paste_uv_object + bpy.ops.object.mode_set(mode='EDIT') + obj = context.active_object + bm = common.create_bmesh(obj) + + # get UV layer + uv_layers = get_copy_uv_layers(self, bm, self.uv_map) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + src_info = get_src_face_info(self, bm, uv_layers) + if src_info is None: + return {'CANCELLED'} + props.src_info = src_info + + self.report({'INFO'}, + "{}'s UV coordinates are copied".format(obj.name)) + + return {'FINISHED'} + + +@BlClassRegistry() +class MUV_MT_CopyPasteUVObject_CopyUV(bpy.types.Menu): + """ + Menu class: Copy UV coordinate among objects + """ + + bl_idname = "object.muv_mt_copy_paste_uv_object_copy_uv" + bl_label = "Copy UV (Among Objects) (Menu)" + bl_description = "Menu of Copy UV coordinate (Among Objects)" + + @classmethod + def poll(cls, context): + return _is_valid_context(context) + + def draw(self, _): + layout = self.layout + # create sub menu + uv_maps = compat.get_object_uv_layers(bpy.context.active_object).keys() + + ops = layout.operator(MUV_OT_CopyPasteUVObject_CopyUV.bl_idname, + text="[Default]") + ops.uv_map = "__default" + + ops = layout.operator(MUV_OT_CopyPasteUVObject_CopyUV.bl_idname, + text="[All]") + ops.uv_map = "__all" + + for m in uv_maps: + ops = layout.operator(MUV_OT_CopyPasteUVObject_CopyUV.bl_idname, + text=m) + ops.uv_map = m + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_CopyPasteUVObject_PasteUV(bpy.types.Operator): + """ + Operation class: Paste UV coordinate among objects + """ + + bl_idname = "object.muv_ot_copy_paste_uv_object_paste_uv" + bl_label = "Paste UV (Among Objects)" + bl_description = "Paste UV coordinate (Among Objects)" + bl_options = {'REGISTER', 'UNDO'} + + uv_map = StringProperty(default="__default", options={'HIDDEN'}) + copy_seams = BoolProperty( + name="Seams", + description="Copy Seams", + default=True + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.copy_paste_uv_object + if not props.src_info: + return False + return _is_valid_context(context) + + @memorize_view_3d_mode + def execute(self, context): + props = context.scene.muv_props.copy_paste_uv_object + if not props.src_info: + self.report({'WARNING'}, "Need copy UV at first") + return {'CANCELLED'} + + for o in bpy.data.objects: + if not compat.object_has_uv_layers(o): + continue + if not compat.get_object_select(o): + continue + + bpy.ops.object.mode_set(mode='OBJECT') + compat.set_active_object(o) + bpy.ops.object.mode_set(mode='EDIT') + + obj = context.active_object + bm = common.create_bmesh(obj) + + # get UV layer + uv_layers = get_paste_uv_layers(self, obj, bm, props.src_info, + self.uv_map) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + dest_info = get_dest_face_info(self, bm, uv_layers, + props.src_info, 'N_N') + if dest_info is None: + return {'CANCELLED'} + + # paste + ret = paste_uv(self, bm, props.src_info, dest_info, uv_layers, + 'N_N', 0, 0, self.copy_seams) + if ret: + return {'CANCELLED'} + + bmesh.update_edit_mesh(obj.data) + + if compat.check_version(2, 80, 0) < 0: + if self.copy_seams is True: + obj.data.show_edge_seams = True + + self.report( + {'INFO'}, "{}'s UV coordinates are pasted".format(obj.name)) + + return {'FINISHED'} + + +@BlClassRegistry() +class MUV_MT_CopyPasteUVObject_PasteUV(bpy.types.Menu): + """ + Menu class: Paste UV coordinate among objects + """ + + bl_idname = "object.muv_mt_copy_paste_uv_object_paste_uv" + bl_label = "Paste UV (Among Objects) (Menu)" + bl_description = "Menu of Paste UV coordinate (Among Objects)" + + @classmethod + def poll(cls, context): + sc = context.scene + props = sc.muv_props.copy_paste_uv_object + if not props.src_info: + return False + return _is_valid_context(context) + + def draw(self, context): + sc = context.scene + layout = self.layout + # create sub menu + uv_maps = [] + for obj in bpy.data.objects: + if not compat.object_has_uv_layers(obj): + continue + if not compat.get_object_select(obj): + continue + uv_maps.extend(compat.get_object_uv_layers(obj).keys()) + + ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname, + text="[Default]") + ops.uv_map = "__default" + ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams + + ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname, + text="[New]") + ops.uv_map = "__new" + ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams + + ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname, + text="[All]") + ops.uv_map = "__all" + ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams + + for m in uv_maps: + ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname, + text=m) + ops.uv_map = m + ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams diff --git a/magic_uv/op/copy_paste_uv_uvedit.py b/magic_uv/op/copy_paste_uv_uvedit.py new file mode 100644 index 00000000..b448f866 --- /dev/null +++ b/magic_uv/op/copy_paste_uv_uvedit.py @@ -0,0 +1,198 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import math +from math import atan2, sin, cos + +import bpy +import bmesh +from mathutils import Vector + +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +@PropertyClassRegistry() +class _Properties: + idname = "copy_paste_uv_uvedit" + + @classmethod + def init_props(cls, scene): + class Props(): + src_uvs = None + + scene.muv_props.copy_paste_uv_uvedit = Props() + + @classmethod + def del_props(cls, scene): + del scene.muv_props.copy_paste_uv_uvedit + + +@BlClassRegistry() +class MUV_OT_CopyPasteUVUVEdit_CopyUV(bpy.types.Operator): + """ + Operation class: Copy UV coordinate on UV/Image Editor + """ + + bl_idname = "uv.muv_ot_copy_paste_uv_uvedit_copy_uv" + bl_label = "Copy UV (UV/Image Editor)" + bl_description = "Copy UV coordinate (only selected in UV/Image Editor)" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + def execute(self, context): + props = context.scene.muv_props.copy_paste_uv_uvedit + 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() + + props.src_uvs = [] + 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'} + + +@BlClassRegistry() +class MUV_OT_CopyPasteUVUVEdit_PasteUV(bpy.types.Operator): + """ + Operation class: Paste UV coordinate on UV/Image Editor + """ + + bl_idname = "uv.muv_ot_copy_paste_uv_uvedit_paste_uv" + bl_label = "Paste UV (UV/Image Editor)" + bl_description = "Paste UV coordinate (only selected in UV/Image Editor)" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.copy_paste_uv_uvedit + if not props.src_uvs: + return False + return _is_valid_context(context) + + def execute(self, context): + props = context.scene.muv_props.copy_paste_uv_uvedit + 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/magic_uv/op/flip_rotate_uv.py b/magic_uv/op/flip_rotate_uv.py new file mode 100644 index 00000000..c4c05169 --- /dev/null +++ b/magic_uv/op/flip_rotate_uv.py @@ -0,0 +1,232 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy +import bmesh +from bpy.props import ( + BoolProperty, + IntProperty, +) + +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +def _get_uv_layer(ops_obj, bm): + # get UV layer + if not bm.loops.layers.uv: + ops_obj.report({'WARNING'}, "Object must have more than one UV map") + return None + uv_layer = bm.loops.layers.uv.verify() + + return uv_layer + + +def _get_src_face_info(ops_obj, bm, uv_layers, only_select=False): + src_info = {} + for layer in uv_layers: + face_info = [] + for face in bm.faces: + if not only_select or face.select: + info = { + "index": face.index, + "uvs": [l[layer].uv.copy() for l in face.loops], + "pin_uvs": [l[layer].pin_uv for l in face.loops], + "seams": [l.edge.seam for l in face.loops], + } + face_info.append(info) + if not face_info: + ops_obj.report({'WARNING'}, "No faces are selected") + return None + src_info[layer.name] = face_info + + return src_info + + +def _paste_uv(ops_obj, bm, src_info, dest_info, uv_layers, strategy, flip, + rotate, copy_seams): + for slayer_name, dlayer in zip(src_info.keys(), uv_layers): + src_faces = src_info[slayer_name] + dest_faces = dest_info[dlayer.name] + + for idx, dinfo in enumerate(dest_faces): + sinfo = None + if strategy == 'N_N': + sinfo = src_faces[idx] + elif strategy == 'N_M': + sinfo = src_faces[idx % len(src_faces)] + + suv = sinfo["uvs"] + spuv = sinfo["pin_uvs"] + ss = sinfo["seams"] + if len(sinfo["uvs"]) != len(dinfo["uvs"]): + ops_obj.report({'WARNING'}, "Some faces are different size") + return -1 + + 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 flip is True: + suvs_fr.reverse() + spuvs_fr.reverse() + ss_fr.reverse() + + # rotate UVs + for _ in range(rotate): + 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[dinfo["index"]].loops, + suvs_fr, spuvs_fr, ss_fr): + l[dlayer].uv = suv + l[dlayer].pin_uv = spuv + if copy_seams is True: + l.edge.seam = ss + + return 0 + + +@PropertyClassRegistry() +class _Properties: + idname = "flip_rotate_uv" + + @classmethod + def init_props(cls, scene): + scene.muv_flip_rotate_uv_enabled = BoolProperty( + name="Flip/Rotate UV Enabled", + description="Flip/Rotate UV is enabled", + default=False + ) + scene.muv_flip_rotate_uv_seams = BoolProperty( + name="Seams", + description="Seams", + default=True + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_flip_rotate_uv_enabled + del scene.muv_flip_rotate_uv_seams + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_FlipRotate(bpy.types.Operator): + """ + Operation class: Flip and Rotate UV coordinate + """ + + bl_idname = "uv.muv_ot_flip_rotate_uv" + bl_label = "Flip/Rotate UV" + bl_description = "Flip/Rotate UV coordinate" + bl_options = {'REGISTER', 'UNDO'} + + flip = BoolProperty( + name="Flip UV", + description="Flip UV...", + default=False + ) + rotate = IntProperty( + default=0, + name="Rotate UV", + min=0, + max=30 + ) + seams = BoolProperty( + name="Seams", + description="Seams", + default=True + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + def execute(self, context): + self.report({'INFO'}, "Flip/Rotate UV") + 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 + uv_layer = _get_uv_layer(self, bm) + if not uv_layer: + return {'CANCELLED'} + + # get selected face + src_info = _get_src_face_info(self, bm, [uv_layer], True) + if not src_info: + return {'CANCELLED'} + + face_count = len(src_info[list(src_info.keys())[0]]) + self.report({'INFO'}, "{} face(s) are selected".format(face_count)) + + # paste + ret = _paste_uv(self, bm, src_info, src_info, [uv_layer], 'N_N', + self.flip, self.rotate, self.seams) + if ret: + return {'CANCELLED'} + + bmesh.update_edit_mesh(obj.data) + + if compat.check_version(2, 80, 0) < 0: + if self.seams is True: + obj.data.show_edge_seams = True + + return {'FINISHED'} diff --git a/magic_uv/op/mirror_uv.py b/magic_uv/op/mirror_uv.py new file mode 100644 index 00000000..fb98bb05 --- /dev/null +++ b/magic_uv/op/mirror_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__ = "Keith (Wahooney) Boshoff, Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy +from bpy.props import ( + EnumProperty, + FloatProperty, + BoolProperty, +) +import bmesh +from mathutils import Vector + +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat +from .. import common + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +def _is_vector_similar(v1, v2, error): + """ + Check if two vectors are similar, within an error threshold + """ + within_err_x = abs(v2.x - v1.x) < error + within_err_y = abs(v2.y - v1.y) < error + within_err_z = abs(v2.z - v1.z) < error + + return within_err_x and within_err_y and within_err_z + + +def _mirror_uvs(uv_layer, src, dst, axis, error): + """ + Copy UV coordinates from one UV face to another + """ + for sl in src.loops: + suv = sl[uv_layer].uv.copy() + svco = sl.vert.co.copy() + for dl in dst.loops: + dvco = dl.vert.co.copy() + if axis == 'X': + dvco.x = -dvco.x + elif axis == 'Y': + dvco.y = -dvco.y + elif axis == 'Z': + dvco.z = -dvco.z + + if _is_vector_similar(svco, dvco, error): + dl[uv_layer].uv = suv.copy() + + +def _get_face_center(face): + """ + Get center coordinate of the face + """ + center = Vector((0.0, 0.0, 0.0)) + for v in face.verts: + center = center + v.co + + return center / len(face.verts) + + +@PropertyClassRegistry() +class _Properties: + idname = "mirror_uv" + + @classmethod + def init_props(cls, scene): + scene.muv_mirror_uv_enabled = BoolProperty( + name="Mirror UV Enabled", + description="Mirror UV is enabled", + default=False + ) + scene.muv_mirror_uv_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' + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_mirror_uv_enabled + del scene.muv_mirror_uv_axis + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_MirrorUV(bpy.types.Operator): + """ + Operation class: Mirror UV + """ + + bl_idname = "uv.muv_ot_mirror_uv" + bl_label = "Mirror UV" + bl_options = {'REGISTER', 'UNDO'} + + axis = EnumProperty( + items=( + ('X', "X", "Mirror Along X axis"), + ('Y', "Y", "Mirror Along Y axis"), + ('Z', "Z", "Mirror Along Z axis") + ), + name="Axis", + description="Mirror Axis", + default='X' + ) + error = FloatProperty( + name="Error", + description="Error threshold", + default=0.001, + min=0.0, + max=100.0, + soft_min=0.0, + soft_max=1.0 + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + def execute(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + + error = self.error + axis = self.axis + + 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() + + faces = [f for f in bm.faces if f.select] + for f_dst in faces: + count = len(f_dst.verts) + for f_src in bm.faces: + # check if this is a candidate to do mirror UV + if f_src.index == f_dst.index: + continue + if count != len(f_src.verts): + continue + + # test if the vertices x values are the same sign + dst = _get_face_center(f_dst) + src = _get_face_center(f_src) + if (dst.x > 0 and src.x > 0) or (dst.x < 0 and src.x < 0): + continue + + # invert source axis + if axis == 'X': + src.x = -src.x + elif axis == 'Y': + src.y = -src.z + elif axis == 'Z': + src.z = -src.z + + # do mirror UV + if _is_vector_similar(dst, src, error): + _mirror_uvs(uv_layer, f_src, f_dst, self.axis, self.error) + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/magic_uv/op/move_uv.py b/magic_uv/op/move_uv.py new file mode 100644 index 00000000..be019e9f --- /dev/null +++ b/magic_uv/op/move_uv.py @@ -0,0 +1,185 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "kgeogeo, mem, Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy +from bpy.props import BoolProperty +import bmesh +from mathutils import Vector + +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +def _find_uv(context): + bm = bmesh.from_edit_mesh(context.object.data) + topology_dict = [] + uvs = [] + active_uv = bm.loops.layers.uv.active + for fidx, f in enumerate(bm.faces): + for vidx, v in enumerate(f.verts): + if v.select: + uvs.append(f.loops[vidx][active_uv].uv.copy()) + topology_dict.append([fidx, vidx]) + + return topology_dict, uvs + + +@PropertyClassRegistry() +class _Properties: + idname = "move_uv" + + @classmethod + def init_props(cls, scene): + scene.muv_move_uv_enabled = BoolProperty( + name="Move UV Enabled", + description="Move UV is enabled", + default=False + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_move_uv_enabled + + +@BlClassRegistry() +class MUV_OT_MoveUV(bpy.types.Operator): + """ + Operator class: Move UV + """ + + bl_idname = "uv.muv_ot_move_uv" + bl_label = "Move UV" + bl_options = {'REGISTER', 'UNDO'} + + __running = False + + def __init__(self): + self.__topology_dict = [] + self.__prev_mouse = Vector((0.0, 0.0)) + self.__offset_uv = Vector((0.0, 0.0)) + self.__prev_offset_uv = Vector((0.0, 0.0)) + self.__first_time = True + self.__ini_uvs = [] + self.__operating = False + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return False + if cls.is_running(context): + return False + return _is_valid_context(context) + + @classmethod + def is_running(cls, _): + return cls.__running + + def modal(self, context, event): + if self.__first_time is True: + self.__prev_mouse = Vector(( + event.mouse_region_x, event.mouse_region_y)) + self.__first_time = False + return {'RUNNING_MODAL'} + + # move UV + div = 10000 + self.__offset_uv += Vector(( + (event.mouse_region_x - self.__prev_mouse.x) / div, + (event.mouse_region_y - self.__prev_mouse.y) / div)) + ouv = self.__offset_uv + pouv = self.__prev_offset_uv + vec = Vector((ouv.x - ouv.y, ouv.x + ouv.y)) + dv = vec - pouv + self.__prev_offset_uv = vec + self.__prev_mouse = Vector(( + event.mouse_region_x, event.mouse_region_y)) + + # check if operation is started + if not self.__operating: + if event.type == 'LEFTMOUSE' and event.value == 'RELEASE': + self.__operating = True + return {'RUNNING_MODAL'} + + # update UV + obj = context.object + bm = bmesh.from_edit_mesh(obj.data) + active_uv = bm.loops.layers.uv.active + for fidx, vidx in self.__topology_dict: + l = bm.faces[fidx].loops[vidx] + l[active_uv].uv = l[active_uv].uv + dv + bmesh.update_edit_mesh(obj.data) + + # check mouse preference + confirm_btn = 'LEFTMOUSE' + cancel_btn = 'RIGHTMOUSE' + + # cancelled + if event.type == cancel_btn and event.value == 'PRESS': + for (fidx, vidx), uv in zip(self.__topology_dict, self.__ini_uvs): + bm.faces[fidx].loops[vidx][active_uv].uv = uv + MUV_OT_MoveUV.__running = False + return {'FINISHED'} + # confirmed + if event.type == confirm_btn and event.value == 'PRESS': + MUV_OT_MoveUV.__running = False + return {'FINISHED'} + + return {'RUNNING_MODAL'} + + def execute(self, context): + MUV_OT_MoveUV.__running = True + self.__operating = False + self.__first_time = True + + context.window_manager.modal_handler_add(self) + self.__topology_dict, self.__ini_uvs = _find_uv(context) + + if context.area: + context.area.tag_redraw() + + return {'RUNNING_MODAL'} diff --git a/magic_uv/op/pack_uv.py b/magic_uv/op/pack_uv.py new file mode 100644 index 00000000..4eb3841d --- /dev/null +++ b/magic_uv/op/pack_uv.py @@ -0,0 +1,282 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +from math import fabs + +import bpy +from bpy.props import ( + FloatProperty, + FloatVectorProperty, + BoolProperty, +) +import bmesh +import mathutils +from mathutils import Vector + +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat +from .. import common + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +def _sort_island_faces(kd, uvs, isl1, isl2): + """ + Sort faces in island + """ + + sorted_faces = [] + for f in isl1['sorted']: + _, idx, _ = kd.find( + Vector((f['ave_uv'].x, f['ave_uv'].y, 0.0))) + sorted_faces.append(isl2['faces'][uvs[idx]['face_idx']]) + return sorted_faces + + +def _group_island(island_info, allowable_center_deviation, + allowable_size_deviation): + """ + Group island + """ + + num_group = 0 + while True: + # search islands which is not parsed yet + isl_1 = None + for isl_1 in island_info: + if isl_1['group'] == -1: + break + else: + break # all faces are parsed + if isl_1 is None: + break + isl_1['group'] = num_group + isl_1['sorted'] = isl_1['faces'] + + # search same island + for isl_2 in island_info: + if isl_2['group'] == -1: + dcx = isl_2['center'].x - isl_1['center'].x + dcy = isl_2['center'].y - isl_1['center'].y + dsx = isl_2['size'].x - isl_1['size'].x + dsy = isl_2['size'].y - isl_1['size'].y + center_x_matched = ( + fabs(dcx) < allowable_center_deviation[0] + ) + center_y_matched = ( + fabs(dcy) < allowable_center_deviation[1] + ) + size_x_matched = ( + fabs(dsx) < allowable_size_deviation[0] + ) + size_y_matched = ( + fabs(dsy) < allowable_size_deviation[1] + ) + center_matched = center_x_matched and center_y_matched + size_matched = size_x_matched and size_y_matched + num_uv_matched = (isl_2['num_uv'] == isl_1['num_uv']) + # are islands have same? + if center_matched and size_matched and num_uv_matched: + isl_2['group'] = num_group + kd = mathutils.kdtree.KDTree(len(isl_2['faces'])) + uvs = [ + { + 'uv': Vector( + (f['ave_uv'].x, f['ave_uv'].y, 0.0) + ), + 'face_idx': fidx + } for fidx, f in enumerate(isl_2['faces']) + ] + for i, uv in enumerate(uvs): + kd.insert(uv['uv'], i) + kd.balance() + # sort faces for copy/paste UV + isl_2['sorted'] = _sort_island_faces(kd, uvs, isl_1, isl_2) + num_group = num_group + 1 + + return num_group + + +@PropertyClassRegistry() +class _Properties: + idname = "pack_uv" + + @classmethod + def init_props(cls, scene): + scene.muv_pack_uv_enabled = BoolProperty( + name="Pack UV Enabled", + description="Pack UV is enabled", + default=False + ) + scene.muv_pack_uv_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_pack_uv_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 + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_pack_uv_enabled + del scene.muv_pack_uv_allowable_center_deviation + del scene.muv_pack_uv_allowable_size_deviation + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_PackUV(bpy.types.Operator): + """ + Operation class: Pack UV with same UV islands are integrated + Island matching algorithm + - Same center of UV island + - Same size of UV island + - Same number of UV + """ + + bl_idname = "uv.muv_ot_pack_uv" + bl_label = "Pack UV" + bl_description = "Pack UV (Same UV Islands are integrated)" + bl_options = {'REGISTER', 'UNDO'} + + rotate = BoolProperty( + name="Rotate", + description="Rotate option used by default pack UV function", + default=False) + margin = FloatProperty( + name="Margin", + description="Margin used by default pack UV function", + min=0, + max=1, + default=0.001) + allowable_center_deviation = FloatVectorProperty( + name="Allowable Center Deviation", + description="Allowable center deviation to judge same UV island", + min=0.000001, + max=0.1, + default=(0.001, 0.001), + size=2 + ) + allowable_size_deviation = FloatVectorProperty( + name="Allowable Size Deviation", + description="Allowable sizse deviation to judge same UV island", + min=0.000001, + max=0.1, + default=(0.001, 0.001), + size=2 + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + 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() + if not bm.loops.layers.uv: + self.report({'WARNING'}, + "Object must have more than one UV map") + return {'CANCELLED'} + uv_layer = bm.loops.layers.uv.verify() + + selected_faces = [f for f in bm.faces if f.select] + island_info = common.get_island_info(obj) + num_group = _group_island(island_info, + self.allowable_center_deviation, + self.allowable_size_deviation) + + loop_lists = [l for f in bm.faces for l in f.loops] + bpy.ops.mesh.select_all(action='DESELECT') + + # pack UV + for gidx in range(num_group): + group = list(filter( + lambda i, idx=gidx: i['group'] == idx, island_info)) + for f in group[0]['faces']: + f['face'].select = True + bmesh.update_edit_mesh(obj.data) + bpy.ops.uv.select_all(action='SELECT') + bpy.ops.uv.pack_islands(rotate=self.rotate, margin=self.margin) + + # copy/paste UV among same islands + for gidx in range(num_group): + group = list(filter( + lambda i, idx=gidx: i['group'] == idx, island_info)) + if len(group) <= 1: + continue + for g in group[1:]: + for (src_face, dest_face) in zip( + group[0]['sorted'], g['sorted']): + for (src_loop, dest_loop) in zip( + src_face['face'].loops, dest_face['face'].loops): + loop_lists[dest_loop.index][uv_layer].uv = loop_lists[ + src_loop.index][uv_layer].uv + + # restore face/UV selection + bpy.ops.uv.select_all(action='DESELECT') + bpy.ops.mesh.select_all(action='DESELECT') + for f in selected_faces: + f.select = True + bpy.ops.uv.select_all(action='SELECT') + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/magic_uv/op/preserve_uv_aspect.py b/magic_uv/op/preserve_uv_aspect.py new file mode 100644 index 00000000..116fe898 --- /dev/null +++ b/magic_uv/op/preserve_uv_aspect.py @@ -0,0 +1,297 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy +from bpy.props import StringProperty, EnumProperty, BoolProperty +import bmesh +from mathutils import Vector + +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +@PropertyClassRegistry() +class _Properties: + idname = "preserve_uv_aspect" + + @classmethod + def init_props(cls, scene): + def get_loaded_texture_name(_, __): + items = [(key, key, "") for key in bpy.data.images.keys()] + items.append(("None", "None", "")) + return items + + scene.muv_preserve_uv_aspect_enabled = BoolProperty( + name="Preserve UV Aspect Enabled", + description="Preserve UV Aspect is enabled", + default=False + ) + scene.muv_preserve_uv_aspect_tex_image = EnumProperty( + name="Image", + description="Texture Image", + items=get_loaded_texture_name + ) + scene.muv_preserve_uv_aspect_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" + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_preserve_uv_aspect_enabled + del scene.muv_preserve_uv_aspect_tex_image + del scene.muv_preserve_uv_aspect_origin + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_PreserveUVAspect(bpy.types.Operator): + """ + Operation class: Preserve UV Aspect + """ + + bl_idname = "uv.muv_ot_preserve_uv_aspect" + bl_label = "Preserve UV Aspect" + bl_description = "Choose Image" + bl_options = {'REGISTER', 'UNDO'} + + dest_img_name = StringProperty(options={'HIDDEN'}) + 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" + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + def execute(self, context): + # Note: the current system only works if the + # f[tex_layer].image doesn't return None + # which will happen in certain cases + 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() + + sel_faces = [f for f in bm.faces if f.select] + dest_img = bpy.data.images[self.dest_img_name] + + info = {} + + if compat.check_version(2, 80, 0) >= 0: + tex_image = common.find_image(obj) + for f in sel_faces: + if tex_image not in info.keys(): + info[tex_image] = {} + info[tex_image]['faces'] = [] + info[tex_image]['faces'].append(f) + else: + tex_layer = bm.faces.layers.tex.verify() + for f in sel_faces: + if not f[tex_layer].image in info.keys(): + info[f[tex_layer].image] = {} + info[f[tex_layer].image]['faces'] = [] + info[f[tex_layer].image]['faces'].append(f) + + for img in info: + if img is None: + continue + + src_img = img + ratio = Vector(( + dest_img.size[0] / src_img.size[0], + dest_img.size[1] / src_img.size[1])) + + if self.origin == 'CENTER': + origin = Vector((0.0, 0.0)) + num = 0 + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin = origin + uv + num = num + 1 + origin = origin / num + elif self.origin == 'LEFT_TOP': + origin = Vector((100000.0, -100000.0)) + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = min(origin.x, uv.x) + origin.y = max(origin.y, uv.y) + elif self.origin == 'LEFT_CENTER': + origin = Vector((100000.0, 0.0)) + num = 0 + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = min(origin.x, uv.x) + origin.y = origin.y + uv.y + num = num + 1 + origin.y = origin.y / num + elif self.origin == 'LEFT_BOTTOM': + origin = Vector((100000.0, 100000.0)) + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = min(origin.x, uv.x) + origin.y = min(origin.y, uv.y) + elif self.origin == 'CENTER_TOP': + origin = Vector((0.0, -100000.0)) + num = 0 + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = origin.x + uv.x + origin.y = max(origin.y, uv.y) + num = num + 1 + origin.x = origin.x / num + elif self.origin == 'CENTER_BOTTOM': + origin = Vector((0.0, 100000.0)) + num = 0 + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = origin.x + uv.x + origin.y = min(origin.y, uv.y) + num = num + 1 + origin.x = origin.x / num + elif self.origin == 'RIGHT_TOP': + origin = Vector((-100000.0, -100000.0)) + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = max(origin.x, uv.x) + origin.y = max(origin.y, uv.y) + elif self.origin == 'RIGHT_CENTER': + origin = Vector((-100000.0, 0.0)) + num = 0 + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = max(origin.x, uv.x) + origin.y = origin.y + uv.y + num = num + 1 + origin.y = origin.y / num + elif self.origin == 'RIGHT_BOTTOM': + origin = Vector((-100000.0, 100000.0)) + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = max(origin.x, uv.x) + origin.y = min(origin.y, uv.y) + else: + self.report({'ERROR'}, "Unknown Operation") + return {'CANCELLED'} + + info[img]['ratio'] = ratio + info[img]['origin'] = origin + + for img in info: + if img is None: + continue + + if compat.check_version(2, 80, 0) >= 0: + nodes = common.find_texture_nodes(obj) + nodes[0].image = dest_img + + for f in info[img]['faces']: + if compat.check_version(2, 80, 0) < 0: + tex_layer = bm.faces.layers.tex.verify() + f[tex_layer].image = dest_img + for l in f.loops: + uv = l[uv_layer].uv + origin = info[img]['origin'] + ratio = info[img]['ratio'] + diff = uv - origin + diff.x = diff.x / ratio.x + diff.y = diff.y / ratio.y + uv.x = origin.x + diff.x + uv.y = origin.y + diff.y + l[uv_layer].uv = uv + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/magic_uv/op/select_uv.py b/magic_uv/op/select_uv.py new file mode 100644 index 00000000..72757e29 --- /dev/null +++ b/magic_uv/op/select_uv.py @@ -0,0 +1,161 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy +from bpy.props import BoolProperty +import bmesh + +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +@PropertyClassRegistry() +class _Properties: + idname = "select_uv" + + @classmethod + def init_props(cls, scene): + scene.muv_select_uv_enabled = BoolProperty( + name="Select UV Enabled", + description="Select UV is enabled", + default=False + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_select_uv_enabled + + +@BlClassRegistry() +class MUV_OT_SelectUV_SelectOverlapped(bpy.types.Operator): + """ + Operation class: Select faces which have overlapped UVs + """ + + bl_idname = "uv.muv_ot_select_uv_select_overlapped" + bl_label = "Overlapped" + bl_description = "Select faces which have overlapped UVs" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + 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 = common.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'} + + +@BlClassRegistry() +class MUV_OT_SelectUV_SelectFlipped(bpy.types.Operator): + """ + Operation class: Select faces which have flipped UVs + """ + + bl_idname = "uv.muv_ot_select_uv_select_flipped" + bl_label = "Flipped" + bl_description = "Select faces which have flipped UVs" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + 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 = common.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/magic_uv/op/smooth_uv.py b/magic_uv/op/smooth_uv.py new file mode 100644 index 00000000..0cb4df51 --- /dev/null +++ b/magic_uv/op/smooth_uv.py @@ -0,0 +1,283 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy +from bpy.props import BoolProperty, FloatProperty +import bmesh + +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +@PropertyClassRegistry() +class _Properties: + idname = "smooth_uv" + + @classmethod + def init_props(cls, scene): + scene.muv_smooth_uv_enabled = BoolProperty( + name="Smooth UV Enabled", + description="Smooth UV is enabled", + default=False + ) + scene.muv_smooth_uv_transmission = BoolProperty( + name="Transmission", + description="Smooth linked UVs", + default=False + ) + scene.muv_smooth_uv_mesh_infl = FloatProperty( + name="Mesh Influence", + description="Influence rate of mesh vertex", + min=0.0, + max=1.0, + default=0.0 + ) + scene.muv_smooth_uv_select = BoolProperty( + name="Select", + description="Select UVs which are smoothed", + default=False + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_smooth_uv_enabled + del scene.muv_smooth_uv_transmission + del scene.muv_smooth_uv_mesh_infl + del scene.muv_smooth_uv_select + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_SmoothUV(bpy.types.Operator): + + bl_idname = "uv.muv_ot_smooth_uv" + 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): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + 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/magic_uv/op/texture_lock.py b/magic_uv/op/texture_lock.py new file mode 100644 index 00000000..791a7ae6 --- /dev/null +++ b/magic_uv/op/texture_lock.py @@ -0,0 +1,537 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import math +from math import atan2, cos, sqrt, sin, fabs + +import bpy +from bpy.props import BoolProperty +import bmesh +from mathutils import Vector + +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat + + +def _get_vco(verts_orig, loop): + """ + Get vertex original coordinate from loop + """ + for vo in verts_orig: + if vo["vidx"] == loop.vert.index and vo["moved"] is False: + return vo["vco"] + return loop.vert.co + + +def _get_link_loops(vert): + """ + Get loop linked to vertex + """ + link_loops = [] + for f in vert.link_faces: + adj_loops = [] + for loop in f.loops: + # self loop + if loop.vert == vert: + l = loop + # linked loop + else: + for e in loop.vert.link_edges: + if e.other_vert(loop.vert) == vert: + adj_loops.append(loop) + if len(adj_loops) < 2: + return None + + link_loops.append({"l": l, "l0": adj_loops[0], "l1": adj_loops[1]}) + return link_loops + + +def _get_ini_geom(link_loop, uv_layer, verts_orig, v_orig): + """ + Get initial geometory + (Get interior angle of face in vertex/UV space) + """ + u = link_loop["l"][uv_layer].uv + v0 = _get_vco(verts_orig, link_loop["l0"]) + u0 = link_loop["l0"][uv_layer].uv + v1 = _get_vco(verts_orig, link_loop["l1"]) + u1 = link_loop["l1"][uv_layer].uv + + # get interior angle of face in vertex space + v0v1 = v1 - v0 + v0v = v_orig["vco"] - v0 + v1v = v_orig["vco"] - v1 + theta0 = v0v1.angle(v0v) + theta1 = v0v1.angle(-v1v) + if (theta0 + theta1) > math.pi: + theta0 = v0v1.angle(-v0v) + theta1 = v0v1.angle(v1v) + + # get interior angle of face in UV space + u0u1 = u1 - u0 + u0u = u - u0 + u1u = u - u1 + phi0 = u0u1.angle(u0u) + phi1 = u0u1.angle(-u1u) + if (phi0 + phi1) > math.pi: + phi0 = u0u1.angle(-u0u) + phi1 = u0u1.angle(u1u) + + # get direction of linked UV coordinate + # this will be used to judge whether angle is more or less than 180 degree + dir0 = u0u1.cross(u0u) > 0 + dir1 = u0u1.cross(u1u) > 0 + + return { + "theta0": theta0, + "theta1": theta1, + "phi0": phi0, + "phi1": phi1, + "dir0": dir0, + "dir1": dir1} + + +def _get_target_uv(link_loop, uv_layer, verts_orig, v, ini_geom): + """ + Get target UV coordinate + """ + v0 = _get_vco(verts_orig, link_loop["l0"]) + lo0 = link_loop["l0"] + v1 = _get_vco(verts_orig, link_loop["l1"]) + lo1 = link_loop["l1"] + + # get interior angle of face in vertex space + v0v1 = v1 - v0 + v0v = v.co - v0 + v1v = v.co - v1 + theta0 = v0v1.angle(v0v) + theta1 = v0v1.angle(-v1v) + if (theta0 + theta1) > math.pi: + theta0 = v0v1.angle(-v0v) + theta1 = v0v1.angle(v1v) + + # calculate target interior angle in UV space + phi0 = theta0 * ini_geom["phi0"] / ini_geom["theta0"] + phi1 = theta1 * ini_geom["phi1"] / ini_geom["theta1"] + + uv0 = lo0[uv_layer].uv + uv1 = lo1[uv_layer].uv + + # calculate target vertex coordinate from target interior angle + tuv0, tuv1 = _calc_tri_vert(uv0, uv1, phi0, phi1) + + # target UV coordinate depends on direction, so judge using direction of + # linked UV coordinate + u0u1 = uv1 - uv0 + u0u = tuv0 - uv0 + u1u = tuv0 - uv1 + dir0 = u0u1.cross(u0u) > 0 + dir1 = u0u1.cross(u1u) > 0 + if (ini_geom["dir0"] != dir0) or (ini_geom["dir1"] != dir1): + return tuv1 + + return tuv0 + + +def _calc_tri_vert(v0, v1, angle0, angle1): + """ + Calculate rest coordinate from other coordinates and angle of end + """ + angle = math.pi - angle0 - angle1 + + alpha = atan2(v1.y - v0.y, v1.x - v0.x) + d = (v1.x - v0.x) / cos(alpha) + a = d * sin(angle0) / sin(angle) + b = d * sin(angle1) / sin(angle) + s = (a + b + d) / 2.0 + if fabs(d) < 0.0000001: + xd = 0 + yd = 0 + else: + r = s * (s - a) * (s - b) * (s - d) + if r < 0: + xd = 0 + yd = 0 + else: + xd = (b * b - a * a + d * d) / (2 * d) + yd = 2 * sqrt(r) / d + x1 = xd * cos(alpha) - yd * sin(alpha) + v0.x + y1 = xd * sin(alpha) + yd * cos(alpha) + v0.y + x2 = xd * cos(alpha) + yd * sin(alpha) + v0.x + y2 = xd * sin(alpha) - yd * cos(alpha) + v0.y + + return Vector((x1, y1)), Vector((x2, y2)) + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +@PropertyClassRegistry() +class _Properties: + idname = "texture_lock" + + @classmethod + def init_props(cls, scene): + class Props(): + verts_orig = None + + scene.muv_props.texture_lock = Props() + + def get_func(_): + return MUV_OT_TextureLock_Intr.is_running(bpy.context) + + def set_func(_, __): + pass + + def update_func(_, __): + bpy.ops.uv.muv_ot_texture_lock_intr('INVOKE_REGION_WIN') + + scene.muv_texture_lock_enabled = BoolProperty( + name="Texture Lock Enabled", + description="Texture Lock is enabled", + default=False + ) + scene.muv_texture_lock_lock = BoolProperty( + name="Texture Lock Locked", + description="Texture Lock is locked", + default=False, + get=get_func, + set=set_func, + update=update_func + ) + scene.muv_texture_lock_connect = BoolProperty( + name="Connect UV", + default=True + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_props.texture_lock + del scene.muv_texture_lock_enabled + del scene.muv_texture_lock_lock + del scene.muv_texture_lock_connect + + +@BlClassRegistry() +class MUV_OT_TextureLock_Lock(bpy.types.Operator): + """ + Operation class: Lock Texture + """ + + bl_idname = "uv.muv_ot_texture_lock_lock" + bl_label = "Lock Texture" + bl_description = "Lock Texture" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + @classmethod + def is_ready(cls, context): + sc = context.scene + props = sc.muv_props.texture_lock + if props.verts_orig: + return True + return False + + def execute(self, context): + props = context.scene.muv_props.texture_lock + obj = bpy.context.active_object + bm = bmesh.from_edit_mesh(obj.data) + 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'} + + props.verts_orig = [ + {"vidx": v.index, "vco": v.co.copy(), "moved": False} + for v in bm.verts if v.select] + + return {'FINISHED'} + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_TextureLock_Unlock(bpy.types.Operator): + """ + Operation class: Unlock Texture + """ + + bl_idname = "uv.muv_ot_texture_lock_unlock" + bl_label = "Unlock Texture" + bl_description = "Unlock Texture" + bl_options = {'REGISTER', 'UNDO'} + + connect = BoolProperty( + name="Connect UV", + default=True + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.texture_lock + if not props.verts_orig: + return False + if not MUV_OT_TextureLock_Lock.is_ready(context): + return False + if not _is_valid_context(context): + return False + return True + + def execute(self, context): + sc = context.scene + props = sc.muv_props.texture_lock + obj = bpy.context.active_object + bm = bmesh.from_edit_mesh(obj.data) + 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() + + verts = [v.index for v in bm.verts if v.select] + verts_orig = props.verts_orig + + # move UV followed by vertex coordinate + for vidx, v_orig in zip(verts, verts_orig): + if vidx != v_orig["vidx"]: + self.report({'ERROR'}, "Internal Error") + return {"CANCELLED"} + + v = bm.verts[vidx] + link_loops = _get_link_loops(v) + + result = [] + + for ll in link_loops: + ini_geom = _get_ini_geom(ll, uv_layer, verts_orig, v_orig) + target_uv = _get_target_uv( + ll, uv_layer, verts_orig, v, ini_geom) + result.append({"l": ll["l"], "uv": target_uv}) + + # connect other face's UV + if self.connect: + ave = Vector((0.0, 0.0)) + for r in result: + ave = ave + r["uv"] + ave = ave / len(result) + for r in result: + r["l"][uv_layer].uv = ave + else: + for r in result: + r["l"][uv_layer].uv = r["uv"] + v_orig["moved"] = True + bmesh.update_edit_mesh(obj.data) + + props.verts_orig = None + + return {'FINISHED'} + + +@BlClassRegistry() +class MUV_OT_TextureLock_Intr(bpy.types.Operator): + """ + Operation class: Texture Lock (Interactive mode) + """ + + bl_idname = "uv.muv_ot_texture_lock_intr" + bl_label = "Texture Lock (Interactive mode)" + bl_description = "Internal operation for Texture Lock (Interactive mode)" + + __timer = None + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return False + return _is_valid_context(context) + + @classmethod + def is_running(cls, _): + return 1 if cls.__timer else 0 + + @classmethod + def handle_add(cls, ops_obj, context): + if cls.__timer is None: + cls.__timer = context.window_manager.event_timer_add( + 0.10, window=context.window) + context.window_manager.modal_handler_add(ops_obj) + + @classmethod + def handle_remove(cls, context): + if cls.__timer is not None: + context.window_manager.event_timer_remove(cls.__timer) + cls.__timer = None + + def __init__(self): + self.__intr_verts_orig = [] + self.__intr_verts = [] + + def __sel_verts_changed(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + prev = set(self.__intr_verts) + now = set([v.index for v in bm.verts if v.select]) + + return prev != now + + def __reinit_verts(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + self.__intr_verts_orig = [ + {"vidx": v.index, "vco": v.co.copy(), "moved": False} + for v in bm.verts if v.select] + self.__intr_verts = [v.index for v in bm.verts if v.select] + + def __update_uv(self, context): + """ + Update UV when vertex coordinates are changed + """ + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + 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 + uv_layer = bm.loops.layers.uv.verify() + + verts = [v.index for v in bm.verts if v.select] + verts_orig = self.__intr_verts_orig + + for vidx, v_orig in zip(verts, verts_orig): + if vidx != v_orig["vidx"]: + self.report({'ERROR'}, "Internal Error") + return + + v = bm.verts[vidx] + link_loops = _get_link_loops(v) + + result = [] + for ll in link_loops: + ini_geom = _get_ini_geom(ll, uv_layer, verts_orig, v_orig) + target_uv = _get_target_uv( + ll, uv_layer, verts_orig, v, ini_geom) + result.append({"l": ll["l"], "uv": target_uv}) + + # UV connect option is always true, because it raises + # unexpected behavior + ave = Vector((0.0, 0.0)) + for r in result: + ave = ave + r["uv"] + ave = ave / len(result) + for r in result: + r["l"][uv_layer].uv = ave + v_orig["moved"] = True + bmesh.update_edit_mesh(obj.data) + + common.redraw_all_areas() + self.__intr_verts_orig = [ + {"vidx": v.index, "vco": v.co.copy(), "moved": False} + for v in bm.verts if v.select] + + def modal(self, context, event): + if not _is_valid_context(context): + MUV_OT_TextureLock_Intr.handle_remove(context) + return {'FINISHED'} + + if not MUV_OT_TextureLock_Intr.is_running(context): + return {'FINISHED'} + + if context.area: + context.area.tag_redraw() + + if event.type == 'TIMER': + if self.__sel_verts_changed(context): + self.__reinit_verts(context) + else: + self.__update_uv(context) + + return {'PASS_THROUGH'} + + def invoke(self, context, _): + if not _is_valid_context(context): + return {'CANCELLED'} + + if not MUV_OT_TextureLock_Intr.is_running(context): + MUV_OT_TextureLock_Intr.handle_add(self, context) + return {'RUNNING_MODAL'} + else: + MUV_OT_TextureLock_Intr.handle_remove(context) + + if context.area: + context.area.tag_redraw() + + return {'FINISHED'} diff --git a/magic_uv/op/texture_projection.py b/magic_uv/op/texture_projection.py new file mode 100644 index 00000000..b5360e4d --- /dev/null +++ b/magic_uv/op/texture_projection.py @@ -0,0 +1,417 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +from collections import namedtuple + +import bpy +import bmesh +from bpy_extras import view3d_utils +from bpy.props import ( + BoolProperty, + EnumProperty, + FloatProperty, +) +import mathutils + +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat + +if compat.check_version(2, 80, 0) >= 0: + from ..lib import bglx as bgl +else: + import bgl + + +_Rect = namedtuple('Rect', 'x0 y0 x1 y1') +_Rect2 = namedtuple('Rect2', 'x y width height') + + +def _get_loaded_texture_name(_, __): + items = [(key, key, "") for key in bpy.data.images.keys()] + items.append(("None", "None", "")) + return items + + +def _get_canvas(context, magnitude): + """ + Get canvas to be renderred texture + """ + sc = context.scene + user_prefs = compat.get_user_preferences(context) + prefs = user_prefs.addons["magic_uv"].preferences + + region_w = context.region.width + region_h = context.region.height + canvas_w = region_w - prefs.texture_projection_canvas_padding[0] * 2.0 + canvas_h = region_h - prefs.texture_projection_canvas_padding[1] * 2.0 + + img = bpy.data.images[sc.muv_texture_projection_tex_image] + tex_w = img.size[0] + tex_h = img.size[1] + + center_x = region_w * 0.5 + center_y = region_h * 0.5 + + if sc.muv_texture_projection_adjust_window: + ratio_x = canvas_w / tex_w + ratio_y = canvas_h / tex_h + if sc.muv_texture_projection_apply_tex_aspect: + ratio = ratio_y if ratio_x > ratio_y else ratio_x + len_x = ratio * tex_w + len_y = ratio * tex_h + else: + len_x = canvas_w + len_y = canvas_h + else: + if sc.muv_texture_projection_apply_tex_aspect: + len_x = tex_w * magnitude + len_y = tex_h * magnitude + else: + len_x = region_w * magnitude + len_y = region_h * magnitude + + x0 = int(center_x - len_x * 0.5) + y0 = int(center_y - len_y * 0.5) + x1 = int(center_x + len_x * 0.5) + y1 = int(center_y + len_y * 0.5) + + return _Rect(x0, y0, x1, y1) + + +def _rect_to_rect2(rect): + """ + Convert Rect1 to Rect2 + """ + + return _Rect2(rect.x0, rect.y0, rect.x1 - rect.x0, rect.y1 - rect.y0) + + +def _region_to_canvas(rg_vec, canvas): + """ + Convert screen region to canvas + """ + + cv_rect = _rect_to_rect2(canvas) + cv_vec = mathutils.Vector() + cv_vec.x = (rg_vec.x - cv_rect.x) / cv_rect.width + cv_vec.y = (rg_vec.y - cv_rect.y) / cv_rect.height + + return cv_vec + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +@PropertyClassRegistry() +class _Properties: + idname = "texture_projection" + + @classmethod + def init_props(cls, scene): + def get_func(_): + return MUV_OT_TextureProjection.is_running(bpy.context) + + def set_func(_, __): + pass + + def update_func(_, __): + bpy.ops.uv.muv_ot_texture_projection('INVOKE_REGION_WIN') + + scene.muv_texture_projection_enabled = BoolProperty( + name="Texture Projection Enabled", + description="Texture Projection is enabled", + default=False + ) + scene.muv_texture_projection_enable = BoolProperty( + name="Texture Projection Enabled", + description="Texture Projection is enabled", + default=False, + get=get_func, + set=set_func, + update=update_func + ) + scene.muv_texture_projection_tex_magnitude = FloatProperty( + name="Magnitude", + description="Texture Magnitude", + default=0.5, + min=0.0, + max=100.0 + ) + scene.muv_texture_projection_tex_image = EnumProperty( + name="Image", + description="Texture Image", + items=_get_loaded_texture_name + ) + scene.muv_texture_projection_tex_transparency = FloatProperty( + name="Transparency", + description="Texture Transparency", + default=0.2, + min=0.0, + max=1.0 + ) + scene.muv_texture_projection_adjust_window = BoolProperty( + name="Adjust Window", + description="Size of renderered texture is fitted to window", + default=True + ) + scene.muv_texture_projection_apply_tex_aspect = BoolProperty( + name="Texture Aspect Ratio", + description="Apply Texture Aspect ratio to displayed texture", + default=True + ) + scene.muv_texture_projection_assign_uvmap = BoolProperty( + name="Assign UVMap", + description="Assign UVMap when no UVmaps are available", + default=True + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_texture_projection_enabled + del scene.muv_texture_projection_tex_magnitude + del scene.muv_texture_projection_tex_image + del scene.muv_texture_projection_tex_transparency + del scene.muv_texture_projection_adjust_window + del scene.muv_texture_projection_apply_tex_aspect + del scene.muv_texture_projection_assign_uvmap + + +@BlClassRegistry() +class MUV_OT_TextureProjection(bpy.types.Operator): + """ + Operation class: Texture Projection + Render texture + """ + + bl_idname = "uv.muv_ot_texture_projection" + bl_description = "Render selected texture" + bl_label = "Texture renderer" + + __handle = None + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return False + return _is_valid_context(context) + + @classmethod + def is_running(cls, _): + return 1 if cls.__handle else 0 + + @classmethod + def handle_add(cls, obj, context): + cls.__handle = bpy.types.SpaceView3D.draw_handler_add( + MUV_OT_TextureProjection.draw_texture, + (obj, context), 'WINDOW', 'POST_PIXEL') + + @classmethod + def handle_remove(cls): + if cls.__handle is not None: + bpy.types.SpaceView3D.draw_handler_remove(cls.__handle, 'WINDOW') + cls.__handle = None + + @classmethod + def draw_texture(cls, _, context): + sc = context.scene + + if not cls.is_running(context): + return + + # no textures are selected + if sc.muv_texture_projection_tex_image == "None": + return + + # get texture to be renderred + img = bpy.data.images[sc.muv_texture_projection_tex_image] + + # setup rendering region + rect = _get_canvas(context, sc.muv_texture_projection_tex_magnitude) + positions = [ + [rect.x0, rect.y0], + [rect.x0, rect.y1], + [rect.x1, rect.y1], + [rect.x1, rect.y0] + ] + tex_coords = [ + [0.0, 0.0], + [0.0, 1.0], + [1.0, 1.0], + [1.0, 0.0] + ] + + # OpenGL configuration + if compat.check_version(2, 80, 0) >= 0: + bgl.glEnable(bgl.GL_BLEND) + bgl.glEnable(bgl.GL_TEXTURE_2D) + bgl.glActiveTexture(bgl.GL_TEXTURE0) + if img.bindcode: + bind = img.bindcode + bgl.glBindTexture(bgl.GL_TEXTURE_2D, bind) + else: + bgl.glEnable(bgl.GL_BLEND) + bgl.glEnable(bgl.GL_TEXTURE_2D) + if img.bindcode: + bind = img.bindcode[0] + bgl.glBindTexture(bgl.GL_TEXTURE_2D, bind) + bgl.glTexParameteri(bgl.GL_TEXTURE_2D, + bgl.GL_TEXTURE_MIN_FILTER, bgl.GL_LINEAR) + bgl.glTexParameteri(bgl.GL_TEXTURE_2D, + bgl.GL_TEXTURE_MAG_FILTER, bgl.GL_LINEAR) + bgl.glTexEnvi( + bgl.GL_TEXTURE_ENV, bgl.GL_TEXTURE_ENV_MODE, + bgl.GL_MODULATE) + + # render texture + bgl.glBegin(bgl.GL_QUADS) + bgl.glColor4f(1.0, 1.0, 1.0, + sc.muv_texture_projection_tex_transparency) + for (v1, v2), (u, v) in zip(positions, tex_coords): + bgl.glTexCoord2f(u, v) + bgl.glVertex2f(v1, v2) + bgl.glEnd() + + def invoke(self, context, _): + if not MUV_OT_TextureProjection.is_running(context): + MUV_OT_TextureProjection.handle_add(self, context) + else: + MUV_OT_TextureProjection.handle_remove() + + if context.area: + context.area.tag_redraw() + + return {'FINISHED'} + + +@BlClassRegistry() +class MUV_OT_TextureProjection_Project(bpy.types.Operator): + """ + Operation class: Project texture + """ + + bl_idname = "uv.muv_ot_texture_projection_project" + bl_label = "Project Texture" + bl_description = "Project Texture" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + if not MUV_OT_TextureProjection.is_running(context): + return False + return _is_valid_context(context) + + def execute(self, context): + sc = context.scene + + if sc.muv_texture_projection_tex_image == "None": + self.report({'WARNING'}, "No textures are selected") + return {'CANCELLED'} + + _, 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 common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + # get UV and texture layer + if not bm.loops.layers.uv: + if sc.muv_texture_projection_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() + if compat.check_version(2, 80, 0) < 0: + tex_layer = bm.faces.layers.tex.verify() + + sel_faces = [f for f in bm.faces if f.select] + + # transform 3d space to screen region + v_screen = [ + view3d_utils.location_3d_to_region_2d( + region, + space.region_3d, + compat.matmul(world_mat, l.vert.co)) + for f in sel_faces for l in f.loops + ] + + # transform screen region to canvas + v_canvas = [ + _region_to_canvas( + v, + _get_canvas(bpy.context, + sc.muv_texture_projection_tex_magnitude) + ) for v in v_screen + ] + + if compat.check_version(2, 80, 0) >= 0: + # set texture + nodes = common.find_texture_nodes(obj) + nodes[0].image = \ + bpy.data.images[sc.muv_texture_projection_tex_image] + + # project texture to object + i = 0 + for f in sel_faces: + if compat.check_version(2, 80, 0) < 0: + f[tex_layer].image = \ + bpy.data.images[sc.muv_texture_projection_tex_image] + for l in f.loops: + l[uv_layer].uv = v_canvas[i].to_2d() + i = i + 1 + + common.redraw_all_areas() + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/magic_uv/op/texture_wrap.py b/magic_uv/op/texture_wrap.py new file mode 100644 index 00000000..49242b83 --- /dev/null +++ b/magic_uv/op/texture_wrap.py @@ -0,0 +1,294 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy +from bpy.props import ( + BoolProperty, +) +import bmesh + +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +@PropertyClassRegistry() +class _Properties: + idname = "texture_wrap" + + @classmethod + def init_props(cls, scene): + class Props(): + ref_face_index = -1 + ref_obj = None + + scene.muv_props.texture_wrap = Props() + + scene.muv_texture_wrap_enabled = BoolProperty( + name="Texture Wrap", + description="Texture Wrap is enabled", + default=False + ) + scene.muv_texture_wrap_set_and_refer = BoolProperty( + name="Set and Refer", + description="Refer and set UV", + default=True + ) + scene.muv_texture_wrap_selseq = BoolProperty( + name="Selection Sequence", + description="Set UV sequentially", + default=False + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_props.texture_wrap + del scene.muv_texture_wrap_enabled + del scene.muv_texture_wrap_set_and_refer + del scene.muv_texture_wrap_selseq + + +@BlClassRegistry() +class MUV_OT_TextureWrap_Refer(bpy.types.Operator): + """ + Operation class: Refer UV + """ + + bl_idname = "uv.muv_ot_texture_wrap_refer" + bl_label = "Refer" + bl_description = "Refer UV" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + def execute(self, context): + props = context.scene.muv_props.texture_wrap + 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'} + + +@BlClassRegistry() +class MUV_OT_TextureWrap_Set(bpy.types.Operator): + """ + Operation class: Set UV + """ + + bl_idname = "uv.muv_ot_texture_wrap_set" + bl_label = "Set" + bl_description = "Set UV" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.texture_wrap + if not props.ref_obj: + return False + return _is_valid_context(context) + + def execute(self, context): + sc = context.scene + props = sc.muv_props.texture_wrap + 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_texture_wrap_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 vertices 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 Vertices =====") + common.debug_print(tgt_other_verts) + + bmesh.update_edit_mesh(obj.data) + + ref_face_index = tgt_face_index + + if sc.muv_texture_wrap_set_and_refer: + props.ref_face_index = tgt_face_index + + return {'FINISHED'} diff --git a/magic_uv/op/transfer_uv.py b/magic_uv/op/transfer_uv.py new file mode 100644 index 00000000..e812d295 --- /dev/null +++ b/magic_uv/op/transfer_uv.py @@ -0,0 +1,457 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>, Mifth, MaxRobinot" +__status__ = "production" +__version__ = "6.0" +__date__ = "26 Jan 2019" + +from collections import OrderedDict + +import bpy +import bmesh +from bpy.props import BoolProperty + +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +def _get_uv_layer(ops_obj, bm): + # get UV layer + if not bm.loops.layers.uv: + ops_obj.report({'WARNING'}, "Object must have more than one UV map") + return None + uv_layer = bm.loops.layers.uv.verify() + + return uv_layer + + +def _main_parse(ops_obj, uv_layer, sel_faces, active_face, active_face_nor): + all_sorted_faces = OrderedDict() # This is the main stuff + + used_verts = set() + used_edges = set() + + faces_to_parse = [] + + # get shared edge of two faces + cross_edges = [] + for edge in active_face.edges: + if edge in sel_faces[0].edges and edge in sel_faces[1].edges: + cross_edges.append(edge) + + # parse two selected faces + if cross_edges and len(cross_edges) == 1: + shared_edge = cross_edges[0] + vert1 = None + vert2 = None + + dot_n = active_face_nor.normalized() + edge_vec_1 = (shared_edge.verts[1].co - shared_edge.verts[0].co) + edge_vec_len = edge_vec_1.length + edge_vec_1 = edge_vec_1.normalized() + + af_center = active_face.calc_center_median() + af_vec = shared_edge.verts[0].co + (edge_vec_1 * (edge_vec_len * 0.5)) + af_vec = (af_vec - af_center).normalized() + + if af_vec.cross(edge_vec_1).dot(dot_n) > 0: + vert1 = shared_edge.verts[0] + vert2 = shared_edge.verts[1] + else: + vert1 = shared_edge.verts[1] + vert2 = shared_edge.verts[0] + + # get active face stuff and uvs + face_stuff = _get_other_verts_edges( + active_face, vert1, vert2, shared_edge, uv_layer) + all_sorted_faces[active_face] = face_stuff + used_verts.update(active_face.verts) + used_edges.update(active_face.edges) + + # get first selected face stuff and uvs as they share shared_edge + second_face = sel_faces[0] + if second_face is active_face: + second_face = sel_faces[1] + face_stuff = _get_other_verts_edges( + second_face, vert1, vert2, shared_edge, uv_layer) + all_sorted_faces[second_face] = face_stuff + used_verts.update(second_face.verts) + used_edges.update(second_face.edges) + + # first Grow + faces_to_parse.append(active_face) + faces_to_parse.append(second_face) + + else: + ops_obj.report({'WARNING'}, "Two faces should share one edge") + return None + + # parse all faces + while True: + new_parsed_faces = [] + if not faces_to_parse: + break + for face in faces_to_parse: + face_stuff = all_sorted_faces.get(face) + new_faces = _parse_faces(face, face_stuff, used_verts, used_edges, + all_sorted_faces, uv_layer) + if new_faces is None: + ops_obj.report({'WARNING'}, "More than 2 faces share edge") + return None + + new_parsed_faces += new_faces + faces_to_parse = new_parsed_faces + + return all_sorted_faces + + +def _parse_faces(check_face, face_stuff, used_verts, used_edges, + all_sorted_faces, uv_layer): + """recurse faces around the new_grow only""" + + new_shared_faces = [] + for sorted_edge in face_stuff[1]: + shared_faces = sorted_edge.link_faces + if shared_faces: + if len(shared_faces) > 2: + bpy.ops.mesh.select_all(action='DESELECT') + for face_sel in shared_faces: + face_sel.select = True + shared_faces = [] + return None + + clear_shared_faces = _get_new_shared_faces( + check_face, sorted_edge, shared_faces, all_sorted_faces.keys()) + if clear_shared_faces: + shared_face = clear_shared_faces[0] + # get vertices of the edge + vert1 = sorted_edge.verts[0] + vert2 = sorted_edge.verts[1] + + 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] + + 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 common.is_debug_mode(): + shared_face.select = True # test which faces are parsed + + new_shared_faces.append(shared_face) + + return new_shared_faces + + +def _get_new_shared_faces(orig_face, shared_edge, check_faces, used_faces): + shared_faces = [] + + for face in check_faces: + is_shared_edge = shared_edge in face.edges + not_used = face not in used_faces + not_orig = face is not orig_face + not_hide = face.hide is False + if is_shared_edge and not_used and not_orig and not_hide: + shared_faces.append(face) + + return shared_faces + + +def _get_other_verts_edges(face, vert1, vert2, first_edge, uv_layer): + face_edges = [first_edge] + face_verts = [vert1, vert2] + face_loops = [] + + other_edges = [edge for edge in face.edges if edge not in face_edges] + + for _ in range(len(other_edges)): + found_edge = None + # get sorted verts and edges + for edge in other_edges: + if face_verts[-1] in edge.verts: + other_vert = edge.other_vert(face_verts[-1]) + + if other_vert not in face_verts: + face_verts.append(other_vert) + + found_edge = edge + if found_edge not in face_edges: + face_edges.append(edge) + break + + other_edges.remove(found_edge) + + # get sorted uvs + for vert in face_verts: + for loop in face.loops: + if loop.vert is vert: + face_loops.append(loop[uv_layer]) + break + + return [face_verts, face_edges, face_loops] + + +def _get_selected_src_faces(ops_obj, bm, uv_layer): + topology_copied = [] + + # get selected faces + active_face = bm.faces.active + sel_faces = [face for face in bm.faces if face.select] + if len(sel_faces) != 2: + ops_obj.report({'WARNING'}, "Two faces must be selected") + return None + if not active_face or active_face not in sel_faces: + ops_obj.report({'WARNING'}, "Two faces must be active") + return None + + # parse all faces according to selection + active_face_nor = active_face.normal.copy() + all_sorted_faces = _main_parse(ops_obj, uv_layer, sel_faces, active_face, + active_face_nor) + + if all_sorted_faces: + for face_data in all_sorted_faces.values(): + edges = face_data[1] + uv_loops = face_data[2] + uvs = [l.uv.copy() for l in uv_loops] + pin_uvs = [l.pin_uv for l in uv_loops] + seams = [e.seam for e in edges] + topology_copied.append([uvs, pin_uvs, seams]) + else: + return None + + return topology_copied + + +def _paste_uv(ops_obj, bm, uv_layer, src_faces, invert_normals, copy_seams): + # get selection history + all_sel_faces = [e for e in bm.select_history + if isinstance(e, bmesh.types.BMFace) and e.select] + if len(all_sel_faces) % 2 != 0: + ops_obj.report({'WARNING'}, "Two faces must be selected") + return -1 + + # parse selection history + for i, _ in enumerate(all_sel_faces): + if (i == 0) or (i % 2 == 0): + continue + sel_faces = [all_sel_faces[i - 1], all_sel_faces[i]] + active_face = all_sel_faces[i] + + # parse all faces according to selection history + active_face_nor = active_face.normal.copy() + if invert_normals: + active_face_nor.negate() + all_sorted_faces = _main_parse(ops_obj, uv_layer, sel_faces, + active_face, active_face_nor) + + if all_sorted_faces: + # check amount of copied/pasted faces + if len(all_sorted_faces) != len(src_faces): + ops_obj.report({'WARNING'}, + "Mesh has different amount of faces") + return -1 + + for j, face_data in enumerate(all_sorted_faces.values()): + copied_data = src_faces[j] + + # check amount of copied/pasted verts + if len(copied_data[0]) != len(face_data[2]): + bpy.ops.mesh.select_all(action='DESELECT') + # select problematic face + list(all_sorted_faces.keys())[j].select = True + ops_obj.report({'WARNING'}, + "Face have different amount of vertices") + return 0 + + for k, (edge, uvloop) in enumerate(zip(face_data[1], + face_data[2])): + uvloop.uv = copied_data[0][k] + uvloop.pin_uv = copied_data[1][k] + if copy_seams: + edge.seam = copied_data[2][k] + else: + return -1 + + return 0 + + +@PropertyClassRegistry() +class _Properties: + idname = "transfer_uv" + + @classmethod + def init_props(cls, scene): + class Props(): + topology_copied = None + + scene.muv_props.transfer_uv = Props() + + scene.muv_transfer_uv_enabled = BoolProperty( + name="Transfer UV Enabled", + description="Transfer UV is enabled", + default=False + ) + scene.muv_transfer_uv_invert_normals = BoolProperty( + name="Invert Normals", + description="Invert Normals", + default=False + ) + scene.muv_transfer_uv_copy_seams = BoolProperty( + name="Copy Seams", + description="Copy Seams", + default=True + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_transfer_uv_enabled + del scene.muv_transfer_uv_invert_normals + del scene.muv_transfer_uv_copy_seams + + +@BlClassRegistry() +class MUV_OT_TransferUV_CopyUV(bpy.types.Operator): + """ + Operation class: Transfer UV copy + Topological based copy + """ + + bl_idname = "uv.muv_ot_transfer_uv_copy_uv" + bl_label = "Transfer UV Copy UV" + bl_description = "Transfer UV Copy UV (Topological based copy)" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + def execute(self, context): + props = context.scene.muv_props.transfer_uv + active_obj = context.active_object + bm = bmesh.from_edit_mesh(active_obj.data) + if compat.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + uv_layer = _get_uv_layer(self, bm) + if uv_layer is None: + return {'CANCELLED'} + + faces = _get_selected_src_faces(self, bm, uv_layer) + if faces is None: + return {'CANCELLED'} + props.topology_copied = faces + + bmesh.update_edit_mesh(active_obj.data) + + return {'FINISHED'} + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_TransferUV_PasteUV(bpy.types.Operator): + """ + Operation class: Transfer UV paste + Topological based paste + """ + + bl_idname = "uv.muv_ot_transfer_uv_paste_uv" + bl_label = "Transfer UV Paste UV" + bl_description = "Transfer UV Paste UV (Topological based paste)" + bl_options = {'REGISTER', 'UNDO'} + + invert_normals = BoolProperty( + name="Invert Normals", + description="Invert Normals", + default=False + ) + copy_seams = BoolProperty( + name="Copy Seams", + description="Copy Seams", + default=True + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.transfer_uv + if not props.topology_copied: + return False + return _is_valid_context(context) + + def execute(self, context): + props = context.scene.muv_props.transfer_uv + active_obj = context.active_object + bm = bmesh.from_edit_mesh(active_obj.data) + if compat.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + # get UV layer + uv_layer = _get_uv_layer(self, bm) + if uv_layer is None: + return {'CANCELLED'} + + ret = _paste_uv(self, bm, uv_layer, props.topology_copied, + self.invert_normals, self.copy_seams) + if ret: + return {'CANCELLED'} + + bmesh.update_edit_mesh(active_obj.data) + + if compat.check_version(2, 80, 0) < 0: + if self.copy_seams: + active_obj.data.show_edge_seams = True + + return {'FINISHED'} diff --git a/magic_uv/op/unwrap_constraint.py b/magic_uv/op/unwrap_constraint.py new file mode 100644 index 00000000..b622663a --- /dev/null +++ b/magic_uv/op/unwrap_constraint.py @@ -0,0 +1,186 @@ +# ##### 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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy +from bpy.props import ( + BoolProperty, + EnumProperty, + FloatProperty, +) +import bmesh + +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +@PropertyClassRegistry() +class _Properties: + idname = "unwrap_constraint" + + @classmethod + def init_props(cls, scene): + scene.muv_unwrap_constraint_enabled = BoolProperty( + name="Unwrap Constraint Enabled", + description="Unwrap Constraint is enabled", + default=False + ) + scene.muv_unwrap_constraint_u_const = BoolProperty( + name="U-Constraint", + description="Keep UV U-axis coordinate", + default=False + ) + scene.muv_unwrap_constraint_v_const = BoolProperty( + name="V-Constraint", + description="Keep UV V-axis coordinate", + default=False + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_unwrap_constraint_enabled + del scene.muv_unwrap_constraint_u_const + del scene.muv_unwrap_constraint_v_const + + +@BlClassRegistry(legacy=True) +@compat.make_annotations +class MUV_OT_UnwrapConstraint(bpy.types.Operator): + """ + Operation class: Unwrap with constrain UV coordinate + """ + + bl_idname = "uv.muv_ot_unwrap_constraint" + bl_label = "Unwrap Constraint" + bl_description = "Unwrap while keeping uv coordinate" + bl_options = {'REGISTER', 'UNDO'} + + # property for original unwrap + method = EnumProperty( + name="Method", + description="Unwrapping method", + items=[ + ('ANGLE_BASED', 'Angle Based', 'Angle Based'), + ('CONFORMAL', 'Conformal', 'Conformal') + ], + default='ANGLE_BASED') + fill_holes = BoolProperty( + name="Fill Holes", + description="Virtual fill holes in meshes before unwrapping", + default=True) + correct_aspect = BoolProperty( + name="Correct Aspect", + description="Map UVs taking image aspect ratio into account", + default=True) + use_subsurf_data = BoolProperty( + name="Use Subsurf Modifier", + description="""Map UVs taking vertex position after subsurf + into account""", + default=False) + margin = FloatProperty( + name="Margin", + description="Space between islands", + max=1.0, + min=0.0, + default=0.001) + + # property for this operation + u_const = BoolProperty( + name="U-Constraint", + description="Keep UV U-axis coordinate", + default=False + ) + v_const = BoolProperty( + name="V-Constraint", + description="Keep UV V-axis coordinate", + default=False + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + 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() + + # 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'} + uv_layer = bm.loops.layers.uv.verify() + + # get original UV coordinate + faces = [f for f in bm.faces if f.select] + uv_list = [] + for f in faces: + uvs = [l[uv_layer].uv.copy() for l in f.loops] + uv_list.append(uvs) + + # unwrap + bpy.ops.uv.unwrap( + method=self.method, + fill_holes=self.fill_holes, + correct_aspect=self.correct_aspect, + use_subsurf_data=self.use_subsurf_data, + margin=self.margin) + + # when U/V-Constraint is checked, revert original coordinate + for f, uvs in zip(faces, uv_list): + for l, uv in zip(f.loops, uvs): + if self.u_const: + l[uv_layer].uv.x = uv.x + if self.v_const: + l[uv_layer].uv.y = uv.y + + # update mesh + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/magic_uv/op/uv_bounding_box.py b/magic_uv/op/uv_bounding_box.py new file mode 100644 index 00000000..38d665e1 --- /dev/null +++ b/magic_uv/op/uv_bounding_box.py @@ -0,0 +1,842 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +from enum import IntEnum +import math + +import bpy +import mathutils +import bmesh +from bpy.props import BoolProperty, EnumProperty + +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat + +if compat.check_version(2, 80, 0) >= 0: + from ..lib import bglx as bgl +else: + import bgl + + +MAX_VALUE = 100000.0 + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +@PropertyClassRegistry() +class _Properties: + idname = "uv_bounding_box" + + @classmethod + def init_props(cls, scene): + class Props(): + uv_info_ini = [] + ctrl_points_ini = [] + ctrl_points = [] + + scene.muv_props.uv_bounding_box = Props() + + def get_func(_): + return MUV_OT_UVBoundingBox.is_running(bpy.context) + + def set_func(_, __): + pass + + def update_func(_, __): + bpy.ops.uv.muv_ot_uv_bounding_box('INVOKE_REGION_WIN') + + scene.muv_uv_bounding_box_enabled = BoolProperty( + name="UV Bounding Box Enabled", + description="UV Bounding Box is enabled", + default=False + ) + scene.muv_uv_bounding_box_show = BoolProperty( + name="UV Bounding Box Showed", + description="UV Bounding Box is showed", + default=False, + get=get_func, + set=set_func, + update=update_func + ) + scene.muv_uv_bounding_box_uniform_scaling = BoolProperty( + name="Uniform Scaling", + description="Enable Uniform Scaling", + default=False + ) + scene.muv_uv_bounding_box_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") + ] + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_props.uv_bounding_box + del scene.muv_uv_bounding_box_enabled + del scene.muv_uv_bounding_box_show + del scene.muv_uv_bounding_box_uniform_scaling + del scene.muv_uv_bounding_box_boundary + + +class CommandBase: + """ + Custom class: Base class of command + """ + + def __init__(self): + self.op = 'NONE' # operation + + def to_matrix(self): + # mat = I + mat = mathutils.Matrix() + mat.identity() + return mat + + +class TranslationCommand(CommandBase): + """ + Custom class: Translation operation + """ + + def __init__(self, ix, iy): + super().__init__() + self.op = 'TRANSLATION' + self.__x = ix # current x + self.__y = iy # current y + self.__ix = ix # initial x + self.__iy = iy # initial y + + def to_matrix(self): + # mat = Mt + dx = self.__x - self.__ix + dy = self.__y - self.__iy + return mathutils.Matrix.Translation((dx, dy, 0)) + + def set(self, x, y): + self.__x = x + self.__y = y + + +class RotationCommand(CommandBase): + """ + Custom class: Rotation operation + """ + + def __init__(self, ix, iy, cx, cy): + super().__init__() + self.op = 'ROTATION' + self.__x = ix # current x + self.__y = iy # current y + self.__cx = cx # center of rotation x + self.__cy = cy # center of rotation y + dx = self.__x - self.__cx + dy = self.__y - self.__cy + self.__iangle = math.atan2(dy, dx) # initial rotation angle + + def to_matrix(self): + # mat = Mt * Mr * Mt^-1 + dx = self.__x - self.__cx + dy = self.__y - self.__cy + angle = math.atan2(dy, dx) - self.__iangle + mti = mathutils.Matrix.Translation((-self.__cx, -self.__cy, 0.0)) + mr = mathutils.Matrix.Rotation(angle, 4, 'Z') + mt = mathutils.Matrix.Translation((self.__cx, self.__cy, 0.0)) + return compat.matmul(compat.matmul(mt, mr), mti) + + def set(self, x, y): + self.__x = x + self.__y = y + + +class ScalingCommand(CommandBase): + """ + Custom class: Scaling operation + """ + + def __init__(self, ix, iy, ox, oy, dir_x, dir_y, mat): + super().__init__() + self.op = 'SCALING' + self.__ix = ix # initial x + self.__iy = iy # initial y + self.__x = ix # current x + self.__y = iy # current y + self.__ox = ox # origin of scaling x + self.__oy = oy # origin of scaling y + self.__dir_x = dir_x # direction of scaling x + self.__dir_y = dir_y # direction of scaling y + self.__mat = mat + # initial origin of scaling = M(to original transform) * (ox, oy) + iov = compat.matmul(mat, mathutils.Vector((ox, oy, 0.0))) + self.__iox = iov.x # initial origin of scaling X + self.__ioy = iov.y # initial origin of scaling y + + def to_matrix(self): + """ + mat = M(to original transform)^-1 * Mt(to origin) * Ms * + Mt(to origin)^-1 * M(to original transform) + """ + m = self.__mat + mi = self.__mat.inverted() + mtoi = mathutils.Matrix.Translation((-self.__iox, -self.__ioy, 0.0)) + mto = mathutils.Matrix.Translation((self.__iox, self.__ioy, 0.0)) + # every point must be transformed to origin + t = compat.matmul(m, mathutils.Vector((self.__ix, self.__iy, 0.0))) + tix, tiy = t.x, t.y + t = compat.matmul(m, mathutils.Vector((self.__ox, self.__oy, 0.0))) + tox, toy = t.x, t.y + t = compat.matmul(m, mathutils.Vector((self.__x, self.__y, 0.0))) + tx, ty = t.x, t.y + ms = mathutils.Matrix() + ms.identity() + if self.__dir_x == 1: + ms[0][0] = (tx - tox) * self.__dir_x / (tix - tox) + if self.__dir_y == 1: + ms[1][1] = (ty - toy) * self.__dir_y / (tiy - toy) + return compat.matmul(compat.matmul(compat.matmul( + compat.matmul(mi, mto), ms), mtoi), m) + + def set(self, x, y): + self.__x = x + self.__y = y + + +class UniformScalingCommand(CommandBase): + """ + Custom class: Uniform Scaling operation + """ + + def __init__(self, ix, iy, ox, oy, mat): + super().__init__() + self.op = 'SCALING' + self.__ix = ix # initial x + self.__iy = iy # initial y + self.__x = ix # current x + self.__y = iy # current y + self.__ox = ox # origin of scaling x + self.__oy = oy # origin of scaling y + self.__mat = mat + # initial origin of scaling = M(to original transform) * (ox, oy) + iov = compat.matmul(mat, mathutils.Vector((ox, oy, 0.0))) + self.__iox = iov.x # initial origin of scaling x + self.__ioy = iov.y # initial origin of scaling y + self.__dir_x = 1 + self.__dir_y = 1 + + def to_matrix(self): + """ + mat = M(to original transform)^-1 * Mt(to origin) * Ms * + Mt(to origin)^-1 * M(to original transform) + """ + m = self.__mat + mi = self.__mat.inverted() + mtoi = mathutils.Matrix.Translation((-self.__iox, -self.__ioy, 0.0)) + mto = mathutils.Matrix.Translation((self.__iox, self.__ioy, 0.0)) + # every point must be transformed to origin + t = compat.matmul(m, mathutils.Vector((self.__ix, self.__iy, 0.0))) + tix, tiy = t.x, t.y + t = compat.matmul(m, mathutils.Vector((self.__ox, self.__oy, 0.0))) + tox, toy = t.x, t.y + t = compat.matmul(m, mathutils.Vector((self.__x, self.__y, 0.0))) + tx, ty = t.x, t.y + ms = mathutils.Matrix() + ms.identity() + tir = math.sqrt((tix - tox) * (tix - tox) + (tiy - toy) * (tiy - toy)) + tr = math.sqrt((tx - tox) * (tx - tox) + (ty - toy) * (ty - toy)) + + sr = tr / tir + + if ((tx - tox) * (tix - tox)) > 0: + self.__dir_x = 1 + else: + self.__dir_x = -1 + if ((ty - toy) * (tiy - toy)) > 0: + self.__dir_y = 1 + else: + self.__dir_y = -1 + + ms[0][0] = sr * self.__dir_x + ms[1][1] = sr * self.__dir_y + + return compat.matmul(compat.matmul(compat.matmul( + compat.matmul(mi, mto), ms), mtoi), m) + + def set(self, x, y): + self.__x = x + self.__y = y + + +class CommandExecuter: + """ + Custom class: manage command history and execute command + """ + + def __init__(self): + self.__cmd_list = [] # history + self.__cmd_list_redo = [] # redo list + + def execute(self, begin=0, end=-1): + """ + create matrix from history + """ + mat = mathutils.Matrix() + mat.identity() + for i, cmd in enumerate(self.__cmd_list): + if begin <= i and (end == -1 or i <= end): + mat = compat.matmul(cmd.to_matrix(), mat) + return mat + + def undo_size(self): + """ + get history size + """ + return len(self.__cmd_list) + + def top(self): + """ + get top of history + """ + if len(self.__cmd_list) <= 0: + return None + return self.__cmd_list[-1] + + def append(self, cmd): + """ + append command + """ + self.__cmd_list.append(cmd) + self.__cmd_list_redo = [] + + def undo(self): + """ + undo command + """ + if len(self.__cmd_list) <= 0: + return + self.__cmd_list_redo.append(self.__cmd_list.pop()) + + def redo(self): + """ + redo command + """ + if len(self.__cmd_list_redo) <= 0: + return + self.__cmd_list.append(self.__cmd_list_redo.pop()) + + def pop(self): + if len(self.__cmd_list) <= 0: + return None + return self.__cmd_list.pop() + + def push(self, cmd): + self.__cmd_list.append(cmd) + + +class State(IntEnum): + """ + Enum: State definition used by MUV_UVBBStateMgr + """ + NONE = 0 + TRANSLATING = 1 + SCALING_1 = 2 + SCALING_2 = 3 + SCALING_3 = 4 + SCALING_4 = 5 + SCALING_5 = 6 + SCALING_6 = 7 + SCALING_7 = 8 + SCALING_8 = 9 + ROTATING = 10 + UNIFORM_SCALING_1 = 11 + UNIFORM_SCALING_2 = 12 + UNIFORM_SCALING_3 = 13 + UNIFORM_SCALING_4 = 14 + + +class StateBase: + """ + Custom class: Base class of state + """ + + def __init__(self): + pass + + def update(self, context, event, ctrl_points, mouse_view): + raise NotImplementedError + + +class StateNone(StateBase): + """ + Custom class: + No state + Wait for event from mouse + """ + + def __init__(self, cmd_exec): + super().__init__() + self.__cmd_exec = cmd_exec + + def update(self, context, event, ctrl_points, mouse_view): + """ + Update state + """ + user_prefs = compat.get_user_preferences(context) + prefs = user_prefs.addons["magic_uv"].preferences + cp_react_size = prefs.uv_bounding_box_cp_react_size + is_uscaling = context.scene.muv_uv_bounding_box_uniform_scaling + if (event.type == 'LEFTMOUSE') and (event.value == 'PRESS'): + x, y = context.region.view2d.view_to_region( + mouse_view.x, mouse_view.y) + for i, p in enumerate(ctrl_points): + px, py = context.region.view2d.view_to_region(p.x, p.y) + in_cp_x = (px + cp_react_size > x and + px - cp_react_size < x) + in_cp_y = (py + cp_react_size > y and + py - cp_react_size < y) + if in_cp_x and in_cp_y: + if is_uscaling: + arr = [1, 3, 6, 8] + if i in arr: + return ( + State.UNIFORM_SCALING_1 + + arr.index(i) + ) + else: + return State.TRANSLATING + i + + return State.NONE + + +class StateTranslating(StateBase): + """ + Custom class: Translating state + """ + + def __init__(self, cmd_exec, ctrl_points): + super().__init__() + self.__cmd_exec = cmd_exec + ix, iy = ctrl_points[0].x, ctrl_points[0].y + self.__cmd_exec.append(TranslationCommand(ix, iy)) + + def update(self, context, event, ctrl_points, mouse_view): + if event.type == 'LEFTMOUSE': + if event.value == 'RELEASE': + return State.NONE + if event.type == 'MOUSEMOVE': + x, y = mouse_view.x, mouse_view.y + self.__cmd_exec.top().set(x, y) + return State.TRANSLATING + + +class StateScaling(StateBase): + """ + Custom class: Scaling state + """ + + def __init__(self, cmd_exec, state, ctrl_points): + super().__init__() + self.__state = state + self.__cmd_exec = cmd_exec + dir_x_list = [1, 1, 1, 0, 0, 1, 1, 1] + dir_y_list = [1, 0, 1, 1, 1, 1, 0, 1] + idx = state - 2 + ix, iy = ctrl_points[idx + 1].x, ctrl_points[idx + 1].y + ox, oy = ctrl_points[8 - idx].x, ctrl_points[8 - idx].y + dir_x, dir_y = dir_x_list[idx], dir_y_list[idx] + mat = self.__cmd_exec.execute(end=self.__cmd_exec.undo_size()) + self.__cmd_exec.append( + ScalingCommand(ix, iy, ox, oy, dir_x, dir_y, mat.inverted())) + + def update(self, context, event, ctrl_points, mouse_view): + if event.type == 'LEFTMOUSE': + if event.value == 'RELEASE': + return State.NONE + if event.type == 'MOUSEMOVE': + x, y = mouse_view.x, mouse_view.y + self.__cmd_exec.top().set(x, y) + return self.__state + + +class StateUniformScaling(StateBase): + """ + Custom class: Uniform Scaling state + """ + + def __init__(self, cmd_exec, state, ctrl_points): + super().__init__() + self.__state = state + self.__cmd_exec = cmd_exec + icp_idx = [1, 3, 6, 8] + ocp_idx = [8, 6, 3, 1] + idx = state - State.UNIFORM_SCALING_1 + ix, iy = ctrl_points[icp_idx[idx]].x, ctrl_points[icp_idx[idx]].y + ox, oy = ctrl_points[ocp_idx[idx]].x, ctrl_points[ocp_idx[idx]].y + mat = self.__cmd_exec.execute(end=self.__cmd_exec.undo_size()) + self.__cmd_exec.append(UniformScalingCommand( + ix, iy, ox, oy, mat.inverted())) + + def update(self, context, event, ctrl_points, mouse_view): + if event.type == 'LEFTMOUSE': + if event.value == 'RELEASE': + return State.NONE + if event.type == 'MOUSEMOVE': + x, y = mouse_view.x, mouse_view.y + self.__cmd_exec.top().set(x, y) + + return self.__state + + +class StateRotating(StateBase): + """ + Custom class: Rotating state + """ + + def __init__(self, cmd_exec, ctrl_points): + super().__init__() + self.__cmd_exec = cmd_exec + ix, iy = ctrl_points[9].x, ctrl_points[9].y + ox, oy = ctrl_points[0].x, ctrl_points[0].y + self.__cmd_exec.append(RotationCommand(ix, iy, ox, oy)) + + def update(self, context, event, ctrl_points, mouse_view): + if event.type == 'LEFTMOUSE': + if event.value == 'RELEASE': + return State.NONE + if event.type == 'MOUSEMOVE': + x, y = mouse_view.x, mouse_view.y + self.__cmd_exec.top().set(x, y) + return State.ROTATING + + +class StateManager: + """ + Custom class: Manage state about this feature + """ + + def __init__(self, cmd_exec): + self.__cmd_exec = cmd_exec # command executer + self.__state = State.NONE # current state + self.__state_obj = StateNone(self.__cmd_exec) + + def __update_state(self, next_state, ctrl_points): + """ + Update state + """ + + if next_state == self.__state: + return + obj = None + if next_state == State.TRANSLATING: + obj = StateTranslating(self.__cmd_exec, ctrl_points) + elif State.SCALING_1 <= next_state <= State.SCALING_8: + obj = StateScaling( + self.__cmd_exec, next_state, ctrl_points) + elif next_state == State.ROTATING: + obj = StateRotating(self.__cmd_exec, ctrl_points) + elif next_state == State.NONE: + obj = StateNone(self.__cmd_exec) + elif (State.UNIFORM_SCALING_1 <= next_state <= + State.UNIFORM_SCALING_4): + obj = StateUniformScaling( + self.__cmd_exec, next_state, ctrl_points) + + if obj is not None: + self.__state_obj = obj + + self.__state = next_state + + def update(self, context, ctrl_points, event): + mouse_region = mathutils.Vector(( + event.mouse_region_x, event.mouse_region_y)) + mouse_view = mathutils.Vector((context.region.view2d.region_to_view( + mouse_region.x, mouse_region.y))) + next_state = self.__state_obj.update( + context, event, ctrl_points, mouse_view) + self.__update_state(next_state, ctrl_points) + + return self.__state + + +@BlClassRegistry() +class MUV_OT_UVBoundingBox(bpy.types.Operator): + """ + Operation class: UV Bounding Box + """ + + bl_idname = "uv.muv_ot_uv_bounding_box" + bl_label = "UV Bounding Box" + bl_description = "Internal operation for UV Bounding Box" + bl_options = {'REGISTER', 'UNDO'} + + def __init__(self): + self.__timer = None + self.__cmd_exec = CommandExecuter() # Command executor + self.__state_mgr = StateManager(self.__cmd_exec) # State Manager + + __handle = None + __timer = None + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return False + return _is_valid_context(context) + + @classmethod + def is_running(cls, _): + return 1 if cls.__handle else 0 + + @classmethod + def handle_add(cls, obj, context): + if cls.__handle is None: + sie = bpy.types.SpaceImageEditor + cls.__handle = sie.draw_handler_add( + cls.draw_bb, (obj, context), "WINDOW", "POST_PIXEL") + if cls.__timer is None: + cls.__timer = context.window_manager.event_timer_add( + 0.1, window=context.window) + context.window_manager.modal_handler_add(obj) + + @classmethod + def handle_remove(cls, context): + if cls.__handle is not None: + sie = bpy.types.SpaceImageEditor + sie.draw_handler_remove(cls.__handle, "WINDOW") + cls.__handle = None + if cls.__timer is not None: + context.window_manager.event_timer_remove(cls.__timer) + cls.__timer = None + + @classmethod + def __draw_ctrl_point(cls, context, pos): + """ + Draw control point + """ + user_prefs = compat.get_user_preferences(context) + prefs = user_prefs.addons["magic_uv"].preferences + cp_size = prefs.uv_bounding_box_cp_size + offset = cp_size / 2 + verts = [ + [pos.x - offset, pos.y - offset], + [pos.x - offset, pos.y + offset], + [pos.x + offset, pos.y + offset], + [pos.x + offset, pos.y - offset] + ] + bgl.glEnable(bgl.GL_BLEND) + bgl.glBegin(bgl.GL_QUADS) + bgl.glColor4f(1.0, 1.0, 1.0, 1.0) + for (x, y) in verts: + bgl.glVertex2f(x, y) + bgl.glEnd() + + @classmethod + def draw_bb(cls, _, context): + """ + Draw bounding box + """ + props = context.scene.muv_props.uv_bounding_box + + if not MUV_OT_UVBoundingBox.is_running(context): + return + + if not _is_valid_context(context): + return + + for cp in props.ctrl_points: + cls.__draw_ctrl_point( + context, mathutils.Vector( + context.region.view2d.view_to_region(cp.x, cp.y))) + + def __get_uv_info(self, context): + """ + Get UV coordinate + """ + sc = context.scene + obj = context.active_object + uv_info = [] + 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: + return None + uv_layer = bm.loops.layers.uv.verify() + for f in bm.faces: + if not f.select: + continue + for i, l in enumerate(f.loops): + if sc.muv_uv_bounding_box_boundary == 'UV_SEL': + if l[uv_layer].select: + uv_info.append((f.index, i, l[uv_layer].uv.copy())) + elif sc.muv_uv_bounding_box_boundary == 'UV': + uv_info.append((f.index, i, l[uv_layer].uv.copy())) + if not uv_info: + return None + return uv_info + + def __get_ctrl_point(self, uv_info_ini): + """ + Get control point + """ + left = MAX_VALUE + right = -MAX_VALUE + top = -MAX_VALUE + bottom = MAX_VALUE + + for info in uv_info_ini: + uv = info[2] + if uv.x < left: + left = uv.x + if uv.x > right: + right = uv.x + if uv.y < bottom: + bottom = uv.y + if uv.y > top: + top = uv.y + + points = [ + mathutils.Vector(( + (left + right) * 0.5, (top + bottom) * 0.5, 0.0 + )), + mathutils.Vector((left, top, 0.0)), + mathutils.Vector((left, (top + bottom) * 0.5, 0.0)), + mathutils.Vector((left, bottom, 0.0)), + mathutils.Vector(((left + right) * 0.5, top, 0.0)), + mathutils.Vector(((left + right) * 0.5, bottom, 0.0)), + mathutils.Vector((right, top, 0.0)), + mathutils.Vector((right, (top + bottom) * 0.5, 0.0)), + mathutils.Vector((right, bottom, 0.0)), + mathutils.Vector(((left + right) * 0.5, top + 0.03, 0.0)) + ] + + return points + + def __update_uvs(self, context, uv_info_ini, trans_mat): + """ + Update UV coordinate + """ + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + if not bm.loops.layers.uv: + return + uv_layer = bm.loops.layers.uv.verify() + for info in uv_info_ini: + fidx = info[0] + lidx = info[1] + uv = info[2] + v = mathutils.Vector((uv.x, uv.y, 0.0)) + av = compat.matmul(trans_mat, v) + bm.faces[fidx].loops[lidx][uv_layer].uv = mathutils.Vector( + (av.x, av.y)) + bmesh.update_edit_mesh(obj.data) + + def __update_ctrl_point(self, ctrl_points_ini, trans_mat): + """ + Update control point + """ + return [compat.matmul(trans_mat, cp) for cp in ctrl_points_ini] + + def modal(self, context, event): + props = context.scene.muv_props.uv_bounding_box + common.redraw_all_areas() + + if not MUV_OT_UVBoundingBox.is_running(context): + return {'FINISHED'} + + if not _is_valid_context(context): + MUV_OT_UVBoundingBox.handle_remove(context) + return {'FINISHED'} + + region_types = [ + 'HEADER', + 'UI', + 'TOOLS', + ] + if not common.mouse_on_area(event, 'IMAGE_EDITOR') or \ + common.mouse_on_regions(event, 'IMAGE_EDITOR', region_types): + return {'PASS_THROUGH'} + + if event.type == 'TIMER': + trans_mat = self.__cmd_exec.execute() + self.__update_uvs(context, props.uv_info_ini, trans_mat) + props.ctrl_points = self.__update_ctrl_point( + props.ctrl_points_ini, trans_mat) + + state = self.__state_mgr.update(context, props.ctrl_points, event) + if state == State.NONE: + return {'PASS_THROUGH'} + + return {'RUNNING_MODAL'} + + def invoke(self, context, _): + props = context.scene.muv_props.uv_bounding_box + + if MUV_OT_UVBoundingBox.is_running(context): + MUV_OT_UVBoundingBox.handle_remove(context) + return {'FINISHED'} + + props.uv_info_ini = self.__get_uv_info(context) + if props.uv_info_ini is None: + return {'CANCELLED'} + + MUV_OT_UVBoundingBox.handle_add(self, context) + + props.ctrl_points_ini = self.__get_ctrl_point(props.uv_info_ini) + trans_mat = self.__cmd_exec.execute() + # Update is needed in order to display control point + self.__update_uvs(context, props.uv_info_ini, trans_mat) + props.ctrl_points = self.__update_ctrl_point( + props.ctrl_points_ini, trans_mat) + + return {'RUNNING_MODAL'} diff --git a/magic_uv/op/uv_inspection.py b/magic_uv/op/uv_inspection.py new file mode 100644 index 00000000..61cbf1ed --- /dev/null +++ b/magic_uv/op/uv_inspection.py @@ -0,0 +1,281 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy +from bpy.props import BoolProperty, EnumProperty +import bmesh + +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat + +if compat.check_version(2, 80, 0) >= 0: + from ..lib import bglx as bgl +else: + import bgl + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +def _update_uvinsp_info(context): + sc = context.scene + props = sc.muv_props.uv_inspection + + 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 = common.get_overlapped_uv_info( + bm, sel_faces, uv_layer, sc.muv_uv_inspection_show_mode) + props.flipped_info = common.get_flipped_uv_info(sel_faces, uv_layer) + + +@PropertyClassRegistry() +class _Properties: + idname = "uv_inspection" + + @classmethod + def init_props(cls, scene): + class Props(): + overlapped_info = [] + flipped_info = [] + + scene.muv_props.uv_inspection = Props() + + def get_func(_): + return MUV_OT_UVInspection_Render.is_running(bpy.context) + + def set_func(_, __): + pass + + def update_func(_, __): + bpy.ops.uv.muv_ot_uv_inspection_render('INVOKE_REGION_WIN') + + scene.muv_uv_inspection_enabled = BoolProperty( + name="UV Inspection Enabled", + description="UV Inspection is enabled", + default=False + ) + scene.muv_uv_inspection_show = BoolProperty( + name="UV Inspection Showed", + description="UV Inspection is showed", + default=False, + get=get_func, + set=set_func, + update=update_func + ) + scene.muv_uv_inspection_show_overlapped = BoolProperty( + name="Overlapped", + description="Show overlapped UVs", + default=False + ) + scene.muv_uv_inspection_show_flipped = BoolProperty( + name="Flipped", + description="Show flipped UVs", + default=False + ) + scene.muv_uv_inspection_show_mode = EnumProperty( + name="Mode", + description="Show mode", + items=[ + ('PART', "Part", "Show only overlapped/flipped part"), + ('FACE', "Face", "Show overlapped/flipped face") + ], + default='PART' + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_props.uv_inspection + del scene.muv_uv_inspection_enabled + del scene.muv_uv_inspection_show + del scene.muv_uv_inspection_show_overlapped + del scene.muv_uv_inspection_show_flipped + del scene.muv_uv_inspection_show_mode + + +@BlClassRegistry() +class MUV_OT_UVInspection_Render(bpy.types.Operator): + """ + Operation class: Render UV Inspection + No operation (only rendering) + """ + + bl_idname = "uv.muv_ot_uv_inspection_render" + bl_description = "Render overlapped/flipped UVs" + bl_label = "Overlapped/Flipped UV renderer" + + __handle = None + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return False + return _is_valid_context(context) + + @classmethod + def is_running(cls, _): + return 1 if cls.__handle else 0 + + @classmethod + def handle_add(cls, obj, context): + sie = bpy.types.SpaceImageEditor + cls.__handle = sie.draw_handler_add( + MUV_OT_UVInspection_Render.draw, (obj, context), + 'WINDOW', 'POST_PIXEL') + + @classmethod + def handle_remove(cls): + if cls.__handle is not None: + bpy.types.SpaceImageEditor.draw_handler_remove( + cls.__handle, 'WINDOW') + cls.__handle = None + + @staticmethod + def draw(_, context): + sc = context.scene + props = sc.muv_props.uv_inspection + user_prefs = compat.get_user_preferences(context) + prefs = user_prefs.addons["magic_uv"].preferences + + if not MUV_OT_UVInspection_Render.is_running(context): + return + + # OpenGL configuration + bgl.glEnable(bgl.GL_BLEND) + + # render overlapped UV + if sc.muv_uv_inspection_show_overlapped: + color = prefs.uv_inspection_overlapped_color + for info in props.overlapped_info: + if sc.muv_uv_inspection_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_uv_inspection_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_uv_inspection_show_flipped: + color = prefs.uv_inspection_flipped_color + for info in props.flipped_info: + if sc.muv_uv_inspection_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_uv_inspection_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() + + bgl.glDisable(bgl.GL_BLEND) + + def invoke(self, context, _): + if not MUV_OT_UVInspection_Render.is_running(context): + _update_uvinsp_info(context) + MUV_OT_UVInspection_Render.handle_add(self, context) + else: + MUV_OT_UVInspection_Render.handle_remove() + + if context.area: + context.area.tag_redraw() + + return {'FINISHED'} + + +@BlClassRegistry() +class MUV_OT_UVInspection_Update(bpy.types.Operator): + """ + Operation class: Update + """ + + bl_idname = "uv.muv_ot_uv_inspection_update" + bl_label = "Update UV Inspection" + bl_description = "Update UV Inspection" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + if not MUV_OT_UVInspection_Render.is_running(context): + return False + return _is_valid_context(context) + + def execute(self, context): + _update_uvinsp_info(context) + + if context.area: + context.area.tag_redraw() + + return {'FINISHED'} diff --git a/magic_uv/op/uv_sculpt.py b/magic_uv/op/uv_sculpt.py new file mode 100644 index 00000000..de5f1e02 --- /dev/null +++ b/magic_uv/op/uv_sculpt.py @@ -0,0 +1,487 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +from math import pi, cos, tan, sin + +import bpy +import bmesh +from mathutils import Vector +from bpy_extras import view3d_utils +from mathutils.bvhtree import BVHTree +from mathutils.geometry import barycentric_transform +from bpy.props import ( + BoolProperty, + IntProperty, + EnumProperty, + FloatProperty, +) + +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat + + +if compat.check_version(2, 80, 0) >= 0: + from ..lib import bglx as bgl +else: + import bgl + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +def _get_strength(p, len_, factor): + f = factor + + if p > len_: + return 0.0 + + if p < 0.0: + return f + + return (len_ - p) * f / len_ + + +@PropertyClassRegistry() +class _Properties: + idname = "uv_sculpt" + + @classmethod + def init_props(cls, scene): + def get_func(_): + return MUV_OT_UVSculpt.is_running(bpy.context) + + def set_func(_, __): + pass + + def update_func(_, __): + bpy.ops.uv.muv_ot_uv_sculpt('INVOKE_REGION_WIN') + + scene.muv_uv_sculpt_enabled = BoolProperty( + name="UV Sculpt", + description="UV Sculpt is enabled", + default=False + ) + scene.muv_uv_sculpt_enable = BoolProperty( + name="UV Sculpt Showed", + description="UV Sculpt is enabled", + default=False, + get=get_func, + set=set_func, + update=update_func + ) + scene.muv_uv_sculpt_radius = IntProperty( + name="Radius", + description="Radius of the brush", + min=1, + max=500, + default=30 + ) + scene.muv_uv_sculpt_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_uv_sculpt_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_uv_sculpt_show_brush = BoolProperty( + name="Show Brush", + description="Show Brush", + default=True + ) + scene.muv_uv_sculpt_pinch_invert = BoolProperty( + name="Invert", + description="Pinch UV to invert direction", + default=False + ) + scene.muv_uv_sculpt_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' + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_uv_sculpt_enabled + del scene.muv_uv_sculpt_enable + del scene.muv_uv_sculpt_radius + del scene.muv_uv_sculpt_strength + del scene.muv_uv_sculpt_tools + del scene.muv_uv_sculpt_show_brush + del scene.muv_uv_sculpt_pinch_invert + del scene.muv_uv_sculpt_relax_method + + +@BlClassRegistry() +class MUV_OT_UVSculpt(bpy.types.Operator): + """ + Operation class: UV Sculpt in View3D + """ + + bl_idname = "uv.muv_ot_uv_sculpt" + bl_label = "UV Sculpt" + bl_description = "UV Sculpt in View3D" + bl_options = {'REGISTER'} + + __handle = None + __timer = None + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return False + return _is_valid_context(context) + + @classmethod + def is_running(cls, _): + return 1 if cls.__handle else 0 + + @classmethod + def handle_add(cls, obj, context): + if not cls.__handle: + sv = bpy.types.SpaceView3D + cls.__handle = sv.draw_handler_add(cls.draw_brush, (obj, context), + "WINDOW", "POST_PIXEL") + if not cls.__timer: + cls.__timer = context.window_manager.event_timer_add( + 0.1, window=context.window) + context.window_manager.modal_handler_add(obj) + + @classmethod + def handle_remove(cls, context): + if cls.__handle: + sv = bpy.types.SpaceView3D + sv.draw_handler_remove(cls.__handle, "WINDOW") + cls.__handle = None + if cls.__timer: + context.window_manager.event_timer_remove(cls.__timer) + cls.__timer = None + + @classmethod + def draw_brush(cls, obj, context): + sc = context.scene + user_prefs = compat.get_user_preferences(context) + prefs = user_prefs.addons["magic_uv"].preferences + + num_segment = 180 + theta = 2 * pi / num_segment + fact_t = tan(theta) + fact_r = cos(theta) + color = prefs.uv_sculpt_brush_color + + bgl.glBegin(bgl.GL_LINE_STRIP) + bgl.glColor4f(color[0], color[1], color[2], color[3]) + x = sc.muv_uv_sculpt_radius * cos(0.0) + y = sc.muv_uv_sculpt_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() + + def __init__(self): + self.__loop_info = [] + self.__stroking = False + self.current_mco = Vector((0.0, 0.0)) + self.__initial_mco = Vector((0.0, 0.0)) + + 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, + compat.matmul(world_mat, l.vert.co)) + diff = loc_2d - self.__initial_mco + if diff.length < sc.muv_uv_sculpt_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": _get_strength( + diff.length, sc.muv_uv_sculpt_radius, + sc.muv_uv_sculpt_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_uv_sculpt_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_uv_sculpt_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, + compat.matmul(world_mat, l.vert.co)) + diff = loc_2d - self.__initial_mco + if diff.length < sc.muv_uv_sculpt_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": _get_strength( + diff.length, sc.muv_uv_sculpt_radius, + sc.muv_uv_sculpt_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 = compat.matmul(mwi, ray_orig) + ray_tgt_obj = compat.matmul(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_uv_sculpt_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_uv_sculpt_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, + compat.matmul(world_mat, l.vert.co)) + diff = loc_2d - self.__initial_mco + if diff.length >= sc.muv_uv_sculpt_radius: + continue + db = vert_db[l.vert] + strength = _get_strength(diff.length, + sc.muv_uv_sculpt_radius, + sc.muv_uv_sculpt_strength) + + base = (1.0 - strength) * l[uv_layer].uv + if sc.muv_uv_sculpt_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_uv_sculpt_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_uv_sculpt_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): + if context.area: + context.area.tag_redraw() + + if not MUV_OT_UVSculpt.is_running(context): + MUV_OT_UVSculpt.handle_remove(context) + return {'FINISHED'} + + self.current_mco = Vector((event.mouse_region_x, event.mouse_region_y)) + + region_types = [ + 'HEADER', + 'UI', + 'TOOLS', + 'TOOL_PROPS', + ] + if not common.mouse_on_area(event, 'VIEW_3D') or \ + common.mouse_on_regions(event, 'VIEW_3D', region_types): + return {'PASS_THROUGH'} + + 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 + return {'RUNNING_MODAL'} + elif event.type == 'MOUSEMOVE': + if self.__stroking: + self.__stroke_apply(context, event) + return {'RUNNING_MODAL'} + elif event.type == 'TIMER': + if self.__stroking: + self.__stroke_apply(context, event) + return {'RUNNING_MODAL'} + + return {'PASS_THROUGH'} + + def invoke(self, context, _): + if context.area: + context.area.tag_redraw() + + if MUV_OT_UVSculpt.is_running(context): + MUV_OT_UVSculpt.handle_remove(context) + else: + MUV_OT_UVSculpt.handle_add(self, context) + + return {'RUNNING_MODAL'} diff --git a/magic_uv/op/uvw.py b/magic_uv/op/uvw.py new file mode 100644 index 00000000..035dfca3 --- /dev/null +++ b/magic_uv/op/uvw.py @@ -0,0 +1,304 @@ +# <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__ = "Alexander Milovsky, Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "6.0" +__date__ = "26 Jan 2019" + +from math import sin, cos, pi + +import bpy +import bmesh +from bpy.props import ( + FloatProperty, + FloatVectorProperty, + BoolProperty +) +from mathutils import Vector + +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +def _get_uv_layer(ops_obj, bm, assign_uvmap): + # get UV layer + if not bm.loops.layers.uv: + if assign_uvmap: + bm.loops.layers.uv.new() + else: + ops_obj.report({'WARNING'}, + "Object must have more than one UV map") + return None + uv_layer = bm.loops.layers.uv.verify() + + return uv_layer + + +def _apply_box_map(bm, uv_layer, size, offset, rotation, tex_aspect): + scale = 1.0 / size + + sx = 1.0 * scale + sy = 1.0 * scale + sz = 1.0 * scale + ofx = offset[0] + ofy = offset[1] + ofz = offset[2] + rx = rotation[0] * pi / 180.0 + ry = rotation[1] * pi / 180.0 + rz = rotation[2] * pi / 180.0 + aspect = tex_aspect + + sel_faces = [f for f in bm.faces if f.select] + + # update UV coordinate + for f in sel_faces: + n = f.normal + for l in f.loops: + co = l.vert.co + x = co.x * sx + y = co.y * sy + z = co.z * sz + + # X-plane + if abs(n[0]) >= abs(n[1]) and abs(n[0]) >= abs(n[2]): + if n[0] >= 0.0: + u = (y - ofy) * cos(rx) + (z - ofz) * sin(rx) + v = -(y * aspect - ofy) * sin(rx) + \ + (z * aspect - ofz) * cos(rx) + else: + u = -(y - ofy) * cos(rx) + (z - ofz) * sin(rx) + v = (y * aspect - ofy) * sin(rx) + \ + (z * aspect - ofz) * cos(rx) + # Y-plane + elif abs(n[1]) >= abs(n[0]) and abs(n[1]) >= abs(n[2]): + if n[1] >= 0.0: + u = -(x - ofx) * cos(ry) + (z - ofz) * sin(ry) + v = (x * aspect - ofx) * sin(ry) + \ + (z * aspect - ofz) * cos(ry) + else: + u = (x - ofx) * cos(ry) + (z - ofz) * sin(ry) + v = -(x * aspect - ofx) * sin(ry) + \ + (z * aspect - ofz) * cos(ry) + # Z-plane + else: + if n[2] >= 0.0: + u = (x - ofx) * cos(rz) + (y - ofy) * sin(rz) + v = -(x * aspect - ofx) * sin(rz) + \ + (y * aspect - ofy) * cos(rz) + else: + u = -(x - ofx) * cos(rz) - (y + ofy) * sin(rz) + v = -(x * aspect + ofx) * sin(rz) + \ + (y * aspect - ofy) * cos(rz) + + l[uv_layer].uv = Vector((u, v)) + + +def _apply_planer_map(bm, uv_layer, size, offset, rotation, tex_aspect): + scale = 1.0 / size + + sx = 1.0 * scale + sy = 1.0 * scale + ofx = offset[0] + ofy = offset[1] + rz = rotation * pi / 180.0 + aspect = tex_aspect + + sel_faces = [f for f in bm.faces if f.select] + + # calculate average of normal + n_ave = Vector((0.0, 0.0, 0.0)) + for f in sel_faces: + n_ave = n_ave + f.normal + q = n_ave.rotation_difference(Vector((0.0, 0.0, 1.0))) + + # update UV coordinate + for f in sel_faces: + for l in f.loops: + co = compat.matmul(q, l.vert.co) + x = co.x * sx + y = co.y * sy + + u = x * cos(rz) - y * sin(rz) + ofx + v = -x * aspect * sin(rz) - y * aspect * cos(rz) + ofy + + l[uv_layer].uv = Vector((u, v)) + + +@PropertyClassRegistry() +class _Properties: + idname = "uvw" + + @classmethod + def init_props(cls, scene): + 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 + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_uvw_enabled + del scene.muv_uvw_assign_uvmap + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_UVW_BoxMap(bpy.types.Operator): + bl_idname = "uv.muv_ot_uvw_box_map" + bl_label = "Box Map" + bl_options = {'REGISTER', 'UNDO'} + + size = FloatProperty( + name="Size", + default=1.0, + precision=4 + ) + rotation = FloatVectorProperty( + name="XYZ Rotation", + size=3, + default=(0.0, 0.0, 0.0) + ) + offset = FloatVectorProperty( + name="XYZ Offset", + size=3, + default=(0.0, 0.0, 0.0) + ) + tex_aspect = FloatProperty( + name="Texture Aspect", + 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): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + 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() + + # get UV layer + uv_layer = _get_uv_layer(self, bm, self.assign_uvmap) + if not uv_layer: + return {'CANCELLED'} + + _apply_box_map(bm, uv_layer, self.size, self.offset, self.rotation, + self.tex_aspect) + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_UVW_BestPlanerMap(bpy.types.Operator): + bl_idname = "uv.muv_ot_uvw_best_planer_map" + bl_label = "Best Planer Map" + bl_options = {'REGISTER', 'UNDO'} + + size = FloatProperty( + name="Size", + default=1.0, + precision=4 + ) + rotation = FloatProperty( + name="XY Rotation", + default=0.0 + ) + offset = FloatVectorProperty( + name="XY Offset", + size=2, + default=(0.0, 0.0) + ) + tex_aspect = FloatProperty( + name="Texture Aspect", + 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): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + 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() + + # get UV layer + uv_layer = _get_uv_layer(self, bm, self.assign_uvmap) + if not uv_layer: + return {'CANCELLED'} + + _apply_planer_map(bm, uv_layer, self.size, self.offset, self.rotation, + self.tex_aspect) + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/magic_uv/op/world_scale_uv.py b/magic_uv/op/world_scale_uv.py new file mode 100644 index 00000000..1d78b8c7 --- /dev/null +++ b/magic_uv/op/world_scale_uv.py @@ -0,0 +1,649 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "McBuff, Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "6.0" +__date__ = "26 Jan 2019" + +from math import sqrt + +import bpy +from bpy.props import ( + EnumProperty, + FloatProperty, + IntVectorProperty, + BoolProperty, +) +import bmesh +from mathutils import Vector + +from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..utils import compatibility as compat + + +def _is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +def _measure_wsuv_info(obj, tex_size=None): + mesh_area = common.measure_mesh_area(obj) + uv_area = common.measure_uv_area(obj, tex_size) + + if not uv_area: + return None, mesh_area, None + + if mesh_area == 0.0: + density = 0.0 + else: + density = sqrt(uv_area) / sqrt(mesh_area) + + return uv_area, mesh_area, density + + +def _apply(obj, origin, factor): + bm = bmesh.from_edit_mesh(obj.data) + if common.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] + + uv_layer = bm.loops.layers.uv.verify() + + # calculate origin + if origin == 'CENTER': + origin = Vector((0.0, 0.0)) + num = 0 + for f in sel_faces: + for l in f.loops: + uv = l[uv_layer].uv + origin = origin + uv + num = num + 1 + origin = origin / num + elif origin == 'LEFT_TOP': + origin = Vector((100000.0, -100000.0)) + for f in sel_faces: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = min(origin.x, uv.x) + origin.y = max(origin.y, uv.y) + elif origin == 'LEFT_CENTER': + origin = Vector((100000.0, 0.0)) + num = 0 + for f in sel_faces: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = min(origin.x, uv.x) + origin.y = origin.y + uv.y + num = num + 1 + origin.y = origin.y / num + elif origin == 'LEFT_BOTTOM': + origin = Vector((100000.0, 100000.0)) + for f in sel_faces: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = min(origin.x, uv.x) + origin.y = min(origin.y, uv.y) + elif origin == 'CENTER_TOP': + origin = Vector((0.0, -100000.0)) + num = 0 + for f in sel_faces: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = origin.x + uv.x + origin.y = max(origin.y, uv.y) + num = num + 1 + origin.x = origin.x / num + elif origin == 'CENTER_BOTTOM': + origin = Vector((0.0, 100000.0)) + num = 0 + for f in sel_faces: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = origin.x + uv.x + origin.y = min(origin.y, uv.y) + num = num + 1 + origin.x = origin.x / num + elif origin == 'RIGHT_TOP': + origin = Vector((-100000.0, -100000.0)) + for f in sel_faces: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = max(origin.x, uv.x) + origin.y = max(origin.y, uv.y) + elif origin == 'RIGHT_CENTER': + origin = Vector((-100000.0, 0.0)) + num = 0 + for f in sel_faces: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = max(origin.x, uv.x) + origin.y = origin.y + uv.y + num = num + 1 + origin.y = origin.y / num + elif origin == 'RIGHT_BOTTOM': + origin = Vector((-100000.0, 100000.0)) + for f in sel_faces: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = max(origin.x, uv.x) + origin.y = min(origin.y, uv.y) + + # update UV coordinate + for f in sel_faces: + for l in f.loops: + uv = l[uv_layer].uv + diff = uv - origin + l[uv_layer].uv = origin + diff * factor + + bmesh.update_edit_mesh(obj.data) + + +@PropertyClassRegistry() +class _Properties: + idname = "world_scale_uv" + + @classmethod + def init_props(cls, scene): + scene.muv_world_scale_uv_enabled = BoolProperty( + name="World Scale UV Enabled", + description="World Scale UV is enabled", + default=False + ) + scene.muv_world_scale_uv_src_mesh_area = FloatProperty( + name="Mesh Area", + description="Source Mesh Area", + default=0.0, + min=0.0 + ) + scene.muv_world_scale_uv_src_uv_area = FloatProperty( + name="UV Area", + description="Source UV Area", + default=0.0, + min=0.0 + ) + scene.muv_world_scale_uv_src_density = FloatProperty( + name="Density", + description="Source Texel Density", + default=0.0, + min=0.0 + ) + scene.muv_world_scale_uv_tgt_density = FloatProperty( + name="Density", + description="Target Texel Density", + default=0.0, + min=0.0 + ) + scene.muv_world_scale_uv_tgt_scaling_factor = FloatProperty( + name="Scaling Factor", + default=1.0, + max=1000.0, + min=0.00001 + ) + scene.muv_world_scale_uv_tgt_texture_size = IntVectorProperty( + name="Texture Size", + size=2, + min=1, + soft_max=10240, + default=(1024, 1024), + ) + scene.muv_world_scale_uv_mode = EnumProperty( + name="Mode", + description="Density calculation mode", + items=[ + ('PROPORTIONAL_TO_MESH', "Proportional to Mesh", + "Apply density proportionaled by mesh size"), + ('SCALING_DENSITY', "Scaling Density", + "Apply scaled density from source"), + ('SAME_DENSITY', "Same Density", + "Apply same density of source"), + ('MANUAL', "Manual", "Specify density and size by manual"), + ], + default='MANUAL' + ) + scene.muv_world_scale_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' + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_world_scale_uv_enabled + del scene.muv_world_scale_uv_src_mesh_area + del scene.muv_world_scale_uv_src_uv_area + del scene.muv_world_scale_uv_src_density + del scene.muv_world_scale_uv_tgt_density + del scene.muv_world_scale_uv_tgt_scaling_factor + del scene.muv_world_scale_uv_mode + del scene.muv_world_scale_uv_origin + + +@BlClassRegistry() +class MUV_OT_WorldScaleUV_Measure(bpy.types.Operator): + """ + Operation class: Measure face size + """ + + bl_idname = "uv.muv_ot_world_scale_uv_measure" + bl_label = "Measure World Scale UV" + bl_description = "Measure face size for scale calculation" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + def execute(self, context): + sc = context.scene + obj = context.active_object + + 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'} + + sc.muv_world_scale_uv_src_uv_area = uv_area + sc.muv_world_scale_uv_src_mesh_area = mesh_area + sc.muv_world_scale_uv_src_density = density + + self.report({'INFO'}, + "UV Area: {0}, Mesh Area: {1}, Texel Density: {2}" + .format(uv_area, mesh_area, density)) + + return {'FINISHED'} + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_WorldScaleUV_ApplyManual(bpy.types.Operator): + """ + Operation class: Apply scaled UV (Manual) + """ + + bl_idname = "uv.muv_ot_world_scale_uv_apply_manual" + bl_label = "Apply World Scale UV (Manual)" + bl_description = "Apply scaled UV based on user specification" + bl_options = {'REGISTER', 'UNDO'} + + tgt_density = FloatProperty( + name="Density", + description="Target Texel Density", + default=1.0, + min=0.0 + ) + tgt_texture_size = IntVectorProperty( + name="Texture Size", + size=2, + min=1, + soft_max=10240, + default=(1024, 1024), + ) + 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' + ) + show_dialog = BoolProperty( + name="Show Diaglog Menu", + description="Show dialog menu if true", + default=True, + options={'HIDDEN', 'SKIP_SAVE'} + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + def __apply_manual(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + tex_size = self.tgt_texture_size + uv_area, _, density = _measure_wsuv_info(obj, tex_size) + if not uv_area: + self.report({'WARNING'}, "Object must have more than one UV map") + return {'CANCELLED'} + + tgt_density = self.tgt_density + factor = tgt_density / density + + _apply(context.active_object, self.origin, factor) + self.report({'INFO'}, "Scaling factor: {0}".format(factor)) + + return {'FINISHED'} + + def draw(self, _): + layout = self.layout + + layout.prop(self, "tgt_density") + layout.prop(self, "tgt_texture_size") + layout.prop(self, "origin") + + layout.separator() + + def invoke(self, context, _): + if self.show_dialog: + wm = context.window_manager + return wm.invoke_props_dialog(self) + + return self.execute(context) + + def execute(self, context): + return self.__apply_manual(context) + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_WorldScaleUV_ApplyScalingDensity(bpy.types.Operator): + """ + Operation class: Apply scaled UV (Scaling Density) + """ + + bl_idname = "uv.muv_ot_world_scale_uv_apply_scaling_density" + bl_label = "Apply World Scale UV (Scaling Density)" + bl_description = "Apply scaled UV with scaling density" + bl_options = {'REGISTER', 'UNDO'} + + tgt_scaling_factor = FloatProperty( + name="Scaling Factor", + default=1.0, + max=1000.0, + min=0.00001 + ) + 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' + ) + src_density = FloatProperty( + name="Density", + description="Source Texel Density", + default=0.0, + min=0.0, + options={'HIDDEN'} + ) + same_density = BoolProperty( + name="Same Density", + description="Apply same density", + default=False, + options={'HIDDEN'} + ) + show_dialog = BoolProperty( + name="Show Diaglog Menu", + description="Show dialog menu if true", + default=True, + options={'HIDDEN', 'SKIP_SAVE'} + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + def __apply_scaling_density(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + uv_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'} + + tgt_density = self.src_density * self.tgt_scaling_factor + factor = tgt_density / density + + _apply(context.active_object, self.origin, factor) + self.report({'INFO'}, "Scaling factor: {0}".format(factor)) + + return {'FINISHED'} + + def draw(self, _): + layout = self.layout + + layout.label(text="Source:") + col = layout.column() + col.prop(self, "src_density") + col.enabled = False + + layout.separator() + + if not self.same_density: + layout.prop(self, "tgt_scaling_factor") + layout.prop(self, "origin") + + layout.separator() + + def invoke(self, context, _): + sc = context.scene + + if self.show_dialog: + wm = context.window_manager + + if self.same_density: + self.tgt_scaling_factor = 1.0 + else: + self.tgt_scaling_factor = \ + sc.muv_world_scale_uv_tgt_scaling_factor + self.src_density = sc.muv_world_scale_uv_src_density + + return wm.invoke_props_dialog(self) + + return self.execute(context) + + def execute(self, context): + if self.same_density: + self.tgt_scaling_factor = 1.0 + + return self.__apply_scaling_density(context) + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_WorldScaleUV_ApplyProportionalToMesh(bpy.types.Operator): + """ + Operation class: Apply scaled UV (Proportional to mesh) + """ + + bl_idname = "uv.muv_ot_world_scale_uv_apply_proportional_to_mesh" + bl_label = "Apply World Scale UV (Proportional to mesh)" + bl_description = "Apply scaled UV proportionaled to mesh" + bl_options = {'REGISTER', 'UNDO'} + + 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' + ) + src_density = FloatProperty( + name="Source Density", + description="Source Texel Density", + default=0.0, + min=0.0, + options={'HIDDEN'} + ) + src_uv_area = FloatProperty( + name="Source UV Area", + description="Source UV Area", + default=0.0, + min=0.0, + options={'HIDDEN'} + ) + src_mesh_area = FloatProperty( + name="Source Mesh Area", + description="Source Mesh Area", + default=0.0, + min=0.0, + options={'HIDDEN'} + ) + show_dialog = BoolProperty( + name="Show Diaglog Menu", + description="Show dialog menu if true", + default=True, + options={'HIDDEN', 'SKIP_SAVE'} + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + def __apply_proportional_to_mesh(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + 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'} + + tgt_density = self.src_density * sqrt(mesh_area) / sqrt( + self.src_mesh_area) + + factor = tgt_density / density + + _apply(context.active_object, self.origin, factor) + self.report({'INFO'}, "Scaling factor: {0}".format(factor)) + + return {'FINISHED'} + + def draw(self, _): + layout = self.layout + + layout.label(text="Source:") + col = layout.column(align=True) + col.prop(self, "src_density") + col.prop(self, "src_uv_area") + col.prop(self, "src_mesh_area") + col.enabled = False + + layout.separator() + layout.prop(self, "origin") + + layout.separator() + + def invoke(self, context, _): + if self.show_dialog: + wm = context.window_manager + sc = context.scene + + self.src_density = sc.muv_world_scale_uv_src_density + self.src_mesh_area = sc.muv_world_scale_uv_src_mesh_area + + return wm.invoke_props_dialog(self) + + return self.execute(context) + + def execute(self, context): + return self.__apply_proportional_to_mesh(context) diff --git a/magic_uv/preferences.py b/magic_uv/preferences.py new file mode 100644 index 00000000..3a024488 --- /dev/null +++ b/magic_uv/preferences.py @@ -0,0 +1,488 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy +from bpy.props import ( + FloatProperty, + FloatVectorProperty, + BoolProperty, + EnumProperty, +) +from bpy.types import AddonPreferences + +from . import common +from .op.flip_rotate_uv import MUV_OT_FlipRotate +from .op.mirror_uv import MUV_OT_MirrorUV +from .op.move_uv import MUV_OT_MoveUV +from .op.unwrap_constraint import MUV_OT_UnwrapConstraint +from .op.pack_uv import MUV_OT_PackUV +from .op.smooth_uv import MUV_OT_SmoothUV +from .ui.VIEW3D_MT_uv_map import ( + MUV_MT_CopyPasteUV, + MUV_MT_TransferUV, + MUV_MT_WorldScaleUV, + MUV_MT_PreserveUVAspect, + MUV_MT_TextureLock, + MUV_MT_TextureWrap, + MUV_MT_TextureProjection, + MUV_MT_UVW, +) +from .ui.VIEW3D_MT_object import MUV_MT_CopyPasteUV_Object +from .ui.IMAGE_MT_uvs import ( + MUV_MT_CopyPasteUV_UVEdit, + MUV_MT_SelectUV, + MUV_MT_AlignUV, + MUV_MT_AlignUVCursor, + MUV_MT_UVInspection, +) +from .utils.bl_class_registry import BlClassRegistry +from .utils.addon_updator import AddonUpdatorManager +from .utils import compatibility as compat +from . import updater + + +def view3d_uvmap_menu_fn(self, context): + layout = self.layout + sc = context.scene + + layout.separator() + layout.label(text="Copy/Paste UV", icon=compat.icon('IMAGE')) + # Copy/Paste UV + layout.menu(MUV_MT_CopyPasteUV.bl_idname, text="Copy/Paste UV") + # Transfer UV + layout.menu(MUV_MT_TransferUV.bl_idname, text="Transfer UV") + + layout.separator() + layout.label(text="UV Manipulation", icon=compat.icon('IMAGE')) + # Flip/Rotate UV + ops = layout.operator(MUV_OT_FlipRotate.bl_idname, text="Flip/Rotate UV") + ops.seams = sc.muv_flip_rotate_uv_seams + # Mirror UV + ops = layout.operator(MUV_OT_MirrorUV.bl_idname, text="Mirror UV") + ops.axis = sc.muv_mirror_uv_axis + # Move UV + layout.operator(MUV_OT_MoveUV.bl_idname, text="Move UV") + # World Scale UV + layout.menu(MUV_MT_WorldScaleUV.bl_idname, text="World Scale UV") + # Preserve UV + layout.menu(MUV_MT_PreserveUVAspect.bl_idname, text="Preserve UV") + # Texture Lock + layout.menu(MUV_MT_TextureLock.bl_idname, text="Texture Lock") + # Texture Wrap + layout.menu(MUV_MT_TextureWrap.bl_idname, text="Texture Wrap") + # UV Sculpt + layout.prop(sc, "muv_uv_sculpt_enable", text="UV Sculpt") + + layout.separator() + layout.label(text="UV Mapping", icon=compat.icon('IMAGE')) + # Unwrap Constraint + ops = layout.operator(MUV_OT_UnwrapConstraint.bl_idname, + text="Unwrap Constraint") + ops.u_const = sc.muv_unwrap_constraint_u_const + ops.v_const = sc.muv_unwrap_constraint_v_const + # Texture Projection + layout.menu(MUV_MT_TextureProjection.bl_idname, text="Texture Projection") + # UVW + layout.menu(MUV_MT_UVW.bl_idname, text="UVW") + + +def view3d_object_menu_fn(self, _): + layout = self.layout + + layout.separator() + layout.label(text="Copy/Paste UV", icon=compat.icon('IMAGE')) + # Copy/Paste UV (Among Object) + layout.menu(MUV_MT_CopyPasteUV_Object.bl_idname, text="Copy/Paste UV") + + +def image_uvs_menu_fn(self, context): + layout = self.layout + sc = context.scene + + layout.separator() + # Copy/Paste UV (on UV/Image Editor) + layout.label(text="Copy/Paste UV", icon=compat.icon('IMAGE')) + layout.menu(MUV_MT_CopyPasteUV_UVEdit.bl_idname, text="Copy/Paste UV") + + layout.separator() + # Pack UV + layout.label(text="UV Manipulation", icon=compat.icon('IMAGE')) + ops = layout.operator(MUV_OT_PackUV.bl_idname, text="Pack UV") + ops.allowable_center_deviation = sc.muv_pack_uv_allowable_center_deviation + ops.allowable_size_deviation = sc.muv_pack_uv_allowable_size_deviation + # Select UV + layout.menu(MUV_MT_SelectUV.bl_idname, text="Select UV") + # Smooth UV + ops = layout.operator(MUV_OT_SmoothUV.bl_idname, text="Smooth") + ops.transmission = sc.muv_smooth_uv_transmission + ops.select = sc.muv_smooth_uv_select + ops.mesh_infl = sc.muv_smooth_uv_mesh_infl + # Align UV + layout.menu(MUV_MT_AlignUV.bl_idname, text="Align UV") + + layout.separator() + # Align UV Cursor + layout.label(text="Editor Enhancement", icon=compat.icon('IMAGE')) + layout.menu(MUV_MT_AlignUVCursor.bl_idname, text="Align UV Cursor") + # UV Bounding Box + layout.prop(sc, "muv_uv_bounding_box_show", text="UV Bounding Box") + # UV Inspection + layout.menu(MUV_MT_UVInspection.bl_idname, text="UV Inspection") + + +def add_builtin_menu(): + bpy.types.VIEW3D_MT_uv_map.append(view3d_uvmap_menu_fn) + bpy.types.VIEW3D_MT_object.append(view3d_object_menu_fn) + bpy.types.IMAGE_MT_uvs.append(image_uvs_menu_fn) + + +def remove_builtin_menu(): + bpy.types.IMAGE_MT_uvs.remove(image_uvs_menu_fn) + bpy.types.VIEW3D_MT_object.append(view3d_object_menu_fn) + bpy.types.VIEW3D_MT_uv_map.remove(view3d_uvmap_menu_fn) + + +def get_update_candidate_branches(_, __): + manager = AddonUpdatorManager.get_instance() + if not manager.candidate_checked(): + return [] + + return [(name, name, "") for name in manager.get_candidate_branch_names()] + + +def set_debug_mode(self, value): + self['enable_debug_mode'] = value + + +def get_debug_mode(self): + enabled = self.get('enable_debug_mode', False) + if enabled: + common.enable_debugg_mode() + else: + common.disable_debug_mode() + return enabled + + +@BlClassRegistry() +@compat.make_annotations +class Preferences(AddonPreferences): + """Preferences class: Preferences for this add-on""" + + bl_idname = "magic_uv" + + def update_enable_builtin_menu(self, _): + if self['enable_builtin_menu']: + add_builtin_menu() + else: + remove_builtin_menu() + + # enable to add features to built-in menu + enable_builtin_menu = BoolProperty( + name="Built-in Menu", + description="Enable built-in menu", + default=True, + update=update_enable_builtin_menu, + ) + + # enable debug mode + enable_debug_mode = BoolProperty( + name="Debug Mode", + description="Enable debugging mode", + default=False, + set=set_debug_mode, + get=get_debug_mode, + ) + + # for UV Sculpt + uv_sculpt_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 + uv_inspection_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 + uv_inspection_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 + texture_projection_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 + uv_bounding_box_cp_size = FloatProperty( + name="Size", + description="Control Point Size", + default=6.0, + min=3.0, + max=100.0) + uv_bounding_box_cp_react_size = FloatProperty( + name="React Size", + description="Size event fired", + default=10.0, + min=3.0, + max=100.0) + + # for UI + category = EnumProperty( + name="Category", + description="Preferences Category", + items=[ + ('INFO', "Information", "Information about this add-on"), + ('CONFIG', "Configuration", "Configuration about this add-on"), + ('UPDATE', "Update", "Update this add-on"), + ], + default='INFO' + ) + info_desc_expanded = BoolProperty( + name="Description", + description="Description", + default=False + ) + info_loc_expanded = BoolProperty( + name="Location", + description="Location", + default=False + ) + conf_uv_sculpt_expanded = BoolProperty( + name="UV Sculpt", + description="UV Sculpt", + default=False + ) + conf_uv_inspection_expanded = BoolProperty( + name="UV Inspection", + description="UV Inspection", + default=False + ) + conf_texture_projection_expanded = BoolProperty( + name="Texture Projection", + description="Texture Projection", + default=False + ) + conf_uv_bounding_box_expanded = BoolProperty( + name="UV Bounding Box", + description="UV Bounding Box", + default=False + ) + + # for add-on updater + updater_branch_to_update = EnumProperty( + name="branch", + description="Target branch to update add-on", + items=get_update_candidate_branches + ) + + def draw(self, _): + layout = self.layout + + layout.row().prop(self, "category", expand=True) + + if self.category == 'INFO': + layout.separator() + + layout.prop( + self, "info_desc_expanded", text="Description", + icon='DISCLOSURE_TRI_DOWN' if self.info_desc_expanded + else 'DISCLOSURE_TRI_RIGHT') + if self.info_desc_expanded: + col = layout.column(align=True) + col.label(text="Magic UV is composed of many UV editing" + + " features.") + col.label(text="See tutorial page if you are new to this" + + " add-on.") + col.label(text="https://github.com/nutti/Magic-UV" + + "/wiki/Tutorial") + + layout.prop( + self, "info_loc_expanded", text="Location", + icon='DISCLOSURE_TRI_DOWN' if self.info_loc_expanded + else 'DISCLOSURE_TRI_RIGHT') + if self.info_loc_expanded: + row = layout.row(align=True) + sp = compat.layout_split(row, 0.5) + sp.label(text="3D View > Sidebar > " + + "Copy/Paste UV (Object mode)") + sp = compat.layout_split(sp, 1.0) + col = sp.column(align=True) + col.label(text="Copy/Paste UV (Among objects)") + + row = layout.row(align=True) + sp = compat.layout_split(row, 0.5) + sp.label(text="3D View > Sidebar > " + + "Copy/Paste UV (Edit mode)") + sp = compat.layout_split(sp, 1.0) + col = sp.column(align=True) + col.label(text="Copy/Paste UV (Among faces in 3D View)") + col.label(text="Transfer UV") + + row = layout.row(align=True) + sp = compat.layout_split(row, 0.5) + sp.label(text="3D View > Sidebar > " + + "UV Manipulation (Edit mode)") + sp = compat.layout_split(sp, 1.0) + col = sp.column(align=True) + col.label(text="Flip/Rotate UV") + col.label(text="Mirror UV") + col.label(text="Move UV") + col.label(text="World Scale UV") + col.label(text="Preserve UV Aspect") + col.label(text="Texture Lock") + col.label(text="Texture Wrap") + col.label(text="UV Sculpt") + + row = layout.row(align=True) + sp = compat.layout_split(row, 0.5) + sp.label(text="3D View > Sidebar > " + + "UV Manipulation (Edit mode)") + sp = compat.layout_split(sp, 1.0) + col = sp.column(align=True) + col.label(text="Unwrap Constraint") + col.label(text="Texture Projection") + col.label(text="UVW") + + row = layout.row(align=True) + sp = compat.layout_split(row, 0.5) + sp.label(text="UV/Image Editor > Sidebar > Copy/Paste UV") + sp = compat.layout_split(sp, 1.0) + col = sp.column(align=True) + col.label(text="Copy/Paste UV " + + "(Among faces in UV/Image Editor)") + + row = layout.row(align=True) + sp = compat.layout_split(row, 0.5) + sp.label(text="UV/Image Editor > Sidebar > UV Manipulation") + sp = compat.layout_split(sp, 1.0) + col = sp.column(align=True) + col.label(text="Align UV") + col.label(text="Smooth UV") + col.label(text="Select UV") + col.label(text="Pack UV (Extension)") + + row = layout.row(align=True) + sp = compat.layout_split(row, 0.5) + sp.label(text="UV/Image Editor > Sidebar > " + + "Editor Enhancement") + sp = compat.layout_split(sp, 1.0) + col = sp.column(align=True) + col.label(text="Align UV Cursor") + col.label(text="UV Cursor Location") + col.label(text="UV Bounding Box") + col.label(text="UV Inspection") + + elif self.category == 'CONFIG': + layout.separator() + + layout.prop(self, "enable_builtin_menu", text="Built-in Menu") + layout.prop(self, "enable_debug_mode", text="Debug Mode") + + layout.separator() + + layout.prop( + self, "conf_uv_sculpt_expanded", text="UV Sculpt", + icon='DISCLOSURE_TRI_DOWN' if self.conf_uv_sculpt_expanded + else 'DISCLOSURE_TRI_RIGHT') + if self.conf_uv_sculpt_expanded: + sp = compat.layout_split(layout, 0.05) + col = sp.column() # spacer + sp = compat.layout_split(sp, 0.3) + col = sp.column() + col.label(text="Brush Color:") + col.prop(self, "uv_sculpt_brush_color", text="") + layout.separator() + + layout.prop( + self, "conf_uv_inspection_expanded", text="UV Inspection", + icon='DISCLOSURE_TRI_DOWN' if self.conf_uv_inspection_expanded + else 'DISCLOSURE_TRI_RIGHT') + if self.conf_uv_inspection_expanded: + sp = compat.layout_split(layout, 0.05) + col = sp.column() # spacer + sp = compat.layout_split(sp, 0.3) + col = sp.column() + col.label(text="Overlapped UV Color:") + col.prop(self, "uv_inspection_overlapped_color", text="") + sp = compat.layout_split(sp, 0.45) + col = sp.column() + col.label(text="Flipped UV Color:") + col.prop(self, "uv_inspection_flipped_color", text="") + layout.separator() + + layout.prop( + self, "conf_texture_projection_expanded", + text="Texture Projection", + icon='DISCLOSURE_TRI_DOWN' + if self.conf_texture_projection_expanded + else 'DISCLOSURE_TRI_RIGHT') + if self.conf_texture_projection_expanded: + sp = compat.layout_split(layout, 0.05) + col = sp.column() # spacer + sp = compat.layout_split(sp, 0.3) + col = sp.column() + col.prop(self, "texture_projection_canvas_padding") + layout.separator() + + layout.prop( + self, "conf_uv_bounding_box_expanded", text="UV Bounding Box", + icon='DISCLOSURE_TRI_DOWN' + if self.conf_uv_bounding_box_expanded + else 'DISCLOSURE_TRI_RIGHT') + if self.conf_uv_bounding_box_expanded: + sp = compat.layout_split(layout, 0.05) + col = sp.column() # spacer + sp = compat.layout_split(sp, 0.3) + col = sp.column() + col.label(text="Control Point:") + col.prop(self, "uv_bounding_box_cp_size") + col.prop(self, "uv_bounding_box_cp_react_size") + layout.separator() + + elif self.category == 'UPDATE': + updater.draw_updater_ui(self) diff --git a/magic_uv/properites.py b/magic_uv/properites.py new file mode 100644 index 00000000..6ee00edd --- /dev/null +++ b/magic_uv/properites.py @@ -0,0 +1,43 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + + +from .utils.property_class_registry import PropertyClassRegistry + + +# Properties used in this add-on. +# pylint: disable=W0612 +class MUV_Properties(): + pass + + +def init_props(scene): + scene.muv_props = MUV_Properties() + PropertyClassRegistry.init_props(scene) + + +def clear_props(scene): + PropertyClassRegistry.del_props(scene) + del scene.muv_props diff --git a/magic_uv/ui/IMAGE_MT_uvs.py b/magic_uv/ui/IMAGE_MT_uvs.py new file mode 100644 index 00000000..ab7e33f8 --- /dev/null +++ b/magic_uv/ui/IMAGE_MT_uvs.py @@ -0,0 +1,188 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy + +from ..op.copy_paste_uv_uvedit import ( + MUV_OT_CopyPasteUVUVEdit_CopyUV, + MUV_OT_CopyPasteUVUVEdit_PasteUV, +) +from ..op.align_uv_cursor import MUV_OT_AlignUVCursor +from ..op.align_uv import ( + MUV_OT_AlignUV_Circle, + MUV_OT_AlignUV_Straighten, + MUV_OT_AlignUV_Axis, +) +from ..op.select_uv import ( + MUV_OT_SelectUV_SelectOverlapped, + MUV_OT_SelectUV_SelectFlipped, +) +from ..op.uv_inspection import MUV_OT_UVInspection_Update +from ..utils.bl_class_registry import BlClassRegistry + + +@BlClassRegistry() +class MUV_MT_CopyPasteUV_UVEdit(bpy.types.Menu): + """ + Menu class: Master menu of Copy/Paste UV coordinate on UV/ImageEditor + """ + + bl_idname = "uv.muv_mt_copy_paste_uv_uvedit" + bl_label = "Copy/Paste UV" + bl_description = "Copy and Paste UV coordinate among object" + + def draw(self, _): + layout = self.layout + + layout.operator(MUV_OT_CopyPasteUVUVEdit_CopyUV.bl_idname, text="Copy") + layout.operator(MUV_OT_CopyPasteUVUVEdit_PasteUV.bl_idname, + text="Paste") + + +@BlClassRegistry() +class MUV_MT_AlignUV(bpy.types.Menu): + """ + Menu class: Master menu of Align UV + """ + + bl_idname = "uv.muv_mt_align_uv" + bl_label = "Align UV" + bl_description = "Align UV" + + def draw(self, context): + layout = self.layout + sc = context.scene + + ops = layout.operator(MUV_OT_AlignUV_Circle.bl_idname, text="Circle") + ops.transmission = sc.muv_align_uv_transmission + ops.select = sc.muv_align_uv_select + + ops = layout.operator(MUV_OT_AlignUV_Straighten.bl_idname, + text="Straighten") + ops.transmission = sc.muv_align_uv_transmission + ops.select = sc.muv_align_uv_select + ops.vertical = sc.muv_align_uv_vertical + ops.horizontal = sc.muv_align_uv_horizontal + + ops = layout.operator(MUV_OT_AlignUV_Axis.bl_idname, text="XY-axis") + ops.transmission = sc.muv_align_uv_transmission + ops.select = sc.muv_align_uv_select + ops.vertical = sc.muv_align_uv_vertical + ops.horizontal = sc.muv_align_uv_horizontal + ops.location = sc.muv_align_uv_location + + +@BlClassRegistry() +class MUV_MT_SelectUV(bpy.types.Menu): + """ + Menu class: Master menu of Select UV + """ + + bl_idname = "uv.muv_mt_select_uv" + bl_label = "Select UV" + bl_description = "Select UV" + + def draw(self, _): + layout = self.layout + + layout.operator(MUV_OT_SelectUV_SelectOverlapped.bl_idname, + text="Overlapped") + layout.operator(MUV_OT_SelectUV_SelectFlipped.bl_idname, + text="Flipped") + + +@BlClassRegistry() +class MUV_MT_AlignUVCursor(bpy.types.Menu): + """ + Menu class: Master menu of Align UV Cursor + """ + + bl_idname = "uv.muv_mt_align_uv_cursor" + bl_label = "Align UV Cursor" + bl_description = "Align UV cursor" + + def draw(self, context): + layout = self.layout + sc = context.scene + + ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, text="Left Top") + ops.position = 'LEFT_TOP' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, + text="Middle Top") + ops.position = 'MIDDLE_TOP' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, text="Right Top") + ops.position = 'RIGHT_TOP' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, + text="Left Middle") + ops.position = 'LEFT_MIDDLE' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, text="Center") + ops.position = 'CENTER' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, + text="Right Middle") + ops.position = 'RIGHT_MIDDLE' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, + text="Left Bottom") + ops.position = 'LEFT_BOTTOM' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, + text="Middle Bottom") + ops.position = 'MIDDLE_BOTTOM' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, + text="Right Bottom") + ops.position = 'RIGHT_BOTTOM' + ops.base = sc.muv_align_uv_cursor_align_method + + +@BlClassRegistry() +class MUV_MT_UVInspection(bpy.types.Menu): + """ + Menu class: Master menu of UV Inspection + """ + + bl_idname = "uv.muv_mt_uv_inspection" + bl_label = "UV Inspection" + bl_description = "UV Inspection" + + def draw(self, context): + layout = self.layout + sc = context.scene + + layout.prop(sc, "muv_uv_inspection_show", text="UV Inspection") + layout.operator(MUV_OT_UVInspection_Update.bl_idname, text="Update") diff --git a/magic_uv/ui/VIEW3D_MT_object.py b/magic_uv/ui/VIEW3D_MT_object.py new file mode 100644 index 00000000..b691bdd5 --- /dev/null +++ b/magic_uv/ui/VIEW3D_MT_object.py @@ -0,0 +1,49 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy + +from ..op.copy_paste_uv_object import ( + MUV_MT_CopyPasteUVObject_CopyUV, + MUV_MT_CopyPasteUVObject_PasteUV, +) +from ..utils.bl_class_registry import BlClassRegistry + + +@BlClassRegistry() +class MUV_MT_CopyPasteUV_Object(bpy.types.Menu): + """ + Menu class: Master menu of Copy/Paste UV coordinate among object + """ + + bl_idname = "uv.muv_mt_copy_paste_uv_object" + bl_label = "Copy/Paste UV" + bl_description = "Copy and Paste UV coordinate among object" + + def draw(self, _): + layout = self.layout + + layout.menu(MUV_MT_CopyPasteUVObject_CopyUV.bl_idname, text="Copy") + layout.menu(MUV_MT_CopyPasteUVObject_PasteUV.bl_idname, text="Paste") diff --git a/magic_uv/ui/VIEW3D_MT_uv_map.py b/magic_uv/ui/VIEW3D_MT_uv_map.py new file mode 100644 index 00000000..12202602 --- /dev/null +++ b/magic_uv/ui/VIEW3D_MT_uv_map.py @@ -0,0 +1,254 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy.utils + +from ..op.copy_paste_uv import ( + MUV_MT_CopyPasteUV_CopyUV, + MUV_MT_CopyPasteUV_PasteUV, + MUV_MT_CopyPasteUV_SelSeqCopyUV, + MUV_MT_CopyPasteUV_SelSeqPasteUV, +) +from ..op.transfer_uv import ( + MUV_OT_TransferUV_CopyUV, + MUV_OT_TransferUV_PasteUV, +) +from ..op.uvw import ( + MUV_OT_UVW_BoxMap, + MUV_OT_UVW_BestPlanerMap, +) +from ..op.preserve_uv_aspect import MUV_OT_PreserveUVAspect +from ..op.texture_lock import ( + MUV_OT_TextureLock_Lock, + MUV_OT_TextureLock_Unlock, +) +from ..op.texture_wrap import ( + MUV_OT_TextureWrap_Refer, + MUV_OT_TextureWrap_Set, +) +from ..op.world_scale_uv import ( + MUV_OT_WorldScaleUV_Measure, + MUV_OT_WorldScaleUV_ApplyManual, + MUV_OT_WorldScaleUV_ApplyScalingDensity, + MUV_OT_WorldScaleUV_ApplyProportionalToMesh, +) +from ..op.texture_projection import MUV_OT_TextureProjection_Project +from ..utils.bl_class_registry import BlClassRegistry + + +@BlClassRegistry() +class MUV_MT_CopyPasteUV(bpy.types.Menu): + """ + Menu class: Master menu of Copy/Paste UV coordinate + """ + + bl_idname = "uv.muv_mt_copy_paste_uv" + bl_label = "Copy/Paste UV" + bl_description = "Copy and Paste UV coordinate" + + def draw(self, _): + layout = self.layout + + layout.label(text="Default") + layout.menu(MUV_MT_CopyPasteUV_CopyUV.bl_idname, text="Copy") + layout.menu(MUV_MT_CopyPasteUV_PasteUV.bl_idname, text="Paste") + + layout.separator() + + layout.label(text="Selection Sequence") + layout.menu(MUV_MT_CopyPasteUV_SelSeqCopyUV.bl_idname, text="Copy") + layout.menu(MUV_MT_CopyPasteUV_SelSeqPasteUV.bl_idname, text="Paste") + + +@BlClassRegistry() +class MUV_MT_TransferUV(bpy.types.Menu): + """ + Menu class: Master menu of Transfer UV coordinate + """ + + bl_idname = "uv.muv_mt_transfer_uv" + bl_label = "Transfer UV" + bl_description = "Transfer UV coordinate" + + def draw(self, context): + layout = self.layout + sc = context.scene + + layout.operator(MUV_OT_TransferUV_CopyUV.bl_idname, text="Copy") + ops = layout.operator(MUV_OT_TransferUV_PasteUV.bl_idname, + text="Paste") + ops.invert_normals = sc.muv_transfer_uv_invert_normals + ops.copy_seams = sc.muv_transfer_uv_copy_seams + + +@BlClassRegistry() +class MUV_MT_TextureLock(bpy.types.Menu): + """ + Menu class: Master menu of Texture Lock + """ + + bl_idname = "uv.muv_mt_texture_lock" + bl_label = "Texture Lock" + bl_description = "Lock texture when vertices of mesh (Preserve UV)" + + def draw(self, context): + layout = self.layout + sc = context.scene + + layout.label(text="Normal Mode") + layout.operator( + MUV_OT_TextureLock_Lock.bl_idname, + text="Lock" + if not MUV_OT_TextureLock_Lock.is_ready(context) + else "ReLock") + ops = layout.operator(MUV_OT_TextureLock_Unlock.bl_idname, + text="Unlock") + ops.connect = sc.muv_texture_lock_connect + + layout.separator() + + layout.label(text="Interactive Mode") + layout.prop(sc, "muv_texture_lock_lock", text="Lock") + + +@BlClassRegistry() +class MUV_MT_WorldScaleUV(bpy.types.Menu): + """ + Menu class: Master menu of world scale UV + """ + + bl_idname = "uv.muv_mt_world_scale_uv" + bl_label = "World Scale UV" + bl_description = "" + + def draw(self, context): + layout = self.layout + sc = context.scene + + layout.operator(MUV_OT_WorldScaleUV_Measure.bl_idname, + text="Measure") + + layout.operator(MUV_OT_WorldScaleUV_ApplyManual.bl_idname, + text="Apply (Manual)") + + ops = layout.operator( + MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname, + text="Apply (Same Desity)") + ops.src_density = sc.muv_world_scale_uv_src_density + ops.same_density = True + + ops = layout.operator( + MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname, + text="Apply (Scaling Desity)") + ops.src_density = sc.muv_world_scale_uv_src_density + ops.same_density = False + ops.tgt_scaling_factor = sc.muv_world_scale_uv_tgt_scaling_factor + + ops = layout.operator( + MUV_OT_WorldScaleUV_ApplyProportionalToMesh.bl_idname, + text="Apply (Proportional to Mesh)") + ops.src_density = sc.muv_world_scale_uv_src_density + ops.src_uv_area = sc.muv_world_scale_uv_src_uv_area + ops.src_mesh_area = sc.muv_world_scale_uv_src_mesh_area + ops.origin = sc.muv_world_scale_uv_origin + + +@BlClassRegistry() +class MUV_MT_TextureWrap(bpy.types.Menu): + """ + Menu class: Master menu of Texture Wrap + """ + + bl_idname = "uv.muv_mt_texture_wrap" + bl_label = "Texture Wrap" + bl_description = "" + + def draw(self, _): + layout = self.layout + + layout.operator(MUV_OT_TextureWrap_Refer.bl_idname, text="Refer") + layout.operator(MUV_OT_TextureWrap_Set.bl_idname, text="Set") + + +@BlClassRegistry() +class MUV_MT_UVW(bpy.types.Menu): + """ + Menu class: Master menu of UVW + """ + + bl_idname = "uv.muv_mt_uvw" + bl_label = "UVW" + bl_description = "" + + def draw(self, context): + layout = self.layout + sc = context.scene + + ops = layout.operator(MUV_OT_UVW_BoxMap.bl_idname, text="Box") + ops.assign_uvmap = sc.muv_uvw_assign_uvmap + + ops = layout.operator(MUV_OT_UVW_BestPlanerMap.bl_idname, + text="Best Planner") + ops.assign_uvmap = sc.muv_uvw_assign_uvmap + + +@BlClassRegistry() +class MUV_MT_PreserveUVAspect(bpy.types.Menu): + """ + Menu class: Master menu of Preserve UV Aspect + """ + + bl_idname = "uv.muv_mt_preserve_uv_aspect" + bl_label = "Preserve UV Aspect" + bl_description = "" + + def draw(self, context): + layout = self.layout + sc = context.scene + + for key in bpy.data.images.keys(): + ops = layout.operator(MUV_OT_PreserveUVAspect.bl_idname, text=key) + ops.dest_img_name = key + ops.origin = sc.muv_preserve_uv_aspect_origin + + +@BlClassRegistry() +class MUV_MT_TextureProjection(bpy.types.Menu): + """ + Menu class: Master menu of Texture Projection + """ + + bl_idname = "uv.muv_mt_texture_projection" + bl_label = "Texture Projection" + bl_description = "" + + def draw(self, context): + layout = self.layout + sc = context.scene + + layout.prop(sc, "muv_texture_projection_enable", + text="Texture Projection") + layout.operator(MUV_OT_TextureProjection_Project.bl_idname, + text="Project") diff --git a/magic_uv/ui/__init__.py b/magic_uv/ui/__init__.py new file mode 100644 index 00000000..032cc3bd --- /dev/null +++ b/magic_uv/ui/__init__.py @@ -0,0 +1,50 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +if "bpy" in locals(): + import importlib + importlib.reload(view3d_copy_paste_uv_editmode) + importlib.reload(view3d_copy_paste_uv_objectmode) + 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_enhancement) + importlib.reload(VIEW3D_MT_object) + importlib.reload(VIEW3D_MT_uv_map) + importlib.reload(IMAGE_MT_uvs) +else: + from . import view3d_copy_paste_uv_editmode + from . import view3d_copy_paste_uv_objectmode + 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_enhancement + from . import VIEW3D_MT_object + from . import VIEW3D_MT_uv_map + from . import IMAGE_MT_uvs + +import bpy diff --git a/magic_uv/ui/uvedit_copy_paste_uv.py b/magic_uv/ui/uvedit_copy_paste_uv.py new file mode 100644 index 00000000..39259649 --- /dev/null +++ b/magic_uv/ui/uvedit_copy_paste_uv.py @@ -0,0 +1,59 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy + +from ..op.copy_paste_uv_uvedit import ( + MUV_OT_CopyPasteUVUVEdit_CopyUV, + MUV_OT_CopyPasteUVUVEdit_PasteUV, +) +from ..utils.bl_class_registry import BlClassRegistry +from ..utils import compatibility as compat + + +@BlClassRegistry() +@compat.ChangeRegionType(region_type='TOOLS') +class MUV_PT_UVEdit_CopyPasteUV(bpy.types.Panel): + """ + Panel class: Copy/Paste UV on Property Panel on UV/ImageEditor + """ + + bl_space_type = 'IMAGE_EDITOR' + bl_region_type = 'UI' + 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=compat.icon('IMAGE')) + + def draw(self, _): + layout = self.layout + + row = layout.row(align=True) + row.operator(MUV_OT_CopyPasteUVUVEdit_CopyUV.bl_idname, text="Copy") + row.operator(MUV_OT_CopyPasteUVUVEdit_PasteUV.bl_idname, text="Paste") diff --git a/magic_uv/ui/uvedit_editor_enhancement.py b/magic_uv/ui/uvedit_editor_enhancement.py new file mode 100644 index 00000000..dbae514f --- /dev/null +++ b/magic_uv/ui/uvedit_editor_enhancement.py @@ -0,0 +1,146 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy + +from ..op.align_uv_cursor import MUV_OT_AlignUVCursor +from ..op.uv_bounding_box import ( + MUV_OT_UVBoundingBox, +) +from ..op.uv_inspection import ( + MUV_OT_UVInspection_Render, + MUV_OT_UVInspection_Update, +) +from ..utils.bl_class_registry import BlClassRegistry +from ..utils import compatibility as compat + + +@BlClassRegistry() +@compat.ChangeRegionType(region_type='TOOLS') +class MUV_PT_UVEdit_EditorEnhancement(bpy.types.Panel): + """ + Panel class: UV/Image Editor Enhancement + """ + + bl_space_type = 'IMAGE_EDITOR' + bl_region_type = 'UI' + 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=compat.icon('IMAGE')) + + def draw(self, context): + layout = self.layout + sc = context.scene + + box = layout.box() + box.prop(sc, "muv_align_uv_cursor_enabled", text="Align UV Cursor") + if sc.muv_align_uv_cursor_enabled: + box.prop(sc, "muv_align_uv_cursor_align_method", expand=True) + + col = box.column(align=True) + + row = col.row(align=True) + ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, text="Left Top") + ops.position = 'LEFT_TOP' + ops.base = sc.muv_align_uv_cursor_align_method + ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, + text="Middle Top") + ops.position = 'MIDDLE_TOP' + ops.base = sc.muv_align_uv_cursor_align_method + ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, + text="Right Top") + ops.position = 'RIGHT_TOP' + ops.base = sc.muv_align_uv_cursor_align_method + + row = col.row(align=True) + ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, + text="Left Middle") + ops.position = 'LEFT_MIDDLE' + ops.base = sc.muv_align_uv_cursor_align_method + ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, text="Center") + ops.position = 'CENTER' + ops.base = sc.muv_align_uv_cursor_align_method + ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, + text="Right Middle") + ops.position = 'RIGHT_MIDDLE' + ops.base = sc.muv_align_uv_cursor_align_method + + row = col.row(align=True) + ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, + text="Left Bottom") + ops.position = 'LEFT_BOTTOM' + ops.base = sc.muv_align_uv_cursor_align_method + ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, + text="Middle Bottom") + ops.position = 'MIDDLE_BOTTOM' + ops.base = sc.muv_align_uv_cursor_align_method + ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, + text="Right Bottom") + ops.position = 'RIGHT_BOTTOM' + ops.base = sc.muv_align_uv_cursor_align_method + + box = layout.box() + box.prop(sc, "muv_uv_cursor_location_enabled", + text="UV Cursor Location") + if sc.muv_uv_cursor_location_enabled: + box.prop(sc, "muv_align_uv_cursor_cursor_loc", text="") + + box = layout.box() + box.prop(sc, "muv_uv_bounding_box_enabled", text="UV Bounding Box") + if sc.muv_uv_bounding_box_enabled: + box.prop(sc, "muv_uv_bounding_box_show", + text="Hide" + if MUV_OT_UVBoundingBox.is_running(context) + else "Show", + icon='RESTRICT_VIEW_OFF' + if MUV_OT_UVBoundingBox.is_running(context) + else 'RESTRICT_VIEW_ON') + box.prop(sc, "muv_uv_bounding_box_uniform_scaling", + text="Uniform Scaling") + box.prop(sc, "muv_uv_bounding_box_boundary", text="Boundary") + + box = layout.box() + box.prop(sc, "muv_uv_inspection_enabled", text="UV Inspection") + if sc.muv_uv_inspection_enabled: + row = box.row() + row.prop( + sc, "muv_uv_inspection_show", + text="Hide" + if MUV_OT_UVInspection_Render.is_running(context) + else "Show", + icon='RESTRICT_VIEW_OFF' + if MUV_OT_UVInspection_Render.is_running(context) + else 'RESTRICT_VIEW_ON') + row.operator(MUV_OT_UVInspection_Update.bl_idname, text="Update") + row = box.row() + row.prop(sc, "muv_uv_inspection_show_overlapped") + row.prop(sc, "muv_uv_inspection_show_flipped") + row = box.row() + row.prop(sc, "muv_uv_inspection_show_mode") diff --git a/magic_uv/ui/uvedit_uv_manipulation.py b/magic_uv/ui/uvedit_uv_manipulation.py new file mode 100644 index 00000000..96c8b54b --- /dev/null +++ b/magic_uv/ui/uvedit_uv_manipulation.py @@ -0,0 +1,132 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy + +from ..op.align_uv import ( + MUV_OT_AlignUV_Circle, + MUV_OT_AlignUV_Straighten, + MUV_OT_AlignUV_Axis, +) +from ..op.smooth_uv import ( + MUV_OT_SmoothUV, +) +from ..op.select_uv import ( + MUV_OT_SelectUV_SelectOverlapped, + MUV_OT_SelectUV_SelectFlipped, +) +from ..op.pack_uv import MUV_OT_PackUV +from ..utils.bl_class_registry import BlClassRegistry +from ..utils import compatibility as compat + + +@BlClassRegistry() +@compat.ChangeRegionType(region_type='TOOLS') +class MUV_PT_UVEdit_UVManipulation(bpy.types.Panel): + """ + Panel class: UV Manipulation on Property Panel on UV/ImageEditor + """ + + bl_space_type = 'IMAGE_EDITOR' + bl_region_type = 'UI' + 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=compat.icon('IMAGE')) + + def draw(self, context): + sc = context.scene + layout = self.layout + + box = layout.box() + box.prop(sc, "muv_align_uv_enabled", text="Align UV") + if sc.muv_align_uv_enabled: + col = box.column() + row = col.row(align=True) + ops = row.operator(MUV_OT_AlignUV_Circle.bl_idname, text="Circle") + ops.transmission = sc.muv_align_uv_transmission + ops.select = sc.muv_align_uv_select + ops = row.operator(MUV_OT_AlignUV_Straighten.bl_idname, + text="Straighten") + ops.transmission = sc.muv_align_uv_transmission + ops.select = sc.muv_align_uv_select + ops.vertical = sc.muv_align_uv_vertical + ops.horizontal = sc.muv_align_uv_horizontal + ops.mesh_infl = sc.muv_align_uv_mesh_infl + row = col.row() + ops = row.operator(MUV_OT_AlignUV_Axis.bl_idname, text="XY-axis") + ops.transmission = sc.muv_align_uv_transmission + ops.select = sc.muv_align_uv_select + ops.vertical = sc.muv_align_uv_vertical + ops.horizontal = sc.muv_align_uv_horizontal + ops.location = sc.muv_align_uv_location + ops.mesh_infl = sc.muv_align_uv_mesh_infl + row.prop(sc, "muv_align_uv_location", text="") + + col = box.column(align=True) + row = col.row(align=True) + row.prop(sc, "muv_align_uv_transmission", text="Transmission") + row.prop(sc, "muv_align_uv_select", text="Select") + row = col.row(align=True) + row.prop(sc, "muv_align_uv_vertical", text="Vertical") + row.prop(sc, "muv_align_uv_horizontal", text="Horizontal") + col.prop(sc, "muv_align_uv_mesh_infl", text="Mesh Influence") + + box = layout.box() + box.prop(sc, "muv_smooth_uv_enabled", text="Smooth UV") + if sc.muv_smooth_uv_enabled: + ops = box.operator(MUV_OT_SmoothUV.bl_idname, text="Smooth") + ops.transmission = sc.muv_smooth_uv_transmission + ops.select = sc.muv_smooth_uv_select + ops.mesh_infl = sc.muv_smooth_uv_mesh_infl + col = box.column(align=True) + row = col.row(align=True) + row.prop(sc, "muv_smooth_uv_transmission", text="Transmission") + row.prop(sc, "muv_smooth_uv_select", text="Select") + col.prop(sc, "muv_smooth_uv_mesh_infl", text="Mesh Influence") + + box = layout.box() + box.prop(sc, "muv_select_uv_enabled", text="Select UV") + if sc.muv_select_uv_enabled: + row = box.row(align=True) + row.operator(MUV_OT_SelectUV_SelectOverlapped.bl_idname) + row.operator(MUV_OT_SelectUV_SelectFlipped.bl_idname) + + box = layout.box() + box.prop(sc, "muv_pack_uv_enabled", text="Pack UV (Extension)") + if sc.muv_pack_uv_enabled: + ops = box.operator(MUV_OT_PackUV.bl_idname, text="Pack UV") + ops.allowable_center_deviation = \ + sc.muv_pack_uv_allowable_center_deviation + ops.allowable_size_deviation = \ + sc.muv_pack_uv_allowable_size_deviation + box.label(text="Allowable Center Deviation:") + box.prop(sc, "muv_pack_uv_allowable_center_deviation", text="") + box.label(text="Allowable Size Deviation:") + box.prop(sc, "muv_pack_uv_allowable_size_deviation", text="") diff --git a/magic_uv/ui/view3d_copy_paste_uv_editmode.py b/magic_uv/ui/view3d_copy_paste_uv_editmode.py new file mode 100644 index 00000000..49a4e0a3 --- /dev/null +++ b/magic_uv/ui/view3d_copy_paste_uv_editmode.py @@ -0,0 +1,92 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy + +from ..op.copy_paste_uv import ( + MUV_MT_CopyPasteUV_CopyUV, + MUV_MT_CopyPasteUV_PasteUV, + MUV_MT_CopyPasteUV_SelSeqCopyUV, + MUV_MT_CopyPasteUV_SelSeqPasteUV, +) +from ..op.transfer_uv import ( + MUV_OT_TransferUV_CopyUV, + MUV_OT_TransferUV_PasteUV, +) +from ..utils.bl_class_registry import BlClassRegistry +from ..utils import compatibility as compat + + +@BlClassRegistry() +@compat.ChangeRegionType(region_type='TOOLS') +class MUV_PT_CopyPasteUVEditMode(bpy.types.Panel): + """ + Panel class: Copy/Paste UV on Property Panel on View3D + """ + + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + 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=compat.icon('IMAGE')) + + def draw(self, context): + sc = context.scene + layout = self.layout + + box = layout.box() + box.prop(sc, "muv_copy_paste_uv_enabled", text="Copy/Paste UV") + if sc.muv_copy_paste_uv_enabled: + row = box.row(align=True) + if sc.muv_copy_paste_uv_mode == 'DEFAULT': + row.menu(MUV_MT_CopyPasteUV_CopyUV.bl_idname, text="Copy") + row.menu(MUV_MT_CopyPasteUV_PasteUV.bl_idname, text="Paste") + elif sc.muv_copy_paste_uv_mode == 'SEL_SEQ': + row.menu(MUV_MT_CopyPasteUV_SelSeqCopyUV.bl_idname, + text="Copy") + row.menu(MUV_MT_CopyPasteUV_SelSeqPasteUV.bl_idname, + text="Paste") + box.prop(sc, "muv_copy_paste_uv_mode", expand=True) + box.prop(sc, "muv_copy_paste_uv_copy_seams", text="Seams") + box.prop(sc, "muv_copy_paste_uv_strategy", text="Strategy") + + box = layout.box() + box.prop(sc, "muv_transfer_uv_enabled", text="Transfer UV") + if sc.muv_transfer_uv_enabled: + row = box.row(align=True) + row.operator(MUV_OT_TransferUV_CopyUV.bl_idname, text="Copy") + ops = row.operator(MUV_OT_TransferUV_PasteUV.bl_idname, + text="Paste") + ops.invert_normals = sc.muv_transfer_uv_invert_normals + ops.copy_seams = sc.muv_transfer_uv_copy_seams + row = box.row() + row.prop(sc, "muv_transfer_uv_invert_normals", + text="Invert Normals") + row.prop(sc, "muv_transfer_uv_copy_seams", text="Seams") diff --git a/magic_uv/ui/view3d_copy_paste_uv_objectmode.py b/magic_uv/ui/view3d_copy_paste_uv_objectmode.py new file mode 100644 index 00000000..574a0e43 --- /dev/null +++ b/magic_uv/ui/view3d_copy_paste_uv_objectmode.py @@ -0,0 +1,62 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy + +from ..op.copy_paste_uv_object import ( + MUV_MT_CopyPasteUVObject_CopyUV, + MUV_MT_CopyPasteUVObject_PasteUV, +) +from ..utils.bl_class_registry import BlClassRegistry +from ..utils import compatibility as compat + + +@BlClassRegistry() +@compat.ChangeRegionType(region_type='TOOLS') +class MUV_PT_View3D_Object_CopyPasteUV(bpy.types.Panel): + """ + Panel class: Copy/Paste UV on Property Panel on View3D + """ + + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + 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=compat.icon('IMAGE')) + + def draw(self, context): + sc = context.scene + layout = self.layout + + row = layout.row(align=True) + row.menu(MUV_MT_CopyPasteUVObject_CopyUV.bl_idname, text="Copy") + row.menu(MUV_MT_CopyPasteUVObject_PasteUV.bl_idname, text="Paste") + layout.prop(sc, "muv_copy_paste_uv_object_copy_seams", + text="Seams") diff --git a/magic_uv/ui/view3d_uv_manipulation.py b/magic_uv/ui/view3d_uv_manipulation.py new file mode 100644 index 00000000..312ae171 --- /dev/null +++ b/magic_uv/ui/view3d_uv_manipulation.py @@ -0,0 +1,282 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy + +from ..op.texture_lock import ( + MUV_OT_TextureLock_Lock, + MUV_OT_TextureLock_Unlock, + MUV_OT_TextureLock_Intr, +) +from ..op.texture_wrap import ( + MUV_OT_TextureWrap_Refer, + MUV_OT_TextureWrap_Set, +) +from ..op.uv_sculpt import ( + MUV_OT_UVSculpt, +) +from ..op.world_scale_uv import ( + MUV_OT_WorldScaleUV_Measure, + MUV_OT_WorldScaleUV_ApplyManual, + MUV_OT_WorldScaleUV_ApplyScalingDensity, + MUV_OT_WorldScaleUV_ApplyProportionalToMesh, +) +from ..op.flip_rotate_uv import MUV_OT_FlipRotate +from ..op.mirror_uv import MUV_OT_MirrorUV +from ..op.move_uv import MUV_OT_MoveUV +from ..op.preserve_uv_aspect import MUV_OT_PreserveUVAspect +from ..utils.bl_class_registry import BlClassRegistry +from ..utils import compatibility as compat + + +@BlClassRegistry() +@compat.ChangeRegionType(region_type='TOOLS') +class MUV_PT_View3D_UVManipulation(bpy.types.Panel): + """ + Panel class: UV Manipulation on Property Panel on View3D + """ + + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + 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=compat.icon('IMAGE')) + + def draw(self, context): + sc = context.scene + layout = self.layout + + box = layout.box() + box.prop(sc, "muv_flip_rotate_uv_enabled", text="Flip/Rotate UV") + if sc.muv_flip_rotate_uv_enabled: + row = box.row() + ops = row.operator(MUV_OT_FlipRotate.bl_idname, text="Flip/Rotate") + ops.seams = sc.muv_flip_rotate_uv_seams + row.prop(sc, "muv_flip_rotate_uv_seams", text="Seams") + + box = layout.box() + box.prop(sc, "muv_mirror_uv_enabled", text="Mirror UV") + if sc.muv_mirror_uv_enabled: + row = box.row() + ops = row.operator(MUV_OT_MirrorUV.bl_idname, text="Mirror") + ops.axis = sc.muv_mirror_uv_axis + row.prop(sc, "muv_mirror_uv_axis", text="") + + box = layout.box() + box.prop(sc, "muv_move_uv_enabled", text="Move UV") + if sc.muv_move_uv_enabled: + col = box.column() + if not MUV_OT_MoveUV.is_running(context): + col.operator(MUV_OT_MoveUV.bl_idname, icon='PLAY', + text="Start") + else: + col.operator(MUV_OT_MoveUV.bl_idname, icon='PAUSE', + text="Stop") + + box = layout.box() + box.prop(sc, "muv_world_scale_uv_enabled", text="World Scale UV") + if sc.muv_world_scale_uv_enabled: + box.prop(sc, "muv_world_scale_uv_mode", text="") + + if sc.muv_world_scale_uv_mode == 'MANUAL': + sp = compat.layout_split(box, 0.5) + col = sp.column() + col.prop(sc, "muv_world_scale_uv_tgt_texture_size", + text="Texture Size") + sp = compat.layout_split(sp, 1.0) + col = sp.column() + col.label(text="Density:") + col.prop(sc, "muv_world_scale_uv_tgt_density", text="") + box.prop(sc, "muv_world_scale_uv_origin", text="Origin") + ops = box.operator(MUV_OT_WorldScaleUV_ApplyManual.bl_idname, + text="Apply") + ops.tgt_density = sc.muv_world_scale_uv_tgt_density + ops.tgt_texture_size = sc.muv_world_scale_uv_tgt_texture_size + ops.origin = sc.muv_world_scale_uv_origin + ops.show_dialog = False + + elif sc.muv_world_scale_uv_mode == 'SAME_DENSITY': + sp = compat.layout_split(box, 0.4) + col = sp.column(align=True) + col.label(text="Source:") + sp = compat.layout_split(sp, 1.0) + col = sp.column(align=True) + col.operator(MUV_OT_WorldScaleUV_Measure.bl_idname, + text="Measure") + + sp = compat.layout_split(box, 0.7) + col = sp.column(align=True) + col.prop(sc, "muv_world_scale_uv_src_density", text="Density") + col.enabled = False + sp = compat.layout_split(sp, 1.0) + col = sp.column(align=True) + col.label(text="px2/cm2") + + box.separator() + box.prop(sc, "muv_world_scale_uv_origin", text="Origin") + ops = box.operator( + MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname, + text="Apply") + ops.src_density = sc.muv_world_scale_uv_src_density + ops.origin = sc.muv_world_scale_uv_origin + ops.same_density = True + ops.show_dialog = False + + elif sc.muv_world_scale_uv_mode == 'SCALING_DENSITY': + sp = compat.layout_split(box, 0.4) + col = sp.column(align=True) + col.label(text="Source:") + sp = compat.layout_split(sp, 1.0) + col = sp.column(align=True) + col.operator(MUV_OT_WorldScaleUV_Measure.bl_idname, + text="Measure") + + sp = compat.layout_split(box, 0.7) + col = sp.column(align=True) + col.prop(sc, "muv_world_scale_uv_src_density", text="Density") + col.enabled = False + sp = compat.layout_split(sp, 1.0) + col = sp.column(align=True) + col.label(text="px2/cm2") + + box.separator() + box.prop(sc, "muv_world_scale_uv_tgt_scaling_factor", + text="Scaling Factor") + box.prop(sc, "muv_world_scale_uv_origin", text="Origin") + ops = box.operator( + MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname, + text="Apply") + ops.src_density = sc.muv_world_scale_uv_src_density + ops.origin = sc.muv_world_scale_uv_origin + ops.same_density = False + ops.show_dialog = False + ops.tgt_scaling_factor = \ + sc.muv_world_scale_uv_tgt_scaling_factor + + elif sc.muv_world_scale_uv_mode == 'PROPORTIONAL_TO_MESH': + sp = compat.layout_split(box, 0.4) + col = sp.column(align=True) + col.label(text="Source:") + sp = compat.layout_split(sp, 1.0) + col = sp.column(align=True) + col.operator(MUV_OT_WorldScaleUV_Measure.bl_idname, + text="Measure") + + sp = compat.layout_split(box, 0.7) + col = sp.column(align=True) + col.prop(sc, "muv_world_scale_uv_src_mesh_area", + text="Mesh Area") + col.prop(sc, "muv_world_scale_uv_src_uv_area", text="UV Area") + col.prop(sc, "muv_world_scale_uv_src_density", text="Density") + col.enabled = False + sp = compat.layout_split(sp, 1.0) + col = sp.column(align=True) + col.label(text="cm2") + col.label(text="px2") + col.label(text="px2/cm2") + col.enabled = False + + box.separator() + box.prop(sc, "muv_world_scale_uv_origin", text="Origin") + ops = box.operator( + MUV_OT_WorldScaleUV_ApplyProportionalToMesh.bl_idname, + text="Apply") + ops.src_density = sc.muv_world_scale_uv_src_density + ops.src_uv_area = sc.muv_world_scale_uv_src_uv_area + ops.src_mesh_area = sc.muv_world_scale_uv_src_mesh_area + ops.origin = sc.muv_world_scale_uv_origin + ops.show_dialog = False + + box = layout.box() + box.prop(sc, "muv_preserve_uv_aspect_enabled", + text="Preserve UV Aspect") + if sc.muv_preserve_uv_aspect_enabled: + row = box.row() + ops = row.operator(MUV_OT_PreserveUVAspect.bl_idname, + text="Change Image") + ops.dest_img_name = sc.muv_preserve_uv_aspect_tex_image + ops.origin = sc.muv_preserve_uv_aspect_origin + row.prop(sc, "muv_preserve_uv_aspect_tex_image", text="") + box.prop(sc, "muv_preserve_uv_aspect_origin", text="Origin") + + box = layout.box() + box.prop(sc, "muv_texture_lock_enabled", text="Texture Lock") + if sc.muv_texture_lock_enabled: + row = box.row(align=True) + col = row.column(align=True) + col.label(text="Normal Mode:") + col = row.column(align=True) + col.operator(MUV_OT_TextureLock_Lock.bl_idname, + text="Lock" + if not MUV_OT_TextureLock_Lock.is_ready(context) + else "ReLock") + ops = col.operator(MUV_OT_TextureLock_Unlock.bl_idname, + text="Unlock") + ops.connect = sc.muv_texture_lock_connect + col.prop(sc, "muv_texture_lock_connect", text="Connect") + + row = box.row(align=True) + row.label(text="Interactive Mode:") + box.prop(sc, "muv_texture_lock_lock", + text="Unlock" + if MUV_OT_TextureLock_Intr.is_running(context) + else "Lock", + icon='RESTRICT_VIEW_OFF' + if MUV_OT_TextureLock_Intr.is_running(context) + else 'RESTRICT_VIEW_ON') + + box = layout.box() + box.prop(sc, "muv_texture_wrap_enabled", text="Texture Wrap") + if sc.muv_texture_wrap_enabled: + row = box.row(align=True) + row.operator(MUV_OT_TextureWrap_Refer.bl_idname, text="Refer") + row.operator(MUV_OT_TextureWrap_Set.bl_idname, text="Set") + box.prop(sc, "muv_texture_wrap_set_and_refer") + box.prop(sc, "muv_texture_wrap_selseq") + + box = layout.box() + box.prop(sc, "muv_uv_sculpt_enabled", text="UV Sculpt") + if sc.muv_uv_sculpt_enabled: + box.prop(sc, "muv_uv_sculpt_enable", + text="Disable"if MUV_OT_UVSculpt.is_running(context) + else "Enable", + icon='RESTRICT_VIEW_OFF' + if MUV_OT_UVSculpt.is_running(context) + else 'RESTRICT_VIEW_ON') + col = box.column() + col.label(text="Brush:") + col.prop(sc, "muv_uv_sculpt_radius") + col.prop(sc, "muv_uv_sculpt_strength") + box.prop(sc, "muv_uv_sculpt_tools") + if sc.muv_uv_sculpt_tools == 'PINCH': + box.prop(sc, "muv_uv_sculpt_pinch_invert") + elif sc.muv_uv_sculpt_tools == 'RELAX': + box.prop(sc, "muv_uv_sculpt_relax_method") + box.prop(sc, "muv_uv_sculpt_show_brush") diff --git a/magic_uv/ui/view3d_uv_mapping.py b/magic_uv/ui/view3d_uv_mapping.py new file mode 100644 index 00000000..278d1725 --- /dev/null +++ b/magic_uv/ui/view3d_uv_mapping.py @@ -0,0 +1,114 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy + +from ..op.uvw import ( + MUV_OT_UVW_BoxMap, + MUV_OT_UVW_BestPlanerMap, +) +from ..op.texture_projection import ( + MUV_OT_TextureProjection, + MUV_OT_TextureProjection_Project, +) +from ..op.unwrap_constraint import MUV_OT_UnwrapConstraint +from ..utils.bl_class_registry import BlClassRegistry +from ..utils import compatibility as compat + + +@BlClassRegistry() +@compat.ChangeRegionType(region_type='TOOLS') +class MUV_PT_View3D_UVMapping(bpy.types.Panel): + """ + Panel class: UV Mapping on Property Panel on View3D + """ + + bl_space_type = 'VIEW_3D' + bl_region_type = 'UI' + 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=compat.icon('IMAGE')) + + def draw(self, context): + sc = context.scene + layout = self.layout + + box = layout.box() + box.prop(sc, "muv_unwrap_constraint_enabled", text="Unwrap Constraint") + if sc.muv_unwrap_constraint_enabled: + ops = box.operator(MUV_OT_UnwrapConstraint.bl_idname, + text="Unwrap") + ops.u_const = sc.muv_unwrap_constraint_u_const + ops.v_const = sc.muv_unwrap_constraint_v_const + row = box.row(align=True) + row.prop(sc, "muv_unwrap_constraint_u_const", text="U-Constraint") + row.prop(sc, "muv_unwrap_constraint_v_const", text="V-Constraint") + + box = layout.box() + box.prop(sc, "muv_texture_projection_enabled", + text="Texture Projection") + if sc.muv_texture_projection_enabled: + row = box.row() + row.prop( + sc, "muv_texture_projection_enable", + text="Disable" + if MUV_OT_TextureProjection.is_running(context) + else "Enable", + icon='RESTRICT_VIEW_OFF' + if MUV_OT_TextureProjection.is_running(context) + else 'RESTRICT_VIEW_ON') + row.prop(sc, "muv_texture_projection_tex_image", text="") + box.prop(sc, "muv_texture_projection_tex_transparency", + text="Transparency") + col = box.column(align=True) + row = col.row() + row.prop(sc, "muv_texture_projection_adjust_window", + text="Adjust Window") + if not sc.muv_texture_projection_adjust_window: + row.prop(sc, "muv_texture_projection_tex_magnitude", + text="Magnitude") + col.prop(sc, "muv_texture_projection_apply_tex_aspect", + text="Texture Aspect Ratio") + col.prop(sc, "muv_texture_projection_assign_uvmap", + text="Assign UVMap") + box.operator( + MUV_OT_TextureProjection_Project.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(MUV_OT_UVW_BoxMap.bl_idname, text="Box") + ops.assign_uvmap = sc.muv_uvw_assign_uvmap + ops = row.operator(MUV_OT_UVW_BestPlanerMap.bl_idname, + text="Best Planner") + ops.assign_uvmap = sc.muv_uvw_assign_uvmap + box.prop(sc, "muv_uvw_assign_uvmap", text="Assign UVMap") diff --git a/magic_uv/updater.py b/magic_uv/updater.py new file mode 100644 index 00000000..8a8da2ba --- /dev/null +++ b/magic_uv/updater.py @@ -0,0 +1,141 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "6.0" +__date__ = "26 Jan 2019" + +import os + +import bpy +from bpy.props import ( + StringProperty, +) + +from .utils.bl_class_registry import BlClassRegistry +from .utils.addon_updator import ( + AddonUpdatorManager, + AddonUpdatorConfig, + get_separator, +) +from .utils import compatibility as compat + + +@BlClassRegistry() +class MUV_OT_CheckAddonUpdate(bpy.types.Operator): + bl_idname = "uv.muv_ot_check_addon_update" + bl_label = "Check Update" + bl_description = "Check Add-on Update" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, _): + updater = AddonUpdatorManager.get_instance() + updater.check_update_candidate() + + return {'FINISHED'} + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_UpdateAddon(bpy.types.Operator): + bl_idname = "uv.muv_ot_update_addon" + bl_label = "Update" + bl_description = "Update Add-on" + bl_options = {'REGISTER', 'UNDO'} + + branch_name = StringProperty( + name="Branch Name", + description="Branch name to update", + default="", + ) + + def execute(self, _): + updater = AddonUpdatorManager.get_instance() + updater.update(self.branch_name) + + return {'FINISHED'} + + +def draw_updater_ui(prefs_obj): + layout = prefs_obj.layout + updater = AddonUpdatorManager.get_instance() + + layout.separator() + + if not updater.candidate_checked(): + col = layout.column() + col.scale_y = 2 + row = col.row() + row.operator(MUV_OT_CheckAddonUpdate.bl_idname, + text="Check 'Magic UV' add-on update", + icon='FILE_REFRESH') + else: + row = layout.row(align=True) + row.scale_y = 2 + col = row.column() + col.operator(MUV_OT_CheckAddonUpdate.bl_idname, + text="Check 'Magic UV' add-on update", + icon='FILE_REFRESH') + col = row.column() + if updater.latest_version() != "": + col.enabled = True + ops = col.operator( + MUV_OT_UpdateAddon.bl_idname, + text="Update to the latest release version (version: {})" + .format(updater.latest_version()), + icon='TRIA_DOWN_BAR') + ops.branch_name = updater.latest_version() + else: + col.enabled = False + col.operator(MUV_OT_UpdateAddon.bl_idname, + text="No updates are available.") + + layout.separator() + layout.label(text="Manual Update:") + row = layout.row(align=True) + row.prop(prefs_obj, "updater_branch_to_update", text="Target") + ops = row.operator( + MUV_OT_UpdateAddon.bl_idname, text="Update", + icon='TRIA_DOWN_BAR') + ops.branch_name = prefs_obj.updater_branch_to_update + + layout.separator() + if updater.has_error(): + box = layout.box() + box.label(text=updater.error(), icon='CANCEL') + elif updater.has_info(): + box = layout.box() + box.label(text=updater.info(), icon='ERROR') + + +def register_updater(bl_info): + config = AddonUpdatorConfig() + config.owner = "nutti" + config.repository = "Magic-UV" + config.current_addon_path = os.path.dirname(os.path.realpath(__file__)) + config.branches = ["master", "develop"] + config.addon_directory = \ + config.current_addon_path[ + :config.current_addon_path.rfind(get_separator())] + config.min_release_version = bl_info["version"] + config.target_addon_path = "src/magic_uv" + updater = AddonUpdatorManager.get_instance() + updater.init(bl_info, config) diff --git a/magic_uv/utils/__init__.py b/magic_uv/utils/__init__.py new file mode 100644 index 00000000..b74ab903 --- /dev/null +++ b/magic_uv/utils/__init__.py @@ -0,0 +1,38 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +if "bpy" in locals(): + import importlib + importlib.reload(addon_updator) + importlib.reload(bl_class_registry) + importlib.reload(compatibility) + importlib.reload(property_class_registry) +else: + from . import addon_updator + from . import bl_class_registry + from . import compatibility + from . import property_class_registry + +import bpy diff --git a/magic_uv/utils/addon_updator.py b/magic_uv/utils/addon_updator.py new file mode 100644 index 00000000..b2ff76cc --- /dev/null +++ b/magic_uv/utils/addon_updator.py @@ -0,0 +1,360 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +from threading import Lock +import urllib +import urllib.request +import ssl +import json +import os +import zipfile +import shutil +import datetime + + +def get_separator(): + if os.name == "nt": + return "\\" + return "/" + + +def _request(url, json_decode=True): + # pylint: disable=W0212 + ssl._create_default_https_context = ssl._create_unverified_context + req = urllib.request.Request(url) + + try: + result = urllib.request.urlopen(req) + except urllib.error.HTTPError as e: + raise RuntimeError("HTTP error ({})".format(str(e.code))) + except urllib.error.URLError as e: + raise RuntimeError("URL error ({})".format(str(e.reason))) + + data = result.read() + result.close() + + if json_decode: + try: + return json.JSONDecoder().decode(data.decode()) + except Exception as e: + raise RuntimeError("API response has invalid JSON format ({})" + .format(str(e.reason))) + + return data.decode() + + +def _download(url, path): + try: + urllib.request.urlretrieve(url, path) + except urllib.error.HTTPError as e: + raise RuntimeError("HTTP error ({})".format(str(e.code))) + except urllib.error.URLError as e: + raise RuntimeError("URL error ({})".format(str(e.reason))) + + +def _make_workspace_path(addon_dir): + return addon_dir + get_separator() + "addon_updator_workspace" + + +def _make_workspace(addon_dir): + dir_path = _make_workspace_path(addon_dir) + os.mkdir(dir_path) + + +def _make_temp_addon_path(addon_dir, url): + filename = url.split("/")[-1] + filepath = _make_workspace_path(addon_dir) + get_separator() + filename + return filepath + + +def _download_addon(addon_dir, url): + filepath = _make_temp_addon_path(addon_dir, url) + _download(url, filepath) + + +def _replace_addon(addon_dir, info, current_addon_path, offset_path=""): + # remove current add-on + if os.path.isfile(current_addon_path): + os.remove(current_addon_path) + elif os.path.isdir(current_addon_path): + shutil.rmtree(current_addon_path) + + # replace to the new add-on + workspace_path = _make_workspace_path(addon_dir) + tmp_addon_path = _make_temp_addon_path(addon_dir, info.url) + _, ext = os.path.splitext(tmp_addon_path) + if ext == ".zip": + with zipfile.ZipFile(tmp_addon_path) as zf: + zf.extractall(workspace_path) + if offset_path != "": + src = workspace_path + get_separator() + offset_path + dst = addon_dir + shutil.move(src, dst) + elif ext == ".py": + shutil.move(tmp_addon_path, addon_dir) + else: + raise RuntimeError("Unsupported file extension. (ext: {})".format(ext)) + + +def _get_all_releases_data(owner, repository): + url = "https://api.github.com/repos/{}/{}/releases"\ + .format(owner, repository) + data = _request(url) + + return data + + +def _get_all_branches_data(owner, repository): + url = "https://api.github.com/repos/{}/{}/branches"\ + .format(owner, repository) + data = _request(url) + + return data + + +def _parse_release_version(version): + return [int(c) for c in version[1:].split(".")] + + +# ver1 > ver2 : > 0 +# ver1 == ver2 : == 0 +# ver1 < ver2 : < 0 +def _compare_version(ver1, ver2): + if len(ver1) < len(ver2): + ver1.extend([-1 for _ in range(len(ver2) - len(ver1))]) + elif len(ver1) > len(ver2): + ver2.extend([-1 for _ in range(len(ver1) - len(ver2))]) + + def comp(v1, v2, idx): + if len(v1) == idx: + return 0 # v1 == v2 + + if v1[idx] > v2[idx]: + return 1 # v1 > v2 + elif v1[idx] < v2[idx]: + return -1 # v1 < v2 + + return comp(v1, v2, idx + 1) + + return comp(ver1, ver2, 0) + + +class AddonUpdatorConfig: + def __init__(self): + # Name of owner + self.owner = "" + + # Name of repository + self.repository = "" + + # Additional branch for update candidate + self.branches = [] + + # Set minimum release version for update candidate. + # e.g. (5, 2) if your release tag name is "v5.2" + # If you specify (-1, -1), ignore versions less than current add-on + # version specified in bl_info. + self.min_release_version = (-1, -1) + + # Target add-on path + self.target_addon_path = "" + + # Current add-on path + self.current_addon_path = "" + + # Blender add-on directory + self.addon_directory = "" + + +class UpdateCandidateInfo: + def __init__(self): + self.name = "" + self.url = "" + self.group = "" # BRANCH|RELEASE + + +class AddonUpdatorManager: + __inst = None + __lock = Lock() + + __initialized = False + __bl_info = None + __config = None + __update_candidate = [] + __candidate_checked = False + __error = "" + __info = "" + + def __init__(self): + raise NotImplementedError("Not allowed to call constructor") + + @classmethod + def __internal_new(cls): + return super().__new__(cls) + + @classmethod + def get_instance(cls): + if not cls.__inst: + with cls.__lock: + if not cls.__inst: + cls.__inst = cls.__internal_new() + + return cls.__inst + + def init(self, bl_info, config): + self.__bl_info = bl_info + self.__config = config + self.__update_candidate = [] + self.__candidate_checked = False + self.__error = "" + self.__info = "" + self.__initialized = True + + def initialized(self): + return self.__initialized + + def candidate_checked(self): + return self.__candidate_checked + + def check_update_candidate(self): + if not self.initialized(): + raise RuntimeError("AddonUpdatorManager must be initialized") + + self.__update_candidate = [] + self.__candidate_checked = False + + try: + # setup branch information + branches = _get_all_branches_data(self.__config.owner, + self.__config.repository) + for b in branches: + if b["name"] in self.__config.branches: + info = UpdateCandidateInfo() + info.name = b["name"] + info.url = "https://github.com/{}/{}/archive/{}.zip"\ + .format(self.__config.owner, + self.__config.repository, b["name"]) + info.group = 'BRANCH' + self.__update_candidate.append(info) + + # setup release information + releases = _get_all_releases_data(self.__config.owner, + self.__config.repository) + for r in releases: + if _compare_version(_parse_release_version(r["tag_name"]), + self.__config.min_release_version) > 0: + info = UpdateCandidateInfo() + info.name = r["tag_name"] + info.url = r["assets"][0]["browser_download_url"] + info.group = 'RELEASE' + self.__update_candidate.append(info) + except RuntimeError as e: + self.__error = "Failed to check update {}. ({})"\ + .format(str(e), datetime.datetime.now()) + + self.__info = "Checked update. ({})"\ + .format(datetime.datetime.now()) + + self.__candidate_checked = True + + def has_error(self): + return self.__error != "" + + def error(self): + return self.__error + + def has_info(self): + return self.__info != "" + + def info(self): + return self.__info + + def update(self, version_name): + if not self.initialized(): + raise RuntimeError("AddonUpdatorManager must be initialized.") + + if not self.candidate_checked(): + raise RuntimeError("Update candidate is not checked.") + + info = None + for info in self.__update_candidate: + if info.name == version_name: + break + else: + raise RuntimeError("{} is not found in update candidate" + .format(version_name)) + + if info is None: + raise RuntimeError("Not found any update candidates") + + try: + # create workspace + _make_workspace(self.__config.addon_directory) + # download add-on + _download_addon(self.__config.addon_directory, info.url) + + # replace add-on + offset_path = "" + if info.group == 'BRANCH': + offset_path = "{}-{}{}{}".format( + self.__config.repository, info.name, get_separator(), + self.__config.target_addon_path) + elif info.group == 'RELEASE': + offset_path = self.__config.target_addon_path + _replace_addon(self.__config.addon_directory, + info, self.__config.current_addon_path, + offset_path) + + self.__info = "Updated to {}. ({})" \ + .format(info.name, datetime.datetime.now()) + except RuntimeError as e: + self.__error = "Failed to update {}. ({})"\ + .format(str(e), datetime.datetime.now()) + + shutil.rmtree(_make_workspace_path(self.__config.addon_directory)) + + def get_candidate_branch_names(self): + if not self.initialized(): + raise RuntimeError("AddonUpdatorManager must be initialized.") + + if not self.candidate_checked(): + raise RuntimeError("Update candidate is not checked.") + + return [info.name for info in self.__update_candidate] + + def latest_version(self): + release_versions = [info.name + for info in self.__update_candidate + if info.group == 'RELEASE'] + + latest = "" + for version in release_versions: + if latest == "": + latest = version + elif _compare_version(_parse_release_version(version), + _parse_release_version(latest)) > 0: + latest = version + + return latest diff --git a/magic_uv/utils/bl_class_registry.py b/magic_uv/utils/bl_class_registry.py new file mode 100644 index 00000000..81e4b770 --- /dev/null +++ b/magic_uv/utils/bl_class_registry.py @@ -0,0 +1,80 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy + +from .. import common + + +class BlClassRegistry: + class_list = [] + + def __init__(self, *_, **kwargs): + self.legacy = kwargs.get('legacy', False) + + def __call__(self, cls): + if hasattr(cls, "bl_idname"): + BlClassRegistry.add_class(cls.bl_idname, cls, self.legacy) + else: + bl_idname = "{}{}{}{}".format(cls.bl_space_type, + cls.bl_region_type, + cls.bl_context, cls.bl_label) + BlClassRegistry.add_class(bl_idname, cls, self.legacy) + return cls + + @classmethod + def add_class(cls, bl_idname, op_class, legacy): + for class_ in cls.class_list: + if (class_["bl_idname"] == bl_idname) and \ + (class_["legacy"] == legacy): + raise RuntimeError("{} is already registered" + .format(bl_idname)) + + new_op = { + "bl_idname": bl_idname, + "class": op_class, + "legacy": legacy, + } + cls.class_list.append(new_op) + common.debug_print("{} is registered.".format(bl_idname)) + + @classmethod + def register(cls): + for class_ in cls.class_list: + bpy.utils.register_class(class_["class"]) + common.debug_print("{} is registered to Blender." + .format(class_["bl_idname"])) + + @classmethod + def unregister(cls): + for class_ in cls.class_list: + bpy.utils.unregister_class(class_["class"]) + common.debug_print("{} is unregistered from Blender." + .format(class_["bl_idname"])) + + @classmethod + def cleanup(cls): + cls.class_list = [] + common.debug_print("Cleanup registry.") diff --git a/magic_uv/utils/compatibility.py b/magic_uv/utils/compatibility.py new file mode 100644 index 00000000..ffa1d8b4 --- /dev/null +++ b/magic_uv/utils/compatibility.py @@ -0,0 +1,189 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +import bpy +import bgl +import blf + + +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 make_annotations(cls): + if check_version(2, 80, 0) < 0: + return cls + + # make annotation from attributes + props = {k: v for k, v in cls.__dict__.items() if isinstance(v, tuple)} + if props: + if '__annotations__' not in cls.__dict__: + setattr(cls, '__annotations__', {}) + annotations = cls.__dict__['__annotations__'] + for k, v in props.items(): + annotations[k] = v + delattr(cls, k) + + return cls + + +class ChangeRegionType: + def __init__(self, *_, **kwargs): + self.region_type = kwargs.get('region_type', False) + + def __call__(self, cls): + if check_version(2, 80, 0) >= 0: + return cls + + cls.bl_region_type = self.region_type + + return cls + + +def matmul(m1, m2): + if check_version(2, 80, 0) < 0: + return m1 * m2 + + return m1 @ m2 + + +def layout_split(layout, factor=0.0, align=False): + if check_version(2, 80, 0) < 0: + return layout.split(percentage=factor, align=align) + + return layout.split(factor=factor, align=align) + + +def get_user_preferences(context): + if hasattr(context, "user_preferences"): + return context.user_preferences + + return context.preferences + + +def get_object_select(obj): + if check_version(2, 80, 0) < 0: + return obj.select + + return obj.select_get() + + +def set_active_object(obj): + if check_version(2, 80, 0) < 0: + bpy.context.scene.objects.active = obj + else: + bpy.context.view_layer.objects.active = obj + + +def get_active_object(context): + if check_version(2, 80, 0) >= 0: + return context.scene.active_object + else: + return context.active_object + + +def object_has_uv_layers(obj): + if check_version(2, 80, 0) < 0: + return hasattr(obj.data, "uv_textures") + else: + return hasattr(obj.data, "uv_layers") + + +def get_object_uv_layers(obj): + if check_version(2, 80, 0) < 0: + return obj.data.uv_textures + else: + return obj.data.uv_layers + + +def icon(icon): + if icon == 'IMAGE': + if check_version(2, 80, 0) < 0: + return 'IMAGE_COL' + + return icon + + +def set_blf_font_color(font_id, r, g, b, a): + if check_version(2, 80, 0) >= 0: + blf.color(font_id, r, g, b, a) + else: + bgl.glColor4f(r, g, b, a) + + +def set_blf_blur(font_id, radius): + if check_version(2, 80, 0) < 0: + blf.blur(font_id, radius) + + +def get_all_space_types(): + if check_version(2, 80, 0) >= 0: + return { + 'CLIP_EDITOR': bpy.types.SpaceClipEditor, + 'CONSOLE': bpy.types.SpaceConsole, + 'DOPESHEET_EDITOR': bpy.types.SpaceDopeSheetEditor, + 'FILE_BROWSER': bpy.types.SpaceFileBrowser, + 'GRAPH_EDITOR': bpy.types.SpaceGraphEditor, + 'IMAGE_EDITOR': bpy.types.SpaceImageEditor, + 'INFO': bpy.types.SpaceInfo, + 'NLA_EDITOR': bpy.types.SpaceNLA, + 'NODE_EDITOR': bpy.types.SpaceNodeEditor, + 'OUTLINER': bpy.types.SpaceOutliner, + 'PROPERTIES': bpy.types.SpaceProperties, + 'SEQUENCE_EDITOR': bpy.types.SpaceSequenceEditor, + 'TEXT_EDITOR': bpy.types.SpaceTextEditor, + 'USER_PREFERENCES': bpy.types.SpacePreferences, + 'VIEW_3D': bpy.types.SpaceView3D, + } + else: + return { + 'VIEW_3D': bpy.types.SpaceView3D, + 'TIMELINE': bpy.types.SpaceTimeline, + 'GRAPH_EDITOR': bpy.types.SpaceGraphEditor, + 'DOPESHEET_EDITOR': bpy.types.SpaceDopeSheetEditor, + 'NLA_EDITOR': bpy.types.SpaceNLA, + 'IMAGE_EDITOR': bpy.types.SpaceImageEditor, + 'SEQUENCE_EDITOR': bpy.types.SpaceSequenceEditor, + 'CLIP_EDITOR': bpy.types.SpaceClipEditor, + 'TEXT_EDITOR': bpy.types.SpaceTextEditor, + 'NODE_EDITOR': bpy.types.SpaceNodeEditor, + 'LOGIC_EDITOR': bpy.types.SpaceLogicEditor, + 'PROPERTIES': bpy.types.SpaceProperties, + 'OUTLINER': bpy.types.SpaceOutliner, + 'USER_PREFERENCES': bpy.types.SpaceUserPreferences, + 'INFO': bpy.types.SpaceInfo, + 'FILE_BROWSER': bpy.types.SpaceFileBrowser, + 'CONSOLE': bpy.types.SpaceConsole, + } diff --git a/magic_uv/utils/property_class_registry.py b/magic_uv/utils/property_class_registry.py new file mode 100644 index 00000000..e99cd28b --- /dev/null +++ b/magic_uv/utils/property_class_registry.py @@ -0,0 +1,68 @@ +# <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__ = "6.0" +__date__ = "26 Jan 2019" + +from .. import common + + +class PropertyClassRegistry: + class_list = [] + + def __init__(self, *_, **kwargs): + self.legacy = kwargs.get('legacy', False) + + def __call__(self, cls): + PropertyClassRegistry.add_class(cls.idname, cls, self.legacy) + return cls + + @classmethod + def add_class(cls, idname, prop_class, legacy): + for class_ in cls.class_list: + if (class_["idname"] == idname) and (class_["legacy"] == legacy): + raise RuntimeError("{} is already registered".format(idname)) + + new_op = { + "idname": idname, + "class": prop_class, + "legacy": legacy, + } + cls.class_list.append(new_op) + common.debug_print("{} is registered.".format(idname)) + + @classmethod + def init_props(cls, scene): + for class_ in cls.class_list: + class_["class"].init_props(scene) + common.debug_print("{} is initialized.".format(class_["idname"])) + + @classmethod + def del_props(cls, scene): + for class_ in cls.class_list: + class_["class"].del_props(scene) + common.debug_print("{} is cleared.".format(class_["idname"])) + + @classmethod + def cleanup(cls): + cls.class_list = [] + common.debug_print("Cleanup registry.") |