diff --git a/magic_uv/__init__.py b/magic_uv/__init__.py index dc3c96416c9de76de054be72db79cf4a6505ded2..88385107536efc8f7b2aea414d87e928476469ae 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 4e6334083ae64c42f9b3725040b6e839af25f9fc..034936c552bb8a822f9bc2aab6e515b531860f76 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 76eaf4800d995da68a9f5d65c6d99fb89787917b..bccf4e17084f2bcf666677640bbb1112650bcf1b 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 c1f696ab4de7c90a3f02a7bd4a066f713888fa95..044141b692874281df56e3aabebd0570f72060f7 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 da77b17b306c4f14a14f4fdd1ca10547c336b538..223ed004ac577f5edd6d7739ca54acd229aca34b 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 9f606db9bc9006c1954511f21fe6a7a8379edc42..cf5a49debfa73bf8fd69a24b5ce9ec1bfb7bd02d 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 2b7f1491bd0f0d22e1181e28b7ed6e00b3821bbf..696b7cb85da4af57d3f0747e5e3103f7bc0a497a 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 c74755438eed1399e9844d1fa8b66cfba8046e33..e3815453c6661fa72d212dfc0bf966fb0981b88f 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 8ee83ad9468b542e29e3ea37a2b0b98e5bebc319..1496d67ac9b89de1106613dd4de106dbd2b68db4 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 4e5d500afaf5873741e8ba423756927f2c99bcb0..af5df07c34531219a6c02ec799c7d8b6fbfb8364 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 7055915f98353998ea0cd92cb651e6fdeadbe528..733c30b32ef16979a1faa0c14acf71e956df2221 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 cb40ede880cfc6f45b13af63466e712007b945fa..c9a79420f3c8aee1b42e1d9875c9ad4fe8c5a706 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 dce3ca013ede81f3d715146c90dc7087b55621ef..2f637535b6a5b3730c4119ef368c24a8a237d685 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 76022d12d7c2b9d51711c2f255173d896d92dc80..b99a6b15dc334af17b3efa66179ace30add0d8cf 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 5eaceaf49f3e04fa0f97c84b9a241ca73d48d72e..2163d4f39d313dbd15f5d80bf548fe72aa02235d 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 9d3cbddeb0608387a749b45a077b226c2d44d336..32e0103f523750cb871f0e5ddb4b9e23c4cc9acf 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 affc41e434c0a5976efbe2754627aed2de5fcaa8..3e1f160c53862ef80846e8897637b01739b5ee1b 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 020bc78f8da23398e8acbca29d1e9bc89b8b1e21..b7c06c7c2579f33be977defc766ac032175c76cc 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 f54c9453063bd952c946c0c5331d404ec8adceac..94d91e573979834b860a326ba8a931d4065e98dc 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 912447c33f715f057abb4c804bcaba58817b0e30..1288e9b79894662eb493c6bea696e64ea2838683 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 4f9c868d856a0f3c33064566377313d6b7264bfc..55e878120943065dedb727b82e5f7dafec94e2a4 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 029a5de3580723f27a3e7ee139a68ec4c706e029..4d332a55bd04293825d8b018ae0dbeb814f0a102 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 da94c495d82f93e17b122d9f59e84f547640cbc6..06b33c277afafa604e05625a69b9e01ee89e7628 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 436f667935cb9ec42e1deb8ec456724ea4ad6af6..07519c248f5c2642aaad04be817b50e4c075954d 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 e974221e5804d579ba004c1e1e5b19a57cfa3672..cfbdbacff8a0396d3880322fef83af4e0e583ac5 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 24c76e13022bfd6ee65f5da5b60fafaef597d69e..d1cfc69099c8008f2496b8c16259e71a0927b5b0 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 516a5da89250f3376902cbf3c67c37dddb5972e7..24588b82a0287b27f7a6794056138a780670a1df 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 9c617ee40ad997ac26527a684be6b06fc013b4d5..41f258525f3302c60b47c7ee523ea806159f8144 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 27785ad9a32c05da948c6643c1abb9dc249d22f5..d273f41ffd8211d519c22011206d229d2b3623e4 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 215069156f7e8445868391f650ec1039b42a9a9a..127fc6f262a1705770b965b7a625bd4f5629333d 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 adad2fe916c7b12839d0a7f4a71ed1faac449805..0f94409b7fceec1d6f5236854cdc60ca021dd34e 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 29d5d607c3476658991a6b6b96e35b9375ee5f10..e9349ee00ff2a5bed38ef830ee33da7254cd7b28 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 ee99ccba6704e5c3204ba0022c64fe616ba157de..544dabe47b0724d5d21d582a66fd0903d17160bb 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 883e966ff2fafe03062e52b90a8fd171bf458a59..9b56e3aea0b778dd654ae6d0fe882aae74ebda62 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 847b6e9a2b4d0887d82a66650efe61eab068b404..95da5be4da752a3444f73eb1578b5e503654708f 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 b73e5eb9d0311f6ff85b778c02c685b22fcb2d37..0af607ecf785ea2b4df5371375099d54989ad318 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 636a0acae5a0962f42954476a05b508cd4aeabc6..e538ea4d892c454efd79257478abbb37ac2fc041 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 d0b52021b886f5e370f42213d0ef66ed1bde3d67..49bbaac3ffed5c7a39f4a8525cb99d0597837330 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 1153bedd4280eb7e18d0791468ed4fb51bf50e7e..2219d9e8a8fdd8e2fe2540b82f01dbeaa723d2eb 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 5e7ae28aba654f7dd66f17b47f340b825ceffa75..106ba2bc1a81b1c556c6af923df2ab5c2fc0ee3e 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 1fd05dffac89c0e7e1594d568fdfbf0529dd540d..5da7cb6c45dc0c2f471fdfa5fdf6984cccbf07f1 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 22ed284a3fc13cb6ac9f4b7f498eba59b403b131..2ce7f34a5a7cd05d2a6c94c3ef6e8228100e5b78 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 56ee91eb0a4061ee0acad1be70679c530df5b964..10d63d246e215ac8daa01aa0784d8f5be9b61932 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 cc8813e58b9ca943da39ce21211f157c01e70008..50ce304f6e8ca7720c8d9ec982d55b5d35883e00 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 0000000000000000000000000000000000000000..c865477398d7e082926ff7e2b45723f33d75cc38 --- /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 62cba90328dddb5bdbbfee7d1dbf5c367d989963..e09ad32084259db39567778adc8b3abdb27f53ea 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