Welcome to mirror list, hosted at ThFree Co, Russian Federation.

git.blender.org/blender-addons.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornutti <nutti.metro@gmail.com>2022-04-22 10:21:10 +0300
committernutti <nutti.metro@gmail.com>2022-04-22 10:21:10 +0300
commit2cbb9e2b960ac343a6f5796e20d0efb443bd4a67 (patch)
treef851332fd5c89fe91a2ac157ee531be8fab42742
parentf6ad5b917e437e3c0b2092b64a82e6cfc56ba1bc (diff)
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
-rw-r--r--magic_uv/__init__.py11
-rw-r--r--magic_uv/common.py137
-rw-r--r--magic_uv/lib/__init__.py4
-rw-r--r--magic_uv/lib/bglx.py2
-rw-r--r--magic_uv/op/__init__.py4
-rw-r--r--magic_uv/op/align_uv.py16
-rw-r--r--magic_uv/op/align_uv_cursor.py41
-rw-r--r--magic_uv/op/clip_uv.py16
-rw-r--r--magic_uv/op/copy_paste_uv.py12
-rw-r--r--magic_uv/op/copy_paste_uv_object.py12
-rw-r--r--magic_uv/op/copy_paste_uv_uvedit.py237
-rw-r--r--magic_uv/op/flip_rotate_uv.py12
-rw-r--r--magic_uv/op/mirror_uv.py25
-rw-r--r--magic_uv/op/move_uv.py12
-rw-r--r--magic_uv/op/pack_uv.py130
-rw-r--r--magic_uv/op/preserve_uv_aspect.py12
-rw-r--r--magic_uv/op/select_uv.py24
-rw-r--r--magic_uv/op/smooth_uv.py16
-rw-r--r--magic_uv/op/texture_lock.py12
-rw-r--r--magic_uv/op/texture_projection.py12
-rw-r--r--magic_uv/op/texture_wrap.py12
-rw-r--r--magic_uv/op/transfer_uv.py12
-rw-r--r--magic_uv/op/unwrap_constraint.py14
-rw-r--r--magic_uv/op/uv_bounding_box.py16
-rw-r--r--magic_uv/op/uv_inspection.py20
-rw-r--r--magic_uv/op/uv_sculpt.py12
-rw-r--r--magic_uv/op/uvw.py12
-rw-r--r--magic_uv/op/world_scale_uv.py20
-rw-r--r--magic_uv/preferences.py7
-rw-r--r--magic_uv/properties.py4
-rw-r--r--magic_uv/ui/IMAGE_MT_uvs.py17
-rw-r--r--magic_uv/ui/VIEW3D_MT_object.py4
-rw-r--r--magic_uv/ui/VIEW3D_MT_uv_map.py4
-rw-r--r--magic_uv/ui/__init__.py4
-rw-r--r--magic_uv/ui/uvedit_copy_paste_uv.py21
-rw-r--r--magic_uv/ui/uvedit_editor_enhancement.py4
-rw-r--r--magic_uv/ui/uvedit_uv_manipulation.py12
-rw-r--r--magic_uv/ui/view3d_copy_paste_uv_editmode.py4
-rw-r--r--magic_uv/ui/view3d_copy_paste_uv_objectmode.py4
-rw-r--r--magic_uv/ui/view3d_uv_manipulation.py4
-rw-r--r--magic_uv/ui/view3d_uv_mapping.py4
-rw-r--r--magic_uv/utils/__init__.py6
-rw-r--r--magic_uv/utils/bl_class_registry.py4
-rw-r--r--magic_uv/utils/compatibility.py4
-rw-r--r--magic_uv/utils/graph.py152
-rw-r--r--magic_uv/utils/property_class_registry.py4
46 files changed, 890 insertions, 238 deletions
diff --git a/magic_uv/__init__.py b/magic_uv/__init__.py
index dc3c9641..88385107 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 4e633408..034936c5 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 76eaf480..bccf4e17 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 c1f696ab..044141b6 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 da77b17b..223ed004 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 9f606db9..cf5a49de 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 2b7f1491..696b7cb8 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 c7475543..e3815453 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 8ee83ad9..1496d67a 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 4e5d500a..af5df07c 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 7055915f..733c30b3 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 cb40ede8..c9a79420 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 dce3ca01..2f637535 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 76022d12..b99a6b15 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 5eaceaf4..2163d4f3 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 9d3cbdde..32e0103f 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 affc41e4..3e1f160c 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 020bc78f..b7c06c7c 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 f54c9453..94d91e57 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 912447c3..1288e9b7 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 4f9c868d..55e87812 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 029a5de3..4d332a55 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 da94c495..06b33c27 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 436f6679..07519c24 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 e974221e..cfbdbacf 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 24c76e13..d1cfc690 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 516a5da8..24588b82 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 9c617ee4..41f25852 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 27785ad9..d273f41f 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 21506915..127fc6f2 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 adad2fe9..0f94409b 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 29d5d607..e9349ee0 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 ee99ccba..544dabe4 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 883e966f..9b56e3ae 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 847b6e9a..95da5be4 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 b73e5eb9..0af607ec 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 636a0aca..e538ea4d 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 d0b52021..49bbaac3 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 1153bedd..2219d9e8 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 5e7ae28a..106ba2bc 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 1fd05dff..5da7cb6c 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 22ed284a..2ce7f34a 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 56ee91eb..10d63d24 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 cc8813e5..50ce304f 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 00000000..c8654773
--- /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 62cba903..e09ad320 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