diff options
Diffstat (limited to 'uv_magic_uv/op')
23 files changed, 3509 insertions, 1749 deletions
diff --git a/uv_magic_uv/op/__init__.py b/uv_magic_uv/op/__init__.py index 75885ef6..9535b76d 100644 --- a/uv_magic_uv/op/__init__.py +++ b/uv_magic_uv/op/__init__.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" if "bpy" in locals(): import importlib @@ -35,6 +35,7 @@ if "bpy" in locals(): 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) @@ -57,6 +58,7 @@ else: 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 diff --git a/uv_magic_uv/op/align_uv.py b/uv_magic_uv/op/align_uv.py index dcfb57c3..90168a56 100644 --- a/uv_magic_uv/op/align_uv.py +++ b/uv_magic_uv/op/align_uv.py @@ -20,8 +20,8 @@ __author__ = "imdjs, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import math from math import atan2, tan, sin, cos @@ -29,11 +29,42 @@ from math import atan2, tan, sin, cos import bpy import bmesh from mathutils import Vector -from bpy.props import EnumProperty, BoolProperty +from bpy.props import EnumProperty, BoolProperty, FloatProperty from .. import common +__all__ = [ + 'Properties', + 'OperatorCircle', + 'OperatorStraighten', + 'OperatorAxis', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + # get sum vertex length of loop sequences def get_loop_vert_len(loops): length = 0 @@ -86,10 +117,69 @@ def calc_v_on_circle(v, center, radius): return new_v -class MUV_AUVCircle(bpy.types.Operator): +class Properties: + @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 + + +class OperatorCircle(bpy.types.Operator): - bl_idname = "uv.muv_auv_circle" - bl_label = "Circle" + bl_idname = "uv.muv_align_uv_operator_circle" + bl_label = "Align UV (Circle)" bl_description = "Align UV coordinates to Circle" bl_options = {'REGISTER', 'UNDO'} @@ -106,7 +196,10 @@ class MUV_AUVCircle(bpy.types.Operator): @classmethod def poll(cls, context): - return context.mode == 'EDIT_MESH' + # 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 @@ -167,66 +260,164 @@ class MUV_AUVCircle(bpy.types.Operator): 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, pair_idx): +def get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl): common.debug_print( - "vidx={0}, hidx={1}, pair_idx={2}".format(vidx, hidx, pair_idx)) + "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx)) - # get total vertex length - hloops = [] - for s in loop_seqs: - hloops.extend([s[vidx][0], s[vidx][1]]) - vert_total_hlen = get_loop_vert_len(hloops) - common.debug_print(vert_total_hlen) + base_uv = loop_seqs[0][vidx][0][uv_layer].uv.copy() - # target vertex length + # calculate original length hloops = [] - for s in loop_seqs[:hidx]: + for s in loop_seqs: hloops.extend([s[vidx][0], s[vidx][1]]) - for pidx, l in enumerate(loop_seqs[hidx][vidx]): - if pidx > pair_idx: + 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 - hloops.append(l) - vert_hlen = get_loop_vert_len(hloops) - common.debug_print(vert_hlen) + else: + raise Exception("Internal Error: horizontal_target_length={}" + " is not in range {} to {}" + .format(target_length, accum_uvlens[0], + accum_uvlens[-1])) - # get total UV length - # uv_all_hdiff = loop_seqs[-1][0][-1][uv_layer].uv - - # loop_seqs[0][0][0][uv_layer].uv - uv_total_hlen = loop_seqs[-1][vidx][-1][uv_layer].uv -\ - loop_seqs[0][vidx][0][uv_layer].uv - common.debug_print(uv_total_hlen) + return target_uv - return uv_total_hlen * vert_hlen / vert_total_hlen + +# --------------------- 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, pair_idx): +def get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl): common.debug_print( - "vidx={0}, hidx={1}, pair_idx={2}".format(vidx, hidx, pair_idx)) - - # get total vertex length - hloops = [] - for s in loop_seqs[hidx]: - hloops.append(s[pair_idx]) - vert_total_hlen = get_loop_vert_len(hloops) - common.debug_print(vert_total_hlen) + "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx)) - # target vertex length - hloops = [] - for s in loop_seqs[hidx][:vidx + 1]: - hloops.append(s[pair_idx]) - vert_hlen = get_loop_vert_len(hloops) - common.debug_print(vert_hlen) + base_uv = loop_seqs[hidx][0][pidx][uv_layer].uv.copy() - # get total UV length - # uv_all_hdiff = loop_seqs[0][-1][pair_idx][uv_layer].uv - \ - # loop_seqs[0][0][pair_idx][uv_layer].uv - uv_total_hlen = loop_seqs[hidx][-1][pair_idx][uv_layer].uv -\ - loop_seqs[hidx][0][pair_idx][uv_layer].uv - common.debug_print(uv_total_hlen) + # 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 uv_total_hlen * vert_hlen / vert_total_hlen + return target_uv # get horizontal differential of UV no influenced @@ -246,10 +437,10 @@ def get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx): return int((vidx + 1) / 2) * v_uv / (len(hseq) / 2) -class MUV_AUVStraighten(bpy.types.Operator): +class OperatorStraighten(bpy.types.Operator): - bl_idname = "uv.muv_auv_straighten" - bl_label = "Straighten" + bl_idname = "uv.muv_align_uv_operator_straighten" + bl_label = "Align UV (Straighten)" bl_description = "Straighten UV coordinates" bl_options = {'REGISTER', 'UNDO'} @@ -275,10 +466,20 @@ class MUV_AUVStraighten(bpy.types.Operator): "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): - return context.mode == 'EDIT_MESH' + # 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): @@ -293,12 +494,14 @@ class MUV_AUVStraighten(bpy.types.Operator): for vidx in range(0, len(hseq), 2): if self.horizontal: hdiff_uvs = [ - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0), - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, 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), + hidx, 0, self.mesh_infl), get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 1), + hidx, 1, self.mesh_infl), ] else: hdiff_uvs = [ @@ -309,12 +512,14 @@ class MUV_AUVStraighten(bpy.types.Operator): ] if self.vertical: vdiff_uvs = [ - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0), - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, 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), + hidx, 0, self.mesh_infl), get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 1), + hidx, 1, self.mesh_infl), ] else: vdiff_uvs = [ @@ -382,10 +587,10 @@ class MUV_AUVStraighten(bpy.types.Operator): return {'FINISHED'} -class MUV_AUVAxis(bpy.types.Operator): +class OperatorAxis(bpy.types.Operator): - bl_idname = "uv.muv_auv_axis" - bl_label = "XY-Axis" + 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'} @@ -421,10 +626,20 @@ class MUV_AUVAxis(bpy.types.Operator): ], 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): - return context.mode == 'EDIT_MESH' + # 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): @@ -579,12 +794,14 @@ class MUV_AUVAxis(bpy.types.Operator): for vidx in range(0, len(hseq), 2): if self.horizontal: hdiff_uvs = [ - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0), - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, 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), + hidx, 0, self.mesh_infl), get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 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 @@ -599,12 +816,14 @@ class MUV_AUVAxis(bpy.types.Operator): ] if self.vertical: vdiff_uvs = [ - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0), - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, 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), + hidx, 0, self.mesh_infl), get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 1), + hidx, 1, self.mesh_infl), ] else: vdiff_uvs = [ @@ -664,12 +883,14 @@ class MUV_AUVAxis(bpy.types.Operator): for vidx in range(0, len(hseq), 2): if self.horizontal: hdiff_uvs = [ - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0), - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, 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), + hidx, 0, self.mesh_infl), get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 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 @@ -684,12 +905,14 @@ class MUV_AUVAxis(bpy.types.Operator): ] if self.vertical: vdiff_uvs = [ - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0), - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, 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), + hidx, 0, self.mesh_infl), get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 1), + hidx, 1, self.mesh_infl), ] else: vdiff_uvs = [ diff --git a/uv_magic_uv/op/align_uv_cursor.py b/uv_magic_uv/op/align_uv_cursor.py index cae1c89a..d787bde9 100644 --- a/uv_magic_uv/op/align_uv_cursor.py +++ b/uv_magic_uv/op/align_uv_cursor.py @@ -20,21 +20,111 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy from mathutils import Vector -from bpy.props import EnumProperty +from bpy.props import EnumProperty, BoolProperty, FloatVectorProperty import bmesh from .. import common -class MUV_AUVCAlignOps(bpy.types.Operator): - - bl_idname = "uv.muv_auvc_align" - bl_label = "Align" +__all__ = [ + 'Properties', + 'Operator', +] + + +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 + + +class Properties: + @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 + + +class Operator(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'} @@ -65,6 +155,13 @@ class MUV_AUVCAlignOps(bpy.types.Operator): 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') diff --git a/uv_magic_uv/op/copy_paste_uv.py b/uv_magic_uv/op/copy_paste_uv.py index ee89b5e9..cc1baa30 100644 --- a/uv_magic_uv/op/copy_paste_uv.py +++ b/uv_magic_uv/op/copy_paste_uv.py @@ -20,11 +20,9 @@ __author__ = "imdjs, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" -import math -from math import atan2, sin, cos import bpy import bmesh @@ -34,104 +32,293 @@ from bpy.props import ( IntProperty, EnumProperty, ) -from mathutils import Vector from .. import common -class MUV_CPUVCopyUV(bpy.types.Operator): +__all__ = [ + 'Properties', + 'OpeartorCopyUV', + 'MenuCopyUV', + 'OperatorPasteUV', + 'MenuPasteUV', + 'OperatorSelSeqCopyUV', + 'MenuSelSeqCopyUV', + 'OperatorSelSeqPasteUV', + 'MenuSelSeqPasteUV', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +def get_copy_uv_layers(ops_obj, bm): + uv_layers = [] + if ops_obj.uv_map == "__default": + if not bm.loops.layers.uv: + ops_obj.report( + {'WARNING'}, "Object must have more than one UV map") + return None + uv_layers.append(bm.loops.layers.uv.verify()) + ops_obj.report({'INFO'}, "Copy UV coordinate") + elif ops_obj.uv_map == "__all": + for uv in bm.loops.layers.uv.keys(): + uv_layers.append(bm.loops.layers.uv[uv]) + ops_obj.report({'INFO'}, "Copy UV coordinate (UV map: ALL)") + else: + uv_layers.append(bm.loops.layers.uv[ops_obj.uv_map]) + ops_obj.report( + {'INFO'}, "Copy UV coordinate (UV map:{})".format(ops_obj.uv_map)) + + return uv_layers + + +def get_paste_uv_layers(ops_obj, obj, bm, src_info): + uv_layers = [] + if ops_obj.uv_map == "__default": + if not bm.loops.layers.uv: + ops_obj.report( + {'WARNING'}, "Object must have more than one UV map") + return None + uv_layers.append(bm.loops.layers.uv.verify()) + ops_obj.report({'INFO'}, "Paste UV coordinate") + elif ops_obj.uv_map == "__new": + new_uv_map = common.create_new_uv_map(obj) + if not new_uv_map: + ops_obj.report({'WARNING'}, + "Reached to the maximum number of UV map") + return None + uv_layers.append(bm.loops.layers.uv[new_uv_map.name]) + ops_obj.report( + {'INFO'}, "Paste UV coordinate (UV map:{})".format(new_uv_map)) + elif ops_obj.uv_map == "__all": + for src_layer in src_info.keys(): + if src_layer not in bm.loops.layers.uv.keys(): + new_uv_map = common.create_new_uv_map(obj, src_layer) + if not new_uv_map: + ops_obj.report({'WARNING'}, + "Reached to the maximum number of UV map") + return None + uv_layers.append(bm.loops.layers.uv[src_layer]) + ops_obj.report({'INFO'}, "Paste UV coordinate (UV map: ALL)") + else: + uv_layers.append(bm.loops.layers.uv[ops_obj.uv_map]) + ops_obj.report( + {'INFO'}, "Paste UV coordinate (UV map:{})".format(ops_obj.uv_map)) + + return uv_layers + + +def paste_uv(ops_obj, bm, src_info, dest_info, uv_layers, strategy, flip, + rotate, copy_seams): + for slayer_name, dlayer in zip(src_info.keys(), uv_layers): + src_faces = src_info[slayer_name] + dest_faces = dest_info[dlayer.name] + + for idx, dinfo in enumerate(dest_faces): + sinfo = None + if strategy == 'N_N': + sinfo = src_faces[idx] + elif strategy == 'N_M': + sinfo = src_faces[idx % len(src_faces)] + + suv = sinfo["uvs"] + spuv = sinfo["pin_uvs"] + ss = sinfo["seams"] + if len(sinfo["uvs"]) != len(dinfo["uvs"]): + ops_obj.report({'WARNING'}, "Some faces are different size") + return -1 + + suvs_fr = [uv for uv in suv] + spuvs_fr = [pin_uv for pin_uv in spuv] + ss_fr = [s for s in ss] + + # flip UVs + if flip is True: + suvs_fr.reverse() + spuvs_fr.reverse() + ss_fr.reverse() + + # rotate UVs + for _ in range(rotate): + uv = suvs_fr.pop() + pin_uv = spuvs_fr.pop() + s = ss_fr.pop() + suvs_fr.insert(0, uv) + spuvs_fr.insert(0, pin_uv) + ss_fr.insert(0, s) + + # paste UVs + for l, suv, spuv, ss in zip(bm.faces[idx].loops, suvs_fr, + spuvs_fr, ss_fr): + l[dlayer].uv = suv + l[dlayer].pin_uv = spuv + if copy_seams is True: + l.edge.seam = ss + + return 0 + + +class Properties: + @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 + + +class OpeartorCopyUV(bpy.types.Operator): """ Operation class: Copy UV coordinate """ - bl_idname = "uv.muv_cpuv_copy_uv" - bl_label = "Copy UV (Operation)" - bl_description = "Copy UV coordinate (Operation)" + bl_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(options={'HIDDEN'}) + uv_map = StringProperty(default="__default", options={'HIDDEN'}) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) def execute(self, context): - props = context.scene.muv_props.cpuv - if self.uv_map == "": - self.report({'INFO'}, "Copy UV coordinate") - else: - self.report( - {'INFO'}, "Copy UV coordinate (UV map:%s)" % (self.uv_map)) + props = context.scene.muv_props.copy_paste_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() + bm = common.create_bmesh(obj) # get UV layer - if self.uv_map == "": - if not bm.loops.layers.uv: - self.report( - {'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - else: - uv_layer = bm.loops.layers.uv[self.uv_map] + uv_layers = get_copy_uv_layers(self, bm) + if not uv_layers: + return {'CANCELLED'} # get selected face - props.src_uvs = [] - props.src_pin_uvs = [] - props.src_seams = [] - for face in bm.faces: - if face.select: - uvs = [l[uv_layer].uv.copy() for l in face.loops] - pin_uvs = [l[uv_layer].pin_uv for l in face.loops] - seams = [l.edge.seam for l in face.loops] - props.src_uvs.append(uvs) - props.src_pin_uvs.append(pin_uvs) - props.src_seams.append(seams) - if not props.src_uvs or not props.src_pin_uvs: - self.report({'WARNING'}, "No faces are selected") - return {'CANCELLED'} - self.report({'INFO'}, "%d face(s) are selected" % len(props.src_uvs)) + props.src_info = {} + for layer in uv_layers: + face_info = [] + for face in bm.faces: + if face.select: + info = { + "uvs": [l[layer].uv.copy() for l in face.loops], + "pin_uvs": [l[layer].pin_uv for l in face.loops], + "seams": [l.edge.seam for l in face.loops], + } + face_info.append(info) + if not face_info: + self.report({'WARNING'}, "No faces are selected") + return {'CANCELLED'} + props.src_info[layer.name] = face_info + + face_count = len([f for f in bm.faces if f.select]) + self.report({'INFO'}, "{} face(s) are copied".format(face_count)) return {'FINISHED'} -class MUV_CPUVCopyUVMenu(bpy.types.Menu): +class MenuCopyUV(bpy.types.Menu): """ Menu class: Copy UV coordinate """ - bl_idname = "uv.muv_cpuv_copy_uv_menu" - bl_label = "Copy UV" - bl_description = "Copy UV coordinate" + 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 is_valid_context(context) def draw(self, context): layout = self.layout # create sub menu obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) + bm = common.create_bmesh(obj) uv_maps = bm.loops.layers.uv.keys() - layout.operator( - MUV_CPUVCopyUV.bl_idname, - text="[Default]", - icon="IMAGE_COL" - ).uv_map = "" + + ops = layout.operator(OpeartorCopyUV.bl_idname, text="[Default]") + ops.uv_map = "__default" + + ops = layout.operator(OpeartorCopyUV.bl_idname, text="[All]") + ops.uv_map = "__all" + for m in uv_maps: - layout.operator( - MUV_CPUVCopyUV.bl_idname, - text=m, - icon="IMAGE_COL" - ).uv_map = m + ops = layout.operator(OpeartorCopyUV.bl_idname, text=m) + ops.uv_map = m -class MUV_CPUVPasteUV(bpy.types.Operator): +class OperatorPasteUV(bpy.types.Operator): """ Operation class: Paste UV coordinate """ - bl_idname = "uv.muv_cpuv_paste_uv" - bl_label = "Paste UV (Operation)" - bl_description = "Paste UV coordinate (Operation)" + bl_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(options={'HIDDEN'}) + uv_map = StringProperty(default="__default", options={'HIDDEN'}) strategy = EnumProperty( name="Strategy", description="Paste Strategy", @@ -153,104 +340,69 @@ class MUV_CPUVPasteUV(bpy.types.Operator): max=30 ) copy_seams = BoolProperty( - name="Copy Seams", + name="Seams", description="Copy Seams", default=True ) + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.copy_paste_uv + if not props.src_info: + return False + return is_valid_context(context) + def execute(self, context): - props = context.scene.muv_props.cpuv - if not props.src_uvs or not props.src_pin_uvs: + props = context.scene.muv_props.copy_paste_uv + if not props.src_info: self.report({'WARNING'}, "Need copy UV at first") return {'CANCELLED'} - if self.uv_map == "": - self.report({'INFO'}, "Paste UV coordinate") - else: - self.report( - {'INFO'}, "Paste UV coordinate (UV map:%s)" % (self.uv_map)) obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() + bm = common.create_bmesh(obj) # get UV layer - if self.uv_map == "": - if not bm.loops.layers.uv: + uv_layers = get_paste_uv_layers(self, obj, bm, props.src_info) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + dest_face_count = 0 + dest_info = {} + for layer in uv_layers: + face_info = [] + for face in bm.faces: + if face.select: + info = { + "uvs": [l[layer].uv.copy() for l in face.loops], + } + face_info.append(info) + if not face_info: + self.report({'WARNING'}, "No faces are selected") + return {'CANCELLED'} + key = list(props.src_info.keys())[0] + src_face_count = len(props.src_info[key]) + dest_face_count = len(face_info) + if self.strategy == 'N_N' and src_face_count != dest_face_count: self.report( - {'WARNING'}, "Object must have more than one UV map") + {'WARNING'}, + "Number of selected faces is different from copied" + + "(src:{}, dest:{})" + .format(src_face_count, dest_face_count)) return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - else: - uv_layer = bm.loops.layers.uv[self.uv_map] + dest_info[layer.name] = face_info - # get selected face - dest_uvs = [] - dest_pin_uvs = [] - dest_seams = [] - dest_face_indices = [] - for face in bm.faces: - if face.select: - dest_face_indices.append(face.index) - uvs = [l[uv_layer].uv.copy() for l in face.loops] - pin_uvs = [l[uv_layer].pin_uv for l in face.loops] - seams = [l.edge.seam for l in face.loops] - dest_uvs.append(uvs) - dest_pin_uvs.append(pin_uvs) - dest_seams.append(seams) - if not dest_uvs or not dest_pin_uvs: - self.report({'WARNING'}, "No faces are selected") - return {'CANCELLED'} - if self.strategy == 'N_N' and len(props.src_uvs) != len(dest_uvs): - self.report( - {'WARNING'}, - "Number of selected faces is different from copied" + - "(src:%d, dest:%d)" % - (len(props.src_uvs), len(dest_uvs))) + # paste + ret = paste_uv(self, bm, props.src_info, dest_info, uv_layers, + self.strategy, self.flip_copied_uv, + self.rotate_copied_uv, self.copy_seams) + if ret: return {'CANCELLED'} - # paste - for i, idx in enumerate(dest_face_indices): - suv = None - spuv = None - ss = None - duv = None - if self.strategy == 'N_N': - suv = props.src_uvs[i] - spuv = props.src_pin_uvs[i] - ss = props.src_seams[i] - duv = dest_uvs[i] - elif self.strategy == 'N_M': - suv = props.src_uvs[i % len(props.src_uvs)] - spuv = props.src_pin_uvs[i % len(props.src_pin_uvs)] - ss = props.src_seams[i % len(props.src_seams)] - duv = dest_uvs[i] - if len(suv) != len(duv): - self.report({'WARNING'}, "Some faces are different size") - return {'CANCELLED'} - suvs_fr = [uv for uv in suv] - spuvs_fr = [pin_uv for pin_uv in spuv] - ss_fr = [s for s in ss] - # flip UVs - if self.flip_copied_uv is True: - suvs_fr.reverse() - spuvs_fr.reverse() - ss_fr.reverse() - # rotate UVs - for _ in range(self.rotate_copied_uv): - uv = suvs_fr.pop() - pin_uv = spuvs_fr.pop() - s = ss_fr.pop() - suvs_fr.insert(0, uv) - spuvs_fr.insert(0, pin_uv) - ss_fr.insert(0, s) - # paste UVs - for l, suv, spuv, ss in zip(bm.faces[idx].loops, suvs_fr, - spuvs_fr, ss_fr): - l[uv_layer].uv = suv - l[uv_layer].pin_uv = spuv - if self.copy_seams is True: - l.edge.seam = ss - self.report({'INFO'}, "%d face(s) are copied" % len(dest_uvs)) + self.report({'INFO'}, "{} face(s) are pasted".format(dest_face_count)) bmesh.update_edit_mesh(obj.data) if self.copy_seams is True: @@ -259,234 +411,146 @@ class MUV_CPUVPasteUV(bpy.types.Operator): return {'FINISHED'} -class MUV_CPUVPasteUVMenu(bpy.types.Menu): +class MenuPasteUV(bpy.types.Menu): """ Menu class: Paste UV coordinate """ - bl_idname = "uv.muv_cpuv_paste_uv_menu" - bl_label = "Paste UV" - bl_description = "Paste UV coordinate" + 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 is_valid_context(context) def draw(self, context): sc = context.scene layout = self.layout # create sub menu obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) + bm = common.create_bmesh(obj) uv_maps = bm.loops.layers.uv.keys() - ops = layout.operator(MUV_CPUVPasteUV.bl_idname, text="[Default]") - ops.uv_map = "" - ops.copy_seams = sc.muv_cpuv_copy_seams - ops.strategy = sc.muv_cpuv_strategy - for m in uv_maps: - ops = layout.operator(MUV_CPUVPasteUV.bl_idname, text=m) - ops.uv_map = m - ops.copy_seams = sc.muv_cpuv_copy_seams - ops.strategy = sc.muv_cpuv_strategy - -class MUV_CPUVIECopyUV(bpy.types.Operator): - """ - Operation class: Copy UV coordinate on UV/Image Editor - """ - - bl_idname = "uv.muv_cpuv_ie_copy_uv" - bl_label = "Copy UV" - bl_description = "Copy UV coordinate (only selected in UV/Image Editor)" - bl_options = {'REGISTER', 'UNDO'} + ops = layout.operator(OperatorPasteUV.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 - @classmethod - def poll(cls, context): - return context.mode == 'EDIT_MESH' + ops = layout.operator(OperatorPasteUV.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 - def execute(self, context): - props = context.scene.muv_props.cpuv - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - uv_layer = bm.loops.layers.uv.verify() - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - - for face in bm.faces: - if not face.select: - continue - skip = False - for l in face.loops: - if not l[uv_layer].select: - skip = True - break - if skip: - continue - props.src_uvs.append([l[uv_layer].uv.copy() for l in face.loops]) + ops = layout.operator(OperatorPasteUV.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 - return {'FINISHED'} + for m in uv_maps: + ops = layout.operator(OperatorPasteUV.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 -class MUV_CPUVIEPasteUV(bpy.types.Operator): +class OperatorSelSeqCopyUV(bpy.types.Operator): """ - Operation class: Paste UV coordinate on UV/Image Editor + Operation class: Copy UV coordinate by selection sequence """ - bl_idname = "uv.muv_cpuv_ie_paste_uv" - bl_label = "Paste UV" - bl_description = "Paste UV coordinate (only selected in UV/Image Editor)" + 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): - return context.mode == 'EDIT_MESH' - - def execute(self, context): - props = context.scene.muv_props.cpuv - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - uv_layer = bm.loops.layers.uv.verify() - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - - dest_uvs = [] - dest_face_indices = [] - for face in bm.faces: - if not face.select: - continue - skip = False - for l in face.loops: - if not l[uv_layer].select: - skip = True - break - if skip: - continue - dest_face_indices.append(face.index) - uvs = [l[uv_layer].uv.copy() for l in face.loops] - dest_uvs.append(uvs) - - for suvs, duvs in zip(props.src_uvs, dest_uvs): - src_diff = suvs[1] - suvs[0] - dest_diff = duvs[1] - duvs[0] - - src_base = suvs[0] - dest_base = duvs[0] - - src_rad = atan2(src_diff.y, src_diff.x) - dest_rad = atan2(dest_diff.y, dest_diff.x) - if src_rad < dest_rad: - radian = dest_rad - src_rad - elif src_rad > dest_rad: - radian = math.pi * 2 - (src_rad - dest_rad) - else: # src_rad == dest_rad - radian = 0.0 - - ratio = dest_diff.length / src_diff.length - break - - for suvs, fidx in zip(props.src_uvs, dest_face_indices): - for l, suv in zip(bm.faces[fidx].loops, suvs): - base = suv - src_base - radian_ref = atan2(base.y, base.x) - radian_fin = (radian + radian_ref) - length = base.length - turn = Vector((length * cos(radian_fin), - length * sin(radian_fin))) - target_uv = Vector((turn.x * ratio, turn.y * ratio)) + \ - dest_base - l[uv_layer].uv = target_uv - - bmesh.update_edit_mesh(obj.data) - - return {'FINISHED'} - - -class MUV_CPUVSelSeqCopyUV(bpy.types.Operator): - """ - Operation class: Copy UV coordinate by selection sequence - """ - - bl_idname = "uv.muv_cpuv_selseq_copy_uv" - bl_label = "Copy UV (Selection Sequence) (Operation)" - bl_description = "Copy UV data by selection sequence (Operation)" - bl_options = {'REGISTER', 'UNDO'} - - uv_map = StringProperty(options={'HIDDEN'}) + # 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.cpuv_selseq - if self.uv_map == "": - self.report({'INFO'}, "Copy UV coordinate (selection sequence)") - else: - self.report( - {'INFO'}, - "Copy UV coordinate (selection sequence) (UV map:%s)" - % (self.uv_map)) + props = context.scene.muv_props.copy_paste_uv_selseq obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() + bm = common.create_bmesh(obj) # get UV layer - if self.uv_map == "": - if not bm.loops.layers.uv: - self.report( - {'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - else: - uv_layer = bm.loops.layers.uv[self.uv_map] + uv_layers = get_copy_uv_layers(self, bm) + if not uv_layers: + return {'CANCELLED'} # get selected face - props.src_uvs = [] - props.src_pin_uvs = [] - props.src_seams = [] - for hist in bm.select_history: - if isinstance(hist, bmesh.types.BMFace) and hist.select: - uvs = [l[uv_layer].uv.copy() for l in hist.loops] - pin_uvs = [l[uv_layer].pin_uv for l in hist.loops] - seams = [l.edge.seam for l in hist.loops] - props.src_uvs.append(uvs) - props.src_pin_uvs.append(pin_uvs) - props.src_seams.append(seams) - if not props.src_uvs or not props.src_pin_uvs: - self.report({'WARNING'}, "No faces are selected") - return {'CANCELLED'} - self.report({'INFO'}, "%d face(s) are selected" % len(props.src_uvs)) + props.src_info = {} + for layer in uv_layers: + face_info = [] + for hist in bm.select_history: + if isinstance(hist, bmesh.types.BMFace) and hist.select: + info = { + "uvs": [l[layer].uv.copy() for l in hist.loops], + "pin_uvs": [l[layer].pin_uv for l in hist.loops], + "seams": [l.edge.seam for l in hist.loops], + } + face_info.append(info) + if not face_info: + self.report({'WARNING'}, "No faces are selected") + return {'CANCELLED'} + props.src_info[layer.name] = face_info + + face_count = len([f for f in bm.faces if f.select]) + self.report({'INFO'}, "{} face(s) are selected".format(face_count)) return {'FINISHED'} -class MUV_CPUVSelSeqCopyUVMenu(bpy.types.Menu): +class MenuSelSeqCopyUV(bpy.types.Menu): """ Menu class: Copy UV coordinate by selection sequence """ - bl_idname = "uv.muv_cpuv_selseq_copy_uv_menu" - bl_label = "Copy UV (Selection Sequence)" - bl_description = "Copy UV coordinate by selection sequence" + 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 is_valid_context(context) def draw(self, context): layout = self.layout obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) + bm = common.create_bmesh(obj) uv_maps = bm.loops.layers.uv.keys() - layout.operator( - MUV_CPUVSelSeqCopyUV.bl_idname, - text="[Default]", icon="IMAGE_COL").uv_map = "" + + ops = layout.operator(OperatorSelSeqCopyUV.bl_idname, text="[Default]") + ops.uv_map = "__default" + + ops = layout.operator(OperatorSelSeqCopyUV.bl_idname, text="[All]") + ops.uv_map = "__all" + for m in uv_maps: - layout.operator( - MUV_CPUVSelSeqCopyUV.bl_idname, - text=m, icon="IMAGE_COL").uv_map = m + ops = layout.operator(OperatorSelSeqCopyUV.bl_idname, text=m) + ops.uv_map = m -class MUV_CPUVSelSeqPasteUV(bpy.types.Operator): +class OperatorSelSeqPasteUV(bpy.types.Operator): """ Operation class: Paste UV coordinate by selection sequence """ - bl_idname = "uv.muv_cpuv_selseq_paste_uv" - bl_label = "Paste UV (Selection Sequence) (Operation)" - bl_description = "Paste UV coordinate by selection sequence (Operation)" + bl_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(options={'HIDDEN'}) + uv_map = StringProperty(default="__default", options={'HIDDEN'}) strategy = EnumProperty( name="Strategy", description="Paste Strategy", @@ -508,108 +572,69 @@ class MUV_CPUVSelSeqPasteUV(bpy.types.Operator): max=30 ) copy_seams = BoolProperty( - name="Copy Seams", + name="Seams", description="Copy Seams", default=True ) + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.copy_paste_uv_selseq + if not props.src_info: + return False + return is_valid_context(context) + def execute(self, context): - props = context.scene.muv_props.cpuv_selseq - if not props.src_uvs or not props.src_pin_uvs: + props = context.scene.muv_props.copy_paste_uv_selseq + if not props.src_info: self.report({'WARNING'}, "Need copy UV at first") return {'CANCELLED'} - if self.uv_map == "": - self.report({'INFO'}, "Paste UV coordinate (selection sequence)") - else: - self.report( - {'INFO'}, - "Paste UV coordinate (selection sequence) (UV map:%s)" - % (self.uv_map)) - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() + bm = common.create_bmesh(obj) # get UV layer - if self.uv_map == "": - if not bm.loops.layers.uv: - self.report( - {'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - else: - uv_layer = bm.loops.layers.uv[self.uv_map] + uv_layers = get_paste_uv_layers(self, obj, bm, props.src_info) + if not uv_layers: + return {'CANCELLED'} # get selected face - dest_uvs = [] - dest_pin_uvs = [] - dest_seams = [] - dest_face_indices = [] - for hist in bm.select_history: - if isinstance(hist, bmesh.types.BMFace) and hist.select: - dest_face_indices.append(hist.index) - uvs = [l[uv_layer].uv.copy() for l in hist.loops] - pin_uvs = [l[uv_layer].pin_uv for l in hist.loops] - seams = [l.edge.seam for l in hist.loops] - dest_uvs.append(uvs) - dest_pin_uvs.append(pin_uvs) - dest_seams.append(seams) - if not dest_uvs or not dest_pin_uvs: - self.report({'WARNING'}, "No faces are selected") - return {'CANCELLED'} - if self.strategy == 'N_N' and len(props.src_uvs) != len(dest_uvs): - self.report( - {'WARNING'}, - "Number of selected faces is different from copied faces " + - "(src:%d, dest:%d)" - % (len(props.src_uvs), len(dest_uvs))) - return {'CANCELLED'} + dest_face_count = 0 + dest_info = {} + for layer in uv_layers: + face_info = [] + for hist in bm.select_history: + if isinstance(hist, bmesh.types.BMFace) and hist.select: + info = { + "uvs": [l[layer].uv.copy() for l in hist.loops], + } + face_info.append(info) + if not face_info: + self.report({'WARNING'}, "No faces are selected") + return {'CANCELLED'} + key = list(props.src_info.keys())[0] + src_face_count = len(props.src_info[key]) + dest_face_count = len(face_info) + if self.strategy == 'N_N' and src_face_count != dest_face_count: + self.report( + {'WARNING'}, + "Number of selected faces is different from copied" + + "(src:{}, dest:{})" + .format(src_face_count, dest_face_count)) + return {'CANCELLED'} + dest_info[layer.name] = face_info # paste - for i, idx in enumerate(dest_face_indices): - suv = None - spuv = None - ss = None - duv = None - if self.strategy == 'N_N': - suv = props.src_uvs[i] - spuv = props.src_pin_uvs[i] - ss = props.src_seams[i] - duv = dest_uvs[i] - elif self.strategy == 'N_M': - suv = props.src_uvs[i % len(props.src_uvs)] - spuv = props.src_pin_uvs[i % len(props.src_pin_uvs)] - ss = props.src_seams[i % len(props.src_seams)] - duv = dest_uvs[i] - if len(suv) != len(duv): - self.report({'WARNING'}, "Some faces are different size") - return {'CANCELLED'} - suvs_fr = [uv for uv in suv] - spuvs_fr = [pin_uv for pin_uv in spuv] - ss_fr = [s for s in ss] - # flip UVs - if self.flip_copied_uv is True: - suvs_fr.reverse() - spuvs_fr.reverse() - ss_fr.reverse() - # rotate UVs - for _ in range(self.rotate_copied_uv): - uv = suvs_fr.pop() - pin_uv = spuvs_fr.pop() - s = ss_fr.pop() - suvs_fr.insert(0, uv) - spuvs_fr.insert(0, pin_uv) - ss_fr.insert(0, s) - # paste UVs - for l, suv, spuv, ss in zip(bm.faces[idx].loops, suvs_fr, - spuvs_fr, ss_fr): - l[uv_layer].uv = suv - l[uv_layer].pin_uv = spuv - if self.copy_seams is True: - l.edge.seam = ss + ret = paste_uv(self, bm, props.src_info, dest_info, uv_layers, + self.strategy, self.flip_copied_uv, + self.rotate_copied_uv, self.copy_seams) + if ret: + return {'CANCELLED'} - self.report({'INFO'}, "%d face(s) are copied" % len(dest_uvs)) + self.report({'INFO'}, "{} face(s) are pasted".format(dest_face_count)) bmesh.update_edit_mesh(obj.data) if self.copy_seams is True: @@ -618,29 +643,49 @@ class MUV_CPUVSelSeqPasteUV(bpy.types.Operator): return {'FINISHED'} -class MUV_CPUVSelSeqPasteUVMenu(bpy.types.Menu): +class MenuSelSeqPasteUV(bpy.types.Menu): """ Menu class: Paste UV coordinate by selection sequence """ - bl_idname = "uv.muv_cpuv_selseq_paste_uv_menu" - bl_label = "Paste UV (Selection Sequence)" - bl_description = "Paste UV coordinate by selection sequence" + 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 is_valid_context(context) def draw(self, context): sc = context.scene layout = self.layout # create sub menu obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) + bm = common.create_bmesh(obj) uv_maps = bm.loops.layers.uv.keys() - ops = layout.operator(MUV_CPUVSelSeqPasteUV.bl_idname, + + ops = layout.operator(OperatorSelSeqPasteUV.bl_idname, text="[Default]") - ops.uv_map = "" - ops.copy_seams = sc.muv_cpuv_copy_seams - ops.strategy = sc.muv_cpuv_strategy + 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(OperatorSelSeqPasteUV.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(OperatorSelSeqPasteUV.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_CPUVSelSeqPasteUV.bl_idname, text=m) + ops = layout.operator(OperatorSelSeqPasteUV.bl_idname, text=m) ops.uv_map = m - ops.copy_seams = sc.muv_cpuv_copy_seams - ops.strategy = sc.muv_cpuv_strategy + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy diff --git a/uv_magic_uv/op/copy_paste_uv_object.py b/uv_magic_uv/op/copy_paste_uv_object.py index d80ee415..eb6d87c9 100644 --- a/uv_magic_uv/op/copy_paste_uv_object.py +++ b/uv_magic_uv/op/copy_paste_uv_object.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy import bmesh @@ -31,6 +31,41 @@ from bpy.props import ( ) from .. import common +from .copy_paste_uv import ( + get_copy_uv_layers, + get_paste_uv_layers, + paste_uv +) + + +__all__ = [ + 'Properties', + 'OperatorCopyUV', + 'MenuCopyUV', + 'OperatorPasteUV', + 'MenuPasteUV', +] + + +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 def memorize_view_3d_mode(fn): @@ -42,101 +77,138 @@ def memorize_view_3d_mode(fn): return __memorize_view_3d_mode -class MUV_CPUVObjCopyUV(bpy.types.Operator): +class Properties: + @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 + + +class OperatorCopyUV(bpy.types.Operator): """ - Operation class: Copy UV coordinate per object + Operation class: Copy UV coordinate among objects """ - bl_idname = "object.muv_cpuv_obj_copy_uv" - bl_label = "Copy UV" - bl_description = "Copy UV coordinate" + 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(options={'HIDDEN'}) + 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.cpuv_obj - if self.uv_map == "": - self.report({'INFO'}, "Copy UV coordinate per object") - else: - self.report( - {'INFO'}, - "Copy UV coordinate per object (UV map:%s)" % (self.uv_map)) + props = context.scene.muv_props.copy_paste_uv_object bpy.ops.object.mode_set(mode='EDIT') - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() + bm = common.create_bmesh(obj) # get UV layer - if self.uv_map == "": - if not bm.loops.layers.uv: - self.report( - {'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - else: - uv_layer = bm.loops.layers.uv[self.uv_map] + uv_layers = get_copy_uv_layers(self, bm) + if not uv_layers: + return {'CANCELLED'} # get selected face - props.src_uvs = [] - props.src_pin_uvs = [] - props.src_seams = [] - for face in bm.faces: - uvs = [l[uv_layer].uv.copy() for l in face.loops] - pin_uvs = [l[uv_layer].pin_uv for l in face.loops] - seams = [l.edge.seam for l in face.loops] - props.src_uvs.append(uvs) - props.src_pin_uvs.append(pin_uvs) - props.src_seams.append(seams) - - self.report({'INFO'}, "%s's UV coordinates are copied" % (obj.name)) + props.src_info = {} + for layer in uv_layers: + face_info = [] + for face in bm.faces: + if face.select: + info = { + "uvs": [l[layer].uv.copy() for l in face.loops], + "pin_uvs": [l[layer].pin_uv for l in face.loops], + "seams": [l.edge.seam for l in face.loops], + } + face_info.append(info) + props.src_info[layer.name] = face_info + + self.report({'INFO'}, + "{}'s UV coordinates are copied".format(obj.name)) return {'FINISHED'} -class MUV_CPUVObjCopyUVMenu(bpy.types.Menu): +class MenuCopyUV(bpy.types.Menu): """ - Menu class: Copy UV coordinate per object + Menu class: Copy UV coordinate among objects """ - bl_idname = "object.muv_cpuv_obj_copy_uv_menu" - bl_label = "Copy UV" - bl_description = "Copy UV coordinate per object" + 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() - layout.operator(MUV_CPUVObjCopyUV.bl_idname, text="[Default]")\ - .uv_map = "" + + ops = layout.operator(OperatorCopyUV.bl_idname, text="[Default]") + ops.uv_map = "__default" + + ops = layout.operator(OperatorCopyUV.bl_idname, text="[All]") + ops.uv_map = "__all" + for m in uv_maps: - layout.operator(MUV_CPUVObjCopyUV.bl_idname, text=m).uv_map = m + ops = layout.operator(OperatorCopyUV.bl_idname, text=m) + ops.uv_map = m -class MUV_CPUVObjPasteUV(bpy.types.Operator): +class OperatorPasteUV(bpy.types.Operator): """ - Operation class: Paste UV coordinate per object + Operation class: Paste UV coordinate among objects """ - bl_idname = "object.muv_cpuv_obj_paste_uv" - bl_label = "Paste UV" - bl_description = "Paste UV coordinate" + 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(options={'HIDDEN'}) + uv_map = StringProperty(default="__default", options={'HIDDEN'}) copy_seams = BoolProperty( - name="Copy Seams", + 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.cpuv_obj - if not props.src_uvs or not props.src_pin_uvs: + props = context.scene.muv_props.copy_paste_uv_object + if not props.src_info: self.report({'WARNING'}, "Need copy UV at first") return {'CANCELLED'} @@ -149,90 +221,67 @@ class MUV_CPUVObjPasteUV(bpy.types.Operator): bpy.ops.object.mode_set(mode='EDIT') obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - - if (self.uv_map == "" or - self.uv_map not in bm.loops.layers.uv.keys()): - self.report({'INFO'}, "Paste UV coordinate per object") - else: - self.report( - {'INFO'}, - "Paste UV coordinate per object (UV map: %s)" - % (self.uv_map)) + bm = common.create_bmesh(obj) # get UV layer - if (self.uv_map == "" or - self.uv_map not in bm.loops.layers.uv.keys()): - if not bm.loops.layers.uv: - self.report( - {'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - else: - uv_layer = bm.loops.layers.uv[self.uv_map] + uv_layers = get_paste_uv_layers(self, obj, bm, props.src_info) + if not uv_layers: + return {'CANCELLED'} # get selected face - dest_uvs = [] - dest_pin_uvs = [] - dest_seams = [] - dest_face_indices = [] - for face in bm.faces: - dest_face_indices.append(face.index) - uvs = [l[uv_layer].uv.copy() for l in face.loops] - pin_uvs = [l[uv_layer].pin_uv for l in face.loops] - seams = [l.edge.seam for l in face.loops] - dest_uvs.append(uvs) - dest_pin_uvs.append(pin_uvs) - dest_seams.append(seams) - if len(props.src_uvs) != len(dest_uvs): - self.report( - {'WARNING'}, - "Number of faces is different from copied " + - "(src:%d, dest:%d)" - % (len(props.src_uvs), len(dest_uvs)) - ) - return {'CANCELLED'} + dest_info = {} + for layer in uv_layers: + face_info = [] + for face in bm.faces: + if face.select: + info = { + "uvs": [l[layer].uv.copy() for l in face.loops], + } + face_info.append(info) + key = list(props.src_info.keys())[0] + src_face_count = len(props.src_info[key]) + dest_face_count = len(face_info) + if src_face_count != dest_face_count: + self.report( + {'WARNING'}, + "Number of selected faces is different from copied" + + "(src:{}, dest:{})" + .format(src_face_count, dest_face_count)) + return {'CANCELLED'} + dest_info[layer.name] = face_info # paste - for i, idx in enumerate(dest_face_indices): - suv = props.src_uvs[i] - spuv = props.src_pin_uvs[i] - ss = props.src_seams[i] - duv = dest_uvs[i] - if len(suv) != len(duv): - self.report({'WARNING'}, "Some faces are different size") - return {'CANCELLED'} - suvs_fr = [uv for uv in suv] - spuvs_fr = [pin_uv for pin_uv in spuv] - ss_fr = [s for s in ss] - # paste UVs - for l, suv, spuv, ss in zip( - bm.faces[idx].loops, suvs_fr, spuvs_fr, ss_fr): - l[uv_layer].uv = suv - l[uv_layer].pin_uv = spuv - if self.copy_seams is True: - l.edge.seam = ss + ret = paste_uv(self, bm, props.src_info, dest_info, uv_layers, + 'N_N', 0, 0, self.copy_seams) + if ret: + return {'CANCELLED'} bmesh.update_edit_mesh(obj.data) if self.copy_seams is True: obj.data.show_edge_seams = True self.report( - {'INFO'}, "%s's UV coordinates are pasted" % (obj.name)) + {'INFO'}, "{}'s UV coordinates are pasted".format(obj.name)) return {'FINISHED'} -class MUV_CPUVObjPasteUVMenu(bpy.types.Menu): +class MenuPasteUV(bpy.types.Menu): """ - Menu class: Paste UV coordinate per object + Menu class: Paste UV coordinate among objects """ - bl_idname = "object.muv_cpuv_obj_paste_uv_menu" - bl_label = "Paste UV" - bl_description = "Paste UV coordinate per object" + 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 @@ -242,11 +291,20 @@ class MUV_CPUVObjPasteUVMenu(bpy.types.Menu): for obj in bpy.data.objects: if hasattr(obj.data, "uv_textures") and obj.select: uv_maps.extend(obj.data.uv_textures.keys()) - uv_maps = list(set(uv_maps)) - ops = layout.operator(MUV_CPUVObjPasteUV.bl_idname, text="[Default]") - ops.uv_map = "" - ops.copy_seams = sc.muv_cpuv_copy_seams + + ops = layout.operator(OperatorPasteUV.bl_idname, text="[Default]") + ops.uv_map = "__default" + ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams + + ops = layout.operator(OperatorPasteUV.bl_idname, text="[New]") + ops.uv_map = "__new" + ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams + + ops = layout.operator(OperatorPasteUV.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_CPUVObjPasteUV.bl_idname, text=m) + ops = layout.operator(OperatorPasteUV.bl_idname, text=m) ops.uv_map = m - ops.copy_seams = sc.muv_cpuv_copy_seams + ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams diff --git a/uv_magic_uv/op/copy_paste_uv_uvedit.py b/uv_magic_uv/op/copy_paste_uv_uvedit.py index 96908020..e591b5f1 100644 --- a/uv_magic_uv/op/copy_paste_uv_uvedit.py +++ b/uv_magic_uv/op/copy_paste_uv_uvedit.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>, Jace Priester" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import math from math import atan2, sin, cos @@ -33,28 +33,75 @@ from mathutils import Vector from .. import common -class MUV_CPUVIECopyUV(bpy.types.Operator): +__all__ = [ + 'Properties', + 'OperatorCopyUV', + 'OperatorPasteUV', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +class Properties: + @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 + + +class OperatorCopyUV(bpy.types.Operator): """ Operation class: Copy UV coordinate on UV/Image Editor """ - bl_idname = "uv.muv_cpuv_ie_copy_uv" - bl_label = "Copy UV" + bl_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'} @classmethod def poll(cls, context): - return context.mode == 'EDIT_MESH' + # 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.cpuv + props = context.scene.muv_props.copy_paste_uv_uvedit obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) uv_layer = bm.loops.layers.uv.verify() if common.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() + props.src_uvs = [] for face in bm.faces: if not face.select: continue @@ -70,22 +117,29 @@ class MUV_CPUVIECopyUV(bpy.types.Operator): return {'FINISHED'} -class MUV_CPUVIEPasteUV(bpy.types.Operator): +class OperatorPasteUV(bpy.types.Operator): """ Operation class: Paste UV coordinate on UV/Image Editor """ - bl_idname = "uv.muv_cpuv_ie_paste_uv" - bl_label = "Paste UV" + bl_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'} @classmethod def poll(cls, context): - return context.mode == 'EDIT_MESH' + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.copy_paste_uv_uvedit + if not props.src_uvs: + return False + return is_valid_context(context) def execute(self, context): - props = context.scene.muv_props.cpuv + props = context.scene.muv_props.copy_paste_uv_uvedit obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) uv_layer = bm.loops.layers.uv.verify() diff --git a/uv_magic_uv/op/flip_rotate_uv.py b/uv_magic_uv/op/flip_rotate_uv.py index 30f6b0f7..751bb8fb 100644 --- a/uv_magic_uv/op/flip_rotate_uv.py +++ b/uv_magic_uv/op/flip_rotate_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy import bmesh @@ -33,12 +33,59 @@ from bpy.props import ( from .. import common -class MUV_FlipRot(bpy.types.Operator): +__all__ = [ + 'Properties', + 'Operator', +] + + +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 + + +class Properties: + @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 + + +class Operator(bpy.types.Operator): """ Operation class: Flip and Rotate UV coordinate """ - bl_idname = "uv.muv_fliprot" + bl_idname = "uv.muv_flip_rotate_uv_operator" bl_label = "Flip/Rotate UV" bl_description = "Flip/Rotate UV coordinate" bl_options = {'REGISTER', 'UNDO'} @@ -60,6 +107,13 @@ class MUV_FlipRot(bpy.types.Operator): default=True ) + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + def execute(self, context): self.report({'INFO'}, "Flip/Rotate UV") obj = context.active_object diff --git a/uv_magic_uv/op/mirror_uv.py b/uv_magic_uv/op/mirror_uv.py index f4849d18..11ad2bca 100644 --- a/uv_magic_uv/op/mirror_uv.py +++ b/uv_magic_uv/op/mirror_uv.py @@ -20,13 +20,14 @@ __author__ = "Keith (Wahooney) Boshoff, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy from bpy.props import ( EnumProperty, FloatProperty, + BoolProperty, ) import bmesh from mathutils import Vector @@ -34,12 +35,64 @@ from mathutils import Vector from .. import common -class MUV_MirrorUV(bpy.types.Operator): +__all__ = [ + 'Properties', + 'Operator', +] + + +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 + + +class Properties: + @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 + + +class Operator(bpy.types.Operator): """ Operation class: Mirror UV """ - bl_idname = "uv.muv_mirror_uv" + bl_idname = "uv.muv_mirror_uv_operator" bl_label = "Mirror UV" bl_options = {'REGISTER', 'UNDO'} @@ -104,8 +157,10 @@ class MUV_MirrorUV(bpy.types.Operator): @classmethod def poll(cls, context): - obj = context.active_object - return obj and obj.type == 'MESH' + # 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 diff --git a/uv_magic_uv/op/move_uv.py b/uv_magic_uv/op/move_uv.py index 6382376c..a229ae34 100644 --- a/uv_magic_uv/op/move_uv.py +++ b/uv_magic_uv/op/move_uv.py @@ -20,23 +20,68 @@ __author__ = "kgeogeo, mem, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy import bmesh from mathutils import Vector +from bpy.props import BoolProperty +from .. import common -class MUV_MVUV(bpy.types.Operator): +__all__ = [ + 'Properties', + 'Operator', +] + + +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 + + +class Properties: + @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 + + +class Operator(bpy.types.Operator): """ - Operator class: Move UV from View3D + Operator class: Move UV """ - bl_idname = "view3d.muv_mvuv" - bl_label = "Move the UV from View3D" + bl_idname = "uv.muv_move_uv_operator" + bl_label = "Move UV" bl_options = {'REGISTER', 'UNDO'} + __running = False + def __init__(self): self.__topology_dict = [] self.__prev_mouse = Vector((0.0, 0.0)) @@ -44,7 +89,20 @@ class MUV_MVUV(bpy.types.Operator): self.__prev_offset_uv = Vector((0.0, 0.0)) self.__first_time = True self.__ini_uvs = [] - self.__running = False + self.__operating = False + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return False + if cls.is_running(context): + return False + return is_valid_context(context) + + @classmethod + def is_running(cls, _): + return cls.__running def __find_uv(self, context): bm = bmesh.from_edit_mesh(context.object.data) @@ -59,12 +117,7 @@ class MUV_MVUV(bpy.types.Operator): return topology_dict, uvs - @classmethod - def poll(cls, context): - return context.edit_object - def modal(self, context, event): - props = context.scene.muv_props.mvuv if self.__first_time is True: self.__prev_mouse = Vector(( event.mouse_region_x, event.mouse_region_y)) @@ -85,9 +138,9 @@ class MUV_MVUV(bpy.types.Operator): event.mouse_region_x, event.mouse_region_y)) # check if operation is started - if self.__running: + if not self.__operating: if event.type == 'LEFTMOUSE' and event.value == 'RELEASE': - self.__running = False + self.__operating = True return {'RUNNING_MODAL'} # update UV @@ -111,20 +164,24 @@ class MUV_MVUV(bpy.types.Operator): if event.type == cancel_btn and event.value == 'PRESS': for (fidx, vidx), uv in zip(self.__topology_dict, self.__ini_uvs): bm.faces[fidx].loops[vidx][active_uv].uv = uv - props.running = False + Operator.__running = False return {'FINISHED'} # confirmed if event.type == confirm_btn and event.value == 'PRESS': - props.running = False + Operator.__running = False return {'FINISHED'} return {'RUNNING_MODAL'} def execute(self, context): - props = context.scene.muv_props.mvuv - props.running = True - self.__running = True + Operator.__running = True + self.__operating = False self.__first_time = True + context.window_manager.modal_handler_add(self) self.__topology_dict, self.__ini_uvs = self.__find_uv(context) + + if context.area: + context.area.tag_redraw() + return {'RUNNING_MODAL'} diff --git a/uv_magic_uv/op/pack_uv.py b/uv_magic_uv/op/pack_uv.py index a780af3e..39340fda 100644 --- a/uv_magic_uv/op/pack_uv.py +++ b/uv_magic_uv/op/pack_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" from math import fabs @@ -38,7 +38,68 @@ from mathutils import Vector from .. import common -class MUV_PackUV(bpy.types.Operator): +__all__ = [ + 'Properties', + 'Operator', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +class Properties: + @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 + + +class Operator(bpy.types.Operator): """ Operation class: Pack UV with same UV islands are integrated Island matching algorithm @@ -47,7 +108,7 @@ class MUV_PackUV(bpy.types.Operator): - Same number of UV """ - bl_idname = "uv.muv_packuv" + bl_idname = "uv.muv_pack_uv_operator" bl_label = "Pack UV" bl_description = "Pack UV (Same UV Islands are integrated)" bl_options = {'REGISTER', 'UNDO'} @@ -79,6 +140,13 @@ class MUV_PackUV(bpy.types.Operator): 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) diff --git a/uv_magic_uv/op/preserve_uv_aspect.py b/uv_magic_uv/op/preserve_uv_aspect.py index bc2f1b81..cb11bd45 100644 --- a/uv_magic_uv/op/preserve_uv_aspect.py +++ b/uv_magic_uv/op/preserve_uv_aspect.py @@ -20,23 +20,93 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy import bmesh -from bpy.props import StringProperty, EnumProperty +from bpy.props import StringProperty, EnumProperty, BoolProperty from mathutils import Vector from .. import common -class MUV_PreserveUVAspect(bpy.types.Operator): +__all__ = [ + 'Properties', + 'Operator', +] + + +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 + + +class Properties: + @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 + + +class Operator(bpy.types.Operator): """ Operation class: Preserve UV Aspect """ - bl_idname = "uv.muv_preserve_uv_aspect" + bl_idname = "uv.muv_preserve_uv_aspect_operator" bl_label = "Preserve UV Aspect" bl_description = "Choose Image" bl_options = {'REGISTER', 'UNDO'} @@ -62,8 +132,10 @@ class MUV_PreserveUVAspect(bpy.types.Operator): @classmethod def poll(cls, context): - obj = context.active_object - return obj and obj.type == 'MESH' + # 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 diff --git a/uv_magic_uv/op/select_uv.py b/uv_magic_uv/op/select_uv.py new file mode 100644 index 00000000..3a7bcbc3 --- /dev/null +++ b/uv_magic_uv/op/select_uv.py @@ -0,0 +1,161 @@ +# <pep8-80 compliant> + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bpy +import bmesh +from bpy.props import BoolProperty + +from .. import common + + +__all__ = [ + 'Properties', + 'OperatorSelectFlipped', + 'OperatorSelectOverlapped', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +class Properties: + @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 + + +class OperatorSelectOverlapped(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'} + + +class OperatorSelectFlipped(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/op/smooth_uv.py b/uv_magic_uv/op/smooth_uv.py index aa9b22c0..31bef155 100644 --- a/uv_magic_uv/op/smooth_uv.py +++ b/uv_magic_uv/op/smooth_uv.py @@ -20,8 +20,8 @@ __author__ = "imdjs, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy import bmesh @@ -30,9 +30,72 @@ from bpy.props import BoolProperty, FloatProperty from .. import common -class MUV_AUVSmooth(bpy.types.Operator): +__all__ = [ + 'Properties', + 'Operator', +] - bl_idname = "uv.muv_auv_smooth" + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +class Properties: + @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 + + +class Operator(bpy.types.Operator): + + bl_idname = "uv.muv_smooth_uv_operator" bl_label = "Smooth" bl_description = "Smooth UV coordinates" bl_options = {'REGISTER', 'UNDO'} @@ -57,7 +120,10 @@ class MUV_AUVSmooth(bpy.types.Operator): @classmethod def poll(cls, context): - return context.mode == 'EDIT_MESH' + # 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 diff --git a/uv_magic_uv/op/texture_lock.py b/uv_magic_uv/op/texture_lock.py index d6c56f5a..4be97c62 100644 --- a/uv_magic_uv/op/texture_lock.py +++ b/uv_magic_uv/op/texture_lock.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import math from math import atan2, cos, sqrt, sin, fabs @@ -34,6 +34,14 @@ from bpy.props import BoolProperty from .. import common +__all__ = [ + 'Properties', + 'OperatorLock', + 'OperatorUnlock', + 'OperatorIntr', +] + + def get_vco(verts_orig, loop): """ Get vertex original coordinate from loop @@ -169,8 +177,13 @@ def calc_tri_vert(v0, v1, angle0, angle1): xd = 0 yd = 0 else: - xd = (b * b - a * a + d * d) / (2 * d) - yd = 2 * sqrt(s * (s - a) * (s - b) * (s - d)) / d + 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 @@ -179,18 +192,97 @@ def calc_tri_vert(v0, v1, angle0, angle1): return Vector((x1, y1)), Vector((x2, y2)) -class MUV_TexLockStart(bpy.types.Operator): +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 + + +class Properties: + @classmethod + def init_props(cls, scene): + class Props(): + verts_orig = None + + scene.muv_props.texture_lock = Props() + + def get_func(_): + return OperatorIntr.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 + + +class OperatorLock(bpy.types.Operator): """ - Operation class: Start Texture Lock + Operation class: Lock Texture """ - bl_idname = "uv.muv_texlock_start" - bl_label = "Start" - bl_description = "Start Texture Lock" + 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.texlock + 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: @@ -210,14 +302,14 @@ class MUV_TexLockStart(bpy.types.Operator): return {'FINISHED'} -class MUV_TexLockStop(bpy.types.Operator): +class OperatorUnlock(bpy.types.Operator): """ - Operation class: Stop Texture Lock + Operation class: Unlock Texture """ - bl_idname = "uv.muv_texlock_stop" - bl_label = "Stop" - bl_description = "Stop Texture Lock" + bl_idname = "uv.muv_texture_lock_operator_unlock" + bl_label = "Unlock Texture" + bl_description = "Unlock Texture" bl_options = {'REGISTER', 'UNDO'} connect = BoolProperty( @@ -225,9 +317,20 @@ class MUV_TexLockStop(bpy.types.Operator): 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 + return OperatorLock.is_ready(context) and is_valid_context(context) + def execute(self, context): sc = context.scene - props = sc.muv_props.texlock + 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: @@ -275,27 +378,81 @@ class MUV_TexLockStop(bpy.types.Operator): v_orig["moved"] = True bmesh.update_edit_mesh(obj.data) + props.verts_orig = None + return {'FINISHED'} -class MUV_TexLockUpdater(bpy.types.Operator): +class OperatorIntr(bpy.types.Operator): """ - Operation class: Texture locking updater + Operation class: Texture Lock (Interactive mode) """ - bl_idname = "uv.muv_texlock_updater" - bl_label = "Texture Lock Updater" - bl_description = "Texture Lock Updater" + 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.__timer = None + 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 """ - props = context.scene.muv_props.texlock - obj = bpy.context.active_object + obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) if common.check_version(2, 73, 0) >= 0: bm.verts.ensure_lookup_table() @@ -308,7 +465,7 @@ class MUV_TexLockUpdater(bpy.types.Operator): uv_layer = bm.loops.layers.uv.verify() verts = [v.index for v in bm.verts if v.select] - verts_orig = props.intr_verts_orig + verts_orig = self.__intr_verts_orig for vidx, v_orig in zip(verts, verts_orig): if vidx != v_orig["vidx"]: @@ -337,98 +494,40 @@ class MUV_TexLockUpdater(bpy.types.Operator): bmesh.update_edit_mesh(obj.data) common.redraw_all_areas() - props.intr_verts_orig = [ + 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): - props = context.scene.muv_props.texlock - if context.area: - context.area.tag_redraw() - if props.intr_running is False: - self.__handle_remove(context) + if not is_valid_context(context): + OperatorIntr.handle_remove(context) return {'FINISHED'} - if event.type == 'TIMER': - self.__update_uv(context) - - return {'PASS_THROUGH'} - - def __handle_add(self, context): - if self.__timer is None: - self.__timer = context.window_manager.event_timer_add( - 0.10, context.window) - context.window_manager.modal_handler_add(self) - def __handle_remove(self, context): - if self.__timer is not None: - context.window_manager.event_timer_remove(self.__timer) - self.__timer = None + if not OperatorIntr.is_running(context): + return {'FINISHED'} - def execute(self, context): - props = context.scene.muv_props.texlock - if props.intr_running is False: - self.__handle_add(context) - props.intr_running = True - return {'RUNNING_MODAL'} - else: - props.intr_running = False if context.area: context.area.tag_redraw() - return {'FINISHED'} - - -class MUV_TexLockIntrStart(bpy.types.Operator): - """ - Operation class: Start texture locking (Interactive mode) - """ - - bl_idname = "uv.muv_texlock_intr_start" - bl_label = "Texture Lock Start (Interactive mode)" - bl_description = "Texture Lock Start (Realtime UV update)" - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context): - props = context.scene.muv_props.texlock - if props.intr_running is True: - return {'CANCELLED'} + if event.type == 'TIMER': + if self.__sel_verts_changed(context): + self.__reinit_verts(context) + else: + self.__update_uv(context) - 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() + return {'PASS_THROUGH'} - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") + def invoke(self, context, _): + if not is_valid_context(context): return {'CANCELLED'} - props.intr_verts_orig = [ - {"vidx": v.index, "vco": v.co.copy(), "moved": False} - for v in bm.verts if v.select] - - bpy.ops.uv.muv_texlock_updater() - - return {'FINISHED'} - - -# Texture lock (Stop, Interactive mode) -class MUV_TexLockIntrStop(bpy.types.Operator): - """ - Operation class: Stop texture locking (interactive mode) - """ - - bl_idname = "uv.muv_texlock_intr_stop" - bl_label = "Texture Lock Stop (Interactive mode)" - bl_description = "Texture Lock Stop (Realtime UV update)" - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context): - props = context.scene.muv_props.texlock - if props.intr_running is False: - return {'CANCELLED'} + if not OperatorIntr.is_running(context): + OperatorIntr.handle_add(self, context) + return {'RUNNING_MODAL'} + else: + OperatorIntr.handle_remove(context) - bpy.ops.uv.muv_texlock_updater() + if context.area: + context.area.tag_redraw() return {'FINISHED'} diff --git a/uv_magic_uv/op/texture_projection.py b/uv_magic_uv/op/texture_projection.py index 77a81aa0..bdf0ad67 100644 --- a/uv_magic_uv/op/texture_projection.py +++ b/uv_magic_uv/op/texture_projection.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" from collections import namedtuple @@ -30,14 +30,32 @@ import bgl import bmesh import mathutils from bpy_extras import view3d_utils +from bpy.props import ( + BoolProperty, + EnumProperty, + FloatProperty, +) from .. import common +__all__ = [ + 'Properties', + 'Operator', + 'OperatorProject', +] + + 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 @@ -47,20 +65,20 @@ def get_canvas(context, magnitude): region_w = context.region.width region_h = context.region.height - canvas_w = region_w - prefs.texproj_canvas_padding[0] * 2.0 - canvas_h = region_h - prefs.texproj_canvas_padding[1] * 2.0 + 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_texproj_tex_image] + 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_texproj_adjust_window: + if sc.muv_texture_projection_adjust_window: ratio_x = canvas_w / tex_w ratio_y = canvas_h / tex_h - if sc.muv_texproj_apply_tex_aspect: + 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 @@ -68,7 +86,7 @@ def get_canvas(context, magnitude): len_x = canvas_w len_y = canvas_h else: - if sc.muv_texproj_apply_tex_aspect: + if sc.muv_texture_projection_apply_tex_aspect: len_x = tex_w * magnitude len_y = tex_h * magnitude else: @@ -104,44 +122,149 @@ def region_to_canvas(rg_vec, canvas): return cv_vec -class MUV_TexProjRenderer(bpy.types.Operator): +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 + + +class Properties: + @classmethod + def init_props(cls, scene): + def get_func(_): + return Operator.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 + + +class Operator(bpy.types.Operator): """ - Operation class: Render selected texture - No operation (only rendering texture) + Operation class: Texture Projection + Render texture """ - bl_idname = "uv.muv_texproj_renderer" + bl_idname = "uv.muv_texture_projection_operator" bl_description = "Render selected texture" bl_label = "Texture renderer" __handle = None - @staticmethod - def handle_add(obj, context): - MUV_TexProjRenderer.__handle = bpy.types.SpaceView3D.draw_handler_add( - MUV_TexProjRenderer.draw_texture, + @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( + Operator.draw_texture, (obj, context), 'WINDOW', 'POST_PIXEL') - @staticmethod - def handle_remove(): - if MUV_TexProjRenderer.__handle is not None: - bpy.types.SpaceView3D.draw_handler_remove( - MUV_TexProjRenderer.__handle, 'WINDOW') - MUV_TexProjRenderer.__handle = None + @classmethod + def handle_remove(cls): + if cls.__handle is not None: + bpy.types.SpaceView3D.draw_handler_remove(cls.__handle, 'WINDOW') + cls.__handle = None - @staticmethod - def draw_texture(_, context): + @classmethod + def draw_texture(cls, _, context): sc = context.scene + if not cls.is_running(context): + return + # no textures are selected - if sc.muv_texproj_tex_image == "None": + if sc.muv_texture_projection_tex_image == "None": return # get texture to be renderred - img = bpy.data.images[sc.muv_texproj_tex_image] + img = bpy.data.images[sc.muv_texture_projection_tex_image] # setup rendering region - rect = get_canvas(context, sc.muv_texproj_tex_magnitude) + rect = get_canvas(context, sc.muv_texture_projection_tex_magnitude) positions = [ [rect.x0, rect.y0], [rect.x0, rect.y1], @@ -170,74 +293,48 @@ class MUV_TexProjRenderer(bpy.types.Operator): # render texture bgl.glBegin(bgl.GL_QUADS) - bgl.glColor4f(1.0, 1.0, 1.0, sc.muv_texproj_tex_transparency) + 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 Operator.is_running(context): + Operator.handle_add(self, context) + else: + Operator.handle_remove() -class MUV_TexProjStart(bpy.types.Operator): - """ - Operation class: Start Texture Projection - """ - - bl_idname = "uv.muv_texproj_start" - bl_label = "Start Texture Projection" - bl_description = "Start Texture Projection" - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context): - props = context.scene.muv_props.texproj - if props.running is False: - MUV_TexProjRenderer.handle_add(self, context) - props.running = True - if context.area: - context.area.tag_redraw() - - return {'FINISHED'} - - -class MUV_TexProjStop(bpy.types.Operator): - """ - Operation class: Stop Texture Projection - """ - - bl_idname = "uv.muv_texproj_stop" - bl_label = "Stop Texture Projection" - bl_description = "Stop Texture Projection" - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context): - props = context.scene.muv_props.texproj - if props.running is True: - MUV_TexProjRenderer.handle_remove() - props.running = False if context.area: context.area.tag_redraw() return {'FINISHED'} -class MUV_TexProjProject(bpy.types.Operator): +class OperatorProject(bpy.types.Operator): """ Operation class: Project texture """ - bl_idname = "uv.muv_texproj_project" + 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): - obj = context.active_object - return obj is not None and obj.type == "MESH" + # we can not get area/space/region from console + if common.is_console_mode(): + return True + if not Operator.is_running(context): + return False + return is_valid_context(context) def execute(self, context): sc = context.scene - if sc.muv_texproj_tex_image == "None": + if sc.muv_texture_projection_tex_image == "None": self.report({'WARNING'}, "No textures are selected") return {'CANCELLED'} @@ -253,7 +350,7 @@ class MUV_TexProjProject(bpy.types.Operator): # get UV and texture layer if not bm.loops.layers.uv: - if sc.muv_texproj_assign_uvmap: + if sc.muv_texture_projection_assign_uvmap: bm.loops.layers.uv.new() else: self.report({'WARNING'}, @@ -278,14 +375,16 @@ class MUV_TexProjProject(bpy.types.Operator): v_canvas = [ region_to_canvas( v, - get_canvas(bpy.context, sc.muv_texproj_tex_magnitude)) - for v in v_screen + 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_texproj_tex_image] + 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 diff --git a/uv_magic_uv/op/texture_wrap.py b/uv_magic_uv/op/texture_wrap.py index 04669214..a7c58847 100644 --- a/uv_magic_uv/op/texture_wrap.py +++ b/uv_magic_uv/op/texture_wrap.py @@ -20,27 +20,98 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy import bmesh +from bpy.props import ( + BoolProperty, +) from .. import common -class MUV_TexWrapRefer(bpy.types.Operator): +__all__ = [ + 'Properties', + 'OperatorRefer', + 'OperatorSet', +] + + +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 + + +class Properties: + @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 + + +class OperatorRefer(bpy.types.Operator): """ Operation class: Refer UV """ - bl_idname = "uv.muv_texwrap_refer" + 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.texwrap + 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: @@ -61,19 +132,30 @@ class MUV_TexWrapRefer(bpy.types.Operator): return {'FINISHED'} -class MUV_TexWrapSet(bpy.types.Operator): +class OperatorSet(bpy.types.Operator): """ Operation class: Set UV """ - bl_idname = "uv.muv_texwrap_set" + 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.texwrap + 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: @@ -84,7 +166,7 @@ class MUV_TexWrapSet(bpy.types.Operator): return {'CANCELLED'} uv_layer = bm.loops.layers.uv.verify() - if sc.muv_texwrap_selseq: + if sc.muv_texture_wrap_selseq: sel_faces = [] for hist in bm.select_history: if isinstance(hist, bmesh.types.BMFace) and hist.select: @@ -206,7 +288,7 @@ class MUV_TexWrapSet(bpy.types.Operator): ref_face_index = tgt_face_index - if sc.muv_texwrap_set_and_refer: + if sc.muv_texture_wrap_set_and_refer: props.ref_face_index = tgt_face_index return {'FINISHED'} diff --git a/uv_magic_uv/op/transfer_uv.py b/uv_magic_uv/op/transfer_uv.py index 132f395e..ef6fc3be 100644 --- a/uv_magic_uv/op/transfer_uv.py +++ b/uv_magic_uv/op/transfer_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>, Mifth, MaxRobinot" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" from collections import OrderedDict @@ -32,19 +32,84 @@ from bpy.props import BoolProperty from .. import common -class MUV_TransUVCopy(bpy.types.Operator): +__all__ = [ + 'OperatorCopyUV', + 'OperatorPasteUV', +] + + +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 + + +class Properties: + @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 + + +class OperatorCopyUV(bpy.types.Operator): """ Operation class: Transfer UV copy Topological based copy """ - bl_idname = "uv.muv_transuv_copy" - bl_label = "Transfer UV Copy" - bl_description = "Transfer UV Copy (Topological based copy)" + bl_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 is_valid_context(context) + def execute(self, context): - props = context.scene.muv_props.transuv + 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: @@ -56,7 +121,7 @@ class MUV_TransUVCopy(bpy.types.Operator): return {'CANCELLED'} uv_layer = bm.loops.layers.uv.verify() - props.topology_copied.clear() + props.topology_copied = [] # get selected faces active_face = bm.faces.active @@ -82,21 +147,23 @@ class MUV_TransUVCopy(bpy.types.Operator): pin_uvs = [l.pin_uv for l in uv_loops] seams = [e.seam for e in edges] props.topology_copied.append([uvs, pin_uvs, seams]) + else: + return {'CANCELLED'} bmesh.update_edit_mesh(active_obj.data) return {'FINISHED'} -class MUV_TransUVPaste(bpy.types.Operator): +class OperatorPasteUV(bpy.types.Operator): """ Operation class: Transfer UV paste Topological based paste """ - bl_idname = "uv.muv_transuv_paste" - bl_label = "Transfer UV Paste" - bl_description = "Transfer UV Paste (Topological based paste)" + bl_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( @@ -110,8 +177,19 @@ class MUV_TransUVPaste(bpy.types.Operator): default=True ) + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.transfer_uv + if not props.topology_copied: + return False + return is_valid_context(context) + def execute(self, context): - props = context.scene.muv_props.transuv + 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: @@ -153,7 +231,7 @@ class MUV_TransUVPaste(bpy.types.Operator): {'WARNING'}, "Mesh has different amount of faces" ) - return {'FINISHED'} + return {'CANCELLED'} for j, face_data in enumerate(all_sorted_faces.values()): copied_data = props.topology_copied[j] @@ -175,6 +253,8 @@ class MUV_TransUVPaste(bpy.types.Operator): uvloop.pin_uv = copied_data[1][k] if self.copy_seams: edge.seam = copied_data[2][k] + else: + return {'CANCELLED'} bmesh.update_edit_mesh(active_obj.data) if self.copy_seams: @@ -302,7 +382,7 @@ def parse_faces( used_verts.update(shared_face.verts) used_edges.update(shared_face.edges) - if common.DEBUG: + if common.is_debug_mode(): shared_face.select = True # test which faces are parsed new_shared_faces.append(shared_face) diff --git a/uv_magic_uv/op/unwrap_constraint.py b/uv_magic_uv/op/unwrap_constraint.py index e98879b7..b2368fc4 100644 --- a/uv_magic_uv/op/unwrap_constraint.py +++ b/uv_magic_uv/op/unwrap_constraint.py @@ -18,8 +18,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy import bmesh @@ -32,12 +32,65 @@ from bpy.props import ( from .. import common -class MUV_UnwrapConstraint(bpy.types.Operator): +__all__ = [ + 'Properties', + 'Operator', +] + + +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 + + +class Properties: + @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 + + +class Operator(bpy.types.Operator): """ Operation class: Unwrap with constrain UV coordinate """ - bl_idname = "uv.muv_unwrap_constraint" + bl_idname = "uv.muv_unwrap_constraint_operator" bl_label = "Unwrap Constraint" bl_description = "Unwrap while keeping uv coordinate" bl_options = {'REGISTER', 'UNDO'} @@ -83,6 +136,13 @@ class MUV_UnwrapConstraint(bpy.types.Operator): 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) diff --git a/uv_magic_uv/op/uv_bounding_box.py b/uv_magic_uv/op/uv_bounding_box.py index 9ebc76c4..4aa8874b 100644 --- a/uv_magic_uv/op/uv_bounding_box.py +++ b/uv_magic_uv/op/uv_bounding_box.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" from enum import IntEnum import math @@ -30,14 +30,101 @@ import bpy import bgl import mathutils import bmesh +from bpy.props import BoolProperty, EnumProperty from .. import common +__all__ = [ + 'Properties', + 'Operator', +] + + MAX_VALUE = 100000.0 -class MUV_UVBBCmd(): +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +class Properties: + @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 Operator.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 """ @@ -52,7 +139,7 @@ class MUV_UVBBCmd(): return mat -class MUV_UVBBTranslationCmd(MUV_UVBBCmd): +class TranslationCommand(CommandBase): """ Custom class: Translation operation """ @@ -76,7 +163,7 @@ class MUV_UVBBTranslationCmd(MUV_UVBBCmd): self.__y = y -class MUV_UVBBRotationCmd(MUV_UVBBCmd): +class RotationCommand(CommandBase): """ Custom class: Rotation operation """ @@ -107,7 +194,7 @@ class MUV_UVBBRotationCmd(MUV_UVBBCmd): self.__y = y -class MUV_UVBBScalingCmd(MUV_UVBBCmd): +class ScalingCommand(CommandBase): """ Custom class: Scaling operation """ @@ -158,7 +245,7 @@ class MUV_UVBBScalingCmd(MUV_UVBBCmd): self.__y = y -class MUV_UVBBUniformScalingCmd(MUV_UVBBCmd): +class UniformScalingCommand(CommandBase): """ Custom class: Uniform Scaling operation """ @@ -222,7 +309,7 @@ class MUV_UVBBUniformScalingCmd(MUV_UVBBCmd): self.__y = y -class MUV_UVBBCmdExecuter(): +class CommandExecuter(): """ Custom class: manage command history and execute command """ @@ -288,67 +375,7 @@ class MUV_UVBBCmdExecuter(): self.__cmd_list.append(cmd) -class MUV_UVBBRenderer(bpy.types.Operator): - """ - Operation class: Render UV bounding box - """ - - bl_idname = "uv.muv_uvbb_renderer" - bl_label = "UV Bounding Box Renderer" - bl_description = "Bounding Box Renderer about UV in Image Editor" - - __handle = None - - @staticmethod - def handle_add(obj, context): - if MUV_UVBBRenderer.__handle is None: - sie = bpy.types.SpaceImageEditor - MUV_UVBBRenderer.__handle = sie.draw_handler_add( - MUV_UVBBRenderer.draw_bb, - (obj, context), "WINDOW", "POST_PIXEL") - - @staticmethod - def handle_remove(): - if MUV_UVBBRenderer.__handle is not None: - sie = bpy.types.SpaceImageEditor - sie.draw_handler_remove( - MUV_UVBBRenderer.__handle, "WINDOW") - MUV_UVBBRenderer.__handle = None - - @staticmethod - def __draw_ctrl_point(context, pos): - """ - Draw control point - """ - prefs = context.user_preferences.addons["uv_magic_uv"].preferences - cp_size = prefs.uvbb_cp_size - offset = cp_size / 2 - verts = [ - [pos.x - offset, pos.y - offset], - [pos.x - offset, pos.y + offset], - [pos.x + offset, pos.y + offset], - [pos.x + offset, pos.y - offset] - ] - bgl.glEnable(bgl.GL_BLEND) - bgl.glBegin(bgl.GL_QUADS) - bgl.glColor4f(1.0, 1.0, 1.0, 1.0) - for (x, y) in verts: - bgl.glVertex2f(x, y) - bgl.glEnd() - - @staticmethod - def draw_bb(_, context): - """ - Draw bounding box - """ - props = context.scene.muv_props.uvbb - for cp in props.ctrl_points: - MUV_UVBBRenderer.__draw_ctrl_point( - context, mathutils.Vector( - context.region.view2d.view_to_region(cp.x, cp.y))) - - -class MUV_UVBBState(IntEnum): +class State(IntEnum): """ Enum: State definition used by MUV_UVBBStateMgr """ @@ -369,7 +396,7 @@ class MUV_UVBBState(IntEnum): UNIFORM_SCALING_4 = 14 -class MUV_UVBBStateBase(): +class StateBase(): """ Custom class: Base class of state """ @@ -381,7 +408,7 @@ class MUV_UVBBStateBase(): raise NotImplementedError -class MUV_UVBBStateNone(MUV_UVBBStateBase): +class StateNone(StateBase): """ Custom class: No state @@ -397,8 +424,8 @@ class MUV_UVBBStateNone(MUV_UVBBStateBase): Update state """ prefs = context.user_preferences.addons["uv_magic_uv"].preferences - cp_react_size = prefs.uvbb_cp_react_size - is_uscaling = context.scene.muv_uvbb_uniform_scaling + 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) @@ -413,16 +440,16 @@ class MUV_UVBBStateNone(MUV_UVBBStateBase): arr = [1, 3, 6, 8] if i in arr: return ( - MUV_UVBBState.UNIFORM_SCALING_1 + + State.UNIFORM_SCALING_1 + arr.index(i) ) else: - return MUV_UVBBState.TRANSLATING + i + return State.TRANSLATING + i - return MUV_UVBBState.NONE + return State.NONE -class MUV_UVBBStateTranslating(MUV_UVBBStateBase): +class StateTranslating(StateBase): """ Custom class: Translating state """ @@ -431,19 +458,19 @@ class MUV_UVBBStateTranslating(MUV_UVBBStateBase): super().__init__() self.__cmd_exec = cmd_exec ix, iy = ctrl_points[0].x, ctrl_points[0].y - self.__cmd_exec.append(MUV_UVBBTranslationCmd(ix, iy)) + 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 MUV_UVBBState.NONE + return State.NONE if event.type == 'MOUSEMOVE': x, y = mouse_view.x, mouse_view.y self.__cmd_exec.top().set(x, y) - return MUV_UVBBState.TRANSLATING + return State.TRANSLATING -class MUV_UVBBStateScaling(MUV_UVBBStateBase): +class StateScaling(StateBase): """ Custom class: Scaling state """ @@ -460,19 +487,19 @@ class MUV_UVBBStateScaling(MUV_UVBBStateBase): dir_x, dir_y = dir_x_list[idx], dir_y_list[idx] mat = self.__cmd_exec.execute(end=self.__cmd_exec.undo_size()) self.__cmd_exec.append( - MUV_UVBBScalingCmd(ix, iy, ox, oy, dir_x, dir_y, mat.inverted())) + 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 MUV_UVBBState.NONE + 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 MUV_UVBBStateUniformScaling(MUV_UVBBStateBase): +class StateUniformScaling(StateBase): """ Custom class: Uniform Scaling state """ @@ -483,17 +510,17 @@ class MUV_UVBBStateUniformScaling(MUV_UVBBStateBase): self.__cmd_exec = cmd_exec icp_idx = [1, 3, 6, 8] ocp_idx = [8, 6, 3, 1] - idx = state - MUV_UVBBState.UNIFORM_SCALING_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(MUV_UVBBUniformScalingCmd( + 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 MUV_UVBBState.NONE + return State.NONE if event.type == 'MOUSEMOVE': x, y = mouse_view.x, mouse_view.y self.__cmd_exec.top().set(x, y) @@ -501,7 +528,7 @@ class MUV_UVBBStateUniformScaling(MUV_UVBBStateBase): return self.__state -class MUV_UVBBStateRotating(MUV_UVBBStateBase): +class StateRotating(StateBase): """ Custom class: Rotating state """ @@ -511,27 +538,27 @@ class MUV_UVBBStateRotating(MUV_UVBBStateBase): self.__cmd_exec = cmd_exec ix, iy = ctrl_points[9].x, ctrl_points[9].y ox, oy = ctrl_points[0].x, ctrl_points[0].y - self.__cmd_exec.append(MUV_UVBBRotationCmd(ix, iy, ox, oy)) + 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 MUV_UVBBState.NONE + return State.NONE if event.type == 'MOUSEMOVE': x, y = mouse_view.x, mouse_view.y self.__cmd_exec.top().set(x, y) - return MUV_UVBBState.ROTATING + return State.ROTATING -class MUV_UVBBStateMgr(): +class StateManager(): """ Custom class: Manage state about this feature """ def __init__(self, cmd_exec): self.__cmd_exec = cmd_exec # command executer - self.__state = MUV_UVBBState.NONE # current state - self.__state_obj = MUV_UVBBStateNone(self.__cmd_exec) + self.__state = State.NONE # current state + self.__state_obj = StateNone(self.__cmd_exec) def __update_state(self, next_state, ctrl_points): """ @@ -541,18 +568,18 @@ class MUV_UVBBStateMgr(): if next_state == self.__state: return obj = None - if next_state == MUV_UVBBState.TRANSLATING: - obj = MUV_UVBBStateTranslating(self.__cmd_exec, ctrl_points) - elif MUV_UVBBState.SCALING_1 <= next_state <= MUV_UVBBState.SCALING_8: - obj = MUV_UVBBStateScaling( + 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 == MUV_UVBBState.ROTATING: - obj = MUV_UVBBStateRotating(self.__cmd_exec, ctrl_points) - elif next_state == MUV_UVBBState.NONE: - obj = MUV_UVBBStateNone(self.__cmd_exec) - elif (MUV_UVBBState.UNIFORM_SCALING_1 <= next_state <= - MUV_UVBBState.UNIFORM_SCALING_4): - obj = MUV_UVBBStateUniformScaling( + 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: @@ -569,34 +596,97 @@ class MUV_UVBBStateMgr(): context, event, ctrl_points, mouse_view) self.__update_state(next_state, ctrl_points) + return self.__state + -class MUV_UVBBUpdater(bpy.types.Operator): +class Operator(bpy.types.Operator): """ - Operation class: Update state and handle event by modal function + Operation class: UV Bounding Box """ - bl_idname = "uv.muv_uvbb_updater" - bl_label = "UV Bounding Box Updater" - bl_description = "Update 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 = MUV_UVBBCmdExecuter() # Command executer - self.__state_mgr = MUV_UVBBStateMgr(self.__cmd_exec) # State Manager + self.__cmd_exec = CommandExecuter() # Command executor + self.__state_mgr = StateManager(self.__cmd_exec) # State Manager - def __handle_add(self, context): - if self.__timer is None: - self.__timer = context.window_manager.event_timer_add( + __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(self) - MUV_UVBBRenderer.handle_add(self, context) + context.window_manager.modal_handler_add(obj) - def __handle_remove(self, context): - MUV_UVBBRenderer.handle_remove() - if self.__timer is not None: - context.window_manager.event_timer_remove(self.__timer) - self.__timer = None + @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 Operator.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): """ @@ -615,10 +705,10 @@ class MUV_UVBBUpdater(bpy.types.Operator): if not f.select: continue for i, l in enumerate(f.loops): - if sc.muv_uvbb_boundary == 'UV_SEL': + 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_uvbb_boundary == 'UV': + 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 @@ -688,16 +778,23 @@ class MUV_UVBBUpdater(bpy.types.Operator): return [trans_mat * cp for cp in ctrl_points_ini] def modal(self, context, event): - props = context.scene.muv_props.uvbb + props = context.scene.muv_props.uv_bounding_box common.redraw_all_areas() - if props.running is False: - self.__handle_remove(context) + + if not Operator.is_running(context): return {'FINISHED'} - area, _, _ = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') + if not is_valid_context(context): + Operator.handle_remove(context) + return {'FINISHED'} - if event.mouse_region_x < 0 or event.mouse_region_x > area.width or \ - event.mouse_region_y < 0 or event.mouse_region_y > area.height: + 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': @@ -706,27 +803,30 @@ class MUV_UVBBUpdater(bpy.types.Operator): props.ctrl_points = self.__update_ctrl_point( props.ctrl_points_ini, trans_mat) - self.__state_mgr.update(context, props.ctrl_points, event) + state = self.__state_mgr.update(context, props.ctrl_points, event) + if state == State.NONE: + return {'PASS_THROUGH'} return {'RUNNING_MODAL'} - def execute(self, context): - props = context.scene.muv_props.uvbb + def invoke(self, context, _): + props = context.scene.muv_props.uv_bounding_box - if props.running is True: - props.running = False + if Operator.is_running(context): + Operator.handle_remove(context) return {'FINISHED'} props.uv_info_ini = self.__get_uv_info(context) if props.uv_info_ini is None: return {'CANCELLED'} + + Operator.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) - self.__handle_add(context) - props.running = True return {'RUNNING_MODAL'} diff --git a/uv_magic_uv/op/uv_inspection.py b/uv_magic_uv/op/uv_inspection.py index 60a754a3..0c05e03d 100644 --- a/uv_magic_uv/op/uv_inspection.py +++ b/uv_magic_uv/op/uv_inspection.py @@ -20,343 +20,161 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy import bmesh import bgl -from mathutils import Vector +from bpy.props import BoolProperty, EnumProperty from .. import common -def is_polygon_same(points1, points2): - if len(points1) != len(points2): - return False - - pts1 = points1.as_list() - pts2 = points2.as_list() - - for p1 in pts1: - for p2 in pts2: - diff = p2 - p1 - if diff.length < 0.0000001: - pts2.remove(p2) - break - else: - return False - - return True - - -def is_segment_intersect(start1, end1, start2, end2): - seg1 = end1 - start1 - seg2 = end2 - start2 - - a1 = -seg1.y - b1 = seg1.x - d1 = -(a1 * start1.x + b1 * start1.y) - - a2 = -seg2.y - b2 = seg2.x - d2 = -(a2 * start2.x + b2 * start2.y) +__all__ = [ + 'Properties', + 'OperatorRender', + 'OperatorUpdate', +] - seg1_line2_start = a2 * start1.x + b2 * start1.y + d2 - seg1_line2_end = a2 * end1.x + b2 * end1.y + d2 - seg2_line1_start = a1 * start2.x + b1 * start2.y + d1 - seg2_line1_end = a1 * end2.x + b1 * end2.y + d1 +def is_valid_context(context): + obj = context.object - if (seg1_line2_start * seg1_line2_end >= 0) or \ - (seg2_line1_start * seg2_line1_end >= 0): - return False, None - - u = seg1_line2_start / (seg1_line2_start - seg1_line2_end) - out = start1 + u * seg1 - - return True, out - - -class RingBuffer: - def __init__(self, arr): - self.__buffer = arr.copy() - self.__pointer = 0 - - def __repr__(self): - return repr(self.__buffer) - - def __len__(self): - return len(self.__buffer) - - def insert(self, val, offset=0): - self.__buffer.insert(self.__pointer + offset, val) - - def head(self): - return self.__buffer[0] - - def tail(self): - return self.__buffer[-1] - - def get(self, offset=0): - size = len(self.__buffer) - val = self.__buffer[(self.__pointer + offset) % size] - return val - - def next(self): - size = len(self.__buffer) - self.__pointer = (self.__pointer + 1) % size - - def reset(self): - self.__pointer = 0 - - def find(self, obj): - try: - idx = self.__buffer.index(obj) - except ValueError: - return None - return self.__buffer[idx] - - def find_and_next(self, obj): - size = len(self.__buffer) - idx = self.__buffer.index(obj) - self.__pointer = (idx + 1) % size - - def find_and_set(self, obj): - idx = self.__buffer.index(obj) - self.__pointer = idx - - def as_list(self): - return self.__buffer.copy() - - def reverse(self): - self.__buffer.reverse() - self.reset() - - -# clip: reference polygon -# subject: tested polygon -def do_weiler_atherton_cliping(clip, subject, uv_layer, mode): - - clip_uvs = RingBuffer([l[uv_layer].uv.copy() for l in clip.loops]) - if is_polygon_flipped(clip_uvs): - clip_uvs.reverse() - subject_uvs = RingBuffer([l[uv_layer].uv.copy() for l in subject.loops]) - if is_polygon_flipped(subject_uvs): - subject_uvs.reverse() - - common.debug_print("===== Clip UV List =====") - common.debug_print(clip_uvs) - common.debug_print("===== Subject UV List =====") - common.debug_print(subject_uvs) - - # check if clip and subject is overlapped completely - if is_polygon_same(clip_uvs, subject_uvs): - polygons = [subject_uvs.as_list()] - common.debug_print("===== Polygons Overlapped Completely =====") - common.debug_print(polygons) - return True, polygons - - # check if subject is in clip - if is_points_in_polygon(subject_uvs, clip_uvs): - polygons = [subject_uvs.as_list()] - return True, polygons - - # check if clip is in subject - if is_points_in_polygon(clip_uvs, subject_uvs): - polygons = [subject_uvs.as_list()] - return True, polygons - - # check if clip and subject is overlapped partially - intersections = [] - while True: - subject_uvs.reset() - while True: - uv_start1 = clip_uvs.get() - uv_end1 = clip_uvs.get(1) - uv_start2 = subject_uvs.get() - uv_end2 = subject_uvs.get(1) - intersected, point = is_segment_intersect(uv_start1, uv_end1, - uv_start2, uv_end2) - if intersected: - clip_uvs.insert(point, 1) - subject_uvs.insert(point, 1) - intersections.append([point, - [clip_uvs.get(), clip_uvs.get(1)]]) - subject_uvs.next() - if subject_uvs.get() == subject_uvs.head(): - break - clip_uvs.next() - if clip_uvs.get() == clip_uvs.head(): - break - - common.debug_print("===== Intersection List =====") - common.debug_print(intersections) - - # no intersection, so subject and clip is not overlapped - if not intersections: - return False, None - - def get_intersection_pair(intersections, key): - for sect in intersections: - if sect[0] == key: - return sect[1] - - return None - - # make enter/exit pair - subject_uvs.reset() - subject_entering = [] - subject_exiting = [] - clip_entering = [] - clip_exiting = [] - intersect_uv_list = [] - while True: - pair = get_intersection_pair(intersections, subject_uvs.get()) - if pair: - sub = subject_uvs.get(1) - subject_uvs.get(-1) - inter = pair[1] - pair[0] - cross = sub.x * inter.y - inter.x * sub.y - if cross < 0: - subject_entering.append(subject_uvs.get()) - clip_exiting.append(subject_uvs.get()) - else: - subject_exiting.append(subject_uvs.get()) - clip_entering.append(subject_uvs.get()) - intersect_uv_list.append(subject_uvs.get()) - - subject_uvs.next() - if subject_uvs.get() == subject_uvs.head(): - break + # 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 - common.debug_print("===== Enter List =====") - common.debug_print(clip_entering) - common.debug_print(subject_entering) - common.debug_print("===== Exit List =====") - common.debug_print(clip_exiting) - common.debug_print(subject_exiting) - - # for now, can't handle the situation when fulfill all below conditions - # * two faces have common edge - # * each face is intersected - # * Show Mode is "Part" - # so for now, ignore this situation - if len(subject_entering) != len(subject_exiting): - if mode == 'FACE': - polygons = [subject_uvs.as_list()] - return True, polygons - return False, None - - def traverse(current_list, entering, exiting, poly, current, other_list): - result = current_list.find(current) - if not result: - return None - if result != current: - print("Internal Error") - return None - - # enter - if entering.count(current) >= 1: - entering.remove(current) - - current_list.find_and_next(current) - current = current_list.get() - - while exiting.count(current) == 0: - poly.append(current.copy()) - current_list.find_and_next(current) - current = current_list.get() - - # exit - poly.append(current.copy()) - exiting.remove(current) - - other_list.find_and_set(current) - return other_list.get() - - # Traverse - polygons = [] - current_uv_list = subject_uvs - other_uv_list = clip_uvs - current_entering = subject_entering - current_exiting = subject_exiting - - poly = [] - current_uv = current_entering[0] - - while True: - current_uv = traverse(current_uv_list, current_entering, - current_exiting, poly, current_uv, other_uv_list) - - if current_uv_list == subject_uvs: - current_uv_list = clip_uvs - other_uv_list = subject_uvs - current_entering = clip_entering - current_exiting = clip_exiting - common.debug_print("-- Next: Clip --") - else: - current_uv_list = subject_uvs - other_uv_list = clip_uvs - current_entering = subject_entering - current_exiting = subject_exiting - common.debug_print("-- Next: Subject --") - - common.debug_print(clip_entering) - common.debug_print(clip_exiting) - common.debug_print(subject_entering) - common.debug_print(subject_exiting) - - if not clip_entering and not clip_exiting \ - and not subject_entering and not subject_exiting: + # '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 - polygons.append(poly) - - common.debug_print("===== Polygons Overlapped Partially =====") - common.debug_print(polygons) - - return True, polygons + return True -class MUV_UVInspRenderer(bpy.types.Operator): +class Properties: + @classmethod + def init_props(cls, scene): + class Props(): + overlapped_info = [] + flipped_info = [] + + scene.muv_props.uv_inspection = Props() + + def get_func(_): + return OperatorRender.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 + + +class OperatorRender(bpy.types.Operator): """ Operation class: Render UV Inspection No operation (only rendering) """ - bl_idname = "uv.muv_uvinsp_renderer" + bl_idname = "uv.muv_uv_inspection_operator_render" bl_description = "Render overlapped/flipped UVs" bl_label = "Overlapped/Flipped UV renderer" __handle = None - @staticmethod - def handle_add(obj, context): + @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 - MUV_UVInspRenderer.__handle = sie.draw_handler_add( - MUV_UVInspRenderer.draw, (obj, context), 'WINDOW', 'POST_PIXEL') + cls.__handle = sie.draw_handler_add( + OperatorRender.draw, (obj, context), 'WINDOW', 'POST_PIXEL') - @staticmethod - def handle_remove(): - if MUV_UVInspRenderer.__handle is not None: + @classmethod + def handle_remove(cls): + if cls.__handle is not None: bpy.types.SpaceImageEditor.draw_handler_remove( - MUV_UVInspRenderer.__handle, 'WINDOW') - MUV_UVInspRenderer.__handle = None + cls.__handle, 'WINDOW') + cls.__handle = None @staticmethod def draw(_, context): sc = context.scene - props = sc.muv_props.uvinsp + props = sc.muv_props.uv_inspection prefs = context.user_preferences.addons["uv_magic_uv"].preferences + if not OperatorRender.is_running(context): + return + # OpenGL configuration bgl.glEnable(bgl.GL_BLEND) # render overlapped UV - if sc.muv_uvinsp_show_overlapped: - color = prefs.uvinsp_overlapped_color + if sc.muv_uv_inspection_show_overlapped: + color = prefs.uv_inspection_overlapped_color for info in props.overlapped_info: - if sc.muv_uvinsp_show_mode == 'PART': + 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]) @@ -365,7 +183,7 @@ class MUV_UVInspRenderer(bpy.types.Operator): uv.x, uv.y) bgl.glVertex2f(x, y) bgl.glEnd() - elif sc.muv_uvinsp_show_mode == 'FACE': + 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"]: @@ -374,10 +192,10 @@ class MUV_UVInspRenderer(bpy.types.Operator): bgl.glEnd() # render flipped UV - if sc.muv_uvinsp_show_flipped: - color = prefs.uvinsp_flipped_color + if sc.muv_uv_inspection_show_flipped: + color = prefs.uv_inspection_flipped_color for info in props.flipped_info: - if sc.muv_uvinsp_show_mode == 'PART': + 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]) @@ -386,7 +204,7 @@ class MUV_UVInspRenderer(bpy.types.Operator): uv.x, uv.y) bgl.glVertex2f(x, y) bgl.glEnd() - elif sc.muv_uvinsp_show_mode == 'FACE': + 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"]: @@ -394,100 +212,22 @@ class MUV_UVInspRenderer(bpy.types.Operator): bgl.glVertex2f(x, y) bgl.glEnd() + def invoke(self, context, _): + if not OperatorRender.is_running(context): + update_uvinsp_info(context) + OperatorRender.handle_add(self, context) + else: + OperatorRender.handle_remove() -def is_polygon_flipped(points): - area = 0.0 - for i in range(len(points)): - uv1 = points.get(i) - uv2 = points.get(i + 1) - a = uv1.x * uv2.y - uv1.y * uv2.x - area = area + a - if area < 0: - # clock-wise - return True - return False - - -def is_point_in_polygon(point, subject_points): - count = 0 - for i in range(len(subject_points)): - uv_start1 = subject_points.get(i) - uv_end1 = subject_points.get(i + 1) - uv_start2 = point - uv_end2 = Vector((1000000.0, point.y)) - intersected, _ = is_segment_intersect(uv_start1, uv_end1, - uv_start2, uv_end2) - if intersected: - count = count + 1 - - return count % 2 - - -def is_points_in_polygon(points, subject_points): - for i in range(len(points)): - internal = is_point_in_polygon(points.get(i), subject_points) - if not internal: - return False - - return True - + if context.area: + context.area.tag_redraw() -def get_overlapped_uv_info(bm, faces, uv_layer, mode): - # at first, check island overlapped - isl = common.get_island_info_from_faces(bm, faces, uv_layer) - overlapped_isl_pairs = [] - for i, i1 in enumerate(isl): - for i2 in isl[i + 1:]: - if (i1["max"].x < i2["min"].x) or (i2["max"].x < i1["min"].x) or \ - (i1["max"].y < i2["min"].y) or (i2["max"].y < i1["min"].y): - continue - overlapped_isl_pairs.append([i1, i2]) - - # next, check polygon overlapped - overlapped_uvs = [] - for oip in overlapped_isl_pairs: - for clip in oip[0]["faces"]: - f_clip = clip["face"] - for subject in oip[1]["faces"]: - f_subject = subject["face"] - - # fast operation, apply bounding box algorithm - if (clip["max_uv"].x < subject["min_uv"].x) or \ - (subject["max_uv"].x < clip["min_uv"].x) or \ - (clip["max_uv"].y < subject["min_uv"].y) or \ - (subject["max_uv"].y < clip["min_uv"].y): - continue - - # slow operation, apply Weiler-Atherton cliping algorithm - result, polygons = do_weiler_atherton_cliping(f_clip, - f_subject, - uv_layer, mode) - if result: - subject_uvs = [l[uv_layer].uv.copy() - for l in f_subject.loops] - overlapped_uvs.append({"clip_face": f_clip, - "subject_face": f_subject, - "subject_uvs": subject_uvs, - "polygons": polygons}) - - return overlapped_uvs - - -def get_flipped_uv_info(faces, uv_layer): - flipped_uvs = [] - for f in faces: - polygon = RingBuffer([l[uv_layer].uv.copy() for l in f.loops]) - if is_polygon_flipped(polygon): - uvs = [l[uv_layer].uv.copy() for l in f.loops] - flipped_uvs.append({"face": f, "uvs": uvs, - "polygons": [polygon.as_list()]}) - - return flipped_uvs + return {'FINISHED'} def update_uvinsp_info(context): sc = context.scene - props = sc.muv_props.uvinsp + props = sc.muv_props.uv_inspection obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) @@ -499,125 +239,34 @@ def update_uvinsp_info(context): sel_faces = [f for f in bm.faces] else: sel_faces = [f for f in bm.faces if f.select] - props.overlapped_info = get_overlapped_uv_info(bm, sel_faces, uv_layer, - sc.muv_uvinsp_show_mode) - props.flipped_info = get_flipped_uv_info(sel_faces, uv_layer) + 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) -class MUV_UVInspUpdate(bpy.types.Operator): +class OperatorUpdate(bpy.types.Operator): """ Operation class: Update """ - bl_idname = "uv.muv_uvinsp_update" - bl_label = "Update" - bl_description = "Update Overlapped/Flipped UV" + bl_idname = "uv.muv_uv_inspection_operator_update" + bl_label = "Update UV Inspection" + bl_description = "Update UV Inspection" bl_options = {'REGISTER', 'UNDO'} - def execute(self, context): - update_uvinsp_info(context) - - if context.area: - context.area.tag_redraw() - - return {'FINISHED'} - - -class MUV_UVInspDisplay(bpy.types.Operator): - """ - Operation class: Display - """ - - bl_idname = "uv.muv_uvinsp_display" - bl_label = "Display" - bl_description = "Display Overlapped/Flipped UV" - bl_options = {'REGISTER', 'UNDO'} + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + if not OperatorRender.is_running(context): + return False + return is_valid_context(context) def execute(self, context): - sc = context.scene - props = sc.muv_props.uvinsp - if not props.display_running: - update_uvinsp_info(context) - MUV_UVInspRenderer.handle_add(self, context) - props.display_running = True - else: - MUV_UVInspRenderer.handle_remove() - props.display_running = False + update_uvinsp_info(context) if context.area: context.area.tag_redraw() return {'FINISHED'} - - -class MUV_UVInspSelectOverlapped(bpy.types.Operator): - """ - Operation class: Select faces which have overlapped UVs - """ - - bl_idname = "uv.muv_uvinsp_select_overlapped" - bl_label = "Overlapped" - bl_description = "Select faces which have overlapped UVs" - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - uv_layer = bm.loops.layers.uv.verify() - - if context.tool_settings.use_uv_select_sync: - sel_faces = [f for f in bm.faces] - else: - sel_faces = [f for f in bm.faces if f.select] - - overlapped_info = get_overlapped_uv_info(bm, sel_faces, uv_layer, - 'FACE') - - for info in overlapped_info: - if context.tool_settings.use_uv_select_sync: - info["subject_face"].select = True - else: - for l in info["subject_face"].loops: - l[uv_layer].select = True - - bmesh.update_edit_mesh(obj.data) - - return {'FINISHED'} - - -class MUV_UVInspSelectFlipped(bpy.types.Operator): - """ - Operation class: Select faces which have flipped UVs - """ - - bl_idname = "uv.muv_uvinsp_select_flipped" - bl_label = "Flipped" - bl_description = "Select faces which have flipped UVs" - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - uv_layer = bm.loops.layers.uv.verify() - - if context.tool_settings.use_uv_select_sync: - sel_faces = [f for f in bm.faces] - else: - sel_faces = [f for f in bm.faces if f.select] - - flipped_info = get_flipped_uv_info(sel_faces, uv_layer) - - for info in flipped_info: - if context.tool_settings.use_uv_select_sync: - info["face"].select = True - else: - for l in info["face"].loops: - l[uv_layer].select = True - - bmesh.update_edit_mesh(obj.data) - - return {'FINISHED'} diff --git a/uv_magic_uv/op/uv_sculpt.py b/uv_magic_uv/op/uv_sculpt.py index 2bf76abd..63c1adfe 100644 --- a/uv_magic_uv/op/uv_sculpt.py +++ b/uv_magic_uv/op/uv_sculpt.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" from math import pi, cos, tan, sin @@ -32,39 +32,172 @@ 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 -class MUV_UVSculptRenderer(bpy.types.Operator): +__all__ = [ + 'Properties', + 'Operator', +] + + +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 + + +class Properties: + @classmethod + def init_props(cls, scene): + def get_func(_): + return Operator.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 + + +class Operator(bpy.types.Operator): """ - Operation class: Render Brush + Operation class: UV Sculpt in View3D """ - bl_idname = "uv.muv_uvsculpt_renderer" - bl_label = "Brush Renderer" - bl_description = "Brush Renderer in View3D" + bl_idname = "uv.muv_uv_sculpt_operator" + bl_label = "UV Sculpt" + bl_description = "UV Sculpt in View3D" + bl_options = {'REGISTER'} __handle = None - - @staticmethod - def handle_add(obj, context): - if MUV_UVSculptRenderer.__handle is 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 - MUV_UVSculptRenderer.__handle = sv.draw_handler_add( - MUV_UVSculptRenderer.draw_brush, - (obj, context), "WINDOW", "POST_PIXEL") + 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) - @staticmethod - def handle_remove(): - if MUV_UVSculptRenderer.__handle is not None: + @classmethod + def handle_remove(cls, context): + if cls.__handle: sv = bpy.types.SpaceView3D - sv.draw_handler_remove( - MUV_UVSculptRenderer.__handle, "WINDOW") - MUV_UVSculptRenderer.__handle = None - - @staticmethod - def draw_brush(obj, context): + 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 @@ -72,12 +205,12 @@ class MUV_UVSculptRenderer(bpy.types.Operator): theta = 2 * pi / num_segment fact_t = tan(theta) fact_r = cos(theta) - color = prefs.uvsculpt_brush_color + 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_uvsculpt_radius * cos(0.0) - y = sc.muv_uvsculpt_radius * sin(0.0) + 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 @@ -88,19 +221,7 @@ class MUV_UVSculptRenderer(bpy.types.Operator): y = y * fact_r bgl.glEnd() - -class MUV_UVSculptOps(bpy.types.Operator): - """ - Operation class: UV Sculpt in View3D - """ - - bl_idname = "uv.muv_uvsculpt_ops" - bl_label = "UV Sculpt" - bl_description = "UV Sculpt in View3D" - bl_options = {'REGISTER'} - def __init__(self): - self.__timer = None self.__loop_info = [] self.__stroking = False self.current_mco = Vector((0.0, 0.0)) @@ -137,7 +258,7 @@ class MUV_UVSculptOps(bpy.types.Operator): loc_2d = view3d_utils.location_3d_to_region_2d( region, space.region_3d, world_mat * l.vert.co) diff = loc_2d - self.__initial_mco - if diff.length < sc.muv_uvsculpt_radius: + if diff.length < sc.muv_uv_sculpt_radius: info = { "face_idx": f.index, "loop_idx": i, @@ -145,8 +266,8 @@ class MUV_UVSculptOps(bpy.types.Operator): "initial_vco_2d": loc_2d, "initial_uv": l[uv_layer].uv.copy(), "strength": self.__get_strength( - diff.length, sc.muv_uvsculpt_radius, - sc.muv_uvsculpt_strength) + diff.length, sc.muv_uv_sculpt_radius, + sc.muv_uv_sculpt_strength) } self.__loop_info.append(info) @@ -158,13 +279,13 @@ class MUV_UVSculptOps(bpy.types.Operator): uv_layer = bm.loops.layers.uv.verify() mco = self.current_mco - if sc.muv_uvsculpt_tools == 'GRAB': + 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_uvsculpt_tools == 'PINCH': + elif sc.muv_uv_sculpt_tools == 'PINCH': _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') loop_info = [] for f in bm.faces: @@ -174,7 +295,7 @@ class MUV_UVSculptOps(bpy.types.Operator): loc_2d = view3d_utils.location_3d_to_region_2d( region, space.region_3d, world_mat * l.vert.co) diff = loc_2d - self.__initial_mco - if diff.length < sc.muv_uvsculpt_radius: + if diff.length < sc.muv_uv_sculpt_radius: info = { "face_idx": f.index, "loop_idx": i, @@ -182,8 +303,8 @@ class MUV_UVSculptOps(bpy.types.Operator): "initial_vco_2d": loc_2d, "initial_uv": l[uv_layer].uv.copy(), "strength": self.__get_strength( - diff.length, sc.muv_uvsculpt_radius, - sc.muv_uvsculpt_strength) + diff.length, sc.muv_uv_sculpt_radius, + sc.muv_uv_sculpt_strength) } loop_info.append(info) @@ -215,13 +336,13 @@ class MUV_UVSculptOps(bpy.types.Operator): # move to target UV coordinate for info in loop_info: l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] - if sc.muv_uvsculpt_pinch_invert: + 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_uvsculpt_tools == 'RELAX': + elif sc.muv_uv_sculpt_tools == 'RELAX': _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') # get vertex and loop relation @@ -265,19 +386,19 @@ class MUV_UVSculptOps(bpy.types.Operator): loc_2d = view3d_utils.location_3d_to_region_2d( region, space.region_3d, world_mat * l.vert.co) diff = loc_2d - self.__initial_mco - if diff.length >= sc.muv_uvsculpt_radius: + if diff.length >= sc.muv_uv_sculpt_radius: continue db = vert_db[l.vert] strength = self.__get_strength(diff.length, - sc.muv_uvsculpt_radius, - sc.muv_uvsculpt_strength) + sc.muv_uv_sculpt_radius, + sc.muv_uv_sculpt_strength) base = (1.0 - strength) * l[uv_layer].uv - if sc.muv_uvsculpt_relax_method == 'HC': + 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_uvsculpt_relax_method == 'LAPLACIAN': + elif sc.muv_uv_sculpt_relax_method == 'LAPLACIAN': diff = strength * db["uv_p"] target_uv = base + diff else: @@ -294,7 +415,7 @@ class MUV_UVSculptOps(bpy.types.Operator): uv_layer = bm.loops.layers.uv.verify() mco = self.current_mco - if sc.muv_uvsculpt_tools == 'GRAB': + 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"]] @@ -303,23 +424,24 @@ class MUV_UVSculptOps(bpy.types.Operator): bmesh.update_edit_mesh(obj.data) def modal(self, context, event): - props = context.scene.muv_props.uvsculpt - if context.area: context.area.tag_redraw() - if not props.running: - if self.__timer is not None: - MUV_UVSculptRenderer.handle_remove() - context.window_manager.event_timer_remove(self.__timer) - self.__timer = None + if not Operator.is_running(context): + Operator.handle_remove(context) + return {'FINISHED'} self.current_mco = Vector((event.mouse_region_x, event.mouse_region_y)) - area, _, _ = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') - if self.current_mco.x < 0 or self.current_mco.x > area.width or \ - self.current_mco.y < 0 or self.current_mco.y > area.height: + 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': @@ -331,30 +453,25 @@ class MUV_UVSculptOps(bpy.types.Operator): 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 {'RUNNING_MODAL'} + return {'PASS_THROUGH'} def invoke(self, context, _): - props = context.scene.muv_props.uvsculpt - if context.area: context.area.tag_redraw() - if props.running: - props.running = False - return {'FINISHED'} - - props.running = True - if self.__timer is None: - self.__timer = context.window_manager.event_timer_add( - 0.1, context.window) - context.window_manager.modal_handler_add(self) - MUV_UVSculptRenderer.handle_add(self, context) + if Operator.is_running(context): + Operator.handle_remove(context) + else: + Operator.handle_add(self, context) return {'RUNNING_MODAL'} diff --git a/uv_magic_uv/op/uvw.py b/uv_magic_uv/op/uvw.py index 10202677..44858187 100644 --- a/uv_magic_uv/op/uvw.py +++ b/uv_magic_uv/op/uvw.py @@ -20,8 +20,8 @@ __author__ = "Alexander Milovsky, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" from math import sin, cos, pi @@ -37,8 +37,56 @@ from mathutils import Vector from .. import common -class MUV_UVWBoxMap(bpy.types.Operator): - bl_idname = "uv.muv_uvw_box_map" +__all__ = [ + 'Properties', + 'OperatorBoxMap', + 'OperatorBestPlanerMap', +] + + +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 + + +class Properties: + @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 + + +class OperatorBoxMap(bpy.types.Operator): + bl_idname = "uv.muv_uvw_operator_box_map" bl_label = "Box Map" bl_options = {'REGISTER', 'UNDO'} @@ -70,8 +118,10 @@ class MUV_UVWBoxMap(bpy.types.Operator): @classmethod def poll(cls, context): - obj = context.active_object - return obj and obj.type == 'MESH' + # 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 @@ -151,8 +201,8 @@ class MUV_UVWBoxMap(bpy.types.Operator): return {'FINISHED'} -class MUV_UVWBestPlanerMap(bpy.types.Operator): - bl_idname = "uv.muv_uvw_best_planer_map" +class OperatorBestPlanerMap(bpy.types.Operator): + bl_idname = "uv.muv_uvw_operator_best_planer_map" bl_label = "Best Planer Map" bl_options = {'REGISTER', 'UNDO'} @@ -183,8 +233,10 @@ class MUV_UVWBestPlanerMap(bpy.types.Operator): @classmethod def poll(cls, context): - obj = context.active_object - return obj and obj.type == 'MESH' + # 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 diff --git a/uv_magic_uv/op/world_scale_uv.py b/uv_magic_uv/op/world_scale_uv.py index e256fbac..e1a44954 100644 --- a/uv_magic_uv/op/world_scale_uv.py +++ b/uv_magic_uv/op/world_scale_uv.py @@ -20,25 +20,60 @@ __author__ = "McBuff, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" from math import sqrt import bpy import bmesh from mathutils import Vector -from bpy.props import EnumProperty +from bpy.props import ( + EnumProperty, + FloatProperty, + IntVectorProperty, + BoolProperty, +) from .. import common -def measure_wsuv_info(obj): +__all__ = [ + 'Properties', + 'OperatorMeasure', + 'OperatorApplyManual', + 'OperatorApplyScalingDensity', + 'OperatorApplyProportionalToMesh', +] + + +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) + uv_area = common.measure_uv_area(obj, tex_size) if not uv_area: - return None, None, None + return None, mesh_area, None if mesh_area == 0.0: density = 0.0 @@ -48,16 +83,112 @@ def measure_wsuv_info(obj): return uv_area, mesh_area, density -class MUV_WSUVMeasure(bpy.types.Operator): +class Properties: + @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 + + +class OperatorMeasure(bpy.types.Operator): """ Operation class: Measure face size """ - bl_idname = "uv.muv_wsuv_measure" - bl_label = "Measure" + 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 @@ -68,9 +199,9 @@ class MUV_WSUVMeasure(bpy.types.Operator): "Object must have more than one UV map and texture") return {'CANCELLED'} - sc.muv_wsuv_src_uv_area = uv_area - sc.muv_wsuv_src_mesh_area = mesh_area - sc.muv_wsuv_src_density = density + 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}" @@ -79,41 +210,264 @@ class MUV_WSUVMeasure(bpy.types.Operator): return {'FINISHED'} -class MUV_WSUVApply(bpy.types.Operator): +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) + + +class OperatorApplyManual(bpy.types.Operator): """ - Operation class: Apply scaled UV + Operation class: Apply scaled UV (Manual) """ - bl_idname = "uv.muv_wsuv_apply" - bl_label = "Apply" - bl_description = "Apply scaled UV based on scale calculation" + 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') + ('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" + 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): - sc = context.scene + return self.__apply_manual(context) + + +class OperatorApplyScalingDensity(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: @@ -121,116 +475,172 @@ class MUV_WSUVApply(bpy.types.Operator): bm.edges.ensure_lookup_table() bm.faces.ensure_lookup_table() - sel_faces = [f for f in bm.faces if f.select] - - uv_area, mesh_area, density = measure_wsuv_info(obj) + 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'} - uv_layer = bm.loops.layers.uv.verify() + tgt_density = self.src_density * self.tgt_scaling_factor + factor = tgt_density / density - if sc.muv_wsuv_mode == 'PROPORTIONAL': - tgt_density = sc.muv_wsuv_src_density * sqrt(mesh_area) / \ - sqrt(sc.muv_wsuv_src_mesh_area) - elif sc.muv_wsuv_mode == 'SCALING': - tgt_density = sc.muv_wsuv_src_density * sc.muv_wsuv_scaling_factor - elif sc.muv_wsuv_mode == 'USER': - tgt_density = sc.muv_wsuv_tgt_density - elif sc.muv_wsuv_mode == 'CONSTANT': - tgt_density = sc.muv_wsuv_src_density + apply(context.active_object, self.origin, factor) + self.report({'INFO'}, "Scaling factor: {0}".format(factor)) - factor = tgt_density / density + return {'FINISHED'} - # calculate origin - if self.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 self.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 self.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 self.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 self.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 self.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 self.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 self.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 self.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 + 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) + + +class OperatorApplyProportionalToMesh(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'} - bmesh.update_edit_mesh(obj.data) + 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) |