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>2019-01-26 05:22:38 +0300
committerNutti <nutti.metro@gmail.com>2019-01-26 05:22:38 +0300
commitc034e1968465acb939efc089e63c5c51302947f5 (patch)
tree84f04a329e082ff4d7f860a926f8848cadb63428 /magic_uv
parent2eb519ceca77a4fe2fd5f8d071767db06aa01aa5 (diff)
Magic UV: Release v6.0
Support Blender 2.8.
Diffstat (limited to 'magic_uv')
-rw-r--r--magic_uv/__init__.py87
-rw-r--r--magic_uv/common.py1134
-rw-r--r--magic_uv/lib/__init__.py32
-rw-r--r--magic_uv/lib/bglx.py275
-rw-r--r--magic_uv/op/__init__.py74
-rw-r--r--magic_uv/op/align_uv.py991
-rw-r--r--magic_uv/op/align_uv_cursor.py269
-rw-r--r--magic_uv/op/copy_paste_uv.py755
-rw-r--r--magic_uv/op/copy_paste_uv_object.py306
-rw-r--r--magic_uv/op/copy_paste_uv_uvedit.py198
-rw-r--r--magic_uv/op/flip_rotate_uv.py232
-rw-r--r--magic_uv/op/mirror_uv.py215
-rw-r--r--magic_uv/op/move_uv.py185
-rw-r--r--magic_uv/op/pack_uv.py282
-rw-r--r--magic_uv/op/preserve_uv_aspect.py297
-rw-r--r--magic_uv/op/select_uv.py161
-rw-r--r--magic_uv/op/smooth_uv.py283
-rw-r--r--magic_uv/op/texture_lock.py537
-rw-r--r--magic_uv/op/texture_projection.py417
-rw-r--r--magic_uv/op/texture_wrap.py294
-rw-r--r--magic_uv/op/transfer_uv.py457
-rw-r--r--magic_uv/op/unwrap_constraint.py186
-rw-r--r--magic_uv/op/uv_bounding_box.py842
-rw-r--r--magic_uv/op/uv_inspection.py281
-rw-r--r--magic_uv/op/uv_sculpt.py487
-rw-r--r--magic_uv/op/uvw.py304
-rw-r--r--magic_uv/op/world_scale_uv.py649
-rw-r--r--magic_uv/preferences.py488
-rw-r--r--magic_uv/properites.py43
-rw-r--r--magic_uv/ui/IMAGE_MT_uvs.py188
-rw-r--r--magic_uv/ui/VIEW3D_MT_object.py49
-rw-r--r--magic_uv/ui/VIEW3D_MT_uv_map.py254
-rw-r--r--magic_uv/ui/__init__.py50
-rw-r--r--magic_uv/ui/uvedit_copy_paste_uv.py59
-rw-r--r--magic_uv/ui/uvedit_editor_enhancement.py146
-rw-r--r--magic_uv/ui/uvedit_uv_manipulation.py132
-rw-r--r--magic_uv/ui/view3d_copy_paste_uv_editmode.py92
-rw-r--r--magic_uv/ui/view3d_copy_paste_uv_objectmode.py62
-rw-r--r--magic_uv/ui/view3d_uv_manipulation.py282
-rw-r--r--magic_uv/ui/view3d_uv_mapping.py114
-rw-r--r--magic_uv/updater.py141
-rw-r--r--magic_uv/utils/__init__.py38
-rw-r--r--magic_uv/utils/addon_updator.py360
-rw-r--r--magic_uv/utils/bl_class_registry.py80
-rw-r--r--magic_uv/utils/compatibility.py189
-rw-r--r--magic_uv/utils/property_class_registry.py68
46 files changed, 13065 insertions, 0 deletions
diff --git a/magic_uv/__init__.py b/magic_uv/__init__.py
new file mode 100644
index 00000000..5bac5fd2
--- /dev/null
+++ b/magic_uv/__init__.py
@@ -0,0 +1,87 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+
+bl_info = {
+ "name": "Magic UV",
+ "author": "Nutti, Mifth, Jace Priester, kgeogeo, mem, imdjs"
+ "Keith (Wahooney) Boshoff, McBuff, MaxRobinot, "
+ "Alexander Milovsky",
+ "version": (6, 0, 0),
+ "blender": (2, 80, 0),
+ "location": "See Add-ons Preferences",
+ "description": "UV Toolset. See Add-ons Preferences for details",
+ "warning": "",
+ "support": "COMMUNITY",
+ "wiki_url": "https://wiki.blender.org/index.php/Extensions:2.6/"
+ "Py/Scripts/UV/Magic_UV",
+ "tracker_url": "https://github.com/nutti/Magic-UV",
+ "category": "UV"
+}
+
+
+if "bpy" in locals():
+ import importlib
+ importlib.reload(common)
+ importlib.reload(utils)
+ utils.bl_class_registry.BlClassRegistry.cleanup()
+ importlib.reload(op)
+ importlib.reload(ui)
+ importlib.reload(properites)
+ importlib.reload(preferences)
+ importlib.reload(updater)
+else:
+ import bpy
+ from . import common
+ from . import utils
+ from . import op
+ from . import ui
+ from . import properites
+ from . import preferences
+ from . import updater
+
+import bpy
+
+
+def register():
+ updater.register_updater(bl_info)
+
+ utils.bl_class_registry.BlClassRegistry.register()
+ properites.init_props(bpy.types.Scene)
+ user_prefs = utils.compatibility.get_user_preferences(bpy.context)
+ if user_prefs.addons['magic_uv'].preferences.enable_builtin_menu:
+ preferences.add_builtin_menu()
+
+
+def unregister():
+ user_prefs = utils.compatibility.get_user_preferences(bpy.context)
+ if user_prefs.addons['magic_uv'].preferences.enable_builtin_menu:
+ preferences.remove_builtin_menu()
+ properites.clear_props(bpy.types.Scene)
+ utils.bl_class_registry.BlClassRegistry.unregister()
+
+
+if __name__ == "__main__":
+ register()
diff --git a/magic_uv/common.py b/magic_uv/common.py
new file mode 100644
index 00000000..78a88308
--- /dev/null
+++ b/magic_uv/common.py
@@ -0,0 +1,1134 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+from collections import defaultdict
+from pprint import pprint
+from math import fabs, sqrt
+import os
+
+import bpy
+from mathutils import Vector
+import bmesh
+
+from .utils import compatibility as compat
+
+
+__DEBUG_MODE = False
+
+
+def is_console_mode():
+ if "MUV_CONSOLE_MODE" not in os.environ:
+ return False
+ return os.environ["MUV_CONSOLE_MODE"] == "True"
+
+
+def is_debug_mode():
+ return __DEBUG_MODE
+
+
+def enable_debugg_mode():
+ # pylint: disable=W0603
+ global __DEBUG_MODE
+ __DEBUG_MODE = True
+
+
+def disable_debug_mode():
+ # pylint: disable=W0603
+ global __DEBUG_MODE
+ __DEBUG_MODE = False
+
+
+def debug_print(*s):
+ """
+ Print message to console in debugging mode
+ """
+
+ if is_debug_mode():
+ pprint(s)
+
+
+def check_version(major, minor, _):
+ """
+ Check blender version
+ """
+
+ if bpy.app.version[0] == major and bpy.app.version[1] == minor:
+ return 0
+ if bpy.app.version[0] > major:
+ return 1
+ if bpy.app.version[1] > minor:
+ return 1
+ return -1
+
+
+def redraw_all_areas():
+ """
+ Redraw all areas
+ """
+
+ for area in bpy.context.screen.areas:
+ area.tag_redraw()
+
+
+def get_space(area_type, region_type, space_type):
+ """
+ Get current area/region/space
+ """
+
+ area = None
+ region = None
+ space = None
+
+ for area in bpy.context.screen.areas:
+ if area.type == area_type:
+ break
+ else:
+ return (None, None, None)
+ for region in area.regions:
+ if region.type == region_type:
+ if compat.check_version(2, 80, 0) >= 0:
+ if region.width <= 1 or region.height <= 1:
+ continue
+ break
+ else:
+ return (area, None, None)
+ for space in area.spaces:
+ if space.type == space_type:
+ break
+ else:
+ return (area, region, None)
+
+ return (area, region, space)
+
+
+def mouse_on_region(event, area_type, region_type):
+ pos = Vector((event.mouse_x, event.mouse_y))
+
+ _, region, _ = get_space(area_type, region_type, "")
+ if region is None:
+ return False
+
+ if (pos.x > region.x) and (pos.x < region.x + region.width) and \
+ (pos.y > region.y) and (pos.y < region.y + region.height):
+ return True
+
+ return False
+
+
+def mouse_on_area(event, area_type):
+ pos = Vector((event.mouse_x, event.mouse_y))
+
+ area, _, _ = get_space(area_type, "", "")
+ if area is None:
+ return False
+
+ if (pos.x > area.x) and (pos.x < area.x + area.width) and \
+ (pos.y > area.y) and (pos.y < area.y + area.height):
+ return True
+
+ return False
+
+
+def mouse_on_regions(event, area_type, regions):
+ if not mouse_on_area(event, area_type):
+ return False
+
+ for region in regions:
+ result = mouse_on_region(event, area_type, region)
+ if result:
+ return True
+
+ return False
+
+
+def create_bmesh(obj):
+ bm = bmesh.from_edit_mesh(obj.data)
+ if check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ return bm
+
+
+def create_new_uv_map(obj, name=None):
+ uv_maps_old = {l.name for l in obj.data.uv_layers}
+ bpy.ops.mesh.uv_texture_add()
+ uv_maps_new = {l.name for l in obj.data.uv_layers}
+ diff = uv_maps_new - uv_maps_old
+
+ if not list(diff):
+ return None # no more UV maps can not be created
+
+ # rename UV map
+ new = obj.data.uv_layers[list(diff)[0]]
+ if name:
+ new.name = name
+
+ return new
+
+
+def __get_island_info(uv_layer, islands):
+ """
+ get information about each island
+ """
+
+ island_info = []
+ for isl in islands:
+ info = {}
+ max_uv = Vector((-10000000.0, -10000000.0))
+ min_uv = Vector((10000000.0, 10000000.0))
+ ave_uv = Vector((0.0, 0.0))
+ num_uv = 0
+ for face in isl:
+ n = 0
+ a = Vector((0.0, 0.0))
+ ma = Vector((-10000000.0, -10000000.0))
+ mi = Vector((10000000.0, 10000000.0))
+ for l in face['face'].loops:
+ uv = l[uv_layer].uv
+ ma.x = max(uv.x, ma.x)
+ ma.y = max(uv.y, ma.y)
+ mi.x = min(uv.x, mi.x)
+ mi.y = min(uv.y, mi.y)
+ a = a + uv
+ n = n + 1
+ ave_uv = ave_uv + a
+ num_uv = num_uv + n
+ a = a / n
+ max_uv.x = max(ma.x, max_uv.x)
+ max_uv.y = max(ma.y, max_uv.y)
+ min_uv.x = min(mi.x, min_uv.x)
+ min_uv.y = min(mi.y, min_uv.y)
+ face['max_uv'] = ma
+ face['min_uv'] = mi
+ face['ave_uv'] = a
+ ave_uv = ave_uv / num_uv
+
+ info['center'] = ave_uv
+ info['size'] = max_uv - min_uv
+ info['num_uv'] = num_uv
+ info['group'] = -1
+ info['faces'] = isl
+ info['max'] = max_uv
+ info['min'] = min_uv
+
+ island_info.append(info)
+
+ return island_info
+
+
+def __parse_island(bm, face_idx, faces_left, island,
+ face_to_verts, vert_to_faces):
+ """
+ Parse island
+ """
+
+ if face_idx in faces_left:
+ faces_left.remove(face_idx)
+ island.append({'face': bm.faces[face_idx]})
+ for v in face_to_verts[face_idx]:
+ connected_faces = vert_to_faces[v]
+ if connected_faces:
+ for cf in connected_faces:
+ __parse_island(bm, cf, faces_left, island, face_to_verts,
+ vert_to_faces)
+
+
+def __get_island(bm, face_to_verts, vert_to_faces):
+ """
+ Get island list
+ """
+
+ uv_island_lists = []
+ faces_left = set(face_to_verts.keys())
+ while faces_left:
+ current_island = []
+ face_idx = list(faces_left)[0]
+ __parse_island(bm, face_idx, faces_left, current_island,
+ face_to_verts, vert_to_faces)
+ uv_island_lists.append(current_island)
+
+ return uv_island_lists
+
+
+def __create_vert_face_db(faces, uv_layer):
+ # create mesh database for all faces
+ face_to_verts = defaultdict(set)
+ vert_to_faces = defaultdict(set)
+ for f in faces:
+ for l in f.loops:
+ id_ = l[uv_layer].uv.to_tuple(5), l.vert.index
+ face_to_verts[f.index].add(id_)
+ vert_to_faces[id_].add(f.index)
+
+ return (face_to_verts, vert_to_faces)
+
+
+def get_island_info(obj, only_selected=True):
+ bm = bmesh.from_edit_mesh(obj.data)
+ if check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ return get_island_info_from_bmesh(bm, only_selected)
+
+
+def get_island_info_from_bmesh(bm, only_selected=True):
+ if not bm.loops.layers.uv:
+ return None
+ uv_layer = bm.loops.layers.uv.verify()
+
+ # create database
+ if only_selected:
+ selected_faces = [f for f in bm.faces if f.select]
+ else:
+ selected_faces = [f for f in bm.faces]
+
+ return get_island_info_from_faces(bm, selected_faces, uv_layer)
+
+
+def get_island_info_from_faces(bm, faces, uv_layer):
+ ftv, vtf = __create_vert_face_db(faces, uv_layer)
+
+ # Get island information
+ uv_island_lists = __get_island(bm, ftv, vtf)
+ island_info = __get_island_info(uv_layer, uv_island_lists)
+
+ return island_info
+
+
+def get_uvimg_editor_board_size(area):
+ if area.spaces.active.image:
+ return area.spaces.active.image.size
+
+ return (255.0, 255.0)
+
+
+def calc_polygon_2d_area(points):
+ area = 0.0
+ for i, p1 in enumerate(points):
+ p2 = points[(i + 1) % len(points)]
+ v1 = p1 - points[0]
+ v2 = p2 - points[0]
+ a = v1.x * v2.y - v1.y * v2.x
+ area = area + a
+
+ return fabs(0.5 * area)
+
+
+def calc_polygon_3d_area(points):
+ area = 0.0
+ for i, p1 in enumerate(points):
+ p2 = points[(i + 1) % len(points)]
+ v1 = p1 - points[0]
+ v2 = p2 - points[0]
+ cx = v1.y * v2.z - v1.z * v2.y
+ cy = v1.z * v2.x - v1.x * v2.z
+ cz = v1.x * v2.y - v1.y * v2.x
+ a = sqrt(cx * cx + cy * cy + cz * cz)
+ area = area + a
+
+ return 0.5 * area
+
+
+def measure_mesh_area(obj):
+ bm = bmesh.from_edit_mesh(obj.data)
+ if check_version(2, 73, 0) >= 0:
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ bm.faces.ensure_lookup_table()
+
+ sel_faces = [f for f in bm.faces if f.select]
+
+ # measure
+ mesh_area = 0.0
+ for f in sel_faces:
+ verts = [l.vert.co for l in f.loops]
+ f_mesh_area = calc_polygon_3d_area(verts)
+ mesh_area = mesh_area + f_mesh_area
+
+ return mesh_area
+
+
+def find_texture_layer(bm):
+ if check_version(2, 80, 0) >= 0:
+ return None
+ if bm.faces.layers.tex is None:
+ return None
+
+ return bm.faces.layers.tex.verify()
+
+
+def find_texture_nodes(obj):
+ nodes = []
+ for mat in obj.material_slots:
+ if not mat.material.node_tree:
+ continue
+ for node in mat.material.node_tree.nodes:
+ tex_node_types = [
+ 'TEX_ENVIRONMENT',
+ 'TEX_IMAGE',
+ ]
+ if node.type not in tex_node_types:
+ continue
+ if not node.image:
+ continue
+ nodes.append(node)
+
+ return nodes
+
+
+def find_image(obj, face=None, tex_layer=None):
+ # try to find from texture_layer
+ img = None
+ if tex_layer and face:
+ img = face[tex_layer].image
+
+ # not found, then try to search from node
+ if not img:
+ nodes = find_texture_nodes(obj)
+ if len(nodes) >= 2:
+ raise RuntimeError("Find more than 2 texture nodes")
+ if len(nodes) == 1:
+ img = nodes[0].image
+
+ return img
+
+
+def measure_uv_area(obj, tex_size=None):
+ bm = bmesh.from_edit_mesh(obj.data)
+ if check_version(2, 73, 0) >= 0:
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ bm.faces.ensure_lookup_table()
+
+ if not bm.loops.layers.uv:
+ return None
+ uv_layer = bm.loops.layers.uv.verify()
+
+ tex_layer = find_texture_layer(bm)
+
+ sel_faces = [f for f in bm.faces if f.select]
+
+ # measure
+ uv_area = 0.0
+ for f in sel_faces:
+ uvs = [l[uv_layer].uv for l in f.loops]
+ f_uv_area = calc_polygon_2d_area(uvs)
+
+ # user specified
+ if tex_size:
+ uv_area = uv_area + f_uv_area * tex_size[0] * tex_size[1]
+ continue
+
+ img = find_image(obj, f, tex_layer)
+
+ # can not find from node, so we can not get texture size
+ if not img:
+ return None
+
+ img_size = img.size
+ uv_area = uv_area + f_uv_area * img_size[0] * img_size[1]
+
+ return uv_area
+
+
+def diff_point_to_segment(a, b, p):
+ ab = b - a
+ normal_ab = ab.normalized()
+
+ ap = p - a
+ dist_ax = normal_ab.dot(ap)
+
+ # cross point
+ x = a + normal_ab * dist_ax
+
+ # difference between cross point and point
+ xp = p - x
+
+ return xp, x
+
+
+# get selected loop pair whose loops are connected each other
+def __get_loop_pairs(l, uv_layer):
+
+ def __get_loop_pairs_internal(l_, pairs_, uv_layer_, parsed_):
+ parsed_.append(l_)
+ for ll in l_.vert.link_loops:
+ # forward direction
+ lln = ll.link_loop_next
+ # if there is same pair, skip it
+ found = False
+ for p in pairs_:
+ if (ll in p) and (lln in p):
+ found = True
+ break
+ # two loops must be selected
+ if ll[uv_layer_].select and lln[uv_layer_].select:
+ if not found:
+ pairs_.append([ll, lln])
+ if lln not in parsed_:
+ __get_loop_pairs_internal(lln, pairs_, uv_layer_, parsed_)
+
+ # backward direction
+ llp = ll.link_loop_prev
+ # if there is same pair, skip it
+ found = False
+ for p in pairs_:
+ if (ll in p) and (llp in p):
+ found = True
+ break
+ # two loops must be selected
+ if ll[uv_layer_].select and llp[uv_layer_].select:
+ if not found:
+ pairs_.append([ll, llp])
+ if llp not in parsed_:
+ __get_loop_pairs_internal(llp, pairs_, uv_layer_, parsed_)
+
+ pairs = []
+ parsed = []
+ __get_loop_pairs_internal(l, pairs, uv_layer, parsed)
+
+ return pairs
+
+
+# sort pair by vertex
+# (v0, v1) - (v1, v2) - (v2, v3) ....
+def __sort_loop_pairs(uv_layer, pairs, closed):
+ rest = pairs
+ sorted_pairs = [rest[0]]
+ rest.remove(rest[0])
+
+ # prepend
+ while True:
+ p1 = sorted_pairs[0]
+ for p2 in rest:
+ if p1[0].vert == p2[0].vert:
+ sorted_pairs.insert(0, [p2[1], p2[0]])
+ rest.remove(p2)
+ break
+ elif p1[0].vert == p2[1].vert:
+ sorted_pairs.insert(0, [p2[0], p2[1]])
+ rest.remove(p2)
+ break
+ else:
+ break
+
+ # append
+ while True:
+ p1 = sorted_pairs[-1]
+ for p2 in rest:
+ if p1[1].vert == p2[0].vert:
+ sorted_pairs.append([p2[0], p2[1]])
+ rest.remove(p2)
+ break
+ elif p1[1].vert == p2[1].vert:
+ sorted_pairs.append([p2[1], p2[0]])
+ rest.remove(p2)
+ break
+ else:
+ break
+
+ begin_vert = sorted_pairs[0][0].vert
+ end_vert = sorted_pairs[-1][-1].vert
+ if begin_vert != end_vert:
+ return sorted_pairs, ""
+ if closed and (begin_vert == end_vert):
+ # if the sequence of UV is circular, it is ok
+ return sorted_pairs, ""
+
+ # if the begin vertex and the end vertex are same, search the UVs which
+ # are separated each other
+ tmp_pairs = sorted_pairs
+ for i, (p1, p2) in enumerate(zip(tmp_pairs[:-1], tmp_pairs[1:])):
+ diff = p2[0][uv_layer].uv - p1[-1][uv_layer].uv
+ if diff.length > 0.000000001:
+ # UVs are separated
+ sorted_pairs = tmp_pairs[i + 1:]
+ sorted_pairs.extend(tmp_pairs[:i + 1])
+ break
+ else:
+ p1 = tmp_pairs[0]
+ p2 = tmp_pairs[-1]
+ diff = p2[-1][uv_layer].uv - p1[0][uv_layer].uv
+ if diff.length < 0.000000001:
+ # all UVs are not separated
+ return None, "All UVs are not separated"
+
+ return sorted_pairs, ""
+
+
+# get index of the island group which includes loop
+def __get_island_group_include_loop(loop, island_info):
+ for i, isl in enumerate(island_info):
+ for f in isl['faces']:
+ for l in f['face'].loops:
+ if l == loop:
+ return i # found
+
+ return -1 # not found
+
+
+# get index of the island group which includes pair.
+# if island group is not same between loops, it will be invalid
+def __get_island_group_include_pair(pair, island_info):
+ l1_grp = __get_island_group_include_loop(pair[0], island_info)
+ if l1_grp == -1:
+ return -1 # not found
+
+ for p in pair[1:]:
+ l2_grp = __get_island_group_include_loop(p, island_info)
+ if (l2_grp == -1) or (l1_grp != l2_grp):
+ return -1 # not found or invalid
+
+ return l1_grp
+
+
+# x ---- x <- next_loop_pair
+# | |
+# o ---- o <- pair
+def __get_next_loop_pair(pair):
+ lp = pair[0].link_loop_prev
+ if lp.vert == pair[1].vert:
+ lp = pair[0].link_loop_next
+ if lp.vert == pair[1].vert:
+ # no loop is found
+ return None
+
+ ln = pair[1].link_loop_next
+ if ln.vert == pair[0].vert:
+ ln = pair[1].link_loop_prev
+ if ln.vert == pair[0].vert:
+ # no loop is found
+ return None
+
+ # tri-face
+ if lp == ln:
+ return [lp]
+
+ # quad-face
+ return [lp, ln]
+
+
+# | ---- |
+# % ---- % <- next_poly_loop_pair
+# x ---- x <- next_loop_pair
+# | |
+# o ---- o <- pair
+def __get_next_poly_loop_pair(pair):
+ v1 = pair[0].vert
+ v2 = pair[1].vert
+ for l1 in v1.link_loops:
+ if l1 == pair[0]:
+ continue
+ for l2 in v2.link_loops:
+ if l2 == pair[1]:
+ continue
+ if l1.link_loop_next == l2:
+ return [l1, l2]
+ elif l1.link_loop_prev == l2:
+ return [l1, l2]
+
+ # no next poly loop is found
+ return None
+
+
+# get loop sequence in the same island
+def __get_loop_sequence_internal(uv_layer, pairs, island_info, closed):
+ loop_sequences = []
+ for pair in pairs:
+ seqs = [pair]
+ p = pair
+ isl_grp = __get_island_group_include_pair(pair, island_info)
+ if isl_grp == -1:
+ return None, "Can not find the island or invalid island"
+
+ while True:
+ nlp = __get_next_loop_pair(p)
+ if not nlp:
+ break # no more loop pair
+ nlp_isl_grp = __get_island_group_include_pair(nlp, island_info)
+ if nlp_isl_grp != isl_grp:
+ break # another island
+ for nlpl in nlp:
+ if nlpl[uv_layer].select:
+ return None, "Do not select UV which does not belong to " \
+ "the end edge"
+
+ seqs.append(nlp)
+
+ # when face is triangle, it indicates CLOSED
+ if (len(nlp) == 1) and closed:
+ break
+
+ nplp = __get_next_poly_loop_pair(nlp)
+ if not nplp:
+ break # no more loop pair
+ nplp_isl_grp = __get_island_group_include_pair(nplp, island_info)
+ if nplp_isl_grp != isl_grp:
+ break # another island
+
+ # check if the UVs are already parsed.
+ # this check is needed for the mesh which has the circular
+ # sequence of the vertices
+ matched = False
+ for p1 in seqs:
+ p2 = nplp
+ if ((p1[0] == p2[0]) and (p1[1] == p2[1])) or \
+ ((p1[0] == p2[1]) and (p1[1] == p2[0])):
+ matched = True
+ if matched:
+ debug_print("This is a circular sequence")
+ break
+
+ for nlpl in nplp:
+ if nlpl[uv_layer].select:
+ return None, "Do not select UV which does not belong to " \
+ "the end edge"
+
+ seqs.append(nplp)
+
+ p = nplp
+
+ loop_sequences.append(seqs)
+ return loop_sequences, ""
+
+
+def get_loop_sequences(bm, uv_layer, closed=False):
+ sel_faces = [f for f in bm.faces if f.select]
+
+ # get candidate loops
+ cand_loops = []
+ for f in sel_faces:
+ for l in f.loops:
+ if l[uv_layer].select:
+ cand_loops.append(l)
+
+ if len(cand_loops) < 2:
+ return None, "More than 2 UVs must be selected"
+
+ first_loop = cand_loops[0]
+ isl_info = get_island_info_from_bmesh(bm, False)
+ loop_pairs = __get_loop_pairs(first_loop, uv_layer)
+ loop_pairs, err = __sort_loop_pairs(uv_layer, loop_pairs, closed)
+ if not loop_pairs:
+ return None, err
+ loop_seqs, err = __get_loop_sequence_internal(uv_layer, loop_pairs,
+ isl_info, closed)
+ if not loop_seqs:
+ return None, err
+
+ return loop_seqs, ""
+
+
+def __is_segment_intersect(start1, end1, start2, end2):
+ seg1 = end1 - start1
+ seg2 = end2 - start2
+
+ a1 = -seg1.y
+ b1 = seg1.x
+ d1 = -(a1 * start1.x + b1 * start1.y)
+
+ a2 = -seg2.y
+ b2 = seg2.x
+ d2 = -(a2 * start2.x + b2 * start2.y)
+
+ seg1_line2_start = a2 * start1.x + b2 * start1.y + d2
+ seg1_line2_end = a2 * end1.x + b2 * end1.y + d2
+
+ seg2_line1_start = a1 * start2.x + b1 * start2.y + d1
+ seg2_line1_end = a1 * end2.x + b1 * end2.y + d1
+
+ if (seg1_line2_start * seg1_line2_end >= 0) or \
+ (seg2_line1_start * seg2_line1_end >= 0):
+ return False, None
+
+ u = seg1_line2_start / (seg1_line2_start - seg1_line2_end)
+ out = start1 + u * seg1
+
+ return True, out
+
+
+class RingBuffer:
+ def __init__(self, arr):
+ self.__buffer = arr.copy()
+ self.__pointer = 0
+
+ def __repr__(self):
+ return repr(self.__buffer)
+
+ def __len__(self):
+ return len(self.__buffer)
+
+ def insert(self, val, offset=0):
+ self.__buffer.insert(self.__pointer + offset, val)
+
+ def head(self):
+ return self.__buffer[0]
+
+ def tail(self):
+ return self.__buffer[-1]
+
+ def get(self, offset=0):
+ size = len(self.__buffer)
+ val = self.__buffer[(self.__pointer + offset) % size]
+ return val
+
+ def next(self):
+ size = len(self.__buffer)
+ self.__pointer = (self.__pointer + 1) % size
+
+ def reset(self):
+ self.__pointer = 0
+
+ def find(self, obj):
+ try:
+ idx = self.__buffer.index(obj)
+ except ValueError:
+ return None
+ return self.__buffer[idx]
+
+ def find_and_next(self, obj):
+ size = len(self.__buffer)
+ idx = self.__buffer.index(obj)
+ self.__pointer = (idx + 1) % size
+
+ def find_and_set(self, obj):
+ idx = self.__buffer.index(obj)
+ self.__pointer = idx
+
+ def as_list(self):
+ return self.__buffer.copy()
+
+ def reverse(self):
+ self.__buffer.reverse()
+ self.reset()
+
+
+# clip: reference polygon
+# subject: tested polygon
+def __do_weiler_atherton_cliping(clip, subject, uv_layer, mode):
+
+ clip_uvs = RingBuffer([l[uv_layer].uv.copy() for l in clip.loops])
+ if __is_polygon_flipped(clip_uvs):
+ clip_uvs.reverse()
+ subject_uvs = RingBuffer([l[uv_layer].uv.copy() for l in subject.loops])
+ if __is_polygon_flipped(subject_uvs):
+ subject_uvs.reverse()
+
+ debug_print("===== Clip UV List =====")
+ debug_print(clip_uvs)
+ debug_print("===== Subject UV List =====")
+ debug_print(subject_uvs)
+
+ # check if clip and subject is overlapped completely
+ if __is_polygon_same(clip_uvs, subject_uvs):
+ polygons = [subject_uvs.as_list()]
+ debug_print("===== Polygons Overlapped Completely =====")
+ debug_print(polygons)
+ return True, polygons
+
+ # check if subject is in clip
+ if __is_points_in_polygon(subject_uvs, clip_uvs):
+ polygons = [subject_uvs.as_list()]
+ return True, polygons
+
+ # check if clip is in subject
+ if __is_points_in_polygon(clip_uvs, subject_uvs):
+ polygons = [subject_uvs.as_list()]
+ return True, polygons
+
+ # check if clip and subject is overlapped partially
+ intersections = []
+ while True:
+ subject_uvs.reset()
+ while True:
+ uv_start1 = clip_uvs.get()
+ uv_end1 = clip_uvs.get(1)
+ uv_start2 = subject_uvs.get()
+ uv_end2 = subject_uvs.get(1)
+ intersected, point = __is_segment_intersect(uv_start1, uv_end1,
+ uv_start2, uv_end2)
+ if intersected:
+ clip_uvs.insert(point, 1)
+ subject_uvs.insert(point, 1)
+ intersections.append([point,
+ [clip_uvs.get(), clip_uvs.get(1)]])
+ subject_uvs.next()
+ if subject_uvs.get() == subject_uvs.head():
+ break
+ clip_uvs.next()
+ if clip_uvs.get() == clip_uvs.head():
+ break
+
+ debug_print("===== Intersection List =====")
+ debug_print(intersections)
+
+ # no intersection, so subject and clip is not overlapped
+ if not intersections:
+ return False, None
+
+ def get_intersection_pair(intersects, key):
+ for sect in intersects:
+ if sect[0] == key:
+ return sect[1]
+
+ return None
+
+ # make enter/exit pair
+ subject_uvs.reset()
+ subject_entering = []
+ subject_exiting = []
+ clip_entering = []
+ clip_exiting = []
+ intersect_uv_list = []
+ while True:
+ pair = get_intersection_pair(intersections, subject_uvs.get())
+ if pair:
+ sub = subject_uvs.get(1) - subject_uvs.get(-1)
+ inter = pair[1] - pair[0]
+ cross = sub.x * inter.y - inter.x * sub.y
+ if cross < 0:
+ subject_entering.append(subject_uvs.get())
+ clip_exiting.append(subject_uvs.get())
+ else:
+ subject_exiting.append(subject_uvs.get())
+ clip_entering.append(subject_uvs.get())
+ intersect_uv_list.append(subject_uvs.get())
+
+ subject_uvs.next()
+ if subject_uvs.get() == subject_uvs.head():
+ break
+
+ debug_print("===== Enter List =====")
+ debug_print(clip_entering)
+ debug_print(subject_entering)
+ debug_print("===== Exit List =====")
+ debug_print(clip_exiting)
+ debug_print(subject_exiting)
+
+ # for now, can't handle the situation when fulfill all below conditions
+ # * two faces have common edge
+ # * each face is intersected
+ # * Show Mode is "Part"
+ # so for now, ignore this situation
+ if len(subject_entering) != len(subject_exiting):
+ if mode == 'FACE':
+ polygons = [subject_uvs.as_list()]
+ return True, polygons
+ return False, None
+
+ def traverse(current_list, entering, exiting, p, current, other_list):
+ result = current_list.find(current)
+ if not result:
+ return None
+ if result != current:
+ print("Internal Error")
+ return None
+ if not exiting:
+ print("Internal Error: No exiting UV")
+ return None
+
+ # enter
+ if entering.count(current) >= 1:
+ entering.remove(current)
+
+ current_list.find_and_next(current)
+ current = current_list.get()
+
+ prev = None
+ error = False
+ while exiting.count(current) == 0:
+ p.append(current.copy())
+ current_list.find_and_next(current)
+ current = current_list.get()
+ if prev == current:
+ error = True
+ break
+ prev = current
+
+ if error:
+ print("Internal Error: Infinite loop")
+ return None
+
+ # exit
+ p.append(current.copy())
+ exiting.remove(current)
+
+ other_list.find_and_set(current)
+ return other_list.get()
+
+ # Traverse
+ polygons = []
+ current_uv_list = subject_uvs
+ other_uv_list = clip_uvs
+ current_entering = subject_entering
+ current_exiting = subject_exiting
+
+ poly = []
+ current_uv = current_entering[0]
+
+ while True:
+ current_uv = traverse(current_uv_list, current_entering,
+ current_exiting, poly, current_uv, other_uv_list)
+
+ if current_uv is None:
+ break
+
+ if current_uv_list == subject_uvs:
+ current_uv_list = clip_uvs
+ other_uv_list = subject_uvs
+ current_entering = clip_entering
+ current_exiting = clip_exiting
+ debug_print("-- Next: Clip --")
+ else:
+ current_uv_list = subject_uvs
+ other_uv_list = clip_uvs
+ current_entering = subject_entering
+ current_exiting = subject_exiting
+ debug_print("-- Next: Subject --")
+
+ debug_print(clip_entering)
+ debug_print(clip_exiting)
+ debug_print(subject_entering)
+ debug_print(subject_exiting)
+
+ if not clip_entering and not clip_exiting \
+ and not subject_entering and not subject_exiting:
+ break
+
+ polygons.append(poly)
+
+ debug_print("===== Polygons Overlapped Partially =====")
+ debug_print(polygons)
+
+ return True, polygons
+
+
+def __is_polygon_flipped(points):
+ area = 0.0
+ for i in range(len(points)):
+ uv1 = points.get(i)
+ uv2 = points.get(i + 1)
+ a = uv1.x * uv2.y - uv1.y * uv2.x
+ area = area + a
+ if area < 0:
+ # clock-wise
+ return True
+ return False
+
+
+def __is_point_in_polygon(point, subject_points):
+ count = 0
+ for i in range(len(subject_points)):
+ uv_start1 = subject_points.get(i)
+ uv_end1 = subject_points.get(i + 1)
+ uv_start2 = point
+ uv_end2 = Vector((1000000.0, point.y))
+ intersected, _ = __is_segment_intersect(uv_start1, uv_end1,
+ uv_start2, uv_end2)
+ if intersected:
+ count = count + 1
+
+ return count % 2
+
+
+def __is_points_in_polygon(points, subject_points):
+ for i in range(len(points)):
+ internal = __is_point_in_polygon(points.get(i), subject_points)
+ if not internal:
+ return False
+
+ return True
+
+
+def get_overlapped_uv_info(bm, faces, uv_layer, mode):
+ # at first, check island overlapped
+ isl = get_island_info_from_faces(bm, faces, uv_layer)
+ overlapped_isl_pairs = []
+ for i, i1 in enumerate(isl):
+ for i2 in isl[i + 1:]:
+ if (i1["max"].x < i2["min"].x) or (i2["max"].x < i1["min"].x) or \
+ (i1["max"].y < i2["min"].y) or (i2["max"].y < i1["min"].y):
+ continue
+ overlapped_isl_pairs.append([i1, i2])
+
+ # next, check polygon overlapped
+ overlapped_uvs = []
+ for oip in overlapped_isl_pairs:
+ for clip in oip[0]["faces"]:
+ f_clip = clip["face"]
+ for subject in oip[1]["faces"]:
+ f_subject = subject["face"]
+
+ # fast operation, apply bounding box algorithm
+ if (clip["max_uv"].x < subject["min_uv"].x) or \
+ (subject["max_uv"].x < clip["min_uv"].x) or \
+ (clip["max_uv"].y < subject["min_uv"].y) or \
+ (subject["max_uv"].y < clip["min_uv"].y):
+ continue
+
+ # slow operation, apply Weiler-Atherton cliping algorithm
+ result, polygons = __do_weiler_atherton_cliping(f_clip,
+ f_subject,
+ uv_layer, mode)
+ if result:
+ subject_uvs = [l[uv_layer].uv.copy()
+ for l in f_subject.loops]
+ overlapped_uvs.append({"clip_face": f_clip,
+ "subject_face": f_subject,
+ "subject_uvs": subject_uvs,
+ "polygons": polygons})
+
+ return overlapped_uvs
+
+
+def get_flipped_uv_info(faces, uv_layer):
+ flipped_uvs = []
+ for f in faces:
+ polygon = RingBuffer([l[uv_layer].uv.copy() for l in f.loops])
+ if __is_polygon_flipped(polygon):
+ uvs = [l[uv_layer].uv.copy() for l in f.loops]
+ flipped_uvs.append({"face": f, "uvs": uvs,
+ "polygons": [polygon.as_list()]})
+
+ return flipped_uvs
+
+
+def __is_polygon_same(points1, points2):
+ if len(points1) != len(points2):
+ return False
+
+ pts1 = points1.as_list()
+ pts2 = points2.as_list()
+
+ for p1 in pts1:
+ for p2 in pts2:
+ diff = p2 - p1
+ if diff.length < 0.0000001:
+ pts2.remove(p2)
+ break
+ else:
+ return False
+
+ return True
diff --git a/magic_uv/lib/__init__.py b/magic_uv/lib/__init__.py
new file mode 100644
index 00000000..db6f9df9
--- /dev/null
+++ b/magic_uv/lib/__init__.py
@@ -0,0 +1,32 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+if "bpy" in locals():
+ import importlib
+ importlib.reload(bglx)
+else:
+ from . import bglx
+
+import bpy
diff --git a/magic_uv/lib/bglx.py b/magic_uv/lib/bglx.py
new file mode 100644
index 00000000..5abde12c
--- /dev/null
+++ b/magic_uv/lib/bglx.py
@@ -0,0 +1,275 @@
+from threading import Lock
+
+import bgl
+from bgl import Buffer as Buffer
+import gpu
+from gpu_extras.batch import batch_for_shader
+
+GL_LINES = 0
+GL_LINE_STRIP = 1
+GL_LINE_LOOP = 2
+GL_TRIANGLES = 5
+GL_TRIANGLE_FAN = 6
+GL_QUADS = 4
+
+class InternalData:
+ __inst = None
+ __lock = Lock()
+
+ def __init__(self):
+ raise NotImplementedError("Not allowed to call constructor")
+
+ @classmethod
+ def __internal_new(cls):
+ inst = super().__new__(cls)
+ inst.color = [1.0, 1.0, 1.0, 1.0]
+ inst.line_width = 1.0
+
+ return inst
+
+ @classmethod
+ def get_instance(cls):
+ if not cls.__inst:
+ with cls.__lock:
+ if not cls.__inst:
+ cls.__inst = cls.__internal_new()
+
+ return cls.__inst
+
+ def init(self):
+ self.clear()
+
+ def set_prim_mode(self, mode):
+ self.prim_mode = mode
+
+ def set_dims(self, dims):
+ self.dims = dims
+
+ def add_vert(self, v):
+ self.verts.append(v)
+
+ def add_tex_coord(self, uv):
+ self.tex_coords.append(uv)
+
+ def set_color(self, c):
+ self.color = c
+
+ def set_line_width(self, width):
+ self.line_width = width
+
+ def clear(self):
+ self.prim_mode = None
+ self.verts = []
+ self.dims = None
+ self.tex_coords = []
+
+ def get_verts(self):
+ return self.verts
+
+ def get_dims(self):
+ return self.dims
+
+ def get_prim_mode(self):
+ return self.prim_mode
+
+ def get_color(self):
+ return self.color
+
+ def get_line_width(self):
+ return self.line_width
+
+ def get_tex_coords(self):
+ return self.tex_coords
+
+
+def glLineWidth(width):
+ inst = InternalData.get_instance()
+ inst.set_line_width(width)
+
+
+def glColor3f(r, g, b):
+ inst = InternalData.get_instance()
+ inst.set_color([r, g, b, 1.0])
+
+
+def glColor4f(r, g, b, a):
+ inst = InternalData.get_instance()
+ inst.set_color([r, g, b, a])
+
+
+def glRecti(x0, y0, x1, y1):
+ glBegin(GL_QUADS)
+ glVertex2f(x0, y0)
+ glVertex2f(x0, y1)
+ glVertex2f(x1, y1)
+ glVertex2f(x1, y0)
+ glEnd()
+
+
+def glBegin(mode):
+ inst = InternalData.get_instance()
+ inst.init()
+ inst.set_prim_mode(mode)
+
+
+def _get_transparency_shader():
+ vertex_shader = '''
+ uniform mat4 modelViewMatrix;
+ uniform mat4 projectionMatrix;
+
+ in vec2 pos;
+ in vec2 texCoord;
+ out vec2 uvInterp;
+
+ void main()
+ {
+ uvInterp = texCoord;
+ gl_Position = projectionMatrix * modelViewMatrix * vec4(pos.xy, 0.0, 1.0);
+ gl_Position.z = 1.0;
+ }
+ '''
+
+ fragment_shader = '''
+ uniform sampler2D image;
+ uniform vec4 color;
+
+ in vec2 uvInterp;
+ out vec4 fragColor;
+
+ void main()
+ {
+ fragColor = texture(image, uvInterp);
+ fragColor.a = color.a;
+ }
+ '''
+
+ return vertex_shader, fragment_shader
+
+
+def glEnd():
+ inst = InternalData.get_instance()
+
+ color = inst.get_color()
+ coords = inst.get_verts()
+ tex_coords = inst.get_tex_coords()
+ if inst.get_dims() == 2:
+ if len(tex_coords) == 0:
+ shader = gpu.shader.from_builtin('2D_UNIFORM_COLOR')
+ else:
+ #shader = gpu.shader.from_builtin('2D_IMAGE')
+ vert_shader, frag_shader = _get_transparency_shader()
+ shader = gpu.types.GPUShader(vert_shader, frag_shader)
+ else:
+ raise NotImplemented("get_dims() != 2")
+
+ if len(tex_coords) == 0:
+ data = {
+ "pos": coords,
+ }
+ else:
+ data = {
+ "pos": coords,
+ "texCoord": tex_coords
+ }
+
+ if inst.get_prim_mode() == GL_LINES:
+ indices = []
+ for i in range(0, len(coords), 2):
+ indices.append([i, i + 1])
+ batch = batch_for_shader(shader, 'LINES', data, indices=indices)
+
+ elif inst.get_prim_mode() == GL_LINE_STRIP:
+ batch = batch_for_shader(shader, 'LINE_STRIP', data)
+
+
+ elif inst.get_prim_mode() == GL_LINE_LOOP:
+ data["pos"].append(data["pos"][0])
+ batch = batch_for_shader(shader, 'LINE_STRIP', data)
+
+ elif inst.get_prim_mode() == GL_TRIANGLES:
+ indices = []
+ for i in range(0, len(coords), 3):
+ indices.append([i, i + 1, i + 2])
+ batch = batch_for_shader(shader, 'TRIS', data, indices=indices)
+
+ elif inst.get_prim_mode() == GL_TRIANGLE_FAN:
+ indices = []
+ for i in range(1, len(coords) - 1):
+ indices.append([0, i, i + 1])
+ batch = batch_for_shader(shader, 'TRIS', data, indices=indices)
+
+ elif inst.get_prim_mode() == GL_QUADS:
+ indices = []
+ for i in range(0, len(coords), 4):
+ indices.extend([[i, i + 1, i + 2], [i + 2, i + 3, i]])
+ batch = batch_for_shader(shader, 'TRIS', data, indices=indices)
+ else:
+ raise NotImplemented("get_prim_mode() != (GL_LINES|GL_TRIANGLES|GL_QUADS)")
+
+ shader.bind()
+ if len(tex_coords) != 0:
+ shader.uniform_float("modelViewMatrix", gpu.matrix.get_model_view_matrix())
+ shader.uniform_float("projectionMatrix", gpu.matrix.get_projection_matrix())
+ shader.uniform_int("image", 0)
+ shader.uniform_float("color", color)
+ batch.draw(shader)
+
+ inst.clear()
+
+
+def glVertex2f(x, y):
+ inst = InternalData.get_instance()
+ inst.add_vert([x, y])
+ inst.set_dims(2)
+
+
+def glTexCoord2f(u, v):
+ inst = InternalData.get_instance()
+ inst.add_tex_coord([u, v])
+
+
+GL_BLEND = bgl.GL_BLEND
+GL_LINE_SMOOTH = bgl.GL_LINE_SMOOTH
+GL_INT = bgl.GL_INT
+GL_SCISSOR_BOX = bgl.GL_SCISSOR_BOX
+GL_TEXTURE_2D = bgl.GL_TEXTURE_2D
+GL_TEXTURE0 = bgl.GL_TEXTURE0
+
+GL_TEXTURE_MIN_FILTER = 0
+GL_TEXTURE_MAG_FILTER = 0
+GL_LINEAR = 0
+GL_TEXTURE_ENV = 0
+GL_TEXTURE_ENV_MODE = 0
+GL_MODULATE = 0
+
+def glEnable(cap):
+ bgl.glEnable(cap)
+
+
+def glDisable(cap):
+ bgl.glDisable(cap)
+
+
+def glScissor(x, y, width, height):
+ bgl.glScissor(x, y, width, height)
+
+
+def glGetIntegerv(pname, params):
+ bgl.glGetIntegerv(pname, params)
+
+
+def glActiveTexture(texture):
+ bgl.glActiveTexture(texture)
+
+
+def glBindTexture(target, texture):
+ bgl.glBindTexture(target, texture)
+
+
+def glTexParameteri(target, pname, param):
+ pass
+
+
+def glTexEnvi(target, pname, param):
+ pass
+
diff --git a/magic_uv/op/__init__.py b/magic_uv/op/__init__.py
new file mode 100644
index 00000000..d637e78a
--- /dev/null
+++ b/magic_uv/op/__init__.py
@@ -0,0 +1,74 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+if "bpy" in locals():
+ import importlib
+ importlib.reload(align_uv)
+ importlib.reload(align_uv_cursor)
+ importlib.reload(copy_paste_uv)
+ importlib.reload(copy_paste_uv_object)
+ importlib.reload(copy_paste_uv_uvedit)
+ importlib.reload(flip_rotate_uv)
+ importlib.reload(mirror_uv)
+ importlib.reload(move_uv)
+ importlib.reload(pack_uv)
+ importlib.reload(preserve_uv_aspect)
+ importlib.reload(select_uv)
+ importlib.reload(smooth_uv)
+ importlib.reload(texture_lock)
+ importlib.reload(texture_projection)
+ importlib.reload(texture_wrap)
+ importlib.reload(transfer_uv)
+ importlib.reload(unwrap_constraint)
+ importlib.reload(uv_bounding_box)
+ importlib.reload(uv_inspection)
+ importlib.reload(uv_sculpt)
+ importlib.reload(uvw)
+ importlib.reload(world_scale_uv)
+else:
+ from . import align_uv
+ from . import align_uv_cursor
+ from . import copy_paste_uv
+ from . import copy_paste_uv_object
+ from . import copy_paste_uv_uvedit
+ from . import flip_rotate_uv
+ from . import mirror_uv
+ from . import move_uv
+ from . import pack_uv
+ from . import preserve_uv_aspect
+ from . import select_uv
+ from . import smooth_uv
+ from . import texture_lock
+ from . import texture_projection
+ from . import texture_wrap
+ from . import transfer_uv
+ from . import unwrap_constraint
+ from . import uv_bounding_box
+ from . import uv_inspection
+ from . import uv_sculpt
+ from . import uvw
+ from . import world_scale_uv
+
+import bpy
diff --git a/magic_uv/op/align_uv.py b/magic_uv/op/align_uv.py
new file mode 100644
index 00000000..f8ea4176
--- /dev/null
+++ b/magic_uv/op/align_uv.py
@@ -0,0 +1,991 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "imdjs, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import math
+from math import atan2, tan, sin, cos
+
+import bpy
+from bpy.props import EnumProperty, BoolProperty, FloatProperty
+import bmesh
+from mathutils import Vector
+
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..utils import compatibility as compat
+
+from .. import common
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
+ # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
+ # after the execution
+ for space in context.area.spaces:
+ if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'):
+ break
+ else:
+ return False
+
+ return True
+
+
+# get sum vertex length of loop sequences
+def _get_loop_vert_len(loops):
+ length = 0
+ for l1, l2 in zip(loops[:-1], loops[1:]):
+ diff = l2.vert.co - l1.vert.co
+ length = length + abs(diff.length)
+
+ return length
+
+
+# get sum uv length of loop sequences
+def _get_loop_uv_len(loops, uv_layer):
+ length = 0
+ for l1, l2 in zip(loops[:-1], loops[1:]):
+ diff = l2[uv_layer].uv - l1[uv_layer].uv
+ length = length + abs(diff.length)
+
+ return length
+
+
+# get center/radius of circle by 3 vertices
+def _get_circle(v):
+ alpha = atan2((v[0].y - v[1].y), (v[0].x - v[1].x)) + math.pi / 2
+ beta = atan2((v[1].y - v[2].y), (v[1].x - v[2].x)) + math.pi / 2
+ ex = (v[0].x + v[1].x) / 2.0
+ ey = (v[0].y + v[1].y) / 2.0
+ fx = (v[1].x + v[2].x) / 2.0
+ fy = (v[1].y + v[2].y) / 2.0
+ cx = (ey - fy - ex * tan(alpha) + fx * tan(beta)) / \
+ (tan(beta) - tan(alpha))
+ cy = ey - (ex - cx) * tan(alpha)
+ center = Vector((cx, cy))
+
+ r = v[0] - center
+ radian = r.length
+
+ return center, radian
+
+
+# get position on circle with same arc length
+def _calc_v_on_circle(v, center, radius):
+ base = v[0]
+ theta = atan2(base.y - center.y, base.x - center.x)
+ new_v = []
+ for i in range(len(v)):
+ angle = theta + i * 2 * math.pi / len(v)
+ new_v.append(Vector((center.x + radius * sin(angle),
+ center.y + radius * cos(angle))))
+
+ return new_v
+
+
+# get accumulate vertex lengths of loop sequences
+def _get_loop_vert_accum_len(loops):
+ accum_lengths = [0.0]
+ length = 0
+ for l1, l2 in zip(loops[:-1], loops[1:]):
+ diff = l2.vert.co - l1.vert.co
+ length = length + abs(diff.length)
+ accum_lengths.extend([length])
+
+ return accum_lengths
+
+
+# get sum uv length of loop sequences
+def _get_loop_uv_accum_len(loops, uv_layer):
+ accum_lengths = [0.0]
+ length = 0
+ for l1, l2 in zip(loops[:-1], loops[1:]):
+ diff = l2[uv_layer].uv - l1[uv_layer].uv
+ length = length + abs(diff.length)
+ accum_lengths.extend([length])
+
+ return accum_lengths
+
+
+# get horizontal differential of UV influenced by mesh vertex
+def _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl):
+ common.debug_print(
+ "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx))
+
+ base_uv = loop_seqs[0][vidx][0][uv_layer].uv.copy()
+
+ # calculate original length
+ hloops = []
+ for s in loop_seqs:
+ hloops.extend([s[vidx][0], s[vidx][1]])
+ total_vlen = _get_loop_vert_len(hloops)
+ accum_vlens = _get_loop_vert_accum_len(hloops)
+ total_uvlen = _get_loop_uv_len(hloops, uv_layer)
+ accum_uvlens = _get_loop_uv_accum_len(hloops, uv_layer)
+ orig_uvs = [l[uv_layer].uv.copy() for l in hloops]
+
+ # calculate target length
+ tgt_noinfl = total_uvlen * (hidx + pidx) / len(loop_seqs)
+ tgt_infl = total_uvlen * accum_vlens[hidx * 2 + pidx] / total_vlen
+ target_length = tgt_noinfl * (1 - infl) + tgt_infl * infl
+ common.debug_print(target_length)
+ common.debug_print(accum_uvlens)
+
+ # calculate target UV
+ for i in range(len(accum_uvlens[:-1])):
+ # get line segment which UV will be placed
+ if ((accum_uvlens[i] <= target_length) and
+ (accum_uvlens[i + 1] > target_length)):
+ tgt_seg_len = target_length - accum_uvlens[i]
+ seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
+ uv1 = orig_uvs[i]
+ uv2 = orig_uvs[i + 1]
+ target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
+ break
+ elif i == (len(accum_uvlens[:-1]) - 1):
+ if abs(accum_uvlens[i + 1] - target_length) > 0.000001:
+ raise Exception(
+ "Internal Error: horizontal_target_length={}"
+ " is not equal to {}"
+ .format(target_length, accum_uvlens[-1]))
+ tgt_seg_len = target_length - accum_uvlens[i]
+ seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
+ uv1 = orig_uvs[i]
+ uv2 = orig_uvs[i + 1]
+ target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
+ break
+ else:
+ raise Exception("Internal Error: horizontal_target_length={}"
+ " is not in range {} to {}"
+ .format(target_length, accum_uvlens[0],
+ accum_uvlens[-1]))
+
+ return target_uv
+
+
+# --------------------- LOOP STRUCTURE ----------------------
+#
+# loops[hidx][vidx][pidx]
+# hidx: horizontal index
+# vidx: vertical index
+# pidx: pair index
+#
+# <----- horizontal ----->
+#
+# (hidx, vidx, pidx) = (0, 3, 0)
+# | (hidx, vidx, pidx) = (1, 3, 0)
+# v v
+# ^ o --- oo --- o
+# | | || |
+# vertical | o --- oo --- o <- (hidx, vidx, pidx)
+# | o --- oo --- o = (1, 2, 1)
+# | | || |
+# v o --- oo --- o
+# ^ ^
+# | (hidx, vidx, pidx) = (1, 0, 1)
+# (hidx, vidx, pidx) = (0, 0, 0)
+#
+# -----------------------------------------------------------
+
+
+# get vertical differential of UV influenced by mesh vertex
+def _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl):
+ common.debug_print(
+ "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx))
+
+ base_uv = loop_seqs[hidx][0][pidx][uv_layer].uv.copy()
+
+ # calculate original length
+ vloops = []
+ for s in loop_seqs[hidx]:
+ vloops.append(s[pidx])
+ total_vlen = _get_loop_vert_len(vloops)
+ accum_vlens = _get_loop_vert_accum_len(vloops)
+ total_uvlen = _get_loop_uv_len(vloops, uv_layer)
+ accum_uvlens = _get_loop_uv_accum_len(vloops, uv_layer)
+ orig_uvs = [l[uv_layer].uv.copy() for l in vloops]
+
+ # calculate target length
+ tgt_noinfl = total_uvlen * int((vidx + 1) / 2) * 2 / len(loop_seqs[hidx])
+ tgt_infl = total_uvlen * accum_vlens[vidx] / total_vlen
+ target_length = tgt_noinfl * (1 - infl) + tgt_infl * infl
+ common.debug_print(target_length)
+ common.debug_print(accum_uvlens)
+
+ # calculate target UV
+ for i in range(len(accum_uvlens[:-1])):
+ # get line segment which UV will be placed
+ if ((accum_uvlens[i] <= target_length) and
+ (accum_uvlens[i + 1] > target_length)):
+ tgt_seg_len = target_length - accum_uvlens[i]
+ seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
+ uv1 = orig_uvs[i]
+ uv2 = orig_uvs[i + 1]
+ target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
+ break
+ elif i == (len(accum_uvlens[:-1]) - 1):
+ if abs(accum_uvlens[i + 1] - target_length) > 0.000001:
+ raise Exception("Internal Error: horizontal_target_length={}"
+ " is not equal to {}"
+ .format(target_length, accum_uvlens[-1]))
+ tgt_seg_len = target_length - accum_uvlens[i]
+ seg_len = accum_uvlens[i + 1] - accum_uvlens[i]
+ uv1 = orig_uvs[i]
+ uv2 = orig_uvs[i + 1]
+ target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len
+ break
+ else:
+ raise Exception("Internal Error: horizontal_target_length={}"
+ " is not in range {} to {}"
+ .format(target_length, accum_uvlens[0],
+ accum_uvlens[-1]))
+
+ return target_uv
+
+
+# get horizontal differential of UV no influenced
+def _get_hdiff_uv(uv_layer, loop_seqs, hidx):
+ base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
+ h_uv = loop_seqs[-1][0][1][uv_layer].uv.copy() - base_uv
+
+ return hidx * h_uv / len(loop_seqs)
+
+
+# get vertical differential of UV no influenced
+def _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx):
+ base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
+ v_uv = loop_seqs[0][-1][0][uv_layer].uv.copy() - base_uv
+
+ hseq = loop_seqs[hidx]
+ return int((vidx + 1) / 2) * v_uv / (len(hseq) / 2)
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "align_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_align_uv_enabled = BoolProperty(
+ name="Align UV Enabled",
+ description="Align UV is enabled",
+ default=False
+ )
+ scene.muv_align_uv_transmission = BoolProperty(
+ name="Transmission",
+ description="Align linked UVs",
+ default=False
+ )
+ scene.muv_align_uv_select = BoolProperty(
+ name="Select",
+ description="Select UVs which are aligned",
+ default=False
+ )
+ scene.muv_align_uv_vertical = BoolProperty(
+ name="Vert-Infl (Vertical)",
+ description="Align vertical direction influenced "
+ "by mesh vertex proportion",
+ default=False
+ )
+ scene.muv_align_uv_horizontal = BoolProperty(
+ name="Vert-Infl (Horizontal)",
+ description="Align horizontal direction influenced "
+ "by mesh vertex proportion",
+ default=False
+ )
+ scene.muv_align_uv_mesh_infl = FloatProperty(
+ name="Mesh Influence",
+ description="Influence rate of mesh vertex",
+ min=0.0,
+ max=1.0,
+ default=0.0
+ )
+ scene.muv_align_uv_location = EnumProperty(
+ name="Location",
+ description="Align location",
+ items=[
+ ('LEFT_TOP', "Left/Top", "Align to Left or Top"),
+ ('MIDDLE', "Middle", "Align to middle"),
+ ('RIGHT_BOTTOM', "Right/Bottom", "Align to Right or Bottom")
+ ],
+ default='MIDDLE'
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_align_uv_enabled
+ del scene.muv_align_uv_transmission
+ del scene.muv_align_uv_select
+ del scene.muv_align_uv_vertical
+ del scene.muv_align_uv_horizontal
+ del scene.muv_align_uv_mesh_infl
+ del scene.muv_align_uv_location
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_AlignUV_Circle(bpy.types.Operator):
+
+ bl_idname = "uv.muv_ot_align_uv_circle"
+ bl_label = "Align UV (Circle)"
+ bl_description = "Align UV coordinates to Circle"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ transmission = BoolProperty(
+ name="Transmission",
+ description="Align linked UVs",
+ default=False
+ )
+ select = BoolProperty(
+ name="Select",
+ description="Select UVs which are aligned",
+ default=False
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+ uv_layer = bm.loops.layers.uv.verify()
+
+ # loop_seqs[horizontal][vertical][loop]
+ loop_seqs, error = common.get_loop_sequences(bm, uv_layer, True)
+ if not loop_seqs:
+ self.report({'WARNING'}, error)
+ return {'CANCELLED'}
+
+ # get circle and new UVs
+ uvs = [hseq[0][0][uv_layer].uv.copy() for hseq in loop_seqs]
+ c, r = _get_circle(uvs[0:3])
+ new_uvs = _calc_v_on_circle(uvs, c, r)
+
+ # check center UV of circle
+ center = loop_seqs[0][-1][0].vert
+ for hseq in loop_seqs[1:]:
+ if len(hseq[-1]) != 1:
+ self.report({'WARNING'}, "Last face must be triangle")
+ return {'CANCELLED'}
+ if hseq[-1][0].vert != center:
+ self.report({'WARNING'}, "Center must be identical")
+ return {'CANCELLED'}
+
+ # align to circle
+ if self.transmission:
+ for hidx, hseq in enumerate(loop_seqs):
+ for vidx, pair in enumerate(hseq):
+ all_ = int((len(hseq) + 1) / 2)
+ r = (all_ - int((vidx + 1) / 2)) / all_
+ pair[0][uv_layer].uv = c + (new_uvs[hidx] - c) * r
+ if self.select:
+ pair[0][uv_layer].select = True
+
+ if len(pair) < 2:
+ continue
+ # for quad polygon
+ next_hidx = (hidx + 1) % len(loop_seqs)
+ pair[1][uv_layer].uv = c + ((new_uvs[next_hidx]) - c) * r
+ if self.select:
+ pair[1][uv_layer].select = True
+ else:
+ for hidx, hseq in enumerate(loop_seqs):
+ pair = hseq[0]
+ pair[0][uv_layer].uv = new_uvs[hidx]
+ pair[1][uv_layer].uv = new_uvs[(hidx + 1) % len(loop_seqs)]
+ if self.select:
+ pair[0][uv_layer].select = True
+ pair[1][uv_layer].select = True
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_AlignUV_Straighten(bpy.types.Operator):
+
+ bl_idname = "uv.muv_ot_align_uv_straighten"
+ bl_label = "Align UV (Straighten)"
+ bl_description = "Straighten UV coordinates"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ transmission = BoolProperty(
+ name="Transmission",
+ description="Align linked UVs",
+ default=False
+ )
+ select = BoolProperty(
+ name="Select",
+ description="Select UVs which are aligned",
+ default=False
+ )
+ vertical = BoolProperty(
+ name="Vert-Infl (Vertical)",
+ description="Align vertical direction influenced "
+ "by mesh vertex proportion",
+ default=False
+ )
+ horizontal = BoolProperty(
+ name="Vert-Infl (Horizontal)",
+ description="Align horizontal direction influenced "
+ "by mesh vertex proportion",
+ default=False
+ )
+ mesh_infl = FloatProperty(
+ name="Mesh Influence",
+ description="Influence rate of mesh vertex",
+ min=0.0,
+ max=1.0,
+ default=0.0
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ # selected and paralleled UV loop sequence will be aligned
+ def __align_w_transmission(self, loop_seqs, uv_layer):
+ base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
+
+ # calculate diff UVs
+ diff_uvs = []
+ # hseq[vertical][loop]
+ for hidx, hseq in enumerate(loop_seqs):
+ # pair[loop]
+ diffs = []
+ for vidx in range(0, len(hseq), 2):
+ if self.horizontal:
+ hdiff_uvs = [
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
+ self.mesh_infl),
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
+ self.mesh_infl),
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 0, self.mesh_infl),
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 1, self.mesh_infl),
+ ]
+ else:
+ hdiff_uvs = [
+ _get_hdiff_uv(uv_layer, loop_seqs, hidx),
+ _get_hdiff_uv(uv_layer, loop_seqs, hidx + 1),
+ _get_hdiff_uv(uv_layer, loop_seqs, hidx),
+ _get_hdiff_uv(uv_layer, loop_seqs, hidx + 1)
+ ]
+ if self.vertical:
+ vdiff_uvs = [
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
+ self.mesh_infl),
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
+ self.mesh_infl),
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 0, self.mesh_infl),
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 1, self.mesh_infl),
+ ]
+ else:
+ vdiff_uvs = [
+ _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
+ _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
+ _get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx),
+ _get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx)
+ ]
+ diffs.append([hdiff_uvs, vdiff_uvs])
+ diff_uvs.append(diffs)
+
+ # update UV
+ for hseq, diffs in zip(loop_seqs, diff_uvs):
+ for vidx in range(0, len(hseq), 2):
+ loops = [
+ hseq[vidx][0], hseq[vidx][1],
+ hseq[vidx + 1][0], hseq[vidx + 1][1]
+ ]
+ for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0],
+ diffs[int(vidx / 2)][1]):
+ l[uv_layer].uv = base_uv + hdiff + vdiff
+ if self.select:
+ l[uv_layer].select = True
+
+ # only selected UV loop sequence will be aligned
+ def __align_wo_transmission(self, loop_seqs, uv_layer):
+ base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
+
+ h_uv = loop_seqs[-1][0][1][uv_layer].uv.copy() - base_uv
+ for hidx, hseq in enumerate(loop_seqs):
+ # only selected loop pair is targeted
+ pair = hseq[0]
+ hdiff_uv_0 = hidx * h_uv / len(loop_seqs)
+ hdiff_uv_1 = (hidx + 1) * h_uv / len(loop_seqs)
+ pair[0][uv_layer].uv = base_uv + hdiff_uv_0
+ pair[1][uv_layer].uv = base_uv + hdiff_uv_1
+ if self.select:
+ pair[0][uv_layer].select = True
+ pair[1][uv_layer].select = True
+
+ def __align(self, loop_seqs, uv_layer):
+ if self.transmission:
+ self.__align_w_transmission(loop_seqs, uv_layer)
+ else:
+ self.__align_wo_transmission(loop_seqs, uv_layer)
+
+ def execute(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+ uv_layer = bm.loops.layers.uv.verify()
+
+ # loop_seqs[horizontal][vertical][loop]
+ loop_seqs, error = common.get_loop_sequences(bm, uv_layer)
+ if not loop_seqs:
+ self.report({'WARNING'}, error)
+ return {'CANCELLED'}
+
+ # align
+ self.__align(loop_seqs, uv_layer)
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_AlignUV_Axis(bpy.types.Operator):
+
+ bl_idname = "uv.muv_ot_align_uv_axis"
+ bl_label = "Align UV (XY-Axis)"
+ bl_description = "Align UV to XY-axis"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ transmission = BoolProperty(
+ name="Transmission",
+ description="Align linked UVs",
+ default=False
+ )
+ select = BoolProperty(
+ name="Select",
+ description="Select UVs which are aligned",
+ default=False
+ )
+ vertical = BoolProperty(
+ name="Vert-Infl (Vertical)",
+ description="Align vertical direction influenced "
+ "by mesh vertex proportion",
+ default=False
+ )
+ horizontal = BoolProperty(
+ name="Vert-Infl (Horizontal)",
+ description="Align horizontal direction influenced "
+ "by mesh vertex proportion",
+ default=False
+ )
+ location = EnumProperty(
+ name="Location",
+ description="Align location",
+ items=[
+ ('LEFT_TOP', "Left/Top", "Align to Left or Top"),
+ ('MIDDLE', "Middle", "Align to middle"),
+ ('RIGHT_BOTTOM', "Right/Bottom", "Align to Right or Bottom")
+ ],
+ default='MIDDLE'
+ )
+ mesh_infl = FloatProperty(
+ name="Mesh Influence",
+ description="Influence rate of mesh vertex",
+ min=0.0,
+ max=1.0,
+ default=0.0
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ # get min/max of UV
+ def __get_uv_max_min(self, loop_seqs, uv_layer):
+ uv_max = Vector((-1000000.0, -1000000.0))
+ uv_min = Vector((1000000.0, 1000000.0))
+ for hseq in loop_seqs:
+ for l in hseq[0]:
+ uv = l[uv_layer].uv
+ uv_max.x = max(uv.x, uv_max.x)
+ uv_max.y = max(uv.y, uv_max.y)
+ uv_min.x = min(uv.x, uv_min.x)
+ uv_min.y = min(uv.y, uv_min.y)
+
+ return uv_max, uv_min
+
+ # get UV differentiation when UVs are aligned to X-axis
+ def __get_x_axis_align_diff_uvs(self, loop_seqs, uv_layer, uv_min,
+ width, height):
+ diff_uvs = []
+ for hidx, hseq in enumerate(loop_seqs):
+ pair = hseq[0]
+ luv0 = pair[0][uv_layer]
+ luv1 = pair[1][uv_layer]
+ target_uv0 = Vector((0.0, 0.0))
+ target_uv1 = Vector((0.0, 0.0))
+ if self.location == 'RIGHT_BOTTOM':
+ target_uv0.y = target_uv1.y = uv_min.y
+ elif self.location == 'MIDDLE':
+ target_uv0.y = target_uv1.y = uv_min.y + height * 0.5
+ elif self.location == 'LEFT_TOP':
+ target_uv0.y = target_uv1.y = uv_min.y + height
+ if luv0.uv.x < luv1.uv.x:
+ target_uv0.x = uv_min.x + hidx * width / len(loop_seqs)
+ target_uv1.x = uv_min.x + (hidx + 1) * width / len(loop_seqs)
+ else:
+ target_uv0.x = uv_min.x + (hidx + 1) * width / len(loop_seqs)
+ target_uv1.x = uv_min.x + hidx * width / len(loop_seqs)
+ diff_uvs.append([target_uv0 - luv0.uv, target_uv1 - luv1.uv])
+
+ return diff_uvs
+
+ # get UV differentiation when UVs are aligned to Y-axis
+ def __get_y_axis_align_diff_uvs(self, loop_seqs, uv_layer, uv_min,
+ width, height):
+ diff_uvs = []
+ for hidx, hseq in enumerate(loop_seqs):
+ pair = hseq[0]
+ luv0 = pair[0][uv_layer]
+ luv1 = pair[1][uv_layer]
+ target_uv0 = Vector((0.0, 0.0))
+ target_uv1 = Vector((0.0, 0.0))
+ if self.location == 'RIGHT_BOTTOM':
+ target_uv0.x = target_uv1.x = uv_min.x + width
+ elif self.location == 'MIDDLE':
+ target_uv0.x = target_uv1.x = uv_min.x + width * 0.5
+ elif self.location == 'LEFT_TOP':
+ target_uv0.x = target_uv1.x = uv_min.x
+ if luv0.uv.y < luv1.uv.y:
+ target_uv0.y = uv_min.y + hidx * height / len(loop_seqs)
+ target_uv1.y = uv_min.y + (hidx + 1) * height / len(loop_seqs)
+ else:
+ target_uv0.y = uv_min.y + (hidx + 1) * height / len(loop_seqs)
+ target_uv1.y = uv_min.y + hidx * height / len(loop_seqs)
+ diff_uvs.append([target_uv0 - luv0.uv, target_uv1 - luv1.uv])
+
+ return diff_uvs
+
+ # only selected UV loop sequence will be aligned along to X-axis
+ def __align_to_x_axis_wo_transmission(self, loop_seqs, uv_layer,
+ uv_min, width, height):
+ # reverse if the UV coordinate is not sorted by position
+ need_revese = loop_seqs[0][0][0][uv_layer].uv.x > \
+ loop_seqs[-1][0][0][uv_layer].uv.x
+ if need_revese:
+ loop_seqs.reverse()
+ for hidx, hseq in enumerate(loop_seqs):
+ for vidx, pair in enumerate(hseq):
+ tmp = loop_seqs[hidx][vidx][0]
+ loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1]
+ loop_seqs[hidx][vidx][1] = tmp
+
+ # get UV differential
+ diff_uvs = self.__get_x_axis_align_diff_uvs(loop_seqs,
+ uv_layer, uv_min,
+ width, height)
+
+ # update UV
+ for hseq, duv in zip(loop_seqs, diff_uvs):
+ pair = hseq[0]
+ luv0 = pair[0][uv_layer]
+ luv1 = pair[1][uv_layer]
+ luv0.uv = luv0.uv + duv[0]
+ luv1.uv = luv1.uv + duv[1]
+
+ # only selected UV loop sequence will be aligned along to Y-axis
+ def __align_to_y_axis_wo_transmission(self, loop_seqs, uv_layer,
+ uv_min, width, height):
+ # reverse if the UV coordinate is not sorted by position
+ need_revese = loop_seqs[0][0][0][uv_layer].uv.y > \
+ loop_seqs[-1][0][0][uv_layer].uv.y
+ if need_revese:
+ loop_seqs.reverse()
+ for hidx, hseq in enumerate(loop_seqs):
+ for vidx, pair in enumerate(hseq):
+ tmp = loop_seqs[hidx][vidx][0]
+ loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1]
+ loop_seqs[hidx][vidx][1] = tmp
+
+ # get UV differential
+ diff_uvs = self.__get_y_axis_align_diff_uvs(loop_seqs,
+ uv_layer, uv_min,
+ width, height)
+
+ # update UV
+ for hseq, duv in zip(loop_seqs, diff_uvs):
+ pair = hseq[0]
+ luv0 = pair[0][uv_layer]
+ luv1 = pair[1][uv_layer]
+ luv0.uv = luv0.uv + duv[0]
+ luv1.uv = luv1.uv + duv[1]
+
+ # selected and paralleled UV loop sequence will be aligned along to X-axis
+ def __align_to_x_axis_w_transmission(self, loop_seqs, uv_layer,
+ uv_min, width, height):
+ # reverse if the UV coordinate is not sorted by position
+ need_revese = loop_seqs[0][0][0][uv_layer].uv.x > \
+ loop_seqs[-1][0][0][uv_layer].uv.x
+ if need_revese:
+ loop_seqs.reverse()
+ for hidx, hseq in enumerate(loop_seqs):
+ for vidx in range(len(hseq)):
+ tmp = loop_seqs[hidx][vidx][0]
+ loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1]
+ loop_seqs[hidx][vidx][1] = tmp
+
+ # get offset UVs when the UVs are aligned to X-axis
+ align_diff_uvs = self.__get_x_axis_align_diff_uvs(loop_seqs,
+ uv_layer, uv_min,
+ width, height)
+ base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
+ offset_uvs = []
+ for hseq, aduv in zip(loop_seqs, align_diff_uvs):
+ luv0 = hseq[0][0][uv_layer]
+ luv1 = hseq[0][1][uv_layer]
+ offset_uvs.append([luv0.uv + aduv[0] - base_uv,
+ luv1.uv + aduv[1] - base_uv])
+
+ # get UV differential
+ diff_uvs = []
+ # hseq[vertical][loop]
+ for hidx, hseq in enumerate(loop_seqs):
+ # pair[loop]
+ diffs = []
+ for vidx in range(0, len(hseq), 2):
+ if self.horizontal:
+ hdiff_uvs = [
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
+ self.mesh_infl),
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
+ self.mesh_infl),
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 0, self.mesh_infl),
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 1, self.mesh_infl),
+ ]
+ hdiff_uvs[0].y = hdiff_uvs[0].y + offset_uvs[hidx][0].y
+ hdiff_uvs[1].y = hdiff_uvs[1].y + offset_uvs[hidx][1].y
+ hdiff_uvs[2].y = hdiff_uvs[2].y + offset_uvs[hidx][0].y
+ hdiff_uvs[3].y = hdiff_uvs[3].y + offset_uvs[hidx][1].y
+ else:
+ hdiff_uvs = [
+ offset_uvs[hidx][0],
+ offset_uvs[hidx][1],
+ offset_uvs[hidx][0],
+ offset_uvs[hidx][1],
+ ]
+ if self.vertical:
+ vdiff_uvs = [
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
+ self.mesh_infl),
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
+ self.mesh_infl),
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 0, self.mesh_infl),
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 1, self.mesh_infl),
+ ]
+ else:
+ vdiff_uvs = [
+ _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
+ _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
+ _get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx),
+ _get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx)
+ ]
+ diffs.append([hdiff_uvs, vdiff_uvs])
+ diff_uvs.append(diffs)
+
+ # update UV
+ for hseq, diffs in zip(loop_seqs, diff_uvs):
+ for vidx in range(0, len(hseq), 2):
+ loops = [
+ hseq[vidx][0], hseq[vidx][1],
+ hseq[vidx + 1][0], hseq[vidx + 1][1]
+ ]
+ for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0],
+ diffs[int(vidx / 2)][1]):
+ l[uv_layer].uv = base_uv + hdiff + vdiff
+ if self.select:
+ l[uv_layer].select = True
+
+ # selected and paralleled UV loop sequence will be aligned along to Y-axis
+ def __align_to_y_axis_w_transmission(self, loop_seqs, uv_layer,
+ uv_min, width, height):
+ # reverse if the UV coordinate is not sorted by position
+ need_revese = loop_seqs[0][0][0][uv_layer].uv.y > \
+ loop_seqs[-1][0][-1][uv_layer].uv.y
+ if need_revese:
+ loop_seqs.reverse()
+ for hidx, hseq in enumerate(loop_seqs):
+ for vidx in range(len(hseq)):
+ tmp = loop_seqs[hidx][vidx][0]
+ loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1]
+ loop_seqs[hidx][vidx][1] = tmp
+
+ # get offset UVs when the UVs are aligned to Y-axis
+ align_diff_uvs = self.__get_y_axis_align_diff_uvs(loop_seqs,
+ uv_layer, uv_min,
+ width, height)
+ base_uv = loop_seqs[0][0][0][uv_layer].uv.copy()
+ offset_uvs = []
+ for hseq, aduv in zip(loop_seqs, align_diff_uvs):
+ luv0 = hseq[0][0][uv_layer]
+ luv1 = hseq[0][1][uv_layer]
+ offset_uvs.append([luv0.uv + aduv[0] - base_uv,
+ luv1.uv + aduv[1] - base_uv])
+
+ # get UV differential
+ diff_uvs = []
+ # hseq[vertical][loop]
+ for hidx, hseq in enumerate(loop_seqs):
+ # pair[loop]
+ diffs = []
+ for vidx in range(0, len(hseq), 2):
+ if self.horizontal:
+ hdiff_uvs = [
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
+ self.mesh_infl),
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
+ self.mesh_infl),
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 0, self.mesh_infl),
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 1, self.mesh_infl),
+ ]
+ hdiff_uvs[0].x = hdiff_uvs[0].x + offset_uvs[hidx][0].x
+ hdiff_uvs[1].x = hdiff_uvs[1].x + offset_uvs[hidx][1].x
+ hdiff_uvs[2].x = hdiff_uvs[2].x + offset_uvs[hidx][0].x
+ hdiff_uvs[3].x = hdiff_uvs[3].x + offset_uvs[hidx][1].x
+ else:
+ hdiff_uvs = [
+ offset_uvs[hidx][0],
+ offset_uvs[hidx][1],
+ offset_uvs[hidx][0],
+ offset_uvs[hidx][1],
+ ]
+ if self.vertical:
+ vdiff_uvs = [
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
+ self.mesh_infl),
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
+ self.mesh_infl),
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 0, self.mesh_infl),
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 1, self.mesh_infl),
+ ]
+ else:
+ vdiff_uvs = [
+ _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
+ _get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx),
+ _get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx),
+ _get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx)
+ ]
+ diffs.append([hdiff_uvs, vdiff_uvs])
+ diff_uvs.append(diffs)
+
+ # update UV
+ for hseq, diffs in zip(loop_seqs, diff_uvs):
+ for vidx in range(0, len(hseq), 2):
+ loops = [
+ hseq[vidx][0], hseq[vidx][1],
+ hseq[vidx + 1][0], hseq[vidx + 1][1]
+ ]
+ for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0],
+ diffs[int(vidx / 2)][1]):
+ l[uv_layer].uv = base_uv + hdiff + vdiff
+ if self.select:
+ l[uv_layer].select = True
+
+ def __align(self, loop_seqs, uv_layer, uv_min, width, height):
+ # align along to x-axis
+ if width > height:
+ if self.transmission:
+ self.__align_to_x_axis_w_transmission(loop_seqs,
+ uv_layer, uv_min,
+ width, height)
+ else:
+ self.__align_to_x_axis_wo_transmission(loop_seqs,
+ uv_layer, uv_min,
+ width, height)
+ # align along to y-axis
+ else:
+ if self.transmission:
+ self.__align_to_y_axis_w_transmission(loop_seqs,
+ uv_layer, uv_min,
+ width, height)
+ else:
+ self.__align_to_y_axis_wo_transmission(loop_seqs,
+ uv_layer, uv_min,
+ width, height)
+
+ def execute(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+ uv_layer = bm.loops.layers.uv.verify()
+
+ # loop_seqs[horizontal][vertical][loop]
+ loop_seqs, error = common.get_loop_sequences(bm, uv_layer)
+ if not loop_seqs:
+ self.report({'WARNING'}, error)
+ return {'CANCELLED'}
+
+ # get height and width
+ uv_max, uv_min = self.__get_uv_max_min(loop_seqs, uv_layer)
+ width = uv_max.x - uv_min.x
+ height = uv_max.y - uv_min.y
+
+ self.__align(loop_seqs, uv_layer, uv_min, width, height)
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
diff --git a/magic_uv/op/align_uv_cursor.py b/magic_uv/op/align_uv_cursor.py
new file mode 100644
index 00000000..86d13179
--- /dev/null
+++ b/magic_uv/op/align_uv_cursor.py
@@ -0,0 +1,269 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+from mathutils import Vector
+from bpy.props import EnumProperty, BoolProperty, FloatVectorProperty
+import bmesh
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..utils import compatibility as compat
+
+
+def _is_valid_context(context):
+ # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
+ # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
+ # after the execution
+ for space in context.area.spaces:
+ if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'):
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "align_uv_cursor"
+
+ @classmethod
+ def init_props(cls, scene):
+ def auvc_get_cursor_loc(self):
+ area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW',
+ 'IMAGE_EDITOR')
+ if compat.check_version(2, 80, 0) < 0:
+ bd_size = common.get_uvimg_editor_board_size(area)
+ else:
+ bd_size = [1.0, 1.0]
+ loc = space.cursor_location
+
+ if bd_size[0] < 0.000001:
+ cx = 0.0
+ else:
+ cx = loc[0] / bd_size[0]
+ if bd_size[1] < 0.000001:
+ cy = 0.0
+ else:
+ cy = loc[1] / bd_size[1]
+
+ self['muv_align_uv_cursor_cursor_loc'] = Vector((cx, cy))
+ return self.get('muv_align_uv_cursor_cursor_loc', (0.0, 0.0))
+
+ def auvc_set_cursor_loc(self, value):
+ self['muv_align_uv_cursor_cursor_loc'] = value
+ area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW',
+ 'IMAGE_EDITOR')
+ if compat.check_version(2, 80, 0) < 0:
+ bd_size = common.get_uvimg_editor_board_size(area)
+ else:
+ bd_size = [1.0, 1.0]
+ cx = bd_size[0] * value[0]
+ cy = bd_size[1] * value[1]
+ space.cursor_location = Vector((cx, cy))
+
+ scene.muv_align_uv_cursor_enabled = BoolProperty(
+ name="Align UV Cursor Enabled",
+ description="Align UV Cursor is enabled",
+ default=False
+ )
+
+ scene.muv_align_uv_cursor_cursor_loc = FloatVectorProperty(
+ name="UV Cursor Location",
+ size=2,
+ precision=4,
+ soft_min=-1.0,
+ soft_max=1.0,
+ step=1,
+ default=(0.000, 0.000),
+ get=auvc_get_cursor_loc,
+ set=auvc_set_cursor_loc
+ )
+ scene.muv_align_uv_cursor_align_method = EnumProperty(
+ name="Align Method",
+ description="Align Method",
+ default='TEXTURE',
+ items=[
+ ('TEXTURE', "Texture", "Align to texture"),
+ ('UV', "UV", "Align to UV"),
+ ('UV_SEL', "UV (Selected)", "Align to Selected UV")
+ ]
+ )
+
+ scene.muv_uv_cursor_location_enabled = BoolProperty(
+ name="UV Cursor Location Enabled",
+ description="UV Cursor Location is enabled",
+ default=False
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_align_uv_cursor_enabled
+ del scene.muv_align_uv_cursor_cursor_loc
+ del scene.muv_align_uv_cursor_align_method
+
+ del scene.muv_uv_cursor_location_enabled
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_AlignUVCursor(bpy.types.Operator):
+
+ bl_idname = "uv.muv_ot_align_uv_cursor"
+ bl_label = "Align UV Cursor"
+ bl_description = "Align cursor to the center of UV island"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ position = EnumProperty(
+ items=(
+ ('CENTER', "Center", "Align to Center"),
+ ('LEFT_TOP', "Left Top", "Align to Left Top"),
+ ('LEFT_MIDDLE', "Left Middle", "Align to Left Middle"),
+ ('LEFT_BOTTOM', "Left Bottom", "Align to Left Bottom"),
+ ('MIDDLE_TOP', "Middle Top", "Align to Middle Top"),
+ ('MIDDLE_BOTTOM', "Middle Bottom", "Align to Middle Bottom"),
+ ('RIGHT_TOP', "Right Top", "Align to Right Top"),
+ ('RIGHT_MIDDLE', "Right Middle", "Align to Right Middle"),
+ ('RIGHT_BOTTOM', "Right Bottom", "Align to Right Bottom")
+ ),
+ name="Position",
+ description="Align position",
+ default='CENTER'
+ )
+ base = EnumProperty(
+ items=(
+ ('TEXTURE', "Texture", "Align based on Texture"),
+ ('UV', "UV", "Align to UV"),
+ ('UV_SEL', "UV (Selected)", "Align to Selected UV")
+ ),
+ name="Base",
+ description="Align base",
+ default='TEXTURE'
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW',
+ 'IMAGE_EDITOR')
+ if compat.check_version(2, 80, 0) < 0:
+ bd_size = common.get_uvimg_editor_board_size(area)
+ else:
+ bd_size = [1.0, 1.0]
+
+ if self.base == 'UV':
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if not bm.loops.layers.uv:
+ return None
+ uv_layer = bm.loops.layers.uv.verify()
+
+ max_ = Vector((-10000000.0, -10000000.0))
+ min_ = Vector((10000000.0, 10000000.0))
+ for f in bm.faces:
+ if not f.select:
+ continue
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ max_.x = max(max_.x, uv.x)
+ max_.y = max(max_.y, uv.y)
+ min_.x = min(min_.x, uv.x)
+ min_.y = min(min_.y, uv.y)
+ center = Vector(((max_.x + min_.x) / 2.0, (max_.y + min_.y) / 2.0))
+
+ elif self.base == 'UV_SEL':
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if not bm.loops.layers.uv:
+ return None
+ uv_layer = bm.loops.layers.uv.verify()
+
+ max_ = Vector((-10000000.0, -10000000.0))
+ min_ = Vector((10000000.0, 10000000.0))
+ for f in bm.faces:
+ if not f.select:
+ continue
+ for l in f.loops:
+ if not l[uv_layer].select:
+ continue
+ uv = l[uv_layer].uv
+ max_.x = max(max_.x, uv.x)
+ max_.y = max(max_.y, uv.y)
+ min_.x = min(min_.x, uv.x)
+ min_.y = min(min_.y, uv.y)
+ center = Vector(((max_.x + min_.x) / 2.0, (max_.y + min_.y) / 2.0))
+
+ elif self.base == 'TEXTURE':
+ min_ = Vector((0.0, 0.0))
+ max_ = Vector((1.0, 1.0))
+ center = Vector((0.5, 0.5))
+ else:
+ self.report({'ERROR'}, "Unknown Operation")
+ return {'CANCELLED'}
+
+ if self.position == 'CENTER':
+ cx = center.x
+ cy = center.y
+ elif self.position == 'LEFT_TOP':
+ cx = min_.x
+ cy = max_.y
+ elif self.position == 'LEFT_MIDDLE':
+ cx = min_.x
+ cy = center.y
+ elif self.position == 'LEFT_BOTTOM':
+ cx = min_.x
+ cy = min_.y
+ elif self.position == 'MIDDLE_TOP':
+ cx = center.x
+ cy = max_.y
+ elif self.position == 'MIDDLE_BOTTOM':
+ cx = center.x
+ cy = min_.y
+ elif self.position == 'RIGHT_TOP':
+ cx = max_.x
+ cy = max_.y
+ elif self.position == 'RIGHT_MIDDLE':
+ cx = max_.x
+ cy = center.y
+ elif self.position == 'RIGHT_BOTTOM':
+ cx = max_.x
+ cy = min_.y
+ else:
+ self.report({'ERROR'}, "Unknown Operation")
+ return {'CANCELLED'}
+
+ cx = cx * bd_size[0]
+ cy = cy * bd_size[1]
+
+ space.cursor_location = Vector((cx, cy))
+
+ return {'FINISHED'}
diff --git a/magic_uv/op/copy_paste_uv.py b/magic_uv/op/copy_paste_uv.py
new file mode 100644
index 00000000..fca412ad
--- /dev/null
+++ b/magic_uv/op/copy_paste_uv.py
@@ -0,0 +1,755 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>, Jace Priester"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bmesh
+import bpy.utils
+from bpy.props import (
+ StringProperty,
+ BoolProperty,
+ IntProperty,
+ EnumProperty,
+)
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..utils import compatibility as compat
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+def get_copy_uv_layers(ops_obj, bm, uv_map):
+ uv_layers = []
+ if uv_map == "__default":
+ if not bm.loops.layers.uv:
+ ops_obj.report(
+ {'WARNING'}, "Object must have more than one UV map")
+ return None
+ uv_layers.append(bm.loops.layers.uv.verify())
+ ops_obj.report({'INFO'}, "Copy UV coordinate")
+ elif uv_map == "__all":
+ for uv in bm.loops.layers.uv.keys():
+ uv_layers.append(bm.loops.layers.uv[uv])
+ ops_obj.report({'INFO'}, "Copy UV coordinate (UV map: ALL)")
+ else:
+ uv_layers.append(bm.loops.layers.uv[uv_map])
+ ops_obj.report(
+ {'INFO'}, "Copy UV coordinate (UV map:{})".format(uv_map))
+
+ return uv_layers
+
+
+def get_paste_uv_layers(ops_obj, obj, bm, src_info, uv_map):
+ uv_layers = []
+ if uv_map == "__default":
+ if not bm.loops.layers.uv:
+ ops_obj.report(
+ {'WARNING'}, "Object must have more than one UV map")
+ return None
+ uv_layers.append(bm.loops.layers.uv.verify())
+ ops_obj.report({'INFO'}, "Paste UV coordinate")
+ elif uv_map == "__new":
+ new_uv_map = common.create_new_uv_map(obj)
+ if not new_uv_map:
+ ops_obj.report({'WARNING'},
+ "Reached to the maximum number of UV map")
+ return None
+ uv_layers.append(bm.loops.layers.uv[new_uv_map.name])
+ ops_obj.report(
+ {'INFO'}, "Paste UV coordinate (UV map:{})".format(new_uv_map))
+ elif uv_map == "__all":
+ for src_layer in src_info.keys():
+ if src_layer not in bm.loops.layers.uv.keys():
+ new_uv_map = common.create_new_uv_map(obj, src_layer)
+ if not new_uv_map:
+ ops_obj.report({'WARNING'},
+ "Reached to the maximum number of UV map")
+ return None
+ uv_layers.append(bm.loops.layers.uv[src_layer])
+ ops_obj.report({'INFO'}, "Paste UV coordinate (UV map: ALL)")
+ else:
+ uv_layers.append(bm.loops.layers.uv[uv_map])
+ ops_obj.report(
+ {'INFO'}, "Paste UV coordinate (UV map:{})".format(uv_map))
+
+ return uv_layers
+
+
+def get_src_face_info(ops_obj, bm, uv_layers, only_select=False):
+ src_info = {}
+ for layer in uv_layers:
+ face_info = []
+ for face in bm.faces:
+ if not only_select or face.select:
+ info = {
+ "index": face.index,
+ "uvs": [l[layer].uv.copy() for l in face.loops],
+ "pin_uvs": [l[layer].pin_uv for l in face.loops],
+ "seams": [l.edge.seam for l in face.loops],
+ }
+ face_info.append(info)
+ if not face_info:
+ ops_obj.report({'WARNING'}, "No faces are selected")
+ return None
+ src_info[layer.name] = face_info
+
+ return src_info
+
+
+def get_dest_face_info(ops_obj, bm, uv_layers, src_info, strategy,
+ only_select=False):
+ dest_info = {}
+ for layer in uv_layers:
+ face_info = []
+ for face in bm.faces:
+ if not only_select or face.select:
+ info = {
+ "index": face.index,
+ "uvs": [l[layer].uv.copy() for l in face.loops],
+ }
+ face_info.append(info)
+ if not face_info:
+ ops_obj.report({'WARNING'}, "No faces are selected")
+ return None
+ key = list(src_info.keys())[0]
+ src_face_count = len(src_info[key])
+ dest_face_count = len(face_info)
+ if strategy == 'N_N' and src_face_count != dest_face_count:
+ ops_obj.report(
+ {'WARNING'},
+ "Number of selected faces is different from copied" +
+ "(src:{}, dest:{})"
+ .format(src_face_count, dest_face_count))
+ return None
+ dest_info[layer.name] = face_info
+
+ return dest_info
+
+
+def _get_select_history_src_face_info(ops_obj, bm, uv_layers):
+ src_info = {}
+ for layer in uv_layers:
+ face_info = []
+ for hist in bm.select_history:
+ if isinstance(hist, bmesh.types.BMFace) and hist.select:
+ info = {
+ "index": hist.index,
+ "uvs": [l[layer].uv.copy() for l in hist.loops],
+ "pin_uvs": [l[layer].pin_uv for l in hist.loops],
+ "seams": [l.edge.seam for l in hist.loops],
+ }
+ face_info.append(info)
+ if not face_info:
+ ops_obj.report({'WARNING'}, "No faces are selected")
+ return None
+ src_info[layer.name] = face_info
+
+ return src_info
+
+
+def _get_select_history_dest_face_info(ops_obj, bm, uv_layers, src_info,
+ strategy):
+ dest_info = {}
+ for layer in uv_layers:
+ face_info = []
+ for hist in bm.select_history:
+ if isinstance(hist, bmesh.types.BMFace) and hist.select:
+ info = {
+ "index": hist.index,
+ "uvs": [l[layer].uv.copy() for l in hist.loops],
+ }
+ face_info.append(info)
+ if not face_info:
+ ops_obj.report({'WARNING'}, "No faces are selected")
+ return None
+ key = list(src_info.keys())[0]
+ src_face_count = len(src_info[key])
+ dest_face_count = len(face_info)
+ if strategy == 'N_N' and src_face_count != dest_face_count:
+ ops_obj.report(
+ {'WARNING'},
+ "Number of selected faces is different from copied" +
+ "(src:{}, dest:{})"
+ .format(src_face_count, dest_face_count))
+ return None
+ dest_info[layer.name] = face_info
+
+ return dest_info
+
+
+def paste_uv(ops_obj, bm, src_info, dest_info, uv_layers, strategy, flip,
+ rotate, copy_seams):
+ for slayer_name, dlayer in zip(src_info.keys(), uv_layers):
+ src_faces = src_info[slayer_name]
+ dest_faces = dest_info[dlayer.name]
+
+ for idx, dinfo in enumerate(dest_faces):
+ sinfo = None
+ if strategy == 'N_N':
+ sinfo = src_faces[idx]
+ elif strategy == 'N_M':
+ sinfo = src_faces[idx % len(src_faces)]
+
+ suv = sinfo["uvs"]
+ spuv = sinfo["pin_uvs"]
+ ss = sinfo["seams"]
+ if len(sinfo["uvs"]) != len(dinfo["uvs"]):
+ ops_obj.report({'WARNING'}, "Some faces are different size")
+ return -1
+
+ suvs_fr = [uv for uv in suv]
+ spuvs_fr = [pin_uv for pin_uv in spuv]
+ ss_fr = [s for s in ss]
+
+ # flip UVs
+ if flip is True:
+ suvs_fr.reverse()
+ spuvs_fr.reverse()
+ ss_fr.reverse()
+
+ # rotate UVs
+ for _ in range(rotate):
+ uv = suvs_fr.pop()
+ pin_uv = spuvs_fr.pop()
+ s = ss_fr.pop()
+ suvs_fr.insert(0, uv)
+ spuvs_fr.insert(0, pin_uv)
+ ss_fr.insert(0, s)
+
+ # paste UVs
+ for l, suv, spuv, ss in zip(bm.faces[dinfo["index"]].loops,
+ suvs_fr, spuvs_fr, ss_fr):
+ l[dlayer].uv = suv
+ l[dlayer].pin_uv = spuv
+ if copy_seams is True:
+ l.edge.seam = ss
+
+ return 0
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "copy_paste_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ src_info = None
+
+ scene.muv_props.copy_paste_uv = Props()
+ scene.muv_props.copy_paste_uv_selseq = Props()
+
+ scene.muv_copy_paste_uv_enabled = BoolProperty(
+ name="Copy/Paste UV Enabled",
+ description="Copy/Paste UV is enabled",
+ default=False
+ )
+ scene.muv_copy_paste_uv_copy_seams = BoolProperty(
+ name="Seams",
+ description="Copy Seams",
+ default=True
+ )
+ scene.muv_copy_paste_uv_mode = EnumProperty(
+ items=[
+ ('DEFAULT', "Default", "Default Mode"),
+ ('SEL_SEQ', "Selection Sequence", "Selection Sequence Mode")
+ ],
+ name="Copy/Paste UV Mode",
+ description="Copy/Paste UV Mode",
+ default='DEFAULT'
+ )
+ scene.muv_copy_paste_uv_strategy = EnumProperty(
+ name="Strategy",
+ description="Paste Strategy",
+ items=[
+ ('N_N', 'N:N', 'Number of faces must be equal to source'),
+ ('N_M', 'N:M', 'Number of faces must not be equal to source')
+ ],
+ default='N_M'
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_props.copy_paste_uv
+ del scene.muv_props.copy_paste_uv_selseq
+ del scene.muv_copy_paste_uv_enabled
+ del scene.muv_copy_paste_uv_copy_seams
+ del scene.muv_copy_paste_uv_mode
+ del scene.muv_copy_paste_uv_strategy
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_CopyPasteUV_CopyUV(bpy.types.Operator):
+ """
+ Operation class: Copy UV coordinate
+ """
+
+ bl_idname = "uv.muv_ot_copy_paste_uv_copy_uv"
+ bl_label = "Copy UV"
+ bl_description = "Copy UV coordinate"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ uv_map = StringProperty(default="__default", options={'HIDDEN'})
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ props = context.scene.muv_props.copy_paste_uv
+ obj = context.active_object
+ bm = common.create_bmesh(obj)
+
+ # get UV layer
+ uv_layers = get_copy_uv_layers(self, bm, self.uv_map)
+ if not uv_layers:
+ return {'CANCELLED'}
+
+ # get selected face
+ src_info = get_src_face_info(self, bm, uv_layers, True)
+ if src_info is None:
+ return {'CANCELLED'}
+ props.src_info = src_info
+
+ face_count = len(props.src_info[list(props.src_info.keys())[0]])
+ self.report({'INFO'}, "{} face(s) are copied".format(face_count))
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+class MUV_MT_CopyPasteUV_CopyUV(bpy.types.Menu):
+ """
+ Menu class: Copy UV coordinate
+ """
+
+ bl_idname = "uv.muv_mt_copy_paste_uv_copy_uv"
+ bl_label = "Copy UV (Menu)"
+ bl_description = "Menu of Copy UV coordinate"
+
+ @classmethod
+ def poll(cls, context):
+ return _is_valid_context(context)
+
+ def draw(self, context):
+ layout = self.layout
+ # create sub menu
+ obj = context.active_object
+ bm = common.create_bmesh(obj)
+ uv_maps = bm.loops.layers.uv.keys()
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_CopyUV.bl_idname,
+ text="[Default]")
+ ops.uv_map = "__default"
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_CopyUV.bl_idname,
+ text="[All]")
+ ops.uv_map = "__all"
+
+ for m in uv_maps:
+ ops = layout.operator(MUV_OT_CopyPasteUV_CopyUV.bl_idname, text=m)
+ ops.uv_map = m
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_CopyPasteUV_PasteUV(bpy.types.Operator):
+ """
+ Operation class: Paste UV coordinate
+ """
+
+ bl_idname = "uv.muv_ot_copy_paste_uv_paste_uv"
+ bl_label = "Paste UV"
+ bl_description = "Paste UV coordinate"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ uv_map = StringProperty(default="__default", options={'HIDDEN'})
+ strategy = EnumProperty(
+ name="Strategy",
+ description="Paste Strategy",
+ items=[
+ ('N_N', 'N:N', 'Number of faces must be equal to source'),
+ ('N_M', 'N:M', 'Number of faces must not be equal to source')
+ ],
+ default="N_M"
+ )
+ flip_copied_uv = BoolProperty(
+ name="Flip Copied UV",
+ description="Flip Copied UV...",
+ default=False
+ )
+ rotate_copied_uv = IntProperty(
+ default=0,
+ name="Rotate Copied UV",
+ min=0,
+ max=30
+ )
+ copy_seams = BoolProperty(
+ name="Seams",
+ description="Copy Seams",
+ default=True
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv
+ if not props.src_info:
+ return False
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ props = context.scene.muv_props.copy_paste_uv
+ if not props.src_info:
+ self.report({'WARNING'}, "Need copy UV at first")
+ return {'CANCELLED'}
+ obj = context.active_object
+ bm = common.create_bmesh(obj)
+
+ # get UV layer
+ uv_layers = get_paste_uv_layers(self, obj, bm, props.src_info,
+ self.uv_map)
+ if not uv_layers:
+ return {'CANCELLED'}
+
+ # get selected face
+ dest_info = get_dest_face_info(self, bm, uv_layers,
+ props.src_info, self.strategy, True)
+ if dest_info is None:
+ return {'CANCELLED'}
+
+ # paste
+ ret = paste_uv(self, bm, props.src_info, dest_info, uv_layers,
+ self.strategy, self.flip_copied_uv,
+ self.rotate_copied_uv, self.copy_seams)
+ if ret:
+ return {'CANCELLED'}
+
+ face_count = len(props.src_info[list(dest_info.keys())[0]])
+ self.report({'INFO'}, "{} face(s) are pasted".format(face_count))
+
+ bmesh.update_edit_mesh(obj.data)
+
+ if compat.check_version(2, 80, 0) < 0:
+ if self.copy_seams is True:
+ obj.data.show_edge_seams = True
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+class MUV_MT_CopyPasteUV_PasteUV(bpy.types.Menu):
+ """
+ Menu class: Paste UV coordinate
+ """
+
+ bl_idname = "uv.muv_mt_copy_paste_uv_paste_uv"
+ bl_label = "Paste UV (Menu)"
+ bl_description = "Menu of Paste UV coordinate"
+
+ @classmethod
+ def poll(cls, context):
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv
+ if not props.src_info:
+ return False
+ return _is_valid_context(context)
+
+ def draw(self, context):
+ sc = context.scene
+ layout = self.layout
+ # create sub menu
+ obj = context.active_object
+ bm = common.create_bmesh(obj)
+ uv_maps = bm.loops.layers.uv.keys()
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname,
+ text="[Default]")
+ ops.uv_map = "__default"
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname,
+ text="[New]")
+ ops.uv_map = "__new"
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname,
+ text="[All]")
+ ops.uv_map = "__all"
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
+ for m in uv_maps:
+ ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname, text=m)
+ ops.uv_map = m
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_CopyPasteUV_SelSeqCopyUV(bpy.types.Operator):
+ """
+ Operation class: Copy UV coordinate by selection sequence
+ """
+
+ bl_idname = "uv.muv_ot_copy_paste_uv_selseq_copy_uv"
+ bl_label = "Copy UV (Selection Sequence)"
+ bl_description = "Copy UV data by selection sequence"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ uv_map = StringProperty(default="__default", options={'HIDDEN'})
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ props = context.scene.muv_props.copy_paste_uv_selseq
+ obj = context.active_object
+ bm = common.create_bmesh(obj)
+
+ # get UV layer
+ uv_layers = get_copy_uv_layers(self, bm, self.uv_map)
+ if not uv_layers:
+ return {'CANCELLED'}
+
+ # get selected face
+ src_info = _get_select_history_src_face_info(self, bm, uv_layers)
+ if src_info is None:
+ return {'CANCELLED'}
+ props.src_info = src_info
+
+ face_count = len(props.src_info[list(props.src_info.keys())[0]])
+ self.report({'INFO'}, "{} face(s) are selected".format(face_count))
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+class MUV_MT_CopyPasteUV_SelSeqCopyUV(bpy.types.Menu):
+ """
+ Menu class: Copy UV coordinate by selection sequence
+ """
+
+ bl_idname = "uv.muv_mt_copy_paste_uv_selseq_copy_uv"
+ bl_label = "Copy UV (Selection Sequence) (Menu)"
+ bl_description = "Menu of Copy UV coordinate by selection sequence"
+
+ @classmethod
+ def poll(cls, context):
+ return _is_valid_context(context)
+
+ def draw(self, context):
+ layout = self.layout
+ obj = context.active_object
+ bm = common.create_bmesh(obj)
+ uv_maps = bm.loops.layers.uv.keys()
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV.bl_idname,
+ text="[Default]")
+ ops.uv_map = "__default"
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV.bl_idname,
+ text="[All]")
+ ops.uv_map = "__all"
+
+ for m in uv_maps:
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV.bl_idname,
+ text=m)
+ ops.uv_map = m
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_CopyPasteUV_SelSeqPasteUV(bpy.types.Operator):
+ """
+ Operation class: Paste UV coordinate by selection sequence
+ """
+
+ bl_idname = "uv.muv_ot_copy_paste_uv_selseq_paste_uv"
+ bl_label = "Paste UV (Selection Sequence)"
+ bl_description = "Paste UV coordinate by selection sequence"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ uv_map = StringProperty(default="__default", options={'HIDDEN'})
+ strategy = EnumProperty(
+ name="Strategy",
+ description="Paste Strategy",
+ items=[
+ ('N_N', 'N:N', 'Number of faces must be equal to source'),
+ ('N_M', 'N:M', 'Number of faces must not be equal to source')
+ ],
+ default="N_M"
+ )
+ flip_copied_uv = BoolProperty(
+ name="Flip Copied UV",
+ description="Flip Copied UV...",
+ default=False
+ )
+ rotate_copied_uv = IntProperty(
+ default=0,
+ name="Rotate Copied UV",
+ min=0,
+ max=30
+ )
+ copy_seams = BoolProperty(
+ name="Seams",
+ description="Copy Seams",
+ default=True
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv_selseq
+ if not props.src_info:
+ return False
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ props = context.scene.muv_props.copy_paste_uv_selseq
+ if not props.src_info:
+ self.report({'WARNING'}, "Need copy UV at first")
+ return {'CANCELLED'}
+ obj = context.active_object
+ bm = common.create_bmesh(obj)
+
+ # get UV layer
+ uv_layers = get_paste_uv_layers(self, obj, bm, props.src_info,
+ self.uv_map)
+ if not uv_layers:
+ return {'CANCELLED'}
+
+ # get selected face
+ dest_info = _get_select_history_dest_face_info(self, bm, uv_layers,
+ props.src_info,
+ self.strategy)
+ if dest_info is None:
+ return {'CANCELLED'}
+
+ # paste
+ ret = paste_uv(self, bm, props.src_info, dest_info, uv_layers,
+ self.strategy, self.flip_copied_uv,
+ self.rotate_copied_uv, self.copy_seams)
+ if ret:
+ return {'CANCELLED'}
+
+ face_count = len(props.src_info[list(dest_info.keys())[0]])
+ self.report({'INFO'}, "{} face(s) are pasted".format(face_count))
+
+ bmesh.update_edit_mesh(obj.data)
+
+ if compat.check_version(2, 80, 0) < 0:
+ if self.copy_seams is True:
+ obj.data.show_edge_seams = True
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+class MUV_MT_CopyPasteUV_SelSeqPasteUV(bpy.types.Menu):
+ """
+ Menu class: Paste UV coordinate by selection sequence
+ """
+
+ bl_idname = "uv.muv_mt_copy_paste_uv_selseq_paste_uv"
+ bl_label = "Paste UV (Selection Sequence) (Menu)"
+ bl_description = "Menu of Paste UV coordinate by selection sequence"
+
+ @classmethod
+ def poll(cls, context):
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv_selseq
+ if not props.src_uvs or not props.src_pin_uvs:
+ return False
+ return _is_valid_context(context)
+
+ def draw(self, context):
+ sc = context.scene
+ layout = self.layout
+ # create sub menu
+ obj = context.active_object
+ bm = common.create_bmesh(obj)
+ uv_maps = bm.loops.layers.uv.keys()
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname,
+ text="[Default]")
+ ops.uv_map = "__default"
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname,
+ text="[New]")
+ ops.uv_map = "__new"
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname,
+ text="[All]")
+ ops.uv_map = "__all"
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
+
+ for m in uv_maps:
+ ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname,
+ text=m)
+ ops.uv_map = m
+ ops.copy_seams = sc.muv_copy_paste_uv_copy_seams
+ ops.strategy = sc.muv_copy_paste_uv_strategy
diff --git a/magic_uv/op/copy_paste_uv_object.py b/magic_uv/op/copy_paste_uv_object.py
new file mode 100644
index 00000000..23ff412b
--- /dev/null
+++ b/magic_uv/op/copy_paste_uv_object.py
@@ -0,0 +1,306 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bmesh
+import bpy
+from bpy.props import (
+ StringProperty,
+ BoolProperty,
+)
+
+from .copy_paste_uv import (
+ get_copy_uv_layers,
+ get_src_face_info,
+ get_paste_uv_layers,
+ get_dest_face_info,
+ paste_uv,
+)
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..utils import compatibility as compat
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only object mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'OBJECT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "copy_paste_uv_object"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ src_info = None
+
+ scene.muv_props.copy_paste_uv_object = Props()
+
+ scene.muv_copy_paste_uv_object_copy_seams = BoolProperty(
+ name="Seams",
+ description="Copy Seams",
+ default=True
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_props.copy_paste_uv_object
+ del scene.muv_copy_paste_uv_object_copy_seams
+
+
+def memorize_view_3d_mode(fn):
+ def __memorize_view_3d_mode(self, context):
+ mode_orig = bpy.context.object.mode
+ result = fn(self, context)
+ bpy.ops.object.mode_set(mode=mode_orig)
+ return result
+ return __memorize_view_3d_mode
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_CopyPasteUVObject_CopyUV(bpy.types.Operator):
+ """
+ Operation class: Copy UV coordinate among objects
+ """
+
+ bl_idname = "object.muv_ot_copy_paste_uv_object_copy_uv"
+ bl_label = "Copy UV (Among Objects)"
+ bl_description = "Copy UV coordinate (Among Objects)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ uv_map = StringProperty(default="__default", options={'HIDDEN'})
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ @memorize_view_3d_mode
+ def execute(self, context):
+ props = context.scene.muv_props.copy_paste_uv_object
+ bpy.ops.object.mode_set(mode='EDIT')
+ obj = context.active_object
+ bm = common.create_bmesh(obj)
+
+ # get UV layer
+ uv_layers = get_copy_uv_layers(self, bm, self.uv_map)
+ if not uv_layers:
+ return {'CANCELLED'}
+
+ # get selected face
+ src_info = get_src_face_info(self, bm, uv_layers)
+ if src_info is None:
+ return {'CANCELLED'}
+ props.src_info = src_info
+
+ self.report({'INFO'},
+ "{}'s UV coordinates are copied".format(obj.name))
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+class MUV_MT_CopyPasteUVObject_CopyUV(bpy.types.Menu):
+ """
+ Menu class: Copy UV coordinate among objects
+ """
+
+ bl_idname = "object.muv_mt_copy_paste_uv_object_copy_uv"
+ bl_label = "Copy UV (Among Objects) (Menu)"
+ bl_description = "Menu of Copy UV coordinate (Among Objects)"
+
+ @classmethod
+ def poll(cls, context):
+ return _is_valid_context(context)
+
+ def draw(self, _):
+ layout = self.layout
+ # create sub menu
+ uv_maps = compat.get_object_uv_layers(bpy.context.active_object).keys()
+
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_CopyUV.bl_idname,
+ text="[Default]")
+ ops.uv_map = "__default"
+
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_CopyUV.bl_idname,
+ text="[All]")
+ ops.uv_map = "__all"
+
+ for m in uv_maps:
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_CopyUV.bl_idname,
+ text=m)
+ ops.uv_map = m
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_CopyPasteUVObject_PasteUV(bpy.types.Operator):
+ """
+ Operation class: Paste UV coordinate among objects
+ """
+
+ bl_idname = "object.muv_ot_copy_paste_uv_object_paste_uv"
+ bl_label = "Paste UV (Among Objects)"
+ bl_description = "Paste UV coordinate (Among Objects)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ uv_map = StringProperty(default="__default", options={'HIDDEN'})
+ copy_seams = BoolProperty(
+ name="Seams",
+ description="Copy Seams",
+ default=True
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv_object
+ if not props.src_info:
+ return False
+ return _is_valid_context(context)
+
+ @memorize_view_3d_mode
+ def execute(self, context):
+ props = context.scene.muv_props.copy_paste_uv_object
+ if not props.src_info:
+ self.report({'WARNING'}, "Need copy UV at first")
+ return {'CANCELLED'}
+
+ for o in bpy.data.objects:
+ if not compat.object_has_uv_layers(o):
+ continue
+ if not compat.get_object_select(o):
+ continue
+
+ bpy.ops.object.mode_set(mode='OBJECT')
+ compat.set_active_object(o)
+ bpy.ops.object.mode_set(mode='EDIT')
+
+ obj = context.active_object
+ bm = common.create_bmesh(obj)
+
+ # get UV layer
+ uv_layers = get_paste_uv_layers(self, obj, bm, props.src_info,
+ self.uv_map)
+ if not uv_layers:
+ return {'CANCELLED'}
+
+ # get selected face
+ dest_info = get_dest_face_info(self, bm, uv_layers,
+ props.src_info, 'N_N')
+ if dest_info is None:
+ return {'CANCELLED'}
+
+ # paste
+ ret = paste_uv(self, bm, props.src_info, dest_info, uv_layers,
+ 'N_N', 0, 0, self.copy_seams)
+ if ret:
+ return {'CANCELLED'}
+
+ bmesh.update_edit_mesh(obj.data)
+
+ if compat.check_version(2, 80, 0) < 0:
+ if self.copy_seams is True:
+ obj.data.show_edge_seams = True
+
+ self.report(
+ {'INFO'}, "{}'s UV coordinates are pasted".format(obj.name))
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+class MUV_MT_CopyPasteUVObject_PasteUV(bpy.types.Menu):
+ """
+ Menu class: Paste UV coordinate among objects
+ """
+
+ bl_idname = "object.muv_mt_copy_paste_uv_object_paste_uv"
+ bl_label = "Paste UV (Among Objects) (Menu)"
+ bl_description = "Menu of Paste UV coordinate (Among Objects)"
+
+ @classmethod
+ def poll(cls, context):
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv_object
+ if not props.src_info:
+ return False
+ return _is_valid_context(context)
+
+ def draw(self, context):
+ sc = context.scene
+ layout = self.layout
+ # create sub menu
+ uv_maps = []
+ for obj in bpy.data.objects:
+ if not compat.object_has_uv_layers(obj):
+ continue
+ if not compat.get_object_select(obj):
+ continue
+ uv_maps.extend(compat.get_object_uv_layers(obj).keys())
+
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname,
+ text="[Default]")
+ ops.uv_map = "__default"
+ ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams
+
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname,
+ text="[New]")
+ ops.uv_map = "__new"
+ ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams
+
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname,
+ text="[All]")
+ ops.uv_map = "__all"
+ ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams
+
+ for m in uv_maps:
+ ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname,
+ text=m)
+ ops.uv_map = m
+ ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams
diff --git a/magic_uv/op/copy_paste_uv_uvedit.py b/magic_uv/op/copy_paste_uv_uvedit.py
new file mode 100644
index 00000000..b448f866
--- /dev/null
+++ b/magic_uv/op/copy_paste_uv_uvedit.py
@@ -0,0 +1,198 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "imdjs, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import math
+from math import atan2, sin, cos
+
+import bpy
+import bmesh
+from mathutils import Vector
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
+ # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
+ # after the execution
+ for space in context.area.spaces:
+ if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'):
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "copy_paste_uv_uvedit"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ src_uvs = None
+
+ scene.muv_props.copy_paste_uv_uvedit = Props()
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_props.copy_paste_uv_uvedit
+
+
+@BlClassRegistry()
+class MUV_OT_CopyPasteUVUVEdit_CopyUV(bpy.types.Operator):
+ """
+ Operation class: Copy UV coordinate on UV/Image Editor
+ """
+
+ bl_idname = "uv.muv_ot_copy_paste_uv_uvedit_copy_uv"
+ bl_label = "Copy UV (UV/Image Editor)"
+ bl_description = "Copy UV coordinate (only selected in UV/Image Editor)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ props = context.scene.muv_props.copy_paste_uv_uvedit
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ uv_layer = bm.loops.layers.uv.verify()
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ props.src_uvs = []
+ for face in bm.faces:
+ if not face.select:
+ continue
+ skip = False
+ for l in face.loops:
+ if not l[uv_layer].select:
+ skip = True
+ break
+ if skip:
+ continue
+ props.src_uvs.append([l[uv_layer].uv.copy() for l in face.loops])
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+class MUV_OT_CopyPasteUVUVEdit_PasteUV(bpy.types.Operator):
+ """
+ Operation class: Paste UV coordinate on UV/Image Editor
+ """
+
+ bl_idname = "uv.muv_ot_copy_paste_uv_uvedit_paste_uv"
+ bl_label = "Paste UV (UV/Image Editor)"
+ bl_description = "Paste UV coordinate (only selected in UV/Image Editor)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ sc = context.scene
+ props = sc.muv_props.copy_paste_uv_uvedit
+ if not props.src_uvs:
+ return False
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ props = context.scene.muv_props.copy_paste_uv_uvedit
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ uv_layer = bm.loops.layers.uv.verify()
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ dest_uvs = []
+ dest_face_indices = []
+ for face in bm.faces:
+ if not face.select:
+ continue
+ skip = False
+ for l in face.loops:
+ if not l[uv_layer].select:
+ skip = True
+ break
+ if skip:
+ continue
+ dest_face_indices.append(face.index)
+ uvs = [l[uv_layer].uv.copy() for l in face.loops]
+ dest_uvs.append(uvs)
+
+ for suvs, duvs in zip(props.src_uvs, dest_uvs):
+ src_diff = suvs[1] - suvs[0]
+ dest_diff = duvs[1] - duvs[0]
+
+ src_base = suvs[0]
+ dest_base = duvs[0]
+
+ src_rad = atan2(src_diff.y, src_diff.x)
+ dest_rad = atan2(dest_diff.y, dest_diff.x)
+ if src_rad < dest_rad:
+ radian = dest_rad - src_rad
+ elif src_rad > dest_rad:
+ radian = math.pi * 2 - (src_rad - dest_rad)
+ else: # src_rad == dest_rad
+ radian = 0.0
+
+ ratio = dest_diff.length / src_diff.length
+ break
+
+ for suvs, fidx in zip(props.src_uvs, dest_face_indices):
+ for l, suv in zip(bm.faces[fidx].loops, suvs):
+ base = suv - src_base
+ radian_ref = atan2(base.y, base.x)
+ radian_fin = (radian + radian_ref)
+ length = base.length
+ turn = Vector((length * cos(radian_fin),
+ length * sin(radian_fin)))
+ target_uv = Vector((turn.x * ratio, turn.y * ratio)) + \
+ dest_base
+ l[uv_layer].uv = target_uv
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
diff --git a/magic_uv/op/flip_rotate_uv.py b/magic_uv/op/flip_rotate_uv.py
new file mode 100644
index 00000000..c4c05169
--- /dev/null
+++ b/magic_uv/op/flip_rotate_uv.py
@@ -0,0 +1,232 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+import bmesh
+from bpy.props import (
+ BoolProperty,
+ IntProperty,
+)
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..utils import compatibility as compat
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+def _get_uv_layer(ops_obj, bm):
+ # get UV layer
+ if not bm.loops.layers.uv:
+ ops_obj.report({'WARNING'}, "Object must have more than one UV map")
+ return None
+ uv_layer = bm.loops.layers.uv.verify()
+
+ return uv_layer
+
+
+def _get_src_face_info(ops_obj, bm, uv_layers, only_select=False):
+ src_info = {}
+ for layer in uv_layers:
+ face_info = []
+ for face in bm.faces:
+ if not only_select or face.select:
+ info = {
+ "index": face.index,
+ "uvs": [l[layer].uv.copy() for l in face.loops],
+ "pin_uvs": [l[layer].pin_uv for l in face.loops],
+ "seams": [l.edge.seam for l in face.loops],
+ }
+ face_info.append(info)
+ if not face_info:
+ ops_obj.report({'WARNING'}, "No faces are selected")
+ return None
+ src_info[layer.name] = face_info
+
+ return src_info
+
+
+def _paste_uv(ops_obj, bm, src_info, dest_info, uv_layers, strategy, flip,
+ rotate, copy_seams):
+ for slayer_name, dlayer in zip(src_info.keys(), uv_layers):
+ src_faces = src_info[slayer_name]
+ dest_faces = dest_info[dlayer.name]
+
+ for idx, dinfo in enumerate(dest_faces):
+ sinfo = None
+ if strategy == 'N_N':
+ sinfo = src_faces[idx]
+ elif strategy == 'N_M':
+ sinfo = src_faces[idx % len(src_faces)]
+
+ suv = sinfo["uvs"]
+ spuv = sinfo["pin_uvs"]
+ ss = sinfo["seams"]
+ if len(sinfo["uvs"]) != len(dinfo["uvs"]):
+ ops_obj.report({'WARNING'}, "Some faces are different size")
+ return -1
+
+ suvs_fr = [uv for uv in suv]
+ spuvs_fr = [pin_uv for pin_uv in spuv]
+ ss_fr = [s for s in ss]
+
+ # flip UVs
+ if flip is True:
+ suvs_fr.reverse()
+ spuvs_fr.reverse()
+ ss_fr.reverse()
+
+ # rotate UVs
+ for _ in range(rotate):
+ uv = suvs_fr.pop()
+ pin_uv = spuvs_fr.pop()
+ s = ss_fr.pop()
+ suvs_fr.insert(0, uv)
+ spuvs_fr.insert(0, pin_uv)
+ ss_fr.insert(0, s)
+
+ # paste UVs
+ for l, suv, spuv, ss in zip(bm.faces[dinfo["index"]].loops,
+ suvs_fr, spuvs_fr, ss_fr):
+ l[dlayer].uv = suv
+ l[dlayer].pin_uv = spuv
+ if copy_seams is True:
+ l.edge.seam = ss
+
+ return 0
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "flip_rotate_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_flip_rotate_uv_enabled = BoolProperty(
+ name="Flip/Rotate UV Enabled",
+ description="Flip/Rotate UV is enabled",
+ default=False
+ )
+ scene.muv_flip_rotate_uv_seams = BoolProperty(
+ name="Seams",
+ description="Seams",
+ default=True
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_flip_rotate_uv_enabled
+ del scene.muv_flip_rotate_uv_seams
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_FlipRotate(bpy.types.Operator):
+ """
+ Operation class: Flip and Rotate UV coordinate
+ """
+
+ bl_idname = "uv.muv_ot_flip_rotate_uv"
+ bl_label = "Flip/Rotate UV"
+ bl_description = "Flip/Rotate UV coordinate"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ flip = BoolProperty(
+ name="Flip UV",
+ description="Flip UV...",
+ default=False
+ )
+ rotate = IntProperty(
+ default=0,
+ name="Rotate UV",
+ min=0,
+ max=30
+ )
+ seams = BoolProperty(
+ name="Seams",
+ description="Seams",
+ default=True
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ self.report({'INFO'}, "Flip/Rotate UV")
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ # get UV layer
+ uv_layer = _get_uv_layer(self, bm)
+ if not uv_layer:
+ return {'CANCELLED'}
+
+ # get selected face
+ src_info = _get_src_face_info(self, bm, [uv_layer], True)
+ if not src_info:
+ return {'CANCELLED'}
+
+ face_count = len(src_info[list(src_info.keys())[0]])
+ self.report({'INFO'}, "{} face(s) are selected".format(face_count))
+
+ # paste
+ ret = _paste_uv(self, bm, src_info, src_info, [uv_layer], 'N_N',
+ self.flip, self.rotate, self.seams)
+ if ret:
+ return {'CANCELLED'}
+
+ bmesh.update_edit_mesh(obj.data)
+
+ if compat.check_version(2, 80, 0) < 0:
+ if self.seams is True:
+ obj.data.show_edge_seams = True
+
+ return {'FINISHED'}
diff --git a/magic_uv/op/mirror_uv.py b/magic_uv/op/mirror_uv.py
new file mode 100644
index 00000000..fb98bb05
--- /dev/null
+++ b/magic_uv/op/mirror_uv.py
@@ -0,0 +1,215 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Keith (Wahooney) Boshoff, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+from bpy.props import (
+ EnumProperty,
+ FloatProperty,
+ BoolProperty,
+)
+import bmesh
+from mathutils import Vector
+
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..utils import compatibility as compat
+from .. import common
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+def _is_vector_similar(v1, v2, error):
+ """
+ Check if two vectors are similar, within an error threshold
+ """
+ within_err_x = abs(v2.x - v1.x) < error
+ within_err_y = abs(v2.y - v1.y) < error
+ within_err_z = abs(v2.z - v1.z) < error
+
+ return within_err_x and within_err_y and within_err_z
+
+
+def _mirror_uvs(uv_layer, src, dst, axis, error):
+ """
+ Copy UV coordinates from one UV face to another
+ """
+ for sl in src.loops:
+ suv = sl[uv_layer].uv.copy()
+ svco = sl.vert.co.copy()
+ for dl in dst.loops:
+ dvco = dl.vert.co.copy()
+ if axis == 'X':
+ dvco.x = -dvco.x
+ elif axis == 'Y':
+ dvco.y = -dvco.y
+ elif axis == 'Z':
+ dvco.z = -dvco.z
+
+ if _is_vector_similar(svco, dvco, error):
+ dl[uv_layer].uv = suv.copy()
+
+
+def _get_face_center(face):
+ """
+ Get center coordinate of the face
+ """
+ center = Vector((0.0, 0.0, 0.0))
+ for v in face.verts:
+ center = center + v.co
+
+ return center / len(face.verts)
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "mirror_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_mirror_uv_enabled = BoolProperty(
+ name="Mirror UV Enabled",
+ description="Mirror UV is enabled",
+ default=False
+ )
+ scene.muv_mirror_uv_axis = EnumProperty(
+ items=[
+ ('X', "X", "Mirror Along X axis"),
+ ('Y', "Y", "Mirror Along Y axis"),
+ ('Z', "Z", "Mirror Along Z axis")
+ ],
+ name="Axis",
+ description="Mirror Axis",
+ default='X'
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_mirror_uv_enabled
+ del scene.muv_mirror_uv_axis
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_MirrorUV(bpy.types.Operator):
+ """
+ Operation class: Mirror UV
+ """
+
+ bl_idname = "uv.muv_ot_mirror_uv"
+ bl_label = "Mirror UV"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ axis = EnumProperty(
+ items=(
+ ('X', "X", "Mirror Along X axis"),
+ ('Y', "Y", "Mirror Along Y axis"),
+ ('Z', "Z", "Mirror Along Z axis")
+ ),
+ name="Axis",
+ description="Mirror Axis",
+ default='X'
+ )
+ error = FloatProperty(
+ name="Error",
+ description="Error threshold",
+ default=0.001,
+ min=0.0,
+ max=100.0,
+ soft_min=0.0,
+ soft_max=1.0
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+
+ error = self.error
+ axis = self.axis
+
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+ if not bm.loops.layers.uv:
+ self.report({'WARNING'},
+ "Object must have more than one UV map")
+ return {'CANCELLED'}
+ uv_layer = bm.loops.layers.uv.verify()
+
+ faces = [f for f in bm.faces if f.select]
+ for f_dst in faces:
+ count = len(f_dst.verts)
+ for f_src in bm.faces:
+ # check if this is a candidate to do mirror UV
+ if f_src.index == f_dst.index:
+ continue
+ if count != len(f_src.verts):
+ continue
+
+ # test if the vertices x values are the same sign
+ dst = _get_face_center(f_dst)
+ src = _get_face_center(f_src)
+ if (dst.x > 0 and src.x > 0) or (dst.x < 0 and src.x < 0):
+ continue
+
+ # invert source axis
+ if axis == 'X':
+ src.x = -src.x
+ elif axis == 'Y':
+ src.y = -src.z
+ elif axis == 'Z':
+ src.z = -src.z
+
+ # do mirror UV
+ if _is_vector_similar(dst, src, error):
+ _mirror_uvs(uv_layer, f_src, f_dst, self.axis, self.error)
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
diff --git a/magic_uv/op/move_uv.py b/magic_uv/op/move_uv.py
new file mode 100644
index 00000000..be019e9f
--- /dev/null
+++ b/magic_uv/op/move_uv.py
@@ -0,0 +1,185 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "kgeogeo, mem, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+from bpy.props import BoolProperty
+import bmesh
+from mathutils import Vector
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+def _find_uv(context):
+ bm = bmesh.from_edit_mesh(context.object.data)
+ topology_dict = []
+ uvs = []
+ active_uv = bm.loops.layers.uv.active
+ for fidx, f in enumerate(bm.faces):
+ for vidx, v in enumerate(f.verts):
+ if v.select:
+ uvs.append(f.loops[vidx][active_uv].uv.copy())
+ topology_dict.append([fidx, vidx])
+
+ return topology_dict, uvs
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "move_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_move_uv_enabled = BoolProperty(
+ name="Move UV Enabled",
+ description="Move UV is enabled",
+ default=False
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_move_uv_enabled
+
+
+@BlClassRegistry()
+class MUV_OT_MoveUV(bpy.types.Operator):
+ """
+ Operator class: Move UV
+ """
+
+ bl_idname = "uv.muv_ot_move_uv"
+ bl_label = "Move UV"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ __running = False
+
+ def __init__(self):
+ self.__topology_dict = []
+ self.__prev_mouse = Vector((0.0, 0.0))
+ self.__offset_uv = Vector((0.0, 0.0))
+ self.__prev_offset_uv = Vector((0.0, 0.0))
+ self.__first_time = True
+ self.__ini_uvs = []
+ self.__operating = False
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return False
+ if cls.is_running(context):
+ return False
+ return _is_valid_context(context)
+
+ @classmethod
+ def is_running(cls, _):
+ return cls.__running
+
+ def modal(self, context, event):
+ if self.__first_time is True:
+ self.__prev_mouse = Vector((
+ event.mouse_region_x, event.mouse_region_y))
+ self.__first_time = False
+ return {'RUNNING_MODAL'}
+
+ # move UV
+ div = 10000
+ self.__offset_uv += Vector((
+ (event.mouse_region_x - self.__prev_mouse.x) / div,
+ (event.mouse_region_y - self.__prev_mouse.y) / div))
+ ouv = self.__offset_uv
+ pouv = self.__prev_offset_uv
+ vec = Vector((ouv.x - ouv.y, ouv.x + ouv.y))
+ dv = vec - pouv
+ self.__prev_offset_uv = vec
+ self.__prev_mouse = Vector((
+ event.mouse_region_x, event.mouse_region_y))
+
+ # check if operation is started
+ if not self.__operating:
+ if event.type == 'LEFTMOUSE' and event.value == 'RELEASE':
+ self.__operating = True
+ return {'RUNNING_MODAL'}
+
+ # update UV
+ obj = context.object
+ bm = bmesh.from_edit_mesh(obj.data)
+ active_uv = bm.loops.layers.uv.active
+ for fidx, vidx in self.__topology_dict:
+ l = bm.faces[fidx].loops[vidx]
+ l[active_uv].uv = l[active_uv].uv + dv
+ bmesh.update_edit_mesh(obj.data)
+
+ # check mouse preference
+ confirm_btn = 'LEFTMOUSE'
+ cancel_btn = 'RIGHTMOUSE'
+
+ # cancelled
+ if event.type == cancel_btn and event.value == 'PRESS':
+ for (fidx, vidx), uv in zip(self.__topology_dict, self.__ini_uvs):
+ bm.faces[fidx].loops[vidx][active_uv].uv = uv
+ MUV_OT_MoveUV.__running = False
+ return {'FINISHED'}
+ # confirmed
+ if event.type == confirm_btn and event.value == 'PRESS':
+ MUV_OT_MoveUV.__running = False
+ return {'FINISHED'}
+
+ return {'RUNNING_MODAL'}
+
+ def execute(self, context):
+ MUV_OT_MoveUV.__running = True
+ self.__operating = False
+ self.__first_time = True
+
+ context.window_manager.modal_handler_add(self)
+ self.__topology_dict, self.__ini_uvs = _find_uv(context)
+
+ if context.area:
+ context.area.tag_redraw()
+
+ return {'RUNNING_MODAL'}
diff --git a/magic_uv/op/pack_uv.py b/magic_uv/op/pack_uv.py
new file mode 100644
index 00000000..4eb3841d
--- /dev/null
+++ b/magic_uv/op/pack_uv.py
@@ -0,0 +1,282 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+from math import fabs
+
+import bpy
+from bpy.props import (
+ FloatProperty,
+ FloatVectorProperty,
+ BoolProperty,
+)
+import bmesh
+import mathutils
+from mathutils import Vector
+
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..utils import compatibility as compat
+from .. import common
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
+ # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
+ # after the execution
+ for space in context.area.spaces:
+ if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'):
+ break
+ else:
+ return False
+
+ return True
+
+
+def _sort_island_faces(kd, uvs, isl1, isl2):
+ """
+ Sort faces in island
+ """
+
+ sorted_faces = []
+ for f in isl1['sorted']:
+ _, idx, _ = kd.find(
+ Vector((f['ave_uv'].x, f['ave_uv'].y, 0.0)))
+ sorted_faces.append(isl2['faces'][uvs[idx]['face_idx']])
+ return sorted_faces
+
+
+def _group_island(island_info, allowable_center_deviation,
+ allowable_size_deviation):
+ """
+ Group island
+ """
+
+ num_group = 0
+ while True:
+ # search islands which is not parsed yet
+ isl_1 = None
+ for isl_1 in island_info:
+ if isl_1['group'] == -1:
+ break
+ else:
+ break # all faces are parsed
+ if isl_1 is None:
+ break
+ isl_1['group'] = num_group
+ isl_1['sorted'] = isl_1['faces']
+
+ # search same island
+ for isl_2 in island_info:
+ if isl_2['group'] == -1:
+ dcx = isl_2['center'].x - isl_1['center'].x
+ dcy = isl_2['center'].y - isl_1['center'].y
+ dsx = isl_2['size'].x - isl_1['size'].x
+ dsy = isl_2['size'].y - isl_1['size'].y
+ center_x_matched = (
+ fabs(dcx) < allowable_center_deviation[0]
+ )
+ center_y_matched = (
+ fabs(dcy) < allowable_center_deviation[1]
+ )
+ size_x_matched = (
+ fabs(dsx) < allowable_size_deviation[0]
+ )
+ size_y_matched = (
+ fabs(dsy) < allowable_size_deviation[1]
+ )
+ center_matched = center_x_matched and center_y_matched
+ size_matched = size_x_matched and size_y_matched
+ num_uv_matched = (isl_2['num_uv'] == isl_1['num_uv'])
+ # are islands have same?
+ if center_matched and size_matched and num_uv_matched:
+ isl_2['group'] = num_group
+ kd = mathutils.kdtree.KDTree(len(isl_2['faces']))
+ uvs = [
+ {
+ 'uv': Vector(
+ (f['ave_uv'].x, f['ave_uv'].y, 0.0)
+ ),
+ 'face_idx': fidx
+ } for fidx, f in enumerate(isl_2['faces'])
+ ]
+ for i, uv in enumerate(uvs):
+ kd.insert(uv['uv'], i)
+ kd.balance()
+ # sort faces for copy/paste UV
+ isl_2['sorted'] = _sort_island_faces(kd, uvs, isl_1, isl_2)
+ num_group = num_group + 1
+
+ return num_group
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "pack_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_pack_uv_enabled = BoolProperty(
+ name="Pack UV Enabled",
+ description="Pack UV is enabled",
+ default=False
+ )
+ scene.muv_pack_uv_allowable_center_deviation = FloatVectorProperty(
+ name="Allowable Center Deviation",
+ description="Allowable center deviation to judge same UV island",
+ min=0.000001,
+ max=0.1,
+ default=(0.001, 0.001),
+ size=2
+ )
+ scene.muv_pack_uv_allowable_size_deviation = FloatVectorProperty(
+ name="Allowable Size Deviation",
+ description="Allowable sizse deviation to judge same UV island",
+ min=0.000001,
+ max=0.1,
+ default=(0.001, 0.001),
+ size=2
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_pack_uv_enabled
+ del scene.muv_pack_uv_allowable_center_deviation
+ del scene.muv_pack_uv_allowable_size_deviation
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_PackUV(bpy.types.Operator):
+ """
+ Operation class: Pack UV with same UV islands are integrated
+ Island matching algorithm
+ - Same center of UV island
+ - Same size of UV island
+ - Same number of UV
+ """
+
+ bl_idname = "uv.muv_ot_pack_uv"
+ bl_label = "Pack UV"
+ bl_description = "Pack UV (Same UV Islands are integrated)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ rotate = BoolProperty(
+ name="Rotate",
+ description="Rotate option used by default pack UV function",
+ default=False)
+ margin = FloatProperty(
+ name="Margin",
+ description="Margin used by default pack UV function",
+ min=0,
+ max=1,
+ default=0.001)
+ allowable_center_deviation = FloatVectorProperty(
+ name="Allowable Center Deviation",
+ description="Allowable center deviation to judge same UV island",
+ min=0.000001,
+ max=0.1,
+ default=(0.001, 0.001),
+ size=2
+ )
+ allowable_size_deviation = FloatVectorProperty(
+ name="Allowable Size Deviation",
+ description="Allowable sizse deviation to judge same UV island",
+ min=0.000001,
+ max=0.1,
+ default=(0.001, 0.001),
+ size=2
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+ if not bm.loops.layers.uv:
+ self.report({'WARNING'},
+ "Object must have more than one UV map")
+ return {'CANCELLED'}
+ uv_layer = bm.loops.layers.uv.verify()
+
+ selected_faces = [f for f in bm.faces if f.select]
+ island_info = common.get_island_info(obj)
+ num_group = _group_island(island_info,
+ self.allowable_center_deviation,
+ self.allowable_size_deviation)
+
+ loop_lists = [l for f in bm.faces for l in f.loops]
+ bpy.ops.mesh.select_all(action='DESELECT')
+
+ # pack UV
+ for gidx in range(num_group):
+ group = list(filter(
+ lambda i, idx=gidx: i['group'] == idx, island_info))
+ for f in group[0]['faces']:
+ f['face'].select = True
+ bmesh.update_edit_mesh(obj.data)
+ bpy.ops.uv.select_all(action='SELECT')
+ bpy.ops.uv.pack_islands(rotate=self.rotate, margin=self.margin)
+
+ # copy/paste UV among same islands
+ for gidx in range(num_group):
+ group = list(filter(
+ lambda i, idx=gidx: i['group'] == idx, island_info))
+ if len(group) <= 1:
+ continue
+ for g in group[1:]:
+ for (src_face, dest_face) in zip(
+ group[0]['sorted'], g['sorted']):
+ for (src_loop, dest_loop) in zip(
+ src_face['face'].loops, dest_face['face'].loops):
+ loop_lists[dest_loop.index][uv_layer].uv = loop_lists[
+ src_loop.index][uv_layer].uv
+
+ # restore face/UV selection
+ bpy.ops.uv.select_all(action='DESELECT')
+ bpy.ops.mesh.select_all(action='DESELECT')
+ for f in selected_faces:
+ f.select = True
+ bpy.ops.uv.select_all(action='SELECT')
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
diff --git a/magic_uv/op/preserve_uv_aspect.py b/magic_uv/op/preserve_uv_aspect.py
new file mode 100644
index 00000000..116fe898
--- /dev/null
+++ b/magic_uv/op/preserve_uv_aspect.py
@@ -0,0 +1,297 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+from bpy.props import StringProperty, EnumProperty, BoolProperty
+import bmesh
+from mathutils import Vector
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..utils import compatibility as compat
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "preserve_uv_aspect"
+
+ @classmethod
+ def init_props(cls, scene):
+ def get_loaded_texture_name(_, __):
+ items = [(key, key, "") for key in bpy.data.images.keys()]
+ items.append(("None", "None", ""))
+ return items
+
+ scene.muv_preserve_uv_aspect_enabled = BoolProperty(
+ name="Preserve UV Aspect Enabled",
+ description="Preserve UV Aspect is enabled",
+ default=False
+ )
+ scene.muv_preserve_uv_aspect_tex_image = EnumProperty(
+ name="Image",
+ description="Texture Image",
+ items=get_loaded_texture_name
+ )
+ scene.muv_preserve_uv_aspect_origin = EnumProperty(
+ name="Origin",
+ description="Aspect Origin",
+ items=[
+ ('CENTER', 'Center', 'Center'),
+ ('LEFT_TOP', 'Left Top', 'Left Bottom'),
+ ('LEFT_CENTER', 'Left Center', 'Left Center'),
+ ('LEFT_BOTTOM', 'Left Bottom', 'Left Bottom'),
+ ('CENTER_TOP', 'Center Top', 'Center Top'),
+ ('CENTER_BOTTOM', 'Center Bottom', 'Center Bottom'),
+ ('RIGHT_TOP', 'Right Top', 'Right Top'),
+ ('RIGHT_CENTER', 'Right Center', 'Right Center'),
+ ('RIGHT_BOTTOM', 'Right Bottom', 'Right Bottom')
+
+ ],
+ default="CENTER"
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_preserve_uv_aspect_enabled
+ del scene.muv_preserve_uv_aspect_tex_image
+ del scene.muv_preserve_uv_aspect_origin
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_PreserveUVAspect(bpy.types.Operator):
+ """
+ Operation class: Preserve UV Aspect
+ """
+
+ bl_idname = "uv.muv_ot_preserve_uv_aspect"
+ bl_label = "Preserve UV Aspect"
+ bl_description = "Choose Image"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ dest_img_name = StringProperty(options={'HIDDEN'})
+ origin = EnumProperty(
+ name="Origin",
+ description="Aspect Origin",
+ items=[
+ ('CENTER', 'Center', 'Center'),
+ ('LEFT_TOP', 'Left Top', 'Left Bottom'),
+ ('LEFT_CENTER', 'Left Center', 'Left Center'),
+ ('LEFT_BOTTOM', 'Left Bottom', 'Left Bottom'),
+ ('CENTER_TOP', 'Center Top', 'Center Top'),
+ ('CENTER_BOTTOM', 'Center Bottom', 'Center Bottom'),
+ ('RIGHT_TOP', 'Right Top', 'Right Top'),
+ ('RIGHT_CENTER', 'Right Center', 'Right Center'),
+ ('RIGHT_BOTTOM', 'Right Bottom', 'Right Bottom')
+
+ ],
+ default="CENTER"
+ )
+
+ @classmethod
+ def 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):
+ # Note: the current system only works if the
+ # f[tex_layer].image doesn't return None
+ # which will happen in certain cases
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ if not bm.loops.layers.uv:
+ self.report({'WARNING'},
+ "Object must have more than one UV map")
+ return {'CANCELLED'}
+
+ uv_layer = bm.loops.layers.uv.verify()
+
+ sel_faces = [f for f in bm.faces if f.select]
+ dest_img = bpy.data.images[self.dest_img_name]
+
+ info = {}
+
+ if compat.check_version(2, 80, 0) >= 0:
+ tex_image = common.find_image(obj)
+ for f in sel_faces:
+ if tex_image not in info.keys():
+ info[tex_image] = {}
+ info[tex_image]['faces'] = []
+ info[tex_image]['faces'].append(f)
+ else:
+ tex_layer = bm.faces.layers.tex.verify()
+ for f in sel_faces:
+ if not f[tex_layer].image in info.keys():
+ info[f[tex_layer].image] = {}
+ info[f[tex_layer].image]['faces'] = []
+ info[f[tex_layer].image]['faces'].append(f)
+
+ for img in info:
+ if img is None:
+ continue
+
+ src_img = img
+ ratio = Vector((
+ dest_img.size[0] / src_img.size[0],
+ dest_img.size[1] / src_img.size[1]))
+
+ if self.origin == 'CENTER':
+ origin = Vector((0.0, 0.0))
+ num = 0
+ for f in info[img]['faces']:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin = origin + uv
+ num = num + 1
+ origin = origin / num
+ elif self.origin == 'LEFT_TOP':
+ origin = Vector((100000.0, -100000.0))
+ for f in info[img]['faces']:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = min(origin.x, uv.x)
+ origin.y = max(origin.y, uv.y)
+ elif self.origin == 'LEFT_CENTER':
+ origin = Vector((100000.0, 0.0))
+ num = 0
+ for f in info[img]['faces']:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = min(origin.x, uv.x)
+ origin.y = origin.y + uv.y
+ num = num + 1
+ origin.y = origin.y / num
+ elif self.origin == 'LEFT_BOTTOM':
+ origin = Vector((100000.0, 100000.0))
+ for f in info[img]['faces']:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = min(origin.x, uv.x)
+ origin.y = min(origin.y, uv.y)
+ elif self.origin == 'CENTER_TOP':
+ origin = Vector((0.0, -100000.0))
+ num = 0
+ for f in info[img]['faces']:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = origin.x + uv.x
+ origin.y = max(origin.y, uv.y)
+ num = num + 1
+ origin.x = origin.x / num
+ elif self.origin == 'CENTER_BOTTOM':
+ origin = Vector((0.0, 100000.0))
+ num = 0
+ for f in info[img]['faces']:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = origin.x + uv.x
+ origin.y = min(origin.y, uv.y)
+ num = num + 1
+ origin.x = origin.x / num
+ elif self.origin == 'RIGHT_TOP':
+ origin = Vector((-100000.0, -100000.0))
+ for f in info[img]['faces']:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = max(origin.x, uv.x)
+ origin.y = max(origin.y, uv.y)
+ elif self.origin == 'RIGHT_CENTER':
+ origin = Vector((-100000.0, 0.0))
+ num = 0
+ for f in info[img]['faces']:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = max(origin.x, uv.x)
+ origin.y = origin.y + uv.y
+ num = num + 1
+ origin.y = origin.y / num
+ elif self.origin == 'RIGHT_BOTTOM':
+ origin = Vector((-100000.0, 100000.0))
+ for f in info[img]['faces']:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = max(origin.x, uv.x)
+ origin.y = min(origin.y, uv.y)
+ else:
+ self.report({'ERROR'}, "Unknown Operation")
+ return {'CANCELLED'}
+
+ info[img]['ratio'] = ratio
+ info[img]['origin'] = origin
+
+ for img in info:
+ if img is None:
+ continue
+
+ if compat.check_version(2, 80, 0) >= 0:
+ nodes = common.find_texture_nodes(obj)
+ nodes[0].image = dest_img
+
+ for f in info[img]['faces']:
+ if compat.check_version(2, 80, 0) < 0:
+ tex_layer = bm.faces.layers.tex.verify()
+ f[tex_layer].image = dest_img
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin = info[img]['origin']
+ ratio = info[img]['ratio']
+ diff = uv - origin
+ diff.x = diff.x / ratio.x
+ diff.y = diff.y / ratio.y
+ uv.x = origin.x + diff.x
+ uv.y = origin.y + diff.y
+ l[uv_layer].uv = uv
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
diff --git a/magic_uv/op/select_uv.py b/magic_uv/op/select_uv.py
new file mode 100644
index 00000000..72757e29
--- /dev/null
+++ b/magic_uv/op/select_uv.py
@@ -0,0 +1,161 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+from bpy.props import BoolProperty
+import bmesh
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
+ # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
+ # after the execution
+ for space in context.area.spaces:
+ if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'):
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "select_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_select_uv_enabled = BoolProperty(
+ name="Select UV Enabled",
+ description="Select UV is enabled",
+ default=False
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_select_uv_enabled
+
+
+@BlClassRegistry()
+class MUV_OT_SelectUV_SelectOverlapped(bpy.types.Operator):
+ """
+ Operation class: Select faces which have overlapped UVs
+ """
+
+ bl_idname = "uv.muv_ot_select_uv_select_overlapped"
+ bl_label = "Overlapped"
+ bl_description = "Select faces which have overlapped UVs"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+ uv_layer = bm.loops.layers.uv.verify()
+
+ if context.tool_settings.use_uv_select_sync:
+ sel_faces = [f for f in bm.faces]
+ else:
+ sel_faces = [f for f in bm.faces if f.select]
+
+ overlapped_info = common.get_overlapped_uv_info(bm, sel_faces,
+ uv_layer, 'FACE')
+
+ for info in overlapped_info:
+ if context.tool_settings.use_uv_select_sync:
+ info["subject_face"].select = True
+ else:
+ for l in info["subject_face"].loops:
+ l[uv_layer].select = True
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+class MUV_OT_SelectUV_SelectFlipped(bpy.types.Operator):
+ """
+ Operation class: Select faces which have flipped UVs
+ """
+
+ bl_idname = "uv.muv_ot_select_uv_select_flipped"
+ bl_label = "Flipped"
+ bl_description = "Select faces which have flipped UVs"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+ uv_layer = bm.loops.layers.uv.verify()
+
+ if context.tool_settings.use_uv_select_sync:
+ sel_faces = [f for f in bm.faces]
+ else:
+ sel_faces = [f for f in bm.faces if f.select]
+
+ flipped_info = common.get_flipped_uv_info(sel_faces, uv_layer)
+
+ for info in flipped_info:
+ if context.tool_settings.use_uv_select_sync:
+ info["face"].select = True
+ else:
+ for l in info["face"].loops:
+ l[uv_layer].select = True
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
diff --git a/magic_uv/op/smooth_uv.py b/magic_uv/op/smooth_uv.py
new file mode 100644
index 00000000..0cb4df51
--- /dev/null
+++ b/magic_uv/op/smooth_uv.py
@@ -0,0 +1,283 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "imdjs, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+from bpy.props import BoolProperty, FloatProperty
+import bmesh
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..utils import compatibility as compat
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
+ # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
+ # after the execution
+ for space in context.area.spaces:
+ if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'):
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "smooth_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_smooth_uv_enabled = BoolProperty(
+ name="Smooth UV Enabled",
+ description="Smooth UV is enabled",
+ default=False
+ )
+ scene.muv_smooth_uv_transmission = BoolProperty(
+ name="Transmission",
+ description="Smooth linked UVs",
+ default=False
+ )
+ scene.muv_smooth_uv_mesh_infl = FloatProperty(
+ name="Mesh Influence",
+ description="Influence rate of mesh vertex",
+ min=0.0,
+ max=1.0,
+ default=0.0
+ )
+ scene.muv_smooth_uv_select = BoolProperty(
+ name="Select",
+ description="Select UVs which are smoothed",
+ default=False
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_smooth_uv_enabled
+ del scene.muv_smooth_uv_transmission
+ del scene.muv_smooth_uv_mesh_infl
+ del scene.muv_smooth_uv_select
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_SmoothUV(bpy.types.Operator):
+
+ bl_idname = "uv.muv_ot_smooth_uv"
+ bl_label = "Smooth"
+ bl_description = "Smooth UV coordinates"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ transmission = BoolProperty(
+ name="Transmission",
+ description="Smooth linked UVs",
+ default=False
+ )
+ mesh_infl = FloatProperty(
+ name="Mesh Influence",
+ description="Influence rate of mesh vertex",
+ min=0.0,
+ max=1.0,
+ default=0.0
+ )
+ select = BoolProperty(
+ name="Select",
+ description="Select UVs which are smoothed",
+ default=False
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def __smooth_wo_transmission(self, loop_seqs, uv_layer):
+ # calculate path length
+ loops = []
+ for hseq in loop_seqs:
+ loops.extend([hseq[0][0], hseq[0][1]])
+ full_vlen = 0
+ accm_vlens = [0.0]
+ full_uvlen = 0
+ accm_uvlens = [0.0]
+ orig_uvs = [loop_seqs[0][0][0][uv_layer].uv.copy()]
+ for l1, l2 in zip(loops[:-1], loops[1:]):
+ diff_v = l2.vert.co - l1.vert.co
+ full_vlen = full_vlen + diff_v.length
+ accm_vlens.append(full_vlen)
+ diff_uv = l2[uv_layer].uv - l1[uv_layer].uv
+ full_uvlen = full_uvlen + diff_uv.length
+ accm_uvlens.append(full_uvlen)
+ orig_uvs.append(l2[uv_layer].uv.copy())
+
+ for hidx, hseq in enumerate(loop_seqs):
+ pair = hseq[0]
+ for pidx, l in enumerate(pair):
+ if self.select:
+ l[uv_layer].select = True
+
+ # ignore start/end loop
+ if (hidx == 0 and pidx == 0) or\
+ ((hidx == len(loop_seqs) - 1) and (pidx == len(pair) - 1)):
+ continue
+
+ # calculate target path length
+ # target = no influenced * (1 - infl) + influenced * infl
+ tgt_noinfl = full_uvlen * (hidx + pidx) / (len(loop_seqs))
+ tgt_infl = full_uvlen * accm_vlens[hidx * 2 + pidx] / full_vlen
+ target_length = tgt_noinfl * (1 - self.mesh_infl) + \
+ tgt_infl * self.mesh_infl
+
+ # get target UV
+ for i in range(len(accm_uvlens[:-1])):
+ # get line segment which UV will be placed
+ if ((accm_uvlens[i] <= target_length) and
+ (accm_uvlens[i + 1] > target_length)):
+ tgt_seg_len = target_length - accm_uvlens[i]
+ seg_len = accm_uvlens[i + 1] - accm_uvlens[i]
+ uv1 = orig_uvs[i]
+ uv2 = orig_uvs[i + 1]
+ target_uv = uv1 + (uv2 - uv1) * tgt_seg_len / seg_len
+ break
+ else:
+ self.report({'ERROR'}, "Failed to get target UV")
+ return {'CANCELLED'}
+
+ # update UV
+ l[uv_layer].uv = target_uv
+
+ def __smooth_w_transmission(self, loop_seqs, uv_layer):
+ # calculate path length
+ loops = []
+ for vidx in range(len(loop_seqs[0])):
+ ls = []
+ for hseq in loop_seqs:
+ ls.extend(hseq[vidx])
+ loops.append(ls)
+
+ orig_uvs = []
+ accm_vlens = []
+ full_vlens = []
+ accm_uvlens = []
+ full_uvlens = []
+ for ls in loops:
+ full_v = 0.0
+ accm_v = [0.0]
+ full_uv = 0.0
+ accm_uv = [0.0]
+ uvs = [ls[0][uv_layer].uv.copy()]
+ for l1, l2 in zip(ls[:-1], ls[1:]):
+ diff_v = l2.vert.co - l1.vert.co
+ full_v = full_v + diff_v.length
+ accm_v.append(full_v)
+ diff_uv = l2[uv_layer].uv - l1[uv_layer].uv
+ full_uv = full_uv + diff_uv.length
+ accm_uv.append(full_uv)
+ uvs.append(l2[uv_layer].uv.copy())
+ accm_vlens.append(accm_v)
+ full_vlens.append(full_v)
+ accm_uvlens.append(accm_uv)
+ full_uvlens.append(full_uv)
+ orig_uvs.append(uvs)
+
+ for hidx, hseq in enumerate(loop_seqs):
+ for vidx, (pair, uvs, accm_v, full_v, accm_uv, full_uv)\
+ in enumerate(zip(hseq, orig_uvs, accm_vlens, full_vlens,
+ accm_uvlens, full_uvlens)):
+ for pidx, l in enumerate(pair):
+ if self.select:
+ l[uv_layer].select = True
+
+ # ignore start/end loop
+ if hidx == 0 and pidx == 0:
+ continue
+ if hidx == len(loop_seqs) - 1 and pidx == len(pair) - 1:
+ continue
+
+ # calculate target path length
+ # target = no influenced * (1 - infl) + influenced * infl
+ tgt_noinfl = full_uv * (hidx + pidx) / (len(loop_seqs))
+ tgt_infl = full_uv * accm_v[hidx * 2 + pidx] / full_v
+ target_length = tgt_noinfl * (1 - self.mesh_infl) + \
+ tgt_infl * self.mesh_infl
+
+ # get target UV
+ for i in range(len(accm_uv[:-1])):
+ # get line segment to be placed
+ if ((accm_uv[i] <= target_length) and
+ (accm_uv[i + 1] > target_length)):
+ tgt_seg_len = target_length - accm_uv[i]
+ seg_len = accm_uv[i + 1] - accm_uv[i]
+ uv1 = uvs[i]
+ uv2 = uvs[i + 1]
+ target_uv = uv1 +\
+ (uv2 - uv1) * tgt_seg_len / seg_len
+ break
+ else:
+ self.report({'ERROR'}, "Failed to get target UV")
+ return {'CANCELLED'}
+
+ # update UV
+ l[uv_layer].uv = target_uv
+
+ def __smooth(self, loop_seqs, uv_layer):
+ if self.transmission:
+ self.__smooth_w_transmission(loop_seqs, uv_layer)
+ else:
+ self.__smooth_wo_transmission(loop_seqs, uv_layer)
+
+ def execute(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+ uv_layer = bm.loops.layers.uv.verify()
+
+ # loop_seqs[horizontal][vertical][loop]
+ loop_seqs, error = common.get_loop_sequences(bm, uv_layer)
+ if not loop_seqs:
+ self.report({'WARNING'}, error)
+ return {'CANCELLED'}
+
+ # smooth
+ self.__smooth(loop_seqs, uv_layer)
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
diff --git a/magic_uv/op/texture_lock.py b/magic_uv/op/texture_lock.py
new file mode 100644
index 00000000..791a7ae6
--- /dev/null
+++ b/magic_uv/op/texture_lock.py
@@ -0,0 +1,537 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import math
+from math import atan2, cos, sqrt, sin, fabs
+
+import bpy
+from bpy.props import BoolProperty
+import bmesh
+from mathutils import Vector
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..utils import compatibility as compat
+
+
+def _get_vco(verts_orig, loop):
+ """
+ Get vertex original coordinate from loop
+ """
+ for vo in verts_orig:
+ if vo["vidx"] == loop.vert.index and vo["moved"] is False:
+ return vo["vco"]
+ return loop.vert.co
+
+
+def _get_link_loops(vert):
+ """
+ Get loop linked to vertex
+ """
+ link_loops = []
+ for f in vert.link_faces:
+ adj_loops = []
+ for loop in f.loops:
+ # self loop
+ if loop.vert == vert:
+ l = loop
+ # linked loop
+ else:
+ for e in loop.vert.link_edges:
+ if e.other_vert(loop.vert) == vert:
+ adj_loops.append(loop)
+ if len(adj_loops) < 2:
+ return None
+
+ link_loops.append({"l": l, "l0": adj_loops[0], "l1": adj_loops[1]})
+ return link_loops
+
+
+def _get_ini_geom(link_loop, uv_layer, verts_orig, v_orig):
+ """
+ Get initial geometory
+ (Get interior angle of face in vertex/UV space)
+ """
+ u = link_loop["l"][uv_layer].uv
+ v0 = _get_vco(verts_orig, link_loop["l0"])
+ u0 = link_loop["l0"][uv_layer].uv
+ v1 = _get_vco(verts_orig, link_loop["l1"])
+ u1 = link_loop["l1"][uv_layer].uv
+
+ # get interior angle of face in vertex space
+ v0v1 = v1 - v0
+ v0v = v_orig["vco"] - v0
+ v1v = v_orig["vco"] - v1
+ theta0 = v0v1.angle(v0v)
+ theta1 = v0v1.angle(-v1v)
+ if (theta0 + theta1) > math.pi:
+ theta0 = v0v1.angle(-v0v)
+ theta1 = v0v1.angle(v1v)
+
+ # get interior angle of face in UV space
+ u0u1 = u1 - u0
+ u0u = u - u0
+ u1u = u - u1
+ phi0 = u0u1.angle(u0u)
+ phi1 = u0u1.angle(-u1u)
+ if (phi0 + phi1) > math.pi:
+ phi0 = u0u1.angle(-u0u)
+ phi1 = u0u1.angle(u1u)
+
+ # get direction of linked UV coordinate
+ # this will be used to judge whether angle is more or less than 180 degree
+ dir0 = u0u1.cross(u0u) > 0
+ dir1 = u0u1.cross(u1u) > 0
+
+ return {
+ "theta0": theta0,
+ "theta1": theta1,
+ "phi0": phi0,
+ "phi1": phi1,
+ "dir0": dir0,
+ "dir1": dir1}
+
+
+def _get_target_uv(link_loop, uv_layer, verts_orig, v, ini_geom):
+ """
+ Get target UV coordinate
+ """
+ v0 = _get_vco(verts_orig, link_loop["l0"])
+ lo0 = link_loop["l0"]
+ v1 = _get_vco(verts_orig, link_loop["l1"])
+ lo1 = link_loop["l1"]
+
+ # get interior angle of face in vertex space
+ v0v1 = v1 - v0
+ v0v = v.co - v0
+ v1v = v.co - v1
+ theta0 = v0v1.angle(v0v)
+ theta1 = v0v1.angle(-v1v)
+ if (theta0 + theta1) > math.pi:
+ theta0 = v0v1.angle(-v0v)
+ theta1 = v0v1.angle(v1v)
+
+ # calculate target interior angle in UV space
+ phi0 = theta0 * ini_geom["phi0"] / ini_geom["theta0"]
+ phi1 = theta1 * ini_geom["phi1"] / ini_geom["theta1"]
+
+ uv0 = lo0[uv_layer].uv
+ uv1 = lo1[uv_layer].uv
+
+ # calculate target vertex coordinate from target interior angle
+ tuv0, tuv1 = _calc_tri_vert(uv0, uv1, phi0, phi1)
+
+ # target UV coordinate depends on direction, so judge using direction of
+ # linked UV coordinate
+ u0u1 = uv1 - uv0
+ u0u = tuv0 - uv0
+ u1u = tuv0 - uv1
+ dir0 = u0u1.cross(u0u) > 0
+ dir1 = u0u1.cross(u1u) > 0
+ if (ini_geom["dir0"] != dir0) or (ini_geom["dir1"] != dir1):
+ return tuv1
+
+ return tuv0
+
+
+def _calc_tri_vert(v0, v1, angle0, angle1):
+ """
+ Calculate rest coordinate from other coordinates and angle of end
+ """
+ angle = math.pi - angle0 - angle1
+
+ alpha = atan2(v1.y - v0.y, v1.x - v0.x)
+ d = (v1.x - v0.x) / cos(alpha)
+ a = d * sin(angle0) / sin(angle)
+ b = d * sin(angle1) / sin(angle)
+ s = (a + b + d) / 2.0
+ if fabs(d) < 0.0000001:
+ xd = 0
+ yd = 0
+ else:
+ r = s * (s - a) * (s - b) * (s - d)
+ if r < 0:
+ xd = 0
+ yd = 0
+ else:
+ xd = (b * b - a * a + d * d) / (2 * d)
+ yd = 2 * sqrt(r) / d
+ x1 = xd * cos(alpha) - yd * sin(alpha) + v0.x
+ y1 = xd * sin(alpha) + yd * cos(alpha) + v0.y
+ x2 = xd * cos(alpha) + yd * sin(alpha) + v0.x
+ y2 = xd * sin(alpha) - yd * cos(alpha) + v0.y
+
+ return Vector((x1, y1)), Vector((x2, y2))
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "texture_lock"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ verts_orig = None
+
+ scene.muv_props.texture_lock = Props()
+
+ def get_func(_):
+ return MUV_OT_TextureLock_Intr.is_running(bpy.context)
+
+ def set_func(_, __):
+ pass
+
+ def update_func(_, __):
+ bpy.ops.uv.muv_ot_texture_lock_intr('INVOKE_REGION_WIN')
+
+ scene.muv_texture_lock_enabled = BoolProperty(
+ name="Texture Lock Enabled",
+ description="Texture Lock is enabled",
+ default=False
+ )
+ scene.muv_texture_lock_lock = BoolProperty(
+ name="Texture Lock Locked",
+ description="Texture Lock is locked",
+ default=False,
+ get=get_func,
+ set=set_func,
+ update=update_func
+ )
+ scene.muv_texture_lock_connect = BoolProperty(
+ name="Connect UV",
+ default=True
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_props.texture_lock
+ del scene.muv_texture_lock_enabled
+ del scene.muv_texture_lock_lock
+ del scene.muv_texture_lock_connect
+
+
+@BlClassRegistry()
+class MUV_OT_TextureLock_Lock(bpy.types.Operator):
+ """
+ Operation class: Lock Texture
+ """
+
+ bl_idname = "uv.muv_ot_texture_lock_lock"
+ bl_label = "Lock Texture"
+ bl_description = "Lock Texture"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ @classmethod
+ def is_ready(cls, context):
+ sc = context.scene
+ props = sc.muv_props.texture_lock
+ if props.verts_orig:
+ return True
+ return False
+
+ def execute(self, context):
+ props = context.scene.muv_props.texture_lock
+ obj = bpy.context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ bm.faces.ensure_lookup_table()
+
+ if not bm.loops.layers.uv:
+ self.report({'WARNING'}, "Object must have more than one UV map")
+ return {'CANCELLED'}
+
+ props.verts_orig = [
+ {"vidx": v.index, "vco": v.co.copy(), "moved": False}
+ for v in bm.verts if v.select]
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_TextureLock_Unlock(bpy.types.Operator):
+ """
+ Operation class: Unlock Texture
+ """
+
+ bl_idname = "uv.muv_ot_texture_lock_unlock"
+ bl_label = "Unlock Texture"
+ bl_description = "Unlock Texture"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ connect = BoolProperty(
+ name="Connect UV",
+ default=True
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ sc = context.scene
+ props = sc.muv_props.texture_lock
+ if not props.verts_orig:
+ return False
+ if not MUV_OT_TextureLock_Lock.is_ready(context):
+ return False
+ if not _is_valid_context(context):
+ return False
+ return True
+
+ def execute(self, context):
+ sc = context.scene
+ props = sc.muv_props.texture_lock
+ obj = bpy.context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ bm.faces.ensure_lookup_table()
+
+ if not bm.loops.layers.uv:
+ self.report({'WARNING'}, "Object must have more than one UV map")
+ return {'CANCELLED'}
+ uv_layer = bm.loops.layers.uv.verify()
+
+ verts = [v.index for v in bm.verts if v.select]
+ verts_orig = props.verts_orig
+
+ # move UV followed by vertex coordinate
+ for vidx, v_orig in zip(verts, verts_orig):
+ if vidx != v_orig["vidx"]:
+ self.report({'ERROR'}, "Internal Error")
+ return {"CANCELLED"}
+
+ v = bm.verts[vidx]
+ link_loops = _get_link_loops(v)
+
+ result = []
+
+ for ll in link_loops:
+ ini_geom = _get_ini_geom(ll, uv_layer, verts_orig, v_orig)
+ target_uv = _get_target_uv(
+ ll, uv_layer, verts_orig, v, ini_geom)
+ result.append({"l": ll["l"], "uv": target_uv})
+
+ # connect other face's UV
+ if self.connect:
+ ave = Vector((0.0, 0.0))
+ for r in result:
+ ave = ave + r["uv"]
+ ave = ave / len(result)
+ for r in result:
+ r["l"][uv_layer].uv = ave
+ else:
+ for r in result:
+ r["l"][uv_layer].uv = r["uv"]
+ v_orig["moved"] = True
+ bmesh.update_edit_mesh(obj.data)
+
+ props.verts_orig = None
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+class MUV_OT_TextureLock_Intr(bpy.types.Operator):
+ """
+ Operation class: Texture Lock (Interactive mode)
+ """
+
+ bl_idname = "uv.muv_ot_texture_lock_intr"
+ bl_label = "Texture Lock (Interactive mode)"
+ bl_description = "Internal operation for Texture Lock (Interactive mode)"
+
+ __timer = None
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return False
+ return _is_valid_context(context)
+
+ @classmethod
+ def is_running(cls, _):
+ return 1 if cls.__timer else 0
+
+ @classmethod
+ def handle_add(cls, ops_obj, context):
+ if cls.__timer is None:
+ cls.__timer = context.window_manager.event_timer_add(
+ 0.10, window=context.window)
+ context.window_manager.modal_handler_add(ops_obj)
+
+ @classmethod
+ def handle_remove(cls, context):
+ if cls.__timer is not None:
+ context.window_manager.event_timer_remove(cls.__timer)
+ cls.__timer = None
+
+ def __init__(self):
+ self.__intr_verts_orig = []
+ self.__intr_verts = []
+
+ def __sel_verts_changed(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ bm.faces.ensure_lookup_table()
+
+ prev = set(self.__intr_verts)
+ now = set([v.index for v in bm.verts if v.select])
+
+ return prev != now
+
+ def __reinit_verts(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ bm.faces.ensure_lookup_table()
+
+ self.__intr_verts_orig = [
+ {"vidx": v.index, "vco": v.co.copy(), "moved": False}
+ for v in bm.verts if v.select]
+ self.__intr_verts = [v.index for v in bm.verts if v.select]
+
+ def __update_uv(self, context):
+ """
+ Update UV when vertex coordinates are changed
+ """
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ bm.faces.ensure_lookup_table()
+
+ if not bm.loops.layers.uv:
+ self.report({'WARNING'}, "Object must have more than one UV map")
+ return
+ uv_layer = bm.loops.layers.uv.verify()
+
+ verts = [v.index for v in bm.verts if v.select]
+ verts_orig = self.__intr_verts_orig
+
+ for vidx, v_orig in zip(verts, verts_orig):
+ if vidx != v_orig["vidx"]:
+ self.report({'ERROR'}, "Internal Error")
+ return
+
+ v = bm.verts[vidx]
+ link_loops = _get_link_loops(v)
+
+ result = []
+ for ll in link_loops:
+ ini_geom = _get_ini_geom(ll, uv_layer, verts_orig, v_orig)
+ target_uv = _get_target_uv(
+ ll, uv_layer, verts_orig, v, ini_geom)
+ result.append({"l": ll["l"], "uv": target_uv})
+
+ # UV connect option is always true, because it raises
+ # unexpected behavior
+ ave = Vector((0.0, 0.0))
+ for r in result:
+ ave = ave + r["uv"]
+ ave = ave / len(result)
+ for r in result:
+ r["l"][uv_layer].uv = ave
+ v_orig["moved"] = True
+ bmesh.update_edit_mesh(obj.data)
+
+ common.redraw_all_areas()
+ self.__intr_verts_orig = [
+ {"vidx": v.index, "vco": v.co.copy(), "moved": False}
+ for v in bm.verts if v.select]
+
+ def modal(self, context, event):
+ if not _is_valid_context(context):
+ MUV_OT_TextureLock_Intr.handle_remove(context)
+ return {'FINISHED'}
+
+ if not MUV_OT_TextureLock_Intr.is_running(context):
+ return {'FINISHED'}
+
+ if context.area:
+ context.area.tag_redraw()
+
+ if event.type == 'TIMER':
+ if self.__sel_verts_changed(context):
+ self.__reinit_verts(context)
+ else:
+ self.__update_uv(context)
+
+ return {'PASS_THROUGH'}
+
+ def invoke(self, context, _):
+ if not _is_valid_context(context):
+ return {'CANCELLED'}
+
+ if not MUV_OT_TextureLock_Intr.is_running(context):
+ MUV_OT_TextureLock_Intr.handle_add(self, context)
+ return {'RUNNING_MODAL'}
+ else:
+ MUV_OT_TextureLock_Intr.handle_remove(context)
+
+ if context.area:
+ context.area.tag_redraw()
+
+ return {'FINISHED'}
diff --git a/magic_uv/op/texture_projection.py b/magic_uv/op/texture_projection.py
new file mode 100644
index 00000000..b5360e4d
--- /dev/null
+++ b/magic_uv/op/texture_projection.py
@@ -0,0 +1,417 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+from collections import namedtuple
+
+import bpy
+import bmesh
+from bpy_extras import view3d_utils
+from bpy.props import (
+ BoolProperty,
+ EnumProperty,
+ FloatProperty,
+)
+import mathutils
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..utils import compatibility as compat
+
+if compat.check_version(2, 80, 0) >= 0:
+ from ..lib import bglx as bgl
+else:
+ import bgl
+
+
+_Rect = namedtuple('Rect', 'x0 y0 x1 y1')
+_Rect2 = namedtuple('Rect2', 'x y width height')
+
+
+def _get_loaded_texture_name(_, __):
+ items = [(key, key, "") for key in bpy.data.images.keys()]
+ items.append(("None", "None", ""))
+ return items
+
+
+def _get_canvas(context, magnitude):
+ """
+ Get canvas to be renderred texture
+ """
+ sc = context.scene
+ user_prefs = compat.get_user_preferences(context)
+ prefs = user_prefs.addons["magic_uv"].preferences
+
+ region_w = context.region.width
+ region_h = context.region.height
+ canvas_w = region_w - prefs.texture_projection_canvas_padding[0] * 2.0
+ canvas_h = region_h - prefs.texture_projection_canvas_padding[1] * 2.0
+
+ img = bpy.data.images[sc.muv_texture_projection_tex_image]
+ tex_w = img.size[0]
+ tex_h = img.size[1]
+
+ center_x = region_w * 0.5
+ center_y = region_h * 0.5
+
+ if sc.muv_texture_projection_adjust_window:
+ ratio_x = canvas_w / tex_w
+ ratio_y = canvas_h / tex_h
+ if sc.muv_texture_projection_apply_tex_aspect:
+ ratio = ratio_y if ratio_x > ratio_y else ratio_x
+ len_x = ratio * tex_w
+ len_y = ratio * tex_h
+ else:
+ len_x = canvas_w
+ len_y = canvas_h
+ else:
+ if sc.muv_texture_projection_apply_tex_aspect:
+ len_x = tex_w * magnitude
+ len_y = tex_h * magnitude
+ else:
+ len_x = region_w * magnitude
+ len_y = region_h * magnitude
+
+ x0 = int(center_x - len_x * 0.5)
+ y0 = int(center_y - len_y * 0.5)
+ x1 = int(center_x + len_x * 0.5)
+ y1 = int(center_y + len_y * 0.5)
+
+ return _Rect(x0, y0, x1, y1)
+
+
+def _rect_to_rect2(rect):
+ """
+ Convert Rect1 to Rect2
+ """
+
+ return _Rect2(rect.x0, rect.y0, rect.x1 - rect.x0, rect.y1 - rect.y0)
+
+
+def _region_to_canvas(rg_vec, canvas):
+ """
+ Convert screen region to canvas
+ """
+
+ cv_rect = _rect_to_rect2(canvas)
+ cv_vec = mathutils.Vector()
+ cv_vec.x = (rg_vec.x - cv_rect.x) / cv_rect.width
+ cv_vec.y = (rg_vec.y - cv_rect.y) / cv_rect.height
+
+ return cv_vec
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "texture_projection"
+
+ @classmethod
+ def init_props(cls, scene):
+ def get_func(_):
+ return MUV_OT_TextureProjection.is_running(bpy.context)
+
+ def set_func(_, __):
+ pass
+
+ def update_func(_, __):
+ bpy.ops.uv.muv_ot_texture_projection('INVOKE_REGION_WIN')
+
+ scene.muv_texture_projection_enabled = BoolProperty(
+ name="Texture Projection Enabled",
+ description="Texture Projection is enabled",
+ default=False
+ )
+ scene.muv_texture_projection_enable = BoolProperty(
+ name="Texture Projection Enabled",
+ description="Texture Projection is enabled",
+ default=False,
+ get=get_func,
+ set=set_func,
+ update=update_func
+ )
+ scene.muv_texture_projection_tex_magnitude = FloatProperty(
+ name="Magnitude",
+ description="Texture Magnitude",
+ default=0.5,
+ min=0.0,
+ max=100.0
+ )
+ scene.muv_texture_projection_tex_image = EnumProperty(
+ name="Image",
+ description="Texture Image",
+ items=_get_loaded_texture_name
+ )
+ scene.muv_texture_projection_tex_transparency = FloatProperty(
+ name="Transparency",
+ description="Texture Transparency",
+ default=0.2,
+ min=0.0,
+ max=1.0
+ )
+ scene.muv_texture_projection_adjust_window = BoolProperty(
+ name="Adjust Window",
+ description="Size of renderered texture is fitted to window",
+ default=True
+ )
+ scene.muv_texture_projection_apply_tex_aspect = BoolProperty(
+ name="Texture Aspect Ratio",
+ description="Apply Texture Aspect ratio to displayed texture",
+ default=True
+ )
+ scene.muv_texture_projection_assign_uvmap = BoolProperty(
+ name="Assign UVMap",
+ description="Assign UVMap when no UVmaps are available",
+ default=True
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_texture_projection_enabled
+ del scene.muv_texture_projection_tex_magnitude
+ del scene.muv_texture_projection_tex_image
+ del scene.muv_texture_projection_tex_transparency
+ del scene.muv_texture_projection_adjust_window
+ del scene.muv_texture_projection_apply_tex_aspect
+ del scene.muv_texture_projection_assign_uvmap
+
+
+@BlClassRegistry()
+class MUV_OT_TextureProjection(bpy.types.Operator):
+ """
+ Operation class: Texture Projection
+ Render texture
+ """
+
+ bl_idname = "uv.muv_ot_texture_projection"
+ bl_description = "Render selected texture"
+ bl_label = "Texture renderer"
+
+ __handle = None
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return False
+ return _is_valid_context(context)
+
+ @classmethod
+ def is_running(cls, _):
+ return 1 if cls.__handle else 0
+
+ @classmethod
+ def handle_add(cls, obj, context):
+ cls.__handle = bpy.types.SpaceView3D.draw_handler_add(
+ MUV_OT_TextureProjection.draw_texture,
+ (obj, context), 'WINDOW', 'POST_PIXEL')
+
+ @classmethod
+ def handle_remove(cls):
+ if cls.__handle is not None:
+ bpy.types.SpaceView3D.draw_handler_remove(cls.__handle, 'WINDOW')
+ cls.__handle = None
+
+ @classmethod
+ def draw_texture(cls, _, context):
+ sc = context.scene
+
+ if not cls.is_running(context):
+ return
+
+ # no textures are selected
+ if sc.muv_texture_projection_tex_image == "None":
+ return
+
+ # get texture to be renderred
+ img = bpy.data.images[sc.muv_texture_projection_tex_image]
+
+ # setup rendering region
+ rect = _get_canvas(context, sc.muv_texture_projection_tex_magnitude)
+ positions = [
+ [rect.x0, rect.y0],
+ [rect.x0, rect.y1],
+ [rect.x1, rect.y1],
+ [rect.x1, rect.y0]
+ ]
+ tex_coords = [
+ [0.0, 0.0],
+ [0.0, 1.0],
+ [1.0, 1.0],
+ [1.0, 0.0]
+ ]
+
+ # OpenGL configuration
+ if compat.check_version(2, 80, 0) >= 0:
+ bgl.glEnable(bgl.GL_BLEND)
+ bgl.glEnable(bgl.GL_TEXTURE_2D)
+ bgl.glActiveTexture(bgl.GL_TEXTURE0)
+ if img.bindcode:
+ bind = img.bindcode
+ bgl.glBindTexture(bgl.GL_TEXTURE_2D, bind)
+ else:
+ bgl.glEnable(bgl.GL_BLEND)
+ bgl.glEnable(bgl.GL_TEXTURE_2D)
+ if img.bindcode:
+ bind = img.bindcode[0]
+ bgl.glBindTexture(bgl.GL_TEXTURE_2D, bind)
+ bgl.glTexParameteri(bgl.GL_TEXTURE_2D,
+ bgl.GL_TEXTURE_MIN_FILTER, bgl.GL_LINEAR)
+ bgl.glTexParameteri(bgl.GL_TEXTURE_2D,
+ bgl.GL_TEXTURE_MAG_FILTER, bgl.GL_LINEAR)
+ bgl.glTexEnvi(
+ bgl.GL_TEXTURE_ENV, bgl.GL_TEXTURE_ENV_MODE,
+ bgl.GL_MODULATE)
+
+ # render texture
+ bgl.glBegin(bgl.GL_QUADS)
+ bgl.glColor4f(1.0, 1.0, 1.0,
+ sc.muv_texture_projection_tex_transparency)
+ for (v1, v2), (u, v) in zip(positions, tex_coords):
+ bgl.glTexCoord2f(u, v)
+ bgl.glVertex2f(v1, v2)
+ bgl.glEnd()
+
+ def invoke(self, context, _):
+ if not MUV_OT_TextureProjection.is_running(context):
+ MUV_OT_TextureProjection.handle_add(self, context)
+ else:
+ MUV_OT_TextureProjection.handle_remove()
+
+ if context.area:
+ context.area.tag_redraw()
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+class MUV_OT_TextureProjection_Project(bpy.types.Operator):
+ """
+ Operation class: Project texture
+ """
+
+ bl_idname = "uv.muv_ot_texture_projection_project"
+ bl_label = "Project Texture"
+ bl_description = "Project Texture"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ if not MUV_OT_TextureProjection.is_running(context):
+ return False
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ sc = context.scene
+
+ if sc.muv_texture_projection_tex_image == "None":
+ self.report({'WARNING'}, "No textures are selected")
+ return {'CANCELLED'}
+
+ _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D')
+
+ # get faces to be texture projected
+ obj = context.active_object
+ world_mat = obj.matrix_world
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ # get UV and texture layer
+ if not bm.loops.layers.uv:
+ if sc.muv_texture_projection_assign_uvmap:
+ bm.loops.layers.uv.new()
+ else:
+ self.report({'WARNING'},
+ "Object must have more than one UV map")
+ return {'CANCELLED'}
+
+ uv_layer = bm.loops.layers.uv.verify()
+ if compat.check_version(2, 80, 0) < 0:
+ tex_layer = bm.faces.layers.tex.verify()
+
+ sel_faces = [f for f in bm.faces if f.select]
+
+ # transform 3d space to screen region
+ v_screen = [
+ view3d_utils.location_3d_to_region_2d(
+ region,
+ space.region_3d,
+ compat.matmul(world_mat, l.vert.co))
+ for f in sel_faces for l in f.loops
+ ]
+
+ # transform screen region to canvas
+ v_canvas = [
+ _region_to_canvas(
+ v,
+ _get_canvas(bpy.context,
+ sc.muv_texture_projection_tex_magnitude)
+ ) for v in v_screen
+ ]
+
+ if compat.check_version(2, 80, 0) >= 0:
+ # set texture
+ nodes = common.find_texture_nodes(obj)
+ nodes[0].image = \
+ bpy.data.images[sc.muv_texture_projection_tex_image]
+
+ # project texture to object
+ i = 0
+ for f in sel_faces:
+ if compat.check_version(2, 80, 0) < 0:
+ f[tex_layer].image = \
+ bpy.data.images[sc.muv_texture_projection_tex_image]
+ for l in f.loops:
+ l[uv_layer].uv = v_canvas[i].to_2d()
+ i = i + 1
+
+ common.redraw_all_areas()
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
diff --git a/magic_uv/op/texture_wrap.py b/magic_uv/op/texture_wrap.py
new file mode 100644
index 00000000..49242b83
--- /dev/null
+++ b/magic_uv/op/texture_wrap.py
@@ -0,0 +1,294 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+from bpy.props import (
+ BoolProperty,
+)
+import bmesh
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "texture_wrap"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ ref_face_index = -1
+ ref_obj = None
+
+ scene.muv_props.texture_wrap = Props()
+
+ scene.muv_texture_wrap_enabled = BoolProperty(
+ name="Texture Wrap",
+ description="Texture Wrap is enabled",
+ default=False
+ )
+ scene.muv_texture_wrap_set_and_refer = BoolProperty(
+ name="Set and Refer",
+ description="Refer and set UV",
+ default=True
+ )
+ scene.muv_texture_wrap_selseq = BoolProperty(
+ name="Selection Sequence",
+ description="Set UV sequentially",
+ default=False
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_props.texture_wrap
+ del scene.muv_texture_wrap_enabled
+ del scene.muv_texture_wrap_set_and_refer
+ del scene.muv_texture_wrap_selseq
+
+
+@BlClassRegistry()
+class MUV_OT_TextureWrap_Refer(bpy.types.Operator):
+ """
+ Operation class: Refer UV
+ """
+
+ bl_idname = "uv.muv_ot_texture_wrap_refer"
+ bl_label = "Refer"
+ bl_description = "Refer UV"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ props = context.scene.muv_props.texture_wrap
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ if not bm.loops.layers.uv:
+ self.report({'WARNING'}, "Object must have more than one UV map")
+ return {'CANCELLED'}
+
+ sel_faces = [f for f in bm.faces if f.select]
+ if len(sel_faces) != 1:
+ self.report({'WARNING'}, "Must select only one face")
+ return {'CANCELLED'}
+
+ props.ref_face_index = sel_faces[0].index
+ props.ref_obj = obj
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+class MUV_OT_TextureWrap_Set(bpy.types.Operator):
+ """
+ Operation class: Set UV
+ """
+
+ bl_idname = "uv.muv_ot_texture_wrap_set"
+ bl_label = "Set"
+ bl_description = "Set UV"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ sc = context.scene
+ props = sc.muv_props.texture_wrap
+ if not props.ref_obj:
+ return False
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ sc = context.scene
+ props = sc.muv_props.texture_wrap
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ if not bm.loops.layers.uv:
+ self.report({'WARNING'}, "Object must have more than one UV map")
+ return {'CANCELLED'}
+ uv_layer = bm.loops.layers.uv.verify()
+
+ if sc.muv_texture_wrap_selseq:
+ sel_faces = []
+ for hist in bm.select_history:
+ if isinstance(hist, bmesh.types.BMFace) and hist.select:
+ sel_faces.append(hist)
+ if not sel_faces:
+ self.report({'WARNING'}, "Must select more than one face")
+ return {'CANCELLED'}
+ else:
+ sel_faces = [f for f in bm.faces if f.select]
+ if len(sel_faces) != 1:
+ self.report({'WARNING'}, "Must select only one face")
+ return {'CANCELLED'}
+
+ ref_face_index = props.ref_face_index
+ for face in sel_faces:
+ tgt_face_index = face.index
+ if ref_face_index == tgt_face_index:
+ self.report({'WARNING'}, "Must select different face")
+ return {'CANCELLED'}
+
+ if props.ref_obj != obj:
+ self.report({'WARNING'}, "Object must be same")
+ return {'CANCELLED'}
+
+ ref_face = bm.faces[ref_face_index]
+ tgt_face = bm.faces[tgt_face_index]
+
+ # get common vertices info
+ common_verts = []
+ for sl in ref_face.loops:
+ for dl in tgt_face.loops:
+ if sl.vert == dl.vert:
+ info = {"vert": sl.vert, "ref_loop": sl,
+ "tgt_loop": dl}
+ common_verts.append(info)
+ break
+
+ if len(common_verts) != 2:
+ self.report({'WARNING'},
+ "2 vertices must be shared among faces")
+ return {'CANCELLED'}
+
+ # get reference other vertices info
+ ref_other_verts = []
+ for sl in ref_face.loops:
+ for ci in common_verts:
+ if sl.vert == ci["vert"]:
+ break
+ else:
+ info = {"vert": sl.vert, "loop": sl}
+ ref_other_verts.append(info)
+
+ if not ref_other_verts:
+ self.report({'WARNING'}, "More than 1 vertex must be unshared")
+ return {'CANCELLED'}
+
+ # get reference info
+ ref_info = {}
+ cv0 = common_verts[0]["vert"].co
+ cv1 = common_verts[1]["vert"].co
+ cuv0 = common_verts[0]["ref_loop"][uv_layer].uv
+ cuv1 = common_verts[1]["ref_loop"][uv_layer].uv
+ ov0 = ref_other_verts[0]["vert"].co
+ ouv0 = ref_other_verts[0]["loop"][uv_layer].uv
+ ref_info["vert_vdiff"] = cv1 - cv0
+ ref_info["uv_vdiff"] = cuv1 - cuv0
+ ref_info["vert_hdiff"], _ = common.diff_point_to_segment(
+ cv0, cv1, ov0)
+ ref_info["uv_hdiff"], _ = common.diff_point_to_segment(
+ cuv0, cuv1, ouv0)
+
+ # get target other vertices info
+ tgt_other_verts = []
+ for dl in tgt_face.loops:
+ for ci in common_verts:
+ if dl.vert == ci["vert"]:
+ break
+ else:
+ info = {"vert": dl.vert, "loop": dl}
+ tgt_other_verts.append(info)
+
+ if not tgt_other_verts:
+ self.report({'WARNING'}, "More than 1 vertex must be unshared")
+ return {'CANCELLED'}
+
+ # get target info
+ for info in tgt_other_verts:
+ cv0 = common_verts[0]["vert"].co
+ cv1 = common_verts[1]["vert"].co
+ cuv0 = common_verts[0]["ref_loop"][uv_layer].uv
+ ov = info["vert"].co
+ info["vert_hdiff"], x = common.diff_point_to_segment(
+ cv0, cv1, ov)
+ info["vert_vdiff"] = x - common_verts[0]["vert"].co
+
+ # calclulate factor
+ fact_h = -info["vert_hdiff"].length / \
+ ref_info["vert_hdiff"].length
+ fact_v = info["vert_vdiff"].length / \
+ ref_info["vert_vdiff"].length
+ duv_h = ref_info["uv_hdiff"] * fact_h
+ duv_v = ref_info["uv_vdiff"] * fact_v
+
+ # get target UV
+ info["target_uv"] = cuv0 + duv_h + duv_v
+
+ # apply to common UVs
+ for info in common_verts:
+ info["tgt_loop"][uv_layer].uv = \
+ info["ref_loop"][uv_layer].uv.copy()
+ # apply to other UVs
+ for info in tgt_other_verts:
+ info["loop"][uv_layer].uv = info["target_uv"]
+
+ common.debug_print("===== Target Other Vertices =====")
+ common.debug_print(tgt_other_verts)
+
+ bmesh.update_edit_mesh(obj.data)
+
+ ref_face_index = tgt_face_index
+
+ if sc.muv_texture_wrap_set_and_refer:
+ props.ref_face_index = tgt_face_index
+
+ return {'FINISHED'}
diff --git a/magic_uv/op/transfer_uv.py b/magic_uv/op/transfer_uv.py
new file mode 100644
index 00000000..e812d295
--- /dev/null
+++ b/magic_uv/op/transfer_uv.py
@@ -0,0 +1,457 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>, Mifth, MaxRobinot"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+from collections import OrderedDict
+
+import bpy
+import bmesh
+from bpy.props import BoolProperty
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..utils import compatibility as compat
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+def _get_uv_layer(ops_obj, bm):
+ # get UV layer
+ if not bm.loops.layers.uv:
+ ops_obj.report({'WARNING'}, "Object must have more than one UV map")
+ return None
+ uv_layer = bm.loops.layers.uv.verify()
+
+ return uv_layer
+
+
+def _main_parse(ops_obj, uv_layer, sel_faces, active_face, active_face_nor):
+ all_sorted_faces = OrderedDict() # This is the main stuff
+
+ used_verts = set()
+ used_edges = set()
+
+ faces_to_parse = []
+
+ # get shared edge of two faces
+ cross_edges = []
+ for edge in active_face.edges:
+ if edge in sel_faces[0].edges and edge in sel_faces[1].edges:
+ cross_edges.append(edge)
+
+ # parse two selected faces
+ if cross_edges and len(cross_edges) == 1:
+ shared_edge = cross_edges[0]
+ vert1 = None
+ vert2 = None
+
+ dot_n = active_face_nor.normalized()
+ edge_vec_1 = (shared_edge.verts[1].co - shared_edge.verts[0].co)
+ edge_vec_len = edge_vec_1.length
+ edge_vec_1 = edge_vec_1.normalized()
+
+ af_center = active_face.calc_center_median()
+ af_vec = shared_edge.verts[0].co + (edge_vec_1 * (edge_vec_len * 0.5))
+ af_vec = (af_vec - af_center).normalized()
+
+ if af_vec.cross(edge_vec_1).dot(dot_n) > 0:
+ vert1 = shared_edge.verts[0]
+ vert2 = shared_edge.verts[1]
+ else:
+ vert1 = shared_edge.verts[1]
+ vert2 = shared_edge.verts[0]
+
+ # get active face stuff and uvs
+ face_stuff = _get_other_verts_edges(
+ active_face, vert1, vert2, shared_edge, uv_layer)
+ all_sorted_faces[active_face] = face_stuff
+ used_verts.update(active_face.verts)
+ used_edges.update(active_face.edges)
+
+ # get first selected face stuff and uvs as they share shared_edge
+ second_face = sel_faces[0]
+ if second_face is active_face:
+ second_face = sel_faces[1]
+ face_stuff = _get_other_verts_edges(
+ second_face, vert1, vert2, shared_edge, uv_layer)
+ all_sorted_faces[second_face] = face_stuff
+ used_verts.update(second_face.verts)
+ used_edges.update(second_face.edges)
+
+ # first Grow
+ faces_to_parse.append(active_face)
+ faces_to_parse.append(second_face)
+
+ else:
+ ops_obj.report({'WARNING'}, "Two faces should share one edge")
+ return None
+
+ # parse all faces
+ while True:
+ new_parsed_faces = []
+ if not faces_to_parse:
+ break
+ for face in faces_to_parse:
+ face_stuff = all_sorted_faces.get(face)
+ new_faces = _parse_faces(face, face_stuff, used_verts, used_edges,
+ all_sorted_faces, uv_layer)
+ if new_faces is None:
+ ops_obj.report({'WARNING'}, "More than 2 faces share edge")
+ return None
+
+ new_parsed_faces += new_faces
+ faces_to_parse = new_parsed_faces
+
+ return all_sorted_faces
+
+
+def _parse_faces(check_face, face_stuff, used_verts, used_edges,
+ all_sorted_faces, uv_layer):
+ """recurse faces around the new_grow only"""
+
+ new_shared_faces = []
+ for sorted_edge in face_stuff[1]:
+ shared_faces = sorted_edge.link_faces
+ if shared_faces:
+ if len(shared_faces) > 2:
+ bpy.ops.mesh.select_all(action='DESELECT')
+ for face_sel in shared_faces:
+ face_sel.select = True
+ shared_faces = []
+ return None
+
+ clear_shared_faces = _get_new_shared_faces(
+ check_face, sorted_edge, shared_faces, all_sorted_faces.keys())
+ if clear_shared_faces:
+ shared_face = clear_shared_faces[0]
+ # get vertices of the edge
+ vert1 = sorted_edge.verts[0]
+ vert2 = sorted_edge.verts[1]
+
+ common.debug_print(face_stuff[0], vert1, vert2)
+ if face_stuff[0].index(vert1) > face_stuff[0].index(vert2):
+ vert1 = sorted_edge.verts[1]
+ vert2 = sorted_edge.verts[0]
+
+ common.debug_print(shared_face.verts, vert1, vert2)
+ new_face_stuff = _get_other_verts_edges(
+ shared_face, vert1, vert2, sorted_edge, uv_layer)
+ all_sorted_faces[shared_face] = new_face_stuff
+ used_verts.update(shared_face.verts)
+ used_edges.update(shared_face.edges)
+
+ if common.is_debug_mode():
+ shared_face.select = True # test which faces are parsed
+
+ new_shared_faces.append(shared_face)
+
+ return new_shared_faces
+
+
+def _get_new_shared_faces(orig_face, shared_edge, check_faces, used_faces):
+ shared_faces = []
+
+ for face in check_faces:
+ is_shared_edge = shared_edge in face.edges
+ not_used = face not in used_faces
+ not_orig = face is not orig_face
+ not_hide = face.hide is False
+ if is_shared_edge and not_used and not_orig and not_hide:
+ shared_faces.append(face)
+
+ return shared_faces
+
+
+def _get_other_verts_edges(face, vert1, vert2, first_edge, uv_layer):
+ face_edges = [first_edge]
+ face_verts = [vert1, vert2]
+ face_loops = []
+
+ other_edges = [edge for edge in face.edges if edge not in face_edges]
+
+ for _ in range(len(other_edges)):
+ found_edge = None
+ # get sorted verts and edges
+ for edge in other_edges:
+ if face_verts[-1] in edge.verts:
+ other_vert = edge.other_vert(face_verts[-1])
+
+ if other_vert not in face_verts:
+ face_verts.append(other_vert)
+
+ found_edge = edge
+ if found_edge not in face_edges:
+ face_edges.append(edge)
+ break
+
+ other_edges.remove(found_edge)
+
+ # get sorted uvs
+ for vert in face_verts:
+ for loop in face.loops:
+ if loop.vert is vert:
+ face_loops.append(loop[uv_layer])
+ break
+
+ return [face_verts, face_edges, face_loops]
+
+
+def _get_selected_src_faces(ops_obj, bm, uv_layer):
+ topology_copied = []
+
+ # get selected faces
+ active_face = bm.faces.active
+ sel_faces = [face for face in bm.faces if face.select]
+ if len(sel_faces) != 2:
+ ops_obj.report({'WARNING'}, "Two faces must be selected")
+ return None
+ if not active_face or active_face not in sel_faces:
+ ops_obj.report({'WARNING'}, "Two faces must be active")
+ return None
+
+ # parse all faces according to selection
+ active_face_nor = active_face.normal.copy()
+ all_sorted_faces = _main_parse(ops_obj, uv_layer, sel_faces, active_face,
+ active_face_nor)
+
+ if all_sorted_faces:
+ for face_data in all_sorted_faces.values():
+ edges = face_data[1]
+ uv_loops = face_data[2]
+ uvs = [l.uv.copy() for l in uv_loops]
+ pin_uvs = [l.pin_uv for l in uv_loops]
+ seams = [e.seam for e in edges]
+ topology_copied.append([uvs, pin_uvs, seams])
+ else:
+ return None
+
+ return topology_copied
+
+
+def _paste_uv(ops_obj, bm, uv_layer, src_faces, invert_normals, copy_seams):
+ # get selection history
+ all_sel_faces = [e for e in bm.select_history
+ if isinstance(e, bmesh.types.BMFace) and e.select]
+ if len(all_sel_faces) % 2 != 0:
+ ops_obj.report({'WARNING'}, "Two faces must be selected")
+ return -1
+
+ # parse selection history
+ for i, _ in enumerate(all_sel_faces):
+ if (i == 0) or (i % 2 == 0):
+ continue
+ sel_faces = [all_sel_faces[i - 1], all_sel_faces[i]]
+ active_face = all_sel_faces[i]
+
+ # parse all faces according to selection history
+ active_face_nor = active_face.normal.copy()
+ if invert_normals:
+ active_face_nor.negate()
+ all_sorted_faces = _main_parse(ops_obj, uv_layer, sel_faces,
+ active_face, active_face_nor)
+
+ if all_sorted_faces:
+ # check amount of copied/pasted faces
+ if len(all_sorted_faces) != len(src_faces):
+ ops_obj.report({'WARNING'},
+ "Mesh has different amount of faces")
+ return -1
+
+ for j, face_data in enumerate(all_sorted_faces.values()):
+ copied_data = src_faces[j]
+
+ # check amount of copied/pasted verts
+ if len(copied_data[0]) != len(face_data[2]):
+ bpy.ops.mesh.select_all(action='DESELECT')
+ # select problematic face
+ list(all_sorted_faces.keys())[j].select = True
+ ops_obj.report({'WARNING'},
+ "Face have different amount of vertices")
+ return 0
+
+ for k, (edge, uvloop) in enumerate(zip(face_data[1],
+ face_data[2])):
+ uvloop.uv = copied_data[0][k]
+ uvloop.pin_uv = copied_data[1][k]
+ if copy_seams:
+ edge.seam = copied_data[2][k]
+ else:
+ return -1
+
+ return 0
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "transfer_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ topology_copied = None
+
+ scene.muv_props.transfer_uv = Props()
+
+ scene.muv_transfer_uv_enabled = BoolProperty(
+ name="Transfer UV Enabled",
+ description="Transfer UV is enabled",
+ default=False
+ )
+ scene.muv_transfer_uv_invert_normals = BoolProperty(
+ name="Invert Normals",
+ description="Invert Normals",
+ default=False
+ )
+ scene.muv_transfer_uv_copy_seams = BoolProperty(
+ name="Copy Seams",
+ description="Copy Seams",
+ default=True
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_transfer_uv_enabled
+ del scene.muv_transfer_uv_invert_normals
+ del scene.muv_transfer_uv_copy_seams
+
+
+@BlClassRegistry()
+class MUV_OT_TransferUV_CopyUV(bpy.types.Operator):
+ """
+ Operation class: Transfer UV copy
+ Topological based copy
+ """
+
+ bl_idname = "uv.muv_ot_transfer_uv_copy_uv"
+ bl_label = "Transfer UV Copy UV"
+ bl_description = "Transfer UV Copy UV (Topological based copy)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ props = context.scene.muv_props.transfer_uv
+ active_obj = context.active_object
+ bm = bmesh.from_edit_mesh(active_obj.data)
+ if compat.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ uv_layer = _get_uv_layer(self, bm)
+ if uv_layer is None:
+ return {'CANCELLED'}
+
+ faces = _get_selected_src_faces(self, bm, uv_layer)
+ if faces is None:
+ return {'CANCELLED'}
+ props.topology_copied = faces
+
+ bmesh.update_edit_mesh(active_obj.data)
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_TransferUV_PasteUV(bpy.types.Operator):
+ """
+ Operation class: Transfer UV paste
+ Topological based paste
+ """
+
+ bl_idname = "uv.muv_ot_transfer_uv_paste_uv"
+ bl_label = "Transfer UV Paste UV"
+ bl_description = "Transfer UV Paste UV (Topological based paste)"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ invert_normals = BoolProperty(
+ name="Invert Normals",
+ description="Invert Normals",
+ default=False
+ )
+ copy_seams = BoolProperty(
+ name="Copy Seams",
+ description="Copy Seams",
+ default=True
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ sc = context.scene
+ props = sc.muv_props.transfer_uv
+ if not props.topology_copied:
+ return False
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ props = context.scene.muv_props.transfer_uv
+ active_obj = context.active_object
+ bm = bmesh.from_edit_mesh(active_obj.data)
+ if compat.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ # get UV layer
+ uv_layer = _get_uv_layer(self, bm)
+ if uv_layer is None:
+ return {'CANCELLED'}
+
+ ret = _paste_uv(self, bm, uv_layer, props.topology_copied,
+ self.invert_normals, self.copy_seams)
+ if ret:
+ return {'CANCELLED'}
+
+ bmesh.update_edit_mesh(active_obj.data)
+
+ if compat.check_version(2, 80, 0) < 0:
+ if self.copy_seams:
+ active_obj.data.show_edge_seams = True
+
+ return {'FINISHED'}
diff --git a/magic_uv/op/unwrap_constraint.py b/magic_uv/op/unwrap_constraint.py
new file mode 100644
index 00000000..b622663a
--- /dev/null
+++ b/magic_uv/op/unwrap_constraint.py
@@ -0,0 +1,186 @@
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+from bpy.props import (
+ BoolProperty,
+ EnumProperty,
+ FloatProperty,
+)
+import bmesh
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..utils import compatibility as compat
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "unwrap_constraint"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_unwrap_constraint_enabled = BoolProperty(
+ name="Unwrap Constraint Enabled",
+ description="Unwrap Constraint is enabled",
+ default=False
+ )
+ scene.muv_unwrap_constraint_u_const = BoolProperty(
+ name="U-Constraint",
+ description="Keep UV U-axis coordinate",
+ default=False
+ )
+ scene.muv_unwrap_constraint_v_const = BoolProperty(
+ name="V-Constraint",
+ description="Keep UV V-axis coordinate",
+ default=False
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_unwrap_constraint_enabled
+ del scene.muv_unwrap_constraint_u_const
+ del scene.muv_unwrap_constraint_v_const
+
+
+@BlClassRegistry(legacy=True)
+@compat.make_annotations
+class MUV_OT_UnwrapConstraint(bpy.types.Operator):
+ """
+ Operation class: Unwrap with constrain UV coordinate
+ """
+
+ bl_idname = "uv.muv_ot_unwrap_constraint"
+ bl_label = "Unwrap Constraint"
+ bl_description = "Unwrap while keeping uv coordinate"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ # property for original unwrap
+ method = EnumProperty(
+ name="Method",
+ description="Unwrapping method",
+ items=[
+ ('ANGLE_BASED', 'Angle Based', 'Angle Based'),
+ ('CONFORMAL', 'Conformal', 'Conformal')
+ ],
+ default='ANGLE_BASED')
+ fill_holes = BoolProperty(
+ name="Fill Holes",
+ description="Virtual fill holes in meshes before unwrapping",
+ default=True)
+ correct_aspect = BoolProperty(
+ name="Correct Aspect",
+ description="Map UVs taking image aspect ratio into account",
+ default=True)
+ use_subsurf_data = BoolProperty(
+ name="Use Subsurf Modifier",
+ description="""Map UVs taking vertex position after subsurf
+ into account""",
+ default=False)
+ margin = FloatProperty(
+ name="Margin",
+ description="Space between islands",
+ max=1.0,
+ min=0.0,
+ default=0.001)
+
+ # property for this operation
+ u_const = BoolProperty(
+ name="U-Constraint",
+ description="Keep UV U-axis coordinate",
+ default=False
+ )
+ v_const = BoolProperty(
+ name="V-Constraint",
+ description="Keep UV V-axis coordinate",
+ default=False
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ # bpy.ops.uv.unwrap() makes one UV map at least
+ if not bm.loops.layers.uv:
+ self.report({'WARNING'}, "Object must have more than one UV map")
+ return {'CANCELLED'}
+ uv_layer = bm.loops.layers.uv.verify()
+
+ # get original UV coordinate
+ faces = [f for f in bm.faces if f.select]
+ uv_list = []
+ for f in faces:
+ uvs = [l[uv_layer].uv.copy() for l in f.loops]
+ uv_list.append(uvs)
+
+ # unwrap
+ bpy.ops.uv.unwrap(
+ method=self.method,
+ fill_holes=self.fill_holes,
+ correct_aspect=self.correct_aspect,
+ use_subsurf_data=self.use_subsurf_data,
+ margin=self.margin)
+
+ # when U/V-Constraint is checked, revert original coordinate
+ for f, uvs in zip(faces, uv_list):
+ for l, uv in zip(f.loops, uvs):
+ if self.u_const:
+ l[uv_layer].uv.x = uv.x
+ if self.v_const:
+ l[uv_layer].uv.y = uv.y
+
+ # update mesh
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
diff --git a/magic_uv/op/uv_bounding_box.py b/magic_uv/op/uv_bounding_box.py
new file mode 100644
index 00000000..38d665e1
--- /dev/null
+++ b/magic_uv/op/uv_bounding_box.py
@@ -0,0 +1,842 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+from enum import IntEnum
+import math
+
+import bpy
+import mathutils
+import bmesh
+from bpy.props import BoolProperty, EnumProperty
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..utils import compatibility as compat
+
+if compat.check_version(2, 80, 0) >= 0:
+ from ..lib import bglx as bgl
+else:
+ import bgl
+
+
+MAX_VALUE = 100000.0
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
+ # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
+ # after the execution
+ for space in context.area.spaces:
+ if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'):
+ break
+ else:
+ return False
+
+ return True
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "uv_bounding_box"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ uv_info_ini = []
+ ctrl_points_ini = []
+ ctrl_points = []
+
+ scene.muv_props.uv_bounding_box = Props()
+
+ def get_func(_):
+ return MUV_OT_UVBoundingBox.is_running(bpy.context)
+
+ def set_func(_, __):
+ pass
+
+ def update_func(_, __):
+ bpy.ops.uv.muv_ot_uv_bounding_box('INVOKE_REGION_WIN')
+
+ scene.muv_uv_bounding_box_enabled = BoolProperty(
+ name="UV Bounding Box Enabled",
+ description="UV Bounding Box is enabled",
+ default=False
+ )
+ scene.muv_uv_bounding_box_show = BoolProperty(
+ name="UV Bounding Box Showed",
+ description="UV Bounding Box is showed",
+ default=False,
+ get=get_func,
+ set=set_func,
+ update=update_func
+ )
+ scene.muv_uv_bounding_box_uniform_scaling = BoolProperty(
+ name="Uniform Scaling",
+ description="Enable Uniform Scaling",
+ default=False
+ )
+ scene.muv_uv_bounding_box_boundary = EnumProperty(
+ name="Boundary",
+ description="Boundary",
+ default='UV_SEL',
+ items=[
+ ('UV', "UV", "Boundary is decided by UV"),
+ ('UV_SEL', "UV (Selected)",
+ "Boundary is decided by Selected UV")
+ ]
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_props.uv_bounding_box
+ del scene.muv_uv_bounding_box_enabled
+ del scene.muv_uv_bounding_box_show
+ del scene.muv_uv_bounding_box_uniform_scaling
+ del scene.muv_uv_bounding_box_boundary
+
+
+class CommandBase:
+ """
+ Custom class: Base class of command
+ """
+
+ def __init__(self):
+ self.op = 'NONE' # operation
+
+ def to_matrix(self):
+ # mat = I
+ mat = mathutils.Matrix()
+ mat.identity()
+ return mat
+
+
+class TranslationCommand(CommandBase):
+ """
+ Custom class: Translation operation
+ """
+
+ def __init__(self, ix, iy):
+ super().__init__()
+ self.op = 'TRANSLATION'
+ self.__x = ix # current x
+ self.__y = iy # current y
+ self.__ix = ix # initial x
+ self.__iy = iy # initial y
+
+ def to_matrix(self):
+ # mat = Mt
+ dx = self.__x - self.__ix
+ dy = self.__y - self.__iy
+ return mathutils.Matrix.Translation((dx, dy, 0))
+
+ def set(self, x, y):
+ self.__x = x
+ self.__y = y
+
+
+class RotationCommand(CommandBase):
+ """
+ Custom class: Rotation operation
+ """
+
+ def __init__(self, ix, iy, cx, cy):
+ super().__init__()
+ self.op = 'ROTATION'
+ self.__x = ix # current x
+ self.__y = iy # current y
+ self.__cx = cx # center of rotation x
+ self.__cy = cy # center of rotation y
+ dx = self.__x - self.__cx
+ dy = self.__y - self.__cy
+ self.__iangle = math.atan2(dy, dx) # initial rotation angle
+
+ def to_matrix(self):
+ # mat = Mt * Mr * Mt^-1
+ dx = self.__x - self.__cx
+ dy = self.__y - self.__cy
+ angle = math.atan2(dy, dx) - self.__iangle
+ mti = mathutils.Matrix.Translation((-self.__cx, -self.__cy, 0.0))
+ mr = mathutils.Matrix.Rotation(angle, 4, 'Z')
+ mt = mathutils.Matrix.Translation((self.__cx, self.__cy, 0.0))
+ return compat.matmul(compat.matmul(mt, mr), mti)
+
+ def set(self, x, y):
+ self.__x = x
+ self.__y = y
+
+
+class ScalingCommand(CommandBase):
+ """
+ Custom class: Scaling operation
+ """
+
+ def __init__(self, ix, iy, ox, oy, dir_x, dir_y, mat):
+ super().__init__()
+ self.op = 'SCALING'
+ self.__ix = ix # initial x
+ self.__iy = iy # initial y
+ self.__x = ix # current x
+ self.__y = iy # current y
+ self.__ox = ox # origin of scaling x
+ self.__oy = oy # origin of scaling y
+ self.__dir_x = dir_x # direction of scaling x
+ self.__dir_y = dir_y # direction of scaling y
+ self.__mat = mat
+ # initial origin of scaling = M(to original transform) * (ox, oy)
+ iov = compat.matmul(mat, mathutils.Vector((ox, oy, 0.0)))
+ self.__iox = iov.x # initial origin of scaling X
+ self.__ioy = iov.y # initial origin of scaling y
+
+ def to_matrix(self):
+ """
+ mat = M(to original transform)^-1 * Mt(to origin) * Ms *
+ Mt(to origin)^-1 * M(to original transform)
+ """
+ m = self.__mat
+ mi = self.__mat.inverted()
+ mtoi = mathutils.Matrix.Translation((-self.__iox, -self.__ioy, 0.0))
+ mto = mathutils.Matrix.Translation((self.__iox, self.__ioy, 0.0))
+ # every point must be transformed to origin
+ t = compat.matmul(m, mathutils.Vector((self.__ix, self.__iy, 0.0)))
+ tix, tiy = t.x, t.y
+ t = compat.matmul(m, mathutils.Vector((self.__ox, self.__oy, 0.0)))
+ tox, toy = t.x, t.y
+ t = compat.matmul(m, mathutils.Vector((self.__x, self.__y, 0.0)))
+ tx, ty = t.x, t.y
+ ms = mathutils.Matrix()
+ ms.identity()
+ if self.__dir_x == 1:
+ ms[0][0] = (tx - tox) * self.__dir_x / (tix - tox)
+ if self.__dir_y == 1:
+ ms[1][1] = (ty - toy) * self.__dir_y / (tiy - toy)
+ return compat.matmul(compat.matmul(compat.matmul(
+ compat.matmul(mi, mto), ms), mtoi), m)
+
+ def set(self, x, y):
+ self.__x = x
+ self.__y = y
+
+
+class UniformScalingCommand(CommandBase):
+ """
+ Custom class: Uniform Scaling operation
+ """
+
+ def __init__(self, ix, iy, ox, oy, mat):
+ super().__init__()
+ self.op = 'SCALING'
+ self.__ix = ix # initial x
+ self.__iy = iy # initial y
+ self.__x = ix # current x
+ self.__y = iy # current y
+ self.__ox = ox # origin of scaling x
+ self.__oy = oy # origin of scaling y
+ self.__mat = mat
+ # initial origin of scaling = M(to original transform) * (ox, oy)
+ iov = compat.matmul(mat, mathutils.Vector((ox, oy, 0.0)))
+ self.__iox = iov.x # initial origin of scaling x
+ self.__ioy = iov.y # initial origin of scaling y
+ self.__dir_x = 1
+ self.__dir_y = 1
+
+ def to_matrix(self):
+ """
+ mat = M(to original transform)^-1 * Mt(to origin) * Ms *
+ Mt(to origin)^-1 * M(to original transform)
+ """
+ m = self.__mat
+ mi = self.__mat.inverted()
+ mtoi = mathutils.Matrix.Translation((-self.__iox, -self.__ioy, 0.0))
+ mto = mathutils.Matrix.Translation((self.__iox, self.__ioy, 0.0))
+ # every point must be transformed to origin
+ t = compat.matmul(m, mathutils.Vector((self.__ix, self.__iy, 0.0)))
+ tix, tiy = t.x, t.y
+ t = compat.matmul(m, mathutils.Vector((self.__ox, self.__oy, 0.0)))
+ tox, toy = t.x, t.y
+ t = compat.matmul(m, mathutils.Vector((self.__x, self.__y, 0.0)))
+ tx, ty = t.x, t.y
+ ms = mathutils.Matrix()
+ ms.identity()
+ tir = math.sqrt((tix - tox) * (tix - tox) + (tiy - toy) * (tiy - toy))
+ tr = math.sqrt((tx - tox) * (tx - tox) + (ty - toy) * (ty - toy))
+
+ sr = tr / tir
+
+ if ((tx - tox) * (tix - tox)) > 0:
+ self.__dir_x = 1
+ else:
+ self.__dir_x = -1
+ if ((ty - toy) * (tiy - toy)) > 0:
+ self.__dir_y = 1
+ else:
+ self.__dir_y = -1
+
+ ms[0][0] = sr * self.__dir_x
+ ms[1][1] = sr * self.__dir_y
+
+ return compat.matmul(compat.matmul(compat.matmul(
+ compat.matmul(mi, mto), ms), mtoi), m)
+
+ def set(self, x, y):
+ self.__x = x
+ self.__y = y
+
+
+class CommandExecuter:
+ """
+ Custom class: manage command history and execute command
+ """
+
+ def __init__(self):
+ self.__cmd_list = [] # history
+ self.__cmd_list_redo = [] # redo list
+
+ def execute(self, begin=0, end=-1):
+ """
+ create matrix from history
+ """
+ mat = mathutils.Matrix()
+ mat.identity()
+ for i, cmd in enumerate(self.__cmd_list):
+ if begin <= i and (end == -1 or i <= end):
+ mat = compat.matmul(cmd.to_matrix(), mat)
+ return mat
+
+ def undo_size(self):
+ """
+ get history size
+ """
+ return len(self.__cmd_list)
+
+ def top(self):
+ """
+ get top of history
+ """
+ if len(self.__cmd_list) <= 0:
+ return None
+ return self.__cmd_list[-1]
+
+ def append(self, cmd):
+ """
+ append command
+ """
+ self.__cmd_list.append(cmd)
+ self.__cmd_list_redo = []
+
+ def undo(self):
+ """
+ undo command
+ """
+ if len(self.__cmd_list) <= 0:
+ return
+ self.__cmd_list_redo.append(self.__cmd_list.pop())
+
+ def redo(self):
+ """
+ redo command
+ """
+ if len(self.__cmd_list_redo) <= 0:
+ return
+ self.__cmd_list.append(self.__cmd_list_redo.pop())
+
+ def pop(self):
+ if len(self.__cmd_list) <= 0:
+ return None
+ return self.__cmd_list.pop()
+
+ def push(self, cmd):
+ self.__cmd_list.append(cmd)
+
+
+class State(IntEnum):
+ """
+ Enum: State definition used by MUV_UVBBStateMgr
+ """
+ NONE = 0
+ TRANSLATING = 1
+ SCALING_1 = 2
+ SCALING_2 = 3
+ SCALING_3 = 4
+ SCALING_4 = 5
+ SCALING_5 = 6
+ SCALING_6 = 7
+ SCALING_7 = 8
+ SCALING_8 = 9
+ ROTATING = 10
+ UNIFORM_SCALING_1 = 11
+ UNIFORM_SCALING_2 = 12
+ UNIFORM_SCALING_3 = 13
+ UNIFORM_SCALING_4 = 14
+
+
+class StateBase:
+ """
+ Custom class: Base class of state
+ """
+
+ def __init__(self):
+ pass
+
+ def update(self, context, event, ctrl_points, mouse_view):
+ raise NotImplementedError
+
+
+class StateNone(StateBase):
+ """
+ Custom class:
+ No state
+ Wait for event from mouse
+ """
+
+ def __init__(self, cmd_exec):
+ super().__init__()
+ self.__cmd_exec = cmd_exec
+
+ def update(self, context, event, ctrl_points, mouse_view):
+ """
+ Update state
+ """
+ user_prefs = compat.get_user_preferences(context)
+ prefs = user_prefs.addons["magic_uv"].preferences
+ cp_react_size = prefs.uv_bounding_box_cp_react_size
+ is_uscaling = context.scene.muv_uv_bounding_box_uniform_scaling
+ if (event.type == 'LEFTMOUSE') and (event.value == 'PRESS'):
+ x, y = context.region.view2d.view_to_region(
+ mouse_view.x, mouse_view.y)
+ for i, p in enumerate(ctrl_points):
+ px, py = context.region.view2d.view_to_region(p.x, p.y)
+ in_cp_x = (px + cp_react_size > x and
+ px - cp_react_size < x)
+ in_cp_y = (py + cp_react_size > y and
+ py - cp_react_size < y)
+ if in_cp_x and in_cp_y:
+ if is_uscaling:
+ arr = [1, 3, 6, 8]
+ if i in arr:
+ return (
+ State.UNIFORM_SCALING_1 +
+ arr.index(i)
+ )
+ else:
+ return State.TRANSLATING + i
+
+ return State.NONE
+
+
+class StateTranslating(StateBase):
+ """
+ Custom class: Translating state
+ """
+
+ def __init__(self, cmd_exec, ctrl_points):
+ super().__init__()
+ self.__cmd_exec = cmd_exec
+ ix, iy = ctrl_points[0].x, ctrl_points[0].y
+ self.__cmd_exec.append(TranslationCommand(ix, iy))
+
+ def update(self, context, event, ctrl_points, mouse_view):
+ if event.type == 'LEFTMOUSE':
+ if event.value == 'RELEASE':
+ return State.NONE
+ if event.type == 'MOUSEMOVE':
+ x, y = mouse_view.x, mouse_view.y
+ self.__cmd_exec.top().set(x, y)
+ return State.TRANSLATING
+
+
+class StateScaling(StateBase):
+ """
+ Custom class: Scaling state
+ """
+
+ def __init__(self, cmd_exec, state, ctrl_points):
+ super().__init__()
+ self.__state = state
+ self.__cmd_exec = cmd_exec
+ dir_x_list = [1, 1, 1, 0, 0, 1, 1, 1]
+ dir_y_list = [1, 0, 1, 1, 1, 1, 0, 1]
+ idx = state - 2
+ ix, iy = ctrl_points[idx + 1].x, ctrl_points[idx + 1].y
+ ox, oy = ctrl_points[8 - idx].x, ctrl_points[8 - idx].y
+ dir_x, dir_y = dir_x_list[idx], dir_y_list[idx]
+ mat = self.__cmd_exec.execute(end=self.__cmd_exec.undo_size())
+ self.__cmd_exec.append(
+ ScalingCommand(ix, iy, ox, oy, dir_x, dir_y, mat.inverted()))
+
+ def update(self, context, event, ctrl_points, mouse_view):
+ if event.type == 'LEFTMOUSE':
+ if event.value == 'RELEASE':
+ return State.NONE
+ if event.type == 'MOUSEMOVE':
+ x, y = mouse_view.x, mouse_view.y
+ self.__cmd_exec.top().set(x, y)
+ return self.__state
+
+
+class StateUniformScaling(StateBase):
+ """
+ Custom class: Uniform Scaling state
+ """
+
+ def __init__(self, cmd_exec, state, ctrl_points):
+ super().__init__()
+ self.__state = state
+ self.__cmd_exec = cmd_exec
+ icp_idx = [1, 3, 6, 8]
+ ocp_idx = [8, 6, 3, 1]
+ idx = state - State.UNIFORM_SCALING_1
+ ix, iy = ctrl_points[icp_idx[idx]].x, ctrl_points[icp_idx[idx]].y
+ ox, oy = ctrl_points[ocp_idx[idx]].x, ctrl_points[ocp_idx[idx]].y
+ mat = self.__cmd_exec.execute(end=self.__cmd_exec.undo_size())
+ self.__cmd_exec.append(UniformScalingCommand(
+ ix, iy, ox, oy, mat.inverted()))
+
+ def update(self, context, event, ctrl_points, mouse_view):
+ if event.type == 'LEFTMOUSE':
+ if event.value == 'RELEASE':
+ return State.NONE
+ if event.type == 'MOUSEMOVE':
+ x, y = mouse_view.x, mouse_view.y
+ self.__cmd_exec.top().set(x, y)
+
+ return self.__state
+
+
+class StateRotating(StateBase):
+ """
+ Custom class: Rotating state
+ """
+
+ def __init__(self, cmd_exec, ctrl_points):
+ super().__init__()
+ self.__cmd_exec = cmd_exec
+ ix, iy = ctrl_points[9].x, ctrl_points[9].y
+ ox, oy = ctrl_points[0].x, ctrl_points[0].y
+ self.__cmd_exec.append(RotationCommand(ix, iy, ox, oy))
+
+ def update(self, context, event, ctrl_points, mouse_view):
+ if event.type == 'LEFTMOUSE':
+ if event.value == 'RELEASE':
+ return State.NONE
+ if event.type == 'MOUSEMOVE':
+ x, y = mouse_view.x, mouse_view.y
+ self.__cmd_exec.top().set(x, y)
+ return State.ROTATING
+
+
+class StateManager:
+ """
+ Custom class: Manage state about this feature
+ """
+
+ def __init__(self, cmd_exec):
+ self.__cmd_exec = cmd_exec # command executer
+ self.__state = State.NONE # current state
+ self.__state_obj = StateNone(self.__cmd_exec)
+
+ def __update_state(self, next_state, ctrl_points):
+ """
+ Update state
+ """
+
+ if next_state == self.__state:
+ return
+ obj = None
+ if next_state == State.TRANSLATING:
+ obj = StateTranslating(self.__cmd_exec, ctrl_points)
+ elif State.SCALING_1 <= next_state <= State.SCALING_8:
+ obj = StateScaling(
+ self.__cmd_exec, next_state, ctrl_points)
+ elif next_state == State.ROTATING:
+ obj = StateRotating(self.__cmd_exec, ctrl_points)
+ elif next_state == State.NONE:
+ obj = StateNone(self.__cmd_exec)
+ elif (State.UNIFORM_SCALING_1 <= next_state <=
+ State.UNIFORM_SCALING_4):
+ obj = StateUniformScaling(
+ self.__cmd_exec, next_state, ctrl_points)
+
+ if obj is not None:
+ self.__state_obj = obj
+
+ self.__state = next_state
+
+ def update(self, context, ctrl_points, event):
+ mouse_region = mathutils.Vector((
+ event.mouse_region_x, event.mouse_region_y))
+ mouse_view = mathutils.Vector((context.region.view2d.region_to_view(
+ mouse_region.x, mouse_region.y)))
+ next_state = self.__state_obj.update(
+ context, event, ctrl_points, mouse_view)
+ self.__update_state(next_state, ctrl_points)
+
+ return self.__state
+
+
+@BlClassRegistry()
+class MUV_OT_UVBoundingBox(bpy.types.Operator):
+ """
+ Operation class: UV Bounding Box
+ """
+
+ bl_idname = "uv.muv_ot_uv_bounding_box"
+ bl_label = "UV Bounding Box"
+ bl_description = "Internal operation for UV Bounding Box"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def __init__(self):
+ self.__timer = None
+ self.__cmd_exec = CommandExecuter() # Command executor
+ self.__state_mgr = StateManager(self.__cmd_exec) # State Manager
+
+ __handle = None
+ __timer = None
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return False
+ return _is_valid_context(context)
+
+ @classmethod
+ def is_running(cls, _):
+ return 1 if cls.__handle else 0
+
+ @classmethod
+ def handle_add(cls, obj, context):
+ if cls.__handle is None:
+ sie = bpy.types.SpaceImageEditor
+ cls.__handle = sie.draw_handler_add(
+ cls.draw_bb, (obj, context), "WINDOW", "POST_PIXEL")
+ if cls.__timer is None:
+ cls.__timer = context.window_manager.event_timer_add(
+ 0.1, window=context.window)
+ context.window_manager.modal_handler_add(obj)
+
+ @classmethod
+ def handle_remove(cls, context):
+ if cls.__handle is not None:
+ sie = bpy.types.SpaceImageEditor
+ sie.draw_handler_remove(cls.__handle, "WINDOW")
+ cls.__handle = None
+ if cls.__timer is not None:
+ context.window_manager.event_timer_remove(cls.__timer)
+ cls.__timer = None
+
+ @classmethod
+ def __draw_ctrl_point(cls, context, pos):
+ """
+ Draw control point
+ """
+ user_prefs = compat.get_user_preferences(context)
+ prefs = user_prefs.addons["magic_uv"].preferences
+ cp_size = prefs.uv_bounding_box_cp_size
+ offset = cp_size / 2
+ verts = [
+ [pos.x - offset, pos.y - offset],
+ [pos.x - offset, pos.y + offset],
+ [pos.x + offset, pos.y + offset],
+ [pos.x + offset, pos.y - offset]
+ ]
+ bgl.glEnable(bgl.GL_BLEND)
+ bgl.glBegin(bgl.GL_QUADS)
+ bgl.glColor4f(1.0, 1.0, 1.0, 1.0)
+ for (x, y) in verts:
+ bgl.glVertex2f(x, y)
+ bgl.glEnd()
+
+ @classmethod
+ def draw_bb(cls, _, context):
+ """
+ Draw bounding box
+ """
+ props = context.scene.muv_props.uv_bounding_box
+
+ if not MUV_OT_UVBoundingBox.is_running(context):
+ return
+
+ if not _is_valid_context(context):
+ return
+
+ for cp in props.ctrl_points:
+ cls.__draw_ctrl_point(
+ context, mathutils.Vector(
+ context.region.view2d.view_to_region(cp.x, cp.y)))
+
+ def __get_uv_info(self, context):
+ """
+ Get UV coordinate
+ """
+ sc = context.scene
+ obj = context.active_object
+ uv_info = []
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+ if not bm.loops.layers.uv:
+ return None
+ uv_layer = bm.loops.layers.uv.verify()
+ for f in bm.faces:
+ if not f.select:
+ continue
+ for i, l in enumerate(f.loops):
+ if sc.muv_uv_bounding_box_boundary == 'UV_SEL':
+ if l[uv_layer].select:
+ uv_info.append((f.index, i, l[uv_layer].uv.copy()))
+ elif sc.muv_uv_bounding_box_boundary == 'UV':
+ uv_info.append((f.index, i, l[uv_layer].uv.copy()))
+ if not uv_info:
+ return None
+ return uv_info
+
+ def __get_ctrl_point(self, uv_info_ini):
+ """
+ Get control point
+ """
+ left = MAX_VALUE
+ right = -MAX_VALUE
+ top = -MAX_VALUE
+ bottom = MAX_VALUE
+
+ for info in uv_info_ini:
+ uv = info[2]
+ if uv.x < left:
+ left = uv.x
+ if uv.x > right:
+ right = uv.x
+ if uv.y < bottom:
+ bottom = uv.y
+ if uv.y > top:
+ top = uv.y
+
+ points = [
+ mathutils.Vector((
+ (left + right) * 0.5, (top + bottom) * 0.5, 0.0
+ )),
+ mathutils.Vector((left, top, 0.0)),
+ mathutils.Vector((left, (top + bottom) * 0.5, 0.0)),
+ mathutils.Vector((left, bottom, 0.0)),
+ mathutils.Vector(((left + right) * 0.5, top, 0.0)),
+ mathutils.Vector(((left + right) * 0.5, bottom, 0.0)),
+ mathutils.Vector((right, top, 0.0)),
+ mathutils.Vector((right, (top + bottom) * 0.5, 0.0)),
+ mathutils.Vector((right, bottom, 0.0)),
+ mathutils.Vector(((left + right) * 0.5, top + 0.03, 0.0))
+ ]
+
+ return points
+
+ def __update_uvs(self, context, uv_info_ini, trans_mat):
+ """
+ Update UV coordinate
+ """
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+ if not bm.loops.layers.uv:
+ return
+ uv_layer = bm.loops.layers.uv.verify()
+ for info in uv_info_ini:
+ fidx = info[0]
+ lidx = info[1]
+ uv = info[2]
+ v = mathutils.Vector((uv.x, uv.y, 0.0))
+ av = compat.matmul(trans_mat, v)
+ bm.faces[fidx].loops[lidx][uv_layer].uv = mathutils.Vector(
+ (av.x, av.y))
+ bmesh.update_edit_mesh(obj.data)
+
+ def __update_ctrl_point(self, ctrl_points_ini, trans_mat):
+ """
+ Update control point
+ """
+ return [compat.matmul(trans_mat, cp) for cp in ctrl_points_ini]
+
+ def modal(self, context, event):
+ props = context.scene.muv_props.uv_bounding_box
+ common.redraw_all_areas()
+
+ if not MUV_OT_UVBoundingBox.is_running(context):
+ return {'FINISHED'}
+
+ if not _is_valid_context(context):
+ MUV_OT_UVBoundingBox.handle_remove(context)
+ return {'FINISHED'}
+
+ region_types = [
+ 'HEADER',
+ 'UI',
+ 'TOOLS',
+ ]
+ if not common.mouse_on_area(event, 'IMAGE_EDITOR') or \
+ common.mouse_on_regions(event, 'IMAGE_EDITOR', region_types):
+ return {'PASS_THROUGH'}
+
+ if event.type == 'TIMER':
+ trans_mat = self.__cmd_exec.execute()
+ self.__update_uvs(context, props.uv_info_ini, trans_mat)
+ props.ctrl_points = self.__update_ctrl_point(
+ props.ctrl_points_ini, trans_mat)
+
+ state = self.__state_mgr.update(context, props.ctrl_points, event)
+ if state == State.NONE:
+ return {'PASS_THROUGH'}
+
+ return {'RUNNING_MODAL'}
+
+ def invoke(self, context, _):
+ props = context.scene.muv_props.uv_bounding_box
+
+ if MUV_OT_UVBoundingBox.is_running(context):
+ MUV_OT_UVBoundingBox.handle_remove(context)
+ return {'FINISHED'}
+
+ props.uv_info_ini = self.__get_uv_info(context)
+ if props.uv_info_ini is None:
+ return {'CANCELLED'}
+
+ MUV_OT_UVBoundingBox.handle_add(self, context)
+
+ props.ctrl_points_ini = self.__get_ctrl_point(props.uv_info_ini)
+ trans_mat = self.__cmd_exec.execute()
+ # Update is needed in order to display control point
+ self.__update_uvs(context, props.uv_info_ini, trans_mat)
+ props.ctrl_points = self.__update_ctrl_point(
+ props.ctrl_points_ini, trans_mat)
+
+ return {'RUNNING_MODAL'}
diff --git a/magic_uv/op/uv_inspection.py b/magic_uv/op/uv_inspection.py
new file mode 100644
index 00000000..61cbf1ed
--- /dev/null
+++ b/magic_uv/op/uv_inspection.py
@@ -0,0 +1,281 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+from bpy.props import BoolProperty, EnumProperty
+import bmesh
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..utils import compatibility as compat
+
+if compat.check_version(2, 80, 0) >= 0:
+ from ..lib import bglx as bgl
+else:
+ import bgl
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
+ # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
+ # after the execution
+ for space in context.area.spaces:
+ if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'):
+ break
+ else:
+ return False
+
+ return True
+
+
+def _update_uvinsp_info(context):
+ sc = context.scene
+ props = sc.muv_props.uv_inspection
+
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+ uv_layer = bm.loops.layers.uv.verify()
+
+ if context.tool_settings.use_uv_select_sync:
+ sel_faces = [f for f in bm.faces]
+ else:
+ sel_faces = [f for f in bm.faces if f.select]
+ props.overlapped_info = common.get_overlapped_uv_info(
+ bm, sel_faces, uv_layer, sc.muv_uv_inspection_show_mode)
+ props.flipped_info = common.get_flipped_uv_info(sel_faces, uv_layer)
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "uv_inspection"
+
+ @classmethod
+ def init_props(cls, scene):
+ class Props():
+ overlapped_info = []
+ flipped_info = []
+
+ scene.muv_props.uv_inspection = Props()
+
+ def get_func(_):
+ return MUV_OT_UVInspection_Render.is_running(bpy.context)
+
+ def set_func(_, __):
+ pass
+
+ def update_func(_, __):
+ bpy.ops.uv.muv_ot_uv_inspection_render('INVOKE_REGION_WIN')
+
+ scene.muv_uv_inspection_enabled = BoolProperty(
+ name="UV Inspection Enabled",
+ description="UV Inspection is enabled",
+ default=False
+ )
+ scene.muv_uv_inspection_show = BoolProperty(
+ name="UV Inspection Showed",
+ description="UV Inspection is showed",
+ default=False,
+ get=get_func,
+ set=set_func,
+ update=update_func
+ )
+ scene.muv_uv_inspection_show_overlapped = BoolProperty(
+ name="Overlapped",
+ description="Show overlapped UVs",
+ default=False
+ )
+ scene.muv_uv_inspection_show_flipped = BoolProperty(
+ name="Flipped",
+ description="Show flipped UVs",
+ default=False
+ )
+ scene.muv_uv_inspection_show_mode = EnumProperty(
+ name="Mode",
+ description="Show mode",
+ items=[
+ ('PART', "Part", "Show only overlapped/flipped part"),
+ ('FACE', "Face", "Show overlapped/flipped face")
+ ],
+ default='PART'
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_props.uv_inspection
+ del scene.muv_uv_inspection_enabled
+ del scene.muv_uv_inspection_show
+ del scene.muv_uv_inspection_show_overlapped
+ del scene.muv_uv_inspection_show_flipped
+ del scene.muv_uv_inspection_show_mode
+
+
+@BlClassRegistry()
+class MUV_OT_UVInspection_Render(bpy.types.Operator):
+ """
+ Operation class: Render UV Inspection
+ No operation (only rendering)
+ """
+
+ bl_idname = "uv.muv_ot_uv_inspection_render"
+ bl_description = "Render overlapped/flipped UVs"
+ bl_label = "Overlapped/Flipped UV renderer"
+
+ __handle = None
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return False
+ return _is_valid_context(context)
+
+ @classmethod
+ def is_running(cls, _):
+ return 1 if cls.__handle else 0
+
+ @classmethod
+ def handle_add(cls, obj, context):
+ sie = bpy.types.SpaceImageEditor
+ cls.__handle = sie.draw_handler_add(
+ MUV_OT_UVInspection_Render.draw, (obj, context),
+ 'WINDOW', 'POST_PIXEL')
+
+ @classmethod
+ def handle_remove(cls):
+ if cls.__handle is not None:
+ bpy.types.SpaceImageEditor.draw_handler_remove(
+ cls.__handle, 'WINDOW')
+ cls.__handle = None
+
+ @staticmethod
+ def draw(_, context):
+ sc = context.scene
+ props = sc.muv_props.uv_inspection
+ user_prefs = compat.get_user_preferences(context)
+ prefs = user_prefs.addons["magic_uv"].preferences
+
+ if not MUV_OT_UVInspection_Render.is_running(context):
+ return
+
+ # OpenGL configuration
+ bgl.glEnable(bgl.GL_BLEND)
+
+ # render overlapped UV
+ if sc.muv_uv_inspection_show_overlapped:
+ color = prefs.uv_inspection_overlapped_color
+ for info in props.overlapped_info:
+ if sc.muv_uv_inspection_show_mode == 'PART':
+ for poly in info["polygons"]:
+ bgl.glBegin(bgl.GL_TRIANGLE_FAN)
+ bgl.glColor4f(color[0], color[1], color[2], color[3])
+ for uv in poly:
+ x, y = context.region.view2d.view_to_region(
+ uv.x, uv.y)
+ bgl.glVertex2f(x, y)
+ bgl.glEnd()
+ elif sc.muv_uv_inspection_show_mode == 'FACE':
+ bgl.glBegin(bgl.GL_TRIANGLE_FAN)
+ bgl.glColor4f(color[0], color[1], color[2], color[3])
+ for uv in info["subject_uvs"]:
+ x, y = context.region.view2d.view_to_region(uv.x, uv.y)
+ bgl.glVertex2f(x, y)
+ bgl.glEnd()
+
+ # render flipped UV
+ if sc.muv_uv_inspection_show_flipped:
+ color = prefs.uv_inspection_flipped_color
+ for info in props.flipped_info:
+ if sc.muv_uv_inspection_show_mode == 'PART':
+ for poly in info["polygons"]:
+ bgl.glBegin(bgl.GL_TRIANGLE_FAN)
+ bgl.glColor4f(color[0], color[1], color[2], color[3])
+ for uv in poly:
+ x, y = context.region.view2d.view_to_region(
+ uv.x, uv.y)
+ bgl.glVertex2f(x, y)
+ bgl.glEnd()
+ elif sc.muv_uv_inspection_show_mode == 'FACE':
+ bgl.glBegin(bgl.GL_TRIANGLE_FAN)
+ bgl.glColor4f(color[0], color[1], color[2], color[3])
+ for uv in info["uvs"]:
+ x, y = context.region.view2d.view_to_region(uv.x, uv.y)
+ bgl.glVertex2f(x, y)
+ bgl.glEnd()
+
+ bgl.glDisable(bgl.GL_BLEND)
+
+ def invoke(self, context, _):
+ if not MUV_OT_UVInspection_Render.is_running(context):
+ _update_uvinsp_info(context)
+ MUV_OT_UVInspection_Render.handle_add(self, context)
+ else:
+ MUV_OT_UVInspection_Render.handle_remove()
+
+ if context.area:
+ context.area.tag_redraw()
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+class MUV_OT_UVInspection_Update(bpy.types.Operator):
+ """
+ Operation class: Update
+ """
+
+ bl_idname = "uv.muv_ot_uv_inspection_update"
+ bl_label = "Update UV Inspection"
+ bl_description = "Update UV Inspection"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ if not MUV_OT_UVInspection_Render.is_running(context):
+ return False
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ _update_uvinsp_info(context)
+
+ if context.area:
+ context.area.tag_redraw()
+
+ return {'FINISHED'}
diff --git a/magic_uv/op/uv_sculpt.py b/magic_uv/op/uv_sculpt.py
new file mode 100644
index 00000000..de5f1e02
--- /dev/null
+++ b/magic_uv/op/uv_sculpt.py
@@ -0,0 +1,487 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+from math import pi, cos, tan, sin
+
+import bpy
+import bmesh
+from mathutils import Vector
+from bpy_extras import view3d_utils
+from mathutils.bvhtree import BVHTree
+from mathutils.geometry import barycentric_transform
+from bpy.props import (
+ BoolProperty,
+ IntProperty,
+ EnumProperty,
+ FloatProperty,
+)
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..utils import compatibility as compat
+
+
+if compat.check_version(2, 80, 0) >= 0:
+ from ..lib import bglx as bgl
+else:
+ import bgl
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+def _get_strength(p, len_, factor):
+ f = factor
+
+ if p > len_:
+ return 0.0
+
+ if p < 0.0:
+ return f
+
+ return (len_ - p) * f / len_
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "uv_sculpt"
+
+ @classmethod
+ def init_props(cls, scene):
+ def get_func(_):
+ return MUV_OT_UVSculpt.is_running(bpy.context)
+
+ def set_func(_, __):
+ pass
+
+ def update_func(_, __):
+ bpy.ops.uv.muv_ot_uv_sculpt('INVOKE_REGION_WIN')
+
+ scene.muv_uv_sculpt_enabled = BoolProperty(
+ name="UV Sculpt",
+ description="UV Sculpt is enabled",
+ default=False
+ )
+ scene.muv_uv_sculpt_enable = BoolProperty(
+ name="UV Sculpt Showed",
+ description="UV Sculpt is enabled",
+ default=False,
+ get=get_func,
+ set=set_func,
+ update=update_func
+ )
+ scene.muv_uv_sculpt_radius = IntProperty(
+ name="Radius",
+ description="Radius of the brush",
+ min=1,
+ max=500,
+ default=30
+ )
+ scene.muv_uv_sculpt_strength = FloatProperty(
+ name="Strength",
+ description="How powerful the effect of the brush when applied",
+ min=0.0,
+ max=1.0,
+ default=0.03,
+ )
+ scene.muv_uv_sculpt_tools = EnumProperty(
+ name="Tools",
+ description="Select Tools for the UV sculpt brushes",
+ items=[
+ ('GRAB', "Grab", "Grab UVs"),
+ ('RELAX', "Relax", "Relax UVs"),
+ ('PINCH', "Pinch", "Pinch UVs")
+ ],
+ default='GRAB'
+ )
+ scene.muv_uv_sculpt_show_brush = BoolProperty(
+ name="Show Brush",
+ description="Show Brush",
+ default=True
+ )
+ scene.muv_uv_sculpt_pinch_invert = BoolProperty(
+ name="Invert",
+ description="Pinch UV to invert direction",
+ default=False
+ )
+ scene.muv_uv_sculpt_relax_method = EnumProperty(
+ name="Method",
+ description="Algorithm used for relaxation",
+ items=[
+ ('HC', "HC", "Use HC method for relaxation"),
+ ('LAPLACIAN', "Laplacian",
+ "Use laplacian method for relaxation")
+ ],
+ default='HC'
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_uv_sculpt_enabled
+ del scene.muv_uv_sculpt_enable
+ del scene.muv_uv_sculpt_radius
+ del scene.muv_uv_sculpt_strength
+ del scene.muv_uv_sculpt_tools
+ del scene.muv_uv_sculpt_show_brush
+ del scene.muv_uv_sculpt_pinch_invert
+ del scene.muv_uv_sculpt_relax_method
+
+
+@BlClassRegistry()
+class MUV_OT_UVSculpt(bpy.types.Operator):
+ """
+ Operation class: UV Sculpt in View3D
+ """
+
+ bl_idname = "uv.muv_ot_uv_sculpt"
+ bl_label = "UV Sculpt"
+ bl_description = "UV Sculpt in View3D"
+ bl_options = {'REGISTER'}
+
+ __handle = None
+ __timer = None
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return False
+ return _is_valid_context(context)
+
+ @classmethod
+ def is_running(cls, _):
+ return 1 if cls.__handle else 0
+
+ @classmethod
+ def handle_add(cls, obj, context):
+ if not cls.__handle:
+ sv = bpy.types.SpaceView3D
+ cls.__handle = sv.draw_handler_add(cls.draw_brush, (obj, context),
+ "WINDOW", "POST_PIXEL")
+ if not cls.__timer:
+ cls.__timer = context.window_manager.event_timer_add(
+ 0.1, window=context.window)
+ context.window_manager.modal_handler_add(obj)
+
+ @classmethod
+ def handle_remove(cls, context):
+ if cls.__handle:
+ sv = bpy.types.SpaceView3D
+ sv.draw_handler_remove(cls.__handle, "WINDOW")
+ cls.__handle = None
+ if cls.__timer:
+ context.window_manager.event_timer_remove(cls.__timer)
+ cls.__timer = None
+
+ @classmethod
+ def draw_brush(cls, obj, context):
+ sc = context.scene
+ user_prefs = compat.get_user_preferences(context)
+ prefs = user_prefs.addons["magic_uv"].preferences
+
+ num_segment = 180
+ theta = 2 * pi / num_segment
+ fact_t = tan(theta)
+ fact_r = cos(theta)
+ color = prefs.uv_sculpt_brush_color
+
+ bgl.glBegin(bgl.GL_LINE_STRIP)
+ bgl.glColor4f(color[0], color[1], color[2], color[3])
+ x = sc.muv_uv_sculpt_radius * cos(0.0)
+ y = sc.muv_uv_sculpt_radius * sin(0.0)
+ for _ in range(num_segment):
+ bgl.glVertex2f(x + obj.current_mco.x, y + obj.current_mco.y)
+ tx = -y
+ ty = x
+ x = x + tx * fact_t
+ y = y + ty * fact_t
+ x = x * fact_r
+ y = y * fact_r
+ bgl.glEnd()
+
+ def __init__(self):
+ self.__loop_info = []
+ self.__stroking = False
+ self.current_mco = Vector((0.0, 0.0))
+ self.__initial_mco = Vector((0.0, 0.0))
+
+ def __stroke_init(self, context, _):
+ sc = context.scene
+
+ self.__initial_mco = self.current_mco
+
+ # get influenced UV
+ obj = context.active_object
+ world_mat = obj.matrix_world
+ bm = bmesh.from_edit_mesh(obj.data)
+ uv_layer = bm.loops.layers.uv.verify()
+ _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D')
+
+ self.__loop_info = []
+ for f in bm.faces:
+ if not f.select:
+ continue
+ for i, l in enumerate(f.loops):
+ loc_2d = view3d_utils.location_3d_to_region_2d(
+ region, space.region_3d,
+ compat.matmul(world_mat, l.vert.co))
+ diff = loc_2d - self.__initial_mco
+ if diff.length < sc.muv_uv_sculpt_radius:
+ info = {
+ "face_idx": f.index,
+ "loop_idx": i,
+ "initial_vco": l.vert.co.copy(),
+ "initial_vco_2d": loc_2d,
+ "initial_uv": l[uv_layer].uv.copy(),
+ "strength": _get_strength(
+ diff.length, sc.muv_uv_sculpt_radius,
+ sc.muv_uv_sculpt_strength)
+ }
+ self.__loop_info.append(info)
+
+ def __stroke_apply(self, context, _):
+ sc = context.scene
+ obj = context.active_object
+ world_mat = obj.matrix_world
+ bm = bmesh.from_edit_mesh(obj.data)
+ uv_layer = bm.loops.layers.uv.verify()
+ mco = self.current_mco
+
+ if sc.muv_uv_sculpt_tools == 'GRAB':
+ for info in self.__loop_info:
+ diff_uv = (mco - self.__initial_mco) * info["strength"]
+ l = bm.faces[info["face_idx"]].loops[info["loop_idx"]]
+ l[uv_layer].uv = info["initial_uv"] + diff_uv / 100.0
+
+ elif sc.muv_uv_sculpt_tools == 'PINCH':
+ _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D')
+ loop_info = []
+ for f in bm.faces:
+ if not f.select:
+ continue
+ for i, l in enumerate(f.loops):
+ loc_2d = view3d_utils.location_3d_to_region_2d(
+ region, space.region_3d,
+ compat.matmul(world_mat, l.vert.co))
+ diff = loc_2d - self.__initial_mco
+ if diff.length < sc.muv_uv_sculpt_radius:
+ info = {
+ "face_idx": f.index,
+ "loop_idx": i,
+ "initial_vco": l.vert.co.copy(),
+ "initial_vco_2d": loc_2d,
+ "initial_uv": l[uv_layer].uv.copy(),
+ "strength": _get_strength(
+ diff.length, sc.muv_uv_sculpt_radius,
+ sc.muv_uv_sculpt_strength)
+ }
+ loop_info.append(info)
+
+ # mouse coordinate to UV coordinate
+ ray_vec = view3d_utils.region_2d_to_vector_3d(region,
+ space.region_3d, mco)
+ ray_vec.normalize()
+ ray_orig = view3d_utils.region_2d_to_origin_3d(region,
+ space.region_3d,
+ mco)
+ ray_tgt = ray_orig + ray_vec * 1000000.0
+ mwi = world_mat.inverted()
+ ray_orig_obj = compat.matmul(mwi, ray_orig)
+ ray_tgt_obj = compat.matmul(mwi, ray_tgt)
+ ray_dir_obj = ray_tgt_obj - ray_orig_obj
+ ray_dir_obj.normalize()
+ tree = BVHTree.FromBMesh(bm)
+ loc, _, fidx, _ = tree.ray_cast(ray_orig_obj, ray_dir_obj)
+ if not loc:
+ return
+ loops = [l for l in bm.faces[fidx].loops]
+ uvs = [Vector((l[uv_layer].uv.x, l[uv_layer].uv.y, 0.0))
+ for l in loops]
+ target_uv = barycentric_transform(
+ loc, loops[0].vert.co, loops[1].vert.co, loops[2].vert.co,
+ uvs[0], uvs[1], uvs[2])
+ target_uv = Vector((target_uv.x, target_uv.y))
+
+ # move to target UV coordinate
+ for info in loop_info:
+ l = bm.faces[info["face_idx"]].loops[info["loop_idx"]]
+ if sc.muv_uv_sculpt_pinch_invert:
+ diff_uv = (l[uv_layer].uv - target_uv) * info["strength"]
+ else:
+ diff_uv = (target_uv - l[uv_layer].uv) * info["strength"]
+ l[uv_layer].uv = l[uv_layer].uv + diff_uv / 10.0
+
+ elif sc.muv_uv_sculpt_tools == 'RELAX':
+ _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D')
+
+ # get vertex and loop relation
+ vert_db = {}
+ for f in bm.faces:
+ for l in f.loops:
+ if l.vert in vert_db:
+ vert_db[l.vert]["loops"].append(l)
+ else:
+ vert_db[l.vert] = {"loops": [l]}
+
+ # get relaxation information
+ for k in vert_db.keys():
+ d = vert_db[k]
+ d["uv_sum"] = Vector((0.0, 0.0))
+ d["uv_count"] = 0
+
+ for l in d["loops"]:
+ ln = l.link_loop_next
+ lp = l.link_loop_prev
+ d["uv_sum"] = d["uv_sum"] + ln[uv_layer].uv
+ d["uv_sum"] = d["uv_sum"] + lp[uv_layer].uv
+ d["uv_count"] = d["uv_count"] + 2
+ d["uv_p"] = d["uv_sum"] / d["uv_count"]
+ d["uv_b"] = d["uv_p"] - d["loops"][0][uv_layer].uv
+ for k in vert_db.keys():
+ d = vert_db[k]
+ d["uv_sum_b"] = Vector((0.0, 0.0))
+ for l in d["loops"]:
+ ln = l.link_loop_next
+ lp = l.link_loop_prev
+ dn = vert_db[ln.vert]
+ dp = vert_db[lp.vert]
+ d["uv_sum_b"] = d["uv_sum_b"] + dn["uv_b"] + dp["uv_b"]
+
+ # apply
+ for f in bm.faces:
+ if not f.select:
+ continue
+ for i, l in enumerate(f.loops):
+ loc_2d = view3d_utils.location_3d_to_region_2d(
+ region, space.region_3d,
+ compat.matmul(world_mat, l.vert.co))
+ diff = loc_2d - self.__initial_mco
+ if diff.length >= sc.muv_uv_sculpt_radius:
+ continue
+ db = vert_db[l.vert]
+ strength = _get_strength(diff.length,
+ sc.muv_uv_sculpt_radius,
+ sc.muv_uv_sculpt_strength)
+
+ base = (1.0 - strength) * l[uv_layer].uv
+ if sc.muv_uv_sculpt_relax_method == 'HC':
+ t = 0.5 * (db["uv_b"] + db["uv_sum_b"] / d["uv_count"])
+ diff = strength * (db["uv_p"] - t)
+ target_uv = base + diff
+ elif sc.muv_uv_sculpt_relax_method == 'LAPLACIAN':
+ diff = strength * db["uv_p"]
+ target_uv = base + diff
+ else:
+ continue
+
+ l[uv_layer].uv = target_uv
+
+ bmesh.update_edit_mesh(obj.data)
+
+ def __stroke_exit(self, context, _):
+ sc = context.scene
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ uv_layer = bm.loops.layers.uv.verify()
+ mco = self.current_mco
+
+ if sc.muv_uv_sculpt_tools == 'GRAB':
+ for info in self.__loop_info:
+ diff_uv = (mco - self.__initial_mco) * info["strength"]
+ l = bm.faces[info["face_idx"]].loops[info["loop_idx"]]
+ l[uv_layer].uv = info["initial_uv"] + diff_uv / 100.0
+
+ bmesh.update_edit_mesh(obj.data)
+
+ def modal(self, context, event):
+ if context.area:
+ context.area.tag_redraw()
+
+ if not MUV_OT_UVSculpt.is_running(context):
+ MUV_OT_UVSculpt.handle_remove(context)
+ return {'FINISHED'}
+
+ self.current_mco = Vector((event.mouse_region_x, event.mouse_region_y))
+
+ region_types = [
+ 'HEADER',
+ 'UI',
+ 'TOOLS',
+ 'TOOL_PROPS',
+ ]
+ if not common.mouse_on_area(event, 'VIEW_3D') or \
+ common.mouse_on_regions(event, 'VIEW_3D', region_types):
+ return {'PASS_THROUGH'}
+
+ if event.type == 'LEFTMOUSE':
+ if event.value == 'PRESS':
+ if not self.__stroking:
+ self.__stroke_init(context, event)
+ self.__stroking = True
+ elif event.value == 'RELEASE':
+ if self.__stroking:
+ self.__stroke_exit(context, event)
+ self.__stroking = False
+ return {'RUNNING_MODAL'}
+ elif event.type == 'MOUSEMOVE':
+ if self.__stroking:
+ self.__stroke_apply(context, event)
+ return {'RUNNING_MODAL'}
+ elif event.type == 'TIMER':
+ if self.__stroking:
+ self.__stroke_apply(context, event)
+ return {'RUNNING_MODAL'}
+
+ return {'PASS_THROUGH'}
+
+ def invoke(self, context, _):
+ if context.area:
+ context.area.tag_redraw()
+
+ if MUV_OT_UVSculpt.is_running(context):
+ MUV_OT_UVSculpt.handle_remove(context)
+ else:
+ MUV_OT_UVSculpt.handle_add(self, context)
+
+ return {'RUNNING_MODAL'}
diff --git a/magic_uv/op/uvw.py b/magic_uv/op/uvw.py
new file mode 100644
index 00000000..035dfca3
--- /dev/null
+++ b/magic_uv/op/uvw.py
@@ -0,0 +1,304 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Alexander Milovsky, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+from math import sin, cos, pi
+
+import bpy
+import bmesh
+from bpy.props import (
+ FloatProperty,
+ FloatVectorProperty,
+ BoolProperty
+)
+from mathutils import Vector
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..utils import compatibility as compat
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+def _get_uv_layer(ops_obj, bm, assign_uvmap):
+ # get UV layer
+ if not bm.loops.layers.uv:
+ if assign_uvmap:
+ bm.loops.layers.uv.new()
+ else:
+ ops_obj.report({'WARNING'},
+ "Object must have more than one UV map")
+ return None
+ uv_layer = bm.loops.layers.uv.verify()
+
+ return uv_layer
+
+
+def _apply_box_map(bm, uv_layer, size, offset, rotation, tex_aspect):
+ scale = 1.0 / size
+
+ sx = 1.0 * scale
+ sy = 1.0 * scale
+ sz = 1.0 * scale
+ ofx = offset[0]
+ ofy = offset[1]
+ ofz = offset[2]
+ rx = rotation[0] * pi / 180.0
+ ry = rotation[1] * pi / 180.0
+ rz = rotation[2] * pi / 180.0
+ aspect = tex_aspect
+
+ sel_faces = [f for f in bm.faces if f.select]
+
+ # update UV coordinate
+ for f in sel_faces:
+ n = f.normal
+ for l in f.loops:
+ co = l.vert.co
+ x = co.x * sx
+ y = co.y * sy
+ z = co.z * sz
+
+ # X-plane
+ if abs(n[0]) >= abs(n[1]) and abs(n[0]) >= abs(n[2]):
+ if n[0] >= 0.0:
+ u = (y - ofy) * cos(rx) + (z - ofz) * sin(rx)
+ v = -(y * aspect - ofy) * sin(rx) + \
+ (z * aspect - ofz) * cos(rx)
+ else:
+ u = -(y - ofy) * cos(rx) + (z - ofz) * sin(rx)
+ v = (y * aspect - ofy) * sin(rx) + \
+ (z * aspect - ofz) * cos(rx)
+ # Y-plane
+ elif abs(n[1]) >= abs(n[0]) and abs(n[1]) >= abs(n[2]):
+ if n[1] >= 0.0:
+ u = -(x - ofx) * cos(ry) + (z - ofz) * sin(ry)
+ v = (x * aspect - ofx) * sin(ry) + \
+ (z * aspect - ofz) * cos(ry)
+ else:
+ u = (x - ofx) * cos(ry) + (z - ofz) * sin(ry)
+ v = -(x * aspect - ofx) * sin(ry) + \
+ (z * aspect - ofz) * cos(ry)
+ # Z-plane
+ else:
+ if n[2] >= 0.0:
+ u = (x - ofx) * cos(rz) + (y - ofy) * sin(rz)
+ v = -(x * aspect - ofx) * sin(rz) + \
+ (y * aspect - ofy) * cos(rz)
+ else:
+ u = -(x - ofx) * cos(rz) - (y + ofy) * sin(rz)
+ v = -(x * aspect + ofx) * sin(rz) + \
+ (y * aspect - ofy) * cos(rz)
+
+ l[uv_layer].uv = Vector((u, v))
+
+
+def _apply_planer_map(bm, uv_layer, size, offset, rotation, tex_aspect):
+ scale = 1.0 / size
+
+ sx = 1.0 * scale
+ sy = 1.0 * scale
+ ofx = offset[0]
+ ofy = offset[1]
+ rz = rotation * pi / 180.0
+ aspect = tex_aspect
+
+ sel_faces = [f for f in bm.faces if f.select]
+
+ # calculate average of normal
+ n_ave = Vector((0.0, 0.0, 0.0))
+ for f in sel_faces:
+ n_ave = n_ave + f.normal
+ q = n_ave.rotation_difference(Vector((0.0, 0.0, 1.0)))
+
+ # update UV coordinate
+ for f in sel_faces:
+ for l in f.loops:
+ co = compat.matmul(q, l.vert.co)
+ x = co.x * sx
+ y = co.y * sy
+
+ u = x * cos(rz) - y * sin(rz) + ofx
+ v = -x * aspect * sin(rz) - y * aspect * cos(rz) + ofy
+
+ l[uv_layer].uv = Vector((u, v))
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "uvw"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_uvw_enabled = BoolProperty(
+ name="UVW Enabled",
+ description="UVW is enabled",
+ default=False
+ )
+ scene.muv_uvw_assign_uvmap = BoolProperty(
+ name="Assign UVMap",
+ description="Assign UVMap when no UVmaps are available",
+ default=True
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_uvw_enabled
+ del scene.muv_uvw_assign_uvmap
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_UVW_BoxMap(bpy.types.Operator):
+ bl_idname = "uv.muv_ot_uvw_box_map"
+ bl_label = "Box Map"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ size = FloatProperty(
+ name="Size",
+ default=1.0,
+ precision=4
+ )
+ rotation = FloatVectorProperty(
+ name="XYZ Rotation",
+ size=3,
+ default=(0.0, 0.0, 0.0)
+ )
+ offset = FloatVectorProperty(
+ name="XYZ Offset",
+ size=3,
+ default=(0.0, 0.0, 0.0)
+ )
+ tex_aspect = FloatProperty(
+ name="Texture Aspect",
+ default=1.0,
+ precision=4
+ )
+ assign_uvmap = BoolProperty(
+ name="Assign UVMap",
+ description="Assign UVMap when no UVmaps are available",
+ default=True
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ # get UV layer
+ uv_layer = _get_uv_layer(self, bm, self.assign_uvmap)
+ if not uv_layer:
+ return {'CANCELLED'}
+
+ _apply_box_map(bm, uv_layer, self.size, self.offset, self.rotation,
+ self.tex_aspect)
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_UVW_BestPlanerMap(bpy.types.Operator):
+ bl_idname = "uv.muv_ot_uvw_best_planer_map"
+ bl_label = "Best Planer Map"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ size = FloatProperty(
+ name="Size",
+ default=1.0,
+ precision=4
+ )
+ rotation = FloatProperty(
+ name="XY Rotation",
+ default=0.0
+ )
+ offset = FloatVectorProperty(
+ name="XY Offset",
+ size=2,
+ default=(0.0, 0.0)
+ )
+ tex_aspect = FloatProperty(
+ name="Texture Aspect",
+ default=1.0,
+ precision=4
+ )
+ assign_uvmap = BoolProperty(
+ name="Assign UVMap",
+ description="Assign UVMap when no UVmaps are available",
+ default=True
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.faces.ensure_lookup_table()
+
+ # get UV layer
+ uv_layer = _get_uv_layer(self, bm, self.assign_uvmap)
+ if not uv_layer:
+ return {'CANCELLED'}
+
+ _apply_planer_map(bm, uv_layer, self.size, self.offset, self.rotation,
+ self.tex_aspect)
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
diff --git a/magic_uv/op/world_scale_uv.py b/magic_uv/op/world_scale_uv.py
new file mode 100644
index 00000000..1d78b8c7
--- /dev/null
+++ b/magic_uv/op/world_scale_uv.py
@@ -0,0 +1,649 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "McBuff, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+from math import sqrt
+
+import bpy
+from bpy.props import (
+ EnumProperty,
+ FloatProperty,
+ IntVectorProperty,
+ BoolProperty,
+)
+import bmesh
+from mathutils import Vector
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..utils import compatibility as compat
+
+
+def _is_valid_context(context):
+ obj = context.object
+
+ # only edit mode is allowed to execute
+ if obj is None:
+ return False
+ if obj.type != 'MESH':
+ return False
+ if context.object.mode != 'EDIT':
+ return False
+
+ # only 'VIEW_3D' space is allowed to execute
+ for space in context.area.spaces:
+ if space.type == 'VIEW_3D':
+ break
+ else:
+ return False
+
+ return True
+
+
+def _measure_wsuv_info(obj, tex_size=None):
+ mesh_area = common.measure_mesh_area(obj)
+ uv_area = common.measure_uv_area(obj, tex_size)
+
+ if not uv_area:
+ return None, mesh_area, None
+
+ if mesh_area == 0.0:
+ density = 0.0
+ else:
+ density = sqrt(uv_area) / sqrt(mesh_area)
+
+ return uv_area, mesh_area, density
+
+
+def _apply(obj, origin, factor):
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ bm.faces.ensure_lookup_table()
+
+ sel_faces = [f for f in bm.faces if f.select]
+
+ uv_layer = bm.loops.layers.uv.verify()
+
+ # calculate origin
+ if origin == 'CENTER':
+ origin = Vector((0.0, 0.0))
+ num = 0
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin = origin + uv
+ num = num + 1
+ origin = origin / num
+ elif origin == 'LEFT_TOP':
+ origin = Vector((100000.0, -100000.0))
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = min(origin.x, uv.x)
+ origin.y = max(origin.y, uv.y)
+ elif origin == 'LEFT_CENTER':
+ origin = Vector((100000.0, 0.0))
+ num = 0
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = min(origin.x, uv.x)
+ origin.y = origin.y + uv.y
+ num = num + 1
+ origin.y = origin.y / num
+ elif origin == 'LEFT_BOTTOM':
+ origin = Vector((100000.0, 100000.0))
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = min(origin.x, uv.x)
+ origin.y = min(origin.y, uv.y)
+ elif origin == 'CENTER_TOP':
+ origin = Vector((0.0, -100000.0))
+ num = 0
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = origin.x + uv.x
+ origin.y = max(origin.y, uv.y)
+ num = num + 1
+ origin.x = origin.x / num
+ elif origin == 'CENTER_BOTTOM':
+ origin = Vector((0.0, 100000.0))
+ num = 0
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = origin.x + uv.x
+ origin.y = min(origin.y, uv.y)
+ num = num + 1
+ origin.x = origin.x / num
+ elif origin == 'RIGHT_TOP':
+ origin = Vector((-100000.0, -100000.0))
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = max(origin.x, uv.x)
+ origin.y = max(origin.y, uv.y)
+ elif origin == 'RIGHT_CENTER':
+ origin = Vector((-100000.0, 0.0))
+ num = 0
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = max(origin.x, uv.x)
+ origin.y = origin.y + uv.y
+ num = num + 1
+ origin.y = origin.y / num
+ elif origin == 'RIGHT_BOTTOM':
+ origin = Vector((-100000.0, 100000.0))
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ origin.x = max(origin.x, uv.x)
+ origin.y = min(origin.y, uv.y)
+
+ # update UV coordinate
+ for f in sel_faces:
+ for l in f.loops:
+ uv = l[uv_layer].uv
+ diff = uv - origin
+ l[uv_layer].uv = origin + diff * factor
+
+ bmesh.update_edit_mesh(obj.data)
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "world_scale_uv"
+
+ @classmethod
+ def init_props(cls, scene):
+ scene.muv_world_scale_uv_enabled = BoolProperty(
+ name="World Scale UV Enabled",
+ description="World Scale UV is enabled",
+ default=False
+ )
+ scene.muv_world_scale_uv_src_mesh_area = FloatProperty(
+ name="Mesh Area",
+ description="Source Mesh Area",
+ default=0.0,
+ min=0.0
+ )
+ scene.muv_world_scale_uv_src_uv_area = FloatProperty(
+ name="UV Area",
+ description="Source UV Area",
+ default=0.0,
+ min=0.0
+ )
+ scene.muv_world_scale_uv_src_density = FloatProperty(
+ name="Density",
+ description="Source Texel Density",
+ default=0.0,
+ min=0.0
+ )
+ scene.muv_world_scale_uv_tgt_density = FloatProperty(
+ name="Density",
+ description="Target Texel Density",
+ default=0.0,
+ min=0.0
+ )
+ scene.muv_world_scale_uv_tgt_scaling_factor = FloatProperty(
+ name="Scaling Factor",
+ default=1.0,
+ max=1000.0,
+ min=0.00001
+ )
+ scene.muv_world_scale_uv_tgt_texture_size = IntVectorProperty(
+ name="Texture Size",
+ size=2,
+ min=1,
+ soft_max=10240,
+ default=(1024, 1024),
+ )
+ scene.muv_world_scale_uv_mode = EnumProperty(
+ name="Mode",
+ description="Density calculation mode",
+ items=[
+ ('PROPORTIONAL_TO_MESH', "Proportional to Mesh",
+ "Apply density proportionaled by mesh size"),
+ ('SCALING_DENSITY', "Scaling Density",
+ "Apply scaled density from source"),
+ ('SAME_DENSITY', "Same Density",
+ "Apply same density of source"),
+ ('MANUAL', "Manual", "Specify density and size by manual"),
+ ],
+ default='MANUAL'
+ )
+ scene.muv_world_scale_uv_origin = EnumProperty(
+ name="Origin",
+ description="Aspect Origin",
+ items=[
+ ('CENTER', "Center", "Center"),
+ ('LEFT_TOP', "Left Top", "Left Bottom"),
+ ('LEFT_CENTER', "Left Center", "Left Center"),
+ ('LEFT_BOTTOM', "Left Bottom", "Left Bottom"),
+ ('CENTER_TOP', "Center Top", "Center Top"),
+ ('CENTER_BOTTOM', "Center Bottom", "Center Bottom"),
+ ('RIGHT_TOP', "Right Top", "Right Top"),
+ ('RIGHT_CENTER', "Right Center", "Right Center"),
+ ('RIGHT_BOTTOM', "Right Bottom", "Right Bottom")
+
+ ],
+ default='CENTER'
+ )
+
+ @classmethod
+ def del_props(cls, scene):
+ del scene.muv_world_scale_uv_enabled
+ del scene.muv_world_scale_uv_src_mesh_area
+ del scene.muv_world_scale_uv_src_uv_area
+ del scene.muv_world_scale_uv_src_density
+ del scene.muv_world_scale_uv_tgt_density
+ del scene.muv_world_scale_uv_tgt_scaling_factor
+ del scene.muv_world_scale_uv_mode
+ del scene.muv_world_scale_uv_origin
+
+
+@BlClassRegistry()
+class MUV_OT_WorldScaleUV_Measure(bpy.types.Operator):
+ """
+ Operation class: Measure face size
+ """
+
+ bl_idname = "uv.muv_ot_world_scale_uv_measure"
+ bl_label = "Measure World Scale UV"
+ bl_description = "Measure face size for scale calculation"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def execute(self, context):
+ sc = context.scene
+ obj = context.active_object
+
+ uv_area, mesh_area, density = _measure_wsuv_info(obj)
+ if not uv_area:
+ self.report({'WARNING'},
+ "Object must have more than one UV map and texture")
+ return {'CANCELLED'}
+
+ sc.muv_world_scale_uv_src_uv_area = uv_area
+ sc.muv_world_scale_uv_src_mesh_area = mesh_area
+ sc.muv_world_scale_uv_src_density = density
+
+ self.report({'INFO'},
+ "UV Area: {0}, Mesh Area: {1}, Texel Density: {2}"
+ .format(uv_area, mesh_area, density))
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_WorldScaleUV_ApplyManual(bpy.types.Operator):
+ """
+ Operation class: Apply scaled UV (Manual)
+ """
+
+ bl_idname = "uv.muv_ot_world_scale_uv_apply_manual"
+ bl_label = "Apply World Scale UV (Manual)"
+ bl_description = "Apply scaled UV based on user specification"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ tgt_density = FloatProperty(
+ name="Density",
+ description="Target Texel Density",
+ default=1.0,
+ min=0.0
+ )
+ tgt_texture_size = IntVectorProperty(
+ name="Texture Size",
+ size=2,
+ min=1,
+ soft_max=10240,
+ default=(1024, 1024),
+ )
+ origin = EnumProperty(
+ name="Origin",
+ description="Aspect Origin",
+ items=[
+ ('CENTER', "Center", "Center"),
+ ('LEFT_TOP', "Left Top", "Left Bottom"),
+ ('LEFT_CENTER', "Left Center", "Left Center"),
+ ('LEFT_BOTTOM', "Left Bottom", "Left Bottom"),
+ ('CENTER_TOP', "Center Top", "Center Top"),
+ ('CENTER_BOTTOM', "Center Bottom", "Center Bottom"),
+ ('RIGHT_TOP', "Right Top", "Right Top"),
+ ('RIGHT_CENTER', "Right Center", "Right Center"),
+ ('RIGHT_BOTTOM', "Right Bottom", "Right Bottom")
+
+ ],
+ default='CENTER'
+ )
+ show_dialog = BoolProperty(
+ name="Show Diaglog Menu",
+ description="Show dialog menu if true",
+ default=True,
+ options={'HIDDEN', 'SKIP_SAVE'}
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def __apply_manual(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ bm.faces.ensure_lookup_table()
+
+ tex_size = self.tgt_texture_size
+ uv_area, _, density = _measure_wsuv_info(obj, tex_size)
+ if not uv_area:
+ self.report({'WARNING'}, "Object must have more than one UV map")
+ return {'CANCELLED'}
+
+ tgt_density = self.tgt_density
+ factor = tgt_density / density
+
+ _apply(context.active_object, self.origin, factor)
+ self.report({'INFO'}, "Scaling factor: {0}".format(factor))
+
+ return {'FINISHED'}
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.prop(self, "tgt_density")
+ layout.prop(self, "tgt_texture_size")
+ layout.prop(self, "origin")
+
+ layout.separator()
+
+ def invoke(self, context, _):
+ if self.show_dialog:
+ wm = context.window_manager
+ return wm.invoke_props_dialog(self)
+
+ return self.execute(context)
+
+ def execute(self, context):
+ return self.__apply_manual(context)
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_WorldScaleUV_ApplyScalingDensity(bpy.types.Operator):
+ """
+ Operation class: Apply scaled UV (Scaling Density)
+ """
+
+ bl_idname = "uv.muv_ot_world_scale_uv_apply_scaling_density"
+ bl_label = "Apply World Scale UV (Scaling Density)"
+ bl_description = "Apply scaled UV with scaling density"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ tgt_scaling_factor = FloatProperty(
+ name="Scaling Factor",
+ default=1.0,
+ max=1000.0,
+ min=0.00001
+ )
+ origin = EnumProperty(
+ name="Origin",
+ description="Aspect Origin",
+ items=[
+ ('CENTER', "Center", "Center"),
+ ('LEFT_TOP', "Left Top", "Left Bottom"),
+ ('LEFT_CENTER', "Left Center", "Left Center"),
+ ('LEFT_BOTTOM', "Left Bottom", "Left Bottom"),
+ ('CENTER_TOP', "Center Top", "Center Top"),
+ ('CENTER_BOTTOM', "Center Bottom", "Center Bottom"),
+ ('RIGHT_TOP', "Right Top", "Right Top"),
+ ('RIGHT_CENTER', "Right Center", "Right Center"),
+ ('RIGHT_BOTTOM', "Right Bottom", "Right Bottom")
+
+ ],
+ default='CENTER'
+ )
+ src_density = FloatProperty(
+ name="Density",
+ description="Source Texel Density",
+ default=0.0,
+ min=0.0,
+ options={'HIDDEN'}
+ )
+ same_density = BoolProperty(
+ name="Same Density",
+ description="Apply same density",
+ default=False,
+ options={'HIDDEN'}
+ )
+ show_dialog = BoolProperty(
+ name="Show Diaglog Menu",
+ description="Show dialog menu if true",
+ default=True,
+ options={'HIDDEN', 'SKIP_SAVE'}
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def __apply_scaling_density(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ bm.faces.ensure_lookup_table()
+
+ uv_area, _, density = _measure_wsuv_info(obj)
+ if not uv_area:
+ self.report({'WARNING'},
+ "Object must have more than one UV map and texture")
+ return {'CANCELLED'}
+
+ tgt_density = self.src_density * self.tgt_scaling_factor
+ factor = tgt_density / density
+
+ _apply(context.active_object, self.origin, factor)
+ self.report({'INFO'}, "Scaling factor: {0}".format(factor))
+
+ return {'FINISHED'}
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.label(text="Source:")
+ col = layout.column()
+ col.prop(self, "src_density")
+ col.enabled = False
+
+ layout.separator()
+
+ if not self.same_density:
+ layout.prop(self, "tgt_scaling_factor")
+ layout.prop(self, "origin")
+
+ layout.separator()
+
+ def invoke(self, context, _):
+ sc = context.scene
+
+ if self.show_dialog:
+ wm = context.window_manager
+
+ if self.same_density:
+ self.tgt_scaling_factor = 1.0
+ else:
+ self.tgt_scaling_factor = \
+ sc.muv_world_scale_uv_tgt_scaling_factor
+ self.src_density = sc.muv_world_scale_uv_src_density
+
+ return wm.invoke_props_dialog(self)
+
+ return self.execute(context)
+
+ def execute(self, context):
+ if self.same_density:
+ self.tgt_scaling_factor = 1.0
+
+ return self.__apply_scaling_density(context)
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_WorldScaleUV_ApplyProportionalToMesh(bpy.types.Operator):
+ """
+ Operation class: Apply scaled UV (Proportional to mesh)
+ """
+
+ bl_idname = "uv.muv_ot_world_scale_uv_apply_proportional_to_mesh"
+ bl_label = "Apply World Scale UV (Proportional to mesh)"
+ bl_description = "Apply scaled UV proportionaled to mesh"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ origin = EnumProperty(
+ name="Origin",
+ description="Aspect Origin",
+ items=[
+ ('CENTER', "Center", "Center"),
+ ('LEFT_TOP', "Left Top", "Left Bottom"),
+ ('LEFT_CENTER', "Left Center", "Left Center"),
+ ('LEFT_BOTTOM', "Left Bottom", "Left Bottom"),
+ ('CENTER_TOP', "Center Top", "Center Top"),
+ ('CENTER_BOTTOM', "Center Bottom", "Center Bottom"),
+ ('RIGHT_TOP', "Right Top", "Right Top"),
+ ('RIGHT_CENTER', "Right Center", "Right Center"),
+ ('RIGHT_BOTTOM', "Right Bottom", "Right Bottom")
+
+ ],
+ default='CENTER'
+ )
+ src_density = FloatProperty(
+ name="Source Density",
+ description="Source Texel Density",
+ default=0.0,
+ min=0.0,
+ options={'HIDDEN'}
+ )
+ src_uv_area = FloatProperty(
+ name="Source UV Area",
+ description="Source UV Area",
+ default=0.0,
+ min=0.0,
+ options={'HIDDEN'}
+ )
+ src_mesh_area = FloatProperty(
+ name="Source Mesh Area",
+ description="Source Mesh Area",
+ default=0.0,
+ min=0.0,
+ options={'HIDDEN'}
+ )
+ show_dialog = BoolProperty(
+ name="Show Diaglog Menu",
+ description="Show dialog menu if true",
+ default=True,
+ options={'HIDDEN', 'SKIP_SAVE'}
+ )
+
+ @classmethod
+ def poll(cls, context):
+ # we can not get area/space/region from console
+ if common.is_console_mode():
+ return True
+ return _is_valid_context(context)
+
+ def __apply_proportional_to_mesh(self, context):
+ obj = context.active_object
+ bm = bmesh.from_edit_mesh(obj.data)
+ if common.check_version(2, 73, 0) >= 0:
+ bm.verts.ensure_lookup_table()
+ bm.edges.ensure_lookup_table()
+ bm.faces.ensure_lookup_table()
+
+ uv_area, mesh_area, density = _measure_wsuv_info(obj)
+ if not uv_area:
+ self.report({'WARNING'},
+ "Object must have more than one UV map and texture")
+ return {'CANCELLED'}
+
+ tgt_density = self.src_density * sqrt(mesh_area) / sqrt(
+ self.src_mesh_area)
+
+ factor = tgt_density / density
+
+ _apply(context.active_object, self.origin, factor)
+ self.report({'INFO'}, "Scaling factor: {0}".format(factor))
+
+ return {'FINISHED'}
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.label(text="Source:")
+ col = layout.column(align=True)
+ col.prop(self, "src_density")
+ col.prop(self, "src_uv_area")
+ col.prop(self, "src_mesh_area")
+ col.enabled = False
+
+ layout.separator()
+ layout.prop(self, "origin")
+
+ layout.separator()
+
+ def invoke(self, context, _):
+ if self.show_dialog:
+ wm = context.window_manager
+ sc = context.scene
+
+ self.src_density = sc.muv_world_scale_uv_src_density
+ self.src_mesh_area = sc.muv_world_scale_uv_src_mesh_area
+
+ return wm.invoke_props_dialog(self)
+
+ return self.execute(context)
+
+ def execute(self, context):
+ return self.__apply_proportional_to_mesh(context)
diff --git a/magic_uv/preferences.py b/magic_uv/preferences.py
new file mode 100644
index 00000000..3a024488
--- /dev/null
+++ b/magic_uv/preferences.py
@@ -0,0 +1,488 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+from bpy.props import (
+ FloatProperty,
+ FloatVectorProperty,
+ BoolProperty,
+ EnumProperty,
+)
+from bpy.types import AddonPreferences
+
+from . import common
+from .op.flip_rotate_uv import MUV_OT_FlipRotate
+from .op.mirror_uv import MUV_OT_MirrorUV
+from .op.move_uv import MUV_OT_MoveUV
+from .op.unwrap_constraint import MUV_OT_UnwrapConstraint
+from .op.pack_uv import MUV_OT_PackUV
+from .op.smooth_uv import MUV_OT_SmoothUV
+from .ui.VIEW3D_MT_uv_map import (
+ MUV_MT_CopyPasteUV,
+ MUV_MT_TransferUV,
+ MUV_MT_WorldScaleUV,
+ MUV_MT_PreserveUVAspect,
+ MUV_MT_TextureLock,
+ MUV_MT_TextureWrap,
+ MUV_MT_TextureProjection,
+ MUV_MT_UVW,
+)
+from .ui.VIEW3D_MT_object import MUV_MT_CopyPasteUV_Object
+from .ui.IMAGE_MT_uvs import (
+ MUV_MT_CopyPasteUV_UVEdit,
+ MUV_MT_SelectUV,
+ MUV_MT_AlignUV,
+ MUV_MT_AlignUVCursor,
+ MUV_MT_UVInspection,
+)
+from .utils.bl_class_registry import BlClassRegistry
+from .utils.addon_updator import AddonUpdatorManager
+from .utils import compatibility as compat
+from . import updater
+
+
+def view3d_uvmap_menu_fn(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ layout.separator()
+ layout.label(text="Copy/Paste UV", icon=compat.icon('IMAGE'))
+ # Copy/Paste UV
+ layout.menu(MUV_MT_CopyPasteUV.bl_idname, text="Copy/Paste UV")
+ # Transfer UV
+ layout.menu(MUV_MT_TransferUV.bl_idname, text="Transfer UV")
+
+ layout.separator()
+ layout.label(text="UV Manipulation", icon=compat.icon('IMAGE'))
+ # Flip/Rotate UV
+ ops = layout.operator(MUV_OT_FlipRotate.bl_idname, text="Flip/Rotate UV")
+ ops.seams = sc.muv_flip_rotate_uv_seams
+ # Mirror UV
+ ops = layout.operator(MUV_OT_MirrorUV.bl_idname, text="Mirror UV")
+ ops.axis = sc.muv_mirror_uv_axis
+ # Move UV
+ layout.operator(MUV_OT_MoveUV.bl_idname, text="Move UV")
+ # World Scale UV
+ layout.menu(MUV_MT_WorldScaleUV.bl_idname, text="World Scale UV")
+ # Preserve UV
+ layout.menu(MUV_MT_PreserveUVAspect.bl_idname, text="Preserve UV")
+ # Texture Lock
+ layout.menu(MUV_MT_TextureLock.bl_idname, text="Texture Lock")
+ # Texture Wrap
+ layout.menu(MUV_MT_TextureWrap.bl_idname, text="Texture Wrap")
+ # UV Sculpt
+ layout.prop(sc, "muv_uv_sculpt_enable", text="UV Sculpt")
+
+ layout.separator()
+ layout.label(text="UV Mapping", icon=compat.icon('IMAGE'))
+ # Unwrap Constraint
+ ops = layout.operator(MUV_OT_UnwrapConstraint.bl_idname,
+ text="Unwrap Constraint")
+ ops.u_const = sc.muv_unwrap_constraint_u_const
+ ops.v_const = sc.muv_unwrap_constraint_v_const
+ # Texture Projection
+ layout.menu(MUV_MT_TextureProjection.bl_idname, text="Texture Projection")
+ # UVW
+ layout.menu(MUV_MT_UVW.bl_idname, text="UVW")
+
+
+def view3d_object_menu_fn(self, _):
+ layout = self.layout
+
+ layout.separator()
+ layout.label(text="Copy/Paste UV", icon=compat.icon('IMAGE'))
+ # Copy/Paste UV (Among Object)
+ layout.menu(MUV_MT_CopyPasteUV_Object.bl_idname, text="Copy/Paste UV")
+
+
+def image_uvs_menu_fn(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ layout.separator()
+ # Copy/Paste UV (on UV/Image Editor)
+ layout.label(text="Copy/Paste UV", icon=compat.icon('IMAGE'))
+ layout.menu(MUV_MT_CopyPasteUV_UVEdit.bl_idname, text="Copy/Paste UV")
+
+ layout.separator()
+ # Pack UV
+ layout.label(text="UV Manipulation", icon=compat.icon('IMAGE'))
+ 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
+ # Select UV
+ layout.menu(MUV_MT_SelectUV.bl_idname, text="Select UV")
+ # Smooth UV
+ ops = layout.operator(MUV_OT_SmoothUV.bl_idname, text="Smooth")
+ ops.transmission = sc.muv_smooth_uv_transmission
+ ops.select = sc.muv_smooth_uv_select
+ ops.mesh_infl = sc.muv_smooth_uv_mesh_infl
+ # Align UV
+ layout.menu(MUV_MT_AlignUV.bl_idname, text="Align UV")
+
+ layout.separator()
+ # Align UV Cursor
+ layout.label(text="Editor Enhancement", icon=compat.icon('IMAGE'))
+ layout.menu(MUV_MT_AlignUVCursor.bl_idname, text="Align UV Cursor")
+ # UV Bounding Box
+ layout.prop(sc, "muv_uv_bounding_box_show", text="UV Bounding Box")
+ # UV Inspection
+ layout.menu(MUV_MT_UVInspection.bl_idname, text="UV Inspection")
+
+
+def add_builtin_menu():
+ bpy.types.VIEW3D_MT_uv_map.append(view3d_uvmap_menu_fn)
+ bpy.types.VIEW3D_MT_object.append(view3d_object_menu_fn)
+ bpy.types.IMAGE_MT_uvs.append(image_uvs_menu_fn)
+
+
+def remove_builtin_menu():
+ bpy.types.IMAGE_MT_uvs.remove(image_uvs_menu_fn)
+ bpy.types.VIEW3D_MT_object.append(view3d_object_menu_fn)
+ bpy.types.VIEW3D_MT_uv_map.remove(view3d_uvmap_menu_fn)
+
+
+def get_update_candidate_branches(_, __):
+ manager = AddonUpdatorManager.get_instance()
+ if not manager.candidate_checked():
+ return []
+
+ return [(name, name, "") for name in manager.get_candidate_branch_names()]
+
+
+def set_debug_mode(self, value):
+ self['enable_debug_mode'] = value
+
+
+def get_debug_mode(self):
+ enabled = self.get('enable_debug_mode', False)
+ if enabled:
+ common.enable_debugg_mode()
+ else:
+ common.disable_debug_mode()
+ return enabled
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class Preferences(AddonPreferences):
+ """Preferences class: Preferences for this add-on"""
+
+ bl_idname = "magic_uv"
+
+ def update_enable_builtin_menu(self, _):
+ if self['enable_builtin_menu']:
+ add_builtin_menu()
+ else:
+ remove_builtin_menu()
+
+ # enable to add features to built-in menu
+ enable_builtin_menu = BoolProperty(
+ name="Built-in Menu",
+ description="Enable built-in menu",
+ default=True,
+ update=update_enable_builtin_menu,
+ )
+
+ # enable debug mode
+ enable_debug_mode = BoolProperty(
+ name="Debug Mode",
+ description="Enable debugging mode",
+ default=False,
+ set=set_debug_mode,
+ get=get_debug_mode,
+ )
+
+ # for UV Sculpt
+ uv_sculpt_brush_color = FloatVectorProperty(
+ name="Color",
+ description="Color",
+ default=(1.0, 0.4, 0.4, 1.0),
+ min=0.0,
+ max=1.0,
+ size=4,
+ subtype='COLOR'
+ )
+
+ # for Overlapped UV
+ uv_inspection_overlapped_color = FloatVectorProperty(
+ name="Color",
+ description="Color",
+ default=(0.0, 0.0, 1.0, 0.3),
+ min=0.0,
+ max=1.0,
+ size=4,
+ subtype='COLOR'
+ )
+
+ # for Flipped UV
+ uv_inspection_flipped_color = FloatVectorProperty(
+ name="Color",
+ description="Color",
+ default=(1.0, 0.0, 0.0, 0.3),
+ min=0.0,
+ max=1.0,
+ size=4,
+ subtype='COLOR'
+ )
+
+ # for Texture Projection
+ texture_projection_canvas_padding = FloatVectorProperty(
+ name="Canvas Padding",
+ description="Canvas Padding",
+ size=2,
+ max=50.0,
+ min=0.0,
+ default=(20.0, 20.0))
+
+ # for UV Bounding Box
+ uv_bounding_box_cp_size = FloatProperty(
+ name="Size",
+ description="Control Point Size",
+ default=6.0,
+ min=3.0,
+ max=100.0)
+ uv_bounding_box_cp_react_size = FloatProperty(
+ name="React Size",
+ description="Size event fired",
+ default=10.0,
+ min=3.0,
+ max=100.0)
+
+ # for UI
+ category = EnumProperty(
+ name="Category",
+ description="Preferences Category",
+ items=[
+ ('INFO', "Information", "Information about this add-on"),
+ ('CONFIG', "Configuration", "Configuration about this add-on"),
+ ('UPDATE', "Update", "Update this add-on"),
+ ],
+ default='INFO'
+ )
+ info_desc_expanded = BoolProperty(
+ name="Description",
+ description="Description",
+ default=False
+ )
+ info_loc_expanded = BoolProperty(
+ name="Location",
+ description="Location",
+ default=False
+ )
+ conf_uv_sculpt_expanded = BoolProperty(
+ name="UV Sculpt",
+ description="UV Sculpt",
+ default=False
+ )
+ conf_uv_inspection_expanded = BoolProperty(
+ name="UV Inspection",
+ description="UV Inspection",
+ default=False
+ )
+ conf_texture_projection_expanded = BoolProperty(
+ name="Texture Projection",
+ description="Texture Projection",
+ default=False
+ )
+ conf_uv_bounding_box_expanded = BoolProperty(
+ name="UV Bounding Box",
+ description="UV Bounding Box",
+ default=False
+ )
+
+ # for add-on updater
+ updater_branch_to_update = EnumProperty(
+ name="branch",
+ description="Target branch to update add-on",
+ items=get_update_candidate_branches
+ )
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.row().prop(self, "category", expand=True)
+
+ if self.category == 'INFO':
+ layout.separator()
+
+ layout.prop(
+ self, "info_desc_expanded", text="Description",
+ icon='DISCLOSURE_TRI_DOWN' if self.info_desc_expanded
+ else 'DISCLOSURE_TRI_RIGHT')
+ if self.info_desc_expanded:
+ col = layout.column(align=True)
+ col.label(text="Magic UV is composed of many UV editing" +
+ " features.")
+ col.label(text="See tutorial page if you are new to this" +
+ " add-on.")
+ col.label(text="https://github.com/nutti/Magic-UV" +
+ "/wiki/Tutorial")
+
+ layout.prop(
+ self, "info_loc_expanded", text="Location",
+ icon='DISCLOSURE_TRI_DOWN' if self.info_loc_expanded
+ else 'DISCLOSURE_TRI_RIGHT')
+ if self.info_loc_expanded:
+ row = layout.row(align=True)
+ sp = compat.layout_split(row, 0.5)
+ sp.label(text="3D View > Sidebar > " +
+ "Copy/Paste UV (Object mode)")
+ sp = compat.layout_split(sp, 1.0)
+ col = sp.column(align=True)
+ col.label(text="Copy/Paste UV (Among objects)")
+
+ row = layout.row(align=True)
+ sp = compat.layout_split(row, 0.5)
+ sp.label(text="3D View > Sidebar > " +
+ "Copy/Paste UV (Edit mode)")
+ sp = compat.layout_split(sp, 1.0)
+ col = sp.column(align=True)
+ col.label(text="Copy/Paste UV (Among faces in 3D View)")
+ col.label(text="Transfer UV")
+
+ row = layout.row(align=True)
+ sp = compat.layout_split(row, 0.5)
+ sp.label(text="3D View > Sidebar > " +
+ "UV Manipulation (Edit mode)")
+ sp = compat.layout_split(sp, 1.0)
+ col = sp.column(align=True)
+ col.label(text="Flip/Rotate UV")
+ col.label(text="Mirror UV")
+ col.label(text="Move UV")
+ col.label(text="World Scale UV")
+ col.label(text="Preserve UV Aspect")
+ col.label(text="Texture Lock")
+ col.label(text="Texture Wrap")
+ col.label(text="UV Sculpt")
+
+ row = layout.row(align=True)
+ sp = compat.layout_split(row, 0.5)
+ sp.label(text="3D View > Sidebar > " +
+ "UV Manipulation (Edit mode)")
+ sp = compat.layout_split(sp, 1.0)
+ col = sp.column(align=True)
+ col.label(text="Unwrap Constraint")
+ col.label(text="Texture Projection")
+ col.label(text="UVW")
+
+ row = layout.row(align=True)
+ sp = compat.layout_split(row, 0.5)
+ sp.label(text="UV/Image Editor > Sidebar > Copy/Paste UV")
+ sp = compat.layout_split(sp, 1.0)
+ col = sp.column(align=True)
+ col.label(text="Copy/Paste UV " +
+ "(Among faces in UV/Image Editor)")
+
+ row = layout.row(align=True)
+ sp = compat.layout_split(row, 0.5)
+ sp.label(text="UV/Image Editor > Sidebar > UV Manipulation")
+ sp = compat.layout_split(sp, 1.0)
+ col = sp.column(align=True)
+ col.label(text="Align UV")
+ col.label(text="Smooth UV")
+ col.label(text="Select UV")
+ col.label(text="Pack UV (Extension)")
+
+ row = layout.row(align=True)
+ sp = compat.layout_split(row, 0.5)
+ sp.label(text="UV/Image Editor > Sidebar > " +
+ "Editor Enhancement")
+ sp = compat.layout_split(sp, 1.0)
+ col = sp.column(align=True)
+ col.label(text="Align UV Cursor")
+ col.label(text="UV Cursor Location")
+ col.label(text="UV Bounding Box")
+ col.label(text="UV Inspection")
+
+ elif self.category == 'CONFIG':
+ layout.separator()
+
+ layout.prop(self, "enable_builtin_menu", text="Built-in Menu")
+ layout.prop(self, "enable_debug_mode", text="Debug Mode")
+
+ layout.separator()
+
+ layout.prop(
+ self, "conf_uv_sculpt_expanded", text="UV Sculpt",
+ icon='DISCLOSURE_TRI_DOWN' if self.conf_uv_sculpt_expanded
+ else 'DISCLOSURE_TRI_RIGHT')
+ if self.conf_uv_sculpt_expanded:
+ sp = compat.layout_split(layout, 0.05)
+ col = sp.column() # spacer
+ sp = compat.layout_split(sp, 0.3)
+ col = sp.column()
+ col.label(text="Brush Color:")
+ col.prop(self, "uv_sculpt_brush_color", text="")
+ layout.separator()
+
+ layout.prop(
+ self, "conf_uv_inspection_expanded", text="UV Inspection",
+ icon='DISCLOSURE_TRI_DOWN' if self.conf_uv_inspection_expanded
+ else 'DISCLOSURE_TRI_RIGHT')
+ if self.conf_uv_inspection_expanded:
+ sp = compat.layout_split(layout, 0.05)
+ col = sp.column() # spacer
+ sp = compat.layout_split(sp, 0.3)
+ col = sp.column()
+ col.label(text="Overlapped UV Color:")
+ col.prop(self, "uv_inspection_overlapped_color", text="")
+ sp = compat.layout_split(sp, 0.45)
+ col = sp.column()
+ col.label(text="Flipped UV Color:")
+ col.prop(self, "uv_inspection_flipped_color", text="")
+ layout.separator()
+
+ layout.prop(
+ self, "conf_texture_projection_expanded",
+ text="Texture Projection",
+ icon='DISCLOSURE_TRI_DOWN'
+ if self.conf_texture_projection_expanded
+ else 'DISCLOSURE_TRI_RIGHT')
+ if self.conf_texture_projection_expanded:
+ sp = compat.layout_split(layout, 0.05)
+ col = sp.column() # spacer
+ sp = compat.layout_split(sp, 0.3)
+ col = sp.column()
+ col.prop(self, "texture_projection_canvas_padding")
+ layout.separator()
+
+ layout.prop(
+ self, "conf_uv_bounding_box_expanded", text="UV Bounding Box",
+ icon='DISCLOSURE_TRI_DOWN'
+ if self.conf_uv_bounding_box_expanded
+ else 'DISCLOSURE_TRI_RIGHT')
+ if self.conf_uv_bounding_box_expanded:
+ sp = compat.layout_split(layout, 0.05)
+ col = sp.column() # spacer
+ sp = compat.layout_split(sp, 0.3)
+ col = sp.column()
+ col.label(text="Control Point:")
+ col.prop(self, "uv_bounding_box_cp_size")
+ col.prop(self, "uv_bounding_box_cp_react_size")
+ layout.separator()
+
+ elif self.category == 'UPDATE':
+ updater.draw_updater_ui(self)
diff --git a/magic_uv/properites.py b/magic_uv/properites.py
new file mode 100644
index 00000000..6ee00edd
--- /dev/null
+++ b/magic_uv/properites.py
@@ -0,0 +1,43 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+
+from .utils.property_class_registry import PropertyClassRegistry
+
+
+# Properties used in this add-on.
+# pylint: disable=W0612
+class MUV_Properties():
+ pass
+
+
+def init_props(scene):
+ scene.muv_props = MUV_Properties()
+ PropertyClassRegistry.init_props(scene)
+
+
+def clear_props(scene):
+ PropertyClassRegistry.del_props(scene)
+ del scene.muv_props
diff --git a/magic_uv/ui/IMAGE_MT_uvs.py b/magic_uv/ui/IMAGE_MT_uvs.py
new file mode 100644
index 00000000..ab7e33f8
--- /dev/null
+++ b/magic_uv/ui/IMAGE_MT_uvs.py
@@ -0,0 +1,188 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+
+from ..op.copy_paste_uv_uvedit import (
+ MUV_OT_CopyPasteUVUVEdit_CopyUV,
+ MUV_OT_CopyPasteUVUVEdit_PasteUV,
+)
+from ..op.align_uv_cursor import MUV_OT_AlignUVCursor
+from ..op.align_uv import (
+ MUV_OT_AlignUV_Circle,
+ MUV_OT_AlignUV_Straighten,
+ MUV_OT_AlignUV_Axis,
+)
+from ..op.select_uv import (
+ MUV_OT_SelectUV_SelectOverlapped,
+ MUV_OT_SelectUV_SelectFlipped,
+)
+from ..op.uv_inspection import MUV_OT_UVInspection_Update
+from ..utils.bl_class_registry import BlClassRegistry
+
+
+@BlClassRegistry()
+class MUV_MT_CopyPasteUV_UVEdit(bpy.types.Menu):
+ """
+ Menu class: Master menu of Copy/Paste UV coordinate on UV/ImageEditor
+ """
+
+ bl_idname = "uv.muv_mt_copy_paste_uv_uvedit"
+ bl_label = "Copy/Paste UV"
+ bl_description = "Copy and Paste UV coordinate among object"
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.operator(MUV_OT_CopyPasteUVUVEdit_CopyUV.bl_idname, text="Copy")
+ layout.operator(MUV_OT_CopyPasteUVUVEdit_PasteUV.bl_idname,
+ text="Paste")
+
+
+@BlClassRegistry()
+class MUV_MT_AlignUV(bpy.types.Menu):
+ """
+ Menu class: Master menu of Align UV
+ """
+
+ bl_idname = "uv.muv_mt_align_uv"
+ bl_label = "Align UV"
+ bl_description = "Align UV"
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ ops = layout.operator(MUV_OT_AlignUV_Circle.bl_idname, text="Circle")
+ ops.transmission = sc.muv_align_uv_transmission
+ ops.select = sc.muv_align_uv_select
+
+ ops = layout.operator(MUV_OT_AlignUV_Straighten.bl_idname,
+ text="Straighten")
+ ops.transmission = sc.muv_align_uv_transmission
+ ops.select = sc.muv_align_uv_select
+ ops.vertical = sc.muv_align_uv_vertical
+ ops.horizontal = sc.muv_align_uv_horizontal
+
+ ops = layout.operator(MUV_OT_AlignUV_Axis.bl_idname, text="XY-axis")
+ ops.transmission = sc.muv_align_uv_transmission
+ ops.select = sc.muv_align_uv_select
+ ops.vertical = sc.muv_align_uv_vertical
+ ops.horizontal = sc.muv_align_uv_horizontal
+ ops.location = sc.muv_align_uv_location
+
+
+@BlClassRegistry()
+class MUV_MT_SelectUV(bpy.types.Menu):
+ """
+ Menu class: Master menu of Select UV
+ """
+
+ bl_idname = "uv.muv_mt_select_uv"
+ bl_label = "Select UV"
+ bl_description = "Select UV"
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.operator(MUV_OT_SelectUV_SelectOverlapped.bl_idname,
+ text="Overlapped")
+ layout.operator(MUV_OT_SelectUV_SelectFlipped.bl_idname,
+ text="Flipped")
+
+
+@BlClassRegistry()
+class MUV_MT_AlignUVCursor(bpy.types.Menu):
+ """
+ Menu class: Master menu of Align UV Cursor
+ """
+
+ bl_idname = "uv.muv_mt_align_uv_cursor"
+ bl_label = "Align UV Cursor"
+ bl_description = "Align UV cursor"
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, text="Left Top")
+ ops.position = 'LEFT_TOP'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname,
+ text="Middle Top")
+ ops.position = 'MIDDLE_TOP'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, text="Right Top")
+ ops.position = 'RIGHT_TOP'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname,
+ text="Left Middle")
+ ops.position = 'LEFT_MIDDLE'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, text="Center")
+ ops.position = 'CENTER'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname,
+ text="Right Middle")
+ ops.position = 'RIGHT_MIDDLE'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname,
+ text="Left Bottom")
+ ops.position = 'LEFT_BOTTOM'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname,
+ text="Middle Bottom")
+ ops.position = 'MIDDLE_BOTTOM'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname,
+ text="Right Bottom")
+ ops.position = 'RIGHT_BOTTOM'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+
+@BlClassRegistry()
+class MUV_MT_UVInspection(bpy.types.Menu):
+ """
+ Menu class: Master menu of UV Inspection
+ """
+
+ bl_idname = "uv.muv_mt_uv_inspection"
+ bl_label = "UV Inspection"
+ bl_description = "UV Inspection"
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ layout.prop(sc, "muv_uv_inspection_show", text="UV Inspection")
+ layout.operator(MUV_OT_UVInspection_Update.bl_idname, text="Update")
diff --git a/magic_uv/ui/VIEW3D_MT_object.py b/magic_uv/ui/VIEW3D_MT_object.py
new file mode 100644
index 00000000..b691bdd5
--- /dev/null
+++ b/magic_uv/ui/VIEW3D_MT_object.py
@@ -0,0 +1,49 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+
+from ..op.copy_paste_uv_object import (
+ MUV_MT_CopyPasteUVObject_CopyUV,
+ MUV_MT_CopyPasteUVObject_PasteUV,
+)
+from ..utils.bl_class_registry import BlClassRegistry
+
+
+@BlClassRegistry()
+class MUV_MT_CopyPasteUV_Object(bpy.types.Menu):
+ """
+ Menu class: Master menu of Copy/Paste UV coordinate among object
+ """
+
+ bl_idname = "uv.muv_mt_copy_paste_uv_object"
+ bl_label = "Copy/Paste UV"
+ bl_description = "Copy and Paste UV coordinate among object"
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.menu(MUV_MT_CopyPasteUVObject_CopyUV.bl_idname, text="Copy")
+ layout.menu(MUV_MT_CopyPasteUVObject_PasteUV.bl_idname, text="Paste")
diff --git a/magic_uv/ui/VIEW3D_MT_uv_map.py b/magic_uv/ui/VIEW3D_MT_uv_map.py
new file mode 100644
index 00000000..12202602
--- /dev/null
+++ b/magic_uv/ui/VIEW3D_MT_uv_map.py
@@ -0,0 +1,254 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy.utils
+
+from ..op.copy_paste_uv import (
+ MUV_MT_CopyPasteUV_CopyUV,
+ MUV_MT_CopyPasteUV_PasteUV,
+ MUV_MT_CopyPasteUV_SelSeqCopyUV,
+ MUV_MT_CopyPasteUV_SelSeqPasteUV,
+)
+from ..op.transfer_uv import (
+ MUV_OT_TransferUV_CopyUV,
+ MUV_OT_TransferUV_PasteUV,
+)
+from ..op.uvw import (
+ MUV_OT_UVW_BoxMap,
+ MUV_OT_UVW_BestPlanerMap,
+)
+from ..op.preserve_uv_aspect import MUV_OT_PreserveUVAspect
+from ..op.texture_lock import (
+ MUV_OT_TextureLock_Lock,
+ MUV_OT_TextureLock_Unlock,
+)
+from ..op.texture_wrap import (
+ MUV_OT_TextureWrap_Refer,
+ MUV_OT_TextureWrap_Set,
+)
+from ..op.world_scale_uv import (
+ MUV_OT_WorldScaleUV_Measure,
+ MUV_OT_WorldScaleUV_ApplyManual,
+ MUV_OT_WorldScaleUV_ApplyScalingDensity,
+ MUV_OT_WorldScaleUV_ApplyProportionalToMesh,
+)
+from ..op.texture_projection import MUV_OT_TextureProjection_Project
+from ..utils.bl_class_registry import BlClassRegistry
+
+
+@BlClassRegistry()
+class MUV_MT_CopyPasteUV(bpy.types.Menu):
+ """
+ Menu class: Master menu of Copy/Paste UV coordinate
+ """
+
+ bl_idname = "uv.muv_mt_copy_paste_uv"
+ bl_label = "Copy/Paste UV"
+ bl_description = "Copy and Paste UV coordinate"
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.label(text="Default")
+ layout.menu(MUV_MT_CopyPasteUV_CopyUV.bl_idname, text="Copy")
+ layout.menu(MUV_MT_CopyPasteUV_PasteUV.bl_idname, text="Paste")
+
+ layout.separator()
+
+ layout.label(text="Selection Sequence")
+ layout.menu(MUV_MT_CopyPasteUV_SelSeqCopyUV.bl_idname, text="Copy")
+ layout.menu(MUV_MT_CopyPasteUV_SelSeqPasteUV.bl_idname, text="Paste")
+
+
+@BlClassRegistry()
+class MUV_MT_TransferUV(bpy.types.Menu):
+ """
+ Menu class: Master menu of Transfer UV coordinate
+ """
+
+ bl_idname = "uv.muv_mt_transfer_uv"
+ bl_label = "Transfer UV"
+ bl_description = "Transfer UV coordinate"
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ layout.operator(MUV_OT_TransferUV_CopyUV.bl_idname, text="Copy")
+ ops = layout.operator(MUV_OT_TransferUV_PasteUV.bl_idname,
+ text="Paste")
+ ops.invert_normals = sc.muv_transfer_uv_invert_normals
+ ops.copy_seams = sc.muv_transfer_uv_copy_seams
+
+
+@BlClassRegistry()
+class MUV_MT_TextureLock(bpy.types.Menu):
+ """
+ Menu class: Master menu of Texture Lock
+ """
+
+ bl_idname = "uv.muv_mt_texture_lock"
+ bl_label = "Texture Lock"
+ bl_description = "Lock texture when vertices of mesh (Preserve UV)"
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ layout.label(text="Normal Mode")
+ layout.operator(
+ MUV_OT_TextureLock_Lock.bl_idname,
+ text="Lock"
+ if not MUV_OT_TextureLock_Lock.is_ready(context)
+ else "ReLock")
+ ops = layout.operator(MUV_OT_TextureLock_Unlock.bl_idname,
+ text="Unlock")
+ ops.connect = sc.muv_texture_lock_connect
+
+ layout.separator()
+
+ layout.label(text="Interactive Mode")
+ layout.prop(sc, "muv_texture_lock_lock", text="Lock")
+
+
+@BlClassRegistry()
+class MUV_MT_WorldScaleUV(bpy.types.Menu):
+ """
+ Menu class: Master menu of world scale UV
+ """
+
+ bl_idname = "uv.muv_mt_world_scale_uv"
+ bl_label = "World Scale UV"
+ bl_description = ""
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ layout.operator(MUV_OT_WorldScaleUV_Measure.bl_idname,
+ text="Measure")
+
+ layout.operator(MUV_OT_WorldScaleUV_ApplyManual.bl_idname,
+ text="Apply (Manual)")
+
+ ops = layout.operator(
+ MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname,
+ text="Apply (Same Desity)")
+ ops.src_density = sc.muv_world_scale_uv_src_density
+ ops.same_density = True
+
+ ops = layout.operator(
+ MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname,
+ text="Apply (Scaling Desity)")
+ ops.src_density = sc.muv_world_scale_uv_src_density
+ ops.same_density = False
+ ops.tgt_scaling_factor = sc.muv_world_scale_uv_tgt_scaling_factor
+
+ ops = layout.operator(
+ MUV_OT_WorldScaleUV_ApplyProportionalToMesh.bl_idname,
+ text="Apply (Proportional to Mesh)")
+ ops.src_density = sc.muv_world_scale_uv_src_density
+ ops.src_uv_area = sc.muv_world_scale_uv_src_uv_area
+ ops.src_mesh_area = sc.muv_world_scale_uv_src_mesh_area
+ ops.origin = sc.muv_world_scale_uv_origin
+
+
+@BlClassRegistry()
+class MUV_MT_TextureWrap(bpy.types.Menu):
+ """
+ Menu class: Master menu of Texture Wrap
+ """
+
+ bl_idname = "uv.muv_mt_texture_wrap"
+ bl_label = "Texture Wrap"
+ bl_description = ""
+
+ def draw(self, _):
+ layout = self.layout
+
+ layout.operator(MUV_OT_TextureWrap_Refer.bl_idname, text="Refer")
+ layout.operator(MUV_OT_TextureWrap_Set.bl_idname, text="Set")
+
+
+@BlClassRegistry()
+class MUV_MT_UVW(bpy.types.Menu):
+ """
+ Menu class: Master menu of UVW
+ """
+
+ bl_idname = "uv.muv_mt_uvw"
+ bl_label = "UVW"
+ bl_description = ""
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ ops = layout.operator(MUV_OT_UVW_BoxMap.bl_idname, text="Box")
+ ops.assign_uvmap = sc.muv_uvw_assign_uvmap
+
+ ops = layout.operator(MUV_OT_UVW_BestPlanerMap.bl_idname,
+ text="Best Planner")
+ ops.assign_uvmap = sc.muv_uvw_assign_uvmap
+
+
+@BlClassRegistry()
+class MUV_MT_PreserveUVAspect(bpy.types.Menu):
+ """
+ Menu class: Master menu of Preserve UV Aspect
+ """
+
+ bl_idname = "uv.muv_mt_preserve_uv_aspect"
+ bl_label = "Preserve UV Aspect"
+ bl_description = ""
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ for key in bpy.data.images.keys():
+ ops = layout.operator(MUV_OT_PreserveUVAspect.bl_idname, text=key)
+ ops.dest_img_name = key
+ ops.origin = sc.muv_preserve_uv_aspect_origin
+
+
+@BlClassRegistry()
+class MUV_MT_TextureProjection(bpy.types.Menu):
+ """
+ Menu class: Master menu of Texture Projection
+ """
+
+ bl_idname = "uv.muv_mt_texture_projection"
+ bl_label = "Texture Projection"
+ bl_description = ""
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ layout.prop(sc, "muv_texture_projection_enable",
+ text="Texture Projection")
+ layout.operator(MUV_OT_TextureProjection_Project.bl_idname,
+ text="Project")
diff --git a/magic_uv/ui/__init__.py b/magic_uv/ui/__init__.py
new file mode 100644
index 00000000..032cc3bd
--- /dev/null
+++ b/magic_uv/ui/__init__.py
@@ -0,0 +1,50 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+if "bpy" in locals():
+ import importlib
+ importlib.reload(view3d_copy_paste_uv_editmode)
+ importlib.reload(view3d_copy_paste_uv_objectmode)
+ importlib.reload(view3d_uv_manipulation)
+ importlib.reload(view3d_uv_mapping)
+ importlib.reload(uvedit_copy_paste_uv)
+ importlib.reload(uvedit_uv_manipulation)
+ importlib.reload(uvedit_editor_enhancement)
+ importlib.reload(VIEW3D_MT_object)
+ importlib.reload(VIEW3D_MT_uv_map)
+ importlib.reload(IMAGE_MT_uvs)
+else:
+ from . import view3d_copy_paste_uv_editmode
+ from . import view3d_copy_paste_uv_objectmode
+ from . import view3d_uv_manipulation
+ from . import view3d_uv_mapping
+ from . import uvedit_copy_paste_uv
+ from . import uvedit_uv_manipulation
+ from . import uvedit_editor_enhancement
+ from . import VIEW3D_MT_object
+ from . import VIEW3D_MT_uv_map
+ from . import IMAGE_MT_uvs
+
+import bpy
diff --git a/magic_uv/ui/uvedit_copy_paste_uv.py b/magic_uv/ui/uvedit_copy_paste_uv.py
new file mode 100644
index 00000000..39259649
--- /dev/null
+++ b/magic_uv/ui/uvedit_copy_paste_uv.py
@@ -0,0 +1,59 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+
+from ..op.copy_paste_uv_uvedit import (
+ MUV_OT_CopyPasteUVUVEdit_CopyUV,
+ MUV_OT_CopyPasteUVUVEdit_PasteUV,
+)
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils import compatibility as compat
+
+
+@BlClassRegistry()
+@compat.ChangeRegionType(region_type='TOOLS')
+class MUV_PT_UVEdit_CopyPasteUV(bpy.types.Panel):
+ """
+ Panel class: Copy/Paste UV on Property Panel on UV/ImageEditor
+ """
+
+ bl_space_type = 'IMAGE_EDITOR'
+ bl_region_type = 'UI'
+ bl_label = "Copy/Paste UV"
+ bl_category = "Magic UV"
+ bl_context = 'mesh_edit'
+ bl_options = {'DEFAULT_CLOSED'}
+
+ def draw_header(self, _):
+ layout = self.layout
+ layout.label(text="", icon=compat.icon('IMAGE'))
+
+ def draw(self, _):
+ layout = self.layout
+
+ 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")
diff --git a/magic_uv/ui/uvedit_editor_enhancement.py b/magic_uv/ui/uvedit_editor_enhancement.py
new file mode 100644
index 00000000..dbae514f
--- /dev/null
+++ b/magic_uv/ui/uvedit_editor_enhancement.py
@@ -0,0 +1,146 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+
+from ..op.align_uv_cursor import MUV_OT_AlignUVCursor
+from ..op.uv_bounding_box import (
+ MUV_OT_UVBoundingBox,
+)
+from ..op.uv_inspection import (
+ MUV_OT_UVInspection_Render,
+ MUV_OT_UVInspection_Update,
+)
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils import compatibility as compat
+
+
+@BlClassRegistry()
+@compat.ChangeRegionType(region_type='TOOLS')
+class MUV_PT_UVEdit_EditorEnhancement(bpy.types.Panel):
+ """
+ Panel class: UV/Image Editor Enhancement
+ """
+
+ bl_space_type = 'IMAGE_EDITOR'
+ bl_region_type = 'UI'
+ bl_label = "Editor Enhancement"
+ bl_category = "Magic UV"
+ bl_context = 'mesh_edit'
+ bl_options = {'DEFAULT_CLOSED'}
+
+ def draw_header(self, _):
+ layout = self.layout
+ layout.label(text="", icon=compat.icon('IMAGE'))
+
+ def draw(self, context):
+ layout = self.layout
+ sc = context.scene
+
+ box = layout.box()
+ box.prop(sc, "muv_align_uv_cursor_enabled", text="Align UV Cursor")
+ if sc.muv_align_uv_cursor_enabled:
+ box.prop(sc, "muv_align_uv_cursor_align_method", expand=True)
+
+ col = box.column(align=True)
+
+ row = col.row(align=True)
+ ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, text="Left Top")
+ ops.position = 'LEFT_TOP'
+ ops.base = sc.muv_align_uv_cursor_align_method
+ ops = row.operator(MUV_OT_AlignUVCursor.bl_idname,
+ text="Middle Top")
+ ops.position = 'MIDDLE_TOP'
+ ops.base = sc.muv_align_uv_cursor_align_method
+ ops = row.operator(MUV_OT_AlignUVCursor.bl_idname,
+ text="Right Top")
+ ops.position = 'RIGHT_TOP'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ row = col.row(align=True)
+ ops = row.operator(MUV_OT_AlignUVCursor.bl_idname,
+ text="Left Middle")
+ ops.position = 'LEFT_MIDDLE'
+ ops.base = sc.muv_align_uv_cursor_align_method
+ ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, text="Center")
+ ops.position = 'CENTER'
+ ops.base = sc.muv_align_uv_cursor_align_method
+ ops = row.operator(MUV_OT_AlignUVCursor.bl_idname,
+ text="Right Middle")
+ ops.position = 'RIGHT_MIDDLE'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ row = col.row(align=True)
+ ops = row.operator(MUV_OT_AlignUVCursor.bl_idname,
+ text="Left Bottom")
+ ops.position = 'LEFT_BOTTOM'
+ ops.base = sc.muv_align_uv_cursor_align_method
+ ops = row.operator(MUV_OT_AlignUVCursor.bl_idname,
+ text="Middle Bottom")
+ ops.position = 'MIDDLE_BOTTOM'
+ ops.base = sc.muv_align_uv_cursor_align_method
+ ops = row.operator(MUV_OT_AlignUVCursor.bl_idname,
+ text="Right Bottom")
+ ops.position = 'RIGHT_BOTTOM'
+ ops.base = sc.muv_align_uv_cursor_align_method
+
+ box = layout.box()
+ box.prop(sc, "muv_uv_cursor_location_enabled",
+ text="UV Cursor Location")
+ if sc.muv_uv_cursor_location_enabled:
+ box.prop(sc, "muv_align_uv_cursor_cursor_loc", text="")
+
+ box = layout.box()
+ box.prop(sc, "muv_uv_bounding_box_enabled", text="UV Bounding Box")
+ if sc.muv_uv_bounding_box_enabled:
+ box.prop(sc, "muv_uv_bounding_box_show",
+ text="Hide"
+ if MUV_OT_UVBoundingBox.is_running(context)
+ else "Show",
+ icon='RESTRICT_VIEW_OFF'
+ if MUV_OT_UVBoundingBox.is_running(context)
+ else 'RESTRICT_VIEW_ON')
+ box.prop(sc, "muv_uv_bounding_box_uniform_scaling",
+ text="Uniform Scaling")
+ box.prop(sc, "muv_uv_bounding_box_boundary", text="Boundary")
+
+ box = layout.box()
+ box.prop(sc, "muv_uv_inspection_enabled", text="UV Inspection")
+ if sc.muv_uv_inspection_enabled:
+ row = box.row()
+ row.prop(
+ sc, "muv_uv_inspection_show",
+ text="Hide"
+ if MUV_OT_UVInspection_Render.is_running(context)
+ else "Show",
+ icon='RESTRICT_VIEW_OFF'
+ if MUV_OT_UVInspection_Render.is_running(context)
+ else 'RESTRICT_VIEW_ON')
+ row.operator(MUV_OT_UVInspection_Update.bl_idname, text="Update")
+ row = box.row()
+ row.prop(sc, "muv_uv_inspection_show_overlapped")
+ row.prop(sc, "muv_uv_inspection_show_flipped")
+ row = box.row()
+ row.prop(sc, "muv_uv_inspection_show_mode")
diff --git a/magic_uv/ui/uvedit_uv_manipulation.py b/magic_uv/ui/uvedit_uv_manipulation.py
new file mode 100644
index 00000000..96c8b54b
--- /dev/null
+++ b/magic_uv/ui/uvedit_uv_manipulation.py
@@ -0,0 +1,132 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+
+from ..op.align_uv import (
+ MUV_OT_AlignUV_Circle,
+ MUV_OT_AlignUV_Straighten,
+ MUV_OT_AlignUV_Axis,
+)
+from ..op.smooth_uv import (
+ MUV_OT_SmoothUV,
+)
+from ..op.select_uv import (
+ MUV_OT_SelectUV_SelectOverlapped,
+ MUV_OT_SelectUV_SelectFlipped,
+)
+from ..op.pack_uv import MUV_OT_PackUV
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils import compatibility as compat
+
+
+@BlClassRegistry()
+@compat.ChangeRegionType(region_type='TOOLS')
+class MUV_PT_UVEdit_UVManipulation(bpy.types.Panel):
+ """
+ Panel class: UV Manipulation on Property Panel on UV/ImageEditor
+ """
+
+ bl_space_type = 'IMAGE_EDITOR'
+ bl_region_type = 'UI'
+ bl_label = "UV Manipulation"
+ bl_category = "Magic UV"
+ bl_context = 'mesh_edit'
+ bl_options = {'DEFAULT_CLOSED'}
+
+ def draw_header(self, _):
+ layout = self.layout
+ layout.label(text="", icon=compat.icon('IMAGE'))
+
+ def draw(self, context):
+ sc = context.scene
+ layout = self.layout
+
+ box = layout.box()
+ box.prop(sc, "muv_align_uv_enabled", text="Align UV")
+ if sc.muv_align_uv_enabled:
+ col = box.column()
+ row = col.row(align=True)
+ ops = row.operator(MUV_OT_AlignUV_Circle.bl_idname, text="Circle")
+ ops.transmission = sc.muv_align_uv_transmission
+ ops.select = sc.muv_align_uv_select
+ ops = row.operator(MUV_OT_AlignUV_Straighten.bl_idname,
+ text="Straighten")
+ ops.transmission = sc.muv_align_uv_transmission
+ ops.select = sc.muv_align_uv_select
+ ops.vertical = sc.muv_align_uv_vertical
+ ops.horizontal = sc.muv_align_uv_horizontal
+ ops.mesh_infl = sc.muv_align_uv_mesh_infl
+ row = col.row()
+ ops = row.operator(MUV_OT_AlignUV_Axis.bl_idname, text="XY-axis")
+ ops.transmission = sc.muv_align_uv_transmission
+ ops.select = sc.muv_align_uv_select
+ ops.vertical = sc.muv_align_uv_vertical
+ ops.horizontal = sc.muv_align_uv_horizontal
+ ops.location = sc.muv_align_uv_location
+ ops.mesh_infl = sc.muv_align_uv_mesh_infl
+ row.prop(sc, "muv_align_uv_location", text="")
+
+ col = box.column(align=True)
+ row = col.row(align=True)
+ row.prop(sc, "muv_align_uv_transmission", text="Transmission")
+ row.prop(sc, "muv_align_uv_select", text="Select")
+ row = col.row(align=True)
+ row.prop(sc, "muv_align_uv_vertical", text="Vertical")
+ row.prop(sc, "muv_align_uv_horizontal", text="Horizontal")
+ col.prop(sc, "muv_align_uv_mesh_infl", text="Mesh Influence")
+
+ box = layout.box()
+ box.prop(sc, "muv_smooth_uv_enabled", text="Smooth UV")
+ if sc.muv_smooth_uv_enabled:
+ ops = box.operator(MUV_OT_SmoothUV.bl_idname, text="Smooth")
+ ops.transmission = sc.muv_smooth_uv_transmission
+ ops.select = sc.muv_smooth_uv_select
+ ops.mesh_infl = sc.muv_smooth_uv_mesh_infl
+ col = box.column(align=True)
+ row = col.row(align=True)
+ row.prop(sc, "muv_smooth_uv_transmission", text="Transmission")
+ row.prop(sc, "muv_smooth_uv_select", text="Select")
+ col.prop(sc, "muv_smooth_uv_mesh_infl", text="Mesh Influence")
+
+ box = layout.box()
+ box.prop(sc, "muv_select_uv_enabled", text="Select UV")
+ if sc.muv_select_uv_enabled:
+ row = box.row(align=True)
+ row.operator(MUV_OT_SelectUV_SelectOverlapped.bl_idname)
+ row.operator(MUV_OT_SelectUV_SelectFlipped.bl_idname)
+
+ box = layout.box()
+ box.prop(sc, "muv_pack_uv_enabled", text="Pack UV (Extension)")
+ if sc.muv_pack_uv_enabled:
+ ops = box.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
+ 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="")
diff --git a/magic_uv/ui/view3d_copy_paste_uv_editmode.py b/magic_uv/ui/view3d_copy_paste_uv_editmode.py
new file mode 100644
index 00000000..49a4e0a3
--- /dev/null
+++ b/magic_uv/ui/view3d_copy_paste_uv_editmode.py
@@ -0,0 +1,92 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+
+from ..op.copy_paste_uv import (
+ MUV_MT_CopyPasteUV_CopyUV,
+ MUV_MT_CopyPasteUV_PasteUV,
+ MUV_MT_CopyPasteUV_SelSeqCopyUV,
+ MUV_MT_CopyPasteUV_SelSeqPasteUV,
+)
+from ..op.transfer_uv import (
+ MUV_OT_TransferUV_CopyUV,
+ MUV_OT_TransferUV_PasteUV,
+)
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils import compatibility as compat
+
+
+@BlClassRegistry()
+@compat.ChangeRegionType(region_type='TOOLS')
+class MUV_PT_CopyPasteUVEditMode(bpy.types.Panel):
+ """
+ Panel class: Copy/Paste UV on Property Panel on View3D
+ """
+
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'UI'
+ bl_label = "Copy/Paste UV"
+ bl_category = "Magic UV"
+ bl_context = 'mesh_edit'
+ bl_options = {'DEFAULT_CLOSED'}
+
+ def draw_header(self, _):
+ layout = self.layout
+ layout.label(text="", icon=compat.icon('IMAGE'))
+
+ def draw(self, context):
+ sc = context.scene
+ layout = self.layout
+
+ box = layout.box()
+ box.prop(sc, "muv_copy_paste_uv_enabled", text="Copy/Paste UV")
+ if sc.muv_copy_paste_uv_enabled:
+ row = box.row(align=True)
+ if sc.muv_copy_paste_uv_mode == 'DEFAULT':
+ row.menu(MUV_MT_CopyPasteUV_CopyUV.bl_idname, text="Copy")
+ row.menu(MUV_MT_CopyPasteUV_PasteUV.bl_idname, text="Paste")
+ elif sc.muv_copy_paste_uv_mode == 'SEL_SEQ':
+ row.menu(MUV_MT_CopyPasteUV_SelSeqCopyUV.bl_idname,
+ text="Copy")
+ row.menu(MUV_MT_CopyPasteUV_SelSeqPasteUV.bl_idname,
+ text="Paste")
+ box.prop(sc, "muv_copy_paste_uv_mode", expand=True)
+ box.prop(sc, "muv_copy_paste_uv_copy_seams", text="Seams")
+ box.prop(sc, "muv_copy_paste_uv_strategy", text="Strategy")
+
+ box = layout.box()
+ box.prop(sc, "muv_transfer_uv_enabled", text="Transfer UV")
+ if sc.muv_transfer_uv_enabled:
+ row = box.row(align=True)
+ row.operator(MUV_OT_TransferUV_CopyUV.bl_idname, text="Copy")
+ ops = row.operator(MUV_OT_TransferUV_PasteUV.bl_idname,
+ text="Paste")
+ ops.invert_normals = sc.muv_transfer_uv_invert_normals
+ ops.copy_seams = sc.muv_transfer_uv_copy_seams
+ row = box.row()
+ row.prop(sc, "muv_transfer_uv_invert_normals",
+ text="Invert Normals")
+ row.prop(sc, "muv_transfer_uv_copy_seams", text="Seams")
diff --git a/magic_uv/ui/view3d_copy_paste_uv_objectmode.py b/magic_uv/ui/view3d_copy_paste_uv_objectmode.py
new file mode 100644
index 00000000..574a0e43
--- /dev/null
+++ b/magic_uv/ui/view3d_copy_paste_uv_objectmode.py
@@ -0,0 +1,62 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+
+from ..op.copy_paste_uv_object import (
+ MUV_MT_CopyPasteUVObject_CopyUV,
+ MUV_MT_CopyPasteUVObject_PasteUV,
+)
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils import compatibility as compat
+
+
+@BlClassRegistry()
+@compat.ChangeRegionType(region_type='TOOLS')
+class MUV_PT_View3D_Object_CopyPasteUV(bpy.types.Panel):
+ """
+ Panel class: Copy/Paste UV on Property Panel on View3D
+ """
+
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'UI'
+ bl_label = "Copy/Paste UV"
+ bl_category = "Magic UV"
+ bl_context = 'objectmode'
+ bl_options = {'DEFAULT_CLOSED'}
+
+ def draw_header(self, _):
+ layout = self.layout
+ layout.label(text="", icon=compat.icon('IMAGE'))
+
+ def draw(self, context):
+ sc = context.scene
+ layout = self.layout
+
+ row = layout.row(align=True)
+ row.menu(MUV_MT_CopyPasteUVObject_CopyUV.bl_idname, text="Copy")
+ row.menu(MUV_MT_CopyPasteUVObject_PasteUV.bl_idname, text="Paste")
+ layout.prop(sc, "muv_copy_paste_uv_object_copy_seams",
+ text="Seams")
diff --git a/magic_uv/ui/view3d_uv_manipulation.py b/magic_uv/ui/view3d_uv_manipulation.py
new file mode 100644
index 00000000..312ae171
--- /dev/null
+++ b/magic_uv/ui/view3d_uv_manipulation.py
@@ -0,0 +1,282 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+
+from ..op.texture_lock import (
+ MUV_OT_TextureLock_Lock,
+ MUV_OT_TextureLock_Unlock,
+ MUV_OT_TextureLock_Intr,
+)
+from ..op.texture_wrap import (
+ MUV_OT_TextureWrap_Refer,
+ MUV_OT_TextureWrap_Set,
+)
+from ..op.uv_sculpt import (
+ MUV_OT_UVSculpt,
+)
+from ..op.world_scale_uv import (
+ MUV_OT_WorldScaleUV_Measure,
+ MUV_OT_WorldScaleUV_ApplyManual,
+ MUV_OT_WorldScaleUV_ApplyScalingDensity,
+ MUV_OT_WorldScaleUV_ApplyProportionalToMesh,
+)
+from ..op.flip_rotate_uv import MUV_OT_FlipRotate
+from ..op.mirror_uv import MUV_OT_MirrorUV
+from ..op.move_uv import MUV_OT_MoveUV
+from ..op.preserve_uv_aspect import MUV_OT_PreserveUVAspect
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils import compatibility as compat
+
+
+@BlClassRegistry()
+@compat.ChangeRegionType(region_type='TOOLS')
+class MUV_PT_View3D_UVManipulation(bpy.types.Panel):
+ """
+ Panel class: UV Manipulation on Property Panel on View3D
+ """
+
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'UI'
+ bl_label = "UV Manipulation"
+ bl_category = "Magic UV"
+ bl_context = 'mesh_edit'
+ bl_options = {'DEFAULT_CLOSED'}
+
+ def draw_header(self, _):
+ layout = self.layout
+ layout.label(text="", icon=compat.icon('IMAGE'))
+
+ def draw(self, context):
+ sc = context.scene
+ layout = self.layout
+
+ box = layout.box()
+ box.prop(sc, "muv_flip_rotate_uv_enabled", text="Flip/Rotate UV")
+ if sc.muv_flip_rotate_uv_enabled:
+ row = box.row()
+ ops = row.operator(MUV_OT_FlipRotate.bl_idname, text="Flip/Rotate")
+ ops.seams = sc.muv_flip_rotate_uv_seams
+ row.prop(sc, "muv_flip_rotate_uv_seams", text="Seams")
+
+ box = layout.box()
+ box.prop(sc, "muv_mirror_uv_enabled", text="Mirror UV")
+ if sc.muv_mirror_uv_enabled:
+ row = box.row()
+ ops = row.operator(MUV_OT_MirrorUV.bl_idname, text="Mirror")
+ ops.axis = sc.muv_mirror_uv_axis
+ row.prop(sc, "muv_mirror_uv_axis", text="")
+
+ box = layout.box()
+ box.prop(sc, "muv_move_uv_enabled", text="Move UV")
+ if sc.muv_move_uv_enabled:
+ col = box.column()
+ if not MUV_OT_MoveUV.is_running(context):
+ col.operator(MUV_OT_MoveUV.bl_idname, icon='PLAY',
+ text="Start")
+ else:
+ col.operator(MUV_OT_MoveUV.bl_idname, icon='PAUSE',
+ text="Stop")
+
+ box = layout.box()
+ box.prop(sc, "muv_world_scale_uv_enabled", text="World Scale UV")
+ if sc.muv_world_scale_uv_enabled:
+ box.prop(sc, "muv_world_scale_uv_mode", text="")
+
+ if sc.muv_world_scale_uv_mode == 'MANUAL':
+ sp = compat.layout_split(box, 0.5)
+ col = sp.column()
+ col.prop(sc, "muv_world_scale_uv_tgt_texture_size",
+ text="Texture Size")
+ sp = compat.layout_split(sp, 1.0)
+ col = sp.column()
+ col.label(text="Density:")
+ col.prop(sc, "muv_world_scale_uv_tgt_density", text="")
+ box.prop(sc, "muv_world_scale_uv_origin", text="Origin")
+ ops = box.operator(MUV_OT_WorldScaleUV_ApplyManual.bl_idname,
+ text="Apply")
+ ops.tgt_density = sc.muv_world_scale_uv_tgt_density
+ ops.tgt_texture_size = sc.muv_world_scale_uv_tgt_texture_size
+ ops.origin = sc.muv_world_scale_uv_origin
+ ops.show_dialog = False
+
+ elif sc.muv_world_scale_uv_mode == 'SAME_DENSITY':
+ sp = compat.layout_split(box, 0.4)
+ col = sp.column(align=True)
+ col.label(text="Source:")
+ sp = compat.layout_split(sp, 1.0)
+ col = sp.column(align=True)
+ col.operator(MUV_OT_WorldScaleUV_Measure.bl_idname,
+ text="Measure")
+
+ sp = compat.layout_split(box, 0.7)
+ col = sp.column(align=True)
+ col.prop(sc, "muv_world_scale_uv_src_density", text="Density")
+ col.enabled = False
+ sp = compat.layout_split(sp, 1.0)
+ col = sp.column(align=True)
+ col.label(text="px2/cm2")
+
+ box.separator()
+ box.prop(sc, "muv_world_scale_uv_origin", text="Origin")
+ ops = box.operator(
+ MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname,
+ text="Apply")
+ ops.src_density = sc.muv_world_scale_uv_src_density
+ ops.origin = sc.muv_world_scale_uv_origin
+ ops.same_density = True
+ ops.show_dialog = False
+
+ elif sc.muv_world_scale_uv_mode == 'SCALING_DENSITY':
+ sp = compat.layout_split(box, 0.4)
+ col = sp.column(align=True)
+ col.label(text="Source:")
+ sp = compat.layout_split(sp, 1.0)
+ col = sp.column(align=True)
+ col.operator(MUV_OT_WorldScaleUV_Measure.bl_idname,
+ text="Measure")
+
+ sp = compat.layout_split(box, 0.7)
+ col = sp.column(align=True)
+ col.prop(sc, "muv_world_scale_uv_src_density", text="Density")
+ col.enabled = False
+ sp = compat.layout_split(sp, 1.0)
+ col = sp.column(align=True)
+ col.label(text="px2/cm2")
+
+ box.separator()
+ box.prop(sc, "muv_world_scale_uv_tgt_scaling_factor",
+ text="Scaling Factor")
+ box.prop(sc, "muv_world_scale_uv_origin", text="Origin")
+ ops = box.operator(
+ MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname,
+ text="Apply")
+ ops.src_density = sc.muv_world_scale_uv_src_density
+ ops.origin = sc.muv_world_scale_uv_origin
+ ops.same_density = False
+ ops.show_dialog = False
+ ops.tgt_scaling_factor = \
+ sc.muv_world_scale_uv_tgt_scaling_factor
+
+ elif sc.muv_world_scale_uv_mode == 'PROPORTIONAL_TO_MESH':
+ sp = compat.layout_split(box, 0.4)
+ col = sp.column(align=True)
+ col.label(text="Source:")
+ sp = compat.layout_split(sp, 1.0)
+ col = sp.column(align=True)
+ col.operator(MUV_OT_WorldScaleUV_Measure.bl_idname,
+ text="Measure")
+
+ sp = compat.layout_split(box, 0.7)
+ col = sp.column(align=True)
+ col.prop(sc, "muv_world_scale_uv_src_mesh_area",
+ text="Mesh Area")
+ col.prop(sc, "muv_world_scale_uv_src_uv_area", text="UV Area")
+ col.prop(sc, "muv_world_scale_uv_src_density", text="Density")
+ col.enabled = False
+ sp = compat.layout_split(sp, 1.0)
+ col = sp.column(align=True)
+ col.label(text="cm2")
+ col.label(text="px2")
+ col.label(text="px2/cm2")
+ col.enabled = False
+
+ box.separator()
+ box.prop(sc, "muv_world_scale_uv_origin", text="Origin")
+ ops = box.operator(
+ MUV_OT_WorldScaleUV_ApplyProportionalToMesh.bl_idname,
+ text="Apply")
+ ops.src_density = sc.muv_world_scale_uv_src_density
+ ops.src_uv_area = sc.muv_world_scale_uv_src_uv_area
+ ops.src_mesh_area = sc.muv_world_scale_uv_src_mesh_area
+ ops.origin = sc.muv_world_scale_uv_origin
+ ops.show_dialog = False
+
+ box = layout.box()
+ box.prop(sc, "muv_preserve_uv_aspect_enabled",
+ text="Preserve UV Aspect")
+ if sc.muv_preserve_uv_aspect_enabled:
+ row = box.row()
+ ops = row.operator(MUV_OT_PreserveUVAspect.bl_idname,
+ text="Change Image")
+ ops.dest_img_name = sc.muv_preserve_uv_aspect_tex_image
+ ops.origin = sc.muv_preserve_uv_aspect_origin
+ row.prop(sc, "muv_preserve_uv_aspect_tex_image", text="")
+ box.prop(sc, "muv_preserve_uv_aspect_origin", text="Origin")
+
+ box = layout.box()
+ box.prop(sc, "muv_texture_lock_enabled", text="Texture Lock")
+ if sc.muv_texture_lock_enabled:
+ row = box.row(align=True)
+ col = row.column(align=True)
+ col.label(text="Normal Mode:")
+ col = row.column(align=True)
+ col.operator(MUV_OT_TextureLock_Lock.bl_idname,
+ text="Lock"
+ if not MUV_OT_TextureLock_Lock.is_ready(context)
+ else "ReLock")
+ ops = col.operator(MUV_OT_TextureLock_Unlock.bl_idname,
+ text="Unlock")
+ ops.connect = sc.muv_texture_lock_connect
+ col.prop(sc, "muv_texture_lock_connect", text="Connect")
+
+ row = box.row(align=True)
+ row.label(text="Interactive Mode:")
+ box.prop(sc, "muv_texture_lock_lock",
+ text="Unlock"
+ if MUV_OT_TextureLock_Intr.is_running(context)
+ else "Lock",
+ icon='RESTRICT_VIEW_OFF'
+ if MUV_OT_TextureLock_Intr.is_running(context)
+ else 'RESTRICT_VIEW_ON')
+
+ box = layout.box()
+ box.prop(sc, "muv_texture_wrap_enabled", text="Texture Wrap")
+ if sc.muv_texture_wrap_enabled:
+ row = box.row(align=True)
+ row.operator(MUV_OT_TextureWrap_Refer.bl_idname, text="Refer")
+ row.operator(MUV_OT_TextureWrap_Set.bl_idname, text="Set")
+ box.prop(sc, "muv_texture_wrap_set_and_refer")
+ box.prop(sc, "muv_texture_wrap_selseq")
+
+ box = layout.box()
+ box.prop(sc, "muv_uv_sculpt_enabled", text="UV Sculpt")
+ if sc.muv_uv_sculpt_enabled:
+ box.prop(sc, "muv_uv_sculpt_enable",
+ text="Disable"if MUV_OT_UVSculpt.is_running(context)
+ else "Enable",
+ icon='RESTRICT_VIEW_OFF'
+ if MUV_OT_UVSculpt.is_running(context)
+ else 'RESTRICT_VIEW_ON')
+ col = box.column()
+ col.label(text="Brush:")
+ col.prop(sc, "muv_uv_sculpt_radius")
+ col.prop(sc, "muv_uv_sculpt_strength")
+ box.prop(sc, "muv_uv_sculpt_tools")
+ if sc.muv_uv_sculpt_tools == 'PINCH':
+ box.prop(sc, "muv_uv_sculpt_pinch_invert")
+ elif sc.muv_uv_sculpt_tools == 'RELAX':
+ box.prop(sc, "muv_uv_sculpt_relax_method")
+ box.prop(sc, "muv_uv_sculpt_show_brush")
diff --git a/magic_uv/ui/view3d_uv_mapping.py b/magic_uv/ui/view3d_uv_mapping.py
new file mode 100644
index 00000000..278d1725
--- /dev/null
+++ b/magic_uv/ui/view3d_uv_mapping.py
@@ -0,0 +1,114 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+
+from ..op.uvw import (
+ MUV_OT_UVW_BoxMap,
+ MUV_OT_UVW_BestPlanerMap,
+)
+from ..op.texture_projection import (
+ MUV_OT_TextureProjection,
+ MUV_OT_TextureProjection_Project,
+)
+from ..op.unwrap_constraint import MUV_OT_UnwrapConstraint
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils import compatibility as compat
+
+
+@BlClassRegistry()
+@compat.ChangeRegionType(region_type='TOOLS')
+class MUV_PT_View3D_UVMapping(bpy.types.Panel):
+ """
+ Panel class: UV Mapping on Property Panel on View3D
+ """
+
+ bl_space_type = 'VIEW_3D'
+ bl_region_type = 'UI'
+ bl_label = "UV Mapping"
+ bl_category = "Magic UV"
+ bl_context = 'mesh_edit'
+ bl_options = {'DEFAULT_CLOSED'}
+
+ def draw_header(self, _):
+ layout = self.layout
+ layout.label(text="", icon=compat.icon('IMAGE'))
+
+ def draw(self, context):
+ sc = context.scene
+ layout = self.layout
+
+ box = layout.box()
+ box.prop(sc, "muv_unwrap_constraint_enabled", text="Unwrap Constraint")
+ if sc.muv_unwrap_constraint_enabled:
+ ops = box.operator(MUV_OT_UnwrapConstraint.bl_idname,
+ text="Unwrap")
+ ops.u_const = sc.muv_unwrap_constraint_u_const
+ ops.v_const = sc.muv_unwrap_constraint_v_const
+ row = box.row(align=True)
+ row.prop(sc, "muv_unwrap_constraint_u_const", text="U-Constraint")
+ row.prop(sc, "muv_unwrap_constraint_v_const", text="V-Constraint")
+
+ box = layout.box()
+ box.prop(sc, "muv_texture_projection_enabled",
+ text="Texture Projection")
+ if sc.muv_texture_projection_enabled:
+ row = box.row()
+ row.prop(
+ sc, "muv_texture_projection_enable",
+ text="Disable"
+ if MUV_OT_TextureProjection.is_running(context)
+ else "Enable",
+ icon='RESTRICT_VIEW_OFF'
+ if MUV_OT_TextureProjection.is_running(context)
+ else 'RESTRICT_VIEW_ON')
+ row.prop(sc, "muv_texture_projection_tex_image", text="")
+ box.prop(sc, "muv_texture_projection_tex_transparency",
+ text="Transparency")
+ col = box.column(align=True)
+ row = col.row()
+ row.prop(sc, "muv_texture_projection_adjust_window",
+ text="Adjust Window")
+ if not sc.muv_texture_projection_adjust_window:
+ row.prop(sc, "muv_texture_projection_tex_magnitude",
+ text="Magnitude")
+ col.prop(sc, "muv_texture_projection_apply_tex_aspect",
+ text="Texture Aspect Ratio")
+ col.prop(sc, "muv_texture_projection_assign_uvmap",
+ text="Assign UVMap")
+ box.operator(
+ MUV_OT_TextureProjection_Project.bl_idname,
+ text="Project")
+
+ box = layout.box()
+ box.prop(sc, "muv_uvw_enabled", text="UVW")
+ if sc.muv_uvw_enabled:
+ row = box.row(align=True)
+ ops = row.operator(MUV_OT_UVW_BoxMap.bl_idname, text="Box")
+ ops.assign_uvmap = sc.muv_uvw_assign_uvmap
+ ops = row.operator(MUV_OT_UVW_BestPlanerMap.bl_idname,
+ text="Best Planner")
+ ops.assign_uvmap = sc.muv_uvw_assign_uvmap
+ box.prop(sc, "muv_uvw_assign_uvmap", text="Assign UVMap")
diff --git a/magic_uv/updater.py b/magic_uv/updater.py
new file mode 100644
index 00000000..8a8da2ba
--- /dev/null
+++ b/magic_uv/updater.py
@@ -0,0 +1,141 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import os
+
+import bpy
+from bpy.props import (
+ StringProperty,
+)
+
+from .utils.bl_class_registry import BlClassRegistry
+from .utils.addon_updator import (
+ AddonUpdatorManager,
+ AddonUpdatorConfig,
+ get_separator,
+)
+from .utils import compatibility as compat
+
+
+@BlClassRegistry()
+class MUV_OT_CheckAddonUpdate(bpy.types.Operator):
+ bl_idname = "uv.muv_ot_check_addon_update"
+ bl_label = "Check Update"
+ bl_description = "Check Add-on Update"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, _):
+ updater = AddonUpdatorManager.get_instance()
+ updater.check_update_candidate()
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+@compat.make_annotations
+class MUV_OT_UpdateAddon(bpy.types.Operator):
+ bl_idname = "uv.muv_ot_update_addon"
+ bl_label = "Update"
+ bl_description = "Update Add-on"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ branch_name = StringProperty(
+ name="Branch Name",
+ description="Branch name to update",
+ default="",
+ )
+
+ def execute(self, _):
+ updater = AddonUpdatorManager.get_instance()
+ updater.update(self.branch_name)
+
+ return {'FINISHED'}
+
+
+def draw_updater_ui(prefs_obj):
+ layout = prefs_obj.layout
+ updater = AddonUpdatorManager.get_instance()
+
+ layout.separator()
+
+ if not updater.candidate_checked():
+ col = layout.column()
+ col.scale_y = 2
+ row = col.row()
+ row.operator(MUV_OT_CheckAddonUpdate.bl_idname,
+ text="Check 'Magic UV' add-on update",
+ icon='FILE_REFRESH')
+ else:
+ row = layout.row(align=True)
+ row.scale_y = 2
+ col = row.column()
+ col.operator(MUV_OT_CheckAddonUpdate.bl_idname,
+ text="Check 'Magic UV' add-on update",
+ icon='FILE_REFRESH')
+ col = row.column()
+ if updater.latest_version() != "":
+ col.enabled = True
+ ops = col.operator(
+ MUV_OT_UpdateAddon.bl_idname,
+ text="Update to the latest release version (version: {})"
+ .format(updater.latest_version()),
+ icon='TRIA_DOWN_BAR')
+ ops.branch_name = updater.latest_version()
+ else:
+ col.enabled = False
+ col.operator(MUV_OT_UpdateAddon.bl_idname,
+ text="No updates are available.")
+
+ layout.separator()
+ layout.label(text="Manual Update:")
+ row = layout.row(align=True)
+ row.prop(prefs_obj, "updater_branch_to_update", text="Target")
+ ops = row.operator(
+ MUV_OT_UpdateAddon.bl_idname, text="Update",
+ icon='TRIA_DOWN_BAR')
+ ops.branch_name = prefs_obj.updater_branch_to_update
+
+ layout.separator()
+ if updater.has_error():
+ box = layout.box()
+ box.label(text=updater.error(), icon='CANCEL')
+ elif updater.has_info():
+ box = layout.box()
+ box.label(text=updater.info(), icon='ERROR')
+
+
+def register_updater(bl_info):
+ config = AddonUpdatorConfig()
+ config.owner = "nutti"
+ config.repository = "Magic-UV"
+ config.current_addon_path = os.path.dirname(os.path.realpath(__file__))
+ config.branches = ["master", "develop"]
+ config.addon_directory = \
+ config.current_addon_path[
+ :config.current_addon_path.rfind(get_separator())]
+ config.min_release_version = bl_info["version"]
+ config.target_addon_path = "src/magic_uv"
+ updater = AddonUpdatorManager.get_instance()
+ updater.init(bl_info, config)
diff --git a/magic_uv/utils/__init__.py b/magic_uv/utils/__init__.py
new file mode 100644
index 00000000..b74ab903
--- /dev/null
+++ b/magic_uv/utils/__init__.py
@@ -0,0 +1,38 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+if "bpy" in locals():
+ import importlib
+ importlib.reload(addon_updator)
+ importlib.reload(bl_class_registry)
+ importlib.reload(compatibility)
+ importlib.reload(property_class_registry)
+else:
+ from . import addon_updator
+ from . import bl_class_registry
+ from . import compatibility
+ from . import property_class_registry
+
+import bpy
diff --git a/magic_uv/utils/addon_updator.py b/magic_uv/utils/addon_updator.py
new file mode 100644
index 00000000..b2ff76cc
--- /dev/null
+++ b/magic_uv/utils/addon_updator.py
@@ -0,0 +1,360 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+from threading import Lock
+import urllib
+import urllib.request
+import ssl
+import json
+import os
+import zipfile
+import shutil
+import datetime
+
+
+def get_separator():
+ if os.name == "nt":
+ return "\\"
+ return "/"
+
+
+def _request(url, json_decode=True):
+ # pylint: disable=W0212
+ ssl._create_default_https_context = ssl._create_unverified_context
+ req = urllib.request.Request(url)
+
+ try:
+ result = urllib.request.urlopen(req)
+ except urllib.error.HTTPError as e:
+ raise RuntimeError("HTTP error ({})".format(str(e.code)))
+ except urllib.error.URLError as e:
+ raise RuntimeError("URL error ({})".format(str(e.reason)))
+
+ data = result.read()
+ result.close()
+
+ if json_decode:
+ try:
+ return json.JSONDecoder().decode(data.decode())
+ except Exception as e:
+ raise RuntimeError("API response has invalid JSON format ({})"
+ .format(str(e.reason)))
+
+ return data.decode()
+
+
+def _download(url, path):
+ try:
+ urllib.request.urlretrieve(url, path)
+ except urllib.error.HTTPError as e:
+ raise RuntimeError("HTTP error ({})".format(str(e.code)))
+ except urllib.error.URLError as e:
+ raise RuntimeError("URL error ({})".format(str(e.reason)))
+
+
+def _make_workspace_path(addon_dir):
+ return addon_dir + get_separator() + "addon_updator_workspace"
+
+
+def _make_workspace(addon_dir):
+ dir_path = _make_workspace_path(addon_dir)
+ os.mkdir(dir_path)
+
+
+def _make_temp_addon_path(addon_dir, url):
+ filename = url.split("/")[-1]
+ filepath = _make_workspace_path(addon_dir) + get_separator() + filename
+ return filepath
+
+
+def _download_addon(addon_dir, url):
+ filepath = _make_temp_addon_path(addon_dir, url)
+ _download(url, filepath)
+
+
+def _replace_addon(addon_dir, info, current_addon_path, offset_path=""):
+ # remove current add-on
+ if os.path.isfile(current_addon_path):
+ os.remove(current_addon_path)
+ elif os.path.isdir(current_addon_path):
+ shutil.rmtree(current_addon_path)
+
+ # replace to the new add-on
+ workspace_path = _make_workspace_path(addon_dir)
+ tmp_addon_path = _make_temp_addon_path(addon_dir, info.url)
+ _, ext = os.path.splitext(tmp_addon_path)
+ if ext == ".zip":
+ with zipfile.ZipFile(tmp_addon_path) as zf:
+ zf.extractall(workspace_path)
+ if offset_path != "":
+ src = workspace_path + get_separator() + offset_path
+ dst = addon_dir
+ shutil.move(src, dst)
+ elif ext == ".py":
+ shutil.move(tmp_addon_path, addon_dir)
+ else:
+ raise RuntimeError("Unsupported file extension. (ext: {})".format(ext))
+
+
+def _get_all_releases_data(owner, repository):
+ url = "https://api.github.com/repos/{}/{}/releases"\
+ .format(owner, repository)
+ data = _request(url)
+
+ return data
+
+
+def _get_all_branches_data(owner, repository):
+ url = "https://api.github.com/repos/{}/{}/branches"\
+ .format(owner, repository)
+ data = _request(url)
+
+ return data
+
+
+def _parse_release_version(version):
+ return [int(c) for c in version[1:].split(".")]
+
+
+# ver1 > ver2 : > 0
+# ver1 == ver2 : == 0
+# ver1 < ver2 : < 0
+def _compare_version(ver1, ver2):
+ if len(ver1) < len(ver2):
+ ver1.extend([-1 for _ in range(len(ver2) - len(ver1))])
+ elif len(ver1) > len(ver2):
+ ver2.extend([-1 for _ in range(len(ver1) - len(ver2))])
+
+ def comp(v1, v2, idx):
+ if len(v1) == idx:
+ return 0 # v1 == v2
+
+ if v1[idx] > v2[idx]:
+ return 1 # v1 > v2
+ elif v1[idx] < v2[idx]:
+ return -1 # v1 < v2
+
+ return comp(v1, v2, idx + 1)
+
+ return comp(ver1, ver2, 0)
+
+
+class AddonUpdatorConfig:
+ def __init__(self):
+ # Name of owner
+ self.owner = ""
+
+ # Name of repository
+ self.repository = ""
+
+ # Additional branch for update candidate
+ self.branches = []
+
+ # Set minimum release version for update candidate.
+ # e.g. (5, 2) if your release tag name is "v5.2"
+ # If you specify (-1, -1), ignore versions less than current add-on
+ # version specified in bl_info.
+ self.min_release_version = (-1, -1)
+
+ # Target add-on path
+ self.target_addon_path = ""
+
+ # Current add-on path
+ self.current_addon_path = ""
+
+ # Blender add-on directory
+ self.addon_directory = ""
+
+
+class UpdateCandidateInfo:
+ def __init__(self):
+ self.name = ""
+ self.url = ""
+ self.group = "" # BRANCH|RELEASE
+
+
+class AddonUpdatorManager:
+ __inst = None
+ __lock = Lock()
+
+ __initialized = False
+ __bl_info = None
+ __config = None
+ __update_candidate = []
+ __candidate_checked = False
+ __error = ""
+ __info = ""
+
+ def __init__(self):
+ raise NotImplementedError("Not allowed to call constructor")
+
+ @classmethod
+ def __internal_new(cls):
+ return super().__new__(cls)
+
+ @classmethod
+ def get_instance(cls):
+ if not cls.__inst:
+ with cls.__lock:
+ if not cls.__inst:
+ cls.__inst = cls.__internal_new()
+
+ return cls.__inst
+
+ def init(self, bl_info, config):
+ self.__bl_info = bl_info
+ self.__config = config
+ self.__update_candidate = []
+ self.__candidate_checked = False
+ self.__error = ""
+ self.__info = ""
+ self.__initialized = True
+
+ def initialized(self):
+ return self.__initialized
+
+ def candidate_checked(self):
+ return self.__candidate_checked
+
+ def check_update_candidate(self):
+ if not self.initialized():
+ raise RuntimeError("AddonUpdatorManager must be initialized")
+
+ self.__update_candidate = []
+ self.__candidate_checked = False
+
+ try:
+ # setup branch information
+ branches = _get_all_branches_data(self.__config.owner,
+ self.__config.repository)
+ for b in branches:
+ if b["name"] in self.__config.branches:
+ info = UpdateCandidateInfo()
+ info.name = b["name"]
+ info.url = "https://github.com/{}/{}/archive/{}.zip"\
+ .format(self.__config.owner,
+ self.__config.repository, b["name"])
+ info.group = 'BRANCH'
+ self.__update_candidate.append(info)
+
+ # setup release information
+ releases = _get_all_releases_data(self.__config.owner,
+ self.__config.repository)
+ for r in releases:
+ if _compare_version(_parse_release_version(r["tag_name"]),
+ self.__config.min_release_version) > 0:
+ info = UpdateCandidateInfo()
+ info.name = r["tag_name"]
+ info.url = r["assets"][0]["browser_download_url"]
+ info.group = 'RELEASE'
+ self.__update_candidate.append(info)
+ except RuntimeError as e:
+ self.__error = "Failed to check update {}. ({})"\
+ .format(str(e), datetime.datetime.now())
+
+ self.__info = "Checked update. ({})"\
+ .format(datetime.datetime.now())
+
+ self.__candidate_checked = True
+
+ def has_error(self):
+ return self.__error != ""
+
+ def error(self):
+ return self.__error
+
+ def has_info(self):
+ return self.__info != ""
+
+ def info(self):
+ return self.__info
+
+ def update(self, version_name):
+ if not self.initialized():
+ raise RuntimeError("AddonUpdatorManager must be initialized.")
+
+ if not self.candidate_checked():
+ raise RuntimeError("Update candidate is not checked.")
+
+ info = None
+ for info in self.__update_candidate:
+ if info.name == version_name:
+ break
+ else:
+ raise RuntimeError("{} is not found in update candidate"
+ .format(version_name))
+
+ if info is None:
+ raise RuntimeError("Not found any update candidates")
+
+ try:
+ # create workspace
+ _make_workspace(self.__config.addon_directory)
+ # download add-on
+ _download_addon(self.__config.addon_directory, info.url)
+
+ # replace add-on
+ offset_path = ""
+ if info.group == 'BRANCH':
+ offset_path = "{}-{}{}{}".format(
+ self.__config.repository, info.name, get_separator(),
+ self.__config.target_addon_path)
+ elif info.group == 'RELEASE':
+ offset_path = self.__config.target_addon_path
+ _replace_addon(self.__config.addon_directory,
+ info, self.__config.current_addon_path,
+ offset_path)
+
+ self.__info = "Updated to {}. ({})" \
+ .format(info.name, datetime.datetime.now())
+ except RuntimeError as e:
+ self.__error = "Failed to update {}. ({})"\
+ .format(str(e), datetime.datetime.now())
+
+ shutil.rmtree(_make_workspace_path(self.__config.addon_directory))
+
+ def get_candidate_branch_names(self):
+ if not self.initialized():
+ raise RuntimeError("AddonUpdatorManager must be initialized.")
+
+ if not self.candidate_checked():
+ raise RuntimeError("Update candidate is not checked.")
+
+ return [info.name for info in self.__update_candidate]
+
+ def latest_version(self):
+ release_versions = [info.name
+ for info in self.__update_candidate
+ if info.group == 'RELEASE']
+
+ latest = ""
+ for version in release_versions:
+ if latest == "":
+ latest = version
+ elif _compare_version(_parse_release_version(version),
+ _parse_release_version(latest)) > 0:
+ latest = version
+
+ return latest
diff --git a/magic_uv/utils/bl_class_registry.py b/magic_uv/utils/bl_class_registry.py
new file mode 100644
index 00000000..81e4b770
--- /dev/null
+++ b/magic_uv/utils/bl_class_registry.py
@@ -0,0 +1,80 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+
+from .. import common
+
+
+class BlClassRegistry:
+ class_list = []
+
+ def __init__(self, *_, **kwargs):
+ self.legacy = kwargs.get('legacy', False)
+
+ def __call__(self, cls):
+ if hasattr(cls, "bl_idname"):
+ BlClassRegistry.add_class(cls.bl_idname, cls, self.legacy)
+ else:
+ bl_idname = "{}{}{}{}".format(cls.bl_space_type,
+ cls.bl_region_type,
+ cls.bl_context, cls.bl_label)
+ BlClassRegistry.add_class(bl_idname, cls, self.legacy)
+ return cls
+
+ @classmethod
+ def add_class(cls, bl_idname, op_class, legacy):
+ for class_ in cls.class_list:
+ if (class_["bl_idname"] == bl_idname) and \
+ (class_["legacy"] == legacy):
+ raise RuntimeError("{} is already registered"
+ .format(bl_idname))
+
+ new_op = {
+ "bl_idname": bl_idname,
+ "class": op_class,
+ "legacy": legacy,
+ }
+ cls.class_list.append(new_op)
+ common.debug_print("{} is registered.".format(bl_idname))
+
+ @classmethod
+ def register(cls):
+ for class_ in cls.class_list:
+ bpy.utils.register_class(class_["class"])
+ common.debug_print("{} is registered to Blender."
+ .format(class_["bl_idname"]))
+
+ @classmethod
+ def unregister(cls):
+ for class_ in cls.class_list:
+ bpy.utils.unregister_class(class_["class"])
+ common.debug_print("{} is unregistered from Blender."
+ .format(class_["bl_idname"]))
+
+ @classmethod
+ def cleanup(cls):
+ cls.class_list = []
+ common.debug_print("Cleanup registry.")
diff --git a/magic_uv/utils/compatibility.py b/magic_uv/utils/compatibility.py
new file mode 100644
index 00000000..ffa1d8b4
--- /dev/null
+++ b/magic_uv/utils/compatibility.py
@@ -0,0 +1,189 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+import bpy
+import bgl
+import blf
+
+
+def check_version(major, minor, _):
+ """
+ Check blender version
+ """
+
+ if bpy.app.version[0] == major and bpy.app.version[1] == minor:
+ return 0
+ if bpy.app.version[0] > major:
+ return 1
+ if bpy.app.version[1] > minor:
+ return 1
+ return -1
+
+
+def make_annotations(cls):
+ if check_version(2, 80, 0) < 0:
+ return cls
+
+ # make annotation from attributes
+ props = {k: v for k, v in cls.__dict__.items() if isinstance(v, tuple)}
+ if props:
+ if '__annotations__' not in cls.__dict__:
+ setattr(cls, '__annotations__', {})
+ annotations = cls.__dict__['__annotations__']
+ for k, v in props.items():
+ annotations[k] = v
+ delattr(cls, k)
+
+ return cls
+
+
+class ChangeRegionType:
+ def __init__(self, *_, **kwargs):
+ self.region_type = kwargs.get('region_type', False)
+
+ def __call__(self, cls):
+ if check_version(2, 80, 0) >= 0:
+ return cls
+
+ cls.bl_region_type = self.region_type
+
+ return cls
+
+
+def matmul(m1, m2):
+ if check_version(2, 80, 0) < 0:
+ return m1 * m2
+
+ return m1 @ m2
+
+
+def layout_split(layout, factor=0.0, align=False):
+ if check_version(2, 80, 0) < 0:
+ return layout.split(percentage=factor, align=align)
+
+ return layout.split(factor=factor, align=align)
+
+
+def get_user_preferences(context):
+ if hasattr(context, "user_preferences"):
+ return context.user_preferences
+
+ return context.preferences
+
+
+def get_object_select(obj):
+ if check_version(2, 80, 0) < 0:
+ return obj.select
+
+ return obj.select_get()
+
+
+def set_active_object(obj):
+ if check_version(2, 80, 0) < 0:
+ bpy.context.scene.objects.active = obj
+ else:
+ bpy.context.view_layer.objects.active = obj
+
+
+def get_active_object(context):
+ if check_version(2, 80, 0) >= 0:
+ return context.scene.active_object
+ else:
+ return context.active_object
+
+
+def object_has_uv_layers(obj):
+ if check_version(2, 80, 0) < 0:
+ return hasattr(obj.data, "uv_textures")
+ else:
+ return hasattr(obj.data, "uv_layers")
+
+
+def get_object_uv_layers(obj):
+ if check_version(2, 80, 0) < 0:
+ return obj.data.uv_textures
+ else:
+ return obj.data.uv_layers
+
+
+def icon(icon):
+ if icon == 'IMAGE':
+ if check_version(2, 80, 0) < 0:
+ return 'IMAGE_COL'
+
+ return icon
+
+
+def set_blf_font_color(font_id, r, g, b, a):
+ if check_version(2, 80, 0) >= 0:
+ blf.color(font_id, r, g, b, a)
+ else:
+ bgl.glColor4f(r, g, b, a)
+
+
+def set_blf_blur(font_id, radius):
+ if check_version(2, 80, 0) < 0:
+ blf.blur(font_id, radius)
+
+
+def get_all_space_types():
+ if check_version(2, 80, 0) >= 0:
+ return {
+ 'CLIP_EDITOR': bpy.types.SpaceClipEditor,
+ 'CONSOLE': bpy.types.SpaceConsole,
+ 'DOPESHEET_EDITOR': bpy.types.SpaceDopeSheetEditor,
+ 'FILE_BROWSER': bpy.types.SpaceFileBrowser,
+ 'GRAPH_EDITOR': bpy.types.SpaceGraphEditor,
+ 'IMAGE_EDITOR': bpy.types.SpaceImageEditor,
+ 'INFO': bpy.types.SpaceInfo,
+ 'NLA_EDITOR': bpy.types.SpaceNLA,
+ 'NODE_EDITOR': bpy.types.SpaceNodeEditor,
+ 'OUTLINER': bpy.types.SpaceOutliner,
+ 'PROPERTIES': bpy.types.SpaceProperties,
+ 'SEQUENCE_EDITOR': bpy.types.SpaceSequenceEditor,
+ 'TEXT_EDITOR': bpy.types.SpaceTextEditor,
+ 'USER_PREFERENCES': bpy.types.SpacePreferences,
+ 'VIEW_3D': bpy.types.SpaceView3D,
+ }
+ else:
+ return {
+ 'VIEW_3D': bpy.types.SpaceView3D,
+ 'TIMELINE': bpy.types.SpaceTimeline,
+ 'GRAPH_EDITOR': bpy.types.SpaceGraphEditor,
+ 'DOPESHEET_EDITOR': bpy.types.SpaceDopeSheetEditor,
+ 'NLA_EDITOR': bpy.types.SpaceNLA,
+ 'IMAGE_EDITOR': bpy.types.SpaceImageEditor,
+ 'SEQUENCE_EDITOR': bpy.types.SpaceSequenceEditor,
+ 'CLIP_EDITOR': bpy.types.SpaceClipEditor,
+ 'TEXT_EDITOR': bpy.types.SpaceTextEditor,
+ 'NODE_EDITOR': bpy.types.SpaceNodeEditor,
+ 'LOGIC_EDITOR': bpy.types.SpaceLogicEditor,
+ 'PROPERTIES': bpy.types.SpaceProperties,
+ 'OUTLINER': bpy.types.SpaceOutliner,
+ 'USER_PREFERENCES': bpy.types.SpaceUserPreferences,
+ 'INFO': bpy.types.SpaceInfo,
+ 'FILE_BROWSER': bpy.types.SpaceFileBrowser,
+ 'CONSOLE': bpy.types.SpaceConsole,
+ }
diff --git a/magic_uv/utils/property_class_registry.py b/magic_uv/utils/property_class_registry.py
new file mode 100644
index 00000000..e99cd28b
--- /dev/null
+++ b/magic_uv/utils/property_class_registry.py
@@ -0,0 +1,68 @@
+# <pep8-80 compliant>
+
+# ##### BEGIN GPL LICENSE BLOCK #####
+#
+# This program is free software; you can redistribute it and/or
+# modify it under the terms of the GNU General Public License
+# as published by the Free Software Foundation; either version 2
+# of the License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software Foundation,
+# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+__author__ = "Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "6.0"
+__date__ = "26 Jan 2019"
+
+from .. import common
+
+
+class PropertyClassRegistry:
+ class_list = []
+
+ def __init__(self, *_, **kwargs):
+ self.legacy = kwargs.get('legacy', False)
+
+ def __call__(self, cls):
+ PropertyClassRegistry.add_class(cls.idname, cls, self.legacy)
+ return cls
+
+ @classmethod
+ def add_class(cls, idname, prop_class, legacy):
+ for class_ in cls.class_list:
+ if (class_["idname"] == idname) and (class_["legacy"] == legacy):
+ raise RuntimeError("{} is already registered".format(idname))
+
+ new_op = {
+ "idname": idname,
+ "class": prop_class,
+ "legacy": legacy,
+ }
+ cls.class_list.append(new_op)
+ common.debug_print("{} is registered.".format(idname))
+
+ @classmethod
+ def init_props(cls, scene):
+ for class_ in cls.class_list:
+ class_["class"].init_props(scene)
+ common.debug_print("{} is initialized.".format(class_["idname"]))
+
+ @classmethod
+ def del_props(cls, scene):
+ for class_ in cls.class_list:
+ class_["class"].del_props(scene)
+ common.debug_print("{} is cleared.".format(class_["idname"]))
+
+ @classmethod
+ def cleanup(cls):
+ cls.class_list = []
+ common.debug_print("Cleanup registry.")