From a0044523a66f693c3b8005bc351120c87b5c8c34 Mon Sep 17 00:00:00 2001 From: nutti Date: Sun, 6 Jan 2019 10:27:27 +0900 Subject: Magic UV: Phase 2 for porting to Blender 2.8 All features are available on Blender 2.8 --- uv_magic_uv/__init__.py | 38 +- uv_magic_uv/addon_updater.py | 1501 --------------------------- uv_magic_uv/addon_updater_ops.py | 1357 ------------------------ uv_magic_uv/common.py | 180 +++- uv_magic_uv/impl/__init__.py | 70 ++ uv_magic_uv/impl/align_uv_cursor_impl.py | 239 +++++ uv_magic_uv/impl/align_uv_impl.py | 820 +++++++++++++++ uv_magic_uv/impl/move_uv_impl.py | 2 +- uv_magic_uv/impl/pack_uv_impl.py | 202 ++++ uv_magic_uv/impl/preserve_uv_aspect_impl.py | 359 +++++++ uv_magic_uv/impl/select_uv_impl.py | 120 +++ uv_magic_uv/impl/smooth_uv_impl.py | 215 ++++ uv_magic_uv/impl/texture_lock_impl.py | 455 ++++++++ uv_magic_uv/impl/texture_projection_impl.py | 126 +++ uv_magic_uv/impl/texture_wrap_impl.py | 236 +++++ uv_magic_uv/impl/unwrap_constraint_impl.py | 98 ++ uv_magic_uv/impl/uv_bounding_box_impl.py | 55 + uv_magic_uv/impl/uv_inspection_impl.py | 70 ++ uv_magic_uv/impl/uv_sculpt_impl.py | 57 + uv_magic_uv/impl/uvw_impl.py | 8 +- uv_magic_uv/impl/world_scale_uv_impl.py | 383 +++++++ uv_magic_uv/legacy/op/align_uv.py | 791 +------------- uv_magic_uv/legacy/op/align_uv_cursor.py | 126 +-- uv_magic_uv/legacy/op/pack_uv.py | 164 +-- uv_magic_uv/legacy/op/preserve_uv_aspect.py | 171 +-- uv_magic_uv/legacy/op/select_uv.py | 100 +- uv_magic_uv/legacy/op/smooth_uv.py | 196 +--- uv_magic_uv/legacy/op/texture_lock.py | 429 +------- uv_magic_uv/legacy/op/texture_projection.py | 124 +-- uv_magic_uv/legacy/op/texture_wrap.py | 212 +--- uv_magic_uv/legacy/op/unwrap_constraint.py | 81 +- uv_magic_uv/legacy/op/uv_bounding_box.py | 58 +- uv_magic_uv/legacy/op/uv_inspection.py | 63 +- uv_magic_uv/legacy/op/uv_sculpt.py | 83 +- uv_magic_uv/legacy/op/world_scale_uv.py | 362 +------ uv_magic_uv/legacy/preferences.py | 52 +- uv_magic_uv/lib/__init__.py | 32 + uv_magic_uv/lib/bglx.py | 191 ++++ uv_magic_uv/op/__init__.py | 28 + uv_magic_uv/op/align_uv.py | 231 +++++ uv_magic_uv/op/align_uv_cursor.py | 141 +++ uv_magic_uv/op/pack_uv.py | 129 +++ uv_magic_uv/op/preserve_uv_aspect.py | 124 +++ uv_magic_uv/op/select_uv.py | 92 ++ uv_magic_uv/op/smooth_uv.py | 105 ++ uv_magic_uv/op/texture_lock.py | 158 +++ uv_magic_uv/op/texture_projection.py | 292 ++++++ uv_magic_uv/op/texture_wrap.py | 113 ++ uv_magic_uv/op/unwrap_constraint.py | 125 +++ uv_magic_uv/op/uv_bounding_box.py | 813 +++++++++++++++ uv_magic_uv/op/uv_inspection.py | 235 +++++ uv_magic_uv/op/uv_sculpt.py | 446 ++++++++ uv_magic_uv/op/world_scale_uv.py | 360 +++++++ uv_magic_uv/preferences.py | 360 ++++--- uv_magic_uv/ui/IMAGE_MT_uvs.py | 141 +++ uv_magic_uv/ui/VIEW3D_MT_uv_map.py | 148 +++ uv_magic_uv/ui/__init__.py | 4 + uv_magic_uv/ui/uvedit_editor_enhancement.py | 149 +++ uv_magic_uv/ui/uvedit_uv_manipulation.py | 130 +++ uv_magic_uv/ui/view3d_uv_manipulation.py | 218 +++- uv_magic_uv/ui/view3d_uv_mapping.py | 47 + uv_magic_uv/utils/__init__.py | 2 + uv_magic_uv/utils/addon_updator.py | 345 ++++++ 63 files changed, 8982 insertions(+), 5780 deletions(-) delete mode 100644 uv_magic_uv/addon_updater.py delete mode 100644 uv_magic_uv/addon_updater_ops.py create mode 100644 uv_magic_uv/impl/align_uv_cursor_impl.py create mode 100644 uv_magic_uv/impl/align_uv_impl.py create mode 100644 uv_magic_uv/impl/pack_uv_impl.py create mode 100644 uv_magic_uv/impl/preserve_uv_aspect_impl.py create mode 100644 uv_magic_uv/impl/select_uv_impl.py create mode 100644 uv_magic_uv/impl/smooth_uv_impl.py create mode 100644 uv_magic_uv/impl/texture_lock_impl.py create mode 100644 uv_magic_uv/impl/texture_projection_impl.py create mode 100644 uv_magic_uv/impl/texture_wrap_impl.py create mode 100644 uv_magic_uv/impl/unwrap_constraint_impl.py create mode 100644 uv_magic_uv/impl/uv_bounding_box_impl.py create mode 100644 uv_magic_uv/impl/uv_inspection_impl.py create mode 100644 uv_magic_uv/impl/uv_sculpt_impl.py create mode 100644 uv_magic_uv/impl/world_scale_uv_impl.py create mode 100644 uv_magic_uv/lib/__init__.py create mode 100644 uv_magic_uv/lib/bglx.py create mode 100644 uv_magic_uv/op/align_uv.py create mode 100644 uv_magic_uv/op/align_uv_cursor.py create mode 100644 uv_magic_uv/op/pack_uv.py create mode 100644 uv_magic_uv/op/preserve_uv_aspect.py create mode 100644 uv_magic_uv/op/select_uv.py create mode 100644 uv_magic_uv/op/smooth_uv.py create mode 100644 uv_magic_uv/op/texture_lock.py create mode 100644 uv_magic_uv/op/texture_projection.py create mode 100644 uv_magic_uv/op/texture_wrap.py create mode 100644 uv_magic_uv/op/unwrap_constraint.py create mode 100644 uv_magic_uv/op/uv_bounding_box.py create mode 100644 uv_magic_uv/op/uv_inspection.py create mode 100644 uv_magic_uv/op/uv_sculpt.py create mode 100644 uv_magic_uv/op/world_scale_uv.py create mode 100644 uv_magic_uv/ui/uvedit_editor_enhancement.py create mode 100644 uv_magic_uv/ui/uvedit_uv_manipulation.py create mode 100644 uv_magic_uv/utils/addon_updator.py (limited to 'uv_magic_uv') 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 "".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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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', @@ -161,6 +162,48 @@ def remove_builtin_menu(): bpy.types.VIEW3D_MT_object.remove(view3d_object_menu_fn) +@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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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__ = [ @@ -89,6 +108,95 @@ class MUV_MT_TransferUV(bpy.types.Menu): ops.copy_seams = sc.muv_transfer_uv_copy_seams +@BlClassRegistry() +class MUV_MT_TextureLock(bpy.types.Menu): + """ + Menu class: Master menu of Texture Lock + """ + + bl_idname = "uv.muv_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): """ @@ -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 @@ +# + +# ##### 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 " +__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 @@ +# + +# ##### 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 " +__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__ = [ @@ -56,6 +61,48 @@ class MUV_PT_View3D_UVMapping(bpy.types.Panel): sc = context.scene layout = self.layout + box = layout.box() + box.prop(sc, "muv_unwrap_constraint_enabled", text="Unwrap Constraint") + if sc.muv_unwrap_constraint_enabled: + ops = box.operator(MUV_OT_UnwrapConstraint.bl_idname, + text="Unwrap") + ops.u_const = sc.muv_unwrap_constraint_u_const + ops.v_const = sc.muv_unwrap_constraint_v_const + row = box.row(align=True) + row.prop(sc, "muv_unwrap_constraint_u_const", text="U-Constraint") + row.prop(sc, "muv_unwrap_constraint_v_const", text="V-Constraint") + + box = layout.box() + box.prop(sc, "muv_texture_projection_enabled", + text="Texture Projection") + if sc.muv_texture_projection_enabled: + row = box.row() + row.prop( + sc, "muv_texture_projection_enable", + text="Disable" + if MUV_OT_TextureProjection.is_running(context) + else "Enable", + icon='RESTRICT_VIEW_OFF' + if MUV_OT_TextureProjection.is_running(context) + else 'RESTRICT_VIEW_ON') + row.prop(sc, "muv_texture_projection_tex_image", text="") + box.prop(sc, "muv_texture_projection_tex_transparency", + text="Transparency") + col = box.column(align=True) + row = col.row() + row.prop(sc, "muv_texture_projection_adjust_window", + text="Adjust Window") + if not sc.muv_texture_projection_adjust_window: + row.prop(sc, "muv_texture_projection_tex_magnitude", + text="Magnitude") + col.prop(sc, "muv_texture_projection_apply_tex_aspect", + text="Texture Aspect Ratio") + col.prop(sc, "muv_texture_projection_assign_uvmap", + text="Assign UVMap") + box.operator( + MUV_OT_TextureProjection_Project.bl_idname, + text="Project") + box = layout.box() box.prop(sc, "muv_uvw_enabled", text="UVW") if sc.muv_uvw_enabled: 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 @@ +# + +# ##### 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 " +__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 -- cgit v1.2.3