From 2cbb9e2b960ac343a6f5796e20d0efb443bd4a67 Mon Sep 17 00:00:00 2001 From: nutti <nutti.metro@gmail.com> Date: Fri, 22 Apr 2022 16:21:10 +0900 Subject: [PATCH] Magic UV: Release v6.6 Added Features * Copy/Paste UV Island Updated Features * Pack UV * Add options "Accurate Island Copy", "Stride", "Apply Pack UV" Other Updates * Add 'develop' branch to the update target of updater * Make documents official * Fix bugs --- magic_uv/__init__.py | 11 +- magic_uv/common.py | 137 +++++++++- magic_uv/lib/__init__.py | 4 +- magic_uv/lib/bglx.py | 2 + magic_uv/op/__init__.py | 4 +- magic_uv/op/align_uv.py | 16 +- magic_uv/op/align_uv_cursor.py | 41 +-- magic_uv/op/clip_uv.py | 16 +- magic_uv/op/copy_paste_uv.py | 12 +- magic_uv/op/copy_paste_uv_object.py | 12 +- magic_uv/op/copy_paste_uv_uvedit.py | 237 +++++++++++++++++- magic_uv/op/flip_rotate_uv.py | 12 +- magic_uv/op/mirror_uv.py | 25 +- magic_uv/op/move_uv.py | 12 +- magic_uv/op/pack_uv.py | 130 ++++++++-- magic_uv/op/preserve_uv_aspect.py | 12 +- magic_uv/op/select_uv.py | 24 +- magic_uv/op/smooth_uv.py | 16 +- magic_uv/op/texture_lock.py | 12 +- magic_uv/op/texture_projection.py | 12 +- magic_uv/op/texture_wrap.py | 12 +- magic_uv/op/transfer_uv.py | 12 +- magic_uv/op/unwrap_constraint.py | 14 +- magic_uv/op/uv_bounding_box.py | 16 +- magic_uv/op/uv_inspection.py | 20 +- magic_uv/op/uv_sculpt.py | 12 +- magic_uv/op/uvw.py | 12 +- magic_uv/op/world_scale_uv.py | 20 +- magic_uv/preferences.py | 7 +- magic_uv/properties.py | 4 +- magic_uv/ui/IMAGE_MT_uvs.py | 17 +- magic_uv/ui/VIEW3D_MT_object.py | 4 +- magic_uv/ui/VIEW3D_MT_uv_map.py | 4 +- magic_uv/ui/__init__.py | 4 +- magic_uv/ui/uvedit_copy_paste_uv.py | 21 +- magic_uv/ui/uvedit_editor_enhancement.py | 4 +- magic_uv/ui/uvedit_uv_manipulation.py | 12 +- magic_uv/ui/view3d_copy_paste_uv_editmode.py | 4 +- .../ui/view3d_copy_paste_uv_objectmode.py | 4 +- magic_uv/ui/view3d_uv_manipulation.py | 4 +- magic_uv/ui/view3d_uv_mapping.py | 4 +- magic_uv/utils/__init__.py | 6 +- magic_uv/utils/bl_class_registry.py | 4 +- magic_uv/utils/compatibility.py | 4 +- magic_uv/utils/graph.py | 152 +++++++++++ magic_uv/utils/property_class_registry.py | 4 +- 46 files changed, 890 insertions(+), 238 deletions(-) create mode 100644 magic_uv/utils/graph.py diff --git a/magic_uv/__init__.py b/magic_uv/__init__.py index dc3c96416..883851075 100644 --- a/magic_uv/__init__.py +++ b/magic_uv/__init__.py @@ -4,16 +4,17 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" bl_info = { "name": "Magic UV", - "author": "Nutti, Mifth, Jace Priester, kgeogeo, mem, imdjs" + "author": "Nutti, Mifth, Jace Priester, kgeogeo, mem, imdjs, " "Keith (Wahooney) Boshoff, McBuff, MaxRobinot, " - "Alexander Milovsky, Dusan Stevanovic, MatthiasThDs", - "version": (6, 5, 0), + "Alexander Milovsky, Dusan Stevanovic, MatthiasThDs, " + "theCryingMan, PratikBorhade302", + "version": (6, 6, 0), "blender": (2, 80, 0), "location": "See Add-ons Preferences", "description": "UV Toolset. See Add-ons Preferences for details", diff --git a/magic_uv/common.py b/magic_uv/common.py index 4e6334083..034936c55 100644 --- a/magic_uv/common.py +++ b/magic_uv/common.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" from collections import defaultdict from pprint import pprint @@ -17,6 +17,7 @@ from mathutils import Vector import bmesh from .utils import compatibility as compat +from .utils.graph import Graph, Node __DEBUG_MODE = False @@ -286,6 +287,30 @@ def get_island_info(obj, only_selected=True): return get_island_info_from_bmesh(bm, only_selected) +# Return island info. +# +# Format: +# +# [ +# { +# faces: [ +# { +# face: BMFace +# max_uv: Vector (2D) +# min_uv: Vector (2D) +# ave_uv: Vector (2D) +# }, +# ... +# ] +# center: Vector (2D) +# size: Vector (2D) +# num_uv: int +# group: int +# max: Vector (2D) +# min: Vector (2D) +# }, +# ... +# ] def get_island_info_from_bmesh(bm, only_selected=True): if not bm.loops.layers.uv: return None @@ -1184,12 +1209,22 @@ def __is_polygon_flipped(points): def __is_point_in_polygon(point, subject_points): + """Return true when point is inside of the polygon by using + 'Crossing number algorithm'. + """ + count = 0 for i in range(len(subject_points)): uv_start1 = subject_points.get(i) uv_end1 = subject_points.get(i + 1) uv_start2 = point uv_end2 = Vector((1000000.0, point.y)) + + # If the point exactly matches to the point of the polygon, + # this point is not in polygon. + if uv_start1.x == uv_start2.x and uv_start1.y == uv_start2.y: + return False + intersected, _ = __is_segment_intersect(uv_start1, uv_end1, uv_start2, uv_end2) if intersected: @@ -1239,7 +1274,7 @@ def get_overlapped_uv_info(bm_list, faces_list, uv_layer_list, overlapped_uv_layer_pairs.append([uv_layer_1, uv_layer_2]) overlapped_bm_paris.append([bm_1, bm_2]) - # next, check polygon overlapped + # check polygon overlapped (inter UV islands) overlapped_uvs = [] for oip, uvlp, bmp in zip(overlapped_isl_pairs, overlapped_uv_layer_pairs, @@ -1272,6 +1307,41 @@ def get_overlapped_uv_info(bm_list, faces_list, uv_layer_list, "subject_uvs": subject_uvs, "polygons": polygons}) + # check polygon overlapped (intra UV island) + for info, uv_layer, bm in isl: + for i in range(len(info["faces"])): + clip = info["faces"][i] + f_clip = clip["face"] + clip_uvs = [l[uv_layer].uv.copy() for l in f_clip.loops] + for j in range(len(info["faces"])): + if j <= i: + continue + + subject = info["faces"][j] + f_subject = subject["face"] + + # fast operation, apply bounding box algorithm + if (clip["max_uv"].x < subject["min_uv"].x) or \ + (subject["max_uv"].x < clip["min_uv"].x) or \ + (clip["max_uv"].y < subject["min_uv"].y) or \ + (subject["max_uv"].y < clip["min_uv"].y): + continue + + subject_uvs = [l[uv_layer].uv.copy() for l in f_subject.loops] + # slow operation, apply Weiler-Atherton cliping algorithm + result, polygons = \ + __do_weiler_atherton_cliping(clip_uvs, subject_uvs, + mode, same_polygon_threshold) + if result: + overlapped_uvs.append({"clip_bmesh": bm, + "subject_bmesh": bm, + "clip_face": f_clip, + "subject_face": f_subject, + "clip_uv_layer": uv_layer, + "subject_uv_layer": uv_layer, + "subject_uvs": subject_uvs, + "polygons": polygons}) + return overlapped_uvs @@ -1308,3 +1378,64 @@ def __is_polygon_same(points1, points2, threshold): return False return True + + +def _is_uv_loop_connected(l1, l2, uv_layer): + uv1 = l1[uv_layer].uv + uv2 = l2[uv_layer].uv + return uv1.x == uv2.x and uv1.y == uv2.y + + +def create_uv_graph(loops, uv_layer): + # For looking up faster. + loop_index_to_loop = {} # { loop index: loop } + for l in loops: + loop_index_to_loop[l.index] = l + + # Setup relationship between uv_vert and loops. + # uv_vert is a representative of the loops which shares same + # UV coordinate. + uv_vert_to_loops = {} # { uv_vert: loops belonged to uv_vert } + loop_to_uv_vert = {} # { loop: uv_vert belonged to } + for l in loops: + found = False + for k in uv_vert_to_loops.keys(): + if _is_uv_loop_connected(k, l, uv_layer): + uv_vert_to_loops[k].append(l) + loop_to_uv_vert[l] = k + found = True + break + if not found: + uv_vert_to_loops[l] = [l] + loop_to_uv_vert[l] = l + + # Collect adjacent uv_vert. + uv_adj_verts = {} # { uv_vert: adj uv_vert list } + for v, vs in uv_vert_to_loops.items(): + uv_adj_verts[v] = [] + for ll in vs: + ln = ll.link_loop_next + lp = ll.link_loop_prev + uv_adj_verts[v].append(loop_to_uv_vert[ln]) + uv_adj_verts[v].append(loop_to_uv_vert[lp]) + uv_adj_verts[v] = list(set(uv_adj_verts[v])) + + # Setup uv_vert graph. + graph = Graph() + for v in uv_adj_verts.keys(): + graph.add_node( + Node(v.index, {"uv_vert": v, "loops": uv_vert_to_loops[v]}) + ) + edges = [] + for v, adjs in uv_adj_verts.items(): + n1 = graph.get_node(v.index) + for a in adjs: + n2 = graph.get_node(a.index) + edges.append(tuple(sorted((n1.key, n2.key)))) + edges = list(set(edges)) + for e in edges: + n1 = graph.get_node(e[0]) + n2 = graph.get_node(e[1]) + graph.add_edge(n1, n2) + + return graph diff --git a/magic_uv/lib/__init__.py b/magic_uv/lib/__init__.py index 76eaf4800..bccf4e170 100644 --- a/magic_uv/lib/__init__.py +++ b/magic_uv/lib/__init__.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" if "bpy" in locals(): import importlib diff --git a/magic_uv/lib/bglx.py b/magic_uv/lib/bglx.py index c1f696ab4..044141b69 100644 --- a/magic_uv/lib/bglx.py +++ b/magic_uv/lib/bglx.py @@ -1,5 +1,7 @@ # SPDX-License-Identifier: GPL-2.0-or-later +# <pep8-80 compliant> + from threading import Lock import bgl diff --git a/magic_uv/op/__init__.py b/magic_uv/op/__init__.py index da77b17b3..223ed004a 100644 --- a/magic_uv/op/__init__.py +++ b/magic_uv/op/__init__.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" if "bpy" in locals(): import importlib diff --git a/magic_uv/op/align_uv.py b/magic_uv/op/align_uv.py index 9f606db9b..cf5a49deb 100644 --- a/magic_uv/op/align_uv.py +++ b/magic_uv/op/align_uv.py @@ -4,8 +4,8 @@ __author__ = "imdjs, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import math from math import atan2, tan, sin, cos @@ -28,6 +28,12 @@ from .. import common 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 + if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): + return False + objs = common.get_uv_editable_objects(context) if not objs: return False @@ -36,12 +42,6 @@ def _is_valid_context(context): 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 - if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): - return False - return True diff --git a/magic_uv/op/align_uv_cursor.py b/magic_uv/op/align_uv_cursor.py index 2b7f1491b..696b7cb85 100644 --- a/magic_uv/op/align_uv_cursor.py +++ b/magic_uv/op/align_uv_cursor.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy from mathutils import Vector @@ -175,7 +175,8 @@ class MUV_OT_AlignUVCursor(bpy.types.Operator): uv_layer = bm.loops.layers.uv.verify() for f in bm.faces: - if not f.select: + if (not context.tool_settings.use_uv_select_sync and + not f.select): continue for l in f.loops: uv = l[uv_layer].uv @@ -204,18 +205,30 @@ class MUV_OT_AlignUVCursor(bpy.types.Operator): 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: + if context.tool_settings.use_uv_select_sync: + for v in bm.verts: + if not v.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 + for l in v.link_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 + else: + 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)) diff --git a/magic_uv/op/clip_uv.py b/magic_uv/op/clip_uv.py index c74755438..e3815453c 100644 --- a/magic_uv/op/clip_uv.py +++ b/magic_uv/op/clip_uv.py @@ -4,8 +4,8 @@ __author__ = "Dusan Stevanovic, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import math @@ -22,6 +22,12 @@ from ..utils import compatibility as compat def _is_valid_context(context): + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): + return False + objs = common.get_uv_editable_objects(context) if not objs: return False @@ -30,12 +36,6 @@ def _is_valid_context(context): 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 - if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): - return False - return True diff --git a/magic_uv/op/copy_paste_uv.py b/magic_uv/op/copy_paste_uv.py index 8ee83ad94..1496d67ac 100644 --- a/magic_uv/op/copy_paste_uv.py +++ b/magic_uv/op/copy_paste_uv.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>, Jace Priester" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bmesh import bpy.utils @@ -23,6 +23,10 @@ from ..utils import compatibility as compat def _is_valid_context(context): + # only 'VIEW_3D' space is allowed to execute + if not common.is_valid_space(context, ['VIEW_3D']): + return False + # Multiple objects editing mode is not supported in this feature. objs = common.get_uv_editable_objects(context) if len(objs) != 1: @@ -32,10 +36,6 @@ def _is_valid_context(context): 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 - return True diff --git a/magic_uv/op/copy_paste_uv_object.py b/magic_uv/op/copy_paste_uv_object.py index 4e5d500af..af5df07c3 100644 --- a/magic_uv/op/copy_paste_uv_object.py +++ b/magic_uv/op/copy_paste_uv_object.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bmesh import bpy @@ -28,6 +28,10 @@ from ..utils import compatibility as compat def _is_valid_context(context): + # only 'VIEW_3D' space is allowed to execute + if not common.is_valid_space(context, ['VIEW_3D']): + return False + # Multiple objects editing mode is not supported in this feature. objs = common.get_uv_editable_objects(context) if len(objs) != 1: @@ -37,10 +41,6 @@ def _is_valid_context(context): if context.object.mode != 'OBJECT': return False - # only 'VIEW_3D' space is allowed to execute - if not common.is_valid_space(context, ['VIEW_3D']): - return False - return True diff --git a/magic_uv/op/copy_paste_uv_uvedit.py b/magic_uv/op/copy_paste_uv_uvedit.py index 7055915f9..733c30b32 100644 --- a/magic_uv/op/copy_paste_uv_uvedit.py +++ b/magic_uv/op/copy_paste_uv_uvedit.py @@ -4,8 +4,8 @@ __author__ = "imdjs, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import math from math import atan2, sin, cos @@ -13,13 +13,22 @@ from math import atan2, sin, cos import bpy import bmesh from mathutils import Vector +from bpy.props import BoolProperty from .. import common from ..utils.bl_class_registry import BlClassRegistry from ..utils.property_class_registry import PropertyClassRegistry +from ..utils.graph import graph_is_isomorphic +from ..utils import compatibility as compat def _is_valid_context(context): + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): + return False + # Multiple objects editing mode is not supported in this feature. objs = common.get_uv_editable_objects(context) if len(objs) != 1: @@ -29,12 +38,6 @@ def _is_valid_context(context): 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 - if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): - return False - return True @@ -44,14 +47,33 @@ class _Properties: @classmethod def init_props(cls, scene): - class Props(): + class CopyPastUVProps(): src_uvs = None - scene.muv_props.copy_paste_uv_uvedit = Props() + class CopyPasteUVIslandProps(): + # [ + # { + # "bmesh": BMesh, + # "uv_layer": UV Layer, + # "island": UV Island, + # } + # ] + src_data = [] + src_objects = [] + + scene.muv_props.copy_paste_uv_uvedit = CopyPastUVProps() + scene.muv_props.copy_paste_uv_island = CopyPasteUVIslandProps() + + scene.muv_copy_paste_uv_uvedit_unique_target = BoolProperty( + name="Unique Target", + description="Paste to the target uniquely", + default=False + ) @classmethod def del_props(cls, scene): del scene.muv_props.copy_paste_uv_uvedit + del scene.muv_props.copy_paste_uv_island @BlClassRegistry() @@ -182,3 +204,198 @@ class MUV_OT_CopyPasteUVUVEdit_PasteUV(bpy.types.Operator): bmesh.update_edit_mesh(obj.data) return {'FINISHED'} + + +# Return selected/all count. +# If context.tool_settings.use_uv_select_sync is enabled: +# Return selected/all face count. +# If context.tool_settings.use_uv_select_sync is disabled: +# Return selected/all loop count. +def get_counts(context, island, uv_layer): + selected_count = 0 + all_count = 0 + if context.tool_settings.use_uv_select_sync: + for f in island["faces"]: + all_count += 1 + if f["face"].select: + selected_count += 1 + else: + for f in island["faces"]: + for l in f["face"].loops: + all_count += 1 + if l[uv_layer].select: + selected_count += 1 + + return selected_count, all_count + + +@BlClassRegistry() +class MUV_OT_CopyPasteUVUVEdit_CopyUVIsland(bpy.types.Operator): + """ + Operation class: Copy UV island on UV/Image Editor + """ + + bl_idname = "uv.muv_copy_paste_uv_uvedit_copy_uv_island" + bl_label = "Copy UV Island (UV/Image Editor)" + bl_description = "Copy UV island (only selected in UV/Image Editor)" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return _is_valid_context(context) + + def execute(self, context): + sc = context.scene + props = sc.muv_props.copy_paste_uv_island + + props.src_data = [] + props.src_objects = [] + 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() + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + if context.tool_settings.use_uv_select_sync: + islands = common.get_island_info_from_bmesh( + bm, only_selected=False) + else: + islands = common.get_island_info_from_bmesh( + bm, only_selected=True) + for isl in islands: + # Check if all UVs belonging to the island is selected. + selected_count, all_count = get_counts(context, isl, uv_layer) + if selected_count == 0: + continue + if selected_count != all_count: + self.report( + {'WARNING'}, + "All UVs belonging to the island must be selected") + return {'CANCELLED'} + + data = { + "bmesh": bm, + "uv_layer": uv_layer, + "island": isl + } + props.src_data.append(data) + props.src_objects.append(obj) + + return {'FINISHED'} + + +@BlClassRegistry() +@compat.make_annotations +class MUV_OT_CopyPasteUVUVEdit_PasteUVIsland(bpy.types.Operator): + """ + Operation class: Paste UV island on UV/Image Editor + """ + + bl_idname = "uv.muv_copy_paste_uv_uvedit_paste_uv_island" + bl_label = "Paste UV Island (UV/Image Editor)" + bl_description = "Paste UV island (only selected in UV/Image Editor)" + bl_options = {'REGISTER', 'UNDO'} + + unique_target = BoolProperty( + name="Unique Target", + description="Paste to the target uniquely", + default=False + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.copy_paste_uv_island + if not props.src_data: + return False + return _is_valid_context(context) + + def execute(self, context): + sc = context.scene + props = sc.muv_props.copy_paste_uv_island + + src_data = props.src_data + src_objs = props.src_objects + + bms_and_uv_layers = {} + for d in src_data: + bms_and_uv_layers[d["bmesh"]] = d["uv_layer"] + dst_data = [] + for bm, uv_layer in bms_and_uv_layers.items(): + if context.tool_settings.use_uv_select_sync: + islands = common.get_island_info_from_bmesh( + bm, only_selected=False) + else: + islands = common.get_island_info_from_bmesh( + bm, only_selected=True) + for isl in islands: + # Check if all UVs belonging to the island is selected. + selected_count, all_count = get_counts(context, isl, uv_layer) + if selected_count == 0: + continue + if selected_count != all_count: + self.report( + {'WARNING'}, + "All UVs belonging to the island must be selected") + return {'CANCELLED'} + + dst_data.append( + { + "bm": bm, + "uv_layer": uv_layer, + "island": isl, + } + ) + + used = [] + for ddata in dst_data: + dst_loops = [] + for f in ddata["island"]["faces"]: + for l in f["face"].loops: + dst_loops.append(l) + dst_uv_layer = ddata["uv_layer"] + + # Find a suitable island. + for sdata in src_data: + if self.unique_target and sdata in used: + continue + + src_loops = [] + for f in sdata["island"]["faces"]: + for l in f["face"].loops: + src_loops.append(l) + src_uv_layer = sdata["uv_layer"] + + # Create UV graph. + src_uv_graph = common.create_uv_graph(src_loops, src_uv_layer) + dst_uv_graph = common.create_uv_graph(dst_loops, dst_uv_layer) + + # Check if the graph is isomorphic. + # If the graph is isomorphic, matching pair is returned. + result, pairs = graph_is_isomorphic(src_uv_graph, dst_uv_graph) + if result: + # Paste UV island. + for n1, n2 in pairs.items(): + uv1 = n1.value["uv_vert"][src_uv_layer].uv + l2 = n2.value["loops"] + for l in l2: + l[dst_uv_layer].uv = uv1 + used.append(sdata) + break + else: + self.report({'WARNING'}, "Island does not match") + return {'CANCELLED'} + + for obj in src_objs: + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/magic_uv/op/flip_rotate_uv.py b/magic_uv/op/flip_rotate_uv.py index cb40ede88..c9a79420f 100644 --- a/magic_uv/op/flip_rotate_uv.py +++ b/magic_uv/op/flip_rotate_uv.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy import bmesh @@ -21,6 +21,10 @@ from ..utils import compatibility as compat def _is_valid_context(context): + # only 'VIEW_3D' space is allowed to execute + if not common.is_valid_space(context, ['VIEW_3D']): + return False + objs = common.get_uv_editable_objects(context) if not objs: return False @@ -29,10 +33,6 @@ def _is_valid_context(context): 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 - return True diff --git a/magic_uv/op/mirror_uv.py b/magic_uv/op/mirror_uv.py index dce3ca013..2f637535b 100644 --- a/magic_uv/op/mirror_uv.py +++ b/magic_uv/op/mirror_uv.py @@ -4,8 +4,8 @@ __author__ = "Keith (Wahooney) Boshoff, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy from bpy.props import ( @@ -23,6 +23,10 @@ from .. import common def _is_valid_context(context): + # only 'VIEW_3D' space is allowed to execute + if not common.is_valid_space(context, ['VIEW_3D']): + return False + objs = common.get_uv_editable_objects(context) if not objs: return False @@ -31,10 +35,6 @@ def _is_valid_context(context): 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 - return True @@ -251,15 +251,22 @@ class MUV_OT_MirrorUV(bpy.types.Operator): # test if the vertices x values are the same sign dst = _get_face_center(f_dst, transformed_verts) src = _get_face_center(f_src, transformed_verts) - if (dst.x > 0 and src.x > 0) or (dst.x < 0 and src.x < 0): - continue # invert source axis if axis == 'X': + if ((dst.x > 0 and src.x > 0) or + (dst.x < 0 and src.x < 0)): + continue src.x = -src.x elif axis == 'Y': - src.y = -src.z + if ((dst.y > 0 and src.y > 0) or + (dst.y < 0 and src.y < 0)): + continue + src.y = -src.y elif axis == 'Z': + if ((dst.z > 0 and src.z > 0) or + (dst.z < 0 and src.z < 0)): + continue src.z = -src.z # do mirror UV diff --git a/magic_uv/op/move_uv.py b/magic_uv/op/move_uv.py index 76022d12d..b99a6b15d 100644 --- a/magic_uv/op/move_uv.py +++ b/magic_uv/op/move_uv.py @@ -4,8 +4,8 @@ __author__ = "kgeogeo, mem, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy from bpy.props import BoolProperty @@ -18,6 +18,10 @@ from ..utils.property_class_registry import PropertyClassRegistry def _is_valid_context(context): + # only 'VIEW_3D' space is allowed to execute + if not common.is_valid_space(context, ['VIEW_3D']): + return False + # Multiple objects editing mode is not supported in this feature. objs = common.get_uv_editable_objects(context) if len(objs) != 1: @@ -27,10 +31,6 @@ def _is_valid_context(context): 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 - return True diff --git a/magic_uv/op/pack_uv.py b/magic_uv/op/pack_uv.py index 5eaceaf49..2163d4f39 100644 --- a/magic_uv/op/pack_uv.py +++ b/magic_uv/op/pack_uv.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" from math import fabs @@ -21,11 +21,18 @@ from mathutils import Vector from ..utils.bl_class_registry import BlClassRegistry from ..utils.property_class_registry import PropertyClassRegistry +from ..utils.graph import graph_is_isomorphic from ..utils import compatibility as compat from .. import common 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 + if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): + return False + objs = common.get_uv_editable_objects(context) if not objs: return False @@ -34,12 +41,6 @@ def _is_valid_context(context): 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 - if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): - return False - return True @@ -137,15 +138,36 @@ class _Properties: min=0.000001, max=10.0, default=(0.001, 0.001), - size=2 + size=2, + subtype='XYZ' ) scene.muv_pack_uv_allowable_size_deviation = FloatVectorProperty( name="Allowable Size Deviation", - description="Allowable sizse deviation to judge same UV island", + description="Allowable sizes deviation to judge same UV island", min=0.000001, max=10.0, default=(0.001, 0.001), - size=2 + size=2, + subtype='XYZ' + ) + scene.muv_pack_uv_accurate_island_copy = BoolProperty( + name="Accurate Island Copy", + description="Copy islands topologically", + default=True + ) + scene.muv_pack_uv_stride = FloatVectorProperty( + name="Stride", + description="Stride UV coordinates", + min=-100.0, + max=100.0, + default=(0.0, 0.0), + size=2, + subtype='XYZ' + ) + scene.muv_pack_uv_apply_pack_uv = BoolProperty( + name="Apply Pack UV", + description="Apply Pack UV operation intrinsic to Blender itself", + default=True ) @classmethod @@ -153,6 +175,9 @@ class _Properties: del scene.muv_pack_uv_enabled del scene.muv_pack_uv_allowable_center_deviation del scene.muv_pack_uv_allowable_size_deviation + del scene.muv_pack_uv_accurate_island_copy + del scene.muv_pack_uv_stride + del scene.muv_pack_uv_apply_pack_uv @BlClassRegistry() @@ -188,7 +213,8 @@ class MUV_OT_PackUV(bpy.types.Operator): min=0.000001, max=10.0, default=(0.001, 0.001), - size=2 + size=2, + subtype='XYZ' ) allowable_size_deviation = FloatVectorProperty( name="Allowable Size Deviation", @@ -196,7 +222,27 @@ class MUV_OT_PackUV(bpy.types.Operator): min=0.000001, max=10.0, default=(0.001, 0.001), - size=2 + size=2, + subtype='XYZ' + ) + accurate_island_copy = BoolProperty( + name="Accurate Island Copy", + description="Copy islands topologically", + default=True + ) + stride = FloatVectorProperty( + name="Stride", + description="Stride UV coordinates", + min=-100.0, + max=100.0, + default=(0.0, 0.0), + size=2, + subtype='XYZ' + ) + apply_pack_uv = BoolProperty( + name="Apply Pack UV", + description="Apply Pack UV operation intrinsic to Blender itself", + default=True ) @classmethod @@ -249,7 +295,8 @@ class MUV_OT_PackUV(bpy.types.Operator): 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) + if self.apply_pack_uv: + bpy.ops.uv.pack_islands(rotate=self.rotate, margin=self.margin) # copy/paste UV among same islands for gidx in range(num_group): @@ -260,16 +307,57 @@ class MUV_OT_PackUV(bpy.types.Operator): 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:]: + + src_loops = [] + for f in group[0]["faces"]: + for l in f["face"].loops: + src_loops.append(l) + + src_uv_graph = common.create_uv_graph(src_loops, src_uv_layer) + + for stride_idx, g in enumerate(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): - dst_loop_lists[dest_loop.index][dst_uv_layer].uv = \ - src_loop_lists[src_loop.index][src_uv_layer].uv + + dst_loops = [] + for f in g["faces"]: + for l in f["face"].loops: + dst_loops.append(l) + + dst_uv_graph = common.create_uv_graph(dst_loops, dst_uv_layer) + + uv_stride = Vector(((stride_idx + 1) * self.stride.x, + (stride_idx + 1) * self.stride.y)) + if self.accurate_island_copy: + # Check if the graph is isomorphic. + # If the graph is isomorphic, matching pair is returned. + result, pairs = graph_is_isomorphic( + src_uv_graph, dst_uv_graph) + if not result: + self.report( + {'WARNING'}, + "Island does not match. " + "Disable 'Accurate Island Copy' and try again") + return {'CANCELLED'} + + # Paste UV island. + for n1, n2 in pairs.items(): + uv1 = n1.value["uv_vert"][src_uv_layer].uv + l2 = n2.value["loops"] + for l in l2: + l[dst_uv_layer].uv = uv1 + uv_stride + else: + 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): + src_lidx = src_loop.index + dst_lidx = dest_loop.index + dst_loop_lists[dst_lidx][dst_uv_layer].uv = \ + src_loop_lists[src_lidx][src_uv_layer].uv + \ + uv_stride # restore face/UV selection bpy.ops.uv.select_all(action='DESELECT') diff --git a/magic_uv/op/preserve_uv_aspect.py b/magic_uv/op/preserve_uv_aspect.py index 9d3cbddeb..32e0103f5 100644 --- a/magic_uv/op/preserve_uv_aspect.py +++ b/magic_uv/op/preserve_uv_aspect.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy from bpy.props import StringProperty, EnumProperty, BoolProperty @@ -19,6 +19,10 @@ from ..utils import compatibility as compat def _is_valid_context(context): + # only 'VIEW_3D' space is allowed to execute + if not common.is_valid_space(context, ['VIEW_3D']): + return False + objs = common.get_uv_editable_objects(context) if not objs: return False @@ -27,10 +31,6 @@ def _is_valid_context(context): 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 - return True diff --git a/magic_uv/op/select_uv.py b/magic_uv/op/select_uv.py index affc41e43..3e1f160c5 100644 --- a/magic_uv/op/select_uv.py +++ b/magic_uv/op/select_uv.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy from bpy.props import BoolProperty, FloatProperty, EnumProperty @@ -18,6 +18,12 @@ from ..utils import compatibility as compat def _is_valid_context(context): + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): + return False + objs = common.get_uv_editable_objects(context) if not objs: return False @@ -26,12 +32,6 @@ def _is_valid_context(context): 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 - if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): - return False - return True @@ -49,8 +49,8 @@ class _Properties: scene.muv_select_uv_same_polygon_threshold = FloatProperty( name="Same Polygon Threshold", description="Threshold to distinguish same polygons", - default=0.000001, - min=0.000001, + default=0.00001, + min=0.00001, max=0.01, step=0.00001 ) @@ -91,8 +91,8 @@ class MUV_OT_SelectUV_SelectOverlapped(bpy.types.Operator): same_polygon_threshold = FloatProperty( name="Same Polygon Threshold", description="Threshold to distinguish same polygons", - default=0.000001, - min=0.000001, + default=0.00001, + min=0.00001, max=0.01, step=0.00001 ) diff --git a/magic_uv/op/smooth_uv.py b/magic_uv/op/smooth_uv.py index 020bc78f8..b7c06c7c2 100644 --- a/magic_uv/op/smooth_uv.py +++ b/magic_uv/op/smooth_uv.py @@ -4,8 +4,8 @@ __author__ = "imdjs, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy from bpy.props import BoolProperty, FloatProperty @@ -18,6 +18,12 @@ from ..utils import compatibility as compat def _is_valid_context(context): + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): + return False + objs = common.get_uv_editable_objects(context) if not objs: return False @@ -26,12 +32,6 @@ def _is_valid_context(context): 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 - if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): - return False - return True diff --git a/magic_uv/op/texture_lock.py b/magic_uv/op/texture_lock.py index f54c94530..94d91e573 100644 --- a/magic_uv/op/texture_lock.py +++ b/magic_uv/op/texture_lock.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import math from math import atan2, cos, sqrt, sin, fabs @@ -172,6 +172,10 @@ def _calc_tri_vert(v0, v1, angle0, angle1): def _is_valid_context(context): + # only 'VIEW_3D' space is allowed to execute + if not common.is_valid_space(context, ['VIEW_3D']): + return False + objs = common.get_uv_editable_objects(context) if not objs: return False @@ -180,10 +184,6 @@ def _is_valid_context(context): 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 - return True diff --git a/magic_uv/op/texture_projection.py b/magic_uv/op/texture_projection.py index 912447c33..1288e9b79 100644 --- a/magic_uv/op/texture_projection.py +++ b/magic_uv/op/texture_projection.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" from collections import namedtuple from math import sin, cos @@ -139,6 +139,10 @@ def _create_affine_matrix(identity, scale, rotate, translate): def _is_valid_context(context): + # only 'VIEW_3D' space is allowed to execute + if not common.is_valid_space(context, ['VIEW_3D']): + return False + objs = common.get_uv_editable_objects(context) if not objs: return False @@ -147,10 +151,6 @@ def _is_valid_context(context): 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 - return True diff --git a/magic_uv/op/texture_wrap.py b/magic_uv/op/texture_wrap.py index 4f9c868d8..55e878120 100644 --- a/magic_uv/op/texture_wrap.py +++ b/magic_uv/op/texture_wrap.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy from bpy.props import ( @@ -19,6 +19,10 @@ from ..utils.property_class_registry import PropertyClassRegistry def _is_valid_context(context): + # only 'VIEW_3D' space is allowed to execute + if not common.is_valid_space(context, ['VIEW_3D']): + return False + # Multiple objects editing mode is not supported in this feature. objs = common.get_uv_editable_objects(context) if len(objs) != 1: @@ -28,10 +32,6 @@ def _is_valid_context(context): 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 - return True diff --git a/magic_uv/op/transfer_uv.py b/magic_uv/op/transfer_uv.py index 029a5de35..4d332a55b 100644 --- a/magic_uv/op/transfer_uv.py +++ b/magic_uv/op/transfer_uv.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>, Mifth, MaxRobinot" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" from collections import OrderedDict @@ -20,6 +20,10 @@ from ..utils import compatibility as compat def _is_valid_context(context): + # only 'VIEW_3D' space is allowed to execute + if not common.is_valid_space(context, ['VIEW_3D']): + return False + # Multiple objects editing mode is not supported in this feature. objs = common.get_uv_editable_objects(context) if len(objs) != 1: @@ -29,10 +33,6 @@ def _is_valid_context(context): 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 - return True diff --git a/magic_uv/op/unwrap_constraint.py b/magic_uv/op/unwrap_constraint.py index da94c495d..06b33c277 100644 --- a/magic_uv/op/unwrap_constraint.py +++ b/magic_uv/op/unwrap_constraint.py @@ -1,9 +1,11 @@ # SPDX-License-Identifier: GPL-2.0-or-later +# <pep8-80 compliant> + __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy from bpy.props import ( @@ -20,6 +22,10 @@ from ..utils import compatibility as compat def _is_valid_context(context): + # only 'VIEW_3D' space is allowed to execute + if not common.is_valid_space(context, ['VIEW_3D']): + return False + objs = common.get_uv_editable_objects(context) if not objs: return False @@ -28,10 +34,6 @@ def _is_valid_context(context): 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 - return True diff --git a/magic_uv/op/uv_bounding_box.py b/magic_uv/op/uv_bounding_box.py index 436f66793..07519c248 100644 --- a/magic_uv/op/uv_bounding_box.py +++ b/magic_uv/op/uv_bounding_box.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" from enum import IntEnum import math @@ -30,6 +30,12 @@ MAX_VALUE = 100000.0 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 + if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): + return False + obj = context.object # only edit mode is allowed to execute @@ -40,12 +46,6 @@ def _is_valid_context(context): 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 - if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): - return False - return True diff --git a/magic_uv/op/uv_inspection.py b/magic_uv/op/uv_inspection.py index e974221e5..cfbdbacff 100644 --- a/magic_uv/op/uv_inspection.py +++ b/magic_uv/op/uv_inspection.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import random from math import fabs @@ -26,6 +26,12 @@ else: 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 + if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): + return False + objs = common.get_uv_editable_objects(context) if not objs: return False @@ -34,12 +40,6 @@ def _is_valid_context(context): 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 - if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): - return False - return True @@ -156,8 +156,8 @@ class _Properties: scene.muv_uv_inspection_same_polygon_threshold = FloatProperty( name="Same Polygon Threshold", description="Threshold to distinguish same polygons", - default=0.000001, - min=0.000001, + default=0.00001, + min=0.00001, max=0.01, step=0.00001 ) diff --git a/magic_uv/op/uv_sculpt.py b/magic_uv/op/uv_sculpt.py index 24c76e130..d1cfc6909 100644 --- a/magic_uv/op/uv_sculpt.py +++ b/magic_uv/op/uv_sculpt.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" from math import pi, cos, tan, sin @@ -35,6 +35,10 @@ else: def _is_valid_context(context): + # only 'VIEW_3D' space is allowed to execute + if not common.is_valid_space(context, ['VIEW_3D']): + return False + objs = common.get_uv_editable_objects(context) if not objs: return False @@ -43,10 +47,6 @@ def _is_valid_context(context): 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 - return True diff --git a/magic_uv/op/uvw.py b/magic_uv/op/uvw.py index 516a5da89..24588b82a 100644 --- a/magic_uv/op/uvw.py +++ b/magic_uv/op/uvw.py @@ -4,8 +4,8 @@ __author__ = "Alexander Milovsky, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" from math import sin, cos, pi @@ -26,6 +26,10 @@ from ..utils import compatibility as compat def _is_valid_context(context): + # only 'VIEW_3D' space is allowed to execute + if not common.is_valid_space(context, ['VIEW_3D']): + return False + objs = common.get_uv_editable_objects(context) if not objs: return False @@ -34,10 +38,6 @@ def _is_valid_context(context): 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 - return True diff --git a/magic_uv/op/world_scale_uv.py b/magic_uv/op/world_scale_uv.py index 9c617ee40..41f258525 100644 --- a/magic_uv/op/world_scale_uv.py +++ b/magic_uv/op/world_scale_uv.py @@ -4,8 +4,8 @@ __author__ = "McBuff, Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" from math import sqrt @@ -26,6 +26,10 @@ from ..utils import compatibility as compat def _is_valid_context_for_measure(context): + # only 'VIEW_3D' space is allowed to execute + if not common.is_valid_space(context, ['VIEW_3D']): + return False + # Multiple objects editing mode is not supported in this feature. objs = common.get_uv_editable_objects(context) if len(objs) != 1: @@ -35,14 +39,14 @@ def _is_valid_context_for_measure(context): 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 - return True def _is_valid_context_for_apply(context): + # only 'VIEW_3D' space is allowed to execute + if not common.is_valid_space(context, ['VIEW_3D']): + return False + objs = common.get_uv_editable_objects(context) if not objs: return False @@ -51,10 +55,6 @@ def _is_valid_context_for_apply(context): 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 - return True diff --git a/magic_uv/preferences.py b/magic_uv/preferences.py index 27785ad9a..d273f41ff 100644 --- a/magic_uv/preferences.py +++ b/magic_uv/preferences.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy from bpy.props import ( @@ -119,6 +119,9 @@ def image_uvs_menu_fn(self, context): ops = layout.operator(MUV_OT_PackUV.bl_idname, text="Pack UV") ops.allowable_center_deviation = sc.muv_pack_uv_allowable_center_deviation ops.allowable_size_deviation = sc.muv_pack_uv_allowable_size_deviation + ops.accurate_island_copy = sc.muv_pack_uv_accurate_island_copy + ops.stride = sc.muv_pack_uv_stride + ops.apply_pack_uv = sc.muv_pack_uv_apply_pack_uv # Select UV layout.menu(MUV_MT_SelectUV.bl_idname, text="Select UV") # Smooth UV diff --git a/magic_uv/properties.py b/magic_uv/properties.py index 215069156..127fc6f26 100644 --- a/magic_uv/properties.py +++ b/magic_uv/properties.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" 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 adad2fe91..0f94409b7 100644 --- a/magic_uv/ui/IMAGE_MT_uvs.py +++ b/magic_uv/ui/IMAGE_MT_uvs.py @@ -4,14 +4,16 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy from ..op.copy_paste_uv_uvedit import ( MUV_OT_CopyPasteUVUVEdit_CopyUV, MUV_OT_CopyPasteUVUVEdit_PasteUV, + MUV_OT_CopyPasteUVUVEdit_CopyUVIsland, + MUV_OT_CopyPasteUVUVEdit_PasteUVIsland, ) from ..op.align_uv_cursor import MUV_OT_AlignUVCursor from ..op.align_uv import ( @@ -42,13 +44,22 @@ class MUV_MT_CopyPasteUV_UVEdit(bpy.types.Menu): bl_label = "Copy/Paste UV" bl_description = "Copy and Paste UV coordinate among object" - def draw(self, _): + def draw(self, context): layout = self.layout + sc = context.scene + layout.label(text="Face") layout.operator(MUV_OT_CopyPasteUVUVEdit_CopyUV.bl_idname, text="Copy") layout.operator(MUV_OT_CopyPasteUVUVEdit_PasteUV.bl_idname, text="Paste") + layout.label(text="Island") + layout.operator(MUV_OT_CopyPasteUVUVEdit_CopyUVIsland.bl_idname, + text="Copy") + ops = layout.operator(MUV_OT_CopyPasteUVUVEdit_PasteUVIsland.bl_idname, + text="Paste") + ops.unique_target = sc.muv_copy_paste_uv_uvedit_unique_target + @BlClassRegistry() class MUV_MT_AlignUV(bpy.types.Menu): diff --git a/magic_uv/ui/VIEW3D_MT_object.py b/magic_uv/ui/VIEW3D_MT_object.py index 29d5d607c..e9349ee00 100644 --- a/magic_uv/ui/VIEW3D_MT_object.py +++ b/magic_uv/ui/VIEW3D_MT_object.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy diff --git a/magic_uv/ui/VIEW3D_MT_uv_map.py b/magic_uv/ui/VIEW3D_MT_uv_map.py index ee99ccba6..544dabe47 100644 --- a/magic_uv/ui/VIEW3D_MT_uv_map.py +++ b/magic_uv/ui/VIEW3D_MT_uv_map.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy.utils diff --git a/magic_uv/ui/__init__.py b/magic_uv/ui/__init__.py index 883e966ff..9b56e3aea 100644 --- a/magic_uv/ui/__init__.py +++ b/magic_uv/ui/__init__.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" 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 847b6e9a2..95da5be4d 100644 --- a/magic_uv/ui/uvedit_copy_paste_uv.py +++ b/magic_uv/ui/uvedit_copy_paste_uv.py @@ -4,14 +4,16 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy from ..op.copy_paste_uv_uvedit import ( MUV_OT_CopyPasteUVUVEdit_CopyUV, MUV_OT_CopyPasteUVUVEdit_PasteUV, + MUV_OT_CopyPasteUVUVEdit_CopyUVIsland, + MUV_OT_CopyPasteUVUVEdit_PasteUVIsland, ) from ..utils.bl_class_registry import BlClassRegistry from ..utils import compatibility as compat @@ -34,9 +36,22 @@ class MUV_PT_UVEdit_CopyPasteUV(bpy.types.Panel): layout = self.layout layout.label(text="", icon=compat.icon('IMAGE')) - def draw(self, _): + def draw(self, context): layout = self.layout + sc = context.scene + layout.label(text="Face:") row = layout.row(align=True) row.operator(MUV_OT_CopyPasteUVUVEdit_CopyUV.bl_idname, text="Copy") row.operator(MUV_OT_CopyPasteUVUVEdit_PasteUV.bl_idname, text="Paste") + + layout.separator() + + layout.label(text="Island:") + row = layout.row(align=True) + row.operator(MUV_OT_CopyPasteUVUVEdit_CopyUVIsland.bl_idname, + text="Copy") + ops = row.operator(MUV_OT_CopyPasteUVUVEdit_PasteUVIsland.bl_idname, + text="Paste") + ops.unique_target = sc.muv_copy_paste_uv_uvedit_unique_target + layout.prop(sc, "muv_copy_paste_uv_uvedit_unique_target") diff --git a/magic_uv/ui/uvedit_editor_enhancement.py b/magic_uv/ui/uvedit_editor_enhancement.py index b73e5eb9d..0af607ecf 100644 --- a/magic_uv/ui/uvedit_editor_enhancement.py +++ b/magic_uv/ui/uvedit_editor_enhancement.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy diff --git a/magic_uv/ui/uvedit_uv_manipulation.py b/magic_uv/ui/uvedit_uv_manipulation.py index 636a0acae..e538ea4d8 100644 --- a/magic_uv/ui/uvedit_uv_manipulation.py +++ b/magic_uv/ui/uvedit_uv_manipulation.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy @@ -184,10 +184,18 @@ class MUV_PT_UVEdit_UVManipulation(bpy.types.Panel): sc.muv_pack_uv_allowable_center_deviation ops.allowable_size_deviation = \ sc.muv_pack_uv_allowable_size_deviation + ops.accurate_island_copy = \ + sc.muv_pack_uv_accurate_island_copy + ops.stride = sc.muv_pack_uv_stride + ops.apply_pack_uv = sc.muv_pack_uv_apply_pack_uv + box.prop(sc, "muv_pack_uv_apply_pack_uv") + box.prop(sc, "muv_pack_uv_accurate_island_copy") box.label(text="Allowable Center Deviation:") box.prop(sc, "muv_pack_uv_allowable_center_deviation", text="") box.label(text="Allowable Size Deviation:") box.prop(sc, "muv_pack_uv_allowable_size_deviation", text="") + box.label(text="Stride:") + box.prop(sc, "muv_pack_uv_stride", text="") box = layout.box() box.prop(sc, "muv_clip_uv_enabled", text="Clip UV") diff --git a/magic_uv/ui/view3d_copy_paste_uv_editmode.py b/magic_uv/ui/view3d_copy_paste_uv_editmode.py index d0b52021b..49bbaac3f 100644 --- a/magic_uv/ui/view3d_copy_paste_uv_editmode.py +++ b/magic_uv/ui/view3d_copy_paste_uv_editmode.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" 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 1153bedd4..2219d9e8a 100644 --- a/magic_uv/ui/view3d_copy_paste_uv_objectmode.py +++ b/magic_uv/ui/view3d_copy_paste_uv_objectmode.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy diff --git a/magic_uv/ui/view3d_uv_manipulation.py b/magic_uv/ui/view3d_uv_manipulation.py index 5e7ae28ab..106ba2bc1 100644 --- a/magic_uv/ui/view3d_uv_manipulation.py +++ b/magic_uv/ui/view3d_uv_manipulation.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy diff --git a/magic_uv/ui/view3d_uv_mapping.py b/magic_uv/ui/view3d_uv_mapping.py index 1fd05dffa..5da7cb6c4 100644 --- a/magic_uv/ui/view3d_uv_mapping.py +++ b/magic_uv/ui/view3d_uv_mapping.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy diff --git a/magic_uv/utils/__init__.py b/magic_uv/utils/__init__.py index 22ed284a3..2ce7f34a5 100644 --- a/magic_uv/utils/__init__.py +++ b/magic_uv/utils/__init__.py @@ -4,17 +4,19 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" if "bpy" in locals(): import importlib importlib.reload(bl_class_registry) importlib.reload(compatibility) + importlib.reload(graph) importlib.reload(property_class_registry) else: from . import bl_class_registry from . import compatibility + from . import graph from . import property_class_registry import bpy diff --git a/magic_uv/utils/bl_class_registry.py b/magic_uv/utils/bl_class_registry.py index 56ee91eb0..10d63d246 100644 --- a/magic_uv/utils/bl_class_registry.py +++ b/magic_uv/utils/bl_class_registry.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy diff --git a/magic_uv/utils/compatibility.py b/magic_uv/utils/compatibility.py index cc8813e58..50ce304f6 100644 --- a/magic_uv/utils/compatibility.py +++ b/magic_uv/utils/compatibility.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" import bpy import bgl diff --git a/magic_uv/utils/graph.py b/magic_uv/utils/graph.py new file mode 100644 index 000000000..c86547739 --- /dev/null +++ b/magic_uv/utils/graph.py @@ -0,0 +1,152 @@ +# SPDX-License-Identifier: GPL-2.0-or-later + +# <pep8-80 compliant> + +__author__ = "Nutti <nutti.metro@gmail.com>" +__status__ = "production" +__version__ = "6.6" +__date__ = "22 Apr 2022" + + +class Node: + def __init__(self, key, value=None): + self.key = key + self.value = value + self.edges = [] + + def degree(self): + return len(self.edges) + + def connected_nodes(self): + return [e.other(self) for e in self.edges] + + +class Edge: + def __init__(self, node_1, node_2): + self.node_1 = node_1 + self.node_2 = node_2 + + def other(self, node): + if self.node_1 == node and self.node_2 == node: + raise RuntimeError("Loop edge in {} is not supported." + .format(node.key)) + if node not in (self.node_1, self.node_2): + raise RuntimeError("Node {} does not belog this edge." + .format(node.key)) + if self.node_1 == node: + return self.node_2 + return self.node_1 + + +class Graph: + def __init__(self): + self.edges = [] + self.nodes = {} + + def add_node(self, node): + if node.key in self.nodes: + raise RuntimeError("Node '{}' is already registered." + .format(node.key)) + self.nodes[node.key] = node + + def add_edge(self, node_1, node_2): + if node_1.key not in self.nodes: + raise RuntimeError("Node '{}' is not registered." + .format(node_1.key)) + if node_2.key not in self.nodes: + raise RuntimeError("Node '{}' is not registered." + .format(node_2.key)) + + edge = Edge(node_1, node_2) + self.edges.append(edge) + node_1.edges.append(edge) + node_2.edges.append(edge) + + def get_node(self, key): + return self.nodes[key] + + +def dump_graph(graph): + print("=== Node ===") + for _, node in graph.nodes.items(): + print("Key: {}, Value {}".format(node.key, node.value)) + + print("=== Edge ===") + for edge in graph.edges: + print("{} - {}".format(edge.node_1.key, edge.node_2.key)) + + +# VF2 algorithm +# Ref: https://stackoverflow.com/questions/8176298/ +# vf2-algorithm-steps-with-example +# Ref: https://github.com/satemochi/saaaaah/blob/master/geometric_misc/ +# isomorph/vf2/vf2.py +def graph_is_isomorphic(graph_1, graph_2): + def is_iso(pairs, matching_node, new_node): + # Algorithm: + # 1. The degree is same (It's faster). + # 2. The connected node is same. + if matching_node.degree() != new_node.degree(): + return False + + matching_connected = [c.key for c in matching_node.connected_nodes()] + new_connected = [c.key for c in new_node.connected_nodes()] + + for p in pairs: + n1 = p[0] + n2 = p[1] + if n1 in matching_connected and n2 not in new_connected: + return False + if n1 not in matching_connected and n2 in new_connected: + return False + + return True + + def dfs(graph_1, graph_2): + def generate_pair(g1, g2, pairs): + remove_1 = [p[0] for p in pairs] + remove_2 = [p[1] for p in pairs] + + keys_1 = sorted(list(set(g1.nodes.keys()) - set(remove_1))) + keys_2 = sorted(list(set(g2.nodes.keys()) - set(remove_2))) + for k1 in keys_1: + for k2 in keys_2: + yield (k1, k2) + + pairs = [] + stack = [generate_pair(graph_1, graph_2, pairs)] + while stack: + try: + k1, k2 = next(stack[-1]) + n1 = graph_1.get_node(k1) + n2 = graph_2.get_node(k2) + if is_iso(pairs, n1, n2): + pairs.append([k1, k2]) + stack.append(generate_pair(graph_1, graph_2, pairs)) + if len(pairs) == len(graph_1.nodes): + return True, pairs + except StopIteration: + stack.pop() + diff = len(pairs) - len(stack) + for _ in range(diff): + pairs.pop() + + return False, [] + + # First, check simple condition. + if len(graph_1.nodes) != len(graph_2.nodes): + return False, {} + if len(graph_1.edges) != len(graph_2.edges): + return False, {} + + is_isomorphic, pairs = dfs(graph_1, graph_2) + + node_pairs = {} + for pair in pairs: + n1 = pair[0] + n2 = pair[1] + node_1 = graph_1.get_node(n1) + node_2 = graph_2.get_node(n2) + node_pairs[node_1] = node_2 + + return is_isomorphic, node_pairs diff --git a/magic_uv/utils/property_class_registry.py b/magic_uv/utils/property_class_registry.py index 62cba9032..e09ad3208 100644 --- a/magic_uv/utils/property_class_registry.py +++ b/magic_uv/utils/property_class_registry.py @@ -4,8 +4,8 @@ __author__ = "Nutti <nutti.metro@gmail.com>" __status__ = "production" -__version__ = "6.5" -__date__ = "6 Mar 2021" +__version__ = "6.6" +__date__ = "22 Apr 2022" from .. import common -- GitLab