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-06 04:27:27 +0300
committernutti <nutti.metro@gmail.com>2019-01-06 04:27:27 +0300
commita0044523a66f693c3b8005bc351120c87b5c8c34 (patch)
tree705b02934a2513bf6f918de536c060e569afd9e2 /uv_magic_uv
parent40340832e3992f46034b470aef1af6f7a3a4410f (diff)
Magic UV: Phase 2 for porting to Blender 2.8
All features are available on Blender 2.8
Diffstat (limited to 'uv_magic_uv')
-rw-r--r--uv_magic_uv/__init__.py38
-rw-r--r--uv_magic_uv/addon_updater.py1501
-rw-r--r--uv_magic_uv/addon_updater_ops.py1357
-rw-r--r--uv_magic_uv/common.py180
-rw-r--r--uv_magic_uv/impl/__init__.py70
-rw-r--r--uv_magic_uv/impl/align_uv_cursor_impl.py239
-rw-r--r--uv_magic_uv/impl/align_uv_impl.py820
-rw-r--r--uv_magic_uv/impl/move_uv_impl.py2
-rw-r--r--uv_magic_uv/impl/pack_uv_impl.py202
-rw-r--r--uv_magic_uv/impl/preserve_uv_aspect_impl.py359
-rw-r--r--uv_magic_uv/impl/select_uv_impl.py120
-rw-r--r--uv_magic_uv/impl/smooth_uv_impl.py215
-rw-r--r--uv_magic_uv/impl/texture_lock_impl.py455
-rw-r--r--uv_magic_uv/impl/texture_projection_impl.py126
-rw-r--r--uv_magic_uv/impl/texture_wrap_impl.py236
-rw-r--r--uv_magic_uv/impl/unwrap_constraint_impl.py98
-rw-r--r--uv_magic_uv/impl/uv_bounding_box_impl.py55
-rw-r--r--uv_magic_uv/impl/uv_inspection_impl.py70
-rw-r--r--uv_magic_uv/impl/uv_sculpt_impl.py57
-rw-r--r--uv_magic_uv/impl/uvw_impl.py8
-rw-r--r--uv_magic_uv/impl/world_scale_uv_impl.py383
-rw-r--r--uv_magic_uv/legacy/op/align_uv.py791
-rw-r--r--uv_magic_uv/legacy/op/align_uv_cursor.py126
-rw-r--r--uv_magic_uv/legacy/op/pack_uv.py164
-rw-r--r--uv_magic_uv/legacy/op/preserve_uv_aspect.py171
-rw-r--r--uv_magic_uv/legacy/op/select_uv.py100
-rw-r--r--uv_magic_uv/legacy/op/smooth_uv.py196
-rw-r--r--uv_magic_uv/legacy/op/texture_lock.py429
-rw-r--r--uv_magic_uv/legacy/op/texture_projection.py124
-rw-r--r--uv_magic_uv/legacy/op/texture_wrap.py212
-rw-r--r--uv_magic_uv/legacy/op/unwrap_constraint.py81
-rw-r--r--uv_magic_uv/legacy/op/uv_bounding_box.py58
-rw-r--r--uv_magic_uv/legacy/op/uv_inspection.py63
-rw-r--r--uv_magic_uv/legacy/op/uv_sculpt.py83
-rw-r--r--uv_magic_uv/legacy/op/world_scale_uv.py362
-rw-r--r--uv_magic_uv/legacy/preferences.py52
-rw-r--r--uv_magic_uv/lib/__init__.py32
-rw-r--r--uv_magic_uv/lib/bglx.py191
-rw-r--r--uv_magic_uv/op/__init__.py28
-rw-r--r--uv_magic_uv/op/align_uv.py231
-rw-r--r--uv_magic_uv/op/align_uv_cursor.py141
-rw-r--r--uv_magic_uv/op/pack_uv.py129
-rw-r--r--uv_magic_uv/op/preserve_uv_aspect.py124
-rw-r--r--uv_magic_uv/op/select_uv.py92
-rw-r--r--uv_magic_uv/op/smooth_uv.py105
-rw-r--r--uv_magic_uv/op/texture_lock.py158
-rw-r--r--uv_magic_uv/op/texture_projection.py292
-rw-r--r--uv_magic_uv/op/texture_wrap.py113
-rw-r--r--uv_magic_uv/op/unwrap_constraint.py125
-rw-r--r--uv_magic_uv/op/uv_bounding_box.py813
-rw-r--r--uv_magic_uv/op/uv_inspection.py235
-rw-r--r--uv_magic_uv/op/uv_sculpt.py446
-rw-r--r--uv_magic_uv/op/world_scale_uv.py360
-rw-r--r--uv_magic_uv/preferences.py360
-rw-r--r--uv_magic_uv/ui/IMAGE_MT_uvs.py141
-rw-r--r--uv_magic_uv/ui/VIEW3D_MT_uv_map.py148
-rw-r--r--uv_magic_uv/ui/__init__.py4
-rw-r--r--uv_magic_uv/ui/uvedit_editor_enhancement.py149
-rw-r--r--uv_magic_uv/ui/uvedit_uv_manipulation.py130
-rw-r--r--uv_magic_uv/ui/view3d_uv_manipulation.py218
-rw-r--r--uv_magic_uv/ui/view3d_uv_mapping.py47
-rw-r--r--uv_magic_uv/utils/__init__.py2
-rw-r--r--uv_magic_uv/utils/addon_updator.py345
63 files changed, 8982 insertions, 5780 deletions
diff --git a/uv_magic_uv/__init__.py b/uv_magic_uv/__init__.py
index 63591526..61fcc804 100644
--- a/uv_magic_uv/__init__.py
+++ b/uv_magic_uv/__init__.py
@@ -27,8 +27,9 @@ __date__ = "17 Nov 2018"
bl_info = {
"name": "Magic UV",
"author": "Nutti, Mifth, Jace Priester, kgeogeo, mem, imdjs"
- "Keith (Wahooney) Boshoff, McBuff, MaxRobinot, Alexander Milovsky",
- "version": (5, 3, 0),
+ "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",
@@ -60,56 +61,69 @@ if "bpy" in locals():
importlib.reload(utils)
utils.bl_class_registry.BlClassRegistry.cleanup()
if check_version(2, 80, 0) >= 0:
+ importlib.reload(lib)
importlib.reload(op)
importlib.reload(ui)
importlib.reload(properites)
importlib.reload(preferences)
- importlib.reload(addon_updater_ops)
- importlib.reload(addon_updater)
else:
importlib.reload(legacy)
+ importlib.reload(impl)
else:
import bpy
from . import common
from . import utils
if check_version(2, 80, 0) >= 0:
+ from . import lib
from . import op
from . import ui
from . import properites
from . import preferences
- from . import addon_updater_ops
- from . import addon_updater
else:
from . import legacy
+ from . import impl
+import os
import bpy
+def register_updater(bl_info):
+ config = utils.addon_updator.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("/")]
+ #config.min_release_version = bl_info["version"]
+ config.min_release_version = (5, 1)
+ config.target_addon_path = "uv_magic_uv"
+ updater = utils.addon_updator.AddonUpdatorManager.get_instance()
+ updater.init(bl_info, config)
+
+
def register():
+ register_updater(bl_info)
+
if common.check_version(2, 80, 0) >= 0:
utils.bl_class_registry.BlClassRegistry.register()
properites.init_props(bpy.types.Scene)
- if preferences.Preferences.enable_builtin_menu:
+ if bpy.context.user_preferences.addons['uv_magic_uv'].preferences.enable_builtin_menu:
preferences.add_builtin_menu()
else:
utils.bl_class_registry.BlClassRegistry.register()
legacy.properites.init_props(bpy.types.Scene)
if legacy.preferences.Preferences.enable_builtin_menu:
legacy.preferences.add_builtin_menu()
- if not common.is_console_mode():
- addon_updater_ops.register(bl_info)
def unregister():
if common.check_version(2, 80, 0) >= 0:
- if preferences.Preferences.enable_builtin_menu:
+ if bpy.context.user_preferences.addons['uv_magic_uv'].preferences.enable_builtin_menu:
preferences.remove_builtin_menu()
properites.clear_props(bpy.types.Scene)
utils.bl_class_registry.BlClassRegistry.unregister()
else:
- if not common.is_console_mode():
- addon_updater_ops.unregister()
if legacy.preferences.Preferences.enable_builtin_menu:
legacy.preferences.remove_builtin_menu()
legacy.properites.clear_props(bpy.types.Scene)
diff --git a/uv_magic_uv/addon_updater.py b/uv_magic_uv/addon_updater.py
deleted file mode 100644
index 70b6a287..00000000
--- a/uv_magic_uv/addon_updater.py
+++ /dev/null
@@ -1,1501 +0,0 @@
-# ##### 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 #####
-
-
-"""
-See documentation for usage
-https://github.com/CGCookie/blender-addon-updater
-
-"""
-
-import ssl
-import urllib.request
-import urllib
-import os
-import json
-import zipfile
-import shutil
-import asyncio
-import threading
-import time
-import fnmatch
-from datetime import datetime, timedelta
-
-# blender imports, used in limited cases
-import bpy
-import addon_utils
-
-# -----------------------------------------------------------------------------
-# Define error messages/notices & hard coded globals
-# -----------------------------------------------------------------------------
-
-# currently not used
-DEFAULT_TIMEOUT = 10
-DEFAULT_PER_PAGE = 30
-
-
-# -----------------------------------------------------------------------------
-# The main class
-# -----------------------------------------------------------------------------
-
-class Singleton_updater(object):
- """
- This is the singleton class to reference a copy from,
- it is the shared module level class
- """
- def __init__(self):
-
- self._engine = GithubEngine()
- self._user = None
- self._repo = None
- self._website = None
- self._current_version = None
- self._subfolder_path = None
- self._tags = []
- self._tag_latest = None
- self._tag_names = []
- self._latest_release = None
- self._use_releases = False
- self._include_branches = False
- self._include_branch_list = ['master']
- self._include_branch_autocheck = False
- self._manual_only = False
- self._version_min_update = None
- self._version_max_update = None
-
- # by default, backup current addon if new is being loaded
- self._backup_current = True
- self._backup_ignore_patterns = None
-
- # set patterns for what files to overwrite on update
- self._overwrite_patterns = ["*.py","*.pyc"]
- self._remove_pre_update_patterns = []
-
- # by default, don't auto enable/disable the addon on update
- # as it is slightly less stable/won't always fully reload module
- self._auto_reload_post_update = False
-
- # settings relating to frequency and whether to enable auto background check
- self._check_interval_enable = False
- self._check_interval_months = 0
- self._check_interval_days = 7
- self._check_interval_hours = 0
- self._check_interval_minutes = 0
-
- # runtime variables, initial conditions
- self._verbose = False
- self._fake_install = False
- self._async_checking = False # only true when async daemon started
- self._update_ready = None
- self._update_link = None
- self._update_version = None
- self._source_zip = None
- self._check_thread = None
- self.skip_tag = None
- self.select_link = None
-
- # get from module data
- self._addon = __package__.lower()
- self._addon_package = __package__ # must not change
- self._updater_path = os.path.join(os.path.dirname(__file__),
- self._addon+"_updater")
- self._addon_root = os.path.dirname(__file__)
- self._json = {}
- self._error = None
- self._error_msg = None
- self._prefiltered_tag_count = 0
-
- # UI code only, ie not used within this module but still useful
- # properties to have
-
- # to verify a valid import, in place of placeholder import
- self.showpopups = True # used in UI to show or not show update popups
- self.invalidupdater = False
-
-
- # -------------------------------------------------------------------------
- # Getters and setters
- # -------------------------------------------------------------------------
-
- @property
- def engine(self):
- return self._engine.name
- @engine.setter
- def engine(self, value):
- if value.lower()=="github":
- self._engine = GithubEngine()
- elif value.lower()=="gitlab":
- self._engine = GitlabEngine()
- elif value.lower()=="bitbucket":
- self._engine = BitbucketEngine()
- else:
- raise ValueError("Invalid engine selection")
-
- @property
- def private_token(self):
- return self._engine.token
- @private_token.setter
- def private_token(self, value):
- if value==None:
- self._engine.token = None
- else:
- self._engine.token = str(value)
-
- @property
- def addon(self):
- return self._addon
- @addon.setter
- def addon(self, value):
- self._addon = str(value)
-
- @property
- def verbose(self):
- return self._verbose
- @verbose.setter
- def verbose(self, value):
- try:
- self._verbose = bool(value)
- if self._verbose == True:
- print(self._addon+" updater verbose is enabled")
- except:
- raise ValueError("Verbose must be a boolean value")
-
- @property
- def include_branches(self):
- return self._include_branches
- @include_branches.setter
- def include_branches(self, value):
- try:
- self._include_branches = bool(value)
- except:
- raise ValueError("include_branches must be a boolean value")
-
- @property
- def use_releases(self):
- return self._use_releases
- @use_releases.setter
- def use_releases(self, value):
- try:
- self._use_releases = bool(value)
- except:
- raise ValueError("use_releases must be a boolean value")
-
- @property
- def include_branch_list(self):
- return self._include_branch_list
- @include_branch_list.setter
- def include_branch_list(self, value):
- try:
- if value == None:
- self._include_branch_list = ['master']
- elif type(value) != type(['master']) or value==[]:
- raise ValueError("include_branch_list should be a list of valid branches")
- else:
- self._include_branch_list = value
- except:
- raise ValueError("include_branch_list should be a list of valid branches")
-
- @property
- def overwrite_patterns(self):
- return self._overwrite_patterns
- @overwrite_patterns.setter
- def overwrite_patterns(self, value):
- if value == None:
- self._overwrite_patterns = ["*.py","*.pyc"]
- elif type(value) != type(['']):
- raise ValueError("overwrite_patterns needs to be in a list format")
- else:
- self._overwrite_patterns = value
-
- @property
- def remove_pre_update_patterns(self):
- return self._remove_pre_update_patterns
- @remove_pre_update_patterns.setter
- def remove_pre_update_patterns(self, value):
- if value == None:
- self._remove_pre_update_patterns = []
- elif type(value) != type(['']):
- raise ValueError("remove_pre_update_patterns needs to be in a list format")
- else:
- self._remove_pre_update_patterns = value
-
- # not currently used
- @property
- def include_branch_autocheck(self):
- return self._include_branch_autocheck
- @include_branch_autocheck.setter
- def include_branch_autocheck(self, value):
- try:
- self._include_branch_autocheck = bool(value)
- except:
- raise ValueError("include_branch_autocheck must be a boolean value")
-
- @property
- def manual_only(self):
- return self._manual_only
- @manual_only.setter
- def manual_only(self, value):
- try:
- self._manual_only = bool(value)
- except:
- raise ValueError("manual_only must be a boolean value")
-
- @property
- def auto_reload_post_update(self):
- return self._auto_reload_post_update
- @auto_reload_post_update.setter
- def auto_reload_post_update(self, value):
- try:
- self._auto_reload_post_update = bool(value)
- except:
- raise ValueError("Must be a boolean value")
-
- @property
- def fake_install(self):
- return self._fake_install
- @fake_install.setter
- def fake_install(self, value):
- if type(value) != type(False):
- raise ValueError("fake_install must be a boolean value")
- self._fake_install = bool(value)
-
- @property
- def user(self):
- return self._user
- @user.setter
- def user(self, value):
- try:
- self._user = str(value)
- except:
- raise ValueError("User must be a string value")
-
- @property
- def json(self):
- if self._json == {}:
- self.set_updater_json()
- return self._json
-
- @property
- def repo(self):
- return self._repo
- @repo.setter
- def repo(self, value):
- try:
- self._repo = str(value)
- except:
- raise ValueError("User must be a string")
-
- @property
- def website(self):
- return self._website
- @website.setter
- def website(self, value):
- if self.check_is_url(value) == False:
- raise ValueError("Not a valid URL: " + value)
- self._website = value
-
- @property
- def async_checking(self):
- return self._async_checking
-
- @property
- def api_url(self):
- return self._engine.api_url
- @api_url.setter
- def api_url(self, value):
- if self.check_is_url(value) == False:
- raise ValueError("Not a valid URL: " + value)
- self._engine.api_url = value
-
- @property
- def stage_path(self):
- return self._updater_path
- @stage_path.setter
- def stage_path(self, value):
- if value == None:
- if self._verbose: print("Aborting assigning stage_path, it's null")
- return
- elif value != None and not os.path.exists(value):
- try:
- os.makedirs(value)
- except:
- if self._verbose: print("Error trying to staging path")
- return
- self._updater_path = value
-
- @property
- def tags(self):
- if self._tags == []:
- return []
- tag_names = []
- for tag in self._tags:
- tag_names.append(tag["name"])
- return tag_names
-
- @property
- def tag_latest(self):
- if self._tag_latest == None:
- return None
- return self._tag_latest["name"]
-
- @property
- def latest_release(self):
- if self._releases_latest == None:
- return None
- return self._latest_release
-
- @property
- def current_version(self):
- return self._current_version
-
- @property
- def subfolder_path(self):
- return self._subfolder_path
-
- @subfolder_path.setter
- def subfolder_path(self, value):
- self._subfolder_path = value
-
- @property
- def update_ready(self):
- return self._update_ready
-
- @property
- def update_version(self):
- return self._update_version
-
- @property
- def update_link(self):
- return self._update_link
-
- @current_version.setter
- def current_version(self, tuple_values):
- if tuple_values==None:
- self._current_version = None
- return
- elif type(tuple_values) is not tuple:
- try:
- tuple(tuple_values)
- except:
- raise ValueError(
- "Not a tuple! current_version must be a tuple of integers")
- for i in tuple_values:
- if type(i) is not int:
- raise ValueError(
- "Not an integer! current_version must be a tuple of integers")
- self._current_version = tuple(tuple_values)
-
- def set_check_interval(self,enable=False,months=0,days=14,hours=0,minutes=0):
- # enabled = False, default initially will not check against frequency
- # if enabled, default is then 2 weeks
-
- if type(enable) is not bool:
- raise ValueError("Enable must be a boolean value")
- if type(months) is not int:
- raise ValueError("Months must be an integer value")
- if type(days) is not int:
- raise ValueError("Days must be an integer value")
- if type(hours) is not int:
- raise ValueError("Hours must be an integer value")
- if type(minutes) is not int:
- raise ValueError("Minutes must be an integer value")
-
- if enable==False:
- self._check_interval_enable = False
- else:
- self._check_interval_enable = True
-
- self._check_interval_months = months
- self._check_interval_days = days
- self._check_interval_hours = hours
- self._check_interval_minutes = minutes
-
- @property
- def check_interval(self):
- return (self._check_interval_enable,
- self._check_interval_months,
- self._check_interval_days,
- self._check_interval_hours,
- self._check_interval_minutes)
-
- @property
- def error(self):
- return self._error
-
- @property
- def error_msg(self):
- return self._error_msg
-
- @property
- def version_min_update(self):
- return self._version_min_update
- @version_min_update.setter
- def version_min_update(self, value):
- if value == None:
- self._version_min_update = None
- return
- if type(value) != type((1,2,3)):
- raise ValueError("Version minimum must be a tuple")
- else:
- # potentially check entries are integers
- self._version_min_update = value
-
- @property
- def version_max_update(self):
- return self._version_max_update
- @version_max_update.setter
- def version_max_update(self, value):
- if value == None:
- self._version_max_update = None
- return
- if type(value) != type((1,2,3)):
- raise ValueError("Version maximum must be a tuple")
- else:
- # potentially check entries are integers
- self._version_max_update = value
-
- @property
- def backup_current(self):
- return self._backup_current
- @backup_current.setter
- def backup_current(self, value):
- if value == None:
- self._backup_current = False
- return
- else:
- self._backup_current = value
-
- @property
- def backup_ignore_patterns(self):
- return self._backup_ignore_patterns
- @backup_ignore_patterns.setter
- def backup_ignore_patterns(self, value):
- if value == None:
- self._backup_ignore_patterns = None
- return
- elif type(value) != type(['list']):
- raise ValueError("Backup pattern must be in list format")
- else:
- self._backup_ignore_patterns = value
-
- # -------------------------------------------------------------------------
- # Parameter validation related functions
- # -------------------------------------------------------------------------
-
-
- def check_is_url(self, url):
- if not ("http://" in url or "https://" in url):
- return False
- if "." not in url:
- return False
- return True
-
- def get_tag_names(self):
- tag_names = []
- self.get_tags(self)
- for tag in self._tags:
- tag_names.append(tag["name"])
- return tag_names
-
-
- # declare how the class gets printed
-
- def __repr__(self):
- return "<Module updater from {a}>".format(a=__file__)
-
- def __str__(self):
- return "Updater, with user: {a}, repository: {b}, url: {c}".format(
- a=self._user,
- b=self._repo, c=self.form_repo_url())
-
-
- # -------------------------------------------------------------------------
- # API-related functions
- # -------------------------------------------------------------------------
-
- def form_repo_url(self):
- return self._engine.form_repo_url(self)
-
- def form_tags_url(self):
- return self._engine.form_tags_url(self)
-
- def form_branch_url(self, branch):
- return self._engine.form_branch_url(branch, self)
-
- def get_tags(self):
- request = self.form_tags_url()
- if self._verbose: print("Getting tags from server")
-
- # get all tags, internet call
- all_tags = self._engine.parse_tags(self.get_api(request), self)
- if all_tags is not None:
- self._prefiltered_tag_count = len(all_tags)
- else:
- self._prefiltered_tag_count = 0
- all_tags = []
-
- # pre-process to skip tags
- if self.skip_tag != None:
- self._tags = [tg for tg in all_tags if self.skip_tag(self, tg)==False]
- else:
- self._tags = all_tags
-
- # get additional branches too, if needed, and place in front
- # Does NO checking here whether branch is valid
- if self._include_branches == True:
- temp_branches = self._include_branch_list.copy()
- temp_branches.reverse()
- for branch in temp_branches:
- request = self.form_branch_url(branch)
- include = {
- "name":branch.title(),
- "zipball_url":request
- }
- self._tags = [include] + self._tags # append to front
-
- if self._tags == None:
- # some error occurred
- self._tag_latest = None
- self._tags = []
- return
- elif self._prefiltered_tag_count == 0 and self._include_branches == False:
- self._tag_latest = None
- if self._error == None: # if not None, could have had no internet
- self._error = "No releases found"
- self._error_msg = "No releases or tags found on this repository"
- if self._verbose: print("No releases or tags found on this repository")
- elif self._prefiltered_tag_count == 0 and self._include_branches == True:
- if not self._error: self._tag_latest = self._tags[0]
- if self._verbose:
- branch = self._include_branch_list[0]
- print("{} branch found, no releases".format(branch), self._tags[0])
- elif (len(self._tags)-len(self._include_branch_list)==0 and self._include_branches==True) \
- or (len(self._tags)==0 and self._include_branches==False) \
- and self._prefiltered_tag_count > 0:
- self._tag_latest = None
- self._error = "No releases available"
- self._error_msg = "No versions found within compatible version range"
- if self._verbose: print("No versions found within compatible version range")
- else:
- if self._include_branches == False:
- self._tag_latest = self._tags[0]
- if self._verbose: print("Most recent tag found:",self._tags[0]['name'])
- else:
- # don't return branch if in list
- n = len(self._include_branch_list)
- self._tag_latest = self._tags[n] # guaranteed at least len()=n+1
- if self._verbose: print("Most recent tag found:",self._tags[n]['name'])
-
-
- # all API calls to base url
- def get_raw(self, url):
- # print("Raw request:", url)
- request = urllib.request.Request(url)
- context = ssl._create_unverified_context()
-
- # setup private request headers if appropriate
- if self._engine.token != None:
- if self._engine.name == "gitlab":
- request.add_header('PRIVATE-TOKEN',self._engine.token)
- else:
- if self._verbose: print("Tokens not setup for engine yet")
-
- # run the request
- try:
- result = urllib.request.urlopen(request,context=context)
- except urllib.error.HTTPError as e:
- self._error = "HTTP error"
- self._error_msg = str(e.code)
- self._update_ready = None
- except urllib.error.URLError as e:
- reason = str(e.reason)
- if "TLSV1_ALERT" in reason or "SSL" in reason:
- self._error = "Connection rejected, download manually"
- self._error_msg = reason
- else:
- self._error = "URL error, check internet connection"
- self._error_msg = reason
- self._update_ready = None
- return None
- else:
- result_string = result.read()
- result.close()
- return result_string.decode()
-
-
- # result of all api calls, decoded into json format
- def get_api(self, url):
- # return the json version
- get = None
- get = self.get_raw(url)
- if get != None:
- try:
- return json.JSONDecoder().decode(get)
- except Exception as e:
- self._error = "API response has invalid JSON format"
- self._error_msg = str(e.reason)
- self._update_ready = None
- return None
- else:
- return None
-
-
- # create a working directory and download the new files
- def stage_repository(self, url):
-
- local = os.path.join(self._updater_path,"update_staging")
- error = None
-
- # make/clear the staging folder
- # ensure the folder is always "clean"
- if self._verbose: print("Preparing staging folder for download:\n",local)
- if os.path.isdir(local) == True:
- try:
- shutil.rmtree(local)
- os.makedirs(local)
- except:
- error = "failed to remove existing staging directory"
- else:
- try:
- os.makedirs(local)
- except:
- error = "failed to create staging directory"
-
- if error != None:
- if self._verbose: print("Error: Aborting update, "+error)
- self._error = "Update aborted, staging path error"
- self._error_msg = "Error: {}".format(error)
- return False
-
- if self._backup_current==True:
- self.create_backup()
- if self._verbose: print("Now retrieving the new source zip")
-
- self._source_zip = os.path.join(local,"source.zip")
-
- if self._verbose: print("Starting download update zip")
- try:
- request = urllib.request.Request(url)
- context = ssl._create_unverified_context()
-
- # setup private token if appropriate
- if self._engine.token != None:
- if self._engine.name == "gitlab":
- request.add_header('PRIVATE-TOKEN',self._engine.token)
- else:
- if self._verbose: print("Tokens not setup for selected engine yet")
- self.urlretrieve(urllib.request.urlopen(request,context=context), self._source_zip)
- # add additional checks on file size being non-zero
- if self._verbose: print("Successfully downloaded update zip")
- return True
- except Exception as e:
- self._error = "Error retrieving download, bad link?"
- self._error_msg = "Error: {}".format(e)
- if self._verbose:
- print("Error retrieving download, bad link?")
- print("Error: {}".format(e))
- return False
-
-
- def create_backup(self):
- if self._verbose: print("Backing up current addon folder")
- local = os.path.join(self._updater_path,"backup")
- tempdest = os.path.join(self._addon_root,
- os.pardir,
- self._addon+"_updater_backup_temp")
-
- if self._verbose: print("Backup destination path: ",local)
-
- if os.path.isdir(local):
- try:
- shutil.rmtree(local)
- except:
- if self._verbose:print("Failed to removed previous backup folder, contininuing")
-
- # remove the temp folder; shouldn't exist but could if previously interrupted
- if os.path.isdir(tempdest):
- try:
- shutil.rmtree(tempdest)
- except:
- if self._verbose:print("Failed to remove existing temp folder, contininuing")
- # make the full addon copy, which temporarily places outside the addon folder
- if self._backup_ignore_patterns != None:
- shutil.copytree(
- self._addon_root,tempdest,
- ignore=shutil.ignore_patterns(*self._backup_ignore_patterns))
- else:
- shutil.copytree(self._addon_root,tempdest)
- shutil.move(tempdest,local)
-
- # save the date for future ref
- now = datetime.now()
- self._json["backup_date"] = "{m}-{d}-{yr}".format(
- m=now.strftime("%B"),d=now.day,yr=now.year)
- self.save_updater_json()
-
- def restore_backup(self):
- if self._verbose: print("Restoring backup")
-
- if self._verbose: print("Backing up current addon folder")
- backuploc = os.path.join(self._updater_path,"backup")
- tempdest = os.path.join(self._addon_root,
- os.pardir,
- self._addon+"_updater_backup_temp")
- tempdest = os.path.abspath(tempdest)
-
- # make the copy
- shutil.move(backuploc,tempdest)
- shutil.rmtree(self._addon_root)
- os.rename(tempdest,self._addon_root)
-
- self._json["backup_date"] = ""
- self._json["just_restored"] = True
- self._json["just_updated"] = True
- self.save_updater_json()
-
- self.reload_addon()
-
- def unpack_staged_zip(self,clean=False):
-
- if os.path.isfile(self._source_zip) == False:
- if self._verbose: print("Error, update zip not found")
- return -1
-
- # clear the existing source folder in case previous files remain
- try:
- shutil.rmtree(os.path.join(self._updater_path,"source"))
- os.makedirs(os.path.join(self._updater_path,"source"))
- if self._verbose: print("Source folder cleared and recreated")
- except:
- pass
-
- if self._verbose: print("Begin extracting source")
- if zipfile.is_zipfile(self._source_zip):
- with zipfile.ZipFile(self._source_zip) as zf:
- # extractall is no longer a security hazard, below is safe
- zf.extractall(os.path.join(self._updater_path,"source"))
- else:
- if self._verbose:
- print("Not a zip file, future add support for just .py files")
- raise ValueError("Resulting file is not a zip")
- if self._verbose: print("Extracted source")
-
- # either directly in root of zip, or one folder level deep
- unpath = os.path.join(self._updater_path,"source")
- if os.path.isfile(os.path.join(unpath,"__init__.py")) == False:
- dirlist = os.listdir(unpath)
- if len(dirlist)>0:
- if self._subfolder_path == "" or self._subfolder_path == None:
- unpath = os.path.join(unpath,dirlist[0])
- else:
- unpath = os.path.join(unpath,dirlist[0],self._subfolder_path)
-
- # smarter check for additional sub folders for a single folder
- # containing __init__.py
- if os.path.isfile(os.path.join(unpath,"__init__.py")) == False:
- if self._verbose:
- print("not a valid addon found")
- print("Paths:")
- print(dirlist)
-
- raise ValueError("__init__ file not found in new source")
-
- # now commence merging in the two locations:
- # note this MAY not be accurate, as updater files could be placed elsewhere
- origpath = os.path.dirname(__file__)
-
- # merge code with running addon directory, using blender default behavior
- # plus any modifiers indicated by user (e.g. force remove/keep)
- self.deepMergeDirectory(origpath,unpath,clean)
-
- # Now save the json state
- # Change to True, to trigger the handler on other side
- # if allowing reloading within same blender instance
- self._json["just_updated"] = True
- self.save_updater_json()
- self.reload_addon()
- self._update_ready = False
-
-
- # merge folder 'merger' into folder 'base' without deleting existing
- def deepMergeDirectory(self,base,merger,clean=False):
- if not os.path.exists(base):
- if self._verbose: print("Base path does not exist")
- return -1
- elif not os.path.exists(merger):
- if self._verbose: print("Merger path does not exist")
- return -1
-
- # paths to be aware of and not overwrite/remove/etc
- staging_path = os.path.join(self._updater_path,"update_staging")
- backup_path = os.path.join(self._updater_path,"backup")
- json_path = os.path.join(self._updater_path,"updater_status.json")
-
- # If clean install is enabled, clear existing files ahead of time
- # note: will not delete the update.json, update folder, staging, or staging
- # but will delete all other folders/files in addon directory
- error = None
- if clean==True:
- try:
- # implement clearing of all folders/files, except the
- # updater folder and updater json
- # Careful, this deletes entire subdirectories recursively...
- # make sure that base is not a high level shared folder, but
- # is dedicated just to the addon itself
- if self._verbose: print("clean=True, clearing addon folder to fresh install state")
-
- # remove root files and folders (except update folder)
- files = [f for f in os.listdir(base) if os.path.isfile(os.path.join(base,f))]
- folders = [f for f in os.listdir(base) if os.path.isdir(os.path.join(base,f))]
-
- for f in files:
- os.remove(os.path.join(base,f))
- print("Clean removing file {}".format(os.path.join(base,f)))
- for f in folders:
- if os.path.join(base,f)==self._updater_path: continue
- shutil.rmtree(os.path.join(base,f))
- print("Clean removing folder and contents {}".format(os.path.join(base,f)))
-
- except error:
- error = "failed to create clean existing addon folder"
- print(error,str(e))
-
- # Walk through the base addon folder for rules on pre-removing
- # but avoid removing/altering backup and updater file
- for path, dirs, files in os.walk(base):
- # prune ie skip updater folder
- dirs[:] = [d for d in dirs if os.path.join(path,d) not in [self._updater_path]]
- for file in files:
- for ptrn in self.remove_pre_update_patterns:
- if fnmatch.filter([file],ptrn):
- try:
- fl = os.path.join(path,file)
- os.remove(fl)
- if self._verbose: print("Pre-removed file "+file)
- except OSError:
- print("Failed to pre-remove "+file)
-
- # Walk through the temp addon sub folder for replacements
- # this implements the overwrite rules, which apply after
- # the above pre-removal rules. This also performs the
- # actual file copying/replacements
- for path, dirs, files in os.walk(merger):
- # verify this structure works to prune updater sub folder overwriting
- dirs[:] = [d for d in dirs if os.path.join(path,d) not in [self._updater_path]]
- relPath = os.path.relpath(path, merger)
- destPath = os.path.join(base, relPath)
- if not os.path.exists(destPath):
- os.makedirs(destPath)
- for file in files:
- # bring in additional logic around copying/replacing
- # Blender default: overwrite .py's, don't overwrite the rest
- destFile = os.path.join(destPath, file)
- srcFile = os.path.join(path, file)
-
- # decide whether to replace if file already exists, and copy new over
- if os.path.isfile(destFile):
- # otherwise, check each file to see if matches an overwrite pattern
- replaced=False
- for ptrn in self._overwrite_patterns:
- if fnmatch.filter([destFile],ptrn):
- replaced=True
- break
- if replaced:
- os.remove(destFile)
- os.rename(srcFile, destFile)
- if self._verbose: print("Overwrote file "+os.path.basename(destFile))
- else:
- if self._verbose: print("Pattern not matched to "+os.path.basename(destFile)+", not overwritten")
- else:
- # file did not previously exist, simply move it over
- os.rename(srcFile, destFile)
- if self._verbose: print("New file "+os.path.basename(destFile))
-
- # now remove the temp staging folder and downloaded zip
- try:
- shutil.rmtree(staging_path)
- except:
- error = "Error: Failed to remove existing staging directory, consider manually removing "+staging_path
- if self._verbose: print(error)
-
-
- def reload_addon(self):
- # if post_update false, skip this function
- # else, unload/reload addon & trigger popup
- if self._auto_reload_post_update == False:
- print("Restart blender to reload addon and complete update")
- return
-
- if self._verbose: print("Reloading addon...")
- addon_utils.modules(refresh=True)
- bpy.utils.refresh_script_paths()
-
- # not allowed in restricted context, such as register module
- # toggle to refresh
- bpy.ops.wm.addon_disable(module=self._addon_package)
- bpy.ops.wm.addon_refresh()
- bpy.ops.wm.addon_enable(module=self._addon_package)
-
-
- # -------------------------------------------------------------------------
- # Other non-api functions and setups
- # -------------------------------------------------------------------------
-
- def clear_state(self):
- self._update_ready = None
- self._update_link = None
- self._update_version = None
- self._source_zip = None
- self._error = None
- self._error_msg = None
-
- # custom urlretrieve implementation
- def urlretrieve(self, urlfile, filepath):
- chunk = 1024*8
- f = open(filepath, "wb")
- while 1:
- data = urlfile.read(chunk)
- if not data:
- #print("done.")
- break
- f.write(data)
- #print("Read %s bytes"%len(data))
- f.close()
-
-
- def version_tuple_from_text(self,text):
- if text == None: return ()
-
- # should go through string and remove all non-integers,
- # and for any given break split into a different section
- segments = []
- tmp = ''
- for l in str(text):
- if l.isdigit()==False:
- if len(tmp)>0:
- segments.append(int(tmp))
- tmp = ''
- else:
- tmp+=l
- if len(tmp)>0:
- segments.append(int(tmp))
-
- if len(segments)==0:
- if self._verbose: print("No version strings found text: ",text)
- if self._include_branches == False:
- return ()
- else:
- return (text)
- return tuple(segments)
-
- # called for running check in a background thread
- def check_for_update_async(self, callback=None):
-
- if self._json != None and "update_ready" in self._json and self._json["version_text"]!={}:
- if self._json["update_ready"] == True:
- self._update_ready = True
- self._update_link = self._json["version_text"]["link"]
- self._update_version = str(self._json["version_text"]["version"])
- # cached update
- callback(True)
- return
-
- # do the check
- if self._check_interval_enable == False:
- return
- elif self._async_checking == True:
- if self._verbose: print("Skipping async check, already started")
- return # already running the bg thread
- elif self._update_ready == None:
- self.start_async_check_update(False, callback)
-
-
- def check_for_update_now(self, callback=None):
-
- self._error = None
- self._error_msg = None
-
- if self._verbose:
- print("Check update pressed, first getting current status")
- if self._async_checking == True:
- if self._verbose: print("Skipping async check, already started")
- return # already running the bg thread
- elif self._update_ready == None:
- self.start_async_check_update(True, callback)
- else:
- self._update_ready = None
- self.start_async_check_update(True, callback)
-
-
- # this function is not async, will always return in sequential fashion
- # but should have a parent which calls it in another thread
- def check_for_update(self, now=False):
- if self._verbose: print("Checking for update function")
-
- # clear the errors if any
- self._error = None
- self._error_msg = None
-
- # avoid running again in, just return past result if found
- # but if force now check, then still do it
- if self._update_ready != None and now == False:
- return (self._update_ready,self._update_version,self._update_link)
-
- if self._current_version == None:
- raise ValueError("current_version not yet defined")
- if self._repo == None:
- raise ValueError("repo not yet defined")
- if self._user == None:
- raise ValueError("username not yet defined")
-
- self.set_updater_json() # self._json
-
- if now == False and self.past_interval_timestamp()==False:
- if self._verbose:
- print("Aborting check for updated, check interval not reached")
- return (False, None, None)
-
- # check if using tags or releases
- # note that if called the first time, this will pull tags from online
- if self._fake_install == True:
- if self._verbose:
- print("fake_install = True, setting fake version as ready")
- self._update_ready = True
- self._update_version = "(999,999,999)"
- self._update_link = "http://127.0.0.1"
-
- return (self._update_ready, self._update_version, self._update_link)
-
- # primary internet call
- self.get_tags() # sets self._tags and self._tag_latest
-
- self._json["last_check"] = str(datetime.now())
- self.save_updater_json()
-
- # can be () or ('master') in addition to branches, and version tag
- new_version = self.version_tuple_from_text(self.tag_latest)
-
- if len(self._tags)==0:
- self._update_ready = False
- self._update_version = None
- self._update_link = None
- return (False, None, None)
- if self._include_branches == False:
- link = self.select_link(self, self._tags[0])
- else:
- n = len(self._include_branch_list)
- if len(self._tags)==n:
- # effectively means no tags found on repo
- # so provide the first one as default
- link = self.select_link(self, self._tags[0])
- else:
- link = self.select_link(self, self._tags[n])
-
- if new_version == ():
- self._update_ready = False
- self._update_version = None
- self._update_link = None
- return (False, None, None)
- elif str(new_version).lower() in self._include_branch_list:
- # handle situation where master/whichever branch is included
- # however, this code effectively is not triggered now
- # as new_version will only be tag names, not branch names
- if self._include_branch_autocheck == False:
- # don't offer update as ready,
- # but set the link for the default
- # branch for installing
- self._update_ready = True
- self._update_version = new_version
- self._update_link = link
- self.save_updater_json()
- return (True, new_version, link)
- else:
- raise ValueError("include_branch_autocheck: NOT YET DEVELOPED")
- # bypass releases and look at timestamp of last update
- # from a branch compared to now, see if commit values
- # match or not.
-
- else:
- # situation where branches not included
-
- if new_version > self._current_version:
-
- self._update_ready = True
- self._update_version = new_version
- self._update_link = link
- self.save_updater_json()
- return (True, new_version, link)
-
- # elif new_version != self._current_version:
- # self._update_ready = False
- # self._update_version = new_version
- # self._update_link = link
- # self.save_updater_json()
- # return (True, new_version, link)
-
- # if no update, set ready to False from None
- self._update_ready = False
- self._update_version = None
- self._update_link = None
- return (False, None, None)
-
-
- def set_tag(self,name):
- tg = None
- for tag in self._tags:
- if name == tag["name"]:
- tg = tag
- break
- if tg == None:
- raise ValueError("Version tag not found: "+revert_tag)
- new_version = self.version_tuple_from_text(self.tag_latest)
- self._update_version = new_version
- self._update_link = self.select_link(self, tg)
-
-
- def run_update(self,force=False,revert_tag=None,clean=False,callback=None):
- # revert_tag: could e.g. get from drop down list
- # different versions of the addon to revert back to
- # clean: not used, but in future could use to totally refresh addon
- self._json["update_ready"] = False
- self._json["ignore"] = False # clear ignore flag
- self._json["version_text"] = {}
-
- if revert_tag != None:
- self.set_tag(revert_tag)
- self._update_ready = True
-
- # clear the errors if any
- self._error = None
- self._error_msg = None
-
- if self._verbose: print("Running update")
-
- if self._fake_install == True:
- # change to True, to trigger the reload/"update installed" handler
- if self._verbose:
- print("fake_install=True")
- print("Just reloading and running any handler triggers")
- self._json["just_updated"] = True
- self.save_updater_json()
- if self._backup_current == True:
- self.create_backup()
- self.reload_addon()
- self._update_ready = False
- res = True # fake "success" zip download flag
-
- elif force==False:
- if self._update_ready != True:
- if self._verbose: print("Update stopped, new version not ready")
- return "Update stopped, new version not ready"
- elif self._update_link == None:
- # this shouldn't happen if update is ready
- if self._verbose: print("Update stopped, update link unavailable")
- return "Update stopped, update link unavailable"
-
- if self._verbose and revert_tag==None:
- print("Staging update")
- elif self._verbose:
- print("Staging install")
-
- res = self.stage_repository(self._update_link)
- if res !=True:
- print("Error in staging repository: "+str(res))
- if callback != None: callback(self._error_msg)
- return self._error_msg
- self.unpack_staged_zip(clean)
-
- else:
- if self._update_link == None:
- if self._verbose: print("Update stopped, could not get link")
- return "Update stopped, could not get link"
- if self._verbose: print("Forcing update")
-
- res = self.stage_repository(self._update_link)
- if res !=True:
- print("Error in staging repository: "+str(res))
- if callback != None: callback(self._error_msg)
- return self._error_msg
- self.unpack_staged_zip(clean)
- # would need to compare against other versions held in tags
-
- # run the front-end's callback if provided
- if callback != None: callback()
-
- # return something meaningful, 0 means it worked
- return 0
-
-
- def past_interval_timestamp(self):
- if self._check_interval_enable == False:
- return True # ie this exact feature is disabled
-
- if "last_check" not in self._json or self._json["last_check"] == "":
- return True
- else:
- now = datetime.now()
- last_check = datetime.strptime(self._json["last_check"],
- "%Y-%m-%d %H:%M:%S.%f")
- next_check = last_check
- offset = timedelta(
- days=self._check_interval_days + 30*self._check_interval_months,
- hours=self._check_interval_hours,
- minutes=self._check_interval_minutes
- )
-
- delta = (now - offset) - last_check
- if delta.total_seconds() > 0:
- if self._verbose:
- print("{} Updater: Time to check for updates!".format(self._addon))
- return True
- else:
- if self._verbose:
- print("{} Updater: Determined it's not yet time to check for updates".format(self._addon))
- return False
-
-
- def set_updater_json(self):
- if self._updater_path == None:
- raise ValueError("updater_path is not defined")
- elif os.path.isdir(self._updater_path) == False:
- os.makedirs(self._updater_path)
-
- jpath = os.path.join(self._updater_path,"updater_status.json")
- if os.path.isfile(jpath):
- with open(jpath) as data_file:
- self._json = json.load(data_file)
- if self._verbose: print("{} Updater: Read in json settings from file".format(self._addon))
- else:
- # set data structure
- self._json = {
- "last_check":"",
- "backup_date":"",
- "update_ready":False,
- "ignore":False,
- "just_restored":False,
- "just_updated":False,
- "version_text":{}
- }
- self.save_updater_json()
-
-
- def save_updater_json(self):
- # first save the state
- if self._update_ready == True:
- if type(self._update_version) == type((0,0,0)):
- self._json["update_ready"] = True
- self._json["version_text"]["link"]=self._update_link
- self._json["version_text"]["version"]=self._update_version
- else:
- self._json["update_ready"] = False
- self._json["version_text"] = {}
- else:
- self._json["update_ready"] = False
- self._json["version_text"] = {}
-
- jpath = os.path.join(self._updater_path,"updater_status.json")
- outf = open(jpath,'w')
- data_out = json.dumps(self._json, indent=4)
- outf.write(data_out)
- outf.close()
- if self._verbose:
- print(self._addon+": Wrote out updater json settings to file, with the contents:")
- print(self._json)
-
- def json_reset_postupdate(self):
- self._json["just_updated"] = False
- self._json["update_ready"] = False
- self._json["version_text"] = {}
- self.save_updater_json()
-
- def json_reset_restore(self):
- self._json["just_restored"] = False
- self._json["update_ready"] = False
- self._json["version_text"] = {}
- self.save_updater_json()
- self._update_ready = None # reset so you could check update again
-
- def ignore_update(self):
- self._json["ignore"] = True
- self.save_updater_json()
-
-
- # -------------------------------------------------------------------------
- # ASYNC stuff
- # -------------------------------------------------------------------------
-
- def start_async_check_update(self, now=False, callback=None):
- if self._async_checking == True:
- return
- if self._verbose: print("{} updater: Starting background checking thread".format(self._addon))
- check_thread = threading.Thread(target=self.async_check_update,
- args=(now,callback,))
- check_thread.daemon = True
- self._check_thread = check_thread
- check_thread.start()
-
- return True
-
- def async_check_update(self, now, callback=None):
- self._async_checking = True
- if self._verbose: print("{} BG thread: Checking for update now in background".format(self._addon))
- # time.sleep(3) # to test background, in case internet too fast to tell
- # try:
- self.check_for_update(now=now)
- # except Exception as exception:
- # print("Checking for update error:")
- # print(exception)
- # self._update_ready = False
- # self._update_version = None
- # self._update_link = None
- # self._error = "Error occurred"
- # self._error_msg = "Encountered an error while checking for updates"
-
- self._async_checking = False
- self._check_thread = None
-
- if self._verbose:
- print("{} BG thread: Finished checking for update, doing callback".format(self._addon))
- if callback != None: callback(self._update_ready)
-
-
- def stop_async_check_update(self):
- if self._check_thread != None:
- try:
- if self._verbose: print("Thread will end in normal course.")
- # however, "There is no direct kill method on a thread object."
- # better to let it run its course
- #self._check_thread.stop()
- except:
- pass
- self._async_checking = False
- self._error = None
- self._error_msg = None
-
-
-# -----------------------------------------------------------------------------
-# Updater Engines
-# -----------------------------------------------------------------------------
-
-
-class BitbucketEngine(object):
-
- def __init__(self):
- self.api_url = 'https://api.bitbucket.org'
- self.token = None
- self.name = "bitbucket"
-
- def form_repo_url(self, updater):
- return self.api_url+"/2.0/repositories/"+updater.user+"/"+updater.repo
-
- def form_tags_url(self, updater):
- return self.form_repo_url(updater) + "/refs/tags?sort=-name"
-
- def form_branch_url(self, branch, updater):
- return self.get_zip_url(branch, updater)
-
- def get_zip_url(self, name, updater):
- return "https://bitbucket.org/{user}/{repo}/get/{name}.zip".format(
- user=updater.user,
- repo=updater.repo,
- name=name)
-
- def parse_tags(self, response, updater):
- if response == None:
- return []
- return [{"name": tag["name"], "zipball_url": self.get_zip_url(tag["name"], updater)} for tag in response["values"]]
-
-
-class GithubEngine(object):
-
- def __init__(self):
- self.api_url = 'https://api.github.com'
- self.token = None
- self.name = "github"
-
- def form_repo_url(self, updater):
- return "{}{}{}{}{}".format(self.api_url,"/repos/",updater.user,
- "/",updater.repo)
-
- def form_tags_url(self, updater):
- if updater.use_releases:
- return "{}{}".format(self.form_repo_url(updater),"/releases")
- else:
- return "{}{}".format(self.form_repo_url(updater),"/tags")
-
- def form_branch_list_url(self, updater):
- return "{}{}".format(self.form_repo_url(updater),"/branches")
-
- def form_branch_url(self, branch, updater):
- return "{}{}{}".format(self.form_repo_url(updater),
- "/zipball/",branch)
-
- def parse_tags(self, response, updater):
- if response == None:
- return []
- return response
-
-
-class GitlabEngine(object):
-
- def __init__(self):
- self.api_url = 'https://gitlab.com'
- self.token = None
- self.name = "gitlab"
-
- def form_repo_url(self, updater):
- return "{}{}{}".format(self.api_url,"/api/v3/projects/",updater.repo)
-
- def form_tags_url(self, updater):
- return "{}{}".format(self.form_repo_url(updater),"/repository/tags")
-
- def form_branch_list_url(self, updater):
- # does not validate branch name.
- return "{}{}".format(
- self.form_repo_url(updater),
- "/repository/branches")
-
- def form_branch_url(self, branch, updater):
- # Could clash with tag names and if it does, it will
- # download TAG zip instead of branch zip to get
- # direct path, would need.
- return "{}{}{}".format(
- self.form_repo_url(updater),
- "/repository/archive.zip?sha=",
- branch)
-
- def get_zip_url(self, sha, updater):
- return "{base}/repository/archive.zip?sha:{sha}".format(
- base=self.form_repo_url(updater),
- sha=sha)
-
- # def get_commit_zip(self, id, updater):
- # return self.form_repo_url(updater)+"/repository/archive.zip?sha:"+id
-
- def parse_tags(self, response, updater):
- if response == None:
- return []
- return [{"name": tag["name"], "zipball_url": self.get_zip_url(tag["commit"]["id"], updater)} for tag in response]
-
-
-# -----------------------------------------------------------------------------
-# The module-shared class instance,
-# should be what's imported to other files
-# -----------------------------------------------------------------------------
-
-Updater = Singleton_updater()
diff --git a/uv_magic_uv/addon_updater_ops.py b/uv_magic_uv/addon_updater_ops.py
deleted file mode 100644
index 5f88b331..00000000
--- a/uv_magic_uv/addon_updater_ops.py
+++ /dev/null
@@ -1,1357 +0,0 @@
-# ##### 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 #####
-
-import bpy
-from bpy.app.handlers import persistent
-import os
-
-# updater import, import safely
-# Prevents popups for users with invalid python installs e.g. missing libraries
-try:
- from .addon_updater import Updater as updater
-except Exception as e:
- print("ERROR INITIALIZING UPDATER")
- print(str(e))
- class Singleton_updater_none(object):
- def __init__(self):
- self.addon = None
- self.verbose = False
- self.invalidupdater = True # used to distinguish bad install
- self.error = None
- self.error_msg = None
- self.async_checking = None
- def clear_state(self):
- self.addon = None
- self.verbose = False
- self.invalidupdater = True
- self.error = None
- self.error_msg = None
- self.async_checking = None
- def run_update(self): pass
- def check_for_update(self): pass
- updater = Singleton_updater_none()
- updater.error = "Error initializing updater module"
- updater.error_msg = str(e)
-
-# Must declare this before classes are loaded
-# otherwise the bl_idname's will not match and have errors.
-# Must be all lowercase and no spaces
-updater.addon = "magic_uv"
-
-dispaly_addon_name = "Magic UV"
-
-# -----------------------------------------------------------------------------
-# Updater operators
-# -----------------------------------------------------------------------------
-
-
-# simple popup for prompting checking for update & allow to install if available
-class addon_updater_install_popup(bpy.types.Operator):
- """Check and install update if available"""
- bl_label = "Update {x} addon".format(x=updater.addon)
- bl_idname = updater.addon+".updater_install_popup"
- bl_description = "Popup menu to check and display current updates available"
- bl_options = {'REGISTER', 'INTERNAL'}
-
- # if true, run clean install - ie remove all files before adding new
- # equivalent to deleting the addon and reinstalling, except the
- # updater folder/backup folder remains
- clean_install = bpy.props.BoolProperty(
- name="Clean install",
- description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install",
- default=False,
- options={'HIDDEN'}
- )
- ignore_enum = bpy.props.EnumProperty(
- name="Process update",
- description="Decide to install, ignore, or defer new addon update",
- items=[
- ("install","Update Now","Install update now"),
- ("ignore","Ignore", "Ignore this update to prevent future popups"),
- ("defer","Defer","Defer choice till next blender session")
- ],
- options={'HIDDEN'}
- )
-
- def check (self, context):
- return True
-
- def invoke(self, context, event):
- return context.window_manager.invoke_props_dialog(self)
-
- def draw(self, context):
- layout = self.layout
- if updater.invalidupdater == True:
- layout.label("Updater module error")
- return
- elif updater.update_ready == True:
- col = layout.column()
- col.scale_y = 0.7
- col.label("Update {} ready!".format(str(updater.update_version)),
- icon="LOOP_FORWARDS")
- col.label("Choose 'Update Now' & press OK to install, ",icon="BLANK1")
- col.label("or click outside window to defer",icon="BLANK1")
- row = col.row()
- row.prop(self,"ignore_enum",expand=True)
- col.split()
- elif updater.update_ready == False:
- col = layout.column()
- col.scale_y = 0.7
- col.label("No updates available")
- col.label("Press okay to dismiss dialog")
- # add option to force install
- else:
- # case: updater.update_ready = None
- # we have not yet checked for the update
- layout.label("Check for update now?")
-
- # potentially in future, could have UI for 'check to select old version'
- # to revert back to.
-
- def execute(self,context):
-
- # in case of error importing updater
- if updater.invalidupdater == True:
- return {'CANCELLED'}
-
- if updater.manual_only==True:
- bpy.ops.wm.url_open(url=updater.website)
- elif updater.update_ready == True:
-
- # action based on enum selection
- if self.ignore_enum=='defer':
- return {'FINISHED'}
- elif self.ignore_enum=='ignore':
- updater.ignore_update()
- return {'FINISHED'}
- #else: "install update now!"
-
- res = updater.run_update(
- force=False,
- callback=post_update_callback,
- clean=self.clean_install)
- # should return 0, if not something happened
- if updater.verbose:
- if res==0: print("Updater returned successful")
- else: print("Updater returned "+str(res)+", error occurred")
- elif updater.update_ready == None:
- (update_ready, version, link) = updater.check_for_update(now=True)
-
- # re-launch this dialog
- atr = addon_updater_install_popup.bl_idname.split(".")
- getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT')
- else:
- if updater.verbose:print("Doing nothing, not ready for update")
- return {'FINISHED'}
-
-
-# User preference check-now operator
-class addon_updater_check_now(bpy.types.Operator):
- bl_label = "Check now for "+dispaly_addon_name+" update"
- bl_idname = updater.addon+".updater_check_now"
- bl_description = "Check now for an update to the {x} addon".format(
- x=updater.addon)
- bl_options = {'REGISTER', 'INTERNAL'}
-
- def execute(self,context):
-
- # in case of error importing updater
- if updater.invalidupdater == True:
- return {'CANCELLED'}
-
- if updater.async_checking == True and updater.error == None:
- # Check already happened
- # Used here to just avoid constant applying settings below
- # Ignoring if error, to prevent being stuck on the error screen
- return {'CANCELLED'}
-
- # apply the UI settings
- settings = context.preferences.addons[__package__].preferences
- updater.set_check_interval(enable=settings.auto_check_update,
- months=settings.updater_intrval_months,
- days=settings.updater_intrval_days,
- hours=settings.updater_intrval_hours,
- minutes=settings.updater_intrval_minutes
- ) # optional, if auto_check_update
-
- # input is an optional callback function
- # this function should take a bool input, if true: update ready
- # if false, no update ready
- updater.check_for_update_now(ui_refresh)
-
- return {'FINISHED'}
-
-
-class addon_updater_update_now(bpy.types.Operator):
- bl_label = "Update "+updater.addon+" addon now"
- bl_idname = updater.addon+".updater_update_now"
- bl_description = "Update to the latest version of the {x} addon".format(
- x=updater.addon)
- bl_options = {'REGISTER', 'INTERNAL'}
-
- # if true, run clean install - ie remove all files before adding new
- # equivalent to deleting the addon and reinstalling, except the
- # updater folder/backup folder remains
- clean_install = bpy.props.BoolProperty(
- name="Clean install",
- description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install",
- default=False,
- options={'HIDDEN'}
- )
-
- def execute(self,context):
-
- # in case of error importing updater
- if updater.invalidupdater == True:
- return {'CANCELLED'}
-
- if updater.manual_only == True:
- bpy.ops.wm.url_open(url=updater.website)
- if updater.update_ready == True:
- # if it fails, offer to open the website instead
- try:
- res = updater.run_update(
- force=False,
- callback=post_update_callback,
- clean=self.clean_install)
-
- # should return 0, if not something happened
- if updater.verbose:
- if res==0: print("Updater returned successful")
- else: print("Updater returned "+str(res)+", error occurred")
- except Exception as e:
- updater._error = "Error trying to run update"
- updater._error_msg = str(e)
- atr = addon_updater_install_manually.bl_idname.split(".")
- getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT')
- elif updater.update_ready == None:
- (update_ready, version, link) = updater.check_for_update(now=True)
- # re-launch this dialog
- atr = addon_updater_install_popup.bl_idname.split(".")
- getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT')
-
- elif updater.update_ready == False:
- self.report({'INFO'}, "Nothing to update")
- else:
- self.report({'ERROR'}, "Encountered problem while trying to update")
-
- return {'FINISHED'}
-
-
-class addon_updater_update_target(bpy.types.Operator):
- bl_label = updater.addon+" addon version target"
- bl_idname = updater.addon+".updater_update_target"
- bl_description = "Install a targeted version of the {x} addon".format(
- x=updater.addon)
- bl_options = {'REGISTER', 'INTERNAL'}
-
- def target_version(self, context):
- # in case of error importing updater
- if updater.invalidupdater == True:
- ret = []
-
- ret = []
- i=0
- for tag in updater.tags:
- ret.append( (tag,tag,"Select to install "+tag) )
- i+=1
- return ret
-
- target = bpy.props.EnumProperty(
- name="Target version to install",
- description="Select the version to install",
- items=target_version
- )
-
- # if true, run clean install - ie remove all files before adding new
- # equivalent to deleting the addon and reinstalling, except the
- # updater folder/backup folder remains
- clean_install = bpy.props.BoolProperty(
- name="Clean install",
- description="If enabled, completely clear the addon's folder before installing new update, creating a fresh install",
- default=False,
- options={'HIDDEN'}
- )
-
- @classmethod
- def poll(cls, context):
- if updater.invalidupdater == True: return False
- return updater.update_ready != None and len(updater.tags)>0
-
- def invoke(self, context, event):
- return context.window_manager.invoke_props_dialog(self)
-
- def draw(self, context):
- layout = self.layout
- if updater.invalidupdater == True:
- layout.label("Updater error")
- return
- split = layout.split(percentage=0.66)
- subcol = split.column()
- subcol.label("Select install version")
- subcol = split.column()
- subcol.prop(self, "target", text="")
-
-
- def execute(self,context):
-
- # in case of error importing updater
- if updater.invalidupdater == True:
- return {'CANCELLED'}
-
- res = updater.run_update(
- force=False,
- revert_tag=self.target,
- callback=post_update_callback,
- clean=self.clean_install)
-
- # should return 0, if not something happened
- if updater.verbose:
- if res==0: print("Updater returned successful")
- else: print("Updater returned "+str(res)+", error occurred")
- return {'CANCELLED'}
-
- return {'FINISHED'}
-
-
-class addon_updater_install_manually(bpy.types.Operator):
- """As a fallback, direct the user to download the addon manually"""
- bl_label = "Install update manually"
- bl_idname = updater.addon+".updater_install_manually"
- bl_description = "Proceed to manually install update"
- bl_options = {'REGISTER', 'INTERNAL'}
-
- error = bpy.props.StringProperty(
- name="Error Occurred",
- default="",
- options={'HIDDEN'}
- )
-
- def invoke(self, context, event):
- return context.window_manager.invoke_popup(self)
-
- def draw(self, context):
- layout = self.layout
-
- if updater.invalidupdater == True:
- layout.label("Updater error")
- return
-
- # use a "failed flag"? it shows this label if the case failed.
- if self.error!="":
- col = layout.column()
- col.scale_y = 0.7
- col.label("There was an issue trying to auto-install",icon="ERROR")
- col.label("Press the download button below and install",icon="BLANK1")
- col.label("the zip file like a normal addon.",icon="BLANK1")
- else:
- col = layout.column()
- col.scale_y = 0.7
- col.label("Install the addon manually")
- col.label("Press the download button below and install")
- col.label("the zip file like a normal addon.")
-
- # if check hasn't happened, i.e. accidentally called this menu
- # allow to check here
-
- row = layout.row()
-
- if updater.update_link != None:
- row.operator("wm.url_open",text="Direct download").url=\
- updater.update_link
- else:
- row.operator("wm.url_open",text="(failed to retrieve direct download)")
- row.enabled = False
-
- if updater.website != None:
- row = layout.row()
- row.operator("wm.url_open",text="Open website").url=\
- updater.website
- else:
- row = layout.row()
- row.label("See source website to download the update")
-
- def execute(self,context):
-
- return {'FINISHED'}
-
-
-class addon_updater_updated_successful(bpy.types.Operator):
- """Addon in place, popup telling user it completed or what went wrong"""
- bl_label = "Installation Report"
- bl_idname = updater.addon+".updater_update_successful"
- bl_description = "Update installation response"
- bl_options = {'REGISTER', 'INTERNAL', 'UNDO'}
-
- error = bpy.props.StringProperty(
- name="Error Occurred",
- default="",
- options={'HIDDEN'}
- )
-
- def invoke(self, context, event):
- return context.window_manager.invoke_props_popup(self, event)
-
- def draw(self, context):
- layout = self.layout
-
- if updater.invalidupdater == True:
- layout.label("Updater error")
- return
-
- saved = updater.json
- if self.error != "":
- col = layout.column()
- col.scale_y = 0.7
- col.label("Error occurred, did not install", icon="ERROR")
- col.label(updater.error_msg, icon="BLANK1")
- rw = col.row()
- rw.scale_y = 2
- rw.operator("wm.url_open",
- text="Click for manual download.",
- icon="BLANK1"
- ).url=updater.website
- # manual download button here
- elif updater.auto_reload_post_update == False:
- # tell user to restart blender
- if "just_restored" in saved and saved["just_restored"] == True:
- col = layout.column()
- col.scale_y = 0.7
- col.label("Addon restored", icon="RECOVER_LAST")
- col.label("Restart blender to reload.",icon="BLANK1")
- updater.json_reset_restore()
- else:
- col = layout.column()
- col.scale_y = 0.7
- col.label("Addon successfully installed", icon="FILE_TICK")
- col.label("Restart blender to reload.", icon="BLANK1")
-
- else:
- # reload addon, but still recommend they restart blender
- if "just_restored" in saved and saved["just_restored"] == True:
- col = layout.column()
- col.scale_y = 0.7
- col.label("Addon restored", icon="RECOVER_LAST")
- col.label("Consider restarting blender to fully reload.",icon="BLANK1")
- updater.json_reset_restore()
- else:
- col = layout.column()
- col.scale_y = 0.7
- col.label("Addon successfully installed", icon="FILE_TICK")
- col.label("Consider restarting blender to fully reload.", icon="BLANK1")
-
- def execut(self, context):
- return {'FINISHED'}
-
-
-class addon_updater_restore_backup(bpy.types.Operator):
- """Restore addon from backup"""
- bl_label = "Restore backup"
- bl_idname = updater.addon+".updater_restore_backup"
- bl_description = "Restore addon from backup"
- bl_options = {'REGISTER', 'INTERNAL'}
-
- @classmethod
- def poll(cls, context):
- try:
- return os.path.isdir(os.path.join(updater.stage_path,"backup"))
- except:
- return False
-
- def execute(self, context):
- # in case of error importing updater
- if updater.invalidupdater == True:
- return {'CANCELLED'}
- updater.restore_backup()
- return {'FINISHED'}
-
-
-class addon_updater_ignore(bpy.types.Operator):
- """Prevent future update notice popups"""
- bl_label = "Ignore update"
- bl_idname = updater.addon+".updater_ignore"
- bl_description = "Ignore update to prevent future popups"
- bl_options = {'REGISTER', 'INTERNAL'}
-
- @classmethod
- def poll(cls, context):
- if updater.invalidupdater == True:
- return False
- elif updater.update_ready == True:
- return True
- else:
- return False
-
- def execute(self, context):
- # in case of error importing updater
- if updater.invalidupdater == True:
- return {'CANCELLED'}
- updater.ignore_update()
- self.report({"INFO"},"Open addon preferences for updater options")
- return {'FINISHED'}
-
-
-class addon_updater_end_background(bpy.types.Operator):
- """Stop checking for update in the background"""
- bl_label = "End background check"
- bl_idname = updater.addon+".end_background_check"
- bl_description = "Stop checking for update in the background"
- bl_options = {'REGISTER', 'INTERNAL'}
-
- # @classmethod
- # def poll(cls, context):
- # if updater.async_checking == True:
- # return True
- # else:
- # return False
-
- def execute(self, context):
- # in case of error importing updater
- if updater.invalidupdater == True:
- return {'CANCELLED'}
- updater.stop_async_check_update()
- return {'FINISHED'}
-
-
-# -----------------------------------------------------------------------------
-# Handler related, to create popups
-# -----------------------------------------------------------------------------
-
-
-# global vars used to prevent duplicate popup handlers
-ran_autocheck_install_popup = False
-ran_update_sucess_popup = False
-
-# global var for preventing successive calls
-ran_background_check = False
-
-@persistent
-def updater_run_success_popup_handler(scene):
- global ran_update_sucess_popup
- ran_update_sucess_popup = True
-
- # in case of error importing updater
- if updater.invalidupdater == True:
- return
-
- try:
- bpy.app.handlers.scene_update_post.remove(
- updater_run_success_popup_handler)
- except:
- pass
-
- atr = addon_updater_updated_successful.bl_idname.split(".")
- getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT')
-
-
-@persistent
-def updater_run_install_popup_handler(scene):
- global ran_autocheck_install_popup
- ran_autocheck_install_popup = True
-
- # in case of error importing updater
- if updater.invalidupdater == True:
- return
-
- try:
- bpy.app.handlers.scene_update_post.remove(
- updater_run_install_popup_handler)
- except:
- pass
-
- if "ignore" in updater.json and updater.json["ignore"] == True:
- return # don't do popup if ignore pressed
- # elif type(updater.update_version) != type((0,0,0)):
- # # likely was from master or another branch, shouldn't trigger popup
- # updater.json_reset_restore()
- # return
- elif "version_text" in updater.json and "version" in updater.json["version_text"]:
- version = updater.json["version_text"]["version"]
- ver_tuple = updater.version_tuple_from_text(version)
-
- if ver_tuple < updater.current_version:
- # user probably manually installed to get the up to date addon
- # in here. Clear out the update flag using this function
- if updater.verbose:
- print("{} updater: appears user updated, clearing flag".format(\
- updater.addon))
- updater.json_reset_restore()
- return
- atr = addon_updater_install_popup.bl_idname.split(".")
- getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT')
-
-
-# passed into the updater, background thread updater
-def background_update_callback(update_ready):
- global ran_autocheck_install_popup
-
- # in case of error importing updater
- if updater.invalidupdater == True:
- return
-
- if updater.showpopups == False:
- return
-
- if update_ready != True:
- return
-
- if updater_run_install_popup_handler not in \
- bpy.app.handlers.scene_update_post and \
- ran_autocheck_install_popup==False:
- bpy.app.handlers.scene_update_post.append(
- updater_run_install_popup_handler)
-
- ran_autocheck_install_popup = True
-
-
-# a callback for once the updater has completed
-# Only makes sense to use this if "auto_reload_post_update" == False,
-# i.e. don't auto-restart the addon
-def post_update_callback(res=None):
-
- # in case of error importing updater
- if updater.invalidupdater == True:
- return
-
- if res==None:
- # this is the same code as in conditional at the end of the register function
- # ie if "auto_reload_post_update" == True, comment out this code
- if updater.verbose: print("{} updater: Running post update callback".format(updater.addon))
- #bpy.app.handlers.scene_update_post.append(updater_run_success_popup_handler)
-
- atr = addon_updater_updated_successful.bl_idname.split(".")
- getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT')
- global ran_update_sucess_popup
- ran_update_sucess_popup = True
- else:
- # some kind of error occured and it was unable to install,
- # offer manual download instead
- atr = addon_updater_updated_successful.bl_idname.split(".")
- getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT',error=res)
- return
-
-def ui_refresh(update_status):
- # find a way to just re-draw self?
- # callback intended for trigger by async thread
- for windowManager in bpy.data.window_managers:
- for window in windowManager.windows:
- for area in window.screen.areas:
- area.tag_redraw()
-
-# function for asynchronous background check, which *could* be called on register
-def check_for_update_background():
-
- # in case of error importing updater
- if updater.invalidupdater == True:
- return
-
- global ran_background_check
- if ran_background_check == True:
- # Global var ensures check only happens once
- return
- elif updater.update_ready != None or updater.async_checking == True:
- # Check already happened
- # Used here to just avoid constant applying settings below
- return
-
- # apply the UI settings
- addon_prefs = bpy.context.preferences.addons.get(__package__, None)
- if not addon_prefs:
- return
- settings = addon_prefs.preferences
- updater.set_check_interval(enable=settings.auto_check_update,
- months=settings.updater_intrval_months,
- days=settings.updater_intrval_days,
- hours=settings.updater_intrval_hours,
- minutes=settings.updater_intrval_minutes
- ) # optional, if auto_check_update
-
- # input is an optional callback function
- # this function should take a bool input, if true: update ready
- # if false, no update ready
- if updater.verbose:
- print("{} updater: Running background check for update".format(\
- updater.addon))
- updater.check_for_update_async(background_update_callback)
- ran_background_check = True
-
-
-# can be placed in front of other operators to launch when pressed
-def check_for_update_nonthreaded(self, context):
-
- # in case of error importing updater
- if updater.invalidupdater == True:
- return
-
- # only check if it's ready, ie after the time interval specified
- # should be the async wrapper call here
-
- settings = context.preferences.addons[__package__].preferences
- updater.set_check_interval(enable=settings.auto_check_update,
- months=settings.updater_intrval_months,
- days=settings.updater_intrval_days,
- hours=settings.updater_intrval_hours,
- minutes=settings.updater_intrval_minutes
- ) # optional, if auto_check_update
-
- (update_ready, version, link) = updater.check_for_update(now=False)
- if update_ready == True:
- atr = addon_updater_install_popup.bl_idname.split(".")
- getattr(getattr(bpy.ops, atr[0]),atr[1])('INVOKE_DEFAULT')
- else:
- if updater.verbose: print("No update ready")
- self.report({'INFO'}, "No update ready")
-
-# for use in register only, to show popup after re-enabling the addon
-# must be enabled by developer
-def showReloadPopup():
-
- # in case of error importing updater
- if updater.invalidupdater == True:
- return
-
- saved_state = updater.json
- global ran_update_sucess_popup
-
- a = saved_state != None
- b = "just_updated" in saved_state
- c = saved_state["just_updated"]
-
- if a and b and c:
- updater.json_reset_postupdate() # so this only runs once
-
- # no handlers in this case
- if updater.auto_reload_post_update == False: return
-
- if updater_run_success_popup_handler not in \
- bpy.app.handlers.scene_update_post \
- and ran_update_sucess_popup==False:
- bpy.app.handlers.scene_update_post.append(
- updater_run_success_popup_handler)
- ran_update_sucess_popup = True
-
-
-# -----------------------------------------------------------------------------
-# Example UI integrations
-# -----------------------------------------------------------------------------
-
-
-# Panel - Update Available for placement at end/beginning of panel
-# After a check for update has occurred, this function will draw a box
-# saying an update is ready, and give a button for: update now, open website,
-# or ignore popup. Ideal to be placed at the end / beginning of a panel
-def update_notice_box_ui(self, context):
-
- # in case of error importing updater
- if updater.invalidupdater == True:
- return
-
- saved_state = updater.json
- if updater.auto_reload_post_update == False:
- if "just_updated" in saved_state and saved_state["just_updated"] == True:
- layout = self.layout
- box = layout.box()
- col = box.column()
- col.scale_y = 0.7
- col.label("Restart blender", icon="ERROR")
- col.label("to complete update")
- return
-
- # if user pressed ignore, don't draw the box
- if "ignore" in updater.json and updater.json["ignore"] == True:
- return
-
- if updater.update_ready != True: return
-
- settings = context.preferences.addons[__package__].preferences
- layout = self.layout
- box = layout.box()
- col = box.column(align=True)
- col.label("Update ready!",icon="ERROR")
- col.separator()
- row = col.row(align=True)
- split = row.split(align=True)
- colL = split.column(align=True)
- colL.scale_y = 1.5
- colL.operator(addon_updater_ignore.bl_idname,icon="X",text="Ignore")
- colR = split.column(align=True)
- colR.scale_y = 1.5
- if updater.manual_only==False:
- colR.operator(addon_updater_update_now.bl_idname,
- "Update", icon="LOOP_FORWARDS")
- col.operator("wm.url_open", text="Open website").url = updater.website
- #col.operator("wm.url_open",text="Direct download").url=updater.update_link
- col.operator(addon_updater_install_manually.bl_idname, "Install manually")
- else:
- #col.operator("wm.url_open",text="Direct download").url=updater.update_link
- col.operator("wm.url_open", text="Get it now").url = \
- updater.website
-
-
-# Preferences - for drawing with full width inside user preferences
-# Create a function that can be run inside user preferences panel for prefs UI
-# Place inside UI draw using: addon_updater_ops.updaterSettingsUI(self, context)
-# or by: addon_updater_ops.updaterSettingsUI(context)
-def update_settings_ui(self, context, element=None):
- # element is a UI element, such as layout, a row, column, or box
- if element==None: element = self.layout
- box = element.box()
-
- # in case of error importing updater
- if updater.invalidupdater == True:
- box.label("Error initializing updater code:")
- box.label(updater.error_msg)
- return
-
- settings = context.preferences.addons[__package__].preferences
-
- # auto-update settings
- box.label("Updater Settings")
- row = box.row()
-
- # special case to tell user to restart blender, if set that way
- if updater.auto_reload_post_update == False:
- saved_state = updater.json
- if "just_updated" in saved_state and saved_state["just_updated"] == True:
- row.label("Restart blender to complete update", icon="ERROR")
- return
-
- split = row.split(percentage=0.3)
- subcol = split.column()
- subcol.prop(settings, "auto_check_update")
- subcol = split.column()
-
- if settings.auto_check_update==False: subcol.enabled = False
- subrow = subcol.row()
- subrow.label("Interval between checks")
- subrow = subcol.row(align=True)
- checkcol = subrow.column(align=True)
- checkcol.prop(settings,"updater_intrval_months")
- checkcol = subrow.column(align=True)
- checkcol.prop(settings,"updater_intrval_days")
- checkcol = subrow.column(align=True)
- checkcol.prop(settings,"updater_intrval_hours")
- checkcol = subrow.column(align=True)
- checkcol.prop(settings,"updater_intrval_minutes")
-
- # checking / managing updates
- row = box.row()
- col = row.column()
- if updater.error != None:
- subcol = col.row(align=True)
- subcol.scale_y = 1
- split = subcol.split(align=True)
- split.scale_y = 2
- if "ssl" in updater.error_msg.lower():
- split.enabled = True
- split.operator(addon_updater_install_manually.bl_idname,
- updater.error)
- else:
- split.enabled = False
- split.operator(addon_updater_check_now.bl_idname,
- updater.error)
- split = subcol.split(align=True)
- split.scale_y = 2
- split.operator(addon_updater_check_now.bl_idname,
- text = "", icon="FILE_REFRESH")
-
- elif updater.update_ready == None and updater.async_checking == False:
- col.scale_y = 2
- col.operator(addon_updater_check_now.bl_idname)
- elif updater.update_ready == None: # async is running
- subcol = col.row(align=True)
- subcol.scale_y = 1
- split = subcol.split(align=True)
- split.enabled = False
- split.scale_y = 2
- split.operator(addon_updater_check_now.bl_idname,
- "Checking...")
- split = subcol.split(align=True)
- split.scale_y = 2
- split.operator(addon_updater_end_background.bl_idname,
- text = "", icon="X")
-
- elif updater.include_branches==True and \
- len(updater.tags)==len(updater.include_branch_list) and \
- updater.manual_only==False:
- # no releases found, but still show the appropriate branch
- subcol = col.row(align=True)
- subcol.scale_y = 1
- split = subcol.split(align=True)
- split.scale_y = 2
- split.operator(addon_updater_update_now.bl_idname,
- "Update directly to "+str(updater.include_branch_list[0]))
- split = subcol.split(align=True)
- split.scale_y = 2
- split.operator(addon_updater_check_now.bl_idname,
- text = "", icon="FILE_REFRESH")
-
- elif updater.update_ready==True and updater.manual_only==False:
- subcol = col.row(align=True)
- subcol.scale_y = 1
- split = subcol.split(align=True)
- split.scale_y = 2
- split.operator(addon_updater_update_now.bl_idname,
- "Update now to "+str(updater.update_version))
- split = subcol.split(align=True)
- split.scale_y = 2
- split.operator(addon_updater_check_now.bl_idname,
- text = "", icon="FILE_REFRESH")
-
- elif updater.update_ready==True and updater.manual_only==True:
- col.scale_y = 2
- col.operator("wm.url_open",
- "Download "+str(updater.update_version)).url=updater.website
- else: # i.e. that updater.update_ready == False
- subcol = col.row(align=True)
- subcol.scale_y = 1
- split = subcol.split(align=True)
- split.enabled = False
- split.scale_y = 2
- split.operator(addon_updater_check_now.bl_idname,
- "Addon is up to date")
- split = subcol.split(align=True)
- split.scale_y = 2
- split.operator(addon_updater_check_now.bl_idname,
- text = "", icon="FILE_REFRESH")
-
- if updater.manual_only == False:
- col = row.column(align=True)
- #col.operator(addon_updater_update_target.bl_idname,
- if updater.include_branches == True and len(updater.include_branch_list)>0:
- branch = updater.include_branch_list[0]
- col.operator(addon_updater_update_target.bl_idname,
- "Install latest {} / old version".format(branch))
- else:
- col.operator(addon_updater_update_target.bl_idname,
- "Reinstall / install old version")
- lastdate = "none found"
- backuppath = os.path.join(updater.stage_path,"backup")
- if "backup_date" in updater.json and os.path.isdir(backuppath):
- if updater.json["backup_date"] == "":
- lastdate = "Date not found"
- else:
- lastdate = updater.json["backup_date"]
- backuptext = "Restore addon backup ({})".format(lastdate)
- col.operator(addon_updater_restore_backup.bl_idname, backuptext)
-
- row = box.row()
- row.scale_y = 0.7
- lastcheck = updater.json["last_check"]
- if updater.error != None and updater.error_msg != None:
- row.label(updater.error_msg)
- elif lastcheck != "" and lastcheck != None:
- lastcheck = lastcheck[0: lastcheck.index(".") ]
- row.label("Last update check: " + lastcheck)
- else:
- row.label("Last update check: Never")
-
-
-# Preferences - Condensed drawing within preferences
-# alternate draw for user preferences or other places, does not draw a box
-def update_settings_ui_condensed(self, context, element=None):
- # element is a UI element, such as layout, a row, column, or box
- if element==None: element = self.layout
- row = element.row()
-
- # in case of error importing updater
- if updater.invalidupdater == True:
- row.label("Error initializing updater code:")
- row.label(updater.error_msg)
- return
-
- settings = context.preferences.addons[__package__].preferences
-
- # special case to tell user to restart blender, if set that way
- if updater.auto_reload_post_update == False:
- saved_state = updater.json
- if "just_updated" in saved_state and saved_state["just_updated"] == True:
- row.label("Restart blender to complete update", icon="ERROR")
- return
-
- col = row.column()
- if updater.error != None:
- subcol = col.row(align=True)
- subcol.scale_y = 1
- split = subcol.split(align=True)
- split.scale_y = 2
- if "ssl" in updater.error_msg.lower():
- split.enabled = True
- split.operator(addon_updater_install_manually.bl_idname,
- updater.error)
- else:
- split.enabled = False
- split.operator(addon_updater_check_now.bl_idname,
- updater.error)
- split = subcol.split(align=True)
- split.scale_y = 2
- split.operator(addon_updater_check_now.bl_idname,
- text = "", icon="FILE_REFRESH")
-
- elif updater.update_ready == None and updater.async_checking == False:
- col.scale_y = 2
- col.operator(addon_updater_check_now.bl_idname)
- elif updater.update_ready == None: # async is running
- subcol = col.row(align=True)
- subcol.scale_y = 1
- split = subcol.split(align=True)
- split.enabled = False
- split.scale_y = 2
- split.operator(addon_updater_check_now.bl_idname,
- "Checking...")
- split = subcol.split(align=True)
- split.scale_y = 2
- split.operator(addon_updater_end_background.bl_idname,
- text = "", icon="X")
-
- elif updater.include_branches==True and \
- len(updater.tags)==len(updater.include_branch_list) and \
- updater.manual_only==False:
- # no releases found, but still show the appropriate branch
- subcol = col.row(align=True)
- subcol.scale_y = 1
- split = subcol.split(align=True)
- split.scale_y = 2
- split.operator(addon_updater_update_now.bl_idname,
- "Update directly to "+str(updater.include_branch_list[0]))
- split = subcol.split(align=True)
- split.scale_y = 2
- split.operator(addon_updater_check_now.bl_idname,
- text = "", icon="FILE_REFRESH")
-
- elif updater.update_ready==True and updater.manual_only==False:
- subcol = col.row(align=True)
- subcol.scale_y = 1
- split = subcol.split(align=True)
- split.scale_y = 2
- split.operator(addon_updater_update_now.bl_idname,
- "Update now to "+str(updater.update_version))
- split = subcol.split(align=True)
- split.scale_y = 2
- split.operator(addon_updater_check_now.bl_idname,
- text = "", icon="FILE_REFRESH")
-
- elif updater.update_ready==True and updater.manual_only==True:
- col.scale_y = 2
- col.operator("wm.url_open",
- "Download "+str(updater.update_version)).url=updater.website
- else: # i.e. that updater.update_ready == False
- subcol = col.row(align=True)
- subcol.scale_y = 1
- split = subcol.split(align=True)
- split.enabled = False
- split.scale_y = 2
- split.operator(addon_updater_check_now.bl_idname,
- "Addon is up to date")
- split = subcol.split(align=True)
- split.scale_y = 2
- split.operator(addon_updater_check_now.bl_idname,
- text = "", icon="FILE_REFRESH")
-
- row = element.row()
- row.prop(settings, "auto_check_update")
-
- row = element.row()
- row.scale_y = 0.7
- lastcheck = updater.json["last_check"]
- if updater.error != None and updater.error_msg != None:
- row.label(updater.error_msg)
- elif lastcheck != "" and lastcheck != None:
- lastcheck = lastcheck[0: lastcheck.index(".") ]
- row.label("Last check: " + lastcheck)
- else:
- row.label("Last check: Never")
-
-
-# a global function for tag skipping
-# a way to filter which tags are displayed,
-# e.g. to limit downgrading too far
-# input is a tag text, e.g. "v1.2.3"
-# output is True for skipping this tag number,
-# False if the tag is allowed (default for all)
-# Note: here, "self" is the acting updater shared class instance
-def skip_tag_function(self, tag):
-
- # in case of error importing updater
- if self.invalidupdater == True:
- return False
-
- # ---- write any custom code here, return true to disallow version ---- #
- #
- # # Filter out e.g. if 'beta' is in name of release
- # if 'beta' in tag.lower():
- # return True
- # ---- write any custom code above, return true to disallow version --- #
-
- if self.include_branches == True:
- for branch in self.include_branch_list:
- if tag["name"].lower() == branch: return False
-
- # function converting string to tuple, ignoring e.g. leading 'v'
- tupled = self.version_tuple_from_text(tag["name"])
- if type(tupled) != type( (1,2,3) ): return True
-
- # select the min tag version - change tuple accordingly
- if self.version_min_update != None:
- if tupled < self.version_min_update:
- return True # skip if current version below this
-
- # select the max tag version
- if self.version_max_update != None:
- if tupled >= self.version_max_update:
- return True # skip if current version at or above this
-
- # in all other cases, allow showing the tag for updating/reverting
- return False
-
-# Only customize if trying to leverage "attachments" in *GitHub* releases
-# A way to select from one or multiple attached donwloadable files from the
-# server, instead of downloading the default release/tag source code
-def select_link_function(self, tag):
- link = ""
-
- # -- Default, universal case (and is the only option for GitLab/Bitbucket)
- link = tag["zipball_url"]
-
- # -- Example: select the first (or only) asset instead source code --
- #if "assets" in tag and "browser_download_url" in tag["assets"][0]:
- # link = tag["assets"][0]["browser_download_url"]
-
- # -- Example: select asset based on OS, where multiple builds exist --
- # # not tested/no error checking, modify to fit your own needs!
- # # assume each release has three attached builds:
- # # release_windows.zip, release_OSX.zip, release_linux.zip
- # # This also would logically not be used with "branches" enabled
- # if platform.system() == "Darwin": # ie OSX
- # link = [asset for asset in tag["assets"] if 'OSX' in asset][0]
- # elif platform.system() == "Windows":
- # link = [asset for asset in tag["assets"] if 'windows' in asset][0]
- # elif platform.system() == "Linux":
- # link = [asset for asset in tag["assets"] if 'linux' in asset][0]
-
- return link
-
-
-# -----------------------------------------------------------------------------
-# Register, should be run in the register module itself
-# -----------------------------------------------------------------------------
-
-
-# registering the operators in this module
-def register(bl_info):
-
- # See output to verify this register function is working properly
- # print("Running updater reg")
-
- # safer failure in case of issue loading module
- if updater.error != None:
- print("Exiting updater registration, error return")
- return
-
- # confirm your updater "engine" (Github is default if not specified)
- updater.engine = "Github"
- # updater.engine = "GitLab"
- # updater.engine = "Bitbucket"
-
- # If using private repository, indicate the token here
- # Must be set after assigning the engine.
- # **WARNING** Depending on the engine, this token can act like a password!!
- # Only provide a token if the project is *non-public*, see readme for
- # other considerations and suggestions from a security standpoint
- updater.private_token = None # "tokenstring"
-
- # choose your own username, must match website (not needed for GitLab)
- updater.user = "nutti"
-
- # choose your own repository, must match git name
- updater.repo = "Magic-UV"
-
- #updater.addon = # define at top of module, MUST be done first
-
- # Website for manual addon download, optional but recommended to set
- updater.website = "https://github.com/nutti/Magic-UV"
-
- # Addon subfolder path
- # "sample/path/to/addon"
- # default is "" or None, meaning root
- updater.subfolder_path = "uv_magic_uv"
-
- # used to check/compare versions
- updater.current_version = bl_info["version"]
-
- # Optional, to hard-set update frequency, use this here - however,
- # this demo has this set via UI properties.
- # updater.set_check_interval(
- # enable=False,months=0,days=0,hours=0,minutes=2)
-
- # Optional, consider turning off for production or allow as an option
- # This will print out additional debugging info to the console
- updater.verbose = False # make False for production default
-
- # Optional, customize where the addon updater processing subfolder is,
- # essentially a staging folder used by the updater on its own
- # Needs to be within the same folder as the addon itself
- # Need to supply a full, absolute path to folder
- # updater.updater_path = # set path of updater folder, by default:
- # /addons/{__package__}/{__package__}_updater
-
- # auto create a backup of the addon when installing other versions
- updater.backup_current = True # True by default
-
- # Sample ignore patterns for when creating backup of current during update
- updater.backup_ignore_patterns = ["__pycache__"]
- # Alternate example patterns
- # updater.backup_ignore_patterns = [".git", "__pycache__", "*.bat", ".gitignore", "*.exe"]
-
- # Patterns for files to actively overwrite if found in new update
- # file and are also found in the currently installed addon. Note that
-
- # by default (ie if set to []), updates are installed in the same way as blender:
- # .py files are replaced, but other file types (e.g. json, txt, blend)
- # will NOT be overwritten if already present in current install. Thus
- # if you want to automatically update resources/non py files, add them
- # as a part of the pattern list below so they will always be overwritten by an
- # update. If a pattern file is not found in new update, no action is taken
- # This does NOT detele anything, only defines what is allowed to be overwritten
- updater.overwrite_patterns = ["*.png","*.jpg","README.md","LICENSE.txt"]
- # updater.overwrite_patterns = []
- # other examples:
- # ["*"] means ALL files/folders will be overwritten by update, was the behavior pre updater v1.0.4
- # [] or ["*.py","*.pyc"] matches default blender behavior, ie same effect if user installs update manually without deleting the existing addon first
- # e.g. if existing install and update both have a resource.blend file, the existing installed one will remain
- # ["some.py"] means if some.py is found in addon update, it will overwrite any existing some.py in current addon install, if any
- # ["*.json"] means all json files found in addon update will overwrite those of same name in current install
- # ["*.png","README.md","LICENSE.txt"] means the readme, license, and all pngs will be overwritten by update
-
- # Patterns for files to actively remove prior to running update
- # Useful if wanting to remove old code due to changes in filenames
- # that otherwise would accumulate. Note: this runs after taking
- # a backup (if enabled) but before placing in new update. If the same
- # file name removed exists in the update, then it acts as if pattern
- # is placed in the overwrite_patterns property. Note this is effectively
- # ignored if clean=True in the run_update method
- updater.remove_pre_update_patterns = ["*.py", "*.pyc"]
- # Note setting ["*"] here is equivalent to always running updates with
- # clean = True in the run_update method, ie the equivalent of a fresh,
- # new install. This would also delete any resources or user-made/modified
- # files setting ["__pycache__"] ensures the pycache folder is always removed
- # The configuration of ["*.py","*.pyc"] is a safe option as this
- # will ensure no old python files/caches remain in event different addon
- # versions have different filenames or structures
-
- # Allow branches like 'master' as an option to update to, regardless
- # of release or version.
- # Default behavior: releases will still be used for auto check (popup),
- # but the user has the option from user preferences to directly
- # update to the master branch or any other branches specified using
- # the "install {branch}/older version" operator.
- updater.include_branches = True
-
- # (GitHub only) This options allows the user to use releases over tags for data,
- # which enables pulling down release logs/notes, as well as specify installs from
- # release-attached zips (instead of just the auto-packaged code generated with
- # a release/tag). Setting has no impact on BitBucket or GitLab repos
- updater.use_releases = False
- # note: Releases always have a tag, but a tag may not always be a release
- # Therefore, setting True above will filter out any non-annoted tags
- # note 2: Using this option will also display the release name instead of
- # just the tag name, bear this in mind given the skip_tag_function filtering above
-
- # if using "include_branches",
- # updater.include_branch_list defaults to ['master'] branch if set to none
- # example targeting another multiple branches allowed to pull from
- # updater.include_branch_list = ['master', 'dev'] # example with two branches
- updater.include_branch_list = ['master', 'develop'] # None is the equivalent to setting ['master']
-
- # Only allow manual install, thus prompting the user to open
- # the addon's web page to download, specifically: updater.website
- # Useful if only wanting to get notification of updates but not
- # directly install.
- updater.manual_only = False
-
- # Used for development only, "pretend" to install an update to test
- # reloading conditions
- updater.fake_install = False # Set to true to test callback/reloading
-
- # Show popups, ie if auto-check for update is enabled or a previous
- # check for update in user preferences found a new version, show a popup
- # (at most once per blender session, and it provides an option to ignore
- # for future sessions); default behavior is set to True
- updater.showpopups = True
- # note: if set to false, there will still be an "update ready" box drawn
- # using the `update_notice_box_ui` panel function.
-
- # Override with a custom function on what tags
- # to skip showing for updater; see code for function above.
- # Set the min and max versions allowed to install.
- # Optional, default None
- # min install (>=) will install this and higher
- updater.version_min_update = (5,2,0)
- # updater.version_min_update = None # if not wanting to define a min
-
- # max install (<) will install strictly anything lower
- # updater.version_max_update = (9,9,9)
- updater.version_max_update = None # if not wanting to define a max
-
- # Function defined above, customize as appropriate per repository
- updater.skip_tag = skip_tag_function # min and max used in this function
-
- # Function defined above, customize as appropriate per repository; not required
- updater.select_link = select_link_function
-
- # The register line items for all operators/panels
- # If using bpy.utils.register_module(__name__) to register elsewhere
- # in the addon, delete these lines (also from unregister)
- bpy.utils.register_class(addon_updater_install_popup)
- bpy.utils.register_class(addon_updater_check_now)
- bpy.utils.register_class(addon_updater_update_now)
- bpy.utils.register_class(addon_updater_update_target)
- bpy.utils.register_class(addon_updater_install_manually)
- bpy.utils.register_class(addon_updater_updated_successful)
- bpy.utils.register_class(addon_updater_restore_backup)
- bpy.utils.register_class(addon_updater_ignore)
- bpy.utils.register_class(addon_updater_end_background)
-
- # special situation: we just updated the addon, show a popup
- # to tell the user it worked
- # should be enclosed in try/catch in case other issues arise
- showReloadPopup()
-
-
-def unregister():
- bpy.utils.unregister_class(addon_updater_install_popup)
- bpy.utils.unregister_class(addon_updater_check_now)
- bpy.utils.unregister_class(addon_updater_update_now)
- bpy.utils.unregister_class(addon_updater_update_target)
- bpy.utils.unregister_class(addon_updater_install_manually)
- bpy.utils.unregister_class(addon_updater_updated_successful)
- bpy.utils.unregister_class(addon_updater_restore_backup)
- bpy.utils.unregister_class(addon_updater_ignore)
- bpy.utils.unregister_class(addon_updater_end_background)
-
- # clear global vars since they may persist if not restarting blender
- updater.clear_state() # clear internal vars, avoids reloading oddities
-
- global ran_autocheck_install_popup
- ran_autocheck_install_popup = False
-
- global ran_update_sucess_popup
- ran_update_sucess_popup = False
-
- global ran_background_check
- ran_background_check = False
diff --git a/uv_magic_uv/common.py b/uv_magic_uv/common.py
index bad88167..83c6ae74 100644
--- a/uv_magic_uv/common.py
+++ b/uv_magic_uv/common.py
@@ -41,10 +41,10 @@ __all__ = [
'debug_print',
'check_version',
'redraw_all_areas',
- 'get_space',
- 'mouse_on_region',
- 'mouse_on_area',
- 'mouse_on_regions',
+ 'get_space_legacy',
+ 'mouse_on_region_legacy',
+ 'mouse_on_area_legacy',
+ 'mouse_on_regions_legacy',
'create_bmesh',
'create_new_uv_map',
'get_island_info',
@@ -54,7 +54,7 @@ __all__ = [
'calc_polygon_2d_area',
'calc_polygon_3d_area',
'measure_mesh_area',
- 'measure_uv_area',
+ 'measure_uv_area_legacy',
'diff_point_to_segment',
'get_loop_sequences',
'get_overlapped_uv_info',
@@ -119,6 +119,70 @@ def redraw_all_areas():
area.tag_redraw()
+def get_space_legacy(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:
+ break
+ for space in area.spaces:
+ if space.type == space_type:
+ break
+
+ return (area, region, space)
+
+
+def mouse_on_region_legacy(event, area_type, region_type):
+ pos = Vector((event.mouse_x, event.mouse_y))
+
+ _, region, _ = get_space_legacy(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_legacy(event, area_type):
+ pos = Vector((event.mouse_x, event.mouse_y))
+
+ area, _, _ = get_space_legacy(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_legacy(event, area_type, regions):
+ if not mouse_on_area_legacy(event, area_type):
+ return False
+
+ for region in regions:
+ result = mouse_on_region_legacy(event, area_type, region)
+ if result:
+ return True
+
+ return False
+
+
def get_space(area_type, region_type, space_type):
"""
Get current area/region/space
@@ -135,10 +199,16 @@ def get_space(area_type, region_type, space_type):
return (None, None, None)
for region in area.regions:
if region.type == region_type:
+ 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)
@@ -390,7 +460,7 @@ def measure_mesh_area(obj):
return mesh_area
-def measure_uv_area(obj, tex_size=None):
+def measure_uv_area_legacy(obj, tex_size=None):
bm = bmesh.from_edit_mesh(obj.data)
if check_version(2, 73, 0) >= 0:
bm.verts.ensure_lookup_table()
@@ -449,6 +519,88 @@ def measure_uv_area(obj, tex_size=None):
return uv_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")
+ 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()
@@ -941,6 +1093,9 @@ def __do_weiler_atherton_cliping(clip, subject, uv_layer, mode):
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:
@@ -949,10 +1104,20 @@ def __do_weiler_atherton_cliping(clip, subject, uv_layer, mode):
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())
@@ -975,6 +1140,9 @@ def __do_weiler_atherton_cliping(clip, subject, uv_layer, mode):
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
diff --git a/uv_magic_uv/impl/__init__.py b/uv_magic_uv/impl/__init__.py
index e69de29b..d22125af 100644
--- a/uv_magic_uv/impl/__init__.py
+++ b/uv_magic_uv/impl/__init__.py
@@ -0,0 +1,70 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+if "bpy" in locals():
+ import importlib
+ importlib.reload(align_uv_cursor_impl)
+ importlib.reload(align_uv_impl)
+ importlib.reload(copy_paste_uv_impl)
+ importlib.reload(copy_paste_uv_uvedit_impl)
+ importlib.reload(flip_rotate_impl)
+ importlib.reload(mirror_uv_impl)
+ importlib.reload(move_uv_impl)
+ importlib.reload(pack_uv_impl)
+ importlib.reload(preserve_uv_aspect_impl)
+ importlib.reload(select_uv_impl)
+ importlib.reload(smooth_uv_impl)
+ importlib.reload(texture_lock_impl)
+ importlib.reload(texture_wrap_impl)
+ importlib.reload(transfer_uv_impl)
+ importlib.reload(unwrap_constraint_impl)
+ importlib.reload(uv_bounding_box_impl)
+ importlib.reload(uv_inspection_impl)
+ importlib.reload(uv_sculpt_impl)
+ importlib.reload(uvw_impl)
+ importlib.reload(world_scale_uv_impl)
+else:
+ from . import align_uv_cursor_impl
+ from . import align_uv_impl
+ from . import copy_paste_uv_impl
+ from . import copy_paste_uv_uvedit_impl
+ from . import flip_rotate_impl
+ from . import mirror_uv_impl
+ from . import move_uv_impl
+ from . import pack_uv_impl
+ from . import preserve_uv_aspect_impl
+ from . import select_uv_impl
+ from . import smooth_uv_impl
+ from . import texture_lock_impl
+ from . import texture_wrap_impl
+ from . import transfer_uv_impl
+ from . import unwrap_constraint_impl
+ from . import uv_bounding_box_impl
+ from . import uv_inspection_impl
+ from . import uv_sculpt_impl
+ from . import uvw_impl
+ from . import world_scale_uv_impl
+
+import bpy
diff --git a/uv_magic_uv/impl/align_uv_cursor_impl.py b/uv_magic_uv/impl/align_uv_cursor_impl.py
new file mode 100644
index 00000000..3056e87b
--- /dev/null
+++ b/uv_magic_uv/impl/align_uv_cursor_impl.py
@@ -0,0 +1,239 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+from mathutils import Vector
+import bmesh
+
+from .. import common
+
+
+def _is_valid_context(context):
+ # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute.
+ # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf
+ # after the execution
+ for space in context.area.spaces:
+ if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'):
+ break
+ else:
+ return False
+
+ return True
+
+
+class AlignUVCursorLegacyImpl:
+ @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, ops_obj, context):
+ area, _, space = common.get_space_legacy('IMAGE_EDITOR', 'WINDOW',
+ 'IMAGE_EDITOR')
+ bd_size = common.get_uvimg_editor_board_size(area)
+
+ if ops_obj.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 ops_obj.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 ops_obj.base == 'TEXTURE':
+ min_ = Vector((0.0, 0.0))
+ max_ = Vector((1.0, 1.0))
+ center = Vector((0.5, 0.5))
+ else:
+ ops_obj.report({'ERROR'}, "Unknown Operation")
+ return {'CANCELLED'}
+
+ if ops_obj.position == 'CENTER':
+ cx = center.x * bd_size[0]
+ cy = center.y * bd_size[1]
+ elif ops_obj.position == 'LEFT_TOP':
+ cx = min_.x * bd_size[0]
+ cy = max_.y * bd_size[1]
+ elif ops_obj.position == 'LEFT_MIDDLE':
+ cx = min_.x * bd_size[0]
+ cy = center.y * bd_size[1]
+ elif ops_obj.position == 'LEFT_BOTTOM':
+ cx = min_.x * bd_size[0]
+ cy = min_.y * bd_size[1]
+ elif ops_obj.position == 'MIDDLE_TOP':
+ cx = center.x * bd_size[0]
+ cy = max_.y * bd_size[1]
+ elif ops_obj.position == 'MIDDLE_BOTTOM':
+ cx = center.x * bd_size[0]
+ cy = min_.y * bd_size[1]
+ elif ops_obj.position == 'RIGHT_TOP':
+ cx = max_.x * bd_size[0]
+ cy = max_.y * bd_size[1]
+ elif ops_obj.position == 'RIGHT_MIDDLE':
+ cx = max_.x * bd_size[0]
+ cy = center.y * bd_size[1]
+ elif ops_obj.position == 'RIGHT_BOTTOM':
+ cx = max_.x * bd_size[0]
+ cy = min_.y * bd_size[1]
+ else:
+ ops_obj.report({'ERROR'}, "Unknown Operation")
+ return {'CANCELLED'}
+
+ space.cursor_location = Vector((cx, cy))
+
+ return {'FINISHED'}
+
+
+class AlignUVCursorImpl:
+ @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, ops_obj, context):
+ _, _, space = common.get_space_legacy('IMAGE_EDITOR', 'WINDOW',
+ 'IMAGE_EDITOR')
+
+ if ops_obj.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 ops_obj.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 ops_obj.base == 'TEXTURE':
+ min_ = Vector((0.0, 0.0))
+ max_ = Vector((1.0, 1.0))
+ center = Vector((0.5, 0.5))
+ else:
+ ops_obj.report({'ERROR'}, "Unknown Operation")
+ return {'CANCELLED'}
+
+ if ops_obj.position == 'CENTER':
+ cx = center.x
+ cy = center.y
+ elif ops_obj.position == 'LEFT_TOP':
+ cx = min_.x
+ cy = max_.y
+ elif ops_obj.position == 'LEFT_MIDDLE':
+ cx = min_.x
+ cy = center.y
+ elif ops_obj.position == 'LEFT_BOTTOM':
+ cx = min_.x
+ cy = min_.y
+ elif ops_obj.position == 'MIDDLE_TOP':
+ cx = center.x
+ cy = max_.y
+ elif ops_obj.position == 'MIDDLE_BOTTOM':
+ cx = center.x
+ cy = min_.y
+ elif ops_obj.position == 'RIGHT_TOP':
+ cx = max_.x
+ cy = max_.y
+ elif ops_obj.position == 'RIGHT_MIDDLE':
+ cx = max_.x
+ cy = center.y
+ elif ops_obj.position == 'RIGHT_BOTTOM':
+ cx = max_.x
+ cy = min_.y
+ else:
+ ops_obj.report({'ERROR'}, "Unknown Operation")
+ return {'CANCELLED'}
+
+ space.cursor_location = Vector((cx, cy))
+
+ return {'FINISHED'}
diff --git a/uv_magic_uv/impl/align_uv_impl.py b/uv_magic_uv/impl/align_uv_impl.py
new file mode 100644
index 00000000..b8d7d33d
--- /dev/null
+++ b/uv_magic_uv/impl/align_uv_impl.py
@@ -0,0 +1,820 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import math
+from math import atan2, tan, sin, cos
+
+import bmesh
+from mathutils import Vector
+
+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
+
+
+class CircleImpl:
+ @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, ops_obj, 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:
+ ops_obj.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:
+ ops_obj.report({'WARNING'}, "Last face must be triangle")
+ return {'CANCELLED'}
+ if hseq[-1][0].vert != center:
+ ops_obj.report({'WARNING'}, "Center must be identical")
+ return {'CANCELLED'}
+
+ # align to circle
+ if ops_obj.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 ops_obj.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 ops_obj.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 ops_obj.select:
+ pair[0][uv_layer].select = True
+ pair[1][uv_layer].select = True
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
+
+
+# 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)
+ print("#### {}".format(tgt_noinfl))
+ print("#### {}".format(tgt_infl))
+
+ # 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)
+
+
+class StraightenImpl:
+ @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, ops_obj, 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 ops_obj.horizontal:
+ hdiff_uvs = [
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
+ ops_obj.mesh_infl),
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
+ ops_obj.mesh_infl),
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 0, ops_obj.mesh_infl),
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 1, ops_obj.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 ops_obj.vertical:
+ vdiff_uvs = [
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
+ ops_obj.mesh_infl),
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
+ ops_obj.mesh_infl),
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 0, ops_obj.mesh_infl),
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 1, ops_obj.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 ops_obj.select:
+ l[uv_layer].select = True
+
+ # only selected UV loop sequence will be aligned
+ def __align_wo_transmission(self, ops_obj, 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 ops_obj.select:
+ pair[0][uv_layer].select = True
+ pair[1][uv_layer].select = True
+
+ def __align(self, ops_obj, loop_seqs, uv_layer):
+ if ops_obj.transmission:
+ self.__align_w_transmission(ops_obj, loop_seqs, uv_layer)
+ else:
+ self.__align_wo_transmission(ops_obj, loop_seqs, uv_layer)
+
+ def execute(self, ops_obj, 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:
+ ops_obj.report({'WARNING'}, error)
+ return {'CANCELLED'}
+
+ # align
+ self.__align(ops_obj, loop_seqs, uv_layer)
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
+
+
+class AxisImpl:
+ @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, ops_obj, 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 ops_obj.location == 'RIGHT_BOTTOM':
+ target_uv0.y = target_uv1.y = uv_min.y
+ elif ops_obj.location == 'MIDDLE':
+ target_uv0.y = target_uv1.y = uv_min.y + height * 0.5
+ elif ops_obj.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, ops_obj, 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 ops_obj.location == 'RIGHT_BOTTOM':
+ target_uv0.x = target_uv1.x = uv_min.x + width
+ elif ops_obj.location == 'MIDDLE':
+ target_uv0.x = target_uv1.x = uv_min.x + width * 0.5
+ elif ops_obj.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, ops_obj, 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(ops_obj, 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, ops_obj, 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(ops_obj, 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, ops_obj, 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(ops_obj, 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 ops_obj.horizontal:
+ hdiff_uvs = [
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
+ ops_obj.mesh_infl),
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
+ ops_obj.mesh_infl),
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 0, ops_obj.mesh_infl),
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 1, ops_obj.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 ops_obj.vertical:
+ vdiff_uvs = [
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
+ ops_obj.mesh_infl),
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
+ ops_obj.mesh_infl),
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 0, ops_obj.mesh_infl),
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 1, ops_obj.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 ops_obj.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, ops_obj, 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(ops_obj, 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 ops_obj.horizontal:
+ hdiff_uvs = [
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
+ ops_obj.mesh_infl),
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
+ ops_obj.mesh_infl),
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 0, ops_obj.mesh_infl),
+ _get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 1, ops_obj.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 ops_obj.vertical:
+ vdiff_uvs = [
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0,
+ ops_obj.mesh_infl),
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1,
+ ops_obj.mesh_infl),
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 0, ops_obj.mesh_infl),
+ _get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1,
+ hidx, 1, ops_obj.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 ops_obj.select:
+ l[uv_layer].select = True
+
+ def __align(self, ops_obj, loop_seqs, uv_layer, uv_min, width, height):
+ # align along to x-axis
+ if width > height:
+ if ops_obj.transmission:
+ self.__align_to_x_axis_w_transmission(ops_obj, loop_seqs,
+ uv_layer, uv_min,
+ width, height)
+ else:
+ self.__align_to_x_axis_wo_transmission(ops_obj, loop_seqs,
+ uv_layer, uv_min,
+ width, height)
+ # align along to y-axis
+ else:
+ if ops_obj.transmission:
+ self.__align_to_y_axis_w_transmission(ops_obj, loop_seqs,
+ uv_layer, uv_min,
+ width, height)
+ else:
+ self.__align_to_y_axis_wo_transmission(ops_obj, loop_seqs,
+ uv_layer, uv_min,
+ width, height)
+
+ def execute(self, ops_obj, 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:
+ ops_obj.report({'WARNING'}, error)
+ return {'CANCELLED'}
+
+ # get height and width
+ uv_max, uv_min = self.__get_uv_max_min(ops_obj, loop_seqs, uv_layer)
+ width = uv_max.x - uv_min.x
+ height = uv_max.y - uv_min.y
+
+ self.__align(ops_obj, loop_seqs, uv_layer, uv_min, width, height)
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
diff --git a/uv_magic_uv/impl/move_uv_impl.py b/uv_magic_uv/impl/move_uv_impl.py
index 4340e577..ce507fba 100644
--- a/uv_magic_uv/impl/move_uv_impl.py
+++ b/uv_magic_uv/impl/move_uv_impl.py
@@ -132,7 +132,7 @@ class MoveUVImpl():
bmesh.update_edit_mesh(obj.data)
# check mouse preference
- if context.preferences.inputs.select_mouse == 'RIGHT':
+ if context.user_preferences.inputs.select_mouse == 'RIGHT':
confirm_btn = 'LEFTMOUSE'
cancel_btn = 'RIGHTMOUSE'
else:
diff --git a/uv_magic_uv/impl/pack_uv_impl.py b/uv_magic_uv/impl/pack_uv_impl.py
new file mode 100644
index 00000000..49f954f3
--- /dev/null
+++ b/uv_magic_uv/impl/pack_uv_impl.py
@@ -0,0 +1,202 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+from math import fabs
+
+import bpy
+import bmesh
+import mathutils
+from mathutils import Vector
+
+from .. import common
+
+
+__all__ = [
+ 'PackUVImpl',
+]
+
+
+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
+
+
+class PackUVImpl:
+ @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, ops_obj, 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:
+ ops_obj.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,
+ ops_obj.allowable_center_deviation,
+ ops_obj.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=ops_obj.rotate, margin=ops_obj.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/uv_magic_uv/impl/preserve_uv_aspect_impl.py b/uv_magic_uv/impl/preserve_uv_aspect_impl.py
new file mode 100644
index 00000000..622ee1d3
--- /dev/null
+++ b/uv_magic_uv/impl/preserve_uv_aspect_impl.py
@@ -0,0 +1,359 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+import bmesh
+from mathutils import Vector
+
+from .. import common
+
+
+__all__ = [
+ 'PreserveUVAspectLegacyImpl',
+]
+
+
+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
+
+
+class PreserveUVAspectLegacyImpl:
+ @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, ops_obj, 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:
+ ops_obj.report({'WARNING'},
+ "Object must have more than one UV map")
+ return {'CANCELLED'}
+
+ uv_layer = bm.loops.layers.uv.verify()
+ tex_layer = bm.faces.layers.tex.verify()
+
+ sel_faces = [f for f in bm.faces if f.select]
+ dest_img = bpy.data.images[ops_obj.dest_img_name]
+
+ info = {}
+
+ 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 ops_obj.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 ops_obj.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 ops_obj.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 ops_obj.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 ops_obj.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 ops_obj.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 ops_obj.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 ops_obj.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 ops_obj.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)
+
+ info[img]['ratio'] = ratio
+ info[img]['origin'] = origin
+
+ for img in info:
+ if img is None:
+ continue
+
+ for f in info[img]['faces']:
+ 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'}
+
+
+class PreserveUVAspectImpl:
+ @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, ops_obj, 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:
+ ops_obj.report({'WARNING'},
+ "Object must have more than one UV map")
+ return {'CANCELLED'}
+
+ uv_layer = bm.loops.layers.uv.verify()
+ tex_image = common.find_image(obj)
+
+ sel_faces = [f for f in bm.faces if f.select]
+ dest_img = bpy.data.images[ops_obj.dest_img_name]
+
+ info = {}
+
+ for f in sel_faces:
+ if not tex_image in info.keys():
+ info[tex_image] = {}
+ info[tex_image]['faces'] = []
+ info[tex_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 ops_obj.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 ops_obj.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 ops_obj.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 ops_obj.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 ops_obj.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 ops_obj.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 ops_obj.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 ops_obj.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 ops_obj.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:
+ ops_obj.report({'ERROR'}, "Unknown Operation")
+ return {'CANCELLED'}
+
+ info[img]['ratio'] = ratio
+ info[img]['origin'] = origin
+
+ for img in info:
+ if img is None:
+ continue
+
+ nodes = common.find_texture_nodes(obj)
+ nodes[0].image = dest_img
+
+ for f in info[img]['faces']:
+ 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'} \ No newline at end of file
diff --git a/uv_magic_uv/impl/select_uv_impl.py b/uv_magic_uv/impl/select_uv_impl.py
new file mode 100644
index 00000000..dbcaee7e
--- /dev/null
+++ b/uv_magic_uv/impl/select_uv_impl.py
@@ -0,0 +1,120 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bmesh
+
+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
+
+
+class SelectOverlappedImpl:
+ @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'}
+
+
+class SelectFlippedImpl:
+ @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/uv_magic_uv/impl/smooth_uv_impl.py b/uv_magic_uv/impl/smooth_uv_impl.py
new file mode 100644
index 00000000..dbc8afad
--- /dev/null
+++ b/uv_magic_uv/impl/smooth_uv_impl.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__ = "imdjs, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bmesh
+
+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
+
+
+class SmoothUVImpl:
+ @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, ops_obj, 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 ops_obj.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 - ops_obj.mesh_infl) + \
+ tgt_infl * ops_obj.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:
+ ops_obj.report({'ERROR'}, "Failed to get target UV")
+ return {'CANCELLED'}
+
+ # update UV
+ l[uv_layer].uv = target_uv
+
+ def __smooth_w_transmission(self, ops_obj, 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 ops_obj.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 - ops_obj.mesh_infl) + \
+ tgt_infl * ops_obj.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:
+ ops_obj.report({'ERROR'}, "Failed to get target UV")
+ return {'CANCELLED'}
+
+ # update UV
+ l[uv_layer].uv = target_uv
+
+ def __smooth(self, ops_obj, loop_seqs, uv_layer):
+ if ops_obj.transmission:
+ self.__smooth_w_transmission(ops_obj, loop_seqs, uv_layer)
+ else:
+ self.__smooth_wo_transmission(ops_obj, loop_seqs, uv_layer)
+
+ def execute(self, ops_obj, 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:
+ ops_obj.report({'WARNING'}, error)
+ return {'CANCELLED'}
+
+ # smooth
+ self.__smooth(ops_obj, loop_seqs, uv_layer)
+
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
diff --git a/uv_magic_uv/impl/texture_lock_impl.py b/uv_magic_uv/impl/texture_lock_impl.py
new file mode 100644
index 00000000..c14eddb0
--- /dev/null
+++ b/uv_magic_uv/impl/texture_lock_impl.py
@@ -0,0 +1,455 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import math
+from math import atan2, cos, sqrt, sin, fabs
+
+import bpy
+import bmesh
+from mathutils import Vector
+
+from .. import common
+
+
+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
+
+
+class LockImpl:
+ @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, ops_obj, 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:
+ ops_obj.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'}
+
+
+class UnlockImpl:
+ @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 LockImpl.is_ready(context):
+ return False
+ if not _is_valid_context(context):
+ return False
+ return True
+
+ def execute(self, ops_obj, 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:
+ ops_obj.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"]:
+ ops_obj.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 ops_obj.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'}
+
+
+class IntrImpl:
+ __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, ops_obj, 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:
+ ops_obj.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"]:
+ ops_obj.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, ops_obj, context, event):
+ if not _is_valid_context(context):
+ IntrImpl.handle_remove(context)
+ return {'FINISHED'}
+
+ if not IntrImpl.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(ops_obj, context)
+
+ return {'PASS_THROUGH'}
+
+ def invoke(self, ops_obj, context, _):
+ if not _is_valid_context(context):
+ return {'CANCELLED'}
+
+ if not IntrImpl.is_running(context):
+ IntrImpl.handle_add(ops_obj, context)
+ return {'RUNNING_MODAL'}
+ else:
+ IntrImpl.handle_remove(context)
+
+ if context.area:
+ context.area.tag_redraw()
+
+ return {'FINISHED'}
diff --git a/uv_magic_uv/impl/texture_projection_impl.py b/uv_magic_uv/impl/texture_projection_impl.py
new file mode 100644
index 00000000..b64fa374
--- /dev/null
+++ b/uv_magic_uv/impl/texture_projection_impl.py
@@ -0,0 +1,126 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+from collections import namedtuple
+
+import bpy
+import mathutils
+
+
+_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
+ prefs = context.user_preferences.addons["uv_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
diff --git a/uv_magic_uv/impl/texture_wrap_impl.py b/uv_magic_uv/impl/texture_wrap_impl.py
new file mode 100644
index 00000000..336b1760
--- /dev/null
+++ b/uv_magic_uv/impl/texture_wrap_impl.py
@@ -0,0 +1,236 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bmesh
+
+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
+
+
+class ReferImpl:
+ @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, ops_obj, 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:
+ ops_obj.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:
+ ops_obj.report({'WARNING'}, "Must select only one face")
+ return {'CANCELLED'}
+
+ props.ref_face_index = sel_faces[0].index
+ props.ref_obj = obj
+
+ return {'FINISHED'}
+
+
+class SetImpl:
+ @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, ops_obj, 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:
+ ops_obj.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:
+ ops_obj.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:
+ ops_obj.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:
+ ops_obj.report({'WARNING'}, "Must select different face")
+ return {'CANCELLED'}
+
+ if props.ref_obj != obj:
+ ops_obj.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:
+ ops_obj.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:
+ ops_obj.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:
+ ops_obj.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/uv_magic_uv/impl/unwrap_constraint_impl.py b/uv_magic_uv/impl/unwrap_constraint_impl.py
new file mode 100644
index 00000000..25719798
--- /dev/null
+++ b/uv_magic_uv/impl/unwrap_constraint_impl.py
@@ -0,0 +1,98 @@
+# ##### 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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+import bmesh
+
+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
+
+
+class UnwrapConstraintImpl:
+ @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, ops_obj, 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:
+ ops_obj.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=ops_obj.method,
+ fill_holes=ops_obj.fill_holes,
+ correct_aspect=ops_obj.correct_aspect,
+ use_subsurf_data=ops_obj.use_subsurf_data,
+ margin=ops_obj.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 ops_obj.u_const:
+ l[uv_layer].uv.x = uv.x
+ if ops_obj.v_const:
+ l[uv_layer].uv.y = uv.y
+
+ # update mesh
+ bmesh.update_edit_mesh(obj.data)
+
+ return {'FINISHED'}
diff --git a/uv_magic_uv/impl/uv_bounding_box_impl.py b/uv_magic_uv/impl/uv_bounding_box_impl.py
new file mode 100644
index 00000000..bce51f3e
--- /dev/null
+++ b/uv_magic_uv/impl/uv_bounding_box_impl.py
@@ -0,0 +1,55 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+from enum import IntEnum
+import math
+
+import mathutils
+
+
+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
diff --git a/uv_magic_uv/impl/uv_inspection_impl.py b/uv_magic_uv/impl/uv_inspection_impl.py
new file mode 100644
index 00000000..caa3aa79
--- /dev/null
+++ b/uv_magic_uv/impl/uv_inspection_impl.py
@@ -0,0 +1,70 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bmesh
+
+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 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)
diff --git a/uv_magic_uv/impl/uv_sculpt_impl.py b/uv_magic_uv/impl/uv_sculpt_impl.py
new file mode 100644
index 00000000..284787d8
--- /dev/null
+++ b/uv_magic_uv/impl/uv_sculpt_impl.py
@@ -0,0 +1,57 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+
+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_
diff --git a/uv_magic_uv/impl/uvw_impl.py b/uv_magic_uv/impl/uvw_impl.py
index e815f54f..98da0dc9 100644
--- a/uv_magic_uv/impl/uvw_impl.py
+++ b/uv_magic_uv/impl/uvw_impl.py
@@ -28,6 +28,8 @@ from math import sin, cos, pi
from mathutils import Vector
+from .. import common
+
def is_valid_context(context):
obj = context.object
@@ -144,7 +146,11 @@ def apply_planer_map(bm, uv_layer, size, offset, rotation, tex_aspect):
# update UV coordinate
for f in sel_faces:
for l in f.loops:
- co = q @ l.vert.co
+ if common.check_version(2, 80, 0) >= 0:
+ # pylint: disable=E0001
+ co = q @ l.vert.co
+ else:
+ co = q * l.vert.co
x = co.x * sx
y = co.y * sy
diff --git a/uv_magic_uv/impl/world_scale_uv_impl.py b/uv_magic_uv/impl/world_scale_uv_impl.py
new file mode 100644
index 00000000..3f376f0d
--- /dev/null
+++ b/uv_magic_uv/impl/world_scale_uv_impl.py
@@ -0,0 +1,383 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+from math import sqrt
+
+import bmesh
+from mathutils import Vector
+
+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 _measure_wsuv_info(obj, tex_size=None):
+ mesh_area = common.measure_mesh_area(obj)
+ if common.check_version(2, 80, 0) >= 0:
+ uv_area = common.measure_uv_area(obj, tex_size)
+ else:
+ uv_area = common.measure_uv_area_legacy(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)
+
+
+class MeasureImpl:
+ @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, ops_obj, context):
+ sc = context.scene
+ obj = context.active_object
+
+ uv_area, mesh_area, density = _measure_wsuv_info(obj)
+ if not uv_area:
+ ops_obj.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
+
+ ops_obj.report({'INFO'},
+ "UV Area: {0}, Mesh Area: {1}, Texel Density: {2}"
+ .format(uv_area, mesh_area, density))
+
+ return {'FINISHED'}
+
+
+class ApplyManualImpl:
+ @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, ops_obj, 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 = ops_obj.tgt_texture_size
+ uv_area, _, density = _measure_wsuv_info(obj, tex_size)
+ if not uv_area:
+ ops_obj.report({'WARNING'},
+ "Object must have more than one UV map")
+ return {'CANCELLED'}
+
+ tgt_density = ops_obj.tgt_density
+ factor = tgt_density / density
+
+ _apply(context.active_object, ops_obj.origin, factor)
+ ops_obj.report({'INFO'}, "Scaling factor: {0}".format(factor))
+
+ return {'FINISHED'}
+
+ def draw(self, ops_obj, _):
+ layout = ops_obj.layout
+
+ layout.prop(ops_obj, "tgt_density")
+ layout.prop(ops_obj, "tgt_texture_size")
+ layout.prop(ops_obj, "origin")
+
+ layout.separator()
+
+ def invoke(self, ops_obj, context, _):
+ if ops_obj.show_dialog:
+ wm = context.window_manager
+ return wm.invoke_props_dialog(ops_obj)
+
+ return ops_obj.execute(context)
+
+ def execute(self, ops_obj, context):
+ return self.__apply_manual(ops_obj, context)
+
+
+class ApplyScalingDensityImpl:
+ @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, ops_obj, 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:
+ ops_obj.report({'WARNING'},
+ "Object must have more than one UV map and texture")
+ return {'CANCELLED'}
+
+ tgt_density = ops_obj.src_density * ops_obj.tgt_scaling_factor
+ factor = tgt_density / density
+
+ _apply(context.active_object, ops_obj.origin, factor)
+ ops_obj.report({'INFO'}, "Scaling factor: {0}".format(factor))
+
+ return {'FINISHED'}
+
+ def draw(self, ops_obj, _):
+ layout = ops_obj.layout
+
+ layout.label(text="Source:")
+ col = layout.column()
+ col.prop(ops_obj, "src_density")
+ col.enabled = False
+
+ layout.separator()
+
+ if not ops_obj.same_density:
+ layout.prop(ops_obj, "tgt_scaling_factor")
+ layout.prop(ops_obj, "origin")
+
+ layout.separator()
+
+ def invoke(self, ops_obj, context, _):
+ sc = context.scene
+
+ if ops_obj.show_dialog:
+ wm = context.window_manager
+
+ if ops_obj.same_density:
+ ops_obj.tgt_scaling_factor = 1.0
+ else:
+ ops_obj.tgt_scaling_factor = \
+ sc.muv_world_scale_uv_tgt_scaling_factor
+ ops_obj.src_density = sc.muv_world_scale_uv_src_density
+
+ return wm.invoke_props_dialog(ops_obj)
+
+ return ops_obj.execute(context)
+
+ def execute(self, ops_obj, context):
+ if ops_obj.same_density:
+ ops_obj.tgt_scaling_factor = 1.0
+
+ return self.__apply_scaling_density(ops_obj, context)
+
+
+class ApplyProportionalToMeshImpl:
+ @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, ops_obj, 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:
+ ops_obj.report({'WARNING'},
+ "Object must have more than one UV map and texture")
+ return {'CANCELLED'}
+
+ tgt_density = ops_obj.src_density * sqrt(mesh_area) / sqrt(
+ ops_obj.src_mesh_area)
+
+ factor = tgt_density / density
+
+ _apply(context.active_object, ops_obj.origin, factor)
+ ops_obj.report({'INFO'}, "Scaling factor: {0}".format(factor))
+
+ return {'FINISHED'}
+
+ def draw(self, ops_obj, _):
+ layout = ops_obj.layout
+
+ layout.label(text="Source:")
+ col = layout.column(align=True)
+ col.prop(ops_obj, "src_density")
+ col.prop(ops_obj, "src_uv_area")
+ col.prop(ops_obj, "src_mesh_area")
+ col.enabled = False
+
+ layout.separator()
+ layout.prop(ops_obj, "origin")
+
+ layout.separator()
+
+ def invoke(self, ops_obj, context, _):
+ if ops_obj.show_dialog:
+ wm = context.window_manager
+ sc = context.scene
+
+ ops_obj.src_density = sc.muv_world_scale_uv_src_density
+ ops_obj.src_mesh_area = sc.muv_world_scale_uv_src_mesh_area
+
+ return wm.invoke_props_dialog(ops_obj)
+
+ return ops_obj.execute(context)
+
+ def execute(self, ops_obj, context):
+ return self.__apply_proportional_to_mesh(ops_obj, context)
diff --git a/uv_magic_uv/legacy/op/align_uv.py b/uv_magic_uv/legacy/op/align_uv.py
index 9d0ff5f4..a274d583 100644
--- a/uv_magic_uv/legacy/op/align_uv.py
+++ b/uv_magic_uv/legacy/op/align_uv.py
@@ -23,52 +23,16 @@ __status__ = "production"
__version__ = "5.2"
__date__ = "17 Nov 2018"
-import math
-from math import atan2, tan, sin, cos
-
import bpy
-import bmesh
-from mathutils import Vector
from bpy.props import EnumProperty, BoolProperty, FloatProperty
-from ... import common
from ...utils.bl_class_registry import BlClassRegistry
from ...utils.property_class_registry import PropertyClassRegistry
-
-
-__all__ = [
- 'Properties',
- 'MUV_OT_AlignUV_Circle',
- 'MUV_OT_AlignUV_Straighten',
- 'MUV_OT_AlignUV_Axis',
-]
-
-
-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
+from ...impl import align_uv_impl as impl
@PropertyClassRegistry(legacy=True)
-class Properties:
+class _Properties:
idname = "align_uv"
@classmethod
@@ -129,58 +93,6 @@ class Properties:
del scene.muv_align_uv_location
-# 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
-
-
@BlClassRegistry(legacy=True)
class MUV_OT_AlignUV_Circle(bpy.types.Operator):
@@ -200,247 +112,15 @@ class MUV_OT_AlignUV_Circle(bpy.types.Operator):
default=False
)
+ def __init__(self):
+ self.__impl = impl.CircleImpl()
+
@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)
+ return impl.CircleImpl.poll(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'}
-
-
-# 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 accum_uvlens[i + 1] != target_length:
- 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) / len(loop_seqs)
- 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 accum_uvlens[i + 1] != target_length:
- 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)
+ return self.__impl.execute(self, context)
@BlClassRegistry(legacy=True)
@@ -481,117 +161,15 @@ class MUV_OT_AlignUV_Straighten(bpy.types.Operator):
default=0.0
)
+ def __init__(self):
+ self.__impl = impl.StraightenImpl()
+
@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)
+ return impl.StraightenImpl.poll(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)
- 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'}
+ return self.__impl.execute(self, context)
@BlClassRegistry(legacy=True)
@@ -642,347 +220,12 @@ class MUV_OT_AlignUV_Axis(bpy.types.Operator):
default=0.0
)
+ def __init__(self):
+ self.__impl = impl.AxisImpl()
+
@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)
+ return impl.AxisImpl.poll(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)
- 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'}
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/legacy/op/align_uv_cursor.py b/uv_magic_uv/legacy/op/align_uv_cursor.py
index ec3e7036..6a08de0b 100644
--- a/uv_magic_uv/legacy/op/align_uv_cursor.py
+++ b/uv_magic_uv/legacy/op/align_uv_cursor.py
@@ -26,41 +26,22 @@ __date__ = "17 Nov 2018"
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
-
-
-__all__ = [
- 'Properties',
- 'MUV_OT_AlignUVCursor',
-]
-
-
-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
+from ...impl import align_uv_cursor_impl as impl
@PropertyClassRegistry(legacy=True)
-class Properties:
+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')
+ area, _, space = common.get_space_legacy('IMAGE_EDITOR', 'WINDOW',
+ 'IMAGE_EDITOR')
bd_size = common.get_uvimg_editor_board_size(area)
loc = space.cursor_location
if bd_size[0] < 0.000001:
@@ -76,8 +57,8 @@ class Properties:
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')
+ area, _, space = common.get_space_legacy('IMAGE_EDITOR', 'WINDOW',
+ 'IMAGE_EDITOR')
bd_size = common.get_uvimg_editor_board_size(area)
cx = bd_size[0] * value[0]
cy = bd_size[1] * value[1]
@@ -161,97 +142,12 @@ class MUV_OT_AlignUVCursor(bpy.types.Operator):
default='TEXTURE'
)
+ def __init__(self):
+ self.__impl = impl.AlignUVCursorLegacyImpl()
+
@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)
+ return impl.AlignUVCursorLegacyImpl.poll(context)
def execute(self, context):
- area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW',
- 'IMAGE_EDITOR')
- bd_size = common.get_uvimg_editor_board_size(area)
-
- 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")
-
- if self.position == 'CENTER':
- cx = center.x * bd_size[0]
- cy = center.y * bd_size[1]
- elif self.position == 'LEFT_TOP':
- cx = min_.x * bd_size[0]
- cy = max_.y * bd_size[1]
- elif self.position == 'LEFT_MIDDLE':
- cx = min_.x * bd_size[0]
- cy = center.y * bd_size[1]
- elif self.position == 'LEFT_BOTTOM':
- cx = min_.x * bd_size[0]
- cy = min_.y * bd_size[1]
- elif self.position == 'MIDDLE_TOP':
- cx = center.x * bd_size[0]
- cy = max_.y * bd_size[1]
- elif self.position == 'MIDDLE_BOTTOM':
- cx = center.x * bd_size[0]
- cy = min_.y * bd_size[1]
- elif self.position == 'RIGHT_TOP':
- cx = max_.x * bd_size[0]
- cy = max_.y * bd_size[1]
- elif self.position == 'RIGHT_MIDDLE':
- cx = max_.x * bd_size[0]
- cy = center.y * bd_size[1]
- elif self.position == 'RIGHT_BOTTOM':
- cx = max_.x * bd_size[0]
- cy = min_.y * bd_size[1]
- else:
- self.report({'ERROR'}, "Unknown Operation")
-
- space.cursor_location = Vector((cx, cy))
-
- return {'FINISHED'}
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/legacy/op/pack_uv.py b/uv_magic_uv/legacy/op/pack_uv.py
index f8d58843..f2e1a190 100644
--- a/uv_magic_uv/legacy/op/pack_uv.py
+++ b/uv_magic_uv/legacy/op/pack_uv.py
@@ -23,21 +23,16 @@ __status__ = "production"
__version__ = "5.2"
__date__ = "17 Nov 2018"
-from math import fabs
-
import bpy
-import bmesh
-import mathutils
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 ...impl import pack_uv_impl as impl
__all__ = [
@@ -46,29 +41,6 @@ __all__ = [
]
-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(legacy=True)
class Properties:
idname = "pack_uv"
@@ -146,136 +118,12 @@ class MUV_OT_PackUV(bpy.types.Operator):
size=2
)
+ def __init__(self):
+ self.__impl = impl.PackUVImpl()
+
@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)
+ return impl.PackUVImpl.poll(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 = self.__group_island(island_info)
-
- 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'}
-
- def __sort_island_faces(self, 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(self, island_info):
- """
- 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) < self.allowable_center_deviation[0]
- )
- center_y_matched = (
- fabs(dcy) < self.allowable_center_deviation[1]
- )
- size_x_matched = (
- fabs(dsx) < self.allowable_size_deviation[0]
- )
- size_y_matched = (
- fabs(dsy) < self.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'] = self.__sort_island_faces(
- kd, uvs, isl_1, isl_2)
- num_group = num_group + 1
-
- return num_group
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/legacy/op/preserve_uv_aspect.py b/uv_magic_uv/legacy/op/preserve_uv_aspect.py
index cf9349bc..c6693e9a 100644
--- a/uv_magic_uv/legacy/op/preserve_uv_aspect.py
+++ b/uv_magic_uv/legacy/op/preserve_uv_aspect.py
@@ -24,13 +24,11 @@ __version__ = "5.2"
__date__ = "17 Nov 2018"
import bpy
-import bmesh
from bpy.props import StringProperty, EnumProperty, BoolProperty
-from mathutils import Vector
-from ... import common
from ...utils.bl_class_registry import BlClassRegistry
from ...utils.property_class_registry import PropertyClassRegistry
+from ...impl import preserve_uv_aspect_impl as impl
__all__ = [
@@ -39,27 +37,6 @@ __all__ = [
]
-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(legacy=True)
class Properties:
idname = "preserve_uv_aspect"
@@ -136,148 +113,12 @@ class MUV_OT_PreserveUVAspect(bpy.types.Operator):
default="CENTER"
)
+ def __init__(self):
+ self.__impl = impl.PreserveUVAspectLegacyImpl()
+
@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)
+ return impl.PreserveUVAspectLegacyImpl.poll(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()
- tex_layer = bm.faces.layers.tex.verify()
-
- sel_faces = [f for f in bm.faces if f.select]
- dest_img = bpy.data.images[self.dest_img_name]
-
- info = {}
-
- 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)
-
- info[img]['ratio'] = ratio
- info[img]['origin'] = origin
-
- for img in info:
- if img is None:
- continue
-
- for f in info[img]['faces']:
- 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'}
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/legacy/op/select_uv.py b/uv_magic_uv/legacy/op/select_uv.py
index bdc182d5..c4a7639d 100644
--- a/uv_magic_uv/legacy/op/select_uv.py
+++ b/uv_magic_uv/legacy/op/select_uv.py
@@ -24,46 +24,15 @@ __version__ = "5.2"
__date__ = "17 Nov 2018"
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
-
-
-__all__ = [
- 'Properties',
- 'MUV_OT_SelectUV_SelectFlipped',
- 'MUV_OT_SelectUV_SelectOverlapped',
-]
-
-
-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
+from ...impl import select_uv_impl as impl
@PropertyClassRegistry(legacy=True)
-class Properties:
+class _Properties:
idname = "select_uv"
@classmethod
@@ -90,38 +59,15 @@ class MUV_OT_SelectUV_SelectOverlapped(bpy.types.Operator):
bl_description = "Select faces which have overlapped UVs"
bl_options = {'REGISTER', 'UNDO'}
+ def __init__(self):
+ self.__impl = impl.SelectOverlappedImpl()
+
@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)
+ return impl.SelectOverlappedImpl.poll(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'}
+ return self.__impl.execute(self, context)
@BlClassRegistry(legacy=True)
@@ -135,34 +81,12 @@ class MUV_OT_SelectUV_SelectFlipped(bpy.types.Operator):
bl_description = "Select faces which have flipped UVs"
bl_options = {'REGISTER', 'UNDO'}
+ def __init__(self):
+ self.__impl = impl.SelectFlippedImpl()
+
@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)
+ return impl.SelectFlippedImpl.poll(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'}
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/legacy/op/smooth_uv.py b/uv_magic_uv/legacy/op/smooth_uv.py
index 63062554..2e80e98c 100644
--- a/uv_magic_uv/legacy/op/smooth_uv.py
+++ b/uv_magic_uv/legacy/op/smooth_uv.py
@@ -24,45 +24,15 @@ __version__ = "5.2"
__date__ = "17 Nov 2018"
import bpy
-import bmesh
from bpy.props import BoolProperty, FloatProperty
-from ... import common
from ...utils.bl_class_registry import BlClassRegistry
from ...utils.property_class_registry import PropertyClassRegistry
-
-
-__all__ = [
- 'Properties',
- 'MUV_OT_SmoothUV',
-]
-
-
-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
+from ...impl import smooth_uv_impl as impl
@PropertyClassRegistry(legacy=True)
-class Properties:
+class _Properties:
idname = "smooth_uv"
@classmethod
@@ -124,164 +94,12 @@ class MUV_OT_SmoothUV(bpy.types.Operator):
default=False
)
+ def __init__(self):
+ self.__impl = impl.SmoothUVImpl()
+
@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)
+ return impl.SmoothUVImpl.poll(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)
- 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'}
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/legacy/op/texture_lock.py b/uv_magic_uv/legacy/op/texture_lock.py
index 65873106..ab06baf5 100644
--- a/uv_magic_uv/legacy/op/texture_lock.py
+++ b/uv_magic_uv/legacy/op/texture_lock.py
@@ -23,200 +23,16 @@ __status__ = "production"
__version__ = "5.2"
__date__ = "17 Nov 2018"
-import math
-from math import atan2, cos, sqrt, sin, fabs
-
import bpy
-import bmesh
-from mathutils import Vector
from bpy.props import BoolProperty
-from ... import common
from ...utils.bl_class_registry import BlClassRegistry
from ...utils.property_class_registry import PropertyClassRegistry
-
-
-__all__ = [
- 'Properties',
- 'MUV_OT_TextureLock_Lock',
- 'MUV_OT_TextureLock_Unlock',
- 'MUV_OT_TextureLock_Intr',
-]
-
-
-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
+from ...impl import texture_lock_impl as impl
@PropertyClassRegistry(legacy=True)
-class Properties:
+class _Properties:
idname = "texture_lock"
@classmethod
@@ -272,40 +88,19 @@ class MUV_OT_TextureLock_Lock(bpy.types.Operator):
bl_description = "Lock Texture"
bl_options = {'REGISTER', 'UNDO'}
+ def __init__(self):
+ self.__impl = impl.LockImpl()
+
@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)
+ return impl.LockImpl.poll(context)
@classmethod
def is_ready(cls, context):
- sc = context.scene
- props = sc.muv_props.texture_lock
- if props.verts_orig:
- return True
- return False
+ return impl.LockImpl.is_ready(context)
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'}
+ return self.__impl.execute(self, context)
@BlClassRegistry(legacy=True)
@@ -324,74 +119,15 @@ class MUV_OT_TextureLock_Unlock(bpy.types.Operator):
default=True
)
+ def __init__(self):
+ self.__impl = impl.UnlockImpl()
+
@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
+ return impl.UnlockImpl.poll(context)
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'}
+ return self.__impl.execute(self, context)
@BlClassRegistry(legacy=True)
@@ -404,142 +140,19 @@ class MUV_OT_TextureLock_Intr(bpy.types.Operator):
bl_label = "Texture Lock (Interactive mode)"
bl_description = "Internal operation for Texture Lock (Interactive mode)"
- __timer = None
+ def __init__(self):
+ self.__impl = impl.IntrImpl()
@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, self_, context):
- if cls.__timer is None:
- cls.__timer = context.window_manager.event_timer_add(
- 0.10, context.window)
- context.window_manager.modal_handler_add(self_)
+ return impl.IntrImpl.poll(context)
@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 is_running(cls, context):
+ return impl.IntrImpl.is_running(context)
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 self.__impl.modal(self, context, event)
- return {'FINISHED'}
+ def invoke(self, context, event):
+ return self.__impl.invoke(self, context, event)
diff --git a/uv_magic_uv/legacy/op/texture_projection.py b/uv_magic_uv/legacy/op/texture_projection.py
index ffcd0baf..bb73138b 100644
--- a/uv_magic_uv/legacy/op/texture_projection.py
+++ b/uv_magic_uv/legacy/op/texture_projection.py
@@ -23,12 +23,9 @@ __status__ = "production"
__version__ = "5.2"
__date__ = "17 Nov 2018"
-from collections import namedtuple
-
import bpy
import bgl
import bmesh
-import mathutils
from bpy_extras import view3d_utils
from bpy.props import (
BoolProperty,
@@ -39,110 +36,7 @@ from bpy.props import (
from ... import common
from ...utils.bl_class_registry import BlClassRegistry
from ...utils.property_class_registry import PropertyClassRegistry
-
-
-__all__ = [
- 'Properties',
- 'MUV_OT_TextureProjection',
- 'MUV_OT_TextureProjection_Project',
-]
-
-
-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
- prefs = context.preferences.addons["uv_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
+from ...impl import texture_projection_impl as impl
@PropertyClassRegistry(legacy=True)
@@ -183,7 +77,7 @@ class Properties:
scene.muv_texture_projection_tex_image = EnumProperty(
name="Image",
description="Texture Image",
- items=get_loaded_texture_name
+ items=impl.get_loaded_texture_name
)
scene.muv_texture_projection_tex_transparency = FloatProperty(
name="Transparency",
@@ -237,7 +131,7 @@ class MUV_OT_TextureProjection(bpy.types.Operator):
# we can not get area/space/region from console
if common.is_console_mode():
return False
- return is_valid_context(context)
+ return impl.is_valid_context(context)
@classmethod
def is_running(cls, _):
@@ -270,7 +164,7 @@ class MUV_OT_TextureProjection(bpy.types.Operator):
img = bpy.data.images[sc.muv_texture_projection_tex_image]
# setup rendering region
- rect = get_canvas(context, sc.muv_texture_projection_tex_magnitude)
+ rect = impl.get_canvas(context, sc.muv_texture_projection_tex_magnitude)
positions = [
[rect.x0, rect.y0],
[rect.x0, rect.y1],
@@ -336,7 +230,7 @@ class MUV_OT_TextureProjection_Project(bpy.types.Operator):
return True
if not MUV_OT_TextureProjection.is_running(context):
return False
- return is_valid_context(context)
+ return impl.is_valid_context(context)
def execute(self, context):
sc = context.scene
@@ -345,7 +239,7 @@ class MUV_OT_TextureProjection_Project(bpy.types.Operator):
self.report({'WARNING'}, "No textures are selected")
return {'CANCELLED'}
- _, region, space = common.get_space(
+ _, region, space = common.get_space_legacy(
'VIEW_3D', 'WINDOW', 'VIEW_3D')
# get faces to be texture projected
@@ -380,10 +274,10 @@ class MUV_OT_TextureProjection_Project(bpy.types.Operator):
# transform screen region to canvas
v_canvas = [
- region_to_canvas(
+ impl.region_to_canvas(
v,
- get_canvas(bpy.context,
- sc.muv_texture_projection_tex_magnitude)
+ impl.get_canvas(bpy.context,
+ sc.muv_texture_projection_tex_magnitude)
) for v in v_screen
]
diff --git a/uv_magic_uv/legacy/op/texture_wrap.py b/uv_magic_uv/legacy/op/texture_wrap.py
index cb4cc78c..85c9d174 100644
--- a/uv_magic_uv/legacy/op/texture_wrap.py
+++ b/uv_magic_uv/legacy/op/texture_wrap.py
@@ -24,46 +24,17 @@ __version__ = "5.2"
__date__ = "17 Nov 2018"
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
-
-
-__all__ = [
- 'Properties',
- 'MUV_OT_TextureWrap_Refer',
- 'MUV_OT_TextureWrap_Set',
-]
-
-
-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
+from ...impl import texture_wrap_impl as impl
@PropertyClassRegistry(legacy=True)
-class Properties:
+class _Properties:
idname = "texture_wrap"
@classmethod
@@ -109,33 +80,15 @@ class MUV_OT_TextureWrap_Refer(bpy.types.Operator):
bl_description = "Refer UV"
bl_options = {'REGISTER', 'UNDO'}
+ def __init__(self):
+ self.__impl = impl.ReferImpl()
+
@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)
+ return impl.ReferImpl.poll(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'}
+ return self.__impl.execute(self, context)
@BlClassRegistry(legacy=True)
@@ -149,153 +102,12 @@ class MUV_OT_TextureWrap_Set(bpy.types.Operator):
bl_description = "Set UV"
bl_options = {'REGISTER', 'UNDO'}
+ def __init__(self):
+ self.__impl = impl.SetImpl()
+
@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)
+ return impl.SetImpl.poll(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'}
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/legacy/op/unwrap_constraint.py b/uv_magic_uv/legacy/op/unwrap_constraint.py
index f06efce1..b7faa77a 100644
--- a/uv_magic_uv/legacy/op/unwrap_constraint.py
+++ b/uv_magic_uv/legacy/op/unwrap_constraint.py
@@ -22,47 +22,19 @@ __version__ = "5.2"
__date__ = "17 Nov 2018"
import bpy
-import bmesh
from bpy.props import (
BoolProperty,
EnumProperty,
FloatProperty,
)
-from ... import common
from ...utils.bl_class_registry import BlClassRegistry
from ...utils.property_class_registry import PropertyClassRegistry
-
-
-__all__ = [
- 'Properties',
- 'MUV_OT_UnwrapConstraint',
-]
-
-
-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
+from ...impl import unwrap_constraint_impl as impl
@PropertyClassRegistry(legacy=True)
-class Properties:
+class _Properties:
idname = "unwrap_constraint"
@classmethod
@@ -142,49 +114,12 @@ class MUV_OT_UnwrapConstraint(bpy.types.Operator):
default=False
)
+ def __init__(self):
+ self.__impl = impl.UnwrapConstraintImpl()
+
@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, _):
- obj = bpy.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 impl.UnwrapConstraintImpl.poll(context)
- return {'FINISHED'}
+ def execute(self, context):
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/legacy/op/uv_bounding_box.py b/uv_magic_uv/legacy/op/uv_bounding_box.py
index 0c283f7f..74c5f15c 100644
--- a/uv_magic_uv/legacy/op/uv_bounding_box.py
+++ b/uv_magic_uv/legacy/op/uv_bounding_box.py
@@ -35,42 +35,14 @@ from bpy.props import BoolProperty, EnumProperty
from ... import common
from ...utils.bl_class_registry import BlClassRegistry
from ...utils.property_class_registry import PropertyClassRegistry
-
-
-__all__ = [
- 'Properties',
- 'MUV_OT_UVBoundingBox',
-]
+from ...impl import uv_bounding_box_impl as impl
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(legacy=True)
-class Properties:
+class _Properties:
idname = "uv_bounding_box"
@classmethod
@@ -129,7 +101,7 @@ class Properties:
del scene.muv_uv_bounding_box_boundary
-class CommandBase():
+class CommandBase:
"""
Custom class: Base class of command
"""
@@ -314,7 +286,7 @@ class UniformScalingCommand(CommandBase):
self.__y = y
-class CommandExecuter():
+class CommandExecuter:
"""
Custom class: manage command history and execute command
"""
@@ -401,7 +373,7 @@ class State(IntEnum):
UNIFORM_SCALING_4 = 14
-class StateBase():
+class StateBase:
"""
Custom class: Base class of state
"""
@@ -428,7 +400,7 @@ class StateNone(StateBase):
"""
Update state
"""
- prefs = context.preferences.addons["uv_magic_uv"].preferences
+ prefs = context.user_preferences.addons["uv_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'):
@@ -555,7 +527,7 @@ class StateRotating(StateBase):
return State.ROTATING
-class StateManager():
+class StateManager:
"""
Custom class: Manage state about this feature
"""
@@ -618,7 +590,7 @@ class MUV_OT_UVBoundingBox(bpy.types.Operator):
def __init__(self):
self.__timer = None
self.__cmd_exec = CommandExecuter() # Command executor
- self.__state_mgr = StateManager(self.__cmd_exec) # State Manager
+ self.__state_mgr = StateManager(self.__cmd_exec) # State Manager
__handle = None
__timer = None
@@ -628,7 +600,7 @@ class MUV_OT_UVBoundingBox(bpy.types.Operator):
# we can not get area/space/region from console
if common.is_console_mode():
return False
- return is_valid_context(context)
+ return impl.is_valid_context(context)
@classmethod
def is_running(cls, _):
@@ -642,7 +614,7 @@ class MUV_OT_UVBoundingBox(bpy.types.Operator):
cls.draw_bb, (obj, context), "WINDOW", "POST_PIXEL")
if cls.__timer is None:
cls.__timer = context.window_manager.event_timer_add(
- 0.1, context.window)
+ 0.1, window=context.window)
context.window_manager.modal_handler_add(obj)
@classmethod
@@ -660,7 +632,7 @@ class MUV_OT_UVBoundingBox(bpy.types.Operator):
"""
Draw control point
"""
- prefs = context.preferences.addons["uv_magic_uv"].preferences
+ prefs = context.user_preferences.addons["uv_magic_uv"].preferences
cp_size = prefs.uv_bounding_box_cp_size
offset = cp_size / 2
verts = [
@@ -686,7 +658,7 @@ class MUV_OT_UVBoundingBox(bpy.types.Operator):
if not MUV_OT_UVBoundingBox.is_running(context):
return
- if not is_valid_context(context):
+ if not impl.is_valid_context(context):
return
for cp in props.ctrl_points:
@@ -790,7 +762,7 @@ class MUV_OT_UVBoundingBox(bpy.types.Operator):
if not MUV_OT_UVBoundingBox.is_running(context):
return {'FINISHED'}
- if not is_valid_context(context):
+ if not impl.is_valid_context(context):
MUV_OT_UVBoundingBox.handle_remove(context)
return {'FINISHED'}
@@ -799,8 +771,8 @@ class MUV_OT_UVBoundingBox(bpy.types.Operator):
'UI',
'TOOLS',
]
- if not common.mouse_on_area(event, 'IMAGE_EDITOR') or \
- common.mouse_on_regions(event, 'IMAGE_EDITOR', region_types):
+ if not common.mouse_on_area_legacy(event, 'IMAGE_EDITOR') or \
+ common.mouse_on_regions_legacy(event, 'IMAGE_EDITOR', region_types):
return {'PASS_THROUGH'}
if event.type == 'TIMER':
diff --git a/uv_magic_uv/legacy/op/uv_inspection.py b/uv_magic_uv/legacy/op/uv_inspection.py
index a13c1a03..df7e17e9 100644
--- a/uv_magic_uv/legacy/op/uv_inspection.py
+++ b/uv_magic_uv/legacy/op/uv_inspection.py
@@ -24,47 +24,17 @@ __version__ = "5.2"
__date__ = "17 Nov 2018"
import bpy
-import bmesh
import bgl
from bpy.props import BoolProperty, EnumProperty
from ... import common
from ...utils.bl_class_registry import BlClassRegistry
from ...utils.property_class_registry import PropertyClassRegistry
-
-
-__all__ = [
- 'Properties',
- 'MUV_OT_UVInspection_Render',
- 'MUV_OT_UVInspection_Update',
-]
-
-
-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
+from ...impl import uv_inspection_impl as impl
@PropertyClassRegistry(legacy=True)
-class Properties:
+class _Properties:
idname = "uv_inspection"
@classmethod
@@ -145,7 +115,7 @@ class MUV_OT_UVInspection_Render(bpy.types.Operator):
# we can not get area/space/region from console
if common.is_console_mode():
return False
- return is_valid_context(context)
+ return impl.is_valid_context(context)
@classmethod
def is_running(cls, _):
@@ -169,7 +139,7 @@ class MUV_OT_UVInspection_Render(bpy.types.Operator):
def draw(_, context):
sc = context.scene
props = sc.muv_props.uv_inspection
- prefs = context.preferences.addons["uv_magic_uv"].preferences
+ prefs = context.user_preferences.addons["uv_magic_uv"].preferences
if not MUV_OT_UVInspection_Render.is_running(context):
return
@@ -221,7 +191,7 @@ class MUV_OT_UVInspection_Render(bpy.types.Operator):
def invoke(self, context, _):
if not MUV_OT_UVInspection_Render.is_running(context):
- update_uvinsp_info(context)
+ impl.update_uvinsp_info(context)
MUV_OT_UVInspection_Render.handle_add(self, context)
else:
MUV_OT_UVInspection_Render.handle_remove()
@@ -232,25 +202,6 @@ class MUV_OT_UVInspection_Render(bpy.types.Operator):
return {'FINISHED'}
-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)
-
-
@BlClassRegistry(legacy=True)
class MUV_OT_UVInspection_Update(bpy.types.Operator):
"""
@@ -269,10 +220,10 @@ class MUV_OT_UVInspection_Update(bpy.types.Operator):
return True
if not MUV_OT_UVInspection_Render.is_running(context):
return False
- return is_valid_context(context)
+ return impl.is_valid_context(context)
def execute(self, context):
- update_uvinsp_info(context)
+ impl.update_uvinsp_info(context)
if context.area:
context.area.tag_redraw()
diff --git a/uv_magic_uv/legacy/op/uv_sculpt.py b/uv_magic_uv/legacy/op/uv_sculpt.py
index 556e0a4e..47a850d8 100644
--- a/uv_magic_uv/legacy/op/uv_sculpt.py
+++ b/uv_magic_uv/legacy/op/uv_sculpt.py
@@ -23,56 +23,31 @@ __status__ = "production"
__version__ = "5.2"
__date__ = "17 Nov 2018"
+
from math import pi, cos, tan, sin
import bpy
-import bmesh
-import bgl
-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,
)
+import bmesh
+import bgl
+from mathutils import Vector
+from bpy_extras import view3d_utils
+from mathutils.bvhtree import BVHTree
+from mathutils.geometry import barycentric_transform
from ... import common
from ...utils.bl_class_registry import BlClassRegistry
from ...utils.property_class_registry import PropertyClassRegistry
-
-
-__all__ = [
- 'Properties',
- 'MUV_OT_UVSculpt',
-]
-
-
-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
+from ...impl import uv_sculpt_impl as impl
@PropertyClassRegistry(legacy=True)
-class Properties:
+class _Properties:
idname = "uv_sculpt"
@classmethod
@@ -175,7 +150,7 @@ class MUV_OT_UVSculpt(bpy.types.Operator):
# we can not get area/space/region from console
if common.is_console_mode():
return False
- return is_valid_context(context)
+ return impl.is_valid_context(context)
@classmethod
def is_running(cls, _):
@@ -189,7 +164,7 @@ class MUV_OT_UVSculpt(bpy.types.Operator):
"WINDOW", "POST_PIXEL")
if not cls.__timer:
cls.__timer = context.window_manager.event_timer_add(
- 0.1, context.window)
+ 0.1, window=context.window)
context.window_manager.modal_handler_add(obj)
@classmethod
@@ -205,7 +180,7 @@ class MUV_OT_UVSculpt(bpy.types.Operator):
@classmethod
def draw_brush(cls, obj, context):
sc = context.scene
- prefs = context.preferences.addons["uv_magic_uv"].preferences
+ prefs = context.user_preferences.addons["uv_magic_uv"].preferences
num_segment = 180
theta = 2 * pi / num_segment
@@ -233,17 +208,6 @@ class MUV_OT_UVSculpt(bpy.types.Operator):
self.current_mco = Vector((0.0, 0.0))
self.__initial_mco = Vector((0.0, 0.0))
- def __get_strength(self, p, len_, factor):
- f = factor
-
- if p > len_:
- return 0.0
-
- if p < 0.0:
- return f
-
- return (len_ - p) * f / len_
-
def __stroke_init(self, context, _):
sc = context.scene
@@ -254,7 +218,8 @@ class MUV_OT_UVSculpt(bpy.types.Operator):
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')
+ _, region, space = common.get_space_legacy('VIEW_3D', 'WINDOW',
+ 'VIEW_3D')
self.__loop_info = []
for f in bm.faces:
@@ -271,7 +236,7 @@ class MUV_OT_UVSculpt(bpy.types.Operator):
"initial_vco": l.vert.co.copy(),
"initial_vco_2d": loc_2d,
"initial_uv": l[uv_layer].uv.copy(),
- "strength": self.__get_strength(
+ "strength": impl.get_strength(
diff.length, sc.muv_uv_sculpt_radius,
sc.muv_uv_sculpt_strength)
}
@@ -292,7 +257,8 @@ class MUV_OT_UVSculpt(bpy.types.Operator):
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')
+ _, region, space = common.get_space_legacy('VIEW_3D', 'WINDOW',
+ 'VIEW_3D')
loop_info = []
for f in bm.faces:
if not f.select:
@@ -308,7 +274,7 @@ class MUV_OT_UVSculpt(bpy.types.Operator):
"initial_vco": l.vert.co.copy(),
"initial_vco_2d": loc_2d,
"initial_uv": l[uv_layer].uv.copy(),
- "strength": self.__get_strength(
+ "strength": impl.get_strength(
diff.length, sc.muv_uv_sculpt_radius,
sc.muv_uv_sculpt_strength)
}
@@ -349,7 +315,8 @@ class MUV_OT_UVSculpt(bpy.types.Operator):
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')
+ _, region, space = common.get_space_legacy('VIEW_3D', 'WINDOW',
+ 'VIEW_3D')
# get vertex and loop relation
vert_db = {}
@@ -395,9 +362,9 @@ class MUV_OT_UVSculpt(bpy.types.Operator):
if diff.length >= sc.muv_uv_sculpt_radius:
continue
db = vert_db[l.vert]
- strength = self.__get_strength(diff.length,
- sc.muv_uv_sculpt_radius,
- sc.muv_uv_sculpt_strength)
+ strength = impl.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':
@@ -446,8 +413,8 @@ class MUV_OT_UVSculpt(bpy.types.Operator):
'TOOLS',
'TOOL_PROPS',
]
- if not common.mouse_on_area(event, 'VIEW_3D') or \
- common.mouse_on_regions(event, 'VIEW_3D', region_types):
+ if not common.mouse_on_area_legacy(event, 'VIEW_3D') or \
+ common.mouse_on_regions_legacy(event, 'VIEW_3D', region_types):
return {'PASS_THROUGH'}
if event.type == 'LEFTMOUSE':
diff --git a/uv_magic_uv/legacy/op/world_scale_uv.py b/uv_magic_uv/legacy/op/world_scale_uv.py
index e56b6bfa..4a6b2869 100644
--- a/uv_magic_uv/legacy/op/world_scale_uv.py
+++ b/uv_magic_uv/legacy/op/world_scale_uv.py
@@ -23,11 +23,7 @@ __status__ = "production"
__version__ = "5.2"
__date__ = "17 Nov 2018"
-from math import sqrt
-
import bpy
-import bmesh
-from mathutils import Vector
from bpy.props import (
EnumProperty,
FloatProperty,
@@ -35,54 +31,9 @@ from bpy.props import (
BoolProperty,
)
-from ... import common
from ...utils.bl_class_registry import BlClassRegistry
from ...utils.property_class_registry import PropertyClassRegistry
-
-
-__all__ = [
- 'Properties',
- 'MUV_OT_WorldScaleUV_Measure',
- 'MUV_OT_WorldScaleUV_ApplyManual',
- 'MUV_OT_WorldScaleUV_ApplyScalingDensity',
- 'MUV_OT_WorldScaleUV_ApplyProportionalToMesh',
-]
-
-
-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
+from ...impl import world_scale_uv_impl as impl
@PropertyClassRegistry(legacy=True)
@@ -188,132 +139,15 @@ class MUV_OT_WorldScaleUV_Measure(bpy.types.Operator):
bl_description = "Measure face size for scale calculation"
bl_options = {'REGISTER', 'UNDO'}
+ def __init__(self):
+ self.__impl = impl.MeasureImpl()
+
@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)
+ return impl.MeasureImpl.poll(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'}
-
-
-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)
+ return self.__impl.execute(self, context)
@BlClassRegistry(legacy=True)
@@ -364,54 +198,21 @@ class MUV_OT_WorldScaleUV_ApplyManual(bpy.types.Operator):
options={'HIDDEN', 'SKIP_SAVE'}
)
+ def __init__(self):
+ self.__impl = impl.ApplyManualImpl()
+
@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'}
+ return impl.ApplyManualImpl.poll(context)
- def draw(self, _):
- layout = self.layout
+ def draw(self, context):
+ self.__impl.draw(self, context)
- 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 invoke(self, context, event):
+ return self.__impl.invoke(self, context, event)
def execute(self, context):
- return self.__apply_manual(context)
+ return self.__impl.execute(self, context)
@BlClassRegistry(legacy=True)
@@ -468,73 +269,21 @@ class MUV_OT_WorldScaleUV_ApplyScalingDensity(bpy.types.Operator):
options={'HIDDEN', 'SKIP_SAVE'}
)
+ def __init__(self):
+ self.__impl = impl.ApplyScalingDensityImpl()
+
@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'}
+ return impl.ApplyScalingDensityImpl.poll(context)
- tgt_density = self.src_density * self.tgt_scaling_factor
- factor = tgt_density / density
+ def draw(self, context):
+ self.__impl.draw(self, context)
- 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("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 invoke(self, context, event):
+ return self.__impl.invoke(self, context, event)
def execute(self, context):
- if self.same_density:
- self.tgt_scaling_factor = 1.0
-
- return self.__apply_scaling_density(context)
+ return self.__impl.execute(self, context)
@BlClassRegistry(legacy=True)
@@ -593,63 +342,18 @@ class MUV_OT_WorldScaleUV_ApplyProportionalToMesh(bpy.types.Operator):
options={'HIDDEN', 'SKIP_SAVE'}
)
+ def __init__(self):
+ self.__impl = impl.ApplyProportionalToMeshImpl()
+
@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("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 impl.ApplyProportionalToMeshImpl.poll(context)
- return wm.invoke_props_dialog(self)
+ def draw(self, context):
+ self.__impl.draw(self, context)
- return self.execute(context)
+ def invoke(self, context, event):
+ return self.__impl.invoke(self, context, event)
def execute(self, context):
- return self.__apply_proportional_to_mesh(context)
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/legacy/preferences.py b/uv_magic_uv/legacy/preferences.py
index 931cc1d4..e21f1753 100644
--- a/uv_magic_uv/legacy/preferences.py
+++ b/uv_magic_uv/legacy/preferences.py
@@ -30,13 +30,14 @@ from bpy.props import (
BoolProperty,
EnumProperty,
IntProperty,
+ StringProperty,
)
from bpy.types import AddonPreferences
from . import op
from . import ui
-from .. import addon_updater_ops
from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.addon_updator import AddonUpdatorManager
__all__ = [
'add_builtin_menu',
@@ -162,6 +163,48 @@ def remove_builtin_menu():
@BlClassRegistry(legacy=True)
+class MUV_OT_CheckAddonUpdate(bpy.types.Operator):
+ bl_idname = "uv.muv_check_addon_update"
+ bl_label = "Check Update"
+ bl_description = "Check Add-on Update"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ updater = AddonUpdatorManager.get_instance()
+ updater.check_update_candidate()
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry(legacy=True)
+class MUV_OT_UpdateAddon(bpy.types.Operator):
+ bl_idname = "uv.muv_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, context):
+ updater = AddonUpdatorManager.get_instance()
+ updater.update(self.branch_name)
+
+ return {'FINISHED'}
+
+
+def get_update_candidate_branches(_, __):
+ updater = AddonUpdatorManager.get_instance()
+ if not updater.candidate_checked():
+ return []
+
+ return [(name, name, "") for name in updater.get_candidate_branch_names()]
+
+
+@BlClassRegistry(legacy=True)
class Preferences(AddonPreferences):
"""Preferences class: Preferences for this add-on"""
@@ -312,6 +355,13 @@ class Preferences(AddonPreferences):
max=59
)
+ # 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, context):
layout = self.layout
diff --git a/uv_magic_uv/lib/__init__.py b/uv_magic_uv/lib/__init__.py
new file mode 100644
index 00000000..d49b6822
--- /dev/null
+++ b/uv_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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+if "bpy" in locals():
+ import importlib
+ importlib.reload(bglx)
+else:
+ from . import bglx
+
+import bpy
diff --git a/uv_magic_uv/lib/bglx.py b/uv_magic_uv/lib/bglx.py
new file mode 100644
index 00000000..c4dadd69
--- /dev/null
+++ b/uv_magic_uv/lib/bglx.py
@@ -0,0 +1,191 @@
+from threading import Lock
+
+import gpu
+from gpu_extras.batch import batch_for_shader
+
+GL_LINES = 0
+GL_LINE_STRIP = 1
+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):
+ 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):
+ 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 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_tex_coords(self):
+ return self.tex_coords
+
+
+def glBegin(mode):
+ inst = InternalData.get_instance()
+ inst.init()
+ inst.set_prim_mode(mode)
+
+
+def glColor4f(r, g, b, a):
+ inst = InternalData.get_instance()
+ inst.set_color([r, g, b, a])
+
+
+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_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])
diff --git a/uv_magic_uv/op/__init__.py b/uv_magic_uv/op/__init__.py
index 2142c157..9535b76d 100644
--- a/uv_magic_uv/op/__init__.py
+++ b/uv_magic_uv/op/__init__.py
@@ -25,22 +25,50 @@ __date__ = "17 Nov 2018"
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/uv_magic_uv/op/align_uv.py b/uv_magic_uv/op/align_uv.py
new file mode 100644
index 00000000..fbd119d2
--- /dev/null
+++ b/uv_magic_uv/op/align_uv.py
@@ -0,0 +1,231 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+from bpy.props import EnumProperty, BoolProperty, FloatProperty
+
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..impl import align_uv_impl as impl
+
+
+@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()
+class MUV_OT_AlignUV_Circle(bpy.types.Operator):
+
+ bl_idname = "uv.muv_align_uv_operator_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
+ )
+
+ def __init__(self):
+ self.__impl = impl.CircleImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.CircleImpl.poll(context)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
+
+
+@BlClassRegistry()
+class MUV_OT_AlignUV_Straighten(bpy.types.Operator):
+
+ bl_idname = "uv.muv_align_uv_operator_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
+ )
+
+ def __init__(self):
+ self.__impl = impl.StraightenImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.StraightenImpl.poll(context)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
+
+
+@BlClassRegistry()
+class MUV_OT_AlignUV_Axis(bpy.types.Operator):
+
+ bl_idname = "uv.muv_align_uv_operator_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
+ )
+
+ def __init__(self):
+ self.__impl = impl.AxisImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.AxisImpl.poll(context)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/op/align_uv_cursor.py b/uv_magic_uv/op/align_uv_cursor.py
new file mode 100644
index 00000000..6de4bbcf
--- /dev/null
+++ b/uv_magic_uv/op/align_uv_cursor.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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+from mathutils import Vector
+from bpy.props import EnumProperty, BoolProperty, FloatVectorProperty
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..impl import align_uv_cursor_impl as impl
+
+
+@PropertyClassRegistry()
+class _Properties:
+ idname = "align_uv_cursor"
+
+ @classmethod
+ def init_props(cls, scene):
+ def auvc_get_cursor_loc(self):
+ _, _, space = common.get_space_legacy('IMAGE_EDITOR', 'WINDOW',
+ 'IMAGE_EDITOR')
+ loc = space.cursor_location
+ self['muv_align_uv_cursor_cursor_loc'] = Vector((loc[0], loc[1]))
+ 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
+ _, _, space = common.get_space_legacy('IMAGE_EDITOR', 'WINDOW',
+ 'IMAGE_EDITOR')
+ space.cursor_location = Vector((value[0], value[1]))
+
+ 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()
+class MUV_OT_AlignUVCursor(bpy.types.Operator):
+
+ bl_idname = "uv.muv_align_uv_cursor_operator"
+ 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'
+ )
+
+ def __init__(self):
+ self.__impl = impl.AlignUVCursorImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.AlignUVCursorImpl.poll(context)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/op/pack_uv.py b/uv_magic_uv/op/pack_uv.py
new file mode 100644
index 00000000..84f195c5
--- /dev/null
+++ b/uv_magic_uv/op/pack_uv.py
@@ -0,0 +1,129 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+from bpy.props import (
+ FloatProperty,
+ FloatVectorProperty,
+ BoolProperty,
+)
+
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..impl import pack_uv_impl as impl
+
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_PackUV',
+]
+
+
+@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()
+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_pack_uv_operator"
+ 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
+ )
+
+ def __init__(self):
+ self.__impl = impl.PackUVImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.PackUVImpl.poll(context)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/op/preserve_uv_aspect.py b/uv_magic_uv/op/preserve_uv_aspect.py
new file mode 100644
index 00000000..ca4969fd
--- /dev/null
+++ b/uv_magic_uv/op/preserve_uv_aspect.py
@@ -0,0 +1,124 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+from bpy.props import StringProperty, EnumProperty, BoolProperty
+
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..impl import preserve_uv_aspect_impl as impl
+
+
+__all__ = [
+ 'Properties',
+ 'MUV_OT_PreserveUVAspect',
+]
+
+
+@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()
+class MUV_OT_PreserveUVAspect(bpy.types.Operator):
+ """
+ Operation class: Preserve UV Aspect
+ """
+
+ bl_idname = "uv.muv_preserve_uv_aspect_operator"
+ 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"
+ )
+
+ def __init__(self):
+ self.__impl = impl.PreserveUVAspectImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.PreserveUVAspectImpl.poll(context)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/op/select_uv.py b/uv_magic_uv/op/select_uv.py
new file mode 100644
index 00000000..789af9ce
--- /dev/null
+++ b/uv_magic_uv/op/select_uv.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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+from bpy.props import BoolProperty
+
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..impl import select_uv_impl as impl
+
+
+@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_select_uv_operator_select_overlapped"
+ bl_label = "Overlapped"
+ bl_description = "Select faces which have overlapped UVs"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def __init__(self):
+ self.__impl = impl.SelectOverlappedImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.SelectOverlappedImpl.poll(context)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
+
+
+@BlClassRegistry()
+class MUV_OT_SelectUV_SelectFlipped(bpy.types.Operator):
+ """
+ Operation class: Select faces which have flipped UVs
+ """
+
+ bl_idname = "uv.muv_select_uv_operator_select_flipped"
+ bl_label = "Flipped"
+ bl_description = "Select faces which have flipped UVs"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def __init__(self):
+ self.__impl = impl.SelectFlippedImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.SelectFlippedImpl.poll(context)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/op/smooth_uv.py b/uv_magic_uv/op/smooth_uv.py
new file mode 100644
index 00000000..d448d108
--- /dev/null
+++ b/uv_magic_uv/op/smooth_uv.py
@@ -0,0 +1,105 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+from bpy.props import BoolProperty, FloatProperty
+
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..impl import smooth_uv_impl as impl
+
+
+@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()
+class MUV_OT_SmoothUV(bpy.types.Operator):
+
+ bl_idname = "uv.muv_smooth_uv_operator"
+ 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
+ )
+
+ def __init__(self):
+ self.__impl = impl.SmoothUVImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.SmoothUVImpl.poll(context)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/op/texture_lock.py b/uv_magic_uv/op/texture_lock.py
new file mode 100644
index 00000000..b1b43753
--- /dev/null
+++ b/uv_magic_uv/op/texture_lock.py
@@ -0,0 +1,158 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+from bpy.props import BoolProperty
+
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..impl import texture_lock_impl as impl
+
+
+@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_texture_lock_operator_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_texture_lock_operator_lock"
+ bl_label = "Lock Texture"
+ bl_description = "Lock Texture"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def __init__(self):
+ self.__impl = impl.LockImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.LockImpl.poll(context)
+
+ @classmethod
+ def is_ready(cls, context):
+ return impl.LockImpl.is_ready(context)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
+
+
+@BlClassRegistry()
+class MUV_OT_TextureLock_Unlock(bpy.types.Operator):
+ """
+ Operation class: Unlock Texture
+ """
+
+ bl_idname = "uv.muv_texture_lock_operator_unlock"
+ bl_label = "Unlock Texture"
+ bl_description = "Unlock Texture"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ connect: BoolProperty(
+ name="Connect UV",
+ default=True
+ )
+
+ def __init__(self):
+ self.__impl = impl.UnlockImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.UnlockImpl.poll(context)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
+
+
+@BlClassRegistry()
+class MUV_OT_TextureLock_Intr(bpy.types.Operator):
+ """
+ Operation class: Texture Lock (Interactive mode)
+ """
+
+ bl_idname = "uv.muv_texture_lock_operator_intr"
+ bl_label = "Texture Lock (Interactive mode)"
+ bl_description = "Internal operation for Texture Lock (Interactive mode)"
+
+ def __init__(self):
+ self.__impl = impl.IntrImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.IntrImpl.poll(context)
+
+ @classmethod
+ def is_running(cls, context):
+ return impl.IntrImpl.is_running(context)
+
+ def modal(self, context, event):
+ return self.__impl.modal(self, context, event)
+
+ def invoke(self, context, event):
+ return self.__impl.invoke(self, context, event)
diff --git a/uv_magic_uv/op/texture_projection.py b/uv_magic_uv/op/texture_projection.py
new file mode 100644
index 00000000..f6a3a89f
--- /dev/null
+++ b/uv_magic_uv/op/texture_projection.py
@@ -0,0 +1,292 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+import bgl
+import bmesh
+from bpy_extras import view3d_utils
+from bpy.props import (
+ BoolProperty,
+ EnumProperty,
+ FloatProperty,
+)
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..impl import texture_projection_impl as impl
+
+from ..lib import bglx
+
+
+@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_texture_projection_operator('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=impl.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_texture_projection_operator"
+ 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 impl.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 = impl.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
+ 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)
+
+ # render texture
+ bglx.glBegin(bglx.GL_QUADS)
+ bglx.glColor4f(1.0, 1.0, 1.0,
+ sc.muv_texture_projection_tex_transparency)
+ for (v1, v2), (u, v) in zip(positions, tex_coords):
+ bglx.glTexCoord2f(u, v)
+ bglx.glVertex2f(v1, v2)
+ bglx.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_texture_projection_operator_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 impl.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()
+ 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,
+ world_mat @ l.vert.co)
+ for f in sel_faces for l in f.loops
+ ]
+
+ # transform screen region to canvas
+ v_canvas = [
+ impl.region_to_canvas(
+ v,
+ impl.get_canvas(bpy.context,
+ sc.muv_texture_projection_tex_magnitude)
+ ) for v in v_screen
+ ]
+
+ # 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:
+ 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/uv_magic_uv/op/texture_wrap.py b/uv_magic_uv/op/texture_wrap.py
new file mode 100644
index 00000000..70fb6604
--- /dev/null
+++ b/uv_magic_uv/op/texture_wrap.py
@@ -0,0 +1,113 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+from bpy.props import (
+ BoolProperty,
+)
+
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..impl import texture_wrap_impl as impl
+
+
+@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_texture_wrap_operator_refer"
+ bl_label = "Refer"
+ bl_description = "Refer UV"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def __init__(self):
+ self.__impl = impl.ReferImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.ReferImpl.poll(context)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
+
+
+@BlClassRegistry()
+class MUV_OT_TextureWrap_Set(bpy.types.Operator):
+ """
+ Operation class: Set UV
+ """
+
+ bl_idname = "uv.muv_texture_wrap_operator_set"
+ bl_label = "Set"
+ bl_description = "Set UV"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def __init__(self):
+ self.__impl = impl.SetImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.SetImpl.poll(context)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/op/unwrap_constraint.py b/uv_magic_uv/op/unwrap_constraint.py
new file mode 100644
index 00000000..df16f783
--- /dev/null
+++ b/uv_magic_uv/op/unwrap_constraint.py
@@ -0,0 +1,125 @@
+# ##### 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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+from bpy.props import (
+ BoolProperty,
+ EnumProperty,
+ FloatProperty,
+)
+
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..impl import unwrap_constraint_impl as impl
+
+
+@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)
+class MUV_OT_UnwrapConstraint(bpy.types.Operator):
+ """
+ Operation class: Unwrap with constrain UV coordinate
+ """
+
+ bl_idname = "uv.muv_unwrap_constraint_operator"
+ 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
+ )
+
+ def __init__(self):
+ self.__impl = impl.UnwrapConstraintImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.UnwrapConstraintImpl.poll(context)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/op/uv_bounding_box.py b/uv_magic_uv/op/uv_bounding_box.py
new file mode 100644
index 00000000..82cdea45
--- /dev/null
+++ b/uv_magic_uv/op/uv_bounding_box.py
@@ -0,0 +1,813 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+from enum import IntEnum
+import math
+
+import bpy
+import bgl
+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 ..impl import uv_bounding_box_impl as impl
+
+from ..lib import bglx
+
+
+MAX_VALUE = 100000.0
+
+
+@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_uv_bounding_box_operator('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 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 = 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 = m @ mathutils.Vector((self.__ix, self.__iy, 0.0))
+ tix, tiy = t.x, t.y
+ t = m @ mathutils.Vector((self.__ox, self.__oy, 0.0))
+ tox, toy = t.x, t.y
+ t = 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 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 = 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 = m @ mathutils.Vector((self.__ix, self.__iy, 0.0))
+ tix, tiy = t.x, t.y
+ t = m @ mathutils.Vector((self.__ox, self.__oy, 0.0))
+ tox, toy = t.x, t.y
+ t = 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 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 = 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
+ """
+ prefs = context.user_preferences.addons["uv_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_uv_bounding_box_operator"
+ 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 impl.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
+ """
+ prefs = context.user_preferences.addons["uv_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)
+ bglx.glBegin(bglx.GL_QUADS)
+ bglx.glColor4f(1.0, 1.0, 1.0, 1.0)
+ for (x, y) in verts:
+ bglx.glVertex2f(x, y)
+ bglx.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 impl.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 = 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 [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 impl.is_valid_context(context):
+ MUV_OT_UVBoundingBox.handle_remove(context)
+ return {'FINISHED'}
+
+ region_types = [
+ 'HEADER',
+ 'UI',
+ 'TOOLS',
+ ]
+ if not common.mouse_on_area_legacy(event, 'IMAGE_EDITOR') or \
+ common.mouse_on_regions_legacy(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/uv_magic_uv/op/uv_inspection.py b/uv_magic_uv/op/uv_inspection.py
new file mode 100644
index 00000000..63d73fdf
--- /dev/null
+++ b/uv_magic_uv/op/uv_inspection.py
@@ -0,0 +1,235 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+import bpy
+import bgl
+from bpy.props import BoolProperty, EnumProperty
+
+from .. import common
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..impl import uv_inspection_impl as impl
+
+from ..lib import bglx
+
+
+@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_uv_inspection_operator_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_uv_inspection_operator_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 impl.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
+ prefs = context.user_preferences.addons["uv_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"]:
+ bglx.glBegin(bglx.GL_TRIANGLE_FAN)
+ bglx.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)
+ bglx.glVertex2f(x, y)
+ bglx.glEnd()
+ elif sc.muv_uv_inspection_show_mode == 'FACE':
+ bglx.glBegin(bglx.GL_TRIANGLE_FAN)
+ bglx.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)
+ bglx.glVertex2f(x, y)
+ bglx.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"]:
+ bglx.glBegin(bglx.GL_TRIANGLE_FAN)
+ bglx.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)
+ bglx.glVertex2f(x, y)
+ bglx.glEnd()
+ elif sc.muv_uv_inspection_show_mode == 'FACE':
+ bglx.glBegin(bglx.GL_TRIANGLE_FAN)
+ bglx.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)
+ bglx.glVertex2f(x, y)
+ bglx.glEnd()
+
+ bgl.glDisable(bgl.GL_BLEND)
+
+ def invoke(self, context, _):
+ if not MUV_OT_UVInspection_Render.is_running(context):
+ impl.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_uv_inspection_operator_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 impl.is_valid_context(context)
+
+ def execute(self, context):
+ impl.update_uvinsp_info(context)
+
+ if context.area:
+ context.area.tag_redraw()
+
+ return {'FINISHED'}
diff --git a/uv_magic_uv/op/uv_sculpt.py b/uv_magic_uv/op/uv_sculpt.py
new file mode 100644
index 00000000..cc1c0575
--- /dev/null
+++ b/uv_magic_uv/op/uv_sculpt.py
@@ -0,0 +1,446 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+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 ..impl import uv_sculpt_impl as impl
+
+from ..lib import bglx
+
+
+@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_uv_sculpt_operator('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_uv_sculpt_operator"
+ 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 impl.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
+ prefs = context.user_preferences.addons["uv_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
+
+ bglx.glBegin(bglx.GL_LINE_STRIP)
+ bglx.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):
+ bglx.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
+ bglx.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, 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": impl.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, 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": impl.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 = mwi @ ray_orig
+ ray_tgt_obj = 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, 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 = impl.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/uv_magic_uv/op/world_scale_uv.py b/uv_magic_uv/op/world_scale_uv.py
new file mode 100644
index 00000000..a957d5d4
--- /dev/null
+++ b/uv_magic_uv/op/world_scale_uv.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__ = "McBuff, Nutti <nutti.metro@gmail.com>"
+__status__ = "production"
+__version__ = "5.2"
+__date__ = "17 Nov 2018"
+
+
+import bpy
+from bpy.props import (
+ EnumProperty,
+ FloatProperty,
+ IntVectorProperty,
+ BoolProperty,
+)
+
+from ..utils.bl_class_registry import BlClassRegistry
+from ..utils.property_class_registry import PropertyClassRegistry
+from ..impl import world_scale_uv_impl as impl
+
+
+@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_world_scale_uv_operator_measure"
+ bl_label = "Measure World Scale UV"
+ bl_description = "Measure face size for scale calculation"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def __init__(self):
+ self.__impl = impl.MeasureImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.MeasureImpl.poll(context)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
+
+
+@BlClassRegistry()
+class MUV_OT_WorldScaleUV_ApplyManual(bpy.types.Operator):
+ """
+ Operation class: Apply scaled UV (Manual)
+ """
+
+ bl_idname = "uv.muv_world_scale_uv_operator_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'}
+ )
+
+ def __init__(self):
+ self.__impl = impl.ApplyManualImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.ApplyManualImpl.poll(context)
+
+ def draw(self, context):
+ self.__impl.draw(self, context)
+
+ def invoke(self, context, event):
+ return self.__impl.invoke(self, context, event)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
+
+
+@BlClassRegistry()
+class MUV_OT_WorldScaleUV_ApplyScalingDensity(bpy.types.Operator):
+ """
+ Operation class: Apply scaled UV (Scaling Density)
+ """
+
+ bl_idname = "uv.muv_world_scale_uv_operator_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'}
+ )
+
+ def __init__(self):
+ self.__impl = impl.ApplyScalingDensityImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.ApplyScalingDensityImpl.poll(context)
+
+ def draw(self, context):
+ self.__impl.draw(self, context)
+
+ def invoke(self, context, event):
+ return self.__impl.invoke(self, context, event)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
+
+
+@BlClassRegistry()
+class MUV_OT_WorldScaleUV_ApplyProportionalToMesh(bpy.types.Operator):
+ """
+ Operation class: Apply scaled UV (Proportional to mesh)
+ """
+
+ bl_idname = "uv.muv_world_scale_uv_operator_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'}
+ )
+
+ def __init__(self):
+ self.__impl = impl.ApplyProportionalToMeshImpl()
+
+ @classmethod
+ def poll(cls, context):
+ return impl.ApplyProportionalToMeshImpl.poll(context)
+
+ def draw(self, context):
+ self.__impl.draw(self, context)
+
+ def invoke(self, context, event):
+ return self.__impl.invoke(self, context, event)
+
+ def execute(self, context):
+ return self.__impl.execute(self, context)
diff --git a/uv_magic_uv/preferences.py b/uv_magic_uv/preferences.py
index 3ba94376..a58d08d4 100644
--- a/uv_magic_uv/preferences.py
+++ b/uv_magic_uv/preferences.py
@@ -29,19 +29,14 @@ from bpy.props import (
FloatVectorProperty,
BoolProperty,
EnumProperty,
- IntProperty,
+ StringProperty,
)
from bpy.types import AddonPreferences
from . import op
from . import ui
-from . import addon_updater_ops
-
-__all__ = [
- 'add_builtin_menu',
- 'remove_builtin_menu',
- 'Preferences'
-]
+from .utils.bl_class_registry import BlClassRegistry
+from .utils.addon_updator import AddonUpdatorManager
def view3d_uvmap_menu_fn(self, context):
@@ -69,8 +64,32 @@ def view3d_uvmap_menu_fn(self, context):
ops.axis = sc.muv_mirror_uv_axis
# Move UV
layout.operator(op.move_uv.MUV_OT_MoveUV.bl_idname, text="Move UV")
+ # World Scale UV
+ layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_WorldScaleUV.bl_idname,
+ text="World Scale UV")
+ # Preserve UV
+ layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_PreserveUVAspect.bl_idname,
+ text="Preserve UV")
+ # Texture Lock
+ layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_TextureLock.bl_idname,
+ text="Texture Lock")
+ # Texture Wrap
+ layout.menu(ui.VIEW3D_MT_uv_map.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='IMAGE')
+ # Unwrap Constraint
+ ops = layout.operator(
+ op.unwrap_constraint.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(ui.VIEW3D_MT_uv_map.MUV_MT_TextureProjection.bl_idname,
+ text="Texture Projection")
# UVW
layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_UVW.bl_idname, text="UVW")
@@ -80,13 +99,14 @@ def view3d_object_menu_fn(self, _):
layout.separator()
layout.label(text="Copy/Paste UV", icon='IMAGE')
- # Copy/Paste UV (Among Objecct)
+ # Copy/Paste UV (Among Object)
layout.menu(ui.VIEW3D_MT_object.MUV_MT_CopyPasteUV_Object.bl_idname,
text="Copy/Paste UV")
-def image_uvs_menu_fn(self, _):
+def image_uvs_menu_fn(self, context):
layout = self.layout
+ sc = context.scene
layout.separator()
# Copy/Paste UV (on UV/Image Editor)
@@ -94,6 +114,34 @@ def image_uvs_menu_fn(self, _):
layout.menu(ui.IMAGE_MT_uvs.MUV_MT_CopyPasteUV_UVEdit.bl_idname,
text="Copy/Paste UV")
+ layout.separator()
+ # Pack UV
+ layout.label(text="UV Manipulation", icon='IMAGE')
+ ops = layout.operator(op.pack_uv.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(ui.IMAGE_MT_uvs.MUV_MT_SelectUV.bl_idname, text="Select UV")
+ # Smooth UV
+ ops = layout.operator(op.smooth_uv.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(ui.IMAGE_MT_uvs.MUV_MT_AlignUV.bl_idname, text="Align UV")
+
+ layout.separator()
+ # Align UV Cursor
+ layout.label(text="Editor Enhancement", icon='IMAGE')
+ layout.menu(ui.IMAGE_MT_uvs.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(ui.IMAGE_MT_uvs.MUV_MT_UVInspection.bl_idname,
+ text="UV Inspection")
+
def add_builtin_menu():
bpy.types.VIEW3D_MT_uv_map.append(view3d_uvmap_menu_fn)
@@ -107,6 +155,49 @@ def remove_builtin_menu():
bpy.types.VIEW3D_MT_uv_map.remove(view3d_uvmap_menu_fn)
+@BlClassRegistry()
+class MUV_OT_CheckAddonUpdate(bpy.types.Operator):
+ bl_idname = "uv.muv_check_addon_update"
+ bl_label = "Check Update"
+ bl_description = "Check Add-on Update"
+ bl_options = {'REGISTER', 'UNDO'}
+
+ def execute(self, context):
+ updater = AddonUpdatorManager.get_instance()
+ updater.check_update_candidate()
+
+ return {'FINISHED'}
+
+
+@BlClassRegistry()
+class MUV_OT_UpdateAddon(bpy.types.Operator):
+ bl_idname = "uv.muv_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, context):
+ updater = AddonUpdatorManager.get_instance()
+ updater.update(self.branch_name)
+
+ return {'FINISHED'}
+
+
+def get_update_candidate_branches(_, __):
+ updater = AddonUpdatorManager.get_instance()
+ if not updater.candidate_checked():
+ return []
+
+ return [(name, name, "") for name in updater.get_candidate_branch_names()]
+
+
+@BlClassRegistry()
class Preferences(AddonPreferences):
"""Preferences class: Preferences for this add-on"""
@@ -119,15 +210,15 @@ class Preferences(AddonPreferences):
remove_builtin_menu()
# enable to add features to built-in menu
- enable_builtin_menu = BoolProperty(
+ enable_builtin_menu: BoolProperty(
name="Built-in Menu",
description="Enable built-in menu",
default=True,
- update=update_enable_builtin_menu
+ update=update_enable_builtin_menu,
)
# for UV Sculpt
- uv_sculpt_brush_color = FloatVectorProperty(
+ uv_sculpt_brush_color: FloatVectorProperty(
name="Color",
description="Color",
default=(1.0, 0.4, 0.4, 1.0),
@@ -138,7 +229,7 @@ class Preferences(AddonPreferences):
)
# for Overlapped UV
- uv_inspection_overlapped_color = FloatVectorProperty(
+ uv_inspection_overlapped_color: FloatVectorProperty(
name="Color",
description="Color",
default=(0.0, 0.0, 1.0, 0.3),
@@ -149,7 +240,7 @@ class Preferences(AddonPreferences):
)
# for Flipped UV
- uv_inspection_flipped_color = FloatVectorProperty(
+ uv_inspection_flipped_color: FloatVectorProperty(
name="Color",
description="Color",
default=(1.0, 0.0, 0.0, 0.3),
@@ -160,7 +251,7 @@ class Preferences(AddonPreferences):
)
# for Texture Projection
- texture_projection_canvas_padding = FloatVectorProperty(
+ texture_projection_canvas_padding: FloatVectorProperty(
name="Canvas Padding",
description="Canvas Padding",
size=2,
@@ -169,13 +260,13 @@ class Preferences(AddonPreferences):
default=(20.0, 20.0))
# for UV Bounding Box
- uv_bounding_box_cp_size = FloatProperty(
+ 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(
+ uv_bounding_box_cp_react_size: FloatProperty(
name="React Size",
description="Size event fired",
default=10.0,
@@ -183,7 +274,7 @@ class Preferences(AddonPreferences):
max=100.0)
# for UI
- category = EnumProperty(
+ category: EnumProperty(
name="Category",
description="Preferences Category",
items=[
@@ -193,68 +284,42 @@ class Preferences(AddonPreferences):
],
default='INFO'
)
- info_desc_expanded = BoolProperty(
+ info_desc_expanded: BoolProperty(
name="Description",
description="Description",
default=False
)
- info_loc_expanded = BoolProperty(
+ info_loc_expanded: BoolProperty(
name="Location",
description="Location",
default=False
)
- conf_uv_sculpt_expanded = BoolProperty(
+ conf_uv_sculpt_expanded: BoolProperty(
name="UV Sculpt",
description="UV Sculpt",
default=False
)
- conf_uv_inspection_expanded = BoolProperty(
+ conf_uv_inspection_expanded: BoolProperty(
name="UV Inspection",
description="UV Inspection",
default=False
)
- conf_texture_projection_expanded = BoolProperty(
+ conf_texture_projection_expanded: BoolProperty(
name="Texture Projection",
description="Texture Projection",
default=False
)
- conf_uv_bounding_box_expanded = BoolProperty(
+ conf_uv_bounding_box_expanded: BoolProperty(
name="UV Bounding Box",
description="UV Bounding Box",
default=False
)
# for add-on updater
- auto_check_update = BoolProperty(
- name="Auto-check for Update",
- description="If enabled, auto-check for updates using an interval",
- default=False
- )
- updater_intrval_months = IntProperty(
- name='Months',
- description="Number of months between checking for updates",
- default=0,
- min=0
- )
- updater_intrval_days = IntProperty(
- name='Days',
- description="Number of days between checking for updates",
- default=7,
- min=0
- )
- updater_intrval_hours = IntProperty(
- name='Hours',
- description="Number of hours between checking for updates",
- default=0,
- min=0,
- max=23
- )
- updater_intrval_minutes = IntProperty(
- name='Minutes',
- description="Number of minutes between checking for updates",
- default=0,
- min=0,
- max=59
+ updater_branch_to_update: EnumProperty(
+ name="branch",
+ description="Target branch to update add-on",
+ items=get_update_candidate_branches
)
def draw(self, context):
@@ -263,17 +328,20 @@ class Preferences(AddonPreferences):
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:
- column = layout.column(align=True)
- column.label("Magic UV is composed of many UV editing" +
- " features.")
- column.label("See tutorial page if you are new to this" +
- " add-on.")
- column.label("https://github.com/nutti/Magic-UV/wiki/Tutorial")
+ 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",
@@ -281,71 +349,78 @@ class Preferences(AddonPreferences):
else 'DISCLOSURE_TRI_RIGHT')
if self.info_loc_expanded:
row = layout.row(align=True)
- sp = row.split(percentage=0.5)
- sp.label("3D View > Tool shelf > Copy/Paste UV (Object mode)")
- sp = sp.split(percentage=1.0)
+ sp = row.split(factor=0.5)
+ sp.label(text="3D View > Tool shelf > " +
+ "Copy/Paste UV (Object mode)")
+ sp = sp.split(factor=1.0)
col = sp.column(align=True)
- col.label("Copy/Paste UV (Among objects)")
+ col.label(text="Copy/Paste UV (Among objects)")
row = layout.row(align=True)
- sp = row.split(percentage=0.5)
- sp.label("3D View > Tool shelf > Copy/Paste UV (Edit mode)")
- sp = sp.split(percentage=1.0)
+ sp = row.split(factor=0.5)
+ sp.label(text="3D View > Tool shelf > " +
+ "Copy/Paste UV (Edit mode)")
+ sp = sp.split(factor=1.0)
col = sp.column(align=True)
- col.label("Copy/Paste UV (Among faces in 3D View)")
- col.label("Transfer UV")
+ col.label(text="Copy/Paste UV (Among faces in 3D View)")
+ col.label(text="Transfer UV")
row = layout.row(align=True)
- sp = row.split(percentage=0.5)
- sp.label("3D View > Tool shelf > UV Manipulation (Edit mode)")
- sp = sp.split(percentage=1.0)
+ sp = row.split(factor=0.5)
+ sp.label(text="3D View > Tool shelf > " +
+ "UV Manipulation (Edit mode)")
+ sp = sp.split(factor=1.0)
col = sp.column(align=True)
- col.label("Flip/Rotate UV")
- col.label("Mirror UV")
- col.label("Move UV")
- col.label("World Scale UV")
- col.label("Preserve UV Aspect")
- col.label("Texture Lock")
- col.label("Texture Wrap")
- col.label("UV Sculpt")
+ 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 = row.split(percentage=0.5)
- sp.label("3D View > Tool shelf > UV Manipulation (Edit mode)")
- sp = sp.split(percentage=1.0)
+ sp = row.split(factor=0.5)
+ sp.label(text="3D View > Tool shelf > " +
+ "UV Manipulation (Edit mode)")
+ sp = sp.split(factor=1.0)
col = sp.column(align=True)
- col.label("Unwrap Constraint")
- col.label("Texture Projection")
- col.label("UVW")
+ col.label(text="Unwrap Constraint")
+ col.label(text="Texture Projection")
+ col.label(text="UVW")
row = layout.row(align=True)
- sp = row.split(percentage=0.5)
- sp.label("UV/Image Editor > Tool shelf > Copy/Paste UV")
- sp = sp.split(percentage=1.0)
+ sp = row.split(factor=0.5)
+ sp.label(text="UV/Image Editor > Tool shelf > Copy/Paste UV")
+ sp = sp.split(factor=1.0)
col = sp.column(align=True)
- col.label("Copy/Paste UV (Among faces in UV/Image Editor)")
+ col.label(text="Copy/Paste UV (Among faces in UV/Image Editor)")
row = layout.row(align=True)
- sp = row.split(percentage=0.5)
- sp.label("UV/Image Editor > Tool shelf > UV Manipulation")
- sp = sp.split(percentage=1.0)
+ sp = row.split(factor=0.5)
+ sp.label(text="UV/Image Editor > Tool shelf > UV Manipulation")
+ sp = sp.split(factor=1.0)
col = sp.column(align=True)
- col.label("Align UV")
- col.label("Smooth UV")
- col.label("Select UV")
- col.label("Pack UV (Extension)")
+ 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 = row.split(percentage=0.5)
- sp.label("UV/Image Editor > Tool shelf > Editor Enhancement")
- sp = sp.split(percentage=1.0)
+ sp = row.split(factor=0.5)
+ sp.label(text="UV/Image Editor > Tool shelf > " +
+ "Editor Enhancement")
+ sp = sp.split(factor=1.0)
col = sp.column(align=True)
- col.label("Align UV Cursor")
- col.label("UV Cursor Location")
- col.label("UV Bounding Box")
- col.label("UV Inspection")
+ 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.separator()
@@ -355,11 +430,11 @@ class Preferences(AddonPreferences):
icon='DISCLOSURE_TRI_DOWN' if self.conf_uv_sculpt_expanded
else 'DISCLOSURE_TRI_RIGHT')
if self.conf_uv_sculpt_expanded:
- sp = layout.split(percentage=0.05)
+ sp = layout.split(factor=0.05)
col = sp.column() # spacer
- sp = sp.split(percentage=0.3)
+ sp = sp.split(factor=0.3)
col = sp.column()
- col.label("Brush Color:")
+ col.label(text="Brush Color:")
col.prop(self, "uv_sculpt_brush_color", text="")
layout.separator()
@@ -368,15 +443,15 @@ class Preferences(AddonPreferences):
icon='DISCLOSURE_TRI_DOWN' if self.conf_uv_inspection_expanded
else 'DISCLOSURE_TRI_RIGHT')
if self.conf_uv_inspection_expanded:
- sp = layout.split(percentage=0.05)
+ sp = layout.split(factor=0.05)
col = sp.column() # spacer
- sp = sp.split(percentage=0.3)
+ sp = sp.split(factor=0.3)
col = sp.column()
- col.label("Overlapped UV Color:")
+ col.label(text="Overlapped UV Color:")
col.prop(self, "uv_inspection_overlapped_color", text="")
- sp = sp.split(percentage=0.45)
+ sp = sp.split(factor=0.45)
col = sp.column()
- col.label("Flipped UV Color:")
+ col.label(text="Flipped UV Color:")
col.prop(self, "uv_inspection_flipped_color", text="")
layout.separator()
@@ -387,9 +462,9 @@ class Preferences(AddonPreferences):
if self.conf_texture_projection_expanded
else 'DISCLOSURE_TRI_RIGHT')
if self.conf_texture_projection_expanded:
- sp = layout.split(percentage=0.05)
+ sp = layout.split(factor=0.05)
col = sp.column() # spacer
- sp = sp.split(percentage=0.3)
+ sp = sp.split(factor=0.3)
col = sp.column()
col.prop(self, "texture_projection_canvas_padding")
layout.separator()
@@ -400,14 +475,61 @@ class Preferences(AddonPreferences):
if self.conf_uv_bounding_box_expanded
else 'DISCLOSURE_TRI_RIGHT')
if self.conf_uv_bounding_box_expanded:
- sp = layout.split(percentage=0.05)
+ sp = layout.split(factor=0.05)
col = sp.column() # spacer
- sp = sp.split(percentage=0.3)
+ sp = sp.split(factor=0.3)
col = sp.column()
- col.label("Control Point:")
+ 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':
- addon_updater_ops.update_settings_ui(self, context)
+ 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(self, "updater_branch_to_update", text="Target")
+ ops = row.operator(
+ MUV_OT_UpdateAddon.bl_idname, text="Update",
+ icon='TRIA_DOWN_BAR')
+ ops.branch_name = self.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')
diff --git a/uv_magic_uv/ui/IMAGE_MT_uvs.py b/uv_magic_uv/ui/IMAGE_MT_uvs.py
index e7dda379..dfb509c7 100644
--- a/uv_magic_uv/ui/IMAGE_MT_uvs.py
+++ b/uv_magic_uv/ui/IMAGE_MT_uvs.py
@@ -28,6 +28,17 @@ import bpy
from ..op import (
copy_paste_uv_uvedit,
)
+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
__all__ = [
@@ -54,3 +65,133 @@ class MUV_MT_CopyPasteUV_UVEdit(bpy.types.Menu):
layout.operator(
copy_paste_uv_uvedit.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_align_uv_menu"
+ 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_select_uv_menu"
+ 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_align_uv_cursor_menu"
+ 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_uv_inspection_menu"
+ 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/uv_magic_uv/ui/VIEW3D_MT_uv_map.py b/uv_magic_uv/ui/VIEW3D_MT_uv_map.py
index c5698504..012ce047 100644
--- a/uv_magic_uv/ui/VIEW3D_MT_uv_map.py
+++ b/uv_magic_uv/ui/VIEW3D_MT_uv_map.py
@@ -30,6 +30,25 @@ from ..op import (
transfer_uv,
uvw,
)
+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,
+ MUV_OT_TextureProjection_Project,
+)
from ..utils.bl_class_registry import BlClassRegistry
__all__ = [
@@ -90,6 +109,95 @@ class MUV_MT_TransferUV(bpy.types.Menu):
@BlClassRegistry()
+class MUV_MT_TextureLock(bpy.types.Menu):
+ """
+ Menu class: Master menu of Texture Lock
+ """
+
+ bl_idname = "uv.muv_texture_lock_menu"
+ 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_world_scale_uv_menu"
+ 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_texture_wrap_menu"
+ 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
@@ -109,3 +217,43 @@ class MUV_MT_UVW(bpy.types.Menu):
ops = layout.operator(uvw.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_preserve_uv_aspect_menu"
+ 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_texture_projection_menu"
+ 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") \ No newline at end of file
diff --git a/uv_magic_uv/ui/__init__.py b/uv_magic_uv/ui/__init__.py
index 5f7e0c5e..ce15dbcc 100644
--- a/uv_magic_uv/ui/__init__.py
+++ b/uv_magic_uv/ui/__init__.py
@@ -30,6 +30,8 @@ if "bpy" in locals():
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)
@@ -39,6 +41,8 @@ else:
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
diff --git a/uv_magic_uv/ui/uvedit_editor_enhancement.py b/uv_magic_uv/ui/uvedit_editor_enhancement.py
new file mode 100644
index 00000000..cfd9ef28
--- /dev/null
+++ b/uv_magic_uv/ui/uvedit_editor_enhancement.py
@@ -0,0 +1,149 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+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
+
+__all__ = [
+ 'MUV_PT_UVEdit_EditorEnhancement',
+]
+
+
+@BlClassRegistry()
+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='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/uv_magic_uv/ui/uvedit_uv_manipulation.py b/uv_magic_uv/ui/uvedit_uv_manipulation.py
new file mode 100644
index 00000000..f5bd27e3
--- /dev/null
+++ b/uv_magic_uv/ui/uvedit_uv_manipulation.py
@@ -0,0 +1,130 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+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
+
+
+@BlClassRegistry()
+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='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/uv_magic_uv/ui/view3d_uv_manipulation.py b/uv_magic_uv/ui/view3d_uv_manipulation.py
index 365a0dc8..4c09bdf2 100644
--- a/uv_magic_uv/ui/view3d_uv_manipulation.py
+++ b/uv_magic_uv/ui/view3d_uv_manipulation.py
@@ -25,11 +25,28 @@ __date__ = "17 Nov 2018"
import bpy
-from ..op import (
- flip_rotate_uv,
- mirror_uv,
- move_uv,
+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
__all__ = [
@@ -62,8 +79,7 @@ class MUV_PT_View3D_UVManipulation(bpy.types.Panel):
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(flip_rotate_uv.MUV_OT_FlipRotate.bl_idname,
- text="Flip/Rotate")
+ 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")
@@ -71,8 +87,7 @@ class MUV_PT_View3D_UVManipulation(bpy.types.Panel):
box.prop(sc, "muv_mirror_uv_enabled", text="Mirror UV")
if sc.muv_mirror_uv_enabled:
row = box.row()
- ops = row.operator(mirror_uv.MUV_OT_MirrorUV.bl_idname,
- text="Mirror")
+ 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="")
@@ -80,9 +95,190 @@ class MUV_PT_View3D_UVManipulation(bpy.types.Panel):
box.prop(sc, "muv_move_uv_enabled", text="Move UV")
if sc.muv_move_uv_enabled:
col = box.column()
- if not move_uv.MUV_OT_MoveUV.is_running(context):
- col.operator(move_uv.MUV_OT_MoveUV.bl_idname, icon='PLAY',
+ if not MUV_OT_MoveUV.is_running(context):
+ col.operator(MUV_OT_MoveUV.bl_idname, icon='PLAY',
text="Start")
else:
- col.operator(move_uv.MUV_OT_MoveUV.bl_idname, icon='PAUSE',
+ 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 = box.split(factor=0.5)
+ col = sp.column()
+ col.prop(sc, "muv_world_scale_uv_tgt_texture_size",
+ text="Texture Size")
+ sp = sp.split(factor=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 = box.split(factor=0.4)
+ col = sp.column(align=True)
+ col.label(text="Source:")
+ sp = sp.split(factor=1.0)
+ col = sp.column(align=True)
+ col.operator(MUV_OT_WorldScaleUV_Measure.bl_idname,
+ text="Measure")
+
+ sp = box.split(factor=0.7)
+ col = sp.column(align=True)
+ col.prop(sc, "muv_world_scale_uv_src_density", text="Density")
+ col.enabled = False
+ sp = sp.split(factor=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 = box.split(factor=0.4)
+ col = sp.column(align=True)
+ col.label(text="Source:")
+ sp = sp.split(factor=1.0)
+ col = sp.column(align=True)
+ col.operator(MUV_OT_WorldScaleUV_Measure.bl_idname,
+ text="Measure")
+
+ sp = box.split(factor=0.7)
+ col = sp.column(align=True)
+ col.prop(sc, "muv_world_scale_uv_src_density", text="Density")
+ col.enabled = False
+ sp = sp.split(factor=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 = box.split(factor=0.4)
+ col = sp.column(align=True)
+ col.label(text="Source:")
+ sp = sp.split(factor=1.0)
+ col = sp.column(align=True)
+ col.operator(MUV_OT_WorldScaleUV_Measure.bl_idname,
+ text="Measure")
+
+ sp = box.split(factor=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 = sp.split(factor=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/uv_magic_uv/ui/view3d_uv_mapping.py b/uv_magic_uv/ui/view3d_uv_mapping.py
index c596008e..e64a2ce1 100644
--- a/uv_magic_uv/ui/view3d_uv_mapping.py
+++ b/uv_magic_uv/ui/view3d_uv_mapping.py
@@ -28,6 +28,11 @@ import bpy
from ..op import (
uvw,
)
+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
__all__ = [
@@ -57,6 +62,48 @@ class MUV_PT_View3D_UVMapping(bpy.types.Panel):
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)
diff --git a/uv_magic_uv/utils/__init__.py b/uv_magic_uv/utils/__init__.py
index 4ce9d907..333a3873 100644
--- a/uv_magic_uv/utils/__init__.py
+++ b/uv_magic_uv/utils/__init__.py
@@ -25,9 +25,11 @@ __date__ = "17 Nov 2018"
if "bpy" in locals():
import importlib
+ importlib.reload(addon_updator)
importlib.reload(bl_class_registry)
importlib.reload(property_class_registry)
else:
+ from . import addon_updator
from . import bl_class_registry
from . import property_class_registry
diff --git a/uv_magic_uv/utils/addon_updator.py b/uv_magic_uv/utils/addon_updator.py
new file mode 100644
index 00000000..42e4309e
--- /dev/null
+++ b/uv_magic_uv/utils/addon_updator.py
@@ -0,0 +1,345 @@
+# <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__ = "5.2"
+__date__ = "17 Nov 2018"
+
+from threading import Lock
+import urllib
+import urllib.request
+import ssl
+import json
+import os
+import zipfile
+import shutil
+import datetime
+
+
+def _request(url, json_decode=True):
+ 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 + "/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) + "/" + 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 + "/" + 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.")
+
+ for info in self.__update_candidate:
+ if info.name == version_name:
+ break
+ else:
+ raise RuntimeError("{} is not found in update candidate"
+ .format(version_name))
+
+ 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,
+ 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 == "" or _compare_version(_parse_release_version(version),
+ _parse_release_version(latest)) > 0:
+ latest = version
+
+ return latest