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 From 6f0128c332290c6f3639d4d949d3e06bfaa71022 Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Wed, 21 Nov 2018 13:30:11 +1100 Subject: Fix T57964: Correct spelling mistakes in ANT Landscape presets This fixes the spelling mistakes that were present in the presets panel of ANT landscape. Spelling mistakes include: Canion -> Canyon Canions -> Canyons Cristaline -> Crystalline Vulcano -> Volcano Please tell me if I have made any mistakes, I am relatively new to using this website. Thanks :) Reviewers: campbellbarton Tags: #addons Differential Revision: https://developer.blender.org/D3971 --- presets/operator/mesh.landscape_add/canion.py | 73 ---------------------- presets/operator/mesh.landscape_add/canions.py | 73 ---------------------- presets/operator/mesh.landscape_add/canyon.py | 73 ++++++++++++++++++++++ presets/operator/mesh.landscape_add/canyons.py | 73 ++++++++++++++++++++++ presets/operator/mesh.landscape_add/cristaline.py | 73 ---------------------- presets/operator/mesh.landscape_add/crystalline.py | 73 ++++++++++++++++++++++ presets/operator/mesh.landscape_add/volcano.py | 73 ++++++++++++++++++++++ presets/operator/mesh.landscape_add/vulcano.py | 73 ---------------------- 8 files changed, 292 insertions(+), 292 deletions(-) delete mode 100644 presets/operator/mesh.landscape_add/canion.py delete mode 100644 presets/operator/mesh.landscape_add/canions.py create mode 100644 presets/operator/mesh.landscape_add/canyon.py create mode 100644 presets/operator/mesh.landscape_add/canyons.py delete mode 100644 presets/operator/mesh.landscape_add/cristaline.py create mode 100644 presets/operator/mesh.landscape_add/crystalline.py create mode 100644 presets/operator/mesh.landscape_add/volcano.py delete mode 100644 presets/operator/mesh.landscape_add/vulcano.py diff --git a/presets/operator/mesh.landscape_add/canion.py b/presets/operator/mesh.landscape_add/canion.py deleted file mode 100644 index 1d63470c..00000000 --- a/presets/operator/mesh.landscape_add/canion.py +++ /dev/null @@ -1,73 +0,0 @@ -import bpy -op = bpy.context.active_operator - -op.ant_terrain_name = 'Landscape' -op.land_material = '' -op.water_material = '' -op.texture_block = '' -op.at_cursor = True -op.smooth_mesh = True -op.tri_face = False -op.sphere_mesh = False -op.subdivision_x = 200 -op.subdivision_y = 200 -op.mesh_size = 2.0 -op.mesh_size_x = 2.0 -op.mesh_size_y = 2.0 -op.random_seed = 5 -op.noise_offset_x = 0.0 -op.noise_offset_y = -0.25 -op.noise_offset_z = 0.0 -op.noise_size_x = 1.0 -op.noise_size_y = 1.25 -op.noise_size_z = 1.0 -op.noise_size = 1.5 -op.noise_type = 'marble_noise' -op.basis_type = '0' -op.vl_basis_type = '0' -op.distortion = 2.0 -op.hard_noise = '1' -op.noise_depth = 12 -op.amplitude = 0.5 -op.frequency = 2.0 -op.dimension = 1.0 -op.lacunarity = 2.0 -op.offset = 1.0 -op.gain = 1.0 -op.marble_bias = '0' -op.marble_sharp = '0' -op.marble_shape = '4' -op.height = 0.6000000238418579 -op.height_invert = False -op.height_offset = 0.0 -op.fx_mixfactor = 0.0 -op.fx_mix_mode = '8' -op.fx_type = '20' -op.fx_bias = '0' -op.fx_turb = 0.0 -op.fx_depth = 3 -op.fx_amplitude = 0.5 -op.fx_frequency = 1.649999976158142 -op.fx_size = 1.5 -op.fx_loc_x = 3.0 -op.fx_loc_y = 2.0 -op.fx_height = 0.25 -op.fx_invert = False -op.fx_offset = 0.05000000074505806 -op.edge_falloff = '2' -op.falloff_x = 4.0 -op.falloff_y = 4.0 -op.edge_level = 0.15000000596046448 -op.maximum = 0.5 -op.minimum = -0.20000000298023224 -op.vert_group = '' -op.strata = 6.0 -op.strata_type = '2' -op.water_plane = False -op.water_level = 0.009999999776482582 -op.remove_double = False -op.show_main_settings = True -op.show_noise_settings = True -op.show_displace_settings = True -op.refresh = True -op.auto_refresh = True diff --git a/presets/operator/mesh.landscape_add/canions.py b/presets/operator/mesh.landscape_add/canions.py deleted file mode 100644 index cc0402cb..00000000 --- a/presets/operator/mesh.landscape_add/canions.py +++ /dev/null @@ -1,73 +0,0 @@ -import bpy -op = bpy.context.active_operator - -op.ant_terrain_name = 'Landscape' -op.land_material = '' -op.water_material = '' -op.texture_block = '' -op.at_cursor = True -op.smooth_mesh = True -op.tri_face = False -op.sphere_mesh = False -op.subdivision_x = 128 -op.subdivision_y = 128 -op.mesh_size = 2.0 -op.mesh_size_x = 2.0 -op.mesh_size_y = 2.0 -op.random_seed = 600 -op.noise_offset_x = 0.0 -op.noise_offset_y = 0.0 -op.noise_offset_z = 0.0 -op.noise_size_x = 1.0 -op.noise_size_y = 1.0 -op.noise_size_z = 1.0 -op.noise_size = 0.5 -op.noise_type = 'hetero_terrain' -op.basis_type = '2' -op.vl_basis_type = '9' -op.distortion = 1.0 -op.hard_noise = '0' -op.noise_depth = 8 -op.amplitude = 0.5 -op.frequency = 2.0 -op.dimension = 1.0899999141693115 -op.lacunarity = 1.8599998950958252 -op.offset = 0.7700000405311584 -op.gain = 2.0 -op.marble_bias = '1' -op.marble_sharp = '0' -op.marble_shape = '7' -op.height = 0.5 -op.height_invert = False -op.height_offset = -0.0 -op.fx_mixfactor = 0.0 -op.fx_mix_mode = '0' -op.fx_type = '0' -op.fx_bias = '0' -op.fx_turb = 0.0 -op.fx_depth = 0 -op.fx_amplitude = 0.5 -op.fx_frequency = 2.0 -op.fx_size = 1.0 -op.fx_loc_x = 0.0 -op.fx_loc_y = 0.0 -op.fx_height = 0.5 -op.fx_invert = False -op.fx_offset = 0.0 -op.edge_falloff = '3' -op.falloff_x = 8.0 -op.falloff_y = 8.0 -op.edge_level = 0.0 -op.maximum = 0.5 -op.minimum = -0.5 -op.vert_group = '' -op.strata = 2.0 -op.strata_type = '2' -op.water_plane = False -op.water_level = 0.009999999776482582 -op.remove_double = False -op.show_main_settings = True -op.show_noise_settings = True -op.show_displace_settings = True -op.refresh = True -op.auto_refresh = True diff --git a/presets/operator/mesh.landscape_add/canyon.py b/presets/operator/mesh.landscape_add/canyon.py new file mode 100644 index 00000000..1d63470c --- /dev/null +++ b/presets/operator/mesh.landscape_add/canyon.py @@ -0,0 +1,73 @@ +import bpy +op = bpy.context.active_operator + +op.ant_terrain_name = 'Landscape' +op.land_material = '' +op.water_material = '' +op.texture_block = '' +op.at_cursor = True +op.smooth_mesh = True +op.tri_face = False +op.sphere_mesh = False +op.subdivision_x = 200 +op.subdivision_y = 200 +op.mesh_size = 2.0 +op.mesh_size_x = 2.0 +op.mesh_size_y = 2.0 +op.random_seed = 5 +op.noise_offset_x = 0.0 +op.noise_offset_y = -0.25 +op.noise_offset_z = 0.0 +op.noise_size_x = 1.0 +op.noise_size_y = 1.25 +op.noise_size_z = 1.0 +op.noise_size = 1.5 +op.noise_type = 'marble_noise' +op.basis_type = '0' +op.vl_basis_type = '0' +op.distortion = 2.0 +op.hard_noise = '1' +op.noise_depth = 12 +op.amplitude = 0.5 +op.frequency = 2.0 +op.dimension = 1.0 +op.lacunarity = 2.0 +op.offset = 1.0 +op.gain = 1.0 +op.marble_bias = '0' +op.marble_sharp = '0' +op.marble_shape = '4' +op.height = 0.6000000238418579 +op.height_invert = False +op.height_offset = 0.0 +op.fx_mixfactor = 0.0 +op.fx_mix_mode = '8' +op.fx_type = '20' +op.fx_bias = '0' +op.fx_turb = 0.0 +op.fx_depth = 3 +op.fx_amplitude = 0.5 +op.fx_frequency = 1.649999976158142 +op.fx_size = 1.5 +op.fx_loc_x = 3.0 +op.fx_loc_y = 2.0 +op.fx_height = 0.25 +op.fx_invert = False +op.fx_offset = 0.05000000074505806 +op.edge_falloff = '2' +op.falloff_x = 4.0 +op.falloff_y = 4.0 +op.edge_level = 0.15000000596046448 +op.maximum = 0.5 +op.minimum = -0.20000000298023224 +op.vert_group = '' +op.strata = 6.0 +op.strata_type = '2' +op.water_plane = False +op.water_level = 0.009999999776482582 +op.remove_double = False +op.show_main_settings = True +op.show_noise_settings = True +op.show_displace_settings = True +op.refresh = True +op.auto_refresh = True diff --git a/presets/operator/mesh.landscape_add/canyons.py b/presets/operator/mesh.landscape_add/canyons.py new file mode 100644 index 00000000..cc0402cb --- /dev/null +++ b/presets/operator/mesh.landscape_add/canyons.py @@ -0,0 +1,73 @@ +import bpy +op = bpy.context.active_operator + +op.ant_terrain_name = 'Landscape' +op.land_material = '' +op.water_material = '' +op.texture_block = '' +op.at_cursor = True +op.smooth_mesh = True +op.tri_face = False +op.sphere_mesh = False +op.subdivision_x = 128 +op.subdivision_y = 128 +op.mesh_size = 2.0 +op.mesh_size_x = 2.0 +op.mesh_size_y = 2.0 +op.random_seed = 600 +op.noise_offset_x = 0.0 +op.noise_offset_y = 0.0 +op.noise_offset_z = 0.0 +op.noise_size_x = 1.0 +op.noise_size_y = 1.0 +op.noise_size_z = 1.0 +op.noise_size = 0.5 +op.noise_type = 'hetero_terrain' +op.basis_type = '2' +op.vl_basis_type = '9' +op.distortion = 1.0 +op.hard_noise = '0' +op.noise_depth = 8 +op.amplitude = 0.5 +op.frequency = 2.0 +op.dimension = 1.0899999141693115 +op.lacunarity = 1.8599998950958252 +op.offset = 0.7700000405311584 +op.gain = 2.0 +op.marble_bias = '1' +op.marble_sharp = '0' +op.marble_shape = '7' +op.height = 0.5 +op.height_invert = False +op.height_offset = -0.0 +op.fx_mixfactor = 0.0 +op.fx_mix_mode = '0' +op.fx_type = '0' +op.fx_bias = '0' +op.fx_turb = 0.0 +op.fx_depth = 0 +op.fx_amplitude = 0.5 +op.fx_frequency = 2.0 +op.fx_size = 1.0 +op.fx_loc_x = 0.0 +op.fx_loc_y = 0.0 +op.fx_height = 0.5 +op.fx_invert = False +op.fx_offset = 0.0 +op.edge_falloff = '3' +op.falloff_x = 8.0 +op.falloff_y = 8.0 +op.edge_level = 0.0 +op.maximum = 0.5 +op.minimum = -0.5 +op.vert_group = '' +op.strata = 2.0 +op.strata_type = '2' +op.water_plane = False +op.water_level = 0.009999999776482582 +op.remove_double = False +op.show_main_settings = True +op.show_noise_settings = True +op.show_displace_settings = True +op.refresh = True +op.auto_refresh = True diff --git a/presets/operator/mesh.landscape_add/cristaline.py b/presets/operator/mesh.landscape_add/cristaline.py deleted file mode 100644 index 1155758b..00000000 --- a/presets/operator/mesh.landscape_add/cristaline.py +++ /dev/null @@ -1,73 +0,0 @@ -import bpy -op = bpy.context.active_operator - -op.ant_terrain_name = 'Landscape' -op.land_material = '' -op.water_material = '' -op.texture_block = '' -op.at_cursor = True -op.smooth_mesh = True -op.tri_face = False -op.sphere_mesh = True -op.subdivision_x = 256 -op.subdivision_y = 128 -op.mesh_size = 2.0 -op.mesh_size_x = 2.0 -op.mesh_size_y = 2.0 -op.random_seed = 0 -op.noise_offset_x = 0.0 -op.noise_offset_y = 0.0 -op.noise_offset_z = 0.0 -op.noise_size_x = 1.0 -op.noise_size_y = 1.0 -op.noise_size_z = 1.0 -op.noise_size = 1.0 -op.noise_type = 'turbulence_vector' -op.basis_type = '6' -op.vl_basis_type = '0' -op.distortion = 1.0 -op.hard_noise = '0' -op.noise_depth = 1 -op.amplitude = 0.5 -op.frequency = 2.0 -op.dimension = 1.0 -op.lacunarity = 2.0 -op.offset = 1.0 -op.gain = 1.0 -op.marble_bias = '0' -op.marble_sharp = '0' -op.marble_shape = '0' -op.height = 1.0 -op.height_invert = False -op.height_offset = 0.0 -op.fx_mixfactor = 0.0 -op.fx_mix_mode = '0' -op.fx_type = '0' -op.fx_bias = '0' -op.fx_turb = 0.0 -op.fx_depth = 0 -op.fx_amplitude = 0.5 -op.fx_frequency = 1.5 -op.fx_size = 1.0 -op.fx_loc_x = 0.0 -op.fx_loc_y = 0.0 -op.fx_height = 0.5 -op.fx_invert = False -op.fx_offset = 0.0 -op.edge_falloff = '0' -op.falloff_x = 4.0 -op.falloff_y = 4.0 -op.edge_level = 0.0 -op.maximum = 2.0 -op.minimum = -1.0 -op.vert_group = '' -op.strata = 5.0 -op.strata_type = '0' -op.water_plane = False -op.water_level = 0.009999999776482582 -op.remove_double = False -op.show_main_settings = True -op.show_noise_settings = True -op.show_displace_settings = True -op.refresh = True -op.auto_refresh = True diff --git a/presets/operator/mesh.landscape_add/crystalline.py b/presets/operator/mesh.landscape_add/crystalline.py new file mode 100644 index 00000000..1155758b --- /dev/null +++ b/presets/operator/mesh.landscape_add/crystalline.py @@ -0,0 +1,73 @@ +import bpy +op = bpy.context.active_operator + +op.ant_terrain_name = 'Landscape' +op.land_material = '' +op.water_material = '' +op.texture_block = '' +op.at_cursor = True +op.smooth_mesh = True +op.tri_face = False +op.sphere_mesh = True +op.subdivision_x = 256 +op.subdivision_y = 128 +op.mesh_size = 2.0 +op.mesh_size_x = 2.0 +op.mesh_size_y = 2.0 +op.random_seed = 0 +op.noise_offset_x = 0.0 +op.noise_offset_y = 0.0 +op.noise_offset_z = 0.0 +op.noise_size_x = 1.0 +op.noise_size_y = 1.0 +op.noise_size_z = 1.0 +op.noise_size = 1.0 +op.noise_type = 'turbulence_vector' +op.basis_type = '6' +op.vl_basis_type = '0' +op.distortion = 1.0 +op.hard_noise = '0' +op.noise_depth = 1 +op.amplitude = 0.5 +op.frequency = 2.0 +op.dimension = 1.0 +op.lacunarity = 2.0 +op.offset = 1.0 +op.gain = 1.0 +op.marble_bias = '0' +op.marble_sharp = '0' +op.marble_shape = '0' +op.height = 1.0 +op.height_invert = False +op.height_offset = 0.0 +op.fx_mixfactor = 0.0 +op.fx_mix_mode = '0' +op.fx_type = '0' +op.fx_bias = '0' +op.fx_turb = 0.0 +op.fx_depth = 0 +op.fx_amplitude = 0.5 +op.fx_frequency = 1.5 +op.fx_size = 1.0 +op.fx_loc_x = 0.0 +op.fx_loc_y = 0.0 +op.fx_height = 0.5 +op.fx_invert = False +op.fx_offset = 0.0 +op.edge_falloff = '0' +op.falloff_x = 4.0 +op.falloff_y = 4.0 +op.edge_level = 0.0 +op.maximum = 2.0 +op.minimum = -1.0 +op.vert_group = '' +op.strata = 5.0 +op.strata_type = '0' +op.water_plane = False +op.water_level = 0.009999999776482582 +op.remove_double = False +op.show_main_settings = True +op.show_noise_settings = True +op.show_displace_settings = True +op.refresh = True +op.auto_refresh = True diff --git a/presets/operator/mesh.landscape_add/volcano.py b/presets/operator/mesh.landscape_add/volcano.py new file mode 100644 index 00000000..e5c3de29 --- /dev/null +++ b/presets/operator/mesh.landscape_add/volcano.py @@ -0,0 +1,73 @@ +import bpy +op = bpy.context.active_operator + +op.ant_terrain_name = 'Landscape' +op.land_material = '' +op.water_material = '' +op.texture_block = '' +op.at_cursor = True +op.smooth_mesh = True +op.tri_face = False +op.sphere_mesh = False +op.subdivision_x = 128 +op.subdivision_y = 128 +op.mesh_size = 2.0 +op.mesh_size_x = 2.0 +op.mesh_size_y = 2.0 +op.random_seed = 7 +op.noise_offset_x = 0.0 +op.noise_offset_y = 0.0 +op.noise_offset_z = 0.0 +op.noise_size_x = 1.0 +op.noise_size_y = 1.0 +op.noise_size_z = 1.0 +op.noise_size = 1.0 +op.noise_type = 'marble_noise' +op.basis_type = '0' +op.vl_basis_type = '1' +op.distortion = 1.5 +op.hard_noise = '0' +op.noise_depth = 8 +op.amplitude = 0.5 +op.frequency = 1.7999999523162842 +op.dimension = 1.0 +op.lacunarity = 2.0 +op.offset = 1.0 +op.gain = 2.0 +op.marble_bias = '2' +op.marble_sharp = '3' +op.marble_shape = '1' +op.height = 0.6000000238418579 +op.height_invert = False +op.height_offset = 0.0 +op.fx_mixfactor = 0.0 +op.fx_mix_mode = '1' +op.fx_type = '14' +op.fx_bias = '0' +op.fx_turb = 0.5 +op.fx_depth = 2 +op.fx_amplitude = 0.3799999952316284 +op.fx_frequency = 1.5 +op.fx_size = 1.1500000953674316 +op.fx_loc_x = -1.0 +op.fx_loc_y = 1.0 +op.fx_height = 0.5 +op.fx_invert = False +op.fx_offset = 0.05999999865889549 +op.edge_falloff = '3' +op.falloff_x = 2.0 +op.falloff_y = 2.0 +op.edge_level = 0.0 +op.maximum = 1.0 +op.minimum = -1.0 +op.vert_group = '' +op.strata = 5.0 +op.strata_type = '0' +op.water_plane = False +op.water_level = 0.009999999776482582 +op.remove_double = False +op.show_main_settings = True +op.show_noise_settings = True +op.show_displace_settings = True +op.refresh = True +op.auto_refresh = True diff --git a/presets/operator/mesh.landscape_add/vulcano.py b/presets/operator/mesh.landscape_add/vulcano.py deleted file mode 100644 index e5c3de29..00000000 --- a/presets/operator/mesh.landscape_add/vulcano.py +++ /dev/null @@ -1,73 +0,0 @@ -import bpy -op = bpy.context.active_operator - -op.ant_terrain_name = 'Landscape' -op.land_material = '' -op.water_material = '' -op.texture_block = '' -op.at_cursor = True -op.smooth_mesh = True -op.tri_face = False -op.sphere_mesh = False -op.subdivision_x = 128 -op.subdivision_y = 128 -op.mesh_size = 2.0 -op.mesh_size_x = 2.0 -op.mesh_size_y = 2.0 -op.random_seed = 7 -op.noise_offset_x = 0.0 -op.noise_offset_y = 0.0 -op.noise_offset_z = 0.0 -op.noise_size_x = 1.0 -op.noise_size_y = 1.0 -op.noise_size_z = 1.0 -op.noise_size = 1.0 -op.noise_type = 'marble_noise' -op.basis_type = '0' -op.vl_basis_type = '1' -op.distortion = 1.5 -op.hard_noise = '0' -op.noise_depth = 8 -op.amplitude = 0.5 -op.frequency = 1.7999999523162842 -op.dimension = 1.0 -op.lacunarity = 2.0 -op.offset = 1.0 -op.gain = 2.0 -op.marble_bias = '2' -op.marble_sharp = '3' -op.marble_shape = '1' -op.height = 0.6000000238418579 -op.height_invert = False -op.height_offset = 0.0 -op.fx_mixfactor = 0.0 -op.fx_mix_mode = '1' -op.fx_type = '14' -op.fx_bias = '0' -op.fx_turb = 0.5 -op.fx_depth = 2 -op.fx_amplitude = 0.3799999952316284 -op.fx_frequency = 1.5 -op.fx_size = 1.1500000953674316 -op.fx_loc_x = -1.0 -op.fx_loc_y = 1.0 -op.fx_height = 0.5 -op.fx_invert = False -op.fx_offset = 0.05999999865889549 -op.edge_falloff = '3' -op.falloff_x = 2.0 -op.falloff_y = 2.0 -op.edge_level = 0.0 -op.maximum = 1.0 -op.minimum = -1.0 -op.vert_group = '' -op.strata = 5.0 -op.strata_type = '0' -op.water_plane = False -op.water_level = 0.009999999776482582 -op.remove_double = False -op.show_main_settings = True -op.show_noise_settings = True -op.show_displace_settings = True -op.refresh = True -op.auto_refresh = True -- cgit v1.2.3 From 3a264229d2d37aee3c673545df122e41dc481723 Mon Sep 17 00:00:00 2001 From: Philipp Oeser Date: Thu, 13 Dec 2018 11:49:37 +0100 Subject: FBX export: skip special properties when exporting custom properties '_RNA_UI' (and rna runtime properties) should not be included, these would have been included as string properties causing errors on reimport backport rBAe4f4053de64e (fix for T59202) from 2.8 branch Differential Revision: https://developer.blender.org/D4068 --- io_scene_fbx/export_fbx_bin.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/io_scene_fbx/export_fbx_bin.py b/io_scene_fbx/export_fbx_bin.py index e82ceadd..ed1109e7 100644 --- a/io_scene_fbx/export_fbx_bin.py +++ b/io_scene_fbx/export_fbx_bin.py @@ -533,7 +533,17 @@ def fbx_data_element_custom_properties(props, bid): """ Store custom properties of blender ID bid (any mapping-like object, in fact) into FBX properties props. """ - for k, v in bid.items(): + items = bid.items() + + if not items: + return + + rna_properties = {prop.identifier for prop in bid.bl_rna.properties if prop.is_runtime} + + for k, v in items: + if k == '_RNA_UI' or k in rna_properties: + continue + list_val = getattr(v, "to_list", lambda: None)() if isinstance(v, str): -- cgit v1.2.3 From fe5e51ddcd0bcd1f87428cb3c6098fc66f6962f1 Mon Sep 17 00:00:00 2001 From: Nutti Date: Thu, 13 Dec 2018 22:57:08 +0900 Subject: Magic UV: Phase 1 for porting to Blender 2.8 Below features are available for now. * Copy/Paste UV * Transfer UV * Flip/Rotate UV * Mirror UV * Move UV * UVW Also, Magic UV remains to work at Blender 2.7x by the legacy code. --- uv_magic_uv/__init__.py | 89 +- uv_magic_uv/common.py | 10 +- uv_magic_uv/impl/__init__.py | 0 uv_magic_uv/impl/copy_paste_uv_impl.py | 271 ++++++ uv_magic_uv/impl/copy_paste_uv_uvedit_impl.py | 166 ++++ uv_magic_uv/impl/flip_rotate_impl.py | 133 +++ uv_magic_uv/impl/mirror_uv_impl.py | 158 ++++ uv_magic_uv/impl/move_uv_impl.py | 166 ++++ uv_magic_uv/impl/transfer_uv_impl.py | 330 +++++++ uv_magic_uv/impl/uvw_impl.py | 154 ++++ uv_magic_uv/legacy/__init__.py | 38 + uv_magic_uv/legacy/op/__init__.py | 74 ++ uv_magic_uv/legacy/op/align_uv.py | 988 +++++++++++++++++++++ uv_magic_uv/legacy/op/align_uv_cursor.py | 257 ++++++ uv_magic_uv/legacy/op/copy_paste_uv.py | 531 +++++++++++ uv_magic_uv/legacy/op/copy_paste_uv_object.py | 298 +++++++ uv_magic_uv/legacy/op/copy_paste_uv_uvedit.py | 97 ++ uv_magic_uv/legacy/op/flip_rotate_uv.py | 132 +++ uv_magic_uv/legacy/op/mirror_uv.py | 110 +++ uv_magic_uv/legacy/op/move_uv.py | 82 ++ uv_magic_uv/legacy/op/pack_uv.py | 281 ++++++ uv_magic_uv/legacy/op/preserve_uv_aspect.py | 283 ++++++ uv_magic_uv/legacy/op/select_uv.py | 168 ++++ uv_magic_uv/legacy/op/smooth_uv.py | 287 ++++++ uv_magic_uv/legacy/op/texture_lock.py | 545 ++++++++++++ uv_magic_uv/legacy/op/texture_projection.py | 402 +++++++++ uv_magic_uv/legacy/op/texture_wrap.py | 301 +++++++ uv_magic_uv/legacy/op/transfer_uv.py | 172 ++++ uv_magic_uv/legacy/op/unwrap_constraint.py | 190 ++++ uv_magic_uv/legacy/op/uv_bounding_box.py | 838 +++++++++++++++++ uv_magic_uv/legacy/op/uv_inspection.py | 280 ++++++ uv_magic_uv/legacy/op/uv_sculpt.py | 483 ++++++++++ uv_magic_uv/legacy/op/uvw.py | 181 ++++ uv_magic_uv/legacy/op/world_scale_uv.py | 655 ++++++++++++++ uv_magic_uv/legacy/preferences.py | 468 ++++++++++ uv_magic_uv/legacy/properites.py | 61 ++ uv_magic_uv/legacy/ui/IMAGE_MT_uvs.py | 197 ++++ uv_magic_uv/legacy/ui/VIEW3D_MT_object.py | 54 ++ uv_magic_uv/legacy/ui/VIEW3D_MT_uv_map.py | 257 ++++++ uv_magic_uv/legacy/ui/__init__.py | 50 ++ uv_magic_uv/legacy/ui/uvedit_copy_paste_uv.py | 62 ++ uv_magic_uv/legacy/ui/uvedit_editor_enhancement.py | 149 ++++ uv_magic_uv/legacy/ui/uvedit_uv_manipulation.py | 130 +++ .../legacy/ui/view3d_copy_paste_uv_editmode.py | 93 ++ .../legacy/ui/view3d_copy_paste_uv_objectmode.py | 65 ++ uv_magic_uv/legacy/ui/view3d_uv_manipulation.py | 289 ++++++ uv_magic_uv/legacy/ui/view3d_uv_mapping.py | 116 +++ uv_magic_uv/op/__init__.py | 28 - uv_magic_uv/op/align_uv.py | 980 -------------------- uv_magic_uv/op/align_uv_cursor.py | 251 ------ uv_magic_uv/op/copy_paste_uv.py | 393 +++----- uv_magic_uv/op/copy_paste_uv_object.py | 140 ++- uv_magic_uv/op/copy_paste_uv_uvedit.py | 151 +--- uv_magic_uv/op/flip_rotate_uv.py | 100 +-- uv_magic_uv/op/mirror_uv.py | 130 +-- uv_magic_uv/op/move_uv.py | 135 +-- uv_magic_uv/op/pack_uv.py | 275 ------ uv_magic_uv/op/preserve_uv_aspect.py | 277 ------ uv_magic_uv/op/select_uv.py | 161 ---- uv_magic_uv/op/smooth_uv.py | 281 ------ uv_magic_uv/op/texture_lock.py | 533 ----------- uv_magic_uv/op/texture_projection.py | 395 -------- uv_magic_uv/op/texture_wrap.py | 294 ------ uv_magic_uv/op/transfer_uv.py | 332 +------ uv_magic_uv/op/unwrap_constraint.py | 184 ---- uv_magic_uv/op/uv_bounding_box.py | 832 ----------------- uv_magic_uv/op/uv_inspection.py | 272 ------ uv_magic_uv/op/uv_sculpt.py | 477 ---------- uv_magic_uv/op/uvw.py | 176 +--- uv_magic_uv/op/world_scale_uv.py | 646 -------------- uv_magic_uv/preferences.py | 85 +- uv_magic_uv/properites.py | 73 +- uv_magic_uv/ui/IMAGE_MT_uvs.py | 156 +--- uv_magic_uv/ui/VIEW3D_MT_object.py | 18 +- uv_magic_uv/ui/VIEW3D_MT_uv_map.py | 183 +--- uv_magic_uv/ui/__init__.py | 12 +- uv_magic_uv/ui/uvedit_copy_paste_uv.py | 21 +- uv_magic_uv/ui/uvedit_editor_enhancement.py | 144 --- uv_magic_uv/ui/uvedit_uv_manipulation.py | 126 --- uv_magic_uv/ui/view3d_copy_paste_uv_editmode.py | 36 +- uv_magic_uv/ui/view3d_copy_paste_uv_objectmode.py | 21 +- uv_magic_uv/ui/view3d_uv_manipulation.py | 217 +---- uv_magic_uv/ui/view3d_uv_mapping.py | 62 +- uv_magic_uv/utils/__init__.py | 34 + uv_magic_uv/utils/bl_class_registry.py | 84 ++ uv_magic_uv/utils/property_class_registry.py | 72 ++ 86 files changed, 11775 insertions(+), 8153 deletions(-) create mode 100644 uv_magic_uv/impl/__init__.py create mode 100644 uv_magic_uv/impl/copy_paste_uv_impl.py create mode 100644 uv_magic_uv/impl/copy_paste_uv_uvedit_impl.py create mode 100644 uv_magic_uv/impl/flip_rotate_impl.py create mode 100644 uv_magic_uv/impl/mirror_uv_impl.py create mode 100644 uv_magic_uv/impl/move_uv_impl.py create mode 100644 uv_magic_uv/impl/transfer_uv_impl.py create mode 100644 uv_magic_uv/impl/uvw_impl.py create mode 100644 uv_magic_uv/legacy/__init__.py create mode 100644 uv_magic_uv/legacy/op/__init__.py create mode 100644 uv_magic_uv/legacy/op/align_uv.py create mode 100644 uv_magic_uv/legacy/op/align_uv_cursor.py create mode 100644 uv_magic_uv/legacy/op/copy_paste_uv.py create mode 100644 uv_magic_uv/legacy/op/copy_paste_uv_object.py create mode 100644 uv_magic_uv/legacy/op/copy_paste_uv_uvedit.py create mode 100644 uv_magic_uv/legacy/op/flip_rotate_uv.py create mode 100644 uv_magic_uv/legacy/op/mirror_uv.py create mode 100644 uv_magic_uv/legacy/op/move_uv.py create mode 100644 uv_magic_uv/legacy/op/pack_uv.py create mode 100644 uv_magic_uv/legacy/op/preserve_uv_aspect.py create mode 100644 uv_magic_uv/legacy/op/select_uv.py create mode 100644 uv_magic_uv/legacy/op/smooth_uv.py create mode 100644 uv_magic_uv/legacy/op/texture_lock.py create mode 100644 uv_magic_uv/legacy/op/texture_projection.py create mode 100644 uv_magic_uv/legacy/op/texture_wrap.py create mode 100644 uv_magic_uv/legacy/op/transfer_uv.py create mode 100644 uv_magic_uv/legacy/op/unwrap_constraint.py create mode 100644 uv_magic_uv/legacy/op/uv_bounding_box.py create mode 100644 uv_magic_uv/legacy/op/uv_inspection.py create mode 100644 uv_magic_uv/legacy/op/uv_sculpt.py create mode 100644 uv_magic_uv/legacy/op/uvw.py create mode 100644 uv_magic_uv/legacy/op/world_scale_uv.py create mode 100644 uv_magic_uv/legacy/preferences.py create mode 100644 uv_magic_uv/legacy/properites.py create mode 100644 uv_magic_uv/legacy/ui/IMAGE_MT_uvs.py create mode 100644 uv_magic_uv/legacy/ui/VIEW3D_MT_object.py create mode 100644 uv_magic_uv/legacy/ui/VIEW3D_MT_uv_map.py create mode 100644 uv_magic_uv/legacy/ui/__init__.py create mode 100644 uv_magic_uv/legacy/ui/uvedit_copy_paste_uv.py create mode 100644 uv_magic_uv/legacy/ui/uvedit_editor_enhancement.py create mode 100644 uv_magic_uv/legacy/ui/uvedit_uv_manipulation.py create mode 100644 uv_magic_uv/legacy/ui/view3d_copy_paste_uv_editmode.py create mode 100644 uv_magic_uv/legacy/ui/view3d_copy_paste_uv_objectmode.py create mode 100644 uv_magic_uv/legacy/ui/view3d_uv_manipulation.py create mode 100644 uv_magic_uv/legacy/ui/view3d_uv_mapping.py delete mode 100644 uv_magic_uv/op/align_uv.py delete mode 100644 uv_magic_uv/op/align_uv_cursor.py delete mode 100644 uv_magic_uv/op/pack_uv.py delete mode 100644 uv_magic_uv/op/preserve_uv_aspect.py delete mode 100644 uv_magic_uv/op/select_uv.py delete mode 100644 uv_magic_uv/op/smooth_uv.py delete mode 100644 uv_magic_uv/op/texture_lock.py delete mode 100644 uv_magic_uv/op/texture_projection.py delete mode 100644 uv_magic_uv/op/texture_wrap.py delete mode 100644 uv_magic_uv/op/unwrap_constraint.py delete mode 100644 uv_magic_uv/op/uv_bounding_box.py delete mode 100644 uv_magic_uv/op/uv_inspection.py delete mode 100644 uv_magic_uv/op/uv_sculpt.py delete mode 100644 uv_magic_uv/op/world_scale_uv.py delete mode 100644 uv_magic_uv/ui/uvedit_editor_enhancement.py delete mode 100644 uv_magic_uv/ui/uvedit_uv_manipulation.py create mode 100644 uv_magic_uv/utils/__init__.py create mode 100644 uv_magic_uv/utils/bl_class_registry.py create mode 100644 uv_magic_uv/utils/property_class_registry.py diff --git a/uv_magic_uv/__init__.py b/uv_magic_uv/__init__.py index 20709e79..63591526 100644 --- a/uv_magic_uv/__init__.py +++ b/uv_magic_uv/__init__.py @@ -28,8 +28,8 @@ bl_info = { "name": "Magic UV", "author": "Nutti, Mifth, Jace Priester, kgeogeo, mem, imdjs" "Keith (Wahooney) Boshoff, McBuff, MaxRobinot, Alexander Milovsky", - "version": (5, 2, 0), - "blender": (2, 79, 0), + "version": (5, 3, 0), + "blender": (2, 80, 0), "location": "See Add-ons Preferences", "description": "UV Toolset. See Add-ons Preferences for details", "warning": "", @@ -40,43 +40,80 @@ bl_info = { "category": "UV" } +def check_version(major, minor, _): + """ + Check blender version + """ + + if bpy.app.version[0] == major and bpy.app.version[1] == minor: + return 0 + if bpy.app.version[0] > major: + return 1 + if bpy.app.version[1] > minor: + return 1 + return -1 + + if "bpy" in locals(): import importlib - importlib.reload(op) - importlib.reload(ui) importlib.reload(common) - importlib.reload(preferences) - importlib.reload(properites) - importlib.reload(addon_updater_ops) - importlib.reload(addon_updater) + importlib.reload(utils) + utils.bl_class_registry.BlClassRegistry.cleanup() + if check_version(2, 80, 0) >= 0: + importlib.reload(op) + importlib.reload(ui) + importlib.reload(properites) + importlib.reload(preferences) + importlib.reload(addon_updater_ops) + importlib.reload(addon_updater) + else: + importlib.reload(legacy) else: - from . import op - from . import ui + import bpy from . import common - from . import preferences - from . import properites - from . import addon_updater_ops - from . import addon_updater + from . import utils + if check_version(2, 80, 0) >= 0: + from . import op + from . import ui + from . import properites + from . import preferences + from . import addon_updater_ops + from . import addon_updater + else: + from . import legacy + import bpy def register(): - 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() + if common.check_version(2, 80, 0) >= 0: + utils.bl_class_registry.BlClassRegistry.register() + properites.init_props(bpy.types.Scene) + if preferences.Preferences.enable_builtin_menu: + preferences.add_builtin_menu() + else: + utils.bl_class_registry.BlClassRegistry.register() + legacy.properites.init_props(bpy.types.Scene) + if legacy.preferences.Preferences.enable_builtin_menu: + legacy.preferences.add_builtin_menu() + if not common.is_console_mode(): + addon_updater_ops.register(bl_info) def unregister(): - if 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 common.check_version(2, 80, 0) >= 0: + if preferences.Preferences.enable_builtin_menu: + preferences.remove_builtin_menu() + properites.clear_props(bpy.types.Scene) + utils.bl_class_registry.BlClassRegistry.unregister() + else: + if not common.is_console_mode(): + addon_updater_ops.unregister() + if legacy.preferences.Preferences.enable_builtin_menu: + legacy.preferences.remove_builtin_menu() + legacy.properites.clear_props(bpy.types.Scene) + utils.bl_class_registry.BlClassRegistry.unregister() if __name__ == "__main__": diff --git a/uv_magic_uv/common.py b/uv_magic_uv/common.py index b0c4306e..bad88167 100644 --- a/uv_magic_uv/common.py +++ b/uv_magic_uv/common.py @@ -35,10 +35,16 @@ import bmesh __all__ = [ 'is_console_mode', + 'is_debug_mode', + 'enable_debugg_mode', + 'disable_debug_mode', 'debug_print', 'check_version', 'redraw_all_areas', 'get_space', + 'mouse_on_region', + 'mouse_on_area', + 'mouse_on_regions', 'create_bmesh', 'create_new_uv_map', 'get_island_info', @@ -51,10 +57,12 @@ __all__ = [ 'measure_uv_area', 'diff_point_to_segment', 'get_loop_sequences', + 'get_overlapped_uv_info', + 'get_flipped_uv_info', ] -__DEBUG_MODE = False +__DEBUG_MODE = True def is_console_mode(): diff --git a/uv_magic_uv/impl/__init__.py b/uv_magic_uv/impl/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/uv_magic_uv/impl/copy_paste_uv_impl.py b/uv_magic_uv/impl/copy_paste_uv_impl.py new file mode 100644 index 00000000..ed44637b --- /dev/null +++ b/uv_magic_uv/impl/copy_paste_uv_impl.py @@ -0,0 +1,271 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "imdjs, Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + + +import bmesh + +from .. import common + + +__all__ = [ + 'is_valid_context', + 'get_copy_uv_layers', + 'get_paste_uv_layers', + 'get_src_face_info', + 'get_dest_face_info', + 'get_select_history_src_face_info', + 'get_select_history_dest_face_info', + 'paste_uv', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +def get_copy_uv_layers(ops_obj, bm, uv_map): + uv_layers = [] + if uv_map == "__default": + if not bm.loops.layers.uv: + ops_obj.report( + {'WARNING'}, "Object must have more than one UV map") + return None + uv_layers.append(bm.loops.layers.uv.verify()) + ops_obj.report({'INFO'}, "Copy UV coordinate") + elif uv_map == "__all": + for uv in bm.loops.layers.uv.keys(): + uv_layers.append(bm.loops.layers.uv[uv]) + ops_obj.report({'INFO'}, "Copy UV coordinate (UV map: ALL)") + else: + uv_layers.append(bm.loops.layers.uv[uv_map]) + ops_obj.report( + {'INFO'}, "Copy UV coordinate (UV map:{})".format(uv_map)) + + return uv_layers + + +def get_paste_uv_layers(ops_obj, obj, bm, src_info, uv_map): + uv_layers = [] + if uv_map == "__default": + if not bm.loops.layers.uv: + ops_obj.report( + {'WARNING'}, "Object must have more than one UV map") + return None + uv_layers.append(bm.loops.layers.uv.verify()) + ops_obj.report({'INFO'}, "Paste UV coordinate") + elif uv_map == "__new": + new_uv_map = common.create_new_uv_map(obj) + if not new_uv_map: + ops_obj.report({'WARNING'}, + "Reached to the maximum number of UV map") + return None + uv_layers.append(bm.loops.layers.uv[new_uv_map.name]) + ops_obj.report( + {'INFO'}, "Paste UV coordinate (UV map:{})".format(new_uv_map)) + elif uv_map == "__all": + for src_layer in src_info.keys(): + if src_layer not in bm.loops.layers.uv.keys(): + new_uv_map = common.create_new_uv_map(obj, src_layer) + if not new_uv_map: + ops_obj.report({'WARNING'}, + "Reached to the maximum number of UV map") + return None + uv_layers.append(bm.loops.layers.uv[src_layer]) + ops_obj.report({'INFO'}, "Paste UV coordinate (UV map: ALL)") + else: + uv_layers.append(bm.loops.layers.uv[uv_map]) + ops_obj.report( + {'INFO'}, "Paste UV coordinate (UV map:{})".format(uv_map)) + + return uv_layers + + +def get_src_face_info(ops_obj, bm, uv_layers, only_select=False): + src_info = {} + for layer in uv_layers: + face_info = [] + for face in bm.faces: + if not only_select or face.select: + info = { + "index": face.index, + "uvs": [l[layer].uv.copy() for l in face.loops], + "pin_uvs": [l[layer].pin_uv for l in face.loops], + "seams": [l.edge.seam for l in face.loops], + } + face_info.append(info) + if not face_info: + ops_obj.report({'WARNING'}, "No faces are selected") + return None + src_info[layer.name] = face_info + + return src_info + + +def get_dest_face_info(ops_obj, bm, uv_layers, src_info, strategy, + only_select=False): + dest_info = {} + for layer in uv_layers: + face_info = [] + for face in bm.faces: + if not only_select or face.select: + info = { + "index": face.index, + "uvs": [l[layer].uv.copy() for l in face.loops], + } + face_info.append(info) + if not face_info: + ops_obj.report({'WARNING'}, "No faces are selected") + return None + key = list(src_info.keys())[0] + src_face_count = len(src_info[key]) + dest_face_count = len(face_info) + if strategy == 'N_N' and src_face_count != dest_face_count: + ops_obj.report( + {'WARNING'}, + "Number of selected faces is different from copied" + + "(src:{}, dest:{})" + .format(src_face_count, dest_face_count)) + return None + dest_info[layer.name] = face_info + + return dest_info + + +def get_select_history_src_face_info(ops_obj, bm, uv_layers): + src_info = {} + for layer in uv_layers: + face_info = [] + for hist in bm.select_history: + if isinstance(hist, bmesh.types.BMFace) and hist.select: + info = { + "index": hist.index, + "uvs": [l[layer].uv.copy() for l in hist.loops], + "pin_uvs": [l[layer].pin_uv for l in hist.loops], + "seams": [l.edge.seam for l in hist.loops], + } + face_info.append(info) + if not face_info: + ops_obj.report({'WARNING'}, "No faces are selected") + return None + src_info[layer.name] = face_info + + return src_info + + +def get_select_history_dest_face_info(ops_obj, bm, uv_layers, src_info, + strategy): + dest_info = {} + for layer in uv_layers: + face_info = [] + for hist in bm.select_history: + if isinstance(hist, bmesh.types.BMFace) and hist.select: + info = { + "index": hist.index, + "uvs": [l[layer].uv.copy() for l in hist.loops], + } + face_info.append(info) + if not face_info: + ops_obj.report({'WARNING'}, "No faces are selected") + return None + key = list(src_info.keys())[0] + src_face_count = len(src_info[key]) + dest_face_count = len(face_info) + if strategy == 'N_N' and src_face_count != dest_face_count: + ops_obj.report( + {'WARNING'}, + "Number of selected faces is different from copied" + + "(src:{}, dest:{})" + .format(src_face_count, dest_face_count)) + return None + dest_info[layer.name] = face_info + + return dest_info + + +def paste_uv(ops_obj, bm, src_info, dest_info, uv_layers, strategy, flip, + rotate, copy_seams): + for slayer_name, dlayer in zip(src_info.keys(), uv_layers): + src_faces = src_info[slayer_name] + dest_faces = dest_info[dlayer.name] + + for idx, dinfo in enumerate(dest_faces): + sinfo = None + if strategy == 'N_N': + sinfo = src_faces[idx] + elif strategy == 'N_M': + sinfo = src_faces[idx % len(src_faces)] + + suv = sinfo["uvs"] + spuv = sinfo["pin_uvs"] + ss = sinfo["seams"] + if len(sinfo["uvs"]) != len(dinfo["uvs"]): + ops_obj.report({'WARNING'}, "Some faces are different size") + return -1 + + suvs_fr = [uv for uv in suv] + spuvs_fr = [pin_uv for pin_uv in spuv] + ss_fr = [s for s in ss] + + # flip UVs + if flip is True: + suvs_fr.reverse() + spuvs_fr.reverse() + ss_fr.reverse() + + # rotate UVs + for _ in range(rotate): + uv = suvs_fr.pop() + pin_uv = spuvs_fr.pop() + s = ss_fr.pop() + suvs_fr.insert(0, uv) + spuvs_fr.insert(0, pin_uv) + ss_fr.insert(0, s) + + # paste UVs + for l, suv, spuv, ss in zip(bm.faces[dinfo["index"]].loops, + suvs_fr, spuvs_fr, ss_fr): + l[dlayer].uv = suv + l[dlayer].pin_uv = spuv + if copy_seams is True: + l.edge.seam = ss + + return 0 diff --git a/uv_magic_uv/impl/copy_paste_uv_uvedit_impl.py b/uv_magic_uv/impl/copy_paste_uv_uvedit_impl.py new file mode 100644 index 00000000..f14a70d6 --- /dev/null +++ b/uv_magic_uv/impl/copy_paste_uv_uvedit_impl.py @@ -0,0 +1,166 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "imdjs, Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import math +from math import atan2, sin, cos + +import bmesh +from mathutils import Vector + +from .. import common + + +__all__ = [ + 'is_valid_context', + 'CopyUVImpl', + 'PasteUVImpl', +] + + +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 CopyUVImpl: + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + + def execute(self, _, context): + props = context.scene.muv_props.copy_paste_uv_uvedit + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + uv_layer = bm.loops.layers.uv.verify() + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + props.src_uvs = [] + for face in bm.faces: + if not face.select: + continue + skip = False + for l in face.loops: + if not l[uv_layer].select: + skip = True + break + if skip: + continue + props.src_uvs.append([l[uv_layer].uv.copy() for l in face.loops]) + + return {'FINISHED'} + + +class PasteUVImpl: + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.copy_paste_uv_uvedit + if not props.src_uvs: + return False + return is_valid_context(context) + + def execute(self, _, context): + props = context.scene.muv_props.copy_paste_uv_uvedit + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + uv_layer = bm.loops.layers.uv.verify() + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + dest_uvs = [] + dest_face_indices = [] + for face in bm.faces: + if not face.select: + continue + skip = False + for l in face.loops: + if not l[uv_layer].select: + skip = True + break + if skip: + continue + dest_face_indices.append(face.index) + uvs = [l[uv_layer].uv.copy() for l in face.loops] + dest_uvs.append(uvs) + + for suvs, duvs in zip(props.src_uvs, dest_uvs): + src_diff = suvs[1] - suvs[0] + dest_diff = duvs[1] - duvs[0] + + src_base = suvs[0] + dest_base = duvs[0] + + src_rad = atan2(src_diff.y, src_diff.x) + dest_rad = atan2(dest_diff.y, dest_diff.x) + if src_rad < dest_rad: + radian = dest_rad - src_rad + elif src_rad > dest_rad: + radian = math.pi * 2 - (src_rad - dest_rad) + else: # src_rad == dest_rad + radian = 0.0 + + ratio = dest_diff.length / src_diff.length + break + + for suvs, fidx in zip(props.src_uvs, dest_face_indices): + for l, suv in zip(bm.faces[fidx].loops, suvs): + base = suv - src_base + radian_ref = atan2(base.y, base.x) + radian_fin = (radian + radian_ref) + length = base.length + turn = Vector((length * cos(radian_fin), + length * sin(radian_fin))) + target_uv = Vector((turn.x * ratio, turn.y * ratio)) + \ + dest_base + l[uv_layer].uv = target_uv + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/uv_magic_uv/impl/flip_rotate_impl.py b/uv_magic_uv/impl/flip_rotate_impl.py new file mode 100644 index 00000000..f74bc256 --- /dev/null +++ b/uv_magic_uv/impl/flip_rotate_impl.py @@ -0,0 +1,133 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "imdjs, Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + + +__all__ = [ + 'is_valid_context', + 'get_uv_layer', + 'get_src_face_info', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +def get_uv_layer(ops_obj, bm): + # get UV layer + if not bm.loops.layers.uv: + ops_obj.report({'WARNING'}, "Object must have more than one UV map") + return None + uv_layer = bm.loops.layers.uv.verify() + + return uv_layer + + +def get_src_face_info(ops_obj, bm, uv_layers, only_select=False): + src_info = {} + for layer in uv_layers: + face_info = [] + for face in bm.faces: + if not only_select or face.select: + info = { + "index": face.index, + "uvs": [l[layer].uv.copy() for l in face.loops], + "pin_uvs": [l[layer].pin_uv for l in face.loops], + "seams": [l.edge.seam for l in face.loops], + } + face_info.append(info) + if not face_info: + ops_obj.report({'WARNING'}, "No faces are selected") + return None + src_info[layer.name] = face_info + + return src_info + + +def paste_uv(ops_obj, bm, src_info, dest_info, uv_layers, strategy, flip, + rotate, copy_seams): + for slayer_name, dlayer in zip(src_info.keys(), uv_layers): + src_faces = src_info[slayer_name] + dest_faces = dest_info[dlayer.name] + + for idx, dinfo in enumerate(dest_faces): + sinfo = None + if strategy == 'N_N': + sinfo = src_faces[idx] + elif strategy == 'N_M': + sinfo = src_faces[idx % len(src_faces)] + + suv = sinfo["uvs"] + spuv = sinfo["pin_uvs"] + ss = sinfo["seams"] + if len(sinfo["uvs"]) != len(dinfo["uvs"]): + ops_obj.report({'WARNING'}, "Some faces are different size") + return -1 + + suvs_fr = [uv for uv in suv] + spuvs_fr = [pin_uv for pin_uv in spuv] + ss_fr = [s for s in ss] + + # flip UVs + if flip is True: + suvs_fr.reverse() + spuvs_fr.reverse() + ss_fr.reverse() + + # rotate UVs + for _ in range(rotate): + uv = suvs_fr.pop() + pin_uv = spuvs_fr.pop() + s = ss_fr.pop() + suvs_fr.insert(0, uv) + spuvs_fr.insert(0, pin_uv) + ss_fr.insert(0, s) + + # paste UVs + for l, suv, spuv, ss in zip(bm.faces[dinfo["index"]].loops, + suvs_fr, spuvs_fr, ss_fr): + l[dlayer].uv = suv + l[dlayer].pin_uv = spuv + if copy_seams is True: + l.edge.seam = ss + + return 0 diff --git a/uv_magic_uv/impl/mirror_uv_impl.py b/uv_magic_uv/impl/mirror_uv_impl.py new file mode 100644 index 00000000..e79fbc2c --- /dev/null +++ b/uv_magic_uv/impl/mirror_uv_impl.py @@ -0,0 +1,158 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Keith (Wahooney) Boshoff, Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bmesh +from mathutils import Vector + +from .. import common + + +__all__ = [ + 'is_valid_context', + 'is_vector_similar', + 'mirror_uvs', + 'get_face_center', + 'MirrorUVImpl', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +def is_vector_similar(v1, v2, error): + """ + Check if two vectors are similar, within an error threshold + """ + within_err_x = abs(v2.x - v1.x) < error + within_err_y = abs(v2.y - v1.y) < error + within_err_z = abs(v2.z - v1.z) < error + + return within_err_x and within_err_y and within_err_z + + +def mirror_uvs(uv_layer, src, dst, axis, error): + """ + Copy UV coordinates from one UV face to another + """ + for sl in src.loops: + suv = sl[uv_layer].uv.copy() + svco = sl.vert.co.copy() + for dl in dst.loops: + dvco = dl.vert.co.copy() + if axis == 'X': + dvco.x = -dvco.x + elif axis == 'Y': + dvco.y = -dvco.y + elif axis == 'Z': + dvco.z = -dvco.z + + if is_vector_similar(svco, dvco, error): + dl[uv_layer].uv = suv.copy() + + +def get_face_center(face): + """ + Get center coordinate of the face + """ + center = Vector((0.0, 0.0, 0.0)) + for v in face.verts: + center = center + v.co + + return center / len(face.verts) + + +class MirrorUVImpl: + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + + def execute(self, ops_obj, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + + error = ops_obj.error + axis = ops_obj.axis + + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + if not bm.loops.layers.uv: + ops_obj.report({'WARNING'}, + "Object must have more than one UV map") + return {'CANCELLED'} + uv_layer = bm.loops.layers.uv.verify() + + faces = [f for f in bm.faces if f.select] + for f_dst in faces: + count = len(f_dst.verts) + for f_src in bm.faces: + # check if this is a candidate to do mirror UV + if f_src.index == f_dst.index: + continue + if count != len(f_src.verts): + continue + + # test if the vertices x values are the same sign + dst = get_face_center(f_dst) + src = get_face_center(f_src) + if (dst.x > 0 and src.x > 0) or (dst.x < 0 and src.x < 0): + continue + + # invert source axis + if axis == 'X': + src.x = -src.x + elif axis == 'Y': + src.y = -src.z + elif axis == 'Z': + src.z = -src.z + + # do mirror UV + if is_vector_similar(dst, src, error): + mirror_uvs( + uv_layer, f_src, f_dst, ops_obj.axis, ops_obj.error) + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/uv_magic_uv/impl/move_uv_impl.py b/uv_magic_uv/impl/move_uv_impl.py new file mode 100644 index 00000000..ce507fba --- /dev/null +++ b/uv_magic_uv/impl/move_uv_impl.py @@ -0,0 +1,166 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "imdjs, Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bmesh +from mathutils import Vector + +from .. import common + + +__all__ = [ + 'is_valid_context', + 'find_uv', + 'MoveUVImpl', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +def find_uv(context): + bm = bmesh.from_edit_mesh(context.object.data) + topology_dict = [] + uvs = [] + active_uv = bm.loops.layers.uv.active + for fidx, f in enumerate(bm.faces): + for vidx, v in enumerate(f.verts): + if v.select: + uvs.append(f.loops[vidx][active_uv].uv.copy()) + topology_dict.append([fidx, vidx]) + + return topology_dict, uvs + + +class MoveUVImpl(): + __running = False + + def __init__(self): + self.__topology_dict = [] + self.__prev_mouse = Vector((0.0, 0.0)) + self.__offset_uv = Vector((0.0, 0.0)) + self.__prev_offset_uv = Vector((0.0, 0.0)) + self.__first_time = True + self.__ini_uvs = [] + self.__operating = False + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return False + if cls.is_running(context): + return False + return is_valid_context(context) + + @classmethod + def is_running(cls, _): + return cls.__running + + def modal(self, _, context, event): + if self.__first_time is True: + self.__prev_mouse = Vector(( + event.mouse_region_x, event.mouse_region_y)) + self.__first_time = False + return {'RUNNING_MODAL'} + + # move UV + div = 10000 + self.__offset_uv += Vector(( + (event.mouse_region_x - self.__prev_mouse.x) / div, + (event.mouse_region_y - self.__prev_mouse.y) / div)) + ouv = self.__offset_uv + pouv = self.__prev_offset_uv + vec = Vector((ouv.x - ouv.y, ouv.x + ouv.y)) + dv = vec - pouv + self.__prev_offset_uv = vec + self.__prev_mouse = Vector(( + event.mouse_region_x, event.mouse_region_y)) + + # check if operation is started + if not self.__operating: + if event.type == 'LEFTMOUSE' and event.value == 'RELEASE': + self.__operating = True + return {'RUNNING_MODAL'} + + # update UV + obj = context.object + bm = bmesh.from_edit_mesh(obj.data) + active_uv = bm.loops.layers.uv.active + for fidx, vidx in self.__topology_dict: + l = bm.faces[fidx].loops[vidx] + l[active_uv].uv = l[active_uv].uv + dv + bmesh.update_edit_mesh(obj.data) + + # check mouse preference + if context.user_preferences.inputs.select_mouse == 'RIGHT': + confirm_btn = 'LEFTMOUSE' + cancel_btn = 'RIGHTMOUSE' + else: + confirm_btn = 'RIGHTMOUSE' + cancel_btn = 'LEFTMOUSE' + + # cancelled + if event.type == cancel_btn and event.value == 'PRESS': + for (fidx, vidx), uv in zip(self.__topology_dict, self.__ini_uvs): + bm.faces[fidx].loops[vidx][active_uv].uv = uv + MoveUVImpl.__running = False + return {'FINISHED'} + # confirmed + if event.type == confirm_btn and event.value == 'PRESS': + MoveUVImpl.__running = False + return {'FINISHED'} + + return {'RUNNING_MODAL'} + + def execute(self, ops_obj, context): + MoveUVImpl.__running = True + self.__operating = False + self.__first_time = True + + context.window_manager.modal_handler_add(ops_obj) + self.__topology_dict, self.__ini_uvs = find_uv(context) + + if context.area: + context.area.tag_redraw() + + return {'RUNNING_MODAL'} diff --git a/uv_magic_uv/impl/transfer_uv_impl.py b/uv_magic_uv/impl/transfer_uv_impl.py new file mode 100644 index 00000000..adc97352 --- /dev/null +++ b/uv_magic_uv/impl/transfer_uv_impl.py @@ -0,0 +1,330 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "imdjs, Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +from collections import OrderedDict + +import bpy +import bmesh + +from .. import common + + +__all__ = [ + 'is_valid_context', + 'get_uv_layer', + 'main_parse', + 'parse_faces', + 'get_new_shared_faces', + 'get_other_verts_edges', + 'get_selected_src_faces', + 'paste_uv', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +def get_uv_layer(ops_obj, bm): + # get UV layer + if not bm.loops.layers.uv: + ops_obj.report({'WARNING'}, "Object must have more than one UV map") + return None + uv_layer = bm.loops.layers.uv.verify() + + return uv_layer + + +def main_parse(ops_obj, uv_layer, sel_faces, active_face, active_face_nor): + all_sorted_faces = OrderedDict() # This is the main stuff + + used_verts = set() + used_edges = set() + + faces_to_parse = [] + + # get shared edge of two faces + cross_edges = [] + for edge in active_face.edges: + if edge in sel_faces[0].edges and edge in sel_faces[1].edges: + cross_edges.append(edge) + + # parse two selected faces + if cross_edges and len(cross_edges) == 1: + shared_edge = cross_edges[0] + vert1 = None + vert2 = None + + dot_n = active_face_nor.normalized() + edge_vec_1 = (shared_edge.verts[1].co - shared_edge.verts[0].co) + edge_vec_len = edge_vec_1.length + edge_vec_1 = edge_vec_1.normalized() + + af_center = active_face.calc_center_median() + af_vec = shared_edge.verts[0].co + (edge_vec_1 * (edge_vec_len * 0.5)) + af_vec = (af_vec - af_center).normalized() + + if af_vec.cross(edge_vec_1).dot(dot_n) > 0: + vert1 = shared_edge.verts[0] + vert2 = shared_edge.verts[1] + else: + vert1 = shared_edge.verts[1] + vert2 = shared_edge.verts[0] + + # get active face stuff and uvs + face_stuff = get_other_verts_edges( + active_face, vert1, vert2, shared_edge, uv_layer) + all_sorted_faces[active_face] = face_stuff + used_verts.update(active_face.verts) + used_edges.update(active_face.edges) + + # get first selected face stuff and uvs as they share shared_edge + second_face = sel_faces[0] + if second_face is active_face: + second_face = sel_faces[1] + face_stuff = get_other_verts_edges( + second_face, vert1, vert2, shared_edge, uv_layer) + all_sorted_faces[second_face] = face_stuff + used_verts.update(second_face.verts) + used_edges.update(second_face.edges) + + # first Grow + faces_to_parse.append(active_face) + faces_to_parse.append(second_face) + + else: + ops_obj.report({'WARNING'}, "Two faces should share one edge") + return None + + # parse all faces + while True: + new_parsed_faces = [] + if not faces_to_parse: + break + for face in faces_to_parse: + face_stuff = all_sorted_faces.get(face) + new_faces = parse_faces(face, face_stuff, used_verts, used_edges, + all_sorted_faces, uv_layer) + if new_faces is None: + ops_obj.report({'WARNING'}, "More than 2 faces share edge") + return None + + new_parsed_faces += new_faces + faces_to_parse = new_parsed_faces + + return all_sorted_faces + + +def parse_faces(check_face, face_stuff, used_verts, used_edges, + all_sorted_faces, uv_layer): + """recurse faces around the new_grow only""" + + new_shared_faces = [] + for sorted_edge in face_stuff[1]: + shared_faces = sorted_edge.link_faces + if shared_faces: + if len(shared_faces) > 2: + bpy.ops.mesh.select_all(action='DESELECT') + for face_sel in shared_faces: + face_sel.select = True + shared_faces = [] + return None + + clear_shared_faces = get_new_shared_faces( + check_face, sorted_edge, shared_faces, all_sorted_faces.keys()) + if clear_shared_faces: + shared_face = clear_shared_faces[0] + # get vertices of the edge + vert1 = sorted_edge.verts[0] + vert2 = sorted_edge.verts[1] + + common.debug_print(face_stuff[0], vert1, vert2) + if face_stuff[0].index(vert1) > face_stuff[0].index(vert2): + vert1 = sorted_edge.verts[1] + vert2 = sorted_edge.verts[0] + + common.debug_print(shared_face.verts, vert1, vert2) + new_face_stuff = get_other_verts_edges( + shared_face, vert1, vert2, sorted_edge, uv_layer) + all_sorted_faces[shared_face] = new_face_stuff + used_verts.update(shared_face.verts) + used_edges.update(shared_face.edges) + + if common.is_debug_mode(): + shared_face.select = True # test which faces are parsed + + new_shared_faces.append(shared_face) + + return new_shared_faces + + +def get_new_shared_faces(orig_face, shared_edge, check_faces, used_faces): + shared_faces = [] + + for face in check_faces: + is_shared_edge = shared_edge in face.edges + not_used = face not in used_faces + not_orig = face is not orig_face + not_hide = face.hide is False + if is_shared_edge and not_used and not_orig and not_hide: + shared_faces.append(face) + + return shared_faces + + +def get_other_verts_edges(face, vert1, vert2, first_edge, uv_layer): + face_edges = [first_edge] + face_verts = [vert1, vert2] + face_loops = [] + + other_edges = [edge for edge in face.edges if edge not in face_edges] + + for _ in range(len(other_edges)): + found_edge = None + # get sorted verts and edges + for edge in other_edges: + if face_verts[-1] in edge.verts: + other_vert = edge.other_vert(face_verts[-1]) + + if other_vert not in face_verts: + face_verts.append(other_vert) + + found_edge = edge + if found_edge not in face_edges: + face_edges.append(edge) + break + + other_edges.remove(found_edge) + + # get sorted uvs + for vert in face_verts: + for loop in face.loops: + if loop.vert is vert: + face_loops.append(loop[uv_layer]) + break + + return [face_verts, face_edges, face_loops] + + +def get_selected_src_faces(ops_obj, bm, uv_layer): + topology_copied = [] + + # get selected faces + active_face = bm.faces.active + sel_faces = [face for face in bm.faces if face.select] + if len(sel_faces) != 2: + ops_obj.report({'WARNING'}, "Two faces must be selected") + return None + if not active_face or active_face not in sel_faces: + ops_obj.report({'WARNING'}, "Two faces must be active") + return None + + # parse all faces according to selection + active_face_nor = active_face.normal.copy() + all_sorted_faces = main_parse(ops_obj, uv_layer, sel_faces, active_face, + active_face_nor) + + if all_sorted_faces: + for face_data in all_sorted_faces.values(): + edges = face_data[1] + uv_loops = face_data[2] + uvs = [l.uv.copy() for l in uv_loops] + pin_uvs = [l.pin_uv for l in uv_loops] + seams = [e.seam for e in edges] + topology_copied.append([uvs, pin_uvs, seams]) + else: + return None + + return topology_copied + + +def paste_uv(ops_obj, bm, uv_layer, src_faces, invert_normals, copy_seams): + # get selection history + all_sel_faces = [e for e in bm.select_history + if isinstance(e, bmesh.types.BMFace) and e.select] + if len(all_sel_faces) % 2 != 0: + ops_obj.report({'WARNING'}, "Two faces must be selected") + return -1 + + # parse selection history + for i, _ in enumerate(all_sel_faces): + if (i == 0) or (i % 2 == 0): + continue + sel_faces = [all_sel_faces[i - 1], all_sel_faces[i]] + active_face = all_sel_faces[i] + + # parse all faces according to selection history + active_face_nor = active_face.normal.copy() + if invert_normals: + active_face_nor.negate() + all_sorted_faces = main_parse(ops_obj, uv_layer, sel_faces, + active_face, active_face_nor) + + if all_sorted_faces: + # check amount of copied/pasted faces + if len(all_sorted_faces) != len(src_faces): + ops_obj.report({'WARNING'}, + "Mesh has different amount of faces") + return -1 + + for j, face_data in enumerate(all_sorted_faces.values()): + copied_data = src_faces[j] + + # check amount of copied/pasted verts + if len(copied_data[0]) != len(face_data[2]): + bpy.ops.mesh.select_all(action='DESELECT') + # select problematic face + list(all_sorted_faces.keys())[j].select = True + ops_obj.report({'WARNING'}, + "Face have different amount of vertices") + return 0 + + for k, (edge, uvloop) in enumerate(zip(face_data[1], + face_data[2])): + uvloop.uv = copied_data[0][k] + uvloop.pin_uv = copied_data[1][k] + if copy_seams: + edge.seam = copied_data[2][k] + else: + return -1 + + return 0 diff --git a/uv_magic_uv/impl/uvw_impl.py b/uv_magic_uv/impl/uvw_impl.py new file mode 100644 index 00000000..e815f54f --- /dev/null +++ b/uv_magic_uv/impl/uvw_impl.py @@ -0,0 +1,154 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "imdjs, Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + + +from math import sin, cos, pi + +from mathutils import Vector + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +def get_uv_layer(ops_obj, bm, assign_uvmap): + # get UV layer + if not bm.loops.layers.uv: + if assign_uvmap: + bm.loops.layers.uv.new() + else: + ops_obj.report({'WARNING'}, + "Object must have more than one UV map") + return None + uv_layer = bm.loops.layers.uv.verify() + + return uv_layer + + +def apply_box_map(bm, uv_layer, size, offset, rotation, tex_aspect): + scale = 1.0 / size + + sx = 1.0 * scale + sy = 1.0 * scale + sz = 1.0 * scale + ofx = offset[0] + ofy = offset[1] + ofz = offset[2] + rx = rotation[0] * pi / 180.0 + ry = rotation[1] * pi / 180.0 + rz = rotation[2] * pi / 180.0 + aspect = tex_aspect + + sel_faces = [f for f in bm.faces if f.select] + + # update UV coordinate + for f in sel_faces: + n = f.normal + for l in f.loops: + co = l.vert.co + x = co.x * sx + y = co.y * sy + z = co.z * sz + + # X-plane + if abs(n[0]) >= abs(n[1]) and abs(n[0]) >= abs(n[2]): + if n[0] >= 0.0: + u = (y - ofy) * cos(rx) + (z - ofz) * sin(rx) + v = -(y * aspect - ofy) * sin(rx) + \ + (z * aspect - ofz) * cos(rx) + else: + u = -(y - ofy) * cos(rx) + (z - ofz) * sin(rx) + v = (y * aspect - ofy) * sin(rx) + \ + (z * aspect - ofz) * cos(rx) + # Y-plane + elif abs(n[1]) >= abs(n[0]) and abs(n[1]) >= abs(n[2]): + if n[1] >= 0.0: + u = -(x - ofx) * cos(ry) + (z - ofz) * sin(ry) + v = (x * aspect - ofx) * sin(ry) + \ + (z * aspect - ofz) * cos(ry) + else: + u = (x - ofx) * cos(ry) + (z - ofz) * sin(ry) + v = -(x * aspect - ofx) * sin(ry) + \ + (z * aspect - ofz) * cos(ry) + # Z-plane + else: + if n[2] >= 0.0: + u = (x - ofx) * cos(rz) + (y - ofy) * sin(rz) + v = -(x * aspect - ofx) * sin(rz) + \ + (y * aspect - ofy) * cos(rz) + else: + u = -(x - ofx) * cos(rz) - (y + ofy) * sin(rz) + v = -(x * aspect + ofx) * sin(rz) + \ + (y * aspect - ofy) * cos(rz) + + l[uv_layer].uv = Vector((u, v)) + + +def apply_planer_map(bm, uv_layer, size, offset, rotation, tex_aspect): + scale = 1.0 / size + + sx = 1.0 * scale + sy = 1.0 * scale + ofx = offset[0] + ofy = offset[1] + rz = rotation * pi / 180.0 + aspect = tex_aspect + + sel_faces = [f for f in bm.faces if f.select] + + # calculate average of normal + n_ave = Vector((0.0, 0.0, 0.0)) + for f in sel_faces: + n_ave = n_ave + f.normal + q = n_ave.rotation_difference(Vector((0.0, 0.0, 1.0))) + + # update UV coordinate + for f in sel_faces: + for l in f.loops: + co = q @ l.vert.co + x = co.x * sx + y = co.y * sy + + u = x * cos(rz) - y * sin(rz) + ofx + v = -x * aspect * sin(rz) - y * aspect * cos(rz) + ofy + + l[uv_layer].uv = Vector((u, v)) diff --git a/uv_magic_uv/legacy/__init__.py b/uv_magic_uv/legacy/__init__.py new file mode 100644 index 00000000..794d02bc --- /dev/null +++ b/uv_magic_uv/legacy/__init__.py @@ -0,0 +1,38 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +if "bpy" in locals(): + import importlib + importlib.reload(op) + importlib.reload(ui) + importlib.reload(properites) + importlib.reload(preferences) +else: + from . import op + from . import ui + from . import properites + from . import preferences + +import bpy diff --git a/uv_magic_uv/legacy/op/__init__.py b/uv_magic_uv/legacy/op/__init__.py new file mode 100644 index 00000000..9535b76d --- /dev/null +++ b/uv_magic_uv/legacy/op/__init__.py @@ -0,0 +1,74 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +if "bpy" in locals(): + import importlib + importlib.reload(align_uv) + importlib.reload(align_uv_cursor) + importlib.reload(copy_paste_uv) + importlib.reload(copy_paste_uv_object) + importlib.reload(copy_paste_uv_uvedit) + importlib.reload(flip_rotate_uv) + importlib.reload(mirror_uv) + importlib.reload(move_uv) + importlib.reload(pack_uv) + importlib.reload(preserve_uv_aspect) + importlib.reload(select_uv) + importlib.reload(smooth_uv) + importlib.reload(texture_lock) + importlib.reload(texture_projection) + importlib.reload(texture_wrap) + importlib.reload(transfer_uv) + importlib.reload(unwrap_constraint) + importlib.reload(uv_bounding_box) + importlib.reload(uv_inspection) + importlib.reload(uv_sculpt) + importlib.reload(uvw) + importlib.reload(world_scale_uv) +else: + from . import align_uv + from . import align_uv_cursor + from . import copy_paste_uv + from . import copy_paste_uv_object + from . import copy_paste_uv_uvedit + from . import flip_rotate_uv + from . import mirror_uv + from . import move_uv + from . import pack_uv + from . import preserve_uv_aspect + from . import select_uv + from . import smooth_uv + from . import texture_lock + from . import texture_projection + from . import texture_wrap + from . import transfer_uv + from . import unwrap_constraint + from . import uv_bounding_box + from . import uv_inspection + from . import uv_sculpt + from . import uvw + from . import world_scale_uv + +import bpy diff --git a/uv_magic_uv/legacy/op/align_uv.py b/uv_magic_uv/legacy/op/align_uv.py new file mode 100644 index 00000000..9d0ff5f4 --- /dev/null +++ b/uv_magic_uv/legacy/op/align_uv.py @@ -0,0 +1,988 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "imdjs, Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import math +from math import atan2, tan, sin, cos + +import bpy +import bmesh +from mathutils import Vector +from bpy.props import EnumProperty, BoolProperty, FloatProperty + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_AlignUV_Circle', + 'MUV_OT_AlignUV_Straighten', + 'MUV_OT_AlignUV_Axis', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "align_uv" + + @classmethod + def init_props(cls, scene): + scene.muv_align_uv_enabled = BoolProperty( + name="Align UV Enabled", + description="Align UV is enabled", + default=False + ) + scene.muv_align_uv_transmission = BoolProperty( + name="Transmission", + description="Align linked UVs", + default=False + ) + scene.muv_align_uv_select = BoolProperty( + name="Select", + description="Select UVs which are aligned", + default=False + ) + scene.muv_align_uv_vertical = BoolProperty( + name="Vert-Infl (Vertical)", + description="Align vertical direction influenced " + "by mesh vertex proportion", + default=False + ) + scene.muv_align_uv_horizontal = BoolProperty( + name="Vert-Infl (Horizontal)", + description="Align horizontal direction influenced " + "by mesh vertex proportion", + default=False + ) + scene.muv_align_uv_mesh_infl = FloatProperty( + name="Mesh Influence", + description="Influence rate of mesh vertex", + min=0.0, + max=1.0, + default=0.0 + ) + scene.muv_align_uv_location = EnumProperty( + name="Location", + description="Align location", + items=[ + ('LEFT_TOP', "Left/Top", "Align to Left or Top"), + ('MIDDLE', "Middle", "Align to middle"), + ('RIGHT_BOTTOM', "Right/Bottom", "Align to Right or Bottom") + ], + default='MIDDLE' + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_align_uv_enabled + del scene.muv_align_uv_transmission + del scene.muv_align_uv_select + del scene.muv_align_uv_vertical + del scene.muv_align_uv_horizontal + del scene.muv_align_uv_mesh_infl + del scene.muv_align_uv_location + + +# get sum vertex length of loop sequences +def get_loop_vert_len(loops): + length = 0 + for l1, l2 in zip(loops[:-1], loops[1:]): + diff = l2.vert.co - l1.vert.co + length = length + abs(diff.length) + + return length + + +# get sum uv length of loop sequences +def get_loop_uv_len(loops, uv_layer): + length = 0 + for l1, l2 in zip(loops[:-1], loops[1:]): + diff = l2[uv_layer].uv - l1[uv_layer].uv + length = length + abs(diff.length) + + return length + + +# get center/radius of circle by 3 vertices +def get_circle(v): + alpha = atan2((v[0].y - v[1].y), (v[0].x - v[1].x)) + math.pi / 2 + beta = atan2((v[1].y - v[2].y), (v[1].x - v[2].x)) + math.pi / 2 + ex = (v[0].x + v[1].x) / 2.0 + ey = (v[0].y + v[1].y) / 2.0 + fx = (v[1].x + v[2].x) / 2.0 + fy = (v[1].y + v[2].y) / 2.0 + cx = (ey - fy - ex * tan(alpha) + fx * tan(beta)) / \ + (tan(beta) - tan(alpha)) + cy = ey - (ex - cx) * tan(alpha) + center = Vector((cx, cy)) + + r = v[0] - center + radian = r.length + + return center, radian + + +# get position on circle with same arc length +def calc_v_on_circle(v, center, radius): + base = v[0] + theta = atan2(base.y - center.y, base.x - center.x) + new_v = [] + for i in range(len(v)): + angle = theta + i * 2 * math.pi / len(v) + new_v.append(Vector((center.x + radius * sin(angle), + center.y + radius * cos(angle)))) + + return new_v + + +@BlClassRegistry(legacy=True) +class MUV_OT_AlignUV_Circle(bpy.types.Operator): + + bl_idname = "uv.muv_align_uv_operator_circle" + bl_label = "Align UV (Circle)" + bl_description = "Align UV coordinates to Circle" + bl_options = {'REGISTER', 'UNDO'} + + transmission = BoolProperty( + name="Transmission", + description="Align linked UVs", + default=False + ) + select = BoolProperty( + name="Select", + description="Select UVs which are aligned", + default=False + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + + def execute(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.verify() + + # loop_seqs[horizontal][vertical][loop] + loop_seqs, error = common.get_loop_sequences(bm, uv_layer, True) + if not loop_seqs: + self.report({'WARNING'}, error) + return {'CANCELLED'} + + # get circle and new UVs + uvs = [hseq[0][0][uv_layer].uv.copy() for hseq in loop_seqs] + c, r = get_circle(uvs[0:3]) + new_uvs = calc_v_on_circle(uvs, c, r) + + # check center UV of circle + center = loop_seqs[0][-1][0].vert + for hseq in loop_seqs[1:]: + if len(hseq[-1]) != 1: + self.report({'WARNING'}, "Last face must be triangle") + return {'CANCELLED'} + if hseq[-1][0].vert != center: + self.report({'WARNING'}, "Center must be identical") + return {'CANCELLED'} + + # align to circle + if self.transmission: + for hidx, hseq in enumerate(loop_seqs): + for vidx, pair in enumerate(hseq): + all_ = int((len(hseq) + 1) / 2) + r = (all_ - int((vidx + 1) / 2)) / all_ + pair[0][uv_layer].uv = c + (new_uvs[hidx] - c) * r + if self.select: + pair[0][uv_layer].select = True + + if len(pair) < 2: + continue + # for quad polygon + next_hidx = (hidx + 1) % len(loop_seqs) + pair[1][uv_layer].uv = c + ((new_uvs[next_hidx]) - c) * r + if self.select: + pair[1][uv_layer].select = True + else: + for hidx, hseq in enumerate(loop_seqs): + pair = hseq[0] + pair[0][uv_layer].uv = new_uvs[hidx] + pair[1][uv_layer].uv = new_uvs[(hidx + 1) % len(loop_seqs)] + if self.select: + pair[0][uv_layer].select = True + pair[1][uv_layer].select = True + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} + + +# get accumulate vertex lengths of loop sequences +def get_loop_vert_accum_len(loops): + accum_lengths = [0.0] + length = 0 + for l1, l2 in zip(loops[:-1], loops[1:]): + diff = l2.vert.co - l1.vert.co + length = length + abs(diff.length) + accum_lengths.extend([length]) + + return accum_lengths + + +# get sum uv length of loop sequences +def get_loop_uv_accum_len(loops, uv_layer): + accum_lengths = [0.0] + length = 0 + for l1, l2 in zip(loops[:-1], loops[1:]): + diff = l2[uv_layer].uv - l1[uv_layer].uv + length = length + abs(diff.length) + accum_lengths.extend([length]) + + return accum_lengths + + +# get horizontal differential of UV influenced by mesh vertex +def get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl): + common.debug_print( + "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx)) + + base_uv = loop_seqs[0][vidx][0][uv_layer].uv.copy() + + # calculate original length + hloops = [] + for s in loop_seqs: + hloops.extend([s[vidx][0], s[vidx][1]]) + total_vlen = get_loop_vert_len(hloops) + accum_vlens = get_loop_vert_accum_len(hloops) + total_uvlen = get_loop_uv_len(hloops, uv_layer) + accum_uvlens = get_loop_uv_accum_len(hloops, uv_layer) + orig_uvs = [l[uv_layer].uv.copy() for l in hloops] + + # calculate target length + tgt_noinfl = total_uvlen * (hidx + pidx) / len(loop_seqs) + tgt_infl = total_uvlen * accum_vlens[hidx * 2 + pidx] / total_vlen + target_length = tgt_noinfl * (1 - infl) + tgt_infl * infl + common.debug_print(target_length) + common.debug_print(accum_uvlens) + + # calculate target UV + for i in range(len(accum_uvlens[:-1])): + # get line segment which UV will be placed + if ((accum_uvlens[i] <= target_length) and + (accum_uvlens[i + 1] > target_length)): + tgt_seg_len = target_length - accum_uvlens[i] + seg_len = accum_uvlens[i + 1] - accum_uvlens[i] + uv1 = orig_uvs[i] + uv2 = orig_uvs[i + 1] + target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len + break + elif i == (len(accum_uvlens[:-1]) - 1): + if accum_uvlens[i + 1] != target_length: + raise Exception( + "Internal Error: horizontal_target_length={}" + " is not equal to {}" + .format(target_length, accum_uvlens[-1])) + tgt_seg_len = target_length - accum_uvlens[i] + seg_len = accum_uvlens[i + 1] - accum_uvlens[i] + uv1 = orig_uvs[i] + uv2 = orig_uvs[i + 1] + target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len + break + else: + raise Exception("Internal Error: horizontal_target_length={}" + " is not in range {} to {}" + .format(target_length, accum_uvlens[0], + accum_uvlens[-1])) + + return target_uv + + +# --------------------- LOOP STRUCTURE ---------------------- +# +# loops[hidx][vidx][pidx] +# hidx: horizontal index +# vidx: vertical index +# pidx: pair index +# +# <----- horizontal -----> +# +# (hidx, vidx, pidx) = (0, 3, 0) +# | (hidx, vidx, pidx) = (1, 3, 0) +# v v +# ^ o --- oo --- o +# | | || | +# vertical | o --- oo --- o <- (hidx, vidx, pidx) +# | o --- oo --- o = (1, 2, 1) +# | | || | +# v o --- oo --- o +# ^ ^ +# | (hidx, vidx, pidx) = (1, 0, 1) +# (hidx, vidx, pidx) = (0, 0, 0) +# +# ----------------------------------------------------------- + + +# get vertical differential of UV influenced by mesh vertex +def get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl): + common.debug_print( + "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx)) + + base_uv = loop_seqs[hidx][0][pidx][uv_layer].uv.copy() + + # calculate original length + vloops = [] + for s in loop_seqs[hidx]: + vloops.append(s[pidx]) + total_vlen = get_loop_vert_len(vloops) + accum_vlens = get_loop_vert_accum_len(vloops) + total_uvlen = get_loop_uv_len(vloops, uv_layer) + accum_uvlens = get_loop_uv_accum_len(vloops, uv_layer) + orig_uvs = [l[uv_layer].uv.copy() for l in vloops] + + # calculate target length + tgt_noinfl = total_uvlen * int((vidx + 1) / 2) / len(loop_seqs) + tgt_infl = total_uvlen * accum_vlens[vidx] / total_vlen + target_length = tgt_noinfl * (1 - infl) + tgt_infl * infl + common.debug_print(target_length) + common.debug_print(accum_uvlens) + + # calculate target UV + for i in range(len(accum_uvlens[:-1])): + # get line segment which UV will be placed + if ((accum_uvlens[i] <= target_length) and + (accum_uvlens[i + 1] > target_length)): + tgt_seg_len = target_length - accum_uvlens[i] + seg_len = accum_uvlens[i + 1] - accum_uvlens[i] + uv1 = orig_uvs[i] + uv2 = orig_uvs[i + 1] + target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len + break + elif i == (len(accum_uvlens[:-1]) - 1): + if accum_uvlens[i + 1] != target_length: + raise Exception("Internal Error: horizontal_target_length={}" + " is not equal to {}" + .format(target_length, accum_uvlens[-1])) + tgt_seg_len = target_length - accum_uvlens[i] + seg_len = accum_uvlens[i + 1] - accum_uvlens[i] + uv1 = orig_uvs[i] + uv2 = orig_uvs[i + 1] + target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len + break + else: + raise Exception("Internal Error: horizontal_target_length={}" + " is not in range {} to {}" + .format(target_length, accum_uvlens[0], + accum_uvlens[-1])) + + return target_uv + + +# get horizontal differential of UV no influenced +def get_hdiff_uv(uv_layer, loop_seqs, hidx): + base_uv = loop_seqs[0][0][0][uv_layer].uv.copy() + h_uv = loop_seqs[-1][0][1][uv_layer].uv.copy() - base_uv + + return hidx * h_uv / len(loop_seqs) + + +# get vertical differential of UV no influenced +def get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx): + base_uv = loop_seqs[0][0][0][uv_layer].uv.copy() + v_uv = loop_seqs[0][-1][0][uv_layer].uv.copy() - base_uv + + hseq = loop_seqs[hidx] + return int((vidx + 1) / 2) * v_uv / (len(hseq) / 2) + + +@BlClassRegistry(legacy=True) +class MUV_OT_AlignUV_Straighten(bpy.types.Operator): + + bl_idname = "uv.muv_align_uv_operator_straighten" + bl_label = "Align UV (Straighten)" + bl_description = "Straighten UV coordinates" + bl_options = {'REGISTER', 'UNDO'} + + transmission = BoolProperty( + name="Transmission", + description="Align linked UVs", + default=False + ) + select = BoolProperty( + name="Select", + description="Select UVs which are aligned", + default=False + ) + vertical = BoolProperty( + name="Vert-Infl (Vertical)", + description="Align vertical direction influenced " + "by mesh vertex proportion", + default=False + ) + horizontal = BoolProperty( + name="Vert-Infl (Horizontal)", + description="Align horizontal direction influenced " + "by mesh vertex proportion", + default=False + ) + mesh_infl = FloatProperty( + name="Mesh Influence", + description="Influence rate of mesh vertex", + min=0.0, + max=1.0, + default=0.0 + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + + # selected and paralleled UV loop sequence will be aligned + def __align_w_transmission(self, loop_seqs, uv_layer): + base_uv = loop_seqs[0][0][0][uv_layer].uv.copy() + + # calculate diff UVs + diff_uvs = [] + # hseq[vertical][loop] + for hidx, hseq in enumerate(loop_seqs): + # pair[loop] + diffs = [] + for vidx in range(0, len(hseq), 2): + if self.horizontal: + hdiff_uvs = [ + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0, + self.mesh_infl), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1, + self.mesh_infl), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 0, self.mesh_infl), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 1, self.mesh_infl), + ] + else: + hdiff_uvs = [ + get_hdiff_uv(uv_layer, loop_seqs, hidx), + get_hdiff_uv(uv_layer, loop_seqs, hidx + 1), + get_hdiff_uv(uv_layer, loop_seqs, hidx), + get_hdiff_uv(uv_layer, loop_seqs, hidx + 1) + ] + if self.vertical: + vdiff_uvs = [ + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0, + self.mesh_infl), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1, + self.mesh_infl), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 0, self.mesh_infl), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 1, self.mesh_infl), + ] + else: + vdiff_uvs = [ + get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx), + get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx), + get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx), + get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx) + ] + diffs.append([hdiff_uvs, vdiff_uvs]) + diff_uvs.append(diffs) + + # update UV + for hseq, diffs in zip(loop_seqs, diff_uvs): + for vidx in range(0, len(hseq), 2): + loops = [ + hseq[vidx][0], hseq[vidx][1], + hseq[vidx + 1][0], hseq[vidx + 1][1] + ] + for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0], + diffs[int(vidx / 2)][1]): + l[uv_layer].uv = base_uv + hdiff + vdiff + if self.select: + l[uv_layer].select = True + + # only selected UV loop sequence will be aligned + def __align_wo_transmission(self, loop_seqs, uv_layer): + base_uv = loop_seqs[0][0][0][uv_layer].uv.copy() + + h_uv = loop_seqs[-1][0][1][uv_layer].uv.copy() - base_uv + for hidx, hseq in enumerate(loop_seqs): + # only selected loop pair is targeted + pair = hseq[0] + hdiff_uv_0 = hidx * h_uv / len(loop_seqs) + hdiff_uv_1 = (hidx + 1) * h_uv / len(loop_seqs) + pair[0][uv_layer].uv = base_uv + hdiff_uv_0 + pair[1][uv_layer].uv = base_uv + hdiff_uv_1 + if self.select: + pair[0][uv_layer].select = True + pair[1][uv_layer].select = True + + def __align(self, loop_seqs, uv_layer): + if self.transmission: + self.__align_w_transmission(loop_seqs, uv_layer) + else: + self.__align_wo_transmission(loop_seqs, uv_layer) + + def execute(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.verify() + + # loop_seqs[horizontal][vertical][loop] + loop_seqs, error = common.get_loop_sequences(bm, uv_layer) + if not loop_seqs: + self.report({'WARNING'}, error) + return {'CANCELLED'} + + # align + self.__align(loop_seqs, uv_layer) + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} + + +@BlClassRegistry(legacy=True) +class MUV_OT_AlignUV_Axis(bpy.types.Operator): + + bl_idname = "uv.muv_align_uv_operator_axis" + bl_label = "Align UV (XY-Axis)" + bl_description = "Align UV to XY-axis" + bl_options = {'REGISTER', 'UNDO'} + + transmission = BoolProperty( + name="Transmission", + description="Align linked UVs", + default=False + ) + select = BoolProperty( + name="Select", + description="Select UVs which are aligned", + default=False + ) + vertical = BoolProperty( + name="Vert-Infl (Vertical)", + description="Align vertical direction influenced " + "by mesh vertex proportion", + default=False + ) + horizontal = BoolProperty( + name="Vert-Infl (Horizontal)", + description="Align horizontal direction influenced " + "by mesh vertex proportion", + default=False + ) + location = EnumProperty( + name="Location", + description="Align location", + items=[ + ('LEFT_TOP', "Left/Top", "Align to Left or Top"), + ('MIDDLE', "Middle", "Align to middle"), + ('RIGHT_BOTTOM', "Right/Bottom", "Align to Right or Bottom") + ], + default='MIDDLE' + ) + mesh_infl = FloatProperty( + name="Mesh Influence", + description="Influence rate of mesh vertex", + min=0.0, + max=1.0, + default=0.0 + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + + # get min/max of UV + def __get_uv_max_min(self, loop_seqs, uv_layer): + uv_max = Vector((-1000000.0, -1000000.0)) + uv_min = Vector((1000000.0, 1000000.0)) + for hseq in loop_seqs: + for l in hseq[0]: + uv = l[uv_layer].uv + uv_max.x = max(uv.x, uv_max.x) + uv_max.y = max(uv.y, uv_max.y) + uv_min.x = min(uv.x, uv_min.x) + uv_min.y = min(uv.y, uv_min.y) + + return uv_max, uv_min + + # get UV differentiation when UVs are aligned to X-axis + def __get_x_axis_align_diff_uvs(self, loop_seqs, uv_layer, uv_min, + width, height): + diff_uvs = [] + for hidx, hseq in enumerate(loop_seqs): + pair = hseq[0] + luv0 = pair[0][uv_layer] + luv1 = pair[1][uv_layer] + target_uv0 = Vector((0.0, 0.0)) + target_uv1 = Vector((0.0, 0.0)) + if self.location == 'RIGHT_BOTTOM': + target_uv0.y = target_uv1.y = uv_min.y + elif self.location == 'MIDDLE': + target_uv0.y = target_uv1.y = uv_min.y + height * 0.5 + elif self.location == 'LEFT_TOP': + target_uv0.y = target_uv1.y = uv_min.y + height + if luv0.uv.x < luv1.uv.x: + target_uv0.x = uv_min.x + hidx * width / len(loop_seqs) + target_uv1.x = uv_min.x + (hidx + 1) * width / len(loop_seqs) + else: + target_uv0.x = uv_min.x + (hidx + 1) * width / len(loop_seqs) + target_uv1.x = uv_min.x + hidx * width / len(loop_seqs) + diff_uvs.append([target_uv0 - luv0.uv, target_uv1 - luv1.uv]) + + return diff_uvs + + # get UV differentiation when UVs are aligned to Y-axis + def __get_y_axis_align_diff_uvs(self, loop_seqs, uv_layer, uv_min, + width, height): + diff_uvs = [] + for hidx, hseq in enumerate(loop_seqs): + pair = hseq[0] + luv0 = pair[0][uv_layer] + luv1 = pair[1][uv_layer] + target_uv0 = Vector((0.0, 0.0)) + target_uv1 = Vector((0.0, 0.0)) + if self.location == 'RIGHT_BOTTOM': + target_uv0.x = target_uv1.x = uv_min.x + width + elif self.location == 'MIDDLE': + target_uv0.x = target_uv1.x = uv_min.x + width * 0.5 + elif self.location == 'LEFT_TOP': + target_uv0.x = target_uv1.x = uv_min.x + if luv0.uv.y < luv1.uv.y: + target_uv0.y = uv_min.y + hidx * height / len(loop_seqs) + target_uv1.y = uv_min.y + (hidx + 1) * height / len(loop_seqs) + else: + target_uv0.y = uv_min.y + (hidx + 1) * height / len(loop_seqs) + target_uv1.y = uv_min.y + hidx * height / len(loop_seqs) + diff_uvs.append([target_uv0 - luv0.uv, target_uv1 - luv1.uv]) + + return diff_uvs + + # only selected UV loop sequence will be aligned along to X-axis + def __align_to_x_axis_wo_transmission(self, loop_seqs, uv_layer, + uv_min, width, height): + # reverse if the UV coordinate is not sorted by position + need_revese = loop_seqs[0][0][0][uv_layer].uv.x > \ + loop_seqs[-1][0][0][uv_layer].uv.x + if need_revese: + loop_seqs.reverse() + for hidx, hseq in enumerate(loop_seqs): + for vidx, pair in enumerate(hseq): + tmp = loop_seqs[hidx][vidx][0] + loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1] + loop_seqs[hidx][vidx][1] = tmp + + # get UV differential + diff_uvs = self.__get_x_axis_align_diff_uvs(loop_seqs, uv_layer, + uv_min, width, height) + + # update UV + for hseq, duv in zip(loop_seqs, diff_uvs): + pair = hseq[0] + luv0 = pair[0][uv_layer] + luv1 = pair[1][uv_layer] + luv0.uv = luv0.uv + duv[0] + luv1.uv = luv1.uv + duv[1] + + # only selected UV loop sequence will be aligned along to Y-axis + def __align_to_y_axis_wo_transmission(self, loop_seqs, uv_layer, + uv_min, width, height): + # reverse if the UV coordinate is not sorted by position + need_revese = loop_seqs[0][0][0][uv_layer].uv.y > \ + loop_seqs[-1][0][0][uv_layer].uv.y + if need_revese: + loop_seqs.reverse() + for hidx, hseq in enumerate(loop_seqs): + for vidx, pair in enumerate(hseq): + tmp = loop_seqs[hidx][vidx][0] + loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1] + loop_seqs[hidx][vidx][1] = tmp + + # get UV differential + diff_uvs = self.__get_y_axis_align_diff_uvs(loop_seqs, uv_layer, + uv_min, width, height) + + # update UV + for hseq, duv in zip(loop_seqs, diff_uvs): + pair = hseq[0] + luv0 = pair[0][uv_layer] + luv1 = pair[1][uv_layer] + luv0.uv = luv0.uv + duv[0] + luv1.uv = luv1.uv + duv[1] + + # selected and paralleled UV loop sequence will be aligned along to X-axis + def __align_to_x_axis_w_transmission(self, loop_seqs, uv_layer, + uv_min, width, height): + # reverse if the UV coordinate is not sorted by position + need_revese = loop_seqs[0][0][0][uv_layer].uv.x > \ + loop_seqs[-1][0][0][uv_layer].uv.x + if need_revese: + loop_seqs.reverse() + for hidx, hseq in enumerate(loop_seqs): + for vidx in range(len(hseq)): + tmp = loop_seqs[hidx][vidx][0] + loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1] + loop_seqs[hidx][vidx][1] = tmp + + # get offset UVs when the UVs are aligned to X-axis + align_diff_uvs = self.__get_x_axis_align_diff_uvs(loop_seqs, uv_layer, + uv_min, width, + height) + base_uv = loop_seqs[0][0][0][uv_layer].uv.copy() + offset_uvs = [] + for hseq, aduv in zip(loop_seqs, align_diff_uvs): + luv0 = hseq[0][0][uv_layer] + luv1 = hseq[0][1][uv_layer] + offset_uvs.append([luv0.uv + aduv[0] - base_uv, + luv1.uv + aduv[1] - base_uv]) + + # get UV differential + diff_uvs = [] + # hseq[vertical][loop] + for hidx, hseq in enumerate(loop_seqs): + # pair[loop] + diffs = [] + for vidx in range(0, len(hseq), 2): + if self.horizontal: + hdiff_uvs = [ + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0, + self.mesh_infl), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1, + self.mesh_infl), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 0, self.mesh_infl), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 1, self.mesh_infl), + ] + hdiff_uvs[0].y = hdiff_uvs[0].y + offset_uvs[hidx][0].y + hdiff_uvs[1].y = hdiff_uvs[1].y + offset_uvs[hidx][1].y + hdiff_uvs[2].y = hdiff_uvs[2].y + offset_uvs[hidx][0].y + hdiff_uvs[3].y = hdiff_uvs[3].y + offset_uvs[hidx][1].y + else: + hdiff_uvs = [ + offset_uvs[hidx][0], + offset_uvs[hidx][1], + offset_uvs[hidx][0], + offset_uvs[hidx][1], + ] + if self.vertical: + vdiff_uvs = [ + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0, + self.mesh_infl), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1, + self.mesh_infl), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 0, self.mesh_infl), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 1, self.mesh_infl), + ] + else: + vdiff_uvs = [ + get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx), + get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx), + get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx), + get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx) + ] + diffs.append([hdiff_uvs, vdiff_uvs]) + diff_uvs.append(diffs) + + # update UV + for hseq, diffs in zip(loop_seqs, diff_uvs): + for vidx in range(0, len(hseq), 2): + loops = [ + hseq[vidx][0], hseq[vidx][1], + hseq[vidx + 1][0], hseq[vidx + 1][1] + ] + for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0], + diffs[int(vidx / 2)][1]): + l[uv_layer].uv = base_uv + hdiff + vdiff + if self.select: + l[uv_layer].select = True + + # selected and paralleled UV loop sequence will be aligned along to Y-axis + def __align_to_y_axis_w_transmission(self, loop_seqs, uv_layer, + uv_min, width, height): + # reverse if the UV coordinate is not sorted by position + need_revese = loop_seqs[0][0][0][uv_layer].uv.y > \ + loop_seqs[-1][0][-1][uv_layer].uv.y + if need_revese: + loop_seqs.reverse() + for hidx, hseq in enumerate(loop_seqs): + for vidx in range(len(hseq)): + tmp = loop_seqs[hidx][vidx][0] + loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1] + loop_seqs[hidx][vidx][1] = tmp + + # get offset UVs when the UVs are aligned to Y-axis + align_diff_uvs = self.__get_y_axis_align_diff_uvs(loop_seqs, uv_layer, + uv_min, width, + height) + base_uv = loop_seqs[0][0][0][uv_layer].uv.copy() + offset_uvs = [] + for hseq, aduv in zip(loop_seqs, align_diff_uvs): + luv0 = hseq[0][0][uv_layer] + luv1 = hseq[0][1][uv_layer] + offset_uvs.append([luv0.uv + aduv[0] - base_uv, + luv1.uv + aduv[1] - base_uv]) + + # get UV differential + diff_uvs = [] + # hseq[vertical][loop] + for hidx, hseq in enumerate(loop_seqs): + # pair[loop] + diffs = [] + for vidx in range(0, len(hseq), 2): + if self.horizontal: + hdiff_uvs = [ + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0, + self.mesh_infl), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1, + self.mesh_infl), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 0, self.mesh_infl), + get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 1, self.mesh_infl), + ] + hdiff_uvs[0].x = hdiff_uvs[0].x + offset_uvs[hidx][0].x + hdiff_uvs[1].x = hdiff_uvs[1].x + offset_uvs[hidx][1].x + hdiff_uvs[2].x = hdiff_uvs[2].x + offset_uvs[hidx][0].x + hdiff_uvs[3].x = hdiff_uvs[3].x + offset_uvs[hidx][1].x + else: + hdiff_uvs = [ + offset_uvs[hidx][0], + offset_uvs[hidx][1], + offset_uvs[hidx][0], + offset_uvs[hidx][1], + ] + if self.vertical: + vdiff_uvs = [ + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0, + self.mesh_infl), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1, + self.mesh_infl), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 0, self.mesh_infl), + get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, + hidx, 1, self.mesh_infl), + ] + else: + vdiff_uvs = [ + get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx), + get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx), + get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx), + get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx) + ] + diffs.append([hdiff_uvs, vdiff_uvs]) + diff_uvs.append(diffs) + + # update UV + for hseq, diffs in zip(loop_seqs, diff_uvs): + for vidx in range(0, len(hseq), 2): + loops = [ + hseq[vidx][0], hseq[vidx][1], + hseq[vidx + 1][0], hseq[vidx + 1][1] + ] + for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0], + diffs[int(vidx / 2)][1]): + l[uv_layer].uv = base_uv + hdiff + vdiff + if self.select: + l[uv_layer].select = True + + def __align(self, loop_seqs, uv_layer, uv_min, width, height): + # align along to x-axis + if width > height: + if self.transmission: + self.__align_to_x_axis_w_transmission(loop_seqs, uv_layer, + uv_min, width, height) + else: + self.__align_to_x_axis_wo_transmission(loop_seqs, uv_layer, + uv_min, width, height) + # align along to y-axis + else: + if self.transmission: + self.__align_to_y_axis_w_transmission(loop_seqs, uv_layer, + uv_min, width, height) + else: + self.__align_to_y_axis_wo_transmission(loop_seqs, uv_layer, + uv_min, width, height) + + def execute(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.verify() + + # loop_seqs[horizontal][vertical][loop] + loop_seqs, error = common.get_loop_sequences(bm, uv_layer) + if not loop_seqs: + self.report({'WARNING'}, error) + return {'CANCELLED'} + + # get height and width + uv_max, uv_min = self.__get_uv_max_min(loop_seqs, uv_layer) + width = uv_max.x - uv_min.x + height = uv_max.y - uv_min.y + + self.__align(loop_seqs, uv_layer, uv_min, width, height) + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/uv_magic_uv/legacy/op/align_uv_cursor.py b/uv_magic_uv/legacy/op/align_uv_cursor.py new file mode 100644 index 00000000..ec3e7036 --- /dev/null +++ b/uv_magic_uv/legacy/op/align_uv_cursor.py @@ -0,0 +1,257 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bpy +from mathutils import Vector +from bpy.props import EnumProperty, BoolProperty, FloatVectorProperty +import bmesh + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_AlignUVCursor', +] + + +def is_valid_context(context): + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "align_uv_cursor" + + @classmethod + def init_props(cls, scene): + def auvc_get_cursor_loc(self): + area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW', + 'IMAGE_EDITOR') + 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 + + +@BlClassRegistry(legacy=True) +class MUV_OT_AlignUVCursor(bpy.types.Operator): + + bl_idname = "uv.muv_align_uv_cursor_operator" + bl_label = "Align UV Cursor" + bl_description = "Align cursor to the center of UV island" + bl_options = {'REGISTER', 'UNDO'} + + position = EnumProperty( + items=( + ('CENTER', "Center", "Align to Center"), + ('LEFT_TOP', "Left Top", "Align to Left Top"), + ('LEFT_MIDDLE', "Left Middle", "Align to Left Middle"), + ('LEFT_BOTTOM', "Left Bottom", "Align to Left Bottom"), + ('MIDDLE_TOP', "Middle Top", "Align to Middle Top"), + ('MIDDLE_BOTTOM', "Middle Bottom", "Align to Middle Bottom"), + ('RIGHT_TOP', "Right Top", "Align to Right Top"), + ('RIGHT_MIDDLE', "Right Middle", "Align to Right Middle"), + ('RIGHT_BOTTOM', "Right Bottom", "Align to Right Bottom") + ), + name="Position", + description="Align position", + default='CENTER' + ) + base = EnumProperty( + items=( + ('TEXTURE', "Texture", "Align based on Texture"), + ('UV', "UV", "Align to UV"), + ('UV_SEL', "UV (Selected)", "Align to Selected UV") + ), + name="Base", + description="Align base", + default='TEXTURE' + ) + + @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') + bd_size = common.get_uvimg_editor_board_size(area) + + if self.base == 'UV': + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if not bm.loops.layers.uv: + return None + uv_layer = bm.loops.layers.uv.verify() + + max_ = Vector((-10000000.0, -10000000.0)) + min_ = Vector((10000000.0, 10000000.0)) + for f in bm.faces: + if not f.select: + continue + for l in f.loops: + uv = l[uv_layer].uv + max_.x = max(max_.x, uv.x) + max_.y = max(max_.y, uv.y) + min_.x = min(min_.x, uv.x) + min_.y = min(min_.y, uv.y) + center = Vector(((max_.x + min_.x) / 2.0, (max_.y + min_.y) / 2.0)) + + elif self.base == 'UV_SEL': + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if not bm.loops.layers.uv: + return None + uv_layer = bm.loops.layers.uv.verify() + + max_ = Vector((-10000000.0, -10000000.0)) + min_ = Vector((10000000.0, 10000000.0)) + for f in bm.faces: + if not f.select: + continue + for l in f.loops: + if not l[uv_layer].select: + continue + uv = l[uv_layer].uv + max_.x = max(max_.x, uv.x) + max_.y = max(max_.y, uv.y) + min_.x = min(min_.x, uv.x) + min_.y = min(min_.y, uv.y) + center = Vector(((max_.x + min_.x) / 2.0, (max_.y + min_.y) / 2.0)) + + elif self.base == 'TEXTURE': + min_ = Vector((0.0, 0.0)) + max_ = Vector((1.0, 1.0)) + center = Vector((0.5, 0.5)) + else: + self.report({'ERROR'}, "Unknown Operation") + + if self.position == 'CENTER': + cx = center.x * bd_size[0] + cy = center.y * bd_size[1] + elif self.position == 'LEFT_TOP': + cx = min_.x * bd_size[0] + cy = max_.y * bd_size[1] + elif self.position == 'LEFT_MIDDLE': + cx = min_.x * bd_size[0] + cy = center.y * bd_size[1] + elif self.position == 'LEFT_BOTTOM': + cx = min_.x * bd_size[0] + cy = min_.y * bd_size[1] + elif self.position == 'MIDDLE_TOP': + cx = center.x * bd_size[0] + cy = max_.y * bd_size[1] + elif self.position == 'MIDDLE_BOTTOM': + cx = center.x * bd_size[0] + cy = min_.y * bd_size[1] + elif self.position == 'RIGHT_TOP': + cx = max_.x * bd_size[0] + cy = max_.y * bd_size[1] + elif self.position == 'RIGHT_MIDDLE': + cx = max_.x * bd_size[0] + cy = center.y * bd_size[1] + elif self.position == 'RIGHT_BOTTOM': + cx = max_.x * bd_size[0] + cy = min_.y * bd_size[1] + else: + self.report({'ERROR'}, "Unknown Operation") + + space.cursor_location = Vector((cx, cy)) + + return {'FINISHED'} diff --git a/uv_magic_uv/legacy/op/copy_paste_uv.py b/uv_magic_uv/legacy/op/copy_paste_uv.py new file mode 100644 index 00000000..a8aef017 --- /dev/null +++ b/uv_magic_uv/legacy/op/copy_paste_uv.py @@ -0,0 +1,531 @@ +# + +# ##### 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 , Jace Priester" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + + +import bmesh +import bpy.utils +from bpy.props import ( + StringProperty, + BoolProperty, + IntProperty, + EnumProperty, +) + +from ...impl import copy_paste_uv_impl as impl +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + +__all__ = [ + 'Properties', + 'MUV_OT_CopyPasteUV_CopyUV', + 'MUV_MT_CopyPasteUV_CopyUV', + 'MUV_OT_CopyPasteUV_PasteUV', + 'MUV_MT_CopyPasteUV_PasteUV', + 'MUV_OT_CopyPasteUV_SelSeqCopyUV', + 'MUV_MT_CopyPasteUV_SelSeqCopyUV', + 'MUV_OT_CopyPasteUV_SelSeqPasteUV', + 'MUV_MT_CopyPasteUV_SelSeqPasteUV', +] + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "copy_paste_uv" + + @classmethod + def init_props(cls, scene): + class Props(): + src_info = None + + scene.muv_props.copy_paste_uv = Props() + scene.muv_props.copy_paste_uv_selseq = Props() + + scene.muv_copy_paste_uv_enabled = BoolProperty( + name="Copy/Paste UV Enabled", + description="Copy/Paste UV is enabled", + default=False + ) + scene.muv_copy_paste_uv_copy_seams = BoolProperty( + name="Seams", + description="Copy Seams", + default=True + ) + scene.muv_copy_paste_uv_mode = EnumProperty( + items=[ + ('DEFAULT', "Default", "Default Mode"), + ('SEL_SEQ', "Selection Sequence", "Selection Sequence Mode") + ], + name="Copy/Paste UV Mode", + description="Copy/Paste UV Mode", + default='DEFAULT' + ) + scene.muv_copy_paste_uv_strategy = EnumProperty( + name="Strategy", + description="Paste Strategy", + items=[ + ('N_N', 'N:N', 'Number of faces must be equal to source'), + ('N_M', 'N:M', 'Number of faces must not be equal to source') + ], + default='N_M' + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_props.copy_paste_uv + del scene.muv_props.copy_paste_uv_selseq + del scene.muv_copy_paste_uv_enabled + del scene.muv_copy_paste_uv_copy_seams + del scene.muv_copy_paste_uv_mode + del scene.muv_copy_paste_uv_strategy + + +@BlClassRegistry(legacy=True) +class MUV_OT_CopyPasteUV_CopyUV(bpy.types.Operator): + """ + Operation class: Copy UV coordinate + """ + + 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(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 impl.is_valid_context(context) + + def execute(self, context): + props = context.scene.muv_props.copy_paste_uv + obj = context.active_object + bm = common.create_bmesh(obj) + + # get UV layer + uv_layers = impl.get_copy_uv_layers(self, bm, self.uv_map) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + src_info = impl.get_src_face_info(self, bm, uv_layers, True) + if src_info is None: + return {'CANCELLED'} + props.src_info = src_info + + face_count = len(props.src_info[list(props.src_info.keys())[0]]) + self.report({'INFO'}, "{} face(s) are copied".format(face_count)) + + return {'FINISHED'} + + +@BlClassRegistry(legacy=True) +class MUV_MT_CopyPasteUV_CopyUV(bpy.types.Menu): + """ + Menu class: 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 impl.is_valid_context(context) + + def draw(self, context): + layout = self.layout + # create sub menu + obj = context.active_object + bm = common.create_bmesh(obj) + uv_maps = bm.loops.layers.uv.keys() + + ops = layout.operator(MUV_OT_CopyPasteUV_CopyUV.bl_idname, + text="[Default]") + ops.uv_map = "__default" + + ops = layout.operator(MUV_OT_CopyPasteUV_CopyUV.bl_idname, + text="[All]") + ops.uv_map = "__all" + + for m in uv_maps: + ops = layout.operator(MUV_OT_CopyPasteUV_CopyUV.bl_idname, text=m) + ops.uv_map = m + + +@BlClassRegistry(legacy=True) +class MUV_OT_CopyPasteUV_PasteUV(bpy.types.Operator): + """ + Operation class: Paste UV coordinate + """ + + 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(default="__default", options={'HIDDEN'}) + strategy = EnumProperty( + name="Strategy", + description="Paste Strategy", + items=[ + ('N_N', 'N:N', 'Number of faces must be equal to source'), + ('N_M', 'N:M', 'Number of faces must not be equal to source') + ], + default="N_M" + ) + flip_copied_uv = BoolProperty( + name="Flip Copied UV", + description="Flip Copied UV...", + default=False + ) + rotate_copied_uv = IntProperty( + default=0, + name="Rotate Copied UV", + min=0, + max=30 + ) + copy_seams = BoolProperty( + name="Seams", + description="Copy Seams", + default=True + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.copy_paste_uv + if not props.src_info: + return False + return impl.is_valid_context(context) + + def execute(self, context): + props = context.scene.muv_props.copy_paste_uv + if not props.src_info: + self.report({'WARNING'}, "Need copy UV at first") + return {'CANCELLED'} + obj = context.active_object + bm = common.create_bmesh(obj) + + # get UV layer + uv_layers = impl.get_paste_uv_layers(self, obj, bm, props.src_info, + self.uv_map) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + dest_info = impl.get_dest_face_info(self, bm, uv_layers, + props.src_info, self.strategy, + True) + if dest_info is None: + return {'CANCELLED'} + + # paste + ret = impl.paste_uv(self, bm, props.src_info, dest_info, uv_layers, + self.strategy, self.flip_copied_uv, + self.rotate_copied_uv, self.copy_seams) + if ret: + return {'CANCELLED'} + + face_count = len(props.src_info[list(dest_info.keys())[0]]) + self.report({'INFO'}, "{} face(s) are pasted".format(face_count)) + + bmesh.update_edit_mesh(obj.data) + if self.copy_seams is True: + obj.data.show_edge_seams = True + + return {'FINISHED'} + + +@BlClassRegistry(legacy=True) +class MUV_MT_CopyPasteUV_PasteUV(bpy.types.Menu): + """ + Menu class: 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 impl.is_valid_context(context) + + def draw(self, context): + sc = context.scene + layout = self.layout + # create sub menu + obj = context.active_object + bm = common.create_bmesh(obj) + uv_maps = bm.loops.layers.uv.keys() + + ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname, + text="[Default]") + ops.uv_map = "__default" + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy + + ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname, + text="[New]") + ops.uv_map = "__new" + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy + + ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname, + text="[All]") + ops.uv_map = "__all" + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy + + for m in uv_maps: + ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname, text=m) + ops.uv_map = m + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy + + +@BlClassRegistry(legacy=True) +class MUV_OT_CopyPasteUV_SelSeqCopyUV(bpy.types.Operator): + """ + Operation class: Copy UV coordinate by selection sequence + """ + + 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): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return impl.is_valid_context(context) + + def execute(self, context): + props = context.scene.muv_props.copy_paste_uv_selseq + obj = context.active_object + bm = common.create_bmesh(obj) + + # get UV layer + uv_layers = impl.get_copy_uv_layers(self, bm, self.uv_map) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + src_info = impl.get_select_history_src_face_info(self, bm, uv_layers) + if src_info is None: + return {'CANCELLED'} + props.src_info = src_info + + face_count = len(props.src_info[list(props.src_info.keys())[0]]) + self.report({'INFO'}, "{} face(s) are selected".format(face_count)) + + return {'FINISHED'} + + +@BlClassRegistry(legacy=True) +class MUV_MT_CopyPasteUV_SelSeqCopyUV(bpy.types.Menu): + """ + Menu class: 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 impl.is_valid_context(context) + + def draw(self, context): + layout = self.layout + obj = context.active_object + bm = common.create_bmesh(obj) + uv_maps = bm.loops.layers.uv.keys() + + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV.bl_idname, + text="[Default]") + ops.uv_map = "__default" + + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV.bl_idname, + text="[All]") + ops.uv_map = "__all" + + for m in uv_maps: + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV.bl_idname, + text=m) + ops.uv_map = m + + +@BlClassRegistry(legacy=True) +class MUV_OT_CopyPasteUV_SelSeqPasteUV(bpy.types.Operator): + """ + Operation class: Paste UV coordinate by selection sequence + """ + + 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(default="__default", options={'HIDDEN'}) + strategy = EnumProperty( + name="Strategy", + description="Paste Strategy", + items=[ + ('N_N', 'N:N', 'Number of faces must be equal to source'), + ('N_M', 'N:M', 'Number of faces must not be equal to source') + ], + default="N_M" + ) + flip_copied_uv = BoolProperty( + name="Flip Copied UV", + description="Flip Copied UV...", + default=False + ) + rotate_copied_uv = IntProperty( + default=0, + name="Rotate Copied UV", + min=0, + max=30 + ) + copy_seams = BoolProperty( + name="Seams", + description="Copy Seams", + default=True + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.copy_paste_uv_selseq + if not props.src_info: + return False + return impl.is_valid_context(context) + + def execute(self, context): + props = context.scene.muv_props.copy_paste_uv_selseq + if not props.src_info: + self.report({'WARNING'}, "Need copy UV at first") + return {'CANCELLED'} + obj = context.active_object + bm = common.create_bmesh(obj) + + # get UV layer + uv_layers = impl.get_paste_uv_layers(self, obj, bm, props.src_info, + self.uv_map) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + dest_info = impl.get_select_history_dest_face_info(self, bm, uv_layers, + props.src_info, + self.strategy) + if dest_info is None: + return {'CANCELLED'} + + # paste + ret = impl.paste_uv(self, bm, props.src_info, dest_info, uv_layers, + self.strategy, self.flip_copied_uv, + self.rotate_copied_uv, self.copy_seams) + if ret: + return {'CANCELLED'} + + face_count = len(props.src_info[list(dest_info.keys())[0]]) + self.report({'INFO'}, "{} face(s) are pasted".format(face_count)) + + bmesh.update_edit_mesh(obj.data) + if self.copy_seams is True: + obj.data.show_edge_seams = True + + return {'FINISHED'} + + +@BlClassRegistry(legacy=True) +class MUV_MT_CopyPasteUV_SelSeqPasteUV(bpy.types.Menu): + """ + Menu class: 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 impl.is_valid_context(context) + + def draw(self, context): + sc = context.scene + layout = self.layout + # create sub menu + obj = context.active_object + bm = common.create_bmesh(obj) + uv_maps = bm.loops.layers.uv.keys() + + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname, + text="[Default]") + ops.uv_map = "__default" + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy + + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname, + text="[New]") + ops.uv_map = "__new" + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy + + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname, + text="[All]") + ops.uv_map = "__all" + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy + + for m in uv_maps: + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname, + text=m) + ops.uv_map = m + ops.copy_seams = sc.muv_copy_paste_uv_copy_seams + ops.strategy = sc.muv_copy_paste_uv_strategy diff --git a/uv_magic_uv/legacy/op/copy_paste_uv_object.py b/uv_magic_uv/legacy/op/copy_paste_uv_object.py new file mode 100644 index 00000000..e09b003b --- /dev/null +++ b/uv_magic_uv/legacy/op/copy_paste_uv_object.py @@ -0,0 +1,298 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bmesh +import bpy +from bpy.props import ( + StringProperty, + BoolProperty, +) + +from ...impl import copy_paste_uv_impl as impl +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + +__all__ = [ + 'Properties', + 'MUV_OT_CopyPasteUVObject_CopyUV', + 'MUV_MT_CopyPasteUVObject_CopyUV', + 'MUV_OT_CopyPasteUVObject_PasteUV', + 'MUV_MT_CopyPasteUVObject_PasteUV', +] + + +def is_valid_context(context): + obj = context.object + + # only object mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'OBJECT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "copy_paste_uv_object" + + @classmethod + def init_props(cls, scene): + class Props(): + src_info = None + + scene.muv_props.copy_paste_uv_object = Props() + + scene.muv_copy_paste_uv_object_copy_seams = BoolProperty( + name="Seams", + description="Copy Seams", + default=True + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_props.copy_paste_uv_object + del scene.muv_copy_paste_uv_object_copy_seams + + +def memorize_view_3d_mode(fn): + def __memorize_view_3d_mode(self, context): + mode_orig = bpy.context.object.mode + result = fn(self, context) + bpy.ops.object.mode_set(mode=mode_orig) + return result + return __memorize_view_3d_mode + + +@BlClassRegistry(legacy=True) +class MUV_OT_CopyPasteUVObject_CopyUV(bpy.types.Operator): + """ + Operation class: Copy UV coordinate among objects + """ + + 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(default="__default", options={'HIDDEN'}) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + + @memorize_view_3d_mode + def execute(self, context): + props = context.scene.muv_props.copy_paste_uv_object + bpy.ops.object.mode_set(mode='EDIT') + obj = context.active_object + bm = common.create_bmesh(obj) + + # get UV layer + uv_layers = impl.get_copy_uv_layers(self, bm, self.uv_map) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + src_info = impl.get_src_face_info(self, bm, uv_layers) + if src_info is None: + return {'CANCELLED'} + props.src_info = src_info + + self.report({'INFO'}, + "{}'s UV coordinates are copied".format(obj.name)) + + return {'FINISHED'} + + +@BlClassRegistry(legacy=True) +class MUV_MT_CopyPasteUVObject_CopyUV(bpy.types.Menu): + """ + Menu class: Copy UV coordinate among objects + """ + + 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() + + ops = layout.operator(MUV_OT_CopyPasteUVObject_CopyUV.bl_idname, + text="[Default]") + ops.uv_map = "__default" + + ops = layout.operator(MUV_OT_CopyPasteUVObject_CopyUV.bl_idname, + text="[All]") + ops.uv_map = "__all" + + for m in uv_maps: + ops = layout.operator(MUV_OT_CopyPasteUVObject_CopyUV.bl_idname, + text=m) + ops.uv_map = m + + +@BlClassRegistry(legacy=True) +class MUV_OT_CopyPasteUVObject_PasteUV(bpy.types.Operator): + """ + Operation class: Paste UV coordinate among objects + """ + + 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(default="__default", options={'HIDDEN'}) + copy_seams = BoolProperty( + name="Seams", + description="Copy Seams", + default=True + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.copy_paste_uv_object + if not props.src_info: + return False + return is_valid_context(context) + + @memorize_view_3d_mode + def execute(self, context): + props = context.scene.muv_props.copy_paste_uv_object + if not props.src_info: + self.report({'WARNING'}, "Need copy UV at first") + return {'CANCELLED'} + + for o in bpy.data.objects: + if not hasattr(o.data, "uv_textures") or not o.select: + continue + + bpy.ops.object.mode_set(mode='OBJECT') + bpy.context.scene.objects.active = o + bpy.ops.object.mode_set(mode='EDIT') + + obj = context.active_object + bm = common.create_bmesh(obj) + + # get UV layer + uv_layers = impl.get_paste_uv_layers(self, obj, bm, props.src_info, + self.uv_map) + if not uv_layers: + return {'CANCELLED'} + + # get selected face + dest_info = impl.get_dest_face_info(self, bm, uv_layers, + props.src_info, 'N_N') + if dest_info is None: + return {'CANCELLED'} + + # paste + ret = impl.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 UV coordinates are pasted".format(obj.name)) + + return {'FINISHED'} + + +@BlClassRegistry(legacy=True) +class MUV_MT_CopyPasteUVObject_PasteUV(bpy.types.Menu): + """ + Menu class: Paste UV coordinate among objects + """ + + 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 + layout = self.layout + # create sub menu + uv_maps = [] + for obj in bpy.data.objects: + if hasattr(obj.data, "uv_textures") and obj.select: + uv_maps.extend(obj.data.uv_textures.keys()) + + ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname, + text="[Default]") + ops.uv_map = "__default" + ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams + + ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname, + text="[New]") + ops.uv_map = "__new" + ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams + + ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname, + text="[All]") + ops.uv_map = "__all" + ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams + + for m in uv_maps: + ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname, + text=m) + ops.uv_map = m + ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams diff --git a/uv_magic_uv/legacy/op/copy_paste_uv_uvedit.py b/uv_magic_uv/legacy/op/copy_paste_uv_uvedit.py new file mode 100644 index 00000000..bb72d42a --- /dev/null +++ b/uv_magic_uv/legacy/op/copy_paste_uv_uvedit.py @@ -0,0 +1,97 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "imdjs, Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bpy + +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry +from ...impl import copy_paste_uv_uvedit_impl as impl + + +__all__ = [ + 'Properties', + 'MUV_OT_CopyPasteUVUVEdit_CopyUV', + 'MUV_OT_CopyPasteUVUVEdit_PasteUV', +] + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "copy_paste_uv_uvedit" + + @classmethod + def init_props(cls, scene): + class Props(): + src_uvs = None + + scene.muv_props.copy_paste_uv_uvedit = Props() + + @classmethod + def del_props(cls, scene): + del scene.muv_props.copy_paste_uv_uvedit + + +@BlClassRegistry(legacy=True) +class MUV_OT_CopyPasteUVUVEdit_CopyUV(bpy.types.Operator): + """ + Operation class: Copy UV coordinate on UV/Image Editor + """ + + 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'} + + def __init__(self): + self.__impl = impl.CopyUVImpl() + + @classmethod + def poll(cls, context): + return impl.CopyUVImpl.poll(context) + + def execute(self, context): + return self.__impl.execute(self, context) + + +@BlClassRegistry(legacy=True) +class MUV_OT_CopyPasteUVUVEdit_PasteUV(bpy.types.Operator): + """ + Operation class: Paste UV coordinate on UV/Image Editor + """ + + 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'} + + def __init__(self): + self.__impl = impl.PasteUVImpl() + + @classmethod + def poll(cls, context): + return impl.PasteUVImpl.poll(context) + + def execute(self, context): + return self.__impl.execute(self, context) diff --git a/uv_magic_uv/legacy/op/flip_rotate_uv.py b/uv_magic_uv/legacy/op/flip_rotate_uv.py new file mode 100644 index 00000000..d94e4808 --- /dev/null +++ b/uv_magic_uv/legacy/op/flip_rotate_uv.py @@ -0,0 +1,132 @@ +# + +# ##### 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, + IntProperty, +) + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry +from ...impl import flip_rotate_impl as impl + +__all__ = [ + 'Properties', + 'MUV_OT_FlipRotate', +] + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "flip_rotate_uv" + + @classmethod + def init_props(cls, scene): + scene.muv_flip_rotate_uv_enabled = BoolProperty( + name="Flip/Rotate UV Enabled", + description="Flip/Rotate UV is enabled", + default=False + ) + scene.muv_flip_rotate_uv_seams = BoolProperty( + name="Seams", + description="Seams", + default=True + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_flip_rotate_uv_enabled + del scene.muv_flip_rotate_uv_seams + + +@BlClassRegistry(legacy=True) +class MUV_OT_FlipRotate(bpy.types.Operator): + """ + Operation class: Flip and Rotate UV coordinate + """ + + bl_idname = "uv.muv_flip_rotate_uv_operator" + bl_label = "Flip/Rotate UV" + bl_description = "Flip/Rotate UV coordinate" + bl_options = {'REGISTER', 'UNDO'} + + flip = BoolProperty( + name="Flip UV", + description="Flip UV...", + default=False + ) + rotate = IntProperty( + default=0, + name="Rotate UV", + min=0, + max=30 + ) + seams = BoolProperty( + name="Seams", + description="Seams", + default=True + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return impl.is_valid_context(context) + + def execute(self, context): + self.report({'INFO'}, "Flip/Rotate UV") + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + # get UV layer + uv_layer = impl.get_uv_layer(self, bm) + if not uv_layer: + return {'CANCELLED'} + + # get selected face + src_info = impl.get_src_face_info(self, bm, [uv_layer], True) + if not src_info: + return {'CANCELLED'} + + face_count = len(src_info[list(src_info.keys())[0]]) + self.report({'INFO'}, "{} face(s) are selected".format(face_count)) + + # paste + ret = impl.paste_uv(self, bm, src_info, src_info, [uv_layer], 'N_N', + self.flip, self.rotate, self.seams) + if ret: + return {'CANCELLED'} + + bmesh.update_edit_mesh(obj.data) + if self.seams is True: + obj.data.show_edge_seams = True + + return {'FINISHED'} diff --git a/uv_magic_uv/legacy/op/mirror_uv.py b/uv_magic_uv/legacy/op/mirror_uv.py new file mode 100644 index 00000000..e869e5e8 --- /dev/null +++ b/uv_magic_uv/legacy/op/mirror_uv.py @@ -0,0 +1,110 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Keith (Wahooney) Boshoff, Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bpy +from bpy.props import ( + EnumProperty, + FloatProperty, + BoolProperty, +) + +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry +from ...impl import mirror_uv_impl as impl + + +__all__ = [ + 'Properties', + 'MUV_OT_MirrorUV', +] + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "mirror_uv" + + @classmethod + def init_props(cls, scene): + scene.muv_mirror_uv_enabled = BoolProperty( + name="Mirror UV Enabled", + description="Mirror UV is enabled", + default=False + ) + scene.muv_mirror_uv_axis = EnumProperty( + items=[ + ('X', "X", "Mirror Along X axis"), + ('Y', "Y", "Mirror Along Y axis"), + ('Z', "Z", "Mirror Along Z axis") + ], + name="Axis", + description="Mirror Axis", + default='X' + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_mirror_uv_enabled + del scene.muv_mirror_uv_axis + + +@BlClassRegistry(legacy=True) +class MUV_OT_MirrorUV(bpy.types.Operator): + """ + Operation class: Mirror UV + """ + + bl_idname = "uv.muv_mirror_uv_operator" + bl_label = "Mirror UV" + bl_options = {'REGISTER', 'UNDO'} + + axis = EnumProperty( + items=( + ('X', "X", "Mirror Along X axis"), + ('Y', "Y", "Mirror Along Y axis"), + ('Z', "Z", "Mirror Along Z axis") + ), + name="Axis", + description="Mirror Axis", + default='X' + ) + error = FloatProperty( + name="Error", + description="Error threshold", + default=0.001, + min=0.0, + max=100.0, + soft_min=0.0, + soft_max=1.0 + ) + + def __init__(self): + self.__impl = impl.MirrorUVImpl() + + @classmethod + def poll(cls, context): + return impl.MirrorUVImpl.poll(context) + + def execute(self, context): + return self.__impl.execute(self, context) diff --git a/uv_magic_uv/legacy/op/move_uv.py b/uv_magic_uv/legacy/op/move_uv.py new file mode 100644 index 00000000..2988c2ce --- /dev/null +++ b/uv_magic_uv/legacy/op/move_uv.py @@ -0,0 +1,82 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "kgeogeo, mem, Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bpy +from bpy.props import BoolProperty + +from ...impl import move_uv_impl as impl +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_MoveUV', +] + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "move_uv" + + @classmethod + def init_props(cls, scene): + scene.muv_move_uv_enabled = BoolProperty( + name="Move UV Enabled", + description="Move UV is enabled", + default=False + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_move_uv_enabled + + +@BlClassRegistry(legacy=True) +class MUV_OT_MoveUV(bpy.types.Operator): + """ + Operator class: Move UV + """ + + bl_idname = "uv.muv_move_uv_operator" + bl_label = "Move UV" + bl_options = {'REGISTER', 'UNDO'} + + def __init__(self): + self.__impl = impl.MoveUVImpl() + + @classmethod + def poll(cls, context): + return impl.MoveUVImpl.poll(context) + + @classmethod + def is_running(cls, _): + return impl.MoveUVImpl.is_running(_) + + def modal(self, context, event): + return self.__impl.modal(self, context, event) + + def execute(self, context): + return self.__impl.execute(self, context) diff --git a/uv_magic_uv/legacy/op/pack_uv.py b/uv_magic_uv/legacy/op/pack_uv.py new file mode 100644 index 00000000..f8d58843 --- /dev/null +++ b/uv_magic_uv/legacy/op/pack_uv.py @@ -0,0 +1,281 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +from math import fabs + +import bpy +import bmesh +import mathutils +from bpy.props import ( + FloatProperty, + FloatVectorProperty, + BoolProperty, +) +from mathutils import Vector + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_PackUV', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "pack_uv" + + @classmethod + def init_props(cls, scene): + scene.muv_pack_uv_enabled = BoolProperty( + name="Pack UV Enabled", + description="Pack UV is enabled", + default=False + ) + scene.muv_pack_uv_allowable_center_deviation = FloatVectorProperty( + name="Allowable Center Deviation", + description="Allowable center deviation to judge same UV island", + min=0.000001, + max=0.1, + default=(0.001, 0.001), + size=2 + ) + scene.muv_pack_uv_allowable_size_deviation = FloatVectorProperty( + name="Allowable Size Deviation", + description="Allowable sizse deviation to judge same UV island", + min=0.000001, + max=0.1, + default=(0.001, 0.001), + size=2 + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_pack_uv_enabled + del scene.muv_pack_uv_allowable_center_deviation + del scene.muv_pack_uv_allowable_size_deviation + + +@BlClassRegistry(legacy=True) +class MUV_OT_PackUV(bpy.types.Operator): + """ + Operation class: Pack UV with same UV islands are integrated + Island matching algorithm + - Same center of UV island + - Same size of UV island + - Same number of UV + """ + + bl_idname = "uv.muv_pack_uv_operator" + bl_label = "Pack UV" + bl_description = "Pack UV (Same UV Islands are integrated)" + bl_options = {'REGISTER', 'UNDO'} + + rotate = BoolProperty( + name="Rotate", + description="Rotate option used by default pack UV function", + default=False) + margin = FloatProperty( + name="Margin", + description="Margin used by default pack UV function", + min=0, + max=1, + default=0.001) + allowable_center_deviation = FloatVectorProperty( + name="Allowable Center Deviation", + description="Allowable center deviation to judge same UV island", + min=0.000001, + max=0.1, + default=(0.001, 0.001), + size=2 + ) + allowable_size_deviation = FloatVectorProperty( + name="Allowable Size Deviation", + description="Allowable sizse deviation to judge same UV island", + min=0.000001, + max=0.1, + default=(0.001, 0.001), + size=2 + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + + def execute(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + if not bm.loops.layers.uv: + self.report({'WARNING'}, "Object must have more than one UV map") + return {'CANCELLED'} + uv_layer = bm.loops.layers.uv.verify() + + selected_faces = [f for f in bm.faces if f.select] + island_info = common.get_island_info(obj) + num_group = self.__group_island(island_info) + + loop_lists = [l for f in bm.faces for l in f.loops] + bpy.ops.mesh.select_all(action='DESELECT') + + # pack UV + for gidx in range(num_group): + group = list(filter( + lambda i, idx=gidx: i['group'] == idx, island_info)) + for f in group[0]['faces']: + f['face'].select = True + bmesh.update_edit_mesh(obj.data) + bpy.ops.uv.select_all(action='SELECT') + bpy.ops.uv.pack_islands(rotate=self.rotate, margin=self.margin) + + # copy/paste UV among same islands + for gidx in range(num_group): + group = list(filter( + lambda i, idx=gidx: i['group'] == idx, island_info)) + if len(group) <= 1: + continue + for g in group[1:]: + for (src_face, dest_face) in zip( + group[0]['sorted'], g['sorted']): + for (src_loop, dest_loop) in zip( + src_face['face'].loops, dest_face['face'].loops): + loop_lists[dest_loop.index][uv_layer].uv = loop_lists[ + src_loop.index][uv_layer].uv + + # restore face/UV selection + bpy.ops.uv.select_all(action='DESELECT') + bpy.ops.mesh.select_all(action='DESELECT') + for f in selected_faces: + f.select = True + bpy.ops.uv.select_all(action='SELECT') + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} + + def __sort_island_faces(self, kd, uvs, isl1, isl2): + """ + Sort faces in island + """ + + sorted_faces = [] + for f in isl1['sorted']: + _, idx, _ = kd.find( + Vector((f['ave_uv'].x, f['ave_uv'].y, 0.0))) + sorted_faces.append(isl2['faces'][uvs[idx]['face_idx']]) + return sorted_faces + + def __group_island(self, island_info): + """ + Group island + """ + + num_group = 0 + while True: + # search islands which is not parsed yet + isl_1 = None + for isl_1 in island_info: + if isl_1['group'] == -1: + break + else: + break # all faces are parsed + if isl_1 is None: + break + isl_1['group'] = num_group + isl_1['sorted'] = isl_1['faces'] + + # search same island + for isl_2 in island_info: + if isl_2['group'] == -1: + dcx = isl_2['center'].x - isl_1['center'].x + dcy = isl_2['center'].y - isl_1['center'].y + dsx = isl_2['size'].x - isl_1['size'].x + dsy = isl_2['size'].y - isl_1['size'].y + center_x_matched = ( + fabs(dcx) < self.allowable_center_deviation[0] + ) + center_y_matched = ( + fabs(dcy) < self.allowable_center_deviation[1] + ) + size_x_matched = ( + fabs(dsx) < self.allowable_size_deviation[0] + ) + size_y_matched = ( + fabs(dsy) < self.allowable_size_deviation[1] + ) + center_matched = center_x_matched and center_y_matched + size_matched = size_x_matched and size_y_matched + num_uv_matched = (isl_2['num_uv'] == isl_1['num_uv']) + # are islands have same? + if center_matched and size_matched and num_uv_matched: + isl_2['group'] = num_group + kd = mathutils.kdtree.KDTree(len(isl_2['faces'])) + uvs = [ + { + 'uv': Vector( + (f['ave_uv'].x, f['ave_uv'].y, 0.0) + ), + 'face_idx': fidx + } for fidx, f in enumerate(isl_2['faces']) + ] + for i, uv in enumerate(uvs): + kd.insert(uv['uv'], i) + kd.balance() + # sort faces for copy/paste UV + isl_2['sorted'] = self.__sort_island_faces( + kd, uvs, isl_1, isl_2) + num_group = num_group + 1 + + return num_group diff --git a/uv_magic_uv/legacy/op/preserve_uv_aspect.py b/uv_magic_uv/legacy/op/preserve_uv_aspect.py new file mode 100644 index 00000000..cf9349bc --- /dev/null +++ b/uv_magic_uv/legacy/op/preserve_uv_aspect.py @@ -0,0 +1,283 @@ +# + +# ##### 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 StringProperty, EnumProperty, BoolProperty +from mathutils import Vector + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_PreserveUVAspect', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "preserve_uv_aspect" + + @classmethod + def init_props(cls, scene): + def get_loaded_texture_name(_, __): + items = [(key, key, "") for key in bpy.data.images.keys()] + items.append(("None", "None", "")) + return items + + scene.muv_preserve_uv_aspect_enabled = BoolProperty( + name="Preserve UV Aspect Enabled", + description="Preserve UV Aspect is enabled", + default=False + ) + scene.muv_preserve_uv_aspect_tex_image = EnumProperty( + name="Image", + description="Texture Image", + items=get_loaded_texture_name + ) + scene.muv_preserve_uv_aspect_origin = EnumProperty( + name="Origin", + description="Aspect Origin", + items=[ + ('CENTER', 'Center', 'Center'), + ('LEFT_TOP', 'Left Top', 'Left Bottom'), + ('LEFT_CENTER', 'Left Center', 'Left Center'), + ('LEFT_BOTTOM', 'Left Bottom', 'Left Bottom'), + ('CENTER_TOP', 'Center Top', 'Center Top'), + ('CENTER_BOTTOM', 'Center Bottom', 'Center Bottom'), + ('RIGHT_TOP', 'Right Top', 'Right Top'), + ('RIGHT_CENTER', 'Right Center', 'Right Center'), + ('RIGHT_BOTTOM', 'Right Bottom', 'Right Bottom') + + ], + default="CENTER" + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_preserve_uv_aspect_enabled + del scene.muv_preserve_uv_aspect_tex_image + del scene.muv_preserve_uv_aspect_origin + + +@BlClassRegistry(legacy=True) +class MUV_OT_PreserveUVAspect(bpy.types.Operator): + """ + Operation class: Preserve UV Aspect + """ + + bl_idname = "uv.muv_preserve_uv_aspect_operator" + bl_label = "Preserve UV Aspect" + bl_description = "Choose Image" + bl_options = {'REGISTER', 'UNDO'} + + dest_img_name = StringProperty(options={'HIDDEN'}) + origin = EnumProperty( + name="Origin", + description="Aspect Origin", + items=[ + ('CENTER', 'Center', 'Center'), + ('LEFT_TOP', 'Left Top', 'Left Bottom'), + ('LEFT_CENTER', 'Left Center', 'Left Center'), + ('LEFT_BOTTOM', 'Left Bottom', 'Left Bottom'), + ('CENTER_TOP', 'Center Top', 'Center Top'), + ('CENTER_BOTTOM', 'Center Bottom', 'Center Bottom'), + ('RIGHT_TOP', 'Right Top', 'Right Top'), + ('RIGHT_CENTER', 'Right Center', 'Right Center'), + ('RIGHT_BOTTOM', 'Right Bottom', 'Right Bottom') + + ], + default="CENTER" + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + + def execute(self, context): + # Note: the current system only works if the + # f[tex_layer].image doesn't return None + # which will happen in certain cases + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + if not bm.loops.layers.uv: + self.report({'WARNING'}, "Object must have more than one UV map") + return {'CANCELLED'} + + uv_layer = bm.loops.layers.uv.verify() + tex_layer = bm.faces.layers.tex.verify() + + sel_faces = [f for f in bm.faces if f.select] + dest_img = bpy.data.images[self.dest_img_name] + + info = {} + + for f in sel_faces: + if not f[tex_layer].image in info.keys(): + info[f[tex_layer].image] = {} + info[f[tex_layer].image]['faces'] = [] + info[f[tex_layer].image]['faces'].append(f) + + for img in info: + if img is None: + continue + + src_img = img + ratio = Vector(( + dest_img.size[0] / src_img.size[0], + dest_img.size[1] / src_img.size[1])) + + if self.origin == 'CENTER': + origin = Vector((0.0, 0.0)) + num = 0 + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin = origin + uv + num = num + 1 + origin = origin / num + elif self.origin == 'LEFT_TOP': + origin = Vector((100000.0, -100000.0)) + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = min(origin.x, uv.x) + origin.y = max(origin.y, uv.y) + elif self.origin == 'LEFT_CENTER': + origin = Vector((100000.0, 0.0)) + num = 0 + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = min(origin.x, uv.x) + origin.y = origin.y + uv.y + num = num + 1 + origin.y = origin.y / num + elif self.origin == 'LEFT_BOTTOM': + origin = Vector((100000.0, 100000.0)) + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = min(origin.x, uv.x) + origin.y = min(origin.y, uv.y) + elif self.origin == 'CENTER_TOP': + origin = Vector((0.0, -100000.0)) + num = 0 + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = origin.x + uv.x + origin.y = max(origin.y, uv.y) + num = num + 1 + origin.x = origin.x / num + elif self.origin == 'CENTER_BOTTOM': + origin = Vector((0.0, 100000.0)) + num = 0 + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = origin.x + uv.x + origin.y = min(origin.y, uv.y) + num = num + 1 + origin.x = origin.x / num + elif self.origin == 'RIGHT_TOP': + origin = Vector((-100000.0, -100000.0)) + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = max(origin.x, uv.x) + origin.y = max(origin.y, uv.y) + elif self.origin == 'RIGHT_CENTER': + origin = Vector((-100000.0, 0.0)) + num = 0 + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = max(origin.x, uv.x) + origin.y = origin.y + uv.y + num = num + 1 + origin.y = origin.y / num + elif self.origin == 'RIGHT_BOTTOM': + origin = Vector((-100000.0, 100000.0)) + for f in info[img]['faces']: + for l in f.loops: + uv = l[uv_layer].uv + origin.x = max(origin.x, uv.x) + origin.y = min(origin.y, uv.y) + + info[img]['ratio'] = ratio + info[img]['origin'] = origin + + for img in info: + if img is None: + continue + + for f in info[img]['faces']: + f[tex_layer].image = dest_img + for l in f.loops: + uv = l[uv_layer].uv + origin = info[img]['origin'] + ratio = info[img]['ratio'] + diff = uv - origin + diff.x = diff.x / ratio.x + diff.y = diff.y / ratio.y + uv.x = origin.x + diff.x + uv.y = origin.y + diff.y + l[uv_layer].uv = uv + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/uv_magic_uv/legacy/op/select_uv.py b/uv_magic_uv/legacy/op/select_uv.py new file mode 100644 index 00000000..bdc182d5 --- /dev/null +++ b/uv_magic_uv/legacy/op/select_uv.py @@ -0,0 +1,168 @@ +# + +# ##### 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 +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_SelectUV_SelectFlipped', + 'MUV_OT_SelectUV_SelectOverlapped', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "select_uv" + + @classmethod + def init_props(cls, scene): + scene.muv_select_uv_enabled = BoolProperty( + name="Select UV Enabled", + description="Select UV is enabled", + default=False + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_select_uv_enabled + + +@BlClassRegistry(legacy=True) +class MUV_OT_SelectUV_SelectOverlapped(bpy.types.Operator): + """ + Operation class: Select faces which have overlapped UVs + """ + + bl_idname = "uv.muv_select_uv_operator_select_overlapped" + bl_label = "Overlapped" + bl_description = "Select faces which have overlapped UVs" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + + def execute(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.verify() + + if context.tool_settings.use_uv_select_sync: + sel_faces = [f for f in bm.faces] + else: + sel_faces = [f for f in bm.faces if f.select] + + overlapped_info = common.get_overlapped_uv_info(bm, sel_faces, + uv_layer, 'FACE') + + for info in overlapped_info: + if context.tool_settings.use_uv_select_sync: + info["subject_face"].select = True + else: + for l in info["subject_face"].loops: + l[uv_layer].select = True + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} + + +@BlClassRegistry(legacy=True) +class MUV_OT_SelectUV_SelectFlipped(bpy.types.Operator): + """ + Operation class: Select faces which have flipped UVs + """ + + bl_idname = "uv.muv_select_uv_operator_select_flipped" + bl_label = "Flipped" + bl_description = "Select faces which have flipped UVs" + bl_options = {'REGISTER', 'UNDO'} + + @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/legacy/op/smooth_uv.py b/uv_magic_uv/legacy/op/smooth_uv.py new file mode 100644 index 00000000..63062554 --- /dev/null +++ b/uv_magic_uv/legacy/op/smooth_uv.py @@ -0,0 +1,287 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "imdjs, Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bpy +import bmesh +from bpy.props import BoolProperty, FloatProperty + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_SmoothUV', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "smooth_uv" + + @classmethod + def init_props(cls, scene): + scene.muv_smooth_uv_enabled = BoolProperty( + name="Smooth UV Enabled", + description="Smooth UV is enabled", + default=False + ) + scene.muv_smooth_uv_transmission = BoolProperty( + name="Transmission", + description="Smooth linked UVs", + default=False + ) + scene.muv_smooth_uv_mesh_infl = FloatProperty( + name="Mesh Influence", + description="Influence rate of mesh vertex", + min=0.0, + max=1.0, + default=0.0 + ) + scene.muv_smooth_uv_select = BoolProperty( + name="Select", + description="Select UVs which are smoothed", + default=False + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_smooth_uv_enabled + del scene.muv_smooth_uv_transmission + del scene.muv_smooth_uv_mesh_infl + del scene.muv_smooth_uv_select + + +@BlClassRegistry(legacy=True) +class MUV_OT_SmoothUV(bpy.types.Operator): + + bl_idname = "uv.muv_smooth_uv_operator" + bl_label = "Smooth" + bl_description = "Smooth UV coordinates" + bl_options = {'REGISTER', 'UNDO'} + + transmission = BoolProperty( + name="Transmission", + description="Smooth linked UVs", + default=False + ) + mesh_infl = FloatProperty( + name="Mesh Influence", + description="Influence rate of mesh vertex", + min=0.0, + max=1.0, + default=0.0 + ) + select = BoolProperty( + name="Select", + description="Select UVs which are smoothed", + default=False + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + + def __smooth_wo_transmission(self, loop_seqs, uv_layer): + # calculate path length + loops = [] + for hseq in loop_seqs: + loops.extend([hseq[0][0], hseq[0][1]]) + full_vlen = 0 + accm_vlens = [0.0] + full_uvlen = 0 + accm_uvlens = [0.0] + orig_uvs = [loop_seqs[0][0][0][uv_layer].uv.copy()] + for l1, l2 in zip(loops[:-1], loops[1:]): + diff_v = l2.vert.co - l1.vert.co + full_vlen = full_vlen + diff_v.length + accm_vlens.append(full_vlen) + diff_uv = l2[uv_layer].uv - l1[uv_layer].uv + full_uvlen = full_uvlen + diff_uv.length + accm_uvlens.append(full_uvlen) + orig_uvs.append(l2[uv_layer].uv.copy()) + + for hidx, hseq in enumerate(loop_seqs): + pair = hseq[0] + for pidx, l in enumerate(pair): + if self.select: + l[uv_layer].select = True + + # ignore start/end loop + if (hidx == 0 and pidx == 0) or\ + ((hidx == len(loop_seqs) - 1) and (pidx == len(pair) - 1)): + continue + + # calculate target path length + # target = no influenced * (1 - infl) + influenced * infl + tgt_noinfl = full_uvlen * (hidx + pidx) / (len(loop_seqs)) + tgt_infl = full_uvlen * accm_vlens[hidx * 2 + pidx] / full_vlen + target_length = tgt_noinfl * (1 - self.mesh_infl) + \ + tgt_infl * self.mesh_infl + + # get target UV + for i in range(len(accm_uvlens[:-1])): + # get line segment which UV will be placed + if ((accm_uvlens[i] <= target_length) and + (accm_uvlens[i + 1] > target_length)): + tgt_seg_len = target_length - accm_uvlens[i] + seg_len = accm_uvlens[i + 1] - accm_uvlens[i] + uv1 = orig_uvs[i] + uv2 = orig_uvs[i + 1] + target_uv = uv1 + (uv2 - uv1) * tgt_seg_len / seg_len + break + else: + self.report({'ERROR'}, "Failed to get target UV") + return {'CANCELLED'} + + # update UV + l[uv_layer].uv = target_uv + + def __smooth_w_transmission(self, loop_seqs, uv_layer): + # calculate path length + loops = [] + for vidx in range(len(loop_seqs[0])): + ls = [] + for hseq in loop_seqs: + ls.extend(hseq[vidx]) + loops.append(ls) + + orig_uvs = [] + accm_vlens = [] + full_vlens = [] + accm_uvlens = [] + full_uvlens = [] + for ls in loops: + full_v = 0.0 + accm_v = [0.0] + full_uv = 0.0 + accm_uv = [0.0] + uvs = [ls[0][uv_layer].uv.copy()] + for l1, l2 in zip(ls[:-1], ls[1:]): + diff_v = l2.vert.co - l1.vert.co + full_v = full_v + diff_v.length + accm_v.append(full_v) + diff_uv = l2[uv_layer].uv - l1[uv_layer].uv + full_uv = full_uv + diff_uv.length + accm_uv.append(full_uv) + uvs.append(l2[uv_layer].uv.copy()) + accm_vlens.append(accm_v) + full_vlens.append(full_v) + accm_uvlens.append(accm_uv) + full_uvlens.append(full_uv) + orig_uvs.append(uvs) + + for hidx, hseq in enumerate(loop_seqs): + for vidx, (pair, uvs, accm_v, full_v, accm_uv, full_uv)\ + in enumerate(zip(hseq, orig_uvs, accm_vlens, full_vlens, + accm_uvlens, full_uvlens)): + for pidx, l in enumerate(pair): + if self.select: + l[uv_layer].select = True + + # ignore start/end loop + if hidx == 0 and pidx == 0: + continue + if hidx == len(loop_seqs) - 1 and pidx == len(pair) - 1: + continue + + # calculate target path length + # target = no influenced * (1 - infl) + influenced * infl + tgt_noinfl = full_uv * (hidx + pidx) / (len(loop_seqs)) + tgt_infl = full_uv * accm_v[hidx * 2 + pidx] / full_v + target_length = tgt_noinfl * (1 - self.mesh_infl) + \ + tgt_infl * self.mesh_infl + + # get target UV + for i in range(len(accm_uv[:-1])): + # get line segment to be placed + if ((accm_uv[i] <= target_length) and + (accm_uv[i + 1] > target_length)): + tgt_seg_len = target_length - accm_uv[i] + seg_len = accm_uv[i + 1] - accm_uv[i] + uv1 = uvs[i] + uv2 = uvs[i + 1] + target_uv = uv1 +\ + (uv2 - uv1) * tgt_seg_len / seg_len + break + else: + self.report({'ERROR'}, "Failed to get target UV") + return {'CANCELLED'} + + # update UV + l[uv_layer].uv = target_uv + + def __smooth(self, loop_seqs, uv_layer): + if self.transmission: + self.__smooth_w_transmission(loop_seqs, uv_layer) + else: + self.__smooth_wo_transmission(loop_seqs, uv_layer) + + def execute(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.verify() + + # loop_seqs[horizontal][vertical][loop] + loop_seqs, error = common.get_loop_sequences(bm, uv_layer) + if not loop_seqs: + self.report({'WARNING'}, error) + return {'CANCELLED'} + + # smooth + self.__smooth(loop_seqs, uv_layer) + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/uv_magic_uv/legacy/op/texture_lock.py b/uv_magic_uv/legacy/op/texture_lock.py new file mode 100644 index 00000000..65873106 --- /dev/null +++ b/uv_magic_uv/legacy/op/texture_lock.py @@ -0,0 +1,545 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import math +from math import atan2, cos, sqrt, sin, fabs + +import bpy +import bmesh +from mathutils import Vector +from bpy.props import BoolProperty + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_TextureLock_Lock', + 'MUV_OT_TextureLock_Unlock', + 'MUV_OT_TextureLock_Intr', +] + + +def get_vco(verts_orig, loop): + """ + Get vertex original coordinate from loop + """ + for vo in verts_orig: + if vo["vidx"] == loop.vert.index and vo["moved"] is False: + return vo["vco"] + return loop.vert.co + + +def get_link_loops(vert): + """ + Get loop linked to vertex + """ + link_loops = [] + for f in vert.link_faces: + adj_loops = [] + for loop in f.loops: + # self loop + if loop.vert == vert: + l = loop + # linked loop + else: + for e in loop.vert.link_edges: + if e.other_vert(loop.vert) == vert: + adj_loops.append(loop) + if len(adj_loops) < 2: + return None + + link_loops.append({"l": l, "l0": adj_loops[0], "l1": adj_loops[1]}) + return link_loops + + +def get_ini_geom(link_loop, uv_layer, verts_orig, v_orig): + """ + Get initial geometory + (Get interior angle of face in vertex/UV space) + """ + u = link_loop["l"][uv_layer].uv + v0 = get_vco(verts_orig, link_loop["l0"]) + u0 = link_loop["l0"][uv_layer].uv + v1 = get_vco(verts_orig, link_loop["l1"]) + u1 = link_loop["l1"][uv_layer].uv + + # get interior angle of face in vertex space + v0v1 = v1 - v0 + v0v = v_orig["vco"] - v0 + v1v = v_orig["vco"] - v1 + theta0 = v0v1.angle(v0v) + theta1 = v0v1.angle(-v1v) + if (theta0 + theta1) > math.pi: + theta0 = v0v1.angle(-v0v) + theta1 = v0v1.angle(v1v) + + # get interior angle of face in UV space + u0u1 = u1 - u0 + u0u = u - u0 + u1u = u - u1 + phi0 = u0u1.angle(u0u) + phi1 = u0u1.angle(-u1u) + if (phi0 + phi1) > math.pi: + phi0 = u0u1.angle(-u0u) + phi1 = u0u1.angle(u1u) + + # get direction of linked UV coordinate + # this will be used to judge whether angle is more or less than 180 degree + dir0 = u0u1.cross(u0u) > 0 + dir1 = u0u1.cross(u1u) > 0 + + return { + "theta0": theta0, + "theta1": theta1, + "phi0": phi0, + "phi1": phi1, + "dir0": dir0, + "dir1": dir1} + + +def get_target_uv(link_loop, uv_layer, verts_orig, v, ini_geom): + """ + Get target UV coordinate + """ + v0 = get_vco(verts_orig, link_loop["l0"]) + lo0 = link_loop["l0"] + v1 = get_vco(verts_orig, link_loop["l1"]) + lo1 = link_loop["l1"] + + # get interior angle of face in vertex space + v0v1 = v1 - v0 + v0v = v.co - v0 + v1v = v.co - v1 + theta0 = v0v1.angle(v0v) + theta1 = v0v1.angle(-v1v) + if (theta0 + theta1) > math.pi: + theta0 = v0v1.angle(-v0v) + theta1 = v0v1.angle(v1v) + + # calculate target interior angle in UV space + phi0 = theta0 * ini_geom["phi0"] / ini_geom["theta0"] + phi1 = theta1 * ini_geom["phi1"] / ini_geom["theta1"] + + uv0 = lo0[uv_layer].uv + uv1 = lo1[uv_layer].uv + + # calculate target vertex coordinate from target interior angle + tuv0, tuv1 = calc_tri_vert(uv0, uv1, phi0, phi1) + + # target UV coordinate depends on direction, so judge using direction of + # linked UV coordinate + u0u1 = uv1 - uv0 + u0u = tuv0 - uv0 + u1u = tuv0 - uv1 + dir0 = u0u1.cross(u0u) > 0 + dir1 = u0u1.cross(u1u) > 0 + if (ini_geom["dir0"] != dir0) or (ini_geom["dir1"] != dir1): + return tuv1 + + return tuv0 + + +def calc_tri_vert(v0, v1, angle0, angle1): + """ + Calculate rest coordinate from other coordinates and angle of end + """ + angle = math.pi - angle0 - angle1 + + alpha = atan2(v1.y - v0.y, v1.x - v0.x) + d = (v1.x - v0.x) / cos(alpha) + a = d * sin(angle0) / sin(angle) + b = d * sin(angle1) / sin(angle) + s = (a + b + d) / 2.0 + if fabs(d) < 0.0000001: + xd = 0 + yd = 0 + else: + r = s * (s - a) * (s - b) * (s - d) + if r < 0: + xd = 0 + yd = 0 + else: + xd = (b * b - a * a + d * d) / (2 * d) + yd = 2 * sqrt(r) / d + x1 = xd * cos(alpha) - yd * sin(alpha) + v0.x + y1 = xd * sin(alpha) + yd * cos(alpha) + v0.y + x2 = xd * cos(alpha) + yd * sin(alpha) + v0.x + y2 = xd * sin(alpha) - yd * cos(alpha) + v0.y + + return Vector((x1, y1)), Vector((x2, y2)) + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "texture_lock" + + @classmethod + def init_props(cls, scene): + class Props(): + verts_orig = None + + scene.muv_props.texture_lock = Props() + + def get_func(_): + return MUV_OT_TextureLock_Intr.is_running(bpy.context) + + def set_func(_, __): + pass + + def update_func(_, __): + bpy.ops.uv.muv_texture_lock_operator_intr('INVOKE_REGION_WIN') + + scene.muv_texture_lock_enabled = BoolProperty( + name="Texture Lock Enabled", + description="Texture Lock is enabled", + default=False + ) + scene.muv_texture_lock_lock = BoolProperty( + name="Texture Lock Locked", + description="Texture Lock is locked", + default=False, + get=get_func, + set=set_func, + update=update_func + ) + scene.muv_texture_lock_connect = BoolProperty( + name="Connect UV", + default=True + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_props.texture_lock + del scene.muv_texture_lock_enabled + del scene.muv_texture_lock_lock + del scene.muv_texture_lock_connect + + +@BlClassRegistry(legacy=True) +class MUV_OT_TextureLock_Lock(bpy.types.Operator): + """ + Operation class: Lock Texture + """ + + bl_idname = "uv.muv_texture_lock_operator_lock" + bl_label = "Lock Texture" + bl_description = "Lock Texture" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + + @classmethod + def is_ready(cls, context): + sc = context.scene + props = sc.muv_props.texture_lock + if props.verts_orig: + return True + return False + + def execute(self, context): + props = context.scene.muv_props.texture_lock + obj = bpy.context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + if not bm.loops.layers.uv: + self.report( + {'WARNING'}, "Object must have more than one UV map") + return {'CANCELLED'} + + props.verts_orig = [ + {"vidx": v.index, "vco": v.co.copy(), "moved": False} + for v in bm.verts if v.select] + + return {'FINISHED'} + + +@BlClassRegistry(legacy=True) +class MUV_OT_TextureLock_Unlock(bpy.types.Operator): + """ + Operation class: Unlock Texture + """ + + bl_idname = "uv.muv_texture_lock_operator_unlock" + bl_label = "Unlock Texture" + bl_description = "Unlock Texture" + bl_options = {'REGISTER', 'UNDO'} + + connect = BoolProperty( + name="Connect UV", + default=True + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.texture_lock + if not props.verts_orig: + return False + if not MUV_OT_TextureLock_Lock.is_ready(context): + return False + if not is_valid_context(context): + return False + return True + + def execute(self, context): + sc = context.scene + props = sc.muv_props.texture_lock + obj = bpy.context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + if not bm.loops.layers.uv: + self.report( + {'WARNING'}, "Object must have more than one UV map") + return {'CANCELLED'} + uv_layer = bm.loops.layers.uv.verify() + + verts = [v.index for v in bm.verts if v.select] + verts_orig = props.verts_orig + + # move UV followed by vertex coordinate + for vidx, v_orig in zip(verts, verts_orig): + if vidx != v_orig["vidx"]: + self.report({'ERROR'}, "Internal Error") + return {"CANCELLED"} + + v = bm.verts[vidx] + link_loops = get_link_loops(v) + + result = [] + + for ll in link_loops: + ini_geom = get_ini_geom(ll, uv_layer, verts_orig, v_orig) + target_uv = get_target_uv( + ll, uv_layer, verts_orig, v, ini_geom) + result.append({"l": ll["l"], "uv": target_uv}) + + # connect other face's UV + if self.connect: + ave = Vector((0.0, 0.0)) + for r in result: + ave = ave + r["uv"] + ave = ave / len(result) + for r in result: + r["l"][uv_layer].uv = ave + else: + for r in result: + r["l"][uv_layer].uv = r["uv"] + v_orig["moved"] = True + bmesh.update_edit_mesh(obj.data) + + props.verts_orig = None + + return {'FINISHED'} + + +@BlClassRegistry(legacy=True) +class MUV_OT_TextureLock_Intr(bpy.types.Operator): + """ + Operation class: Texture Lock (Interactive mode) + """ + + bl_idname = "uv.muv_texture_lock_operator_intr" + bl_label = "Texture Lock (Interactive mode)" + bl_description = "Internal operation for Texture Lock (Interactive mode)" + + __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.__intr_verts_orig = [] + self.__intr_verts = [] + + def __sel_verts_changed(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + prev = set(self.__intr_verts) + now = set([v.index for v in bm.verts if v.select]) + + return prev != now + + def __reinit_verts(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + self.__intr_verts_orig = [ + {"vidx": v.index, "vco": v.co.copy(), "moved": False} + for v in bm.verts if v.select] + self.__intr_verts = [v.index for v in bm.verts if v.select] + + def __update_uv(self, context): + """ + Update UV when vertex coordinates are changed + """ + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + if not bm.loops.layers.uv: + self.report({'WARNING'}, "Object must have more than one UV map") + return + uv_layer = bm.loops.layers.uv.verify() + + verts = [v.index for v in bm.verts if v.select] + verts_orig = self.__intr_verts_orig + + for vidx, v_orig in zip(verts, verts_orig): + if vidx != v_orig["vidx"]: + self.report({'ERROR'}, "Internal Error") + return + + v = bm.verts[vidx] + link_loops = get_link_loops(v) + + result = [] + for ll in link_loops: + ini_geom = get_ini_geom(ll, uv_layer, verts_orig, v_orig) + target_uv = get_target_uv( + ll, uv_layer, verts_orig, v, ini_geom) + result.append({"l": ll["l"], "uv": target_uv}) + + # UV connect option is always true, because it raises + # unexpected behavior + ave = Vector((0.0, 0.0)) + for r in result: + ave = ave + r["uv"] + ave = ave / len(result) + for r in result: + r["l"][uv_layer].uv = ave + v_orig["moved"] = True + bmesh.update_edit_mesh(obj.data) + + common.redraw_all_areas() + self.__intr_verts_orig = [ + {"vidx": v.index, "vco": v.co.copy(), "moved": False} + for v in bm.verts if v.select] + + def modal(self, context, event): + if not is_valid_context(context): + MUV_OT_TextureLock_Intr.handle_remove(context) + return {'FINISHED'} + + if not MUV_OT_TextureLock_Intr.is_running(context): + return {'FINISHED'} + + if context.area: + context.area.tag_redraw() + + if event.type == 'TIMER': + if self.__sel_verts_changed(context): + self.__reinit_verts(context) + else: + self.__update_uv(context) + + return {'PASS_THROUGH'} + + def invoke(self, context, _): + if not is_valid_context(context): + return {'CANCELLED'} + + if not MUV_OT_TextureLock_Intr.is_running(context): + MUV_OT_TextureLock_Intr.handle_add(self, context) + return {'RUNNING_MODAL'} + else: + MUV_OT_TextureLock_Intr.handle_remove(context) + + if context.area: + context.area.tag_redraw() + + return {'FINISHED'} diff --git a/uv_magic_uv/legacy/op/texture_projection.py b/uv_magic_uv/legacy/op/texture_projection.py new file mode 100644 index 00000000..58f69c9d --- /dev/null +++ b/uv_magic_uv/legacy/op/texture_projection.py @@ -0,0 +1,402 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +from collections import namedtuple + +import bpy +import bgl +import bmesh +import mathutils +from bpy_extras import view3d_utils +from bpy.props import ( + BoolProperty, + EnumProperty, + FloatProperty, +) + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_TextureProjection', + 'MUV_OT_TextureProjection_Project', +] + + +Rect = namedtuple('Rect', 'x0 y0 x1 y1') +Rect2 = namedtuple('Rect2', 'x y width height') + + +def get_loaded_texture_name(_, __): + items = [(key, key, "") for key in bpy.data.images.keys()] + items.append(("None", "None", "")) + return items + + +def get_canvas(context, magnitude): + """ + Get canvas to be renderred texture + """ + sc = context.scene + prefs = context.user_preferences.addons["uv_magic_uv"].preferences + + region_w = context.region.width + region_h = context.region.height + canvas_w = region_w - prefs.texture_projection_canvas_padding[0] * 2.0 + canvas_h = region_h - prefs.texture_projection_canvas_padding[1] * 2.0 + + img = bpy.data.images[sc.muv_texture_projection_tex_image] + tex_w = img.size[0] + tex_h = img.size[1] + + center_x = region_w * 0.5 + center_y = region_h * 0.5 + + if sc.muv_texture_projection_adjust_window: + ratio_x = canvas_w / tex_w + ratio_y = canvas_h / tex_h + if sc.muv_texture_projection_apply_tex_aspect: + ratio = ratio_y if ratio_x > ratio_y else ratio_x + len_x = ratio * tex_w + len_y = ratio * tex_h + else: + len_x = canvas_w + len_y = canvas_h + else: + if sc.muv_texture_projection_apply_tex_aspect: + len_x = tex_w * magnitude + len_y = tex_h * magnitude + else: + len_x = region_w * magnitude + len_y = region_h * magnitude + + x0 = int(center_x - len_x * 0.5) + y0 = int(center_y - len_y * 0.5) + x1 = int(center_x + len_x * 0.5) + y1 = int(center_y + len_y * 0.5) + + return Rect(x0, y0, x1, y1) + + +def rect_to_rect2(rect): + """ + Convert Rect1 to Rect2 + """ + + return Rect2(rect.x0, rect.y0, rect.x1 - rect.x0, rect.y1 - rect.y0) + + +def region_to_canvas(rg_vec, canvas): + """ + Convert screen region to canvas + """ + + cv_rect = rect_to_rect2(canvas) + cv_vec = mathutils.Vector() + cv_vec.x = (rg_vec.x - cv_rect.x) / cv_rect.width + cv_vec.y = (rg_vec.y - cv_rect.y) / cv_rect.height + + return cv_vec + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "texture_projection" + + @classmethod + def init_props(cls, scene): + def get_func(_): + return MUV_OT_TextureProjection.is_running(bpy.context) + + def set_func(_, __): + pass + + def update_func(_, __): + bpy.ops.uv.muv_texture_projection_operator('INVOKE_REGION_WIN') + + scene.muv_texture_projection_enabled = BoolProperty( + name="Texture Projection Enabled", + description="Texture Projection is enabled", + default=False + ) + scene.muv_texture_projection_enable = BoolProperty( + name="Texture Projection Enabled", + description="Texture Projection is enabled", + default=False, + get=get_func, + set=set_func, + update=update_func + ) + scene.muv_texture_projection_tex_magnitude = FloatProperty( + name="Magnitude", + description="Texture Magnitude", + default=0.5, + min=0.0, + max=100.0 + ) + scene.muv_texture_projection_tex_image = EnumProperty( + name="Image", + description="Texture Image", + items=get_loaded_texture_name + ) + scene.muv_texture_projection_tex_transparency = FloatProperty( + name="Transparency", + description="Texture Transparency", + default=0.2, + min=0.0, + max=1.0 + ) + scene.muv_texture_projection_adjust_window = BoolProperty( + name="Adjust Window", + description="Size of renderered texture is fitted to window", + default=True + ) + scene.muv_texture_projection_apply_tex_aspect = BoolProperty( + name="Texture Aspect Ratio", + description="Apply Texture Aspect ratio to displayed texture", + default=True + ) + scene.muv_texture_projection_assign_uvmap = BoolProperty( + name="Assign UVMap", + description="Assign UVMap when no UVmaps are available", + default=True + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_texture_projection_enabled + del scene.muv_texture_projection_tex_magnitude + del scene.muv_texture_projection_tex_image + del scene.muv_texture_projection_tex_transparency + del scene.muv_texture_projection_adjust_window + del scene.muv_texture_projection_apply_tex_aspect + del scene.muv_texture_projection_assign_uvmap + + +@BlClassRegistry(legacy=True) +class MUV_OT_TextureProjection(bpy.types.Operator): + """ + Operation class: Texture Projection + Render texture + """ + + bl_idname = "uv.muv_texture_projection_operator" + bl_description = "Render selected texture" + bl_label = "Texture renderer" + + __handle = None + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return False + return is_valid_context(context) + + @classmethod + def is_running(cls, _): + return 1 if cls.__handle else 0 + + @classmethod + def handle_add(cls, obj, context): + cls.__handle = bpy.types.SpaceView3D.draw_handler_add( + MUV_OT_TextureProjection.draw_texture, + (obj, context), 'WINDOW', 'POST_PIXEL') + + @classmethod + def handle_remove(cls): + if cls.__handle is not None: + bpy.types.SpaceView3D.draw_handler_remove(cls.__handle, 'WINDOW') + cls.__handle = None + + @classmethod + def draw_texture(cls, _, context): + sc = context.scene + + if not cls.is_running(context): + return + + # no textures are selected + if sc.muv_texture_projection_tex_image == "None": + return + + # get texture to be renderred + img = bpy.data.images[sc.muv_texture_projection_tex_image] + + # setup rendering region + rect = get_canvas(context, sc.muv_texture_projection_tex_magnitude) + positions = [ + [rect.x0, rect.y0], + [rect.x0, rect.y1], + [rect.x1, rect.y1], + [rect.x1, rect.y0] + ] + tex_coords = [ + [0.0, 0.0], + [0.0, 1.0], + [1.0, 1.0], + [1.0, 0.0] + ] + + # OpenGL configuration + bgl.glEnable(bgl.GL_BLEND) + bgl.glEnable(bgl.GL_TEXTURE_2D) + if img.bindcode: + bind = img.bindcode[0] + bgl.glBindTexture(bgl.GL_TEXTURE_2D, bind) + bgl.glTexParameteri( + bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MIN_FILTER, bgl.GL_LINEAR) + bgl.glTexParameteri( + bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MAG_FILTER, bgl.GL_LINEAR) + bgl.glTexEnvi( + bgl.GL_TEXTURE_ENV, bgl.GL_TEXTURE_ENV_MODE, bgl.GL_MODULATE) + + # render texture + bgl.glBegin(bgl.GL_QUADS) + bgl.glColor4f(1.0, 1.0, 1.0, + sc.muv_texture_projection_tex_transparency) + for (v1, v2), (u, v) in zip(positions, tex_coords): + bgl.glTexCoord2f(u, v) + bgl.glVertex2f(v1, v2) + bgl.glEnd() + + def invoke(self, context, _): + if not MUV_OT_TextureProjection.is_running(context): + MUV_OT_TextureProjection.handle_add(self, context) + else: + MUV_OT_TextureProjection.handle_remove() + + if context.area: + context.area.tag_redraw() + + return {'FINISHED'} + + +@BlClassRegistry(legacy=True) +class MUV_OT_TextureProjection_Project(bpy.types.Operator): + """ + Operation class: Project texture + """ + + bl_idname = "uv.muv_texture_projection_operator_project" + bl_label = "Project Texture" + bl_description = "Project Texture" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + if not MUV_OT_TextureProjection.is_running(context): + return False + return is_valid_context(context) + + def execute(self, context): + sc = context.scene + + if sc.muv_texture_projection_tex_image == "None": + self.report({'WARNING'}, "No textures are selected") + return {'CANCELLED'} + + _, region, space = common.get_space( + 'VIEW_3D', 'WINDOW', 'VIEW_3D') + + # get faces to be texture projected + obj = context.active_object + world_mat = obj.matrix_world + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + # get UV and texture layer + if not bm.loops.layers.uv: + if sc.muv_texture_projection_assign_uvmap: + bm.loops.layers.uv.new() + else: + self.report({'WARNING'}, + "Object must have more than one UV map") + return {'CANCELLED'} + + uv_layer = bm.loops.layers.uv.verify() + tex_layer = bm.faces.layers.tex.verify() + + sel_faces = [f for f in bm.faces if f.select] + + # transform 3d space to screen region + v_screen = [ + view3d_utils.location_3d_to_region_2d( + region, + space.region_3d, + world_mat * l.vert.co) + for f in sel_faces for l in f.loops + ] + + # transform screen region to canvas + v_canvas = [ + region_to_canvas( + v, + get_canvas(bpy.context, + sc.muv_texture_projection_tex_magnitude) + ) for v in v_screen + ] + + # project texture to object + i = 0 + for f in sel_faces: + f[tex_layer].image = \ + bpy.data.images[sc.muv_texture_projection_tex_image] + for l in f.loops: + l[uv_layer].uv = v_canvas[i].to_2d() + i = i + 1 + + common.redraw_all_areas() + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/uv_magic_uv/legacy/op/texture_wrap.py b/uv_magic_uv/legacy/op/texture_wrap.py new file mode 100644 index 00000000..cb4cc78c --- /dev/null +++ b/uv_magic_uv/legacy/op/texture_wrap.py @@ -0,0 +1,301 @@ +# + +# ##### 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 +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_TextureWrap_Refer', + 'MUV_OT_TextureWrap_Set', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "texture_wrap" + + @classmethod + def init_props(cls, scene): + class Props(): + ref_face_index = -1 + ref_obj = None + + scene.muv_props.texture_wrap = Props() + + scene.muv_texture_wrap_enabled = BoolProperty( + name="Texture Wrap", + description="Texture Wrap is enabled", + default=False + ) + scene.muv_texture_wrap_set_and_refer = BoolProperty( + name="Set and Refer", + description="Refer and set UV", + default=True + ) + scene.muv_texture_wrap_selseq = BoolProperty( + name="Selection Sequence", + description="Set UV sequentially", + default=False + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_props.texture_wrap + del scene.muv_texture_wrap_enabled + del scene.muv_texture_wrap_set_and_refer + del scene.muv_texture_wrap_selseq + + +@BlClassRegistry(legacy=True) +class MUV_OT_TextureWrap_Refer(bpy.types.Operator): + """ + Operation class: Refer UV + """ + + bl_idname = "uv.muv_texture_wrap_operator_refer" + bl_label = "Refer" + bl_description = "Refer UV" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + + def execute(self, context): + props = context.scene.muv_props.texture_wrap + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + if not bm.loops.layers.uv: + self.report({'WARNING'}, "Object must have more than one UV map") + return {'CANCELLED'} + + sel_faces = [f for f in bm.faces if f.select] + if len(sel_faces) != 1: + self.report({'WARNING'}, "Must select only one face") + return {'CANCELLED'} + + props.ref_face_index = sel_faces[0].index + props.ref_obj = obj + + return {'FINISHED'} + + +@BlClassRegistry(legacy=True) +class MUV_OT_TextureWrap_Set(bpy.types.Operator): + """ + Operation class: Set UV + """ + + bl_idname = "uv.muv_texture_wrap_operator_set" + bl_label = "Set" + bl_description = "Set UV" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.texture_wrap + if not props.ref_obj: + return False + return is_valid_context(context) + + def execute(self, context): + sc = context.scene + props = sc.muv_props.texture_wrap + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + if not bm.loops.layers.uv: + self.report({'WARNING'}, "Object must have more than one UV map") + return {'CANCELLED'} + uv_layer = bm.loops.layers.uv.verify() + + if sc.muv_texture_wrap_selseq: + sel_faces = [] + for hist in bm.select_history: + if isinstance(hist, bmesh.types.BMFace) and hist.select: + sel_faces.append(hist) + if not sel_faces: + self.report({'WARNING'}, "Must select more than one face") + return {'CANCELLED'} + else: + sel_faces = [f for f in bm.faces if f.select] + if len(sel_faces) != 1: + self.report({'WARNING'}, "Must select only one face") + return {'CANCELLED'} + + ref_face_index = props.ref_face_index + for face in sel_faces: + tgt_face_index = face.index + if ref_face_index == tgt_face_index: + self.report({'WARNING'}, "Must select different face") + return {'CANCELLED'} + + if props.ref_obj != obj: + self.report({'WARNING'}, "Object must be same") + return {'CANCELLED'} + + ref_face = bm.faces[ref_face_index] + tgt_face = bm.faces[tgt_face_index] + + # get common vertices info + common_verts = [] + for sl in ref_face.loops: + for dl in tgt_face.loops: + if sl.vert == dl.vert: + info = {"vert": sl.vert, "ref_loop": sl, + "tgt_loop": dl} + common_verts.append(info) + break + + if len(common_verts) != 2: + self.report({'WARNING'}, + "2 vertices must be shared among faces") + return {'CANCELLED'} + + # get reference other vertices info + ref_other_verts = [] + for sl in ref_face.loops: + for ci in common_verts: + if sl.vert == ci["vert"]: + break + else: + info = {"vert": sl.vert, "loop": sl} + ref_other_verts.append(info) + + if not ref_other_verts: + self.report({'WARNING'}, "More than 1 vertex must be unshared") + return {'CANCELLED'} + + # get reference info + ref_info = {} + cv0 = common_verts[0]["vert"].co + cv1 = common_verts[1]["vert"].co + cuv0 = common_verts[0]["ref_loop"][uv_layer].uv + cuv1 = common_verts[1]["ref_loop"][uv_layer].uv + ov0 = ref_other_verts[0]["vert"].co + ouv0 = ref_other_verts[0]["loop"][uv_layer].uv + ref_info["vert_vdiff"] = cv1 - cv0 + ref_info["uv_vdiff"] = cuv1 - cuv0 + ref_info["vert_hdiff"], _ = common.diff_point_to_segment( + cv0, cv1, ov0) + ref_info["uv_hdiff"], _ = common.diff_point_to_segment( + cuv0, cuv1, ouv0) + + # get target other vertices info + tgt_other_verts = [] + for dl in tgt_face.loops: + for ci in common_verts: + if dl.vert == ci["vert"]: + break + else: + info = {"vert": dl.vert, "loop": dl} + tgt_other_verts.append(info) + + if not tgt_other_verts: + self.report({'WARNING'}, "More than 1 vertex must be unshared") + return {'CANCELLED'} + + # get target info + for info in tgt_other_verts: + cv0 = common_verts[0]["vert"].co + cv1 = common_verts[1]["vert"].co + cuv0 = common_verts[0]["ref_loop"][uv_layer].uv + ov = info["vert"].co + info["vert_hdiff"], x = common.diff_point_to_segment( + cv0, cv1, ov) + info["vert_vdiff"] = x - common_verts[0]["vert"].co + + # calclulate factor + fact_h = -info["vert_hdiff"].length / \ + ref_info["vert_hdiff"].length + fact_v = info["vert_vdiff"].length / \ + ref_info["vert_vdiff"].length + duv_h = ref_info["uv_hdiff"] * fact_h + duv_v = ref_info["uv_vdiff"] * fact_v + + # get target UV + info["target_uv"] = cuv0 + duv_h + duv_v + + # apply to common UVs + for info in common_verts: + info["tgt_loop"][uv_layer].uv = \ + info["ref_loop"][uv_layer].uv.copy() + # apply to other UVs + for info in tgt_other_verts: + info["loop"][uv_layer].uv = info["target_uv"] + + common.debug_print("===== Target Other Vertices =====") + common.debug_print(tgt_other_verts) + + bmesh.update_edit_mesh(obj.data) + + ref_face_index = tgt_face_index + + if sc.muv_texture_wrap_set_and_refer: + props.ref_face_index = tgt_face_index + + return {'FINISHED'} diff --git a/uv_magic_uv/legacy/op/transfer_uv.py b/uv_magic_uv/legacy/op/transfer_uv.py new file mode 100644 index 00000000..cd0e4dd9 --- /dev/null +++ b/uv_magic_uv/legacy/op/transfer_uv.py @@ -0,0 +1,172 @@ +# + +# ##### 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 , Mifth, MaxRobinot" +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bpy +import bmesh +from bpy.props import BoolProperty + +from ... import common +from ...impl import transfer_uv_impl as impl +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_TransferUV_CopyUV', + 'MUV_OT_TransferUV_PasteUV', +] + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "transfer_uv" + + @classmethod + def init_props(cls, scene): + class Props(): + topology_copied = None + + scene.muv_props.transfer_uv = Props() + + scene.muv_transfer_uv_enabled = BoolProperty( + name="Transfer UV Enabled", + description="Transfer UV is enabled", + default=False + ) + scene.muv_transfer_uv_invert_normals = BoolProperty( + name="Invert Normals", + description="Invert Normals", + default=False + ) + scene.muv_transfer_uv_copy_seams = BoolProperty( + name="Copy Seams", + description="Copy Seams", + default=True + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_transfer_uv_enabled + del scene.muv_transfer_uv_invert_normals + del scene.muv_transfer_uv_copy_seams + + +@BlClassRegistry(legacy=True) +class MUV_OT_TransferUV_CopyUV(bpy.types.Operator): + """ + Operation class: 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 impl.is_valid_context(context) + + def execute(self, context): + 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: + bm.faces.ensure_lookup_table() + + uv_layer = impl.get_uv_layer(self, bm) + if uv_layer is None: + return {'CANCELLED'} + + faces = impl.get_selected_src_faces(self, bm, uv_layer) + if faces is None: + return {'CANCELLED'} + props.topology_copied = faces + + bmesh.update_edit_mesh(active_obj.data) + + return {'FINISHED'} + + +@BlClassRegistry(legacy=True) +class MUV_OT_TransferUV_PasteUV(bpy.types.Operator): + """ + Operation class: 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( + name="Invert Normals", + description="Invert Normals", + default=False + ) + copy_seams = BoolProperty( + name="Copy Seams", + description="Copy Seams", + default=True + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + sc = context.scene + props = sc.muv_props.transfer_uv + if not props.topology_copied: + return False + return impl.is_valid_context(context) + + def execute(self, context): + 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: + bm.faces.ensure_lookup_table() + + # get UV layer + uv_layer = impl.get_uv_layer(self, bm) + if uv_layer is None: + return {'CANCELLED'} + + ret = impl.paste_uv(self, bm, uv_layer, props.topology_copied, + self.invert_normals, self.copy_seams) + if ret: + return {'CANCELLED'} + + bmesh.update_edit_mesh(active_obj.data) + if self.copy_seams: + active_obj.data.show_edge_seams = True + + return {'FINISHED'} diff --git a/uv_magic_uv/legacy/op/unwrap_constraint.py b/uv_magic_uv/legacy/op/unwrap_constraint.py new file mode 100644 index 00000000..f06efce1 --- /dev/null +++ b/uv_magic_uv/legacy/op/unwrap_constraint.py @@ -0,0 +1,190 @@ +# ##### 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, + EnumProperty, + FloatProperty, +) + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_UnwrapConstraint', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "unwrap_constraint" + + @classmethod + def init_props(cls, scene): + scene.muv_unwrap_constraint_enabled = BoolProperty( + name="Unwrap Constraint Enabled", + description="Unwrap Constraint is enabled", + default=False + ) + scene.muv_unwrap_constraint_u_const = BoolProperty( + name="U-Constraint", + description="Keep UV U-axis coordinate", + default=False + ) + scene.muv_unwrap_constraint_v_const = BoolProperty( + name="V-Constraint", + description="Keep UV V-axis coordinate", + default=False + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_unwrap_constraint_enabled + del scene.muv_unwrap_constraint_u_const + del scene.muv_unwrap_constraint_v_const + + +@BlClassRegistry(legacy=True) +class MUV_OT_UnwrapConstraint(bpy.types.Operator): + """ + Operation class: Unwrap with constrain UV coordinate + """ + + bl_idname = "uv.muv_unwrap_constraint_operator" + bl_label = "Unwrap Constraint" + bl_description = "Unwrap while keeping uv coordinate" + bl_options = {'REGISTER', 'UNDO'} + + # property for original unwrap + method = EnumProperty( + name="Method", + description="Unwrapping method", + items=[ + ('ANGLE_BASED', 'Angle Based', 'Angle Based'), + ('CONFORMAL', 'Conformal', 'Conformal') + ], + default='ANGLE_BASED') + fill_holes = BoolProperty( + name="Fill Holes", + description="Virtual fill holes in meshes before unwrapping", + default=True) + correct_aspect = BoolProperty( + name="Correct Aspect", + description="Map UVs taking image aspect ratio into account", + default=True) + use_subsurf_data = BoolProperty( + name="Use Subsurf Modifier", + description="""Map UVs taking vertex position after subsurf + into account""", + default=False) + margin = FloatProperty( + name="Margin", + description="Space between islands", + max=1.0, + min=0.0, + default=0.001) + + # property for this operation + u_const = BoolProperty( + name="U-Constraint", + description="Keep UV U-axis coordinate", + default=False + ) + v_const = BoolProperty( + name="V-Constraint", + description="Keep UV V-axis coordinate", + default=False + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + + def execute(self, _): + obj = bpy.context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + # bpy.ops.uv.unwrap() makes one UV map at least + if not bm.loops.layers.uv: + self.report({'WARNING'}, "Object must have more than one UV map") + return {'CANCELLED'} + uv_layer = bm.loops.layers.uv.verify() + + # get original UV coordinate + faces = [f for f in bm.faces if f.select] + uv_list = [] + for f in faces: + uvs = [l[uv_layer].uv.copy() for l in f.loops] + uv_list.append(uvs) + + # unwrap + bpy.ops.uv.unwrap( + method=self.method, + fill_holes=self.fill_holes, + correct_aspect=self.correct_aspect, + use_subsurf_data=self.use_subsurf_data, + margin=self.margin) + + # when U/V-Constraint is checked, revert original coordinate + for f, uvs in zip(faces, uv_list): + for l, uv in zip(f.loops, uvs): + if self.u_const: + l[uv_layer].uv.x = uv.x + if self.v_const: + l[uv_layer].uv.y = uv.y + + # update mesh + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/uv_magic_uv/legacy/op/uv_bounding_box.py b/uv_magic_uv/legacy/op/uv_bounding_box.py new file mode 100644 index 00000000..47c27e41 --- /dev/null +++ b/uv_magic_uv/legacy/op/uv_bounding_box.py @@ -0,0 +1,838 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +from enum import IntEnum +import math + +import bpy +import bgl +import mathutils +import bmesh +from bpy.props import BoolProperty, EnumProperty + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_UVBoundingBox', +] + + +MAX_VALUE = 100000.0 + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "uv_bounding_box" + + @classmethod + def init_props(cls, scene): + class Props(): + uv_info_ini = [] + ctrl_points_ini = [] + ctrl_points = [] + + scene.muv_props.uv_bounding_box = Props() + + def get_func(_): + return MUV_OT_UVBoundingBox.is_running(bpy.context) + + def set_func(_, __): + pass + + def update_func(_, __): + bpy.ops.uv.muv_uv_bounding_box_operator('INVOKE_REGION_WIN') + + scene.muv_uv_bounding_box_enabled = BoolProperty( + name="UV Bounding Box Enabled", + description="UV Bounding Box is enabled", + default=False + ) + scene.muv_uv_bounding_box_show = BoolProperty( + name="UV Bounding Box Showed", + description="UV Bounding Box is showed", + default=False, + get=get_func, + set=set_func, + update=update_func + ) + scene.muv_uv_bounding_box_uniform_scaling = BoolProperty( + name="Uniform Scaling", + description="Enable Uniform Scaling", + default=False + ) + scene.muv_uv_bounding_box_boundary = EnumProperty( + name="Boundary", + description="Boundary", + default='UV_SEL', + items=[ + ('UV', "UV", "Boundary is decided by UV"), + ('UV_SEL', "UV (Selected)", + "Boundary is decided by Selected UV") + ] + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_props.uv_bounding_box + del scene.muv_uv_bounding_box_enabled + del scene.muv_uv_bounding_box_show + del scene.muv_uv_bounding_box_uniform_scaling + del scene.muv_uv_bounding_box_boundary + + +class CommandBase(): + """ + Custom class: Base class of command + """ + + def __init__(self): + self.op = 'NONE' # operation + + def to_matrix(self): + # mat = I + mat = mathutils.Matrix() + mat.identity() + return mat + + +class TranslationCommand(CommandBase): + """ + Custom class: Translation operation + """ + + def __init__(self, ix, iy): + super().__init__() + self.op = 'TRANSLATION' + self.__x = ix # current x + self.__y = iy # current y + self.__ix = ix # initial x + self.__iy = iy # initial y + + def to_matrix(self): + # mat = Mt + dx = self.__x - self.__ix + dy = self.__y - self.__iy + return mathutils.Matrix.Translation((dx, dy, 0)) + + def set(self, x, y): + self.__x = x + self.__y = y + + +class RotationCommand(CommandBase): + """ + Custom class: Rotation operation + """ + + def __init__(self, ix, iy, cx, cy): + super().__init__() + self.op = 'ROTATION' + self.__x = ix # current x + self.__y = iy # current y + self.__cx = cx # center of rotation x + self.__cy = cy # center of rotation y + dx = self.__x - self.__cx + dy = self.__y - self.__cy + self.__iangle = math.atan2(dy, dx) # initial rotation angle + + def to_matrix(self): + # mat = Mt * Mr * Mt^-1 + dx = self.__x - self.__cx + dy = self.__y - self.__cy + angle = math.atan2(dy, dx) - self.__iangle + mti = mathutils.Matrix.Translation((-self.__cx, -self.__cy, 0.0)) + mr = mathutils.Matrix.Rotation(angle, 4, 'Z') + mt = mathutils.Matrix.Translation((self.__cx, self.__cy, 0.0)) + return mt * mr * mti + + def set(self, x, y): + self.__x = x + self.__y = y + + +class ScalingCommand(CommandBase): + """ + Custom class: Scaling operation + """ + + def __init__(self, ix, iy, ox, oy, dir_x, dir_y, mat): + super().__init__() + self.op = 'SCALING' + self.__ix = ix # initial x + self.__iy = iy # initial y + self.__x = ix # current x + self.__y = iy # current y + self.__ox = ox # origin of scaling x + self.__oy = oy # origin of scaling y + self.__dir_x = dir_x # direction of scaling x + self.__dir_y = dir_y # direction of scaling y + self.__mat = mat + # initial origin of scaling = M(to original transform) * (ox, oy) + iov = mat * mathutils.Vector((ox, oy, 0.0)) + self.__iox = iov.x # initial origin of scaling X + self.__ioy = iov.y # initial origin of scaling y + + def to_matrix(self): + """ + mat = M(to original transform)^-1 * Mt(to origin) * Ms * + Mt(to origin)^-1 * M(to original transform) + """ + m = self.__mat + mi = self.__mat.inverted() + mtoi = mathutils.Matrix.Translation((-self.__iox, -self.__ioy, 0.0)) + mto = mathutils.Matrix.Translation((self.__iox, self.__ioy, 0.0)) + # every point must be transformed to origin + t = m * mathutils.Vector((self.__ix, self.__iy, 0.0)) + tix, tiy = t.x, t.y + t = m * mathutils.Vector((self.__ox, self.__oy, 0.0)) + tox, toy = t.x, t.y + t = m * mathutils.Vector((self.__x, self.__y, 0.0)) + tx, ty = t.x, t.y + ms = mathutils.Matrix() + ms.identity() + if self.__dir_x == 1: + ms[0][0] = (tx - tox) * self.__dir_x / (tix - tox) + if self.__dir_y == 1: + ms[1][1] = (ty - toy) * self.__dir_y / (tiy - toy) + return mi * mto * ms * mtoi * m + + def set(self, x, y): + self.__x = x + self.__y = y + + +class UniformScalingCommand(CommandBase): + """ + Custom class: Uniform Scaling operation + """ + + def __init__(self, ix, iy, ox, oy, mat): + super().__init__() + self.op = 'SCALING' + self.__ix = ix # initial x + self.__iy = iy # initial y + self.__x = ix # current x + self.__y = iy # current y + self.__ox = ox # origin of scaling x + self.__oy = oy # origin of scaling y + self.__mat = mat + # initial origin of scaling = M(to original transform) * (ox, oy) + iov = mat * mathutils.Vector((ox, oy, 0.0)) + self.__iox = iov.x # initial origin of scaling x + self.__ioy = iov.y # initial origin of scaling y + self.__dir_x = 1 + self.__dir_y = 1 + + def to_matrix(self): + """ + mat = M(to original transform)^-1 * Mt(to origin) * Ms * + Mt(to origin)^-1 * M(to original transform) + """ + m = self.__mat + mi = self.__mat.inverted() + mtoi = mathutils.Matrix.Translation((-self.__iox, -self.__ioy, 0.0)) + mto = mathutils.Matrix.Translation((self.__iox, self.__ioy, 0.0)) + # every point must be transformed to origin + t = m * mathutils.Vector((self.__ix, self.__iy, 0.0)) + tix, tiy = t.x, t.y + t = m * mathutils.Vector((self.__ox, self.__oy, 0.0)) + tox, toy = t.x, t.y + t = m * mathutils.Vector((self.__x, self.__y, 0.0)) + tx, ty = t.x, t.y + ms = mathutils.Matrix() + ms.identity() + tir = math.sqrt((tix - tox) * (tix - tox) + (tiy - toy) * (tiy - toy)) + tr = math.sqrt((tx - tox) * (tx - tox) + (ty - toy) * (ty - toy)) + + sr = tr / tir + + if ((tx - tox) * (tix - tox)) > 0: + self.__dir_x = 1 + else: + self.__dir_x = -1 + if ((ty - toy) * (tiy - toy)) > 0: + self.__dir_y = 1 + else: + self.__dir_y = -1 + + ms[0][0] = sr * self.__dir_x + ms[1][1] = sr * self.__dir_y + + return mi * mto * ms * mtoi * m + + def set(self, x, y): + self.__x = x + self.__y = y + + +class CommandExecuter(): + """ + Custom class: manage command history and execute command + """ + + def __init__(self): + self.__cmd_list = [] # history + self.__cmd_list_redo = [] # redo list + + def execute(self, begin=0, end=-1): + """ + create matrix from history + """ + mat = mathutils.Matrix() + mat.identity() + for i, cmd in enumerate(self.__cmd_list): + if begin <= i and (end == -1 or i <= end): + mat = cmd.to_matrix() * mat + return mat + + def undo_size(self): + """ + get history size + """ + return len(self.__cmd_list) + + def top(self): + """ + get top of history + """ + if len(self.__cmd_list) <= 0: + return None + return self.__cmd_list[-1] + + def append(self, cmd): + """ + append command + """ + self.__cmd_list.append(cmd) + self.__cmd_list_redo = [] + + def undo(self): + """ + undo command + """ + if len(self.__cmd_list) <= 0: + return + self.__cmd_list_redo.append(self.__cmd_list.pop()) + + def redo(self): + """ + redo command + """ + if len(self.__cmd_list_redo) <= 0: + return + self.__cmd_list.append(self.__cmd_list_redo.pop()) + + def pop(self): + if len(self.__cmd_list) <= 0: + return None + return self.__cmd_list.pop() + + def push(self, cmd): + self.__cmd_list.append(cmd) + + +class State(IntEnum): + """ + Enum: State definition used by MUV_UVBBStateMgr + """ + NONE = 0 + TRANSLATING = 1 + SCALING_1 = 2 + SCALING_2 = 3 + SCALING_3 = 4 + SCALING_4 = 5 + SCALING_5 = 6 + SCALING_6 = 7 + SCALING_7 = 8 + SCALING_8 = 9 + ROTATING = 10 + UNIFORM_SCALING_1 = 11 + UNIFORM_SCALING_2 = 12 + UNIFORM_SCALING_3 = 13 + UNIFORM_SCALING_4 = 14 + + +class StateBase(): + """ + Custom class: Base class of state + """ + + def __init__(self): + pass + + def update(self, context, event, ctrl_points, mouse_view): + raise NotImplementedError + + +class StateNone(StateBase): + """ + Custom class: + No state + Wait for event from mouse + """ + + def __init__(self, cmd_exec): + super().__init__() + self.__cmd_exec = cmd_exec + + def update(self, context, event, ctrl_points, mouse_view): + """ + Update state + """ + prefs = context.user_preferences.addons["uv_magic_uv"].preferences + cp_react_size = prefs.uv_bounding_box_cp_react_size + is_uscaling = context.scene.muv_uv_bounding_box_uniform_scaling + if (event.type == 'LEFTMOUSE') and (event.value == 'PRESS'): + x, y = context.region.view2d.view_to_region( + mouse_view.x, mouse_view.y) + for i, p in enumerate(ctrl_points): + px, py = context.region.view2d.view_to_region(p.x, p.y) + in_cp_x = (px + cp_react_size > x and + px - cp_react_size < x) + in_cp_y = (py + cp_react_size > y and + py - cp_react_size < y) + if in_cp_x and in_cp_y: + if is_uscaling: + arr = [1, 3, 6, 8] + if i in arr: + return ( + State.UNIFORM_SCALING_1 + + arr.index(i) + ) + else: + return State.TRANSLATING + i + + return State.NONE + + +class StateTranslating(StateBase): + """ + Custom class: Translating state + """ + + def __init__(self, cmd_exec, ctrl_points): + super().__init__() + self.__cmd_exec = cmd_exec + ix, iy = ctrl_points[0].x, ctrl_points[0].y + self.__cmd_exec.append(TranslationCommand(ix, iy)) + + def update(self, context, event, ctrl_points, mouse_view): + if event.type == 'LEFTMOUSE': + if event.value == 'RELEASE': + return State.NONE + if event.type == 'MOUSEMOVE': + x, y = mouse_view.x, mouse_view.y + self.__cmd_exec.top().set(x, y) + return State.TRANSLATING + + +class StateScaling(StateBase): + """ + Custom class: Scaling state + """ + + def __init__(self, cmd_exec, state, ctrl_points): + super().__init__() + self.__state = state + self.__cmd_exec = cmd_exec + dir_x_list = [1, 1, 1, 0, 0, 1, 1, 1] + dir_y_list = [1, 0, 1, 1, 1, 1, 0, 1] + idx = state - 2 + ix, iy = ctrl_points[idx + 1].x, ctrl_points[idx + 1].y + ox, oy = ctrl_points[8 - idx].x, ctrl_points[8 - idx].y + dir_x, dir_y = dir_x_list[idx], dir_y_list[idx] + mat = self.__cmd_exec.execute(end=self.__cmd_exec.undo_size()) + self.__cmd_exec.append( + ScalingCommand(ix, iy, ox, oy, dir_x, dir_y, mat.inverted())) + + def update(self, context, event, ctrl_points, mouse_view): + if event.type == 'LEFTMOUSE': + if event.value == 'RELEASE': + return State.NONE + if event.type == 'MOUSEMOVE': + x, y = mouse_view.x, mouse_view.y + self.__cmd_exec.top().set(x, y) + return self.__state + + +class StateUniformScaling(StateBase): + """ + Custom class: Uniform Scaling state + """ + + def __init__(self, cmd_exec, state, ctrl_points): + super().__init__() + self.__state = state + self.__cmd_exec = cmd_exec + icp_idx = [1, 3, 6, 8] + ocp_idx = [8, 6, 3, 1] + idx = state - State.UNIFORM_SCALING_1 + ix, iy = ctrl_points[icp_idx[idx]].x, ctrl_points[icp_idx[idx]].y + ox, oy = ctrl_points[ocp_idx[idx]].x, ctrl_points[ocp_idx[idx]].y + mat = self.__cmd_exec.execute(end=self.__cmd_exec.undo_size()) + self.__cmd_exec.append(UniformScalingCommand( + ix, iy, ox, oy, mat.inverted())) + + def update(self, context, event, ctrl_points, mouse_view): + if event.type == 'LEFTMOUSE': + if event.value == 'RELEASE': + return State.NONE + if event.type == 'MOUSEMOVE': + x, y = mouse_view.x, mouse_view.y + self.__cmd_exec.top().set(x, y) + + return self.__state + + +class StateRotating(StateBase): + """ + Custom class: Rotating state + """ + + def __init__(self, cmd_exec, ctrl_points): + super().__init__() + self.__cmd_exec = cmd_exec + ix, iy = ctrl_points[9].x, ctrl_points[9].y + ox, oy = ctrl_points[0].x, ctrl_points[0].y + self.__cmd_exec.append(RotationCommand(ix, iy, ox, oy)) + + def update(self, context, event, ctrl_points, mouse_view): + if event.type == 'LEFTMOUSE': + if event.value == 'RELEASE': + return State.NONE + if event.type == 'MOUSEMOVE': + x, y = mouse_view.x, mouse_view.y + self.__cmd_exec.top().set(x, y) + return State.ROTATING + + +class StateManager(): + """ + Custom class: Manage state about this feature + """ + + def __init__(self, cmd_exec): + self.__cmd_exec = cmd_exec # command executer + self.__state = State.NONE # current state + self.__state_obj = StateNone(self.__cmd_exec) + + def __update_state(self, next_state, ctrl_points): + """ + Update state + """ + + if next_state == self.__state: + return + obj = None + if next_state == State.TRANSLATING: + obj = StateTranslating(self.__cmd_exec, ctrl_points) + elif State.SCALING_1 <= next_state <= State.SCALING_8: + obj = StateScaling( + self.__cmd_exec, next_state, ctrl_points) + elif next_state == State.ROTATING: + obj = StateRotating(self.__cmd_exec, ctrl_points) + elif next_state == State.NONE: + obj = StateNone(self.__cmd_exec) + elif (State.UNIFORM_SCALING_1 <= next_state <= + State.UNIFORM_SCALING_4): + obj = StateUniformScaling( + self.__cmd_exec, next_state, ctrl_points) + + if obj is not None: + self.__state_obj = obj + + self.__state = next_state + + def update(self, context, ctrl_points, event): + mouse_region = mathutils.Vector(( + event.mouse_region_x, event.mouse_region_y)) + mouse_view = mathutils.Vector((context.region.view2d.region_to_view( + mouse_region.x, mouse_region.y))) + next_state = self.__state_obj.update( + context, event, ctrl_points, mouse_view) + self.__update_state(next_state, ctrl_points) + + return self.__state + + +@BlClassRegistry(legacy=True) +class MUV_OT_UVBoundingBox(bpy.types.Operator): + """ + Operation class: UV Bounding Box + """ + + bl_idname = "uv.muv_uv_bounding_box_operator" + bl_label = "UV Bounding Box" + bl_description = "Internal operation for UV Bounding Box" + bl_options = {'REGISTER', 'UNDO'} + + def __init__(self): + self.__timer = None + self.__cmd_exec = CommandExecuter() # Command executor + self.__state_mgr = StateManager(self.__cmd_exec) # State Manager + + __handle = None + __timer = None + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return False + return 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(obj) + + @classmethod + def handle_remove(cls, context): + if cls.__handle is not None: + sie = bpy.types.SpaceImageEditor + sie.draw_handler_remove(cls.__handle, "WINDOW") + cls.__handle = None + if cls.__timer is not None: + context.window_manager.event_timer_remove(cls.__timer) + cls.__timer = None + + @classmethod + def __draw_ctrl_point(cls, context, pos): + """ + Draw control point + """ + prefs = context.user_preferences.addons["uv_magic_uv"].preferences + cp_size = prefs.uv_bounding_box_cp_size + offset = cp_size / 2 + verts = [ + [pos.x - offset, pos.y - offset], + [pos.x - offset, pos.y + offset], + [pos.x + offset, pos.y + offset], + [pos.x + offset, pos.y - offset] + ] + bgl.glEnable(bgl.GL_BLEND) + bgl.glBegin(bgl.GL_QUADS) + bgl.glColor4f(1.0, 1.0, 1.0, 1.0) + for (x, y) in verts: + bgl.glVertex2f(x, y) + bgl.glEnd() + + @classmethod + def draw_bb(cls, _, context): + """ + Draw bounding box + """ + props = context.scene.muv_props.uv_bounding_box + + if not MUV_OT_UVBoundingBox.is_running(context): + return + + if not is_valid_context(context): + return + + for cp in props.ctrl_points: + cls.__draw_ctrl_point( + context, mathutils.Vector( + context.region.view2d.view_to_region(cp.x, cp.y))) + + def __get_uv_info(self, context): + """ + Get UV coordinate + """ + sc = context.scene + obj = context.active_object + uv_info = [] + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + if not bm.loops.layers.uv: + return None + uv_layer = bm.loops.layers.uv.verify() + for f in bm.faces: + if not f.select: + continue + for i, l in enumerate(f.loops): + if sc.muv_uv_bounding_box_boundary == 'UV_SEL': + if l[uv_layer].select: + uv_info.append((f.index, i, l[uv_layer].uv.copy())) + elif sc.muv_uv_bounding_box_boundary == 'UV': + uv_info.append((f.index, i, l[uv_layer].uv.copy())) + if not uv_info: + return None + return uv_info + + def __get_ctrl_point(self, uv_info_ini): + """ + Get control point + """ + left = MAX_VALUE + right = -MAX_VALUE + top = -MAX_VALUE + bottom = MAX_VALUE + + for info in uv_info_ini: + uv = info[2] + if uv.x < left: + left = uv.x + if uv.x > right: + right = uv.x + if uv.y < bottom: + bottom = uv.y + if uv.y > top: + top = uv.y + + points = [ + mathutils.Vector(( + (left + right) * 0.5, (top + bottom) * 0.5, 0.0 + )), + mathutils.Vector((left, top, 0.0)), + mathutils.Vector((left, (top + bottom) * 0.5, 0.0)), + mathutils.Vector((left, bottom, 0.0)), + mathutils.Vector(((left + right) * 0.5, top, 0.0)), + mathutils.Vector(((left + right) * 0.5, bottom, 0.0)), + mathutils.Vector((right, top, 0.0)), + mathutils.Vector((right, (top + bottom) * 0.5, 0.0)), + mathutils.Vector((right, bottom, 0.0)), + mathutils.Vector(((left + right) * 0.5, top + 0.03, 0.0)) + ] + + return points + + def __update_uvs(self, context, uv_info_ini, trans_mat): + """ + Update UV coordinate + """ + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + if not bm.loops.layers.uv: + return + uv_layer = bm.loops.layers.uv.verify() + for info in uv_info_ini: + fidx = info[0] + lidx = info[1] + uv = info[2] + v = mathutils.Vector((uv.x, uv.y, 0.0)) + av = trans_mat * v + bm.faces[fidx].loops[lidx][uv_layer].uv = mathutils.Vector( + (av.x, av.y)) + + def __update_ctrl_point(self, ctrl_points_ini, trans_mat): + """ + Update control point + """ + return [trans_mat * cp for cp in ctrl_points_ini] + + def modal(self, context, event): + props = context.scene.muv_props.uv_bounding_box + common.redraw_all_areas() + + if not MUV_OT_UVBoundingBox.is_running(context): + return {'FINISHED'} + + if not is_valid_context(context): + MUV_OT_UVBoundingBox.handle_remove(context) + return {'FINISHED'} + + region_types = [ + 'HEADER', + 'UI', + 'TOOLS', + ] + if not common.mouse_on_area(event, 'IMAGE_EDITOR') or \ + common.mouse_on_regions(event, 'IMAGE_EDITOR', region_types): + return {'PASS_THROUGH'} + + if event.type == 'TIMER': + trans_mat = self.__cmd_exec.execute() + self.__update_uvs(context, props.uv_info_ini, trans_mat) + props.ctrl_points = self.__update_ctrl_point( + props.ctrl_points_ini, trans_mat) + + state = self.__state_mgr.update(context, props.ctrl_points, event) + if state == State.NONE: + return {'PASS_THROUGH'} + + return {'RUNNING_MODAL'} + + def invoke(self, context, _): + props = context.scene.muv_props.uv_bounding_box + + if MUV_OT_UVBoundingBox.is_running(context): + MUV_OT_UVBoundingBox.handle_remove(context) + return {'FINISHED'} + + props.uv_info_ini = self.__get_uv_info(context) + if props.uv_info_ini is None: + return {'CANCELLED'} + + MUV_OT_UVBoundingBox.handle_add(self, context) + + props.ctrl_points_ini = self.__get_ctrl_point(props.uv_info_ini) + trans_mat = self.__cmd_exec.execute() + # Update is needed in order to display control point + self.__update_uvs(context, props.uv_info_ini, trans_mat) + props.ctrl_points = self.__update_ctrl_point( + props.ctrl_points_ini, trans_mat) + + return {'RUNNING_MODAL'} diff --git a/uv_magic_uv/legacy/op/uv_inspection.py b/uv_magic_uv/legacy/op/uv_inspection.py new file mode 100644 index 00000000..57d42468 --- /dev/null +++ b/uv_magic_uv/legacy/op/uv_inspection.py @@ -0,0 +1,280 @@ +# + +# ##### 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 +import bgl +from bpy.props import BoolProperty, EnumProperty + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_UVInspection_Render', + 'MUV_OT_UVInspection_Update', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. + # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf + # after the execution + for space in context.area.spaces: + if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): + break + else: + return False + + return True + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "uv_inspection" + + @classmethod + def init_props(cls, scene): + class Props(): + overlapped_info = [] + flipped_info = [] + + scene.muv_props.uv_inspection = Props() + + def get_func(_): + return MUV_OT_UVInspection_Render.is_running(bpy.context) + + def set_func(_, __): + pass + + def update_func(_, __): + bpy.ops.uv.muv_uv_inspection_operator_render('INVOKE_REGION_WIN') + + scene.muv_uv_inspection_enabled = BoolProperty( + name="UV Inspection Enabled", + description="UV Inspection is enabled", + default=False + ) + scene.muv_uv_inspection_show = BoolProperty( + name="UV Inspection Showed", + description="UV Inspection is showed", + default=False, + get=get_func, + set=set_func, + update=update_func + ) + scene.muv_uv_inspection_show_overlapped = BoolProperty( + name="Overlapped", + description="Show overlapped UVs", + default=False + ) + scene.muv_uv_inspection_show_flipped = BoolProperty( + name="Flipped", + description="Show flipped UVs", + default=False + ) + scene.muv_uv_inspection_show_mode = EnumProperty( + name="Mode", + description="Show mode", + items=[ + ('PART', "Part", "Show only overlapped/flipped part"), + ('FACE', "Face", "Show overlapped/flipped face") + ], + default='PART' + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_props.uv_inspection + del scene.muv_uv_inspection_enabled + del scene.muv_uv_inspection_show + del scene.muv_uv_inspection_show_overlapped + del scene.muv_uv_inspection_show_flipped + del scene.muv_uv_inspection_show_mode + + +@BlClassRegistry(legacy=True) +class MUV_OT_UVInspection_Render(bpy.types.Operator): + """ + Operation class: Render UV Inspection + No operation (only rendering) + """ + + bl_idname = "uv.muv_uv_inspection_operator_render" + bl_description = "Render overlapped/flipped UVs" + bl_label = "Overlapped/Flipped UV renderer" + + __handle = None + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return False + return is_valid_context(context) + + @classmethod + def is_running(cls, _): + return 1 if cls.__handle else 0 + + @classmethod + def handle_add(cls, obj, context): + sie = bpy.types.SpaceImageEditor + cls.__handle = sie.draw_handler_add( + MUV_OT_UVInspection_Render.draw, (obj, context), + 'WINDOW', 'POST_PIXEL') + + @classmethod + def handle_remove(cls): + if cls.__handle is not None: + bpy.types.SpaceImageEditor.draw_handler_remove( + cls.__handle, 'WINDOW') + cls.__handle = None + + @staticmethod + def draw(_, context): + sc = context.scene + props = sc.muv_props.uv_inspection + prefs = context.user_preferences.addons["uv_magic_uv"].preferences + + if not MUV_OT_UVInspection_Render.is_running(context): + return + + # OpenGL configuration + bgl.glEnable(bgl.GL_BLEND) + + # render overlapped UV + if sc.muv_uv_inspection_show_overlapped: + color = prefs.uv_inspection_overlapped_color + for info in props.overlapped_info: + if sc.muv_uv_inspection_show_mode == 'PART': + for poly in info["polygons"]: + bgl.glBegin(bgl.GL_TRIANGLE_FAN) + bgl.glColor4f(color[0], color[1], color[2], color[3]) + for uv in poly: + x, y = context.region.view2d.view_to_region( + uv.x, uv.y) + bgl.glVertex2f(x, y) + bgl.glEnd() + elif sc.muv_uv_inspection_show_mode == 'FACE': + bgl.glBegin(bgl.GL_TRIANGLE_FAN) + bgl.glColor4f(color[0], color[1], color[2], color[3]) + for uv in info["subject_uvs"]: + x, y = context.region.view2d.view_to_region(uv.x, uv.y) + bgl.glVertex2f(x, y) + bgl.glEnd() + + # render flipped UV + if sc.muv_uv_inspection_show_flipped: + color = prefs.uv_inspection_flipped_color + for info in props.flipped_info: + if sc.muv_uv_inspection_show_mode == 'PART': + for poly in info["polygons"]: + bgl.glBegin(bgl.GL_TRIANGLE_FAN) + bgl.glColor4f(color[0], color[1], color[2], color[3]) + for uv in poly: + x, y = context.region.view2d.view_to_region( + uv.x, uv.y) + bgl.glVertex2f(x, y) + bgl.glEnd() + elif sc.muv_uv_inspection_show_mode == 'FACE': + bgl.glBegin(bgl.GL_TRIANGLE_FAN) + bgl.glColor4f(color[0], color[1], color[2], color[3]) + for uv in info["uvs"]: + x, y = context.region.view2d.view_to_region(uv.x, uv.y) + bgl.glVertex2f(x, y) + bgl.glEnd() + + def invoke(self, context, _): + if not MUV_OT_UVInspection_Render.is_running(context): + update_uvinsp_info(context) + MUV_OT_UVInspection_Render.handle_add(self, context) + else: + MUV_OT_UVInspection_Render.handle_remove() + + if context.area: + context.area.tag_redraw() + + return {'FINISHED'} + + +def update_uvinsp_info(context): + sc = context.scene + props = sc.muv_props.uv_inspection + + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + uv_layer = bm.loops.layers.uv.verify() + + if context.tool_settings.use_uv_select_sync: + sel_faces = [f for f in bm.faces] + else: + sel_faces = [f for f in bm.faces if f.select] + props.overlapped_info = common.get_overlapped_uv_info( + bm, sel_faces, uv_layer, sc.muv_uv_inspection_show_mode) + props.flipped_info = common.get_flipped_uv_info(sel_faces, uv_layer) + + +@BlClassRegistry(legacy=True) +class MUV_OT_UVInspection_Update(bpy.types.Operator): + """ + Operation class: Update + """ + + bl_idname = "uv.muv_uv_inspection_operator_update" + bl_label = "Update UV Inspection" + bl_description = "Update UV Inspection" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + if not MUV_OT_UVInspection_Render.is_running(context): + return False + return is_valid_context(context) + + def execute(self, context): + update_uvinsp_info(context) + + if context.area: + context.area.tag_redraw() + + return {'FINISHED'} diff --git a/uv_magic_uv/legacy/op/uv_sculpt.py b/uv_magic_uv/legacy/op/uv_sculpt.py new file mode 100644 index 00000000..3754a759 --- /dev/null +++ b/uv_magic_uv/legacy/op/uv_sculpt.py @@ -0,0 +1,483 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +from math import pi, cos, tan, sin + +import bpy +import bmesh +import bgl +from mathutils import Vector +from bpy_extras import view3d_utils +from mathutils.bvhtree import BVHTree +from mathutils.geometry import barycentric_transform +from bpy.props import ( + BoolProperty, + IntProperty, + EnumProperty, + FloatProperty, +) + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_UVSculpt', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "uv_sculpt" + + @classmethod + def init_props(cls, scene): + def get_func(_): + return MUV_OT_UVSculpt.is_running(bpy.context) + + def set_func(_, __): + pass + + def update_func(_, __): + bpy.ops.uv.muv_uv_sculpt_operator('INVOKE_REGION_WIN') + + scene.muv_uv_sculpt_enabled = BoolProperty( + name="UV Sculpt", + description="UV Sculpt is enabled", + default=False + ) + scene.muv_uv_sculpt_enable = BoolProperty( + name="UV Sculpt Showed", + description="UV Sculpt is enabled", + default=False, + get=get_func, + set=set_func, + update=update_func + ) + scene.muv_uv_sculpt_radius = IntProperty( + name="Radius", + description="Radius of the brush", + min=1, + max=500, + default=30 + ) + scene.muv_uv_sculpt_strength = FloatProperty( + name="Strength", + description="How powerful the effect of the brush when applied", + min=0.0, + max=1.0, + default=0.03, + ) + scene.muv_uv_sculpt_tools = EnumProperty( + name="Tools", + description="Select Tools for the UV sculpt brushes", + items=[ + ('GRAB', "Grab", "Grab UVs"), + ('RELAX', "Relax", "Relax UVs"), + ('PINCH', "Pinch", "Pinch UVs") + ], + default='GRAB' + ) + scene.muv_uv_sculpt_show_brush = BoolProperty( + name="Show Brush", + description="Show Brush", + default=True + ) + scene.muv_uv_sculpt_pinch_invert = BoolProperty( + name="Invert", + description="Pinch UV to invert direction", + default=False + ) + scene.muv_uv_sculpt_relax_method = EnumProperty( + name="Method", + description="Algorithm used for relaxation", + items=[ + ('HC', "HC", "Use HC method for relaxation"), + ('LAPLACIAN', "Laplacian", + "Use laplacian method for relaxation") + ], + default='HC' + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_uv_sculpt_enabled + del scene.muv_uv_sculpt_enable + del scene.muv_uv_sculpt_radius + del scene.muv_uv_sculpt_strength + del scene.muv_uv_sculpt_tools + del scene.muv_uv_sculpt_show_brush + del scene.muv_uv_sculpt_pinch_invert + del scene.muv_uv_sculpt_relax_method + + +@BlClassRegistry(legacy=True) +class MUV_OT_UVSculpt(bpy.types.Operator): + """ + Operation class: UV Sculpt in View3D + """ + + bl_idname = "uv.muv_uv_sculpt_operator" + bl_label = "UV Sculpt" + bl_description = "UV Sculpt in View3D" + bl_options = {'REGISTER'} + + __handle = None + __timer = None + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return False + return is_valid_context(context) + + @classmethod + def is_running(cls, _): + return 1 if cls.__handle else 0 + + @classmethod + def handle_add(cls, obj, context): + if not cls.__handle: + sv = bpy.types.SpaceView3D + cls.__handle = sv.draw_handler_add(cls.draw_brush, (obj, context), + "WINDOW", "POST_PIXEL") + if not cls.__timer: + cls.__timer = context.window_manager.event_timer_add( + 0.1, context.window) + context.window_manager.modal_handler_add(obj) + + @classmethod + def handle_remove(cls, context): + if cls.__handle: + sv = bpy.types.SpaceView3D + sv.draw_handler_remove(cls.__handle, "WINDOW") + cls.__handle = None + if cls.__timer: + context.window_manager.event_timer_remove(cls.__timer) + cls.__timer = None + + @classmethod + def draw_brush(cls, obj, context): + sc = context.scene + prefs = context.user_preferences.addons["uv_magic_uv"].preferences + + num_segment = 180 + theta = 2 * pi / num_segment + fact_t = tan(theta) + fact_r = cos(theta) + color = prefs.uv_sculpt_brush_color + + bgl.glBegin(bgl.GL_LINE_STRIP) + bgl.glColor4f(color[0], color[1], color[2], color[3]) + x = sc.muv_uv_sculpt_radius * cos(0.0) + y = sc.muv_uv_sculpt_radius * sin(0.0) + for _ in range(num_segment): + bgl.glVertex2f(x + obj.current_mco.x, y + obj.current_mco.y) + tx = -y + ty = x + x = x + tx * fact_t + y = y + ty * fact_t + x = x * fact_r + y = y * fact_r + bgl.glEnd() + + def __init__(self): + self.__loop_info = [] + self.__stroking = False + self.current_mco = Vector((0.0, 0.0)) + self.__initial_mco = Vector((0.0, 0.0)) + + def __get_strength(self, p, len_, factor): + f = factor + + if p > len_: + return 0.0 + + if p < 0.0: + return f + + return (len_ - p) * f / len_ + + def __stroke_init(self, context, _): + sc = context.scene + + self.__initial_mco = self.current_mco + + # get influenced UV + obj = context.active_object + world_mat = obj.matrix_world + bm = bmesh.from_edit_mesh(obj.data) + uv_layer = bm.loops.layers.uv.verify() + _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') + + self.__loop_info = [] + for f in bm.faces: + if not f.select: + continue + for i, l in enumerate(f.loops): + loc_2d = view3d_utils.location_3d_to_region_2d( + region, space.region_3d, world_mat * l.vert.co) + diff = loc_2d - self.__initial_mco + if diff.length < sc.muv_uv_sculpt_radius: + info = { + "face_idx": f.index, + "loop_idx": i, + "initial_vco": l.vert.co.copy(), + "initial_vco_2d": loc_2d, + "initial_uv": l[uv_layer].uv.copy(), + "strength": self.__get_strength( + diff.length, sc.muv_uv_sculpt_radius, + sc.muv_uv_sculpt_strength) + } + self.__loop_info.append(info) + + def __stroke_apply(self, context, _): + sc = context.scene + obj = context.active_object + world_mat = obj.matrix_world + bm = bmesh.from_edit_mesh(obj.data) + uv_layer = bm.loops.layers.uv.verify() + mco = self.current_mco + + if sc.muv_uv_sculpt_tools == 'GRAB': + for info in self.__loop_info: + diff_uv = (mco - self.__initial_mco) * info["strength"] + l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] + l[uv_layer].uv = info["initial_uv"] + diff_uv / 100.0 + + elif sc.muv_uv_sculpt_tools == 'PINCH': + _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') + loop_info = [] + for f in bm.faces: + if not f.select: + continue + for i, l in enumerate(f.loops): + loc_2d = view3d_utils.location_3d_to_region_2d( + region, space.region_3d, world_mat * l.vert.co) + diff = loc_2d - self.__initial_mco + if diff.length < sc.muv_uv_sculpt_radius: + info = { + "face_idx": f.index, + "loop_idx": i, + "initial_vco": l.vert.co.copy(), + "initial_vco_2d": loc_2d, + "initial_uv": l[uv_layer].uv.copy(), + "strength": self.__get_strength( + diff.length, sc.muv_uv_sculpt_radius, + sc.muv_uv_sculpt_strength) + } + loop_info.append(info) + + # mouse coordinate to UV coordinate + ray_vec = view3d_utils.region_2d_to_vector_3d(region, + space.region_3d, mco) + ray_vec.normalize() + ray_orig = view3d_utils.region_2d_to_origin_3d(region, + space.region_3d, + mco) + ray_tgt = ray_orig + ray_vec * 1000000.0 + mwi = world_mat.inverted() + ray_orig_obj = mwi * ray_orig + ray_tgt_obj = mwi * ray_tgt + ray_dir_obj = ray_tgt_obj - ray_orig_obj + ray_dir_obj.normalize() + tree = BVHTree.FromBMesh(bm) + loc, _, fidx, _ = tree.ray_cast(ray_orig_obj, ray_dir_obj) + if not loc: + return + loops = [l for l in bm.faces[fidx].loops] + uvs = [Vector((l[uv_layer].uv.x, l[uv_layer].uv.y, 0.0)) + for l in loops] + target_uv = barycentric_transform( + loc, loops[0].vert.co, loops[1].vert.co, loops[2].vert.co, + uvs[0], uvs[1], uvs[2]) + target_uv = Vector((target_uv.x, target_uv.y)) + + # move to target UV coordinate + for info in loop_info: + l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] + if sc.muv_uv_sculpt_pinch_invert: + diff_uv = (l[uv_layer].uv - target_uv) * info["strength"] + else: + diff_uv = (target_uv - l[uv_layer].uv) * info["strength"] + l[uv_layer].uv = l[uv_layer].uv + diff_uv / 10.0 + + elif sc.muv_uv_sculpt_tools == 'RELAX': + _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') + + # get vertex and loop relation + vert_db = {} + for f in bm.faces: + for l in f.loops: + if l.vert in vert_db: + vert_db[l.vert]["loops"].append(l) + else: + vert_db[l.vert] = {"loops": [l]} + + # get relaxation information + for k in vert_db.keys(): + d = vert_db[k] + d["uv_sum"] = Vector((0.0, 0.0)) + d["uv_count"] = 0 + + for l in d["loops"]: + ln = l.link_loop_next + lp = l.link_loop_prev + d["uv_sum"] = d["uv_sum"] + ln[uv_layer].uv + d["uv_sum"] = d["uv_sum"] + lp[uv_layer].uv + d["uv_count"] = d["uv_count"] + 2 + d["uv_p"] = d["uv_sum"] / d["uv_count"] + d["uv_b"] = d["uv_p"] - d["loops"][0][uv_layer].uv + for k in vert_db.keys(): + d = vert_db[k] + d["uv_sum_b"] = Vector((0.0, 0.0)) + for l in d["loops"]: + ln = l.link_loop_next + lp = l.link_loop_prev + dn = vert_db[ln.vert] + dp = vert_db[lp.vert] + d["uv_sum_b"] = d["uv_sum_b"] + dn["uv_b"] + dp["uv_b"] + + # apply + for f in bm.faces: + if not f.select: + continue + for i, l in enumerate(f.loops): + loc_2d = view3d_utils.location_3d_to_region_2d( + region, space.region_3d, world_mat * l.vert.co) + diff = loc_2d - self.__initial_mco + if diff.length >= sc.muv_uv_sculpt_radius: + continue + db = vert_db[l.vert] + strength = self.__get_strength(diff.length, + sc.muv_uv_sculpt_radius, + sc.muv_uv_sculpt_strength) + + base = (1.0 - strength) * l[uv_layer].uv + if sc.muv_uv_sculpt_relax_method == 'HC': + t = 0.5 * (db["uv_b"] + db["uv_sum_b"] / d["uv_count"]) + diff = strength * (db["uv_p"] - t) + target_uv = base + diff + elif sc.muv_uv_sculpt_relax_method == 'LAPLACIAN': + diff = strength * db["uv_p"] + target_uv = base + diff + else: + continue + + l[uv_layer].uv = target_uv + + bmesh.update_edit_mesh(obj.data) + + def __stroke_exit(self, context, _): + sc = context.scene + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + uv_layer = bm.loops.layers.uv.verify() + mco = self.current_mco + + if sc.muv_uv_sculpt_tools == 'GRAB': + for info in self.__loop_info: + diff_uv = (mco - self.__initial_mco) * info["strength"] + l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] + l[uv_layer].uv = info["initial_uv"] + diff_uv / 100.0 + + bmesh.update_edit_mesh(obj.data) + + def modal(self, context, event): + if context.area: + context.area.tag_redraw() + + if not MUV_OT_UVSculpt.is_running(context): + MUV_OT_UVSculpt.handle_remove(context) + + return {'FINISHED'} + + self.current_mco = Vector((event.mouse_region_x, event.mouse_region_y)) + + region_types = [ + 'HEADER', + 'UI', + 'TOOLS', + 'TOOL_PROPS', + ] + if not common.mouse_on_area(event, 'VIEW_3D') or \ + common.mouse_on_regions(event, 'VIEW_3D', region_types): + return {'PASS_THROUGH'} + + if event.type == 'LEFTMOUSE': + if event.value == 'PRESS': + if not self.__stroking: + self.__stroke_init(context, event) + self.__stroking = True + elif event.value == 'RELEASE': + if self.__stroking: + self.__stroke_exit(context, event) + self.__stroking = False + return {'RUNNING_MODAL'} + elif event.type == 'MOUSEMOVE': + if self.__stroking: + self.__stroke_apply(context, event) + return {'RUNNING_MODAL'} + elif event.type == 'TIMER': + if self.__stroking: + self.__stroke_apply(context, event) + return {'RUNNING_MODAL'} + + return {'PASS_THROUGH'} + + def invoke(self, context, _): + if context.area: + context.area.tag_redraw() + + if MUV_OT_UVSculpt.is_running(context): + MUV_OT_UVSculpt.handle_remove(context) + else: + MUV_OT_UVSculpt.handle_add(self, context) + + return {'RUNNING_MODAL'} diff --git a/uv_magic_uv/legacy/op/uvw.py b/uv_magic_uv/legacy/op/uvw.py new file mode 100644 index 00000000..56777b18 --- /dev/null +++ b/uv_magic_uv/legacy/op/uvw.py @@ -0,0 +1,181 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Alexander Milovsky, Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bpy +import bmesh +from bpy.props import ( + FloatProperty, + FloatVectorProperty, + BoolProperty +) + +from ... import common +from ...impl import uvw_impl as impl +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_UVW_BoxMap', + 'MUV_OT_UVW_BestPlanerMap', +] + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "uvw" + + @classmethod + def init_props(cls, scene): + scene.muv_uvw_enabled = BoolProperty( + name="UVW Enabled", + description="UVW is enabled", + default=False + ) + scene.muv_uvw_assign_uvmap = BoolProperty( + name="Assign UVMap", + description="Assign UVMap when no UVmaps are available", + default=True + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_uvw_enabled + del scene.muv_uvw_assign_uvmap + + +@BlClassRegistry(legacy=True) +class MUV_OT_UVW_BoxMap(bpy.types.Operator): + bl_idname = "uv.muv_uvw_operator_box_map" + bl_label = "Box Map" + bl_options = {'REGISTER', 'UNDO'} + + size = FloatProperty( + name="Size", + default=1.0, + precision=4 + ) + rotation = FloatVectorProperty( + name="XYZ Rotation", + size=3, + default=(0.0, 0.0, 0.0) + ) + offset = FloatVectorProperty( + name="XYZ Offset", + size=3, + default=(0.0, 0.0, 0.0) + ) + tex_aspect = FloatProperty( + name="Texture Aspect", + default=1.0, + precision=4 + ) + assign_uvmap = BoolProperty( + name="Assign UVMap", + description="Assign UVMap when no UVmaps are available", + default=True + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return impl.is_valid_context(context) + + def execute(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + # get UV layer + uv_layer = impl.get_uv_layer(self, bm, self.assign_uvmap) + if not uv_layer: + return {'CANCELLED'} + + impl.apply_box_map(bm, uv_layer, self.size, self.offset, + self.rotation, self.tex_aspect) + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} + + +@BlClassRegistry(legacy=True) +class MUV_OT_UVW_BestPlanerMap(bpy.types.Operator): + bl_idname = "uv.muv_uvw_operator_best_planer_map" + bl_label = "Best Planer Map" + bl_options = {'REGISTER', 'UNDO'} + + size = FloatProperty( + name="Size", + default=1.0, + precision=4 + ) + rotation = FloatProperty( + name="XY Rotation", + default=0.0 + ) + offset = FloatVectorProperty( + name="XY Offset", + size=2, + default=(0.0, 0.0) + ) + tex_aspect = FloatProperty( + name="Texture Aspect", + default=1.0, + precision=4 + ) + assign_uvmap = BoolProperty( + name="Assign UVMap", + description="Assign UVMap when no UVmaps are available", + default=True + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return impl.is_valid_context(context) + + def execute(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.faces.ensure_lookup_table() + + # get UV layer + uv_layer = impl.get_uv_layer(self, bm, self.assign_uvmap) + if not uv_layer: + return {'CANCELLED'} + + impl.apply_planer_map(bm, uv_layer, self.size, self.offset, + self.rotation, self.tex_aspect) + + bmesh.update_edit_mesh(obj.data) + + return {'FINISHED'} diff --git a/uv_magic_uv/legacy/op/world_scale_uv.py b/uv_magic_uv/legacy/op/world_scale_uv.py new file mode 100644 index 00000000..e56b6bfa --- /dev/null +++ b/uv_magic_uv/legacy/op/world_scale_uv.py @@ -0,0 +1,655 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "McBuff, Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +from math import sqrt + +import bpy +import bmesh +from mathutils import Vector +from bpy.props import ( + EnumProperty, + FloatProperty, + IntVectorProperty, + BoolProperty, +) + +from ... import common +from ...utils.bl_class_registry import BlClassRegistry +from ...utils.property_class_registry import PropertyClassRegistry + + +__all__ = [ + 'Properties', + 'MUV_OT_WorldScaleUV_Measure', + 'MUV_OT_WorldScaleUV_ApplyManual', + 'MUV_OT_WorldScaleUV_ApplyScalingDensity', + 'MUV_OT_WorldScaleUV_ApplyProportionalToMesh', +] + + +def is_valid_context(context): + obj = context.object + + # only edit mode is allowed to execute + if obj is None: + return False + if obj.type != 'MESH': + return False + if context.object.mode != 'EDIT': + return False + + # only 'VIEW_3D' space is allowed to execute + for space in context.area.spaces: + if space.type == 'VIEW_3D': + break + else: + return False + + return True + + +def measure_wsuv_info(obj, tex_size=None): + mesh_area = common.measure_mesh_area(obj) + uv_area = common.measure_uv_area(obj, tex_size) + + if not uv_area: + return None, mesh_area, None + + if mesh_area == 0.0: + density = 0.0 + else: + density = sqrt(uv_area) / sqrt(mesh_area) + + return uv_area, mesh_area, density + + +@PropertyClassRegistry(legacy=True) +class Properties: + idname = "world_scale_uv" + + @classmethod + def init_props(cls, scene): + scene.muv_world_scale_uv_enabled = BoolProperty( + name="World Scale UV Enabled", + description="World Scale UV is enabled", + default=False + ) + scene.muv_world_scale_uv_src_mesh_area = FloatProperty( + name="Mesh Area", + description="Source Mesh Area", + default=0.0, + min=0.0 + ) + scene.muv_world_scale_uv_src_uv_area = FloatProperty( + name="UV Area", + description="Source UV Area", + default=0.0, + min=0.0 + ) + scene.muv_world_scale_uv_src_density = FloatProperty( + name="Density", + description="Source Texel Density", + default=0.0, + min=0.0 + ) + scene.muv_world_scale_uv_tgt_density = FloatProperty( + name="Density", + description="Target Texel Density", + default=0.0, + min=0.0 + ) + scene.muv_world_scale_uv_tgt_scaling_factor = FloatProperty( + name="Scaling Factor", + default=1.0, + max=1000.0, + min=0.00001 + ) + scene.muv_world_scale_uv_tgt_texture_size = IntVectorProperty( + name="Texture Size", + size=2, + min=1, + soft_max=10240, + default=(1024, 1024), + ) + scene.muv_world_scale_uv_mode = EnumProperty( + name="Mode", + description="Density calculation mode", + items=[ + ('PROPORTIONAL_TO_MESH', "Proportional to Mesh", + "Apply density proportionaled by mesh size"), + ('SCALING_DENSITY', "Scaling Density", + "Apply scaled density from source"), + ('SAME_DENSITY', "Same Density", + "Apply same density of source"), + ('MANUAL', "Manual", "Specify density and size by manual"), + ], + default='MANUAL' + ) + scene.muv_world_scale_uv_origin = EnumProperty( + name="Origin", + description="Aspect Origin", + items=[ + ('CENTER', "Center", "Center"), + ('LEFT_TOP', "Left Top", "Left Bottom"), + ('LEFT_CENTER', "Left Center", "Left Center"), + ('LEFT_BOTTOM', "Left Bottom", "Left Bottom"), + ('CENTER_TOP', "Center Top", "Center Top"), + ('CENTER_BOTTOM', "Center Bottom", "Center Bottom"), + ('RIGHT_TOP', "Right Top", "Right Top"), + ('RIGHT_CENTER', "Right Center", "Right Center"), + ('RIGHT_BOTTOM', "Right Bottom", "Right Bottom") + + ], + default='CENTER' + ) + + @classmethod + def del_props(cls, scene): + del scene.muv_world_scale_uv_enabled + del scene.muv_world_scale_uv_src_mesh_area + del scene.muv_world_scale_uv_src_uv_area + del scene.muv_world_scale_uv_src_density + del scene.muv_world_scale_uv_tgt_density + del scene.muv_world_scale_uv_tgt_scaling_factor + del scene.muv_world_scale_uv_mode + del scene.muv_world_scale_uv_origin + + +@BlClassRegistry(legacy=True) +class MUV_OT_WorldScaleUV_Measure(bpy.types.Operator): + """ + Operation class: Measure face size + """ + + bl_idname = "uv.muv_world_scale_uv_operator_measure" + bl_label = "Measure World Scale UV" + bl_description = "Measure face size for scale calculation" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + + def execute(self, context): + sc = context.scene + obj = context.active_object + + uv_area, mesh_area, density = measure_wsuv_info(obj) + if not uv_area: + self.report({'WARNING'}, + "Object must have more than one UV map and texture") + return {'CANCELLED'} + + sc.muv_world_scale_uv_src_uv_area = uv_area + sc.muv_world_scale_uv_src_mesh_area = mesh_area + sc.muv_world_scale_uv_src_density = density + + self.report({'INFO'}, + "UV Area: {0}, Mesh Area: {1}, Texel Density: {2}" + .format(uv_area, mesh_area, density)) + + return {'FINISHED'} + + +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) + + +@BlClassRegistry(legacy=True) +class MUV_OT_WorldScaleUV_ApplyManual(bpy.types.Operator): + """ + Operation class: Apply scaled UV (Manual) + """ + + bl_idname = "uv.muv_world_scale_uv_operator_apply_manual" + bl_label = "Apply World Scale UV (Manual)" + bl_description = "Apply scaled UV based on user specification" + bl_options = {'REGISTER', 'UNDO'} + + tgt_density = FloatProperty( + name="Density", + description="Target Texel Density", + default=1.0, + min=0.0 + ) + tgt_texture_size = IntVectorProperty( + name="Texture Size", + size=2, + min=1, + soft_max=10240, + default=(1024, 1024), + ) + origin = EnumProperty( + name="Origin", + description="Aspect Origin", + items=[ + ('CENTER', "Center", "Center"), + ('LEFT_TOP', "Left Top", "Left Bottom"), + ('LEFT_CENTER', "Left Center", "Left Center"), + ('LEFT_BOTTOM', "Left Bottom", "Left Bottom"), + ('CENTER_TOP', "Center Top", "Center Top"), + ('CENTER_BOTTOM', "Center Bottom", "Center Bottom"), + ('RIGHT_TOP', "Right Top", "Right Top"), + ('RIGHT_CENTER', "Right Center", "Right Center"), + ('RIGHT_BOTTOM', "Right Bottom", "Right Bottom") + + ], + default='CENTER' + ) + show_dialog = BoolProperty( + name="Show Diaglog Menu", + description="Show dialog menu if true", + default=True, + options={'HIDDEN', 'SKIP_SAVE'} + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + + def __apply_manual(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + tex_size = self.tgt_texture_size + uv_area, _, density = measure_wsuv_info(obj, tex_size) + if not uv_area: + self.report({'WARNING'}, + "Object must have more than one UV map") + return {'CANCELLED'} + + tgt_density = self.tgt_density + factor = tgt_density / density + + apply(context.active_object, self.origin, factor) + self.report({'INFO'}, "Scaling factor: {0}".format(factor)) + + return {'FINISHED'} + + def draw(self, _): + layout = self.layout + + layout.prop(self, "tgt_density") + layout.prop(self, "tgt_texture_size") + layout.prop(self, "origin") + + layout.separator() + + def invoke(self, context, _): + if self.show_dialog: + wm = context.window_manager + return wm.invoke_props_dialog(self) + + return self.execute(context) + + def execute(self, context): + return self.__apply_manual(context) + + +@BlClassRegistry(legacy=True) +class MUV_OT_WorldScaleUV_ApplyScalingDensity(bpy.types.Operator): + """ + Operation class: Apply scaled UV (Scaling Density) + """ + + bl_idname = "uv.muv_world_scale_uv_operator_apply_scaling_density" + bl_label = "Apply World Scale UV (Scaling Density)" + bl_description = "Apply scaled UV with scaling density" + bl_options = {'REGISTER', 'UNDO'} + + tgt_scaling_factor = FloatProperty( + name="Scaling Factor", + default=1.0, + max=1000.0, + min=0.00001 + ) + origin = EnumProperty( + name="Origin", + description="Aspect Origin", + items=[ + ('CENTER', "Center", "Center"), + ('LEFT_TOP', "Left Top", "Left Bottom"), + ('LEFT_CENTER', "Left Center", "Left Center"), + ('LEFT_BOTTOM', "Left Bottom", "Left Bottom"), + ('CENTER_TOP', "Center Top", "Center Top"), + ('CENTER_BOTTOM', "Center Bottom", "Center Bottom"), + ('RIGHT_TOP', "Right Top", "Right Top"), + ('RIGHT_CENTER', "Right Center", "Right Center"), + ('RIGHT_BOTTOM', "Right Bottom", "Right Bottom") + + ], + default='CENTER' + ) + src_density = FloatProperty( + name="Density", + description="Source Texel Density", + default=0.0, + min=0.0, + options={'HIDDEN'} + ) + same_density = BoolProperty( + name="Same Density", + description="Apply same density", + default=False, + options={'HIDDEN'} + ) + show_dialog = BoolProperty( + name="Show Diaglog Menu", + description="Show dialog menu if true", + default=True, + options={'HIDDEN', 'SKIP_SAVE'} + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + + def __apply_scaling_density(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + uv_area, _, density = measure_wsuv_info(obj) + if not uv_area: + self.report({'WARNING'}, + "Object must have more than one UV map and texture") + return {'CANCELLED'} + + tgt_density = self.src_density * self.tgt_scaling_factor + factor = tgt_density / density + + apply(context.active_object, self.origin, factor) + self.report({'INFO'}, "Scaling factor: {0}".format(factor)) + + return {'FINISHED'} + + def draw(self, _): + layout = self.layout + + layout.label("Source:") + col = layout.column() + col.prop(self, "src_density") + col.enabled = False + + layout.separator() + + if not self.same_density: + layout.prop(self, "tgt_scaling_factor") + layout.prop(self, "origin") + + layout.separator() + + def invoke(self, context, _): + sc = context.scene + + if self.show_dialog: + wm = context.window_manager + + if self.same_density: + self.tgt_scaling_factor = 1.0 + else: + self.tgt_scaling_factor = \ + sc.muv_world_scale_uv_tgt_scaling_factor + self.src_density = sc.muv_world_scale_uv_src_density + + return wm.invoke_props_dialog(self) + + return self.execute(context) + + def execute(self, context): + if self.same_density: + self.tgt_scaling_factor = 1.0 + + return self.__apply_scaling_density(context) + + +@BlClassRegistry(legacy=True) +class MUV_OT_WorldScaleUV_ApplyProportionalToMesh(bpy.types.Operator): + """ + Operation class: Apply scaled UV (Proportional to mesh) + """ + + bl_idname = "uv.muv_world_scale_uv_operator_apply_proportional_to_mesh" + bl_label = "Apply World Scale UV (Proportional to mesh)" + bl_description = "Apply scaled UV proportionaled to mesh" + bl_options = {'REGISTER', 'UNDO'} + + origin = EnumProperty( + name="Origin", + description="Aspect Origin", + items=[ + ('CENTER', "Center", "Center"), + ('LEFT_TOP', "Left Top", "Left Bottom"), + ('LEFT_CENTER', "Left Center", "Left Center"), + ('LEFT_BOTTOM', "Left Bottom", "Left Bottom"), + ('CENTER_TOP', "Center Top", "Center Top"), + ('CENTER_BOTTOM', "Center Bottom", "Center Bottom"), + ('RIGHT_TOP', "Right Top", "Right Top"), + ('RIGHT_CENTER', "Right Center", "Right Center"), + ('RIGHT_BOTTOM', "Right Bottom", "Right Bottom") + + ], + default='CENTER' + ) + src_density = FloatProperty( + name="Source Density", + description="Source Texel Density", + default=0.0, + min=0.0, + options={'HIDDEN'} + ) + src_uv_area = FloatProperty( + name="Source UV Area", + description="Source UV Area", + default=0.0, + min=0.0, + options={'HIDDEN'} + ) + src_mesh_area = FloatProperty( + name="Source Mesh Area", + description="Source Mesh Area", + default=0.0, + min=0.0, + options={'HIDDEN'} + ) + show_dialog = BoolProperty( + name="Show Diaglog Menu", + description="Show dialog menu if true", + default=True, + options={'HIDDEN', 'SKIP_SAVE'} + ) + + @classmethod + def poll(cls, context): + # we can not get area/space/region from console + if common.is_console_mode(): + return True + return is_valid_context(context) + + def __apply_proportional_to_mesh(self, context): + obj = context.active_object + bm = bmesh.from_edit_mesh(obj.data) + if common.check_version(2, 73, 0) >= 0: + bm.verts.ensure_lookup_table() + bm.edges.ensure_lookup_table() + bm.faces.ensure_lookup_table() + + uv_area, mesh_area, density = measure_wsuv_info(obj) + if not uv_area: + self.report({'WARNING'}, + "Object must have more than one UV map and texture") + return {'CANCELLED'} + + tgt_density = self.src_density * sqrt(mesh_area) / sqrt( + self.src_mesh_area) + + factor = tgt_density / density + + apply(context.active_object, self.origin, factor) + self.report({'INFO'}, "Scaling factor: {0}".format(factor)) + + return {'FINISHED'} + + def draw(self, _): + layout = self.layout + + layout.label("Source:") + col = layout.column(align=True) + col.prop(self, "src_density") + col.prop(self, "src_uv_area") + col.prop(self, "src_mesh_area") + col.enabled = False + + layout.separator() + layout.prop(self, "origin") + + layout.separator() + + def invoke(self, context, _): + if self.show_dialog: + wm = context.window_manager + sc = context.scene + + self.src_density = sc.muv_world_scale_uv_src_density + self.src_mesh_area = sc.muv_world_scale_uv_src_mesh_area + + return 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/legacy/preferences.py b/uv_magic_uv/legacy/preferences.py new file mode 100644 index 00000000..931cc1d4 --- /dev/null +++ b/uv_magic_uv/legacy/preferences.py @@ -0,0 +1,468 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bpy +from bpy.props import ( + FloatProperty, + FloatVectorProperty, + BoolProperty, + EnumProperty, + IntProperty, +) +from bpy.types import AddonPreferences + +from . import op +from . import ui +from .. import addon_updater_ops +from ..utils.bl_class_registry import BlClassRegistry + +__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(text="Copy/Paste UV", icon='IMAGE_COL') + # Copy/Paste UV + layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_CopyPasteUV.bl_idname, + text="Copy/Paste UV") + # Transfer UV + layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_TransferUV.bl_idname, + text="Transfer UV") + + layout.separator() + layout.label("UV Manipulation", icon='IMAGE_COL') + # Flip/Rotate UV + ops = layout.operator(op.flip_rotate_uv.MUV_OT_FlipRotate.bl_idname, + text="Flip/Rotate UV") + ops.seams = sc.muv_flip_rotate_uv_seams + # Mirror UV + ops = layout.operator(op.mirror_uv.MUV_OT_MirrorUV.bl_idname, + text="Mirror UV") + ops.axis = sc.muv_mirror_uv_axis + # Move UV + layout.operator(op.move_uv.MUV_OT_MoveUV.bl_idname, text="Move UV") + # World Scale UV + layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_WorldScaleUV.bl_idname, + text="World Scale UV") + # Preserve UV + layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_PreserveUVAspect.bl_idname, + text="Preserve UV") + # Texture Lock + layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_TextureLock.bl_idname, + text="Texture Lock") + # Texture Wrap + layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_TextureWrap.bl_idname, + text="Texture Wrap") + # UV Sculpt + layout.prop(sc, "muv_uv_sculpt_enable", text="UV Sculpt") + + layout.separator() + layout.label("UV Mapping", icon='IMAGE_COL') + # Unwrap Constraint + ops = layout.operator( + op.unwrap_constraint.MUV_OT_UnwrapConstraint.bl_idname, + text="Unwrap Constraint") + ops.u_const = sc.muv_unwrap_constraint_u_const + ops.v_const = sc.muv_unwrap_constraint_v_const + # Texture Projection + layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_TextureProjection.bl_idname, + text="Texture Projection") + # UVW + layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_UVW.bl_idname, text="UVW") + + +def view3d_object_menu_fn(self, _): + layout = self.layout + + layout.separator() + # Copy/Paste UV (Among Objecct) + layout.menu(ui.VIEW3D_MT_object.MUV_MT_CopyPasteUV_Object.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.MUV_MT_AlignUVCursor.bl_idname, + text="Align UV Cursor") + # UV Bounding Box + layout.prop(sc, "muv_uv_bounding_box_show", text="UV Bounding Box") + # UV Inspection + layout.menu(ui.IMAGE_MT_uvs.MUV_MT_UVInspection.bl_idname, + text="UV Inspection") + layout.label("Editor Enhancement", icon='IMAGE_COL') + + layout.separator() + # Align UV + layout.menu(ui.IMAGE_MT_uvs.MUV_MT_AlignUV.bl_idname, text="Align UV") + # Smooth UV + ops = layout.operator(op.smooth_uv.MUV_OT_SmoothUV.bl_idname, + text="Smooth") + ops.transmission = sc.muv_smooth_uv_transmission + ops.select = sc.muv_smooth_uv_select + ops.mesh_infl = sc.muv_smooth_uv_mesh_infl + # Select UV + layout.menu(ui.IMAGE_MT_uvs.MUV_MT_SelectUV.bl_idname, text="Select UV") + # Pack UV + ops = layout.operator(op.pack_uv.MUV_OT_PackUV.bl_idname, text="Pack UV") + ops.allowable_center_deviation = sc.muv_pack_uv_allowable_center_deviation + ops.allowable_size_deviation = sc.muv_pack_uv_allowable_size_deviation + layout.label("UV Manipulation", icon='IMAGE_COL') + + layout.separator() + # Copy/Paste UV (on UV/Image Editor) + layout.menu(ui.IMAGE_MT_uvs.MUV_MT_CopyPasteUV_UVEdit.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_uv_map.remove(view3d_uvmap_menu_fn) + bpy.types.VIEW3D_MT_object.remove(view3d_object_menu_fn) + + +@BlClassRegistry(legacy=True) +class Preferences(AddonPreferences): + """Preferences class: Preferences for this add-on""" + + bl_idname = "uv_magic_uv" + + def update_enable_builtin_menu(self, _): + if self['enable_builtin_menu']: + add_builtin_menu() + else: + remove_builtin_menu() + + # enable to add features to built-in menu + enable_builtin_menu = BoolProperty( + name="Built-in Menu", + description="Enable built-in menu", + default=True, + update=update_enable_builtin_menu + ) + + # for UV Sculpt + uv_sculpt_brush_color = FloatVectorProperty( + name="Color", + description="Color", + default=(1.0, 0.4, 0.4, 1.0), + min=0.0, + max=1.0, + size=4, + subtype='COLOR' + ) + + # for Overlapped UV + uv_inspection_overlapped_color = FloatVectorProperty( + name="Color", + description="Color", + default=(0.0, 0.0, 1.0, 0.3), + min=0.0, + max=1.0, + size=4, + subtype='COLOR' + ) + + # for Flipped UV + uv_inspection_flipped_color = FloatVectorProperty( + name="Color", + description="Color", + default=(1.0, 0.0, 0.0, 0.3), + min=0.0, + max=1.0, + size=4, + subtype='COLOR' + ) + + # for Texture Projection + texture_projection_canvas_padding = FloatVectorProperty( + name="Canvas Padding", + description="Canvas Padding", + size=2, + max=50.0, + min=0.0, + default=(20.0, 20.0)) + + # for UV Bounding Box + uv_bounding_box_cp_size = FloatProperty( + name="Size", + description="Control Point Size", + default=6.0, + min=3.0, + max=100.0) + uv_bounding_box_cp_react_size = FloatProperty( + name="React Size", + description="Size event fired", + default=10.0, + min=3.0, + max=100.0) + + # for UI + category = EnumProperty( + name="Category", + description="Preferences Category", + items=[ + ('INFO', "Information", "Information about this add-on"), + ('CONFIG', "Configuration", "Configuration about this add-on"), + ('UPDATE', "Update", "Update this add-on"), + ], + default='INFO' + ) + info_desc_expanded = BoolProperty( + name="Description", + description="Description", + default=False + ) + info_loc_expanded = BoolProperty( + name="Location", + description="Location", + default=False + ) + conf_uv_sculpt_expanded = BoolProperty( + name="UV Sculpt", + description="UV Sculpt", + default=False + ) + conf_uv_inspection_expanded = BoolProperty( + name="UV Inspection", + description="UV Inspection", + default=False + ) + conf_texture_projection_expanded = BoolProperty( + name="Texture Projection", + description="Texture Projection", + default=False + ) + conf_uv_bounding_box_expanded = BoolProperty( + name="UV Bounding Box", + description="UV Bounding Box", + default=False + ) + + # for add-on updater + 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.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/legacy/properites.py b/uv_magic_uv/legacy/properites.py new file mode 100644 index 00000000..b64a48f3 --- /dev/null +++ b/uv_magic_uv/legacy/properites.py @@ -0,0 +1,61 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + + +from ..utils.property_class_registry import PropertyClassRegistry + +__all__ = [ + 'MUV_Properties', + 'init_props', + 'clear_props', +] + + +# Properties used in this add-on. +# pylint: disable=W0612 +class MUV_Properties(): + def __init__(self): + self.prefs = MUV_Prefs() + + +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() + PropertyClassRegistry.init_props(scene) + + +def clear_props(scene): + PropertyClassRegistry.del_props(scene) + del scene.muv_props diff --git a/uv_magic_uv/legacy/ui/IMAGE_MT_uvs.py b/uv_magic_uv/legacy/ui/IMAGE_MT_uvs.py new file mode 100644 index 00000000..bf071bf5 --- /dev/null +++ b/uv_magic_uv/legacy/ui/IMAGE_MT_uvs.py @@ -0,0 +1,197 @@ +# + +# ##### 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, + copy_paste_uv_uvedit, + align_uv, + select_uv, + uv_inspection +) +from ...utils.bl_class_registry import BlClassRegistry + +__all__ = [ + 'MUV_MT_CopyPasteUV_UVEdit', + 'MUV_MT_AlignUV', + 'MUV_MT_SelectUV', + 'MUV_MT_AlignUVCursor', + 'MUV_MT_UVInspection', +] + + +@BlClassRegistry(legacy=True) +class MUV_MT_CopyPasteUV_UVEdit(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.MUV_OT_CopyPasteUVUVEdit_CopyUV.bl_idname, + text="Copy") + layout.operator( + copy_paste_uv_uvedit.MUV_OT_CopyPasteUVUVEdit_PasteUV.bl_idname, + text="Paste") + + +@BlClassRegistry(legacy=True) +class MUV_MT_AlignUV(bpy.types.Menu): + """ + Menu class: Master menu of Align UV + """ + + bl_idname = "uv.muv_align_uv_menu" + bl_label = "Align UV" + bl_description = "Align UV" + + def draw(self, context): + layout = self.layout + sc = context.scene + + ops = layout.operator(align_uv.MUV_OT_AlignUV_Circle.bl_idname, + text="Circle") + ops.transmission = sc.muv_align_uv_transmission + ops.select = sc.muv_align_uv_select + + ops = layout.operator(align_uv.MUV_OT_AlignUV_Straighten.bl_idname, + text="Straighten") + ops.transmission = sc.muv_align_uv_transmission + ops.select = sc.muv_align_uv_select + ops.vertical = sc.muv_align_uv_vertical + ops.horizontal = sc.muv_align_uv_horizontal + + ops = layout.operator(align_uv.MUV_OT_AlignUV_Axis.bl_idname, + text="XY-axis") + ops.transmission = sc.muv_align_uv_transmission + ops.select = sc.muv_align_uv_select + ops.vertical = sc.muv_align_uv_vertical + ops.horizontal = sc.muv_align_uv_horizontal + ops.location = sc.muv_align_uv_location + + +@BlClassRegistry(legacy=True) +class MUV_MT_SelectUV(bpy.types.Menu): + """ + Menu class: Master menu of Select UV + """ + + bl_idname = "uv.muv_select_uv_menu" + bl_label = "Select UV" + bl_description = "Select UV" + + def draw(self, _): + layout = self.layout + + layout.operator(select_uv.MUV_OT_SelectUV_SelectOverlapped.bl_idname, + text="Overlapped") + layout.operator(select_uv.MUV_OT_SelectUV_SelectFlipped.bl_idname, + text="Flipped") + + +@BlClassRegistry(legacy=True) +class MUV_MT_AlignUVCursor(bpy.types.Menu): + """ + Menu class: Master menu of Align UV Cursor + """ + + bl_idname = "uv.muv_align_uv_cursor_menu" + bl_label = "Align UV Cursor" + bl_description = "Align UV cursor" + + def draw(self, context): + layout = self.layout + sc = context.scene + + ops = layout.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname, + text="Left Top") + ops.position = 'LEFT_TOP' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname, + text="Middle Top") + ops.position = 'MIDDLE_TOP' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname, + text="Right Top") + ops.position = 'RIGHT_TOP' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname, + text="Left Middle") + ops.position = 'LEFT_MIDDLE' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname, + text="Center") + ops.position = 'CENTER' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname, + text="Right Middle") + ops.position = 'RIGHT_MIDDLE' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname, + text="Left Bottom") + ops.position = 'LEFT_BOTTOM' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname, + text="Middle Bottom") + ops.position = 'MIDDLE_BOTTOM' + ops.base = sc.muv_align_uv_cursor_align_method + + ops = layout.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname, + text="Right Bottom") + ops.position = 'RIGHT_BOTTOM' + ops.base = sc.muv_align_uv_cursor_align_method + + +@BlClassRegistry(legacy=True) +class MUV_MT_UVInspection(bpy.types.Menu): + """ + Menu class: Master menu of UV Inspection + """ + + bl_idname = "uv.muv_uv_inspection_menu" + bl_label = "UV Inspection" + bl_description = "UV Inspection" + + def draw(self, context): + layout = self.layout + sc = context.scene + + layout.prop(sc, "muv_uv_inspection_show", text="UV Inspection") + layout.operator(uv_inspection.MUV_OT_UVInspection_Update.bl_idname, + text="Update") diff --git a/uv_magic_uv/legacy/ui/VIEW3D_MT_object.py b/uv_magic_uv/legacy/ui/VIEW3D_MT_object.py new file mode 100644 index 00000000..e1c751ae --- /dev/null +++ b/uv_magic_uv/legacy/ui/VIEW3D_MT_object.py @@ -0,0 +1,54 @@ +# + +# ##### 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 +from ...utils.bl_class_registry import BlClassRegistry + +__all__ = [ + 'MUV_MT_CopyPasteUV_Object', +] + + +@BlClassRegistry(legacy=True) +class MUV_MT_CopyPasteUV_Object(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.MUV_MT_CopyPasteUVObject_CopyUV.bl_idname, + text="Copy") + layout.menu( + copy_paste_uv_object.MUV_MT_CopyPasteUVObject_PasteUV.bl_idname, + text="Paste") diff --git a/uv_magic_uv/legacy/ui/VIEW3D_MT_uv_map.py b/uv_magic_uv/legacy/ui/VIEW3D_MT_uv_map.py new file mode 100644 index 00000000..d229322a --- /dev/null +++ b/uv_magic_uv/legacy/ui/VIEW3D_MT_uv_map.py @@ -0,0 +1,257 @@ +# + +# ##### 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 bpy.utils + +from ..op import ( + texture_lock, + copy_paste_uv, + preserve_uv_aspect, + texture_projection, + texture_wrap, + transfer_uv, + uvw, + world_scale_uv +) +from ..op.world_scale_uv import MUV_OT_WorldScaleUV_ApplyProportionalToMesh +from ...utils.bl_class_registry import BlClassRegistry + +__all__ = [ + 'MUV_MT_CopyPasteUV', + 'MUV_MT_TransferUV', + 'MUV_MT_TextureLock', + 'MUV_MT_WorldScaleUV', + 'MUV_MT_TextureWrap', + 'MUV_MT_UVW', + 'MUV_MT_TextureProjection', + 'MUV_MT_PreserveUVAspect', +] + + +@BlClassRegistry(legacy=True) +class MUV_MT_CopyPasteUV(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(text="Default") + layout.menu(copy_paste_uv.MUV_MT_CopyPasteUV_CopyUV.bl_idname, + text="Copy") + layout.menu(copy_paste_uv.MUV_MT_CopyPasteUV_PasteUV.bl_idname, + text="Paste") + + layout.separator() + + layout.label(text="Selection Sequence") + layout.menu(copy_paste_uv.MUV_MT_CopyPasteUV_SelSeqCopyUV.bl_idname, + text="Copy") + layout.menu(copy_paste_uv.MUV_MT_CopyPasteUV_SelSeqPasteUV.bl_idname, + text="Paste") + + +@BlClassRegistry(legacy=True) +class MUV_MT_TransferUV(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.MUV_OT_TransferUV_CopyUV.bl_idname, + text="Copy") + ops = layout.operator(transfer_uv.MUV_OT_TransferUV_PasteUV.bl_idname, + text="Paste") + ops.invert_normals = sc.muv_transfer_uv_invert_normals + ops.copy_seams = sc.muv_transfer_uv_copy_seams + + +@BlClassRegistry(legacy=True) +class MUV_MT_TextureLock(bpy.types.Menu): + """ + Menu class: Master menu of Texture Lock + """ + + bl_idname = "uv.muv_texture_lock_menu" + bl_label = "Texture Lock" + bl_description = "Lock texture when vertices of mesh (Preserve UV)" + + def draw(self, context): + layout = self.layout + sc = context.scene + + layout.label("Normal Mode") + layout.operator( + texture_lock.MUV_OT_TextureLock_Lock.bl_idname, + text="Lock" + if not texture_lock.MUV_OT_TextureLock_Lock.is_ready(context) + else "ReLock") + ops = layout.operator(texture_lock.MUV_OT_TextureLock_Unlock.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") + + +@BlClassRegistry(legacy=True) +class MUV_MT_WorldScaleUV(bpy.types.Menu): + """ + Menu class: Master menu of world scale UV + """ + + bl_idname = "uv.muv_world_scale_uv_menu" + bl_label = "World Scale UV" + bl_description = "" + + def draw(self, context): + layout = self.layout + sc = context.scene + + layout.operator(world_scale_uv.MUV_OT_WorldScaleUV_Measure.bl_idname, + text="Measure") + + layout.operator( + world_scale_uv.MUV_OT_WorldScaleUV_ApplyManual.bl_idname, + text="Apply (Manual)") + + ops = layout.operator( + world_scale_uv.MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname, + text="Apply (Same Desity)") + ops.src_density = sc.muv_world_scale_uv_src_density + ops.same_density = True + + ops = layout.operator( + world_scale_uv.MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname, + text="Apply (Scaling Desity)") + ops.src_density = sc.muv_world_scale_uv_src_density + ops.same_density = False + ops.tgt_scaling_factor = sc.muv_world_scale_uv_tgt_scaling_factor + + ops = layout.operator( + MUV_OT_WorldScaleUV_ApplyProportionalToMesh.bl_idname, + text="Apply (Proportional to Mesh)") + ops.src_density = sc.muv_world_scale_uv_src_density + ops.src_uv_area = sc.muv_world_scale_uv_src_uv_area + ops.src_mesh_area = sc.muv_world_scale_uv_src_mesh_area + ops.origin = sc.muv_world_scale_uv_origin + + +@BlClassRegistry(legacy=True) +class MUV_MT_TextureWrap(bpy.types.Menu): + """ + Menu class: Master menu of Texture Wrap + """ + + bl_idname = "uv.muv_texture_wrap_menu" + bl_label = "Texture Wrap" + bl_description = "" + + def draw(self, _): + layout = self.layout + + layout.operator(texture_wrap.MUV_OT_TextureWrap_Refer.bl_idname, + text="Refer") + layout.operator(texture_wrap.MUV_OT_TextureWrap_Set.bl_idname, + text="Set") + + +@BlClassRegistry(legacy=True) +class MUV_MT_UVW(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.MUV_OT_UVW_BoxMap.bl_idname, text="Box") + ops.assign_uvmap = sc.muv_uvw_assign_uvmap + + ops = layout.operator(uvw.MUV_OT_UVW_BestPlanerMap.bl_idname, + text="Best Planner") + ops.assign_uvmap = sc.muv_uvw_assign_uvmap + + +@BlClassRegistry(legacy=True) +class MUV_MT_TextureProjection(bpy.types.Menu): + """ + Menu class: Master menu of Texture Projection + """ + + bl_idname = "uv.muv_texture_projection_menu" + bl_label = "Texture Projection" + bl_description = "" + + def draw(self, context): + layout = self.layout + sc = context.scene + + layout.prop(sc, "muv_texture_projection_enable", + text="Texture Projection") + layout.operator( + texture_projection.MUV_OT_TextureProjection_Project.bl_idname, + text="Project") + + +@BlClassRegistry(legacy=True) +class MUV_MT_PreserveUVAspect(bpy.types.Menu): + """ + Menu class: Master menu of Preserve UV Aspect + """ + + bl_idname = "uv.muv_preserve_uv_aspect_menu" + bl_label = "Preserve UV Aspect" + bl_description = "" + + def draw(self, context): + layout = self.layout + sc = context.scene + + for key in bpy.data.images.keys(): + ops = layout.operator( + preserve_uv_aspect.MUV_OT_PreserveUVAspect.bl_idname, text=key) + ops.dest_img_name = key + ops.origin = sc.muv_preserve_uv_aspect_origin diff --git a/uv_magic_uv/legacy/ui/__init__.py b/uv_magic_uv/legacy/ui/__init__.py new file mode 100644 index 00000000..bf790a8e --- /dev/null +++ b/uv_magic_uv/legacy/ui/__init__.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" + +if "bpy" in locals(): + import importlib + importlib.reload(view3d_copy_paste_uv_objectmode) + importlib.reload(view3d_copy_paste_uv_editmode) + importlib.reload(view3d_uv_manipulation) + importlib.reload(view3d_uv_mapping) + importlib.reload(uvedit_copy_paste_uv) + importlib.reload(uvedit_uv_manipulation) + importlib.reload(uvedit_editor_enhancement) + importlib.reload(VIEW3D_MT_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 + from . import view3d_uv_manipulation + from . import view3d_uv_mapping + from . import uvedit_copy_paste_uv + from . import uvedit_uv_manipulation + from . import uvedit_editor_enhancement + from . import VIEW3D_MT_uv_map + from . import VIEW3D_MT_object + from . import IMAGE_MT_uvs + +import bpy \ No newline at end of file diff --git a/uv_magic_uv/legacy/ui/uvedit_copy_paste_uv.py b/uv_magic_uv/legacy/ui/uvedit_copy_paste_uv.py new file mode 100644 index 00000000..9848f03b --- /dev/null +++ b/uv_magic_uv/legacy/ui/uvedit_copy_paste_uv.py @@ -0,0 +1,62 @@ +# + +# ##### 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 ...utils.bl_class_registry import BlClassRegistry + +__all__ = [ + 'MUV_PT_UVEdit_CopyPasteUV', +] + + +@BlClassRegistry(legacy=True) +class MUV_PT_UVEdit_CopyPasteUV(bpy.types.Panel): + """ + Panel class: Copy/Paste UV on Property Panel on UV/ImageEditor + """ + + bl_space_type = 'IMAGE_EDITOR' + bl_region_type = 'TOOLS' + bl_label = "Copy/Paste UV" + bl_category = "Magic UV" + bl_context = 'mesh_edit' + bl_options = {'DEFAULT_CLOSED'} + + def draw_header(self, _): + layout = self.layout + layout.label(text="", icon='IMAGE_COL') + + def draw(self, _): + layout = self.layout + + row = layout.row(align=True) + row.operator( + copy_paste_uv_uvedit.MUV_OT_CopyPasteUVUVEdit_CopyUV.bl_idname, + text="Copy") + row.operator( + copy_paste_uv_uvedit.MUV_OT_CopyPasteUVUVEdit_PasteUV.bl_idname, + text="Paste") diff --git a/uv_magic_uv/legacy/ui/uvedit_editor_enhancement.py b/uv_magic_uv/legacy/ui/uvedit_editor_enhancement.py new file mode 100644 index 00000000..3f750feb --- /dev/null +++ b/uv_magic_uv/legacy/ui/uvedit_editor_enhancement.py @@ -0,0 +1,149 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bpy + +from ..op import ( + align_uv_cursor, + uv_bounding_box, + uv_inspection, +) +from ...utils.bl_class_registry import BlClassRegistry + +__all__ = [ + 'MUV_PT_UVEdit_EditorEnhancement', +] + + +@BlClassRegistry(legacy=True) +class MUV_PT_UVEdit_EditorEnhancement(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.MUV_OT_AlignUVCursor.bl_idname, + text="Left Top") + ops.position = 'LEFT_TOP' + ops.base = sc.muv_align_uv_cursor_align_method + ops = row.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname, + text="Middle Top") + ops.position = 'MIDDLE_TOP' + ops.base = sc.muv_align_uv_cursor_align_method + ops = row.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname, + text="Right Top") + ops.position = 'RIGHT_TOP' + ops.base = sc.muv_align_uv_cursor_align_method + + row = col.row(align=True) + ops = row.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname, + text="Left Middle") + ops.position = 'LEFT_MIDDLE' + ops.base = sc.muv_align_uv_cursor_align_method + ops = row.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname, + text="Center") + ops.position = 'CENTER' + ops.base = sc.muv_align_uv_cursor_align_method + ops = row.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname, + text="Right Middle") + ops.position = 'RIGHT_MIDDLE' + ops.base = sc.muv_align_uv_cursor_align_method + + row = col.row(align=True) + ops = row.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname, + text="Left Bottom") + ops.position = 'LEFT_BOTTOM' + ops.base = sc.muv_align_uv_cursor_align_method + ops = row.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname, + text="Middle Bottom") + ops.position = 'MIDDLE_BOTTOM' + ops.base = sc.muv_align_uv_cursor_align_method + ops = row.operator(align_uv_cursor.MUV_OT_AlignUVCursor.bl_idname, + text="Right Bottom") + ops.position = 'RIGHT_BOTTOM' + ops.base = sc.muv_align_uv_cursor_align_method + + box = layout.box() + box.prop(sc, "muv_uv_cursor_location_enabled", + text="UV Cursor Location") + if sc.muv_uv_cursor_location_enabled: + box.prop(sc, "muv_align_uv_cursor_cursor_loc", text="") + + box = layout.box() + box.prop(sc, "muv_uv_bounding_box_enabled", text="UV Bounding Box") + if sc.muv_uv_bounding_box_enabled: + box.prop( + sc, "muv_uv_bounding_box_show", + text="Hide" + if uv_bounding_box.MUV_OT_UVBoundingBox.is_running(context) + else "Show", + icon='RESTRICT_VIEW_OFF' + if uv_bounding_box.MUV_OT_UVBoundingBox.is_running(context) + else 'RESTRICT_VIEW_ON') + box.prop(sc, "muv_uv_bounding_box_uniform_scaling", + text="Uniform Scaling") + box.prop(sc, "muv_uv_bounding_box_boundary", text="Boundary") + + box = layout.box() + box.prop(sc, "muv_uv_inspection_enabled", text="UV Inspection") + if sc.muv_uv_inspection_enabled: + row = box.row() + row.prop( + sc, "muv_uv_inspection_show", + text="Hide" + if uv_inspection.MUV_OT_UVInspection_Render.is_running(context) + else "Show", + icon='RESTRICT_VIEW_OFF' + if uv_inspection.MUV_OT_UVInspection_Render.is_running(context) + else 'RESTRICT_VIEW_ON') + row.operator(uv_inspection.MUV_OT_UVInspection_Update.bl_idname, + text="Update") + row = box.row() + row.prop(sc, "muv_uv_inspection_show_overlapped") + row.prop(sc, "muv_uv_inspection_show_flipped") + row = box.row() + row.prop(sc, "muv_uv_inspection_show_mode") diff --git a/uv_magic_uv/legacy/ui/uvedit_uv_manipulation.py b/uv_magic_uv/legacy/ui/uvedit_uv_manipulation.py new file mode 100644 index 00000000..96cf17d3 --- /dev/null +++ b/uv_magic_uv/legacy/ui/uvedit_uv_manipulation.py @@ -0,0 +1,130 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +import bpy + +from ..op import ( + align_uv, + smooth_uv, + pack_uv, + select_uv, +) +from ...utils.bl_class_registry import BlClassRegistry + +__all__ = [ + 'MUV_PT_UVEdit_UVManipulation', +] + + +@BlClassRegistry(legacy=True) +class MUV_PT_UVEdit_UVManipulation(bpy.types.Panel): + """ + Panel class: UV Manipulation on Property Panel on UV/ImageEditor + """ + + bl_space_type = 'IMAGE_EDITOR' + bl_region_type = 'TOOLS' + bl_label = "UV Manipulation" + bl_category = "Magic UV" + bl_context = 'mesh_edit' + bl_options = {'DEFAULT_CLOSED'} + + def draw_header(self, _): + layout = self.layout + layout.label(text="", icon='IMAGE_COL') + + def draw(self, context): + sc = context.scene + layout = self.layout + + box = layout.box() + box.prop(sc, "muv_align_uv_enabled", text="Align UV") + if sc.muv_align_uv_enabled: + col = box.column() + row = col.row(align=True) + ops = row.operator(align_uv.MUV_OT_AlignUV_Circle.bl_idname, + text="Circle") + ops.transmission = sc.muv_align_uv_transmission + ops.select = sc.muv_align_uv_select + ops = row.operator(align_uv.MUV_OT_AlignUV_Straighten.bl_idname, + text="Straighten") + ops.transmission = sc.muv_align_uv_transmission + ops.select = sc.muv_align_uv_select + ops.vertical = sc.muv_align_uv_vertical + ops.horizontal = sc.muv_align_uv_horizontal + ops.mesh_infl = sc.muv_align_uv_mesh_infl + row = col.row() + ops = row.operator(align_uv.MUV_OT_AlignUV_Axis.bl_idname, + text="XY-axis") + ops.transmission = sc.muv_align_uv_transmission + ops.select = sc.muv_align_uv_select + ops.vertical = sc.muv_align_uv_vertical + ops.horizontal = sc.muv_align_uv_horizontal + ops.location = sc.muv_align_uv_location + ops.mesh_infl = sc.muv_align_uv_mesh_infl + row.prop(sc, "muv_align_uv_location", text="") + + col = box.column(align=True) + row = col.row(align=True) + row.prop(sc, "muv_align_uv_transmission", text="Transmission") + row.prop(sc, "muv_align_uv_select", text="Select") + row = col.row(align=True) + row.prop(sc, "muv_align_uv_vertical", text="Vertical") + row.prop(sc, "muv_align_uv_horizontal", text="Horizontal") + col.prop(sc, "muv_align_uv_mesh_infl", text="Mesh Influence") + + box = layout.box() + box.prop(sc, "muv_smooth_uv_enabled", text="Smooth UV") + if sc.muv_smooth_uv_enabled: + ops = box.operator(smooth_uv.MUV_OT_SmoothUV.bl_idname, + text="Smooth") + ops.transmission = sc.muv_smooth_uv_transmission + ops.select = sc.muv_smooth_uv_select + ops.mesh_infl = sc.muv_smooth_uv_mesh_infl + col = box.column(align=True) + row = col.row(align=True) + row.prop(sc, "muv_smooth_uv_transmission", text="Transmission") + row.prop(sc, "muv_smooth_uv_select", text="Select") + col.prop(sc, "muv_smooth_uv_mesh_infl", text="Mesh Influence") + + box = layout.box() + box.prop(sc, "muv_select_uv_enabled", text="Select UV") + if sc.muv_select_uv_enabled: + row = box.row(align=True) + row.operator(select_uv.MUV_OT_SelectUV_SelectOverlapped.bl_idname) + row.operator(select_uv.MUV_OT_SelectUV_SelectFlipped.bl_idname) + + box = layout.box() + box.prop(sc, "muv_pack_uv_enabled", text="Pack UV (Extension)") + if sc.muv_pack_uv_enabled: + ops = box.operator(pack_uv.MUV_OT_PackUV.bl_idname, text="Pack UV") + ops.allowable_center_deviation = \ + sc.muv_pack_uv_allowable_center_deviation + ops.allowable_size_deviation = \ + sc.muv_pack_uv_allowable_size_deviation + box.label("Allowable Center Deviation:") + box.prop(sc, "muv_pack_uv_allowable_center_deviation", text="") + box.label("Allowable Size Deviation:") + box.prop(sc, "muv_pack_uv_allowable_size_deviation", text="") diff --git a/uv_magic_uv/legacy/ui/view3d_copy_paste_uv_editmode.py b/uv_magic_uv/legacy/ui/view3d_copy_paste_uv_editmode.py new file mode 100644 index 00000000..ee2713b2 --- /dev/null +++ b/uv_magic_uv/legacy/ui/view3d_copy_paste_uv_editmode.py @@ -0,0 +1,93 @@ +# + +# ##### 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 ( + transfer_uv, + copy_paste_uv, +) +from ...utils.bl_class_registry import BlClassRegistry + +__all__ = [ + 'MUV_PT_View3D_Edit_CopyPasteUV', +] + + +@BlClassRegistry(legacy=True) +class MUV_PT_View3D_Edit_CopyPasteUV(bpy.types.Panel): + """ + Panel class: Copy/Paste UV on Property Panel on View3D + """ + + bl_space_type = 'VIEW_3D' + bl_region_type = 'TOOLS' + bl_label = "Copy/Paste UV" + bl_category = "Magic UV" + bl_context = 'mesh_edit' + bl_options = {'DEFAULT_CLOSED'} + + def draw_header(self, _): + layout = self.layout + layout.label(text="", icon='IMAGE_COL') + + def draw(self, context): + sc = context.scene + layout = self.layout + + box = layout.box() + box.prop(sc, "muv_copy_paste_uv_enabled", text="Copy/Paste UV") + if sc.muv_copy_paste_uv_enabled: + row = box.row(align=True) + if sc.muv_copy_paste_uv_mode == 'DEFAULT': + row.menu(copy_paste_uv.MUV_MT_CopyPasteUV_CopyUV.bl_idname, + text="Copy") + row.menu(copy_paste_uv.MUV_MT_CopyPasteUV_PasteUV.bl_idname, + text="Paste") + elif sc.muv_copy_paste_uv_mode == 'SEL_SEQ': + row.menu( + copy_paste_uv.MUV_MT_CopyPasteUV_SelSeqCopyUV.bl_idname, + text="Copy") + row.menu( + copy_paste_uv.MUV_MT_CopyPasteUV_SelSeqPasteUV.bl_idname, + text="Paste") + box.prop(sc, "muv_copy_paste_uv_mode", expand=True) + box.prop(sc, "muv_copy_paste_uv_copy_seams", text="Seams") + box.prop(sc, "muv_copy_paste_uv_strategy", text="Strategy") + + box = layout.box() + box.prop(sc, "muv_transfer_uv_enabled", text="Transfer UV") + if sc.muv_transfer_uv_enabled: + row = box.row(align=True) + row.operator(transfer_uv.MUV_OT_TransferUV_CopyUV.bl_idname, + text="Copy") + ops = row.operator(transfer_uv.MUV_OT_TransferUV_PasteUV.bl_idname, + text="Paste") + ops.invert_normals = sc.muv_transfer_uv_invert_normals + ops.copy_seams = sc.muv_transfer_uv_copy_seams + row = box.row() + row.prop(sc, "muv_transfer_uv_invert_normals", + text="Invert Normals") + row.prop(sc, "muv_transfer_uv_copy_seams", text="Seams") diff --git a/uv_magic_uv/legacy/ui/view3d_copy_paste_uv_objectmode.py b/uv_magic_uv/legacy/ui/view3d_copy_paste_uv_objectmode.py new file mode 100644 index 00000000..58031b0f --- /dev/null +++ b/uv_magic_uv/legacy/ui/view3d_copy_paste_uv_objectmode.py @@ -0,0 +1,65 @@ +# + +# ##### 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 +from ...utils.bl_class_registry import BlClassRegistry + +__all__ = [ + 'MUV_PT_View3D_Object_CopyPasteUV', +] + + +@BlClassRegistry(legacy=True) +class MUV_PT_View3D_Object_CopyPasteUV(bpy.types.Panel): + """ + Panel class: Copy/Paste UV on Property Panel on View3D + """ + + bl_space_type = 'VIEW_3D' + bl_region_type = 'TOOLS' + bl_label = "Copy/Paste UV" + bl_category = "Magic UV" + bl_context = 'objectmode' + bl_options = {'DEFAULT_CLOSED'} + + def draw_header(self, _): + layout = self.layout + layout.label(text="", icon='IMAGE_COL') + + def draw(self, context): + sc = context.scene + layout = self.layout + + row = layout.row(align=True) + row.menu( + copy_paste_uv_object.MUV_MT_CopyPasteUVObject_CopyUV.bl_idname, + text="Copy") + row.menu( + copy_paste_uv_object.MUV_MT_CopyPasteUVObject_PasteUV.bl_idname, + text="Paste") + layout.prop(sc, "muv_copy_paste_uv_object_copy_seams", + text="Seams") diff --git a/uv_magic_uv/legacy/ui/view3d_uv_manipulation.py b/uv_magic_uv/legacy/ui/view3d_uv_manipulation.py new file mode 100644 index 00000000..7d828a38 --- /dev/null +++ b/uv_magic_uv/legacy/ui/view3d_uv_manipulation.py @@ -0,0 +1,289 @@ +# + +# ##### 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 ( + move_uv, + flip_rotate_uv, + mirror_uv, + preserve_uv_aspect, + texture_lock, + texture_wrap, + uv_sculpt, + world_scale_uv, +) +from ..op.world_scale_uv import ( + MUV_OT_WorldScaleUV_ApplyProportionalToMesh, + MUV_OT_WorldScaleUV_ApplyScalingDensity +) +from ...utils.bl_class_registry import BlClassRegistry + +__all__ = [ + 'MUV_PT_View3D_UVManipulation', +] + + +@BlClassRegistry(legacy=True) +class MUV_PT_View3D_UVManipulation(bpy.types.Panel): + """ + Panel class: UV Manipulation on Property Panel on View3D + """ + + bl_space_type = 'VIEW_3D' + bl_region_type = 'TOOLS' + bl_label = "UV Manipulation" + bl_category = "Magic UV" + bl_context = 'mesh_edit' + bl_options = {'DEFAULT_CLOSED'} + + def draw_header(self, _): + layout = self.layout + layout.label(text="", icon='IMAGE_COL') + + def draw(self, context): + sc = context.scene + layout = self.layout + + box = layout.box() + box.prop(sc, "muv_flip_rotate_uv_enabled", text="Flip/Rotate UV") + if sc.muv_flip_rotate_uv_enabled: + row = box.row() + ops = row.operator(flip_rotate_uv.MUV_OT_FlipRotate.bl_idname, + text="Flip/Rotate") + ops.seams = sc.muv_flip_rotate_uv_seams + row.prop(sc, "muv_flip_rotate_uv_seams", text="Seams") + + box = layout.box() + box.prop(sc, "muv_mirror_uv_enabled", text="Mirror UV") + if sc.muv_mirror_uv_enabled: + row = box.row() + ops = row.operator(mirror_uv.MUV_OT_MirrorUV.bl_idname, + text="Mirror") + ops.axis = sc.muv_mirror_uv_axis + row.prop(sc, "muv_mirror_uv_axis", text="") + + box = layout.box() + box.prop(sc, "muv_move_uv_enabled", text="Move UV") + if sc.muv_move_uv_enabled: + col = box.column() + if not move_uv.MUV_OT_MoveUV.is_running(context): + col.operator(move_uv.MUV_OT_MoveUV.bl_idname, icon='PLAY', + text="Start") + else: + col.operator(move_uv.MUV_OT_MoveUV.bl_idname, icon='PAUSE', + text="Stop") + + box = layout.box() + box.prop(sc, "muv_world_scale_uv_enabled", text="World Scale UV") + if sc.muv_world_scale_uv_enabled: + box.prop(sc, "muv_world_scale_uv_mode", text="") + + if sc.muv_world_scale_uv_mode == 'MANUAL': + sp = box.split(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.MUV_OT_WorldScaleUV_ApplyManual.bl_idname, + text="Apply") + ops.tgt_density = sc.muv_world_scale_uv_tgt_density + ops.tgt_texture_size = sc.muv_world_scale_uv_tgt_texture_size + ops.origin = sc.muv_world_scale_uv_origin + ops.show_dialog = False + + elif sc.muv_world_scale_uv_mode == 'SAME_DENSITY': + sp = box.split(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.MUV_OT_WorldScaleUV_Measure.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( + MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname, + text="Apply") + ops.src_density = sc.muv_world_scale_uv_src_density + ops.origin = sc.muv_world_scale_uv_origin + ops.same_density = True + ops.show_dialog = False + + elif sc.muv_world_scale_uv_mode == 'SCALING_DENSITY': + sp = box.split(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.MUV_OT_WorldScaleUV_Measure.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( + MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname, + text="Apply") + ops.src_density = sc.muv_world_scale_uv_src_density + ops.origin = sc.muv_world_scale_uv_origin + ops.same_density = False + ops.show_dialog = False + ops.tgt_scaling_factor = \ + sc.muv_world_scale_uv_tgt_scaling_factor + + elif sc.muv_world_scale_uv_mode == 'PROPORTIONAL_TO_MESH': + sp = box.split(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.MUV_OT_WorldScaleUV_Measure.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( + MUV_OT_WorldScaleUV_ApplyProportionalToMesh.bl_idname, + text="Apply") + ops.src_density = sc.muv_world_scale_uv_src_density + ops.src_uv_area = sc.muv_world_scale_uv_src_uv_area + ops.src_mesh_area = sc.muv_world_scale_uv_src_mesh_area + ops.origin = sc.muv_world_scale_uv_origin + ops.show_dialog = False + + box = layout.box() + box.prop(sc, "muv_preserve_uv_aspect_enabled", + text="Preserve UV Aspect") + if sc.muv_preserve_uv_aspect_enabled: + row = box.row() + ops = row.operator( + preserve_uv_aspect.MUV_OT_PreserveUVAspect.bl_idname, + text="Change Image") + ops.dest_img_name = sc.muv_preserve_uv_aspect_tex_image + ops.origin = sc.muv_preserve_uv_aspect_origin + row.prop(sc, "muv_preserve_uv_aspect_tex_image", text="") + box.prop(sc, "muv_preserve_uv_aspect_origin", text="Origin") + + box = layout.box() + box.prop(sc, "muv_texture_lock_enabled", text="Texture Lock") + if sc.muv_texture_lock_enabled: + row = box.row(align=True) + col = row.column(align=True) + col.label("Normal Mode:") + col = row.column(align=True) + col.operator( + texture_lock.MUV_OT_TextureLock_Lock.bl_idname, + text="Lock" + if not texture_lock.MUV_OT_TextureLock_Lock.is_ready(context) + else "ReLock") + ops = col.operator( + texture_lock.MUV_OT_TextureLock_Unlock.bl_idname, + text="Unlock") + ops.connect = sc.muv_texture_lock_connect + col.prop(sc, "muv_texture_lock_connect", text="Connect") + + row = box.row(align=True) + row.label("Interactive Mode:") + box.prop( + sc, "muv_texture_lock_lock", + text="Unlock" + if texture_lock.MUV_OT_TextureLock_Intr.is_running(context) + else "Lock", + icon='RESTRICT_VIEW_OFF' + if texture_lock.MUV_OT_TextureLock_Intr.is_running(context) + else 'RESTRICT_VIEW_ON') + + box = layout.box() + box.prop(sc, "muv_texture_wrap_enabled", text="Texture Wrap") + if sc.muv_texture_wrap_enabled: + row = box.row(align=True) + row.operator(texture_wrap.MUV_OT_TextureWrap_Refer.bl_idname, + text="Refer") + row.operator(texture_wrap.MUV_OT_TextureWrap_Set.bl_idname, + text="Set") + box.prop(sc, "muv_texture_wrap_set_and_refer") + box.prop(sc, "muv_texture_wrap_selseq") + + box = layout.box() + box.prop(sc, "muv_uv_sculpt_enabled", text="UV Sculpt") + if sc.muv_uv_sculpt_enabled: + box.prop( + sc, "muv_uv_sculpt_enable", + text="Disable"if uv_sculpt.MUV_OT_UVSculpt.is_running(context) + else "Enable", + icon='RESTRICT_VIEW_OFF' + if uv_sculpt.MUV_OT_UVSculpt.is_running(context) + else 'RESTRICT_VIEW_ON') + col = box.column() + col.label("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/legacy/ui/view3d_uv_mapping.py b/uv_magic_uv/legacy/ui/view3d_uv_mapping.py new file mode 100644 index 00000000..3de86d0d --- /dev/null +++ b/uv_magic_uv/legacy/ui/view3d_uv_mapping.py @@ -0,0 +1,116 @@ +# + +# ##### 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 ( + uvw, + texture_projection, + unwrap_constraint, +) +from ..op.texture_projection import ( + MUV_OT_TextureProjection +) +from ...utils.bl_class_registry import BlClassRegistry + +__all__ = [ + 'MUV_PT_View3D_UVMapping', +] + + +@BlClassRegistry(legacy=True) +class MUV_PT_View3D_UVMapping(bpy.types.Panel): + """ + Panel class: UV Mapping on Property Panel on View3D + """ + + bl_space_type = 'VIEW_3D' + bl_region_type = 'TOOLS' + bl_label = "UV Mapping" + bl_category = "Magic UV" + bl_context = 'mesh_edit' + bl_options = {'DEFAULT_CLOSED'} + + def draw_header(self, _): + layout = self.layout + layout.label(text="", icon='IMAGE_COL') + + def draw(self, context): + sc = context.scene + layout = self.layout + + box = layout.box() + box.prop(sc, "muv_unwrap_constraint_enabled", text="Unwrap Constraint") + if sc.muv_unwrap_constraint_enabled: + ops = box.operator( + unwrap_constraint.MUV_OT_UnwrapConstraint.bl_idname, + text="Unwrap") + ops.u_const = sc.muv_unwrap_constraint_u_const + ops.v_const = sc.muv_unwrap_constraint_v_const + row = box.row(align=True) + row.prop(sc, "muv_unwrap_constraint_u_const", text="U-Constraint") + row.prop(sc, "muv_unwrap_constraint_v_const", text="V-Constraint") + + box = layout.box() + box.prop(sc, "muv_texture_projection_enabled", + text="Texture Projection") + if sc.muv_texture_projection_enabled: + row = box.row() + row.prop( + sc, "muv_texture_projection_enable", + text="Disable" + if MUV_OT_TextureProjection.is_running(context) + else "Enable", + icon='RESTRICT_VIEW_OFF' + if MUV_OT_TextureProjection.is_running(context) + else 'RESTRICT_VIEW_ON') + row.prop(sc, "muv_texture_projection_tex_image", text="") + box.prop(sc, "muv_texture_projection_tex_transparency", + text="Transparency") + col = box.column(align=True) + row = col.row() + row.prop(sc, "muv_texture_projection_adjust_window", + text="Adjust Window") + if not sc.muv_texture_projection_adjust_window: + row.prop(sc, "muv_texture_projection_tex_magnitude", + text="Magnitude") + col.prop(sc, "muv_texture_projection_apply_tex_aspect", + text="Texture Aspect Ratio") + col.prop(sc, "muv_texture_projection_assign_uvmap", + text="Assign UVMap") + box.operator( + texture_projection.MUV_OT_TextureProjection_Project.bl_idname, + text="Project") + + box = layout.box() + box.prop(sc, "muv_uvw_enabled", text="UVW") + if sc.muv_uvw_enabled: + row = box.row(align=True) + ops = row.operator(uvw.MUV_OT_UVW_BoxMap.bl_idname, text="Box") + ops.assign_uvmap = sc.muv_uvw_assign_uvmap + ops = row.operator(uvw.MUV_OT_UVW_BestPlanerMap.bl_idname, + text="Best Planner") + ops.assign_uvmap = sc.muv_uvw_assign_uvmap + box.prop(sc, "muv_uvw_assign_uvmap", text="Assign UVMap") diff --git a/uv_magic_uv/op/__init__.py b/uv_magic_uv/op/__init__.py index 9535b76d..2142c157 100644 --- a/uv_magic_uv/op/__init__.py +++ b/uv_magic_uv/op/__init__.py @@ -25,50 +25,22 @@ __date__ = "17 Nov 2018" if "bpy" in locals(): import importlib - importlib.reload(align_uv) - importlib.reload(align_uv_cursor) importlib.reload(copy_paste_uv) importlib.reload(copy_paste_uv_object) importlib.reload(copy_paste_uv_uvedit) importlib.reload(flip_rotate_uv) importlib.reload(mirror_uv) importlib.reload(move_uv) - importlib.reload(pack_uv) - importlib.reload(preserve_uv_aspect) - importlib.reload(select_uv) - importlib.reload(smooth_uv) - importlib.reload(texture_lock) - importlib.reload(texture_projection) - importlib.reload(texture_wrap) importlib.reload(transfer_uv) - importlib.reload(unwrap_constraint) - importlib.reload(uv_bounding_box) - importlib.reload(uv_inspection) - importlib.reload(uv_sculpt) importlib.reload(uvw) - importlib.reload(world_scale_uv) else: - from . import align_uv - from . import align_uv_cursor from . import copy_paste_uv from . import copy_paste_uv_object from . import copy_paste_uv_uvedit from . import flip_rotate_uv from . import mirror_uv from . import move_uv - from . import pack_uv - from . import preserve_uv_aspect - from . import select_uv - from . import smooth_uv - from . import texture_lock - from . import texture_projection - from . import texture_wrap from . import transfer_uv - from . import unwrap_constraint - from . import uv_bounding_box - from . import uv_inspection - from . import uv_sculpt from . import uvw - from . import world_scale_uv import bpy diff --git a/uv_magic_uv/op/align_uv.py b/uv_magic_uv/op/align_uv.py deleted file mode 100644 index 90168a56..00000000 --- a/uv_magic_uv/op/align_uv.py +++ /dev/null @@ -1,980 +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__ = "imdjs, Nutti " -__status__ = "production" -__version__ = "5.2" -__date__ = "17 Nov 2018" - -import math -from math import atan2, tan, sin, cos - -import bpy -import bmesh -from mathutils import Vector -from bpy.props import EnumProperty, BoolProperty, FloatProperty - -from .. import common - - -__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 - for l1, l2 in zip(loops[:-1], loops[1:]): - diff = l2.vert.co - l1.vert.co - length = length + abs(diff.length) - - return length - - -# get sum uv length of loop sequences -def get_loop_uv_len(loops, uv_layer): - length = 0 - for l1, l2 in zip(loops[:-1], loops[1:]): - diff = l2[uv_layer].uv - l1[uv_layer].uv - length = length + abs(diff.length) - - return length - - -# get center/radius of circle by 3 vertices -def get_circle(v): - alpha = atan2((v[0].y - v[1].y), (v[0].x - v[1].x)) + math.pi / 2 - beta = atan2((v[1].y - v[2].y), (v[1].x - v[2].x)) + math.pi / 2 - ex = (v[0].x + v[1].x) / 2.0 - ey = (v[0].y + v[1].y) / 2.0 - fx = (v[1].x + v[2].x) / 2.0 - fy = (v[1].y + v[2].y) / 2.0 - cx = (ey - fy - ex * tan(alpha) + fx * tan(beta)) / \ - (tan(beta) - tan(alpha)) - cy = ey - (ex - cx) * tan(alpha) - center = Vector((cx, cy)) - - r = v[0] - center - radian = r.length - - return center, radian - - -# get position on circle with same arc length -def calc_v_on_circle(v, center, radius): - base = v[0] - theta = atan2(base.y - center.y, base.x - center.x) - new_v = [] - for i in range(len(v)): - angle = theta + i * 2 * math.pi / len(v) - new_v.append(Vector((center.x + radius * sin(angle), - center.y + radius * cos(angle)))) - - return new_v - - -class 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_align_uv_operator_circle" - bl_label = "Align UV (Circle)" - bl_description = "Align UV coordinates to Circle" - bl_options = {'REGISTER', 'UNDO'} - - transmission = BoolProperty( - name="Transmission", - description="Align linked UVs", - default=False - ) - select = BoolProperty( - name="Select", - description="Select UVs which are aligned", - default=False - ) - - @classmethod - def poll(cls, context): - # we can not get area/space/region from console - if common.is_console_mode(): - return True - return is_valid_context(context) - - def execute(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - uv_layer = bm.loops.layers.uv.verify() - - # loop_seqs[horizontal][vertical][loop] - loop_seqs, error = common.get_loop_sequences(bm, uv_layer, True) - if not loop_seqs: - self.report({'WARNING'}, error) - return {'CANCELLED'} - - # get circle and new UVs - uvs = [hseq[0][0][uv_layer].uv.copy() for hseq in loop_seqs] - c, r = get_circle(uvs[0:3]) - new_uvs = calc_v_on_circle(uvs, c, r) - - # check center UV of circle - center = loop_seqs[0][-1][0].vert - for hseq in loop_seqs[1:]: - if len(hseq[-1]) != 1: - self.report({'WARNING'}, "Last face must be triangle") - return {'CANCELLED'} - if hseq[-1][0].vert != center: - self.report({'WARNING'}, "Center must be identical") - return {'CANCELLED'} - - # align to circle - if self.transmission: - for hidx, hseq in enumerate(loop_seqs): - for vidx, pair in enumerate(hseq): - all_ = int((len(hseq) + 1) / 2) - r = (all_ - int((vidx + 1) / 2)) / all_ - pair[0][uv_layer].uv = c + (new_uvs[hidx] - c) * r - if self.select: - pair[0][uv_layer].select = True - - if len(pair) < 2: - continue - # for quad polygon - next_hidx = (hidx + 1) % len(loop_seqs) - pair[1][uv_layer].uv = c + ((new_uvs[next_hidx]) - c) * r - if self.select: - pair[1][uv_layer].select = True - else: - for hidx, hseq in enumerate(loop_seqs): - pair = hseq[0] - pair[0][uv_layer].uv = new_uvs[hidx] - pair[1][uv_layer].uv = new_uvs[(hidx + 1) % len(loop_seqs)] - if self.select: - pair[0][uv_layer].select = True - pair[1][uv_layer].select = True - - bmesh.update_edit_mesh(obj.data) - - return {'FINISHED'} - - -# get accumulate vertex lengths of loop sequences -def get_loop_vert_accum_len(loops): - accum_lengths = [0.0] - length = 0 - for l1, l2 in zip(loops[:-1], loops[1:]): - diff = l2.vert.co - l1.vert.co - length = length + abs(diff.length) - accum_lengths.extend([length]) - - return accum_lengths - - -# get sum uv length of loop sequences -def get_loop_uv_accum_len(loops, uv_layer): - accum_lengths = [0.0] - length = 0 - for l1, l2 in zip(loops[:-1], loops[1:]): - diff = l2[uv_layer].uv - l1[uv_layer].uv - length = length + abs(diff.length) - accum_lengths.extend([length]) - - return accum_lengths - - -# get horizontal differential of UV influenced by mesh vertex -def get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl): - common.debug_print( - "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx)) - - base_uv = loop_seqs[0][vidx][0][uv_layer].uv.copy() - - # calculate original length - hloops = [] - for s in loop_seqs: - hloops.extend([s[vidx][0], s[vidx][1]]) - total_vlen = get_loop_vert_len(hloops) - accum_vlens = get_loop_vert_accum_len(hloops) - total_uvlen = get_loop_uv_len(hloops, uv_layer) - accum_uvlens = get_loop_uv_accum_len(hloops, uv_layer) - orig_uvs = [l[uv_layer].uv.copy() for l in hloops] - - # calculate target length - tgt_noinfl = total_uvlen * (hidx + pidx) / len(loop_seqs) - tgt_infl = total_uvlen * accum_vlens[hidx * 2 + pidx] / total_vlen - target_length = tgt_noinfl * (1 - infl) + tgt_infl * infl - common.debug_print(target_length) - common.debug_print(accum_uvlens) - - # calculate target UV - for i in range(len(accum_uvlens[:-1])): - # get line segment which UV will be placed - if ((accum_uvlens[i] <= target_length) and - (accum_uvlens[i + 1] > target_length)): - tgt_seg_len = target_length - accum_uvlens[i] - seg_len = accum_uvlens[i + 1] - accum_uvlens[i] - uv1 = orig_uvs[i] - uv2 = orig_uvs[i + 1] - target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len - break - elif i == (len(accum_uvlens[:-1]) - 1): - if accum_uvlens[i + 1] != target_length: - raise Exception( - "Internal Error: horizontal_target_length={}" - " is not equal to {}" - .format(target_length, accum_uvlens[-1])) - tgt_seg_len = target_length - accum_uvlens[i] - seg_len = accum_uvlens[i + 1] - accum_uvlens[i] - uv1 = orig_uvs[i] - uv2 = orig_uvs[i + 1] - target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len - break - else: - raise Exception("Internal Error: horizontal_target_length={}" - " is not in range {} to {}" - .format(target_length, accum_uvlens[0], - accum_uvlens[-1])) - - return target_uv - - -# --------------------- LOOP STRUCTURE ---------------------- -# -# loops[hidx][vidx][pidx] -# hidx: horizontal index -# vidx: vertical index -# pidx: pair index -# -# <----- horizontal -----> -# -# (hidx, vidx, pidx) = (0, 3, 0) -# | (hidx, vidx, pidx) = (1, 3, 0) -# v v -# ^ o --- oo --- o -# | | || | -# vertical | o --- oo --- o <- (hidx, vidx, pidx) -# | o --- oo --- o = (1, 2, 1) -# | | || | -# v o --- oo --- o -# ^ ^ -# | (hidx, vidx, pidx) = (1, 0, 1) -# (hidx, vidx, pidx) = (0, 0, 0) -# -# ----------------------------------------------------------- - - -# get vertical differential of UV influenced by mesh vertex -def get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, pidx, infl): - common.debug_print( - "loop_seqs[hidx={0}][vidx={1}][pidx={2}]".format(hidx, vidx, pidx)) - - base_uv = loop_seqs[hidx][0][pidx][uv_layer].uv.copy() - - # calculate original length - vloops = [] - for s in loop_seqs[hidx]: - vloops.append(s[pidx]) - total_vlen = get_loop_vert_len(vloops) - accum_vlens = get_loop_vert_accum_len(vloops) - total_uvlen = get_loop_uv_len(vloops, uv_layer) - accum_uvlens = get_loop_uv_accum_len(vloops, uv_layer) - orig_uvs = [l[uv_layer].uv.copy() for l in vloops] - - # calculate target length - tgt_noinfl = total_uvlen * int((vidx + 1) / 2) / len(loop_seqs) - tgt_infl = total_uvlen * accum_vlens[vidx] / total_vlen - target_length = tgt_noinfl * (1 - infl) + tgt_infl * infl - common.debug_print(target_length) - common.debug_print(accum_uvlens) - - # calculate target UV - for i in range(len(accum_uvlens[:-1])): - # get line segment which UV will be placed - if ((accum_uvlens[i] <= target_length) and - (accum_uvlens[i + 1] > target_length)): - tgt_seg_len = target_length - accum_uvlens[i] - seg_len = accum_uvlens[i + 1] - accum_uvlens[i] - uv1 = orig_uvs[i] - uv2 = orig_uvs[i + 1] - target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len - break - elif i == (len(accum_uvlens[:-1]) - 1): - if accum_uvlens[i + 1] != target_length: - raise Exception("Internal Error: horizontal_target_length={}" - " is not equal to {}" - .format(target_length, accum_uvlens[-1])) - tgt_seg_len = target_length - accum_uvlens[i] - seg_len = accum_uvlens[i + 1] - accum_uvlens[i] - uv1 = orig_uvs[i] - uv2 = orig_uvs[i + 1] - target_uv = (uv1 - base_uv) + (uv2 - uv1) * tgt_seg_len / seg_len - break - else: - raise Exception("Internal Error: horizontal_target_length={}" - " is not in range {} to {}" - .format(target_length, accum_uvlens[0], - accum_uvlens[-1])) - - return target_uv - - -# get horizontal differential of UV no influenced -def get_hdiff_uv(uv_layer, loop_seqs, hidx): - base_uv = loop_seqs[0][0][0][uv_layer].uv.copy() - h_uv = loop_seqs[-1][0][1][uv_layer].uv.copy() - base_uv - - return hidx * h_uv / len(loop_seqs) - - -# get vertical differential of UV no influenced -def get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx): - base_uv = loop_seqs[0][0][0][uv_layer].uv.copy() - v_uv = loop_seqs[0][-1][0][uv_layer].uv.copy() - base_uv - - hseq = loop_seqs[hidx] - return int((vidx + 1) / 2) * v_uv / (len(hseq) / 2) - - -class OperatorStraighten(bpy.types.Operator): - - bl_idname = "uv.muv_align_uv_operator_straighten" - bl_label = "Align UV (Straighten)" - bl_description = "Straighten UV coordinates" - bl_options = {'REGISTER', 'UNDO'} - - transmission = BoolProperty( - name="Transmission", - description="Align linked UVs", - default=False - ) - select = BoolProperty( - name="Select", - description="Select UVs which are aligned", - default=False - ) - vertical = BoolProperty( - name="Vert-Infl (Vertical)", - description="Align vertical direction influenced " - "by mesh vertex proportion", - default=False - ) - horizontal = BoolProperty( - name="Vert-Infl (Horizontal)", - description="Align horizontal direction influenced " - "by mesh vertex proportion", - default=False - ) - mesh_infl = FloatProperty( - name="Mesh Influence", - description="Influence rate of mesh vertex", - min=0.0, - max=1.0, - default=0.0 - ) - - @classmethod - def poll(cls, context): - # we can not get area/space/region from console - if common.is_console_mode(): - return True - return is_valid_context(context) - - # selected and paralleled UV loop sequence will be aligned - def __align_w_transmission(self, loop_seqs, uv_layer): - base_uv = loop_seqs[0][0][0][uv_layer].uv.copy() - - # calculate diff UVs - diff_uvs = [] - # hseq[vertical][loop] - for hidx, hseq in enumerate(loop_seqs): - # pair[loop] - diffs = [] - for vidx in range(0, len(hseq), 2): - if self.horizontal: - hdiff_uvs = [ - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0, - self.mesh_infl), - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1, - self.mesh_infl), - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 0, self.mesh_infl), - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 1, self.mesh_infl), - ] - else: - hdiff_uvs = [ - get_hdiff_uv(uv_layer, loop_seqs, hidx), - get_hdiff_uv(uv_layer, loop_seqs, hidx + 1), - get_hdiff_uv(uv_layer, loop_seqs, hidx), - get_hdiff_uv(uv_layer, loop_seqs, hidx + 1) - ] - if self.vertical: - vdiff_uvs = [ - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0, - self.mesh_infl), - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1, - self.mesh_infl), - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 0, self.mesh_infl), - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 1, self.mesh_infl), - ] - else: - vdiff_uvs = [ - get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx), - get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx), - get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx), - get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx) - ] - diffs.append([hdiff_uvs, vdiff_uvs]) - diff_uvs.append(diffs) - - # update UV - for hseq, diffs in zip(loop_seqs, diff_uvs): - for vidx in range(0, len(hseq), 2): - loops = [ - hseq[vidx][0], hseq[vidx][1], - hseq[vidx + 1][0], hseq[vidx + 1][1] - ] - for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0], - diffs[int(vidx / 2)][1]): - l[uv_layer].uv = base_uv + hdiff + vdiff - if self.select: - l[uv_layer].select = True - - # only selected UV loop sequence will be aligned - def __align_wo_transmission(self, loop_seqs, uv_layer): - base_uv = loop_seqs[0][0][0][uv_layer].uv.copy() - - h_uv = loop_seqs[-1][0][1][uv_layer].uv.copy() - base_uv - for hidx, hseq in enumerate(loop_seqs): - # only selected loop pair is targeted - pair = hseq[0] - hdiff_uv_0 = hidx * h_uv / len(loop_seqs) - hdiff_uv_1 = (hidx + 1) * h_uv / len(loop_seqs) - pair[0][uv_layer].uv = base_uv + hdiff_uv_0 - pair[1][uv_layer].uv = base_uv + hdiff_uv_1 - if self.select: - pair[0][uv_layer].select = True - pair[1][uv_layer].select = True - - def __align(self, loop_seqs, uv_layer): - if self.transmission: - self.__align_w_transmission(loop_seqs, uv_layer) - else: - self.__align_wo_transmission(loop_seqs, uv_layer) - - def execute(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - uv_layer = bm.loops.layers.uv.verify() - - # loop_seqs[horizontal][vertical][loop] - loop_seqs, error = common.get_loop_sequences(bm, uv_layer) - if not loop_seqs: - self.report({'WARNING'}, error) - return {'CANCELLED'} - - # align - self.__align(loop_seqs, uv_layer) - - bmesh.update_edit_mesh(obj.data) - - return {'FINISHED'} - - -class OperatorAxis(bpy.types.Operator): - - bl_idname = "uv.muv_align_uv_operator_axis" - bl_label = "Align UV (XY-Axis)" - bl_description = "Align UV to XY-axis" - bl_options = {'REGISTER', 'UNDO'} - - transmission = BoolProperty( - name="Transmission", - description="Align linked UVs", - default=False - ) - select = BoolProperty( - name="Select", - description="Select UVs which are aligned", - default=False - ) - vertical = BoolProperty( - name="Vert-Infl (Vertical)", - description="Align vertical direction influenced " - "by mesh vertex proportion", - default=False - ) - horizontal = BoolProperty( - name="Vert-Infl (Horizontal)", - description="Align horizontal direction influenced " - "by mesh vertex proportion", - default=False - ) - location = EnumProperty( - name="Location", - description="Align location", - items=[ - ('LEFT_TOP', "Left/Top", "Align to Left or Top"), - ('MIDDLE', "Middle", "Align to middle"), - ('RIGHT_BOTTOM', "Right/Bottom", "Align to Right or Bottom") - ], - default='MIDDLE' - ) - mesh_infl = FloatProperty( - name="Mesh Influence", - description="Influence rate of mesh vertex", - min=0.0, - max=1.0, - default=0.0 - ) - - @classmethod - def poll(cls, context): - # we can not get area/space/region from console - if common.is_console_mode(): - return True - return is_valid_context(context) - - # get min/max of UV - def __get_uv_max_min(self, loop_seqs, uv_layer): - uv_max = Vector((-1000000.0, -1000000.0)) - uv_min = Vector((1000000.0, 1000000.0)) - for hseq in loop_seqs: - for l in hseq[0]: - uv = l[uv_layer].uv - uv_max.x = max(uv.x, uv_max.x) - uv_max.y = max(uv.y, uv_max.y) - uv_min.x = min(uv.x, uv_min.x) - uv_min.y = min(uv.y, uv_min.y) - - return uv_max, uv_min - - # get UV differentiation when UVs are aligned to X-axis - def __get_x_axis_align_diff_uvs(self, loop_seqs, uv_layer, uv_min, - width, height): - diff_uvs = [] - for hidx, hseq in enumerate(loop_seqs): - pair = hseq[0] - luv0 = pair[0][uv_layer] - luv1 = pair[1][uv_layer] - target_uv0 = Vector((0.0, 0.0)) - target_uv1 = Vector((0.0, 0.0)) - if self.location == 'RIGHT_BOTTOM': - target_uv0.y = target_uv1.y = uv_min.y - elif self.location == 'MIDDLE': - target_uv0.y = target_uv1.y = uv_min.y + height * 0.5 - elif self.location == 'LEFT_TOP': - target_uv0.y = target_uv1.y = uv_min.y + height - if luv0.uv.x < luv1.uv.x: - target_uv0.x = uv_min.x + hidx * width / len(loop_seqs) - target_uv1.x = uv_min.x + (hidx + 1) * width / len(loop_seqs) - else: - target_uv0.x = uv_min.x + (hidx + 1) * width / len(loop_seqs) - target_uv1.x = uv_min.x + hidx * width / len(loop_seqs) - diff_uvs.append([target_uv0 - luv0.uv, target_uv1 - luv1.uv]) - - return diff_uvs - - # get UV differentiation when UVs are aligned to Y-axis - def __get_y_axis_align_diff_uvs(self, loop_seqs, uv_layer, uv_min, - width, height): - diff_uvs = [] - for hidx, hseq in enumerate(loop_seqs): - pair = hseq[0] - luv0 = pair[0][uv_layer] - luv1 = pair[1][uv_layer] - target_uv0 = Vector((0.0, 0.0)) - target_uv1 = Vector((0.0, 0.0)) - if self.location == 'RIGHT_BOTTOM': - target_uv0.x = target_uv1.x = uv_min.x + width - elif self.location == 'MIDDLE': - target_uv0.x = target_uv1.x = uv_min.x + width * 0.5 - elif self.location == 'LEFT_TOP': - target_uv0.x = target_uv1.x = uv_min.x - if luv0.uv.y < luv1.uv.y: - target_uv0.y = uv_min.y + hidx * height / len(loop_seqs) - target_uv1.y = uv_min.y + (hidx + 1) * height / len(loop_seqs) - else: - target_uv0.y = uv_min.y + (hidx + 1) * height / len(loop_seqs) - target_uv1.y = uv_min.y + hidx * height / len(loop_seqs) - diff_uvs.append([target_uv0 - luv0.uv, target_uv1 - luv1.uv]) - - return diff_uvs - - # only selected UV loop sequence will be aligned along to X-axis - def __align_to_x_axis_wo_transmission(self, loop_seqs, uv_layer, - uv_min, width, height): - # reverse if the UV coordinate is not sorted by position - need_revese = loop_seqs[0][0][0][uv_layer].uv.x > \ - loop_seqs[-1][0][0][uv_layer].uv.x - if need_revese: - loop_seqs.reverse() - for hidx, hseq in enumerate(loop_seqs): - for vidx, pair in enumerate(hseq): - tmp = loop_seqs[hidx][vidx][0] - loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1] - loop_seqs[hidx][vidx][1] = tmp - - # get UV differential - diff_uvs = self.__get_x_axis_align_diff_uvs(loop_seqs, uv_layer, - uv_min, width, height) - - # update UV - for hseq, duv in zip(loop_seqs, diff_uvs): - pair = hseq[0] - luv0 = pair[0][uv_layer] - luv1 = pair[1][uv_layer] - luv0.uv = luv0.uv + duv[0] - luv1.uv = luv1.uv + duv[1] - - # only selected UV loop sequence will be aligned along to Y-axis - def __align_to_y_axis_wo_transmission(self, loop_seqs, uv_layer, - uv_min, width, height): - # reverse if the UV coordinate is not sorted by position - need_revese = loop_seqs[0][0][0][uv_layer].uv.y > \ - loop_seqs[-1][0][0][uv_layer].uv.y - if need_revese: - loop_seqs.reverse() - for hidx, hseq in enumerate(loop_seqs): - for vidx, pair in enumerate(hseq): - tmp = loop_seqs[hidx][vidx][0] - loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1] - loop_seqs[hidx][vidx][1] = tmp - - # get UV differential - diff_uvs = self.__get_y_axis_align_diff_uvs(loop_seqs, uv_layer, - uv_min, width, height) - - # update UV - for hseq, duv in zip(loop_seqs, diff_uvs): - pair = hseq[0] - luv0 = pair[0][uv_layer] - luv1 = pair[1][uv_layer] - luv0.uv = luv0.uv + duv[0] - luv1.uv = luv1.uv + duv[1] - - # selected and paralleled UV loop sequence will be aligned along to X-axis - def __align_to_x_axis_w_transmission(self, loop_seqs, uv_layer, - uv_min, width, height): - # reverse if the UV coordinate is not sorted by position - need_revese = loop_seqs[0][0][0][uv_layer].uv.x > \ - loop_seqs[-1][0][0][uv_layer].uv.x - if need_revese: - loop_seqs.reverse() - for hidx, hseq in enumerate(loop_seqs): - for vidx in range(len(hseq)): - tmp = loop_seqs[hidx][vidx][0] - loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1] - loop_seqs[hidx][vidx][1] = tmp - - # get offset UVs when the UVs are aligned to X-axis - align_diff_uvs = self.__get_x_axis_align_diff_uvs(loop_seqs, uv_layer, - uv_min, width, - height) - base_uv = loop_seqs[0][0][0][uv_layer].uv.copy() - offset_uvs = [] - for hseq, aduv in zip(loop_seqs, align_diff_uvs): - luv0 = hseq[0][0][uv_layer] - luv1 = hseq[0][1][uv_layer] - offset_uvs.append([luv0.uv + aduv[0] - base_uv, - luv1.uv + aduv[1] - base_uv]) - - # get UV differential - diff_uvs = [] - # hseq[vertical][loop] - for hidx, hseq in enumerate(loop_seqs): - # pair[loop] - diffs = [] - for vidx in range(0, len(hseq), 2): - if self.horizontal: - hdiff_uvs = [ - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0, - self.mesh_infl), - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1, - self.mesh_infl), - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 0, self.mesh_infl), - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 1, self.mesh_infl), - ] - hdiff_uvs[0].y = hdiff_uvs[0].y + offset_uvs[hidx][0].y - hdiff_uvs[1].y = hdiff_uvs[1].y + offset_uvs[hidx][1].y - hdiff_uvs[2].y = hdiff_uvs[2].y + offset_uvs[hidx][0].y - hdiff_uvs[3].y = hdiff_uvs[3].y + offset_uvs[hidx][1].y - else: - hdiff_uvs = [ - offset_uvs[hidx][0], - offset_uvs[hidx][1], - offset_uvs[hidx][0], - offset_uvs[hidx][1], - ] - if self.vertical: - vdiff_uvs = [ - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0, - self.mesh_infl), - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1, - self.mesh_infl), - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 0, self.mesh_infl), - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 1, self.mesh_infl), - ] - else: - vdiff_uvs = [ - get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx), - get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx), - get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx), - get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx) - ] - diffs.append([hdiff_uvs, vdiff_uvs]) - diff_uvs.append(diffs) - - # update UV - for hseq, diffs in zip(loop_seqs, diff_uvs): - for vidx in range(0, len(hseq), 2): - loops = [ - hseq[vidx][0], hseq[vidx][1], - hseq[vidx + 1][0], hseq[vidx + 1][1] - ] - for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0], - diffs[int(vidx / 2)][1]): - l[uv_layer].uv = base_uv + hdiff + vdiff - if self.select: - l[uv_layer].select = True - - # selected and paralleled UV loop sequence will be aligned along to Y-axis - def __align_to_y_axis_w_transmission(self, loop_seqs, uv_layer, - uv_min, width, height): - # reverse if the UV coordinate is not sorted by position - need_revese = loop_seqs[0][0][0][uv_layer].uv.y > \ - loop_seqs[-1][0][-1][uv_layer].uv.y - if need_revese: - loop_seqs.reverse() - for hidx, hseq in enumerate(loop_seqs): - for vidx in range(len(hseq)): - tmp = loop_seqs[hidx][vidx][0] - loop_seqs[hidx][vidx][0] = loop_seqs[hidx][vidx][1] - loop_seqs[hidx][vidx][1] = tmp - - # get offset UVs when the UVs are aligned to Y-axis - align_diff_uvs = self.__get_y_axis_align_diff_uvs(loop_seqs, uv_layer, - uv_min, width, - height) - base_uv = loop_seqs[0][0][0][uv_layer].uv.copy() - offset_uvs = [] - for hseq, aduv in zip(loop_seqs, align_diff_uvs): - luv0 = hseq[0][0][uv_layer] - luv1 = hseq[0][1][uv_layer] - offset_uvs.append([luv0.uv + aduv[0] - base_uv, - luv1.uv + aduv[1] - base_uv]) - - # get UV differential - diff_uvs = [] - # hseq[vertical][loop] - for hidx, hseq in enumerate(loop_seqs): - # pair[loop] - diffs = [] - for vidx in range(0, len(hseq), 2): - if self.horizontal: - hdiff_uvs = [ - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0, - self.mesh_infl), - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1, - self.mesh_infl), - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 0, self.mesh_infl), - get_hdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 1, self.mesh_infl), - ] - hdiff_uvs[0].x = hdiff_uvs[0].x + offset_uvs[hidx][0].x - hdiff_uvs[1].x = hdiff_uvs[1].x + offset_uvs[hidx][1].x - hdiff_uvs[2].x = hdiff_uvs[2].x + offset_uvs[hidx][0].x - hdiff_uvs[3].x = hdiff_uvs[3].x + offset_uvs[hidx][1].x - else: - hdiff_uvs = [ - offset_uvs[hidx][0], - offset_uvs[hidx][1], - offset_uvs[hidx][0], - offset_uvs[hidx][1], - ] - if self.vertical: - vdiff_uvs = [ - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 0, - self.mesh_infl), - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx, hidx, 1, - self.mesh_infl), - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 0, self.mesh_infl), - get_vdiff_uv_vinfl(uv_layer, loop_seqs, vidx + 1, - hidx, 1, self.mesh_infl), - ] - else: - vdiff_uvs = [ - get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx), - get_vdiff_uv(uv_layer, loop_seqs, vidx, hidx), - get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx), - get_vdiff_uv(uv_layer, loop_seqs, vidx + 1, hidx) - ] - diffs.append([hdiff_uvs, vdiff_uvs]) - diff_uvs.append(diffs) - - # update UV - for hseq, diffs in zip(loop_seqs, diff_uvs): - for vidx in range(0, len(hseq), 2): - loops = [ - hseq[vidx][0], hseq[vidx][1], - hseq[vidx + 1][0], hseq[vidx + 1][1] - ] - for l, hdiff, vdiff in zip(loops, diffs[int(vidx / 2)][0], - diffs[int(vidx / 2)][1]): - l[uv_layer].uv = base_uv + hdiff + vdiff - if self.select: - l[uv_layer].select = True - - def __align(self, loop_seqs, uv_layer, uv_min, width, height): - # align along to x-axis - if width > height: - if self.transmission: - self.__align_to_x_axis_w_transmission(loop_seqs, uv_layer, - uv_min, width, height) - else: - self.__align_to_x_axis_wo_transmission(loop_seqs, uv_layer, - uv_min, width, height) - # align along to y-axis - else: - if self.transmission: - self.__align_to_y_axis_w_transmission(loop_seqs, uv_layer, - uv_min, width, height) - else: - self.__align_to_y_axis_wo_transmission(loop_seqs, uv_layer, - uv_min, width, height) - - def execute(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - uv_layer = bm.loops.layers.uv.verify() - - # loop_seqs[horizontal][vertical][loop] - loop_seqs, error = common.get_loop_sequences(bm, uv_layer) - if not loop_seqs: - self.report({'WARNING'}, error) - return {'CANCELLED'} - - # get height and width - uv_max, uv_min = self.__get_uv_max_min(loop_seqs, uv_layer) - width = uv_max.x - uv_min.x - height = uv_max.y - uv_min.y - - self.__align(loop_seqs, uv_layer, uv_min, width, height) - - bmesh.update_edit_mesh(obj.data) - - return {'FINISHED'} diff --git a/uv_magic_uv/op/align_uv_cursor.py b/uv_magic_uv/op/align_uv_cursor.py deleted file mode 100644 index d787bde9..00000000 --- a/uv_magic_uv/op/align_uv_cursor.py +++ /dev/null @@ -1,251 +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.2" -__date__ = "17 Nov 2018" - -import bpy -from mathutils import Vector -from bpy.props import EnumProperty, BoolProperty, FloatVectorProperty -import bmesh - -from .. import common - - -__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'} - - position = EnumProperty( - items=( - ('CENTER', "Center", "Align to Center"), - ('LEFT_TOP', "Left Top", "Align to Left Top"), - ('LEFT_MIDDLE', "Left Middle", "Align to Left Middle"), - ('LEFT_BOTTOM', "Left Bottom", "Align to Left Bottom"), - ('MIDDLE_TOP', "Middle Top", "Align to Middle Top"), - ('MIDDLE_BOTTOM', "Middle Bottom", "Align to Middle Bottom"), - ('RIGHT_TOP', "Right Top", "Align to Right Top"), - ('RIGHT_MIDDLE', "Right Middle", "Align to Right Middle"), - ('RIGHT_BOTTOM', "Right Bottom", "Align to Right Bottom") - ), - name="Position", - description="Align position", - default='CENTER' - ) - base = EnumProperty( - items=( - ('TEXTURE', "Texture", "Align based on Texture"), - ('UV', "UV", "Align to UV"), - ('UV_SEL', "UV (Selected)", "Align to Selected UV") - ), - name="Base", - description="Align base", - default='TEXTURE' - ) - - @classmethod - def poll(cls, context): - # we can not get area/space/region from console - if common.is_console_mode(): - return True - return is_valid_context(context) - - def execute(self, context): - area, _, space = common.get_space('IMAGE_EDITOR', 'WINDOW', - 'IMAGE_EDITOR') - bd_size = common.get_uvimg_editor_board_size(area) - - if self.base == 'UV': - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if not bm.loops.layers.uv: - return None - uv_layer = bm.loops.layers.uv.verify() - - max_ = Vector((-10000000.0, -10000000.0)) - min_ = Vector((10000000.0, 10000000.0)) - for f in bm.faces: - if not f.select: - continue - for l in f.loops: - uv = l[uv_layer].uv - max_.x = max(max_.x, uv.x) - max_.y = max(max_.y, uv.y) - min_.x = min(min_.x, uv.x) - min_.y = min(min_.y, uv.y) - center = Vector(((max_.x + min_.x) / 2.0, (max_.y + min_.y) / 2.0)) - - elif self.base == 'UV_SEL': - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if not bm.loops.layers.uv: - return None - uv_layer = bm.loops.layers.uv.verify() - - max_ = Vector((-10000000.0, -10000000.0)) - min_ = Vector((10000000.0, 10000000.0)) - for f in bm.faces: - if not f.select: - continue - for l in f.loops: - if not l[uv_layer].select: - continue - uv = l[uv_layer].uv - max_.x = max(max_.x, uv.x) - max_.y = max(max_.y, uv.y) - min_.x = min(min_.x, uv.x) - min_.y = min(min_.y, uv.y) - center = Vector(((max_.x + min_.x) / 2.0, (max_.y + min_.y) / 2.0)) - - elif self.base == 'TEXTURE': - min_ = Vector((0.0, 0.0)) - max_ = Vector((1.0, 1.0)) - center = Vector((0.5, 0.5)) - else: - self.report({'ERROR'}, "Unknown Operation") - - if self.position == 'CENTER': - cx = center.x * bd_size[0] - cy = center.y * bd_size[1] - elif self.position == 'LEFT_TOP': - cx = min_.x * bd_size[0] - cy = max_.y * bd_size[1] - elif self.position == 'LEFT_MIDDLE': - cx = min_.x * bd_size[0] - cy = center.y * bd_size[1] - elif self.position == 'LEFT_BOTTOM': - cx = min_.x * bd_size[0] - cy = min_.y * bd_size[1] - elif self.position == 'MIDDLE_TOP': - cx = center.x * bd_size[0] - cy = max_.y * bd_size[1] - elif self.position == 'MIDDLE_BOTTOM': - cx = center.x * bd_size[0] - cy = min_.y * bd_size[1] - elif self.position == 'RIGHT_TOP': - cx = max_.x * bd_size[0] - cy = max_.y * bd_size[1] - elif self.position == 'RIGHT_MIDDLE': - cx = max_.x * bd_size[0] - cy = center.y * bd_size[1] - elif self.position == 'RIGHT_BOTTOM': - cx = max_.x * bd_size[0] - cy = min_.y * bd_size[1] - else: - self.report({'ERROR'}, "Unknown Operation") - - space.cursor_location = Vector((cx, cy)) - - return {'FINISHED'} diff --git a/uv_magic_uv/op/copy_paste_uv.py b/uv_magic_uv/op/copy_paste_uv.py index cc1baa30..23bc8343 100644 --- a/uv_magic_uv/op/copy_paste_uv.py +++ b/uv_magic_uv/op/copy_paste_uv.py @@ -18,14 +18,14 @@ # # ##### END GPL LICENSE BLOCK ##### -__author__ = "imdjs, Nutti " +__author__ = "Nutti , Jace Priester" __status__ = "production" __version__ = "5.2" __date__ = "17 Nov 2018" -import bpy import bmesh +import bpy.utils from bpy.props import ( StringProperty, BoolProperty, @@ -33,151 +33,28 @@ from bpy.props import ( EnumProperty, ) +from ..impl import copy_paste_uv_impl as impl from .. import common - +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry __all__ = [ 'Properties', - 'OpeartorCopyUV', - 'MenuCopyUV', - 'OperatorPasteUV', - 'MenuPasteUV', - 'OperatorSelSeqCopyUV', - 'MenuSelSeqCopyUV', - 'OperatorSelSeqPasteUV', - 'MenuSelSeqPasteUV', + 'MUV_OT_CopyPasteUV_CopyUV', + 'MUV_MT_CopyPasteUV_CopyUV', + 'MUV_OT_CopyPasteUV_PasteUV', + 'MUV_MT_CopyPasteUV_PasteUV', + 'MUV_OT_CopyPasteUV_SelSeqCopyUV', + 'MUV_MT_CopyPasteUV_SelSeqCopyUV', + 'MUV_OT_CopyPasteUV_SelSeqPasteUV', + 'MUV_MT_CopyPasteUV_SelSeqPasteUV', ] -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 - - +@PropertyClassRegistry() class Properties: + idname = "copy_paste_uv" + @classmethod def init_props(cls, scene): class Props(): @@ -225,7 +102,8 @@ class Properties: del scene.muv_copy_paste_uv_strategy -class OpeartorCopyUV(bpy.types.Operator): +@BlClassRegistry() +class MUV_OT_CopyPasteUV_CopyUV(bpy.types.Operator): """ Operation class: Copy UV coordinate """ @@ -235,14 +113,14 @@ class OpeartorCopyUV(bpy.types.Operator): bl_description = "Copy UV coordinate" bl_options = {'REGISTER', 'UNDO'} - uv_map = StringProperty(default="__default", 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) + return impl.is_valid_context(context) def execute(self, context): props = context.scene.muv_props.copy_paste_uv @@ -250,34 +128,24 @@ class OpeartorCopyUV(bpy.types.Operator): bm = common.create_bmesh(obj) # get UV layer - uv_layers = get_copy_uv_layers(self, bm) + uv_layers = impl.get_copy_uv_layers(self, bm, self.uv_map) if not uv_layers: return {'CANCELLED'} # get selected face - 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]) + src_info = impl.get_src_face_info(self, bm, uv_layers) + if src_info is None: + return {'CANCELLED'} + props.src_info = src_info + + face_count = len(props.src_info[list(props.src_info.keys())[0]]) self.report({'INFO'}, "{} face(s) are copied".format(face_count)) return {'FINISHED'} -class MenuCopyUV(bpy.types.Menu): +@BlClassRegistry() +class MUV_MT_CopyPasteUV_CopyUV(bpy.types.Menu): """ Menu class: Copy UV coordinate """ @@ -288,7 +156,7 @@ class MenuCopyUV(bpy.types.Menu): @classmethod def poll(cls, context): - return is_valid_context(context) + return impl.is_valid_context(context) def draw(self, context): layout = self.layout @@ -297,18 +165,21 @@ class MenuCopyUV(bpy.types.Menu): bm = common.create_bmesh(obj) uv_maps = bm.loops.layers.uv.keys() - ops = layout.operator(OpeartorCopyUV.bl_idname, text="[Default]") + ops = layout.operator(MUV_OT_CopyPasteUV_CopyUV.bl_idname, + text="[Default]") ops.uv_map = "__default" - ops = layout.operator(OpeartorCopyUV.bl_idname, text="[All]") + ops = layout.operator(MUV_OT_CopyPasteUV_CopyUV.bl_idname, + text="[All]") ops.uv_map = "__all" for m in uv_maps: - ops = layout.operator(OpeartorCopyUV.bl_idname, text=m) + ops = layout.operator(MUV_OT_CopyPasteUV_CopyUV.bl_idname, text=m) ops.uv_map = m -class OperatorPasteUV(bpy.types.Operator): +@BlClassRegistry() +class MUV_OT_CopyPasteUV_PasteUV(bpy.types.Operator): """ Operation class: Paste UV coordinate """ @@ -318,8 +189,8 @@ class OperatorPasteUV(bpy.types.Operator): bl_description = "Paste UV coordinate" bl_options = {'REGISTER', 'UNDO'} - uv_map = StringProperty(default="__default", options={'HIDDEN'}) - strategy = EnumProperty( + uv_map: StringProperty(default="__default", options={'HIDDEN'}) + strategy: EnumProperty( name="Strategy", description="Paste Strategy", items=[ @@ -328,18 +199,18 @@ class OperatorPasteUV(bpy.types.Operator): ], default="N_M" ) - flip_copied_uv = BoolProperty( + flip_copied_uv: BoolProperty( name="Flip Copied UV", description="Flip Copied UV...", default=False ) - rotate_copied_uv = IntProperty( + rotate_copied_uv: IntProperty( default=0, name="Rotate Copied UV", min=0, max=30 ) - copy_seams = BoolProperty( + copy_seams: BoolProperty( name="Seams", description="Copy Seams", default=True @@ -354,7 +225,7 @@ class OperatorPasteUV(bpy.types.Operator): props = sc.muv_props.copy_paste_uv if not props.src_info: return False - return is_valid_context(context) + return impl.is_valid_context(context) def execute(self, context): props = context.scene.muv_props.copy_paste_uv @@ -365,53 +236,34 @@ class OperatorPasteUV(bpy.types.Operator): bm = common.create_bmesh(obj) # get UV layer - uv_layers = get_paste_uv_layers(self, obj, bm, props.src_info) + uv_layers = impl.get_paste_uv_layers(self, obj, bm, props.src_info, + self.uv_map) 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'}, - "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 + dest_info = impl.get_dest_face_info(self, bm, uv_layers, + props.src_info, self.strategy) + if dest_info is None: + return {'CANCELLED'} # paste - ret = paste_uv(self, bm, props.src_info, dest_info, uv_layers, - self.strategy, self.flip_copied_uv, - self.rotate_copied_uv, self.copy_seams) + ret = impl.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'}, "{} face(s) are pasted".format(dest_face_count)) + face_count = len(props.src_info[list(dest_info.keys())[0]]) + self.report({'INFO'}, "{} face(s) are pasted".format(face_count)) bmesh.update_edit_mesh(obj.data) - if self.copy_seams is True: - obj.data.show_edge_seams = True return {'FINISHED'} -class MenuPasteUV(bpy.types.Menu): +@BlClassRegistry() +class MUV_MT_CopyPasteUV_PasteUV(bpy.types.Menu): """ Menu class: Paste UV coordinate """ @@ -426,7 +278,7 @@ class MenuPasteUV(bpy.types.Menu): props = sc.muv_props.copy_paste_uv if not props.src_info: return False - return is_valid_context(context) + return impl.is_valid_context(context) def draw(self, context): sc = context.scene @@ -436,29 +288,33 @@ class MenuPasteUV(bpy.types.Menu): bm = common.create_bmesh(obj) uv_maps = bm.loops.layers.uv.keys() - ops = layout.operator(OperatorPasteUV.bl_idname, text="[Default]") + ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname, + text="[Default]") ops.uv_map = "__default" ops.copy_seams = sc.muv_copy_paste_uv_copy_seams ops.strategy = sc.muv_copy_paste_uv_strategy - ops = layout.operator(OperatorPasteUV.bl_idname, text="[New]") + ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname, + text="[New]") ops.uv_map = "__new" ops.copy_seams = sc.muv_copy_paste_uv_copy_seams ops.strategy = sc.muv_copy_paste_uv_strategy - ops = layout.operator(OperatorPasteUV.bl_idname, text="[All]") + ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname, + text="[All]") ops.uv_map = "__all" ops.copy_seams = sc.muv_copy_paste_uv_copy_seams ops.strategy = sc.muv_copy_paste_uv_strategy for m in uv_maps: - ops = layout.operator(OperatorPasteUV.bl_idname, text=m) + ops = layout.operator(MUV_OT_CopyPasteUV_PasteUV.bl_idname, text=m) ops.uv_map = m ops.copy_seams = sc.muv_copy_paste_uv_copy_seams ops.strategy = sc.muv_copy_paste_uv_strategy -class OperatorSelSeqCopyUV(bpy.types.Operator): +@BlClassRegistry() +class MUV_OT_CopyPasteUV_SelSeqCopyUV(bpy.types.Operator): """ Operation class: Copy UV coordinate by selection sequence """ @@ -468,14 +324,14 @@ class OperatorSelSeqCopyUV(bpy.types.Operator): bl_description = "Copy UV data by selection sequence" bl_options = {'REGISTER', 'UNDO'} - uv_map = StringProperty(default="__default", 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) + return impl.is_valid_context(context) def execute(self, context): props = context.scene.muv_props.copy_paste_uv_selseq @@ -483,34 +339,24 @@ class OperatorSelSeqCopyUV(bpy.types.Operator): bm = common.create_bmesh(obj) # get UV layer - uv_layers = get_copy_uv_layers(self, bm) + uv_layers = impl.get_copy_uv_layers(self, bm, self.uv_map) if not uv_layers: return {'CANCELLED'} # get selected face - 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]) + src_info = impl.get_select_history_src_face_info(self, bm, uv_layers) + if src_info is None: + return {'CANCELLED'} + props.src_info = src_info + + face_count = len(props.src_info[list(props.src_info.keys())[0]]) self.report({'INFO'}, "{} face(s) are selected".format(face_count)) return {'FINISHED'} -class MenuSelSeqCopyUV(bpy.types.Menu): +@BlClassRegistry() +class MUV_MT_CopyPasteUV_SelSeqCopyUV(bpy.types.Menu): """ Menu class: Copy UV coordinate by selection sequence """ @@ -521,7 +367,7 @@ class MenuSelSeqCopyUV(bpy.types.Menu): @classmethod def poll(cls, context): - return is_valid_context(context) + return impl.is_valid_context(context) def draw(self, context): layout = self.layout @@ -529,18 +375,22 @@ class MenuSelSeqCopyUV(bpy.types.Menu): bm = common.create_bmesh(obj) uv_maps = bm.loops.layers.uv.keys() - ops = layout.operator(OperatorSelSeqCopyUV.bl_idname, text="[Default]") + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV.bl_idname, + text="[Default]") ops.uv_map = "__default" - ops = layout.operator(OperatorSelSeqCopyUV.bl_idname, text="[All]") + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV.bl_idname, + text="[All]") ops.uv_map = "__all" for m in uv_maps: - ops = layout.operator(OperatorSelSeqCopyUV.bl_idname, text=m) + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqCopyUV.bl_idname, + text=m) ops.uv_map = m -class OperatorSelSeqPasteUV(bpy.types.Operator): +@BlClassRegistry() +class MUV_OT_CopyPasteUV_SelSeqPasteUV(bpy.types.Operator): """ Operation class: Paste UV coordinate by selection sequence """ @@ -550,8 +400,8 @@ class OperatorSelSeqPasteUV(bpy.types.Operator): bl_description = "Paste UV coordinate by selection sequence" bl_options = {'REGISTER', 'UNDO'} - uv_map = StringProperty(default="__default", options={'HIDDEN'}) - strategy = EnumProperty( + uv_map: StringProperty(default="__default", options={'HIDDEN'}) + strategy: EnumProperty( name="Strategy", description="Paste Strategy", items=[ @@ -560,18 +410,18 @@ class OperatorSelSeqPasteUV(bpy.types.Operator): ], default="N_M" ) - flip_copied_uv = BoolProperty( + flip_copied_uv: BoolProperty( name="Flip Copied UV", description="Flip Copied UV...", default=False ) - rotate_copied_uv = IntProperty( + rotate_copied_uv: IntProperty( default=0, name="Rotate Copied UV", min=0, max=30 ) - copy_seams = BoolProperty( + copy_seams: BoolProperty( name="Seams", description="Copy Seams", default=True @@ -586,7 +436,7 @@ class OperatorSelSeqPasteUV(bpy.types.Operator): props = sc.muv_props.copy_paste_uv_selseq if not props.src_info: return False - return is_valid_context(context) + return impl.is_valid_context(context) def execute(self, context): props = context.scene.muv_props.copy_paste_uv_selseq @@ -597,53 +447,35 @@ class OperatorSelSeqPasteUV(bpy.types.Operator): bm = common.create_bmesh(obj) # get UV layer - uv_layers = get_paste_uv_layers(self, obj, bm, props.src_info) + uv_layers = impl.get_paste_uv_layers(self, obj, bm, props.src_info, + self.uv_map) if not uv_layers: return {'CANCELLED'} # get selected face - 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 + dest_info = impl.get_select_history_dest_face_info(self, bm, uv_layers, + props.src_info, + self.strategy) + if dest_info is None: + return {'CANCELLED'} # paste - ret = paste_uv(self, bm, props.src_info, dest_info, uv_layers, - self.strategy, self.flip_copied_uv, - self.rotate_copied_uv, self.copy_seams) + ret = impl.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'}, "{} face(s) are pasted".format(dest_face_count)) + face_count = len(props.src_info[list(dest_info.keys())[0]]) + self.report({'INFO'}, "{} face(s) are pasted".format(face_count)) bmesh.update_edit_mesh(obj.data) - if self.copy_seams is True: - obj.data.show_edge_seams = True return {'FINISHED'} -class MenuSelSeqPasteUV(bpy.types.Menu): +@BlClassRegistry() +class MUV_MT_CopyPasteUV_SelSeqPasteUV(bpy.types.Menu): """ Menu class: Paste UV coordinate by selection sequence """ @@ -658,7 +490,7 @@ class MenuSelSeqPasteUV(bpy.types.Menu): 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) + return impl.is_valid_context(context) def draw(self, context): sc = context.scene @@ -668,24 +500,27 @@ class MenuSelSeqPasteUV(bpy.types.Menu): bm = common.create_bmesh(obj) uv_maps = bm.loops.layers.uv.keys() - ops = layout.operator(OperatorSelSeqPasteUV.bl_idname, + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname, text="[Default]") ops.uv_map = "__default" ops.copy_seams = sc.muv_copy_paste_uv_copy_seams ops.strategy = sc.muv_copy_paste_uv_strategy - ops = layout.operator(OperatorSelSeqPasteUV.bl_idname, text="[New]") + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname, + text="[New]") ops.uv_map = "__new" ops.copy_seams = sc.muv_copy_paste_uv_copy_seams ops.strategy = sc.muv_copy_paste_uv_strategy - ops = layout.operator(OperatorSelSeqPasteUV.bl_idname, text="[All]") + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname, + text="[All]") ops.uv_map = "__all" ops.copy_seams = sc.muv_copy_paste_uv_copy_seams ops.strategy = sc.muv_copy_paste_uv_strategy for m in uv_maps: - ops = layout.operator(OperatorSelSeqPasteUV.bl_idname, text=m) + ops = layout.operator(MUV_OT_CopyPasteUV_SelSeqPasteUV.bl_idname, + text=m) ops.uv_map = m ops.copy_seams = sc.muv_copy_paste_uv_copy_seams ops.strategy = sc.muv_copy_paste_uv_strategy diff --git a/uv_magic_uv/op/copy_paste_uv_object.py b/uv_magic_uv/op/copy_paste_uv_object.py index eb6d87c9..d9f42447 100644 --- a/uv_magic_uv/op/copy_paste_uv_object.py +++ b/uv_magic_uv/op/copy_paste_uv_object.py @@ -23,27 +23,24 @@ __status__ = "production" __version__ = "5.2" __date__ = "17 Nov 2018" -import bpy import bmesh +import bpy from bpy.props import ( StringProperty, BoolProperty, ) +from ..impl import copy_paste_uv_impl as impl from .. import common -from .copy_paste_uv import ( - get_copy_uv_layers, - get_paste_uv_layers, - paste_uv -) - +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry __all__ = [ 'Properties', - 'OperatorCopyUV', - 'MenuCopyUV', - 'OperatorPasteUV', - 'MenuPasteUV', + 'MUV_OT_CopyPasteUVObject_CopyUV', + 'MUV_MT_CopyPasteUVObject_CopyUV', + 'MUV_OT_CopyPasteUVObject_PasteUV', + 'MUV_MT_CopyPasteUVObject_PasteUV', ] @@ -68,16 +65,10 @@ def is_valid_context(context): return True -def memorize_view_3d_mode(fn): - def __memorize_view_3d_mode(self, context): - mode_orig = bpy.context.object.mode - result = fn(self, context) - bpy.ops.object.mode_set(mode=mode_orig) - return result - return __memorize_view_3d_mode - - +@PropertyClassRegistry() class Properties: + idname = "copy_paste_uv_object" + @classmethod def init_props(cls, scene): class Props(): @@ -97,7 +88,17 @@ class Properties: del scene.muv_copy_paste_uv_object_copy_seams -class OperatorCopyUV(bpy.types.Operator): +def memorize_view_3d_mode(fn): + def __memorize_view_3d_mode(self, context): + mode_orig = bpy.context.object.mode + result = fn(self, context) + bpy.ops.object.mode_set(mode=mode_orig) + return result + return __memorize_view_3d_mode + + +@BlClassRegistry() +class MUV_OT_CopyPasteUVObject_CopyUV(bpy.types.Operator): """ Operation class: Copy UV coordinate among objects """ @@ -107,7 +108,7 @@ class OperatorCopyUV(bpy.types.Operator): bl_description = "Copy UV coordinate (Among Objects)" bl_options = {'REGISTER', 'UNDO'} - uv_map = StringProperty(default="__default", options={'HIDDEN'}) + uv_map: StringProperty(default="__default", options={'HIDDEN'}) @classmethod def poll(cls, context): @@ -124,23 +125,15 @@ class OperatorCopyUV(bpy.types.Operator): bm = common.create_bmesh(obj) # get UV layer - uv_layers = get_copy_uv_layers(self, bm) + uv_layers = impl.get_copy_uv_layers(self, bm, self.uv_map) if not uv_layers: return {'CANCELLED'} # get selected face - 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 + src_info = impl.get_src_face_info(self, bm, uv_layers) + if src_info is None: + return {'CANCELLED'} + props.src_info = src_info self.report({'INFO'}, "{}'s UV coordinates are copied".format(obj.name)) @@ -148,7 +141,8 @@ class OperatorCopyUV(bpy.types.Operator): return {'FINISHED'} -class MenuCopyUV(bpy.types.Menu): +@BlClassRegistry() +class MUV_MT_CopyPasteUVObject_CopyUV(bpy.types.Menu): """ Menu class: Copy UV coordinate among objects """ @@ -164,20 +158,24 @@ class MenuCopyUV(bpy.types.Menu): def draw(self, _): layout = self.layout # create sub menu - uv_maps = bpy.context.active_object.data.uv_textures.keys() + uv_maps = bpy.context.active_object.data.uv_layers.keys() - ops = layout.operator(OperatorCopyUV.bl_idname, text="[Default]") + ops = layout.operator(MUV_OT_CopyPasteUVObject_CopyUV.bl_idname, + text="[Default]") ops.uv_map = "__default" - ops = layout.operator(OperatorCopyUV.bl_idname, text="[All]") + ops = layout.operator(MUV_OT_CopyPasteUVObject_CopyUV.bl_idname, + text="[All]") ops.uv_map = "__all" for m in uv_maps: - ops = layout.operator(OperatorCopyUV.bl_idname, text=m) + ops = layout.operator(MUV_OT_CopyPasteUVObject_CopyUV.bl_idname, + text=m) ops.uv_map = m -class OperatorPasteUV(bpy.types.Operator): +@BlClassRegistry() +class MUV_OT_CopyPasteUVObject_PasteUV(bpy.types.Operator): """ Operation class: Paste UV coordinate among objects """ @@ -187,8 +185,8 @@ class OperatorPasteUV(bpy.types.Operator): bl_description = "Paste UV coordinate (Among Objects)" bl_options = {'REGISTER', 'UNDO'} - uv_map = StringProperty(default="__default", options={'HIDDEN'}) - copy_seams = BoolProperty( + uv_map: StringProperty(default="__default", options={'HIDDEN'}) + copy_seams: BoolProperty( name="Seams", description="Copy Seams", default=True @@ -213,52 +211,35 @@ class OperatorPasteUV(bpy.types.Operator): return {'CANCELLED'} for o in bpy.data.objects: - if not hasattr(o.data, "uv_textures") or not o.select: + if not hasattr(o.data, "uv_layers") or not o.select_get(): continue bpy.ops.object.mode_set(mode='OBJECT') - bpy.context.scene.objects.active = o + bpy.context.view_layer.objects.active = o bpy.ops.object.mode_set(mode='EDIT') obj = context.active_object bm = common.create_bmesh(obj) # get UV layer - uv_layers = get_paste_uv_layers(self, obj, bm, props.src_info) + uv_layers = impl.get_paste_uv_layers(self, obj, bm, props.src_info, + self.uv_map) if not uv_layers: return {'CANCELLED'} # get selected face - 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 + dest_info = impl.get_dest_face_info(self, bm, uv_layers, + props.src_info, 'N_N') + if dest_info is None: + return {'CANCELLED'} # paste - ret = paste_uv(self, bm, props.src_info, dest_info, uv_layers, - 'N_N', 0, 0, self.copy_seams) + ret = impl.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 UV coordinates are pasted".format(obj.name)) @@ -266,7 +247,8 @@ class OperatorPasteUV(bpy.types.Operator): return {'FINISHED'} -class MenuPasteUV(bpy.types.Menu): +@BlClassRegistry() +class MUV_MT_CopyPasteUVObject_PasteUV(bpy.types.Menu): """ Menu class: Paste UV coordinate among objects """ @@ -289,22 +271,26 @@ class MenuPasteUV(bpy.types.Menu): # create sub menu uv_maps = [] for obj in bpy.data.objects: - if hasattr(obj.data, "uv_textures") and obj.select: - uv_maps.extend(obj.data.uv_textures.keys()) + if hasattr(obj.data, "uv_layers") and obj.select_get(): + uv_maps.extend(obj.data.uv_layers.keys()) - ops = layout.operator(OperatorPasteUV.bl_idname, text="[Default]") + ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname, + text="[Default]") ops.uv_map = "__default" ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams - ops = layout.operator(OperatorPasteUV.bl_idname, text="[New]") + ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname, + text="[New]") ops.uv_map = "__new" ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams - ops = layout.operator(OperatorPasteUV.bl_idname, text="[All]") + ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname, + text="[All]") ops.uv_map = "__all" ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams for m in uv_maps: - ops = layout.operator(OperatorPasteUV.bl_idname, text=m) + ops = layout.operator(MUV_OT_CopyPasteUVObject_PasteUV.bl_idname, + text=m) ops.uv_map = m ops.copy_seams = sc.muv_copy_paste_uv_object_copy_seams diff --git a/uv_magic_uv/op/copy_paste_uv_uvedit.py b/uv_magic_uv/op/copy_paste_uv_uvedit.py index e591b5f1..719687a6 100644 --- a/uv_magic_uv/op/copy_paste_uv_uvedit.py +++ b/uv_magic_uv/op/copy_paste_uv_uvedit.py @@ -18,52 +18,29 @@ # # ##### END GPL LICENSE BLOCK ##### -__author__ = "Nutti , Jace Priester" +__author__ = "imdjs, Nutti " __status__ = "production" __version__ = "5.2" __date__ = "17 Nov 2018" -import math -from math import atan2, sin, cos - import bpy -import bmesh -from mathutils import Vector -from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..impl import copy_paste_uv_uvedit_impl as impl __all__ = [ 'Properties', - 'OperatorCopyUV', - 'OperatorPasteUV', + 'MUV_OT_CopyPasteUVUVEdit_CopyUV', + 'MUV_OT_CopyPasteUVUVEdit_PasteUV', ] -def is_valid_context(context): - obj = context.object - - # only edit mode is allowed to execute - if obj is None: - return False - if obj.type != 'MESH': - return False - if context.object.mode != 'EDIT': - return False - - # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. - # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf - # after the execution - for space in context.area.spaces: - if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): - break - else: - return False - - return True - - +@PropertyClassRegistry() class Properties: + idname = "copy_paste_uv_uvedit" + @classmethod def init_props(cls, scene): class Props(): @@ -76,7 +53,8 @@ class Properties: del scene.muv_props.copy_paste_uv_uvedit -class OperatorCopyUV(bpy.types.Operator): +@BlClassRegistry() +class MUV_OT_CopyPasteUVUVEdit_CopyUV(bpy.types.Operator): """ Operation class: Copy UV coordinate on UV/Image Editor """ @@ -86,38 +64,19 @@ class OperatorCopyUV(bpy.types.Operator): bl_description = "Copy UV coordinate (only selected in UV/Image Editor)" bl_options = {'REGISTER', 'UNDO'} + def __init__(self): + self.__impl = impl.CopyUVImpl() + @classmethod def poll(cls, context): - # we can not get area/space/region from console - if common.is_console_mode(): - return True - return is_valid_context(context) + return impl.CopyUVImpl.poll(context) def execute(self, context): - props = context.scene.muv_props.copy_paste_uv_uvedit - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - uv_layer = bm.loops.layers.uv.verify() - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - - props.src_uvs = [] - for face in bm.faces: - if not face.select: - continue - skip = False - for l in face.loops: - if not l[uv_layer].select: - skip = True - break - if skip: - continue - props.src_uvs.append([l[uv_layer].uv.copy() for l in face.loops]) - - return {'FINISHED'} - - -class OperatorPasteUV(bpy.types.Operator): + return self.__impl.execute(self, context) + + +@BlClassRegistry() +class MUV_OT_CopyPasteUVUVEdit_PasteUV(bpy.types.Operator): """ Operation class: Paste UV coordinate on UV/Image Editor """ @@ -127,72 +86,12 @@ class OperatorPasteUV(bpy.types.Operator): bl_description = "Paste UV coordinate (only selected in UV/Image Editor)" bl_options = {'REGISTER', 'UNDO'} + def __init__(self): + self.__impl = impl.PasteUVImpl() + @classmethod def poll(cls, context): - # we can not get area/space/region from console - if common.is_console_mode(): - return True - sc = context.scene - props = sc.muv_props.copy_paste_uv_uvedit - if not props.src_uvs: - return False - return is_valid_context(context) + return impl.PasteUVImpl.poll(context) def execute(self, context): - props = context.scene.muv_props.copy_paste_uv_uvedit - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - uv_layer = bm.loops.layers.uv.verify() - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - - dest_uvs = [] - dest_face_indices = [] - for face in bm.faces: - if not face.select: - continue - skip = False - for l in face.loops: - if not l[uv_layer].select: - skip = True - break - if skip: - continue - dest_face_indices.append(face.index) - uvs = [l[uv_layer].uv.copy() for l in face.loops] - dest_uvs.append(uvs) - - for suvs, duvs in zip(props.src_uvs, dest_uvs): - src_diff = suvs[1] - suvs[0] - dest_diff = duvs[1] - duvs[0] - - src_base = suvs[0] - dest_base = duvs[0] - - src_rad = atan2(src_diff.y, src_diff.x) - dest_rad = atan2(dest_diff.y, dest_diff.x) - if src_rad < dest_rad: - radian = dest_rad - src_rad - elif src_rad > dest_rad: - radian = math.pi * 2 - (src_rad - dest_rad) - else: # src_rad == dest_rad - radian = 0.0 - - ratio = dest_diff.length / src_diff.length - break - - for suvs, fidx in zip(props.src_uvs, dest_face_indices): - for l, suv in zip(bm.faces[fidx].loops, suvs): - base = suv - src_base - radian_ref = atan2(base.y, base.x) - radian_fin = (radian + radian_ref) - length = base.length - turn = Vector((length * cos(radian_fin), - length * sin(radian_fin))) - target_uv = Vector((turn.x * ratio, turn.y * ratio)) + \ - dest_base - l[uv_layer].uv = target_uv - - bmesh.update_edit_mesh(obj.data) - - return {'FINISHED'} + return self.__impl.execute(self, context) diff --git a/uv_magic_uv/op/flip_rotate_uv.py b/uv_magic_uv/op/flip_rotate_uv.py index 751bb8fb..d1637052 100644 --- a/uv_magic_uv/op/flip_rotate_uv.py +++ b/uv_magic_uv/op/flip_rotate_uv.py @@ -31,36 +31,20 @@ from bpy.props import ( ) from .. import common - +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..impl import flip_rotate_impl as impl __all__ = [ 'Properties', - 'Operator', + 'MUV_OT_FlipRotate', ] -def is_valid_context(context): - obj = context.object - - # only edit mode is allowed to execute - if obj is None: - return False - if obj.type != 'MESH': - return False - if context.object.mode != 'EDIT': - return False - - # only 'VIEW_3D' space is allowed to execute - for space in context.area.spaces: - if space.type == 'VIEW_3D': - break - else: - return False - - return True - - +@PropertyClassRegistry() class Properties: + idname = "flip_rotate_uv" + @classmethod def init_props(cls, scene): scene.muv_flip_rotate_uv_enabled = BoolProperty( @@ -80,7 +64,8 @@ class Properties: del scene.muv_flip_rotate_uv_seams -class Operator(bpy.types.Operator): +@BlClassRegistry() +class MUV_OT_FlipRotate(bpy.types.Operator): """ Operation class: Flip and Rotate UV coordinate """ @@ -90,18 +75,18 @@ class Operator(bpy.types.Operator): bl_description = "Flip/Rotate UV coordinate" bl_options = {'REGISTER', 'UNDO'} - flip = BoolProperty( + flip: BoolProperty( name="Flip UV", description="Flip UV...", default=False ) - rotate = IntProperty( + rotate: IntProperty( default=0, name="Rotate UV", min=0, max=30 ) - seams = BoolProperty( + seams: BoolProperty( name="Seams", description="Seams", default=True @@ -112,7 +97,7 @@ class Operator(bpy.types.Operator): # we can not get area/space/region from console if common.is_console_mode(): return True - return is_valid_context(context) + return impl.is_valid_context(context) def execute(self, context): self.report({'INFO'}, "Flip/Rotate UV") @@ -122,61 +107,24 @@ class Operator(bpy.types.Operator): bm.faces.ensure_lookup_table() # get UV layer - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") + uv_layer = impl.get_uv_layer(self, bm) + if not uv_layer: return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() # 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") + src_info = impl.get_src_face_info(self, bm, [uv_layer], True) + if not src_info: return {'CANCELLED'} - self.report({'INFO'}, "%d face(s) are selected" % len(dest_uvs)) + + face_count = len(src_info[list(src_info.keys())[0]]) + self.report({'INFO'}, "{} face(s) are selected".format(face_count)) # paste - for idx, duvs, dpuvs, dss in zip(dest_face_indices, dest_uvs, - dest_pin_uvs, dest_seams): - duvs_fr = [uv for uv in duvs] - dpuvs_fr = [pin_uv for pin_uv in dpuvs] - dss_fr = [s for s in dss] - # flip UVs - if self.flip is True: - duvs_fr.reverse() - dpuvs_fr.reverse() - dss_fr.reverse() - # rotate UVs - for _ in range(self.rotate): - uv = duvs_fr.pop() - pin_uv = dpuvs_fr.pop() - s = dss_fr.pop() - duvs_fr.insert(0, uv) - dpuvs_fr.insert(0, pin_uv) - dss_fr.insert(0, s) - # paste UVs - for l, duv, dpuv, ds in zip( - bm.faces[idx].loops, duvs_fr, dpuvs_fr, dss_fr): - l[uv_layer].uv = duv - l[uv_layer].pin_uv = dpuv - if self.seams is True: - l.edge.seam = ds - - self.report({'INFO'}, "%d face(s) are flipped/rotated" % len(dest_uvs)) + ret = impl.paste_uv(self, bm, src_info, src_info, [uv_layer], 'N_N', + self.flip, self.rotate, self.seams) + if ret: + return {'CANCELLED'} bmesh.update_edit_mesh(obj.data) - if self.seams is True: - obj.data.show_edge_seams = True return {'FINISHED'} diff --git a/uv_magic_uv/op/mirror_uv.py b/uv_magic_uv/op/mirror_uv.py index 11ad2bca..6793ca23 100644 --- a/uv_magic_uv/op/mirror_uv.py +++ b/uv_magic_uv/op/mirror_uv.py @@ -29,40 +29,22 @@ from bpy.props import ( FloatProperty, BoolProperty, ) -import bmesh -from mathutils import Vector -from .. import common +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry +from ..impl import mirror_uv_impl as impl __all__ = [ 'Properties', - 'Operator', + 'MUV_OT_MirrorUV', ] -def is_valid_context(context): - obj = context.object - - # only edit mode is allowed to execute - if obj is None: - return False - if obj.type != 'MESH': - return False - if context.object.mode != 'EDIT': - return False - - # only 'VIEW_3D' space is allowed to execute - for space in context.area.spaces: - if space.type == 'VIEW_3D': - break - else: - return False - - return True - - +@PropertyClassRegistry() class Properties: + idname = "mirror_uv" + @classmethod def init_props(cls, scene): scene.muv_mirror_uv_enabled = BoolProperty( @@ -87,7 +69,8 @@ class Properties: del scene.muv_mirror_uv_axis -class Operator(bpy.types.Operator): +@BlClassRegistry() +class MUV_OT_MirrorUV(bpy.types.Operator): """ Operation class: Mirror UV """ @@ -96,7 +79,7 @@ class Operator(bpy.types.Operator): bl_label = "Mirror UV" bl_options = {'REGISTER', 'UNDO'} - axis = EnumProperty( + axis: EnumProperty( items=( ('X', "X", "Mirror Along X axis"), ('Y', "Y", "Mirror Along Y axis"), @@ -106,7 +89,7 @@ class Operator(bpy.types.Operator): description="Mirror Axis", default='X' ) - error = FloatProperty( + error: FloatProperty( name="Error", description="Error threshold", default=0.001, @@ -116,95 +99,12 @@ class Operator(bpy.types.Operator): soft_max=1.0 ) - def __is_vector_similar(self, v1, v2, error): - """ - Check if two vectors are similar, within an error threshold - """ - within_err_x = abs(v2.x - v1.x) < error - within_err_y = abs(v2.y - v1.y) < error - within_err_z = abs(v2.z - v1.z) < error - - return within_err_x and within_err_y and within_err_z - - def __mirror_uvs(self, uv_layer, src, dst, axis, error): - """ - Copy UV coordinates from one UV face to another - """ - for sl in src.loops: - suv = sl[uv_layer].uv.copy() - svco = sl.vert.co.copy() - for dl in dst.loops: - dvco = dl.vert.co.copy() - if axis == 'X': - dvco.x = -dvco.x - elif axis == 'Y': - dvco.y = -dvco.y - elif axis == 'Z': - dvco.z = -dvco.z - - if self.__is_vector_similar(svco, dvco, error): - dl[uv_layer].uv = suv.copy() - - def __get_face_center(self, face): - """ - Get center coordinate of the face - """ - center = Vector((0.0, 0.0, 0.0)) - for v in face.verts: - center = center + v.co - - return center / len(face.verts) + def __init__(self): + self.__impl = impl.MirrorUVImpl() @classmethod def poll(cls, context): - # we can not get area/space/region from console - if common.is_console_mode(): - return True - return is_valid_context(context) + return impl.MirrorUVImpl.poll(context) def execute(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - - error = self.error - axis = self.axis - - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - - faces = [f for f in bm.faces if f.select] - for f_dst in faces: - count = len(f_dst.verts) - for f_src in bm.faces: - # check if this is a candidate to do mirror UV - if f_src.index == f_dst.index: - continue - if count != len(f_src.verts): - continue - - # test if the vertices x values are the same sign - dst = self.__get_face_center(f_dst) - src = self.__get_face_center(f_src) - if (dst.x > 0 and src.x > 0) or (dst.x < 0 and src.x < 0): - continue - - # invert source axis - if axis == 'X': - src.x = -src.x - elif axis == 'Y': - src.y = -src.z - elif axis == 'Z': - src.z = -src.z - - # do mirror UV - if self.__is_vector_similar(dst, src, error): - self.__mirror_uvs( - uv_layer, f_src, f_dst, self.axis, self.error) - - bmesh.update_edit_mesh(obj.data) - - return {'FINISHED'} + return self.__impl.execute(self, context) diff --git a/uv_magic_uv/op/move_uv.py b/uv_magic_uv/op/move_uv.py index a229ae34..653918d3 100644 --- a/uv_magic_uv/op/move_uv.py +++ b/uv_magic_uv/op/move_uv.py @@ -24,40 +24,23 @@ __version__ = "5.2" __date__ = "17 Nov 2018" import bpy -import bmesh -from mathutils import Vector from bpy.props import BoolProperty -from .. import common +from ..impl import move_uv_impl as impl +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry + __all__ = [ 'Properties', - 'Operator', + 'MUV_OT_MoveUV', ] -def is_valid_context(context): - obj = context.object - - # only edit mode is allowed to execute - if obj is None: - return False - if obj.type != 'MESH': - return False - if context.object.mode != 'EDIT': - return False - - # only 'VIEW_3D' space is allowed to execute - for space in context.area.spaces: - if space.type == 'VIEW_3D': - break - else: - return False - - return True - - +@PropertyClassRegistry() class Properties: + idname = "move_uv" + @classmethod def init_props(cls, scene): scene.muv_move_uv_enabled = BoolProperty( @@ -71,7 +54,8 @@ class Properties: del scene.muv_move_uv_enabled -class Operator(bpy.types.Operator): +@BlClassRegistry() +class MUV_OT_MoveUV(bpy.types.Operator): """ Operator class: Move UV """ @@ -80,108 +64,19 @@ class Operator(bpy.types.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)) - self.__offset_uv = Vector((0.0, 0.0)) - self.__prev_offset_uv = Vector((0.0, 0.0)) - self.__first_time = True - self.__ini_uvs = [] - self.__operating = False + self.__impl = impl.MoveUVImpl() @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) + return impl.MoveUVImpl.poll(context) @classmethod def is_running(cls, _): - return cls.__running - - def __find_uv(self, context): - bm = bmesh.from_edit_mesh(context.object.data) - topology_dict = [] - uvs = [] - active_uv = bm.loops.layers.uv.active - for fidx, f in enumerate(bm.faces): - for vidx, v in enumerate(f.verts): - if v.select: - uvs.append(f.loops[vidx][active_uv].uv.copy()) - topology_dict.append([fidx, vidx]) - - return topology_dict, uvs + return impl.MoveUVImpl.is_running(_) def modal(self, context, event): - if self.__first_time is True: - self.__prev_mouse = Vector(( - event.mouse_region_x, event.mouse_region_y)) - self.__first_time = False - return {'RUNNING_MODAL'} - - # move UV - div = 10000 - self.__offset_uv += Vector(( - (event.mouse_region_x - self.__prev_mouse.x) / div, - (event.mouse_region_y - self.__prev_mouse.y) / div)) - ouv = self.__offset_uv - pouv = self.__prev_offset_uv - vec = Vector((ouv.x - ouv.y, ouv.x + ouv.y)) - dv = vec - pouv - self.__prev_offset_uv = vec - self.__prev_mouse = Vector(( - event.mouse_region_x, event.mouse_region_y)) - - # check if operation is started - if not self.__operating: - if event.type == 'LEFTMOUSE' and event.value == 'RELEASE': - self.__operating = True - return {'RUNNING_MODAL'} - - # update UV - obj = context.object - bm = bmesh.from_edit_mesh(obj.data) - active_uv = bm.loops.layers.uv.active - for fidx, vidx in self.__topology_dict: - l = bm.faces[fidx].loops[vidx] - l[active_uv].uv = l[active_uv].uv + dv - bmesh.update_edit_mesh(obj.data) - - # check mouse preference - if context.user_preferences.inputs.select_mouse == 'RIGHT': - confirm_btn = 'LEFTMOUSE' - cancel_btn = 'RIGHTMOUSE' - else: - confirm_btn = 'RIGHTMOUSE' - cancel_btn = 'LEFTMOUSE' - - # cancelled - if event.type == cancel_btn and event.value == 'PRESS': - for (fidx, vidx), uv in zip(self.__topology_dict, self.__ini_uvs): - bm.faces[fidx].loops[vidx][active_uv].uv = uv - Operator.__running = False - return {'FINISHED'} - # confirmed - if event.type == confirm_btn and event.value == 'PRESS': - Operator.__running = False - return {'FINISHED'} - - return {'RUNNING_MODAL'} + return self.__impl.modal(self, context, event) def execute(self, context): - 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'} + return self.__impl.execute(self, context) diff --git a/uv_magic_uv/op/pack_uv.py b/uv_magic_uv/op/pack_uv.py deleted file mode 100644 index 39340fda..00000000 --- a/uv_magic_uv/op/pack_uv.py +++ /dev/null @@ -1,275 +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.2" -__date__ = "17 Nov 2018" - -from math import fabs - -import bpy -import bmesh -import mathutils -from bpy.props import ( - FloatProperty, - FloatVectorProperty, - BoolProperty, -) -from mathutils import Vector - -from .. import common - - -__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 - - Same center of UV island - - Same size of UV island - - Same number of UV - """ - - bl_idname = "uv.muv_pack_uv_operator" - bl_label = "Pack UV" - bl_description = "Pack UV (Same UV Islands are integrated)" - bl_options = {'REGISTER', 'UNDO'} - - rotate = BoolProperty( - name="Rotate", - description="Rotate option used by default pack UV function", - default=False) - margin = FloatProperty( - name="Margin", - description="Margin used by default pack UV function", - min=0, - max=1, - default=0.001) - allowable_center_deviation = FloatVectorProperty( - name="Allowable Center Deviation", - description="Allowable center deviation to judge same UV island", - min=0.000001, - max=0.1, - default=(0.001, 0.001), - size=2 - ) - allowable_size_deviation = FloatVectorProperty( - name="Allowable Size Deviation", - description="Allowable sizse deviation to judge same UV island", - min=0.000001, - max=0.1, - default=(0.001, 0.001), - size=2 - ) - - @classmethod - def poll(cls, context): - # we can not get area/space/region from console - if common.is_console_mode(): - return True - return is_valid_context(context) - - def execute(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - - selected_faces = [f for f in bm.faces if f.select] - island_info = common.get_island_info(obj) - num_group = self.__group_island(island_info) - - loop_lists = [l for f in bm.faces for l in f.loops] - bpy.ops.mesh.select_all(action='DESELECT') - - # pack UV - for gidx in range(num_group): - group = list(filter( - lambda i, idx=gidx: i['group'] == idx, island_info)) - for f in group[0]['faces']: - f['face'].select = True - bmesh.update_edit_mesh(obj.data) - bpy.ops.uv.select_all(action='SELECT') - bpy.ops.uv.pack_islands(rotate=self.rotate, margin=self.margin) - - # copy/paste UV among same islands - for gidx in range(num_group): - group = list(filter( - lambda i, idx=gidx: i['group'] == idx, island_info)) - if len(group) <= 1: - continue - for g in group[1:]: - for (src_face, dest_face) in zip( - group[0]['sorted'], g['sorted']): - for (src_loop, dest_loop) in zip( - src_face['face'].loops, dest_face['face'].loops): - loop_lists[dest_loop.index][uv_layer].uv = loop_lists[ - src_loop.index][uv_layer].uv - - # restore face/UV selection - bpy.ops.uv.select_all(action='DESELECT') - bpy.ops.mesh.select_all(action='DESELECT') - for f in selected_faces: - f.select = True - bpy.ops.uv.select_all(action='SELECT') - - bmesh.update_edit_mesh(obj.data) - - return {'FINISHED'} - - def __sort_island_faces(self, kd, uvs, isl1, isl2): - """ - Sort faces in island - """ - - sorted_faces = [] - for f in isl1['sorted']: - _, idx, _ = kd.find( - Vector((f['ave_uv'].x, f['ave_uv'].y, 0.0))) - sorted_faces.append(isl2['faces'][uvs[idx]['face_idx']]) - return sorted_faces - - def __group_island(self, island_info): - """ - Group island - """ - - num_group = 0 - while True: - # search islands which is not parsed yet - isl_1 = None - for isl_1 in island_info: - if isl_1['group'] == -1: - break - else: - break # all faces are parsed - if isl_1 is None: - break - isl_1['group'] = num_group - isl_1['sorted'] = isl_1['faces'] - - # search same island - for isl_2 in island_info: - if isl_2['group'] == -1: - dcx = isl_2['center'].x - isl_1['center'].x - dcy = isl_2['center'].y - isl_1['center'].y - dsx = isl_2['size'].x - isl_1['size'].x - dsy = isl_2['size'].y - isl_1['size'].y - center_x_matched = ( - fabs(dcx) < self.allowable_center_deviation[0] - ) - center_y_matched = ( - fabs(dcy) < self.allowable_center_deviation[1] - ) - size_x_matched = ( - fabs(dsx) < self.allowable_size_deviation[0] - ) - size_y_matched = ( - fabs(dsy) < self.allowable_size_deviation[1] - ) - center_matched = center_x_matched and center_y_matched - size_matched = size_x_matched and size_y_matched - num_uv_matched = (isl_2['num_uv'] == isl_1['num_uv']) - # are islands have same? - if center_matched and size_matched and num_uv_matched: - isl_2['group'] = num_group - kd = mathutils.kdtree.KDTree(len(isl_2['faces'])) - uvs = [ - { - 'uv': Vector( - (f['ave_uv'].x, f['ave_uv'].y, 0.0) - ), - 'face_idx': fidx - } for fidx, f in enumerate(isl_2['faces']) - ] - for i, uv in enumerate(uvs): - kd.insert(uv['uv'], i) - kd.balance() - # sort faces for copy/paste UV - isl_2['sorted'] = self.__sort_island_faces( - kd, uvs, isl_1, isl_2) - num_group = num_group + 1 - - return num_group diff --git a/uv_magic_uv/op/preserve_uv_aspect.py b/uv_magic_uv/op/preserve_uv_aspect.py deleted file mode 100644 index cb11bd45..00000000 --- a/uv_magic_uv/op/preserve_uv_aspect.py +++ /dev/null @@ -1,277 +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.2" -__date__ = "17 Nov 2018" - -import bpy -import bmesh -from bpy.props import StringProperty, EnumProperty, BoolProperty -from mathutils import Vector - -from .. import common - - -__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_operator" - bl_label = "Preserve UV Aspect" - bl_description = "Choose Image" - bl_options = {'REGISTER', 'UNDO'} - - dest_img_name = StringProperty(options={'HIDDEN'}) - origin = EnumProperty( - name="Origin", - description="Aspect Origin", - items=[ - ('CENTER', 'Center', 'Center'), - ('LEFT_TOP', 'Left Top', 'Left Bottom'), - ('LEFT_CENTER', 'Left Center', 'Left Center'), - ('LEFT_BOTTOM', 'Left Bottom', 'Left Bottom'), - ('CENTER_TOP', 'Center Top', 'Center Top'), - ('CENTER_BOTTOM', 'Center Bottom', 'Center Bottom'), - ('RIGHT_TOP', 'Right Top', 'Right Top'), - ('RIGHT_CENTER', 'Right Center', 'Right Center'), - ('RIGHT_BOTTOM', 'Right Bottom', 'Right Bottom') - - ], - default="CENTER" - ) - - @classmethod - def poll(cls, context): - # we can not get area/space/region from console - if common.is_console_mode(): - return True - return is_valid_context(context) - - def execute(self, context): - # Note: the current system only works if the - # f[tex_layer].image doesn't return None - # which will happen in certain cases - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - - uv_layer = bm.loops.layers.uv.verify() - tex_layer = bm.faces.layers.tex.verify() - - sel_faces = [f for f in bm.faces if f.select] - dest_img = bpy.data.images[self.dest_img_name] - - info = {} - - for f in sel_faces: - if not f[tex_layer].image in info.keys(): - info[f[tex_layer].image] = {} - info[f[tex_layer].image]['faces'] = [] - info[f[tex_layer].image]['faces'].append(f) - - for img in info: - if img is None: - continue - - src_img = img - ratio = Vector(( - dest_img.size[0] / src_img.size[0], - dest_img.size[1] / src_img.size[1])) - - if self.origin == 'CENTER': - origin = Vector((0.0, 0.0)) - num = 0 - for f in info[img]['faces']: - for l in f.loops: - uv = l[uv_layer].uv - origin = origin + uv - num = num + 1 - origin = origin / num - elif self.origin == 'LEFT_TOP': - origin = Vector((100000.0, -100000.0)) - for f in info[img]['faces']: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = min(origin.x, uv.x) - origin.y = max(origin.y, uv.y) - elif self.origin == 'LEFT_CENTER': - origin = Vector((100000.0, 0.0)) - num = 0 - for f in info[img]['faces']: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = min(origin.x, uv.x) - origin.y = origin.y + uv.y - num = num + 1 - origin.y = origin.y / num - elif self.origin == 'LEFT_BOTTOM': - origin = Vector((100000.0, 100000.0)) - for f in info[img]['faces']: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = min(origin.x, uv.x) - origin.y = min(origin.y, uv.y) - elif self.origin == 'CENTER_TOP': - origin = Vector((0.0, -100000.0)) - num = 0 - for f in info[img]['faces']: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = origin.x + uv.x - origin.y = max(origin.y, uv.y) - num = num + 1 - origin.x = origin.x / num - elif self.origin == 'CENTER_BOTTOM': - origin = Vector((0.0, 100000.0)) - num = 0 - for f in info[img]['faces']: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = origin.x + uv.x - origin.y = min(origin.y, uv.y) - num = num + 1 - origin.x = origin.x / num - elif self.origin == 'RIGHT_TOP': - origin = Vector((-100000.0, -100000.0)) - for f in info[img]['faces']: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = max(origin.x, uv.x) - origin.y = max(origin.y, uv.y) - elif self.origin == 'RIGHT_CENTER': - origin = Vector((-100000.0, 0.0)) - num = 0 - for f in info[img]['faces']: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = max(origin.x, uv.x) - origin.y = origin.y + uv.y - num = num + 1 - origin.y = origin.y / num - elif self.origin == 'RIGHT_BOTTOM': - origin = Vector((-100000.0, 100000.0)) - for f in info[img]['faces']: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = max(origin.x, uv.x) - origin.y = min(origin.y, uv.y) - - info[img]['ratio'] = ratio - info[img]['origin'] = origin - - for img in info: - if img is None: - continue - - for f in info[img]['faces']: - f[tex_layer].image = dest_img - for l in f.loops: - uv = l[uv_layer].uv - origin = info[img]['origin'] - ratio = info[img]['ratio'] - diff = uv - origin - diff.x = diff.x / ratio.x - diff.y = diff.y / ratio.y - uv.x = origin.x + diff.x - uv.y = origin.y + diff.y - l[uv_layer].uv = uv - - bmesh.update_edit_mesh(obj.data) - - return {'FINISHED'} diff --git a/uv_magic_uv/op/select_uv.py b/uv_magic_uv/op/select_uv.py deleted file mode 100644 index 3a7bcbc3..00000000 --- a/uv_magic_uv/op/select_uv.py +++ /dev/null @@ -1,161 +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.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 deleted file mode 100644 index 31bef155..00000000 --- a/uv_magic_uv/op/smooth_uv.py +++ /dev/null @@ -1,281 +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__ = "imdjs, Nutti " -__status__ = "production" -__version__ = "5.2" -__date__ = "17 Nov 2018" - -import bpy -import bmesh -from bpy.props import BoolProperty, FloatProperty - -from .. import common - - -__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_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'} - - transmission = BoolProperty( - name="Transmission", - description="Smooth linked UVs", - default=False - ) - mesh_infl = FloatProperty( - name="Mesh Influence", - description="Influence rate of mesh vertex", - min=0.0, - max=1.0, - default=0.0 - ) - select = BoolProperty( - name="Select", - description="Select UVs which are smoothed", - default=False - ) - - @classmethod - def poll(cls, context): - # we can not get area/space/region from console - if common.is_console_mode(): - return True - return is_valid_context(context) - - def __smooth_wo_transmission(self, loop_seqs, uv_layer): - # calculate path length - loops = [] - for hseq in loop_seqs: - loops.extend([hseq[0][0], hseq[0][1]]) - full_vlen = 0 - accm_vlens = [0.0] - full_uvlen = 0 - accm_uvlens = [0.0] - orig_uvs = [loop_seqs[0][0][0][uv_layer].uv.copy()] - for l1, l2 in zip(loops[:-1], loops[1:]): - diff_v = l2.vert.co - l1.vert.co - full_vlen = full_vlen + diff_v.length - accm_vlens.append(full_vlen) - diff_uv = l2[uv_layer].uv - l1[uv_layer].uv - full_uvlen = full_uvlen + diff_uv.length - accm_uvlens.append(full_uvlen) - orig_uvs.append(l2[uv_layer].uv.copy()) - - for hidx, hseq in enumerate(loop_seqs): - pair = hseq[0] - for pidx, l in enumerate(pair): - if self.select: - l[uv_layer].select = True - - # ignore start/end loop - if (hidx == 0 and pidx == 0) or\ - ((hidx == len(loop_seqs) - 1) and (pidx == len(pair) - 1)): - continue - - # calculate target path length - # target = no influenced * (1 - infl) + influenced * infl - tgt_noinfl = full_uvlen * (hidx + pidx) / (len(loop_seqs)) - tgt_infl = full_uvlen * accm_vlens[hidx * 2 + pidx] / full_vlen - target_length = tgt_noinfl * (1 - self.mesh_infl) + \ - tgt_infl * self.mesh_infl - - # get target UV - for i in range(len(accm_uvlens[:-1])): - # get line segment which UV will be placed - if ((accm_uvlens[i] <= target_length) and - (accm_uvlens[i + 1] > target_length)): - tgt_seg_len = target_length - accm_uvlens[i] - seg_len = accm_uvlens[i + 1] - accm_uvlens[i] - uv1 = orig_uvs[i] - uv2 = orig_uvs[i + 1] - target_uv = uv1 + (uv2 - uv1) * tgt_seg_len / seg_len - break - else: - self.report({'ERROR'}, "Failed to get target UV") - return {'CANCELLED'} - - # update UV - l[uv_layer].uv = target_uv - - def __smooth_w_transmission(self, loop_seqs, uv_layer): - # calculate path length - loops = [] - for vidx in range(len(loop_seqs[0])): - ls = [] - for hseq in loop_seqs: - ls.extend(hseq[vidx]) - loops.append(ls) - - orig_uvs = [] - accm_vlens = [] - full_vlens = [] - accm_uvlens = [] - full_uvlens = [] - for ls in loops: - full_v = 0.0 - accm_v = [0.0] - full_uv = 0.0 - accm_uv = [0.0] - uvs = [ls[0][uv_layer].uv.copy()] - for l1, l2 in zip(ls[:-1], ls[1:]): - diff_v = l2.vert.co - l1.vert.co - full_v = full_v + diff_v.length - accm_v.append(full_v) - diff_uv = l2[uv_layer].uv - l1[uv_layer].uv - full_uv = full_uv + diff_uv.length - accm_uv.append(full_uv) - uvs.append(l2[uv_layer].uv.copy()) - accm_vlens.append(accm_v) - full_vlens.append(full_v) - accm_uvlens.append(accm_uv) - full_uvlens.append(full_uv) - orig_uvs.append(uvs) - - for hidx, hseq in enumerate(loop_seqs): - for vidx, (pair, uvs, accm_v, full_v, accm_uv, full_uv)\ - in enumerate(zip(hseq, orig_uvs, accm_vlens, full_vlens, - accm_uvlens, full_uvlens)): - for pidx, l in enumerate(pair): - if self.select: - l[uv_layer].select = True - - # ignore start/end loop - if hidx == 0 and pidx == 0: - continue - if hidx == len(loop_seqs) - 1 and pidx == len(pair) - 1: - continue - - # calculate target path length - # target = no influenced * (1 - infl) + influenced * infl - tgt_noinfl = full_uv * (hidx + pidx) / (len(loop_seqs)) - tgt_infl = full_uv * accm_v[hidx * 2 + pidx] / full_v - target_length = tgt_noinfl * (1 - self.mesh_infl) + \ - tgt_infl * self.mesh_infl - - # get target UV - for i in range(len(accm_uv[:-1])): - # get line segment to be placed - if ((accm_uv[i] <= target_length) and - (accm_uv[i + 1] > target_length)): - tgt_seg_len = target_length - accm_uv[i] - seg_len = accm_uv[i + 1] - accm_uv[i] - uv1 = uvs[i] - uv2 = uvs[i + 1] - target_uv = uv1 +\ - (uv2 - uv1) * tgt_seg_len / seg_len - break - else: - self.report({'ERROR'}, "Failed to get target UV") - return {'CANCELLED'} - - # update UV - l[uv_layer].uv = target_uv - - def __smooth(self, loop_seqs, uv_layer): - if self.transmission: - self.__smooth_w_transmission(loop_seqs, uv_layer) - else: - self.__smooth_wo_transmission(loop_seqs, uv_layer) - - def execute(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - uv_layer = bm.loops.layers.uv.verify() - - # loop_seqs[horizontal][vertical][loop] - loop_seqs, error = common.get_loop_sequences(bm, uv_layer) - if not loop_seqs: - self.report({'WARNING'}, error) - return {'CANCELLED'} - - # smooth - self.__smooth(loop_seqs, uv_layer) - - bmesh.update_edit_mesh(obj.data) - - return {'FINISHED'} diff --git a/uv_magic_uv/op/texture_lock.py b/uv_magic_uv/op/texture_lock.py deleted file mode 100644 index 4be97c62..00000000 --- a/uv_magic_uv/op/texture_lock.py +++ /dev/null @@ -1,533 +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.2" -__date__ = "17 Nov 2018" - -import math -from math import atan2, cos, sqrt, sin, fabs - -import bpy -import bmesh -from mathutils import Vector -from bpy.props import BoolProperty - -from .. import common - - -__all__ = [ - 'Properties', - 'OperatorLock', - 'OperatorUnlock', - 'OperatorIntr', -] - - -def get_vco(verts_orig, loop): - """ - Get vertex original coordinate from loop - """ - for vo in verts_orig: - if vo["vidx"] == loop.vert.index and vo["moved"] is False: - return vo["vco"] - return loop.vert.co - - -def get_link_loops(vert): - """ - Get loop linked to vertex - """ - link_loops = [] - for f in vert.link_faces: - adj_loops = [] - for loop in f.loops: - # self loop - if loop.vert == vert: - l = loop - # linked loop - else: - for e in loop.vert.link_edges: - if e.other_vert(loop.vert) == vert: - adj_loops.append(loop) - if len(adj_loops) < 2: - return None - - link_loops.append({"l": l, "l0": adj_loops[0], "l1": adj_loops[1]}) - return link_loops - - -def get_ini_geom(link_loop, uv_layer, verts_orig, v_orig): - """ - Get initial geometory - (Get interior angle of face in vertex/UV space) - """ - u = link_loop["l"][uv_layer].uv - v0 = get_vco(verts_orig, link_loop["l0"]) - u0 = link_loop["l0"][uv_layer].uv - v1 = get_vco(verts_orig, link_loop["l1"]) - u1 = link_loop["l1"][uv_layer].uv - - # get interior angle of face in vertex space - v0v1 = v1 - v0 - v0v = v_orig["vco"] - v0 - v1v = v_orig["vco"] - v1 - theta0 = v0v1.angle(v0v) - theta1 = v0v1.angle(-v1v) - if (theta0 + theta1) > math.pi: - theta0 = v0v1.angle(-v0v) - theta1 = v0v1.angle(v1v) - - # get interior angle of face in UV space - u0u1 = u1 - u0 - u0u = u - u0 - u1u = u - u1 - phi0 = u0u1.angle(u0u) - phi1 = u0u1.angle(-u1u) - if (phi0 + phi1) > math.pi: - phi0 = u0u1.angle(-u0u) - phi1 = u0u1.angle(u1u) - - # get direction of linked UV coordinate - # this will be used to judge whether angle is more or less than 180 degree - dir0 = u0u1.cross(u0u) > 0 - dir1 = u0u1.cross(u1u) > 0 - - return { - "theta0": theta0, - "theta1": theta1, - "phi0": phi0, - "phi1": phi1, - "dir0": dir0, - "dir1": dir1} - - -def get_target_uv(link_loop, uv_layer, verts_orig, v, ini_geom): - """ - Get target UV coordinate - """ - v0 = get_vco(verts_orig, link_loop["l0"]) - lo0 = link_loop["l0"] - v1 = get_vco(verts_orig, link_loop["l1"]) - lo1 = link_loop["l1"] - - # get interior angle of face in vertex space - v0v1 = v1 - v0 - v0v = v.co - v0 - v1v = v.co - v1 - theta0 = v0v1.angle(v0v) - theta1 = v0v1.angle(-v1v) - if (theta0 + theta1) > math.pi: - theta0 = v0v1.angle(-v0v) - theta1 = v0v1.angle(v1v) - - # calculate target interior angle in UV space - phi0 = theta0 * ini_geom["phi0"] / ini_geom["theta0"] - phi1 = theta1 * ini_geom["phi1"] / ini_geom["theta1"] - - uv0 = lo0[uv_layer].uv - uv1 = lo1[uv_layer].uv - - # calculate target vertex coordinate from target interior angle - tuv0, tuv1 = calc_tri_vert(uv0, uv1, phi0, phi1) - - # target UV coordinate depends on direction, so judge using direction of - # linked UV coordinate - u0u1 = uv1 - uv0 - u0u = tuv0 - uv0 - u1u = tuv0 - uv1 - dir0 = u0u1.cross(u0u) > 0 - dir1 = u0u1.cross(u1u) > 0 - if (ini_geom["dir0"] != dir0) or (ini_geom["dir1"] != dir1): - return tuv1 - - return tuv0 - - -def calc_tri_vert(v0, v1, angle0, angle1): - """ - Calculate rest coordinate from other coordinates and angle of end - """ - angle = math.pi - angle0 - angle1 - - alpha = atan2(v1.y - v0.y, v1.x - v0.x) - d = (v1.x - v0.x) / cos(alpha) - a = d * sin(angle0) / sin(angle) - b = d * sin(angle1) / sin(angle) - s = (a + b + d) / 2.0 - if fabs(d) < 0.0000001: - xd = 0 - yd = 0 - else: - r = s * (s - a) * (s - b) * (s - d) - if r < 0: - xd = 0 - yd = 0 - else: - xd = (b * b - a * a + d * d) / (2 * d) - yd = 2 * sqrt(r) / d - x1 = xd * cos(alpha) - yd * sin(alpha) + v0.x - y1 = xd * sin(alpha) + yd * cos(alpha) + v0.y - x2 = xd * cos(alpha) + yd * sin(alpha) + v0.x - y2 = xd * sin(alpha) - yd * cos(alpha) + v0.y - - return Vector((x1, y1)), Vector((x2, y2)) - - -def is_valid_context(context): - obj = context.object - - # only edit mode is allowed to execute - if obj is None: - return False - if obj.type != 'MESH': - return False - if context.object.mode != 'EDIT': - return False - - # only 'VIEW_3D' space is allowed to execute - for space in context.area.spaces: - if space.type == 'VIEW_3D': - break - else: - return False - - return True - - -class 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: Lock Texture - """ - - 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.texture_lock - obj = bpy.context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.verts.ensure_lookup_table() - bm.edges.ensure_lookup_table() - bm.faces.ensure_lookup_table() - - if not bm.loops.layers.uv: - self.report( - {'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - - props.verts_orig = [ - {"vidx": v.index, "vco": v.co.copy(), "moved": False} - for v in bm.verts if v.select] - - return {'FINISHED'} - - -class OperatorUnlock(bpy.types.Operator): - """ - Operation class: Unlock Texture - """ - - bl_idname = "uv.muv_texture_lock_operator_unlock" - bl_label = "Unlock Texture" - bl_description = "Unlock Texture" - bl_options = {'REGISTER', 'UNDO'} - - connect = BoolProperty( - name="Connect UV", - default=True - ) - - @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.texture_lock - obj = bpy.context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.verts.ensure_lookup_table() - bm.edges.ensure_lookup_table() - bm.faces.ensure_lookup_table() - - if not bm.loops.layers.uv: - self.report( - {'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - - verts = [v.index for v in bm.verts if v.select] - verts_orig = props.verts_orig - - # move UV followed by vertex coordinate - for vidx, v_orig in zip(verts, verts_orig): - if vidx != v_orig["vidx"]: - self.report({'ERROR'}, "Internal Error") - return {"CANCELLED"} - - v = bm.verts[vidx] - link_loops = get_link_loops(v) - - result = [] - - for ll in link_loops: - ini_geom = get_ini_geom(ll, uv_layer, verts_orig, v_orig) - target_uv = get_target_uv( - ll, uv_layer, verts_orig, v, ini_geom) - result.append({"l": ll["l"], "uv": target_uv}) - - # connect other face's UV - if self.connect: - ave = Vector((0.0, 0.0)) - for r in result: - ave = ave + r["uv"] - ave = ave / len(result) - for r in result: - r["l"][uv_layer].uv = ave - else: - for r in result: - r["l"][uv_layer].uv = r["uv"] - v_orig["moved"] = True - bmesh.update_edit_mesh(obj.data) - - props.verts_orig = None - - return {'FINISHED'} - - -class OperatorIntr(bpy.types.Operator): - """ - Operation class: Texture Lock (Interactive mode) - """ - - bl_idname = "uv.muv_texture_lock_operator_intr" - bl_label = "Texture Lock (Interactive mode)" - bl_description = "Internal operation for Texture Lock (Interactive mode)" - - __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.__intr_verts_orig = [] - self.__intr_verts = [] - - def __sel_verts_changed(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.verts.ensure_lookup_table() - bm.edges.ensure_lookup_table() - bm.faces.ensure_lookup_table() - - prev = set(self.__intr_verts) - now = set([v.index for v in bm.verts if v.select]) - - return prev != now - - def __reinit_verts(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.verts.ensure_lookup_table() - bm.edges.ensure_lookup_table() - bm.faces.ensure_lookup_table() - - self.__intr_verts_orig = [ - {"vidx": v.index, "vco": v.co.copy(), "moved": False} - for v in bm.verts if v.select] - self.__intr_verts = [v.index for v in bm.verts if v.select] - - def __update_uv(self, context): - """ - Update UV when vertex coordinates are changed - """ - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.verts.ensure_lookup_table() - bm.edges.ensure_lookup_table() - bm.faces.ensure_lookup_table() - - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") - return - uv_layer = bm.loops.layers.uv.verify() - - verts = [v.index for v in bm.verts if v.select] - verts_orig = self.__intr_verts_orig - - for vidx, v_orig in zip(verts, verts_orig): - if vidx != v_orig["vidx"]: - self.report({'ERROR'}, "Internal Error") - return - - v = bm.verts[vidx] - link_loops = get_link_loops(v) - - result = [] - for ll in link_loops: - ini_geom = get_ini_geom(ll, uv_layer, verts_orig, v_orig) - target_uv = get_target_uv( - ll, uv_layer, verts_orig, v, ini_geom) - result.append({"l": ll["l"], "uv": target_uv}) - - # UV connect option is always true, because it raises - # unexpected behavior - ave = Vector((0.0, 0.0)) - for r in result: - ave = ave + r["uv"] - ave = ave / len(result) - for r in result: - r["l"][uv_layer].uv = ave - v_orig["moved"] = True - bmesh.update_edit_mesh(obj.data) - - common.redraw_all_areas() - self.__intr_verts_orig = [ - {"vidx": v.index, "vco": v.co.copy(), "moved": False} - for v in bm.verts if v.select] - - def modal(self, context, event): - if not is_valid_context(context): - OperatorIntr.handle_remove(context) - return {'FINISHED'} - - if not OperatorIntr.is_running(context): - return {'FINISHED'} - - if context.area: - context.area.tag_redraw() - - if event.type == 'TIMER': - if self.__sel_verts_changed(context): - self.__reinit_verts(context) - else: - self.__update_uv(context) - - return {'PASS_THROUGH'} - - def invoke(self, context, _): - if not is_valid_context(context): - return {'CANCELLED'} - - if not OperatorIntr.is_running(context): - OperatorIntr.handle_add(self, context) - return {'RUNNING_MODAL'} - else: - OperatorIntr.handle_remove(context) - - 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 deleted file mode 100644 index bdf0ad67..00000000 --- a/uv_magic_uv/op/texture_projection.py +++ /dev/null @@ -1,395 +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.2" -__date__ = "17 Nov 2018" - -from collections import namedtuple - -import bpy -import bgl -import bmesh -import mathutils -from bpy_extras import view3d_utils -from bpy.props import ( - BoolProperty, - 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 - """ - sc = context.scene - prefs = context.user_preferences.addons["uv_magic_uv"].preferences - - region_w = context.region.width - region_h = context.region.height - canvas_w = region_w - prefs.texture_projection_canvas_padding[0] * 2.0 - canvas_h = region_h - prefs.texture_projection_canvas_padding[1] * 2.0 - - img = bpy.data.images[sc.muv_texture_projection_tex_image] - tex_w = img.size[0] - tex_h = img.size[1] - - center_x = region_w * 0.5 - center_y = region_h * 0.5 - - if sc.muv_texture_projection_adjust_window: - ratio_x = canvas_w / tex_w - ratio_y = canvas_h / tex_h - if sc.muv_texture_projection_apply_tex_aspect: - ratio = ratio_y if ratio_x > ratio_y else ratio_x - len_x = ratio * tex_w - len_y = ratio * tex_h - else: - len_x = canvas_w - len_y = canvas_h - else: - if sc.muv_texture_projection_apply_tex_aspect: - len_x = tex_w * magnitude - len_y = tex_h * magnitude - else: - len_x = region_w * magnitude - len_y = region_h * magnitude - - x0 = int(center_x - len_x * 0.5) - y0 = int(center_y - len_y * 0.5) - x1 = int(center_x + len_x * 0.5) - y1 = int(center_y + len_y * 0.5) - - return Rect(x0, y0, x1, y1) - - -def rect_to_rect2(rect): - """ - Convert Rect1 to Rect2 - """ - - return Rect2(rect.x0, rect.y0, rect.x1 - rect.x0, rect.y1 - rect.y0) - - -def region_to_canvas(rg_vec, canvas): - """ - Convert screen region to canvas - """ - - cv_rect = rect_to_rect2(canvas) - cv_vec = mathutils.Vector() - cv_vec.x = (rg_vec.x - cv_rect.x) / cv_rect.width - cv_vec.y = (rg_vec.y - cv_rect.y) / cv_rect.height - - return cv_vec - - -def is_valid_context(context): - obj = context.object - - # only edit mode is allowed to execute - if obj is None: - return False - if obj.type != 'MESH': - return False - if context.object.mode != 'EDIT': - return False - - # only 'VIEW_3D' space is allowed to execute - for space in context.area.spaces: - if space.type == 'VIEW_3D': - break - else: - return False - - return True - - -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: Texture Projection - Render texture - """ - - bl_idname = "uv.muv_texture_projection_operator" - bl_description = "Render selected texture" - bl_label = "Texture renderer" - - __handle = None - - @classmethod - def poll(cls, context): - # we can not get area/space/region from console - if common.is_console_mode(): - return False - return 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') - - @classmethod - def handle_remove(cls): - if cls.__handle is not None: - bpy.types.SpaceView3D.draw_handler_remove(cls.__handle, 'WINDOW') - cls.__handle = None - - @classmethod - def draw_texture(cls, _, context): - sc = context.scene - - if not cls.is_running(context): - return - - # no textures are selected - if sc.muv_texture_projection_tex_image == "None": - return - - # get texture to be renderred - img = bpy.data.images[sc.muv_texture_projection_tex_image] - - # setup rendering region - rect = get_canvas(context, sc.muv_texture_projection_tex_magnitude) - positions = [ - [rect.x0, rect.y0], - [rect.x0, rect.y1], - [rect.x1, rect.y1], - [rect.x1, rect.y0] - ] - tex_coords = [ - [0.0, 0.0], - [0.0, 1.0], - [1.0, 1.0], - [1.0, 0.0] - ] - - # OpenGL configuration - bgl.glEnable(bgl.GL_BLEND) - bgl.glEnable(bgl.GL_TEXTURE_2D) - if img.bindcode: - bind = img.bindcode[0] - bgl.glBindTexture(bgl.GL_TEXTURE_2D, bind) - bgl.glTexParameteri( - bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MIN_FILTER, bgl.GL_LINEAR) - bgl.glTexParameteri( - bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MAG_FILTER, bgl.GL_LINEAR) - bgl.glTexEnvi( - bgl.GL_TEXTURE_ENV, bgl.GL_TEXTURE_ENV_MODE, bgl.GL_MODULATE) - - # render texture - bgl.glBegin(bgl.GL_QUADS) - bgl.glColor4f(1.0, 1.0, 1.0, - sc.muv_texture_projection_tex_transparency) - for (v1, v2), (u, v) in zip(positions, tex_coords): - bgl.glTexCoord2f(u, v) - bgl.glVertex2f(v1, v2) - bgl.glEnd() - - def invoke(self, context, _): - if not Operator.is_running(context): - Operator.handle_add(self, context) - else: - Operator.handle_remove() - - if context.area: - context.area.tag_redraw() - - return {'FINISHED'} - - -class OperatorProject(bpy.types.Operator): - """ - Operation class: Project texture - """ - - bl_idname = "uv.muv_texture_projection_operator_project" - bl_label = "Project Texture" - bl_description = "Project Texture" - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context): - # we can not get area/space/region from console - if common.is_console_mode(): - return True - if not Operator.is_running(context): - return False - return is_valid_context(context) - - def execute(self, context): - sc = context.scene - - if sc.muv_texture_projection_tex_image == "None": - self.report({'WARNING'}, "No textures are selected") - return {'CANCELLED'} - - _, region, space = common.get_space( - 'VIEW_3D', 'WINDOW', 'VIEW_3D') - - # get faces to be texture projected - obj = context.active_object - world_mat = obj.matrix_world - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - - # get UV and texture layer - if not bm.loops.layers.uv: - if sc.muv_texture_projection_assign_uvmap: - bm.loops.layers.uv.new() - else: - self.report({'WARNING'}, - "Object must have more than one UV map") - return {'CANCELLED'} - - uv_layer = bm.loops.layers.uv.verify() - tex_layer = bm.faces.layers.tex.verify() - - sel_faces = [f for f in bm.faces if f.select] - - # transform 3d space to screen region - v_screen = [ - view3d_utils.location_3d_to_region_2d( - region, - space.region_3d, - world_mat * l.vert.co) - for f in sel_faces for l in f.loops - ] - - # transform screen region to canvas - v_canvas = [ - region_to_canvas( - v, - get_canvas(bpy.context, - sc.muv_texture_projection_tex_magnitude) - ) for v in v_screen - ] - - # project texture to object - i = 0 - for f in sel_faces: - f[tex_layer].image = \ - bpy.data.images[sc.muv_texture_projection_tex_image] - for l in f.loops: - l[uv_layer].uv = v_canvas[i].to_2d() - i = i + 1 - - common.redraw_all_areas() - bmesh.update_edit_mesh(obj.data) - - return {'FINISHED'} diff --git a/uv_magic_uv/op/texture_wrap.py b/uv_magic_uv/op/texture_wrap.py deleted file mode 100644 index a7c58847..00000000 --- a/uv_magic_uv/op/texture_wrap.py +++ /dev/null @@ -1,294 +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.2" -__date__ = "17 Nov 2018" - -import bpy -import bmesh -from bpy.props import ( - BoolProperty, -) - -from .. import common - - -__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_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.texture_wrap - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - - sel_faces = [f for f in bm.faces if f.select] - if len(sel_faces) != 1: - self.report({'WARNING'}, "Must select only one face") - return {'CANCELLED'} - - props.ref_face_index = sel_faces[0].index - props.ref_obj = obj - - return {'FINISHED'} - - -class OperatorSet(bpy.types.Operator): - """ - Operation class: Set UV - """ - - bl_idname = "uv.muv_texture_wrap_operator_set" - bl_label = "Set" - bl_description = "Set UV" - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context): - # we can not get area/space/region from console - if common.is_console_mode(): - return True - sc = context.scene - props = sc.muv_props.texture_wrap - if not props.ref_obj: - return False - return is_valid_context(context) - - def execute(self, context): - sc = context.scene - props = sc.muv_props.texture_wrap - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - - if sc.muv_texture_wrap_selseq: - sel_faces = [] - for hist in bm.select_history: - if isinstance(hist, bmesh.types.BMFace) and hist.select: - sel_faces.append(hist) - if not sel_faces: - self.report({'WARNING'}, "Must select more than one face") - return {'CANCELLED'} - else: - sel_faces = [f for f in bm.faces if f.select] - if len(sel_faces) != 1: - self.report({'WARNING'}, "Must select only one face") - return {'CANCELLED'} - - ref_face_index = props.ref_face_index - for face in sel_faces: - tgt_face_index = face.index - if ref_face_index == tgt_face_index: - self.report({'WARNING'}, "Must select different face") - return {'CANCELLED'} - - if props.ref_obj != obj: - self.report({'WARNING'}, "Object must be same") - return {'CANCELLED'} - - ref_face = bm.faces[ref_face_index] - tgt_face = bm.faces[tgt_face_index] - - # get common vertices info - common_verts = [] - for sl in ref_face.loops: - for dl in tgt_face.loops: - if sl.vert == dl.vert: - info = {"vert": sl.vert, "ref_loop": sl, - "tgt_loop": dl} - common_verts.append(info) - break - - if len(common_verts) != 2: - self.report({'WARNING'}, - "2 vertices must be shared among faces") - return {'CANCELLED'} - - # get reference other vertices info - ref_other_verts = [] - for sl in ref_face.loops: - for ci in common_verts: - if sl.vert == ci["vert"]: - break - else: - info = {"vert": sl.vert, "loop": sl} - ref_other_verts.append(info) - - if not ref_other_verts: - self.report({'WARNING'}, "More than 1 vertex must be unshared") - return {'CANCELLED'} - - # get reference info - ref_info = {} - cv0 = common_verts[0]["vert"].co - cv1 = common_verts[1]["vert"].co - cuv0 = common_verts[0]["ref_loop"][uv_layer].uv - cuv1 = common_verts[1]["ref_loop"][uv_layer].uv - ov0 = ref_other_verts[0]["vert"].co - ouv0 = ref_other_verts[0]["loop"][uv_layer].uv - ref_info["vert_vdiff"] = cv1 - cv0 - ref_info["uv_vdiff"] = cuv1 - cuv0 - ref_info["vert_hdiff"], _ = common.diff_point_to_segment( - cv0, cv1, ov0) - ref_info["uv_hdiff"], _ = common.diff_point_to_segment( - cuv0, cuv1, ouv0) - - # get target other vertices info - tgt_other_verts = [] - for dl in tgt_face.loops: - for ci in common_verts: - if dl.vert == ci["vert"]: - break - else: - info = {"vert": dl.vert, "loop": dl} - tgt_other_verts.append(info) - - if not tgt_other_verts: - self.report({'WARNING'}, "More than 1 vertex must be unshared") - return {'CANCELLED'} - - # get target info - for info in tgt_other_verts: - cv0 = common_verts[0]["vert"].co - cv1 = common_verts[1]["vert"].co - cuv0 = common_verts[0]["ref_loop"][uv_layer].uv - ov = info["vert"].co - info["vert_hdiff"], x = common.diff_point_to_segment( - cv0, cv1, ov) - info["vert_vdiff"] = x - common_verts[0]["vert"].co - - # calclulate factor - fact_h = -info["vert_hdiff"].length / \ - ref_info["vert_hdiff"].length - fact_v = info["vert_vdiff"].length / \ - ref_info["vert_vdiff"].length - duv_h = ref_info["uv_hdiff"] * fact_h - duv_v = ref_info["uv_vdiff"] * fact_v - - # get target UV - info["target_uv"] = cuv0 + duv_h + duv_v - - # apply to common UVs - for info in common_verts: - info["tgt_loop"][uv_layer].uv = \ - info["ref_loop"][uv_layer].uv.copy() - # apply to other UVs - for info in tgt_other_verts: - info["loop"][uv_layer].uv = info["target_uv"] - - common.debug_print("===== Target Other Vertices =====") - common.debug_print(tgt_other_verts) - - bmesh.update_edit_mesh(obj.data) - - ref_face_index = tgt_face_index - - if sc.muv_texture_wrap_set_and_refer: - props.ref_face_index = tgt_face_index - - return {'FINISHED'} diff --git a/uv_magic_uv/op/transfer_uv.py b/uv_magic_uv/op/transfer_uv.py index ef6fc3be..db05b343 100644 --- a/uv_magic_uv/op/transfer_uv.py +++ b/uv_magic_uv/op/transfer_uv.py @@ -23,43 +23,27 @@ __status__ = "production" __version__ = "5.2" __date__ = "17 Nov 2018" -from collections import OrderedDict - import bpy import bmesh from bpy.props import BoolProperty from .. import common +from ..impl import transfer_uv_impl as impl +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry __all__ = [ - 'OperatorCopyUV', - 'OperatorPasteUV', + 'Properties', + 'MUV_OT_TransferUV_CopyUV', + 'MUV_OT_TransferUV_PasteUV', ] -def is_valid_context(context): - obj = context.object - - # only edit mode is allowed to execute - if obj is None: - return False - if obj.type != 'MESH': - return False - if context.object.mode != 'EDIT': - return False - - # only 'VIEW_3D' space is allowed to execute - for space in context.area.spaces: - if space.type == 'VIEW_3D': - break - else: - return False - - return True - - +@PropertyClassRegistry() class Properties: + idname = "transfer_uv" + @classmethod def init_props(cls, scene): class Props(): @@ -90,7 +74,8 @@ class Properties: del scene.muv_transfer_uv_copy_seams -class OperatorCopyUV(bpy.types.Operator): +@BlClassRegistry() +class MUV_OT_TransferUV_CopyUV(bpy.types.Operator): """ Operation class: Transfer UV copy Topological based copy @@ -106,56 +91,30 @@ class OperatorCopyUV(bpy.types.Operator): # we can not get area/space/region from console if common.is_console_mode(): return True - return is_valid_context(context) + return impl.is_valid_context(context) def execute(self, context): props = context.scene.muv_props.transfer_uv - active_obj = context.scene.objects.active + active_obj = context.active_object bm = bmesh.from_edit_mesh(active_obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() + bm.faces.ensure_lookup_table() - # get UV layer - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") + uv_layer = impl.get_uv_layer(self, bm) + if uv_layer is None: return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - props.topology_copied = [] - - # get selected faces - active_face = bm.faces.active - sel_faces = [face for face in bm.faces if face.select] - if len(sel_faces) != 2: - self.report({'WARNING'}, "Two faces must be selected") - return {'CANCELLED'} - if not active_face or active_face not in sel_faces: - self.report({'WARNING'}, "Two faces must be active") - return {'CANCELLED'} - - # parse all faces according to selection - active_face_nor = active_face.normal.copy() - all_sorted_faces = main_parse( - self, uv_layer, sel_faces, active_face, - active_face_nor) - - if all_sorted_faces: - for face_data in all_sorted_faces.values(): - edges = face_data[1] - uv_loops = face_data[2] - uvs = [l.uv.copy() for l in uv_loops] - pin_uvs = [l.pin_uv for l in uv_loops] - seams = [e.seam for e in edges] - props.topology_copied.append([uvs, pin_uvs, seams]) - else: + faces = impl.get_selected_src_faces(self, bm, uv_layer) + if faces is None: return {'CANCELLED'} + props.topology_copied = faces bmesh.update_edit_mesh(active_obj.data) return {'FINISHED'} -class OperatorPasteUV(bpy.types.Operator): +@BlClassRegistry() +class MUV_OT_TransferUV_PasteUV(bpy.types.Operator): """ Operation class: Transfer UV paste Topological based paste @@ -166,12 +125,12 @@ class OperatorPasteUV(bpy.types.Operator): bl_description = "Transfer UV Paste UV (Topological based paste)" bl_options = {'REGISTER', 'UNDO'} - invert_normals = BoolProperty( + invert_normals: BoolProperty( name="Invert Normals", description="Invert Normals", default=False ) - copy_seams = BoolProperty( + copy_seams: BoolProperty( name="Copy Seams", description="Copy Seams", default=True @@ -186,253 +145,24 @@ class OperatorPasteUV(bpy.types.Operator): props = sc.muv_props.transfer_uv if not props.topology_copied: return False - return is_valid_context(context) + return impl.is_valid_context(context) def execute(self, context): props = context.scene.muv_props.transfer_uv - active_obj = context.scene.objects.active + active_obj = context.active_object bm = bmesh.from_edit_mesh(active_obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() + bm.faces.ensure_lookup_table() # get UV layer - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") + uv_layer = impl.get_uv_layer(self, bm) + if uv_layer is None: return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - # get selection history - all_sel_faces = [ - e for e in bm.select_history - if isinstance(e, bmesh.types.BMFace) and e.select] - if len(all_sel_faces) % 2 != 0: - self.report({'WARNING'}, "Two faces must be selected") + ret = impl.paste_uv(self, bm, uv_layer, props.topology_copied, + self.invert_normals, self.copy_seams) + if ret: return {'CANCELLED'} - # parse selection history - for i, _ in enumerate(all_sel_faces): - if (i == 0) or (i % 2 == 0): - continue - sel_faces = [all_sel_faces[i - 1], all_sel_faces[i]] - active_face = all_sel_faces[i] - - # parse all faces according to selection history - active_face_nor = active_face.normal.copy() - if self.invert_normals: - active_face_nor.negate() - all_sorted_faces = main_parse( - self, uv_layer, sel_faces, active_face, - active_face_nor) - - if all_sorted_faces: - # check amount of copied/pasted faces - if len(all_sorted_faces) != len(props.topology_copied): - self.report( - {'WARNING'}, - "Mesh has different amount of faces" - ) - return {'CANCELLED'} - - for j, face_data in enumerate(all_sorted_faces.values()): - copied_data = props.topology_copied[j] - - # check amount of copied/pasted verts - if len(copied_data[0]) != len(face_data[2]): - bpy.ops.mesh.select_all(action='DESELECT') - # select problematic face - list(all_sorted_faces.keys())[j].select = True - self.report( - {'WARNING'}, - "Face have different amount of vertices" - ) - return {'FINISHED'} - - for k, (edge, uvloop) in enumerate(zip(face_data[1], - face_data[2])): - uvloop.uv = copied_data[0][k] - uvloop.pin_uv = copied_data[1][k] - if self.copy_seams: - edge.seam = copied_data[2][k] - else: - return {'CANCELLED'} - bmesh.update_edit_mesh(active_obj.data) - if self.copy_seams: - active_obj.data.show_edge_seams = True return {'FINISHED'} - - -def main_parse( - self, uv_layer, sel_faces, - active_face, active_face_nor): - all_sorted_faces = OrderedDict() # This is the main stuff - - used_verts = set() - used_edges = set() - - faces_to_parse = [] - - # get shared edge of two faces - cross_edges = [] - for edge in active_face.edges: - if edge in sel_faces[0].edges and edge in sel_faces[1].edges: - cross_edges.append(edge) - - # parse two selected faces - if cross_edges and len(cross_edges) == 1: - shared_edge = cross_edges[0] - vert1 = None - vert2 = None - - dot_n = active_face_nor.normalized() - edge_vec_1 = (shared_edge.verts[1].co - shared_edge.verts[0].co) - edge_vec_len = edge_vec_1.length - edge_vec_1 = edge_vec_1.normalized() - - af_center = active_face.calc_center_median() - af_vec = shared_edge.verts[0].co + (edge_vec_1 * (edge_vec_len * 0.5)) - af_vec = (af_vec - af_center).normalized() - - if af_vec.cross(edge_vec_1).dot(dot_n) > 0: - vert1 = shared_edge.verts[0] - vert2 = shared_edge.verts[1] - else: - vert1 = shared_edge.verts[1] - vert2 = shared_edge.verts[0] - - # get active face stuff and uvs - face_stuff = get_other_verts_edges( - active_face, vert1, vert2, shared_edge, uv_layer) - all_sorted_faces[active_face] = face_stuff - used_verts.update(active_face.verts) - used_edges.update(active_face.edges) - - # get first selected face stuff and uvs as they share shared_edge - second_face = sel_faces[0] - if second_face is active_face: - second_face = sel_faces[1] - face_stuff = get_other_verts_edges( - second_face, vert1, vert2, shared_edge, uv_layer) - all_sorted_faces[second_face] = face_stuff - used_verts.update(second_face.verts) - used_edges.update(second_face.edges) - - # first Grow - faces_to_parse.append(active_face) - faces_to_parse.append(second_face) - - else: - self.report({'WARNING'}, "Two faces should share one edge") - return None - - # parse all faces - while True: - new_parsed_faces = [] - if not faces_to_parse: - break - for face in faces_to_parse: - face_stuff = all_sorted_faces.get(face) - new_faces = parse_faces( - face, face_stuff, used_verts, used_edges, all_sorted_faces, - uv_layer) - if new_faces == 'CANCELLED': - self.report({'WARNING'}, "More than 2 faces share edge") - return None - - new_parsed_faces += new_faces - faces_to_parse = new_parsed_faces - - return all_sorted_faces - - -def parse_faces( - check_face, face_stuff, used_verts, used_edges, all_sorted_faces, - uv_layer): - """recurse faces around the new_grow only""" - - new_shared_faces = [] - for sorted_edge in face_stuff[1]: - shared_faces = sorted_edge.link_faces - if shared_faces: - if len(shared_faces) > 2: - bpy.ops.mesh.select_all(action='DESELECT') - for face_sel in shared_faces: - face_sel.select = True - shared_faces = [] - return 'CANCELLED' - - clear_shared_faces = get_new_shared_faces( - check_face, sorted_edge, shared_faces, all_sorted_faces.keys()) - if clear_shared_faces: - shared_face = clear_shared_faces[0] - # get vertices of the edge - vert1 = sorted_edge.verts[0] - vert2 = sorted_edge.verts[1] - - common.debug_print(face_stuff[0], vert1, vert2) - if face_stuff[0].index(vert1) > face_stuff[0].index(vert2): - vert1 = sorted_edge.verts[1] - vert2 = sorted_edge.verts[0] - - common.debug_print(shared_face.verts, vert1, vert2) - new_face_stuff = get_other_verts_edges( - shared_face, vert1, vert2, sorted_edge, uv_layer) - all_sorted_faces[shared_face] = new_face_stuff - used_verts.update(shared_face.verts) - used_edges.update(shared_face.edges) - - if common.is_debug_mode(): - shared_face.select = True # test which faces are parsed - - new_shared_faces.append(shared_face) - - return new_shared_faces - - -def get_new_shared_faces(orig_face, shared_edge, check_faces, used_faces): - shared_faces = [] - - for face in check_faces: - is_shared_edge = shared_edge in face.edges - not_used = face not in used_faces - not_orig = face is not orig_face - not_hide = face.hide is False - if is_shared_edge and not_used and not_orig and not_hide: - shared_faces.append(face) - - return shared_faces - - -def get_other_verts_edges(face, vert1, vert2, first_edge, uv_layer): - face_edges = [first_edge] - face_verts = [vert1, vert2] - face_loops = [] - - other_edges = [edge for edge in face.edges if edge not in face_edges] - - for _ in range(len(other_edges)): - found_edge = None - # get sorted verts and edges - for edge in other_edges: - if face_verts[-1] in edge.verts: - other_vert = edge.other_vert(face_verts[-1]) - - if other_vert not in face_verts: - face_verts.append(other_vert) - - found_edge = edge - if found_edge not in face_edges: - face_edges.append(edge) - break - - other_edges.remove(found_edge) - - # get sorted uvs - for vert in face_verts: - for loop in face.loops: - if loop.vert is vert: - face_loops.append(loop[uv_layer]) - break - - return [face_verts, face_edges, face_loops] diff --git a/uv_magic_uv/op/unwrap_constraint.py b/uv_magic_uv/op/unwrap_constraint.py deleted file mode 100644 index b2368fc4..00000000 --- a/uv_magic_uv/op/unwrap_constraint.py +++ /dev/null @@ -1,184 +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.2" -__date__ = "17 Nov 2018" - -import bpy -import bmesh -from bpy.props import ( - BoolProperty, - EnumProperty, - FloatProperty, -) - -from .. import common - - -__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_operator" - bl_label = "Unwrap Constraint" - bl_description = "Unwrap while keeping uv coordinate" - bl_options = {'REGISTER', 'UNDO'} - - # property for original unwrap - method = EnumProperty( - name="Method", - description="Unwrapping method", - items=[ - ('ANGLE_BASED', 'Angle Based', 'Angle Based'), - ('CONFORMAL', 'Conformal', 'Conformal') - ], - default='ANGLE_BASED') - fill_holes = BoolProperty( - name="Fill Holes", - description="Virtual fill holes in meshes before unwrapping", - default=True) - correct_aspect = BoolProperty( - name="Correct Aspect", - description="Map UVs taking image aspect ratio into account", - default=True) - use_subsurf_data = BoolProperty( - name="Use Subsurf Modifier", - description="""Map UVs taking vertex position after subsurf - into account""", - default=False) - margin = FloatProperty( - name="Margin", - description="Space between islands", - max=1.0, - min=0.0, - default=0.001) - - # property for this operation - u_const = BoolProperty( - name="U-Constraint", - description="Keep UV U-axis coordinate", - default=False - ) - v_const = BoolProperty( - name="V-Constraint", - description="Keep UV V-axis coordinate", - default=False - ) - - @classmethod - def poll(cls, context): - # we can not get area/space/region from console - if common.is_console_mode(): - return True - return is_valid_context(context) - - def execute(self, _): - obj = bpy.context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - - # bpy.ops.uv.unwrap() makes one UV map at least - if not bm.loops.layers.uv: - self.report({'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - - # get original UV coordinate - faces = [f for f in bm.faces if f.select] - uv_list = [] - for f in faces: - uvs = [l[uv_layer].uv.copy() for l in f.loops] - uv_list.append(uvs) - - # unwrap - bpy.ops.uv.unwrap( - method=self.method, - fill_holes=self.fill_holes, - correct_aspect=self.correct_aspect, - use_subsurf_data=self.use_subsurf_data, - margin=self.margin) - - # when U/V-Constraint is checked, revert original coordinate - for f, uvs in zip(faces, uv_list): - for l, uv in zip(f.loops, uvs): - if self.u_const: - l[uv_layer].uv.x = uv.x - if self.v_const: - l[uv_layer].uv.y = uv.y - - # update mesh - bmesh.update_edit_mesh(obj.data) - - return {'FINISHED'} diff --git a/uv_magic_uv/op/uv_bounding_box.py b/uv_magic_uv/op/uv_bounding_box.py deleted file mode 100644 index 4aa8874b..00000000 --- a/uv_magic_uv/op/uv_bounding_box.py +++ /dev/null @@ -1,832 +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.2" -__date__ = "17 Nov 2018" - -from enum import IntEnum -import math - -import bpy -import bgl -import mathutils -import bmesh -from bpy.props import BoolProperty, EnumProperty - -from .. import common - - -__all__ = [ - 'Properties', - 'Operator', -] - - -MAX_VALUE = 100000.0 - - -def is_valid_context(context): - obj = context.object - - # only edit mode is allowed to execute - if obj is None: - return False - if obj.type != 'MESH': - return False - if context.object.mode != 'EDIT': - return False - - # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. - # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf - # after the execution - for space in context.area.spaces: - if (space.type == 'IMAGE_EDITOR') or (space.type == 'VIEW_3D'): - break - else: - return False - - return True - - -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 - """ - - def __init__(self): - self.op = 'NONE' # operation - - def to_matrix(self): - # mat = I - mat = mathutils.Matrix() - mat.identity() - return mat - - -class TranslationCommand(CommandBase): - """ - Custom class: Translation operation - """ - - def __init__(self, ix, iy): - super().__init__() - self.op = 'TRANSLATION' - self.__x = ix # current x - self.__y = iy # current y - self.__ix = ix # initial x - self.__iy = iy # initial y - - def to_matrix(self): - # mat = Mt - dx = self.__x - self.__ix - dy = self.__y - self.__iy - return mathutils.Matrix.Translation((dx, dy, 0)) - - def set(self, x, y): - self.__x = x - self.__y = y - - -class RotationCommand(CommandBase): - """ - Custom class: Rotation operation - """ - - def __init__(self, ix, iy, cx, cy): - super().__init__() - self.op = 'ROTATION' - self.__x = ix # current x - self.__y = iy # current y - self.__cx = cx # center of rotation x - self.__cy = cy # center of rotation y - dx = self.__x - self.__cx - dy = self.__y - self.__cy - self.__iangle = math.atan2(dy, dx) # initial rotation angle - - def to_matrix(self): - # mat = Mt * Mr * Mt^-1 - dx = self.__x - self.__cx - dy = self.__y - self.__cy - angle = math.atan2(dy, dx) - self.__iangle - mti = mathutils.Matrix.Translation((-self.__cx, -self.__cy, 0.0)) - mr = mathutils.Matrix.Rotation(angle, 4, 'Z') - mt = mathutils.Matrix.Translation((self.__cx, self.__cy, 0.0)) - return mt * mr * mti - - def set(self, x, y): - self.__x = x - self.__y = y - - -class ScalingCommand(CommandBase): - """ - Custom class: Scaling operation - """ - - def __init__(self, ix, iy, ox, oy, dir_x, dir_y, mat): - super().__init__() - self.op = 'SCALING' - self.__ix = ix # initial x - self.__iy = iy # initial y - self.__x = ix # current x - self.__y = iy # current y - self.__ox = ox # origin of scaling x - self.__oy = oy # origin of scaling y - self.__dir_x = dir_x # direction of scaling x - self.__dir_y = dir_y # direction of scaling y - self.__mat = mat - # initial origin of scaling = M(to original transform) * (ox, oy) - iov = mat * mathutils.Vector((ox, oy, 0.0)) - self.__iox = iov.x # initial origin of scaling X - self.__ioy = iov.y # initial origin of scaling y - - def to_matrix(self): - """ - mat = M(to original transform)^-1 * Mt(to origin) * Ms * - Mt(to origin)^-1 * M(to original transform) - """ - m = self.__mat - mi = self.__mat.inverted() - mtoi = mathutils.Matrix.Translation((-self.__iox, -self.__ioy, 0.0)) - mto = mathutils.Matrix.Translation((self.__iox, self.__ioy, 0.0)) - # every point must be transformed to origin - t = m * mathutils.Vector((self.__ix, self.__iy, 0.0)) - tix, tiy = t.x, t.y - t = m * mathutils.Vector((self.__ox, self.__oy, 0.0)) - tox, toy = t.x, t.y - t = m * mathutils.Vector((self.__x, self.__y, 0.0)) - tx, ty = t.x, t.y - ms = mathutils.Matrix() - ms.identity() - if self.__dir_x == 1: - ms[0][0] = (tx - tox) * self.__dir_x / (tix - tox) - if self.__dir_y == 1: - ms[1][1] = (ty - toy) * self.__dir_y / (tiy - toy) - return mi * mto * ms * mtoi * m - - def set(self, x, y): - self.__x = x - self.__y = y - - -class UniformScalingCommand(CommandBase): - """ - Custom class: Uniform Scaling operation - """ - - def __init__(self, ix, iy, ox, oy, mat): - super().__init__() - self.op = 'SCALING' - self.__ix = ix # initial x - self.__iy = iy # initial y - self.__x = ix # current x - self.__y = iy # current y - self.__ox = ox # origin of scaling x - self.__oy = oy # origin of scaling y - self.__mat = mat - # initial origin of scaling = M(to original transform) * (ox, oy) - iov = mat * mathutils.Vector((ox, oy, 0.0)) - self.__iox = iov.x # initial origin of scaling x - self.__ioy = iov.y # initial origin of scaling y - self.__dir_x = 1 - self.__dir_y = 1 - - def to_matrix(self): - """ - mat = M(to original transform)^-1 * Mt(to origin) * Ms * - Mt(to origin)^-1 * M(to original transform) - """ - m = self.__mat - mi = self.__mat.inverted() - mtoi = mathutils.Matrix.Translation((-self.__iox, -self.__ioy, 0.0)) - mto = mathutils.Matrix.Translation((self.__iox, self.__ioy, 0.0)) - # every point must be transformed to origin - t = m * mathutils.Vector((self.__ix, self.__iy, 0.0)) - tix, tiy = t.x, t.y - t = m * mathutils.Vector((self.__ox, self.__oy, 0.0)) - tox, toy = t.x, t.y - t = m * mathutils.Vector((self.__x, self.__y, 0.0)) - tx, ty = t.x, t.y - ms = mathutils.Matrix() - ms.identity() - tir = math.sqrt((tix - tox) * (tix - tox) + (tiy - toy) * (tiy - toy)) - tr = math.sqrt((tx - tox) * (tx - tox) + (ty - toy) * (ty - toy)) - - sr = tr / tir - - if ((tx - tox) * (tix - tox)) > 0: - self.__dir_x = 1 - else: - self.__dir_x = -1 - if ((ty - toy) * (tiy - toy)) > 0: - self.__dir_y = 1 - else: - self.__dir_y = -1 - - ms[0][0] = sr * self.__dir_x - ms[1][1] = sr * self.__dir_y - - return mi * mto * ms * mtoi * m - - def set(self, x, y): - self.__x = x - self.__y = y - - -class CommandExecuter(): - """ - Custom class: manage command history and execute command - """ - - def __init__(self): - self.__cmd_list = [] # history - self.__cmd_list_redo = [] # redo list - - def execute(self, begin=0, end=-1): - """ - create matrix from history - """ - mat = mathutils.Matrix() - mat.identity() - for i, cmd in enumerate(self.__cmd_list): - if begin <= i and (end == -1 or i <= end): - mat = cmd.to_matrix() * mat - return mat - - def undo_size(self): - """ - get history size - """ - return len(self.__cmd_list) - - def top(self): - """ - get top of history - """ - if len(self.__cmd_list) <= 0: - return None - return self.__cmd_list[-1] - - def append(self, cmd): - """ - append command - """ - self.__cmd_list.append(cmd) - self.__cmd_list_redo = [] - - def undo(self): - """ - undo command - """ - if len(self.__cmd_list) <= 0: - return - self.__cmd_list_redo.append(self.__cmd_list.pop()) - - def redo(self): - """ - redo command - """ - if len(self.__cmd_list_redo) <= 0: - return - self.__cmd_list.append(self.__cmd_list_redo.pop()) - - def pop(self): - if len(self.__cmd_list) <= 0: - return None - return self.__cmd_list.pop() - - def push(self, cmd): - self.__cmd_list.append(cmd) - - -class State(IntEnum): - """ - Enum: State definition used by MUV_UVBBStateMgr - """ - NONE = 0 - TRANSLATING = 1 - SCALING_1 = 2 - SCALING_2 = 3 - SCALING_3 = 4 - SCALING_4 = 5 - SCALING_5 = 6 - SCALING_6 = 7 - SCALING_7 = 8 - SCALING_8 = 9 - ROTATING = 10 - UNIFORM_SCALING_1 = 11 - UNIFORM_SCALING_2 = 12 - UNIFORM_SCALING_3 = 13 - UNIFORM_SCALING_4 = 14 - - -class StateBase(): - """ - Custom class: Base class of state - """ - - def __init__(self): - pass - - def update(self, context, event, ctrl_points, mouse_view): - raise NotImplementedError - - -class StateNone(StateBase): - """ - Custom class: - No state - Wait for event from mouse - """ - - def __init__(self, cmd_exec): - super().__init__() - self.__cmd_exec = cmd_exec - - def update(self, context, event, ctrl_points, mouse_view): - """ - Update state - """ - prefs = context.user_preferences.addons["uv_magic_uv"].preferences - cp_react_size = prefs.uv_bounding_box_cp_react_size - is_uscaling = context.scene.muv_uv_bounding_box_uniform_scaling - if (event.type == 'LEFTMOUSE') and (event.value == 'PRESS'): - x, y = context.region.view2d.view_to_region( - mouse_view.x, mouse_view.y) - for i, p in enumerate(ctrl_points): - px, py = context.region.view2d.view_to_region(p.x, p.y) - in_cp_x = (px + cp_react_size > x and - px - cp_react_size < x) - in_cp_y = (py + cp_react_size > y and - py - cp_react_size < y) - if in_cp_x and in_cp_y: - if is_uscaling: - arr = [1, 3, 6, 8] - if i in arr: - return ( - State.UNIFORM_SCALING_1 + - arr.index(i) - ) - else: - return State.TRANSLATING + i - - return State.NONE - - -class StateTranslating(StateBase): - """ - Custom class: Translating state - """ - - def __init__(self, cmd_exec, ctrl_points): - super().__init__() - self.__cmd_exec = cmd_exec - ix, iy = ctrl_points[0].x, ctrl_points[0].y - self.__cmd_exec.append(TranslationCommand(ix, iy)) - - def update(self, context, event, ctrl_points, mouse_view): - if event.type == 'LEFTMOUSE': - if event.value == 'RELEASE': - return State.NONE - if event.type == 'MOUSEMOVE': - x, y = mouse_view.x, mouse_view.y - self.__cmd_exec.top().set(x, y) - return State.TRANSLATING - - -class StateScaling(StateBase): - """ - Custom class: Scaling state - """ - - def __init__(self, cmd_exec, state, ctrl_points): - super().__init__() - self.__state = state - self.__cmd_exec = cmd_exec - dir_x_list = [1, 1, 1, 0, 0, 1, 1, 1] - dir_y_list = [1, 0, 1, 1, 1, 1, 0, 1] - idx = state - 2 - ix, iy = ctrl_points[idx + 1].x, ctrl_points[idx + 1].y - ox, oy = ctrl_points[8 - idx].x, ctrl_points[8 - idx].y - dir_x, dir_y = dir_x_list[idx], dir_y_list[idx] - mat = self.__cmd_exec.execute(end=self.__cmd_exec.undo_size()) - self.__cmd_exec.append( - ScalingCommand(ix, iy, ox, oy, dir_x, dir_y, mat.inverted())) - - def update(self, context, event, ctrl_points, mouse_view): - if event.type == 'LEFTMOUSE': - if event.value == 'RELEASE': - return State.NONE - if event.type == 'MOUSEMOVE': - x, y = mouse_view.x, mouse_view.y - self.__cmd_exec.top().set(x, y) - return self.__state - - -class StateUniformScaling(StateBase): - """ - Custom class: Uniform Scaling state - """ - - def __init__(self, cmd_exec, state, ctrl_points): - super().__init__() - self.__state = state - self.__cmd_exec = cmd_exec - icp_idx = [1, 3, 6, 8] - ocp_idx = [8, 6, 3, 1] - idx = state - State.UNIFORM_SCALING_1 - ix, iy = ctrl_points[icp_idx[idx]].x, ctrl_points[icp_idx[idx]].y - ox, oy = ctrl_points[ocp_idx[idx]].x, ctrl_points[ocp_idx[idx]].y - mat = self.__cmd_exec.execute(end=self.__cmd_exec.undo_size()) - self.__cmd_exec.append(UniformScalingCommand( - ix, iy, ox, oy, mat.inverted())) - - def update(self, context, event, ctrl_points, mouse_view): - if event.type == 'LEFTMOUSE': - if event.value == 'RELEASE': - return State.NONE - if event.type == 'MOUSEMOVE': - x, y = mouse_view.x, mouse_view.y - self.__cmd_exec.top().set(x, y) - - return self.__state - - -class StateRotating(StateBase): - """ - Custom class: Rotating state - """ - - def __init__(self, cmd_exec, ctrl_points): - super().__init__() - self.__cmd_exec = cmd_exec - ix, iy = ctrl_points[9].x, ctrl_points[9].y - ox, oy = ctrl_points[0].x, ctrl_points[0].y - self.__cmd_exec.append(RotationCommand(ix, iy, ox, oy)) - - def update(self, context, event, ctrl_points, mouse_view): - if event.type == 'LEFTMOUSE': - if event.value == 'RELEASE': - return State.NONE - if event.type == 'MOUSEMOVE': - x, y = mouse_view.x, mouse_view.y - self.__cmd_exec.top().set(x, y) - return State.ROTATING - - -class StateManager(): - """ - Custom class: Manage state about this feature - """ - - def __init__(self, cmd_exec): - self.__cmd_exec = cmd_exec # command executer - self.__state = State.NONE # current state - self.__state_obj = StateNone(self.__cmd_exec) - - def __update_state(self, next_state, ctrl_points): - """ - Update state - """ - - if next_state == self.__state: - return - obj = None - if next_state == State.TRANSLATING: - obj = StateTranslating(self.__cmd_exec, ctrl_points) - elif State.SCALING_1 <= next_state <= State.SCALING_8: - obj = StateScaling( - self.__cmd_exec, next_state, ctrl_points) - elif next_state == State.ROTATING: - obj = StateRotating(self.__cmd_exec, ctrl_points) - elif next_state == State.NONE: - obj = StateNone(self.__cmd_exec) - elif (State.UNIFORM_SCALING_1 <= next_state <= - State.UNIFORM_SCALING_4): - obj = StateUniformScaling( - self.__cmd_exec, next_state, ctrl_points) - - if obj is not None: - self.__state_obj = obj - - self.__state = next_state - - def update(self, context, ctrl_points, event): - mouse_region = mathutils.Vector(( - event.mouse_region_x, event.mouse_region_y)) - mouse_view = mathutils.Vector((context.region.view2d.region_to_view( - mouse_region.x, mouse_region.y))) - next_state = self.__state_obj.update( - context, event, ctrl_points, mouse_view) - self.__update_state(next_state, ctrl_points) - - return self.__state - - -class Operator(bpy.types.Operator): - """ - Operation class: UV Bounding Box - """ - - bl_idname = "uv.muv_uv_bounding_box_operator" - bl_label = "UV Bounding Box" - bl_description = "Internal operation for UV Bounding Box" - bl_options = {'REGISTER', 'UNDO'} - - def __init__(self): - self.__timer = None - self.__cmd_exec = CommandExecuter() # Command executor - self.__state_mgr = StateManager(self.__cmd_exec) # State Manager - - __handle = None - __timer = None - - @classmethod - def poll(cls, context): - # we can not get area/space/region from console - if common.is_console_mode(): - return False - return 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(obj) - - @classmethod - def handle_remove(cls, context): - if cls.__handle is not None: - sie = bpy.types.SpaceImageEditor - sie.draw_handler_remove(cls.__handle, "WINDOW") - cls.__handle = None - if cls.__timer is not None: - context.window_manager.event_timer_remove(cls.__timer) - cls.__timer = None - - @classmethod - def __draw_ctrl_point(cls, context, pos): - """ - Draw control point - """ - prefs = context.user_preferences.addons["uv_magic_uv"].preferences - cp_size = prefs.uv_bounding_box_cp_size - offset = cp_size / 2 - verts = [ - [pos.x - offset, pos.y - offset], - [pos.x - offset, pos.y + offset], - [pos.x + offset, pos.y + offset], - [pos.x + offset, pos.y - offset] - ] - bgl.glEnable(bgl.GL_BLEND) - 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): - """ - Get UV coordinate - """ - sc = context.scene - obj = context.active_object - uv_info = [] - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - if not bm.loops.layers.uv: - return None - uv_layer = bm.loops.layers.uv.verify() - for f in bm.faces: - if not f.select: - continue - for i, l in enumerate(f.loops): - if sc.muv_uv_bounding_box_boundary == 'UV_SEL': - if l[uv_layer].select: - uv_info.append((f.index, i, l[uv_layer].uv.copy())) - elif sc.muv_uv_bounding_box_boundary == 'UV': - uv_info.append((f.index, i, l[uv_layer].uv.copy())) - if not uv_info: - return None - return uv_info - - def __get_ctrl_point(self, uv_info_ini): - """ - Get control point - """ - left = MAX_VALUE - right = -MAX_VALUE - top = -MAX_VALUE - bottom = MAX_VALUE - - for info in uv_info_ini: - uv = info[2] - if uv.x < left: - left = uv.x - if uv.x > right: - right = uv.x - if uv.y < bottom: - bottom = uv.y - if uv.y > top: - top = uv.y - - points = [ - mathutils.Vector(( - (left + right) * 0.5, (top + bottom) * 0.5, 0.0 - )), - mathutils.Vector((left, top, 0.0)), - mathutils.Vector((left, (top + bottom) * 0.5, 0.0)), - mathutils.Vector((left, bottom, 0.0)), - mathutils.Vector(((left + right) * 0.5, top, 0.0)), - mathutils.Vector(((left + right) * 0.5, bottom, 0.0)), - mathutils.Vector((right, top, 0.0)), - mathutils.Vector((right, (top + bottom) * 0.5, 0.0)), - mathutils.Vector((right, bottom, 0.0)), - mathutils.Vector(((left + right) * 0.5, top + 0.03, 0.0)) - ] - - return points - - def __update_uvs(self, context, uv_info_ini, trans_mat): - """ - Update UV coordinate - """ - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - if not bm.loops.layers.uv: - return - uv_layer = bm.loops.layers.uv.verify() - for info in uv_info_ini: - fidx = info[0] - lidx = info[1] - uv = info[2] - v = mathutils.Vector((uv.x, uv.y, 0.0)) - av = trans_mat * v - bm.faces[fidx].loops[lidx][uv_layer].uv = mathutils.Vector( - (av.x, av.y)) - - def __update_ctrl_point(self, ctrl_points_ini, trans_mat): - """ - Update control point - """ - return [trans_mat * cp for cp in ctrl_points_ini] - - def modal(self, context, event): - props = context.scene.muv_props.uv_bounding_box - common.redraw_all_areas() - - if not Operator.is_running(context): - return {'FINISHED'} - - if not is_valid_context(context): - Operator.handle_remove(context) - return {'FINISHED'} - - region_types = [ - 'HEADER', - 'UI', - 'TOOLS', - ] - if not common.mouse_on_area(event, 'IMAGE_EDITOR') or \ - common.mouse_on_regions(event, 'IMAGE_EDITOR', region_types): - return {'PASS_THROUGH'} - - if event.type == 'TIMER': - trans_mat = self.__cmd_exec.execute() - self.__update_uvs(context, props.uv_info_ini, trans_mat) - props.ctrl_points = self.__update_ctrl_point( - props.ctrl_points_ini, trans_mat) - - state = self.__state_mgr.update(context, props.ctrl_points, event) - if state == State.NONE: - return {'PASS_THROUGH'} - - return {'RUNNING_MODAL'} - - def invoke(self, context, _): - props = context.scene.muv_props.uv_bounding_box - - if 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) - - return {'RUNNING_MODAL'} diff --git a/uv_magic_uv/op/uv_inspection.py b/uv_magic_uv/op/uv_inspection.py deleted file mode 100644 index 0c05e03d..00000000 --- a/uv_magic_uv/op/uv_inspection.py +++ /dev/null @@ -1,272 +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.2" -__date__ = "17 Nov 2018" - -import bpy -import bmesh -import bgl -from bpy.props import BoolProperty, EnumProperty - -from .. import common - - -__all__ = [ - 'Properties', - 'OperatorRender', - 'OperatorUpdate', -] - - -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(): - 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_uv_inspection_operator_render" - bl_description = "Render overlapped/flipped UVs" - bl_label = "Overlapped/Flipped UV renderer" - - __handle = None - - @classmethod - def poll(cls, context): - # we can not get area/space/region from console - if common.is_console_mode(): - return False - return is_valid_context(context) - - @classmethod - def is_running(cls, _): - return 1 if cls.__handle else 0 - - @classmethod - def handle_add(cls, obj, context): - sie = bpy.types.SpaceImageEditor - cls.__handle = sie.draw_handler_add( - OperatorRender.draw, (obj, context), 'WINDOW', 'POST_PIXEL') - - @classmethod - def handle_remove(cls): - if cls.__handle is not None: - bpy.types.SpaceImageEditor.draw_handler_remove( - cls.__handle, 'WINDOW') - cls.__handle = None - - @staticmethod - def draw(_, context): - sc = context.scene - props = sc.muv_props.uv_inspection - prefs = context.user_preferences.addons["uv_magic_uv"].preferences - - if not OperatorRender.is_running(context): - return - - # OpenGL configuration - bgl.glEnable(bgl.GL_BLEND) - - # render overlapped UV - if sc.muv_uv_inspection_show_overlapped: - color = prefs.uv_inspection_overlapped_color - for info in props.overlapped_info: - if sc.muv_uv_inspection_show_mode == 'PART': - for poly in info["polygons"]: - bgl.glBegin(bgl.GL_TRIANGLE_FAN) - bgl.glColor4f(color[0], color[1], color[2], color[3]) - for uv in poly: - x, y = context.region.view2d.view_to_region( - uv.x, uv.y) - bgl.glVertex2f(x, y) - bgl.glEnd() - elif sc.muv_uv_inspection_show_mode == 'FACE': - bgl.glBegin(bgl.GL_TRIANGLE_FAN) - bgl.glColor4f(color[0], color[1], color[2], color[3]) - for uv in info["subject_uvs"]: - x, y = context.region.view2d.view_to_region(uv.x, uv.y) - bgl.glVertex2f(x, y) - bgl.glEnd() - - # render flipped UV - if sc.muv_uv_inspection_show_flipped: - color = prefs.uv_inspection_flipped_color - for info in props.flipped_info: - if sc.muv_uv_inspection_show_mode == 'PART': - for poly in info["polygons"]: - bgl.glBegin(bgl.GL_TRIANGLE_FAN) - bgl.glColor4f(color[0], color[1], color[2], color[3]) - for uv in poly: - x, y = context.region.view2d.view_to_region( - uv.x, uv.y) - bgl.glVertex2f(x, y) - bgl.glEnd() - elif sc.muv_uv_inspection_show_mode == 'FACE': - bgl.glBegin(bgl.GL_TRIANGLE_FAN) - bgl.glColor4f(color[0], color[1], color[2], color[3]) - for uv in info["uvs"]: - x, y = context.region.view2d.view_to_region(uv.x, uv.y) - bgl.glVertex2f(x, y) - bgl.glEnd() - - def invoke(self, context, _): - if not OperatorRender.is_running(context): - update_uvinsp_info(context) - OperatorRender.handle_add(self, context) - else: - OperatorRender.handle_remove() - - if context.area: - context.area.tag_redraw() - - return {'FINISHED'} - - -def update_uvinsp_info(context): - sc = context.scene - props = sc.muv_props.uv_inspection - - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.faces.ensure_lookup_table() - uv_layer = bm.loops.layers.uv.verify() - - if context.tool_settings.use_uv_select_sync: - sel_faces = [f for f in bm.faces] - else: - sel_faces = [f for f in bm.faces if f.select] - props.overlapped_info = common.get_overlapped_uv_info( - bm, sel_faces, uv_layer, sc.muv_uv_inspection_show_mode) - props.flipped_info = common.get_flipped_uv_info(sel_faces, uv_layer) - - -class OperatorUpdate(bpy.types.Operator): - """ - Operation class: Update - """ - - bl_idname = "uv.muv_uv_inspection_operator_update" - bl_label = "Update UV Inspection" - bl_description = "Update UV Inspection" - bl_options = {'REGISTER', 'UNDO'} - - @classmethod - def poll(cls, context): - # we can not get area/space/region from console - if common.is_console_mode(): - return True - if not OperatorRender.is_running(context): - return False - return is_valid_context(context) - - def execute(self, context): - update_uvinsp_info(context) - - if context.area: - context.area.tag_redraw() - - return {'FINISHED'} diff --git a/uv_magic_uv/op/uv_sculpt.py b/uv_magic_uv/op/uv_sculpt.py deleted file mode 100644 index 63c1adfe..00000000 --- a/uv_magic_uv/op/uv_sculpt.py +++ /dev/null @@ -1,477 +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.2" -__date__ = "17 Nov 2018" - -from math import pi, cos, tan, sin - -import bpy -import bmesh -import bgl -from mathutils import Vector -from bpy_extras import view3d_utils -from mathutils.bvhtree import BVHTree -from mathutils.geometry import barycentric_transform -from bpy.props import ( - BoolProperty, - IntProperty, - EnumProperty, - FloatProperty, -) - -from .. import common - - -__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: UV Sculpt in View3D - """ - - bl_idname = "uv.muv_uv_sculpt_operator" - bl_label = "UV Sculpt" - bl_description = "UV Sculpt in View3D" - bl_options = {'REGISTER'} - - __handle = None - __timer = None - - @classmethod - def poll(cls, context): - # we can not get area/space/region from console - if common.is_console_mode(): - return False - return is_valid_context(context) - - @classmethod - def is_running(cls, _): - return 1 if cls.__handle else 0 - - @classmethod - def handle_add(cls, obj, context): - if not cls.__handle: - sv = bpy.types.SpaceView3D - cls.__handle = sv.draw_handler_add(cls.draw_brush, (obj, context), - "WINDOW", "POST_PIXEL") - if not cls.__timer: - cls.__timer = context.window_manager.event_timer_add( - 0.1, context.window) - context.window_manager.modal_handler_add(obj) - - @classmethod - def handle_remove(cls, context): - if cls.__handle: - sv = bpy.types.SpaceView3D - sv.draw_handler_remove(cls.__handle, "WINDOW") - cls.__handle = None - if cls.__timer: - context.window_manager.event_timer_remove(cls.__timer) - cls.__timer = None - - @classmethod - def draw_brush(cls, obj, context): - sc = context.scene - prefs = context.user_preferences.addons["uv_magic_uv"].preferences - - num_segment = 180 - theta = 2 * pi / num_segment - fact_t = tan(theta) - fact_r = cos(theta) - color = prefs.uv_sculpt_brush_color - - bgl.glBegin(bgl.GL_LINE_STRIP) - bgl.glColor4f(color[0], color[1], color[2], color[3]) - x = sc.muv_uv_sculpt_radius * cos(0.0) - y = sc.muv_uv_sculpt_radius * sin(0.0) - for _ in range(num_segment): - bgl.glVertex2f(x + obj.current_mco.x, y + obj.current_mco.y) - tx = -y - ty = x - x = x + tx * fact_t - y = y + ty * fact_t - x = x * fact_r - y = y * fact_r - bgl.glEnd() - - def __init__(self): - self.__loop_info = [] - self.__stroking = False - self.current_mco = Vector((0.0, 0.0)) - self.__initial_mco = Vector((0.0, 0.0)) - - def __get_strength(self, p, len_, factor): - f = factor - - if p > len_: - return 0.0 - - if p < 0.0: - return f - - return (len_ - p) * f / len_ - - def __stroke_init(self, context, _): - sc = context.scene - - self.__initial_mco = self.current_mco - - # get influenced UV - obj = context.active_object - world_mat = obj.matrix_world - bm = bmesh.from_edit_mesh(obj.data) - uv_layer = bm.loops.layers.uv.verify() - _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') - - self.__loop_info = [] - for f in bm.faces: - if not f.select: - continue - for i, l in enumerate(f.loops): - loc_2d = view3d_utils.location_3d_to_region_2d( - region, space.region_3d, world_mat * l.vert.co) - diff = loc_2d - self.__initial_mco - if diff.length < sc.muv_uv_sculpt_radius: - info = { - "face_idx": f.index, - "loop_idx": i, - "initial_vco": l.vert.co.copy(), - "initial_vco_2d": loc_2d, - "initial_uv": l[uv_layer].uv.copy(), - "strength": self.__get_strength( - diff.length, sc.muv_uv_sculpt_radius, - sc.muv_uv_sculpt_strength) - } - self.__loop_info.append(info) - - def __stroke_apply(self, context, _): - sc = context.scene - obj = context.active_object - world_mat = obj.matrix_world - bm = bmesh.from_edit_mesh(obj.data) - uv_layer = bm.loops.layers.uv.verify() - mco = self.current_mco - - if sc.muv_uv_sculpt_tools == 'GRAB': - for info in self.__loop_info: - diff_uv = (mco - self.__initial_mco) * info["strength"] - l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] - l[uv_layer].uv = info["initial_uv"] + diff_uv / 100.0 - - elif sc.muv_uv_sculpt_tools == 'PINCH': - _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') - loop_info = [] - for f in bm.faces: - if not f.select: - continue - for i, l in enumerate(f.loops): - loc_2d = view3d_utils.location_3d_to_region_2d( - region, space.region_3d, world_mat * l.vert.co) - diff = loc_2d - self.__initial_mco - if diff.length < sc.muv_uv_sculpt_radius: - info = { - "face_idx": f.index, - "loop_idx": i, - "initial_vco": l.vert.co.copy(), - "initial_vco_2d": loc_2d, - "initial_uv": l[uv_layer].uv.copy(), - "strength": self.__get_strength( - diff.length, sc.muv_uv_sculpt_radius, - sc.muv_uv_sculpt_strength) - } - loop_info.append(info) - - # mouse coordinate to UV coordinate - ray_vec = view3d_utils.region_2d_to_vector_3d(region, - space.region_3d, mco) - ray_vec.normalize() - ray_orig = view3d_utils.region_2d_to_origin_3d(region, - space.region_3d, - mco) - ray_tgt = ray_orig + ray_vec * 1000000.0 - mwi = world_mat.inverted() - ray_orig_obj = mwi * ray_orig - ray_tgt_obj = mwi * ray_tgt - ray_dir_obj = ray_tgt_obj - ray_orig_obj - ray_dir_obj.normalize() - tree = BVHTree.FromBMesh(bm) - loc, _, fidx, _ = tree.ray_cast(ray_orig_obj, ray_dir_obj) - if not loc: - return - loops = [l for l in bm.faces[fidx].loops] - uvs = [Vector((l[uv_layer].uv.x, l[uv_layer].uv.y, 0.0)) - for l in loops] - target_uv = barycentric_transform( - loc, loops[0].vert.co, loops[1].vert.co, loops[2].vert.co, - uvs[0], uvs[1], uvs[2]) - target_uv = Vector((target_uv.x, target_uv.y)) - - # move to target UV coordinate - for info in loop_info: - l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] - if sc.muv_uv_sculpt_pinch_invert: - diff_uv = (l[uv_layer].uv - target_uv) * info["strength"] - else: - diff_uv = (target_uv - l[uv_layer].uv) * info["strength"] - l[uv_layer].uv = l[uv_layer].uv + diff_uv / 10.0 - - elif sc.muv_uv_sculpt_tools == 'RELAX': - _, region, space = common.get_space('VIEW_3D', 'WINDOW', 'VIEW_3D') - - # get vertex and loop relation - vert_db = {} - for f in bm.faces: - for l in f.loops: - if l.vert in vert_db: - vert_db[l.vert]["loops"].append(l) - else: - vert_db[l.vert] = {"loops": [l]} - - # get relaxation information - for k in vert_db.keys(): - d = vert_db[k] - d["uv_sum"] = Vector((0.0, 0.0)) - d["uv_count"] = 0 - - for l in d["loops"]: - ln = l.link_loop_next - lp = l.link_loop_prev - d["uv_sum"] = d["uv_sum"] + ln[uv_layer].uv - d["uv_sum"] = d["uv_sum"] + lp[uv_layer].uv - d["uv_count"] = d["uv_count"] + 2 - d["uv_p"] = d["uv_sum"] / d["uv_count"] - d["uv_b"] = d["uv_p"] - d["loops"][0][uv_layer].uv - for k in vert_db.keys(): - d = vert_db[k] - d["uv_sum_b"] = Vector((0.0, 0.0)) - for l in d["loops"]: - ln = l.link_loop_next - lp = l.link_loop_prev - dn = vert_db[ln.vert] - dp = vert_db[lp.vert] - d["uv_sum_b"] = d["uv_sum_b"] + dn["uv_b"] + dp["uv_b"] - - # apply - for f in bm.faces: - if not f.select: - continue - for i, l in enumerate(f.loops): - loc_2d = view3d_utils.location_3d_to_region_2d( - region, space.region_3d, world_mat * l.vert.co) - diff = loc_2d - self.__initial_mco - if diff.length >= sc.muv_uv_sculpt_radius: - continue - db = vert_db[l.vert] - strength = self.__get_strength(diff.length, - sc.muv_uv_sculpt_radius, - sc.muv_uv_sculpt_strength) - - base = (1.0 - strength) * l[uv_layer].uv - if sc.muv_uv_sculpt_relax_method == 'HC': - t = 0.5 * (db["uv_b"] + db["uv_sum_b"] / d["uv_count"]) - diff = strength * (db["uv_p"] - t) - target_uv = base + diff - elif sc.muv_uv_sculpt_relax_method == 'LAPLACIAN': - diff = strength * db["uv_p"] - target_uv = base + diff - else: - continue - - l[uv_layer].uv = target_uv - - bmesh.update_edit_mesh(obj.data) - - def __stroke_exit(self, context, _): - sc = context.scene - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - uv_layer = bm.loops.layers.uv.verify() - mco = self.current_mco - - if sc.muv_uv_sculpt_tools == 'GRAB': - for info in self.__loop_info: - diff_uv = (mco - self.__initial_mco) * info["strength"] - l = bm.faces[info["face_idx"]].loops[info["loop_idx"]] - l[uv_layer].uv = info["initial_uv"] + diff_uv / 100.0 - - bmesh.update_edit_mesh(obj.data) - - def modal(self, context, event): - if context.area: - context.area.tag_redraw() - - if not Operator.is_running(context): - Operator.handle_remove(context) - - return {'FINISHED'} - - self.current_mco = Vector((event.mouse_region_x, event.mouse_region_y)) - - region_types = [ - 'HEADER', - 'UI', - 'TOOLS', - 'TOOL_PROPS', - ] - if not common.mouse_on_area(event, 'VIEW_3D') or \ - common.mouse_on_regions(event, 'VIEW_3D', region_types): - return {'PASS_THROUGH'} - - if event.type == 'LEFTMOUSE': - if event.value == 'PRESS': - if not self.__stroking: - self.__stroke_init(context, event) - self.__stroking = True - elif event.value == 'RELEASE': - if self.__stroking: - self.__stroke_exit(context, event) - self.__stroking = False - return {'RUNNING_MODAL'} - elif event.type == 'MOUSEMOVE': - if self.__stroking: - self.__stroke_apply(context, event) - return {'RUNNING_MODAL'} - elif event.type == 'TIMER': - if self.__stroking: - self.__stroke_apply(context, event) - return {'RUNNING_MODAL'} - - return {'PASS_THROUGH'} - - def invoke(self, context, _): - if context.area: - context.area.tag_redraw() - - if 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 44858187..c97e0e04 100644 --- a/uv_magic_uv/op/uvw.py +++ b/uv_magic_uv/op/uvw.py @@ -23,8 +23,6 @@ __status__ = "production" __version__ = "5.2" __date__ = "17 Nov 2018" -from math import sin, cos, pi - import bpy import bmesh from bpy.props import ( @@ -32,40 +30,24 @@ from bpy.props import ( FloatVectorProperty, BoolProperty ) -from mathutils import Vector from .. import common +from ..impl import uvw_impl as impl +from ..utils.bl_class_registry import BlClassRegistry +from ..utils.property_class_registry import PropertyClassRegistry __all__ = [ 'Properties', - 'OperatorBoxMap', - 'OperatorBestPlanerMap', + 'MUV_OT_UVW_BoxMap', + 'MUV_OT_UVW_BestPlanerMap', ] -def is_valid_context(context): - obj = context.object - - # only edit mode is allowed to execute - if obj is None: - return False - if obj.type != 'MESH': - return False - if context.object.mode != 'EDIT': - return False - - # only 'VIEW_3D' space is allowed to execute - for space in context.area.spaces: - if space.type == 'VIEW_3D': - break - else: - return False - - return True - - +@PropertyClassRegistry() class Properties: + idname = "uvw" + @classmethod def init_props(cls, scene): scene.muv_uvw_enabled = BoolProperty( @@ -85,32 +67,33 @@ class Properties: del scene.muv_uvw_assign_uvmap -class OperatorBoxMap(bpy.types.Operator): +@BlClassRegistry() +class MUV_OT_UVW_BoxMap(bpy.types.Operator): bl_idname = "uv.muv_uvw_operator_box_map" bl_label = "Box Map" bl_options = {'REGISTER', 'UNDO'} - size = FloatProperty( + size: FloatProperty( name="Size", default=1.0, precision=4 ) - rotation = FloatVectorProperty( + rotation: FloatVectorProperty( name="XYZ Rotation", size=3, default=(0.0, 0.0, 0.0) ) - offset = FloatVectorProperty( + offset: FloatVectorProperty( name="XYZ Offset", size=3, default=(0.0, 0.0, 0.0) ) - tex_aspect = FloatProperty( + tex_aspect: FloatProperty( name="Texture Aspect", default=1.0, precision=4 ) - assign_uvmap = BoolProperty( + assign_uvmap: BoolProperty( name="Assign UVMap", description="Assign UVMap when no UVmaps are available", default=True @@ -121,7 +104,7 @@ class OperatorBoxMap(bpy.types.Operator): # we can not get area/space/region from console if common.is_console_mode(): return True - return is_valid_context(context) + return impl.is_valid_context(context) def execute(self, context): obj = context.active_object @@ -130,102 +113,43 @@ class OperatorBoxMap(bpy.types.Operator): bm.faces.ensure_lookup_table() # get UV layer - if not bm.loops.layers.uv: - if self.assign_uvmap: - bm.loops.layers.uv.new() - else: - self.report( - {'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - uv_layer = bm.loops.layers.uv.verify() - - scale = 1.0 / self.size - - sx = 1.0 * scale - sy = 1.0 * scale - sz = 1.0 * scale - ofx = self.offset[0] - ofy = self.offset[1] - ofz = self.offset[2] - rx = self.rotation[0] * pi / 180.0 - ry = self.rotation[1] * pi / 180.0 - rz = self.rotation[2] * pi / 180.0 - aspect = self.tex_aspect - - sel_faces = [f for f in bm.faces if f.select] - - # update UV coordinate - for f in sel_faces: - n = f.normal - for l in f.loops: - co = l.vert.co - x = co.x * sx - y = co.y * sy - z = co.z * sz - - # X-plane - if abs(n[0]) >= abs(n[1]) and abs(n[0]) >= abs(n[2]): - if n[0] >= 0.0: - u = (y - ofy) * cos(rx) + (z - ofz) * sin(rx) - v = -(y * aspect - ofy) * sin(rx) +\ - (z * aspect - ofz) * cos(rx) - else: - u = -(y - ofy) * cos(rx) + (z - ofz) * sin(rx) - v = (y * aspect - ofy) * sin(rx) +\ - (z * aspect - ofz) * cos(rx) - # Y-plane - elif abs(n[1]) >= abs(n[0]) and abs(n[1]) >= abs(n[2]): - if n[1] >= 0.0: - u = -(x - ofx) * cos(ry) + (z - ofz) * sin(ry) - v = (x * aspect - ofx) * sin(ry) +\ - (z * aspect - ofz) * cos(ry) - else: - u = (x - ofx) * cos(ry) + (z - ofz) * sin(ry) - v = -(x * aspect - ofx) * sin(ry) +\ - (z * aspect - ofz) * cos(ry) - # Z-plane - else: - if n[2] >= 0.0: - u = (x - ofx) * cos(rz) + (y - ofy) * sin(rz) - v = -(x * aspect - ofx) * sin(rz) +\ - (y * aspect - ofy) * cos(rz) - else: - u = -(x - ofx) * cos(rz) - (y + ofy) * sin(rz) - v = -(x * aspect + ofx) * sin(rz) +\ - (y * aspect - ofy) * cos(rz) - - l[uv_layer].uv = Vector((u, v)) + uv_layer = impl.get_uv_layer(self, bm, self.assign_uvmap) + if not uv_layer: + return {'CANCELLED'} + impl.apply_box_map(bm, uv_layer, self.size, self.offset, + self.rotation, self.tex_aspect) bmesh.update_edit_mesh(obj.data) return {'FINISHED'} -class OperatorBestPlanerMap(bpy.types.Operator): +@BlClassRegistry() +class MUV_OT_UVW_BestPlanerMap(bpy.types.Operator): bl_idname = "uv.muv_uvw_operator_best_planer_map" bl_label = "Best Planer Map" bl_options = {'REGISTER', 'UNDO'} - size = FloatProperty( + size: FloatProperty( name="Size", default=1.0, precision=4 ) - rotation = FloatProperty( + rotation: FloatProperty( name="XY Rotation", default=0.0 ) - offset = FloatVectorProperty( + offset: FloatVectorProperty( name="XY Offset", size=2, default=(0.0, 0.0) ) - tex_aspect = FloatProperty( + tex_aspect: FloatProperty( name="Texture Aspect", default=1.0, precision=4 ) - assign_uvmap = BoolProperty( + assign_uvmap: BoolProperty( name="Assign UVMap", description="Assign UVMap when no UVmaps are available", default=True @@ -236,7 +160,7 @@ class OperatorBestPlanerMap(bpy.types.Operator): # we can not get area/space/region from console if common.is_console_mode(): return True - return is_valid_context(context) + return impl.is_valid_context(context) def execute(self, context): obj = context.active_object @@ -245,44 +169,12 @@ class OperatorBestPlanerMap(bpy.types.Operator): bm.faces.ensure_lookup_table() # get UV layer - if not bm.loops.layers.uv: - if self.assign_uvmap: - bm.loops.layers.uv.new() - else: - self.report( - {'WARNING'}, "Object must have more than one UV map") - return {'CANCELLED'} - - uv_layer = bm.loops.layers.uv.verify() - - scale = 1.0 / self.size - - sx = 1.0 * scale - sy = 1.0 * scale - ofx = self.offset[0] - ofy = self.offset[1] - rz = self.rotation * pi / 180.0 - aspect = self.tex_aspect - - sel_faces = [f for f in bm.faces if f.select] - - # calculate average of normal - n_ave = Vector((0.0, 0.0, 0.0)) - for f in sel_faces: - n_ave = n_ave + f.normal - q = n_ave.rotation_difference(Vector((0.0, 0.0, 1.0))) - - # update UV coordinate - for f in sel_faces: - for l in f.loops: - co = q * l.vert.co - x = co.x * sx - y = co.y * sy - - u = x * cos(rz) - y * sin(rz) + ofx - v = -x * aspect * sin(rz) - y * aspect * cos(rz) + ofy + uv_layer = impl.get_uv_layer(self, bm, self.assign_uvmap) + if not uv_layer: + return {'CANCELLED'} - l[uv_layer].uv = Vector((u, v)) + impl.apply_planer_map(bm, uv_layer, self.size, self.offset, + self.rotation, self.tex_aspect) bmesh.update_edit_mesh(obj.data) diff --git a/uv_magic_uv/op/world_scale_uv.py b/uv_magic_uv/op/world_scale_uv.py deleted file mode 100644 index e1a44954..00000000 --- a/uv_magic_uv/op/world_scale_uv.py +++ /dev/null @@ -1,646 +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__ = "McBuff, Nutti " -__status__ = "production" -__version__ = "5.2" -__date__ = "17 Nov 2018" - -from math import sqrt - -import bpy -import bmesh -from mathutils import Vector -from bpy.props import ( - EnumProperty, - FloatProperty, - IntVectorProperty, - BoolProperty, -) - -from .. import common - - -__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, tex_size) - - if not uv_area: - return None, mesh_area, None - - if mesh_area == 0.0: - density = 0.0 - else: - density = sqrt(uv_area) / sqrt(mesh_area) - - return uv_area, mesh_area, density - - -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_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 - - uv_area, mesh_area, density = measure_wsuv_info(obj) - if not uv_area: - self.report({'WARNING'}, - "Object must have more than one UV map and texture") - return {'CANCELLED'} - - sc.muv_world_scale_uv_src_uv_area = uv_area - sc.muv_world_scale_uv_src_mesh_area = mesh_area - sc.muv_world_scale_uv_src_density = density - - self.report({'INFO'}, - "UV Area: {0}, Mesh Area: {1}, Texel Density: {2}" - .format(uv_area, mesh_area, density)) - - return {'FINISHED'} - - -def apply(obj, origin, factor): - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.verts.ensure_lookup_table() - bm.edges.ensure_lookup_table() - bm.faces.ensure_lookup_table() - - sel_faces = [f for f in bm.faces if f.select] - - uv_layer = bm.loops.layers.uv.verify() - - # calculate origin - if origin == 'CENTER': - origin = Vector((0.0, 0.0)) - num = 0 - for f in sel_faces: - for l in f.loops: - uv = l[uv_layer].uv - origin = origin + uv - num = num + 1 - origin = origin / num - elif origin == 'LEFT_TOP': - origin = Vector((100000.0, -100000.0)) - for f in sel_faces: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = min(origin.x, uv.x) - origin.y = max(origin.y, uv.y) - elif origin == 'LEFT_CENTER': - origin = Vector((100000.0, 0.0)) - num = 0 - for f in sel_faces: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = min(origin.x, uv.x) - origin.y = origin.y + uv.y - num = num + 1 - origin.y = origin.y / num - elif origin == 'LEFT_BOTTOM': - origin = Vector((100000.0, 100000.0)) - for f in sel_faces: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = min(origin.x, uv.x) - origin.y = min(origin.y, uv.y) - elif origin == 'CENTER_TOP': - origin = Vector((0.0, -100000.0)) - num = 0 - for f in sel_faces: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = origin.x + uv.x - origin.y = max(origin.y, uv.y) - num = num + 1 - origin.x = origin.x / num - elif origin == 'CENTER_BOTTOM': - origin = Vector((0.0, 100000.0)) - num = 0 - for f in sel_faces: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = origin.x + uv.x - origin.y = min(origin.y, uv.y) - num = num + 1 - origin.x = origin.x / num - elif origin == 'RIGHT_TOP': - origin = Vector((-100000.0, -100000.0)) - for f in sel_faces: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = max(origin.x, uv.x) - origin.y = max(origin.y, uv.y) - elif origin == 'RIGHT_CENTER': - origin = Vector((-100000.0, 0.0)) - num = 0 - for f in sel_faces: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = max(origin.x, uv.x) - origin.y = origin.y + uv.y - num = num + 1 - origin.y = origin.y / num - elif origin == 'RIGHT_BOTTOM': - origin = Vector((-100000.0, 100000.0)) - for f in sel_faces: - for l in f.loops: - uv = l[uv_layer].uv - origin.x = max(origin.x, uv.x) - origin.y = min(origin.y, uv.y) - - # update UV coordinate - for f in sel_faces: - for l in f.loops: - uv = l[uv_layer].uv - diff = uv - origin - l[uv_layer].uv = origin + diff * factor - - bmesh.update_edit_mesh(obj.data) - - -class OperatorApplyManual(bpy.types.Operator): - """ - Operation class: Apply scaled UV (Manual) - """ - - bl_idname = "uv.muv_world_scale_uv_operator_apply_manual" - bl_label = "Apply World Scale UV (Manual)" - bl_description = "Apply scaled UV based on user specification" - bl_options = {'REGISTER', 'UNDO'} - - tgt_density = FloatProperty( - name="Density", - description="Target Texel Density", - default=1.0, - min=0.0 - ) - tgt_texture_size = IntVectorProperty( - name="Texture Size", - size=2, - min=1, - soft_max=10240, - default=(1024, 1024), - ) - origin = EnumProperty( - name="Origin", - description="Aspect Origin", - items=[ - ('CENTER', "Center", "Center"), - ('LEFT_TOP', "Left Top", "Left Bottom"), - ('LEFT_CENTER', "Left Center", "Left Center"), - ('LEFT_BOTTOM', "Left Bottom", "Left Bottom"), - ('CENTER_TOP', "Center Top", "Center Top"), - ('CENTER_BOTTOM', "Center Bottom", "Center Bottom"), - ('RIGHT_TOP', "Right Top", "Right Top"), - ('RIGHT_CENTER', "Right Center", "Right Center"), - ('RIGHT_BOTTOM', "Right Bottom", "Right Bottom") - - ], - default='CENTER' - ) - show_dialog = BoolProperty( - name="Show Diaglog Menu", - description="Show dialog menu if true", - default=True, - options={'HIDDEN', 'SKIP_SAVE'} - ) - - @classmethod - def poll(cls, context): - # we can not get area/space/region from console - if common.is_console_mode(): - return True - return is_valid_context(context) - - def __apply_manual(self, context): - obj = context.active_object - bm = bmesh.from_edit_mesh(obj.data) - if common.check_version(2, 73, 0) >= 0: - bm.verts.ensure_lookup_table() - bm.edges.ensure_lookup_table() - bm.faces.ensure_lookup_table() - - tex_size = self.tgt_texture_size - uv_area, _, density = measure_wsuv_info(obj, tex_size) - if not uv_area: - self.report({'WARNING'}, - "Object must have more than one UV map") - return {'CANCELLED'} - - tgt_density = self.tgt_density - factor = tgt_density / density - - apply(context.active_object, self.origin, factor) - self.report({'INFO'}, "Scaling factor: {0}".format(factor)) - - return {'FINISHED'} - - def draw(self, _): - layout = self.layout - - layout.prop(self, "tgt_density") - layout.prop(self, "tgt_texture_size") - layout.prop(self, "origin") - - layout.separator() - - def invoke(self, context, _): - if self.show_dialog: - wm = context.window_manager - return wm.invoke_props_dialog(self) - - return self.execute(context) - - def execute(self, context): - return self.__apply_manual(context) - - -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: - bm.verts.ensure_lookup_table() - bm.edges.ensure_lookup_table() - bm.faces.ensure_lookup_table() - - uv_area, _, density = measure_wsuv_info(obj) - if not uv_area: - self.report({'WARNING'}, - "Object must have more than one UV map and texture") - return {'CANCELLED'} - - tgt_density = self.src_density * self.tgt_scaling_factor - factor = tgt_density / density - - apply(context.active_object, self.origin, factor) - self.report({'INFO'}, "Scaling factor: {0}".format(factor)) - - return {'FINISHED'} - - def draw(self, _): - layout = self.layout - - layout.label("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'} - - 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 376258d0..3ba94376 100644 --- a/uv_magic_uv/preferences.py +++ b/uv_magic_uv/preferences.py @@ -33,11 +33,10 @@ from bpy.props import ( ) from bpy.types import AddonPreferences -from . import ui from . import op +from . import ui from . import addon_updater_ops - __all__ = [ 'add_builtin_menu', 'remove_builtin_menu', @@ -50,100 +49,50 @@ def view3d_uvmap_menu_fn(self, context): sc = context.scene layout.separator() - layout.label("Copy/Paste UV", icon='IMAGE_COL') + layout.label(text="Copy/Paste UV", icon='IMAGE') # Copy/Paste UV - layout.menu(ui.VIEW3D_MT_uv_map.MenuCopyPasteUV.bl_idname, + layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_CopyPasteUV.bl_idname, text="Copy/Paste UV") # Transfer UV - layout.menu(ui.VIEW3D_MT_uv_map.MenuTransferUV.bl_idname, + layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_TransferUV.bl_idname, text="Transfer UV") layout.separator() - layout.label("UV Manipulation", icon='IMAGE_COL') + layout.label(text="UV Manipulation", icon='IMAGE') # Flip/Rotate UV - ops = layout.operator(op.flip_rotate_uv.Operator.bl_idname, + ops = layout.operator(op.flip_rotate_uv.MUV_OT_FlipRotate.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 = layout.operator(op.mirror_uv.MUV_OT_MirrorUV.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.operator(op.move_uv.MUV_OT_MoveUV.bl_idname, text="Move UV") 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") + layout.menu(ui.VIEW3D_MT_uv_map.MUV_MT_UVW.bl_idname, text="UVW") def view3d_object_menu_fn(self, _): layout = self.layout layout.separator() + layout.label(text="Copy/Paste UV", icon='IMAGE') # Copy/Paste UV (Among Objecct) - layout.menu(ui.VIEW3D_MT_object.MenuCopyPasteUVObject.bl_idname, + layout.menu(ui.VIEW3D_MT_object.MUV_MT_CopyPasteUV_Object.bl_idname, text="Copy/Paste UV") - layout.label("Copy/Paste UV", icon='IMAGE_COL') -def image_uvs_menu_fn(self, context): +def image_uvs_menu_fn(self, _): 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, + layout.label(text="Copy/Paste UV", icon='IMAGE') + layout.menu(ui.IMAGE_MT_uvs.MUV_MT_CopyPasteUV_UVEdit.bl_idname, text="Copy/Paste UV") - layout.label("Copy/Paste UV", icon='IMAGE_COL') def add_builtin_menu(): @@ -154,14 +103,14 @@ def add_builtin_menu(): 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_object.append(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__ + bl_idname = "uv_magic_uv" def update_enable_builtin_menu(self, _): if self['enable_builtin_menu']: diff --git a/uv_magic_uv/properites.py b/uv_magic_uv/properites.py index e4634e51..60ce26eb 100644 --- a/uv_magic_uv/properites.py +++ b/uv_magic_uv/properites.py @@ -24,30 +24,7 @@ __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 .utils.property_class_registry import PropertyClassRegistry __all__ = [ @@ -77,53 +54,9 @@ class MUV_Prefs(): def init_props(scene): scene.muv_props = MUV_Properties() - - 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) + PropertyClassRegistry.init_props(scene) def clear_props(scene): - 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) - + PropertyClassRegistry.del_props(scene) del scene.muv_props diff --git a/uv_magic_uv/ui/IMAGE_MT_uvs.py b/uv_magic_uv/ui/IMAGE_MT_uvs.py index 9beb7e2f..e7dda379 100644 --- a/uv_magic_uv/ui/IMAGE_MT_uvs.py +++ b/uv_magic_uv/ui/IMAGE_MT_uvs.py @@ -24,23 +24,19 @@ __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 +from ..op import ( + copy_paste_uv_uvedit, +) +from ..utils.bl_class_registry import BlClassRegistry __all__ = [ - 'MenuCopyPasteUVUVEdit', - 'MenuAlignUV', - 'MenuSelectUV', - 'MenuAlignUVCursor', - 'MenuUVInspection', + 'MUV_MT_CopyPasteUV_UVEdit', ] -class MenuCopyPasteUVUVEdit(bpy.types.Menu): +@BlClassRegistry() +class MUV_MT_CopyPasteUV_UVEdit(bpy.types.Menu): """ Menu class: Master menu of Copy/Paste UV coordinate on UV/ImageEditor """ @@ -52,135 +48,9 @@ class MenuCopyPasteUVUVEdit(bpy.types.Menu): 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") + layout.operator( + copy_paste_uv_uvedit.MUV_OT_CopyPasteUVUVEdit_CopyUV.bl_idname, + text="Copy") + layout.operator( + copy_paste_uv_uvedit.MUV_OT_CopyPasteUVUVEdit_PasteUV.bl_idname, + text="Paste") diff --git a/uv_magic_uv/ui/VIEW3D_MT_object.py b/uv_magic_uv/ui/VIEW3D_MT_object.py index c73157cc..318cd82c 100644 --- a/uv_magic_uv/ui/VIEW3D_MT_object.py +++ b/uv_magic_uv/ui/VIEW3D_MT_object.py @@ -24,15 +24,17 @@ __version__ = "5.2" __date__ = "17 Nov 2018" import bpy -from ..op import copy_paste_uv_object +from ..op import copy_paste_uv_object +from ..utils.bl_class_registry import BlClassRegistry __all__ = [ - 'MenuCopyPasteUVObject', + 'MUV_MT_CopyPasteUV_Object', ] -class MenuCopyPasteUVObject(bpy.types.Menu): +@BlClassRegistry() +class MUV_MT_CopyPasteUV_Object(bpy.types.Menu): """ Menu class: Master menu of Copy/Paste UV coordinate among object """ @@ -44,7 +46,9 @@ class MenuCopyPasteUVObject(bpy.types.Menu): 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") + layout.menu( + copy_paste_uv_object.MUV_MT_CopyPasteUVObject_CopyUV.bl_idname, + text="Copy") + layout.menu( + copy_paste_uv_object.MUV_MT_CopyPasteUVObject_PasteUV.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 index bb59c12c..c5698504 100644 --- a/uv_magic_uv/ui/VIEW3D_MT_uv_map.py +++ b/uv_magic_uv/ui/VIEW3D_MT_uv_map.py @@ -23,30 +23,24 @@ __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 +import bpy.utils +from ..op import ( + copy_paste_uv, + transfer_uv, + uvw, +) +from ..utils.bl_class_registry import BlClassRegistry __all__ = [ - 'MenuCopyPasteUV', - 'MenuTransferUV', - 'MenuTextureLock', - 'MenuWorldScaleUV', - 'MenuTextureWrap', - 'MenuUVW', - 'MenuTextureProjection', - 'MenuPreserveUVAspect', + 'MUV_MT_CopyPasteUV', + 'MUV_MT_TransferUV', + 'MUV_MT_UVW', ] -class MenuCopyPasteUV(bpy.types.Menu): +@BlClassRegistry() +class MUV_MT_CopyPasteUV(bpy.types.Menu): """ Menu class: Master menu of Copy/Paste UV coordinate """ @@ -58,20 +52,23 @@ class MenuCopyPasteUV(bpy.types.Menu): 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.label(text="Default") + layout.menu(copy_paste_uv.MUV_MT_CopyPasteUV_CopyUV.bl_idname, + text="Copy") + layout.menu(copy_paste_uv.MUV_MT_CopyPasteUV_PasteUV.bl_idname, + text="Paste") layout.separator() - layout.label("Selection Sequence") - layout.menu(copy_paste_uv.MenuSelSeqCopyUV.bl_idname, + layout.label(text="Selection Sequence") + layout.menu(copy_paste_uv.MUV_MT_CopyPasteUV_SelSeqCopyUV.bl_idname, text="Copy") - layout.menu(copy_paste_uv.MenuSelSeqPasteUV.bl_idname, + layout.menu(copy_paste_uv.MUV_MT_CopyPasteUV_SelSeqPasteUV.bl_idname, text="Paste") -class MenuTransferUV(bpy.types.Menu): +@BlClassRegistry() +class MUV_MT_TransferUV(bpy.types.Menu): """ Menu class: Master menu of Transfer UV coordinate """ @@ -84,99 +81,16 @@ class MenuTransferUV(bpy.types.Menu): layout = self.layout sc = context.scene - layout.operator(transfer_uv.OperatorCopyUV.bl_idname, text="Copy") - ops = layout.operator(transfer_uv.OperatorPasteUV.bl_idname, + layout.operator(transfer_uv.MUV_OT_TransferUV_CopyUV.bl_idname, + text="Copy") + ops = layout.operator(transfer_uv.MUV_OT_TransferUV_PasteUV.bl_idname, text="Paste") ops.invert_normals = sc.muv_transfer_uv_invert_normals ops.copy_seams = sc.muv_transfer_uv_copy_seams -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): +@BlClassRegistry() +class MUV_MT_UVW(bpy.types.Menu): """ Menu class: Master menu of UVW """ @@ -189,48 +103,9 @@ class MenuUVW(bpy.types.Menu): layout = self.layout sc = context.scene - ops = layout.operator(uvw.OperatorBoxMap.bl_idname, text="Box") + ops = layout.operator(uvw.MUV_OT_UVW_BoxMap.bl_idname, text="Box") ops.assign_uvmap = sc.muv_uvw_assign_uvmap - ops = layout.operator(uvw.OperatorBestPlanerMap.bl_idname, + ops = layout.operator(uvw.MUV_OT_UVW_BestPlanerMap.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 b377ed23..5f7e0c5e 100644 --- a/uv_magic_uv/ui/__init__.py +++ b/uv_magic_uv/ui/__init__.py @@ -25,26 +25,22 @@ __date__ = "17 Nov 2018" if "bpy" in locals(): import importlib - importlib.reload(view3d_copy_paste_uv_objectmode) importlib.reload(view3d_copy_paste_uv_editmode) + importlib.reload(view3d_copy_paste_uv_objectmode) importlib.reload(view3d_uv_manipulation) importlib.reload(view3d_uv_mapping) importlib.reload(uvedit_copy_paste_uv) - importlib.reload(uvedit_uv_manipulation) - importlib.reload(uvedit_editor_enhancement) - importlib.reload(VIEW3D_MT_uv_map) importlib.reload(VIEW3D_MT_object) + importlib.reload(VIEW3D_MT_uv_map) importlib.reload(IMAGE_MT_uvs) else: - from . import view3d_copy_paste_uv_objectmode from . import view3d_copy_paste_uv_editmode + from . import view3d_copy_paste_uv_objectmode from . import view3d_uv_manipulation from . import view3d_uv_mapping from . import uvedit_copy_paste_uv - from . import uvedit_uv_manipulation - from . import uvedit_editor_enhancement - from . import VIEW3D_MT_uv_map from . import VIEW3D_MT_object + from . import VIEW3D_MT_uv_map 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 271277a0..e21a5abd 100644 --- a/uv_magic_uv/ui/uvedit_copy_paste_uv.py +++ b/uv_magic_uv/ui/uvedit_copy_paste_uv.py @@ -26,20 +26,21 @@ __date__ = "17 Nov 2018" import bpy from ..op import copy_paste_uv_uvedit - +from ..utils.bl_class_registry import BlClassRegistry __all__ = [ - 'PanelCopyPasteUV', + 'MUV_PT_UVEdit_CopyPasteUV', ] -class PanelCopyPasteUV(bpy.types.Panel): +@BlClassRegistry() +class MUV_PT_UVEdit_CopyPasteUV(bpy.types.Panel): """ Panel class: Copy/Paste UV on Property Panel on UV/ImageEditor """ bl_space_type = 'IMAGE_EDITOR' - bl_region_type = 'TOOLS' + bl_region_type = 'UI' bl_label = "Copy/Paste UV" bl_category = "Magic UV" bl_context = 'mesh_edit' @@ -47,13 +48,15 @@ class PanelCopyPasteUV(bpy.types.Panel): def draw_header(self, _): layout = self.layout - layout.label(text="", icon='IMAGE_COL') + layout.label(text="", icon='IMAGE') def draw(self, _): layout = self.layout row = layout.row(align=True) - row.operator(copy_paste_uv_uvedit.OperatorCopyUV.bl_idname, - text="Copy") - row.operator(copy_paste_uv_uvedit.OperatorPasteUV.bl_idname, - text="Paste") + row.operator( + copy_paste_uv_uvedit.MUV_OT_CopyPasteUVUVEdit_CopyUV.bl_idname, + text="Copy") + row.operator( + copy_paste_uv_uvedit.MUV_OT_CopyPasteUVUVEdit_PasteUV.bl_idname, + text="Paste") diff --git a/uv_magic_uv/ui/uvedit_editor_enhancement.py b/uv_magic_uv/ui/uvedit_editor_enhancement.py deleted file mode 100644 index 9cde0cad..00000000 --- a/uv_magic_uv/ui/uvedit_editor_enhancement.py +++ /dev/null @@ -1,144 +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.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 deleted file mode 100644 index 0f6a105e..00000000 --- a/uv_magic_uv/ui/uvedit_uv_manipulation.py +++ /dev/null @@ -1,126 +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.2" -__date__ = "17 Nov 2018" - -import bpy - -from ..op import align_uv -from ..op import smooth_uv -from ..op import pack_uv -from ..op import select_uv - - -__all__ = [ - 'MenuUVManipulation', -] - - -class MenuUVManipulation(bpy.types.Panel): - """ - Panel class: UV Manipulation on Property Panel on UV/ImageEditor - """ - - bl_space_type = 'IMAGE_EDITOR' - bl_region_type = 'TOOLS' - bl_label = "UV Manipulation" - bl_category = "Magic UV" - bl_context = 'mesh_edit' - bl_options = {'DEFAULT_CLOSED'} - - def draw_header(self, _): - layout = self.layout - layout.label(text="", icon='IMAGE_COL') - - def draw(self, context): - sc = context.scene - layout = self.layout - - box = layout.box() - box.prop(sc, "muv_align_uv_enabled", text="Align UV") - if sc.muv_align_uv_enabled: - col = box.column() - row = col.row(align=True) - ops = row.operator(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_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.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_align_uv_transmission", text="Transmission") - row.prop(sc, "muv_align_uv_select", text="Select") - row = col.row(align=True) - row.prop(sc, "muv_align_uv_vertical", text="Vertical") - row.prop(sc, "muv_align_uv_horizontal", text="Horizontal") - col.prop(sc, "muv_align_uv_mesh_infl", text="Mesh Influence") - - box = layout.box() - box.prop(sc, "muv_smooth_uv_enabled", text="Smooth UV") - if sc.muv_smooth_uv_enabled: - ops = box.operator(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 - col = box.column(align=True) - row = col.row(align=True) - row.prop(sc, "muv_smooth_uv_transmission", text="Transmission") - row.prop(sc, "muv_smooth_uv_select", text="Select") - col.prop(sc, "muv_smooth_uv_mesh_infl", text="Mesh Influence") - - box = layout.box() - box.prop(sc, "muv_select_uv_enabled", text="Select UV") - if sc.muv_select_uv_enabled: - row = box.row(align=True) - row.operator(select_uv.OperatorSelectOverlapped.bl_idname) - row.operator(select_uv.OperatorSelectFlipped.bl_idname) - - box = layout.box() - box.prop(sc, "muv_pack_uv_enabled", text="Pack UV (Extension)") - if sc.muv_pack_uv_enabled: - ops = box.operator(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 - box.label("Allowable Center Deviation:") - box.prop(sc, "muv_pack_uv_allowable_center_deviation", text="") - box.label("Allowable Size Deviation:") - 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 c5906ca0..14fba24a 100644 --- a/uv_magic_uv/ui/view3d_copy_paste_uv_editmode.py +++ b/uv_magic_uv/ui/view3d_copy_paste_uv_editmode.py @@ -25,22 +25,25 @@ __date__ = "17 Nov 2018" import bpy -from ..op import copy_paste_uv -from ..op import transfer_uv - +from ..op import ( + copy_paste_uv, + transfer_uv, +) +from ..utils.bl_class_registry import BlClassRegistry __all__ = [ - 'PanelCopyPasteUVEditMode', + 'MUV_PT_CopyPasteUVEditMode', ] -class PanelCopyPasteUVEditMode(bpy.types.Panel): +@BlClassRegistry() +class MUV_PT_CopyPasteUVEditMode(bpy.types.Panel): """ Panel class: Copy/Paste UV on Property Panel on View3D """ bl_space_type = 'VIEW_3D' - bl_region_type = 'TOOLS' + bl_region_type = 'UI' bl_label = "Copy/Paste UV" bl_category = "Magic UV" bl_context = 'mesh_edit' @@ -48,7 +51,7 @@ class PanelCopyPasteUVEditMode(bpy.types.Panel): def draw_header(self, _): layout = self.layout - layout.label(text="", icon='IMAGE_COL') + layout.label(text="", icon='IMAGE') def draw(self, context): sc = context.scene @@ -59,15 +62,17 @@ class PanelCopyPasteUVEditMode(bpy.types.Panel): if sc.muv_copy_paste_uv_enabled: row = box.row(align=True) if sc.muv_copy_paste_uv_mode == 'DEFAULT': - row.menu(copy_paste_uv.MenuCopyUV.bl_idname, + row.menu(copy_paste_uv.MUV_MT_CopyPasteUV_CopyUV.bl_idname, text="Copy") - row.menu(copy_paste_uv.MenuPasteUV.bl_idname, + row.menu(copy_paste_uv.MUV_MT_CopyPasteUV_PasteUV.bl_idname, text="Paste") elif sc.muv_copy_paste_uv_mode == 'SEL_SEQ': - row.menu(copy_paste_uv.MenuSelSeqCopyUV.bl_idname, - text="Copy") - row.menu(copy_paste_uv.MenuSelSeqPasteUV.bl_idname, - text="Paste") + row.menu( + copy_paste_uv.MUV_MT_CopyPasteUV_SelSeqCopyUV.bl_idname, + text="Copy") + row.menu( + copy_paste_uv.MUV_MT_CopyPasteUV_SelSeqPasteUV.bl_idname, + text="Paste") box.prop(sc, "muv_copy_paste_uv_mode", expand=True) box.prop(sc, "muv_copy_paste_uv_copy_seams", text="Seams") box.prop(sc, "muv_copy_paste_uv_strategy", text="Strategy") @@ -76,8 +81,9 @@ class PanelCopyPasteUVEditMode(bpy.types.Panel): 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.OperatorCopyUV.bl_idname, text="Copy") - ops = row.operator(transfer_uv.OperatorPasteUV.bl_idname, + row.operator(transfer_uv.MUV_OT_TransferUV_CopyUV.bl_idname, + text="Copy") + ops = row.operator(transfer_uv.MUV_OT_TransferUV_PasteUV.bl_idname, text="Paste") ops.invert_normals = sc.muv_transfer_uv_invert_normals ops.copy_seams = sc.muv_transfer_uv_copy_seams 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 a9203d87..6dd0d3b4 100644 --- a/uv_magic_uv/ui/view3d_copy_paste_uv_objectmode.py +++ b/uv_magic_uv/ui/view3d_copy_paste_uv_objectmode.py @@ -26,20 +26,21 @@ __date__ = "17 Nov 2018" import bpy from ..op import copy_paste_uv_object - +from ..utils.bl_class_registry import BlClassRegistry __all__ = [ - 'PanelCopyPasteUVObjectMode', + 'MUV_PT_View3D_Object_CopyPasteUV', ] -class PanelCopyPasteUVObjectMode(bpy.types.Panel): +@BlClassRegistry() +class MUV_PT_View3D_Object_CopyPasteUV(bpy.types.Panel): """ Panel class: Copy/Paste UV on Property Panel on View3D """ bl_space_type = 'VIEW_3D' - bl_region_type = 'TOOLS' + bl_region_type = 'UI' bl_label = "Copy/Paste UV" bl_category = "Magic UV" bl_context = 'objectmode' @@ -47,16 +48,18 @@ class PanelCopyPasteUVObjectMode(bpy.types.Panel): def draw_header(self, _): layout = self.layout - layout.label(text="", icon='IMAGE_COL') + layout.label(text="", icon='IMAGE') def draw(self, context): sc = context.scene layout = self.layout row = layout.row(align=True) - row.menu(copy_paste_uv_object.MenuCopyUV.bl_idname, - text="Copy") - row.menu(copy_paste_uv_object.MenuPasteUV.bl_idname, - text="Paste") + row.menu( + copy_paste_uv_object.MUV_MT_CopyPasteUVObject_CopyUV.bl_idname, + text="Copy") + row.menu( + copy_paste_uv_object.MUV_MT_CopyPasteUVObject_PasteUV.bl_idname, + text="Paste") 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 be0bcf57..365a0dc8 100644 --- a/uv_magic_uv/ui/view3d_uv_manipulation.py +++ b/uv_magic_uv/ui/view3d_uv_manipulation.py @@ -25,28 +25,26 @@ __date__ = "17 Nov 2018" import bpy -from ..op import flip_rotate_uv -from ..op import mirror_uv -from ..op import move_uv -from ..op import preserve_uv_aspect -from ..op import texture_lock -from ..op import texture_wrap -from ..op import uv_sculpt -from ..op import world_scale_uv - +from ..op import ( + flip_rotate_uv, + mirror_uv, + move_uv, +) +from ..utils.bl_class_registry import BlClassRegistry __all__ = [ - 'PanelUVManipulation', + 'MUV_PT_View3D_UVManipulation', ] -class PanelUVManipulation(bpy.types.Panel): +@BlClassRegistry() +class MUV_PT_View3D_UVManipulation(bpy.types.Panel): """ Panel class: UV Manipulation on Property Panel on View3D """ bl_space_type = 'VIEW_3D' - bl_region_type = 'TOOLS' + bl_region_type = 'UI' bl_label = "UV Manipulation" bl_category = "Magic UV" bl_context = 'mesh_edit' @@ -54,7 +52,7 @@ class PanelUVManipulation(bpy.types.Panel): def draw_header(self, _): layout = self.layout - layout.label(text="", icon='IMAGE_COL') + layout.label(text="", icon='IMAGE') def draw(self, context): sc = context.scene @@ -64,7 +62,7 @@ class PanelUVManipulation(bpy.types.Panel): box.prop(sc, "muv_flip_rotate_uv_enabled", text="Flip/Rotate UV") if sc.muv_flip_rotate_uv_enabled: row = box.row() - ops = row.operator(flip_rotate_uv.Operator.bl_idname, + ops = row.operator(flip_rotate_uv.MUV_OT_FlipRotate.bl_idname, text="Flip/Rotate") ops.seams = sc.muv_flip_rotate_uv_seams row.prop(sc, "muv_flip_rotate_uv_seams", text="Seams") @@ -73,7 +71,8 @@ class PanelUVManipulation(bpy.types.Panel): box.prop(sc, "muv_mirror_uv_enabled", text="Mirror UV") if sc.muv_mirror_uv_enabled: row = box.row() - ops = row.operator(mirror_uv.Operator.bl_idname, text="Mirror") + ops = row.operator(mirror_uv.MUV_OT_MirrorUV.bl_idname, + text="Mirror") ops.axis = sc.muv_mirror_uv_axis row.prop(sc, "muv_mirror_uv_axis", text="") @@ -81,191 +80,9 @@ class PanelUVManipulation(bpy.types.Panel): box.prop(sc, "muv_move_uv_enabled", text="Move UV") if sc.muv_move_uv_enabled: col = box.column() - if not move_uv.Operator.is_running(context): - col.operator(move_uv.Operator.bl_idname, icon='PLAY', + if not move_uv.MUV_OT_MoveUV.is_running(context): + col.operator(move_uv.MUV_OT_MoveUV.bl_idname, icon='PLAY', text="Start") else: - col.operator(move_uv.Operator.bl_idname, icon='PAUSE', + col.operator(move_uv.MUV_OT_MoveUV.bl_idname, icon='PAUSE', text="Stop") - - box = layout.box() - box.prop(sc, "muv_world_scale_uv_enabled", text="World Scale UV") - if sc.muv_world_scale_uv_enabled: - box.prop(sc, "muv_world_scale_uv_mode", text="") - - if sc.muv_world_scale_uv_mode == 'MANUAL': - sp = box.split(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_aspect_enabled", - text="Preserve UV Aspect") - if sc.muv_preserve_uv_aspect_enabled: - row = box.row() - ops = row.operator( - preserve_uv_aspect.Operator.bl_idname, - text="Change Image") - ops.dest_img_name = sc.muv_preserve_uv_aspect_tex_image - ops.origin = sc.muv_preserve_uv_aspect_origin - row.prop(sc, "muv_preserve_uv_aspect_tex_image", text="") - box.prop(sc, "muv_preserve_uv_aspect_origin", text="Origin") - - box = layout.box() - box.prop(sc, "muv_texture_lock_enabled", text="Texture Lock") - if sc.muv_texture_lock_enabled: - row = box.row(align=True) - col = row.column(align=True) - col.label("Normal Mode:") - col = row.column(align=True) - 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_texture_lock_connect - col.prop(sc, "muv_texture_lock_connect", text="Connect") - - row = box.row(align=True) - row.label("Interactive Mode:") - 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_texture_wrap_enabled", text="Texture Wrap") - if sc.muv_texture_wrap_enabled: - row = box.row(align=True) - 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_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_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 2aa62c26..c596008e 100644 --- a/uv_magic_uv/ui/view3d_uv_mapping.py +++ b/uv_magic_uv/ui/view3d_uv_mapping.py @@ -25,23 +25,24 @@ __date__ = "17 Nov 2018" import bpy -from ..op import texture_projection -from ..op import unwrap_constraint -from ..op import uvw - +from ..op import ( + uvw, +) +from ..utils.bl_class_registry import BlClassRegistry __all__ = [ - 'UVMapping', + 'MUV_PT_View3D_UVMapping', ] -class UVMapping(bpy.types.Panel): +@BlClassRegistry() +class MUV_PT_View3D_UVMapping(bpy.types.Panel): """ Panel class: UV Mapping on Property Panel on View3D """ bl_space_type = 'VIEW_3D' - bl_region_type = 'TOOLS' + bl_region_type = 'UI' bl_label = "UV Mapping" bl_category = "Magic UV" bl_context = 'mesh_edit' @@ -49,60 +50,19 @@ class UVMapping(bpy.types.Panel): def draw_header(self, _): layout = self.layout - layout.label(text="", icon='IMAGE_COL') + layout.label(text="", icon='IMAGE') def draw(self, context): sc = context.scene layout = self.layout - box = layout.box() - box.prop(sc, "muv_unwrap_constraint_enabled", text="Unwrap Constraint") - if sc.muv_unwrap_constraint_enabled: - ops = box.operator( - unwrap_constraint.Operator.bl_idname, - text="Unwrap") - ops.u_const = sc.muv_unwrap_constraint_u_const - ops.v_const = sc.muv_unwrap_constraint_v_const - row = box.row(align=True) - row.prop(sc, "muv_unwrap_constraint_u_const", text="U-Constraint") - row.prop(sc, "muv_unwrap_constraint_v_const", text="V-Constraint") - - box = layout.box() - box.prop(sc, "muv_texture_projection_enabled", - text="Texture Projection") - if sc.muv_texture_projection_enabled: - row = box.row() - row.prop(sc, "muv_texture_projection_enable", - text="Disable" - if 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_texture_projection_adjust_window", - text="Adjust Window") - if not sc.muv_texture_projection_adjust_window: - row.prop(sc, "muv_texture_projection_tex_magnitude", - text="Magnitude") - col.prop(sc, "muv_texture_projection_apply_tex_aspect", - text="Texture Aspect Ratio") - col.prop(sc, "muv_texture_projection_assign_uvmap", - text="Assign UVMap") - box.operator(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.OperatorBoxMap.bl_idname, text="Box") + ops = row.operator(uvw.MUV_OT_UVW_BoxMap.bl_idname, text="Box") ops.assign_uvmap = sc.muv_uvw_assign_uvmap - ops = row.operator(uvw.OperatorBestPlanerMap.bl_idname, + ops = row.operator(uvw.MUV_OT_UVW_BestPlanerMap.bl_idname, text="Best Planner") ops.assign_uvmap = sc.muv_uvw_assign_uvmap box.prop(sc, "muv_uvw_assign_uvmap", text="Assign UVMap") diff --git a/uv_magic_uv/utils/__init__.py b/uv_magic_uv/utils/__init__.py new file mode 100644 index 00000000..4ce9d907 --- /dev/null +++ b/uv_magic_uv/utils/__init__.py @@ -0,0 +1,34 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +if "bpy" in locals(): + import importlib + importlib.reload(bl_class_registry) + importlib.reload(property_class_registry) +else: + from . import bl_class_registry + from . import property_class_registry + +import bpy diff --git a/uv_magic_uv/utils/bl_class_registry.py b/uv_magic_uv/utils/bl_class_registry.py new file mode 100644 index 00000000..d1730615 --- /dev/null +++ b/uv_magic_uv/utils/bl_class_registry.py @@ -0,0 +1,84 @@ +# + +# ##### 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 .. import common + +__all__ = [ + 'BlClassRegistry', +] + + +class BlClassRegistry: + class_list = [] + + def __init__(self, *_, **kwargs): + self.legacy = kwargs.get('legacy', False) + + def __call__(self, cls): + if hasattr(cls, "bl_idname"): + BlClassRegistry.add_class(cls.bl_idname, cls, self.legacy) + else: + bl_idname = "{}{}{}{}".format(cls.bl_space_type, + cls.bl_region_type, + cls.bl_context, cls.bl_label) + BlClassRegistry.add_class(bl_idname, cls, self.legacy) + return cls + + @classmethod + def add_class(cls, bl_idname, op_class, legacy): + for class_ in cls.class_list: + if (class_["bl_idname"] == bl_idname) and \ + (class_["legacy"] == legacy): + raise RuntimeError("{} is already registered" + .format(bl_idname)) + + new_op = { + "bl_idname": bl_idname, + "class": op_class, + "legacy": legacy, + } + cls.class_list.append(new_op) + common.debug_print("{} is registered.".format(bl_idname)) + + @classmethod + def register(cls): + for class_ in cls.class_list: + bpy.utils.register_class(class_["class"]) + common.debug_print("{} is registered to Blender." + .format(class_["bl_idname"])) + + @classmethod + def unregister(cls): + for class_ in cls.class_list: + bpy.utils.unregister_class(class_["class"]) + common.debug_print("{} is unregistered from Blender." + .format(class_["bl_idname"])) + + @classmethod + def cleanup(cls): + cls.class_list = [] + common.debug_print("Cleanup registry.") diff --git a/uv_magic_uv/utils/property_class_registry.py b/uv_magic_uv/utils/property_class_registry.py new file mode 100644 index 00000000..20df0342 --- /dev/null +++ b/uv_magic_uv/utils/property_class_registry.py @@ -0,0 +1,72 @@ +# + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +__author__ = "Nutti " +__status__ = "production" +__version__ = "5.2" +__date__ = "17 Nov 2018" + +from .. import common + +__all__ = [ + 'PropertyClassRegistry', +] + + +class PropertyClassRegistry: + class_list = [] + + def __init__(self, *_, **kwargs): + self.legacy = kwargs.get('legacy', False) + + def __call__(self, cls): + PropertyClassRegistry.add_class(cls.idname, cls, self.legacy) + return cls + + @classmethod + def add_class(cls, idname, prop_class, legacy): + for class_ in cls.class_list: + if (class_["idname"] == idname) and (class_["legacy"] == legacy): + raise RuntimeError("{} is already registered".format(idname)) + + new_op = { + "idname": idname, + "class": prop_class, + "legacy": legacy, + } + cls.class_list.append(new_op) + common.debug_print("{} is registered.".format(idname)) + + @classmethod + def init_props(cls, scene): + for class_ in cls.class_list: + class_["class"].init_props(scene) + common.debug_print("{} is initialized.".format(class_["idname"])) + + @classmethod + def del_props(cls, scene): + for class_ in cls.class_list: + class_["class"].del_props(scene) + common.debug_print("{} is cleared.".format(class_["idname"])) + + @classmethod + def cleanup(cls): + cls.class_list = [] + common.debug_print("Cleanup registry.") -- cgit v1.2.3 From fb75f259861bf3dcd82abfab8b10b83da947053e Mon Sep 17 00:00:00 2001 From: Brecht Van Lommel Date: Thu, 13 Dec 2018 19:38:08 +0100 Subject: Fix spelling of Miter in freestyle SVG export. Ref T59320. --- render_freestyle_svg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/render_freestyle_svg.py b/render_freestyle_svg.py index 6cc2e1b6..7e1ffc19 100644 --- a/render_freestyle_svg.py +++ b/render_freestyle_svg.py @@ -241,7 +241,7 @@ class SVGExport(bpy.types.PropertyGroup): line_join_type = EnumProperty( name="Linejoin", items=( - ('MITTER', "Mitter", "Corners are sharp", 0), + ('MITER', "Miter", "Corners are sharp", 0), ('ROUND', "Round", "Corners are smoothed", 1), ('BEVEL', "Bevel", "Corners are bevelled", 2), ), -- cgit v1.2.3 From 9cc2ad1eaf941d8ed3b5542a3d5cdfccec7ba60b Mon Sep 17 00:00:00 2001 From: Campbell Barton Date: Wed, 19 Dec 2018 11:35:45 +1100 Subject: Fix invalid string comparisons Identity checks should never be used with strings, it may fail based on Python's interning logic. --- ant_landscape/__init__.py | 4 ++-- ant_landscape/ant_functions.py | 4 ++-- archipack/archipack_fence.py | 2 +- archipack/archipack_stair.py | 2 +- development_iskeyfree.py | 10 +++++----- io_import_images_as_planes.py | 2 +- io_scene_fbx/import_fbx.py | 2 +- measureit/measureit_geometry.py | 6 +++--- mesh_bsurfaces.py | 2 +- mesh_extra_tools/mesh_extrude_and_reshape.py | 2 +- node_wrangler.py | 7 +++++-- space_view3d_pie_menus/pie_save_open_menu.py | 2 +- 12 files changed, 24 insertions(+), 21 deletions(-) diff --git a/ant_landscape/__init__.py b/ant_landscape/__init__.py index 68dd2342..8fbfb79d 100644 --- a/ant_landscape/__init__.py +++ b/ant_landscape/__init__.py @@ -391,7 +391,7 @@ class AntDisplaceSettingsPanel(bpy.types.Panel): if not ant.sphere_mesh: col = box.column() col.prop(ant, "edge_falloff") - if ant.edge_falloff is not "0": + if ant.edge_falloff != "0": col = box.column(align=True) col.prop(ant, "edge_level") if ant.edge_falloff in ["2", "3"]: @@ -401,7 +401,7 @@ class AntDisplaceSettingsPanel(bpy.types.Panel): col = box.column() col.prop(ant, "strata_type") - if ant.strata_type is not "0": + if ant.strata_type != "0": col = box.column() col.prop(ant, "strata") col = box.column() diff --git a/ant_landscape/ant_functions.py b/ant_landscape/ant_functions.py index 3c0f2c34..bee655c0 100644 --- a/ant_landscape/ant_functions.py +++ b/ant_landscape/ant_functions.py @@ -662,7 +662,7 @@ def draw_ant_displace(self, context, generate=True): if not self.sphere_mesh: col = box.column() col.prop(self, "edge_falloff") - if self.edge_falloff is not "0": + if self.edge_falloff != "0": col = box.column(align=True) col.prop(self, "edge_level") if self.edge_falloff in ["2", "3"]: @@ -672,7 +672,7 @@ def draw_ant_displace(self, context, generate=True): col = box.column() col.prop(self, "strata_type") - if self.strata_type is not "0": + if self.strata_type != "0": col = box.column() col.prop(self, "strata") diff --git a/archipack/archipack_fence.py b/archipack/archipack_fence.py index a94023fd..c2989be2 100644 --- a/archipack/archipack_fence.py +++ b/archipack/archipack_fence.py @@ -1489,7 +1489,7 @@ class ARCHIPACK_PT_fence(Panel): row = box.row(align=True) row.operator("archipack.fence_curve_update", text="", icon='FILE_REFRESH') row.prop_search(prop, "user_defined_path", scene, "objects", text="", icon='OUTLINER_OB_CURVE') - if prop.user_defined_path is not "": + if prop.user_defined_path != "": box.prop(prop, 'user_defined_spline') box.prop(prop, 'user_defined_resolution') box.prop(prop, 'angle_limit') diff --git a/archipack/archipack_stair.py b/archipack/archipack_stair.py index 94a8da7c..6e3d6212 100644 --- a/archipack/archipack_stair.py +++ b/archipack/archipack_stair.py @@ -2518,7 +2518,7 @@ class archipack_stair(ArchipackObject, Manipulable, PropertyGroup): self.setup_manipulators() - if self.presets is not 'STAIR_O': + if self.presets != 'STAIR_O': for i, part in enumerate(self.parts): if i >= self.n_parts: break diff --git a/development_iskeyfree.py b/development_iskeyfree.py index 7851afee..332e11d4 100644 --- a/development_iskeyfree.py +++ b/development_iskeyfree.py @@ -105,18 +105,18 @@ class MyChecker(): cls.mylist.clear() for e in sortkeys: cmd = "" - if e[2] is not "": + if e[2] != "": cmd += e[2] + "+" - if e[3] is not "": + if e[3] != "": cmd += e[3] + "+" - if e[4] is not "": + if e[4] != "": cmd += e[4] + "+" - if e[5] is not "": + if e[5] != "": cmd += e[5] + "+" cmd += e[1] - if e[6] is not "": + if e[6] != "": cmd += " " + e[6] cls.mylist.append([e[0], cmd]) diff --git a/io_import_images_as_planes.py b/io_import_images_as_planes.py index 1f32bc5f..a4aa98f2 100644 --- a/io_import_images_as_planes.py +++ b/io_import_images_as_planes.py @@ -1094,7 +1094,7 @@ class IMPORT_IMAGE_OT_to_plane(Operator, AddObjectHelper): bpy.ops.mesh.primitive_plane_add('INVOKE_REGION_WIN') plane = context.scene.objects.active # Why does mesh.primitive_plane_add leave the object in edit mode??? - if plane.mode is not 'OBJECT': + if plane.mode != 'OBJECT': bpy.ops.object.mode_set(mode='OBJECT') plane.dimensions = width, height, 0.0 plane.data.name = plane.name = name diff --git a/io_scene_fbx/import_fbx.py b/io_scene_fbx/import_fbx.py index 90dc08df..addb8ae6 100644 --- a/io_scene_fbx/import_fbx.py +++ b/io_scene_fbx/import_fbx.py @@ -1126,7 +1126,7 @@ def blen_read_geom_layer_normal(fbx_obj, mesh, xform=None): bdata = [None] * len(blen_data) if is_fake else blen_data if func(mesh, bdata, "normal", fbx_layer_data, fbx_layer_index, fbx_layer_mapping, fbx_layer_ref, 3, 3, layer_id, xform, True): - if blen_data_type is "Polygons": + if blen_data_type == "Polygons": for pidx, p in enumerate(mesh.polygons): for lidx in range(p.loop_start, p.loop_start + p.loop_total): mesh.loops[lidx].normal[:] = bdata[pidx] diff --git a/measureit/measureit_geometry.py b/measureit/measureit_geometry.py index 3b2e0186..e0b036d6 100644 --- a/measureit/measureit_geometry.py +++ b/measureit/measureit_geometry.py @@ -831,9 +831,9 @@ def draw_text(myobj, pos2d, display_text, rgb, fsize, align='L', text_rot=0.0): # ------------------- for line in mylines: text_width, text_height = blf.dimensions(font_id, line) - if align is 'C': + if align == 'C': newx = x_pos - text_width / 2 - elif align is 'R': + elif align == 'R': newx = x_pos - text_width - gap else: newx = x_pos @@ -851,7 +851,7 @@ def draw_text(myobj, pos2d, display_text, rgb, fsize, align='L', text_rot=0.0): if maxwidth < text_width: maxwidth = text_width - if align is 'L': + if align == 'L': blf.disable(font_id, ROTATION) return maxwidth, maxheight diff --git a/mesh_bsurfaces.py b/mesh_bsurfaces.py index ffe16e04..53b88d49 100644 --- a/mesh_bsurfaces.py +++ b/mesh_bsurfaces.py @@ -2928,7 +2928,7 @@ class GPENCIL_OT_SURFSK_add_surface(Operator): for i in range(0, len(surface_splines_parsed[0])): surface_splines_parsed[0][i] = self.main_object.matrix_world * verts_ordered_V2[i].co - # When "Automatic join" option is active (and the selection type is not "TWO_CONNECTED"), + # When "Automatic join" option is active (and the selection type != "TWO_CONNECTED"), # merge the verts of the tips of the loops when they are "near enough" if self.automatic_join and selection_type != "TWO_CONNECTED": # Join the tips of "Follow" loops that are near enough and must be "closed" diff --git a/mesh_extra_tools/mesh_extrude_and_reshape.py b/mesh_extra_tools/mesh_extrude_and_reshape.py index 8716bfb1..f4eef683 100644 --- a/mesh_extra_tools/mesh_extrude_and_reshape.py +++ b/mesh_extra_tools/mesh_extrude_and_reshape.py @@ -245,7 +245,7 @@ class Extrude_and_Reshape(Operator): @classmethod def poll(cls, context): - return context.mode is not 'EDIT_MESH' + return context.mode != 'EDIT_MESH' def modal(self, context, event): if self.confirm: diff --git a/node_wrangler.py b/node_wrangler.py index 6ccf2ecd..b535a580 100644 --- a/node_wrangler.py +++ b/node_wrangler.py @@ -2358,7 +2358,10 @@ class NWCopySettings(Operator, NWBase): def poll(cls, context): valid = False if nw_check(context): - if context.active_node is not None and context.active_node.type is not 'FRAME': + if ( + context.active_node is not None and + context.active_node.type != 'FRAME' + ): valid = True return valid @@ -2941,7 +2944,7 @@ class NWAddReroutes(Operator, NWBase): reroutes_count = 0 # will be used when aligning reroutes added to hidden nodes for out_i, output in enumerate(node.outputs): pass_used = False # initial value to be analyzed if 'R_LAYERS' - # if node is not 'R_LAYERS' - "pass_used" not needed, so set it to True + # if node != 'R_LAYERS' - "pass_used" not needed, so set it to True if node.type != 'R_LAYERS': pass_used = True else: # if 'R_LAYERS' check if output represent used render pass diff --git a/space_view3d_pie_menus/pie_save_open_menu.py b/space_view3d_pie_menus/pie_save_open_menu.py index bdd9b63c..3d48e591 100644 --- a/space_view3d_pie_menus/pie_save_open_menu.py +++ b/space_view3d_pie_menus/pie_save_open_menu.py @@ -130,7 +130,7 @@ class FileIncrementalSave(Operator): @classmethod def poll(cls, context): - return (bpy.data.filepath is not "") + return (bpy.data.filepath != "") def execute(self, context): f_path = bpy.data.filepath -- cgit v1.2.3