From 2532b96844c121b710e1a1973d2a5ff824ab3be4 Mon Sep 17 00:00:00 2001 From: Nutti Date: Sat, 17 Nov 2018 21:11:55 +0900 Subject: Magic UV: Release v5.2 * Bulit-in menu preferences * Add-on updater * Copy/Paste UV * Add option "[New]" for pasting to newly allocated UV map * Add option "[All]" for pasting all UV maps at once * Align UV * Add option "Mesh Influence" * World Scale UV * Add mode option "Manual" to allow the user specify the density manually * Improve UI * Cleanup documents * Fix bugs --- uv_magic_uv/__init__.py | 20 +- uv_magic_uv/addon_updater.py | 1501 +++++++++++++++++++++ uv_magic_uv/addon_updater_ops.py | 1357 +++++++++++++++++++ uv_magic_uv/common.py | 526 +++++++- uv_magic_uv/op/__init__.py | 6 +- uv_magic_uv/op/align_uv.py | 389 ++++-- uv_magic_uv/op/align_uv_cursor.py | 111 +- uv_magic_uv/op/copy_paste_uv.py | 883 ++++++------ uv_magic_uv/op/copy_paste_uv_object.py | 312 +++-- uv_magic_uv/op/copy_paste_uv_uvedit.py | 78 +- uv_magic_uv/op/flip_rotate_uv.py | 62 +- uv_magic_uv/op/mirror_uv.py | 67 +- uv_magic_uv/op/move_uv.py | 95 +- uv_magic_uv/op/pack_uv.py | 76 +- uv_magic_uv/op/preserve_uv_aspect.py | 86 +- uv_magic_uv/op/select_uv.py | 161 +++ uv_magic_uv/op/smooth_uv.py | 76 +- uv_magic_uv/op/texture_lock.py | 305 +++-- uv_magic_uv/op/texture_projection.py | 245 +++- uv_magic_uv/op/texture_wrap.py | 102 +- uv_magic_uv/op/transfer_uv.py | 110 +- uv_magic_uv/op/unwrap_constraint.py | 68 +- uv_magic_uv/op/uv_bounding_box.py | 374 +++-- uv_magic_uv/op/uv_inspection.py | 643 ++------- uv_magic_uv/op/uv_sculpt.py | 271 ++-- uv_magic_uv/op/uvw.py | 72 +- uv_magic_uv/op/world_scale_uv.py | 666 +++++++-- uv_magic_uv/preferences.py | 508 +++++-- uv_magic_uv/properites.py | 812 ++--------- uv_magic_uv/ui/IMAGE_MT_uvs.py | 186 +++ uv_magic_uv/ui/VIEW3D_MT_object.py | 50 + uv_magic_uv/ui/VIEW3D_MT_uv_map.py | 236 ++++ uv_magic_uv/ui/__init__.py | 14 +- uv_magic_uv/ui/uvedit_copy_paste_uv.py | 15 +- uv_magic_uv/ui/uvedit_editor_enhance.py | 136 -- uv_magic_uv/ui/uvedit_editor_enhancement.py | 144 ++ uv_magic_uv/ui/uvedit_uv_manipulation.py | 99 +- uv_magic_uv/ui/view3d_copy_paste_uv_editmode.py | 50 +- uv_magic_uv/ui/view3d_copy_paste_uv_objectmode.py | 18 +- uv_magic_uv/ui/view3d_uv_manipulation.py | 275 ++-- uv_magic_uv/ui/view3d_uv_mapping.py | 71 +- 41 files changed, 8316 insertions(+), 2960 deletions(-) create mode 100644 uv_magic_uv/addon_updater.py create mode 100644 uv_magic_uv/addon_updater_ops.py create mode 100644 uv_magic_uv/op/select_uv.py create mode 100644 uv_magic_uv/ui/IMAGE_MT_uvs.py create mode 100644 uv_magic_uv/ui/VIEW3D_MT_object.py create mode 100644 uv_magic_uv/ui/VIEW3D_MT_uv_map.py delete mode 100644 uv_magic_uv/ui/uvedit_editor_enhance.py create mode 100644 uv_magic_uv/ui/uvedit_editor_enhancement.py diff --git a/uv_magic_uv/__init__.py b/uv_magic_uv/__init__.py index 080d2414..20709e79 100644 --- a/uv_magic_uv/__init__.py +++ b/uv_magic_uv/__init__.py @@ -20,15 +20,15 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__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, 1, 0), + "version": (5, 2, 0), "blender": (2, 79, 0), "location": "See Add-ons Preferences", "description": "UV Toolset. See Add-ons Preferences for details", @@ -47,24 +47,36 @@ if "bpy" in locals(): importlib.reload(common) importlib.reload(preferences) importlib.reload(properites) + importlib.reload(addon_updater_ops) + importlib.reload(addon_updater) else: from . import op from . import ui from . import common from . import preferences from . import properites + from . import addon_updater_ops + from . import addon_updater import bpy def register(): - bpy.utils.register_module(__name__) + if not common.is_console_mode(): + addon_updater_ops.register(bl_info) properites.init_props(bpy.types.Scene) + bpy.utils.register_module(__name__) + if preferences.Preferences.enable_builtin_menu: + preferences.add_builtin_menu() def unregister(): + if preferences.Preferences.enable_builtin_menu: + preferences.remove_builtin_menu() bpy.utils.unregister_module(__name__) properites.clear_props(bpy.types.Scene) + if not common.is_console_mode(): + addon_updater_ops.unregister() if __name__ == "__main__": diff --git a/uv_magic_uv/addon_updater.py b/uv_magic_uv/addon_updater.py new file mode 100644 index 00000000..70b6a287 --- /dev/null +++ b/uv_magic_uv/addon_updater.py @@ -0,0 +1,1501 @@ +# ##### 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 new file mode 100644 index 00000000..418334ad --- /dev/null +++ b/uv_magic_uv/addon_updater_ops.py @@ -0,0 +1,1357 @@ +# ##### 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.user_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.user_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.user_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.user_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.user_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.user_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 475efd59..b0c4306e 100644 --- a/uv_magic_uv/common.py +++ b/uv_magic_uv/common.py @@ -20,19 +20,63 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" from collections import defaultdict from pprint import pprint from math import fabs, sqrt +import os import bpy from mathutils import Vector import bmesh -DEBUG = False +__all__ = [ + 'is_console_mode', + 'debug_print', + 'check_version', + 'redraw_all_areas', + 'get_space', + 'create_bmesh', + 'create_new_uv_map', + 'get_island_info', + 'get_island_info_from_bmesh', + 'get_island_info_from_faces', + 'get_uvimg_editor_board_size', + 'calc_polygon_2d_area', + 'calc_polygon_3d_area', + 'measure_mesh_area', + 'measure_uv_area', + 'diff_point_to_segment', + 'get_loop_sequences', +] + + +__DEBUG_MODE = False + + +def is_console_mode(): + if "MUV_CONSOLE_MODE" not in os.environ: + return False + return os.environ["MUV_CONSOLE_MODE"] == "True" + + +def is_debug_mode(): + return __DEBUG_MODE + + +def enable_debugg_mode(): + # pylint: disable=W0603 + global __DEBUG_MODE + __DEBUG_MODE = True + + +def disable_debug_mode(): + # pylint: disable=W0603 + global __DEBUG_MODE + __DEBUG_MODE = False def debug_print(*s): @@ -40,7 +84,7 @@ def debug_print(*s): Print message to console in debugging mode """ - if DEBUG: + if is_debug_mode(): pprint(s) @@ -91,6 +135,71 @@ def get_space(area_type, region_type, space_type): return (area, region, space) +def mouse_on_region(event, area_type, region_type): + pos = Vector((event.mouse_x, event.mouse_y)) + + _, region, _ = get_space(area_type, region_type, "") + if region is None: + return False + + if (pos.x > region.x) and (pos.x < region.x + region.width) and \ + (pos.y > region.y) and (pos.y < region.y + region.height): + return True + + return False + + +def mouse_on_area(event, area_type): + pos = Vector((event.mouse_x, event.mouse_y)) + + area, _, _ = get_space(area_type, "", "") + if area is None: + return False + + if (pos.x > area.x) and (pos.x < area.x + area.width) and \ + (pos.y > area.y) and (pos.y < area.y + area.height): + return True + + return False + + +def mouse_on_regions(event, area_type, regions): + if not mouse_on_area(event, area_type): + return False + + for region in regions: + result = mouse_on_region(event, area_type, region) + if result: + return True + + return False + + +def create_bmesh(obj): + bm = bmesh.from_edit_mesh(obj.data) + if check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + return bm + + +def create_new_uv_map(obj, name=None): + uv_maps_old = {l.name for l in obj.data.uv_layers} + bpy.ops.mesh.uv_texture_add() + uv_maps_new = {l.name for l in obj.data.uv_layers} + diff = uv_maps_new - uv_maps_old + + if not list(diff): + return None # no more UV maps can not be created + + # rename UV map + new = obj.data.uv_layers[list(diff)[0]] + if name: + new.name = name + + return new + + def __get_island_info(uv_layer, islands): """ get information about each island @@ -273,7 +382,7 @@ def measure_mesh_area(obj): return mesh_area -def measure_uv_area(obj): +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() @@ -296,22 +405,38 @@ def measure_uv_area(obj): uvs = [l[uv_layer].uv for l in f.loops] f_uv_area = calc_polygon_2d_area(uvs) - if not tex_layer: - return None - img = f[tex_layer].image - # not found, try to search from node + # user specified + if tex_size: + uv_area = uv_area + f_uv_area * tex_size[0] * tex_size[1] + continue + + # try to find from texture_layer + img = None + if tex_layer: + img = f[tex_layer].image + + # not found, then try to search from node if not img: 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 in tex_node_types) and node.image: - img = node.image + if node.type not in tex_node_types: + continue + if not node.image: + continue + img = node.image + + # can not find from node, so we can not get texture size if not img: return None - uv_area = uv_area + f_uv_area * img.size[0] * img.size[1] + + img_size = img.size + uv_area = uv_area + f_uv_area * img_size[0] * img_size[1] return uv_area @@ -602,3 +727,380 @@ def get_loop_sequences(bm, uv_layer, closed=False): return None, err return loop_seqs, "" + + +def __is_segment_intersect(start1, end1, start2, end2): + seg1 = end1 - start1 + seg2 = end2 - start2 + + a1 = -seg1.y + b1 = seg1.x + d1 = -(a1 * start1.x + b1 * start1.y) + + a2 = -seg2.y + b2 = seg2.x + d2 = -(a2 * start2.x + b2 * start2.y) + + seg1_line2_start = a2 * start1.x + b2 * start1.y + d2 + seg1_line2_end = a2 * end1.x + b2 * end1.y + d2 + + seg2_line1_start = a1 * start2.x + b1 * start2.y + d1 + seg2_line1_end = a1 * end2.x + b1 * end2.y + d1 + + if (seg1_line2_start * seg1_line2_end >= 0) or \ + (seg2_line1_start * seg2_line1_end >= 0): + return False, None + + u = seg1_line2_start / (seg1_line2_start - seg1_line2_end) + out = start1 + u * seg1 + + return True, out + + +class RingBuffer: + def __init__(self, arr): + self.__buffer = arr.copy() + self.__pointer = 0 + + def __repr__(self): + return repr(self.__buffer) + + def __len__(self): + return len(self.__buffer) + + def insert(self, val, offset=0): + self.__buffer.insert(self.__pointer + offset, val) + + def head(self): + return self.__buffer[0] + + def tail(self): + return self.__buffer[-1] + + def get(self, offset=0): + size = len(self.__buffer) + val = self.__buffer[(self.__pointer + offset) % size] + return val + + def next(self): + size = len(self.__buffer) + self.__pointer = (self.__pointer + 1) % size + + def reset(self): + self.__pointer = 0 + + def find(self, obj): + try: + idx = self.__buffer.index(obj) + except ValueError: + return None + return self.__buffer[idx] + + def find_and_next(self, obj): + size = len(self.__buffer) + idx = self.__buffer.index(obj) + self.__pointer = (idx + 1) % size + + def find_and_set(self, obj): + idx = self.__buffer.index(obj) + self.__pointer = idx + + def as_list(self): + return self.__buffer.copy() + + def reverse(self): + self.__buffer.reverse() + self.reset() + + +# clip: reference polygon +# subject: tested polygon +def __do_weiler_atherton_cliping(clip, subject, uv_layer, mode): + + clip_uvs = RingBuffer([l[uv_layer].uv.copy() for l in clip.loops]) + if __is_polygon_flipped(clip_uvs): + clip_uvs.reverse() + subject_uvs = RingBuffer([l[uv_layer].uv.copy() for l in subject.loops]) + if __is_polygon_flipped(subject_uvs): + subject_uvs.reverse() + + debug_print("===== Clip UV List =====") + debug_print(clip_uvs) + debug_print("===== Subject UV List =====") + debug_print(subject_uvs) + + # check if clip and subject is overlapped completely + if __is_polygon_same(clip_uvs, subject_uvs): + polygons = [subject_uvs.as_list()] + debug_print("===== Polygons Overlapped Completely =====") + debug_print(polygons) + return True, polygons + + # check if subject is in clip + if __is_points_in_polygon(subject_uvs, clip_uvs): + polygons = [subject_uvs.as_list()] + return True, polygons + + # check if clip is in subject + if __is_points_in_polygon(clip_uvs, subject_uvs): + polygons = [subject_uvs.as_list()] + return True, polygons + + # check if clip and subject is overlapped partially + intersections = [] + while True: + subject_uvs.reset() + while True: + uv_start1 = clip_uvs.get() + uv_end1 = clip_uvs.get(1) + uv_start2 = subject_uvs.get() + uv_end2 = subject_uvs.get(1) + intersected, point = __is_segment_intersect(uv_start1, uv_end1, + uv_start2, uv_end2) + if intersected: + clip_uvs.insert(point, 1) + subject_uvs.insert(point, 1) + intersections.append([point, + [clip_uvs.get(), clip_uvs.get(1)]]) + subject_uvs.next() + if subject_uvs.get() == subject_uvs.head(): + break + clip_uvs.next() + if clip_uvs.get() == clip_uvs.head(): + break + + debug_print("===== Intersection List =====") + debug_print(intersections) + + # no intersection, so subject and clip is not overlapped + if not intersections: + return False, None + + def get_intersection_pair(intersects, key): + for sect in intersects: + if sect[0] == key: + return sect[1] + + return None + + # make enter/exit pair + subject_uvs.reset() + subject_entering = [] + subject_exiting = [] + clip_entering = [] + clip_exiting = [] + intersect_uv_list = [] + while True: + pair = get_intersection_pair(intersections, subject_uvs.get()) + if pair: + sub = subject_uvs.get(1) - subject_uvs.get(-1) + inter = pair[1] - pair[0] + cross = sub.x * inter.y - inter.x * sub.y + if cross < 0: + subject_entering.append(subject_uvs.get()) + clip_exiting.append(subject_uvs.get()) + else: + subject_exiting.append(subject_uvs.get()) + clip_entering.append(subject_uvs.get()) + intersect_uv_list.append(subject_uvs.get()) + + subject_uvs.next() + if subject_uvs.get() == subject_uvs.head(): + break + + debug_print("===== Enter List =====") + debug_print(clip_entering) + debug_print(subject_entering) + debug_print("===== Exit List =====") + debug_print(clip_exiting) + debug_print(subject_exiting) + + # for now, can't handle the situation when fulfill all below conditions + # * two faces have common edge + # * each face is intersected + # * Show Mode is "Part" + # so for now, ignore this situation + if len(subject_entering) != len(subject_exiting): + if mode == 'FACE': + polygons = [subject_uvs.as_list()] + return True, polygons + return False, None + + def traverse(current_list, entering, exiting, p, current, other_list): + result = current_list.find(current) + if not result: + return None + if result != current: + print("Internal Error") + return None + + # enter + if entering.count(current) >= 1: + entering.remove(current) + + current_list.find_and_next(current) + current = current_list.get() + + while exiting.count(current) == 0: + p.append(current.copy()) + current_list.find_and_next(current) + current = current_list.get() + + # exit + p.append(current.copy()) + exiting.remove(current) + + other_list.find_and_set(current) + return other_list.get() + + # Traverse + polygons = [] + current_uv_list = subject_uvs + other_uv_list = clip_uvs + current_entering = subject_entering + current_exiting = subject_exiting + + poly = [] + current_uv = current_entering[0] + + while True: + current_uv = traverse(current_uv_list, current_entering, + current_exiting, poly, current_uv, other_uv_list) + + if current_uv_list == subject_uvs: + current_uv_list = clip_uvs + other_uv_list = subject_uvs + current_entering = clip_entering + current_exiting = clip_exiting + debug_print("-- Next: Clip --") + else: + current_uv_list = subject_uvs + other_uv_list = clip_uvs + current_entering = subject_entering + current_exiting = subject_exiting + debug_print("-- Next: Subject --") + + debug_print(clip_entering) + debug_print(clip_exiting) + debug_print(subject_entering) + debug_print(subject_exiting) + + if not clip_entering and not clip_exiting \ + and not subject_entering and not subject_exiting: + break + + polygons.append(poly) + + debug_print("===== Polygons Overlapped Partially =====") + debug_print(polygons) + + return True, polygons + + +def __is_polygon_flipped(points): + area = 0.0 + for i in range(len(points)): + uv1 = points.get(i) + uv2 = points.get(i + 1) + a = uv1.x * uv2.y - uv1.y * uv2.x + area = area + a + if area < 0: + # clock-wise + return True + return False + + +def __is_point_in_polygon(point, subject_points): + count = 0 + for i in range(len(subject_points)): + uv_start1 = subject_points.get(i) + uv_end1 = subject_points.get(i + 1) + uv_start2 = point + uv_end2 = Vector((1000000.0, point.y)) + intersected, _ = __is_segment_intersect(uv_start1, uv_end1, + uv_start2, uv_end2) + if intersected: + count = count + 1 + + return count % 2 + + +def __is_points_in_polygon(points, subject_points): + for i in range(len(points)): + internal = __is_point_in_polygon(points.get(i), subject_points) + if not internal: + return False + + return True + + +def get_overlapped_uv_info(bm, faces, uv_layer, mode): + # at first, check island overlapped + isl = get_island_info_from_faces(bm, faces, uv_layer) + overlapped_isl_pairs = [] + for i, i1 in enumerate(isl): + for i2 in isl[i + 1:]: + if (i1["max"].x < i2["min"].x) or (i2["max"].x < i1["min"].x) or \ + (i1["max"].y < i2["min"].y) or (i2["max"].y < i1["min"].y): + continue + overlapped_isl_pairs.append([i1, i2]) + + # next, check polygon overlapped + overlapped_uvs = [] + for oip in overlapped_isl_pairs: + for clip in oip[0]["faces"]: + f_clip = clip["face"] + for subject in oip[1]["faces"]: + f_subject = subject["face"] + + # fast operation, apply bounding box algorithm + if (clip["max_uv"].x < subject["min_uv"].x) or \ + (subject["max_uv"].x < clip["min_uv"].x) or \ + (clip["max_uv"].y < subject["min_uv"].y) or \ + (subject["max_uv"].y < clip["min_uv"].y): + continue + + # slow operation, apply Weiler-Atherton cliping algorithm + result, polygons = __do_weiler_atherton_cliping(f_clip, + f_subject, + uv_layer, mode) + if result: + subject_uvs = [l[uv_layer].uv.copy() + for l in f_subject.loops] + overlapped_uvs.append({"clip_face": f_clip, + "subject_face": f_subject, + "subject_uvs": subject_uvs, + "polygons": polygons}) + + return overlapped_uvs + + +def get_flipped_uv_info(faces, uv_layer): + flipped_uvs = [] + for f in faces: + polygon = RingBuffer([l[uv_layer].uv.copy() for l in f.loops]) + if __is_polygon_flipped(polygon): + uvs = [l[uv_layer].uv.copy() for l in f.loops] + flipped_uvs.append({"face": f, "uvs": uvs, + "polygons": [polygon.as_list()]}) + + return flipped_uvs + + +def __is_polygon_same(points1, points2): + if len(points1) != len(points2): + return False + + pts1 = points1.as_list() + pts2 = points2.as_list() + + for p1 in pts1: + for p2 in pts2: + diff = p2 - p1 + if diff.length < 0.0000001: + pts2.remove(p2) + break + else: + return False + + return True diff --git a/uv_magic_uv/op/__init__.py b/uv_magic_uv/op/__init__.py index 75885ef6..9535b76d 100644 --- a/uv_magic_uv/op/__init__.py +++ b/uv_magic_uv/op/__init__.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" if "bpy" in locals(): import importlib @@ -35,6 +35,7 @@ if "bpy" in locals(): 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) @@ -57,6 +58,7 @@ else: 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 diff --git a/uv_magic_uv/op/align_uv.py b/uv_magic_uv/op/align_uv.py index dcfb57c3..90168a56 100644 --- a/uv_magic_uv/op/align_uv.py +++ b/uv_magic_uv/op/align_uv.py @@ -20,8 +20,8 @@ __author__ = "imdjs, Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import math from math import atan2, tan, sin, cos @@ -29,11 +29,42 @@ from math import atan2, tan, sin, cos import bpy import bmesh from mathutils import Vector -from bpy.props import EnumProperty, BoolProperty +from bpy.props import EnumProperty, BoolProperty, FloatProperty from .. import common +__all__ = [ + 'Properties', + 'OperatorCircle', + 'OperatorStraighten', + 'OperatorAxis', +] + + +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 @@ -86,10 +117,69 @@ def calc_v_on_circle(v, center, radius): return new_v -class MUV_AUVCircle(bpy.types.Operator): +class Properties: + @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 + + +class OperatorCircle(bpy.types.Operator): - bl_idname = "uv.muv_auv_circle" - bl_label = "Circle" + bl_idname = "uv.muv_align_uv_operator_circle" + bl_label = "Align UV (Circle)" bl_description = "Align UV coordinates to Circle" bl_options = {'REGISTER', 'UNDO'} @@ -106,7 +196,10 @@ class MUV_AUVCircle(bpy.types.Operator): @classmethod def poll(cls, context): - return context.mode == 'EDIT_MESH' + # 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 @@ -167,66 +260,164 @@ class MUV_AUVCircle(bpy.types.Operator): 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, pair_idx): +def get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl): common.debug_print( - "vidx={0}, hidx={1}, pair_idx={2}".format(vidx, hidx, pair_idx)) + "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx)) - # get total vertex length - hloops = [] - for s in loop_seqs: - hloops.extend([s[vidx][0], s[vidx][1]]) - vert_total_hlen = get_loop_vert_len(hloops) - common.debug_print(vert_total_hlen) + base_uv = loop_seqs[0][vidx][0][uv_layer].uv.copy() - # target vertex length + # calculate original length hloops = [] - for s in loop_seqs[:hidx]: + for s in loop_seqs: hloops.extend([s[vidx][0], s[vidx][1]]) - for pidx, l in enumerate(loop_seqs[hidx][vidx]): - if pidx > pair_idx: + 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 - hloops.append(l) - vert_hlen = get_loop_vert_len(hloops) - common.debug_print(vert_hlen) + else: + raise Exception("Internal Error: horizontal_target_length={}" + " is not in range {} to {}" + .format(target_length, accum_uvlens[0], + accum_uvlens[-1])) - # get total UV length - # uv_all_hdiff = loop_seqs[-1][0][-1][uv_layer].uv - - # loop_seqs[0][0][0][uv_layer].uv - uv_total_hlen = loop_seqs[-1][vidx][-1][uv_layer].uv -\ - loop_seqs[0][vidx][0][uv_layer].uv - common.debug_print(uv_total_hlen) + return target_uv - return uv_total_hlen * vert_hlen / vert_total_hlen + +# --------------------- 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, pair_idx): +def get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl): common.debug_print( - "vidx={0}, hidx={1}, pair_idx={2}".format(vidx, hidx, pair_idx)) - - # get total vertex length - hloops = [] - for s in loop_seqs[hidx]: - hloops.append(s[pair_idx]) - vert_total_hlen = get_loop_vert_len(hloops) - common.debug_print(vert_total_hlen) + "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx)) - # target vertex length - hloops = [] - for s in loop_seqs[hidx][:vidx + 1]: - hloops.append(s[pair_idx]) - vert_hlen = get_loop_vert_len(hloops) - common.debug_print(vert_hlen) + base_uv = loop_seqs[hidx][0][pidx][uv_layer].uv.copy() - # get total UV length - # uv_all_hdiff = loop_seqs[0][-1][pair_idx][uv_layer].uv - \ - # loop_seqs[0][0][pair_idx][uv_layer].uv - uv_total_hlen = loop_seqs[hidx][-1][pair_idx][uv_layer].uv -\ - loop_seqs[hidx][0][pair_idx][uv_layer].uv - common.debug_print(uv_total_hlen) + # 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 uv_total_hlen * vert_hlen / vert_total_hlen + return target_uv # get horizontal differential of UV no influenced @@ -246,10 +437,10 @@ def get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx): return int((vidx + 1) / 2) * v_uv / (len(hseq) / 2) -class MUV_AUVStraighten(bpy.types.Operator): +class OperatorStraighten(bpy.types.Operator): - bl_idname = "uv.muv_auv_straighten" - bl_label = "Straighten" + bl_idname = "uv.muv_align_uv_operator_straighten" + bl_label = "Align UV (Straighten)" bl_description = "Straighten UV coordinates" bl_options = {'REGISTER', 'UNDO'} @@ -275,10 +466,20 @@ class MUV_AUVStraighten(bpy.types.Operator): "by mesh vertex proportion", default=False ) + mesh_infl = FloatProperty( + name="Mesh Influence", + description="Influence rate of mesh vertex", + min=0.0, + max=1.0, + default=0.0 + ) @classmethod def poll(cls, context): - return context.mode == 'EDIT_MESH' + # 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): @@ -293,12 +494,14 @@ class MUV_AUVStraighten(bpy.types.Operator): for vidx in range(0, len(hseq), 2): if self.horizontal: hdiff_uvs = [ - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0), - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1), + 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), + hidx, 0, self.mesh_infl), get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 1), + hidx, 1, self.mesh_infl), ] else: hdiff_uvs = [ @@ -309,12 +512,14 @@ class MUV_AUVStraighten(bpy.types.Operator): ] if self.vertical: vdiff_uvs = [ - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0), - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1), + 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), + hidx, 0, self.mesh_infl), get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 1), + hidx, 1, self.mesh_infl), ] else: vdiff_uvs = [ @@ -382,10 +587,10 @@ class MUV_AUVStraighten(bpy.types.Operator): return {'FINISHED'} -class MUV_AUVAxis(bpy.types.Operator): +class OperatorAxis(bpy.types.Operator): - bl_idname = "uv.muv_auv_axis" - bl_label = "XY-Axis" + 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'} @@ -421,10 +626,20 @@ class MUV_AUVAxis(bpy.types.Operator): ], default='MIDDLE' ) + mesh_infl = FloatProperty( + name="Mesh Influence", + description="Influence rate of mesh vertex", + min=0.0, + max=1.0, + default=0.0 + ) @classmethod def poll(cls, context): - return context.mode == 'EDIT_MESH' + # 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): @@ -579,12 +794,14 @@ class MUV_AUVAxis(bpy.types.Operator): for vidx in range(0, len(hseq), 2): if self.horizontal: hdiff_uvs = [ - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0), - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1), + 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), + hidx, 0, self.mesh_infl), get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 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 @@ -599,12 +816,14 @@ class MUV_AUVAxis(bpy.types.Operator): ] if self.vertical: vdiff_uvs = [ - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0), - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1), + 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), + hidx, 0, self.mesh_infl), get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 1), + hidx, 1, self.mesh_infl), ] else: vdiff_uvs = [ @@ -664,12 +883,14 @@ class MUV_AUVAxis(bpy.types.Operator): for vidx in range(0, len(hseq), 2): if self.horizontal: hdiff_uvs = [ - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0), - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1), + 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), + hidx, 0, self.mesh_infl), get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 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 @@ -684,12 +905,14 @@ class MUV_AUVAxis(bpy.types.Operator): ] if self.vertical: vdiff_uvs = [ - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0), - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1), + 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), + hidx, 0, self.mesh_infl), get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 1), + hidx, 1, self.mesh_infl), ] else: vdiff_uvs = [ diff --git a/uv_magic_uv/op/align_uv_cursor.py b/uv_magic_uv/op/align_uv_cursor.py index cae1c89a..d787bde9 100644 --- a/uv_magic_uv/op/align_uv_cursor.py +++ b/uv_magic_uv/op/align_uv_cursor.py @@ -20,21 +20,111 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy from mathutils import Vector -from bpy.props import EnumProperty +from bpy.props import EnumProperty, BoolProperty, FloatVectorProperty import bmesh from .. import common -class MUV_AUVCAlignOps(bpy.types.Operator): - - bl_idname = "uv.muv_auvc_align" - bl_label = "Align" +__all__ = [ + 'Properties', + 'Operator', +] + + +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 Properties: + @classmethod + def init_props(cls, scene): + def auvc_get_cursor_loc(self): + area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW', + 'IMAGE_EDITOR') + bd_size = common.get_uvimg_editor_board_size(area) + loc = space.cursor_location + if bd_size[0] < 0.000001: + cx = 0.0 + else: + cx = loc[0] / bd_size[0] + if bd_size[1] < 0.000001: + cy = 0.0 + else: + cy = loc[1] / bd_size[1] + self['muv_align_uv_cursor_cursor_loc'] = Vector((cx, cy)) + return self.get('muv_align_uv_cursor_cursor_loc', (0.0, 0.0)) + + def auvc_set_cursor_loc(self, value): + self['muv_align_uv_cursor_cursor_loc'] = value + area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW', + 'IMAGE_EDITOR') + bd_size = common.get_uvimg_editor_board_size(area) + cx = bd_size[0] * value[0] + cy = bd_size[1] * value[1] + space.cursor_location = Vector((cx, cy)) + + scene.muv_align_uv_cursor_enabled = BoolProperty( + name="Align UV Cursor Enabled", + description="Align UV Cursor is enabled", + default=False + ) + + scene.muv_align_uv_cursor_cursor_loc = FloatVectorProperty( + name="UV Cursor Location", + size=2, + precision=4, + soft_min=-1.0, + soft_max=1.0, + step=1, + default=(0.000, 0.000), + get=auvc_get_cursor_loc, + set=auvc_set_cursor_loc + ) + scene.muv_align_uv_cursor_align_method = EnumProperty( + name="Align Method", + description="Align Method", + default='TEXTURE', + items=[ + ('TEXTURE', "Texture", "Align to texture"), + ('UV', "UV", "Align to UV"), + ('UV_SEL', "UV (Selected)", "Align to Selected UV") + ] + ) + + scene.muv_uv_cursor_location_enabled = BoolProperty( + name="UV Cursor Location Enabled", + description="UV Cursor Location is enabled", + default=False + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_align_uv_cursor_enabled + del scene.muv_align_uv_cursor_cursor_loc + del scene.muv_align_uv_cursor_align_method + + del scene.muv_uv_cursor_location_enabled + + +class Operator(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'} @@ -65,6 +155,13 @@ class MUV_AUVCAlignOps(bpy.types.Operator): default='TEXTURE' ) + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + def execute(self, context): area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW', 'IMAGE_EDITOR') diff --git a/uv_magic_uv/op/copy_paste_uv.py b/uv_magic_uv/op/copy_paste_uv.py index ee89b5e9..cc1baa30 100644 --- a/uv_magic_uv/op/copy_paste_uv.py +++ b/uv_magic_uv/op/copy_paste_uv.py @@ -20,11 +20,9 @@ __author__ = "imdjs, Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" -import math -from math import atan2, sin, cos import bpy import bmesh @@ -34,104 +32,293 @@ from bpy.props import ( IntProperty, EnumProperty, ) -from mathutils import Vector from .. import common -class MUV_CPUVCopyUV(bpy.types.Operator): +__all__ = [ + 'Properties', + 'OpeartorCopyUV', + 'MenuCopyUV', + 'OperatorPasteUV', + 'MenuPasteUV', + 'OperatorSelSeqCopyUV', + 'MenuSelSeqCopyUV', + 'OperatorSelSeqPasteUV', + 'MenuSelSeqPasteUV', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +def get_copy_uv_layers(ops_obj, bm): + uv_layers = [] + if ops_obj.uv_map == "__default": + if not bm.loops.layers.uv: + ops_obj.report( + {'WARNING'}, "Object must have more than one UV map") + return None + uv_layers.append(bm.loops.layers.uv.verify()) + ops_obj.report({'INFO'}, "Copy UV coordinate") + elif ops_obj.uv_map == "__all": + for uv in bm.loops.layers.uv.keys(): + uv_layers.append(bm.loops.layers.uv[uv]) + ops_obj.report({'INFO'}, "Copy UV coordinate (UV map: ALL)") + else: + uv_layers.append(bm.loops.layers.uv[ops_obj.uv_map]) + ops_obj.report( + {'INFO'}, "Copy UV coordinate (UV map:{})".format(ops_obj.uv_map)) + + return uv_layers + + +def get_paste_uv_layers(ops_obj, obj, bm, src_info): + uv_layers = [] + if ops_obj.uv_map == "__default": + if not bm.loops.layers.uv: + ops_obj.report( + {'WARNING'}, "Object must have more than one UV map") + return None + uv_layers.append(bm.loops.layers.uv.verify()) + ops_obj.report({'INFO'}, "Paste UV coordinate") + elif ops_obj.uv_map == "__new": + new_uv_map = common.create_new_uv_map(obj) + if not new_uv_map: + ops_obj.report({'WARNING'}, + "Reached to the maximum number of UV map") + return None + uv_layers.append(bm.loops.layers.uv[new_uv_map.name]) + ops_obj.report( + {'INFO'}, "Paste UV coordinate (UV map:{})".format(new_uv_map)) + elif ops_obj.uv_map == "__all": + for src_layer in src_info.keys(): + if src_layer not in bm.loops.layers.uv.keys(): + new_uv_map = common.create_new_uv_map(obj, src_layer) + if not new_uv_map: + ops_obj.report({'WARNING'}, + "Reached to the maximum number of UV map") + return None + uv_layers.append(bm.loops.layers.uv[src_layer]) + ops_obj.report({'INFO'}, "Paste UV coordinate (UV map: ALL)") + else: + uv_layers.append(bm.loops.layers.uv[ops_obj.uv_map]) + ops_obj.report( + {'INFO'}, "Paste UV coordinate (UV map:{})".format(ops_obj.uv_map)) + + return uv_layers + + +def paste_uv(ops_obj, bm, src_info, dest_info, uv_layers, strategy, flip, + rotate, copy_seams): + for slayer_name, dlayer in zip(src_info.keys(), uv_layers): + src_faces = src_info[slayer_name] + dest_faces = dest_info[dlayer.name] + + for idx, dinfo in enumerate(dest_faces): + sinfo = None + if strategy == 'N_N': + sinfo = src_faces[idx] + elif strategy == 'N_M': + sinfo = src_faces[idx % len(src_faces)] + + suv = sinfo["uvs"] + spuv = sinfo["pin_uvs"] + ss = sinfo["seams"] + if len(sinfo["uvs"]) != len(dinfo["uvs"]): + ops_obj.report({'WARNING'}, "Some faces are different size") + return -1 + + suvs_fr = [uv for uv in suv] + spuvs_fr = [pin_uv for pin_uv in spuv] + ss_fr = [s for s in ss] + + # flip UVs + if flip is True: + suvs_fr.reverse() + spuvs_fr.reverse() + ss_fr.reverse() + + # rotate UVs + for _ in range(rotate): + uv = suvs_fr.pop() + pin_uv = spuvs_fr.pop() + s = ss_fr.pop() + suvs_fr.insert(0, uv) + spuvs_fr.insert(0, pin_uv) + ss_fr.insert(0, s) + + # paste UVs + for l, suv, spuv, ss in zip(bm.faces[idx].loops, suvs_fr, + spuvs_fr, ss_fr): + l[dlayer].uv = suv + l[dlayer].pin_uv = spuv + if copy_seams is True: + l.edge.seam = ss + + return 0 + + +class Properties: + @classmethod + def init_props(cls, scene): + class Props(): + src_info = None + + scene.muv_props.copy_paste_uv = Props() + scene.muv_props.copy_paste_uv_selseq = Props() + + scene.muv_copy_paste_uv_enabled = BoolProperty( + name="Copy/Paste UV Enabled", + description="Copy/Paste UV is enabled", + default=False + ) + scene.muv_copy_paste_uv_copy_seams = BoolProperty( + name="Seams", + description="Copy Seams", + default=True + ) + scene.muv_copy_paste_uv_mode = EnumProperty( + items=[ + ('DEFAULT', "Default", "Default Mode"), + ('SEL_SEQ', "Selection Sequence", "Selection Sequence Mode") + ], + name="Copy/Paste UV Mode", + description="Copy/Paste UV Mode", + default='DEFAULT' + ) + scene.muv_copy_paste_uv_strategy = EnumProperty( + name="Strategy", + description="Paste Strategy", + items=[ + ('N_N', 'N:N', 'Number of faces must be equal to source'), + ('N_M', 'N:M', 'Number of faces must not be equal to source') + ], + default='N_M' + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_props.copy_paste_uv + del scene.muv_props.copy_paste_uv_selseq + del scene.muv_copy_paste_uv_enabled + del scene.muv_copy_paste_uv_copy_seams + del scene.muv_copy_paste_uv_mode + del scene.muv_copy_paste_uv_strategy + + +class OpeartorCopyUV(bpy.types.Operator): """ Operation class: Copy UV coordinate """ - bl_idname = "uv.muv_cpuv_copy_uv" - bl_label = "Copy UV (Operation)" - bl_description = "Copy UV coordinate (Operation)" + bl_idname = "uv.muv_copy_paste_uv_operator_copy_uv" + bl_label = "Copy UV" + bl_description = "Copy UV coordinate" bl_options = {'REGISTER', 'UNDO'} - uv_map = StringProperty(options={'HIDDEN'}) + uv_map = StringProperty(default="__default", options={'HIDDEN'}) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) def execute(self, context): - props = context.scene.muv_props.cpuv - if self.uv_map == "": - self.report({'INFO'}, "Copy UV coordinate") - else: - self.report( - {'INFO'}, "Copy UV coordinate (UV map:%s)" % (self.uv_map)) + props = context.scene.muv_props.copy_paste_uv obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() + bm = common.create_bmesh(obj) # get UV layer - if self.uv_map == "": - 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() - else: - uv_layer = bm.loops.layers.uv[self.uv_map] + uv_layers = get_copy_uv_layers(self, bm) + if not uv_layers: + return {'CANCELLED'} # get selected face - props.src_uvs = [] - props.src_pin_uvs = [] - props.src_seams = [] - for face in bm.faces: - if face.select: - uvs = [l[uv_layer].uv.copy() for l in face.loops] - pin_uvs = [l[uv_layer].pin_uv for l in face.loops] - seams = [l.edge.seam for l in face.loops] - props.src_uvs.append(uvs) - props.src_pin_uvs.append(pin_uvs) - props.src_seams.append(seams) - if not props.src_uvs or not props.src_pin_uvs: - self.report({'WARNING'}, "No faces are selected") - return {'CANCELLED'} - self.report({'INFO'}, "%d face(s) are selected" % len(props.src_uvs)) + props.src_info = {} + for layer in uv_layers: + face_info = [] + for face in bm.faces: + if face.select: + info = { + "uvs": [l[layer].uv.copy() for l in face.loops], + "pin_uvs": [l[layer].pin_uv for l in face.loops], + "seams": [l.edge.seam for l in face.loops], + } + face_info.append(info) + if not face_info: + self.report({'WARNING'}, "No faces are selected") + return {'CANCELLED'} + props.src_info[layer.name] = face_info + + face_count = len([f for f in bm.faces if f.select]) + self.report({'INFO'}, "{} face(s) are copied".format(face_count)) return {'FINISHED'} -class MUV_CPUVCopyUVMenu(bpy.types.Menu): +class MenuCopyUV(bpy.types.Menu): """ Menu class: Copy UV coordinate """ - bl_idname = "uv.muv_cpuv_copy_uv_menu" - bl_label = "Copy UV" - bl_description = "Copy UV coordinate" + bl_idname = "uv.muv_copy_paste_uv_menu_copy_uv" + bl_label = "Copy UV (Menu)" + bl_description = "Menu of Copy UV coordinate" + + @classmethod + def poll(cls, context): + return is_valid_context(context) def draw(self, context): layout = self.layout # create sub menu obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) + bm = common.create_bmesh(obj) uv_maps = bm.loops.layers.uv.keys() - layout.operator( - MUV_CPUVCopyUV.bl_idname, - text="[Default]", - icon="IMAGE_COL" - ).uv_map = "" + + ops = layout.operator(OpeartorCopyUV.bl_idname, text="[Default]") + ops.uv_map = "__default" + + ops = layout.operator(OpeartorCopyUV.bl_idname, text="[All]") + ops.uv_map = "__all" + for m in uv_maps: - layout.operator( - MUV_CPUVCopyUV.bl_idname, - text=m, - icon="IMAGE_COL" - ).uv_map = m + ops = layout.operator(OpeartorCopyUV.bl_idname, text=m) + ops.uv_map = m -class MUV_CPUVPasteUV(bpy.types.Operator): +class OperatorPasteUV(bpy.types.Operator): """ Operation class: Paste UV coordinate """ - bl_idname = "uv.muv_cpuv_paste_uv" - bl_label = "Paste UV (Operation)" - bl_description = "Paste UV coordinate (Operation)" + bl_idname = "uv.muv_copy_paste_uv_operator_paste_uv" + bl_label = "Paste UV" + bl_description = "Paste UV coordinate" bl_options = {'REGISTER', 'UNDO'} - uv_map = StringProperty(options={'HIDDEN'}) + uv_map = StringProperty(default="__default", options={'HIDDEN'}) strategy = EnumProperty( name="Strategy", description="Paste Strategy", @@ -153,104 +340,69 @@ class MUV_CPUVPasteUV(bpy.types.Operator): max=30 ) copy_seams = BoolProperty( - name="Copy Seams", + name="Seams", description="Copy Seams", default=True ) + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.copy_paste_uv + if not props.src_info: + return False + return is_valid_context(context) + def execute(self, context): - props = context.scene.muv_props.cpuv - if not props.src_uvs or not props.src_pin_uvs: + props = context.scene.muv_props.copy_paste_uv + if not props.src_info: self.report({'WARNING'}, "Need copy UV at first") return {'CANCELLED'} - if self.uv_map == "": - self.report({'INFO'}, "Paste UV coordinate") - else: - self.report( - {'INFO'}, "Paste UV coordinate (UV map:%s)" % (self.uv_map)) obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() + bm = common.create_bmesh(obj) # get UV layer - if self.uv_map == "": - if not bm.loops.layers.uv: + uv_layers = get_paste_uv_layers(self, obj, bm, props.src_info) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + dest_face_count = 0 + dest_info = {} + for layer in uv_layers: + face_info = [] + for face in bm.faces: + if face.select: + info = { + "uvs": [l[layer].uv.copy() for l in face.loops], + } + face_info.append(info) + if not face_info: + self.report({'WARNING'}, "No faces are selected") + return {'CANCELLED'} + key = list(props.src_info.keys())[0] + src_face_count = len(props.src_info[key]) + dest_face_count = len(face_info) + if self.strategy == 'N_N' and src_face_count != dest_face_count: self.report( - {'WARNING'}, "Object must have more than one UV map") + {'WARNING'}, + "Number of selected faces is different from copied" + + "(src:{}, dest:{})" + .format(src_face_count, dest_face_count)) return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - else: - uv_layer = bm.loops.layers.uv[self.uv_map] + dest_info[layer.name] = face_info - # get selected face - dest_uvs = [] - dest_pin_uvs = [] - dest_seams = [] - dest_face_indices = [] - for face in bm.faces: - if face.select: - dest_face_indices.append(face.index) - uvs = [l[uv_layer].uv.copy() for l in face.loops] - pin_uvs = [l[uv_layer].pin_uv for l in face.loops] - seams = [l.edge.seam for l in face.loops] - dest_uvs.append(uvs) - dest_pin_uvs.append(pin_uvs) - dest_seams.append(seams) - if not dest_uvs or not dest_pin_uvs: - self.report({'WARNING'}, "No faces are selected") - return {'CANCELLED'} - if self.strategy == 'N_N' and len(props.src_uvs) != len(dest_uvs): - self.report( - {'WARNING'}, - "Number of selected faces is different from copied" + - "(src:%d, dest:%d)" % - (len(props.src_uvs), len(dest_uvs))) + # paste + ret = paste_uv(self, bm, props.src_info, dest_info, uv_layers, + self.strategy, self.flip_copied_uv, + self.rotate_copied_uv, self.copy_seams) + if ret: return {'CANCELLED'} - # paste - for i, idx in enumerate(dest_face_indices): - suv = None - spuv = None - ss = None - duv = None - if self.strategy == 'N_N': - suv = props.src_uvs[i] - spuv = props.src_pin_uvs[i] - ss = props.src_seams[i] - duv = dest_uvs[i] - elif self.strategy == 'N_M': - suv = props.src_uvs[i % len(props.src_uvs)] - spuv = props.src_pin_uvs[i % len(props.src_pin_uvs)] - ss = props.src_seams[i % len(props.src_seams)] - duv = dest_uvs[i] - if len(suv) != len(duv): - self.report({'WARNING'}, "Some faces are different size") - return {'CANCELLED'} - suvs_fr = [uv for uv in suv] - spuvs_fr = [pin_uv for pin_uv in spuv] - ss_fr = [s for s in ss] - # flip UVs - if self.flip_copied_uv is True: - suvs_fr.reverse() - spuvs_fr.reverse() - ss_fr.reverse() - # rotate UVs - for _ in range(self.rotate_copied_uv): - uv = suvs_fr.pop() - pin_uv = spuvs_fr.pop() - s = ss_fr.pop() - suvs_fr.insert(0, uv) - spuvs_fr.insert(0, pin_uv) - ss_fr.insert(0, s) - # paste UVs - for l, suv, spuv, ss in zip(bm.faces[idx].loops, suvs_fr, - spuvs_fr, ss_fr): - l[uv_layer].uv = suv - l[uv_layer].pin_uv = spuv - if self.copy_seams is True: - l.edge.seam = ss - self.report({'INFO'}, "%d face(s) are copied" % len(dest_uvs)) + self.report({'INFO'}, "{} face(s) are pasted".format(dest_face_count)) bmesh.update_edit_mesh(obj.data) if self.copy_seams is True: @@ -259,234 +411,146 @@ class MUV_CPUVPasteUV(bpy.types.Operator): return {'FINISHED'} -class MUV_CPUVPasteUVMenu(bpy.types.Menu): +class MenuPasteUV(bpy.types.Menu): """ Menu class: Paste UV coordinate """ - bl_idname = "uv.muv_cpuv_paste_uv_menu" - bl_label = "Paste UV" - bl_description = "Paste UV coordinate" + bl_idname = "uv.muv_copy_paste_uv_menu_paste_uv" + bl_label = "Paste UV (Menu)" + bl_description = "Menu of Paste UV coordinate" + + @classmethod + def poll(cls, context): + sc = context.scene + props = sc.muv_props.copy_paste_uv + if not props.src_info: + return False + return is_valid_context(context) def draw(self, context): sc = context.scene layout = self.layout # create sub menu obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) + bm = common.create_bmesh(obj) uv_maps = bm.loops.layers.uv.keys() - ops = layout.operator(MUV_CPUVPasteUV.bl_idname, text="[Default]") - ops.uv_map = "" - ops.copy_seams = sc.muv_cpuv_copy_seams - ops.strategy = sc.muv_cpuv_strategy - for m in uv_maps: - ops = layout.operator(MUV_CPUVPasteUV.bl_idname, text=m) - ops.uv_map = m - ops.copy_seams = sc.muv_cpuv_copy_seams - ops.strategy = sc.muv_cpuv_strategy - -class MUV_CPUVIECopyUV(bpy.types.Operator): - """ - Operation class: Copy UV coordinate on UV/Image Editor - """ - - bl_idname = "uv.muv_cpuv_ie_copy_uv" - bl_label = "Copy UV" - bl_description = "Copy UV coordinate (only selected in UV/Image Editor)" - bl_options = {'REGISTER', 'UNDO'} + ops = layout.operator(OperatorPasteUV.bl_idname, text="[Default]") + ops.uv_map = "__default" + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy - @classmethod - def poll(cls, context): - return context.mode == 'EDIT_MESH' + ops = layout.operator(OperatorPasteUV.bl_idname, text="[New]") + ops.uv_map = "__new" + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy - def execute(self, context): - props = context.scene.muv_props.cpuv - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - uv_layer = bm.loops.layers.uv.verify() - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - - for face in bm.faces: - if not face.select: - continue - skip = False - for l in face.loops: - if not l[uv_layer].select: - skip = True - break - if skip: - continue - props.src_uvs.append([l[uv_layer].uv.copy() for l in face.loops]) + ops = layout.operator(OperatorPasteUV.bl_idname, text="[All]") + ops.uv_map = "__all" + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy - return {'FINISHED'} + for m in uv_maps: + ops = layout.operator(OperatorPasteUV.bl_idname, text=m) + ops.uv_map = m + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy -class MUV_CPUVIEPasteUV(bpy.types.Operator): +class OperatorSelSeqCopyUV(bpy.types.Operator): """ - Operation class: Paste UV coordinate on UV/Image Editor + Operation class: Copy UV coordinate by selection sequence """ - bl_idname = "uv.muv_cpuv_ie_paste_uv" - bl_label = "Paste UV" - bl_description = "Paste UV coordinate (only selected in UV/Image Editor)" + bl_idname = "uv.muv_copy_paste_uv_operator_selseq_copy_uv" + bl_label = "Copy UV (Selection Sequence)" + bl_description = "Copy UV data by selection sequence" bl_options = {'REGISTER', 'UNDO'} + uv_map = StringProperty(default="__default", options={'HIDDEN'}) + @classmethod def poll(cls, context): - return context.mode == 'EDIT_MESH' - - def execute(self, context): - props = context.scene.muv_props.cpuv - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - uv_layer = bm.loops.layers.uv.verify() - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - - dest_uvs = [] - dest_face_indices = [] - for face in bm.faces: - if not face.select: - continue - skip = False - for l in face.loops: - if not l[uv_layer].select: - skip = True - break - if skip: - continue - dest_face_indices.append(face.index) - uvs = [l[uv_layer].uv.copy() for l in face.loops] - dest_uvs.append(uvs) - - for suvs, duvs in zip(props.src_uvs, dest_uvs): - src_diff = suvs[1] - suvs[0] - dest_diff = duvs[1] - duvs[0] - - src_base = suvs[0] - dest_base = duvs[0] - - src_rad = atan2(src_diff.y, src_diff.x) - dest_rad = atan2(dest_diff.y, dest_diff.x) - if src_rad < dest_rad: - radian = dest_rad - src_rad - elif src_rad > dest_rad: - radian = math.pi * 2 - (src_rad - dest_rad) - else: # src_rad == dest_rad - radian = 0.0 - - ratio = dest_diff.length / src_diff.length - break - - for suvs, fidx in zip(props.src_uvs, dest_face_indices): - for l, suv in zip(bm.faces[fidx].loops, suvs): - base = suv - src_base - radian_ref = atan2(base.y, base.x) - radian_fin = (radian + radian_ref) - length = base.length - turn = Vector((length * cos(radian_fin), - length * sin(radian_fin))) - target_uv = Vector((turn.x * ratio, turn.y * ratio)) + \ - dest_base - l[uv_layer].uv = target_uv - - bmesh.update_edit_mesh(obj.data) - - return {'FINISHED'} - - -class MUV_CPUVSelSeqCopyUV(bpy.types.Operator): - """ - Operation class: Copy UV coordinate by selection sequence - """ - - bl_idname = "uv.muv_cpuv_selseq_copy_uv" - bl_label = "Copy UV (Selection Sequence) (Operation)" - bl_description = "Copy UV data by selection sequence (Operation)" - bl_options = {'REGISTER', 'UNDO'} - - uv_map = StringProperty(options={'HIDDEN'}) + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) def execute(self, context): - props = context.scene.muv_props.cpuv_selseq - if self.uv_map == "": - self.report({'INFO'}, "Copy UV coordinate (selection sequence)") - else: - self.report( - {'INFO'}, - "Copy UV coordinate (selection sequence) (UV map:%s)" - % (self.uv_map)) + props = context.scene.muv_props.copy_paste_uv_selseq obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() + bm = common.create_bmesh(obj) # get UV layer - if self.uv_map == "": - 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() - else: - uv_layer = bm.loops.layers.uv[self.uv_map] + uv_layers = get_copy_uv_layers(self, bm) + if not uv_layers: + return {'CANCELLED'} # get selected face - props.src_uvs = [] - props.src_pin_uvs = [] - props.src_seams = [] - for hist in bm.select_history: - if isinstance(hist, bmesh.types.BMFace) and hist.select: - uvs = [l[uv_layer].uv.copy() for l in hist.loops] - pin_uvs = [l[uv_layer].pin_uv for l in hist.loops] - seams = [l.edge.seam for l in hist.loops] - props.src_uvs.append(uvs) - props.src_pin_uvs.append(pin_uvs) - props.src_seams.append(seams) - if not props.src_uvs or not props.src_pin_uvs: - self.report({'WARNING'}, "No faces are selected") - return {'CANCELLED'} - self.report({'INFO'}, "%d face(s) are selected" % len(props.src_uvs)) + props.src_info = {} + for layer in uv_layers: + face_info = [] + for hist in bm.select_history: + if isinstance(hist, bmesh.types.BMFace) and hist.select: + info = { + "uvs": [l[layer].uv.copy() for l in hist.loops], + "pin_uvs": [l[layer].pin_uv for l in hist.loops], + "seams": [l.edge.seam for l in hist.loops], + } + face_info.append(info) + if not face_info: + self.report({'WARNING'}, "No faces are selected") + return {'CANCELLED'} + props.src_info[layer.name] = face_info + + face_count = len([f for f in bm.faces if f.select]) + self.report({'INFO'}, "{} face(s) are selected".format(face_count)) return {'FINISHED'} -class MUV_CPUVSelSeqCopyUVMenu(bpy.types.Menu): +class MenuSelSeqCopyUV(bpy.types.Menu): """ Menu class: Copy UV coordinate by selection sequence """ - bl_idname = "uv.muv_cpuv_selseq_copy_uv_menu" - bl_label = "Copy UV (Selection Sequence)" - bl_description = "Copy UV coordinate by selection sequence" + bl_idname = "uv.muv_copy_paste_uv_menu_selseq_copy_uv" + bl_label = "Copy UV (Selection Sequence) (Menu)" + bl_description = "Menu of Copy UV coordinate by selection sequence" + + @classmethod + def poll(cls, context): + return is_valid_context(context) def draw(self, context): layout = self.layout obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) + bm = common.create_bmesh(obj) uv_maps = bm.loops.layers.uv.keys() - layout.operator( - MUV_CPUVSelSeqCopyUV.bl_idname, - text="[Default]", icon="IMAGE_COL").uv_map = "" + + ops = layout.operator(OperatorSelSeqCopyUV.bl_idname, text="[Default]") + ops.uv_map = "__default" + + ops = layout.operator(OperatorSelSeqCopyUV.bl_idname, text="[All]") + ops.uv_map = "__all" + for m in uv_maps: - layout.operator( - MUV_CPUVSelSeqCopyUV.bl_idname, - text=m, icon="IMAGE_COL").uv_map = m + ops = layout.operator(OperatorSelSeqCopyUV.bl_idname, text=m) + ops.uv_map = m -class MUV_CPUVSelSeqPasteUV(bpy.types.Operator): +class OperatorSelSeqPasteUV(bpy.types.Operator): """ Operation class: Paste UV coordinate by selection sequence """ - bl_idname = "uv.muv_cpuv_selseq_paste_uv" - bl_label = "Paste UV (Selection Sequence) (Operation)" - bl_description = "Paste UV coordinate by selection sequence (Operation)" + bl_idname = "uv.muv_copy_paste_uv_operator_selseq_paste_uv" + bl_label = "Paste UV (Selection Sequence)" + bl_description = "Paste UV coordinate by selection sequence" bl_options = {'REGISTER', 'UNDO'} - uv_map = StringProperty(options={'HIDDEN'}) + uv_map = StringProperty(default="__default", options={'HIDDEN'}) strategy = EnumProperty( name="Strategy", description="Paste Strategy", @@ -508,108 +572,69 @@ class MUV_CPUVSelSeqPasteUV(bpy.types.Operator): max=30 ) copy_seams = BoolProperty( - name="Copy Seams", + name="Seams", description="Copy Seams", default=True ) + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.copy_paste_uv_selseq + if not props.src_info: + return False + return is_valid_context(context) + def execute(self, context): - props = context.scene.muv_props.cpuv_selseq - if not props.src_uvs or not props.src_pin_uvs: + props = context.scene.muv_props.copy_paste_uv_selseq + if not props.src_info: self.report({'WARNING'}, "Need copy UV at first") return {'CANCELLED'} - if self.uv_map == "": - self.report({'INFO'}, "Paste UV coordinate (selection sequence)") - else: - self.report( - {'INFO'}, - "Paste UV coordinate (selection sequence) (UV map:%s)" - % (self.uv_map)) - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() + bm = common.create_bmesh(obj) # get UV layer - if self.uv_map == "": - 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() - else: - uv_layer = bm.loops.layers.uv[self.uv_map] + uv_layers = get_paste_uv_layers(self, obj, bm, props.src_info) + if not uv_layers: + return {'CANCELLED'} # get selected face - dest_uvs = [] - dest_pin_uvs = [] - dest_seams = [] - dest_face_indices = [] - for hist in bm.select_history: - if isinstance(hist, bmesh.types.BMFace) and hist.select: - dest_face_indices.append(hist.index) - uvs = [l[uv_layer].uv.copy() for l in hist.loops] - pin_uvs = [l[uv_layer].pin_uv for l in hist.loops] - seams = [l.edge.seam for l in hist.loops] - dest_uvs.append(uvs) - dest_pin_uvs.append(pin_uvs) - dest_seams.append(seams) - if not dest_uvs or not dest_pin_uvs: - self.report({'WARNING'}, "No faces are selected") - return {'CANCELLED'} - if self.strategy == 'N_N' and len(props.src_uvs) != len(dest_uvs): - self.report( - {'WARNING'}, - "Number of selected faces is different from copied faces " + - "(src:%d, dest:%d)" - % (len(props.src_uvs), len(dest_uvs))) - return {'CANCELLED'} + dest_face_count = 0 + dest_info = {} + for layer in uv_layers: + face_info = [] + for hist in bm.select_history: + if isinstance(hist, bmesh.types.BMFace) and hist.select: + info = { + "uvs": [l[layer].uv.copy() for l in hist.loops], + } + face_info.append(info) + if not face_info: + self.report({'WARNING'}, "No faces are selected") + return {'CANCELLED'} + key = list(props.src_info.keys())[0] + src_face_count = len(props.src_info[key]) + dest_face_count = len(face_info) + if self.strategy == 'N_N' and src_face_count != dest_face_count: + self.report( + {'WARNING'}, + "Number of selected faces is different from copied" + + "(src:{}, dest:{})" + .format(src_face_count, dest_face_count)) + return {'CANCELLED'} + dest_info[layer.name] = face_info # paste - for i, idx in enumerate(dest_face_indices): - suv = None - spuv = None - ss = None - duv = None - if self.strategy == 'N_N': - suv = props.src_uvs[i] - spuv = props.src_pin_uvs[i] - ss = props.src_seams[i] - duv = dest_uvs[i] - elif self.strategy == 'N_M': - suv = props.src_uvs[i % len(props.src_uvs)] - spuv = props.src_pin_uvs[i % len(props.src_pin_uvs)] - ss = props.src_seams[i % len(props.src_seams)] - duv = dest_uvs[i] - if len(suv) != len(duv): - self.report({'WARNING'}, "Some faces are different size") - return {'CANCELLED'} - suvs_fr = [uv for uv in suv] - spuvs_fr = [pin_uv for pin_uv in spuv] - ss_fr = [s for s in ss] - # flip UVs - if self.flip_copied_uv is True: - suvs_fr.reverse() - spuvs_fr.reverse() - ss_fr.reverse() - # rotate UVs - for _ in range(self.rotate_copied_uv): - uv = suvs_fr.pop() - pin_uv = spuvs_fr.pop() - s = ss_fr.pop() - suvs_fr.insert(0, uv) - spuvs_fr.insert(0, pin_uv) - ss_fr.insert(0, s) - # paste UVs - for l, suv, spuv, ss in zip(bm.faces[idx].loops, suvs_fr, - spuvs_fr, ss_fr): - l[uv_layer].uv = suv - l[uv_layer].pin_uv = spuv - if self.copy_seams is True: - l.edge.seam = ss + ret = paste_uv(self, bm, props.src_info, dest_info, uv_layers, + self.strategy, self.flip_copied_uv, + self.rotate_copied_uv, self.copy_seams) + if ret: + return {'CANCELLED'} - self.report({'INFO'}, "%d face(s) are copied" % len(dest_uvs)) + self.report({'INFO'}, "{} face(s) are pasted".format(dest_face_count)) bmesh.update_edit_mesh(obj.data) if self.copy_seams is True: @@ -618,29 +643,49 @@ class MUV_CPUVSelSeqPasteUV(bpy.types.Operator): return {'FINISHED'} -class MUV_CPUVSelSeqPasteUVMenu(bpy.types.Menu): +class MenuSelSeqPasteUV(bpy.types.Menu): """ Menu class: Paste UV coordinate by selection sequence """ - bl_idname = "uv.muv_cpuv_selseq_paste_uv_menu" - bl_label = "Paste UV (Selection Sequence)" - bl_description = "Paste UV coordinate by selection sequence" + bl_idname = "uv.muv_copy_paste_uv_menu_selseq_paste_uv" + bl_label = "Paste UV (Selection Sequence) (Menu)" + bl_description = "Menu of Paste UV coordinate by selection sequence" + + @classmethod + def poll(cls, context): + sc = context.scene + props = sc.muv_props.copy_paste_uv_selseq + if not props.src_uvs or not props.src_pin_uvs: + return False + return is_valid_context(context) def draw(self, context): sc = context.scene layout = self.layout # create sub menu obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) + bm = common.create_bmesh(obj) uv_maps = bm.loops.layers.uv.keys() - ops = layout.operator(MUV_CPUVSelSeqPasteUV.bl_idname, + + ops = layout.operator(OperatorSelSeqPasteUV.bl_idname, text="[Default]") - ops.uv_map = "" - ops.copy_seams = sc.muv_cpuv_copy_seams - ops.strategy = sc.muv_cpuv_strategy + ops.uv_map = "__default" + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy + + ops = layout.operator(OperatorSelSeqPasteUV.bl_idname, text="[New]") + ops.uv_map = "__new" + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy + + ops = layout.operator(OperatorSelSeqPasteUV.bl_idname, text="[All]") + ops.uv_map = "__all" + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy + for m in uv_maps: - ops = layout.operator(MUV_CPUVSelSeqPasteUV.bl_idname, text=m) + ops = layout.operator(OperatorSelSeqPasteUV.bl_idname, text=m) ops.uv_map = m - ops.copy_seams = sc.muv_cpuv_copy_seams - ops.strategy = sc.muv_cpuv_strategy + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy diff --git a/uv_magic_uv/op/copy_paste_uv_object.py b/uv_magic_uv/op/copy_paste_uv_object.py index d80ee415..eb6d87c9 100644 --- a/uv_magic_uv/op/copy_paste_uv_object.py +++ b/uv_magic_uv/op/copy_paste_uv_object.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy import bmesh @@ -31,6 +31,41 @@ from bpy.props import ( ) from .. import common +from .copy_paste_uv import ( + get_copy_uv_layers, + get_paste_uv_layers, + paste_uv +) + + +__all__ = [ + 'Properties', + 'OperatorCopyUV', + 'MenuCopyUV', + 'OperatorPasteUV', + 'MenuPasteUV', +] + + +def is_valid_context(context): + obj = context.object + + # only object mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'OBJECT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True def memorize_view_3d_mode(fn): @@ -42,101 +77,138 @@ def memorize_view_3d_mode(fn): return __memorize_view_3d_mode -class MUV_CPUVObjCopyUV(bpy.types.Operator): +class Properties: + @classmethod + def init_props(cls, scene): + class Props(): + src_info = None + + scene.muv_props.copy_paste_uv_object = Props() + + scene.muv_copy_paste_uv_object_copy_seams = BoolProperty( + name="Seams", + description="Copy Seams", + default=True + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_props.copy_paste_uv_object + del scene.muv_copy_paste_uv_object_copy_seams + + +class OperatorCopyUV(bpy.types.Operator): """ - Operation class: Copy UV coordinate per object + Operation class: Copy UV coordinate among objects """ - bl_idname = "object.muv_cpuv_obj_copy_uv" - bl_label = "Copy UV" - bl_description = "Copy UV coordinate" + bl_idname = "object.muv_copy_paste_uv_object_operator_copy_uv" + bl_label = "Copy UV (Among Objects)" + bl_description = "Copy UV coordinate (Among Objects)" bl_options = {'REGISTER', 'UNDO'} - uv_map = StringProperty(options={'HIDDEN'}) + uv_map = StringProperty(default="__default", options={'HIDDEN'}) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) @memorize_view_3d_mode def execute(self, context): - props = context.scene.muv_props.cpuv_obj - if self.uv_map == "": - self.report({'INFO'}, "Copy UV coordinate per object") - else: - self.report( - {'INFO'}, - "Copy UV coordinate per object (UV map:%s)" % (self.uv_map)) + props = context.scene.muv_props.copy_paste_uv_object bpy.ops.object.mode_set(mode='EDIT') - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() + bm = common.create_bmesh(obj) # get UV layer - if self.uv_map == "": - 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() - else: - uv_layer = bm.loops.layers.uv[self.uv_map] + uv_layers = get_copy_uv_layers(self, bm) + if not uv_layers: + return {'CANCELLED'} # get selected face - props.src_uvs = [] - props.src_pin_uvs = [] - props.src_seams = [] - for face in bm.faces: - uvs = [l[uv_layer].uv.copy() for l in face.loops] - pin_uvs = [l[uv_layer].pin_uv for l in face.loops] - seams = [l.edge.seam for l in face.loops] - props.src_uvs.append(uvs) - props.src_pin_uvs.append(pin_uvs) - props.src_seams.append(seams) - - self.report({'INFO'}, "%s's UV coordinates are copied" % (obj.name)) + props.src_info = {} + for layer in uv_layers: + face_info = [] + for face in bm.faces: + if face.select: + info = { + "uvs": [l[layer].uv.copy() for l in face.loops], + "pin_uvs": [l[layer].pin_uv for l in face.loops], + "seams": [l.edge.seam for l in face.loops], + } + face_info.append(info) + props.src_info[layer.name] = face_info + + self.report({'INFO'}, + "{}'s UV coordinates are copied".format(obj.name)) return {'FINISHED'} -class MUV_CPUVObjCopyUVMenu(bpy.types.Menu): +class MenuCopyUV(bpy.types.Menu): """ - Menu class: Copy UV coordinate per object + Menu class: Copy UV coordinate among objects """ - bl_idname = "object.muv_cpuv_obj_copy_uv_menu" - bl_label = "Copy UV" - bl_description = "Copy UV coordinate per object" + bl_idname = "object.muv_copy_paste_uv_object_menu_copy_uv" + bl_label = "Copy UV (Among Objects) (Menu)" + bl_description = "Menu of Copy UV coordinate (Among Objects)" + + @classmethod + def poll(cls, context): + return is_valid_context(context) def draw(self, _): layout = self.layout # create sub menu uv_maps = bpy.context.active_object.data.uv_textures.keys() - layout.operator(MUV_CPUVObjCopyUV.bl_idname, text="[Default]")\ - .uv_map = "" + + ops = layout.operator(OperatorCopyUV.bl_idname, text="[Default]") + ops.uv_map = "__default" + + ops = layout.operator(OperatorCopyUV.bl_idname, text="[All]") + ops.uv_map = "__all" + for m in uv_maps: - layout.operator(MUV_CPUVObjCopyUV.bl_idname, text=m).uv_map = m + ops = layout.operator(OperatorCopyUV.bl_idname, text=m) + ops.uv_map = m -class MUV_CPUVObjPasteUV(bpy.types.Operator): +class OperatorPasteUV(bpy.types.Operator): """ - Operation class: Paste UV coordinate per object + Operation class: Paste UV coordinate among objects """ - bl_idname = "object.muv_cpuv_obj_paste_uv" - bl_label = "Paste UV" - bl_description = "Paste UV coordinate" + bl_idname = "object.muv_copy_paste_uv_object_operator_paste_uv" + bl_label = "Paste UV (Among Objects)" + bl_description = "Paste UV coordinate (Among Objects)" bl_options = {'REGISTER', 'UNDO'} - uv_map = StringProperty(options={'HIDDEN'}) + uv_map = StringProperty(default="__default", options={'HIDDEN'}) copy_seams = BoolProperty( - name="Copy Seams", + name="Seams", description="Copy Seams", default=True ) + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.copy_paste_uv_object + if not props.src_info: + return False + return is_valid_context(context) + @memorize_view_3d_mode def execute(self, context): - props = context.scene.muv_props.cpuv_obj - if not props.src_uvs or not props.src_pin_uvs: + props = context.scene.muv_props.copy_paste_uv_object + if not props.src_info: self.report({'WARNING'}, "Need copy UV at first") return {'CANCELLED'} @@ -149,90 +221,67 @@ class MUV_CPUVObjPasteUV(bpy.types.Operator): bpy.ops.object.mode_set(mode='EDIT') 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 (self.uv_map == "" or - self.uv_map not in bm.loops.layers.uv.keys()): - self.report({'INFO'}, "Paste UV coordinate per object") - else: - self.report( - {'INFO'}, - "Paste UV coordinate per object (UV map: %s)" - % (self.uv_map)) + bm = common.create_bmesh(obj) # get UV layer - if (self.uv_map == "" or - self.uv_map not in bm.loops.layers.uv.keys()): - 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() - else: - uv_layer = bm.loops.layers.uv[self.uv_map] + uv_layers = get_paste_uv_layers(self, obj, bm, props.src_info) + if not uv_layers: + return {'CANCELLED'} # get selected face - dest_uvs = [] - dest_pin_uvs = [] - dest_seams = [] - dest_face_indices = [] - for face in bm.faces: - dest_face_indices.append(face.index) - uvs = [l[uv_layer].uv.copy() for l in face.loops] - pin_uvs = [l[uv_layer].pin_uv for l in face.loops] - seams = [l.edge.seam for l in face.loops] - dest_uvs.append(uvs) - dest_pin_uvs.append(pin_uvs) - dest_seams.append(seams) - if len(props.src_uvs) != len(dest_uvs): - self.report( - {'WARNING'}, - "Number of faces is different from copied " + - "(src:%d, dest:%d)" - % (len(props.src_uvs), len(dest_uvs)) - ) - return {'CANCELLED'} + dest_info = {} + for layer in uv_layers: + face_info = [] + for face in bm.faces: + if face.select: + info = { + "uvs": [l[layer].uv.copy() for l in face.loops], + } + face_info.append(info) + key = list(props.src_info.keys())[0] + src_face_count = len(props.src_info[key]) + dest_face_count = len(face_info) + if src_face_count != dest_face_count: + self.report( + {'WARNING'}, + "Number of selected faces is different from copied" + + "(src:{}, dest:{})" + .format(src_face_count, dest_face_count)) + return {'CANCELLED'} + dest_info[layer.name] = face_info # paste - for i, idx in enumerate(dest_face_indices): - suv = props.src_uvs[i] - spuv = props.src_pin_uvs[i] - ss = props.src_seams[i] - duv = dest_uvs[i] - if len(suv) != len(duv): - self.report({'WARNING'}, "Some faces are different size") - return {'CANCELLED'} - suvs_fr = [uv for uv in suv] - spuvs_fr = [pin_uv for pin_uv in spuv] - ss_fr = [s for s in ss] - # paste UVs - for l, suv, spuv, ss in zip( - bm.faces[idx].loops, suvs_fr, spuvs_fr, ss_fr): - l[uv_layer].uv = suv - l[uv_layer].pin_uv = spuv - if self.copy_seams is True: - l.edge.seam = ss + ret = paste_uv(self, bm, props.src_info, dest_info, uv_layers, + 'N_N', 0, 0, self.copy_seams) + if ret: + return {'CANCELLED'} bmesh.update_edit_mesh(obj.data) if self.copy_seams is True: obj.data.show_edge_seams = True self.report( - {'INFO'}, "%s's UV coordinates are pasted" % (obj.name)) + {'INFO'}, "{}'s UV coordinates are pasted".format(obj.name)) return {'FINISHED'} -class MUV_CPUVObjPasteUVMenu(bpy.types.Menu): +class MenuPasteUV(bpy.types.Menu): """ - Menu class: Paste UV coordinate per object + Menu class: Paste UV coordinate among objects """ - bl_idname = "object.muv_cpuv_obj_paste_uv_menu" - bl_label = "Paste UV" - bl_description = "Paste UV coordinate per object" + bl_idname = "object.muv_copy_paste_uv_object_menu_paste_uv" + bl_label = "Paste UV (Among Objects) (Menu)" + bl_description = "Menu of Paste UV coordinate (Among Objects)" + + @classmethod + def poll(cls, context): + sc = context.scene + props = sc.muv_props.copy_paste_uv_object + if not props.src_info: + return False + return is_valid_context(context) def draw(self, context): sc = context.scene @@ -242,11 +291,20 @@ class MUV_CPUVObjPasteUVMenu(bpy.types.Menu): for obj in bpy.data.objects: if hasattr(obj.data, "uv_textures") and obj.select: uv_maps.extend(obj.data.uv_textures.keys()) - uv_maps = list(set(uv_maps)) - ops = layout.operator(MUV_CPUVObjPasteUV.bl_idname, text="[Default]") - ops.uv_map = "" - ops.copy_seams = sc.muv_cpuv_copy_seams + + ops = layout.operator(OperatorPasteUV.bl_idname, text="[Default]") + ops.uv_map = "__default" + ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams + + ops = layout.operator(OperatorPasteUV.bl_idname, text="[New]") + ops.uv_map = "__new" + ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams + + ops = layout.operator(OperatorPasteUV.bl_idname, text="[All]") + ops.uv_map = "__all" + ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams + for m in uv_maps: - ops = layout.operator(MUV_CPUVObjPasteUV.bl_idname, text=m) + ops = layout.operator(OperatorPasteUV.bl_idname, text=m) ops.uv_map = m - ops.copy_seams = sc.muv_cpuv_copy_seams + ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams diff --git a/uv_magic_uv/op/copy_paste_uv_uvedit.py b/uv_magic_uv/op/copy_paste_uv_uvedit.py index 96908020..e591b5f1 100644 --- a/uv_magic_uv/op/copy_paste_uv_uvedit.py +++ b/uv_magic_uv/op/copy_paste_uv_uvedit.py @@ -20,8 +20,8 @@ __author__ = "Nutti , Jace Priester" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import math from math import atan2, sin, cos @@ -33,28 +33,75 @@ from mathutils import Vector from .. import common -class MUV_CPUVIECopyUV(bpy.types.Operator): +__all__ = [ + 'Properties', + 'OperatorCopyUV', + 'OperatorPasteUV', +] + + +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 Properties: + @classmethod + def init_props(cls, scene): + class Props(): + src_uvs = None + + scene.muv_props.copy_paste_uv_uvedit = Props() + + @classmethod + def del_props(cls, scene): + del scene.muv_props.copy_paste_uv_uvedit + + +class OperatorCopyUV(bpy.types.Operator): """ Operation class: Copy UV coordinate on UV/Image Editor """ - bl_idname = "uv.muv_cpuv_ie_copy_uv" - bl_label = "Copy UV" + bl_idname = "uv.muv_copy_paste_uv_uvedit_operator_copy_uv" + bl_label = "Copy UV (UV/Image Editor)" bl_description = "Copy UV coordinate (only selected in UV/Image Editor)" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): - return context.mode == 'EDIT_MESH' + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) def execute(self, context): - props = context.scene.muv_props.cpuv + props = context.scene.muv_props.copy_paste_uv_uvedit obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) uv_layer = bm.loops.layers.uv.verify() if common.check_version(2, 73, 0) >= 0: bm.faces.ensure_lookup_table() + props.src_uvs = [] for face in bm.faces: if not face.select: continue @@ -70,22 +117,29 @@ class MUV_CPUVIECopyUV(bpy.types.Operator): return {'FINISHED'} -class MUV_CPUVIEPasteUV(bpy.types.Operator): +class OperatorPasteUV(bpy.types.Operator): """ Operation class: Paste UV coordinate on UV/Image Editor """ - bl_idname = "uv.muv_cpuv_ie_paste_uv" - bl_label = "Paste UV" + bl_idname = "uv.muv_copy_paste_uv_uvedit_operator_paste_uv" + bl_label = "Paste UV (UV/Image Editor)" bl_description = "Paste UV coordinate (only selected in UV/Image Editor)" bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context): - return context.mode == 'EDIT_MESH' + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.copy_paste_uv_uvedit + if not props.src_uvs: + return False + return is_valid_context(context) def execute(self, context): - props = context.scene.muv_props.cpuv + props = context.scene.muv_props.copy_paste_uv_uvedit obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) uv_layer = bm.loops.layers.uv.verify() diff --git a/uv_magic_uv/op/flip_rotate_uv.py b/uv_magic_uv/op/flip_rotate_uv.py index 30f6b0f7..751bb8fb 100644 --- a/uv_magic_uv/op/flip_rotate_uv.py +++ b/uv_magic_uv/op/flip_rotate_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy import bmesh @@ -33,12 +33,59 @@ from bpy.props import ( from .. import common -class MUV_FlipRot(bpy.types.Operator): +__all__ = [ + 'Properties', + 'Operator', +] + + +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 Properties: + @classmethod + def init_props(cls, scene): + scene.muv_flip_rotate_uv_enabled = BoolProperty( + name="Flip/Rotate UV Enabled", + description="Flip/Rotate UV is enabled", + default=False + ) + scene.muv_flip_rotate_uv_seams = BoolProperty( + name="Seams", + description="Seams", + default=True + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_flip_rotate_uv_enabled + del scene.muv_flip_rotate_uv_seams + + +class Operator(bpy.types.Operator): """ Operation class: Flip and Rotate UV coordinate """ - bl_idname = "uv.muv_fliprot" + bl_idname = "uv.muv_flip_rotate_uv_operator" bl_label = "Flip/Rotate UV" bl_description = "Flip/Rotate UV coordinate" bl_options = {'REGISTER', 'UNDO'} @@ -60,6 +107,13 @@ class MUV_FlipRot(bpy.types.Operator): default=True ) + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + def execute(self, context): self.report({'INFO'}, "Flip/Rotate UV") obj = context.active_object diff --git a/uv_magic_uv/op/mirror_uv.py b/uv_magic_uv/op/mirror_uv.py index f4849d18..11ad2bca 100644 --- a/uv_magic_uv/op/mirror_uv.py +++ b/uv_magic_uv/op/mirror_uv.py @@ -20,13 +20,14 @@ __author__ = "Keith (Wahooney) Boshoff, Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy from bpy.props import ( EnumProperty, FloatProperty, + BoolProperty, ) import bmesh from mathutils import Vector @@ -34,12 +35,64 @@ from mathutils import Vector from .. import common -class MUV_MirrorUV(bpy.types.Operator): +__all__ = [ + 'Properties', + 'Operator', +] + + +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 Properties: + @classmethod + def init_props(cls, scene): + scene.muv_mirror_uv_enabled = BoolProperty( + name="Mirror UV Enabled", + description="Mirror UV is enabled", + default=False + ) + scene.muv_mirror_uv_axis = EnumProperty( + items=[ + ('X', "X", "Mirror Along X axis"), + ('Y', "Y", "Mirror Along Y axis"), + ('Z', "Z", "Mirror Along Z axis") + ], + name="Axis", + description="Mirror Axis", + default='X' + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_mirror_uv_enabled + del scene.muv_mirror_uv_axis + + +class Operator(bpy.types.Operator): """ Operation class: Mirror UV """ - bl_idname = "uv.muv_mirror_uv" + bl_idname = "uv.muv_mirror_uv_operator" bl_label = "Mirror UV" bl_options = {'REGISTER', 'UNDO'} @@ -104,8 +157,10 @@ class MUV_MirrorUV(bpy.types.Operator): @classmethod def poll(cls, context): - obj = context.active_object - return obj and obj.type == 'MESH' + # 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 diff --git a/uv_magic_uv/op/move_uv.py b/uv_magic_uv/op/move_uv.py index 6382376c..a229ae34 100644 --- a/uv_magic_uv/op/move_uv.py +++ b/uv_magic_uv/op/move_uv.py @@ -20,23 +20,68 @@ __author__ = "kgeogeo, mem, Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy import bmesh from mathutils import Vector +from bpy.props import BoolProperty +from .. import common -class MUV_MVUV(bpy.types.Operator): +__all__ = [ + 'Properties', + 'Operator', +] + + +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 Properties: + @classmethod + def init_props(cls, scene): + scene.muv_move_uv_enabled = BoolProperty( + name="Move UV Enabled", + description="Move UV is enabled", + default=False + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_move_uv_enabled + + +class Operator(bpy.types.Operator): """ - Operator class: Move UV from View3D + Operator class: Move UV """ - bl_idname = "view3d.muv_mvuv" - bl_label = "Move the UV from View3D" + bl_idname = "uv.muv_move_uv_operator" + bl_label = "Move UV" bl_options = {'REGISTER', 'UNDO'} + __running = False + def __init__(self): self.__topology_dict = [] self.__prev_mouse = Vector((0.0, 0.0)) @@ -44,7 +89,20 @@ class MUV_MVUV(bpy.types.Operator): self.__prev_offset_uv = Vector((0.0, 0.0)) self.__first_time = True self.__ini_uvs = [] - self.__running = False + self.__operating = False + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return False + if cls.is_running(context): + return False + return is_valid_context(context) + + @classmethod + def is_running(cls, _): + return cls.__running def __find_uv(self, context): bm = bmesh.from_edit_mesh(context.object.data) @@ -59,12 +117,7 @@ class MUV_MVUV(bpy.types.Operator): return topology_dict, uvs - @classmethod - def poll(cls, context): - return context.edit_object - def modal(self, context, event): - props = context.scene.muv_props.mvuv if self.__first_time is True: self.__prev_mouse = Vector(( event.mouse_region_x, event.mouse_region_y)) @@ -85,9 +138,9 @@ class MUV_MVUV(bpy.types.Operator): event.mouse_region_x, event.mouse_region_y)) # check if operation is started - if self.__running: + if not self.__operating: if event.type == 'LEFTMOUSE' and event.value == 'RELEASE': - self.__running = False + self.__operating = True return {'RUNNING_MODAL'} # update UV @@ -111,20 +164,24 @@ class MUV_MVUV(bpy.types.Operator): if event.type == cancel_btn and event.value == 'PRESS': for (fidx, vidx), uv in zip(self.__topology_dict, self.__ini_uvs): bm.faces[fidx].loops[vidx][active_uv].uv = uv - props.running = False + Operator.__running = False return {'FINISHED'} # confirmed if event.type == confirm_btn and event.value == 'PRESS': - props.running = False + Operator.__running = False return {'FINISHED'} return {'RUNNING_MODAL'} def execute(self, context): - props = context.scene.muv_props.mvuv - props.running = True - self.__running = True + Operator.__running = True + self.__operating = False self.__first_time = True + context.window_manager.modal_handler_add(self) self.__topology_dict, self.__ini_uvs = self.__find_uv(context) + + if context.area: + context.area.tag_redraw() + return {'RUNNING_MODAL'} diff --git a/uv_magic_uv/op/pack_uv.py b/uv_magic_uv/op/pack_uv.py index a780af3e..39340fda 100644 --- a/uv_magic_uv/op/pack_uv.py +++ b/uv_magic_uv/op/pack_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" from math import fabs @@ -38,7 +38,68 @@ from mathutils import Vector from .. import common -class MUV_PackUV(bpy.types.Operator): +__all__ = [ + 'Properties', + 'Operator', +] + + +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 Properties: + @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 + + +class Operator(bpy.types.Operator): """ Operation class: Pack UV with same UV islands are integrated Island matching algorithm @@ -47,7 +108,7 @@ class MUV_PackUV(bpy.types.Operator): - Same number of UV """ - bl_idname = "uv.muv_packuv" + bl_idname = "uv.muv_pack_uv_operator" bl_label = "Pack UV" bl_description = "Pack UV (Same UV Islands are integrated)" bl_options = {'REGISTER', 'UNDO'} @@ -79,6 +140,13 @@ class MUV_PackUV(bpy.types.Operator): size=2 ) + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + def execute(self, context): obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) diff --git a/uv_magic_uv/op/preserve_uv_aspect.py b/uv_magic_uv/op/preserve_uv_aspect.py index bc2f1b81..cb11bd45 100644 --- a/uv_magic_uv/op/preserve_uv_aspect.py +++ b/uv_magic_uv/op/preserve_uv_aspect.py @@ -20,23 +20,93 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy import bmesh -from bpy.props import StringProperty, EnumProperty +from bpy.props import StringProperty, EnumProperty, BoolProperty from mathutils import Vector from .. import common -class MUV_PreserveUVAspect(bpy.types.Operator): +__all__ = [ + 'Properties', + 'Operator', +] + + +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 Properties: + @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 + + +class Operator(bpy.types.Operator): """ Operation class: Preserve UV Aspect """ - bl_idname = "uv.muv_preserve_uv_aspect" + bl_idname = "uv.muv_preserve_uv_aspect_operator" bl_label = "Preserve UV Aspect" bl_description = "Choose Image" bl_options = {'REGISTER', 'UNDO'} @@ -62,8 +132,10 @@ class MUV_PreserveUVAspect(bpy.types.Operator): @classmethod def poll(cls, context): - obj = context.active_object - return obj and obj.type == 'MESH' + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) def execute(self, context): # Note: the current system only works if the diff --git a/uv_magic_uv/op/select_uv.py b/uv_magic_uv/op/select_uv.py new file mode 100644 index 00000000..3a7bcbc3 --- /dev/null +++ b/uv_magic_uv/op/select_uv.py @@ -0,0 +1,161 @@ +# + +# ##### 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 bpy.props import BoolProperty + +from .. import common + + +__all__ = [ + 'Properties', + 'OperatorSelectFlipped', + 'OperatorSelectOverlapped', +] + + +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 Properties: + @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 + + +class OperatorSelectOverlapped(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'} + + @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 OperatorSelectFlipped(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'} + + @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/op/smooth_uv.py b/uv_magic_uv/op/smooth_uv.py index aa9b22c0..31bef155 100644 --- a/uv_magic_uv/op/smooth_uv.py +++ b/uv_magic_uv/op/smooth_uv.py @@ -20,8 +20,8 @@ __author__ = "imdjs, Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy import bmesh @@ -30,9 +30,72 @@ from bpy.props import BoolProperty, FloatProperty from .. import common -class MUV_AUVSmooth(bpy.types.Operator): +__all__ = [ + 'Properties', + 'Operator', +] - bl_idname = "uv.muv_auv_smooth" + +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 Properties: + @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 + + +class Operator(bpy.types.Operator): + + bl_idname = "uv.muv_smooth_uv_operator" bl_label = "Smooth" bl_description = "Smooth UV coordinates" bl_options = {'REGISTER', 'UNDO'} @@ -57,7 +120,10 @@ class MUV_AUVSmooth(bpy.types.Operator): @classmethod def poll(cls, context): - return context.mode == 'EDIT_MESH' + # 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 diff --git a/uv_magic_uv/op/texture_lock.py b/uv_magic_uv/op/texture_lock.py index d6c56f5a..4be97c62 100644 --- a/uv_magic_uv/op/texture_lock.py +++ b/uv_magic_uv/op/texture_lock.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import math from math import atan2, cos, sqrt, sin, fabs @@ -34,6 +34,14 @@ from bpy.props import BoolProperty from .. import common +__all__ = [ + 'Properties', + 'OperatorLock', + 'OperatorUnlock', + 'OperatorIntr', +] + + def get_vco(verts_orig, loop): """ Get vertex original coordinate from loop @@ -169,8 +177,13 @@ def calc_tri_vert(v0, v1, angle0, angle1): xd = 0 yd = 0 else: - xd = (b * b - a * a + d * d) / (2 * d) - yd = 2 * sqrt(s * (s - a) * (s - b) * (s - d)) / d + 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 @@ -179,18 +192,97 @@ def calc_tri_vert(v0, v1, angle0, angle1): return Vector((x1, y1)), Vector((x2, y2)) -class MUV_TexLockStart(bpy.types.Operator): +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 Properties: + @classmethod + def init_props(cls, scene): + class Props(): + verts_orig = None + + scene.muv_props.texture_lock = Props() + + def get_func(_): + return OperatorIntr.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 + + +class OperatorLock(bpy.types.Operator): """ - Operation class: Start Texture Lock + Operation class: Lock Texture """ - bl_idname = "uv.muv_texlock_start" - bl_label = "Start" - bl_description = "Start Texture Lock" + bl_idname = "uv.muv_texture_lock_operator_lock" + bl_label = "Lock Texture" + bl_description = "Lock Texture" bl_options = {'REGISTER', 'UNDO'} + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + + @classmethod + def is_ready(cls, context): + sc = context.scene + props = sc.muv_props.texture_lock + if props.verts_orig: + return True + return False + def execute(self, context): - props = context.scene.muv_props.texlock + 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: @@ -210,14 +302,14 @@ class MUV_TexLockStart(bpy.types.Operator): return {'FINISHED'} -class MUV_TexLockStop(bpy.types.Operator): +class OperatorUnlock(bpy.types.Operator): """ - Operation class: Stop Texture Lock + Operation class: Unlock Texture """ - bl_idname = "uv.muv_texlock_stop" - bl_label = "Stop" - bl_description = "Stop Texture Lock" + bl_idname = "uv.muv_texture_lock_operator_unlock" + bl_label = "Unlock Texture" + bl_description = "Unlock Texture" bl_options = {'REGISTER', 'UNDO'} connect = BoolProperty( @@ -225,9 +317,20 @@ class MUV_TexLockStop(bpy.types.Operator): default=True ) + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.texture_lock + if not props.verts_orig: + return False + return OperatorLock.is_ready(context) and is_valid_context(context) + def execute(self, context): sc = context.scene - props = sc.muv_props.texlock + 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: @@ -275,27 +378,81 @@ class MUV_TexLockStop(bpy.types.Operator): v_orig["moved"] = True bmesh.update_edit_mesh(obj.data) + props.verts_orig = None + return {'FINISHED'} -class MUV_TexLockUpdater(bpy.types.Operator): +class OperatorIntr(bpy.types.Operator): """ - Operation class: Texture locking updater + Operation class: Texture Lock (Interactive mode) """ - bl_idname = "uv.muv_texlock_updater" - bl_label = "Texture Lock Updater" - bl_description = "Texture Lock Updater" + bl_idname = "uv.muv_texture_lock_operator_intr" + bl_label = "Texture Lock (Interactive mode)" + bl_description = "Internal operation for Texture Lock (Interactive mode)" + + __timer = None + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return False + return is_valid_context(context) + + @classmethod + def is_running(cls, _): + return 1 if cls.__timer else 0 + + @classmethod + def handle_add(cls, 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_) + + @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.__timer = None + 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 """ - props = context.scene.muv_props.texlock - obj = bpy.context.active_object + obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) if common.check_version(2, 73, 0) >= 0: bm.verts.ensure_lookup_table() @@ -308,7 +465,7 @@ class MUV_TexLockUpdater(bpy.types.Operator): uv_layer = bm.loops.layers.uv.verify() verts = [v.index for v in bm.verts if v.select] - verts_orig = props.intr_verts_orig + verts_orig = self.__intr_verts_orig for vidx, v_orig in zip(verts, verts_orig): if vidx != v_orig["vidx"]: @@ -337,98 +494,40 @@ class MUV_TexLockUpdater(bpy.types.Operator): bmesh.update_edit_mesh(obj.data) common.redraw_all_areas() - props.intr_verts_orig = [ + self.__intr_verts_orig = [ {"vidx": v.index, "vco": v.co.copy(), "moved": False} for v in bm.verts if v.select] def modal(self, context, event): - props = context.scene.muv_props.texlock - if context.area: - context.area.tag_redraw() - if props.intr_running is False: - self.__handle_remove(context) + if not is_valid_context(context): + OperatorIntr.handle_remove(context) return {'FINISHED'} - if event.type == 'TIMER': - self.__update_uv(context) - - return {'PASS_THROUGH'} - - def __handle_add(self, context): - if self.__timer is None: - self.__timer = context.window_manager.event_timer_add( - 0.10, context.window) - context.window_manager.modal_handler_add(self) - def __handle_remove(self, context): - if self.__timer is not None: - context.window_manager.event_timer_remove(self.__timer) - self.__timer = None + if not OperatorIntr.is_running(context): + return {'FINISHED'} - def execute(self, context): - props = context.scene.muv_props.texlock - if props.intr_running is False: - self.__handle_add(context) - props.intr_running = True - return {'RUNNING_MODAL'} - else: - props.intr_running = False if context.area: context.area.tag_redraw() - return {'FINISHED'} - - -class MUV_TexLockIntrStart(bpy.types.Operator): - """ - Operation class: Start texture locking (Interactive mode) - """ - - bl_idname = "uv.muv_texlock_intr_start" - bl_label = "Texture Lock Start (Interactive mode)" - bl_description = "Texture Lock Start (Realtime UV update)" - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context): - props = context.scene.muv_props.texlock - if props.intr_running is True: - return {'CANCELLED'} + if event.type == 'TIMER': + if self.__sel_verts_changed(context): + self.__reinit_verts(context) + else: + self.__update_uv(context) - 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() + return {'PASS_THROUGH'} - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") + def invoke(self, context, _): + if not is_valid_context(context): return {'CANCELLED'} - props.intr_verts_orig = [ - {"vidx": v.index, "vco": v.co.copy(), "moved": False} - for v in bm.verts if v.select] - - bpy.ops.uv.muv_texlock_updater() - - return {'FINISHED'} - - -# Texture lock (Stop, Interactive mode) -class MUV_TexLockIntrStop(bpy.types.Operator): - """ - Operation class: Stop texture locking (interactive mode) - """ - - bl_idname = "uv.muv_texlock_intr_stop" - bl_label = "Texture Lock Stop (Interactive mode)" - bl_description = "Texture Lock Stop (Realtime UV update)" - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context): - props = context.scene.muv_props.texlock - if props.intr_running is False: - return {'CANCELLED'} + if not OperatorIntr.is_running(context): + OperatorIntr.handle_add(self, context) + return {'RUNNING_MODAL'} + else: + OperatorIntr.handle_remove(context) - bpy.ops.uv.muv_texlock_updater() + if context.area: + context.area.tag_redraw() return {'FINISHED'} diff --git a/uv_magic_uv/op/texture_projection.py b/uv_magic_uv/op/texture_projection.py index 77a81aa0..bdf0ad67 100644 --- a/uv_magic_uv/op/texture_projection.py +++ b/uv_magic_uv/op/texture_projection.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" from collections import namedtuple @@ -30,14 +30,32 @@ import bgl import bmesh import mathutils from bpy_extras import view3d_utils +from bpy.props import ( + BoolProperty, + EnumProperty, + FloatProperty, +) from .. import common +__all__ = [ + 'Properties', + 'Operator', + 'OperatorProject', +] + + 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 @@ -47,20 +65,20 @@ def get_canvas(context, magnitude): region_w = context.region.width region_h = context.region.height - canvas_w = region_w - prefs.texproj_canvas_padding[0] * 2.0 - canvas_h = region_h - prefs.texproj_canvas_padding[1] * 2.0 + 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_texproj_tex_image] + 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_texproj_adjust_window: + if sc.muv_texture_projection_adjust_window: ratio_x = canvas_w / tex_w ratio_y = canvas_h / tex_h - if sc.muv_texproj_apply_tex_aspect: + 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 @@ -68,7 +86,7 @@ def get_canvas(context, magnitude): len_x = canvas_w len_y = canvas_h else: - if sc.muv_texproj_apply_tex_aspect: + if sc.muv_texture_projection_apply_tex_aspect: len_x = tex_w * magnitude len_y = tex_h * magnitude else: @@ -104,44 +122,149 @@ def region_to_canvas(rg_vec, canvas): return cv_vec -class MUV_TexProjRenderer(bpy.types.Operator): +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 Properties: + @classmethod + def init_props(cls, scene): + def get_func(_): + return Operator.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=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 + + +class Operator(bpy.types.Operator): """ - Operation class: Render selected texture - No operation (only rendering texture) + Operation class: Texture Projection + Render texture """ - bl_idname = "uv.muv_texproj_renderer" + bl_idname = "uv.muv_texture_projection_operator" bl_description = "Render selected texture" bl_label = "Texture renderer" __handle = None - @staticmethod - def handle_add(obj, context): - MUV_TexProjRenderer.__handle = bpy.types.SpaceView3D.draw_handler_add( - MUV_TexProjRenderer.draw_texture, + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return False + return is_valid_context(context) + + @classmethod + def is_running(cls, _): + return 1 if cls.__handle else 0 + + @classmethod + def handle_add(cls, obj, context): + cls.__handle = bpy.types.SpaceView3D.draw_handler_add( + Operator.draw_texture, (obj, context), 'WINDOW', 'POST_PIXEL') - @staticmethod - def handle_remove(): - if MUV_TexProjRenderer.__handle is not None: - bpy.types.SpaceView3D.draw_handler_remove( - MUV_TexProjRenderer.__handle, 'WINDOW') - MUV_TexProjRenderer.__handle = None + @classmethod + def handle_remove(cls): + if cls.__handle is not None: + bpy.types.SpaceView3D.draw_handler_remove(cls.__handle, 'WINDOW') + cls.__handle = None - @staticmethod - def draw_texture(_, context): + @classmethod + def draw_texture(cls, _, context): sc = context.scene + if not cls.is_running(context): + return + # no textures are selected - if sc.muv_texproj_tex_image == "None": + if sc.muv_texture_projection_tex_image == "None": return # get texture to be renderred - img = bpy.data.images[sc.muv_texproj_tex_image] + img = bpy.data.images[sc.muv_texture_projection_tex_image] # setup rendering region - rect = get_canvas(context, sc.muv_texproj_tex_magnitude) + rect = get_canvas(context, sc.muv_texture_projection_tex_magnitude) positions = [ [rect.x0, rect.y0], [rect.x0, rect.y1], @@ -170,74 +293,48 @@ class MUV_TexProjRenderer(bpy.types.Operator): # render texture bgl.glBegin(bgl.GL_QUADS) - bgl.glColor4f(1.0, 1.0, 1.0, sc.muv_texproj_tex_transparency) + bgl.glColor4f(1.0, 1.0, 1.0, + sc.muv_texture_projection_tex_transparency) for (v1, v2), (u, v) in zip(positions, tex_coords): bgl.glTexCoord2f(u, v) bgl.glVertex2f(v1, v2) bgl.glEnd() + def invoke(self, context, _): + if not Operator.is_running(context): + Operator.handle_add(self, context) + else: + Operator.handle_remove() -class MUV_TexProjStart(bpy.types.Operator): - """ - Operation class: Start Texture Projection - """ - - bl_idname = "uv.muv_texproj_start" - bl_label = "Start Texture Projection" - bl_description = "Start Texture Projection" - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context): - props = context.scene.muv_props.texproj - if props.running is False: - MUV_TexProjRenderer.handle_add(self, context) - props.running = True - if context.area: - context.area.tag_redraw() - - return {'FINISHED'} - - -class MUV_TexProjStop(bpy.types.Operator): - """ - Operation class: Stop Texture Projection - """ - - bl_idname = "uv.muv_texproj_stop" - bl_label = "Stop Texture Projection" - bl_description = "Stop Texture Projection" - bl_options = {'REGISTER', 'UNDO'} - - def execute(self, context): - props = context.scene.muv_props.texproj - if props.running is True: - MUV_TexProjRenderer.handle_remove() - props.running = False if context.area: context.area.tag_redraw() return {'FINISHED'} -class MUV_TexProjProject(bpy.types.Operator): +class OperatorProject(bpy.types.Operator): """ Operation class: Project texture """ - bl_idname = "uv.muv_texproj_project" + 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): - obj = context.active_object - return obj is not None and obj.type == "MESH" + # we can not get area/space/region from console + if common.is_console_mode(): + return True + if not Operator.is_running(context): + return False + return is_valid_context(context) def execute(self, context): sc = context.scene - if sc.muv_texproj_tex_image == "None": + if sc.muv_texture_projection_tex_image == "None": self.report({'WARNING'}, "No textures are selected") return {'CANCELLED'} @@ -253,7 +350,7 @@ class MUV_TexProjProject(bpy.types.Operator): # get UV and texture layer if not bm.loops.layers.uv: - if sc.muv_texproj_assign_uvmap: + if sc.muv_texture_projection_assign_uvmap: bm.loops.layers.uv.new() else: self.report({'WARNING'}, @@ -278,14 +375,16 @@ class MUV_TexProjProject(bpy.types.Operator): v_canvas = [ region_to_canvas( v, - get_canvas(bpy.context, sc.muv_texproj_tex_magnitude)) - for v in v_screen + get_canvas(bpy.context, + sc.muv_texture_projection_tex_magnitude) + ) for v in v_screen ] # project texture to object i = 0 for f in sel_faces: - f[tex_layer].image = bpy.data.images[sc.muv_texproj_tex_image] + f[tex_layer].image = \ + bpy.data.images[sc.muv_texture_projection_tex_image] for l in f.loops: l[uv_layer].uv = v_canvas[i].to_2d() i = i + 1 diff --git a/uv_magic_uv/op/texture_wrap.py b/uv_magic_uv/op/texture_wrap.py index 04669214..a7c58847 100644 --- a/uv_magic_uv/op/texture_wrap.py +++ b/uv_magic_uv/op/texture_wrap.py @@ -20,27 +20,98 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy import bmesh +from bpy.props import ( + BoolProperty, +) from .. import common -class MUV_TexWrapRefer(bpy.types.Operator): +__all__ = [ + 'Properties', + 'OperatorRefer', + 'OperatorSet', +] + + +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 Properties: + @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 + + +class OperatorRefer(bpy.types.Operator): """ Operation class: Refer UV """ - bl_idname = "uv.muv_texwrap_refer" + bl_idname = "uv.muv_texture_wrap_operator_refer" bl_label = "Refer" bl_description = "Refer UV" bl_options = {'REGISTER', 'UNDO'} + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + def execute(self, context): - props = context.scene.muv_props.texwrap + 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: @@ -61,19 +132,30 @@ class MUV_TexWrapRefer(bpy.types.Operator): return {'FINISHED'} -class MUV_TexWrapSet(bpy.types.Operator): +class OperatorSet(bpy.types.Operator): """ Operation class: Set UV """ - bl_idname = "uv.muv_texwrap_set" + bl_idname = "uv.muv_texture_wrap_operator_set" bl_label = "Set" bl_description = "Set UV" bl_options = {'REGISTER', 'UNDO'} + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.texture_wrap + if not props.ref_obj: + return False + return is_valid_context(context) + def execute(self, context): sc = context.scene - props = sc.muv_props.texwrap + 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: @@ -84,7 +166,7 @@ class MUV_TexWrapSet(bpy.types.Operator): return {'CANCELLED'} uv_layer = bm.loops.layers.uv.verify() - if sc.muv_texwrap_selseq: + if sc.muv_texture_wrap_selseq: sel_faces = [] for hist in bm.select_history: if isinstance(hist, bmesh.types.BMFace) and hist.select: @@ -206,7 +288,7 @@ class MUV_TexWrapSet(bpy.types.Operator): ref_face_index = tgt_face_index - if sc.muv_texwrap_set_and_refer: + if sc.muv_texture_wrap_set_and_refer: props.ref_face_index = tgt_face_index return {'FINISHED'} diff --git a/uv_magic_uv/op/transfer_uv.py b/uv_magic_uv/op/transfer_uv.py index 132f395e..ef6fc3be 100644 --- a/uv_magic_uv/op/transfer_uv.py +++ b/uv_magic_uv/op/transfer_uv.py @@ -20,8 +20,8 @@ __author__ = "Nutti , Mifth, MaxRobinot" __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" from collections import OrderedDict @@ -32,19 +32,84 @@ from bpy.props import BoolProperty from .. import common -class MUV_TransUVCopy(bpy.types.Operator): +__all__ = [ + 'OperatorCopyUV', + 'OperatorPasteUV', +] + + +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 Properties: + @classmethod + def init_props(cls, scene): + class Props(): + topology_copied = None + + scene.muv_props.transfer_uv = Props() + + scene.muv_transfer_uv_enabled = BoolProperty( + name="Transfer UV Enabled", + description="Transfer UV is enabled", + default=False + ) + scene.muv_transfer_uv_invert_normals = BoolProperty( + name="Invert Normals", + description="Invert Normals", + default=False + ) + scene.muv_transfer_uv_copy_seams = BoolProperty( + name="Copy Seams", + description="Copy Seams", + default=True + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_transfer_uv_enabled + del scene.muv_transfer_uv_invert_normals + del scene.muv_transfer_uv_copy_seams + + +class OperatorCopyUV(bpy.types.Operator): """ Operation class: Transfer UV copy Topological based copy """ - bl_idname = "uv.muv_transuv_copy" - bl_label = "Transfer UV Copy" - bl_description = "Transfer UV Copy (Topological based copy)" + bl_idname = "uv.muv_transfer_uv_operator_copy_uv" + bl_label = "Transfer UV Copy UV" + bl_description = "Transfer UV Copy UV (Topological based copy)" bl_options = {'REGISTER', 'UNDO'} + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + def execute(self, context): - props = context.scene.muv_props.transuv + props = context.scene.muv_props.transfer_uv active_obj = context.scene.objects.active bm = bmesh.from_edit_mesh(active_obj.data) if common.check_version(2, 73, 0) >= 0: @@ -56,7 +121,7 @@ class MUV_TransUVCopy(bpy.types.Operator): return {'CANCELLED'} uv_layer = bm.loops.layers.uv.verify() - props.topology_copied.clear() + props.topology_copied = [] # get selected faces active_face = bm.faces.active @@ -82,21 +147,23 @@ class MUV_TransUVCopy(bpy.types.Operator): pin_uvs = [l.pin_uv for l in uv_loops] seams = [e.seam for e in edges] props.topology_copied.append([uvs, pin_uvs, seams]) + else: + return {'CANCELLED'} bmesh.update_edit_mesh(active_obj.data) return {'FINISHED'} -class MUV_TransUVPaste(bpy.types.Operator): +class OperatorPasteUV(bpy.types.Operator): """ Operation class: Transfer UV paste Topological based paste """ - bl_idname = "uv.muv_transuv_paste" - bl_label = "Transfer UV Paste" - bl_description = "Transfer UV Paste (Topological based paste)" + bl_idname = "uv.muv_transfer_uv_operator_paste_uv" + bl_label = "Transfer UV Paste UV" + bl_description = "Transfer UV Paste UV (Topological based paste)" bl_options = {'REGISTER', 'UNDO'} invert_normals = BoolProperty( @@ -110,8 +177,19 @@ class MUV_TransUVPaste(bpy.types.Operator): default=True ) + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.transfer_uv + if not props.topology_copied: + return False + return is_valid_context(context) + def execute(self, context): - props = context.scene.muv_props.transuv + props = context.scene.muv_props.transfer_uv active_obj = context.scene.objects.active bm = bmesh.from_edit_mesh(active_obj.data) if common.check_version(2, 73, 0) >= 0: @@ -153,7 +231,7 @@ class MUV_TransUVPaste(bpy.types.Operator): {'WARNING'}, "Mesh has different amount of faces" ) - return {'FINISHED'} + return {'CANCELLED'} for j, face_data in enumerate(all_sorted_faces.values()): copied_data = props.topology_copied[j] @@ -175,6 +253,8 @@ class MUV_TransUVPaste(bpy.types.Operator): uvloop.pin_uv = copied_data[1][k] if self.copy_seams: edge.seam = copied_data[2][k] + else: + return {'CANCELLED'} bmesh.update_edit_mesh(active_obj.data) if self.copy_seams: @@ -302,7 +382,7 @@ def parse_faces( used_verts.update(shared_face.verts) used_edges.update(shared_face.edges) - if common.DEBUG: + if common.is_debug_mode(): shared_face.select = True # test which faces are parsed new_shared_faces.append(shared_face) diff --git a/uv_magic_uv/op/unwrap_constraint.py b/uv_magic_uv/op/unwrap_constraint.py index e98879b7..b2368fc4 100644 --- a/uv_magic_uv/op/unwrap_constraint.py +++ b/uv_magic_uv/op/unwrap_constraint.py @@ -18,8 +18,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy import bmesh @@ -32,12 +32,65 @@ from bpy.props import ( from .. import common -class MUV_UnwrapConstraint(bpy.types.Operator): +__all__ = [ + 'Properties', + 'Operator', +] + + +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 Properties: + @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 + + +class Operator(bpy.types.Operator): """ Operation class: Unwrap with constrain UV coordinate """ - bl_idname = "uv.muv_unwrap_constraint" + bl_idname = "uv.muv_unwrap_constraint_operator" bl_label = "Unwrap Constraint" bl_description = "Unwrap while keeping uv coordinate" bl_options = {'REGISTER', 'UNDO'} @@ -83,6 +136,13 @@ class MUV_UnwrapConstraint(bpy.types.Operator): default=False ) + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + def execute(self, _): obj = bpy.context.active_object bm = bmesh.from_edit_mesh(obj.data) diff --git a/uv_magic_uv/op/uv_bounding_box.py b/uv_magic_uv/op/uv_bounding_box.py index 9ebc76c4..4aa8874b 100644 --- a/uv_magic_uv/op/uv_bounding_box.py +++ b/uv_magic_uv/op/uv_bounding_box.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" from enum import IntEnum import math @@ -30,14 +30,101 @@ import bpy import bgl import mathutils import bmesh +from bpy.props import BoolProperty, EnumProperty from .. import common +__all__ = [ + 'Properties', + 'Operator', +] + + MAX_VALUE = 100000.0 -class MUV_UVBBCmd(): +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 Properties: + @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 Operator.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 """ @@ -52,7 +139,7 @@ class MUV_UVBBCmd(): return mat -class MUV_UVBBTranslationCmd(MUV_UVBBCmd): +class TranslationCommand(CommandBase): """ Custom class: Translation operation """ @@ -76,7 +163,7 @@ class MUV_UVBBTranslationCmd(MUV_UVBBCmd): self.__y = y -class MUV_UVBBRotationCmd(MUV_UVBBCmd): +class RotationCommand(CommandBase): """ Custom class: Rotation operation """ @@ -107,7 +194,7 @@ class MUV_UVBBRotationCmd(MUV_UVBBCmd): self.__y = y -class MUV_UVBBScalingCmd(MUV_UVBBCmd): +class ScalingCommand(CommandBase): """ Custom class: Scaling operation """ @@ -158,7 +245,7 @@ class MUV_UVBBScalingCmd(MUV_UVBBCmd): self.__y = y -class MUV_UVBBUniformScalingCmd(MUV_UVBBCmd): +class UniformScalingCommand(CommandBase): """ Custom class: Uniform Scaling operation """ @@ -222,7 +309,7 @@ class MUV_UVBBUniformScalingCmd(MUV_UVBBCmd): self.__y = y -class MUV_UVBBCmdExecuter(): +class CommandExecuter(): """ Custom class: manage command history and execute command """ @@ -288,67 +375,7 @@ class MUV_UVBBCmdExecuter(): self.__cmd_list.append(cmd) -class MUV_UVBBRenderer(bpy.types.Operator): - """ - Operation class: Render UV bounding box - """ - - bl_idname = "uv.muv_uvbb_renderer" - bl_label = "UV Bounding Box Renderer" - bl_description = "Bounding Box Renderer about UV in Image Editor" - - __handle = None - - @staticmethod - def handle_add(obj, context): - if MUV_UVBBRenderer.__handle is None: - sie = bpy.types.SpaceImageEditor - MUV_UVBBRenderer.__handle = sie.draw_handler_add( - MUV_UVBBRenderer.draw_bb, - (obj, context), "WINDOW", "POST_PIXEL") - - @staticmethod - def handle_remove(): - if MUV_UVBBRenderer.__handle is not None: - sie = bpy.types.SpaceImageEditor - sie.draw_handler_remove( - MUV_UVBBRenderer.__handle, "WINDOW") - MUV_UVBBRenderer.__handle = None - - @staticmethod - def __draw_ctrl_point(context, pos): - """ - Draw control point - """ - prefs = context.user_preferences.addons["uv_magic_uv"].preferences - cp_size = prefs.uvbb_cp_size - offset = cp_size / 2 - verts = [ - [pos.x - offset, pos.y - offset], - [pos.x - offset, pos.y + offset], - [pos.x + offset, pos.y + offset], - [pos.x + offset, pos.y - offset] - ] - bgl.glEnable(bgl.GL_BLEND) - bgl.glBegin(bgl.GL_QUADS) - bgl.glColor4f(1.0, 1.0, 1.0, 1.0) - for (x, y) in verts: - bgl.glVertex2f(x, y) - bgl.glEnd() - - @staticmethod - def draw_bb(_, context): - """ - Draw bounding box - """ - props = context.scene.muv_props.uvbb - for cp in props.ctrl_points: - MUV_UVBBRenderer.__draw_ctrl_point( - context, mathutils.Vector( - context.region.view2d.view_to_region(cp.x, cp.y))) - - -class MUV_UVBBState(IntEnum): +class State(IntEnum): """ Enum: State definition used by MUV_UVBBStateMgr """ @@ -369,7 +396,7 @@ class MUV_UVBBState(IntEnum): UNIFORM_SCALING_4 = 14 -class MUV_UVBBStateBase(): +class StateBase(): """ Custom class: Base class of state """ @@ -381,7 +408,7 @@ class MUV_UVBBStateBase(): raise NotImplementedError -class MUV_UVBBStateNone(MUV_UVBBStateBase): +class StateNone(StateBase): """ Custom class: No state @@ -397,8 +424,8 @@ class MUV_UVBBStateNone(MUV_UVBBStateBase): Update state """ prefs = context.user_preferences.addons["uv_magic_uv"].preferences - cp_react_size = prefs.uvbb_cp_react_size - is_uscaling = context.scene.muv_uvbb_uniform_scaling + 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) @@ -413,16 +440,16 @@ class MUV_UVBBStateNone(MUV_UVBBStateBase): arr = [1, 3, 6, 8] if i in arr: return ( - MUV_UVBBState.UNIFORM_SCALING_1 + + State.UNIFORM_SCALING_1 + arr.index(i) ) else: - return MUV_UVBBState.TRANSLATING + i + return State.TRANSLATING + i - return MUV_UVBBState.NONE + return State.NONE -class MUV_UVBBStateTranslating(MUV_UVBBStateBase): +class StateTranslating(StateBase): """ Custom class: Translating state """ @@ -431,19 +458,19 @@ class MUV_UVBBStateTranslating(MUV_UVBBStateBase): super().__init__() self.__cmd_exec = cmd_exec ix, iy = ctrl_points[0].x, ctrl_points[0].y - self.__cmd_exec.append(MUV_UVBBTranslationCmd(ix, iy)) + 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 MUV_UVBBState.NONE + return State.NONE if event.type == 'MOUSEMOVE': x, y = mouse_view.x, mouse_view.y self.__cmd_exec.top().set(x, y) - return MUV_UVBBState.TRANSLATING + return State.TRANSLATING -class MUV_UVBBStateScaling(MUV_UVBBStateBase): +class StateScaling(StateBase): """ Custom class: Scaling state """ @@ -460,19 +487,19 @@ class MUV_UVBBStateScaling(MUV_UVBBStateBase): 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( - MUV_UVBBScalingCmd(ix, iy, ox, oy, dir_x, dir_y, mat.inverted())) + 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 MUV_UVBBState.NONE + 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 MUV_UVBBStateUniformScaling(MUV_UVBBStateBase): +class StateUniformScaling(StateBase): """ Custom class: Uniform Scaling state """ @@ -483,17 +510,17 @@ class MUV_UVBBStateUniformScaling(MUV_UVBBStateBase): self.__cmd_exec = cmd_exec icp_idx = [1, 3, 6, 8] ocp_idx = [8, 6, 3, 1] - idx = state - MUV_UVBBState.UNIFORM_SCALING_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(MUV_UVBBUniformScalingCmd( + 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 MUV_UVBBState.NONE + return State.NONE if event.type == 'MOUSEMOVE': x, y = mouse_view.x, mouse_view.y self.__cmd_exec.top().set(x, y) @@ -501,7 +528,7 @@ class MUV_UVBBStateUniformScaling(MUV_UVBBStateBase): return self.__state -class MUV_UVBBStateRotating(MUV_UVBBStateBase): +class StateRotating(StateBase): """ Custom class: Rotating state """ @@ -511,27 +538,27 @@ class MUV_UVBBStateRotating(MUV_UVBBStateBase): 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(MUV_UVBBRotationCmd(ix, iy, ox, oy)) + 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 MUV_UVBBState.NONE + return State.NONE if event.type == 'MOUSEMOVE': x, y = mouse_view.x, mouse_view.y self.__cmd_exec.top().set(x, y) - return MUV_UVBBState.ROTATING + return State.ROTATING -class MUV_UVBBStateMgr(): +class StateManager(): """ Custom class: Manage state about this feature """ def __init__(self, cmd_exec): self.__cmd_exec = cmd_exec # command executer - self.__state = MUV_UVBBState.NONE # current state - self.__state_obj = MUV_UVBBStateNone(self.__cmd_exec) + self.__state = State.NONE # current state + self.__state_obj = StateNone(self.__cmd_exec) def __update_state(self, next_state, ctrl_points): """ @@ -541,18 +568,18 @@ class MUV_UVBBStateMgr(): if next_state == self.__state: return obj = None - if next_state == MUV_UVBBState.TRANSLATING: - obj = MUV_UVBBStateTranslating(self.__cmd_exec, ctrl_points) - elif MUV_UVBBState.SCALING_1 <= next_state <= MUV_UVBBState.SCALING_8: - obj = MUV_UVBBStateScaling( + 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 == MUV_UVBBState.ROTATING: - obj = MUV_UVBBStateRotating(self.__cmd_exec, ctrl_points) - elif next_state == MUV_UVBBState.NONE: - obj = MUV_UVBBStateNone(self.__cmd_exec) - elif (MUV_UVBBState.UNIFORM_SCALING_1 <= next_state <= - MUV_UVBBState.UNIFORM_SCALING_4): - obj = MUV_UVBBStateUniformScaling( + 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: @@ -569,34 +596,97 @@ class MUV_UVBBStateMgr(): context, event, ctrl_points, mouse_view) self.__update_state(next_state, ctrl_points) + return self.__state + -class MUV_UVBBUpdater(bpy.types.Operator): +class Operator(bpy.types.Operator): """ - Operation class: Update state and handle event by modal function + Operation class: UV Bounding Box """ - bl_idname = "uv.muv_uvbb_updater" - bl_label = "UV Bounding Box Updater" - bl_description = "Update 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 = MUV_UVBBCmdExecuter() # Command executer - self.__state_mgr = MUV_UVBBStateMgr(self.__cmd_exec) # State Manager + self.__cmd_exec = CommandExecuter() # Command executor + self.__state_mgr = StateManager(self.__cmd_exec) # State Manager - def __handle_add(self, context): - if self.__timer is None: - self.__timer = context.window_manager.event_timer_add( + __handle = None + __timer = None + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return False + return is_valid_context(context) + + @classmethod + def is_running(cls, _): + return 1 if cls.__handle else 0 + + @classmethod + def handle_add(cls, obj, context): + if cls.__handle is None: + sie = bpy.types.SpaceImageEditor + cls.__handle = sie.draw_handler_add( + cls.draw_bb, (obj, context), "WINDOW", "POST_PIXEL") + if cls.__timer is None: + cls.__timer = context.window_manager.event_timer_add( 0.1, context.window) - context.window_manager.modal_handler_add(self) - MUV_UVBBRenderer.handle_add(self, context) + context.window_manager.modal_handler_add(obj) - def __handle_remove(self, context): - MUV_UVBBRenderer.handle_remove() - if self.__timer is not None: - context.window_manager.event_timer_remove(self.__timer) - self.__timer = None + @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) + bgl.glBegin(bgl.GL_QUADS) + bgl.glColor4f(1.0, 1.0, 1.0, 1.0) + for (x, y) in verts: + bgl.glVertex2f(x, y) + bgl.glEnd() + + @classmethod + def draw_bb(cls, _, context): + """ + Draw bounding box + """ + props = context.scene.muv_props.uv_bounding_box + + if not Operator.is_running(context): + return + + if not is_valid_context(context): + return + + for cp in props.ctrl_points: + cls.__draw_ctrl_point( + context, mathutils.Vector( + context.region.view2d.view_to_region(cp.x, cp.y))) def __get_uv_info(self, context): """ @@ -615,10 +705,10 @@ class MUV_UVBBUpdater(bpy.types.Operator): if not f.select: continue for i, l in enumerate(f.loops): - if sc.muv_uvbb_boundary == 'UV_SEL': + 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_uvbb_boundary == 'UV': + 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 @@ -688,16 +778,23 @@ class MUV_UVBBUpdater(bpy.types.Operator): return [trans_mat * cp for cp in ctrl_points_ini] def modal(self, context, event): - props = context.scene.muv_props.uvbb + props = context.scene.muv_props.uv_bounding_box common.redraw_all_areas() - if props.running is False: - self.__handle_remove(context) + + if not Operator.is_running(context): return {'FINISHED'} - area, _, _ = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') + if not is_valid_context(context): + Operator.handle_remove(context) + return {'FINISHED'} - if event.mouse_region_x < 0 or event.mouse_region_x > area.width or \ - event.mouse_region_y < 0 or event.mouse_region_y > area.height: + region_types = [ + 'HEADER', + 'UI', + 'TOOLS', + ] + if not common.mouse_on_area(event, 'IMAGE_EDITOR') or \ + common.mouse_on_regions(event, 'IMAGE_EDITOR', region_types): return {'PASS_THROUGH'} if event.type == 'TIMER': @@ -706,27 +803,30 @@ class MUV_UVBBUpdater(bpy.types.Operator): props.ctrl_points = self.__update_ctrl_point( props.ctrl_points_ini, trans_mat) - self.__state_mgr.update(context, props.ctrl_points, event) + state = self.__state_mgr.update(context, props.ctrl_points, event) + if state == State.NONE: + return {'PASS_THROUGH'} return {'RUNNING_MODAL'} - def execute(self, context): - props = context.scene.muv_props.uvbb + def invoke(self, context, _): + props = context.scene.muv_props.uv_bounding_box - if props.running is True: - props.running = False + if Operator.is_running(context): + Operator.handle_remove(context) return {'FINISHED'} props.uv_info_ini = self.__get_uv_info(context) if props.uv_info_ini is None: return {'CANCELLED'} + + Operator.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) - self.__handle_add(context) - props.running = True return {'RUNNING_MODAL'} diff --git a/uv_magic_uv/op/uv_inspection.py b/uv_magic_uv/op/uv_inspection.py index 60a754a3..0c05e03d 100644 --- a/uv_magic_uv/op/uv_inspection.py +++ b/uv_magic_uv/op/uv_inspection.py @@ -20,343 +20,161 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy import bmesh import bgl -from mathutils import Vector +from bpy.props import BoolProperty, EnumProperty from .. import common -def is_polygon_same(points1, points2): - if len(points1) != len(points2): - return False - - pts1 = points1.as_list() - pts2 = points2.as_list() - - for p1 in pts1: - for p2 in pts2: - diff = p2 - p1 - if diff.length < 0.0000001: - pts2.remove(p2) - break - else: - return False - - return True - - -def is_segment_intersect(start1, end1, start2, end2): - seg1 = end1 - start1 - seg2 = end2 - start2 - - a1 = -seg1.y - b1 = seg1.x - d1 = -(a1 * start1.x + b1 * start1.y) - - a2 = -seg2.y - b2 = seg2.x - d2 = -(a2 * start2.x + b2 * start2.y) +__all__ = [ + 'Properties', + 'OperatorRender', + 'OperatorUpdate', +] - seg1_line2_start = a2 * start1.x + b2 * start1.y + d2 - seg1_line2_end = a2 * end1.x + b2 * end1.y + d2 - seg2_line1_start = a1 * start2.x + b1 * start2.y + d1 - seg2_line1_end = a1 * end2.x + b1 * end2.y + d1 +def is_valid_context(context): + obj = context.object - if (seg1_line2_start * seg1_line2_end >= 0) or \ - (seg2_line1_start * seg2_line1_end >= 0): - return False, None - - u = seg1_line2_start / (seg1_line2_start - seg1_line2_end) - out = start1 + u * seg1 - - return True, out - - -class RingBuffer: - def __init__(self, arr): - self.__buffer = arr.copy() - self.__pointer = 0 - - def __repr__(self): - return repr(self.__buffer) - - def __len__(self): - return len(self.__buffer) - - def insert(self, val, offset=0): - self.__buffer.insert(self.__pointer + offset, val) - - def head(self): - return self.__buffer[0] - - def tail(self): - return self.__buffer[-1] - - def get(self, offset=0): - size = len(self.__buffer) - val = self.__buffer[(self.__pointer + offset) % size] - return val - - def next(self): - size = len(self.__buffer) - self.__pointer = (self.__pointer + 1) % size - - def reset(self): - self.__pointer = 0 - - def find(self, obj): - try: - idx = self.__buffer.index(obj) - except ValueError: - return None - return self.__buffer[idx] - - def find_and_next(self, obj): - size = len(self.__buffer) - idx = self.__buffer.index(obj) - self.__pointer = (idx + 1) % size - - def find_and_set(self, obj): - idx = self.__buffer.index(obj) - self.__pointer = idx - - def as_list(self): - return self.__buffer.copy() - - def reverse(self): - self.__buffer.reverse() - self.reset() - - -# clip: reference polygon -# subject: tested polygon -def do_weiler_atherton_cliping(clip, subject, uv_layer, mode): - - clip_uvs = RingBuffer([l[uv_layer].uv.copy() for l in clip.loops]) - if is_polygon_flipped(clip_uvs): - clip_uvs.reverse() - subject_uvs = RingBuffer([l[uv_layer].uv.copy() for l in subject.loops]) - if is_polygon_flipped(subject_uvs): - subject_uvs.reverse() - - common.debug_print("===== Clip UV List =====") - common.debug_print(clip_uvs) - common.debug_print("===== Subject UV List =====") - common.debug_print(subject_uvs) - - # check if clip and subject is overlapped completely - if is_polygon_same(clip_uvs, subject_uvs): - polygons = [subject_uvs.as_list()] - common.debug_print("===== Polygons Overlapped Completely =====") - common.debug_print(polygons) - return True, polygons - - # check if subject is in clip - if is_points_in_polygon(subject_uvs, clip_uvs): - polygons = [subject_uvs.as_list()] - return True, polygons - - # check if clip is in subject - if is_points_in_polygon(clip_uvs, subject_uvs): - polygons = [subject_uvs.as_list()] - return True, polygons - - # check if clip and subject is overlapped partially - intersections = [] - while True: - subject_uvs.reset() - while True: - uv_start1 = clip_uvs.get() - uv_end1 = clip_uvs.get(1) - uv_start2 = subject_uvs.get() - uv_end2 = subject_uvs.get(1) - intersected, point = is_segment_intersect(uv_start1, uv_end1, - uv_start2, uv_end2) - if intersected: - clip_uvs.insert(point, 1) - subject_uvs.insert(point, 1) - intersections.append([point, - [clip_uvs.get(), clip_uvs.get(1)]]) - subject_uvs.next() - if subject_uvs.get() == subject_uvs.head(): - break - clip_uvs.next() - if clip_uvs.get() == clip_uvs.head(): - break - - common.debug_print("===== Intersection List =====") - common.debug_print(intersections) - - # no intersection, so subject and clip is not overlapped - if not intersections: - return False, None - - def get_intersection_pair(intersections, key): - for sect in intersections: - if sect[0] == key: - return sect[1] - - return None - - # make enter/exit pair - subject_uvs.reset() - subject_entering = [] - subject_exiting = [] - clip_entering = [] - clip_exiting = [] - intersect_uv_list = [] - while True: - pair = get_intersection_pair(intersections, subject_uvs.get()) - if pair: - sub = subject_uvs.get(1) - subject_uvs.get(-1) - inter = pair[1] - pair[0] - cross = sub.x * inter.y - inter.x * sub.y - if cross < 0: - subject_entering.append(subject_uvs.get()) - clip_exiting.append(subject_uvs.get()) - else: - subject_exiting.append(subject_uvs.get()) - clip_entering.append(subject_uvs.get()) - intersect_uv_list.append(subject_uvs.get()) - - subject_uvs.next() - if subject_uvs.get() == subject_uvs.head(): - break + # 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 - common.debug_print("===== Enter List =====") - common.debug_print(clip_entering) - common.debug_print(subject_entering) - common.debug_print("===== Exit List =====") - common.debug_print(clip_exiting) - common.debug_print(subject_exiting) - - # for now, can't handle the situation when fulfill all below conditions - # * two faces have common edge - # * each face is intersected - # * Show Mode is "Part" - # so for now, ignore this situation - if len(subject_entering) != len(subject_exiting): - if mode == 'FACE': - polygons = [subject_uvs.as_list()] - return True, polygons - return False, None - - def traverse(current_list, entering, exiting, poly, current, other_list): - result = current_list.find(current) - if not result: - return None - if result != current: - print("Internal Error") - return None - - # enter - if entering.count(current) >= 1: - entering.remove(current) - - current_list.find_and_next(current) - current = current_list.get() - - while exiting.count(current) == 0: - poly.append(current.copy()) - current_list.find_and_next(current) - current = current_list.get() - - # exit - poly.append(current.copy()) - exiting.remove(current) - - other_list.find_and_set(current) - return other_list.get() - - # Traverse - polygons = [] - current_uv_list = subject_uvs - other_uv_list = clip_uvs - current_entering = subject_entering - current_exiting = subject_exiting - - poly = [] - current_uv = current_entering[0] - - while True: - current_uv = traverse(current_uv_list, current_entering, - current_exiting, poly, current_uv, other_uv_list) - - if current_uv_list == subject_uvs: - current_uv_list = clip_uvs - other_uv_list = subject_uvs - current_entering = clip_entering - current_exiting = clip_exiting - common.debug_print("-- Next: Clip --") - else: - current_uv_list = subject_uvs - other_uv_list = clip_uvs - current_entering = subject_entering - current_exiting = subject_exiting - common.debug_print("-- Next: Subject --") - - common.debug_print(clip_entering) - common.debug_print(clip_exiting) - common.debug_print(subject_entering) - common.debug_print(subject_exiting) - - if not clip_entering and not clip_exiting \ - and not subject_entering and not subject_exiting: + # '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 - polygons.append(poly) - - common.debug_print("===== Polygons Overlapped Partially =====") - common.debug_print(polygons) - - return True, polygons + return True -class MUV_UVInspRenderer(bpy.types.Operator): +class Properties: + @classmethod + def init_props(cls, scene): + class Props(): + overlapped_info = [] + flipped_info = [] + + scene.muv_props.uv_inspection = Props() + + def get_func(_): + return OperatorRender.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 + + +class OperatorRender(bpy.types.Operator): """ Operation class: Render UV Inspection No operation (only rendering) """ - bl_idname = "uv.muv_uvinsp_renderer" + bl_idname = "uv.muv_uv_inspection_operator_render" bl_description = "Render overlapped/flipped UVs" bl_label = "Overlapped/Flipped UV renderer" __handle = None - @staticmethod - def handle_add(obj, context): + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return False + return is_valid_context(context) + + @classmethod + def is_running(cls, _): + return 1 if cls.__handle else 0 + + @classmethod + def handle_add(cls, obj, context): sie = bpy.types.SpaceImageEditor - MUV_UVInspRenderer.__handle = sie.draw_handler_add( - MUV_UVInspRenderer.draw, (obj, context), 'WINDOW', 'POST_PIXEL') + cls.__handle = sie.draw_handler_add( + OperatorRender.draw, (obj, context), 'WINDOW', 'POST_PIXEL') - @staticmethod - def handle_remove(): - if MUV_UVInspRenderer.__handle is not None: + @classmethod + def handle_remove(cls): + if cls.__handle is not None: bpy.types.SpaceImageEditor.draw_handler_remove( - MUV_UVInspRenderer.__handle, 'WINDOW') - MUV_UVInspRenderer.__handle = None + cls.__handle, 'WINDOW') + cls.__handle = None @staticmethod def draw(_, context): sc = context.scene - props = sc.muv_props.uvinsp + props = sc.muv_props.uv_inspection prefs = context.user_preferences.addons["uv_magic_uv"].preferences + if not OperatorRender.is_running(context): + return + # OpenGL configuration bgl.glEnable(bgl.GL_BLEND) # render overlapped UV - if sc.muv_uvinsp_show_overlapped: - color = prefs.uvinsp_overlapped_color + if sc.muv_uv_inspection_show_overlapped: + color = prefs.uv_inspection_overlapped_color for info in props.overlapped_info: - if sc.muv_uvinsp_show_mode == 'PART': + if sc.muv_uv_inspection_show_mode == 'PART': for poly in info["polygons"]: bgl.glBegin(bgl.GL_TRIANGLE_FAN) bgl.glColor4f(color[0], color[1], color[2], color[3]) @@ -365,7 +183,7 @@ class MUV_UVInspRenderer(bpy.types.Operator): uv.x, uv.y) bgl.glVertex2f(x, y) bgl.glEnd() - elif sc.muv_uvinsp_show_mode == 'FACE': + elif sc.muv_uv_inspection_show_mode == 'FACE': bgl.glBegin(bgl.GL_TRIANGLE_FAN) bgl.glColor4f(color[0], color[1], color[2], color[3]) for uv in info["subject_uvs"]: @@ -374,10 +192,10 @@ class MUV_UVInspRenderer(bpy.types.Operator): bgl.glEnd() # render flipped UV - if sc.muv_uvinsp_show_flipped: - color = prefs.uvinsp_flipped_color + if sc.muv_uv_inspection_show_flipped: + color = prefs.uv_inspection_flipped_color for info in props.flipped_info: - if sc.muv_uvinsp_show_mode == 'PART': + if sc.muv_uv_inspection_show_mode == 'PART': for poly in info["polygons"]: bgl.glBegin(bgl.GL_TRIANGLE_FAN) bgl.glColor4f(color[0], color[1], color[2], color[3]) @@ -386,7 +204,7 @@ class MUV_UVInspRenderer(bpy.types.Operator): uv.x, uv.y) bgl.glVertex2f(x, y) bgl.glEnd() - elif sc.muv_uvinsp_show_mode == 'FACE': + elif sc.muv_uv_inspection_show_mode == 'FACE': bgl.glBegin(bgl.GL_TRIANGLE_FAN) bgl.glColor4f(color[0], color[1], color[2], color[3]) for uv in info["uvs"]: @@ -394,100 +212,22 @@ class MUV_UVInspRenderer(bpy.types.Operator): bgl.glVertex2f(x, y) bgl.glEnd() + def invoke(self, context, _): + if not OperatorRender.is_running(context): + update_uvinsp_info(context) + OperatorRender.handle_add(self, context) + else: + OperatorRender.handle_remove() -def is_polygon_flipped(points): - area = 0.0 - for i in range(len(points)): - uv1 = points.get(i) - uv2 = points.get(i + 1) - a = uv1.x * uv2.y - uv1.y * uv2.x - area = area + a - if area < 0: - # clock-wise - return True - return False - - -def is_point_in_polygon(point, subject_points): - count = 0 - for i in range(len(subject_points)): - uv_start1 = subject_points.get(i) - uv_end1 = subject_points.get(i + 1) - uv_start2 = point - uv_end2 = Vector((1000000.0, point.y)) - intersected, _ = is_segment_intersect(uv_start1, uv_end1, - uv_start2, uv_end2) - if intersected: - count = count + 1 - - return count % 2 - - -def is_points_in_polygon(points, subject_points): - for i in range(len(points)): - internal = is_point_in_polygon(points.get(i), subject_points) - if not internal: - return False - - return True - + if context.area: + context.area.tag_redraw() -def get_overlapped_uv_info(bm, faces, uv_layer, mode): - # at first, check island overlapped - isl = common.get_island_info_from_faces(bm, faces, uv_layer) - overlapped_isl_pairs = [] - for i, i1 in enumerate(isl): - for i2 in isl[i + 1:]: - if (i1["max"].x < i2["min"].x) or (i2["max"].x < i1["min"].x) or \ - (i1["max"].y < i2["min"].y) or (i2["max"].y < i1["min"].y): - continue - overlapped_isl_pairs.append([i1, i2]) - - # next, check polygon overlapped - overlapped_uvs = [] - for oip in overlapped_isl_pairs: - for clip in oip[0]["faces"]: - f_clip = clip["face"] - for subject in oip[1]["faces"]: - f_subject = subject["face"] - - # fast operation, apply bounding box algorithm - if (clip["max_uv"].x < subject["min_uv"].x) or \ - (subject["max_uv"].x < clip["min_uv"].x) or \ - (clip["max_uv"].y < subject["min_uv"].y) or \ - (subject["max_uv"].y < clip["min_uv"].y): - continue - - # slow operation, apply Weiler-Atherton cliping algorithm - result, polygons = do_weiler_atherton_cliping(f_clip, - f_subject, - uv_layer, mode) - if result: - subject_uvs = [l[uv_layer].uv.copy() - for l in f_subject.loops] - overlapped_uvs.append({"clip_face": f_clip, - "subject_face": f_subject, - "subject_uvs": subject_uvs, - "polygons": polygons}) - - return overlapped_uvs - - -def get_flipped_uv_info(faces, uv_layer): - flipped_uvs = [] - for f in faces: - polygon = RingBuffer([l[uv_layer].uv.copy() for l in f.loops]) - if is_polygon_flipped(polygon): - uvs = [l[uv_layer].uv.copy() for l in f.loops] - flipped_uvs.append({"face": f, "uvs": uvs, - "polygons": [polygon.as_list()]}) - - return flipped_uvs + return {'FINISHED'} def update_uvinsp_info(context): sc = context.scene - props = sc.muv_props.uvinsp + props = sc.muv_props.uv_inspection obj = context.active_object bm = bmesh.from_edit_mesh(obj.data) @@ -499,125 +239,34 @@ def update_uvinsp_info(context): sel_faces = [f for f in bm.faces] else: sel_faces = [f for f in bm.faces if f.select] - props.overlapped_info = get_overlapped_uv_info(bm, sel_faces, uv_layer, - sc.muv_uvinsp_show_mode) - props.flipped_info = get_flipped_uv_info(sel_faces, uv_layer) + 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) -class MUV_UVInspUpdate(bpy.types.Operator): +class OperatorUpdate(bpy.types.Operator): """ Operation class: Update """ - bl_idname = "uv.muv_uvinsp_update" - bl_label = "Update" - bl_description = "Update Overlapped/Flipped UV" + bl_idname = "uv.muv_uv_inspection_operator_update" + bl_label = "Update UV Inspection" + bl_description = "Update UV Inspection" bl_options = {'REGISTER', 'UNDO'} - def execute(self, context): - update_uvinsp_info(context) - - if context.area: - context.area.tag_redraw() - - return {'FINISHED'} - - -class MUV_UVInspDisplay(bpy.types.Operator): - """ - Operation class: Display - """ - - bl_idname = "uv.muv_uvinsp_display" - bl_label = "Display" - bl_description = "Display Overlapped/Flipped UV" - bl_options = {'REGISTER', 'UNDO'} + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + if not OperatorRender.is_running(context): + return False + return is_valid_context(context) def execute(self, context): - sc = context.scene - props = sc.muv_props.uvinsp - if not props.display_running: - update_uvinsp_info(context) - MUV_UVInspRenderer.handle_add(self, context) - props.display_running = True - else: - MUV_UVInspRenderer.handle_remove() - props.display_running = False + update_uvinsp_info(context) if context.area: context.area.tag_redraw() return {'FINISHED'} - - -class MUV_UVInspSelectOverlapped(bpy.types.Operator): - """ - Operation class: Select faces which have overlapped UVs - """ - - bl_idname = "uv.muv_uvinsp_select_overlapped" - bl_label = "Overlapped" - bl_description = "Select faces which have overlapped UVs" - bl_options = {'REGISTER', 'UNDO'} - - 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 = 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 MUV_UVInspSelectFlipped(bpy.types.Operator): - """ - Operation class: Select faces which have flipped UVs - """ - - bl_idname = "uv.muv_uvinsp_select_flipped" - bl_label = "Flipped" - bl_description = "Select faces which have flipped UVs" - bl_options = {'REGISTER', 'UNDO'} - - 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 = 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/op/uv_sculpt.py b/uv_magic_uv/op/uv_sculpt.py index 2bf76abd..63c1adfe 100644 --- a/uv_magic_uv/op/uv_sculpt.py +++ b/uv_magic_uv/op/uv_sculpt.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" from math import pi, cos, tan, sin @@ -32,39 +32,172 @@ 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 -class MUV_UVSculptRenderer(bpy.types.Operator): +__all__ = [ + 'Properties', + 'Operator', +] + + +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 Properties: + @classmethod + def init_props(cls, scene): + def get_func(_): + return Operator.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 + + +class Operator(bpy.types.Operator): """ - Operation class: Render Brush + Operation class: UV Sculpt in View3D """ - bl_idname = "uv.muv_uvsculpt_renderer" - bl_label = "Brush Renderer" - bl_description = "Brush Renderer in View3D" + bl_idname = "uv.muv_uv_sculpt_operator" + bl_label = "UV Sculpt" + bl_description = "UV Sculpt in View3D" + bl_options = {'REGISTER'} __handle = None - - @staticmethod - def handle_add(obj, context): - if MUV_UVSculptRenderer.__handle is None: + __timer = None + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return False + return is_valid_context(context) + + @classmethod + def is_running(cls, _): + return 1 if cls.__handle else 0 + + @classmethod + def handle_add(cls, obj, context): + if not cls.__handle: sv = bpy.types.SpaceView3D - MUV_UVSculptRenderer.__handle = sv.draw_handler_add( - MUV_UVSculptRenderer.draw_brush, - (obj, context), "WINDOW", "POST_PIXEL") + 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, context.window) + context.window_manager.modal_handler_add(obj) - @staticmethod - def handle_remove(): - if MUV_UVSculptRenderer.__handle is not None: + @classmethod + def handle_remove(cls, context): + if cls.__handle: sv = bpy.types.SpaceView3D - sv.draw_handler_remove( - MUV_UVSculptRenderer.__handle, "WINDOW") - MUV_UVSculptRenderer.__handle = None - - @staticmethod - def draw_brush(obj, context): + 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 @@ -72,12 +205,12 @@ class MUV_UVSculptRenderer(bpy.types.Operator): theta = 2 * pi / num_segment fact_t = tan(theta) fact_r = cos(theta) - color = prefs.uvsculpt_brush_color + color = prefs.uv_sculpt_brush_color bgl.glBegin(bgl.GL_LINE_STRIP) bgl.glColor4f(color[0], color[1], color[2], color[3]) - x = sc.muv_uvsculpt_radius * cos(0.0) - y = sc.muv_uvsculpt_radius * sin(0.0) + x = sc.muv_uv_sculpt_radius * cos(0.0) + y = sc.muv_uv_sculpt_radius * sin(0.0) for _ in range(num_segment): bgl.glVertex2f(x + obj.current_mco.x, y + obj.current_mco.y) tx = -y @@ -88,19 +221,7 @@ class MUV_UVSculptRenderer(bpy.types.Operator): y = y * fact_r bgl.glEnd() - -class MUV_UVSculptOps(bpy.types.Operator): - """ - Operation class: UV Sculpt in View3D - """ - - bl_idname = "uv.muv_uvsculpt_ops" - bl_label = "UV Sculpt" - bl_description = "UV Sculpt in View3D" - bl_options = {'REGISTER'} - def __init__(self): - self.__timer = None self.__loop_info = [] self.__stroking = False self.current_mco = Vector((0.0, 0.0)) @@ -137,7 +258,7 @@ class MUV_UVSculptOps(bpy.types.Operator): 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_uvsculpt_radius: + if diff.length < sc.muv_uv_sculpt_radius: info = { "face_idx": f.index, "loop_idx": i, @@ -145,8 +266,8 @@ class MUV_UVSculptOps(bpy.types.Operator): "initial_vco_2d": loc_2d, "initial_uv": l[uv_layer].uv.copy(), "strength": self.__get_strength( - diff.length, sc.muv_uvsculpt_radius, - sc.muv_uvsculpt_strength) + diff.length, sc.muv_uv_sculpt_radius, + sc.muv_uv_sculpt_strength) } self.__loop_info.append(info) @@ -158,13 +279,13 @@ class MUV_UVSculptOps(bpy.types.Operator): uv_layer = bm.loops.layers.uv.verify() mco = self.current_mco - if sc.muv_uvsculpt_tools == 'GRAB': + 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_uvsculpt_tools == 'PINCH': + elif sc.muv_uv_sculpt_tools == 'PINCH': _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') loop_info = [] for f in bm.faces: @@ -174,7 +295,7 @@ class MUV_UVSculptOps(bpy.types.Operator): 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_uvsculpt_radius: + if diff.length < sc.muv_uv_sculpt_radius: info = { "face_idx": f.index, "loop_idx": i, @@ -182,8 +303,8 @@ class MUV_UVSculptOps(bpy.types.Operator): "initial_vco_2d": loc_2d, "initial_uv": l[uv_layer].uv.copy(), "strength": self.__get_strength( - diff.length, sc.muv_uvsculpt_radius, - sc.muv_uvsculpt_strength) + diff.length, sc.muv_uv_sculpt_radius, + sc.muv_uv_sculpt_strength) } loop_info.append(info) @@ -215,13 +336,13 @@ class MUV_UVSculptOps(bpy.types.Operator): # move to target UV coordinate for info in loop_info: l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] - if sc.muv_uvsculpt_pinch_invert: + 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_uvsculpt_tools == 'RELAX': + elif sc.muv_uv_sculpt_tools == 'RELAX': _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') # get vertex and loop relation @@ -265,19 +386,19 @@ class MUV_UVSculptOps(bpy.types.Operator): 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_uvsculpt_radius: + if diff.length >= sc.muv_uv_sculpt_radius: continue db = vert_db[l.vert] strength = self.__get_strength(diff.length, - sc.muv_uvsculpt_radius, - sc.muv_uvsculpt_strength) + sc.muv_uv_sculpt_radius, + sc.muv_uv_sculpt_strength) base = (1.0 - strength) * l[uv_layer].uv - if sc.muv_uvsculpt_relax_method == 'HC': + 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_uvsculpt_relax_method == 'LAPLACIAN': + elif sc.muv_uv_sculpt_relax_method == 'LAPLACIAN': diff = strength * db["uv_p"] target_uv = base + diff else: @@ -294,7 +415,7 @@ class MUV_UVSculptOps(bpy.types.Operator): uv_layer = bm.loops.layers.uv.verify() mco = self.current_mco - if sc.muv_uvsculpt_tools == 'GRAB': + 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"]] @@ -303,23 +424,24 @@ class MUV_UVSculptOps(bpy.types.Operator): bmesh.update_edit_mesh(obj.data) def modal(self, context, event): - props = context.scene.muv_props.uvsculpt - if context.area: context.area.tag_redraw() - if not props.running: - if self.__timer is not None: - MUV_UVSculptRenderer.handle_remove() - context.window_manager.event_timer_remove(self.__timer) - self.__timer = None + if not Operator.is_running(context): + Operator.handle_remove(context) + return {'FINISHED'} self.current_mco = Vector((event.mouse_region_x, event.mouse_region_y)) - area, _, _ = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') - if self.current_mco.x < 0 or self.current_mco.x > area.width or \ - self.current_mco.y < 0 or self.current_mco.y > area.height: + 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': @@ -331,30 +453,25 @@ class MUV_UVSculptOps(bpy.types.Operator): 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 {'RUNNING_MODAL'} + return {'PASS_THROUGH'} def invoke(self, context, _): - props = context.scene.muv_props.uvsculpt - if context.area: context.area.tag_redraw() - if props.running: - props.running = False - return {'FINISHED'} - - props.running = True - if self.__timer is None: - self.__timer = context.window_manager.event_timer_add( - 0.1, context.window) - context.window_manager.modal_handler_add(self) - MUV_UVSculptRenderer.handle_add(self, context) + if Operator.is_running(context): + Operator.handle_remove(context) + else: + Operator.handle_add(self, context) return {'RUNNING_MODAL'} diff --git a/uv_magic_uv/op/uvw.py b/uv_magic_uv/op/uvw.py index 10202677..44858187 100644 --- a/uv_magic_uv/op/uvw.py +++ b/uv_magic_uv/op/uvw.py @@ -20,8 +20,8 @@ __author__ = "Alexander Milovsky, Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" from math import sin, cos, pi @@ -37,8 +37,56 @@ from mathutils import Vector from .. import common -class MUV_UVWBoxMap(bpy.types.Operator): - bl_idname = "uv.muv_uvw_box_map" +__all__ = [ + 'Properties', + 'OperatorBoxMap', + 'OperatorBestPlanerMap', +] + + +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 Properties: + @classmethod + def init_props(cls, scene): + scene.muv_uvw_enabled = BoolProperty( + name="UVW Enabled", + description="UVW is enabled", + default=False + ) + scene.muv_uvw_assign_uvmap = BoolProperty( + name="Assign UVMap", + description="Assign UVMap when no UVmaps are available", + default=True + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_uvw_enabled + del scene.muv_uvw_assign_uvmap + + +class OperatorBoxMap(bpy.types.Operator): + bl_idname = "uv.muv_uvw_operator_box_map" bl_label = "Box Map" bl_options = {'REGISTER', 'UNDO'} @@ -70,8 +118,10 @@ class MUV_UVWBoxMap(bpy.types.Operator): @classmethod def poll(cls, context): - obj = context.active_object - return obj and obj.type == 'MESH' + # 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 @@ -151,8 +201,8 @@ class MUV_UVWBoxMap(bpy.types.Operator): return {'FINISHED'} -class MUV_UVWBestPlanerMap(bpy.types.Operator): - bl_idname = "uv.muv_uvw_best_planer_map" +class OperatorBestPlanerMap(bpy.types.Operator): + bl_idname = "uv.muv_uvw_operator_best_planer_map" bl_label = "Best Planer Map" bl_options = {'REGISTER', 'UNDO'} @@ -183,8 +233,10 @@ class MUV_UVWBestPlanerMap(bpy.types.Operator): @classmethod def poll(cls, context): - obj = context.active_object - return obj and obj.type == 'MESH' + # 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 diff --git a/uv_magic_uv/op/world_scale_uv.py b/uv_magic_uv/op/world_scale_uv.py index e256fbac..e1a44954 100644 --- a/uv_magic_uv/op/world_scale_uv.py +++ b/uv_magic_uv/op/world_scale_uv.py @@ -20,25 +20,60 @@ __author__ = "McBuff, Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" from math import sqrt import bpy import bmesh from mathutils import Vector -from bpy.props import EnumProperty +from bpy.props import ( + EnumProperty, + FloatProperty, + IntVectorProperty, + BoolProperty, +) from .. import common -def measure_wsuv_info(obj): +__all__ = [ + 'Properties', + 'OperatorMeasure', + 'OperatorApplyManual', + 'OperatorApplyScalingDensity', + 'OperatorApplyProportionalToMesh', +] + + +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) + uv_area = common.measure_uv_area(obj, tex_size) if not uv_area: - return None, None, None + return None, mesh_area, None if mesh_area == 0.0: density = 0.0 @@ -48,16 +83,112 @@ def measure_wsuv_info(obj): return uv_area, mesh_area, density -class MUV_WSUVMeasure(bpy.types.Operator): +class Properties: + @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 + + +class OperatorMeasure(bpy.types.Operator): """ Operation class: Measure face size """ - bl_idname = "uv.muv_wsuv_measure" - bl_label = "Measure" + 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'} + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + def execute(self, context): sc = context.scene obj = context.active_object @@ -68,9 +199,9 @@ class MUV_WSUVMeasure(bpy.types.Operator): "Object must have more than one UV map and texture") return {'CANCELLED'} - sc.muv_wsuv_src_uv_area = uv_area - sc.muv_wsuv_src_mesh_area = mesh_area - sc.muv_wsuv_src_density = density + 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}" @@ -79,41 +210,264 @@ class MUV_WSUVMeasure(bpy.types.Operator): return {'FINISHED'} -class MUV_WSUVApply(bpy.types.Operator): +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 OperatorApplyManual(bpy.types.Operator): """ - Operation class: Apply scaled UV + Operation class: Apply scaled UV (Manual) """ - bl_idname = "uv.muv_wsuv_apply" - bl_label = "Apply" - bl_description = "Apply scaled UV based on scale calculation" + 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') + ('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" + default='CENTER' ) + show_dialog = BoolProperty( + name="Show Diaglog Menu", + description="Show dialog menu if true", + default=True, + options={'HIDDEN', 'SKIP_SAVE'} + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + + def __apply_manual(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + tex_size = self.tgt_texture_size + uv_area, _, density = measure_wsuv_info(obj, tex_size) + if not uv_area: + self.report({'WARNING'}, + "Object must have more than one UV map") + return {'CANCELLED'} + + tgt_density = self.tgt_density + factor = tgt_density / density + + apply(context.active_object, self.origin, factor) + self.report({'INFO'}, "Scaling factor: {0}".format(factor)) + + return {'FINISHED'} def draw(self, _): layout = self.layout + layout.prop(self, "tgt_density") + layout.prop(self, "tgt_texture_size") layout.prop(self, "origin") + layout.separator() + + def invoke(self, context, _): + if self.show_dialog: + wm = context.window_manager + return wm.invoke_props_dialog(self) + + return self.execute(context) + def execute(self, context): - sc = context.scene + return self.__apply_manual(context) + + +class OperatorApplyScalingDensity(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'} + ) + + @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: @@ -121,116 +475,172 @@ class MUV_WSUVApply(bpy.types.Operator): bm.edges.ensure_lookup_table() bm.faces.ensure_lookup_table() - sel_faces = [f for f in bm.faces if f.select] - - uv_area, mesh_area, density = measure_wsuv_info(obj) + 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'} - uv_layer = bm.loops.layers.uv.verify() + tgt_density = self.src_density * self.tgt_scaling_factor + factor = tgt_density / density - if sc.muv_wsuv_mode == 'PROPORTIONAL': - tgt_density = sc.muv_wsuv_src_density * sqrt(mesh_area) / \ - sqrt(sc.muv_wsuv_src_mesh_area) - elif sc.muv_wsuv_mode == 'SCALING': - tgt_density = sc.muv_wsuv_src_density * sc.muv_wsuv_scaling_factor - elif sc.muv_wsuv_mode == 'USER': - tgt_density = sc.muv_wsuv_tgt_density - elif sc.muv_wsuv_mode == 'CONSTANT': - tgt_density = sc.muv_wsuv_src_density + apply(context.active_object, self.origin, factor) + self.report({'INFO'}, "Scaling factor: {0}".format(factor)) - factor = tgt_density / density + return {'FINISHED'} - # calculate origin - if self.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 self.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 self.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 self.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 self.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 self.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 self.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 self.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 self.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 + 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 execute(self, context): + if self.same_density: + self.tgt_scaling_factor = 1.0 + + return self.__apply_scaling_density(context) + + +class OperatorApplyProportionalToMesh(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'} + ) + + @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'} - bmesh.update_edit_mesh(obj.data) + 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 wm.invoke_props_dialog(self) + + return self.execute(context) + + def execute(self, context): + return self.__apply_proportional_to_mesh(context) diff --git a/uv_magic_uv/preferences.py b/uv_magic_uv/preferences.py index d8cdf86b..376258d0 100644 --- a/uv_magic_uv/preferences.py +++ b/uv_magic_uv/preferences.py @@ -20,23 +20,165 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" +import bpy from bpy.props import ( FloatProperty, FloatVectorProperty, + BoolProperty, + EnumProperty, + IntProperty, ) from bpy.types import AddonPreferences +from . import ui +from . import op +from . import addon_updater_ops -class MUV_Preferences(AddonPreferences): + +__all__ = [ + 'add_builtin_menu', + 'remove_builtin_menu', + 'Preferences' +] + + +def view3d_uvmap_menu_fn(self, context): + layout = self.layout + sc = context.scene + + layout.separator() + layout.label("Copy/Paste UV", icon='IMAGE_COL') + # Copy/Paste UV + layout.menu(ui.VIEW3D_MT_uv_map.MenuCopyPasteUV.bl_idname, + text="Copy/Paste UV") + # Transfer UV + layout.menu(ui.VIEW3D_MT_uv_map.MenuTransferUV.bl_idname, + text="Transfer UV") + + layout.separator() + layout.label("UV Manipulation", icon='IMAGE_COL') + # Flip/Rotate UV + ops = layout.operator(op.flip_rotate_uv.Operator.bl_idname, + text="Flip/Rotate UV") + ops.seams = sc.muv_flip_rotate_uv_seams + # Mirror UV + ops = layout.operator(op.mirror_uv.Operator.bl_idname, text="Mirror UV") + ops.axis = sc.muv_mirror_uv_axis + # Move UV + layout.operator(op.move_uv.Operator.bl_idname, text="Move UV") + # World Scale UV + layout.menu(ui.VIEW3D_MT_uv_map.MenuWorldScaleUV.bl_idname, + text="World Scale UV") + # Preserve UV + layout.menu(ui.VIEW3D_MT_uv_map.MenuPreserveUVAspect.bl_idname, + text="Preserve UV") + # Texture Lock + layout.menu(ui.VIEW3D_MT_uv_map.MenuTextureLock.bl_idname, + text="Texture Lock") + # Texture Wrap + layout.menu(ui.VIEW3D_MT_uv_map.MenuTextureWrap.bl_idname, + text="Texture Wrap") + # UV Sculpt + layout.prop(sc, "muv_uv_sculpt_enable", text="UV Sculpt") + + layout.separator() + layout.label("UV Mapping", icon='IMAGE_COL') + # Unwrap Constraint + ops = layout.operator(op.unwrap_constraint.Operator.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.MenuTextureProjection.bl_idname, + text="Texture Projection") + # UVW + layout.menu(ui.VIEW3D_MT_uv_map.MenuUVW.bl_idname, text="UVW") + + +def view3d_object_menu_fn(self, _): + layout = self.layout + + layout.separator() + # Copy/Paste UV (Among Objecct) + layout.menu(ui.VIEW3D_MT_object.MenuCopyPasteUVObject.bl_idname, + text="Copy/Paste UV") + layout.label("Copy/Paste UV", icon='IMAGE_COL') + + +def image_uvs_menu_fn(self, context): + layout = self.layout + sc = context.scene + + layout.separator() + # Align UV Cursor + layout.menu(ui.IMAGE_MT_uvs.MenuAlignUVCursor.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.MenuUVInspection.bl_idname, + text="UV Inspection") + layout.label("Editor Enhancement", icon='IMAGE_COL') + + layout.separator() + # Align UV + layout.menu(ui.IMAGE_MT_uvs.MenuAlignUV.bl_idname, text="Align UV") + # Smooth UV + ops = layout.operator(op.smooth_uv.Operator.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 + # Select UV + layout.menu(ui.IMAGE_MT_uvs.MenuSelectUV.bl_idname, text="Select UV") + # Pack UV + ops = layout.operator(op.pack_uv.Operator.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 + layout.label("UV Manipulation", icon='IMAGE_COL') + + layout.separator() + # Copy/Paste UV (on UV/Image Editor) + layout.menu(ui.IMAGE_MT_uvs.MenuCopyPasteUVUVEdit.bl_idname, + text="Copy/Paste UV") + layout.label("Copy/Paste UV", icon='IMAGE_COL') + + +def add_builtin_menu(): + bpy.types.VIEW3D_MT_uv_map.append(view3d_uvmap_menu_fn) + bpy.types.VIEW3D_MT_object.append(view3d_object_menu_fn) + bpy.types.IMAGE_MT_uvs.append(image_uvs_menu_fn) + + +def remove_builtin_menu(): + bpy.types.IMAGE_MT_uvs.remove(image_uvs_menu_fn) + bpy.types.VIEW3D_MT_object.remove(view3d_object_menu_fn) + bpy.types.VIEW3D_MT_uv_map.remove(view3d_uvmap_menu_fn) + + +class Preferences(AddonPreferences): """Preferences class: Preferences for this add-on""" bl_idname = __package__ + def update_enable_builtin_menu(self, _): + if self['enable_builtin_menu']: + add_builtin_menu() + else: + remove_builtin_menu() + + # enable to add features to built-in menu + enable_builtin_menu = BoolProperty( + name="Built-in Menu", + description="Enable built-in menu", + default=True, + update=update_enable_builtin_menu + ) + # for UV Sculpt - uvsculpt_brush_color = FloatVectorProperty( + uv_sculpt_brush_color = FloatVectorProperty( name="Color", description="Color", default=(1.0, 0.4, 0.4, 1.0), @@ -47,7 +189,7 @@ class MUV_Preferences(AddonPreferences): ) # for Overlapped UV - uvinsp_overlapped_color = FloatVectorProperty( + uv_inspection_overlapped_color = FloatVectorProperty( name="Color", description="Color", default=(0.0, 0.0, 1.0, 0.3), @@ -58,7 +200,7 @@ class MUV_Preferences(AddonPreferences): ) # for Flipped UV - uvinsp_flipped_color = FloatVectorProperty( + uv_inspection_flipped_color = FloatVectorProperty( name="Color", description="Color", default=(1.0, 0.0, 0.0, 0.3), @@ -69,7 +211,7 @@ class MUV_Preferences(AddonPreferences): ) # for Texture Projection - texproj_canvas_padding = FloatVectorProperty( + texture_projection_canvas_padding = FloatVectorProperty( name="Canvas Padding", description="Canvas Padding", size=2, @@ -78,139 +220,245 @@ class MUV_Preferences(AddonPreferences): default=(20.0, 20.0)) # for UV Bounding Box - uvbb_cp_size = FloatProperty( + uv_bounding_box_cp_size = FloatProperty( name="Size", description="Control Point Size", default=6.0, min=3.0, max=100.0) - uvbb_cp_react_size = FloatProperty( + uv_bounding_box_cp_react_size = FloatProperty( name="React Size", description="Size event fired", default=10.0, min=3.0, max=100.0) - def draw(self, _): + # for UI + category = EnumProperty( + name="Category", + description="Preferences Category", + items=[ + ('INFO', "Information", "Information about this add-on"), + ('CONFIG', "Configuration", "Configuration about this add-on"), + ('UPDATE', "Update", "Update this add-on"), + ], + default='INFO' + ) + info_desc_expanded = BoolProperty( + name="Description", + description="Description", + default=False + ) + info_loc_expanded = BoolProperty( + name="Location", + description="Location", + default=False + ) + conf_uv_sculpt_expanded = BoolProperty( + name="UV Sculpt", + description="UV Sculpt", + default=False + ) + conf_uv_inspection_expanded = BoolProperty( + name="UV Inspection", + description="UV Inspection", + default=False + ) + conf_texture_projection_expanded = BoolProperty( + name="Texture Projection", + description="Texture Projection", + default=False + ) + conf_uv_bounding_box_expanded = BoolProperty( + name="UV Bounding Box", + description="UV Bounding Box", + default=False + ) + + # for add-on updater + 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 + ) + + def draw(self, context): layout = self.layout - layout.label("[Configuration]") - - layout.label("UV Sculpt:") - sp = layout.split(percentage=0.05) - col = sp.column() # spacer - sp = sp.split(percentage=0.3) - col = sp.column() - col.label("Brush Color:") - col.prop(self, "uvsculpt_brush_color", text="") - - layout.separator() - - layout.label("UV Inspection:") - sp = layout.split(percentage=0.05) - col = sp.column() # spacer - sp = sp.split(percentage=0.3) - col = sp.column() - col.label("Overlapped UV Color:") - col.prop(self, "uvinsp_overlapped_color", text="") - sp = sp.split(percentage=0.45) - col = sp.column() - col.label("Flipped UV Color:") - col.prop(self, "uvinsp_flipped_color", text="") - - layout.separator() - - layout.label("Texture Projection:") - sp = layout.split(percentage=0.05) - col = sp.column() # spacer - sp = sp.split(percentage=0.3) - col = sp.column() - col.prop(self, "texproj_canvas_padding") - - layout.separator() - - layout.label("UV Bounding Box:") - sp = layout.split(percentage=0.05) - col = sp.column() # spacer - sp = sp.split(percentage=0.3) - col = sp.column() - col.label("Control Point:") - col.prop(self, "uvbb_cp_size") - col.prop(self, "uvbb_cp_react_size") - - layout.label("--------------------------------------") - - layout.label("[Description]") - 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") - - layout.label("--------------------------------------") - - layout.label("[Location]") - - 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) - col = sp.column(align=True) - col.label("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) - col = sp.column(align=True) - col.label("Copy/Paste UV (Among faces in 3D View)") - col.label("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) - 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") - - 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) - col = sp.column(align=True) - col.label("Unwrap Constraint") - col.label("Texture Projection") - col.label("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) - col = sp.column(align=True) - col.label("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) - col = sp.column(align=True) - col.label("Align UV") - col.label("Smooth UV") - col.label("Select UV") - col.label("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) - col = sp.column(align=True) - col.label("Align UV Cursor") - col.label("UV Cursor Location") - col.label("UV Bounding Box") - col.label("UV Inspection") + layout.row().prop(self, "category", expand=True) + + if self.category == 'INFO': + 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") + + layout.prop( + self, "info_loc_expanded", text="Location", + icon='DISCLOSURE_TRI_DOWN' if self.info_loc_expanded + else 'DISCLOSURE_TRI_RIGHT') + if self.info_loc_expanded: + row = layout.row(align=True) + sp = row.split(percentage=0.5) + sp.label("3D View > Tool shelf > Copy/Paste UV (Object mode)") + sp = sp.split(percentage=1.0) + col = sp.column(align=True) + col.label("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) + col = sp.column(align=True) + col.label("Copy/Paste UV (Among faces in 3D View)") + col.label("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) + 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") + + 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) + col = sp.column(align=True) + col.label("Unwrap Constraint") + col.label("Texture Projection") + col.label("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) + col = sp.column(align=True) + col.label("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) + col = sp.column(align=True) + col.label("Align UV") + col.label("Smooth UV") + col.label("Select UV") + col.label("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) + col = sp.column(align=True) + col.label("Align UV Cursor") + col.label("UV Cursor Location") + col.label("UV Bounding Box") + col.label("UV Inspection") + + elif self.category == 'CONFIG': + layout.prop(self, "enable_builtin_menu", text="Built-in Menu") + + layout.separator() + + layout.prop( + self, "conf_uv_sculpt_expanded", text="UV Sculpt", + icon='DISCLOSURE_TRI_DOWN' if self.conf_uv_sculpt_expanded + else 'DISCLOSURE_TRI_RIGHT') + if self.conf_uv_sculpt_expanded: + sp = layout.split(percentage=0.05) + col = sp.column() # spacer + sp = sp.split(percentage=0.3) + col = sp.column() + col.label("Brush Color:") + col.prop(self, "uv_sculpt_brush_color", text="") + layout.separator() + + layout.prop( + self, "conf_uv_inspection_expanded", text="UV Inspection", + icon='DISCLOSURE_TRI_DOWN' if self.conf_uv_inspection_expanded + else 'DISCLOSURE_TRI_RIGHT') + if self.conf_uv_inspection_expanded: + sp = layout.split(percentage=0.05) + col = sp.column() # spacer + sp = sp.split(percentage=0.3) + col = sp.column() + col.label("Overlapped UV Color:") + col.prop(self, "uv_inspection_overlapped_color", text="") + sp = sp.split(percentage=0.45) + col = sp.column() + col.label("Flipped UV Color:") + col.prop(self, "uv_inspection_flipped_color", text="") + layout.separator() + + layout.prop( + self, "conf_texture_projection_expanded", + text="Texture Projection", + icon='DISCLOSURE_TRI_DOWN' + if self.conf_texture_projection_expanded + else 'DISCLOSURE_TRI_RIGHT') + if self.conf_texture_projection_expanded: + sp = layout.split(percentage=0.05) + col = sp.column() # spacer + sp = sp.split(percentage=0.3) + col = sp.column() + col.prop(self, "texture_projection_canvas_padding") + layout.separator() + + layout.prop( + self, "conf_uv_bounding_box_expanded", text="UV Bounding Box", + icon='DISCLOSURE_TRI_DOWN' + if self.conf_uv_bounding_box_expanded + else 'DISCLOSURE_TRI_RIGHT') + if self.conf_uv_bounding_box_expanded: + sp = layout.split(percentage=0.05) + col = sp.column() # spacer + sp = sp.split(percentage=0.3) + col = sp.column() + col.label("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) diff --git a/uv_magic_uv/properites.py b/uv_magic_uv/properites.py index d882063a..e4634e51 100644 --- a/uv_magic_uv/properites.py +++ b/uv_magic_uv/properites.py @@ -20,746 +20,110 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" - -import bpy -from bpy.props import ( - FloatProperty, - EnumProperty, - BoolProperty, - FloatVectorProperty, - IntProperty +__version__ = "5.2" +__date__ = "17 Nov 2018" + + +from .op import ( + align_uv, + align_uv_cursor, + copy_paste_uv, + copy_paste_uv_object, + copy_paste_uv_uvedit, + flip_rotate_uv, + mirror_uv, + move_uv, + pack_uv, + preserve_uv_aspect, + select_uv, + smooth_uv, + texture_lock, + texture_projection, + texture_wrap, + transfer_uv, + unwrap_constraint, + uv_bounding_box, + uv_inspection, + uv_sculpt, + uvw, + world_scale_uv, ) -from mathutils import Vector - -from . import common -def get_loaded_texture_name(_, __): - items = [(key, key, "") for key in bpy.data.images.keys()] - items.append(("None", "None", "")) - return items +__all__ = [ + 'MUV_Properties', + 'init_props', + 'clear_props', +] # Properties used in this add-on. +# pylint: disable=W0612 class MUV_Properties(): - cpuv = None - cpuv_obj = None - cpuv_selseq = None - transuv = None - uvbb = None - texlock = None - texproj = None - texwrap = None - mvuv = None - uvinsp = None - uvsculpt = None - def __init__(self): - self.cpuv = MUV_CPUVProps() - self.cpuv_obj = MUV_CPUVProps() - self.cpuv_selseq = MUV_CPUVSelSeqProps() - self.transuv = MUV_TransUVProps() - self.uvbb = MUV_UVBBProps() - self.texlock = MUV_TexLockProps() - self.texproj = MUV_TexProjProps() - self.texwrap = MUV_TexWrapProps() - self.mvuv = MUV_MVUVProps() - self.uvinsp = MUV_UVInspProps() - self.uvsculpt = MUV_UVSculptProps() - - -class MUV_CPUVProps(): - src_uvs = [] - src_pin_uvs = [] - src_seams = [] - - -class MUV_CPUVSelSeqProps(): - src_uvs = [] - src_pin_uvs = [] - src_seams = [] - - -class MUV_TransUVProps(): - topology_copied = [] - - -class MUV_TexProjProps(): - running = False - - -class MUV_UVBBProps(): - uv_info_ini = [] - ctrl_points_ini = [] - ctrl_points = [] - running = False - - -class MUV_TexLockProps(): - verts_orig = None - intr_verts_orig = None - intr_running = False - - -class MUV_TexWrapProps(): - ref_face_index = -1 - ref_obj = None + self.prefs = MUV_Prefs() -class MUV_MVUVProps(): - running = False - - -class MUV_UVInspProps(): - display_running = False - overlapped_info = [] - flipped_info = [] - - -class MUV_UVSculptProps(): - running = False +class MUV_Prefs(): + expanded = { + "info_desc": False, + "info_loc": False, + "conf_uvsculpt": False, + "conf_uvinsp": False, + "conf_texproj": False, + "conf_uvbb": False + } def init_props(scene): scene.muv_props = MUV_Properties() - # UV Sculpt - scene.muv_uvsculpt_enabled = BoolProperty( - name="UV Sculpt", - description="UV Sculpt is enabled", - default=False - ) - scene.muv_uvsculpt_radius = IntProperty( - name="Radius", - description="Radius of the brush", - min=1, - max=500, - default=30 - ) - scene.muv_uvsculpt_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_uvsculpt_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_uvsculpt_show_brush = BoolProperty( - name="Show Brush", - description="Show Brush", - default=True - ) - scene.muv_uvsculpt_pinch_invert = BoolProperty( - name="Invert", - description="Pinch UV to invert direction", - default=False - ) - scene.muv_uvsculpt_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' - ) - - # Texture Wrap - scene.muv_texwrap_enabled = BoolProperty( - name="Texture Wrap", - description="Texture Wrap is enabled", - default=False - ) - scene.muv_texwrap_set_and_refer = BoolProperty( - name="Set and Refer", - description="Refer and set UV", - default=True - ) - scene.muv_texwrap_selseq = BoolProperty( - name="Selection Sequence", - description="Set UV sequentially", - default=False - ) - - # UV inspection - scene.muv_seluv_enabled = BoolProperty( - name="Select UV Enabled", - description="Select UV is enabled", - default=False - ) - scene.muv_uvinsp_enabled = BoolProperty( - name="UV Inspection Enabled", - description="UV Inspection is enabled", - default=False - ) - scene.muv_uvinsp_show_overlapped = BoolProperty( - name="Overlapped", - description="Show overlapped UVs", - default=False - ) - scene.muv_uvinsp_show_flipped = BoolProperty( - name="Flipped", - description="Show flipped UVs", - default=False - ) - scene.muv_uvinsp_show_mode = EnumProperty( - name="Mode", - description="Show mode", - items=[ - ('PART', "Part", "Show only overlapped/flipped part"), - ('FACE', "Face", "Show overlapped/flipped face") - ], - default='PART' - ) - - # Align UV - scene.muv_auv_enabled = BoolProperty( - name="Align UV Enabled", - description="Align UV is enabled", - default=False - ) - scene.muv_auv_transmission = BoolProperty( - name="Transmission", - description="Align linked UVs", - default=False - ) - scene.muv_auv_select = BoolProperty( - name="Select", - description="Select UVs which are aligned", - default=False - ) - scene.muv_auv_vertical = BoolProperty( - name="Vert-Infl (Vertical)", - description="Align vertical direction influenced " - "by mesh vertex proportion", - default=False - ) - scene.muv_auv_horizontal = BoolProperty( - name="Vert-Infl (Horizontal)", - description="Align horizontal direction influenced " - "by mesh vertex proportion", - default=False - ) - scene.muv_auv_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' - ) - - # Smooth UV - scene.muv_smuv_enabled = BoolProperty( - name="Smooth UV Enabled", - description="Smooth UV is enabled", - default=False - ) - scene.muv_smuv_transmission = BoolProperty( - name="Transmission", - description="Smooth linked UVs", - default=False - ) - scene.muv_smuv_mesh_infl = FloatProperty( - name="Mesh Influence", - description="Influence rate of mesh vertex", - min=0.0, - max=1.0, - default=0.0 - ) - scene.muv_smuv_select = BoolProperty( - name="Select", - description="Select UVs which are smoothed", - default=False - ) - - # UV Bounding Box - scene.muv_uvbb_enabled = BoolProperty( - name="UV Bounding Box Enabled", - description="UV Bounding Box is enabled", - default=False - ) - scene.muv_uvbb_uniform_scaling = BoolProperty( - name="Uniform Scaling", - description="Enable Uniform Scaling", - default=False - ) - scene.muv_uvbb_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") - ] - ) - - # Pack UV - scene.muv_packuv_enabled = BoolProperty( - name="Pack UV Enabled", - description="Pack UV is enabled", - default=False - ) - scene.muv_packuv_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_packuv_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 - ) - - # Move UV - scene.muv_mvuv_enabled = BoolProperty( - name="Move UV Enabled", - description="Move UV is enabled", - default=False - ) - - # UVW - scene.muv_uvw_enabled = BoolProperty( - name="UVW Enabled", - description="UVW is enabled", - default=False - ) - scene.muv_uvw_assign_uvmap = BoolProperty( - name="Assign UVMap", - description="Assign UVMap when no UVmaps are available", - default=True - ) - - # Texture Projection - scene.muv_texproj_enabled = BoolProperty( - name="Texture Projection Enabled", - description="Texture Projection is enabled", - default=False - ) - scene.muv_texproj_tex_magnitude = FloatProperty( - name="Magnitude", - description="Texture Magnitude", - default=0.5, - min=0.0, - max=100.0 - ) - scene.muv_texproj_tex_image = EnumProperty( - name="Image", - description="Texture Image", - items=get_loaded_texture_name - ) - scene.muv_texproj_tex_transparency = FloatProperty( - name="Transparency", - description="Texture Transparency", - default=0.2, - min=0.0, - max=1.0 - ) - scene.muv_texproj_adjust_window = BoolProperty( - name="Adjust Window", - description="Size of renderered texture is fitted to window", - default=True - ) - scene.muv_texproj_apply_tex_aspect = BoolProperty( - name="Texture Aspect Ratio", - description="Apply Texture Aspect ratio to displayed texture", - default=True - ) - scene.muv_texproj_assign_uvmap = BoolProperty( - name="Assign UVMap", - description="Assign UVMap when no UVmaps are available", - default=True - ) - - # Texture Lock - scene.muv_texlock_enabled = BoolProperty( - name="Texture Lock Enabled", - description="Texture Lock is enabled", - default=False - ) - scene.muv_texlock_connect = BoolProperty( - name="Connect UV", - default=True - ) - - # World Scale UV - scene.muv_wsuv_enabled = BoolProperty( - name="World Scale UV Enabled", - description="World Scale UV is enabled", - default=False - ) - scene.muv_wsuv_src_mesh_area = FloatProperty( - name="Mesh Area", - description="Source Mesh Area", - default=0.0, - min=0.0 - ) - scene.muv_wsuv_src_uv_area = FloatProperty( - name="UV Area", - description="Source UV Area", - default=0.0, - min=0.0 - ) - scene.muv_wsuv_src_density = FloatProperty( - name="Density", - description="Source Texel Density", - default=0.0, - min=0.0 - ) - scene.muv_wsuv_tgt_density = FloatProperty( - name="Density", - description="Target Texel Density", - default=0.0, - min=0.0 - ) - scene.muv_wsuv_mode = EnumProperty( - name="Mode", - description="Density calculation mode", - items=[ - ('PROPORTIONAL', 'Proportional', 'Scale proportionally by mesh'), - ('SCALING', 'Scaling', 'Specify scale factor'), - ('USER', 'User', 'Specify density'), - ('CONSTANT', 'Constant', 'Constant density') - ], - default='CONSTANT' - ) - scene.muv_wsuv_scaling_factor = FloatProperty( - name="Scaling Factor", - default=1.0, - max=1000.0, - min=0.00001 - ) - scene.muv_wsuv_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' - ) - - # Unwrap Constraint - scene.muv_unwrapconst_enabled = BoolProperty( - name="Unwrap Constraint Enabled", - description="Unwrap Constraint is enabled", - default=False - ) - scene.muv_unwrapconst_u_const = BoolProperty( - name="U-Constraint", - description="Keep UV U-axis coordinate", - default=False - ) - scene.muv_unwrapconst_v_const = BoolProperty( - name="V-Constraint", - description="Keep UV V-axis coordinate", - default=False - ) - - # Preserve UV Aspect - scene.muv_preserve_uv_enabled = BoolProperty( - name="Preserve UV Aspect Enabled", - description="Preserve UV Aspect is enabled", - default=False - ) - scene.muv_preserve_uv_tex_image = EnumProperty( - name="Image", - description="Texture Image", - items=get_loaded_texture_name - ) - scene.muv_preserve_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" - ) - - # Flip/Rotate UV - scene.muv_fliprot_enabled = BoolProperty( - name="Flip/Rotate UV Enabled", - description="Flip/Rotate UV is enabled", - default=False - ) - scene.muv_fliprot_seams = BoolProperty( - name="Seams", - description="Seams", - default=True - ) - - # Mirror UV - scene.muv_mirroruv_enabled = BoolProperty( - name="Mirror UV Enabled", - description="Mirror UV is enabled", - default=False - ) - scene.muv_mirroruv_axis = EnumProperty( - items=[ - ('X', "X", "Mirror Along X axis"), - ('Y', "Y", "Mirror Along Y axis"), - ('Z', "Z", "Mirror Along Z axis") - ], - name="Axis", - description="Mirror Axis", - default='X' - ) - - # Copy/Paste UV - scene.muv_cpuv_enabled = BoolProperty( - name="Copy/Paste UV Enabled", - description="Copy/Paste UV is enabled", - default=False - ) - scene.muv_cpuv_copy_seams = BoolProperty( - name="Copy Seams", - description="Copy Seams", - default=True - ) - scene.muv_cpuv_mode = EnumProperty( - items=[ - ('DEFAULT', "Default", "Default Mode"), - ('SEL_SEQ', "Selection Sequence", "Selection Sequence Mode") - ], - name="Copy/Paste UV Mode", - description="Copy/Paste UV Mode", - default='DEFAULT' - ) - scene.muv_cpuv_strategy = EnumProperty( - name="Strategy", - description="Paste Strategy", - items=[ - ('N_N', 'N:N', 'Number of faces must be equal to source'), - ('N_M', 'N:M', 'Number of faces must not be equal to source') - ], - default='N_M' - ) - - # Transfer UV - scene.muv_transuv_enabled = BoolProperty( - name="Transfer UV Enabled", - description="Transfer UV is enabled", - default=False - ) - scene.muv_transuv_invert_normals = BoolProperty( - name="Invert Normals", - description="Invert Normals", - default=False - ) - scene.muv_transuv_copy_seams = BoolProperty( - name="Copy Seams", - description="Copy Seams", - default=True - ) - - # Align UV Cursor - def auvc_get_cursor_loc(self): - area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW', - 'IMAGE_EDITOR') - bd_size = common.get_uvimg_editor_board_size(area) - loc = space.cursor_location - if bd_size[0] < 0.000001: - cx = 0.0 - else: - cx = loc[0] / bd_size[0] - if bd_size[1] < 0.000001: - cy = 0.0 - else: - cy = loc[1] / bd_size[1] - self['muv_auvc_cursor_loc'] = Vector((cx, cy)) - return self.get('muv_auvc_cursor_loc', (0.0, 0.0)) - - def auvc_set_cursor_loc(self, value): - self['muv_auvc_cursor_loc'] = value - area, _, space = common.get_space('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] - space.cursor_location = Vector((cx, cy)) - - scene.muv_auvc_enabled = BoolProperty( - name="Align UV Cursor Enabled", - description="Align UV Cursor is enabled", - default=False - ) - scene.muv_auvc_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_auvc_align_menu = 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") - ] - ) - - # UV Cursor Location - scene.muv_uvcloc_enabled = BoolProperty( - name="UV Cursor Location Enabled", - description="UV Cursor Location is enabled", - default=False - ) + align_uv.Properties.init_props(scene) + align_uv_cursor.Properties.init_props(scene) + copy_paste_uv.Properties.init_props(scene) + copy_paste_uv_object.Properties.init_props(scene) + copy_paste_uv_uvedit.Properties.init_props(scene) + flip_rotate_uv.Properties.init_props(scene) + mirror_uv.Properties.init_props(scene) + move_uv.Properties.init_props(scene) + pack_uv.Properties.init_props(scene) + preserve_uv_aspect.Properties.init_props(scene) + select_uv.Properties.init_props(scene) + smooth_uv.Properties.init_props(scene) + texture_lock.Properties.init_props(scene) + texture_projection.Properties.init_props(scene) + texture_wrap.Properties.init_props(scene) + transfer_uv.Properties.init_props(scene) + unwrap_constraint.Properties.init_props(scene) + uv_bounding_box.Properties.init_props(scene) + uv_inspection.Properties.init_props(scene) + uv_sculpt.Properties.init_props(scene) + uvw.Properties.init_props(scene) + world_scale_uv.Properties.init_props(scene) def clear_props(scene): - del scene.muv_props - - # UV Sculpt - del scene.muv_uvsculpt_enabled - del scene.muv_uvsculpt_radius - del scene.muv_uvsculpt_strength - del scene.muv_uvsculpt_tools - del scene.muv_uvsculpt_show_brush - del scene.muv_uvsculpt_pinch_invert - del scene.muv_uvsculpt_relax_method - - # Texture Wrap - del scene.muv_texwrap_enabled - del scene.muv_texwrap_set_and_refer - del scene.muv_texwrap_selseq - - # UV Inspection - del scene.muv_seluv_enabled - del scene.muv_uvinsp_enabled - del scene.muv_uvinsp_show_overlapped - del scene.muv_uvinsp_show_flipped - del scene.muv_uvinsp_show_mode - - # Align UV - del scene.muv_auv_enabled - del scene.muv_auv_transmission - del scene.muv_auv_select - del scene.muv_auv_vertical - del scene.muv_auv_horizontal - del scene.muv_auv_location + align_uv.Properties.del_props(scene) + align_uv_cursor.Properties.del_props(scene) + copy_paste_uv.Properties.del_props(scene) + copy_paste_uv_object.Properties.del_props(scene) + copy_paste_uv_uvedit.Properties.del_props(scene) + flip_rotate_uv.Properties.del_props(scene) + mirror_uv.Properties.del_props(scene) + move_uv.Properties.del_props(scene) + pack_uv.Properties.del_props(scene) + preserve_uv_aspect.Properties.del_props(scene) + select_uv.Properties.del_props(scene) + smooth_uv.Properties.del_props(scene) + texture_lock.Properties.del_props(scene) + texture_projection.Properties.del_props(scene) + texture_wrap.Properties.del_props(scene) + transfer_uv.Properties.del_props(scene) + unwrap_constraint.Properties.del_props(scene) + uv_bounding_box.Properties.del_props(scene) + uv_inspection.Properties.del_props(scene) + uv_sculpt.Properties.del_props(scene) + uvw.Properties.del_props(scene) + world_scale_uv.Properties.del_props(scene) - # Smooth UV - del scene.muv_smuv_enabled - del scene.muv_smuv_transmission - del scene.muv_smuv_mesh_infl - del scene.muv_smuv_select - - # UV Bounding Box - del scene.muv_uvbb_enabled - del scene.muv_uvbb_uniform_scaling - del scene.muv_uvbb_boundary - - # Pack UV - del scene.muv_packuv_enabled - del scene.muv_packuv_allowable_center_deviation - del scene.muv_packuv_allowable_size_deviation - - # Move UV - del scene.muv_mvuv_enabled - - # UVW - del scene.muv_uvw_enabled - del scene.muv_uvw_assign_uvmap - - # Texture Projection - del scene.muv_texproj_enabled - del scene.muv_texproj_tex_magnitude - del scene.muv_texproj_tex_image - del scene.muv_texproj_tex_transparency - del scene.muv_texproj_adjust_window - del scene.muv_texproj_apply_tex_aspect - del scene.muv_texproj_assign_uvmap - - # Texture Lock - del scene.muv_texlock_enabled - del scene.muv_texlock_connect - - # World Scale UV - del scene.muv_wsuv_enabled - del scene.muv_wsuv_src_mesh_area - del scene.muv_wsuv_src_uv_area - del scene.muv_wsuv_src_density - del scene.muv_wsuv_tgt_density - del scene.muv_wsuv_mode - del scene.muv_wsuv_scaling_factor - del scene.muv_wsuv_origin - - # Unwrap Constraint - del scene.muv_unwrapconst_enabled - del scene.muv_unwrapconst_u_const - del scene.muv_unwrapconst_v_const - - # Preserve UV Aspect - del scene.muv_preserve_uv_enabled - del scene.muv_preserve_uv_tex_image - del scene.muv_preserve_uv_origin - - # Flip/Rotate UV - del scene.muv_fliprot_enabled - del scene.muv_fliprot_seams - - # Mirror UV - del scene.muv_mirroruv_enabled - del scene.muv_mirroruv_axis - - # Copy/Paste UV - del scene.muv_cpuv_enabled - del scene.muv_cpuv_copy_seams - del scene.muv_cpuv_mode - del scene.muv_cpuv_strategy - - # Transfer UV - del scene.muv_transuv_enabled - del scene.muv_transuv_invert_normals - del scene.muv_transuv_copy_seams - - # Align UV Cursor - del scene.muv_auvc_enabled - del scene.muv_auvc_cursor_loc - del scene.muv_auvc_align_menu - - # UV Cursor Location - del scene.muv_uvcloc_enabled + del scene.muv_props diff --git a/uv_magic_uv/ui/IMAGE_MT_uvs.py b/uv_magic_uv/ui/IMAGE_MT_uvs.py new file mode 100644 index 00000000..9beb7e2f --- /dev/null +++ b/uv_magic_uv/ui/IMAGE_MT_uvs.py @@ -0,0 +1,186 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bpy +from ..op import copy_paste_uv_uvedit +from ..op import align_uv +from ..op import uv_inspection +from ..op import align_uv_cursor +from ..op import select_uv + + +__all__ = [ + 'MenuCopyPasteUVUVEdit', + 'MenuAlignUV', + 'MenuSelectUV', + 'MenuAlignUVCursor', + 'MenuUVInspection', +] + + +class MenuCopyPasteUVUVEdit(bpy.types.Menu): + """ + Menu class: Master menu of Copy/Paste UV coordinate on UV/ImageEditor + """ + + bl_idname = "uv.muv_copy_paste_uv_uvedit_menu" + bl_label = "Copy/Paste UV" + bl_description = "Copy and Paste UV coordinate among object" + + def draw(self, _): + layout = self.layout + + layout.operator(copy_paste_uv_uvedit.OperatorCopyUV.bl_idname, + text="Copy") + layout.operator(copy_paste_uv_uvedit.OperatorPasteUV.bl_idname, + text="Paste") + + +class MenuAlignUV(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(align_uv.OperatorCircle.bl_idname, text="Circle") + ops.transmission = sc.muv_align_uv_transmission + ops.select = sc.muv_align_uv_select + + ops = layout.operator(align_uv.OperatorStraighten.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(align_uv.OperatorAxis.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 + + +class MenuSelectUV(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(select_uv.OperatorSelectOverlapped.bl_idname, + text="Overlapped") + layout.operator(select_uv.OperatorSelectFlipped.bl_idname, + text="Flipped") + + +class MenuAlignUVCursor(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(align_uv_cursor.Operator.bl_idname, + text="Left Top") + ops.position = 'LEFT_TOP' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(align_uv_cursor.Operator.bl_idname, + text="Middle Top") + ops.position = 'MIDDLE_TOP' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(align_uv_cursor.Operator.bl_idname, + text="Right Top") + ops.position = 'RIGHT_TOP' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(align_uv_cursor.Operator.bl_idname, + text="Left Middle") + ops.position = 'LEFT_MIDDLE' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(align_uv_cursor.Operator.bl_idname, + text="Center") + ops.position = 'CENTER' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(align_uv_cursor.Operator.bl_idname, + text="Right Middle") + ops.position = 'RIGHT_MIDDLE' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(align_uv_cursor.Operator.bl_idname, + text="Left Bottom") + ops.position = 'LEFT_BOTTOM' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(align_uv_cursor.Operator.bl_idname, + text="Middle Bottom") + ops.position = 'MIDDLE_BOTTOM' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(align_uv_cursor.Operator.bl_idname, + text="Right Bottom") + ops.position = 'RIGHT_BOTTOM' + ops.base = sc.muv_align_uv_cursor_align_method + + +class MenuUVInspection(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(uv_inspection.OperatorUpdate.bl_idname, + text="Update") diff --git a/uv_magic_uv/ui/VIEW3D_MT_object.py b/uv_magic_uv/ui/VIEW3D_MT_object.py new file mode 100644 index 00000000..c73157cc --- /dev/null +++ b/uv_magic_uv/ui/VIEW3D_MT_object.py @@ -0,0 +1,50 @@ +# + +# ##### 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 import copy_paste_uv_object + + +__all__ = [ + 'MenuCopyPasteUVObject', +] + + +class MenuCopyPasteUVObject(bpy.types.Menu): + """ + Menu class: Master menu of Copy/Paste UV coordinate among object + """ + + bl_idname = "uv.muv_copy_paste_uv_object_menu" + bl_label = "Copy/Paste UV" + bl_description = "Copy and Paste UV coordinate among object" + + def draw(self, _): + layout = self.layout + + layout.menu(copy_paste_uv_object.MenuCopyUV.bl_idname, + text="Copy") + layout.menu(copy_paste_uv_object.MenuPasteUV.bl_idname, + text="Paste") diff --git a/uv_magic_uv/ui/VIEW3D_MT_uv_map.py b/uv_magic_uv/ui/VIEW3D_MT_uv_map.py new file mode 100644 index 00000000..bb59c12c --- /dev/null +++ b/uv_magic_uv/ui/VIEW3D_MT_uv_map.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 bpy +from ..op import copy_paste_uv +from ..op import transfer_uv +from ..op import texture_lock +from ..op import world_scale_uv +from ..op import uvw +from ..op import texture_projection +from ..op import texture_wrap +from ..op import preserve_uv_aspect + + +__all__ = [ + 'MenuCopyPasteUV', + 'MenuTransferUV', + 'MenuTextureLock', + 'MenuWorldScaleUV', + 'MenuTextureWrap', + 'MenuUVW', + 'MenuTextureProjection', + 'MenuPreserveUVAspect', +] + + +class MenuCopyPasteUV(bpy.types.Menu): + """ + Menu class: Master menu of Copy/Paste UV coordinate + """ + + bl_idname = "uv.muv_copy_paste_uv_menu" + bl_label = "Copy/Paste UV" + bl_description = "Copy and Paste UV coordinate" + + def draw(self, _): + layout = self.layout + + layout.label("Default") + layout.menu(copy_paste_uv.MenuCopyUV.bl_idname, text="Copy") + layout.menu(copy_paste_uv.MenuPasteUV.bl_idname, text="Paste") + + layout.separator() + + layout.label("Selection Sequence") + layout.menu(copy_paste_uv.MenuSelSeqCopyUV.bl_idname, + text="Copy") + layout.menu(copy_paste_uv.MenuSelSeqPasteUV.bl_idname, + text="Paste") + + +class MenuTransferUV(bpy.types.Menu): + """ + Menu class: Master menu of Transfer UV coordinate + """ + + bl_idname = "uv.muv_transfer_uv_menu" + bl_label = "Transfer UV" + bl_description = "Transfer UV coordinate" + + def draw(self, context): + layout = self.layout + sc = context.scene + + layout.operator(transfer_uv.OperatorCopyUV.bl_idname, text="Copy") + ops = layout.operator(transfer_uv.OperatorPasteUV.bl_idname, + text="Paste") + ops.invert_normals = sc.muv_transfer_uv_invert_normals + ops.copy_seams = sc.muv_transfer_uv_copy_seams + + +class MenuTextureLock(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("Normal Mode") + layout.operator(texture_lock.OperatorLock.bl_idname, + text="Lock" + if not texture_lock.OperatorLock.is_ready(context) + else "ReLock") + ops = layout.operator(texture_lock.OperatorUnlock.bl_idname, + text="Unlock") + ops.connect = sc.muv_texture_lock_connect + + layout.separator() + + layout.label("Interactive Mode") + layout.prop(sc, "muv_texture_lock_lock", text="Lock") + + +class MenuWorldScaleUV(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(world_scale_uv.OperatorMeasure.bl_idname, + text="Measure") + + layout.operator(world_scale_uv.OperatorApplyManual.bl_idname, + text="Apply (Manual)") + + ops = layout.operator( + world_scale_uv.OperatorApplyScalingDensity.bl_idname, + text="Apply (Same Desity)") + ops.src_density = sc.muv_world_scale_uv_src_density + ops.same_density = True + + ops = layout.operator( + world_scale_uv.OperatorApplyScalingDensity.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( + world_scale_uv.OperatorApplyProportionalToMesh.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 + + +class MenuTextureWrap(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(texture_wrap.OperatorRefer.bl_idname, text="Refer") + layout.operator(texture_wrap.OperatorSet.bl_idname, text="Set") + + +class MenuUVW(bpy.types.Menu): + """ + Menu class: Master menu of UVW + """ + + bl_idname = "uv.muv_uvw_menu" + bl_label = "UVW" + bl_description = "" + + def draw(self, context): + layout = self.layout + sc = context.scene + + ops = layout.operator(uvw.OperatorBoxMap.bl_idname, text="Box") + ops.assign_uvmap = sc.muv_uvw_assign_uvmap + + ops = layout.operator(uvw.OperatorBestPlanerMap.bl_idname, + text="Best Planner") + ops.assign_uvmap = sc.muv_uvw_assign_uvmap + + +class MenuTextureProjection(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(texture_projection.OperatorProject.bl_idname, + text="Project") + + +class MenuPreserveUVAspect(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( + preserve_uv_aspect.Operator.bl_idname, text=key) + ops.dest_img_name = key + ops.origin = sc.muv_preserve_uv_aspect_origin diff --git a/uv_magic_uv/ui/__init__.py b/uv_magic_uv/ui/__init__.py index ad56aeb3..b377ed23 100644 --- a/uv_magic_uv/ui/__init__.py +++ b/uv_magic_uv/ui/__init__.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" if "bpy" in locals(): import importlib @@ -31,7 +31,10 @@ if "bpy" in locals(): importlib.reload(view3d_uv_mapping) importlib.reload(uvedit_copy_paste_uv) importlib.reload(uvedit_uv_manipulation) - importlib.reload(uvedit_editor_enhance) + importlib.reload(uvedit_editor_enhancement) + importlib.reload(VIEW3D_MT_uv_map) + importlib.reload(VIEW3D_MT_object) + importlib.reload(IMAGE_MT_uvs) else: from . import view3d_copy_paste_uv_objectmode from . import view3d_copy_paste_uv_editmode @@ -39,6 +42,9 @@ else: from . import view3d_uv_mapping from . import uvedit_copy_paste_uv from . import uvedit_uv_manipulation - from . import uvedit_editor_enhance + from . import uvedit_editor_enhancement + from . import VIEW3D_MT_uv_map + from . import VIEW3D_MT_object + from . import IMAGE_MT_uvs import bpy diff --git a/uv_magic_uv/ui/uvedit_copy_paste_uv.py b/uv_magic_uv/ui/uvedit_copy_paste_uv.py index d87dbef3..271277a0 100644 --- a/uv_magic_uv/ui/uvedit_copy_paste_uv.py +++ b/uv_magic_uv/ui/uvedit_copy_paste_uv.py @@ -20,15 +20,20 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy from ..op import copy_paste_uv_uvedit -class IMAGE_PT_MUV_CPUV(bpy.types.Panel): +__all__ = [ + 'PanelCopyPasteUV', +] + + +class PanelCopyPasteUV(bpy.types.Panel): """ Panel class: Copy/Paste UV on Property Panel on UV/ImageEditor """ @@ -48,7 +53,7 @@ class IMAGE_PT_MUV_CPUV(bpy.types.Panel): layout = self.layout row = layout.row(align=True) - row.operator(copy_paste_uv_uvedit.MUV_CPUVIECopyUV.bl_idname, + row.operator(copy_paste_uv_uvedit.OperatorCopyUV.bl_idname, text="Copy") - row.operator(copy_paste_uv_uvedit.MUV_CPUVIEPasteUV.bl_idname, + row.operator(copy_paste_uv_uvedit.OperatorPasteUV.bl_idname, text="Paste") diff --git a/uv_magic_uv/ui/uvedit_editor_enhance.py b/uv_magic_uv/ui/uvedit_editor_enhance.py deleted file mode 100644 index 88a2492c..00000000 --- a/uv_magic_uv/ui/uvedit_editor_enhance.py +++ /dev/null @@ -1,136 +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 ##### - -__author__ = "Nutti " -__status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" - -import bpy - -from ..op import align_uv_cursor -from ..op import uv_bounding_box -from ..op import uv_inspection - - -class IMAGE_PT_MUV_EE(bpy.types.Panel): - """ - Panel class: UV/Image Editor Enhancement - """ - - bl_space_type = 'IMAGE_EDITOR' - bl_region_type = 'TOOLS' - 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_COL') - - def draw(self, context): - layout = self.layout - sc = context.scene - props = sc.muv_props - - box = layout.box() - box.prop(sc, "muv_auvc_enabled", text="Align UV Cursor") - if sc.muv_auvc_enabled: - box.prop(sc, "muv_auvc_align_menu", expand=True) - - col = box.column(align=True) - - row = col.row(align=True) - ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname, - text="Left Top") - ops.position = 'LEFT_TOP' - ops.base = sc.muv_auvc_align_menu - ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname, - text="Middle Top") - ops.position = 'MIDDLE_TOP' - ops.base = sc.muv_auvc_align_menu - ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname, - text="Right Top") - ops.position = 'RIGHT_TOP' - ops.base = sc.muv_auvc_align_menu - - row = col.row(align=True) - ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname, - text="Left Middle") - ops.position = 'LEFT_MIDDLE' - ops.base = sc.muv_auvc_align_menu - ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname, - text="Center") - ops.position = 'CENTER' - ops.base = sc.muv_auvc_align_menu - ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname, - text="Right Middle") - ops.position = 'RIGHT_MIDDLE' - ops.base = sc.muv_auvc_align_menu - - row = col.row(align=True) - ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname, - text="Left Bottom") - ops.position = 'LEFT_BOTTOM' - ops.base = sc.muv_auvc_align_menu - ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname, - text="Middle Bottom") - ops.position = 'MIDDLE_BOTTOM' - ops.base = sc.muv_auvc_align_menu - ops = row.operator(align_uv_cursor.MUV_AUVCAlignOps.bl_idname, - text="Right Bottom") - ops.position = 'RIGHT_BOTTOM' - ops.base = sc.muv_auvc_align_menu - - box = layout.box() - box.prop(sc, "muv_uvcloc_enabled", text="UV Cursor Location") - if sc.muv_uvcloc_enabled: - box.prop(sc, "muv_auvc_cursor_loc", text="") - - box = layout.box() - box.prop(sc, "muv_uvbb_enabled", text="UV Bounding Box") - if sc.muv_uvbb_enabled: - if props.uvbb.running is False: - box.operator(uv_bounding_box.MUV_UVBBUpdater.bl_idname, - text="Display", icon='PLAY') - else: - box.operator(uv_bounding_box.MUV_UVBBUpdater.bl_idname, - text="Hide", icon='PAUSE') - box.prop(sc, "muv_uvbb_uniform_scaling", text="Uniform Scaling") - box.prop(sc, "muv_uvbb_boundary", text="Boundary") - - box = layout.box() - box.prop(sc, "muv_uvinsp_enabled", text="UV Inspection") - if sc.muv_uvinsp_enabled: - row = box.row() - if not sc.muv_props.uvinsp.display_running: - row.operator(uv_inspection.MUV_UVInspDisplay.bl_idname, - text="Display", icon='PLAY') - else: - row.operator(uv_inspection.MUV_UVInspDisplay.bl_idname, - text="Hide", icon='PAUSE') - row.operator(uv_inspection.MUV_UVInspUpdate.bl_idname, - text="Update") - row = box.row() - row.prop(sc, "muv_uvinsp_show_overlapped") - row.prop(sc, "muv_uvinsp_show_flipped") - row = box.row() - row.prop(sc, "muv_uvinsp_show_mode") 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..9cde0cad --- /dev/null +++ b/uv_magic_uv/ui/uvedit_editor_enhancement.py @@ -0,0 +1,144 @@ +# + +# ##### 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 import align_uv_cursor +from ..op import uv_bounding_box +from ..op import uv_inspection + + +__all__ = [ + 'PanelEditorEnhancement', +] + + +class PanelEditorEnhancement(bpy.types.Panel): + """ + Panel class: UV/Image Editor Enhancement + """ + + bl_space_type = 'IMAGE_EDITOR' + bl_region_type = 'TOOLS' + 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_COL') + + 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(align_uv_cursor.Operator.bl_idname, + text="Left Top") + ops.position = 'LEFT_TOP' + ops.base = sc.muv_align_uv_cursor_align_method + ops = row.operator(align_uv_cursor.Operator.bl_idname, + text="Middle Top") + ops.position = 'MIDDLE_TOP' + ops.base = sc.muv_align_uv_cursor_align_method + ops = row.operator(align_uv_cursor.Operator.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(align_uv_cursor.Operator.bl_idname, + text="Left Middle") + ops.position = 'LEFT_MIDDLE' + ops.base = sc.muv_align_uv_cursor_align_method + ops = row.operator(align_uv_cursor.Operator.bl_idname, + text="Center") + ops.position = 'CENTER' + ops.base = sc.muv_align_uv_cursor_align_method + ops = row.operator(align_uv_cursor.Operator.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(align_uv_cursor.Operator.bl_idname, + text="Left Bottom") + ops.position = 'LEFT_BOTTOM' + ops.base = sc.muv_align_uv_cursor_align_method + ops = row.operator(align_uv_cursor.Operator.bl_idname, + text="Middle Bottom") + ops.position = 'MIDDLE_BOTTOM' + ops.base = sc.muv_align_uv_cursor_align_method + ops = row.operator(align_uv_cursor.Operator.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 uv_bounding_box.Operator.is_running(context) + else "Show", + icon='RESTRICT_VIEW_OFF' + if uv_bounding_box.Operator.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 uv_inspection.OperatorRender.is_running(context) + else "Show", + icon='RESTRICT_VIEW_OFF' + if uv_inspection.OperatorRender.is_running(context) + else 'RESTRICT_VIEW_ON') + row.operator(uv_inspection.OperatorUpdate.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 index f391c4cb..0f6a105e 100644 --- a/uv_magic_uv/ui/uvedit_uv_manipulation.py +++ b/uv_magic_uv/ui/uvedit_uv_manipulation.py @@ -20,18 +20,23 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy -from ..op import uv_inspection from ..op import align_uv from ..op import smooth_uv from ..op import pack_uv +from ..op import select_uv -class IMAGE_PT_MUV_UVManip(bpy.types.Panel): +__all__ = [ + 'MenuUVManipulation', +] + + +class MenuUVManipulation(bpy.types.Panel): """ Panel class: UV Manipulation on Property Panel on UV/ImageEditor """ @@ -52,66 +57,70 @@ class IMAGE_PT_MUV_UVManip(bpy.types.Panel): layout = self.layout box = layout.box() - box.prop(sc, "muv_auv_enabled", text="Align UV") - if sc.muv_auv_enabled: + 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(align_uv.MUV_AUVCircle.bl_idname, text="Circle") - ops.transmission = sc.muv_auv_transmission - ops.select = sc.muv_auv_select - ops = row.operator(align_uv.MUV_AUVStraighten.bl_idname, + ops = row.operator(align_uv.OperatorCircle.bl_idname, + text="Circle") + ops.transmission = sc.muv_align_uv_transmission + ops.select = sc.muv_align_uv_select + ops = row.operator(align_uv.OperatorStraighten.bl_idname, text="Straighten") - ops.transmission = sc.muv_auv_transmission - ops.select = sc.muv_auv_select - ops.vertical = sc.muv_auv_vertical - ops.horizontal = sc.muv_auv_horizontal + 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(align_uv.MUV_AUVAxis.bl_idname, text="XY-axis") - ops.transmission = sc.muv_auv_transmission - ops.select = sc.muv_auv_select - ops.vertical = sc.muv_auv_vertical - ops.horizontal = sc.muv_auv_horizontal - ops.location = sc.muv_auv_location - row.prop(sc, "muv_auv_location", text="") + ops = row.operator(align_uv.OperatorAxis.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_auv_transmission", text="Transmission") - row.prop(sc, "muv_auv_select", text="Select") + 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_auv_vertical", text="Vertical") - row.prop(sc, "muv_auv_horizontal", text="Horizontal") + 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_smuv_enabled", text="Smooth UV") - if sc.muv_smuv_enabled: - ops = box.operator(smooth_uv.MUV_AUVSmooth.bl_idname, + box.prop(sc, "muv_smooth_uv_enabled", text="Smooth UV") + if sc.muv_smooth_uv_enabled: + ops = box.operator(smooth_uv.Operator.bl_idname, text="Smooth") - ops.transmission = sc.muv_smuv_transmission - ops.select = sc.muv_smuv_select - ops.mesh_infl = sc.muv_smuv_mesh_infl + 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_smuv_transmission", text="Transmission") - row.prop(sc, "muv_smuv_select", text="Select") - col.prop(sc, "muv_smuv_mesh_infl", text="Mesh Influence") + 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_seluv_enabled", text="Select UV") - if sc.muv_seluv_enabled: + box.prop(sc, "muv_select_uv_enabled", text="Select UV") + if sc.muv_select_uv_enabled: row = box.row(align=True) - row.operator(uv_inspection.MUV_UVInspSelectOverlapped.bl_idname) - row.operator(uv_inspection.MUV_UVInspSelectFlipped.bl_idname) + row.operator(select_uv.OperatorSelectOverlapped.bl_idname) + row.operator(select_uv.OperatorSelectFlipped.bl_idname) box = layout.box() - box.prop(sc, "muv_packuv_enabled", text="Pack UV (Extension)") - if sc.muv_packuv_enabled: - ops = box.operator(pack_uv.MUV_PackUV.bl_idname, text="Pack UV") + box.prop(sc, "muv_pack_uv_enabled", text="Pack UV (Extension)") + if sc.muv_pack_uv_enabled: + ops = box.operator(pack_uv.Operator.bl_idname, text="Pack UV") ops.allowable_center_deviation = \ - sc.muv_packuv_allowable_center_deviation + sc.muv_pack_uv_allowable_center_deviation ops.allowable_size_deviation = \ - sc.muv_packuv_allowable_size_deviation + sc.muv_pack_uv_allowable_size_deviation box.label("Allowable Center Deviation:") - box.prop(sc, "muv_packuv_allowable_center_deviation", text="") + box.prop(sc, "muv_pack_uv_allowable_center_deviation", text="") box.label("Allowable Size Deviation:") - box.prop(sc, "muv_packuv_allowable_size_deviation", text="") + box.prop(sc, "muv_pack_uv_allowable_size_deviation", text="") diff --git a/uv_magic_uv/ui/view3d_copy_paste_uv_editmode.py b/uv_magic_uv/ui/view3d_copy_paste_uv_editmode.py index a22adf03..c5906ca0 100644 --- a/uv_magic_uv/ui/view3d_copy_paste_uv_editmode.py +++ b/uv_magic_uv/ui/view3d_copy_paste_uv_editmode.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy @@ -29,7 +29,12 @@ from ..op import copy_paste_uv from ..op import transfer_uv -class OBJECT_PT_MUV_CPUV(bpy.types.Panel): +__all__ = [ + 'PanelCopyPasteUVEditMode', +] + + +class PanelCopyPasteUVEditMode(bpy.types.Panel): """ Panel class: Copy/Paste UV on Property Panel on View3D """ @@ -50,32 +55,33 @@ class OBJECT_PT_MUV_CPUV(bpy.types.Panel): layout = self.layout box = layout.box() - box.prop(sc, "muv_cpuv_enabled", text="Copy/Paste UV") - if sc.muv_cpuv_enabled: + box.prop(sc, "muv_copy_paste_uv_enabled", text="Copy/Paste UV") + if sc.muv_copy_paste_uv_enabled: row = box.row(align=True) - if sc.muv_cpuv_mode == 'DEFAULT': - row.menu(copy_paste_uv.MUV_CPUVCopyUVMenu.bl_idname, + if sc.muv_copy_paste_uv_mode == 'DEFAULT': + row.menu(copy_paste_uv.MenuCopyUV.bl_idname, text="Copy") - row.menu(copy_paste_uv.MUV_CPUVPasteUVMenu.bl_idname, + row.menu(copy_paste_uv.MenuPasteUV.bl_idname, text="Paste") - elif sc.muv_cpuv_mode == 'SEL_SEQ': - row.menu(copy_paste_uv.MUV_CPUVSelSeqCopyUVMenu.bl_idname, + elif sc.muv_copy_paste_uv_mode == 'SEL_SEQ': + row.menu(copy_paste_uv.MenuSelSeqCopyUV.bl_idname, text="Copy") - row.menu(copy_paste_uv.MUV_CPUVSelSeqPasteUVMenu.bl_idname, + row.menu(copy_paste_uv.MenuSelSeqPasteUV.bl_idname, text="Paste") - box.prop(sc, "muv_cpuv_mode", expand=True) - box.prop(sc, "muv_cpuv_copy_seams", text="Seams") - box.prop(sc, "muv_cpuv_strategy", text="Strategy") + box.prop(sc, "muv_copy_paste_uv_mode", expand=True) + box.prop(sc, "muv_copy_paste_uv_copy_seams", text="Seams") + box.prop(sc, "muv_copy_paste_uv_strategy", text="Strategy") box = layout.box() - box.prop(sc, "muv_transuv_enabled", text="Transfer UV") - if sc.muv_transuv_enabled: + box.prop(sc, "muv_transfer_uv_enabled", text="Transfer UV") + if sc.muv_transfer_uv_enabled: row = box.row(align=True) - row.operator(transfer_uv.MUV_TransUVCopy.bl_idname, text="Copy") - ops = row.operator(transfer_uv.MUV_TransUVPaste.bl_idname, + row.operator(transfer_uv.OperatorCopyUV.bl_idname, text="Copy") + ops = row.operator(transfer_uv.OperatorPasteUV.bl_idname, text="Paste") - ops.invert_normals = sc.muv_transuv_invert_normals - ops.copy_seams = sc.muv_transuv_copy_seams + ops.invert_normals = sc.muv_transfer_uv_invert_normals + ops.copy_seams = sc.muv_transfer_uv_copy_seams row = box.row() - row.prop(sc, "muv_transuv_invert_normals", text="Invert Normals") - row.prop(sc, "muv_transuv_copy_seams", text="Seams") + row.prop(sc, "muv_transfer_uv_invert_normals", + text="Invert Normals") + row.prop(sc, "muv_transfer_uv_copy_seams", text="Seams") diff --git a/uv_magic_uv/ui/view3d_copy_paste_uv_objectmode.py b/uv_magic_uv/ui/view3d_copy_paste_uv_objectmode.py index f9e2bec0..a9203d87 100644 --- a/uv_magic_uv/ui/view3d_copy_paste_uv_objectmode.py +++ b/uv_magic_uv/ui/view3d_copy_paste_uv_objectmode.py @@ -20,15 +20,20 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy from ..op import copy_paste_uv_object -class OBJECT_PT_MUV_CPUVObj(bpy.types.Panel): +__all__ = [ + 'PanelCopyPasteUVObjectMode', +] + + +class PanelCopyPasteUVObjectMode(bpy.types.Panel): """ Panel class: Copy/Paste UV on Property Panel on View3D """ @@ -49,8 +54,9 @@ class OBJECT_PT_MUV_CPUVObj(bpy.types.Panel): layout = self.layout row = layout.row(align=True) - row.menu(copy_paste_uv_object.MUV_CPUVObjCopyUVMenu.bl_idname, + row.menu(copy_paste_uv_object.MenuCopyUV.bl_idname, text="Copy") - row.menu(copy_paste_uv_object.MUV_CPUVObjPasteUVMenu.bl_idname, + row.menu(copy_paste_uv_object.MenuPasteUV.bl_idname, text="Paste") - layout.prop(sc, "muv_cpuv_copy_seams", text="Copy Seams") + layout.prop(sc, "muv_copy_paste_uv_object_copy_seams", + text="Seams") diff --git a/uv_magic_uv/ui/view3d_uv_manipulation.py b/uv_magic_uv/ui/view3d_uv_manipulation.py index 1e9b7d7e..be0bcf57 100644 --- a/uv_magic_uv/ui/view3d_uv_manipulation.py +++ b/uv_magic_uv/ui/view3d_uv_manipulation.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy @@ -35,7 +35,12 @@ from ..op import uv_sculpt from ..op import world_scale_uv -class OBJECT_PT_MUV_UVManip(bpy.types.Panel): +__all__ = [ + 'PanelUVManipulation', +] + + +class PanelUVManipulation(bpy.types.Panel): """ Panel class: UV Manipulation on Property Panel on View3D """ @@ -53,128 +58,214 @@ class OBJECT_PT_MUV_UVManip(bpy.types.Panel): def draw(self, context): sc = context.scene - props = sc.muv_props layout = self.layout box = layout.box() - box.prop(sc, "muv_fliprot_enabled", text="Flip/Rotate UV") - if sc.muv_fliprot_enabled: + 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_FlipRot.bl_idname, + ops = row.operator(flip_rotate_uv.Operator.bl_idname, text="Flip/Rotate") - ops.seams = sc.muv_fliprot_seams - row.prop(sc, "muv_fliprot_seams", text="Seams") + ops.seams = sc.muv_flip_rotate_uv_seams + row.prop(sc, "muv_flip_rotate_uv_seams", text="Seams") box = layout.box() - box.prop(sc, "muv_mirroruv_enabled", text="Mirror UV") - if sc.muv_mirroruv_enabled: + 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_MirrorUV.bl_idname, text="Mirror") - ops.axis = sc.muv_mirroruv_axis - row.prop(sc, "muv_mirroruv_axis", text="") + ops = row.operator(mirror_uv.Operator.bl_idname, text="Mirror") + ops.axis = sc.muv_mirror_uv_axis + row.prop(sc, "muv_mirror_uv_axis", text="") box = layout.box() - box.prop(sc, "muv_mvuv_enabled", text="Move UV") - if sc.muv_mvuv_enabled: + box.prop(sc, "muv_move_uv_enabled", text="Move UV") + if sc.muv_move_uv_enabled: col = box.column() - col.operator(move_uv.MUV_MVUV.bl_idname, icon='PLAY', text="Start") - if props.mvuv.running: - col.enabled = False + if not move_uv.Operator.is_running(context): + col.operator(move_uv.Operator.bl_idname, icon='PLAY', + text="Start") else: - col.enabled = True + col.operator(move_uv.Operator.bl_idname, icon='PAUSE', + text="Stop") box = layout.box() - box.prop(sc, "muv_wsuv_enabled", text="World Scale UV") - if sc.muv_wsuv_enabled: - row = box.row(align=True) - row.operator(world_scale_uv.MUV_WSUVMeasure.bl_idname, - text="Measure") - ops = row.operator(world_scale_uv.MUV_WSUVApply.bl_idname, - text="Apply") - ops.origin = sc.muv_wsuv_origin - box.label("Source:") - sp = box.split(percentage=0.7) - col = sp.column(align=True) - col.prop(sc, "muv_wsuv_src_mesh_area", text="Mesh Area") - col.prop(sc, "muv_wsuv_src_uv_area", text="UV Area") - col.prop(sc, "muv_wsuv_src_density", text="Density") - col.enabled = False - sp = sp.split(percentage=1.0) - col = sp.column(align=True) - col.label("cm x cm") - col.label("px x px") - col.label("px/cm") - col.enabled = False - sp = box.split(percentage=0.3) - sp.label("Mode:") - sp = sp.split(percentage=1.0) - col = sp.column() - col.prop(sc, "muv_wsuv_mode", text="") - if sc.muv_wsuv_mode == 'USER': - col.prop(sc, "muv_wsuv_tgt_density", text="Density") - if sc.muv_wsuv_mode == 'SCALING': - col.prop(sc, "muv_wsuv_scaling_factor", text="Scaling Factor") - box.prop(sc, "muv_wsuv_origin", text="Origin") + 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(percentage=0.5) + col = sp.column() + col.prop(sc, "muv_world_scale_uv_tgt_texture_size", + text="Texture Size") + sp = sp.split(percentage=1.0) + col = sp.column() + col.label("Density:") + col.prop(sc, "muv_world_scale_uv_tgt_density", text="") + box.prop(sc, "muv_world_scale_uv_origin", text="Origin") + ops = box.operator( + world_scale_uv.OperatorApplyManual.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(percentage=0.4) + col = sp.column(align=True) + col.label("Source:") + sp = sp.split(percentage=1.0) + col = sp.column(align=True) + col.operator(world_scale_uv.OperatorMeasure.bl_idname, + text="Measure") + + sp = box.split(percentage=0.7) + col = sp.column(align=True) + col.prop(sc, "muv_world_scale_uv_src_density", text="Density") + col.enabled = False + sp = sp.split(percentage=1.0) + col = sp.column(align=True) + col.label("px2/cm2") + + box.separator() + box.prop(sc, "muv_world_scale_uv_origin", text="Origin") + ops = box.operator( + world_scale_uv.OperatorApplyScalingDensity.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(percentage=0.4) + col = sp.column(align=True) + col.label("Source:") + sp = sp.split(percentage=1.0) + col = sp.column(align=True) + col.operator(world_scale_uv.OperatorMeasure.bl_idname, + text="Measure") + + sp = box.split(percentage=0.7) + col = sp.column(align=True) + col.prop(sc, "muv_world_scale_uv_src_density", text="Density") + col.enabled = False + sp = sp.split(percentage=1.0) + col = sp.column(align=True) + col.label("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( + world_scale_uv.OperatorApplyScalingDensity.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(percentage=0.4) + col = sp.column(align=True) + col.label("Source:") + sp = sp.split(percentage=1.0) + col = sp.column(align=True) + col.operator(world_scale_uv.OperatorMeasure.bl_idname, + text="Measure") + + sp = box.split(percentage=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(percentage=1.0) + col = sp.column(align=True) + col.label("cm2") + col.label("px2") + col.label("px2/cm2") + col.enabled = False + + box.separator() + box.prop(sc, "muv_world_scale_uv_origin", text="Origin") + ops = box.operator( + world_scale_uv.OperatorApplyProportionalToMesh.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_enabled", text="Preserve UV Aspect") - if sc.muv_preserve_uv_enabled: + 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( - preserve_uv_aspect.MUV_PreserveUVAspect.bl_idname, + preserve_uv_aspect.Operator.bl_idname, text="Change Image") - ops.dest_img_name = sc.muv_preserve_uv_tex_image - ops.origin = sc.muv_preserve_uv_origin - row.prop(sc, "muv_preserve_uv_tex_image", text="") - box.prop(sc, "muv_preserve_uv_origin", text="Origin") + 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_texlock_enabled", text="Texture Lock") - if sc.muv_texlock_enabled: + 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("Normal Mode:") col = row.column(align=True) - col.operator(texture_lock.MUV_TexLockStart.bl_idname, text="Lock") - ops = col.operator(texture_lock.MUV_TexLockStop.bl_idname, + col.operator(texture_lock.OperatorLock.bl_idname, + text="Lock" + if not texture_lock.OperatorLock.is_ready(context) + else "ReLock") + ops = col.operator(texture_lock.OperatorUnlock.bl_idname, text="Unlock") - ops.connect = sc.muv_texlock_connect - col.prop(sc, "muv_texlock_connect", text="Connect") + ops.connect = sc.muv_texture_lock_connect + col.prop(sc, "muv_texture_lock_connect", text="Connect") row = box.row(align=True) row.label("Interactive Mode:") - if not props.texlock.intr_running: - row.operator(texture_lock.MUV_TexLockIntrStart.bl_idname, - icon='PLAY', text="Start") - else: - row.operator(texture_lock.MUV_TexLockIntrStop.bl_idname, - icon="PAUSE", text="Stop") + box.prop(sc, "muv_texture_lock_lock", + text="Unlock" + if texture_lock.OperatorIntr.is_running(context) + else "Lock", + icon='RESTRICT_VIEW_OFF' + if texture_lock.OperatorIntr.is_running(context) + else 'RESTRICT_VIEW_ON') box = layout.box() - box.prop(sc, "muv_texwrap_enabled", text="Texture Wrap") - if sc.muv_texwrap_enabled: + box.prop(sc, "muv_texture_wrap_enabled", text="Texture Wrap") + if sc.muv_texture_wrap_enabled: row = box.row(align=True) - row.operator(texture_wrap.MUV_TexWrapRefer.bl_idname, text="Refer") - row.operator(texture_wrap.MUV_TexWrapSet.bl_idname, text="Set") - box.prop(sc, "muv_texwrap_set_and_refer") - box.prop(sc, "muv_texwrap_selseq") + row.operator(texture_wrap.OperatorRefer.bl_idname, text="Refer") + row.operator(texture_wrap.OperatorSet.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_uvsculpt_enabled", text="UV Sculpt") - if sc.muv_uvsculpt_enabled: - if not props.uvsculpt.running: - box.operator(uv_sculpt.MUV_UVSculptOps.bl_idname, - icon='PLAY', text="Start") - else: - box.operator(uv_sculpt.MUV_UVSculptOps.bl_idname, - icon='PAUSE', text="Stop") + 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 uv_sculpt.Operator.is_running(context) + else "Enable", + icon='RESTRICT_VIEW_OFF' + if uv_sculpt.Operator.is_running(context) + else 'RESTRICT_VIEW_ON') col = box.column() col.label("Brush:") - col.prop(sc, "muv_uvsculpt_radius") - col.prop(sc, "muv_uvsculpt_strength") - box.prop(sc, "muv_uvsculpt_tools") - if sc.muv_uvsculpt_tools == 'PINCH': - box.prop(sc, "muv_uvsculpt_pinch_invert") - elif sc.muv_uvsculpt_tools == 'RELAX': - box.prop(sc, "muv_uvsculpt_relax_method") - box.prop(sc, "muv_uvsculpt_show_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 2dc241c0..2aa62c26 100644 --- a/uv_magic_uv/ui/view3d_uv_mapping.py +++ b/uv_magic_uv/ui/view3d_uv_mapping.py @@ -20,8 +20,8 @@ __author__ = "Nutti " __status__ = "production" -__version__ = "5.1" -__date__ = "24 Feb 2018" +__version__ = "5.2" +__date__ = "17 Nov 2018" import bpy @@ -30,7 +30,12 @@ from ..op import unwrap_constraint from ..op import uvw -class OBJECT_PT_MUV_UVMapping(bpy.types.Panel): +__all__ = [ + 'UVMapping', +] + + +class UVMapping(bpy.types.Panel): """ Panel class: UV Mapping on Property Panel on View3D """ @@ -48,52 +53,56 @@ class OBJECT_PT_MUV_UVMapping(bpy.types.Panel): def draw(self, context): sc = context.scene - props = sc.muv_props layout = self.layout box = layout.box() - box.prop(sc, "muv_unwrapconst_enabled", text="Unwrap Constraint") - if sc.muv_unwrapconst_enabled: + box.prop(sc, "muv_unwrap_constraint_enabled", text="Unwrap Constraint") + if sc.muv_unwrap_constraint_enabled: ops = box.operator( - unwrap_constraint.MUV_UnwrapConstraint.bl_idname, + unwrap_constraint.Operator.bl_idname, text="Unwrap") - ops.u_const = sc.muv_unwrapconst_u_const - ops.v_const = sc.muv_unwrapconst_v_const + 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_unwrapconst_u_const", text="U-Constraint") - row.prop(sc, "muv_unwrapconst_v_const", text="V-Constraint") + 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_texproj_enabled", text="Texture Projection") - if sc.muv_texproj_enabled: + box.prop(sc, "muv_texture_projection_enabled", + text="Texture Projection") + if sc.muv_texture_projection_enabled: row = box.row() - if not props.texproj.running: - row.operator(texture_projection.MUV_TexProjStart.bl_idname, - text="Start", icon='PLAY') - else: - row.operator(texture_projection.MUV_TexProjStop.bl_idname, - text="Stop", icon='PAUSE') - row.prop(sc, "muv_texproj_tex_image", text="") - box.prop(sc, "muv_texproj_tex_transparency", text="Transparency") + row.prop(sc, "muv_texture_projection_enable", + text="Disable" + if texture_projection.Operator.is_running(context) + else "Enable", + icon='RESTRICT_VIEW_OFF' + if texture_projection.Operator.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_texproj_adjust_window", text="Adjust Window") - if not sc.muv_texproj_adjust_window: - row.prop(sc, "muv_texproj_tex_magnitude", text="Magnitude") - col.prop(sc, "muv_texproj_apply_tex_aspect", + 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_texproj_assign_uvmap", text="Assign UVMap") - if props.texproj.running: - box.operator(texture_projection.MUV_TexProjProject.bl_idname, - text="Project") + col.prop(sc, "muv_texture_projection_assign_uvmap", + text="Assign UVMap") + box.operator(texture_projection.OperatorProject.bl_idname, + text="Project") box = layout.box() box.prop(sc, "muv_uvw_enabled", text="UVW") if sc.muv_uvw_enabled: row = box.row(align=True) - ops = row.operator(uvw.MUV_UVWBoxMap.bl_idname, text="Box") + ops = row.operator(uvw.OperatorBoxMap.bl_idname, text="Box") ops.assign_uvmap = sc.muv_uvw_assign_uvmap - ops = row.operator(uvw.MUV_UVWBestPlanerMap.bl_idname, + ops = row.operator(uvw.OperatorBestPlanerMap.bl_idname, text="Best Planner") ops.assign_uvmap = sc.muv_uvw_assign_uvmap box.prop(sc, "muv_uvw_assign_uvmap", text="Assign UVMap") -- cgit v1.2.3