diff options
46 files changed, 2315 insertions, 1480 deletions
diff --git a/magic_uv/__init__.py b/magic_uv/__init__.py index 0df3d49f..e9c95f24 100644 --- a/magic_uv/__init__.py +++ b/magic_uv/__init__.py @@ -20,21 +20,23 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" bl_info = { "name": "Magic UV", "author": "Nutti, Mifth, Jace Priester, kgeogeo, mem, imdjs" "Keith (Wahooney) Boshoff, McBuff, MaxRobinot, " - "Alexander Milovsky, Dusan Stevanovic", - "version": (6, 3, 0), + "Alexander Milovsky, Dusan Stevanovic, MatthiasThDs", + "version": (6, 4, 0), "blender": (2, 80, 0), "location": "See Add-ons Preferences", "description": "UV Toolset. See Add-ons Preferences for details", "warning": "", "support": "COMMUNITY", + "wiki_url": "https://docs.blender.org/manual/en/dev/addons/" + "uv/magic_uv.html", "doc_url": "{BLENDER_MANUAL_URL}/addons/uv/magic_uv.html", "tracker_url": "https://github.com/nutti/Magic-UV", "category": "UV", diff --git a/magic_uv/common.py b/magic_uv/common.py index 11696667..3817486c 100644 --- a/magic_uv/common.py +++ b/magic_uv/common.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" from collections import defaultdict from pprint import pprint @@ -44,6 +44,14 @@ def is_console_mode(): return os.environ["MUV_CONSOLE_MODE"] == "true" +def is_valid_space(context, allowed_spaces): + for area in context.screen.areas: + for space in area.spaces: + if space.type in allowed_spaces: + return True + return False + + def is_debug_mode(): return __DEBUG_MODE @@ -422,23 +430,30 @@ def find_texture_layer(bm): return bm.faces.layers.tex.verify() -def find_texture_nodes(obj): +def find_texture_nodes_from_material(mtrl): nodes = [] - for mat in obj.material_slots: - if not mat.material: + if not mtrl.node_tree: + return nodes + for node in mtrl.node_tree.nodes: + tex_node_types = [ + 'TEX_ENVIRONMENT', + 'TEX_IMAGE', + ] + if node.type not in tex_node_types: continue - if not mat.material.node_tree: + if not node.image: continue - for node in mat.material.node_tree.nodes: - tex_node_types = [ - 'TEX_ENVIRONMENT', - 'TEX_IMAGE', - ] - if node.type not in tex_node_types: - continue - if not node.image: - continue - nodes.append(node) + nodes.append(node) + + return nodes + + +def find_texture_nodes(obj): + nodes = [] + for slot in obj.material_slots: + if not slot.material: + continue + nodes.extend(find_texture_nodes_from_material(slot.material)) return nodes @@ -1166,6 +1181,18 @@ def __is_points_in_polygon(points, subject_points): return True +def get_uv_editable_objects(context): + if compat.check_version(2, 80, 0) < 0: + objs = [context.active_object] + else: + objs = [o for o in bpy.data.objects + if compat.get_object_select(o) and o.type == 'MESH'] + objs.append(context.active_object) + + objs = list(set(objs)) + return objs + + def get_overlapped_uv_info(bm_list, faces_list, uv_layer_list, mode): # at first, check island overlapped isl = [] diff --git a/magic_uv/lib/__init__.py b/magic_uv/lib/__init__.py index 5e06552d..8bed4656 100644 --- a/magic_uv/lib/__init__.py +++ b/magic_uv/lib/__init__.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" if "bpy" in locals(): import importlib diff --git a/magic_uv/op/__init__.py b/magic_uv/op/__init__.py index b7316192..459a37e0 100644 --- a/magic_uv/op/__init__.py +++ b/magic_uv/op/__init__.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" if "bpy" in locals(): import importlib diff --git a/magic_uv/op/align_uv.py b/magic_uv/op/align_uv.py index 77afcc25..cb68bf25 100644 --- a/magic_uv/op/align_uv.py +++ b/magic_uv/op/align_uv.py @@ -20,14 +20,19 @@ __author__ = "imdjs, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import math from math import atan2, tan, sin, cos import bpy -from bpy.props import EnumProperty, BoolProperty, FloatProperty +from bpy.props import ( + EnumProperty, + BoolProperty, + FloatProperty, + FloatVectorProperty, +) import bmesh from mathutils import Vector @@ -39,23 +44,18 @@ from .. import common def _is_valid_context(context): - obj = context.object + objs = common.get_uv_editable_objects(context) + if not objs: + return False # 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: + if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): return False return True @@ -339,6 +339,66 @@ class _Properties: default='MIDDLE' ) + scene.muv_align_uv_snap_method = EnumProperty( + name="Snap Method", + description="Snap method", + items=[ + ('POINT', "Point", "Snap to point"), + ('EDGE', "Edge", "Snap to edge"), + ], + default='POINT' + ) + scene.muv_align_uv_snap_point_group = EnumProperty( + name="Snap Group (Point)", + description="Group that snap (point) operation applies for", + items=[ + ('VERT', "Vertex", "Vertex"), + ('FACE', "Face", "Face"), + ('UV_ISLAND', "UV Island", "UV Island"), + ], + default='FACE' + ) + scene.muv_align_uv_snap_point_target = FloatVectorProperty( + name="Snap Target (Point)", + description="Target point where UV vertices snap to", + size=2, + precision=4, + soft_min=-1.0, + soft_max=1.0, + step=1, + default=(0.000, 0.000), + ) + scene.muv_align_uv_snap_edge_group = EnumProperty( + name="Snap Group (Edge)", + description="Group that snap (edge) operation applies for", + items=[ + ('EDGE', "Edge", "Edge"), + ('FACE', "Face", "Face"), + ('UV_ISLAND', "UV Island", "UV Island"), + ], + default='FACE' + ) + scene.muv_align_uv_snap_edge_target_1 = FloatVectorProperty( + name="Snap Target (Edge)", + description="Target edge where UV vertices snap to", + size=2, + precision=4, + soft_min=-1.0, + soft_max=1.0, + step=1, + default=(0.000, 0.000), + ) + scene.muv_align_uv_snap_edge_target_2 = FloatVectorProperty( + name="Snap Target (Edge)", + description="Target edge where UV vertices snap to", + size=2, + precision=4, + soft_min=-1.0, + soft_max=1.0, + step=1, + default=(0.000, 0.000), + ) + @classmethod def del_props(cls, scene): del scene.muv_align_uv_enabled @@ -348,6 +408,12 @@ class _Properties: del scene.muv_align_uv_horizontal del scene.muv_align_uv_mesh_infl del scene.muv_align_uv_location + del scene.muv_align_uv_snap_method + del scene.muv_align_uv_snap_point_group + del scene.muv_align_uv_snap_point_target + del scene.muv_align_uv_snap_edge_group + del scene.muv_align_uv_snap_edge_target_1 + del scene.muv_align_uv_snap_edge_target_2 @BlClassRegistry() @@ -378,77 +444,90 @@ class MUV_OT_AlignUV_Circle(bpy.types.Operator): return _is_valid_context(context) def execute(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - uv_layer = bm.loops.layers.uv.verify() - - # loop_seqs[horizontal][vertical][loop] - loop_seqs, error = common.get_loop_sequences(bm, uv_layer, True) - if not loop_seqs: - self.report({'WARNING'}, error) - return {'CANCELLED'} - - # get circle and new UVs - uvs = [hseq[0][0][uv_layer].uv.copy() for hseq in loop_seqs] - c, r = _get_circle(uvs[0:3]) - new_uvs = _calc_v_on_circle(uvs, c, r) - - # check if center is identical - center_is_identical = False - center = loop_seqs[0][-1][0].vert - if (len(loop_seqs[0][-1]) == 1) and loop_seqs[0][-1][0].vert == center: - center_is_identical = True - - # check if topology is correct - if center_is_identical: - for hseq in loop_seqs[1:]: - if len(hseq[-1]) != 1: - self.report({'WARNING'}, "Last face must be triangle") - return {'CANCELLED'} - if hseq[-1][0].vert != center: - self.report({'WARNING'}, "Center must be identical") - return {'CANCELLED'} - else: - for hseq in loop_seqs[1:]: - if len(hseq[-1]) == 1: - self.report({'WARNING'}, "Last face must not be triangle") - return {'CANCELLED'} - if hseq[-1][0].vert == center: - self.report({'WARNING'}, "Center must not be identical") - return {'CANCELLED'} - - # align to circle - if self.transmission: - for hidx, hseq in enumerate(loop_seqs): - for vidx, pair in enumerate(hseq): - all_ = int((len(hseq) + 1) / 2) - if center_is_identical: - r = (all_ - int((vidx + 1) / 2)) / all_ - else: - r = (1 + all_ - int((vidx + 1) / 2)) / all_ - pair[0][uv_layer].uv = c + (new_uvs[hidx] - c) * r + objs = common.get_uv_editable_objects(context) + + for obj in objs: + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.verify() + + # loop_seqs[horizontal][vertical][loop] + loop_seqs, error = common.get_loop_sequences(bm, uv_layer, True) + if not loop_seqs: + self.report({'WARNING'}, + "Object {}: {}".format(obj.name, error)) + return {'CANCELLED'} + + # get circle and new UVs + uvs = [hseq[0][0][uv_layer].uv.copy() for hseq in loop_seqs] + c, r = _get_circle(uvs[0:3]) + new_uvs = _calc_v_on_circle(uvs, c, r) + + # check if center is identical + center_is_identical = False + center = loop_seqs[0][-1][0].vert + if (len(loop_seqs[0][-1]) == 1) and \ + loop_seqs[0][-1][0].vert == center: + center_is_identical = True + + # check if topology is correct + if center_is_identical: + for hseq in loop_seqs[1:]: + if len(hseq[-1]) != 1: + self.report({'WARNING'}, + "Object {}: Last face must be triangle" + .format(obj.name)) + return {'CANCELLED'} + if hseq[-1][0].vert != center: + self.report({'WARNING'}, + "Object {}: Center must be identical" + .format(obj.name)) + return {'CANCELLED'} + else: + for hseq in loop_seqs[1:]: + if len(hseq[-1]) == 1: + self.report({'WARNING'}, + "Object {}: Last face must not be triangle" + .format(obj.name)) + return {'CANCELLED'} + if hseq[-1][0].vert == center: + self.report({'WARNING'}, + "Object {}: Center must not be identical" + .format(obj.name)) + return {'CANCELLED'} + + # align to circle + if self.transmission: + for hidx, hseq in enumerate(loop_seqs): + for vidx, pair in enumerate(hseq): + all_ = int((len(hseq) + 1) / 2) + if center_is_identical: + r = (all_ - int((vidx + 1) / 2)) / all_ + else: + r = (1 + all_ - int((vidx + 1) / 2)) / all_ + pair[0][uv_layer].uv = c + (new_uvs[hidx] - c) * r + if self.select: + pair[0][uv_layer].select = True + + if len(pair) < 2: + continue + # for quad polygon + next_hidx = (hidx + 1) % len(loop_seqs) + pair[1][uv_layer].uv = \ + c + ((new_uvs[next_hidx]) - c) * r + if self.select: + pair[1][uv_layer].select = True + else: + for hidx, hseq in enumerate(loop_seqs): + pair = hseq[0] + pair[0][uv_layer].uv = new_uvs[hidx] + pair[1][uv_layer].uv = new_uvs[(hidx + 1) % len(loop_seqs)] if self.select: pair[0][uv_layer].select = True - - if len(pair) < 2: - continue - # for quad polygon - next_hidx = (hidx + 1) % len(loop_seqs) - pair[1][uv_layer].uv = c + ((new_uvs[next_hidx]) - c) * r - if self.select: pair[1][uv_layer].select = True - else: - for hidx, hseq in enumerate(loop_seqs): - pair = hseq[0] - pair[0][uv_layer].uv = new_uvs[hidx] - pair[1][uv_layer].uv = new_uvs[(hidx + 1) % len(loop_seqs)] - if self.select: - pair[0][uv_layer].select = True - pair[1][uv_layer].select = True - bmesh.update_edit_mesh(obj.data) + bmesh.update_edit_mesh(obj.data) return {'FINISHED'} @@ -585,22 +664,25 @@ class MUV_OT_AlignUV_Straighten(bpy.types.Operator): self.__align_wo_transmission(loop_seqs, uv_layer) def execute(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - uv_layer = bm.loops.layers.uv.verify() - - # loop_seqs[horizontal][vertical][loop] - loop_seqs, error = common.get_loop_sequences(bm, uv_layer) - if not loop_seqs: - self.report({'WARNING'}, error) - return {'CANCELLED'} + objs = common.get_uv_editable_objects(context) - # align - self.__align(loop_seqs, uv_layer) + for obj in objs: + 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() - bmesh.update_edit_mesh(obj.data) + # loop_seqs[horizontal][vertical][loop] + loop_seqs, error = common.get_loop_sequences(bm, uv_layer) + if not loop_seqs: + self.report({'WARNING'}, + "Object {}: {}".format(obj.name, error)) + return {'CANCELLED'} + + # align + self.__align(loop_seqs, uv_layer) + + bmesh.update_edit_mesh(obj.data) return {'FINISHED'} @@ -982,25 +1064,469 @@ class MUV_OT_AlignUV_Axis(bpy.types.Operator): width, height) def execute(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - uv_layer = bm.loops.layers.uv.verify() - - # loop_seqs[horizontal][vertical][loop] - loop_seqs, error = common.get_loop_sequences(bm, uv_layer) - if not loop_seqs: - self.report({'WARNING'}, error) + objs = common.get_uv_editable_objects(context) + + for obj in objs: + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.verify() + + # loop_seqs[horizontal][vertical][loop] + loop_seqs, error = common.get_loop_sequences(bm, uv_layer) + if not loop_seqs: + self.report({'WARNING'}, + "Object {}: {}".format(obj.name, error)) + return {'CANCELLED'} + + # get height and width + uv_max, uv_min = self.__get_uv_max_min(loop_seqs, uv_layer) + width = uv_max.x - uv_min.x + height = uv_max.y - uv_min.y + + self.__align(loop_seqs, uv_layer, uv_min, width, height) + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_AlignUV_SnapToPoint(bpy.types.Operator): + + bl_idname = "uv.muv_align_uv_snap_to_point" + bl_label = "Align UV (Snap to Point)" + bl_description = "Align UV to the target point" + bl_options = {'REGISTER', 'UNDO'} + + group = EnumProperty( + name="Snap Group", + description="Group that snap operation applies for", + items=[ + ('VERT', "Vertex", "Vertex"), + ('FACE', "Face", "Face"), + ('UV_ISLAND', "UV Island", "UV Island"), + ], + default='FACE' + ) + target = FloatVectorProperty( + name="Snap Target", + description="Target where UV vertices snap to", + size=2, + precision=4, + soft_min=-1.0, + soft_max=1.0, + step=1, + default=(0.000, 0.000), + ) + + def _get_snap_target_loops(self, context, bm, uv_layer): + target_loops = [] + + selected_faces = [f for f in bm.faces if f.select] + + # Process snap operation. + for face in selected_faces: + for l in face.loops: + if context.tool_settings.use_uv_select_sync or \ + l[uv_layer].select: + target_loops.append(l) + + return target_loops + + def _get_snap_target_faces(self, context, bm, uv_layer): + target_faces = [] + + selected_faces = [f for f in bm.faces if f.select] + + for face in selected_faces: + for l in face.loops: + if not context.tool_settings.use_uv_select_sync and \ + not l[uv_layer].select: + break + else: + target_faces.append(face) + + return target_faces + + def _get_snap_target_islands(self, context, bm, uv_layer): + target_islands = [] + + islands = common.get_island_info_from_bmesh(bm, only_selected=True) + + for isl in islands: + some_verts_not_selected = False + for face in isl["faces"]: + for l in face["face"].loops: + if not context.tool_settings.use_uv_select_sync and \ + not l[uv_layer].select: + some_verts_not_selected = True + break + if not some_verts_not_selected: + target_islands.append(isl) + + return target_islands + + def execute(self, context): + objs = common.get_uv_editable_objects(context) + + group_to_reason = { + 'VERT': "Vertex", + 'FACE': "Face", + 'UV_ISLAND': "UV Island", + } + no_selection_reason = group_to_reason[self.group] + + for obj in objs: + 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 self.group == 'VERT': + target_loops = \ + self._get_snap_target_loops(context, bm, uv_layer) + + # Process snap operation. + for l in target_loops: + l[uv_layer].uv = self.target + no_selection_reason = None + + elif self.group == 'FACE': + target_faces = \ + self._get_snap_target_faces(context, bm, uv_layer) + + for face in target_faces: + ave_uv = Vector((0.0, 0.0)) + for l in face.loops: + ave_uv += l[uv_layer].uv + ave_uv /= len(face.loops) + diff = Vector(self.target) - ave_uv + + # Process snap operation. + for l in face.loops: + l[uv_layer].uv += diff + no_selection_reason = None + + elif self.group == 'UV_ISLAND': + target_islands = \ + self._get_snap_target_islands(context, bm, uv_layer) + + for isl in target_islands: + ave_uv = Vector((0.0, 0.0)) + count = 0 + for face in isl["faces"]: + for l in face["face"].loops: + ave_uv += l[uv_layer].uv + count += 1 + if count != 0: + ave_uv /= count + diff = Vector(self.target) - ave_uv + + # Process snap operation. + for face in isl["faces"]: + for l in face["face"].loops: + l[uv_layer].uv += diff + no_selection_reason = None + + bmesh.update_edit_mesh(obj.data) + + if no_selection_reason: + self.report( + {'WARNING'}, + "Must select more than 1 {}.".format(no_selection_reason) + ) return {'CANCELLED'} - # get height and width - uv_max, uv_min = self.__get_uv_max_min(loop_seqs, uv_layer) - width = uv_max.x - uv_min.x - height = uv_max.y - uv_min.y + return {'FINISHED'} + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_AlignUV_Snap_SetPointTargetToCursor(bpy.types.Operator): + + bl_idname = "uv.muv_align_uv_snap_set_point_target_to_cursor" + bl_label = "Set Point Target to Cursor" + bl_description = """Set point target to the cursor for + 'Align UV (Snap to Point)'""" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + sc = context.scene + + _, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW', + 'IMAGE_EDITOR') + cursor_loc = space.cursor_location + + sc.muv_align_uv_snap_point_target = cursor_loc + + return {'FINISHED'} + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_AlignUV_Snap_SetPointTargetToVertexGroup(bpy.types.Operator): + + bl_idname = "uv.muv_align_uv_snap_set_point_target_to_vertex_group" + bl_label = "Set Point Target to Vertex Group" + bl_description = """Set point target to the average of vertices for + 'Align UV (Snap to Point)'""" + bl_options = {'REGISTER', 'UNDO'} + + def execute(self, context): + sc = context.scene + objs = common.get_uv_editable_objects(context) + + ave_uv = Vector((0.0, 0.0)) + count = 0 + for obj in objs: + 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() + + selected_faces = [f for f in bm.faces if f.select] + for face in selected_faces: + for l in face.loops: + if context.tool_settings.use_uv_select_sync or \ + l[uv_layer].select: + ave_uv += l[uv_layer].uv + count += 1 + if count != 0: + ave_uv /= count + + sc.muv_align_uv_snap_point_target = ave_uv + + return {'FINISHED'} + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_AlignUV_SnapToEdge(bpy.types.Operator): - self.__align(loop_seqs, uv_layer, uv_min, width, height) + bl_idname = "uv.muv_align_uv_snap_to_edge" + bl_label = "Align UV (Snap to Edge)" + bl_description = "Align UV to the target edge" + bl_options = {'REGISTER', 'UNDO'} - bmesh.update_edit_mesh(obj.data) + group = EnumProperty( + name="Snap Group", + description="Group that snap operation applies for", + items=[ + ('EDGE', "Edge", "Edge"), + ('FACE', "Face", "Face"), + ('UV_ISLAND', "UV Island", "UV Island"), + ], + default='FACE' + ) + target_1 = FloatVectorProperty( + name="Snap Target 1", + description="Vertex 1 of the target edge", + size=2, + precision=4, + soft_min=-1.0, + soft_max=1.0, + step=1, + default=(0.000, 0.000), + ) + target_2 = FloatVectorProperty( + name="Snap Target 2", + description="Vertex 2 of the target edge", + size=2, + precision=4, + soft_min=-1.0, + soft_max=1.0, + step=1, + default=(0.000, 0.000), + ) + + def _calc_snap_move_amount(self, loops, uv_layer): + ave = (loops[0][uv_layer].uv + loops[1][uv_layer].uv) / 2 + target = (Vector(self.target_1) + Vector(self.target_2)) / 2 + + return target - ave + + def _get_snap_target_loop_pairs(self, bm, uv_layer): + target_loop_pairs = [] + + selected_edges = [e for e in bm.edges if e.select] + + cand_loops = [] + for edge in selected_edges: + for l in edge.link_loops: + if l[uv_layer].select: + cand_loops.append(l) + + for l in cand_loops: + if l[uv_layer].select and l.link_loop_next[uv_layer].select: + d = {l, l.link_loop_next} + if d not in target_loop_pairs: + assert l.face == l.link_loop_next.face + target_loop_pairs.append(d) + + return target_loop_pairs + + def _find_target_island_from_face(self, islands, face): + for isl in islands: + for f in isl["faces"]: + if f["face"] == face: + return isl + + return None + + def execute(self, context): + objs = common.get_uv_editable_objects(context) + + no_selection = True + for obj in objs: + 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 self.group == 'EDGE': + target_loop_pairs = \ + self._get_snap_target_loop_pairs(bm, uv_layer) + + for pair in target_loop_pairs: + p = list(pair) + diff = self._calc_snap_move_amount(p, uv_layer) + + # Process snap operation. + p[0][uv_layer].uv += diff + p[1][uv_layer].uv += diff + + no_selection = False + + elif self.group == 'FACE': + target_loop_pairs = \ + self._get_snap_target_loop_pairs(bm, uv_layer) + + face_processed = [] + for pair in target_loop_pairs: + p = list(pair) + diff = self._calc_snap_move_amount(p, uv_layer) + + # Process snap operation. + face = p[0].face + if face in face_processed: + self.report( + {'WARNING'}, + "Must select only one edge per face. (Object: {})" + .format(obj.name) + ) + return {'CANCELLED'} + face_processed.append(face) + for l in face.loops: + l[uv_layer].uv += diff + no_selection = False + + elif self.group == 'UV_ISLAND': + target_loop_pairs = \ + self._get_snap_target_loop_pairs(bm, uv_layer) + + islands = common.get_island_info_from_bmesh( + bm, only_selected=False) + + isl_processed = [] + for pair in target_loop_pairs: + p = list(pair) + diff = self._calc_snap_move_amount(p, uv_layer) + + # Find island to process. + face = p[0].face + target_isl = \ + self._find_target_island_from_face(islands, face) + if target_isl is None: + self.report( + {'WARNING'}, + "Failed to find island. (Object: {})" + .format(obj.name) + ) + return {'CANCELLED'} + if target_isl in isl_processed: + self.report( + {'WARNING'}, + """Must select only one edge per island. + (Object: {})""" + .format(obj.name) + ) + return {'CANCELLED'} + isl_processed.append(target_isl) + + # Process snap operation. + for f in target_isl["faces"]: + for l in f["face"].loops: + l[uv_layer].uv += diff + no_selection = False + + bmesh.update_edit_mesh(obj.data) + + if no_selection: + self.report({'WARNING'}, "Must select more than 1 Edge.") + return {'CANCELLED'} + + return {'FINISHED'} + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_AlignUV_Snap_SetEdgeTargetToEdgeCenter(bpy.types.Operator): + + bl_idname = "uv.muv_align_uv_snap_set_edge_target_to_edge_center" + bl_label = "Set Edge Target to Edge Center" + bl_description = """Set edge target to the center of edge for + 'Align UV (Snap to Edge)'""" + bl_options = {'REGISTER', 'UNDO'} + + def _get_target_loop_pairs(self, bm, uv_layer): + target_loop_pairs = [] + + selected_edges = [e for e in bm.edges if e.select] + + cand_loops = [] + for edge in selected_edges: + for l in edge.link_loops: + if l[uv_layer].select: + cand_loops.append(l) + + for l in cand_loops: + if l[uv_layer].select and l.link_loop_next[uv_layer].select: + d = {l, l.link_loop_next} + if d not in target_loop_pairs: + assert l.face == l.link_loop_next.face + target_loop_pairs.append(d) + + return target_loop_pairs + + def execute(self, context): + sc = context.scene + objs = common.get_uv_editable_objects(context) + + ave_uv_1 = Vector((0.0, 0.0)) + ave_uv_2 = Vector((0.0, 0.0)) + count = 0 + for obj in objs: + 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() + + target_loop_pairs = self._get_target_loop_pairs(bm, uv_layer) + for pair in target_loop_pairs: + p = list(pair) + uv_1 = p[0][uv_layer].uv + uv_2 = p[1][uv_layer].uv + ave_uv_1 += uv_1 + ave_uv_2 += uv_2 + count += 1 + + if count != 0: + ave_uv_1 /= count + ave_uv_2 /= count + + sc.muv_align_uv_snap_edge_target_1 = ave_uv_1 + sc.muv_align_uv_snap_edge_target_2 = ave_uv_2 return {'FINISHED'} diff --git a/magic_uv/op/align_uv_cursor.py b/magic_uv/op/align_uv_cursor.py index 884f645a..08c90db7 100644 --- a/magic_uv/op/align_uv_cursor.py +++ b/magic_uv/op/align_uv_cursor.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy from mathutils import Vector @@ -38,10 +38,7 @@ 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: + if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): return False return True @@ -180,47 +177,67 @@ class MUV_OT_AlignUVCursor(bpy.types.Operator): else: bd_size = [1.0, 1.0] + large_value = 1e7 if self.base == 'UV': - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if not bm.loops.layers.uv: - return None - uv_layer = bm.loops.layers.uv.verify() - - max_ = Vector((-10000000.0, -10000000.0)) - min_ = Vector((10000000.0, 10000000.0)) - for f in bm.faces: - if not f.select: - continue - for l in f.loops: - uv = l[uv_layer].uv - max_.x = max(max_.x, uv.x) - max_.y = max(max_.y, uv.y) - min_.x = min(min_.x, uv.x) - min_.y = min(min_.y, uv.y) - center = Vector(((max_.x + min_.x) / 2.0, (max_.y + min_.y) / 2.0)) - + objs = common.get_uv_editable_objects(context) + no_selected_face = True + if objs: + max_ = Vector((-large_value, -large_value)) + min_ = Vector((large_value, large_value)) + for obj in objs: + bm = bmesh.from_edit_mesh(obj.data) + if not bm.loops.layers.uv: + return None + uv_layer = bm.loops.layers.uv.verify() + + for f in bm.faces: + if not f.select: + continue + for l in f.loops: + uv = l[uv_layer].uv + max_.x = max(max_.x, uv.x) + max_.y = max(max_.y, uv.y) + min_.x = min(min_.x, uv.x) + min_.y = min(min_.y, uv.y) + no_selected_face = False + if no_selected_face: + max_ = Vector((1.0, 1.0)) + min_ = Vector((0.0, 0.0)) + center = Vector(( + (max_.x + min_.x) / 2.0, (max_.y + min_.y) / 2.0 + )) + + # pylint: disable=R1702 elif self.base == 'UV_SEL': - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if not bm.loops.layers.uv: - return None - uv_layer = bm.loops.layers.uv.verify() - - max_ = Vector((-10000000.0, -10000000.0)) - min_ = Vector((10000000.0, 10000000.0)) - for f in bm.faces: - if not f.select: - continue - for l in f.loops: - if not l[uv_layer].select: - continue - uv = l[uv_layer].uv - max_.x = max(max_.x, uv.x) - max_.y = max(max_.y, uv.y) - min_.x = min(min_.x, uv.x) - min_.y = min(min_.y, uv.y) - center = Vector(((max_.x + min_.x) / 2.0, (max_.y + min_.y) / 2.0)) + objs = common.get_uv_editable_objects(context) + no_selected_face = True + if objs: + max_ = Vector((-large_value, -large_value)) + min_ = Vector((large_value, large_value)) + for obj in objs: + bm = bmesh.from_edit_mesh(obj.data) + if not bm.loops.layers.uv: + return None + uv_layer = bm.loops.layers.uv.verify() + + for f in bm.faces: + if not f.select: + continue + for l in f.loops: + if not l[uv_layer].select: + continue + uv = l[uv_layer].uv + max_.x = max(max_.x, uv.x) + max_.y = max(max_.y, uv.y) + min_.x = min(min_.x, uv.x) + min_.y = min(min_.y, uv.y) + no_selected_face = False + if no_selected_face: + max_ = Vector((1.0, 1.0)) + min_ = Vector((0.0, 0.0)) + center = Vector(( + (max_.x + min_.x) / 2.0, (max_.y + min_.y) / 2.0 + )) elif self.base == 'TEXTURE': min_ = Vector((0.0, 0.0)) diff --git a/magic_uv/op/clip_uv.py b/magic_uv/op/clip_uv.py index c6f006e2..990d35a6 100644 --- a/magic_uv/op/clip_uv.py +++ b/magic_uv/op/clip_uv.py @@ -20,8 +20,8 @@ __author__ = "Dusan Stevanovic, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import math @@ -38,13 +38,18 @@ from ..utils import compatibility as compat def _is_valid_context(context): + objs = common.get_uv_editable_objects(context) + if not objs: + return False + + # only edit mode is allowed to execute + 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: + if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): return False return True @@ -156,72 +161,78 @@ class MUV_OT_ClipUV(bpy.types.Operator): return _is_valid_context(context) def execute(self, context): - obj = context.active_object - bm = common.create_bmesh(obj) - - 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() - - for face in bm.faces: - if not face.select: - continue - - selected_loops = [l for l in face.loops - if l[uv_layer].select or - context.scene.tool_settings.use_uv_select_sync] - if not selected_loops: - continue - - # average of UV coordinates on the face - max_uv = Vector((-10000000.0, -10000000.0)) - min_uv = Vector((10000000.0, 10000000.0)) - for l in selected_loops: - uv = l[uv_layer].uv - max_uv.x = max(max_uv.x, uv.x) - max_uv.y = max(max_uv.y, uv.y) - min_uv.x = min(min_uv.x, uv.x) - min_uv.y = min(min_uv.y, uv.y) - - # clip - move_uv = Vector((0.0, 0.0)) - clip_size = Vector(self.clip_uv_range_max) - \ - Vector(self.clip_uv_range_min) - if max_uv.x > self.clip_uv_range_max[0]: - target_x = math.fmod(max_uv.x - self.clip_uv_range_min[0], - clip_size.x) - if target_x < 0.0: - target_x += clip_size.x - target_x += self.clip_uv_range_min[0] - move_uv.x = target_x - max_uv.x - if min_uv.x < self.clip_uv_range_min[0]: - target_x = math.fmod(min_uv.x - self.clip_uv_range_min[0], - clip_size.x) - if target_x < 0.0: - target_x += clip_size.x - target_x += self.clip_uv_range_min[0] - move_uv.x = target_x - min_uv.x - if max_uv.y > self.clip_uv_range_max[1]: - target_y = math.fmod(max_uv.y - self.clip_uv_range_min[1], - clip_size.y) - if target_y < 0.0: - target_y += clip_size.y - target_y += self.clip_uv_range_min[1] - move_uv.y = target_y - max_uv.y - if min_uv.y < self.clip_uv_range_min[1]: - target_y = math.fmod(min_uv.y - self.clip_uv_range_min[1], - clip_size.y) - if target_y < 0.0: - target_y += clip_size.y - target_y += self.clip_uv_range_min[1] - move_uv.y = target_y - min_uv.y - - # update UV - for l in selected_loops: - l[uv_layer].uv = l[uv_layer].uv + move_uv - - bmesh.update_edit_mesh(obj.data) + objs = common.get_uv_editable_objects(context) + + for obj in objs: + bm = common.create_bmesh(obj) + + if not bm.loops.layers.uv: + self.report({'WARNING'}, + "Object {} must have more than one UV map" + .format(obj.name)) + return {'CANCELLED'} + + uv_layer = bm.loops.layers.uv.verify() + + for face in bm.faces: + if not face.select: + continue + + selected_loops = [ + l for l in face.loops + if l[uv_layer].select or + context.scene.tool_settings.use_uv_select_sync + ] + if not selected_loops: + continue + + # average of UV coordinates on the face + max_uv = Vector((-10000000.0, -10000000.0)) + min_uv = Vector((10000000.0, 10000000.0)) + for l in selected_loops: + uv = l[uv_layer].uv + max_uv.x = max(max_uv.x, uv.x) + max_uv.y = max(max_uv.y, uv.y) + min_uv.x = min(min_uv.x, uv.x) + min_uv.y = min(min_uv.y, uv.y) + + # clip + move_uv = Vector((0.0, 0.0)) + clip_size = Vector(self.clip_uv_range_max) - \ + Vector(self.clip_uv_range_min) + if max_uv.x > self.clip_uv_range_max[0]: + target_x = math.fmod(max_uv.x - self.clip_uv_range_min[0], + clip_size.x) + if target_x < 0.0: + target_x += clip_size.x + target_x += self.clip_uv_range_min[0] + move_uv.x = target_x - max_uv.x + if min_uv.x < self.clip_uv_range_min[0]: + target_x = math.fmod(min_uv.x - self.clip_uv_range_min[0], + clip_size.x) + if target_x < 0.0: + target_x += clip_size.x + target_x += self.clip_uv_range_min[0] + move_uv.x = target_x - min_uv.x + if max_uv.y > self.clip_uv_range_max[1]: + target_y = math.fmod(max_uv.y - self.clip_uv_range_min[1], + clip_size.y) + if target_y < 0.0: + target_y += clip_size.y + target_y += self.clip_uv_range_min[1] + move_uv.y = target_y - max_uv.y + if min_uv.y < self.clip_uv_range_min[1]: + target_y = math.fmod(min_uv.y - self.clip_uv_range_min[1], + clip_size.y) + if target_y < 0.0: + target_y += clip_size.y + target_y += self.clip_uv_range_min[1] + move_uv.y = target_y - min_uv.y + + # update UV + for l in selected_loops: + l[uv_layer].uv = l[uv_layer].uv + move_uv + + bmesh.update_edit_mesh(obj.data) return {'FINISHED'} diff --git a/magic_uv/op/copy_paste_uv.py b/magic_uv/op/copy_paste_uv.py index ba754425..0410ee8d 100644 --- a/magic_uv/op/copy_paste_uv.py +++ b/magic_uv/op/copy_paste_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>, Jace Priester" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bmesh import bpy.utils @@ -39,21 +39,17 @@ from ..utils import compatibility as compat def _is_valid_context(context): - obj = context.object + # Multiple objects editing mode is not supported in this feature. + objs = common.get_uv_editable_objects(context) + if len(objs) != 1: + return False # 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: + if not common.is_valid_space(context, ['VIEW_3D']): return False return True @@ -343,7 +339,10 @@ class MUV_OT_CopyPasteUV_CopyUV(bpy.types.Operator): def execute(self, context): props = context.scene.muv_props.copy_paste_uv - obj = context.active_object + + objs = common.get_uv_editable_objects(context) + # poll() method ensures that only one object is selected. + obj = objs[0] bm = common.create_bmesh(obj) # get UV layer @@ -379,8 +378,11 @@ class MUV_MT_CopyPasteUV_CopyUV(bpy.types.Menu): def draw(self, context): layout = self.layout + objs = common.get_uv_editable_objects(context) + # poll() method ensures that only one object is selected. + obj = objs[0] + # create sub menu - obj = context.active_object bm = common.create_bmesh(obj) uv_maps = bm.loops.layers.uv.keys() @@ -452,7 +454,10 @@ class MUV_OT_CopyPasteUV_PasteUV(bpy.types.Operator): if not props.src_info: self.report({'WARNING'}, "Need copy UV at first") return {'CANCELLED'} - obj = context.active_object + + objs = common.get_uv_editable_objects(context) + # poll() method ensures that only one object is selected. + obj = objs[0] bm = common.create_bmesh(obj) # get UV layer @@ -507,8 +512,11 @@ class MUV_MT_CopyPasteUV_PasteUV(bpy.types.Menu): def draw(self, context): sc = context.scene layout = self.layout + objs = common.get_uv_editable_objects(context) + # poll() method ensures that only one object is selected. + obj = objs[0] + # create sub menu - obj = context.active_object bm = common.create_bmesh(obj) uv_maps = bm.loops.layers.uv.keys() @@ -560,7 +568,10 @@ class MUV_OT_CopyPasteUV_SelSeqCopyUV(bpy.types.Operator): def execute(self, context): props = context.scene.muv_props.copy_paste_uv_selseq - obj = context.active_object + + objs = common.get_uv_editable_objects(context) + # poll() method ensures that only one object is selected. + obj = objs[0] bm = common.create_bmesh(obj) # get UV layer @@ -596,7 +607,10 @@ class MUV_MT_CopyPasteUV_SelSeqCopyUV(bpy.types.Menu): def draw(self, context): layout = self.layout - obj = context.active_object + objs = common.get_uv_editable_objects(context) + # poll() method ensures that only one object is selected. + obj = objs[0] + bm = common.create_bmesh(obj) uv_maps = bm.loops.layers.uv.keys() @@ -669,7 +683,10 @@ class MUV_OT_CopyPasteUV_SelSeqPasteUV(bpy.types.Operator): if not props.src_info: self.report({'WARNING'}, "Need copy UV at first") return {'CANCELLED'} - obj = context.active_object + + objs = common.get_uv_editable_objects(context) + # poll() method ensures that only one object is selected. + obj = objs[0] bm = common.create_bmesh(obj) # get UV layer @@ -718,15 +735,18 @@ class MUV_MT_CopyPasteUV_SelSeqPasteUV(bpy.types.Menu): 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: + if not props.src_info: return False return _is_valid_context(context) def draw(self, context): sc = context.scene layout = self.layout + objs = common.get_uv_editable_objects(context) + # poll() method ensures that only one object is selected. + obj = objs[0] + # create sub menu - obj = context.active_object bm = common.create_bmesh(obj) uv_maps = bm.loops.layers.uv.keys() diff --git a/magic_uv/op/copy_paste_uv_object.py b/magic_uv/op/copy_paste_uv_object.py index 1b812b82..2130ea38 100644 --- a/magic_uv/op/copy_paste_uv_object.py +++ b/magic_uv/op/copy_paste_uv_object.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bmesh import bpy @@ -44,21 +44,17 @@ from ..utils import compatibility as compat def _is_valid_context(context): - obj = context.object + # Multiple objects editing mode is not supported in this feature. + objs = common.get_uv_editable_objects(context) + if len(objs) != 1: + return False # 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: + if not common.is_valid_space(context, ['VIEW_3D']): return False return True @@ -121,7 +117,10 @@ class MUV_OT_CopyPasteUVObject_CopyUV(bpy.types.Operator): def execute(self, context): props = context.scene.muv_props.copy_paste_uv_object bpy.ops.object.mode_set(mode='EDIT') - obj = context.active_object + + objs = common.get_uv_editable_objects(context) + # poll() method ensures that only one object is selected. + obj = objs[0] bm = common.create_bmesh(obj) # get UV layer @@ -155,10 +154,14 @@ class MUV_MT_CopyPasteUVObject_CopyUV(bpy.types.Menu): def poll(cls, context): return _is_valid_context(context) - def draw(self, _): + def draw(self, context): layout = self.layout + objs = common.get_uv_editable_objects(context) + # poll() method ensures that only one object is selected. + obj = objs[0] + # create sub menu - uv_maps = compat.get_object_uv_layers(bpy.context.active_object).keys() + uv_maps = compat.get_object_uv_layers(obj).keys() ops = layout.operator(MUV_OT_CopyPasteUVObject_CopyUV.bl_idname, text="[Default]") @@ -211,11 +214,10 @@ class MUV_OT_CopyPasteUVObject_PasteUV(bpy.types.Operator): self.report({'WARNING'}, "Need copy UV at first") return {'CANCELLED'} - for o in bpy.data.objects: + objs = common.get_uv_editable_objects(context) + for o in objs: if not compat.object_has_uv_layers(o): continue - if not compat.get_object_select(o): - continue bpy.ops.object.mode_set(mode='OBJECT') compat.set_active_object(o) @@ -277,11 +279,10 @@ class MUV_MT_CopyPasteUVObject_PasteUV(bpy.types.Menu): layout = self.layout # create sub menu uv_maps = [] - for obj in bpy.data.objects: + objs = common.get_uv_editable_objects(context) + for obj in objs: if not compat.object_has_uv_layers(obj): continue - if not compat.get_object_select(obj): - continue uv_maps.extend(compat.get_object_uv_layers(obj).keys()) ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname, diff --git a/magic_uv/op/copy_paste_uv_uvedit.py b/magic_uv/op/copy_paste_uv_uvedit.py index f12851dd..abfb69a8 100644 --- a/magic_uv/op/copy_paste_uv_uvedit.py +++ b/magic_uv/op/copy_paste_uv_uvedit.py @@ -20,8 +20,8 @@ __author__ = "imdjs, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import math from math import atan2, sin, cos @@ -36,23 +36,19 @@ from ..utils.property_class_registry import PropertyClassRegistry def _is_valid_context(context): - obj = context.object + # Multiple objects editing mode is not supported in this feature. + objs = common.get_uv_editable_objects(context) + if len(objs) != 1: + return False # 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: + if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): return False return True @@ -94,7 +90,10 @@ class MUV_OT_CopyPasteUVUVEdit_CopyUV(bpy.types.Operator): def execute(self, context): props = context.scene.muv_props.copy_paste_uv_uvedit - obj = context.active_object + + objs = common.get_uv_editable_objects(context) + # poll() method ensures that only one object is selected. + obj = objs[0] bm = bmesh.from_edit_mesh(obj.data) uv_layer = bm.loops.layers.uv.verify() if common.check_version(2, 73, 0) >= 0: @@ -140,7 +139,10 @@ class MUV_OT_CopyPasteUVUVEdit_PasteUV(bpy.types.Operator): def execute(self, context): props = context.scene.muv_props.copy_paste_uv_uvedit - obj = context.active_object + + objs = common.get_uv_editable_objects(context) + # poll() method ensures that only one object is selected. + obj = objs[0] bm = bmesh.from_edit_mesh(obj.data) uv_layer = bm.loops.layers.uv.verify() if common.check_version(2, 73, 0) >= 0: diff --git a/magic_uv/op/flip_rotate_uv.py b/magic_uv/op/flip_rotate_uv.py index d0ac6a83..7e6dbdf9 100644 --- a/magic_uv/op/flip_rotate_uv.py +++ b/magic_uv/op/flip_rotate_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy import bmesh @@ -37,37 +37,31 @@ from ..utils import compatibility as compat def _is_valid_context(context): - obj = context.object + objs = common.get_uv_editable_objects(context) + if not objs: + return False # 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: + if not common.is_valid_space(context, ['VIEW_3D']): return False return True -def _get_uv_layer(ops_obj, bm): +def _get_uv_layer(bm): # get UV layer if not bm.loops.layers.uv: - ops_obj.report({'WARNING'}, "Object must have more than one UV map") return None uv_layer = bm.loops.layers.uv.verify() return uv_layer -def _get_src_face_info(ops_obj, bm, uv_layers, only_select=False): +def _get_src_face_info(bm, uv_layers, only_select=False): src_info = {} for layer in uv_layers: face_info = [] @@ -81,14 +75,13 @@ def _get_src_face_info(ops_obj, bm, uv_layers, only_select=False): } face_info.append(info) if not face_info: - ops_obj.report({'WARNING'}, "No faces are selected") return None src_info[layer.name] = face_info return src_info -def _paste_uv(ops_obj, bm, src_info, dest_info, uv_layers, strategy, flip, +def _paste_uv(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] @@ -105,7 +98,6 @@ def _paste_uv(ops_obj, bm, src_info, dest_info, uv_layers, strategy, flip, 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] @@ -199,34 +191,47 @@ class MUV_OT_FlipRotateUV(bpy.types.Operator): def execute(self, context): self.report({'INFO'}, "Flip/Rotate UV") - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - - # get UV layer - uv_layer = _get_uv_layer(self, bm) - if not uv_layer: - return {'CANCELLED'} - - # get selected face - src_info = _get_src_face_info(self, bm, [uv_layer], True) - if not src_info: + objs = common.get_uv_editable_objects(context) + + face_count = 0 + for obj in objs: + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + # get UV layer + uv_layer = _get_uv_layer(bm) + if not uv_layer: + self.report({'WARNING'}, + "Object {} must have more than one UV map" + .format(obj.name)) + return {'CANCELLED'} + + # get selected face + src_info = _get_src_face_info(bm, [uv_layer], True) + if not src_info: + continue + + # paste + ret = _paste_uv(bm, src_info, src_info, [uv_layer], 'N_N', + self.flip, self.rotate, self.seams) + if ret: + self.report({'WARNING'}, + "Some Object {}'s faces are different size" + .format(obj.name)) + return {'CANCELLED'} + + bmesh.update_edit_mesh(obj.data) + if compat.check_version(2, 80, 0) < 0: + if self.seams is True: + obj.data.show_edge_seams = True + + face_count += len(src_info[list(src_info.keys())[0]]) + + if face_count == 0: + self.report({'WARNING'}, "No faces are selected") return {'CANCELLED'} - - face_count = len(src_info[list(src_info.keys())[0]]) - self.report({'INFO'}, "{} face(s) are selected".format(face_count)) - - # paste - ret = _paste_uv(self, bm, src_info, src_info, [uv_layer], 'N_N', - self.flip, self.rotate, self.seams) - if ret: - return {'CANCELLED'} - - bmesh.update_edit_mesh(obj.data) - - if compat.check_version(2, 80, 0) < 0: - if self.seams is True: - obj.data.show_edge_seams = True + self.report({'INFO'}, + "{} face(s) are fliped/rotated".format(face_count)) return {'FINISHED'} diff --git a/magic_uv/op/mirror_uv.py b/magic_uv/op/mirror_uv.py index dcbaad5e..a893ab83 100644 --- a/magic_uv/op/mirror_uv.py +++ b/magic_uv/op/mirror_uv.py @@ -20,8 +20,8 @@ __author__ = "Keith (Wahooney) Boshoff, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy from bpy.props import ( @@ -39,21 +39,16 @@ from .. import common def _is_valid_context(context): - obj = context.object + objs = common.get_uv_editable_objects(context) + if not objs: + return False # 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: + if not common.is_valid_space(context, ['VIEW_3D']): return False return True @@ -168,48 +163,52 @@ class MUV_OT_MirrorUV(bpy.types.Operator): return _is_valid_context(context) def execute(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - - error = self.error - axis = self.axis - - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - if not bm.loops.layers.uv: - self.report({'WARNING'}, - "Object must have more than one UV map") - return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - - faces = [f for f in bm.faces if f.select] - for f_dst in faces: - count = len(f_dst.verts) - for f_src in bm.faces: - # check if this is a candidate to do mirror UV - if f_src.index == f_dst.index: - continue - if count != len(f_src.verts): - continue - - # test if the vertices x values are the same sign - dst = _get_face_center(f_dst) - src = _get_face_center(f_src) - if (dst.x > 0 and src.x > 0) or (dst.x < 0 and src.x < 0): - continue - - # invert source axis - if axis == 'X': - src.x = -src.x - elif axis == 'Y': - src.y = -src.z - elif axis == 'Z': - src.z = -src.z - - # do mirror UV - if _is_vector_similar(dst, src, error): - _mirror_uvs(uv_layer, f_src, f_dst, self.axis, self.error) - - bmesh.update_edit_mesh(obj.data) + objs = common.get_uv_editable_objects(context) + + for obj in objs: + bm = bmesh.from_edit_mesh(obj.data) + + error = self.error + axis = self.axis + + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + if not bm.loops.layers.uv: + self.report({'WARNING'}, + "Object {} must have more than one UV map" + .format(obj.name)) + return {'CANCELLED'} + uv_layer = bm.loops.layers.uv.verify() + + faces = [f for f in bm.faces if f.select] + for f_dst in faces: + count = len(f_dst.verts) + for f_src in bm.faces: + # check if this is a candidate to do mirror UV + if f_src.index == f_dst.index: + continue + if count != len(f_src.verts): + continue + + # test if the vertices x values are the same sign + dst = _get_face_center(f_dst) + src = _get_face_center(f_src) + if (dst.x > 0 and src.x > 0) or (dst.x < 0 and src.x < 0): + continue + + # invert source axis + if axis == 'X': + src.x = -src.x + elif axis == 'Y': + src.y = -src.z + elif axis == 'Z': + src.z = -src.z + + # do mirror UV + if _is_vector_similar(dst, src, error): + _mirror_uvs(uv_layer, f_src, f_dst, + self.axis, self.error) + + bmesh.update_edit_mesh(obj.data) return {'FINISHED'} diff --git a/magic_uv/op/move_uv.py b/magic_uv/op/move_uv.py index 19160a46..fb7c287d 100644 --- a/magic_uv/op/move_uv.py +++ b/magic_uv/op/move_uv.py @@ -20,8 +20,8 @@ __author__ = "kgeogeo, mem, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy from bpy.props import BoolProperty @@ -34,21 +34,17 @@ from ..utils.property_class_registry import PropertyClassRegistry def _is_valid_context(context): - obj = context.object + # Multiple objects editing mode is not supported in this feature. + objs = common.get_uv_editable_objects(context) + if len(objs) != 1: + return False # 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: + if not common.is_valid_space(context, ['VIEW_3D']): return False return True @@ -179,7 +175,9 @@ class MUV_OT_MoveUV(bpy.types.Operator): context.window_manager.modal_handler_add(self) - obj = context.active_object + objs = common.get_uv_editable_objects(context) + # poll() method ensures that only one object is selected. + obj = objs[0] bm = bmesh.from_edit_mesh(obj.data) active_uv = bm.loops.layers.uv.active self.__topology_dict, self.__ini_uvs = self._find_uv(bm, active_uv) diff --git a/magic_uv/op/pack_uv.py b/magic_uv/op/pack_uv.py index 0d7ed966..75fc760c 100644 --- a/magic_uv/op/pack_uv.py +++ b/magic_uv/op/pack_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" from math import fabs @@ -42,23 +42,18 @@ from .. import common def _is_valid_context(context): - obj = context.object + objs = common.get_uv_editable_objects(context) + if not objs: + return False # 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: + if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): return False return True @@ -227,23 +222,37 @@ class MUV_OT_PackUV(bpy.types.Operator): return _is_valid_context(context) def execute(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - if not bm.loops.layers.uv: - self.report({'WARNING'}, - "Object must have more than one UV map") - return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - - selected_faces = [f for f in bm.faces if f.select] - island_info = common.get_island_info(obj) + objs = common.get_uv_editable_objects(context) + + island_info = [] + selected_faces = [] + island_to_bm = {} + island_to_uv_layer = {} + bm_to_loop_lists = {} + for obj in objs: + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + if not bm.loops.layers.uv: + self.report({'WARNING'}, + "Object {} must have more than one UV map" + .format(obj.name)) + return {'CANCELLED'} + uv_layer = bm.loops.layers.uv.verify() + + selected_faces.extend([f for f in bm.faces if f.select]) + isl = common.get_island_info(obj) + for i, info in enumerate(isl): + id_ = i + len(island_info) + island_to_bm[id_] = bm + island_to_uv_layer[id_] = uv_layer + info["id"] = id_ + island_info.extend(isl) + bm_to_loop_lists[bm] = [l for f in bm.faces for l in f.loops] + num_group = _group_island(island_info, self.allowable_center_deviation, self.allowable_size_deviation) - - loop_lists = [l for f in bm.faces for l in f.loops] bpy.ops.mesh.select_all(action='DESELECT') # pack UV @@ -252,7 +261,8 @@ class MUV_OT_PackUV(bpy.types.Operator): lambda i, idx=gidx: i['group'] == idx, island_info)) for f in group[0]['faces']: f['face'].select = True - bmesh.update_edit_mesh(obj.data) + for obj in objs: + bmesh.update_edit_mesh(obj.data) bpy.ops.uv.select_all(action='SELECT') bpy.ops.uv.pack_islands(rotate=self.rotate, margin=self.margin) @@ -262,13 +272,19 @@ class MUV_OT_PackUV(bpy.types.Operator): lambda i, idx=gidx: i['group'] == idx, island_info)) if len(group) <= 1: continue + src_bm = island_to_bm[group[0]["id"]] + src_uv_layer = island_to_uv_layer[group[0]["id"]] + src_loop_lists = bm_to_loop_lists[src_bm] for g in group[1:]: + dst_bm = island_to_bm[g["id"]] + dst_uv_layer = island_to_uv_layer[g["id"]] + dst_loop_lists = bm_to_loop_lists[dst_bm] for (src_face, dest_face) in zip( group[0]['sorted'], g['sorted']): for (src_loop, dest_loop) in zip( src_face['face'].loops, dest_face['face'].loops): - loop_lists[dest_loop.index][uv_layer].uv = loop_lists[ - src_loop.index][uv_layer].uv + dst_loop_lists[dest_loop.index][dst_uv_layer].uv = \ + src_loop_lists[src_loop.index][src_uv_layer].uv # restore face/UV selection bpy.ops.uv.select_all(action='DESELECT') @@ -277,6 +293,7 @@ class MUV_OT_PackUV(bpy.types.Operator): f.select = True bpy.ops.uv.select_all(action='SELECT') - bmesh.update_edit_mesh(obj.data) + for obj in objs: + bmesh.update_edit_mesh(obj.data) return {'FINISHED'} diff --git a/magic_uv/op/preserve_uv_aspect.py b/magic_uv/op/preserve_uv_aspect.py index 5b3e50cf..270bc7ec 100644 --- a/magic_uv/op/preserve_uv_aspect.py +++ b/magic_uv/op/preserve_uv_aspect.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy from bpy.props import StringProperty, EnumProperty, BoolProperty @@ -35,21 +35,16 @@ from ..utils import compatibility as compat def _is_valid_context(context): - obj = context.object + objs = common.get_uv_editable_objects(context) + if not objs: + return False # 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: + if not common.is_valid_space(context, ['VIEW_3D']): return False return True @@ -143,155 +138,204 @@ class MUV_OT_PreserveUVAspect(bpy.types.Operator): # Note: the current system only works if the # f[tex_layer].image doesn't return None # which will happen in certain cases - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - - if not bm.loops.layers.uv: - self.report({'WARNING'}, - "Object must have more than one UV map") - return {'CANCELLED'} - - uv_layer = bm.loops.layers.uv.verify() - - sel_faces = [f for f in bm.faces if f.select] - dest_img = bpy.data.images[self.dest_img_name] - - info = {} - - if compat.check_version(2, 80, 0) >= 0: - tex_image = common.find_image(obj) - for f in sel_faces: - if tex_image not in info.keys(): - info[tex_image] = {} - info[tex_image]['faces'] = [] - info[tex_image]['faces'].append(f) - else: - tex_layer = bm.faces.layers.tex.verify() - for f in sel_faces: - if not f[tex_layer].image in info.keys(): - info[f[tex_layer].image] = {} - info[f[tex_layer].image]['faces'] = [] - info[f[tex_layer].image]['faces'].append(f) - - for img in info: - if img is None: - continue - - src_img = img - ratio = Vector(( - dest_img.size[0] / src_img.size[0], - dest_img.size[1] / src_img.size[1])) - - if self.origin == 'CENTER': - origin = Vector((0.0, 0.0)) - num = 0 - for f in info[img]['faces']: - for l in f.loops: - uv = l[uv_layer].uv - origin = origin + uv - num = num + 1 - origin = origin / num - elif self.origin == 'LEFT_TOP': - origin = Vector((100000.0, -100000.0)) - for f in info[img]['faces']: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = min(origin.x, uv.x) - origin.y = max(origin.y, uv.y) - elif self.origin == 'LEFT_CENTER': - origin = Vector((100000.0, 0.0)) - num = 0 - for f in info[img]['faces']: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = min(origin.x, uv.x) - origin.y = origin.y + uv.y - num = num + 1 - origin.y = origin.y / num - elif self.origin == 'LEFT_BOTTOM': - origin = Vector((100000.0, 100000.0)) - for f in info[img]['faces']: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = min(origin.x, uv.x) - origin.y = min(origin.y, uv.y) - elif self.origin == 'CENTER_TOP': - origin = Vector((0.0, -100000.0)) - num = 0 - for f in info[img]['faces']: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = origin.x + uv.x - origin.y = max(origin.y, uv.y) - num = num + 1 - origin.x = origin.x / num - elif self.origin == 'CENTER_BOTTOM': - origin = Vector((0.0, 100000.0)) - num = 0 - for f in info[img]['faces']: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = origin.x + uv.x - origin.y = min(origin.y, uv.y) - num = num + 1 - origin.x = origin.x / num - elif self.origin == 'RIGHT_TOP': - origin = Vector((-100000.0, -100000.0)) - for f in info[img]['faces']: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = max(origin.x, uv.x) - origin.y = max(origin.y, uv.y) - elif self.origin == 'RIGHT_CENTER': - origin = Vector((-100000.0, 0.0)) - num = 0 - for f in info[img]['faces']: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = max(origin.x, uv.x) - origin.y = origin.y + uv.y - num = num + 1 - origin.y = origin.y / num - elif self.origin == 'RIGHT_BOTTOM': - origin = Vector((-100000.0, 100000.0)) - for f in info[img]['faces']: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = max(origin.x, uv.x) - origin.y = min(origin.y, uv.y) + objs = common.get_uv_editable_objects(context) + + obj_list = {} # { Material: Object } + for obj in objs: + if common.check_version(2, 80, 0) >= 0: + # If more than two selected objects shares same + # material, we need to calculate new UV coordinates + # before image on texture node is overwritten. + material_to_rewrite = [] + for slot in obj.material_slots: + if not slot.material: + continue + nodes = common.find_texture_nodes_from_material( + slot.material) + if len(nodes) >= 2: + self.report( + {'WARNING'}, + "Object {} must not have more than 2 " + "shader nodes with image texture" + .format(obj.name)) + return {'CANCELLED'} + if not nodes: + continue + material_to_rewrite.append(slot.material) + + if len(material_to_rewrite) >= 2: + self.report( + {'WARNING'}, + "Object {} must not have more than 2 " + "materials with image texture" + .format(obj.name)) + return {'CANCELLED'} + if len(material_to_rewrite) == 0: + self.report( + {'WARNING'}, + "Object {} must not have more than 1 " + "material with image texture" + .format(obj.name)) + return {'CANCELLED'} + if material_to_rewrite[0] not in obj_list.keys(): + obj_list[material_to_rewrite[0]] = [] + obj_list[material_to_rewrite[0]].append(obj) else: - self.report({'ERROR'}, "Unknown Operation") - return {'CANCELLED'} - - info[img]['ratio'] = ratio - info[img]['origin'] = origin - - for img in info: - if img is None: - continue + # If blender version is < (2, 79), multiple objects editing + # mode is not supported. So, we add dummy key to obj_list. + obj_list["Dummy"] = [obj] + + # pylint: disable=R1702 + for mtrl, o in obj_list.items(): + for obj in o: + bm = bmesh.from_edit_mesh(obj.data) + + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + if not bm.loops.layers.uv: + self.report({'WARNING'}, + "Object must have more than one UV map") + return {'CANCELLED'} + + uv_layer = bm.loops.layers.uv.verify() + + sel_faces = [f for f in bm.faces if f.select] + dest_img = bpy.data.images[self.dest_img_name] + + info = {} + + if compat.check_version(2, 80, 0) >= 0: + tex_image = common.find_image(obj) + for f in sel_faces: + if tex_image not in info.keys(): + info[tex_image] = {} + info[tex_image]['faces'] = [] + info[tex_image]['faces'].append(f) + else: + tex_layer = bm.faces.layers.tex.verify() + for f in sel_faces: + if not f[tex_layer].image in info.keys(): + info[f[tex_layer].image] = {} + info[f[tex_layer].image]['faces'] = [] + info[f[tex_layer].image]['faces'].append(f) + + for img in info: + if img is None: + continue + + src_img = img + ratio = Vector(( + dest_img.size[0] / src_img.size[0], + dest_img.size[1] / src_img.size[1])) + + if self.origin == 'CENTER': + origin = Vector((0.0, 0.0)) + num = 0 + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin = origin + uv + num = num + 1 + origin = origin / num + elif self.origin == 'LEFT_TOP': + origin = Vector((100000.0, -100000.0)) + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = min(origin.x, uv.x) + origin.y = max(origin.y, uv.y) + elif self.origin == 'LEFT_CENTER': + origin = Vector((100000.0, 0.0)) + num = 0 + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = min(origin.x, uv.x) + origin.y = origin.y + uv.y + num = num + 1 + origin.y = origin.y / num + elif self.origin == 'LEFT_BOTTOM': + origin = Vector((100000.0, 100000.0)) + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = min(origin.x, uv.x) + origin.y = min(origin.y, uv.y) + elif self.origin == 'CENTER_TOP': + origin = Vector((0.0, -100000.0)) + num = 0 + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = origin.x + uv.x + origin.y = max(origin.y, uv.y) + num = num + 1 + origin.x = origin.x / num + elif self.origin == 'CENTER_BOTTOM': + origin = Vector((0.0, 100000.0)) + num = 0 + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = origin.x + uv.x + origin.y = min(origin.y, uv.y) + num = num + 1 + origin.x = origin.x / num + elif self.origin == 'RIGHT_TOP': + origin = Vector((-100000.0, -100000.0)) + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = max(origin.x, uv.x) + origin.y = max(origin.y, uv.y) + elif self.origin == 'RIGHT_CENTER': + origin = Vector((-100000.0, 0.0)) + num = 0 + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = max(origin.x, uv.x) + origin.y = origin.y + uv.y + num = num + 1 + origin.y = origin.y / num + elif self.origin == 'RIGHT_BOTTOM': + origin = Vector((-100000.0, 100000.0)) + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = max(origin.x, uv.x) + origin.y = min(origin.y, uv.y) + else: + self.report({'ERROR'}, "Unknown Operation") + return {'CANCELLED'} + + info[img]['ratio'] = ratio + info[img]['origin'] = origin + + for img in info: + if img is None: + continue + + for f in info[img]['faces']: + if compat.check_version(2, 80, 0) < 0: + tex_layer = bm.faces.layers.tex.verify() + f[tex_layer].image = dest_img + for l in f.loops: + uv = l[uv_layer].uv + origin = info[img]['origin'] + ratio = info[img]['ratio'] + diff = uv - origin + diff.x = diff.x / ratio.x + diff.y = diff.y / ratio.y + uv.x = origin.x + diff.x + uv.y = origin.y + diff.y + l[uv_layer].uv = uv + + bmesh.update_edit_mesh(obj.data) if compat.check_version(2, 80, 0) >= 0: - nodes = common.find_texture_nodes(obj) + nodes = common.find_texture_nodes_from_material(mtrl) nodes[0].image = dest_img - for f in info[img]['faces']: - if compat.check_version(2, 80, 0) < 0: - tex_layer = bm.faces.layers.tex.verify() - f[tex_layer].image = dest_img - for l in f.loops: - uv = l[uv_layer].uv - origin = info[img]['origin'] - ratio = info[img]['ratio'] - diff = uv - origin - diff.x = diff.x / ratio.x - diff.y = diff.y / ratio.y - uv.x = origin.x + diff.x - uv.y = origin.y + diff.y - l[uv_layer].uv = uv - - bmesh.update_edit_mesh(obj.data) - return {'FINISHED'} diff --git a/magic_uv/op/select_uv.py b/magic_uv/op/select_uv.py index d80b43a8..a405e66d 100644 --- a/magic_uv/op/select_uv.py +++ b/magic_uv/op/select_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy from bpy.props import BoolProperty @@ -30,27 +30,21 @@ import bmesh from .. import common from ..utils.bl_class_registry import BlClassRegistry from ..utils.property_class_registry import PropertyClassRegistry -from ..utils import compatibility as compat def _is_valid_context(context): - obj = context.object + objs = common.get_uv_editable_objects(context) + if not objs: + return False # 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: + if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): return False return True @@ -92,18 +86,13 @@ class MUV_OT_SelectUV_SelectOverlapped(bpy.types.Operator): return _is_valid_context(context) def execute(self, context): - objs = [o for o in bpy.data.objects if compat.get_object_select(o)] + objs = common.get_uv_editable_objects(context) bm_list = [] uv_layer_list = [] faces_list = [] - for o in bpy.data.objects: - if not compat.get_object_select(o): - continue - if o.type != 'MESH': - continue - - bm = bmesh.from_edit_mesh(o.data) + for obj in objs: + 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() @@ -126,8 +115,8 @@ class MUV_OT_SelectUV_SelectOverlapped(bpy.types.Operator): for l in info["subject_face"].loops: l[info["subject_uv_layer"]].select = True - for o in objs: - bmesh.update_edit_mesh(o.data) + for obj in objs: + bmesh.update_edit_mesh(obj.data) return {'FINISHED'} @@ -151,18 +140,13 @@ class MUV_OT_SelectUV_SelectFlipped(bpy.types.Operator): return _is_valid_context(context) def execute(self, context): - objs = [o for o in bpy.data.objects if compat.get_object_select(o)] + objs = common.get_uv_editable_objects(context) bm_list = [] uv_layer_list = [] faces_list = [] - for o in bpy.data.objects: - if not compat.get_object_select(o): - continue - if o.type != 'MESH': - continue - - bm = bmesh.from_edit_mesh(o.data) + for obj in objs: + 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() @@ -184,7 +168,7 @@ class MUV_OT_SelectUV_SelectFlipped(bpy.types.Operator): for l in info["face"].loops: l[info["uv_layer"]].select = True - for o in objs: - bmesh.update_edit_mesh(o.data) + for obj in objs: + bmesh.update_edit_mesh(obj.data) return {'FINISHED'} diff --git a/magic_uv/op/smooth_uv.py b/magic_uv/op/smooth_uv.py index 94e41367..9b721615 100644 --- a/magic_uv/op/smooth_uv.py +++ b/magic_uv/op/smooth_uv.py @@ -20,8 +20,8 @@ __author__ = "imdjs, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy from bpy.props import BoolProperty, FloatProperty @@ -34,23 +34,18 @@ from ..utils import compatibility as compat def _is_valid_context(context): - obj = context.object + objs = common.get_uv_editable_objects(context) + if not objs: + return False # 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: + if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): return False return True @@ -261,21 +256,24 @@ class MUV_OT_SmoothUV(bpy.types.Operator): self.__smooth_wo_transmission(loop_seqs, uv_layer) def execute(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - uv_layer = bm.loops.layers.uv.verify() - - # loop_seqs[horizontal][vertical][loop] - loop_seqs, error = common.get_loop_sequences(bm, uv_layer) - if not loop_seqs: - self.report({'WARNING'}, error) - return {'CANCELLED'} - - # smooth - self.__smooth(loop_seqs, uv_layer) - - bmesh.update_edit_mesh(obj.data) + objs = common.get_uv_editable_objects(context) + + for obj in objs: + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.verify() + + # loop_seqs[horizontal][vertical][loop] + loop_seqs, error = common.get_loop_sequences(bm, uv_layer) + if not loop_seqs: + self.report({'WARNING'}, + "Object {}: {}".format(obj.name, error)) + return {'CANCELLED'} + + # smooth + self.__smooth(loop_seqs, uv_layer) + + bmesh.update_edit_mesh(obj.data) return {'FINISHED'} diff --git a/magic_uv/op/texture_lock.py b/magic_uv/op/texture_lock.py index ddcaf315..fb9ac4c7 100644 --- a/magic_uv/op/texture_lock.py +++ b/magic_uv/op/texture_lock.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import math from math import atan2, cos, sqrt, sin, fabs @@ -188,21 +188,16 @@ def _calc_tri_vert(v0, v1, angle0, angle1): def _is_valid_context(context): - obj = context.object + objs = common.get_uv_editable_objects(context) + if not objs: + return False # 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: + if not common.is_valid_space(context, ['VIEW_3D']): return False return True @@ -215,7 +210,7 @@ class _Properties: @classmethod def init_props(cls, scene): class Props(): - verts_orig = None + verts_orig = {} # { Object: verts_orig } scene.muv_props.texture_lock = Props() @@ -282,20 +277,26 @@ class MUV_OT_TextureLock_Lock(bpy.types.Operator): def execute(self, context): props = context.scene.muv_props.texture_lock - obj = bpy.context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.verts.ensure_lookup_table() - bm.edges.ensure_lookup_table() - bm.faces.ensure_lookup_table() - - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - - props.verts_orig = [ - {"vidx": v.index, "vco": v.co.copy(), "moved": False} - for v in bm.verts if v.select] + objs = common.get_uv_editable_objects(context) + + props.verts_orig = {} + for obj in objs: + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + if not bm.loops.layers.uv: + self.report({'WARNING'}, + "Object {} must have more than one UV map" + .format(obj.name)) + return {'CANCELLED'} + + props.verts_orig[obj] = [ + {"vidx": v.index, "vco": v.co.copy(), "moved": False} + for v in bm.verts if v.select + ] return {'FINISHED'} @@ -335,53 +336,63 @@ class MUV_OT_TextureLock_Unlock(bpy.types.Operator): def execute(self, context): sc = context.scene props = sc.muv_props.texture_lock - obj = bpy.context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.verts.ensure_lookup_table() - bm.edges.ensure_lookup_table() - bm.faces.ensure_lookup_table() - - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - - verts = [v.index for v in bm.verts if v.select] - verts_orig = props.verts_orig - - # move UV followed by vertex coordinate - for vidx, v_orig in zip(verts, verts_orig): - if vidx != v_orig["vidx"]: - self.report({'ERROR'}, "Internal Error") - return {"CANCELLED"} + objs = common.get_uv_editable_objects(context) - v = bm.verts[vidx] - link_loops = _get_link_loops(v) - - result = [] - - for ll in link_loops: - ini_geom = _get_ini_geom(ll, uv_layer, verts_orig, v_orig) - target_uv = _get_target_uv( - ll, uv_layer, verts_orig, v, ini_geom) - result.append({"l": ll["l"], "uv": target_uv}) - - # connect other face's UV - if self.connect: - ave = Vector((0.0, 0.0)) - for r in result: - ave = ave + r["uv"] - ave = ave / len(result) - for r in result: - r["l"][uv_layer].uv = ave - else: - for r in result: - r["l"][uv_layer].uv = r["uv"] - v_orig["moved"] = True - bmesh.update_edit_mesh(obj.data) + if set(objs) != set(props.verts_orig.keys()): + self.report({'WARNING'}, + "Object list does not match between Lock and Unlock") + return {'CANCELLED'} - props.verts_orig = None + for obj in objs: + verts_orig = props.verts_orig[obj] + + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + if not bm.loops.layers.uv: + self.report({'WARNING'}, + "Object {} must have more than one UV map" + .format(obj.name)) + return {'CANCELLED'} + uv_layer = bm.loops.layers.uv.verify() + + verts = [v.index for v in bm.verts if v.select] + + # move UV followed by vertex coordinate + for vidx, v_orig in zip(verts, verts_orig): + if vidx != v_orig["vidx"]: + self.report({'ERROR'}, "Internal Error") + return {"CANCELLED"} + + v = bm.verts[vidx] + link_loops = _get_link_loops(v) + + result = [] + + for ll in link_loops: + ini_geom = _get_ini_geom(ll, uv_layer, verts_orig, v_orig) + target_uv = _get_target_uv( + ll, uv_layer, verts_orig, v, ini_geom) + result.append({"l": ll["l"], "uv": target_uv}) + + # connect other face's UV + if self.connect: + ave = Vector((0.0, 0.0)) + for r in result: + ave = ave + r["uv"] + ave = ave / len(result) + for r in result: + r["l"][uv_layer].uv = ave + else: + for r in result: + r["l"][uv_layer].uv = r["uv"] + v_orig["moved"] = True + bmesh.update_edit_mesh(obj.data) + + props.verts_orig = {} return {'FINISHED'} @@ -423,84 +434,100 @@ class MUV_OT_TextureLock_Intr(bpy.types.Operator): cls.__timer = None def __init__(self): - self.__intr_verts_orig = [] - self.__intr_verts = [] + self.__intr_verts_orig = {} # { Object: verts_orig } + self.__intr_verts = {} # { Object: verts_orig } 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() + objs = common.get_uv_editable_objects(context) + + for obj in objs: + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + if obj not in self.__intr_verts: + return True - prev = set(self.__intr_verts) - now = {v.index for v in bm.verts if v.select} + prev = set(self.__intr_verts[obj]) + now = {v.index for v in bm.verts if v.select} - return prev != now + if prev != now: + return True + + return False 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] + objs = common.get_uv_editable_objects(context) + self.__intr_verts_orig = {} + self.__intr_verts = {} + + for obj in objs: + 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[obj] = [ + {"vidx": v.index, "vco": v.co.copy(), "moved": False} + for v in bm.verts if v.select] + self.__intr_verts[obj] = [v.index for v in bm.verts if v.select] def __update_uv(self, context): """ Update UV when vertex coordinates are changed """ - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.verts.ensure_lookup_table() - bm.edges.ensure_lookup_table() - bm.faces.ensure_lookup_table() - - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") - return - uv_layer = bm.loops.layers.uv.verify() - - verts = [v.index for v in bm.verts if v.select] - verts_orig = self.__intr_verts_orig - - for vidx, v_orig in zip(verts, verts_orig): - if vidx != v_orig["vidx"]: - self.report({'ERROR'}, "Internal Error") + objs = common.get_uv_editable_objects(context) + + for obj in objs: + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + if not bm.loops.layers.uv: + self.report({'WARNING'}, + "Object {} must have more than one UV map" + .format(obj.data)) return + uv_layer = bm.loops.layers.uv.verify() + + verts = [v.index for v in bm.verts if v.select] + verts_orig = self.__intr_verts_orig[obj] + + for vidx, v_orig in zip(verts, verts_orig): + if vidx != v_orig["vidx"]: + self.report({'ERROR'}, "Internal Error") + return + + v = bm.verts[vidx] + link_loops = _get_link_loops(v) + + result = [] + for ll in link_loops: + ini_geom = _get_ini_geom(ll, uv_layer, verts_orig, v_orig) + target_uv = _get_target_uv( + ll, uv_layer, verts_orig, v, ini_geom) + result.append({"l": ll["l"], "uv": target_uv}) + + # UV connect option is always true, because it raises + # unexpected behavior + ave = Vector((0.0, 0.0)) + for r in result: + ave = ave + r["uv"] + ave = ave / len(result) + for r in result: + r["l"][uv_layer].uv = ave + v_orig["moved"] = True + bmesh.update_edit_mesh(obj.data) - v = bm.verts[vidx] - link_loops = _get_link_loops(v) - - result = [] - for ll in link_loops: - ini_geom = _get_ini_geom(ll, uv_layer, verts_orig, v_orig) - target_uv = _get_target_uv( - ll, uv_layer, verts_orig, v, ini_geom) - result.append({"l": ll["l"], "uv": target_uv}) - - # UV connect option is always true, because it raises - # unexpected behavior - ave = Vector((0.0, 0.0)) - for r in result: - ave = ave + r["uv"] - ave = ave / len(result) - for r in result: - r["l"][uv_layer].uv = ave - v_orig["moved"] = True - bmesh.update_edit_mesh(obj.data) - - common.redraw_all_areas() - self.__intr_verts_orig = [ - {"vidx": v.index, "vco": v.co.copy(), "moved": False} - for v in bm.verts if v.select] + common.redraw_all_areas() + self.__intr_verts_orig[obj] = [ + {"vidx": v.index, "vco": v.co.copy(), "moved": False} + for v in bm.verts if v.select] def modal(self, context, event): if not _is_valid_context(context): diff --git a/magic_uv/op/texture_projection.py b/magic_uv/op/texture_projection.py index b754dd88..694fac0d 100644 --- a/magic_uv/op/texture_projection.py +++ b/magic_uv/op/texture_projection.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" from collections import namedtuple @@ -124,21 +124,16 @@ def _region_to_canvas(rg_vec, canvas): def _is_valid_context(context): - obj = context.object + objs = common.get_uv_editable_objects(context) + if not objs: + return False # 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: + if not common.is_valid_space(context, ['VIEW_3D']): return False return True @@ -356,62 +351,76 @@ class MUV_OT_TextureProjection_Project(bpy.types.Operator): _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') # get faces to be texture projected - obj = context.active_object - world_mat = obj.matrix_world - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - - # get UV and texture layer - if not bm.loops.layers.uv: - if sc.muv_texture_projection_assign_uvmap: - bm.loops.layers.uv.new() - else: - self.report({'WARNING'}, - "Object must have more than one UV map") - return {'CANCELLED'} - - uv_layer = bm.loops.layers.uv.verify() - if compat.check_version(2, 80, 0) < 0: - tex_layer = bm.faces.layers.tex.verify() - - sel_faces = [f for f in bm.faces if f.select] - - # transform 3d space to screen region - v_screen = [ - view3d_utils.location_3d_to_region_2d( - region, - space.region_3d, - compat.matmul(world_mat, l.vert.co)) - for f in sel_faces for l in f.loops - ] - - # transform screen region to canvas - v_canvas = [ - _region_to_canvas( - v, - _get_canvas(bpy.context, - sc.muv_texture_projection_tex_magnitude) - ) for v in v_screen - ] - - if compat.check_version(2, 80, 0) >= 0: - # set texture - nodes = common.find_texture_nodes(obj) - nodes[0].image = \ - bpy.data.images[sc.muv_texture_projection_tex_image] - - # project texture to object - i = 0 - for f in sel_faces: + objs = common.get_uv_editable_objects(context) + + for obj in objs: + world_mat = obj.matrix_world + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + # get UV and texture layer + if not bm.loops.layers.uv: + if sc.muv_texture_projection_assign_uvmap: + bm.loops.layers.uv.new() + else: + self.report({'WARNING'}, + "Object {} must have more than one UV map" + .format(obj.name)) + return {'CANCELLED'} + + uv_layer = bm.loops.layers.uv.verify() if compat.check_version(2, 80, 0) < 0: - f[tex_layer].image = \ + tex_layer = bm.faces.layers.tex.verify() + + sel_faces = [f for f in bm.faces if f.select] + + # transform 3d space to screen region + v_screen = [ + view3d_utils.location_3d_to_region_2d( + region, + space.region_3d, + compat.matmul(world_mat, l.vert.co)) + for f in sel_faces for l in f.loops + ] + + # transform screen region to canvas + v_canvas = [ + _region_to_canvas( + v, + _get_canvas(bpy.context, + sc.muv_texture_projection_tex_magnitude) + ) for v in v_screen + ] + + # assign image + if compat.check_version(2, 80, 0) >= 0: + node_tree = obj.active_material.node_tree + output_node = node_tree.nodes["Material Output"] + + nodes = common.find_texture_nodes_from_material( + obj.active_material) + if len(nodes) >= 1: + tex_node = nodes[0] + else: + tex_node = node_tree.nodes.new(type="ShaderNodeTexImage") + tex_node.image = \ bpy.data.images[sc.muv_texture_projection_tex_image] - for l in f.loops: - l[uv_layer].uv = v_canvas[i].to_2d() - i = i + 1 - - common.redraw_all_areas() - bmesh.update_edit_mesh(obj.data) + node_tree.links.new( + output_node.inputs["Surface"], tex_node.outputs["Color"]) + else: + for f in sel_faces: + f[tex_layer].image = \ + bpy.data.images[sc.muv_texture_projection_tex_image] + + # project texture to object + i = 0 + for f in sel_faces: + for l in f.loops: + l[uv_layer].uv = v_canvas[i].to_2d() + i = i + 1 + + common.redraw_all_areas() + bmesh.update_edit_mesh(obj.data) return {'FINISHED'} diff --git a/magic_uv/op/texture_wrap.py b/magic_uv/op/texture_wrap.py index 92512438..5fd0cfe6 100644 --- a/magic_uv/op/texture_wrap.py +++ b/magic_uv/op/texture_wrap.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy from bpy.props import ( @@ -35,21 +35,17 @@ from ..utils.property_class_registry import PropertyClassRegistry def _is_valid_context(context): - obj = context.object + # Multiple objects editing mode is not supported in this feature. + objs = common.get_uv_editable_objects(context) + if len(objs) != 1: + return False # 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: + if not common.is_valid_space(context, ['VIEW_3D']): return False return True @@ -111,7 +107,10 @@ class MUV_OT_TextureWrap_Refer(bpy.types.Operator): def execute(self, context): props = context.scene.muv_props.texture_wrap - obj = context.active_object + + objs = common.get_uv_editable_objects(context) + # poll() method ensures that only one object is selected. + obj = objs[0] bm = bmesh.from_edit_mesh(obj.data) if common.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() @@ -156,7 +155,10 @@ class MUV_OT_TextureWrap_Set(bpy.types.Operator): def execute(self, context): sc = context.scene props = sc.muv_props.texture_wrap - obj = context.active_object + + objs = common.get_uv_editable_objects(context) + # poll() method ensures that only one object is selected. + obj = objs[0] bm = bmesh.from_edit_mesh(obj.data) if common.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() diff --git a/magic_uv/op/transfer_uv.py b/magic_uv/op/transfer_uv.py index ce9639a7..bcf9fab9 100644 --- a/magic_uv/op/transfer_uv.py +++ b/magic_uv/op/transfer_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>, Mifth, MaxRobinot" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" from collections import OrderedDict @@ -36,21 +36,17 @@ from ..utils import compatibility as compat def _is_valid_context(context): - obj = context.object + # Multiple objects editing mode is not supported in this feature. + objs = common.get_uv_editable_objects(context) + if len(objs) != 1: + return False # 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: + if not common.is_valid_space(context, ['VIEW_3D']): return False return True @@ -377,8 +373,11 @@ class MUV_OT_TransferUV_CopyUV(bpy.types.Operator): def execute(self, context): props = context.scene.muv_props.transfer_uv - active_obj = context.active_object - bm = bmesh.from_edit_mesh(active_obj.data) + + objs = common.get_uv_editable_objects(context) + # poll() method ensures that only one object is selected. + obj = objs[0] + bm = bmesh.from_edit_mesh(obj.data) if compat.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() @@ -391,7 +390,7 @@ class MUV_OT_TransferUV_CopyUV(bpy.types.Operator): return {'CANCELLED'} props.topology_copied = faces - bmesh.update_edit_mesh(active_obj.data) + bmesh.update_edit_mesh(obj.data) return {'FINISHED'} @@ -433,8 +432,11 @@ class MUV_OT_TransferUV_PasteUV(bpy.types.Operator): def execute(self, context): props = context.scene.muv_props.transfer_uv - active_obj = context.active_object - bm = bmesh.from_edit_mesh(active_obj.data) + + objs = common.get_uv_editable_objects(context) + # poll() method ensures that only one object is selected. + obj = objs[0] + bm = bmesh.from_edit_mesh(obj.data) if compat.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() @@ -448,10 +450,10 @@ class MUV_OT_TransferUV_PasteUV(bpy.types.Operator): if ret: return {'CANCELLED'} - bmesh.update_edit_mesh(active_obj.data) + bmesh.update_edit_mesh(obj.data) if compat.check_version(2, 80, 0) < 0: if self.copy_seams: - active_obj.data.show_edge_seams = True + obj.data.show_edge_seams = True return {'FINISHED'} diff --git a/magic_uv/op/unwrap_constraint.py b/magic_uv/op/unwrap_constraint.py index 3c23575a..dcaa79b4 100644 --- a/magic_uv/op/unwrap_constraint.py +++ b/magic_uv/op/unwrap_constraint.py @@ -18,8 +18,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy from bpy.props import ( @@ -36,21 +36,16 @@ from ..utils import compatibility as compat def _is_valid_context(context): - obj = context.object + objs = common.get_uv_editable_objects(context) + if not objs: + return False # 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: + if not common.is_valid_space(context, ['VIEW_3D']): return False return True @@ -146,23 +141,28 @@ class MUV_OT_UnwrapConstraint(bpy.types.Operator): return _is_valid_context(context) def execute(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - - # bpy.ops.uv.unwrap() makes one UV map at least - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - - # get original UV coordinate - faces = [f for f in bm.faces if f.select] - uv_list = [] - for f in faces: - uvs = [l[uv_layer].uv.copy() for l in f.loops] - uv_list.append(uvs) + objs = common.get_uv_editable_objects(context) + + uv_list = {} # { Object: uv_list } + for obj in objs: + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + # bpy.ops.uv.unwrap() makes one UV map at least + if not bm.loops.layers.uv: + self.report({'WARNING'}, + "Object {} must have more than one UV map" + .format(obj.name)) + return {'CANCELLED'} + uv_layer = bm.loops.layers.uv.verify() + + # get original UV coordinate + faces = [f for f in bm.faces if f.select] + uv_list[obj] = [] + for f in faces: + uvs = [l[uv_layer].uv.copy() for l in f.loops] + uv_list[obj].append(uvs) # unwrap bpy.ops.uv.unwrap( @@ -173,14 +173,21 @@ class MUV_OT_UnwrapConstraint(bpy.types.Operator): margin=self.margin) # when U/V-Constraint is checked, revert original coordinate - for f, uvs in zip(faces, uv_list): - for l, uv in zip(f.loops, uvs): - if self.u_const: - l[uv_layer].uv.x = uv.x - if self.v_const: - l[uv_layer].uv.y = uv.y - - # update mesh - bmesh.update_edit_mesh(obj.data) + for obj in objs: + 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() + faces = [f for f in bm.faces if f.select] + + for f, uvs in zip(faces, uv_list[obj]): + for l, uv in zip(f.loops, uvs): + if self.u_const: + l[uv_layer].uv.x = uv.x + if self.v_const: + l[uv_layer].uv.y = uv.y + + # update mesh + bmesh.update_edit_mesh(obj.data) return {'FINISHED'} diff --git a/magic_uv/op/uv_bounding_box.py b/magic_uv/op/uv_bounding_box.py index d4edac9c..fc3455b4 100644 --- a/magic_uv/op/uv_bounding_box.py +++ b/magic_uv/op/uv_bounding_box.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" from enum import IntEnum import math @@ -59,10 +59,7 @@ 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: + if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): return False return True @@ -700,23 +697,35 @@ class MUV_OT_UVBoundingBox(bpy.types.Operator): Get UV coordinate """ sc = context.scene - obj = context.active_object + objs = common.get_uv_editable_objects(context) uv_info = [] - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - if not bm.loops.layers.uv: - return None - uv_layer = bm.loops.layers.uv.verify() - for f in bm.faces: - if not f.select: + + for obj in objs: + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + if not bm.loops.layers.uv: continue - for i, l in enumerate(f.loops): - if sc.muv_uv_bounding_box_boundary == 'UV_SEL': - if l[uv_layer].select: - uv_info.append((f.index, i, l[uv_layer].uv.copy())) - elif sc.muv_uv_bounding_box_boundary == 'UV': - uv_info.append((f.index, i, l[uv_layer].uv.copy())) + uv_layer = bm.loops.layers.uv.verify() + for f in bm.faces: + if not f.select: + continue + for i, l in enumerate(f.loops): + if sc.muv_uv_bounding_box_boundary == 'UV_SEL': + if l[uv_layer].select: + uv_info.append({ + "bmesh": bm, + "fidx": f.index, + "lidx": i, + "uv": l[uv_layer].uv.copy() + }) + elif sc.muv_uv_bounding_box_boundary == 'UV': + uv_info.append({ + "bmesh": bm, + "fidx": f.index, + "lidx": i, + "uv": l[uv_layer].uv.copy() + }) if not uv_info: return None return uv_info @@ -731,7 +740,7 @@ class MUV_OT_UVBoundingBox(bpy.types.Operator): bottom = MAX_VALUE for info in uv_info_ini: - uv = info[2] + uv = info["uv"] if uv.x < left: left = uv.x if uv.x > right: @@ -762,22 +771,21 @@ class MUV_OT_UVBoundingBox(bpy.types.Operator): """ Update UV coordinate """ - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - if not bm.loops.layers.uv: - return - uv_layer = bm.loops.layers.uv.verify() + for info in uv_info_ini: - fidx = info[0] - lidx = info[1] - uv = info[2] + bm = info["bmesh"] + uv_layer = bm.loops.layers.uv.verify() + fidx = info["fidx"] + lidx = info["lidx"] + uv = info["uv"] v = mathutils.Vector((uv.x, uv.y, 0.0)) av = compat.matmul(trans_mat, v) bm.faces[fidx].loops[lidx][uv_layer].uv = mathutils.Vector( (av.x, av.y)) - bmesh.update_edit_mesh(obj.data) + + objs = common.get_uv_editable_objects(context) + for obj in objs: + bmesh.update_edit_mesh(obj.data) def __update_ctrl_point(self, ctrl_points_ini, trans_mat): """ diff --git a/magic_uv/op/uv_inspection.py b/magic_uv/op/uv_inspection.py index 8aae181e..49525b98 100644 --- a/magic_uv/op/uv_inspection.py +++ b/magic_uv/op/uv_inspection.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import random from math import fabs @@ -42,23 +42,18 @@ else: def _is_valid_context(context): - obj = context.object + objs = common.get_uv_editable_objects(context) + if not objs: + return False # 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: + if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): return False return True @@ -67,17 +62,13 @@ def _is_valid_context(context): def _update_uvinsp_info(context): sc = context.scene props = sc.muv_props.uv_inspection + objs = common.get_uv_editable_objects(context) bm_list = [] uv_layer_list = [] faces_list = [] - for o in bpy.data.objects: - if not compat.get_object_select(o): - continue - if o.type != 'MESH': - continue - - bm = bmesh.from_edit_mesh(o.data) + for obj in objs: + 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() @@ -359,137 +350,156 @@ class MUV_OT_UVInspection_PaintUVIsland(bpy.types.Operator): return None def execute(self, context): - obj = context.active_object + selected_objs_orig = [o for o in bpy.data.objects + if compat.get_object_select(o)] + active_obj_orig = compat.get_active_object(context) + + objs = common.get_uv_editable_objects(context) mode_orig = context.object.mode override_context = self._get_override_context(context) if override_context is None: self.report({'WARNING'}, "More than one 'VIEW_3D' area must exist") return {'CANCELLED'} - # Setup material of drawing target. - target_image = self._get_or_new_image( - "MagicUV_PaintUVIsland", 4096, 4096) - target_mtrl = self._get_or_new_material("MagicUV_PaintUVMaterial") - if compat.check_version(2, 80, 0) >= 0: - target_mtrl.use_nodes = True - output_node = target_mtrl.node_tree.nodes["Material Output"] - nodes_to_remove = [n for n in target_mtrl.node_tree.nodes - if n != output_node] - for n in nodes_to_remove: - target_mtrl.node_tree.nodes.remove(n) - texture_node = \ - target_mtrl.node_tree.nodes.new("ShaderNodeTexImage") - texture_node.image = target_image - target_mtrl.node_tree.links.new(output_node.inputs["Surface"], - texture_node.outputs["Color"]) - obj.data.use_paint_mask = True - - # Apply material to object (all faces). - found = False - for mtrl_idx, mtrl_slot in enumerate(obj.material_slots): - if mtrl_slot.material == target_mtrl: - found = True - break - if not found: - bpy.ops.object.material_slot_add() - mtrl_idx = len(obj.material_slots) - 1 - obj.material_slots[mtrl_idx].material = target_mtrl - bpy.ops.object.mode_set(mode='EDIT') - bm = bmesh.from_edit_mesh(obj.data) - bm.faces.ensure_lookup_table() - for f in bm.faces: - f.select = True - bmesh.update_edit_mesh(obj.data) - obj.active_material_index = mtrl_idx - obj.active_material = target_mtrl - bpy.ops.object.material_slot_assign() - else: - target_tex_slot = target_mtrl.texture_slots.add() - target_tex = self._get_or_new_texture("MagicUV_PaintUVTexture") - target_tex_slot.texture = target_tex - obj.data.use_paint_mask = True - - # Apply material to object (all faces). - found = False - for mtrl_idx, mtrl_slot in enumerate(obj.material_slots): - if mtrl_slot.material == target_mtrl: - found = True - break - if not found: - bpy.ops.object.material_slot_add() - mtrl_idx = len(obj.material_slots) - 1 - obj.material_slots[mtrl_idx].material = target_mtrl - bpy.ops.object.mode_set(mode='EDIT') - bm = bmesh.from_edit_mesh(obj.data) - bm.faces.ensure_lookup_table() - for f in bm.faces: - f.select = True - bmesh.update_edit_mesh(obj.data) - obj.active_material_index = mtrl_idx - obj.active_material = target_mtrl - bpy.ops.object.material_slot_assign() - - # Update active image in Image Editor. - _, _, space = common.get_space( - 'IMAGE_EDITOR', 'WINDOW', 'IMAGE_EDITOR') - if space is None: - return {'CANCELLED'} - space.image = target_image - - # Analyze island to make map between face and paint color. - islands = common.get_island_info_from_bmesh(bm) - color_to_faces = [] - for isl in islands: - color = self._create_unique_color([c[0] for c in color_to_faces]) - if color is None: - self.report({'WARNING'}, - "Failed to create color. Please try again") - return {'CANCELLED'} - indices = [f["face"].index for f in isl["faces"]] - color_to_faces.append((color, indices)) - - for cf in color_to_faces: - # Update selection information. - bpy.ops.object.mode_set(mode='EDIT') - bm = bmesh.from_edit_mesh(obj.data) - bm.faces.ensure_lookup_table() - for f in bm.faces: - f.select = False - for fidx in cf[1]: - bm.faces[fidx].select = True - bmesh.update_edit_mesh(obj.data) - bpy.ops.object.mode_set(mode='OBJECT') - - # Update brush color. - bpy.data.brushes["Fill"].color = cf[0] - - # Paint. - bpy.ops.object.mode_set(mode='TEXTURE_PAINT') + for i, obj in enumerate(objs): + # Select/Active only one object to paint. + for o in objs: + compat.set_object_select(o, False) + compat.set_object_select(obj, True) + compat.set_active_object(obj) + + # Setup material of drawing target. + target_image = self._get_or_new_image( + "MagicUV_PaintUVIsland_{}".format(i), 4096, 4096) + target_mtrl = self._get_or_new_material( + "MagicUV_PaintUVMaterial_{}".format(i)) if compat.check_version(2, 80, 0) >= 0: - bpy.ops.paint.brush_select(override_context, image_tool='FILL') + target_mtrl.use_nodes = True + output_node = target_mtrl.node_tree.nodes["Material Output"] + nodes_to_remove = [n for n in target_mtrl.node_tree.nodes + if n != output_node] + for n in nodes_to_remove: + target_mtrl.node_tree.nodes.remove(n) + texture_node = \ + target_mtrl.node_tree.nodes.new("ShaderNodeTexImage") + texture_node.image = target_image + target_mtrl.node_tree.links.new(output_node.inputs["Surface"], + texture_node.outputs["Color"]) + obj.data.use_paint_mask = True + + # Apply material to object (all faces). + found = False + for mtrl_idx, mtrl_slot in enumerate(obj.material_slots): + if mtrl_slot.material == target_mtrl: + found = True + break + if not found: + bpy.ops.object.material_slot_add() + mtrl_idx = len(obj.material_slots) - 1 + obj.material_slots[mtrl_idx].material = target_mtrl + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(obj.data) + bm.faces.ensure_lookup_table() + for f in bm.faces: + f.select = True + bmesh.update_edit_mesh(obj.data) + obj.active_material_index = mtrl_idx + obj.active_material = target_mtrl + bpy.ops.object.material_slot_assign() else: - paint_settings = \ - bpy.data.scenes['Scene'].tool_settings.image_paint - paint_mode_orig = paint_settings.mode - paint_canvas_orig = paint_settings.canvas - paint_settings.mode = 'IMAGE' - paint_settings.canvas = target_image - bpy.ops.paint.brush_select(override_context, - texture_paint_tool='FILL') - bpy.ops.paint.image_paint(override_context, stroke=[{ - "name": "", - "location": (0, 0, 0), - "mouse": (0, 0), - "size": 0, - "pressure": 0, - "pen_flip": False, - "time": 0, - "is_start": False - }]) - - if compat.check_version(2, 80, 0) < 0: - paint_settings.mode = paint_mode_orig - paint_settings.canvas = paint_canvas_orig + target_tex_slot = target_mtrl.texture_slots.add() + target_tex = self._get_or_new_texture( + "MagicUV_PaintUVTexture_{}".format(i)) + target_tex_slot.texture = target_tex + obj.data.use_paint_mask = True + + # Apply material to object (all faces). + found = False + for mtrl_idx, mtrl_slot in enumerate(obj.material_slots): + if mtrl_slot.material == target_mtrl: + found = True + break + if not found: + bpy.ops.object.material_slot_add() + mtrl_idx = len(obj.material_slots) - 1 + obj.material_slots[mtrl_idx].material = target_mtrl + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(obj.data) + bm.faces.ensure_lookup_table() + for f in bm.faces: + f.select = True + bmesh.update_edit_mesh(obj.data) + obj.active_material_index = mtrl_idx + obj.active_material = target_mtrl + bpy.ops.object.material_slot_assign() + + # Update active image in Image Editor. + _, _, space = common.get_space( + 'IMAGE_EDITOR', 'WINDOW', 'IMAGE_EDITOR') + if space is None: + return {'CANCELLED'} + space.image = target_image + + # Analyze island to make map between face and paint color. + islands = common.get_island_info_from_bmesh(bm) + color_to_faces = [] + for isl in islands: + color = self._create_unique_color( + [c[0] for c in color_to_faces]) + if color is None: + self.report({'WARNING'}, + "Failed to create color. Please try again") + return {'CANCELLED'} + indices = [f["face"].index for f in isl["faces"]] + color_to_faces.append((color, indices)) + + for cf in color_to_faces: + # Update selection information. + bpy.ops.object.mode_set(mode='EDIT') + bm = bmesh.from_edit_mesh(obj.data) + bm.faces.ensure_lookup_table() + for f in bm.faces: + f.select = False + for fidx in cf[1]: + bm.faces[fidx].select = True + bmesh.update_edit_mesh(obj.data) + bpy.ops.object.mode_set(mode='OBJECT') + + # Update brush color. + bpy.data.brushes["Fill"].color = cf[0] + + # Paint. + bpy.ops.object.mode_set(mode='TEXTURE_PAINT') + if compat.check_version(2, 80, 0) >= 0: + bpy.ops.paint.brush_select(override_context, + image_tool='FILL') + else: + paint_settings = \ + bpy.data.scenes['Scene'].tool_settings.image_paint + paint_mode_orig = paint_settings.mode + paint_canvas_orig = paint_settings.canvas + paint_settings.mode = 'IMAGE' + paint_settings.canvas = target_image + bpy.ops.paint.brush_select(override_context, + texture_paint_tool='FILL') + bpy.ops.paint.image_paint(override_context, stroke=[{ + "name": "", + "location": (0, 0, 0), + "mouse": (0, 0), + "size": 0, + "pressure": 0, + "pen_flip": False, + "time": 0, + "is_start": False + }]) + + if compat.check_version(2, 80, 0) < 0: + paint_settings.mode = paint_mode_orig + paint_settings.canvas = paint_canvas_orig + + for obj in selected_objs_orig: + compat.set_object_select(obj, True) + compat.set_active_object(active_obj_orig) bpy.ops.object.mode_set(mode=mode_orig) diff --git a/magic_uv/op/uv_sculpt.py b/magic_uv/op/uv_sculpt.py index f40ab253..c9ed4f34 100644 --- a/magic_uv/op/uv_sculpt.py +++ b/magic_uv/op/uv_sculpt.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" from math import pi, cos, tan, sin @@ -51,21 +51,16 @@ else: def _is_valid_context(context): - obj = context.object + objs = common.get_uv_editable_objects(context) + if not objs: + return False # 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: + if not common.is_valid_space(context, ['VIEW_3D']): return False return True @@ -255,7 +250,7 @@ class MUV_OT_UVSculpt(bpy.types.Operator): bgl.glEnd() def __init__(self): - self.__loop_info = [] + self.__loop_info = {} # { Object: loop_info } self.__stroking = False self.current_mco = Vector((0.0, 0.0)) self.__initial_mco = Vector((0.0, 0.0)) @@ -265,52 +260,17 @@ class MUV_OT_UVSculpt(bpy.types.Operator): self.__initial_mco = self.current_mco - # get influenced UV - obj = context.active_object - world_mat = obj.matrix_world - bm = bmesh.from_edit_mesh(obj.data) - uv_layer = bm.loops.layers.uv.verify() - _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') - - self.__loop_info = [] - for f in bm.faces: - if not f.select: - continue - for i, l in enumerate(f.loops): - loc_2d = location_3d_to_region_2d_extra( - region, space.region_3d, - compat.matmul(world_mat, l.vert.co)) - diff = loc_2d - self.__initial_mco - if diff.length < sc.muv_uv_sculpt_radius: - info = { - "face_idx": f.index, - "loop_idx": i, - "initial_vco": l.vert.co.copy(), - "initial_vco_2d": loc_2d, - "initial_uv": l[uv_layer].uv.copy(), - "strength": _get_strength( - diff.length, sc.muv_uv_sculpt_radius, - sc.muv_uv_sculpt_strength) - } - self.__loop_info.append(info) + objs = common.get_uv_editable_objects(context) - def __stroke_apply(self, context, _): - sc = context.scene - obj = context.active_object - world_mat = obj.matrix_world - bm = bmesh.from_edit_mesh(obj.data) - uv_layer = bm.loops.layers.uv.verify() - mco = self.current_mco - - if sc.muv_uv_sculpt_tools == 'GRAB': - for info in self.__loop_info: - diff_uv = (mco - self.__initial_mco) * info["strength"] - l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] - l[uv_layer].uv = info["initial_uv"] + diff_uv / 100.0 - - elif sc.muv_uv_sculpt_tools == 'PINCH': + # get influenced UV + self.__loop_info = {} + for obj in objs: + world_mat = obj.matrix_world + bm = bmesh.from_edit_mesh(obj.data) + uv_layer = bm.loops.layers.uv.verify() _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') - loop_info = [] + + self.__loop_info[obj] = [] for f in bm.faces: if not f.select: continue @@ -330,123 +290,170 @@ class MUV_OT_UVSculpt(bpy.types.Operator): diff.length, sc.muv_uv_sculpt_radius, sc.muv_uv_sculpt_strength) } - loop_info.append(info) - - # mouse coordinate to UV coordinate - ray_vec = view3d_utils.region_2d_to_vector_3d(region, - space.region_3d, mco) - ray_vec.normalize() - ray_orig = view3d_utils.region_2d_to_origin_3d(region, - space.region_3d, - mco) - ray_tgt = ray_orig + ray_vec * 1000000.0 - mwi = world_mat.inverted() - ray_orig_obj = compat.matmul(mwi, ray_orig) - ray_tgt_obj = compat.matmul(mwi, ray_tgt) - ray_dir_obj = ray_tgt_obj - ray_orig_obj - ray_dir_obj.normalize() - tree = BVHTree.FromBMesh(bm) - loc, _, fidx, _ = tree.ray_cast(ray_orig_obj, ray_dir_obj) - if not loc: - return - loops = [l for l in bm.faces[fidx].loops] - uvs = [Vector((l[uv_layer].uv.x, l[uv_layer].uv.y, 0.0)) - for l in loops] - target_uv = barycentric_transform( - loc, loops[0].vert.co, loops[1].vert.co, loops[2].vert.co, - uvs[0], uvs[1], uvs[2]) - target_uv = Vector((target_uv.x, target_uv.y)) - - # move to target UV coordinate - for info in loop_info: - l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] - if sc.muv_uv_sculpt_pinch_invert: - diff_uv = (l[uv_layer].uv - target_uv) * info["strength"] - else: - diff_uv = (target_uv - l[uv_layer].uv) * info["strength"] - l[uv_layer].uv = l[uv_layer].uv + diff_uv / 10.0 - - elif sc.muv_uv_sculpt_tools == 'RELAX': - _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') + self.__loop_info[obj].append(info) - # get vertex and loop relation - vert_db = {} - for f in bm.faces: - for l in f.loops: - if l.vert in vert_db: - vert_db[l.vert]["loops"].append(l) - else: - vert_db[l.vert] = {"loops": [l]} - - # get relaxation information - for k in vert_db.keys(): - d = vert_db[k] - d["uv_sum"] = Vector((0.0, 0.0)) - d["uv_count"] = 0 - - for l in d["loops"]: - ln = l.link_loop_next - lp = l.link_loop_prev - d["uv_sum"] = d["uv_sum"] + ln[uv_layer].uv - d["uv_sum"] = d["uv_sum"] + lp[uv_layer].uv - d["uv_count"] = d["uv_count"] + 2 - d["uv_p"] = d["uv_sum"] / d["uv_count"] - d["uv_b"] = d["uv_p"] - d["loops"][0][uv_layer].uv - for k in vert_db.keys(): - d = vert_db[k] - d["uv_sum_b"] = Vector((0.0, 0.0)) - for l in d["loops"]: - ln = l.link_loop_next - lp = l.link_loop_prev - dn = vert_db[ln.vert] - dp = vert_db[lp.vert] - d["uv_sum_b"] = d["uv_sum_b"] + dn["uv_b"] + dp["uv_b"] - - # apply - for f in bm.faces: - if not f.select: - continue - for i, l in enumerate(f.loops): - loc_2d = location_3d_to_region_2d_extra( - region, space.region_3d, - compat.matmul(world_mat, l.vert.co)) - diff = loc_2d - self.__initial_mco - if diff.length >= sc.muv_uv_sculpt_radius: + def __stroke_apply(self, context, _): + sc = context.scene + objs = common.get_uv_editable_objects(context) + + for obj in objs: + world_mat = obj.matrix_world + bm = bmesh.from_edit_mesh(obj.data) + uv_layer = bm.loops.layers.uv.verify() + mco = self.current_mco + + if sc.muv_uv_sculpt_tools == 'GRAB': + for info in self.__loop_info[obj]: + diff_uv = (mco - self.__initial_mco) * info["strength"] + l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] + l[uv_layer].uv = info["initial_uv"] + diff_uv / 100.0 + + elif sc.muv_uv_sculpt_tools == 'PINCH': + _, region, space = common.get_space( + 'VIEW_3D', 'WINDOW', 'VIEW_3D') + loop_info = [] + for f in bm.faces: + if not f.select: continue - db = vert_db[l.vert] - strength = _get_strength(diff.length, - sc.muv_uv_sculpt_radius, - sc.muv_uv_sculpt_strength) - - base = (1.0 - strength) * l[uv_layer].uv - if sc.muv_uv_sculpt_relax_method == 'HC': - t = 0.5 * (db["uv_b"] + db["uv_sum_b"] / d["uv_count"]) - diff = strength * (db["uv_p"] - t) - target_uv = base + diff - elif sc.muv_uv_sculpt_relax_method == 'LAPLACIAN': - diff = strength * db["uv_p"] - target_uv = base + diff + for i, l in enumerate(f.loops): + loc_2d = location_3d_to_region_2d_extra( + region, space.region_3d, + compat.matmul(world_mat, l.vert.co)) + diff = loc_2d - self.__initial_mco + if diff.length < sc.muv_uv_sculpt_radius: + info = { + "face_idx": f.index, + "loop_idx": i, + "initial_vco": l.vert.co.copy(), + "initial_vco_2d": loc_2d, + "initial_uv": l[uv_layer].uv.copy(), + "strength": _get_strength( + diff.length, sc.muv_uv_sculpt_radius, + sc.muv_uv_sculpt_strength) + } + loop_info.append(info) + + # mouse coordinate to UV coordinate + ray_vec = view3d_utils.region_2d_to_vector_3d( + region, space.region_3d, mco) + ray_vec.normalize() + ray_orig = view3d_utils.region_2d_to_origin_3d( + region, space.region_3d, mco) + ray_tgt = ray_orig + ray_vec * 1000000.0 + mwi = world_mat.inverted() + ray_orig_obj = compat.matmul(mwi, ray_orig) + ray_tgt_obj = compat.matmul(mwi, ray_tgt) + ray_dir_obj = ray_tgt_obj - ray_orig_obj + ray_dir_obj.normalize() + tree = BVHTree.FromBMesh(bm) + loc, _, fidx, _ = tree.ray_cast(ray_orig_obj, ray_dir_obj) + if not loc: + return + loops = [l for l in bm.faces[fidx].loops] + uvs = [Vector((l[uv_layer].uv.x, l[uv_layer].uv.y, 0.0)) + for l in loops] + target_uv = barycentric_transform( + loc, + loops[0].vert.co, loops[1].vert.co, loops[2].vert.co, + uvs[0], uvs[1], uvs[2]) + target_uv = Vector((target_uv.x, target_uv.y)) + + # move to target UV coordinate + for info in loop_info: + l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] + if sc.muv_uv_sculpt_pinch_invert: + diff_uv = \ + (l[uv_layer].uv - target_uv) * info["strength"] else: + diff_uv = \ + (target_uv - l[uv_layer].uv) * info["strength"] + l[uv_layer].uv = l[uv_layer].uv + diff_uv / 10.0 + + elif sc.muv_uv_sculpt_tools == 'RELAX': + _, region, space = common.get_space( + 'VIEW_3D', 'WINDOW', 'VIEW_3D') + + # get vertex and loop relation + vert_db = {} + for f in bm.faces: + for l in f.loops: + if l.vert in vert_db: + vert_db[l.vert]["loops"].append(l) + else: + vert_db[l.vert] = {"loops": [l]} + + # get relaxation information + for k in vert_db.keys(): + d = vert_db[k] + d["uv_sum"] = Vector((0.0, 0.0)) + d["uv_count"] = 0 + + for l in d["loops"]: + ln = l.link_loop_next + lp = l.link_loop_prev + d["uv_sum"] = d["uv_sum"] + ln[uv_layer].uv + d["uv_sum"] = d["uv_sum"] + lp[uv_layer].uv + d["uv_count"] = d["uv_count"] + 2 + d["uv_p"] = d["uv_sum"] / d["uv_count"] + d["uv_b"] = d["uv_p"] - d["loops"][0][uv_layer].uv + for k in vert_db.keys(): + d = vert_db[k] + d["uv_sum_b"] = Vector((0.0, 0.0)) + for l in d["loops"]: + ln = l.link_loop_next + lp = l.link_loop_prev + dn = vert_db[ln.vert] + dp = vert_db[lp.vert] + d["uv_sum_b"] = d["uv_sum_b"] + dn["uv_b"] + dp["uv_b"] + + # apply + for f in bm.faces: + if not f.select: continue - - l[uv_layer].uv = target_uv - - bmesh.update_edit_mesh(obj.data) + for i, l in enumerate(f.loops): + loc_2d = location_3d_to_region_2d_extra( + region, space.region_3d, + compat.matmul(world_mat, l.vert.co)) + diff = loc_2d - self.__initial_mco + if diff.length >= sc.muv_uv_sculpt_radius: + continue + db = vert_db[l.vert] + strength = _get_strength(diff.length, + sc.muv_uv_sculpt_radius, + sc.muv_uv_sculpt_strength) + + base = (1.0 - strength) * l[uv_layer].uv + if sc.muv_uv_sculpt_relax_method == 'HC': + t = 0.5 * \ + (db["uv_b"] + db["uv_sum_b"] / d["uv_count"]) + diff = strength * (db["uv_p"] - t) + target_uv = base + diff + elif sc.muv_uv_sculpt_relax_method == 'LAPLACIAN': + diff = strength * db["uv_p"] + target_uv = base + diff + else: + continue + + l[uv_layer].uv = target_uv + + bmesh.update_edit_mesh(obj.data) def __stroke_exit(self, context, _): sc = context.scene - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - uv_layer = bm.loops.layers.uv.verify() - mco = self.current_mco - - if sc.muv_uv_sculpt_tools == 'GRAB': - for info in self.__loop_info: - diff_uv = (mco - self.__initial_mco) * info["strength"] - l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] - l[uv_layer].uv = info["initial_uv"] + diff_uv / 100.0 - - bmesh.update_edit_mesh(obj.data) + objs = common.get_uv_editable_objects(context) + + for obj in objs: + bm = bmesh.from_edit_mesh(obj.data) + uv_layer = bm.loops.layers.uv.verify() + mco = self.current_mco + + if sc.muv_uv_sculpt_tools == 'GRAB': + for info in self.__loop_info[obj]: + diff_uv = (mco - self.__initial_mco) * info["strength"] + l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] + l[uv_layer].uv = info["initial_uv"] + diff_uv / 100.0 + + bmesh.update_edit_mesh(obj.data) def modal(self, context, event): if context.area: diff --git a/magic_uv/op/uvw.py b/magic_uv/op/uvw.py index fca72d2c..42918dd5 100644 --- a/magic_uv/op/uvw.py +++ b/magic_uv/op/uvw.py @@ -20,8 +20,8 @@ __author__ = "Alexander Milovsky, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" from math import sin, cos, pi @@ -41,21 +41,16 @@ from ..utils import compatibility as compat def _is_valid_context(context): - obj = context.object + objs = common.get_uv_editable_objects(context) + if not objs: + return False # 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: + if not common.is_valid_space(context, ['VIEW_3D']): return False return True @@ -228,15 +223,11 @@ class MUV_OT_UVW_BoxMap(bpy.types.Operator): return True return _is_valid_context(context) - def execute(self, _): - if compat.check_version(2, 80, 0) < 0: - objs = [bpy.context.active_object] - else: - objs = [o for o in bpy.data.objects - if compat.get_object_select(o) and o.type == 'MESH'] + def execute(self, context): + objs = common.get_uv_editable_objects(context) - for o in objs: - bm = bmesh.from_edit_mesh(o.data) + for obj in objs: + bm = bmesh.from_edit_mesh(obj.data) if common.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() @@ -247,7 +238,7 @@ class MUV_OT_UVW_BoxMap(bpy.types.Operator): _apply_box_map(bm, uv_layer, self.size, self.offset, self.rotation, self.tex_aspect) - bmesh.update_edit_mesh(o.data) + bmesh.update_edit_mesh(obj.data) return {'FINISHED'} @@ -291,15 +282,11 @@ class MUV_OT_UVW_BestPlanerMap(bpy.types.Operator): return True return _is_valid_context(context) - def execute(self, _): - if compat.check_version(2, 80, 0) < 0: - objs = [bpy.context.active_object] - else: - objs = [o for o in bpy.data.objects - if compat.get_object_select(o) and o.type == 'MESH'] + def execute(self, context): + objs = common.get_uv_editable_objects(context) - for o in objs: - bm = bmesh.from_edit_mesh(o.data) + for obj in objs: + bm = bmesh.from_edit_mesh(obj.data) if common.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() @@ -311,6 +298,6 @@ class MUV_OT_UVW_BestPlanerMap(bpy.types.Operator): _apply_planer_map(bm, uv_layer, self.size, self.offset, self.rotation, self.tex_aspect) - bmesh.update_edit_mesh(o.data) + bmesh.update_edit_mesh(obj.data) return {'FINISHED'} diff --git a/magic_uv/op/world_scale_uv.py b/magic_uv/op/world_scale_uv.py index 9ed86eb0..a2806db5 100644 --- a/magic_uv/op/world_scale_uv.py +++ b/magic_uv/op/world_scale_uv.py @@ -20,8 +20,8 @@ __author__ = "McBuff, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" from math import sqrt @@ -41,22 +41,34 @@ from ..utils.property_class_registry import PropertyClassRegistry from ..utils import compatibility as compat -def _is_valid_context(context): - obj = context.object +def _is_valid_context_for_measure(context): + # Multiple objects editing mode is not supported in this feature. + objs = common.get_uv_editable_objects(context) + if len(objs) != 1: + return False # only edit mode is allowed to execute - if obj is None: + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + if not common.is_valid_space(context, ['VIEW_3D']): return False - if obj.type != 'MESH': + + return True + + +def _is_valid_context_for_apply(context): + objs = common.get_uv_editable_objects(context) + if not objs: return False + + # only edit mode is allowed to execute 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: + if not common.is_valid_space(context, ['VIEW_3D']): return False return True @@ -191,7 +203,11 @@ def _apply(faces, uv_layer, origin, factor): def _get_target_textures(_, __): - images = common.find_images(bpy.context.active_object) + objs = common.get_uv_editable_objects(bpy.context) + images = [] + for obj in objs: + images.extend(common.find_images(obj)) + items = [] items.append(("[Average]", "[Average]", "Average of all textures")) items.append(("[Max]", "[Max]", "Max of all textures")) @@ -356,7 +372,7 @@ class MUV_OT_WorldScaleUV_Measure(bpy.types.Operator): # we can not get area/space/region from console if common.is_console_mode(): return True - return _is_valid_context(context) + return _is_valid_context_for_measure(context) @staticmethod def setup_argument(ops, scene): @@ -365,7 +381,9 @@ class MUV_OT_WorldScaleUV_Measure(bpy.types.Operator): def execute(self, context): sc = context.scene - obj = context.active_object + objs = common.get_uv_editable_objects(context) + # poll() method ensures that only one object is selected. + obj = objs[0] if self.tgt_texture == "[Average]": uv_areas, mesh_areas, densities = _measure_wsuv_info( @@ -469,7 +487,7 @@ class MUV_OT_WorldScaleUV_ApplyManual(bpy.types.Operator): # we can not get area/space/region from console if common.is_console_mode(): return True - return _is_valid_context(context) + return _is_valid_context_for_apply(context) @staticmethod def setup_argument(ops, scene): @@ -482,42 +500,49 @@ class MUV_OT_WorldScaleUV_ApplyManual(bpy.types.Operator): ops.only_selected = scene.muv_world_scale_uv_apply_only_selected 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() - - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - tex_layer = common.find_texture_layer(bm) - faces_list = common.get_faces_list( - bm, self.tgt_area_calc_method, self.only_selected) - - tex_size = self.tgt_texture_size + objs = common.get_uv_editable_objects(context) - factors = [] - for faces in faces_list: - uv_area, _, density = _measure_wsuv_info_from_faces( - obj, faces, uv_layer, tex_layer, - tex_selection_method='USER_SPECIFIED', tex_size=tex_size) + for obj in objs: + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() - if not uv_area: + if not bm.loops.layers.uv: self.report({'WARNING'}, - "Object must have more than one UV map") + "Object {} must have more than one UV map" + .format(obj.name)) return {'CANCELLED'} + uv_layer = bm.loops.layers.uv.verify() + tex_layer = common.find_texture_layer(bm) + faces_list = common.get_faces_list( + bm, self.tgt_area_calc_method, self.only_selected) - tgt_density = self.tgt_density - factor = tgt_density / density + tex_size = self.tgt_texture_size + + factors = [] + for faces in faces_list: + uv_area, _, density = _measure_wsuv_info_from_faces( + obj, faces, uv_layer, tex_layer, + tex_selection_method='USER_SPECIFIED', tex_size=tex_size) - _apply(faces, uv_layer, self.origin, factor) - factors.append(factor) + if not uv_area: + self.report({'WARNING'}, + "Object {} must have more than one UV map" + .format(obj.name)) + return {'CANCELLED'} - bmesh.update_edit_mesh(obj.data) - self.report({'INFO'}, "Scaling factor: {0}".format(factors)) + tgt_density = self.tgt_density + factor = tgt_density / density + + _apply(faces, uv_layer, self.origin, factor) + factors.append(factor) + + bmesh.update_edit_mesh(obj.data) + self.report({'INFO'}, + "Scaling factor of object {}: {}" + .format(obj.name, factors)) return {'FINISHED'} @@ -624,7 +649,7 @@ class MUV_OT_WorldScaleUV_ApplyScalingDensity(bpy.types.Operator): # we can not get area/space/region from console if common.is_console_mode(): return True - return _is_valid_context(context) + return _is_valid_context_for_apply(context) @staticmethod def setup_argument(ops, scene): @@ -640,56 +665,62 @@ class MUV_OT_WorldScaleUV_ApplyScalingDensity(bpy.types.Operator): ops.only_selected = scene.muv_world_scale_uv_apply_only_selected def __apply_scaling_density(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.verts.ensure_lookup_table() - bm.edges.ensure_lookup_table() - bm.faces.ensure_lookup_table() - - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - tex_layer = common.find_texture_layer(bm) - faces_list = common.get_faces_list( - bm, self.tgt_area_calc_method, self.only_selected) - - factors = [] - for faces in faces_list: - if self.tgt_texture == "[Average]": - uv_area, _, density = _measure_wsuv_info_from_faces( - obj, faces, uv_layer, tex_layer, - tex_selection_method='AVERAGE') - elif self.tgt_texture == "[Max]": - uv_area, _, density = _measure_wsuv_info_from_faces( - obj, faces, uv_layer, tex_layer, - tex_selection_method='MAX') - elif self.tgt_texture == "[Min]": - uv_area, _, density = _measure_wsuv_info_from_faces( - obj, faces, uv_layer, tex_layer, - tex_selection_method='MIN') - else: - tgt_texture = bpy.data.images[self.tgt_texture] - uv_area, _, density = _measure_wsuv_info_from_faces( - obj, faces, uv_layer, tex_layer, - tex_selection_method='USER_SPECIFIED', - tex_size=tgt_texture.size) + objs = common.get_uv_editable_objects(context) + + for obj in objs: + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() - if not uv_area: + if not bm.loops.layers.uv: self.report({'WARNING'}, - "Object must have more than one UV map and " - "texture") + "Object {} must have more than one UV map" + .format(obj.name)) return {'CANCELLED'} - - tgt_density = self.src_density * self.tgt_scaling_factor - factor = tgt_density / density - - _apply(faces, uv_layer, self.origin, factor) - factors.append(factor) - - bmesh.update_edit_mesh(obj.data) - self.report({'INFO'}, "Scaling factor: {0}".format(factors)) + uv_layer = bm.loops.layers.uv.verify() + tex_layer = common.find_texture_layer(bm) + faces_list = common.get_faces_list( + bm, self.tgt_area_calc_method, self.only_selected) + + factors = [] + for faces in faces_list: + if self.tgt_texture == "[Average]": + uv_area, _, density = _measure_wsuv_info_from_faces( + obj, faces, uv_layer, tex_layer, + tex_selection_method='AVERAGE') + elif self.tgt_texture == "[Max]": + uv_area, _, density = _measure_wsuv_info_from_faces( + obj, faces, uv_layer, tex_layer, + tex_selection_method='MAX') + elif self.tgt_texture == "[Min]": + uv_area, _, density = _measure_wsuv_info_from_faces( + obj, faces, uv_layer, tex_layer, + tex_selection_method='MIN') + else: + tgt_texture = bpy.data.images[self.tgt_texture] + uv_area, _, density = _measure_wsuv_info_from_faces( + obj, faces, uv_layer, tex_layer, + tex_selection_method='USER_SPECIFIED', + tex_size=tgt_texture.size) + + if not uv_area: + self.report({'WARNING'}, + "Object {} must have more than one UV map and " + "texture".format(obj.name)) + return {'CANCELLED'} + + tgt_density = self.src_density * self.tgt_scaling_factor + factor = tgt_density / density + + _apply(faces, uv_layer, self.origin, factor) + factors.append(factor) + + bmesh.update_edit_mesh(obj.data) + self.report({'INFO'}, + "Scaling factor of object {}: {}" + .format(obj.name, factors)) return {'FINISHED'} @@ -819,7 +850,7 @@ class MUV_OT_WorldScaleUV_ApplyProportionalToMesh(bpy.types.Operator): # we can not get area/space/region from console if common.is_console_mode(): return True - return _is_valid_context(context) + return _is_valid_context_for_apply(context) @staticmethod def setup_argument(ops, scene): @@ -834,56 +865,66 @@ class MUV_OT_WorldScaleUV_ApplyProportionalToMesh(bpy.types.Operator): ops.only_selected = scene.muv_world_scale_uv_apply_only_selected 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() - - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - tex_layer = common.find_texture_layer(bm) - faces_list = common.get_faces_list( - bm, self.tgt_area_calc_method, self.only_selected) - - factors = [] - for faces in faces_list: - if self.tgt_texture == "[Average]": - uv_area, mesh_area, density = _measure_wsuv_info_from_faces( - obj, faces, uv_layer, tex_layer, - tex_selection_method='AVERAGE') - elif self.tgt_texture == "[Max]": - uv_area, mesh_area, density = _measure_wsuv_info_from_faces( - obj, faces, uv_layer, tex_layer, - tex_selection_method='MAX') - elif self.tgt_texture == "[Min]": - uv_area, mesh_area, density = _measure_wsuv_info_from_faces( - obj, faces, uv_layer, tex_layer, - tex_selection_method='MIN') - else: - tgt_texture = bpy.data.images[self.tgt_texture] - uv_area, mesh_area, density = _measure_wsuv_info_from_faces( - obj, faces, uv_layer, tex_layer, - tex_selection_method='USER_SPECIFIED', - tex_size=tgt_texture.size) - if not uv_area: - self.report({'WARNING'}, - "Object must have more than one UV map and " - "texture") - return {'CANCELLED'} - - tgt_density = self.src_density * sqrt(mesh_area) / sqrt( - self.src_mesh_area) - factor = tgt_density / density + objs = common.get_uv_editable_objects(context) - _apply(faces, uv_layer, self.origin, factor) - factors.append(factor) + for obj in objs: + 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() - bmesh.update_edit_mesh(obj.data) - self.report({'INFO'}, "Scaling factor: {0}".format(factors)) + if not bm.loops.layers.uv: + self.report({'WARNING'}, + "Object {} must have more than one UV map" + .format(obj.name)) + return {'CANCELLED'} + uv_layer = bm.loops.layers.uv.verify() + tex_layer = common.find_texture_layer(bm) + faces_list = common.get_faces_list( + bm, self.tgt_area_calc_method, self.only_selected) + + factors = [] + for faces in faces_list: + if self.tgt_texture == "[Average]": + uv_area, mesh_area, density = \ + _measure_wsuv_info_from_faces( + obj, faces, uv_layer, tex_layer, + tex_selection_method='AVERAGE') + elif self.tgt_texture == "[Max]": + uv_area, mesh_area, density = \ + _measure_wsuv_info_from_faces( + obj, faces, uv_layer, tex_layer, + tex_selection_method='MAX') + elif self.tgt_texture == "[Min]": + uv_area, mesh_area, density = \ + _measure_wsuv_info_from_faces( + obj, faces, uv_layer, tex_layer, + tex_selection_method='MIN') + else: + tgt_texture = bpy.data.images[self.tgt_texture] + uv_area, mesh_area, density = \ + _measure_wsuv_info_from_faces( + obj, faces, uv_layer, tex_layer, + tex_selection_method='USER_SPECIFIED', + tex_size=tgt_texture.size) + if not uv_area: + self.report({'WARNING'}, + "Object {} must have more than one UV map and " + "texture".format(obj.name)) + return {'CANCELLED'} + + tgt_density = self.src_density * sqrt(mesh_area) / sqrt( + self.src_mesh_area) + factor = tgt_density / density + + _apply(faces, uv_layer, self.origin, factor) + factors.append(factor) + + bmesh.update_edit_mesh(obj.data) + self.report({'INFO'}, + "Scaling factor of object {}: {}" + .format(obj.name, factors)) return {'FINISHED'} diff --git a/magic_uv/preferences.py b/magic_uv/preferences.py index 926ec728..ea8e7434 100644 --- a/magic_uv/preferences.py +++ b/magic_uv/preferences.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy from bpy.props import ( diff --git a/magic_uv/properites.py b/magic_uv/properites.py index b269cbed..28b9216d 100644 --- a/magic_uv/properites.py +++ b/magic_uv/properites.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" from .utils.property_class_registry import PropertyClassRegistry diff --git a/magic_uv/ui/IMAGE_MT_uvs.py b/magic_uv/ui/IMAGE_MT_uvs.py index 00d95d9e..3984c20f 100644 --- a/magic_uv/ui/IMAGE_MT_uvs.py +++ b/magic_uv/ui/IMAGE_MT_uvs.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy @@ -34,6 +34,8 @@ from ..op.align_uv import ( MUV_OT_AlignUV_Circle, MUV_OT_AlignUV_Straighten, MUV_OT_AlignUV_Axis, + MUV_OT_AlignUV_SnapToPoint, + MUV_OT_AlignUV_SnapToEdge, ) from ..op.select_uv import ( MUV_OT_SelectUV_SelectOverlapped, @@ -78,6 +80,8 @@ class MUV_MT_AlignUV(bpy.types.Menu): layout = self.layout sc = context.scene + layout.label(text="Align") + ops = layout.operator(MUV_OT_AlignUV_Circle.bl_idname, text="Circle") ops.transmission = sc.muv_align_uv_transmission ops.select = sc.muv_align_uv_select @@ -96,6 +100,18 @@ class MUV_MT_AlignUV(bpy.types.Menu): ops.horizontal = sc.muv_align_uv_horizontal ops.location = sc.muv_align_uv_location + layout.label(text="Snap") + + ops = layout.operator(MUV_OT_AlignUV_SnapToPoint.bl_idname, + text="Snap to Point") + ops.group = sc.muv_align_uv_snap_point_group + ops.target = sc.muv_align_uv_snap_point_target + + ops = layout.operator(MUV_OT_AlignUV_SnapToEdge, text="Snap to Edge") + ops.group = sc.muv_align_uv_snap_edge_group + ops.target_1 = sc.muv_align_uv_snap_edge_target_1 + ops.target_2 = sc.muv_align_uv_snap_edge_target_2 + @BlClassRegistry() class MUV_MT_SelectUV(bpy.types.Menu): diff --git a/magic_uv/ui/VIEW3D_MT_object.py b/magic_uv/ui/VIEW3D_MT_object.py index f34c74f9..c8980592 100644 --- a/magic_uv/ui/VIEW3D_MT_object.py +++ b/magic_uv/ui/VIEW3D_MT_object.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy diff --git a/magic_uv/ui/VIEW3D_MT_uv_map.py b/magic_uv/ui/VIEW3D_MT_uv_map.py index 7ab50ace..e6574f4d 100644 --- a/magic_uv/ui/VIEW3D_MT_uv_map.py +++ b/magic_uv/ui/VIEW3D_MT_uv_map.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy.utils diff --git a/magic_uv/ui/__init__.py b/magic_uv/ui/__init__.py index bb16a847..083590a6 100644 --- a/magic_uv/ui/__init__.py +++ b/magic_uv/ui/__init__.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" if "bpy" in locals(): import importlib diff --git a/magic_uv/ui/uvedit_copy_paste_uv.py b/magic_uv/ui/uvedit_copy_paste_uv.py index 211737c8..5f029f6f 100644 --- a/magic_uv/ui/uvedit_copy_paste_uv.py +++ b/magic_uv/ui/uvedit_copy_paste_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy diff --git a/magic_uv/ui/uvedit_editor_enhancement.py b/magic_uv/ui/uvedit_editor_enhancement.py index f98e5193..a0eba3a9 100644 --- a/magic_uv/ui/uvedit_editor_enhancement.py +++ b/magic_uv/ui/uvedit_editor_enhancement.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy diff --git a/magic_uv/ui/uvedit_uv_manipulation.py b/magic_uv/ui/uvedit_uv_manipulation.py index 79a1731a..1b05cb00 100644 --- a/magic_uv/ui/uvedit_uv_manipulation.py +++ b/magic_uv/ui/uvedit_uv_manipulation.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy @@ -29,6 +29,11 @@ from ..op.align_uv import ( MUV_OT_AlignUV_Circle, MUV_OT_AlignUV_Straighten, MUV_OT_AlignUV_Axis, + MUV_OT_AlignUV_SnapToPoint, + MUV_OT_AlignUV_Snap_SetPointTargetToCursor, + MUV_OT_AlignUV_Snap_SetPointTargetToVertexGroup, + MUV_OT_AlignUV_SnapToEdge, + MUV_OT_AlignUV_Snap_SetEdgeTargetToEdgeCenter, ) from ..op.smooth_uv import ( MUV_OT_SmoothUV, @@ -67,6 +72,8 @@ class MUV_PT_UVEdit_UVManipulation(bpy.types.Panel): box = layout.box() box.prop(sc, "muv_align_uv_enabled", text="Align UV") if sc.muv_align_uv_enabled: + box.label(text="Align:") + col = box.column() row = col.row(align=True) ops = row.operator(MUV_OT_AlignUV_Circle.bl_idname, text="Circle") @@ -98,6 +105,60 @@ class MUV_PT_UVEdit_UVManipulation(bpy.types.Panel): row.prop(sc, "muv_align_uv_horizontal", text="Horizontal") col.prop(sc, "muv_align_uv_mesh_infl", text="Mesh Influence") + box.separator() + + sp = compat.layout_split(box, factor=0.5) + sp.label(text="Snap:") + sp = compat.layout_split(sp, factor=1.0) + sp.prop(sc, "muv_align_uv_snap_method", text="") + + if sc.muv_align_uv_snap_method == 'POINT': + row = box.row(align=True) + ops = row.operator(MUV_OT_AlignUV_SnapToPoint.bl_idname, + text="Snap to Point") + ops.group = sc.muv_align_uv_snap_point_group + ops.target = sc.muv_align_uv_snap_point_target + + col = box.column(align=True) + row = col.row(align=True) + row.prop(sc, "muv_align_uv_snap_point_group", text="Group") + + col.label(text="Target Point:") + row = col.row(align=True) + row.prop(sc, "muv_align_uv_snap_point_target", text="") + row.operator( + MUV_OT_AlignUV_Snap_SetPointTargetToCursor.bl_idname, + text="", icon=compat.icon('CURSOR')) + row.operator( + MUV_OT_AlignUV_Snap_SetPointTargetToVertexGroup.bl_idname, + text="", icon=compat.icon('UV_VERTEXSEL')) + + elif sc.muv_align_uv_snap_method == 'EDGE': + row = box.row(align=True) + ops = row.operator(MUV_OT_AlignUV_SnapToEdge.bl_idname, + text="Snap to Edge") + ops.group = sc.muv_align_uv_snap_edge_group + ops.target_1 = sc.muv_align_uv_snap_edge_target_1 + ops.target_2 = sc.muv_align_uv_snap_edge_target_2 + + col = box.column(align=True) + row = col.row(align=True) + row.prop(sc, "muv_align_uv_snap_edge_group", text="Group") + + col.label(text="Target Edge:") + sp = compat.layout_split(col, factor=0.33) + subcol = sp.column() + subcol.label(text="Vertex 1:") + subcol.prop(sc, "muv_align_uv_snap_edge_target_1", text="") + sp = compat.layout_split(sp, factor=0.5) + subcol = sp.column() + subcol.label(text="Vertex 2:") + subcol.prop(sc, "muv_align_uv_snap_edge_target_2", text="") + sp = compat.layout_split(sp, factor=1.0) + sp.operator( + MUV_OT_AlignUV_Snap_SetEdgeTargetToEdgeCenter.bl_idname, + text="", icon=compat.icon('UV_EDGESEL')) + box = layout.box() box.prop(sc, "muv_smooth_uv_enabled", text="Smooth UV") if sc.muv_smooth_uv_enabled: diff --git a/magic_uv/ui/view3d_copy_paste_uv_editmode.py b/magic_uv/ui/view3d_copy_paste_uv_editmode.py index 0c7273a3..762fd9d9 100644 --- a/magic_uv/ui/view3d_copy_paste_uv_editmode.py +++ b/magic_uv/ui/view3d_copy_paste_uv_editmode.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy diff --git a/magic_uv/ui/view3d_copy_paste_uv_objectmode.py b/magic_uv/ui/view3d_copy_paste_uv_objectmode.py index b2a33e9a..71d30755 100644 --- a/magic_uv/ui/view3d_copy_paste_uv_objectmode.py +++ b/magic_uv/ui/view3d_copy_paste_uv_objectmode.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy diff --git a/magic_uv/ui/view3d_uv_manipulation.py b/magic_uv/ui/view3d_uv_manipulation.py index 1d10eb65..6d0fce6d 100644 --- a/magic_uv/ui/view3d_uv_manipulation.py +++ b/magic_uv/ui/view3d_uv_manipulation.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy diff --git a/magic_uv/ui/view3d_uv_mapping.py b/magic_uv/ui/view3d_uv_mapping.py index 4344adb7..6a4217c0 100644 --- a/magic_uv/ui/view3d_uv_mapping.py +++ b/magic_uv/ui/view3d_uv_mapping.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy diff --git a/magic_uv/updater.py b/magic_uv/updater.py index 8d610b16..a9e09bfa 100644 --- a/magic_uv/updater.py +++ b/magic_uv/updater.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import os diff --git a/magic_uv/utils/__init__.py b/magic_uv/utils/__init__.py index c96b9225..918bc207 100644 --- a/magic_uv/utils/__init__.py +++ b/magic_uv/utils/__init__.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" if "bpy" in locals(): import importlib diff --git a/magic_uv/utils/addon_updater.py b/magic_uv/utils/addon_updater.py index 5df59fd4..813813eb 100644 --- a/magic_uv/utils/addon_updater.py +++ b/magic_uv/utils/addon_updater.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" from threading import Lock import urllib diff --git a/magic_uv/utils/bl_class_registry.py b/magic_uv/utils/bl_class_registry.py index f9f05faf..080a1a45 100644 --- a/magic_uv/utils/bl_class_registry.py +++ b/magic_uv/utils/bl_class_registry.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy diff --git a/magic_uv/utils/compatibility.py b/magic_uv/utils/compatibility.py index b4c7c4ea..517c33af 100644 --- a/magic_uv/utils/compatibility.py +++ b/magic_uv/utils/compatibility.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" import bpy import bgl @@ -100,6 +100,13 @@ def get_object_select(obj): return obj.select_get() +def set_object_select(obj, select): + if check_version(2, 80, 0) < 0: + obj.select = select + else: + obj.select_set(select) + + def set_active_object(obj): if check_version(2, 80, 0) < 0: bpy.context.scene.objects.active = obj @@ -108,10 +115,10 @@ def set_active_object(obj): def get_active_object(context): - if check_version(2, 80, 0) >= 0: - return context.scene.active_object + if check_version(2, 80, 0) < 0: + return context.scene.objects.active else: - return context.active_object + return context.view_layer.objects.active def object_has_uv_layers(obj): diff --git a/magic_uv/utils/property_class_registry.py b/magic_uv/utils/property_class_registry.py index 9caa735c..f107aed3 100644 --- a/magic_uv/utils/property_class_registry.py +++ b/magic_uv/utils/property_class_registry.py @@ -20,8 +20,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.3" -__date__ = "10 Aug 2020" +__version__ = "6.4" +__date__ = "23 Oct 2020" from .. import common |