diff options
Diffstat (limited to 'uv_magic_uv/legacy/op')
23 files changed, 7635 insertions, 0 deletions
diff --git a/uv_magic_uv/legacy/op/__init__.py b/uv_magic_uv/legacy/op/__init__.py new file mode 100644 index 00000000..9535b76d --- /dev/null +++ b/uv_magic_uv/legacy/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__ = "5.2" +__date__ = "17 Nov 2018" + +if "bpy" in locals(): + import importlib + importlib.reload(align_uv) + importlib.reload(align_uv_cursor) + importlib.reload(copy_paste_uv) + importlib.reload(copy_paste_uv_object) + importlib.reload(copy_paste_uv_uvedit) + importlib.reload(flip_rotate_uv) + importlib.reload(mirror_uv) + importlib.reload(move_uv) + importlib.reload(pack_uv) + importlib.reload(preserve_uv_aspect) + importlib.reload(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/uv_magic_uv/legacy/op/align_uv.py b/uv_magic_uv/legacy/op/align_uv.py new file mode 100644 index 00000000..9d0ff5f4 --- /dev/null +++ b/uv_magic_uv/legacy/op/align_uv.py @@ -0,0 +1,988 @@ +# <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, tan, sin, cos + +import bpy +import bmesh +from mathutils import Vector +from bpy.props import EnumProperty, BoolProperty, FloatProperty + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_AlignUV_Circle', + 'MUV_OT_AlignUV_Straighten', + 'MUV_OT_AlignUV_Axis', +] + + +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(legacy=True) +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 + + +# 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 + + +@BlClassRegistry(legacy=True) +class MUV_OT_AlignUV_Circle(bpy.types.Operator): + + bl_idname = "uv.muv_align_uv_operator_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'} + + +# 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 accum_uvlens[i + 1] != target_length: + 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) / len(loop_seqs) + 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 accum_uvlens[i + 1] != target_length: + 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) + + +@BlClassRegistry(legacy=True) +class MUV_OT_AlignUV_Straighten(bpy.types.Operator): + + bl_idname = "uv.muv_align_uv_operator_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(legacy=True) +class MUV_OT_AlignUV_Axis(bpy.types.Operator): + + bl_idname = "uv.muv_align_uv_operator_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/uv_magic_uv/legacy/op/align_uv_cursor.py b/uv_magic_uv/legacy/op/align_uv_cursor.py new file mode 100644 index 00000000..ec3e7036 --- /dev/null +++ b/uv_magic_uv/legacy/op/align_uv_cursor.py @@ -0,0 +1,257 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +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 + + +__all__ = [ + 'Properties', + 'MUV_OT_AlignUVCursor', +] + + +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(legacy=True) +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') + bd_size = common.get_uvimg_editor_board_size(area) + loc = space.cursor_location + if bd_size[0] < 0.000001: + cx = 0.0 + else: + cx = loc[0] / bd_size[0] + if bd_size[1] < 0.000001: + cy = 0.0 + else: + cy = loc[1] / bd_size[1] + self['muv_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') + bd_size = common.get_uvimg_editor_board_size(area) + cx = bd_size[0] * value[0] + cy = bd_size[1] * value[1] + space.cursor_location = Vector((cx, cy)) + + scene.muv_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(legacy=True) +class MUV_OT_AlignUVCursor(bpy.types.Operator): + + bl_idname = "uv.muv_align_uv_cursor_operator" + 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') + bd_size = common.get_uvimg_editor_board_size(area) + + if self.base == 'UV': + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if not bm.loops.layers.uv: + return None + uv_layer = bm.loops.layers.uv.verify() + + max_ = Vector((-10000000.0, -10000000.0)) + min_ = Vector((10000000.0, 10000000.0)) + for f in bm.faces: + if not f.select: + continue + for l in f.loops: + uv = l[uv_layer].uv + max_.x = max(max_.x, uv.x) + max_.y = max(max_.y, uv.y) + min_.x = min(min_.x, uv.x) + min_.y = min(min_.y, uv.y) + center = Vector(((max_.x + min_.x) / 2.0, (max_.y + min_.y) / 2.0)) + + elif self.base == 'UV_SEL': + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if not bm.loops.layers.uv: + return None + uv_layer = bm.loops.layers.uv.verify() + + max_ = Vector((-10000000.0, -10000000.0)) + min_ = Vector((10000000.0, 10000000.0)) + for f in bm.faces: + if not f.select: + continue + for l in f.loops: + if not l[uv_layer].select: + continue + uv = l[uv_layer].uv + max_.x = max(max_.x, uv.x) + max_.y = max(max_.y, uv.y) + min_.x = min(min_.x, uv.x) + min_.y = min(min_.y, uv.y) + center = Vector(((max_.x + min_.x) / 2.0, (max_.y + min_.y) / 2.0)) + + elif self.base == 'TEXTURE': + min_ = Vector((0.0, 0.0)) + max_ = Vector((1.0, 1.0)) + center = Vector((0.5, 0.5)) + else: + self.report({'ERROR'}, "Unknown Operation") + + if self.position == 'CENTER': + cx = center.x * bd_size[0] + cy = center.y * bd_size[1] + elif self.position == 'LEFT_TOP': + cx = min_.x * bd_size[0] + cy = max_.y * bd_size[1] + elif self.position == 'LEFT_MIDDLE': + cx = min_.x * bd_size[0] + cy = center.y * bd_size[1] + elif self.position == 'LEFT_BOTTOM': + cx = min_.x * bd_size[0] + cy = min_.y * bd_size[1] + elif self.position == 'MIDDLE_TOP': + cx = center.x * bd_size[0] + cy = max_.y * bd_size[1] + elif self.position == 'MIDDLE_BOTTOM': + cx = center.x * bd_size[0] + cy = min_.y * bd_size[1] + elif self.position == 'RIGHT_TOP': + cx = max_.x * bd_size[0] + cy = max_.y * bd_size[1] + elif self.position == 'RIGHT_MIDDLE': + cx = max_.x * bd_size[0] + cy = center.y * bd_size[1] + elif self.position == 'RIGHT_BOTTOM': + cx = max_.x * bd_size[0] + cy = min_.y * bd_size[1] + else: + self.report({'ERROR'}, "Unknown Operation") + + space.cursor_location = Vector((cx, cy)) + + return {'FINISHED'} diff --git a/uv_magic_uv/legacy/op/copy_paste_uv.py b/uv_magic_uv/legacy/op/copy_paste_uv.py new file mode 100644 index 00000000..a8aef017 --- /dev/null +++ b/uv_magic_uv/legacy/op/copy_paste_uv.py @@ -0,0 +1,531 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>, Jace Priester" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + + +import bmesh +import bpy.utils +from bpy.props import ( + StringProperty, + BoolProperty, + IntProperty, + EnumProperty, +) + +from ...impl import copy_paste_uv_impl as impl +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + +__all__ = [ + 'Properties', + 'MUV_OT_CopyPasteUV_CopyUV', + 'MUV_MT_CopyPasteUV_CopyUV', + 'MUV_OT_CopyPasteUV_PasteUV', + 'MUV_MT_CopyPasteUV_PasteUV', + 'MUV_OT_CopyPasteUV_SelSeqCopyUV', + 'MUV_MT_CopyPasteUV_SelSeqCopyUV', + 'MUV_OT_CopyPasteUV_SelSeqPasteUV', + 'MUV_MT_CopyPasteUV_SelSeqPasteUV', +] + + +@PropertyClassRegistry(legacy=True) +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(legacy=True) +class MUV_OT_CopyPasteUV_CopyUV(bpy.types.Operator): + """ + Operation class: Copy UV coordinate + """ + + bl_idname = "uv.muv_copy_paste_uv_operator_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 impl.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 = impl.get_copy_uv_layers(self, bm, self.uv_map) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + src_info = impl.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(legacy=True) +class MUV_MT_CopyPasteUV_CopyUV(bpy.types.Menu): + """ + Menu class: Copy UV coordinate + """ + + bl_idname = "uv.muv_copy_paste_uv_menu_copy_uv" + bl_label = "Copy UV (Menu)" + bl_description = "Menu of Copy UV coordinate" + + @classmethod + def poll(cls, context): + return impl.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(legacy=True) +class MUV_OT_CopyPasteUV_PasteUV(bpy.types.Operator): + """ + Operation class: Paste UV coordinate + """ + + bl_idname = "uv.muv_copy_paste_uv_operator_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 impl.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 = impl.get_paste_uv_layers(self, obj, bm, props.src_info, + self.uv_map) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + dest_info = impl.get_dest_face_info(self, bm, uv_layers, + props.src_info, self.strategy, + True) + if dest_info is None: + return {'CANCELLED'} + + # paste + ret = impl.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 self.copy_seams is True: + obj.data.show_edge_seams = True + + return {'FINISHED'} + + +@BlClassRegistry(legacy=True) +class MUV_MT_CopyPasteUV_PasteUV(bpy.types.Menu): + """ + Menu class: Paste UV coordinate + """ + + bl_idname = "uv.muv_copy_paste_uv_menu_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 impl.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(legacy=True) +class MUV_OT_CopyPasteUV_SelSeqCopyUV(bpy.types.Operator): + """ + Operation class: Copy UV coordinate by selection sequence + """ + + bl_idname = "uv.muv_copy_paste_uv_operator_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 impl.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 = impl.get_copy_uv_layers(self, bm, self.uv_map) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + src_info = impl.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(legacy=True) +class MUV_MT_CopyPasteUV_SelSeqCopyUV(bpy.types.Menu): + """ + Menu class: Copy UV coordinate by selection sequence + """ + + bl_idname = "uv.muv_copy_paste_uv_menu_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 impl.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(legacy=True) +class MUV_OT_CopyPasteUV_SelSeqPasteUV(bpy.types.Operator): + """ + Operation class: Paste UV coordinate by selection sequence + """ + + bl_idname = "uv.muv_copy_paste_uv_operator_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 impl.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 = impl.get_paste_uv_layers(self, obj, bm, props.src_info, + self.uv_map) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + dest_info = impl.get_select_history_dest_face_info(self, bm, uv_layers, + props.src_info, + self.strategy) + if dest_info is None: + return {'CANCELLED'} + + # paste + ret = impl.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 self.copy_seams is True: + obj.data.show_edge_seams = True + + return {'FINISHED'} + + +@BlClassRegistry(legacy=True) +class MUV_MT_CopyPasteUV_SelSeqPasteUV(bpy.types.Menu): + """ + Menu class: Paste UV coordinate by selection sequence + """ + + bl_idname = "uv.muv_copy_paste_uv_menu_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 impl.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/uv_magic_uv/legacy/op/copy_paste_uv_object.py b/uv_magic_uv/legacy/op/copy_paste_uv_object.py new file mode 100644 index 00000000..e09b003b --- /dev/null +++ b/uv_magic_uv/legacy/op/copy_paste_uv_object.py @@ -0,0 +1,298 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bmesh +import bpy +from bpy.props import ( + StringProperty, + BoolProperty, +) + +from ...impl import copy_paste_uv_impl as impl +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + +__all__ = [ + 'Properties', + 'MUV_OT_CopyPasteUVObject_CopyUV', + 'MUV_MT_CopyPasteUVObject_CopyUV', + 'MUV_OT_CopyPasteUVObject_PasteUV', + 'MUV_MT_CopyPasteUVObject_PasteUV', +] + + +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(legacy=True) +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(legacy=True) +class MUV_OT_CopyPasteUVObject_CopyUV(bpy.types.Operator): + """ + Operation class: Copy UV coordinate among objects + """ + + bl_idname = "object.muv_copy_paste_uv_object_operator_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 = impl.get_copy_uv_layers(self, bm, self.uv_map) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + src_info = impl.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(legacy=True) +class MUV_MT_CopyPasteUVObject_CopyUV(bpy.types.Menu): + """ + Menu class: Copy UV coordinate among objects + """ + + bl_idname = "object.muv_copy_paste_uv_object_menu_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 = bpy.context.active_object.data.uv_textures.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(legacy=True) +class MUV_OT_CopyPasteUVObject_PasteUV(bpy.types.Operator): + """ + Operation class: Paste UV coordinate among objects + """ + + bl_idname = "object.muv_copy_paste_uv_object_operator_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 hasattr(o.data, "uv_textures") or not o.select: + continue + + bpy.ops.object.mode_set(mode='OBJECT') + bpy.context.scene.objects.active = o + bpy.ops.object.mode_set(mode='EDIT') + + obj = context.active_object + bm = common.create_bmesh(obj) + + # get UV layer + uv_layers = impl.get_paste_uv_layers(self, obj, bm, props.src_info, + self.uv_map) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + dest_info = impl.get_dest_face_info(self, bm, uv_layers, + props.src_info, 'N_N') + if dest_info is None: + return {'CANCELLED'} + + # paste + ret = impl.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 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(legacy=True) +class MUV_MT_CopyPasteUVObject_PasteUV(bpy.types.Menu): + """ + Menu class: Paste UV coordinate among objects + """ + + bl_idname = "object.muv_copy_paste_uv_object_menu_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 hasattr(obj.data, "uv_textures") and obj.select: + uv_maps.extend(obj.data.uv_textures.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/uv_magic_uv/legacy/op/copy_paste_uv_uvedit.py b/uv_magic_uv/legacy/op/copy_paste_uv_uvedit.py new file mode 100644 index 00000000..bb72d42a --- /dev/null +++ b/uv_magic_uv/legacy/op/copy_paste_uv_uvedit.py @@ -0,0 +1,97 @@ +# <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 bpy + +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry +from ...impl import copy_paste_uv_uvedit_impl as impl + + +__all__ = [ + 'Properties', + 'MUV_OT_CopyPasteUVUVEdit_CopyUV', + 'MUV_OT_CopyPasteUVUVEdit_PasteUV', +] + + +@PropertyClassRegistry(legacy=True) +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(legacy=True) +class MUV_OT_CopyPasteUVUVEdit_CopyUV(bpy.types.Operator): + """ + Operation class: Copy UV coordinate on UV/Image Editor + """ + + bl_idname = "uv.muv_copy_paste_uv_uvedit_operator_copy_uv" + bl_label = "Copy UV (UV/Image Editor)" + bl_description = "Copy UV coordinate (only selected in UV/Image Editor)" + bl_options = {'REGISTER', 'UNDO'} + + def __init__(self): + self.__impl = impl.CopyUVImpl() + + @classmethod + def poll(cls, context): + return impl.CopyUVImpl.poll(context) + + def execute(self, context): + return self.__impl.execute(self, context) + + +@BlClassRegistry(legacy=True) +class MUV_OT_CopyPasteUVUVEdit_PasteUV(bpy.types.Operator): + """ + Operation class: Paste UV coordinate on UV/Image Editor + """ + + bl_idname = "uv.muv_copy_paste_uv_uvedit_operator_paste_uv" + bl_label = "Paste UV (UV/Image Editor)" + bl_description = "Paste UV coordinate (only selected in UV/Image Editor)" + bl_options = {'REGISTER', 'UNDO'} + + def __init__(self): + self.__impl = impl.PasteUVImpl() + + @classmethod + def poll(cls, context): + return impl.PasteUVImpl.poll(context) + + def execute(self, context): + return self.__impl.execute(self, context) diff --git a/uv_magic_uv/legacy/op/flip_rotate_uv.py b/uv_magic_uv/legacy/op/flip_rotate_uv.py new file mode 100644 index 00000000..d94e4808 --- /dev/null +++ b/uv_magic_uv/legacy/op/flip_rotate_uv.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__ = "5.2" +__date__ = "17 Nov 2018" + +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 ...impl import flip_rotate_impl as impl + +__all__ = [ + 'Properties', + 'MUV_OT_FlipRotate', +] + + +@PropertyClassRegistry(legacy=True) +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(legacy=True) +class MUV_OT_FlipRotate(bpy.types.Operator): + """ + Operation class: Flip and Rotate UV coordinate + """ + + bl_idname = "uv.muv_flip_rotate_uv_operator" + 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 impl.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 = impl.get_uv_layer(self, bm) + if not uv_layer: + return {'CANCELLED'} + + # get selected face + src_info = impl.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 = impl.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 self.seams is True: + obj.data.show_edge_seams = True + + return {'FINISHED'} diff --git a/uv_magic_uv/legacy/op/mirror_uv.py b/uv_magic_uv/legacy/op/mirror_uv.py new file mode 100644 index 00000000..e869e5e8 --- /dev/null +++ b/uv_magic_uv/legacy/op/mirror_uv.py @@ -0,0 +1,110 @@ +# <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 bpy +from bpy.props import ( + EnumProperty, + FloatProperty, + BoolProperty, +) + +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry +from ...impl import mirror_uv_impl as impl + + +__all__ = [ + 'Properties', + 'MUV_OT_MirrorUV', +] + + +@PropertyClassRegistry(legacy=True) +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(legacy=True) +class MUV_OT_MirrorUV(bpy.types.Operator): + """ + Operation class: Mirror UV + """ + + bl_idname = "uv.muv_mirror_uv_operator" + bl_label = "Mirror UV" + bl_options = {'REGISTER', 'UNDO'} + + axis = EnumProperty( + items=( + ('X', "X", "Mirror Along X axis"), + ('Y', "Y", "Mirror Along Y axis"), + ('Z', "Z", "Mirror Along Z axis") + ), + name="Axis", + description="Mirror Axis", + default='X' + ) + error = FloatProperty( + name="Error", + description="Error threshold", + default=0.001, + min=0.0, + max=100.0, + soft_min=0.0, + soft_max=1.0 + ) + + def __init__(self): + self.__impl = impl.MirrorUVImpl() + + @classmethod + def poll(cls, context): + return impl.MirrorUVImpl.poll(context) + + def execute(self, context): + return self.__impl.execute(self, context) diff --git a/uv_magic_uv/legacy/op/move_uv.py b/uv_magic_uv/legacy/op/move_uv.py new file mode 100644 index 00000000..2988c2ce --- /dev/null +++ b/uv_magic_uv/legacy/op/move_uv.py @@ -0,0 +1,82 @@ +# <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__ = "5.2" +__date__ = "17 Nov 2018" + +import bpy +from bpy.props import BoolProperty + +from ...impl import move_uv_impl as impl +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_MoveUV', +] + + +@PropertyClassRegistry(legacy=True) +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(legacy=True) +class MUV_OT_MoveUV(bpy.types.Operator): + """ + Operator class: Move UV + """ + + bl_idname = "uv.muv_move_uv_operator" + bl_label = "Move UV" + bl_options = {'REGISTER', 'UNDO'} + + def __init__(self): + self.__impl = impl.MoveUVImpl() + + @classmethod + def poll(cls, context): + return impl.MoveUVImpl.poll(context) + + @classmethod + def is_running(cls, _): + return impl.MoveUVImpl.is_running(_) + + def modal(self, context, event): + return self.__impl.modal(self, context, event) + + def execute(self, context): + return self.__impl.execute(self, context) diff --git a/uv_magic_uv/legacy/op/pack_uv.py b/uv_magic_uv/legacy/op/pack_uv.py new file mode 100644 index 00000000..f8d58843 --- /dev/null +++ b/uv_magic_uv/legacy/op/pack_uv.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__ = "5.2" +__date__ = "17 Nov 2018" + +from math import fabs + +import bpy +import bmesh +import mathutils +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 + + +__all__ = [ + 'Properties', + 'MUV_OT_PackUV', +] + + +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(legacy=True) +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(legacy=True) +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_pack_uv_operator" + 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 = self.__group_island(island_info) + + loop_lists = [l for f in bm.faces for l in f.loops] + bpy.ops.mesh.select_all(action='DESELECT') + + # pack UV + for gidx in range(num_group): + group = list(filter( + lambda i, idx=gidx: i['group'] == idx, island_info)) + for f in group[0]['faces']: + f['face'].select = True + bmesh.update_edit_mesh(obj.data) + bpy.ops.uv.select_all(action='SELECT') + bpy.ops.uv.pack_islands(rotate=self.rotate, margin=self.margin) + + # copy/paste UV among same islands + for gidx in range(num_group): + group = list(filter( + lambda i, idx=gidx: i['group'] == idx, island_info)) + if len(group) <= 1: + continue + for g in group[1:]: + for (src_face, dest_face) in zip( + group[0]['sorted'], g['sorted']): + for (src_loop, dest_loop) in zip( + src_face['face'].loops, dest_face['face'].loops): + loop_lists[dest_loop.index][uv_layer].uv = loop_lists[ + src_loop.index][uv_layer].uv + + # restore face/UV selection + bpy.ops.uv.select_all(action='DESELECT') + bpy.ops.mesh.select_all(action='DESELECT') + for f in selected_faces: + f.select = True + bpy.ops.uv.select_all(action='SELECT') + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} + + def __sort_island_faces(self, kd, uvs, isl1, isl2): + """ + Sort faces in island + """ + + sorted_faces = [] + for f in isl1['sorted']: + _, idx, _ = kd.find( + Vector((f['ave_uv'].x, f['ave_uv'].y, 0.0))) + sorted_faces.append(isl2['faces'][uvs[idx]['face_idx']]) + return sorted_faces + + def __group_island(self, island_info): + """ + Group island + """ + + num_group = 0 + while True: + # search islands which is not parsed yet + isl_1 = None + for isl_1 in island_info: + if isl_1['group'] == -1: + break + else: + break # all faces are parsed + if isl_1 is None: + break + isl_1['group'] = num_group + isl_1['sorted'] = isl_1['faces'] + + # search same island + for isl_2 in island_info: + if isl_2['group'] == -1: + dcx = isl_2['center'].x - isl_1['center'].x + dcy = isl_2['center'].y - isl_1['center'].y + dsx = isl_2['size'].x - isl_1['size'].x + dsy = isl_2['size'].y - isl_1['size'].y + center_x_matched = ( + fabs(dcx) < self.allowable_center_deviation[0] + ) + center_y_matched = ( + fabs(dcy) < self.allowable_center_deviation[1] + ) + size_x_matched = ( + fabs(dsx) < self.allowable_size_deviation[0] + ) + size_y_matched = ( + fabs(dsy) < self.allowable_size_deviation[1] + ) + center_matched = center_x_matched and center_y_matched + size_matched = size_x_matched and size_y_matched + num_uv_matched = (isl_2['num_uv'] == isl_1['num_uv']) + # are islands have same? + if center_matched and size_matched and num_uv_matched: + isl_2['group'] = num_group + kd = mathutils.kdtree.KDTree(len(isl_2['faces'])) + uvs = [ + { + 'uv': Vector( + (f['ave_uv'].x, f['ave_uv'].y, 0.0) + ), + 'face_idx': fidx + } for fidx, f in enumerate(isl_2['faces']) + ] + for i, uv in enumerate(uvs): + kd.insert(uv['uv'], i) + kd.balance() + # sort faces for copy/paste UV + isl_2['sorted'] = self.__sort_island_faces( + kd, uvs, isl_1, isl_2) + num_group = num_group + 1 + + return num_group diff --git a/uv_magic_uv/legacy/op/preserve_uv_aspect.py b/uv_magic_uv/legacy/op/preserve_uv_aspect.py new file mode 100644 index 00000000..cf9349bc --- /dev/null +++ b/uv_magic_uv/legacy/op/preserve_uv_aspect.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__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bpy +import bmesh +from bpy.props import StringProperty, EnumProperty, BoolProperty +from mathutils import Vector + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_PreserveUVAspect', +] + + +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(legacy=True) +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(legacy=True) +class MUV_OT_PreserveUVAspect(bpy.types.Operator): + """ + Operation class: Preserve UV Aspect + """ + + bl_idname = "uv.muv_preserve_uv_aspect_operator" + 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() + tex_layer = bm.faces.layers.tex.verify() + + sel_faces = [f for f in bm.faces if f.select] + dest_img = bpy.data.images[self.dest_img_name] + + info = {} + + for f in sel_faces: + if not f[tex_layer].image in info.keys(): + info[f[tex_layer].image] = {} + info[f[tex_layer].image]['faces'] = [] + info[f[tex_layer].image]['faces'].append(f) + + for img in info: + 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) + + info[img]['ratio'] = ratio + info[img]['origin'] = origin + + for img in info: + if img is None: + continue + + for f in info[img]['faces']: + 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/uv_magic_uv/legacy/op/select_uv.py b/uv_magic_uv/legacy/op/select_uv.py new file mode 100644 index 00000000..bdc182d5 --- /dev/null +++ b/uv_magic_uv/legacy/op/select_uv.py @@ -0,0 +1,168 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +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 + + +__all__ = [ + 'Properties', + 'MUV_OT_SelectUV_SelectFlipped', + 'MUV_OT_SelectUV_SelectOverlapped', +] + + +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(legacy=True) +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(legacy=True) +class MUV_OT_SelectUV_SelectOverlapped(bpy.types.Operator): + """ + Operation class: Select faces which have overlapped UVs + """ + + bl_idname = "uv.muv_select_uv_operator_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(legacy=True) +class MUV_OT_SelectUV_SelectFlipped(bpy.types.Operator): + """ + Operation class: Select faces which have flipped UVs + """ + + bl_idname = "uv.muv_select_uv_operator_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/uv_magic_uv/legacy/op/smooth_uv.py b/uv_magic_uv/legacy/op/smooth_uv.py new file mode 100644 index 00000000..63062554 --- /dev/null +++ b/uv_magic_uv/legacy/op/smooth_uv.py @@ -0,0 +1,287 @@ +# <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 bpy +import bmesh +from bpy.props import BoolProperty, FloatProperty + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_SmoothUV', +] + + +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(legacy=True) +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(legacy=True) +class MUV_OT_SmoothUV(bpy.types.Operator): + + bl_idname = "uv.muv_smooth_uv_operator" + 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/uv_magic_uv/legacy/op/texture_lock.py b/uv_magic_uv/legacy/op/texture_lock.py new file mode 100644 index 00000000..65873106 --- /dev/null +++ b/uv_magic_uv/legacy/op/texture_lock.py @@ -0,0 +1,545 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import math +from math import atan2, cos, sqrt, sin, fabs + +import bpy +import bmesh +from mathutils import Vector +from bpy.props import BoolProperty + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_TextureLock_Lock', + 'MUV_OT_TextureLock_Unlock', + 'MUV_OT_TextureLock_Intr', +] + + +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(legacy=True) +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_texture_lock_operator_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(legacy=True) +class MUV_OT_TextureLock_Lock(bpy.types.Operator): + """ + Operation class: Lock Texture + """ + + bl_idname = "uv.muv_texture_lock_operator_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(legacy=True) +class MUV_OT_TextureLock_Unlock(bpy.types.Operator): + """ + Operation class: Unlock Texture + """ + + bl_idname = "uv.muv_texture_lock_operator_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(legacy=True) +class MUV_OT_TextureLock_Intr(bpy.types.Operator): + """ + Operation class: Texture Lock (Interactive mode) + """ + + bl_idname = "uv.muv_texture_lock_operator_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, self_, context): + if cls.__timer is None: + cls.__timer = context.window_manager.event_timer_add( + 0.10, context.window) + context.window_manager.modal_handler_add(self_) + + @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/uv_magic_uv/legacy/op/texture_projection.py b/uv_magic_uv/legacy/op/texture_projection.py new file mode 100644 index 00000000..58f69c9d --- /dev/null +++ b/uv_magic_uv/legacy/op/texture_projection.py @@ -0,0 +1,402 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +from collections import namedtuple + +import bpy +import bgl +import bmesh +import mathutils +from bpy_extras import view3d_utils +from bpy.props import ( + BoolProperty, + EnumProperty, + FloatProperty, +) + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_TextureProjection', + 'MUV_OT_TextureProjection_Project', +] + + +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 + prefs = context.user_preferences.addons["uv_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(legacy=True) +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_texture_projection_operator('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(legacy=True) +class MUV_OT_TextureProjection(bpy.types.Operator): + """ + Operation class: Texture Projection + Render texture + """ + + bl_idname = "uv.muv_texture_projection_operator" + 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 + 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(legacy=True) +class MUV_OT_TextureProjection_Project(bpy.types.Operator): + """ + Operation class: Project texture + """ + + bl_idname = "uv.muv_texture_projection_operator_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() + tex_layer = bm.faces.layers.tex.verify() + + sel_faces = [f for f in bm.faces if f.select] + + # transform 3d space to screen region + v_screen = [ + view3d_utils.location_3d_to_region_2d( + region, + space.region_3d, + world_mat * l.vert.co) + for f in sel_faces for l in f.loops + ] + + # transform screen region to canvas + v_canvas = [ + region_to_canvas( + v, + get_canvas(bpy.context, + sc.muv_texture_projection_tex_magnitude) + ) for v in v_screen + ] + + # project texture to object + i = 0 + for f in sel_faces: + f[tex_layer].image = \ + bpy.data.images[sc.muv_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/uv_magic_uv/legacy/op/texture_wrap.py b/uv_magic_uv/legacy/op/texture_wrap.py new file mode 100644 index 00000000..cb4cc78c --- /dev/null +++ b/uv_magic_uv/legacy/op/texture_wrap.py @@ -0,0 +1,301 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +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 + + +__all__ = [ + 'Properties', + 'MUV_OT_TextureWrap_Refer', + 'MUV_OT_TextureWrap_Set', +] + + +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(legacy=True) +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(legacy=True) +class MUV_OT_TextureWrap_Refer(bpy.types.Operator): + """ + Operation class: Refer UV + """ + + bl_idname = "uv.muv_texture_wrap_operator_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(legacy=True) +class MUV_OT_TextureWrap_Set(bpy.types.Operator): + """ + Operation class: Set UV + """ + + bl_idname = "uv.muv_texture_wrap_operator_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/uv_magic_uv/legacy/op/transfer_uv.py b/uv_magic_uv/legacy/op/transfer_uv.py new file mode 100644 index 00000000..cd0e4dd9 --- /dev/null +++ b/uv_magic_uv/legacy/op/transfer_uv.py @@ -0,0 +1,172 @@ +# <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__ = "5.2" +__date__ = "17 Nov 2018" + +import bpy +import bmesh +from bpy.props import BoolProperty + +from ... import common +from ...impl import transfer_uv_impl as impl +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_TransferUV_CopyUV', + 'MUV_OT_TransferUV_PasteUV', +] + + +@PropertyClassRegistry(legacy=True) +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(legacy=True) +class MUV_OT_TransferUV_CopyUV(bpy.types.Operator): + """ + Operation class: Transfer UV copy + Topological based copy + """ + + bl_idname = "uv.muv_transfer_uv_operator_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 impl.is_valid_context(context) + + def execute(self, context): + props = context.scene.muv_props.transfer_uv + active_obj = context.scene.objects.active + bm = bmesh.from_edit_mesh(active_obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + uv_layer = impl.get_uv_layer(self, bm) + if uv_layer is None: + return {'CANCELLED'} + + faces = impl.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(legacy=True) +class MUV_OT_TransferUV_PasteUV(bpy.types.Operator): + """ + Operation class: Transfer UV paste + Topological based paste + """ + + bl_idname = "uv.muv_transfer_uv_operator_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 impl.is_valid_context(context) + + def execute(self, context): + props = context.scene.muv_props.transfer_uv + active_obj = context.scene.objects.active + bm = bmesh.from_edit_mesh(active_obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + # get UV layer + uv_layer = impl.get_uv_layer(self, bm) + if uv_layer is None: + return {'CANCELLED'} + + ret = impl.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 self.copy_seams: + active_obj.data.show_edge_seams = True + + return {'FINISHED'} diff --git a/uv_magic_uv/legacy/op/unwrap_constraint.py b/uv_magic_uv/legacy/op/unwrap_constraint.py new file mode 100644 index 00000000..f06efce1 --- /dev/null +++ b/uv_magic_uv/legacy/op/unwrap_constraint.py @@ -0,0 +1,190 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bpy +import bmesh +from bpy.props import ( + BoolProperty, + EnumProperty, + FloatProperty, +) + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_UnwrapConstraint', +] + + +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(legacy=True) +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) +class MUV_OT_UnwrapConstraint(bpy.types.Operator): + """ + Operation class: Unwrap with constrain UV coordinate + """ + + bl_idname = "uv.muv_unwrap_constraint_operator" + 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, _): + obj = bpy.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/uv_magic_uv/legacy/op/uv_bounding_box.py b/uv_magic_uv/legacy/op/uv_bounding_box.py new file mode 100644 index 00000000..47c27e41 --- /dev/null +++ b/uv_magic_uv/legacy/op/uv_bounding_box.py @@ -0,0 +1,838 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +from enum import IntEnum +import math + +import bpy +import bgl +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 + + +__all__ = [ + 'Properties', + 'MUV_OT_UVBoundingBox', +] + + +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(legacy=True) +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_uv_bounding_box_operator('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 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 = mat * mathutils.Vector((ox, oy, 0.0)) + self.__iox = iov.x # initial origin of scaling X + self.__ioy = iov.y # initial origin of scaling y + + def to_matrix(self): + """ + mat = M(to original transform)^-1 * Mt(to origin) * Ms * + Mt(to origin)^-1 * M(to original transform) + """ + m = self.__mat + mi = self.__mat.inverted() + mtoi = mathutils.Matrix.Translation((-self.__iox, -self.__ioy, 0.0)) + mto = mathutils.Matrix.Translation((self.__iox, self.__ioy, 0.0)) + # every point must be transformed to origin + t = m * mathutils.Vector((self.__ix, self.__iy, 0.0)) + tix, tiy = t.x, t.y + t = m * mathutils.Vector((self.__ox, self.__oy, 0.0)) + tox, toy = t.x, t.y + t = m * mathutils.Vector((self.__x, self.__y, 0.0)) + tx, ty = t.x, t.y + ms = mathutils.Matrix() + ms.identity() + if self.__dir_x == 1: + ms[0][0] = (tx - tox) * self.__dir_x / (tix - tox) + if self.__dir_y == 1: + ms[1][1] = (ty - toy) * self.__dir_y / (tiy - toy) + return mi * mto * ms * mtoi * m + + def set(self, x, y): + self.__x = x + self.__y = y + + +class 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 = mat * mathutils.Vector((ox, oy, 0.0)) + self.__iox = iov.x # initial origin of scaling x + self.__ioy = iov.y # initial origin of scaling y + self.__dir_x = 1 + self.__dir_y = 1 + + def to_matrix(self): + """ + mat = M(to original transform)^-1 * Mt(to origin) * Ms * + Mt(to origin)^-1 * M(to original transform) + """ + m = self.__mat + mi = self.__mat.inverted() + mtoi = mathutils.Matrix.Translation((-self.__iox, -self.__ioy, 0.0)) + mto = mathutils.Matrix.Translation((self.__iox, self.__ioy, 0.0)) + # every point must be transformed to origin + t = m * mathutils.Vector((self.__ix, self.__iy, 0.0)) + tix, tiy = t.x, t.y + t = m * mathutils.Vector((self.__ox, self.__oy, 0.0)) + tox, toy = t.x, t.y + t = m * mathutils.Vector((self.__x, self.__y, 0.0)) + tx, ty = t.x, t.y + ms = mathutils.Matrix() + ms.identity() + tir = math.sqrt((tix - tox) * (tix - tox) + (tiy - toy) * (tiy - toy)) + tr = math.sqrt((tx - tox) * (tx - tox) + (ty - toy) * (ty - toy)) + + sr = tr / tir + + if ((tx - tox) * (tix - tox)) > 0: + self.__dir_x = 1 + else: + self.__dir_x = -1 + if ((ty - toy) * (tiy - toy)) > 0: + self.__dir_y = 1 + else: + self.__dir_y = -1 + + ms[0][0] = sr * self.__dir_x + ms[1][1] = sr * self.__dir_y + + return mi * mto * ms * mtoi * m + + def set(self, x, y): + self.__x = x + self.__y = y + + +class 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 = 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 + """ + prefs = context.user_preferences.addons["uv_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(legacy=True) +class MUV_OT_UVBoundingBox(bpy.types.Operator): + """ + Operation class: UV Bounding Box + """ + + bl_idname = "uv.muv_uv_bounding_box_operator" + 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, 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 + """ + prefs = context.user_preferences.addons["uv_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 = trans_mat * v + bm.faces[fidx].loops[lidx][uv_layer].uv = mathutils.Vector( + (av.x, av.y)) + + def __update_ctrl_point(self, ctrl_points_ini, trans_mat): + """ + Update control point + """ + return [trans_mat * cp for cp in ctrl_points_ini] + + def modal(self, context, event): + props = context.scene.muv_props.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/uv_magic_uv/legacy/op/uv_inspection.py b/uv_magic_uv/legacy/op/uv_inspection.py new file mode 100644 index 00000000..57d42468 --- /dev/null +++ b/uv_magic_uv/legacy/op/uv_inspection.py @@ -0,0 +1,280 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bpy +import bmesh +import bgl +from bpy.props import BoolProperty, EnumProperty + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_UVInspection_Render', + 'MUV_OT_UVInspection_Update', +] + + +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(legacy=True) +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_uv_inspection_operator_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(legacy=True) +class MUV_OT_UVInspection_Render(bpy.types.Operator): + """ + Operation class: Render UV Inspection + No operation (only rendering) + """ + + bl_idname = "uv.muv_uv_inspection_operator_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 + prefs = context.user_preferences.addons["uv_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() + + 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'} + + +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) + + +@BlClassRegistry(legacy=True) +class MUV_OT_UVInspection_Update(bpy.types.Operator): + """ + Operation class: Update + """ + + bl_idname = "uv.muv_uv_inspection_operator_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/uv_magic_uv/legacy/op/uv_sculpt.py b/uv_magic_uv/legacy/op/uv_sculpt.py new file mode 100644 index 00000000..3754a759 --- /dev/null +++ b/uv_magic_uv/legacy/op/uv_sculpt.py @@ -0,0 +1,483 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +from math import pi, cos, tan, sin + +import bpy +import bmesh +import bgl +from mathutils import Vector +from bpy_extras import view3d_utils +from mathutils.bvhtree import BVHTree +from mathutils.geometry import barycentric_transform +from bpy.props import ( + BoolProperty, + IntProperty, + EnumProperty, + FloatProperty, +) + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_UVSculpt', +] + + +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(legacy=True) +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_uv_sculpt_operator('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(legacy=True) +class MUV_OT_UVSculpt(bpy.types.Operator): + """ + Operation class: UV Sculpt in View3D + """ + + bl_idname = "uv.muv_uv_sculpt_operator" + 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, 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 + prefs = context.user_preferences.addons["uv_magic_uv"].preferences + + num_segment = 180 + theta = 2 * pi / num_segment + fact_t = tan(theta) + fact_r = cos(theta) + color = prefs.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 __get_strength(self, p, len_, factor): + f = factor + + if p > len_: + return 0.0 + + if p < 0.0: + return f + + return (len_ - p) * f / len_ + + def __stroke_init(self, context, _): + sc = context.scene + + self.__initial_mco = self.current_mco + + # get influenced UV + obj = context.active_object + world_mat = obj.matrix_world + bm = bmesh.from_edit_mesh(obj.data) + uv_layer = bm.loops.layers.uv.verify() + _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') + + self.__loop_info = [] + for f in bm.faces: + if not f.select: + continue + for i, l in enumerate(f.loops): + loc_2d = view3d_utils.location_3d_to_region_2d( + region, space.region_3d, world_mat * l.vert.co) + diff = loc_2d - self.__initial_mco + if diff.length < sc.muv_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": self.__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, 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": self.__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 = mwi * ray_orig + ray_tgt_obj = mwi * ray_tgt + ray_dir_obj = ray_tgt_obj - ray_orig_obj + ray_dir_obj.normalize() + tree = BVHTree.FromBMesh(bm) + loc, _, fidx, _ = tree.ray_cast(ray_orig_obj, ray_dir_obj) + if not loc: + return + loops = [l for l in bm.faces[fidx].loops] + uvs = [Vector((l[uv_layer].uv.x, l[uv_layer].uv.y, 0.0)) + for l in loops] + target_uv = barycentric_transform( + loc, loops[0].vert.co, loops[1].vert.co, loops[2].vert.co, + uvs[0], uvs[1], uvs[2]) + target_uv = Vector((target_uv.x, target_uv.y)) + + # move to target UV coordinate + for info in loop_info: + l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] + if sc.muv_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, 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 = self.__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/uv_magic_uv/legacy/op/uvw.py b/uv_magic_uv/legacy/op/uvw.py new file mode 100644 index 00000000..56777b18 --- /dev/null +++ b/uv_magic_uv/legacy/op/uvw.py @@ -0,0 +1,181 @@ +# <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__ = "5.2" +__date__ = "17 Nov 2018" + +import bpy +import bmesh +from bpy.props import ( + FloatProperty, + FloatVectorProperty, + BoolProperty +) + +from ... import common +from ...impl import uvw_impl as impl +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_UVW_BoxMap', + 'MUV_OT_UVW_BestPlanerMap', +] + + +@PropertyClassRegistry(legacy=True) +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(legacy=True) +class MUV_OT_UVW_BoxMap(bpy.types.Operator): + bl_idname = "uv.muv_uvw_operator_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 impl.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 = impl.get_uv_layer(self, bm, self.assign_uvmap) + if not uv_layer: + return {'CANCELLED'} + + impl.apply_box_map(bm, uv_layer, self.size, self.offset, + self.rotation, self.tex_aspect) + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} + + +@BlClassRegistry(legacy=True) +class MUV_OT_UVW_BestPlanerMap(bpy.types.Operator): + bl_idname = "uv.muv_uvw_operator_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 impl.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 = impl.get_uv_layer(self, bm, self.assign_uvmap) + if not uv_layer: + return {'CANCELLED'} + + impl.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/uv_magic_uv/legacy/op/world_scale_uv.py b/uv_magic_uv/legacy/op/world_scale_uv.py new file mode 100644 index 00000000..e56b6bfa --- /dev/null +++ b/uv_magic_uv/legacy/op/world_scale_uv.py @@ -0,0 +1,655 @@ +# <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__ = "5.2" +__date__ = "17 Nov 2018" + +from math import sqrt + +import bpy +import bmesh +from mathutils import Vector +from bpy.props import ( + EnumProperty, + FloatProperty, + IntVectorProperty, + BoolProperty, +) + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_WorldScaleUV_Measure', + 'MUV_OT_WorldScaleUV_ApplyManual', + 'MUV_OT_WorldScaleUV_ApplyScalingDensity', + 'MUV_OT_WorldScaleUV_ApplyProportionalToMesh', +] + + +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 + + +@PropertyClassRegistry(legacy=True) +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(legacy=True) +class MUV_OT_WorldScaleUV_Measure(bpy.types.Operator): + """ + Operation class: Measure face size + """ + + bl_idname = "uv.muv_world_scale_uv_operator_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'} + + +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) + + +@BlClassRegistry(legacy=True) +class MUV_OT_WorldScaleUV_ApplyManual(bpy.types.Operator): + """ + Operation class: Apply scaled UV (Manual) + """ + + bl_idname = "uv.muv_world_scale_uv_operator_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(legacy=True) +class MUV_OT_WorldScaleUV_ApplyScalingDensity(bpy.types.Operator): + """ + Operation class: Apply scaled UV (Scaling Density) + """ + + bl_idname = "uv.muv_world_scale_uv_operator_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("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(legacy=True) +class MUV_OT_WorldScaleUV_ApplyProportionalToMesh(bpy.types.Operator): + """ + Operation class: Apply scaled UV (Proportional to mesh) + """ + + bl_idname = "uv.muv_world_scale_uv_operator_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("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) |