diff options
Diffstat (limited to 'uv_magic_uv/impl')
-rw-r--r-- | uv_magic_uv/impl/__init__.py | 0 | ||||
-rw-r--r-- | uv_magic_uv/impl/copy_paste_uv_impl.py | 271 | ||||
-rw-r--r-- | uv_magic_uv/impl/copy_paste_uv_uvedit_impl.py | 166 | ||||
-rw-r--r-- | uv_magic_uv/impl/flip_rotate_impl.py | 133 | ||||
-rw-r--r-- | uv_magic_uv/impl/mirror_uv_impl.py | 158 | ||||
-rw-r--r-- | uv_magic_uv/impl/move_uv_impl.py | 166 | ||||
-rw-r--r-- | uv_magic_uv/impl/transfer_uv_impl.py | 330 | ||||
-rw-r--r-- | uv_magic_uv/impl/uvw_impl.py | 154 |
8 files changed, 1378 insertions, 0 deletions
diff --git a/uv_magic_uv/impl/__init__.py b/uv_magic_uv/impl/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/uv_magic_uv/impl/__init__.py diff --git a/uv_magic_uv/impl/copy_paste_uv_impl.py b/uv_magic_uv/impl/copy_paste_uv_impl.py new file mode 100644 index 00000000..ed44637b --- /dev/null +++ b/uv_magic_uv/impl/copy_paste_uv_impl.py @@ -0,0 +1,271 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "imdjs, Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + + +import bmesh + +from .. import common + + +__all__ = [ + 'is_valid_context', + 'get_copy_uv_layers', + 'get_paste_uv_layers', + 'get_src_face_info', + 'get_dest_face_info', + 'get_select_history_src_face_info', + 'get_select_history_dest_face_info', + 'paste_uv', +] + + +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 diff --git a/uv_magic_uv/impl/copy_paste_uv_uvedit_impl.py b/uv_magic_uv/impl/copy_paste_uv_uvedit_impl.py new file mode 100644 index 00000000..f14a70d6 --- /dev/null +++ b/uv_magic_uv/impl/copy_paste_uv_uvedit_impl.py @@ -0,0 +1,166 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "imdjs, Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import math +from math import atan2, sin, cos + +import bmesh +from mathutils import Vector + +from .. import common + + +__all__ = [ + 'is_valid_context', + 'CopyUVImpl', + 'PasteUVImpl', +] + + +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 + + +class CopyUVImpl: + @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'} + + +class PasteUVImpl: + @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/uv_magic_uv/impl/flip_rotate_impl.py b/uv_magic_uv/impl/flip_rotate_impl.py new file mode 100644 index 00000000..f74bc256 --- /dev/null +++ b/uv_magic_uv/impl/flip_rotate_impl.py @@ -0,0 +1,133 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "imdjs, Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + + +__all__ = [ + 'is_valid_context', + 'get_uv_layer', + 'get_src_face_info', +] + + +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 diff --git a/uv_magic_uv/impl/mirror_uv_impl.py b/uv_magic_uv/impl/mirror_uv_impl.py new file mode 100644 index 00000000..e79fbc2c --- /dev/null +++ b/uv_magic_uv/impl/mirror_uv_impl.py @@ -0,0 +1,158 @@ +# <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__ = "5.2" +__date__ = "17 Nov 2018" + +import bmesh +from mathutils import Vector + +from .. import common + + +__all__ = [ + 'is_valid_context', + 'is_vector_similar', + 'mirror_uvs', + 'get_face_center', + 'MirrorUVImpl', +] + + +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) + + +class MirrorUVImpl: + @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, ops_obj, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + + error = ops_obj.error + axis = ops_obj.axis + + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + if not bm.loops.layers.uv: + ops_obj.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, ops_obj.axis, ops_obj.error) + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/uv_magic_uv/impl/move_uv_impl.py b/uv_magic_uv/impl/move_uv_impl.py new file mode 100644 index 00000000..ce507fba --- /dev/null +++ b/uv_magic_uv/impl/move_uv_impl.py @@ -0,0 +1,166 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "imdjs, Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bmesh +from mathutils import Vector + +from .. import common + + +__all__ = [ + 'is_valid_context', + 'find_uv', + 'MoveUVImpl', +] + + +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 + + +class MoveUVImpl(): + __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 + if context.user_preferences.inputs.select_mouse == 'RIGHT': + confirm_btn = 'LEFTMOUSE' + cancel_btn = 'RIGHTMOUSE' + else: + confirm_btn = 'RIGHTMOUSE' + cancel_btn = 'LEFTMOUSE' + + # cancelled + if event.type == cancel_btn and event.value == 'PRESS': + for (fidx, vidx), uv in zip(self.__topology_dict, self.__ini_uvs): + bm.faces[fidx].loops[vidx][active_uv].uv = uv + MoveUVImpl.__running = False + return {'FINISHED'} + # confirmed + if event.type == confirm_btn and event.value == 'PRESS': + MoveUVImpl.__running = False + return {'FINISHED'} + + return {'RUNNING_MODAL'} + + def execute(self, ops_obj, context): + MoveUVImpl.__running = True + self.__operating = False + self.__first_time = True + + context.window_manager.modal_handler_add(ops_obj) + self.__topology_dict, self.__ini_uvs = find_uv(context) + + if context.area: + context.area.tag_redraw() + + return {'RUNNING_MODAL'} diff --git a/uv_magic_uv/impl/transfer_uv_impl.py b/uv_magic_uv/impl/transfer_uv_impl.py new file mode 100644 index 00000000..adc97352 --- /dev/null +++ b/uv_magic_uv/impl/transfer_uv_impl.py @@ -0,0 +1,330 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "imdjs, Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +from collections import OrderedDict + +import bpy +import bmesh + +from .. import common + + +__all__ = [ + 'is_valid_context', + 'get_uv_layer', + 'main_parse', + 'parse_faces', + 'get_new_shared_faces', + 'get_other_verts_edges', + 'get_selected_src_faces', + 'paste_uv', +] + + +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 diff --git a/uv_magic_uv/impl/uvw_impl.py b/uv_magic_uv/impl/uvw_impl.py new file mode 100644 index 00000000..e815f54f --- /dev/null +++ b/uv_magic_uv/impl/uvw_impl.py @@ -0,0 +1,154 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "imdjs, Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + + +from math import sin, cos, pi + +from mathutils import Vector + + +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 = 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)) |