├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.md │ └── feature-request.md └── workflows │ └── magic-uv-ci.yml ├── .gitignore ├── .pep8 ├── .pylintrc ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── ISSUES.md ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── dev_tools ├── debug.py └── update_timestamp.rb ├── docs ├── installation.md ├── official │ └── magic_uv.rst └── tutorial.md ├── pycodestyle ├── requirements.txt ├── src └── magic_uv │ ├── __init__.py │ ├── common.py │ ├── gpu_utils │ ├── __init__.py │ ├── imm.py │ ├── shader.py │ └── shaders │ │ ├── image_color_frag.glsl │ │ ├── image_color_scissor_frag.glsl │ │ ├── image_color_vert.glsl │ │ ├── polyline_uniform_color_scissor_frag.glsl │ │ ├── polyline_uniform_color_scissor_geom.glsl │ │ ├── polyline_uniform_color_scissor_vert.glsl │ │ ├── uniform_color_scissor_frag.glsl │ │ └── uniform_color_scissor_vert.glsl │ ├── op │ ├── __init__.py │ ├── align_uv.py │ ├── align_uv_cursor.py │ ├── clip_uv.py │ ├── copy_paste_uv.py │ ├── copy_paste_uv_object.py │ ├── copy_paste_uv_uvedit.py │ ├── flip_rotate_uv.py │ ├── mirror_uv.py │ ├── move_uv.py │ ├── pack_uv.py │ ├── preserve_uv_aspect.py │ ├── select_uv.py │ ├── smooth_uv.py │ ├── texture_lock.py │ ├── texture_projection.py │ ├── texture_wrap.py │ ├── transfer_uv.py │ ├── unwrap_constraint.py │ ├── uv_bounding_box.py │ ├── uv_inspection.py │ ├── uv_sculpt.py │ ├── uvw.py │ └── world_scale_uv.py │ ├── preferences.py │ ├── properties.py │ ├── ui │ ├── IMAGE_MT_uvs.py │ ├── VIEW3D_MT_object.py │ ├── VIEW3D_MT_uv_map.py │ ├── __init__.py │ ├── uvedit_copy_paste_uv.py │ ├── uvedit_editor_enhancement.py │ ├── uvedit_uv_manipulation.py │ ├── view3d_copy_paste_uv_editmode.py │ ├── view3d_copy_paste_uv_objectmode.py │ ├── view3d_uv_manipulation.py │ └── view3d_uv_mapping.py │ └── utils │ ├── __init__.py │ ├── bl_class_registry.py │ ├── compatibility.py │ ├── graph.py │ └── property_class_registry.py ├── tests ├── Dockerfile ├── lint │ ├── pep8.sh │ └── pylint.sh └── python │ ├── magic_uv_test │ ├── __init__.py │ ├── align_uv_cursor_test.py │ ├── align_uv_test.py │ ├── clip_uv_test.py │ ├── common.py │ ├── compatibility.py │ ├── copy_paste_uv_object_test.py │ ├── copy_paste_uv_test.py │ ├── copy_paste_uv_uvedit_test.py │ ├── flip_rotate_uv_test.py │ ├── mirror_uv_test.py │ ├── move_uv_test.py │ ├── pack_uv_test.py │ ├── preserve_uv_aspect_test.py │ ├── select_uv_test.py │ ├── smooth_uv_test.py │ ├── texture_lock_test.py │ ├── texture_projection_test.py │ ├── texture_wrap_test.py │ ├── transfer_uv_test.py │ ├── unwrap_constraint_test.py │ ├── uv_bounding_box_test.py │ ├── uv_inspection_test.py │ ├── uv_sculpt_test.py │ ├── uvw_test.py │ └── world_scale_uv_test.py │ └── run_tests.py └── tools ├── install.sh └── muv_piemenu.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [nutti] 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 3 | about: Use this template for reporting a bug. 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **System Information** 11 | 12 | * OS: [e.g. Windows] 13 | * Blender version: [e.g. 2.79] 14 | * Add-on version: [e.g. 5.2] 15 | 16 | 17 | **Expected behavior** 18 | *The behavior you expect about the feature you reported.* 19 | 20 | 21 | **Description about the bug** 22 | *The description about the bug.* 23 | 24 | 25 | **Screenshots/Files** [Optional] 26 | *It is good to solve the bug if you attach the screenshots or .blend file.* 27 | 28 | 29 | **Additional comments** [Optional] 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature-request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Use this template for feature request. 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Description about the feature** 11 | *The description about the feature. It is good to clear who will benefit with this feature.* 12 | 13 | 14 | **Are you willing to contribute about this feature. (Yes/No)** 15 | 16 | 17 | **Screenshots** [Optional] 18 | *It is good to understand the features if you attach the screenshotsfile.* 19 | 20 | 21 | **Additional comments** [Optional] 22 | -------------------------------------------------------------------------------- /.github/workflows/magic-uv-ci.yml: -------------------------------------------------------------------------------- 1 | name: Magic-UV CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - 'run-ci/**' 8 | tags: 9 | - 'v*' 10 | pull_request: 11 | 12 | jobs: 13 | build: 14 | name: Build add-on 15 | runs-on: ubuntu-18.04 16 | steps: 17 | - name: Checkout repo 18 | uses: actions/checkout@v2 19 | - name: Setup Python 20 | uses: actions/setup-python@v2 21 | with: 22 | python-version: "3.7" 23 | - name: Get required packages for Blender 24 | run: | 25 | sudo apt-get update -qq 26 | sudo apt-get install -y blender wget python3 python3-pip zip 27 | - name: Get required pip packages 28 | run: pip3 install -r requirements.txt 29 | - name: Do pylint test 30 | run: bash tests/lint/pylint.sh src/magic_uv 31 | - name: Do pep8 test 32 | run: bash tests/lint/pep8.sh src/magic_uv 33 | - name: Download Blender (2.7x) 34 | run: | 35 | wget http://mirror.cs.umn.edu/blender.org/release/Blender2.77/blender-2.77-linux-glibc211-x86_64.tar.bz2 36 | tar jxf blender-2.77-linux-glibc211-x86_64.tar.bz2 37 | - name: Copy add-on to Blender add-on's directory (2.7x) 38 | run: cp -r src/magic_uv blender-2.77-linux-glibc211-x86_64/2.77/scripts/addons 39 | - name: Run add-on unittest (2.7x) 40 | run: blender-2.77-linux-glibc211-x86_64/blender --factory-startup --background -noaudio --python tests/python/run_tests.py 41 | env: 42 | MUV_CONSOLE_MODE: true 43 | - name: Download Blender (2.8x) 44 | run: | 45 | wget https://download.blender.org/release/Blender2.83/blender-2.83.3-linux64.tar.xz 46 | tar xf blender-2.83.3-linux64.tar.xz 47 | - name: Copy add-on to Blender add-on's directory (2.8x) 48 | run: cp -r src/magic_uv blender-2.83.3-linux64/2.83/scripts/addons 49 | - name: Run add-on unittest (2.8x) 50 | run: blender-2.83.3-linux64/blender --factory-startup --background -noaudio --python tests/python/run_tests.py 51 | env: 52 | MUV_CONSOLE_MODE: true 53 | - name: Compress add-on 54 | run: | 55 | mkdir release 56 | cd src 57 | zip -r magic_uv.zip magic_uv 58 | cd .. 59 | mv src/magic_uv.zip release 60 | - name: Upload artifact 61 | uses: actions/upload-artifact@v2 62 | with: 63 | name: magic_uv 64 | path: "release" 65 | 66 | publish: 67 | name: Publish Add-on 68 | needs: build 69 | if: startsWith(github.ref, 'refs/tags/v') 70 | runs-on: ubuntu-18.04 71 | steps: 72 | - name: Fetch Artifacts 73 | uses: actions/download-artifact@v2 74 | with: 75 | path: dist 76 | - name: Create Release 77 | id: create_release 78 | uses: actions/create-release@v1 79 | env: 80 | GITHUB_TOKEN: ${{ secrets.TOKEN_FOR_ACTIONS }} 81 | with: 82 | tag_name: ${{ github.ref }} 83 | release_name: ${{ github.ref }} 84 | draft: true 85 | prerelease: false 86 | - name: Publish Add-on to GitHub Release Page 87 | uses: actions/upload-release-asset@v1 88 | env: 89 | GITHUB_TOKEN: ${{ secrets.TOKEN_FOR_ACTIONS }} 90 | with: 91 | upload_url: ${{ steps.create_release.outputs.upload_url }} 92 | asset_path: dist/magic_uv/magic_uv.zip 93 | asset_name: magic_uv.zip 94 | asset_content_type: application/zip 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # not allow temporary files 2 | *~ 3 | *.swp 4 | .DS_Store 5 | 6 | venv 7 | 8 | .idea/ 9 | __pycache__/ 10 | -------------------------------------------------------------------------------- /.pep8: -------------------------------------------------------------------------------- 1 | [pep8] 2 | ignore = E402, W503, E701 3 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | disable= 4 | C0111, 5 | C0201, 6 | C0302, 7 | C0412, 8 | F0401, 9 | R0201, 10 | R0204, 11 | R0902, 12 | R0903, 13 | R0911, 14 | R0912, 15 | R0913, 16 | R0914, 17 | R0915, 18 | R1705, 19 | R1721, 20 | R1723, 21 | W0511, 22 | W0640, 23 | E1121 24 | 25 | # C0412, R0201, R0204, R0912, R0913, R0914, R0915, W0640 should removed 26 | 27 | variable-rgx=[a-z_][a-z0-9_]{0,30}$ 28 | function-rgx=[a-z_][a-z0-9_]{2,40}$ 29 | class-rgx=[A-Z_][a-zA-Z0-9_]+$ 30 | argument-rgx=[a-z_][a-z0-9_]{0,40}$ 31 | attr-rgx=[a-z_][a-z0-9_]{0,30}$ 32 | ignored-modules=magic_uv.common 33 | class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,40}|(__.*__))$ 34 | module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9_]+))$ 35 | method-rgx=[a-z_][a-z0-9_]{2,40}$ 36 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at nutti.metro@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution 2 | 3 | If you want to contribute this project, please send pull request to **master** branch. 4 | 5 | https://github.com/nutti/Magic-UV/tree/master 6 | -------------------------------------------------------------------------------- /ISSUES.md: -------------------------------------------------------------------------------- 1 | # Bug Report / Feature Request / Disscussions 2 | 3 | If you want to report problem or request feature, please make issues. 4 | 5 | https://github.com/nutti/Magic-UV/issues 6 | 7 | You can discuss Magic UV at other places. 8 | See the link below for further details. 9 | 10 | * [Blender Wiki page](https://en.blender.org/index.php/Extensions:2.6/Py/Scripts/UV/Magic_UV) 11 | * [Blender Artist](https://blenderartists.org/t/magic-uv/1134694) 12 | * [Google+](https://plus.google.com/100058529622539760372/posts/82eS2tGE6Nc) 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This program is free software; you can redistribute it and/or 2 | modify it under the terms of the GNU General Public License 3 | as published by the Free Software Foundation; either version 2 4 | of the License, or (at your option) any later version. 5 | 6 | This program is distributed in the hope that it will be useful, 7 | but WITHOUT ANY WARRANTY; without even the implied warranty of 8 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 9 | GNU General Public License for more details. 10 | 11 | You should have received a copy of the GNU General Public License 12 | along with this program; if not, write to the Free Software Foundation, 13 | Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 14 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **Purpose of the pull request** 2 | *The purpose of the pull request. (e.g. Fix Bug, Feature request)* 3 | 4 | 5 | **Description about the pull request** 6 | *The description about this pull request.* 7 | 8 | 9 | **Additional comments** [Optional] 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Blender Add-on: Magic UV 2 | 3 | This is a blender add-on **Magic UV** consisted of many UV manipulation features which Blender lack of. 4 | Magic UV is also known as Copy/Paste UV for older version. 5 | 6 | ![Magic UV Thumbnail](https://raw.githubusercontent.com/wiki/nutti/Magic-UV/images/magic_uv_thumbnail.png) 7 | 8 | "Magic UV" is in **Release** support level. (**Contrib** support level in Blender version <2.79) 9 | So, stable version is included on **Blender**. 10 | Of course, you can also download older version. 11 | 12 | If you want to try newest but unstable version, you can download it from [unstable version](https://github.com/nutti/Magic-UV/archive/master.zip). 13 | 14 | 15 | ## Download 16 | 17 | Magic UV is in **Release** support level, so it is included on Blender itself. (**Contrib** support level in Blender version <2.79) 18 | Of course, you can also download older version. 19 | 20 | You can all released version from [Release Page](https://github.com/nutti/Magic-UV/releases). 21 | 22 | If you want to try newest but unstable version, you can download it from [unstable version](https://github.com/nutti/Magic-UV/archive/master.zip). 23 | 24 | 25 | ## Features / Tutorials 26 | 27 | Magic UV supports **English** only. 28 | Magic UV supports both Blender 2.7x (>=2.78) and Blender 2.8. 29 | Features list and Tutorials are available on [Wiki Page](https://github.com/nutti/Magic-UV/wiki/Tutorial). 30 | 31 | 32 | ## Change Log 33 | 34 | See [CHANGELOG.md](CHANGELOG.md) 35 | 36 | 37 | ## Bug report / Feature request / Disscussions 38 | 39 | If you want to report bug, request features or discuss about this add-on, see [ISSUES.md](ISSUES.md). 40 | 41 | 42 | ## Contribution 43 | 44 | If you want to contribute to this project, see [CONTRIBUTING.md](CONTRIBUTING.md). 45 | 46 | 47 | ## Project Authors 48 | 49 | 50 | ### Owner 51 | 52 | [**@nutti**](https://github.com/nutti) 53 | 54 | Indie Game/Application Developer. 55 | Especially, I spend most time to improve Blender and Unreal Game Engine via providing the extensions. 56 | 57 | Support via [GitHub Sponsors](https://github.com/sponsors/nutti) 58 | 59 | * CONTACTS: [Twitter](https://twitter.com/nutti__) 60 | * WEBSITE: [Japanese Only](https://colorful-pico.net/) 61 | 62 | 63 | ### Contributors 64 | 65 | * [**@mifth**](https://github.com/mifth) 66 | * [**@maxRobinot**](https://github.com/maxRobinot) 67 | * [**@MatthiasThDs**](https://github.com/MatthiasThDs) 68 | * [**@theCryingMan**](https://github.com/theCryingMan) 69 | * [**@PratikBorhade302**](https://github.com/PratikBorhade302) 70 | * [**@c-d-a**](https://github.com/c-d-a) 71 | -------------------------------------------------------------------------------- /dev_tools/debug.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | DEBUGGING = False 4 | 5 | def start_debug(): 6 | if DEBUGGING is False: 7 | return 8 | 9 | PYDEV_SRC_DIR = 'XXXXXX\\eclipse\\plugins\\org.python.pydev_3.9.2.201502050007\\pysrc' 10 | if PYDEV_SRC_DIR not in sys.path: 11 | sys.path.append(PYDEV_SRC_DIR) 12 | import pydevd 13 | pydevd.settrace() 14 | print("started blender script debugging...") 15 | -------------------------------------------------------------------------------- /dev_tools/update_timestamp.rb: -------------------------------------------------------------------------------- 1 | require 'fileutils' 2 | require 'date' 3 | 4 | if ARGV.size != 1 then 5 | puts "Usage: update_timestamp.rb " 6 | end 7 | 8 | src_dir_path = ARGV[0].sub(/\/$/, "") 9 | tmp_dir_path = 'tmp' 10 | 11 | filelist = [] 12 | ignore_filelist = [] 13 | entry = Dir.glob(src_dir_path + '/**/**') 14 | entry.each {|e| 15 | next e if File::ftype(e) == 'directory' 16 | next e if ignore_filelist.include?(e) 17 | filelist.push(e) 18 | } 19 | 20 | FileUtils.mkdir_p(tmp_dir_path) unless FileTest.exist?(tmp_dir_path) 21 | 22 | bl_ver = nil 23 | 24 | src_file = File.open(src_dir_path + '/__init__.py', 'r') 25 | src_file.each_line do |line| 26 | if /\s*"version"\s*:\s*\(\s*(\d+)\s*,\s*(\d+)\s*\,\s*(\d+)\s*\),\s*/ =~ line 27 | bl_ver = $1 + '.' + $2 28 | end 29 | end 30 | 31 | if bl_ver == nil 32 | print "Blender Version is not found" 33 | exit 1 34 | end 35 | 36 | tmp_filelist = [] 37 | 38 | filelist.each {|src_path| 39 | next src_path if File.extname(src_path) != '.py' 40 | 41 | path = src_path.dup 42 | path.slice!(src_dir_path) 43 | dest_path = tmp_dir_path + path 44 | 45 | # make sub directory 46 | idx = dest_path.rindex("/") 47 | sub_dir_path = dest_path[0..idx-1] 48 | FileUtils.mkdir_p(sub_dir_path) unless FileTest.exist?(sub_dir_path) 49 | 50 | # make converted file 51 | File.open(src_path, 'r') {|src_file| 52 | File.open(dest_path, 'wb') {|dest_file| 53 | src_file.each_line do |line| 54 | if /^__date__/ =~ line 55 | today = Date.today 56 | mon = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] 57 | line = '__date__ = "' + today.day.to_s + ' ' + mon[today.month-1] + ' ' + today.year.to_s + '"' 58 | end 59 | if /^__version__/ =~ line 60 | line = '__version__ = "' + bl_ver + '"' 61 | end 62 | dest_file.puts(line) 63 | end 64 | } 65 | } 66 | tmp_filelist.push([src_path, dest_path]) 67 | } 68 | 69 | tmp_filelist.each {|file| 70 | FileUtils.cp(file[1], file[0]) 71 | puts file[0] 72 | } 73 | 74 | 75 | FileUtils.rm_rf(tmp_dir_path) if FileTest.exist?(tmp_dir_path) 76 | 77 | exit 0 78 | -------------------------------------------------------------------------------- /docs/installation.md: -------------------------------------------------------------------------------- 1 | # Installation 2 | 3 | *Notice: From Blender >=2.79, the newest stable version of Magic UV is included on Blender by default. If you use Blender >=2.79 you can skip step from 1 to 3* 4 | 5 | 6 | #### 1. Download zip archive from below links 7 | 8 | |Version|Download URL| 9 | |---|---| 10 | |*unstable*|[Download](https://github.com/nutti/Magic-UV/archive/master.zip)| 11 | |6.6|[Download](https://github.com/nutti/Magic-UV/releases/tag/v6.6)| 12 | |6.5|[Download](https://github.com/nutti/Magic-UV/releases/tag/v6.5)| 13 | |6.4|[Download](https://github.com/nutti/Magic-UV/releases/tag/v6.4)| 14 | |6.3|[Download](https://github.com/nutti/Magic-UV/releases/tag/v6.3)| 15 | |6.2|[Download](https://github.com/nutti/Magic-UV/releases/tag/v6.2)| 16 | |6.1|[Download](https://github.com/nutti/Magic-UV/releases/tag/v6.1)| 17 | |6.0|[Download](https://github.com/nutti/Magic-UV/releases/tag/v6.0)| 18 | |5.2|[Download](https://github.com/nutti/Magic-UV/releases/tag/v5.2)| 19 | |5.1|[Download](https://github.com/nutti/Magic-UV/releases/tag/v5.1)| 20 | |5.0|[Download](https://github.com/nutti/Magic-UV/releases/tag/v5.0)| 21 | |4.5|[Download](https://github.com/nutti/Magic-UV/releases/tag/v4.5)| 22 | |4.4|[Download](https://github.com/nutti/Magic-UV/releases/tag/v4.4)| 23 | |4.3|[Download](https://github.com/nutti/Magic-UV/releases/tag/v4.3)| 24 | |4.2|[Download](https://github.com/nutti/Magic-UV/releases/tag/v4.2)| 25 | |4.1|[Download](https://github.com/nutti/Magic-UV/releases/tag/v4.1)| 26 | |4.0|[Download](https://github.com/nutti/Magic-UV/releases/tag/v4.0)| 27 | |3.2|[Download](https://github.com/nutti/Magic-UV/releases/tag/v3.2)| 28 | |3.1|[Download](https://github.com/nutti/Magic-UV/releases/tag/v3.1)| 29 | |3.0|[Download](https://github.com/nutti/Magic-UV/releases/tag/v3.0)| 30 | |2.2|[Download](https://github.com/nutti/Magic-UV/releases/tag/v2.2)| 31 | |2.1|[Download](https://github.com/nutti/Magic-UV/releases/tag/v2.1)| 32 | |2.0|[Download](https://github.com/nutti/Magic-UV/releases/tag/v2.0)| 33 | |1.1|[Download](https://github.com/nutti/Magic-UV/releases/tag/v1.1)| 34 | |1.0|[Download](https://github.com/nutti/Magic-UV/releases/tag/v1.0)| 35 | 36 | 37 | #### 2. Unzip it, and check the add-on sources 38 | 39 | Unzip the .zip file you downloaded. 40 | Then, check if there are add-on sources. 41 | 42 | Add-on sources are located on the different places depending on the add-on version. 43 | 44 | |Version|Sources| 45 | |---|---| 46 | |*unstable*|src/magic_uv| 47 | |6.0 - 6.6|magic_uv| 48 | |4.0 - 5.2|uv_magic_uv| 49 | |2.2 - 3.2|uv_copy_and_paste_uv| 50 | |1.0 - 2.2|uv_copy_and_paste_uv.py| 51 | 52 | 53 | #### 3. Copy add-on sources into your add-on folder 54 | 55 | Location of add-on folder depends on your operating system. 56 | 57 | |OS|Location| 58 | |---|---| 59 | |Windows|`C:\Users\\AppData\Roaming\Blender Foundation\Blender\\scripts\addons`| 60 | |Mac|`/Users//Library/Application Support/Blender//scripts/addons`| 61 | |Linux|`/home//.config/blender//scripts/addons`| 62 | 63 | If you use Blender which version is >= 2.79, you must remove released Magic UV add-on from Blender official add-ons directory. 64 | Then, copy add-on sources into Blender official add-ons directory. 65 | 66 | 67 | #### 4. Enable add-on 68 | 69 | Enable add-on, then you can use add-on Magic UV. 70 | Be careful that this add-on has different name depending on the version. 71 | 72 | |Version|Name| 73 | |---|---| 74 | |*unstable*|Magic UV| 75 | |4.0 - 6.6|Magic UV| 76 | |1.0 - 3.2|Copy and Paste UV| 77 | -------------------------------------------------------------------------------- /docs/tutorial.md: -------------------------------------------------------------------------------- 1 | # Tutorials 2 | 3 | See [official documents](https://docs.blender.org/manual/en/latest/addons/uv/magic_uv.html). 4 | 5 | *NOTE: Because of the abolition of Tool shelf, all panels of Magic UV were moved to the Sidebar since Blender 2.8. Before Blender 2.7x, you can find all panels on the Tool shelf.* 6 | -------------------------------------------------------------------------------- /pycodestyle: -------------------------------------------------------------------------------- 1 | [pycodestyle] 2 | ignore = E741 -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pep8==1.7.0 2 | pylint==2.5.3 3 | -------------------------------------------------------------------------------- /src/magic_uv/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | 9 | bl_info = { 10 | "name": "Magic UV", 11 | "author": "Nutti, Mifth, Jace Priester, kgeogeo, mem, imdjs, " 12 | "Keith (Wahooney) Boshoff, McBuff, MaxRobinot, " 13 | "Alexander Milovsky, Dusan Stevanovic, MatthiasThDs, " 14 | "theCryingMan, PratikBorhade302, chedap", 15 | "version": (6, 6, 0), 16 | "blender": (2, 80, 0), 17 | "location": "See Add-ons Preferences", 18 | "description": "UV Toolset. See Add-ons Preferences for details", 19 | "warning": "", 20 | "support": "COMMUNITY", 21 | "doc_url": "{BLENDER_MANUAL_URL}/addons/uv/magic_uv.html", 22 | "tracker_url": "https://github.com/nutti/Magic-UV", 23 | "category": "UV", 24 | } 25 | 26 | 27 | if "bpy" in locals(): 28 | import importlib 29 | importlib.reload(common) 30 | importlib.reload(gpu_utils) 31 | importlib.reload(utils) 32 | utils.bl_class_registry.BlClassRegistry.cleanup() 33 | importlib.reload(op) 34 | importlib.reload(ui) 35 | importlib.reload(properties) 36 | importlib.reload(preferences) 37 | else: 38 | import bpy 39 | from . import common 40 | from . import gpu_utils 41 | from . import utils 42 | from . import op 43 | from . import ui 44 | from . import properties 45 | from . import preferences 46 | 47 | import bpy 48 | 49 | 50 | def register(): 51 | gpu_utils.shader.ShaderManager.register_shaders() 52 | utils.bl_class_registry.BlClassRegistry.register() 53 | properties.init_props(bpy.types.Scene) 54 | user_prefs = utils.compatibility.get_user_preferences(bpy.context) 55 | if user_prefs.addons['magic_uv'].preferences.enable_builtin_menu: 56 | preferences.add_builtin_menu() 57 | 58 | 59 | def unregister(): 60 | preferences.remove_builtin_menu() 61 | properties.clear_props(bpy.types.Scene) 62 | utils.bl_class_registry.BlClassRegistry.unregister() 63 | gpu_utils.shader.ShaderManager.unregister_shaders() 64 | 65 | 66 | if __name__ == "__main__": 67 | register() 68 | -------------------------------------------------------------------------------- /src/magic_uv/gpu_utils/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | if "bpy" in locals(): 4 | import importlib 5 | # pylint: disable=E0601 6 | importlib.reload(shader) 7 | importlib.reload(imm) 8 | else: 9 | from . import shader 10 | from . import imm 11 | 12 | # pylint: disable=C0413 13 | import bpy 14 | -------------------------------------------------------------------------------- /src/magic_uv/gpu_utils/shader.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | import os 4 | import gpu 5 | 6 | 7 | class ShaderManager: 8 | shader_instances = {} 9 | 10 | SHADER_FILES = { 11 | 'IMAGE_COLOR': { 12 | "vertex": "image_color_vert.glsl", 13 | "fragment": "image_color_frag.glsl", 14 | }, 15 | 'IMAGE_COLOR_SCISSOR': { 16 | "vertex": "image_color_vert.glsl", 17 | "fragment": "image_color_scissor_frag.glsl", 18 | }, 19 | 'UNIFORM_COLOR_SCISSOR': { 20 | "vertex": "uniform_color_scissor_vert.glsl", 21 | "fragment": "uniform_color_scissor_frag.glsl", 22 | }, 23 | 'POLYLINE_UNIFORM_COLOR_SCISSOR': { 24 | "vertex": "polyline_uniform_color_scissor_vert.glsl", 25 | "fragment": "polyline_uniform_color_scissor_frag.glsl", 26 | "geometry": "polyline_uniform_color_scissor_geom.glsl", 27 | }, 28 | } 29 | 30 | @classmethod 31 | def register_shaders(cls): 32 | if gpu.platform.backend_type_get() != 'OPENGL': 33 | return 34 | 35 | for shader_name, shader_files in cls.SHADER_FILES.items(): 36 | vert_code = None 37 | frag_code = None 38 | geom_code = None 39 | for category, filename in shader_files.items(): 40 | filepath = f"{os.path.dirname(__file__)}/shaders/{filename}" 41 | with open(filepath, "r", encoding="utf-8") as f: 42 | code = f.read() 43 | 44 | if category == "vertex": 45 | vert_code = code 46 | elif category == "fragment": 47 | frag_code = code 48 | elif category == 'geometry': 49 | geom_code = code 50 | if geom_code is not None: 51 | instance = gpu.types.GPUShader( 52 | vert_code, frag_code, geocode=geom_code) 53 | else: 54 | instance = gpu.types.GPUShader(vert_code, frag_code) 55 | cls.shader_instances[shader_name] = instance 56 | 57 | @classmethod 58 | def unregister_shaders(cls): 59 | if gpu.platform.backend_type_get() != 'OPENGL': 60 | return 61 | 62 | for instance in cls.shader_instances.values(): 63 | del instance 64 | cls.shader_instances = {} 65 | 66 | @classmethod 67 | def get_shader(cls, shader_name): 68 | if gpu.platform.backend_type_get() != 'OPENGL': 69 | return None 70 | 71 | return cls.shader_instances[shader_name] 72 | -------------------------------------------------------------------------------- /src/magic_uv/gpu_utils/shaders/image_color_frag.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D image; 2 | uniform vec4 color; 3 | 4 | in vec2 connection; 5 | out vec4 fragColor; 6 | 7 | void main() 8 | { 9 | fragColor = texture(image, connection) * color; 10 | } -------------------------------------------------------------------------------- /src/magic_uv/gpu_utils/shaders/image_color_scissor_frag.glsl: -------------------------------------------------------------------------------- 1 | uniform sampler2D image; 2 | uniform vec4 color; 3 | uniform vec4 scissor; 4 | 5 | in vec2 connection; 6 | out vec4 fragColor; 7 | 8 | void main() 9 | { 10 | vec2 co = gl_FragCoord.xy; 11 | if (co.x < scissor.x || co.y < scissor.y || 12 | co.x > scissor.z || co.y > scissor.w ) { 13 | discard; 14 | } 15 | 16 | fragColor = texture(image, connection) * color; 17 | } -------------------------------------------------------------------------------- /src/magic_uv/gpu_utils/shaders/image_color_vert.glsl: -------------------------------------------------------------------------------- 1 | uniform mat4 ModelViewProjectionMatrix; 2 | 3 | in vec2 pos; 4 | in vec2 texCoord; 5 | out vec2 connection; 6 | 7 | void main() 8 | { 9 | connection = texCoord; 10 | gl_Position = ModelViewProjectionMatrix * vec4(pos.xy, 0.0, 1.0); 11 | gl_Position.z = 1.0; 12 | } 13 | -------------------------------------------------------------------------------- /src/magic_uv/gpu_utils/shaders/polyline_uniform_color_scissor_frag.glsl: -------------------------------------------------------------------------------- 1 | // Ref: source/blender/gpu/shaders/gpu_shader_3D_polyline_frag.glsl 2 | 3 | uniform vec4 color; 4 | uniform float lineWidth; 5 | uniform bool lineSmooth; 6 | uniform vec4 scissor; 7 | 8 | const int SMOOTH_WIDTH = 1; 9 | 10 | struct geom_frag_connection { 11 | vec4 final_color; 12 | float smoothline; 13 | }; 14 | 15 | in geom_frag_connection connection; 16 | out vec4 fragColor; 17 | 18 | void main() 19 | { 20 | vec2 co = gl_FragCoord.xy; 21 | if (co.x < scissor.x || co.y < scissor.y || 22 | co.x > scissor.z || co.y > scissor.w ) { 23 | discard; 24 | } 25 | 26 | fragColor = color; 27 | if (lineSmooth) { 28 | fragColor.a *= clamp((lineWidth + SMOOTH_WIDTH) * 0.5 - abs(connection.smoothline), 0.0, 1.0); 29 | } 30 | fragColor = blender_srgb_to_framebuffer_space(fragColor); 31 | } 32 | -------------------------------------------------------------------------------- /src/magic_uv/gpu_utils/shaders/polyline_uniform_color_scissor_geom.glsl: -------------------------------------------------------------------------------- 1 | // Ref: source/blender/gpu/shaders/gpu_shader_3D_polyline_geom.glsl 2 | 3 | layout (lines) in; 4 | layout (triangle_strip, max_vertices = 4) out; 5 | 6 | uniform vec4 color; 7 | uniform float lineWidth; 8 | uniform bool lineSmooth; 9 | uniform vec2 viewportSize; 10 | 11 | const float SMOOTH_WIDTH = 1.0; 12 | 13 | struct geom_frag_connection { 14 | vec4 final_color; 15 | float smoothline; 16 | }; 17 | 18 | out geom_frag_connection connection; 19 | 20 | vec4 clip_line_point_homogeneous_space(vec4 p, vec4 q) 21 | { 22 | if (p.z < -p.w) { 23 | float denom = q.z - p.z + q.w - p.w; 24 | if (denom == 0.0) { 25 | return p; 26 | } 27 | float A = (-p.z - p.w) / denom; 28 | p = p + (q - p) * A; 29 | } 30 | return p; 31 | } 32 | 33 | void do_vertex(const int i, vec4 pos, vec2 ofs) 34 | { 35 | connection.final_color = color; 36 | 37 | connection.smoothline = (lineWidth + SMOOTH_WIDTH * float(lineSmooth)) * 0.5; 38 | gl_Position = pos; 39 | gl_Position.xy += ofs * pos.w; 40 | EmitVertex(); 41 | 42 | connection.smoothline = -(lineWidth + SMOOTH_WIDTH * float(lineSmooth)) * 0.5; 43 | gl_Position = pos; 44 | gl_Position.xy -= ofs * pos.w; 45 | EmitVertex(); 46 | } 47 | 48 | void main(void) 49 | { 50 | vec4 p0 = clip_line_point_homogeneous_space(gl_in[0].gl_Position, gl_in[1].gl_Position); 51 | vec4 p1 = clip_line_point_homogeneous_space(gl_in[1].gl_Position, gl_in[0].gl_Position); 52 | vec2 e = normalize(((p1.xy / p1.w) - (p0.xy / p0.w)) * viewportSize.xy); 53 | 54 | vec2 ofs = vec2(-e.y, e.x); 55 | ofs /= viewportSize.xy; 56 | ofs *= lineWidth + SMOOTH_WIDTH * float(lineSmooth); 57 | 58 | do_vertex(0, p0, ofs); 59 | do_vertex(1, p1, ofs); 60 | 61 | EndPrimitive(); 62 | } 63 | -------------------------------------------------------------------------------- /src/magic_uv/gpu_utils/shaders/polyline_uniform_color_scissor_vert.glsl: -------------------------------------------------------------------------------- 1 | // Ref: source/blender/gpu/shaders/gpu_shader_3D_polyline_vert.glsl 2 | 3 | uniform mat4 ModelViewProjectionMatrix; 4 | 5 | in vec3 pos; 6 | 7 | void main() 8 | { 9 | gl_Position = ModelViewProjectionMatrix * vec4(pos, 1.0); 10 | } 11 | -------------------------------------------------------------------------------- /src/magic_uv/gpu_utils/shaders/uniform_color_scissor_frag.glsl: -------------------------------------------------------------------------------- 1 | uniform vec4 scissor; 2 | uniform vec4 color; 3 | 4 | out vec4 fragColor; 5 | 6 | void main() 7 | { 8 | vec2 co = gl_FragCoord.xy; 9 | if (co.x < scissor.x || co.y < scissor.y || 10 | co.x > scissor.z || co.y > scissor.w ) { 11 | discard; 12 | } 13 | 14 | fragColor = blender_srgb_to_framebuffer_space(color); 15 | } -------------------------------------------------------------------------------- /src/magic_uv/gpu_utils/shaders/uniform_color_scissor_vert.glsl: -------------------------------------------------------------------------------- 1 | uniform mat4 ModelViewProjectionMatrix; 2 | 3 | in vec2 pos; 4 | 5 | void main() 6 | { 7 | gl_Position = ModelViewProjectionMatrix * vec4(pos, 0.0, 1.0); 8 | } 9 | -------------------------------------------------------------------------------- /src/magic_uv/op/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | if "bpy" in locals(): 9 | import importlib 10 | importlib.reload(align_uv) 11 | importlib.reload(align_uv_cursor) 12 | importlib.reload(clip_uv) 13 | importlib.reload(copy_paste_uv) 14 | importlib.reload(copy_paste_uv_object) 15 | importlib.reload(copy_paste_uv_uvedit) 16 | importlib.reload(flip_rotate_uv) 17 | importlib.reload(mirror_uv) 18 | importlib.reload(move_uv) 19 | importlib.reload(pack_uv) 20 | importlib.reload(preserve_uv_aspect) 21 | importlib.reload(select_uv) 22 | importlib.reload(smooth_uv) 23 | importlib.reload(texture_lock) 24 | importlib.reload(texture_projection) 25 | importlib.reload(texture_wrap) 26 | importlib.reload(transfer_uv) 27 | importlib.reload(unwrap_constraint) 28 | importlib.reload(uv_bounding_box) 29 | importlib.reload(uv_inspection) 30 | importlib.reload(uv_sculpt) 31 | importlib.reload(uvw) 32 | importlib.reload(world_scale_uv) 33 | else: 34 | from . import align_uv 35 | from . import align_uv_cursor 36 | from . import clip_uv 37 | from . import copy_paste_uv 38 | from . import copy_paste_uv_object 39 | from . import copy_paste_uv_uvedit 40 | from . import flip_rotate_uv 41 | from . import mirror_uv 42 | from . import move_uv 43 | from . import pack_uv 44 | from . import preserve_uv_aspect 45 | from . import select_uv 46 | from . import smooth_uv 47 | from . import texture_lock 48 | from . import texture_projection 49 | from . import texture_wrap 50 | from . import transfer_uv 51 | from . import unwrap_constraint 52 | from . import uv_bounding_box 53 | from . import uv_inspection 54 | from . import uv_sculpt 55 | from . import uvw 56 | from . import world_scale_uv 57 | 58 | import bpy 59 | -------------------------------------------------------------------------------- /src/magic_uv/op/clip_uv.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Dusan Stevanovic, Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | 9 | import math 10 | 11 | import bpy 12 | import bmesh 13 | from mathutils import Vector 14 | from bpy.props import BoolProperty, FloatVectorProperty 15 | 16 | from .. import common 17 | from ..utils.bl_class_registry import BlClassRegistry 18 | from ..utils.property_class_registry import PropertyClassRegistry 19 | from ..utils import compatibility as compat 20 | 21 | 22 | def _is_valid_context(context): 23 | # 'IMAGE_EDITOR' and 'VIEW_3D' space is allowed to execute. 24 | # If 'View_3D' space is not allowed, you can't find option in Tool-Shelf 25 | # after the execution 26 | if not common.is_valid_space(context, ['IMAGE_EDITOR', 'VIEW_3D']): 27 | return False 28 | 29 | objs = common.get_uv_editable_objects(context) 30 | if not objs: 31 | return False 32 | 33 | # only edit mode is allowed to execute 34 | if context.object.mode != 'EDIT': 35 | return False 36 | 37 | return True 38 | 39 | 40 | def round_clip_uv_range(v): 41 | sign = 1 if v >= 0.0 else -1 42 | return int((math.fabs(v) + 0.25) / 0.5) * 0.5 * sign 43 | 44 | 45 | def get_clip_uv_range_max(self): 46 | return self.get('muv_clip_uv_range_max', (0.5, 0.5)) 47 | 48 | 49 | def set_clip_uv_range_max(self, value): 50 | u = round_clip_uv_range(value[0]) 51 | u = 0.5 if u <= 0.5 else u 52 | v = round_clip_uv_range(value[1]) 53 | v = 0.5 if v <= 0.5 else v 54 | self['muv_clip_uv_range_max'] = (u, v) 55 | 56 | 57 | def get_clip_uv_range_min(self): 58 | return self.get('muv_clip_uv_range_min', (-0.5, -0.5)) 59 | 60 | 61 | def set_clip_uv_range_min(self, value): 62 | u = round_clip_uv_range(value[0]) 63 | u = -0.5 if u >= -0.5 else u 64 | v = round_clip_uv_range(value[1]) 65 | v = -0.5 if v >= -0.5 else v 66 | self['muv_clip_uv_range_min'] = (u, v) 67 | 68 | 69 | @PropertyClassRegistry() 70 | class _Properties: 71 | idname = "clip_uv" 72 | 73 | @classmethod 74 | def init_props(cls, scene): 75 | scene.muv_clip_uv_enabled = BoolProperty( 76 | name="Clip UV Enabled", 77 | description="Clip UV is enabled", 78 | default=False 79 | ) 80 | 81 | scene.muv_clip_uv_range_max = FloatVectorProperty( 82 | name="Range Max", 83 | description="Max UV coordinates of the range to be clipped", 84 | size=2, 85 | default=(0.5, 0.5), 86 | min=0.5, 87 | step=50, 88 | get=get_clip_uv_range_max, 89 | set=set_clip_uv_range_max, 90 | ) 91 | 92 | scene.muv_clip_uv_range_min = FloatVectorProperty( 93 | name="Range Min", 94 | description="Min UV coordinates of the range to be clipped", 95 | size=2, 96 | default=(-0.5, -0.5), 97 | max=-0.5, 98 | step=50, 99 | get=get_clip_uv_range_min, 100 | set=set_clip_uv_range_min, 101 | ) 102 | 103 | # TODO: add option to preserve UV island 104 | 105 | @classmethod 106 | def del_props(cls, scene): 107 | del scene.muv_clip_uv_range_max 108 | del scene.muv_clip_uv_range_min 109 | 110 | 111 | @BlClassRegistry() 112 | @compat.make_annotations 113 | class MUV_OT_ClipUV(bpy.types.Operator): 114 | 115 | bl_idname = "uv.muv_clip_uv" 116 | bl_label = "Clip UV" 117 | bl_description = "Clip selected UV in the specified range" 118 | bl_options = {'REGISTER', 'UNDO'} 119 | 120 | clip_uv_range_max = FloatVectorProperty( 121 | name="Range Max", 122 | description="Max UV coordinates of the range to be clipped", 123 | size=2, 124 | default=(0.5, 0.5), 125 | min=0.5, 126 | step=50, 127 | ) 128 | 129 | clip_uv_range_min = FloatVectorProperty( 130 | name="Range Min", 131 | description="Min UV coordinates of the range to be clipped", 132 | size=2, 133 | default=(-0.5, -0.5), 134 | max=-0.5, 135 | step=50, 136 | ) 137 | 138 | @classmethod 139 | def poll(cls, context): 140 | # we can not get area/space/region from console 141 | if common.is_console_mode(): 142 | return True 143 | return _is_valid_context(context) 144 | 145 | def execute(self, context): 146 | objs = common.get_uv_editable_objects(context) 147 | 148 | for obj in objs: 149 | bm = common.create_bmesh(obj) 150 | 151 | if not bm.loops.layers.uv: 152 | self.report({'WARNING'}, 153 | "Object {} must have more than one UV map" 154 | .format(obj.name)) 155 | return {'CANCELLED'} 156 | 157 | uv_layer = bm.loops.layers.uv.verify() 158 | 159 | for face in bm.faces: 160 | if not face.select: 161 | continue 162 | 163 | selected_loops = [ 164 | l for l in face.loops 165 | if l[uv_layer].select or 166 | context.scene.tool_settings.use_uv_select_sync 167 | ] 168 | if not selected_loops: 169 | continue 170 | 171 | # average of UV coordinates on the face 172 | max_uv = Vector((-10000000.0, -10000000.0)) 173 | min_uv = Vector((10000000.0, 10000000.0)) 174 | for l in selected_loops: 175 | uv = l[uv_layer].uv 176 | max_uv.x = max(max_uv.x, uv.x) 177 | max_uv.y = max(max_uv.y, uv.y) 178 | min_uv.x = min(min_uv.x, uv.x) 179 | min_uv.y = min(min_uv.y, uv.y) 180 | 181 | # clip 182 | move_uv = Vector((0.0, 0.0)) 183 | clip_size = Vector(self.clip_uv_range_max) - \ 184 | Vector(self.clip_uv_range_min) 185 | if max_uv.x > self.clip_uv_range_max[0]: 186 | target_x = math.fmod(max_uv.x - self.clip_uv_range_min[0], 187 | clip_size.x) 188 | if target_x < 0.0: 189 | target_x += clip_size.x 190 | target_x += self.clip_uv_range_min[0] 191 | move_uv.x = target_x - max_uv.x 192 | if min_uv.x < self.clip_uv_range_min[0]: 193 | target_x = math.fmod(min_uv.x - self.clip_uv_range_min[0], 194 | clip_size.x) 195 | if target_x < 0.0: 196 | target_x += clip_size.x 197 | target_x += self.clip_uv_range_min[0] 198 | move_uv.x = target_x - min_uv.x 199 | if max_uv.y > self.clip_uv_range_max[1]: 200 | target_y = math.fmod(max_uv.y - self.clip_uv_range_min[1], 201 | clip_size.y) 202 | if target_y < 0.0: 203 | target_y += clip_size.y 204 | target_y += self.clip_uv_range_min[1] 205 | move_uv.y = target_y - max_uv.y 206 | if min_uv.y < self.clip_uv_range_min[1]: 207 | target_y = math.fmod(min_uv.y - self.clip_uv_range_min[1], 208 | clip_size.y) 209 | if target_y < 0.0: 210 | target_y += clip_size.y 211 | target_y += self.clip_uv_range_min[1] 212 | move_uv.y = target_y - min_uv.y 213 | 214 | # update UV 215 | for l in selected_loops: 216 | l[uv_layer].uv = l[uv_layer].uv + move_uv 217 | 218 | bmesh.update_edit_mesh(obj.data) 219 | 220 | return {'FINISHED'} 221 | -------------------------------------------------------------------------------- /src/magic_uv/op/flip_rotate_uv.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | import bpy 9 | import bmesh 10 | from bpy.props import ( 11 | BoolProperty, 12 | IntProperty, 13 | ) 14 | 15 | from .. import common 16 | from ..utils.bl_class_registry import BlClassRegistry 17 | from ..utils.property_class_registry import PropertyClassRegistry 18 | from ..utils import compatibility as compat 19 | 20 | 21 | def _is_valid_context(context): 22 | # only 'VIEW_3D' space is allowed to execute 23 | if not common.is_valid_space(context, ['VIEW_3D']): 24 | return False 25 | 26 | objs = common.get_uv_editable_objects(context) 27 | if not objs: 28 | return False 29 | 30 | # only edit mode is allowed to execute 31 | if context.object.mode != 'EDIT': 32 | return False 33 | 34 | return True 35 | 36 | 37 | def _get_uv_layer(bm): 38 | # get UV layer 39 | if not bm.loops.layers.uv: 40 | return None 41 | uv_layer = bm.loops.layers.uv.verify() 42 | 43 | return uv_layer 44 | 45 | 46 | def _get_src_face_info(bm, uv_layers, only_select=False): 47 | src_info = {} 48 | for layer in uv_layers: 49 | face_info = [] 50 | for face in bm.faces: 51 | if not only_select or face.select: 52 | info = { 53 | "index": face.index, 54 | "uvs": [l[layer].uv.copy() for l in face.loops], 55 | "pin_uvs": [l[layer].pin_uv for l in face.loops], 56 | "seams": [l.edge.seam for l in face.loops], 57 | } 58 | face_info.append(info) 59 | if not face_info: 60 | return None 61 | src_info[layer.name] = face_info 62 | 63 | return src_info 64 | 65 | 66 | def _paste_uv(bm, src_info, dest_info, uv_layers, strategy, flip, 67 | rotate, copy_seams): 68 | for slayer_name, dlayer in zip(src_info.keys(), uv_layers): 69 | src_faces = src_info[slayer_name] 70 | dest_faces = dest_info[dlayer.name] 71 | 72 | for idx, dinfo in enumerate(dest_faces): 73 | sinfo = None 74 | if strategy == 'N_N': 75 | sinfo = src_faces[idx] 76 | elif strategy == 'N_M': 77 | sinfo = src_faces[idx % len(src_faces)] 78 | 79 | suv = sinfo["uvs"] 80 | spuv = sinfo["pin_uvs"] 81 | ss = sinfo["seams"] 82 | if len(sinfo["uvs"]) != len(dinfo["uvs"]): 83 | return -1 84 | 85 | suvs_fr = [uv for uv in suv] 86 | spuvs_fr = [pin_uv for pin_uv in spuv] 87 | ss_fr = [s for s in ss] 88 | 89 | # flip UVs 90 | if flip is True: 91 | suvs_fr.reverse() 92 | spuvs_fr.reverse() 93 | ss_fr.reverse() 94 | 95 | # rotate UVs 96 | for _ in range(rotate): 97 | uv = suvs_fr.pop() 98 | pin_uv = spuvs_fr.pop() 99 | s = ss_fr.pop() 100 | suvs_fr.insert(0, uv) 101 | spuvs_fr.insert(0, pin_uv) 102 | ss_fr.insert(0, s) 103 | 104 | # paste UVs 105 | for l, suv, spuv, ss in zip(bm.faces[dinfo["index"]].loops, 106 | suvs_fr, spuvs_fr, ss_fr): 107 | l[dlayer].uv = suv 108 | l[dlayer].pin_uv = spuv 109 | if copy_seams is True: 110 | l.edge.seam = ss 111 | 112 | return 0 113 | 114 | 115 | @PropertyClassRegistry() 116 | class _Properties: 117 | idname = "flip_rotate_uv" 118 | 119 | @classmethod 120 | def init_props(cls, scene): 121 | scene.muv_flip_rotate_uv_enabled = BoolProperty( 122 | name="Flip/Rotate UV Enabled", 123 | description="Flip/Rotate UV is enabled", 124 | default=False 125 | ) 126 | scene.muv_flip_rotate_uv_seams = BoolProperty( 127 | name="Seams", 128 | description="Seams", 129 | default=True 130 | ) 131 | 132 | @classmethod 133 | def del_props(cls, scene): 134 | del scene.muv_flip_rotate_uv_enabled 135 | del scene.muv_flip_rotate_uv_seams 136 | 137 | 138 | @BlClassRegistry() 139 | @compat.make_annotations 140 | class MUV_OT_FlipRotateUV(bpy.types.Operator): 141 | """ 142 | Operation class: Flip and Rotate UV coordinate 143 | """ 144 | 145 | bl_idname = "uv.muv_flip_rotate_uv" 146 | bl_label = "Flip/Rotate UV" 147 | bl_description = "Flip/Rotate UV coordinate" 148 | bl_options = {'REGISTER', 'UNDO'} 149 | 150 | flip = BoolProperty( 151 | name="Flip UV", 152 | description="Flip UV...", 153 | default=False 154 | ) 155 | rotate = IntProperty( 156 | default=0, 157 | name="Rotate UV", 158 | min=0, 159 | max=30 160 | ) 161 | seams = BoolProperty( 162 | name="Seams", 163 | description="Seams", 164 | default=True 165 | ) 166 | 167 | @classmethod 168 | def poll(cls, context): 169 | # we can not get area/space/region from console 170 | if common.is_console_mode(): 171 | return True 172 | return _is_valid_context(context) 173 | 174 | def execute(self, context): 175 | self.report({'INFO'}, "Flip/Rotate UV") 176 | objs = common.get_uv_editable_objects(context) 177 | 178 | face_count = 0 179 | for obj in objs: 180 | bm = bmesh.from_edit_mesh(obj.data) 181 | if common.check_version(2, 73, 0) >= 0: 182 | bm.faces.ensure_lookup_table() 183 | 184 | # get UV layer 185 | uv_layer = _get_uv_layer(bm) 186 | if not uv_layer: 187 | self.report({'WARNING'}, 188 | "Object {} must have more than one UV map" 189 | .format(obj.name)) 190 | return {'CANCELLED'} 191 | 192 | # get selected face 193 | src_info = _get_src_face_info(bm, [uv_layer], True) 194 | if not src_info: 195 | continue 196 | 197 | # paste 198 | ret = _paste_uv(bm, src_info, src_info, [uv_layer], 'N_N', 199 | self.flip, self.rotate, self.seams) 200 | if ret: 201 | self.report({'WARNING'}, 202 | "Some Object {}'s faces are different size" 203 | .format(obj.name)) 204 | return {'CANCELLED'} 205 | 206 | bmesh.update_edit_mesh(obj.data) 207 | if compat.check_version(2, 80, 0) < 0: 208 | if self.seams is True: 209 | obj.data.show_edge_seams = True 210 | 211 | face_count += len(src_info[list(src_info.keys())[0]]) 212 | 213 | if face_count == 0: 214 | self.report({'WARNING'}, "No faces are selected") 215 | return {'CANCELLED'} 216 | self.report({'INFO'}, 217 | "{} face(s) are fliped/rotated".format(face_count)) 218 | 219 | return {'FINISHED'} 220 | -------------------------------------------------------------------------------- /src/magic_uv/op/mirror_uv.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Keith (Wahooney) Boshoff, Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | import bpy 9 | from bpy.props import ( 10 | EnumProperty, 11 | FloatProperty, 12 | BoolProperty, 13 | ) 14 | import bmesh 15 | from mathutils import Vector, Euler 16 | 17 | from ..utils.bl_class_registry import BlClassRegistry 18 | from ..utils.property_class_registry import PropertyClassRegistry 19 | from ..utils import compatibility as compat 20 | from .. import common 21 | 22 | 23 | def _is_valid_context(context): 24 | # only 'VIEW_3D' space is allowed to execute 25 | if not common.is_valid_space(context, ['VIEW_3D']): 26 | return False 27 | 28 | objs = common.get_uv_editable_objects(context) 29 | if not objs: 30 | return False 31 | 32 | # only edit mode is allowed to execute 33 | if context.object.mode != 'EDIT': 34 | return False 35 | 36 | return True 37 | 38 | 39 | def _is_vector_similar(v1, v2, error): 40 | """ 41 | Check if two vectors are similar, within an error threshold 42 | """ 43 | within_err_x = abs(v2.x - v1.x) < error 44 | within_err_y = abs(v2.y - v1.y) < error 45 | within_err_z = abs(v2.z - v1.z) < error 46 | 47 | return within_err_x and within_err_y and within_err_z 48 | 49 | 50 | def _mirror_uvs(uv_layer, src, dst, axis, error, transformed): 51 | """ 52 | Copy UV coordinates from one UV face to another 53 | """ 54 | for sl in src.loops: 55 | suv = sl[uv_layer].uv.copy() 56 | svco = transformed[sl.vert].copy() 57 | for dl in dst.loops: 58 | dvco = transformed[dl.vert].copy() 59 | if axis == 'X': 60 | dvco.x = -dvco.x 61 | elif axis == 'Y': 62 | dvco.y = -dvco.y 63 | elif axis == 'Z': 64 | dvco.z = -dvco.z 65 | 66 | if _is_vector_similar(svco, dvco, error): 67 | dl[uv_layer].uv = suv.copy() 68 | 69 | 70 | def _get_face_center(face, transformed): 71 | """ 72 | Get center coordinate of the face 73 | """ 74 | center = Vector((0.0, 0.0, 0.0)) 75 | for v in face.verts: 76 | tv = transformed[v] 77 | center = center + tv 78 | 79 | return center / len(face.verts) 80 | 81 | 82 | @PropertyClassRegistry() 83 | class _Properties: 84 | idname = "mirror_uv" 85 | 86 | @classmethod 87 | def init_props(cls, scene): 88 | scene.muv_mirror_uv_enabled = BoolProperty( 89 | name="Mirror UV Enabled", 90 | description="Mirror UV is enabled", 91 | default=False 92 | ) 93 | scene.muv_mirror_uv_axis = EnumProperty( 94 | items=[ 95 | ('X', "X", "Mirror Along X axis"), 96 | ('Y', "Y", "Mirror Along Y axis"), 97 | ('Z', "Z", "Mirror Along Z axis") 98 | ], 99 | name="Axis", 100 | description="Mirror Axis", 101 | default='X' 102 | ) 103 | scene.muv_mirror_uv_origin = EnumProperty( 104 | items=( 105 | ('WORLD', "World", "World"), 106 | ("GLOBAL", "Global", "Global"), 107 | ('LOCAL', "Local", "Local"), 108 | ), 109 | name="Origin", 110 | description="Origin of the mirror operation", 111 | default='LOCAL' 112 | ) 113 | 114 | @classmethod 115 | def del_props(cls, scene): 116 | del scene.muv_mirror_uv_enabled 117 | del scene.muv_mirror_uv_axis 118 | del scene.muv_mirror_uv_origin 119 | 120 | 121 | @BlClassRegistry() 122 | @compat.make_annotations 123 | class MUV_OT_MirrorUV(bpy.types.Operator): 124 | """ 125 | Operation class: Mirror UV 126 | """ 127 | 128 | bl_idname = "uv.muv_mirror_uv" 129 | bl_label = "Mirror UV" 130 | bl_options = {'REGISTER', 'UNDO'} 131 | 132 | axis = EnumProperty( 133 | items=( 134 | ('X', "X", "Mirror Along X axis"), 135 | ('Y', "Y", "Mirror Along Y axis"), 136 | ('Z', "Z", "Mirror Along Z axis") 137 | ), 138 | name="Axis", 139 | description="Mirror Axis", 140 | default='X' 141 | ) 142 | error = FloatProperty( 143 | name="Error", 144 | description="Error threshold", 145 | default=0.001, 146 | min=0.0, 147 | max=100.0, 148 | soft_min=0.0, 149 | soft_max=1.0 150 | ) 151 | origin = EnumProperty( 152 | items=( 153 | ('WORLD', "World", "World"), 154 | ("GLOBAL", "Global", "Global"), 155 | ('LOCAL', "Local", "Local"), 156 | ), 157 | name="Origin", 158 | description="Origin of the mirror operation", 159 | default='LOCAL' 160 | ) 161 | 162 | @classmethod 163 | def poll(cls, context): 164 | # we can not get area/space/region from console 165 | if common.is_console_mode(): 166 | return True 167 | return _is_valid_context(context) 168 | 169 | def _get_world_vertices(self, obj, bm): 170 | # Get world orientation matrix. 171 | world_orientation_mat = obj.matrix_world 172 | 173 | # Move to local to world. 174 | transformed = {} 175 | for v in bm.verts: 176 | transformed[v] = compat.matmul(world_orientation_mat, v.co) 177 | 178 | return transformed 179 | 180 | def _get_global_vertices(self, obj, bm): 181 | # Get world rotation matrix. 182 | eular = Euler(obj.rotation_euler) 183 | rotation_mat = eular.to_matrix() 184 | 185 | # Get center location of all vertices. 186 | center_location = Vector((0.0, 0.0, 0.0)) 187 | for v in bm.verts: 188 | center_location += v.co 189 | center_location /= len(bm.verts) 190 | 191 | # Move to local to global. 192 | transformed = {} 193 | for v in bm.verts: 194 | transformed[v] = compat.matmul(rotation_mat, v.co) 195 | transformed[v] -= center_location 196 | 197 | return transformed 198 | 199 | def _get_local_vertices(self, _, bm): 200 | transformed = {} 201 | 202 | # Get center location of all vertices. 203 | center_location = Vector((0.0, 0.0, 0.0)) 204 | for v in bm.verts: 205 | center_location += v.co 206 | center_location /= len(bm.verts) 207 | 208 | for v in bm.verts: 209 | transformed[v] = v.co.copy() 210 | transformed[v] -= center_location 211 | 212 | return transformed 213 | 214 | def execute(self, context): 215 | objs = common.get_uv_editable_objects(context) 216 | 217 | for obj in objs: 218 | bm = bmesh.from_edit_mesh(obj.data) 219 | 220 | error = self.error 221 | axis = self.axis 222 | 223 | if common.check_version(2, 73, 0) >= 0: 224 | bm.faces.ensure_lookup_table() 225 | if not bm.loops.layers.uv: 226 | self.report({'WARNING'}, 227 | "Object {} must have more than one UV map" 228 | .format(obj.name)) 229 | return {'CANCELLED'} 230 | uv_layer = bm.loops.layers.uv.verify() 231 | 232 | if self.origin == 'WORLD': 233 | transformed_verts = self._get_world_vertices(obj, bm) 234 | elif self.origin == 'GLOBAL': 235 | transformed_verts = self._get_global_vertices(obj, bm) 236 | elif self.origin == 'LOCAL': 237 | transformed_verts = self._get_local_vertices(obj, bm) 238 | 239 | faces = [f for f in bm.faces if f.select] 240 | for f_dst in faces: 241 | count = len(f_dst.verts) 242 | for f_src in bm.faces: 243 | # check if this is a candidate to do mirror UV 244 | if f_src.index == f_dst.index: 245 | continue 246 | if count != len(f_src.verts): 247 | continue 248 | 249 | # test if the vertices x values are the same sign 250 | dst = _get_face_center(f_dst, transformed_verts) 251 | src = _get_face_center(f_src, transformed_verts) 252 | 253 | # invert source axis 254 | if axis == 'X': 255 | if ((dst.x > 0 and src.x > 0) or 256 | (dst.x < 0 and src.x < 0)): 257 | continue 258 | src.x = -src.x 259 | elif axis == 'Y': 260 | if ((dst.y > 0 and src.y > 0) or 261 | (dst.y < 0 and src.y < 0)): 262 | continue 263 | src.y = -src.y 264 | elif axis == 'Z': 265 | if ((dst.z > 0 and src.z > 0) or 266 | (dst.z < 0 and src.z < 0)): 267 | continue 268 | src.z = -src.z 269 | 270 | # do mirror UV 271 | if _is_vector_similar(dst, src, error): 272 | _mirror_uvs(uv_layer, f_src, f_dst, 273 | self.axis, self.error, transformed_verts) 274 | 275 | bmesh.update_edit_mesh(obj.data) 276 | 277 | return {'FINISHED'} 278 | -------------------------------------------------------------------------------- /src/magic_uv/op/move_uv.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "kgeogeo, mem, Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | import bpy 9 | from bpy.props import BoolProperty 10 | import bmesh 11 | from mathutils import Vector 12 | 13 | from .. import common 14 | from ..utils.bl_class_registry import BlClassRegistry 15 | from ..utils.property_class_registry import PropertyClassRegistry 16 | 17 | 18 | def _is_valid_context(context): 19 | # only 'VIEW_3D' space is allowed to execute 20 | if not common.is_valid_space(context, ['VIEW_3D']): 21 | return False 22 | 23 | # Multiple objects editing mode is not supported in this feature. 24 | objs = common.get_uv_editable_objects(context) 25 | if len(objs) != 1: 26 | return False 27 | 28 | # only edit mode is allowed to execute 29 | if context.object.mode != 'EDIT': 30 | return False 31 | 32 | return True 33 | 34 | 35 | @PropertyClassRegistry() 36 | class _Properties: 37 | idname = "move_uv" 38 | 39 | @classmethod 40 | def init_props(cls, scene): 41 | scene.muv_move_uv_enabled = BoolProperty( 42 | name="Move UV Enabled", 43 | description="Move UV is enabled", 44 | default=False 45 | ) 46 | 47 | @classmethod 48 | def del_props(cls, scene): 49 | del scene.muv_move_uv_enabled 50 | 51 | 52 | @BlClassRegistry() 53 | class MUV_OT_MoveUV(bpy.types.Operator): 54 | """ 55 | Operator class: Move UV 56 | """ 57 | 58 | bl_idname = "uv.muv_move_uv" 59 | bl_label = "Move UV" 60 | bl_options = {'REGISTER', 'UNDO'} 61 | 62 | __running = False 63 | 64 | def __init__(self): 65 | self.__topology_dict = [] 66 | self.__prev_mouse = Vector((0.0, 0.0)) 67 | self.__offset_uv = Vector((0.0, 0.0)) 68 | self.__prev_offset_uv = Vector((0.0, 0.0)) 69 | self.__first_time = True 70 | self.__ini_uvs = [] 71 | self.__operating = False 72 | 73 | # Creation of BMesh is high cost, so cache related objects. 74 | self.__cache = {} 75 | 76 | @classmethod 77 | def poll(cls, context): 78 | # we can not get area/space/region from console 79 | if common.is_console_mode(): 80 | return False 81 | if cls.is_running(context): 82 | return False 83 | return _is_valid_context(context) 84 | 85 | @classmethod 86 | def is_running(cls, _): 87 | return cls.__running 88 | 89 | def _find_uv(self, bm, active_uv): 90 | topology_dict = [] 91 | uvs = [] 92 | for fidx, f in enumerate(bm.faces): 93 | for vidx, v in enumerate(f.verts): 94 | if v.select: 95 | uvs.append(f.loops[vidx][active_uv].uv.copy()) 96 | topology_dict.append([fidx, vidx]) 97 | 98 | return topology_dict, uvs 99 | 100 | def modal(self, _, event): 101 | if self.__first_time is True: 102 | self.__prev_mouse = Vector(( 103 | event.mouse_region_x, event.mouse_region_y)) 104 | self.__first_time = False 105 | return {'RUNNING_MODAL'} 106 | 107 | # move UV 108 | div = 10000 109 | self.__offset_uv += Vector(( 110 | (event.mouse_region_x - self.__prev_mouse.x) / div, 111 | (event.mouse_region_y - self.__prev_mouse.y) / div)) 112 | ouv = self.__offset_uv 113 | pouv = self.__prev_offset_uv 114 | vec = Vector((ouv.x - ouv.y, ouv.x + ouv.y)) 115 | dv = vec - pouv 116 | self.__prev_offset_uv = vec 117 | self.__prev_mouse = Vector(( 118 | event.mouse_region_x, event.mouse_region_y)) 119 | 120 | # check if operation is started 121 | if not self.__operating: 122 | if event.type == 'LEFTMOUSE' and event.value == 'RELEASE': 123 | self.__operating = True 124 | return {'RUNNING_MODAL'} 125 | 126 | # update UV 127 | obj = self.__cache["active_object"] 128 | bm = self.__cache["bmesh"] 129 | active_uv = self.__cache["active_uv"] 130 | for uv in self.__cache["target_uv"]: 131 | uv += dv 132 | bmesh.update_edit_mesh(obj.data) 133 | 134 | # check mouse preference 135 | confirm_btn = 'LEFTMOUSE' 136 | cancel_btn = 'RIGHTMOUSE' 137 | 138 | # cancelled 139 | if event.type == cancel_btn and event.value == 'PRESS': 140 | for (fidx, vidx), uv in zip(self.__topology_dict, self.__ini_uvs): 141 | bm.faces[fidx].loops[vidx][active_uv].uv = uv 142 | MUV_OT_MoveUV.__running = False 143 | self.__cache = {} 144 | return {'FINISHED'} 145 | # confirmed 146 | if event.type == confirm_btn and event.value == 'PRESS': 147 | MUV_OT_MoveUV.__running = False 148 | self.__cache = {} 149 | return {'FINISHED'} 150 | 151 | return {'RUNNING_MODAL'} 152 | 153 | def execute(self, context): 154 | MUV_OT_MoveUV.__running = True 155 | self.__operating = False 156 | self.__first_time = True 157 | 158 | context.window_manager.modal_handler_add(self) 159 | 160 | objs = common.get_uv_editable_objects(context) 161 | # poll() method ensures that only one object is selected. 162 | obj = objs[0] 163 | bm = bmesh.from_edit_mesh(obj.data) 164 | active_uv = bm.loops.layers.uv.active 165 | self.__topology_dict, self.__ini_uvs = self._find_uv(bm, active_uv) 166 | 167 | # Optimization: Store temporary variables which cause heavy 168 | # calculation. 169 | self.__cache["active_object"] = obj 170 | self.__cache["bmesh"] = bm 171 | self.__cache["active_uv"] = active_uv 172 | self.__cache["target_uv"] = [] 173 | for fidx, vidx in self.__topology_dict: 174 | l = bm.faces[fidx].loops[vidx] 175 | self.__cache["target_uv"].append(l[active_uv].uv) 176 | 177 | if context.area: 178 | context.area.tag_redraw() 179 | 180 | return {'RUNNING_MODAL'} 181 | -------------------------------------------------------------------------------- /src/magic_uv/op/unwrap_constraint.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | import bpy 9 | from bpy.props import ( 10 | BoolProperty, 11 | EnumProperty, 12 | FloatProperty, 13 | ) 14 | import bmesh 15 | 16 | from .. import common 17 | from ..utils.bl_class_registry import BlClassRegistry 18 | from ..utils.property_class_registry import PropertyClassRegistry 19 | from ..utils import compatibility as compat 20 | 21 | 22 | def _is_valid_context(context): 23 | # only 'VIEW_3D' space is allowed to execute 24 | if not common.is_valid_space(context, ['VIEW_3D']): 25 | return False 26 | 27 | objs = common.get_uv_editable_objects(context) 28 | if not objs: 29 | return False 30 | 31 | # only edit mode is allowed to execute 32 | if context.object.mode != 'EDIT': 33 | return False 34 | 35 | return True 36 | 37 | 38 | @PropertyClassRegistry() 39 | class _Properties: 40 | idname = "unwrap_constraint" 41 | 42 | @classmethod 43 | def init_props(cls, scene): 44 | scene.muv_unwrap_constraint_enabled = BoolProperty( 45 | name="Unwrap Constraint Enabled", 46 | description="Unwrap Constraint is enabled", 47 | default=False 48 | ) 49 | scene.muv_unwrap_constraint_u_const = BoolProperty( 50 | name="U-Constraint", 51 | description="Keep UV U-axis coordinate", 52 | default=False 53 | ) 54 | scene.muv_unwrap_constraint_v_const = BoolProperty( 55 | name="V-Constraint", 56 | description="Keep UV V-axis coordinate", 57 | default=False 58 | ) 59 | 60 | @classmethod 61 | def del_props(cls, scene): 62 | del scene.muv_unwrap_constraint_enabled 63 | del scene.muv_unwrap_constraint_u_const 64 | del scene.muv_unwrap_constraint_v_const 65 | 66 | 67 | @BlClassRegistry(legacy=True) 68 | @compat.make_annotations 69 | class MUV_OT_UnwrapConstraint(bpy.types.Operator): 70 | """ 71 | Operation class: Unwrap with constrain UV coordinate 72 | """ 73 | 74 | bl_idname = "uv.muv_unwrap_constraint" 75 | bl_label = "Unwrap Constraint" 76 | bl_description = "Unwrap while keeping uv coordinate" 77 | bl_options = {'REGISTER', 'UNDO'} 78 | 79 | # property for original unwrap 80 | method = EnumProperty( 81 | name="Method", 82 | description="Unwrapping method", 83 | items=[ 84 | ('ANGLE_BASED', 'Angle Based', 'Angle Based'), 85 | ('CONFORMAL', 'Conformal', 'Conformal') 86 | ], 87 | default='ANGLE_BASED') 88 | fill_holes = BoolProperty( 89 | name="Fill Holes", 90 | description="Virtual fill holes in meshes before unwrapping", 91 | default=True) 92 | correct_aspect = BoolProperty( 93 | name="Correct Aspect", 94 | description="Map UVs taking image aspect ratio into account", 95 | default=True) 96 | use_subsurf_data = BoolProperty( 97 | name="Use Subsurf Modifier", 98 | description="""Map UVs taking vertex position after subsurf 99 | into account""", 100 | default=False) 101 | margin = FloatProperty( 102 | name="Margin", 103 | description="Space between islands", 104 | max=1.0, 105 | min=0.0, 106 | default=0.001) 107 | 108 | # property for this operation 109 | u_const = BoolProperty( 110 | name="U-Constraint", 111 | description="Keep UV U-axis coordinate", 112 | default=False 113 | ) 114 | v_const = BoolProperty( 115 | name="V-Constraint", 116 | description="Keep UV V-axis coordinate", 117 | default=False 118 | ) 119 | 120 | @classmethod 121 | def poll(cls, context): 122 | # we can not get area/space/region from console 123 | if common.is_console_mode(): 124 | return True 125 | return _is_valid_context(context) 126 | 127 | def execute(self, context): 128 | objs = common.get_uv_editable_objects(context) 129 | 130 | uv_list = {} # { Object: uv_list } 131 | for obj in objs: 132 | bm = bmesh.from_edit_mesh(obj.data) 133 | if common.check_version(2, 73, 0) >= 0: 134 | bm.faces.ensure_lookup_table() 135 | 136 | # bpy.ops.uv.unwrap() makes one UV map at least 137 | if not bm.loops.layers.uv: 138 | self.report({'WARNING'}, 139 | "Object {} must have more than one UV map" 140 | .format(obj.name)) 141 | return {'CANCELLED'} 142 | uv_layer = bm.loops.layers.uv.verify() 143 | 144 | # get original UV coordinate 145 | faces = [f for f in bm.faces if f.select] 146 | uv_list[obj] = [] 147 | for f in faces: 148 | uvs = [l[uv_layer].uv.copy() for l in f.loops] 149 | uv_list[obj].append(uvs) 150 | 151 | # unwrap 152 | bpy.ops.uv.unwrap( 153 | method=self.method, 154 | fill_holes=self.fill_holes, 155 | correct_aspect=self.correct_aspect, 156 | use_subsurf_data=self.use_subsurf_data, 157 | margin=self.margin) 158 | 159 | # when U/V-Constraint is checked, revert original coordinate 160 | for obj in objs: 161 | bm = bmesh.from_edit_mesh(obj.data) 162 | if common.check_version(2, 73, 0) >= 0: 163 | bm.faces.ensure_lookup_table() 164 | uv_layer = bm.loops.layers.uv.verify() 165 | faces = [f for f in bm.faces if f.select] 166 | 167 | for f, uvs in zip(faces, uv_list[obj]): 168 | for l, uv in zip(f.loops, uvs): 169 | if self.u_const: 170 | l[uv_layer].uv.x = uv.x 171 | if self.v_const: 172 | l[uv_layer].uv.y = uv.y 173 | 174 | # update mesh 175 | bmesh.update_edit_mesh(obj.data) 176 | 177 | return {'FINISHED'} 178 | -------------------------------------------------------------------------------- /src/magic_uv/properties.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | 9 | from .utils.property_class_registry import PropertyClassRegistry 10 | 11 | 12 | # Properties used in this add-on. 13 | # pylint: disable=W0612 14 | class MUV_Properties(): 15 | pass 16 | 17 | 18 | def init_props(scene): 19 | scene.muv_props = MUV_Properties() 20 | PropertyClassRegistry.init_props(scene) 21 | 22 | 23 | def clear_props(scene): 24 | PropertyClassRegistry.del_props(scene) 25 | del scene.muv_props 26 | -------------------------------------------------------------------------------- /src/magic_uv/ui/IMAGE_MT_uvs.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | import bpy 9 | 10 | from ..op.copy_paste_uv_uvedit import ( 11 | MUV_OT_CopyPasteUVUVEdit_CopyUV, 12 | MUV_OT_CopyPasteUVUVEdit_PasteUV, 13 | MUV_OT_CopyPasteUVUVEdit_CopyUVIsland, 14 | MUV_OT_CopyPasteUVUVEdit_PasteUVIsland, 15 | ) 16 | from ..op.align_uv_cursor import MUV_OT_AlignUVCursor 17 | from ..op.align_uv import ( 18 | MUV_OT_AlignUV_Circle, 19 | MUV_OT_AlignUV_Straighten, 20 | MUV_OT_AlignUV_Axis, 21 | MUV_OT_AlignUV_SnapToPoint, 22 | MUV_OT_AlignUV_SnapToEdge, 23 | ) 24 | from ..op.select_uv import ( 25 | MUV_OT_SelectUV_SelectOverlapped, 26 | MUV_OT_SelectUV_SelectFlipped, 27 | ) 28 | from ..op.uv_inspection import ( 29 | MUV_OT_UVInspection_Update, 30 | MUV_OT_UVInspection_PaintUVIsland, 31 | ) 32 | from ..utils.bl_class_registry import BlClassRegistry 33 | 34 | 35 | @BlClassRegistry() 36 | class MUV_MT_CopyPasteUV_UVEdit(bpy.types.Menu): 37 | """ 38 | Menu class: Master menu of Copy/Paste UV coordinate on UV/ImageEditor 39 | """ 40 | 41 | bl_idname = "MUV_MT_CopyPasteUV_UVEdit" 42 | bl_label = "Copy/Paste UV" 43 | bl_description = "Copy and Paste UV coordinate among object" 44 | 45 | def draw(self, context): 46 | layout = self.layout 47 | sc = context.scene 48 | 49 | layout.label(text="Face") 50 | layout.operator(MUV_OT_CopyPasteUVUVEdit_CopyUV.bl_idname, text="Copy") 51 | layout.operator(MUV_OT_CopyPasteUVUVEdit_PasteUV.bl_idname, 52 | text="Paste") 53 | 54 | layout.label(text="Island") 55 | layout.operator(MUV_OT_CopyPasteUVUVEdit_CopyUVIsland.bl_idname, 56 | text="Copy") 57 | ops = layout.operator(MUV_OT_CopyPasteUVUVEdit_PasteUVIsland.bl_idname, 58 | text="Paste") 59 | ops.unique_target = sc.muv_copy_paste_uv_uvedit_unique_target 60 | 61 | 62 | @BlClassRegistry() 63 | class MUV_MT_AlignUV(bpy.types.Menu): 64 | """ 65 | Menu class: Master menu of Align UV 66 | """ 67 | 68 | bl_idname = "MUV_MT_AlignUV" 69 | bl_label = "Align UV" 70 | bl_description = "Align UV" 71 | 72 | def draw(self, context): 73 | layout = self.layout 74 | sc = context.scene 75 | 76 | layout.label(text="Align") 77 | 78 | ops = layout.operator(MUV_OT_AlignUV_Circle.bl_idname, text="Circle") 79 | ops.transmission = sc.muv_align_uv_transmission 80 | ops.select = sc.muv_align_uv_select 81 | 82 | ops = layout.operator(MUV_OT_AlignUV_Straighten.bl_idname, 83 | text="Straighten") 84 | ops.transmission = sc.muv_align_uv_transmission 85 | ops.select = sc.muv_align_uv_select 86 | ops.vertical = sc.muv_align_uv_vertical 87 | ops.horizontal = sc.muv_align_uv_horizontal 88 | 89 | ops = layout.operator(MUV_OT_AlignUV_Axis.bl_idname, text="XY-axis") 90 | ops.transmission = sc.muv_align_uv_transmission 91 | ops.select = sc.muv_align_uv_select 92 | ops.vertical = sc.muv_align_uv_vertical 93 | ops.horizontal = sc.muv_align_uv_horizontal 94 | ops.location = sc.muv_align_uv_location 95 | 96 | layout.label(text="Snap") 97 | 98 | ops = layout.operator(MUV_OT_AlignUV_SnapToPoint.bl_idname, 99 | text="Snap to Point") 100 | ops.group = sc.muv_align_uv_snap_point_group 101 | ops.target = sc.muv_align_uv_snap_point_target 102 | 103 | ops = layout.operator(MUV_OT_AlignUV_SnapToEdge.bl_idname, 104 | text="Snap to Edge") 105 | ops.group = sc.muv_align_uv_snap_edge_group 106 | ops.target_1 = sc.muv_align_uv_snap_edge_target_1 107 | ops.target_2 = sc.muv_align_uv_snap_edge_target_2 108 | 109 | 110 | @BlClassRegistry() 111 | class MUV_MT_SelectUV(bpy.types.Menu): 112 | """ 113 | Menu class: Master menu of Select UV 114 | """ 115 | 116 | bl_idname = "MUV_MT_SelectUV" 117 | bl_label = "Select UV" 118 | bl_description = "Select UV" 119 | 120 | def draw(self, context): 121 | sc = context.scene 122 | layout = self.layout 123 | 124 | ops = layout.operator(MUV_OT_SelectUV_SelectOverlapped.bl_idname, 125 | text="Overlapped") 126 | MUV_OT_SelectUV_SelectOverlapped.setup_argument(ops, sc) 127 | ops = layout.operator(MUV_OT_SelectUV_SelectFlipped.bl_idname, 128 | text="Flipped") 129 | MUV_OT_SelectUV_SelectFlipped.setup_argument(ops, sc) 130 | 131 | 132 | @BlClassRegistry() 133 | class MUV_MT_AlignUVCursor(bpy.types.Menu): 134 | """ 135 | Menu class: Master menu of Align UV Cursor 136 | """ 137 | 138 | bl_idname = "MUV_MT_AlignUVCursor" 139 | bl_label = "Align UV Cursor" 140 | bl_description = "Align UV cursor" 141 | 142 | def draw(self, context): 143 | layout = self.layout 144 | sc = context.scene 145 | 146 | ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, text="Left Top") 147 | ops.position = 'LEFT_TOP' 148 | ops.base = sc.muv_align_uv_cursor_align_method 149 | 150 | ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, 151 | text="Middle Top") 152 | ops.position = 'MIDDLE_TOP' 153 | ops.base = sc.muv_align_uv_cursor_align_method 154 | 155 | ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, text="Right Top") 156 | ops.position = 'RIGHT_TOP' 157 | ops.base = sc.muv_align_uv_cursor_align_method 158 | 159 | ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, 160 | text="Left Middle") 161 | ops.position = 'LEFT_MIDDLE' 162 | ops.base = sc.muv_align_uv_cursor_align_method 163 | 164 | ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, text="Center") 165 | ops.position = 'CENTER' 166 | ops.base = sc.muv_align_uv_cursor_align_method 167 | 168 | ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, 169 | text="Right Middle") 170 | ops.position = 'RIGHT_MIDDLE' 171 | ops.base = sc.muv_align_uv_cursor_align_method 172 | 173 | ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, 174 | text="Left Bottom") 175 | ops.position = 'LEFT_BOTTOM' 176 | ops.base = sc.muv_align_uv_cursor_align_method 177 | 178 | ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, 179 | text="Middle Bottom") 180 | ops.position = 'MIDDLE_BOTTOM' 181 | ops.base = sc.muv_align_uv_cursor_align_method 182 | 183 | ops = layout.operator(MUV_OT_AlignUVCursor.bl_idname, 184 | text="Right Bottom") 185 | ops.position = 'RIGHT_BOTTOM' 186 | ops.base = sc.muv_align_uv_cursor_align_method 187 | 188 | 189 | @BlClassRegistry() 190 | class MUV_MT_UVInspection(bpy.types.Menu): 191 | """ 192 | Menu class: Master menu of UV Inspection 193 | """ 194 | 195 | bl_idname = "MUV_MT_UVInspection" 196 | bl_label = "UV Inspection" 197 | bl_description = "UV Inspection" 198 | 199 | def draw(self, context): 200 | layout = self.layout 201 | sc = context.scene 202 | 203 | layout.prop(sc, "muv_uv_inspection_show", 204 | text="Show Overlapped/Flipped") 205 | layout.operator(MUV_OT_UVInspection_Update.bl_idname, text="Update") 206 | layout.separator() 207 | layout.operator(MUV_OT_UVInspection_PaintUVIsland.bl_idname) 208 | -------------------------------------------------------------------------------- /src/magic_uv/ui/VIEW3D_MT_object.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | import bpy 9 | 10 | from ..op.copy_paste_uv_object import ( 11 | MUV_MT_CopyPasteUVObject_CopyUV, 12 | MUV_MT_CopyPasteUVObject_PasteUV, 13 | ) 14 | from ..utils.bl_class_registry import BlClassRegistry 15 | 16 | 17 | @BlClassRegistry() 18 | class MUV_MT_CopyPasteUV_Object(bpy.types.Menu): 19 | """ 20 | Menu class: Master menu of Copy/Paste UV coordinate among object 21 | """ 22 | 23 | bl_idname = "MUV_MT_CopyPasteUV_Object" 24 | bl_label = "Copy/Paste UV" 25 | bl_description = "Copy and Paste UV coordinate among object" 26 | 27 | def draw(self, _): 28 | layout = self.layout 29 | 30 | layout.menu(MUV_MT_CopyPasteUVObject_CopyUV.bl_idname, text="Copy") 31 | layout.menu(MUV_MT_CopyPasteUVObject_PasteUV.bl_idname, text="Paste") 32 | -------------------------------------------------------------------------------- /src/magic_uv/ui/VIEW3D_MT_uv_map.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | import bpy.utils 9 | 10 | from ..op.copy_paste_uv import ( 11 | MUV_MT_CopyPasteUV_CopyUV, 12 | MUV_MT_CopyPasteUV_PasteUV, 13 | MUV_MT_CopyPasteUV_SelSeqCopyUV, 14 | MUV_MT_CopyPasteUV_SelSeqPasteUV, 15 | ) 16 | from ..op.transfer_uv import ( 17 | MUV_OT_TransferUV_CopyUV, 18 | MUV_OT_TransferUV_PasteUV, 19 | ) 20 | from ..op.uvw import ( 21 | MUV_OT_UVW_BoxMap, 22 | MUV_OT_UVW_BestPlanerMap, 23 | ) 24 | from ..op.preserve_uv_aspect import MUV_OT_PreserveUVAspect 25 | from ..op.texture_lock import ( 26 | MUV_OT_TextureLock_Lock, 27 | MUV_OT_TextureLock_Unlock, 28 | ) 29 | from ..op.texture_wrap import ( 30 | MUV_OT_TextureWrap_Refer, 31 | MUV_OT_TextureWrap_Set, 32 | ) 33 | from ..op.world_scale_uv import ( 34 | MUV_OT_WorldScaleUV_Measure, 35 | MUV_OT_WorldScaleUV_ApplyManual, 36 | MUV_OT_WorldScaleUV_ApplyScalingDensity, 37 | MUV_OT_WorldScaleUV_ApplyProportionalToMesh, 38 | ) 39 | from ..op.texture_projection import MUV_OT_TextureProjection_Project 40 | from ..utils.bl_class_registry import BlClassRegistry 41 | 42 | 43 | @BlClassRegistry() 44 | class MUV_MT_CopyPasteUV(bpy.types.Menu): 45 | """ 46 | Menu class: Master menu of Copy/Paste UV coordinate 47 | """ 48 | 49 | bl_idname = "MUV_MT_CopyPasteUV" 50 | bl_label = "Copy/Paste UV" 51 | bl_description = "Copy and Paste UV coordinate" 52 | 53 | def draw(self, _): 54 | layout = self.layout 55 | 56 | layout.label(text="Default") 57 | layout.menu(MUV_MT_CopyPasteUV_CopyUV.bl_idname, text="Copy") 58 | layout.menu(MUV_MT_CopyPasteUV_PasteUV.bl_idname, text="Paste") 59 | 60 | layout.separator() 61 | 62 | layout.label(text="Selection Sequence") 63 | layout.menu(MUV_MT_CopyPasteUV_SelSeqCopyUV.bl_idname, text="Copy") 64 | layout.menu(MUV_MT_CopyPasteUV_SelSeqPasteUV.bl_idname, text="Paste") 65 | 66 | 67 | @BlClassRegistry() 68 | class MUV_MT_TransferUV(bpy.types.Menu): 69 | """ 70 | Menu class: Master menu of Transfer UV coordinate 71 | """ 72 | 73 | bl_idname = "MUV_MT_TransferUV" 74 | bl_label = "Transfer UV" 75 | bl_description = "Transfer UV coordinate" 76 | 77 | def draw(self, context): 78 | layout = self.layout 79 | sc = context.scene 80 | 81 | layout.operator(MUV_OT_TransferUV_CopyUV.bl_idname, text="Copy") 82 | ops = layout.operator(MUV_OT_TransferUV_PasteUV.bl_idname, 83 | text="Paste") 84 | ops.invert_normals = sc.muv_transfer_uv_invert_normals 85 | ops.copy_seams = sc.muv_transfer_uv_copy_seams 86 | 87 | 88 | @BlClassRegistry() 89 | class MUV_MT_TextureLock(bpy.types.Menu): 90 | """ 91 | Menu class: Master menu of Texture Lock 92 | """ 93 | 94 | bl_idname = "MUV_MT_TextureLock" 95 | bl_label = "Texture Lock" 96 | bl_description = "Lock texture when vertices of mesh (Preserve UV)" 97 | 98 | def draw(self, context): 99 | layout = self.layout 100 | sc = context.scene 101 | 102 | layout.label(text="Normal Mode") 103 | layout.operator( 104 | MUV_OT_TextureLock_Lock.bl_idname, 105 | text="Lock" 106 | if not MUV_OT_TextureLock_Lock.is_ready(context) 107 | else "ReLock") 108 | ops = layout.operator(MUV_OT_TextureLock_Unlock.bl_idname, 109 | text="Unlock") 110 | ops.connect = sc.muv_texture_lock_connect 111 | 112 | layout.separator() 113 | 114 | layout.label(text="Interactive Mode") 115 | layout.prop(sc, "muv_texture_lock_lock", text="Lock") 116 | 117 | 118 | @BlClassRegistry() 119 | class MUV_MT_WorldScaleUV(bpy.types.Menu): 120 | """ 121 | Menu class: Master menu of world scale UV 122 | """ 123 | 124 | bl_idname = "MUV_MT_WorldScaleUV" 125 | bl_label = "World Scale UV" 126 | bl_description = "" 127 | 128 | def draw(self, context): 129 | layout = self.layout 130 | sc = context.scene 131 | 132 | layout.operator(MUV_OT_WorldScaleUV_Measure.bl_idname, text="Measure") 133 | 134 | ops = layout.operator(MUV_OT_WorldScaleUV_ApplyManual.bl_idname, 135 | text="Apply (Manual)") 136 | ops.show_dialog = True 137 | 138 | ops = layout.operator( 139 | MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname, 140 | text="Apply (Same Density)") 141 | ops.src_density = sc.muv_world_scale_uv_src_density 142 | ops.same_density = True 143 | ops.show_dialog = True 144 | 145 | ops = layout.operator( 146 | MUV_OT_WorldScaleUV_ApplyScalingDensity.bl_idname, 147 | text="Apply (Scaling Density)") 148 | ops.src_density = sc.muv_world_scale_uv_src_density 149 | ops.same_density = False 150 | ops.show_dialog = True 151 | 152 | ops = layout.operator( 153 | MUV_OT_WorldScaleUV_ApplyProportionalToMesh.bl_idname, 154 | text="Apply (Proportional to Mesh)") 155 | ops.src_density = sc.muv_world_scale_uv_src_density 156 | ops.src_uv_area = sc.muv_world_scale_uv_src_uv_area 157 | ops.src_mesh_area = sc.muv_world_scale_uv_src_mesh_area 158 | ops.show_dialog = True 159 | 160 | 161 | @BlClassRegistry() 162 | class MUV_MT_TextureWrap(bpy.types.Menu): 163 | """ 164 | Menu class: Master menu of Texture Wrap 165 | """ 166 | 167 | bl_idname = "MUV_MT_TextureWrap" 168 | bl_label = "Texture Wrap" 169 | bl_description = "" 170 | 171 | def draw(self, _): 172 | layout = self.layout 173 | 174 | layout.operator(MUV_OT_TextureWrap_Refer.bl_idname, text="Refer") 175 | layout.operator(MUV_OT_TextureWrap_Set.bl_idname, text="Set") 176 | 177 | 178 | @BlClassRegistry() 179 | class MUV_MT_UVW(bpy.types.Menu): 180 | """ 181 | Menu class: Master menu of UVW 182 | """ 183 | 184 | bl_idname = "MUV_MT_UVW" 185 | bl_label = "UVW" 186 | bl_description = "" 187 | 188 | def draw(self, context): 189 | layout = self.layout 190 | sc = context.scene 191 | 192 | ops = layout.operator(MUV_OT_UVW_BoxMap.bl_idname, text="Box") 193 | ops.assign_uvmap = sc.muv_uvw_assign_uvmap 194 | 195 | ops = layout.operator(MUV_OT_UVW_BestPlanerMap.bl_idname, 196 | text="Best Planner") 197 | ops.assign_uvmap = sc.muv_uvw_assign_uvmap 198 | 199 | 200 | @BlClassRegistry() 201 | class MUV_MT_PreserveUVAspect(bpy.types.Menu): 202 | """ 203 | Menu class: Master menu of Preserve UV Aspect 204 | """ 205 | 206 | bl_idname = "MUV_MT_PreserveUVAspect" 207 | bl_label = "Preserve UV Aspect" 208 | bl_description = "" 209 | 210 | def draw(self, context): 211 | layout = self.layout 212 | sc = context.scene 213 | 214 | for key in bpy.data.images.keys(): 215 | ops = layout.operator(MUV_OT_PreserveUVAspect.bl_idname, text=key) 216 | ops.dest_img_name = key 217 | ops.origin = sc.muv_preserve_uv_aspect_origin 218 | 219 | 220 | @BlClassRegistry() 221 | class MUV_MT_TextureProjection(bpy.types.Menu): 222 | """ 223 | Menu class: Master menu of Texture Projection 224 | """ 225 | 226 | bl_idname = "MUV_MT_TextureProjection" 227 | bl_label = "Texture Projection" 228 | bl_description = "" 229 | 230 | def draw(self, context): 231 | layout = self.layout 232 | sc = context.scene 233 | 234 | layout.prop(sc, "muv_texture_projection_enable", 235 | text="Texture Projection") 236 | layout.operator(MUV_OT_TextureProjection_Project.bl_idname, 237 | text="Project") 238 | -------------------------------------------------------------------------------- /src/magic_uv/ui/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | if "bpy" in locals(): 9 | import importlib 10 | importlib.reload(view3d_copy_paste_uv_editmode) 11 | importlib.reload(view3d_copy_paste_uv_objectmode) 12 | importlib.reload(view3d_uv_manipulation) 13 | importlib.reload(view3d_uv_mapping) 14 | importlib.reload(uvedit_copy_paste_uv) 15 | importlib.reload(uvedit_uv_manipulation) 16 | importlib.reload(uvedit_editor_enhancement) 17 | importlib.reload(VIEW3D_MT_object) 18 | importlib.reload(VIEW3D_MT_uv_map) 19 | importlib.reload(IMAGE_MT_uvs) 20 | else: 21 | from . import view3d_copy_paste_uv_editmode 22 | from . import view3d_copy_paste_uv_objectmode 23 | from . import view3d_uv_manipulation 24 | from . import view3d_uv_mapping 25 | from . import uvedit_copy_paste_uv 26 | from . import uvedit_uv_manipulation 27 | from . import uvedit_editor_enhancement 28 | from . import VIEW3D_MT_object 29 | from . import VIEW3D_MT_uv_map 30 | from . import IMAGE_MT_uvs 31 | 32 | import bpy 33 | -------------------------------------------------------------------------------- /src/magic_uv/ui/uvedit_copy_paste_uv.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | import bpy 9 | 10 | from ..op.copy_paste_uv_uvedit import ( 11 | MUV_OT_CopyPasteUVUVEdit_CopyUV, 12 | MUV_OT_CopyPasteUVUVEdit_PasteUV, 13 | MUV_OT_CopyPasteUVUVEdit_CopyUVIsland, 14 | MUV_OT_CopyPasteUVUVEdit_PasteUVIsland, 15 | ) 16 | from ..utils.bl_class_registry import BlClassRegistry 17 | from ..utils import compatibility as compat 18 | 19 | 20 | @BlClassRegistry() 21 | @compat.ChangeRegionType(region_type='TOOLS') 22 | class MUV_PT_UVEdit_CopyPasteUV(bpy.types.Panel): 23 | """ 24 | Panel class: Copy/Paste UV on Property Panel on UV/ImageEditor 25 | """ 26 | 27 | bl_space_type = 'IMAGE_EDITOR' 28 | bl_region_type = 'UI' 29 | bl_label = "Copy/Paste UV" 30 | bl_category = "Magic UV" 31 | bl_options = {'DEFAULT_CLOSED'} 32 | 33 | def draw_header(self, _): 34 | layout = self.layout 35 | layout.label(text="", icon=compat.icon('IMAGE')) 36 | 37 | def draw(self, context): 38 | layout = self.layout 39 | sc = context.scene 40 | 41 | layout.label(text="Face:") 42 | row = layout.row(align=True) 43 | row.operator(MUV_OT_CopyPasteUVUVEdit_CopyUV.bl_idname, text="Copy") 44 | row.operator(MUV_OT_CopyPasteUVUVEdit_PasteUV.bl_idname, text="Paste") 45 | 46 | layout.separator() 47 | 48 | layout.label(text="Island:") 49 | row = layout.row(align=True) 50 | row.operator(MUV_OT_CopyPasteUVUVEdit_CopyUVIsland.bl_idname, 51 | text="Copy") 52 | ops = row.operator(MUV_OT_CopyPasteUVUVEdit_PasteUVIsland.bl_idname, 53 | text="Paste") 54 | ops.unique_target = sc.muv_copy_paste_uv_uvedit_unique_target 55 | layout.prop(sc, "muv_copy_paste_uv_uvedit_unique_target") 56 | -------------------------------------------------------------------------------- /src/magic_uv/ui/uvedit_editor_enhancement.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | import bpy 9 | 10 | from ..op.align_uv_cursor import MUV_OT_AlignUVCursor 11 | from ..op.uv_bounding_box import ( 12 | MUV_OT_UVBoundingBox, 13 | ) 14 | from ..op.uv_inspection import ( 15 | MUV_OT_UVInspection_Render, 16 | MUV_OT_UVInspection_Update, 17 | MUV_OT_UVInspection_PaintUVIsland, 18 | ) 19 | from ..utils.bl_class_registry import BlClassRegistry 20 | from ..utils import compatibility as compat 21 | 22 | 23 | @BlClassRegistry() 24 | @compat.ChangeRegionType(region_type='TOOLS') 25 | class MUV_PT_UVEdit_EditorEnhancement(bpy.types.Panel): 26 | """ 27 | Panel class: UV/Image Editor Enhancement 28 | """ 29 | 30 | bl_space_type = 'IMAGE_EDITOR' 31 | bl_region_type = 'UI' 32 | bl_label = "Editor Enhancement" 33 | bl_category = "Magic UV" 34 | bl_options = {'DEFAULT_CLOSED'} 35 | 36 | def draw_header(self, _): 37 | layout = self.layout 38 | layout.label(text="", icon=compat.icon('IMAGE')) 39 | 40 | def draw(self, context): 41 | layout = self.layout 42 | sc = context.scene 43 | 44 | box = layout.box() 45 | box.prop(sc, "muv_align_uv_cursor_enabled", text="Align UV Cursor") 46 | if sc.muv_align_uv_cursor_enabled: 47 | box.prop(sc, "muv_align_uv_cursor_align_method", expand=True) 48 | 49 | col = box.column(align=True) 50 | 51 | row = col.row(align=True) 52 | ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, text="Left Top") 53 | ops.position = 'LEFT_TOP' 54 | ops.base = sc.muv_align_uv_cursor_align_method 55 | ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, 56 | text="Middle Top") 57 | ops.position = 'MIDDLE_TOP' 58 | ops.base = sc.muv_align_uv_cursor_align_method 59 | ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, 60 | text="Right Top") 61 | ops.position = 'RIGHT_TOP' 62 | ops.base = sc.muv_align_uv_cursor_align_method 63 | 64 | row = col.row(align=True) 65 | ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, 66 | text="Left Middle") 67 | ops.position = 'LEFT_MIDDLE' 68 | ops.base = sc.muv_align_uv_cursor_align_method 69 | ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, text="Center") 70 | ops.position = 'CENTER' 71 | ops.base = sc.muv_align_uv_cursor_align_method 72 | ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, 73 | text="Right Middle") 74 | ops.position = 'RIGHT_MIDDLE' 75 | ops.base = sc.muv_align_uv_cursor_align_method 76 | 77 | row = col.row(align=True) 78 | ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, 79 | text="Left Bottom") 80 | ops.position = 'LEFT_BOTTOM' 81 | ops.base = sc.muv_align_uv_cursor_align_method 82 | ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, 83 | text="Middle Bottom") 84 | ops.position = 'MIDDLE_BOTTOM' 85 | ops.base = sc.muv_align_uv_cursor_align_method 86 | ops = row.operator(MUV_OT_AlignUVCursor.bl_idname, 87 | text="Right Bottom") 88 | ops.position = 'RIGHT_BOTTOM' 89 | ops.base = sc.muv_align_uv_cursor_align_method 90 | 91 | box = layout.box() 92 | box.prop(sc, "muv_uv_cursor_location_enabled", 93 | text="UV Cursor Location") 94 | if sc.muv_uv_cursor_location_enabled: 95 | box.prop(sc, "muv_align_uv_cursor_cursor_loc", text="") 96 | 97 | box = layout.box() 98 | box.prop(sc, "muv_uv_bounding_box_enabled", text="UV Bounding Box") 99 | if sc.muv_uv_bounding_box_enabled: 100 | box.prop(sc, "muv_uv_bounding_box_show", 101 | text="Hide" 102 | if MUV_OT_UVBoundingBox.is_running(context) 103 | else "Show", 104 | icon='RESTRICT_VIEW_OFF' 105 | if MUV_OT_UVBoundingBox.is_running(context) 106 | else 'RESTRICT_VIEW_ON') 107 | box.prop(sc, "muv_uv_bounding_box_uniform_scaling", 108 | text="Uniform Scaling") 109 | box.prop(sc, "muv_uv_bounding_box_boundary", text="Boundary") 110 | 111 | box = layout.box() 112 | box.prop(sc, "muv_uv_inspection_enabled", text="UV Inspection") 113 | if sc.muv_uv_inspection_enabled: 114 | row = box.row() 115 | row.prop( 116 | sc, "muv_uv_inspection_show", 117 | text="Hide" 118 | if MUV_OT_UVInspection_Render.is_running(context) 119 | else "Show", 120 | icon='RESTRICT_VIEW_OFF' 121 | if MUV_OT_UVInspection_Render.is_running(context) 122 | else 'RESTRICT_VIEW_ON') 123 | row.operator(MUV_OT_UVInspection_Update.bl_idname, text="Update") 124 | row = box.row() 125 | row.prop(sc, "muv_uv_inspection_show_overlapped") 126 | row.prop(sc, "muv_uv_inspection_show_flipped") 127 | row = box.row() 128 | row.prop(sc, "muv_uv_inspection_display_in_v3d", text="3D") 129 | row.prop(sc, "muv_uv_inspection_show_mode") 130 | if sc.muv_uv_inspection_show_overlapped: 131 | row = box.row() 132 | row.prop(sc, "muv_uv_inspection_same_polygon_threshold") 133 | box.separator() 134 | box.operator(MUV_OT_UVInspection_PaintUVIsland.bl_idname) 135 | -------------------------------------------------------------------------------- /src/magic_uv/ui/uvedit_uv_manipulation.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | import bpy 9 | 10 | from ..op.align_uv import ( 11 | MUV_OT_AlignUV_Circle, 12 | MUV_OT_AlignUV_Straighten, 13 | MUV_OT_AlignUV_Axis, 14 | MUV_OT_AlignUV_SnapToPoint, 15 | MUV_OT_AlignUV_Snap_SetPointTargetToCursor, 16 | MUV_OT_AlignUV_Snap_SetPointTargetToVertexGroup, 17 | MUV_OT_AlignUV_SnapToEdge, 18 | MUV_OT_AlignUV_Snap_SetEdgeTargetToEdgeCenter, 19 | ) 20 | from ..op.smooth_uv import ( 21 | MUV_OT_SmoothUV, 22 | ) 23 | from ..op.select_uv import ( 24 | MUV_OT_SelectUV_SelectOverlapped, 25 | MUV_OT_SelectUV_SelectFlipped, 26 | MUV_OT_SelectUV_ZoomSelectedUV, 27 | ) 28 | from ..op.pack_uv import MUV_OT_PackUV 29 | from ..op.clip_uv import MUV_OT_ClipUV 30 | from ..utils.bl_class_registry import BlClassRegistry 31 | from ..utils import compatibility as compat 32 | 33 | 34 | @BlClassRegistry() 35 | @compat.ChangeRegionType(region_type='TOOLS') 36 | class MUV_PT_UVEdit_UVManipulation(bpy.types.Panel): 37 | """ 38 | Panel class: UV Manipulation on Property Panel on UV/ImageEditor 39 | """ 40 | 41 | bl_space_type = 'IMAGE_EDITOR' 42 | bl_region_type = 'UI' 43 | bl_label = "UV Manipulation" 44 | bl_category = "Magic UV" 45 | bl_options = {'DEFAULT_CLOSED'} 46 | 47 | def draw_header(self, _): 48 | layout = self.layout 49 | layout.label(text="", icon=compat.icon('IMAGE')) 50 | 51 | def draw(self, context): 52 | sc = context.scene 53 | layout = self.layout 54 | 55 | box = layout.box() 56 | box.prop(sc, "muv_align_uv_enabled", text="Align UV") 57 | if sc.muv_align_uv_enabled: 58 | box.label(text="Align:") 59 | 60 | col = box.column() 61 | row = col.row(align=True) 62 | ops = row.operator(MUV_OT_AlignUV_Circle.bl_idname, text="Circle") 63 | ops.transmission = sc.muv_align_uv_transmission 64 | ops.select = sc.muv_align_uv_select 65 | ops = row.operator(MUV_OT_AlignUV_Straighten.bl_idname, 66 | text="Straighten") 67 | ops.transmission = sc.muv_align_uv_transmission 68 | ops.select = sc.muv_align_uv_select 69 | ops.vertical = sc.muv_align_uv_vertical 70 | ops.horizontal = sc.muv_align_uv_horizontal 71 | ops.mesh_infl = sc.muv_align_uv_mesh_infl 72 | row = col.row() 73 | ops = row.operator(MUV_OT_AlignUV_Axis.bl_idname, text="XY-axis") 74 | ops.transmission = sc.muv_align_uv_transmission 75 | ops.select = sc.muv_align_uv_select 76 | ops.vertical = sc.muv_align_uv_vertical 77 | ops.horizontal = sc.muv_align_uv_horizontal 78 | ops.location = sc.muv_align_uv_location 79 | ops.mesh_infl = sc.muv_align_uv_mesh_infl 80 | row.prop(sc, "muv_align_uv_location", text="") 81 | 82 | col = box.column(align=True) 83 | row = col.row(align=True) 84 | row.prop(sc, "muv_align_uv_transmission", text="Transmission") 85 | row.prop(sc, "muv_align_uv_select", text="Select") 86 | row = col.row(align=True) 87 | row.prop(sc, "muv_align_uv_vertical", text="Vertical") 88 | row.prop(sc, "muv_align_uv_horizontal", text="Horizontal") 89 | col.prop(sc, "muv_align_uv_mesh_infl", text="Mesh Influence") 90 | 91 | box.separator() 92 | 93 | sp = compat.layout_split(box, factor=0.5) 94 | sp.label(text="Snap:") 95 | sp = compat.layout_split(sp, factor=1.0) 96 | sp.prop(sc, "muv_align_uv_snap_method", text="") 97 | 98 | if sc.muv_align_uv_snap_method == 'POINT': 99 | row = box.row(align=True) 100 | ops = row.operator(MUV_OT_AlignUV_SnapToPoint.bl_idname, 101 | text="Snap to Point") 102 | ops.group = sc.muv_align_uv_snap_point_group 103 | ops.target = sc.muv_align_uv_snap_point_target 104 | 105 | col = box.column(align=True) 106 | row = col.row(align=True) 107 | row.prop(sc, "muv_align_uv_snap_point_group", text="Group") 108 | 109 | col.label(text="Target Point:") 110 | row = col.row(align=True) 111 | row.prop(sc, "muv_align_uv_snap_point_target", text="") 112 | row.operator( 113 | MUV_OT_AlignUV_Snap_SetPointTargetToCursor.bl_idname, 114 | text="", icon=compat.icon('CURSOR')) 115 | row.operator( 116 | MUV_OT_AlignUV_Snap_SetPointTargetToVertexGroup.bl_idname, 117 | text="", icon=compat.icon('UV_VERTEXSEL')) 118 | 119 | elif sc.muv_align_uv_snap_method == 'EDGE': 120 | row = box.row(align=True) 121 | ops = row.operator(MUV_OT_AlignUV_SnapToEdge.bl_idname, 122 | text="Snap to Edge") 123 | ops.group = sc.muv_align_uv_snap_edge_group 124 | ops.target_1 = sc.muv_align_uv_snap_edge_target_1 125 | ops.target_2 = sc.muv_align_uv_snap_edge_target_2 126 | 127 | col = box.column(align=True) 128 | row = col.row(align=True) 129 | row.prop(sc, "muv_align_uv_snap_edge_group", text="Group") 130 | 131 | col.label(text="Target Edge:") 132 | sp = compat.layout_split(col, factor=0.33) 133 | subcol = sp.column() 134 | subcol.label(text="Vertex 1:") 135 | subcol.prop(sc, "muv_align_uv_snap_edge_target_1", text="") 136 | sp = compat.layout_split(sp, factor=0.5) 137 | subcol = sp.column() 138 | subcol.label(text="Vertex 2:") 139 | subcol.prop(sc, "muv_align_uv_snap_edge_target_2", text="") 140 | sp = compat.layout_split(sp, factor=1.0) 141 | sp.operator( 142 | MUV_OT_AlignUV_Snap_SetEdgeTargetToEdgeCenter.bl_idname, 143 | text="", icon=compat.icon('UV_EDGESEL')) 144 | 145 | box = layout.box() 146 | box.prop(sc, "muv_smooth_uv_enabled", text="Smooth UV") 147 | if sc.muv_smooth_uv_enabled: 148 | ops = box.operator(MUV_OT_SmoothUV.bl_idname, text="Smooth") 149 | ops.transmission = sc.muv_smooth_uv_transmission 150 | ops.select = sc.muv_smooth_uv_select 151 | ops.mesh_infl = sc.muv_smooth_uv_mesh_infl 152 | col = box.column(align=True) 153 | row = col.row(align=True) 154 | row.prop(sc, "muv_smooth_uv_transmission", text="Transmission") 155 | row.prop(sc, "muv_smooth_uv_select", text="Select") 156 | col.prop(sc, "muv_smooth_uv_mesh_infl", text="Mesh Influence") 157 | 158 | box = layout.box() 159 | box.prop(sc, "muv_select_uv_enabled", text="Select UV") 160 | if sc.muv_select_uv_enabled: 161 | row = box.row(align=True) 162 | ops = row.operator(MUV_OT_SelectUV_SelectOverlapped.bl_idname) 163 | MUV_OT_SelectUV_SelectOverlapped.setup_argument(ops, sc) 164 | ops = row.operator(MUV_OT_SelectUV_SelectFlipped.bl_idname) 165 | MUV_OT_SelectUV_SelectFlipped.setup_argument(ops, sc) 166 | 167 | col = box.column() 168 | col.label(text="Same Polygon Threshold:") 169 | col.prop(sc, "muv_select_uv_same_polygon_threshold", text="") 170 | col.prop(sc, "muv_select_uv_selection_method") 171 | col.prop(sc, "muv_select_uv_sync_mesh_selection") 172 | 173 | box.separator() 174 | 175 | box.operator(MUV_OT_SelectUV_ZoomSelectedUV.bl_idname) 176 | 177 | box = layout.box() 178 | box.prop(sc, "muv_pack_uv_enabled", text="Pack UV (Extension)") 179 | if sc.muv_pack_uv_enabled: 180 | ops = box.operator(MUV_OT_PackUV.bl_idname, text="Pack UV") 181 | ops.allowable_center_deviation = \ 182 | sc.muv_pack_uv_allowable_center_deviation 183 | ops.allowable_size_deviation = \ 184 | sc.muv_pack_uv_allowable_size_deviation 185 | ops.accurate_island_copy = \ 186 | sc.muv_pack_uv_accurate_island_copy 187 | ops.stride = sc.muv_pack_uv_stride 188 | ops.apply_pack_uv = sc.muv_pack_uv_apply_pack_uv 189 | box.prop(sc, "muv_pack_uv_apply_pack_uv") 190 | box.prop(sc, "muv_pack_uv_accurate_island_copy") 191 | box.label(text="Allowable Center Deviation:") 192 | box.prop(sc, "muv_pack_uv_allowable_center_deviation", text="") 193 | box.label(text="Allowable Size Deviation:") 194 | box.prop(sc, "muv_pack_uv_allowable_size_deviation", text="") 195 | box.label(text="Stride:") 196 | box.prop(sc, "muv_pack_uv_stride", text="") 197 | 198 | box = layout.box() 199 | box.prop(sc, "muv_clip_uv_enabled", text="Clip UV") 200 | if sc.muv_clip_uv_enabled: 201 | ops = box.operator(MUV_OT_ClipUV.bl_idname, text="Clip UV") 202 | ops.clip_uv_range_max = sc.muv_clip_uv_range_max 203 | ops.clip_uv_range_min = sc.muv_clip_uv_range_min 204 | box.label(text="Range:") 205 | row = box.row() 206 | col = row.column() 207 | col.prop(sc, "muv_clip_uv_range_max", text="Max") 208 | col = row.column() 209 | col.prop(sc, "muv_clip_uv_range_min", text="Min") 210 | -------------------------------------------------------------------------------- /src/magic_uv/ui/view3d_copy_paste_uv_editmode.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | import bpy 9 | 10 | from ..op.copy_paste_uv import ( 11 | MUV_MT_CopyPasteUV_CopyUV, 12 | MUV_MT_CopyPasteUV_PasteUV, 13 | MUV_MT_CopyPasteUV_SelSeqCopyUV, 14 | MUV_MT_CopyPasteUV_SelSeqPasteUV, 15 | ) 16 | from ..op.transfer_uv import ( 17 | MUV_OT_TransferUV_CopyUV, 18 | MUV_OT_TransferUV_PasteUV, 19 | ) 20 | from ..utils.bl_class_registry import BlClassRegistry 21 | from ..utils import compatibility as compat 22 | 23 | 24 | @BlClassRegistry() 25 | @compat.ChangeRegionType(region_type='TOOLS') 26 | class MUV_PT_CopyPasteUVEditMode(bpy.types.Panel): 27 | """ 28 | Panel class: Copy/Paste UV on Property Panel on View3D 29 | """ 30 | 31 | bl_space_type = 'VIEW_3D' 32 | bl_region_type = 'UI' 33 | bl_label = "Copy/Paste UV" 34 | bl_category = "Edit" 35 | bl_context = 'mesh_edit' 36 | bl_options = {'DEFAULT_CLOSED'} 37 | 38 | def draw_header(self, _): 39 | layout = self.layout 40 | layout.label(text="", icon=compat.icon('IMAGE')) 41 | 42 | def draw(self, context): 43 | sc = context.scene 44 | layout = self.layout 45 | 46 | box = layout.box() 47 | box.prop(sc, "muv_copy_paste_uv_enabled", text="Copy/Paste UV") 48 | if sc.muv_copy_paste_uv_enabled: 49 | row = box.row(align=True) 50 | if sc.muv_copy_paste_uv_mode == 'DEFAULT': 51 | row.menu(MUV_MT_CopyPasteUV_CopyUV.bl_idname, text="Copy") 52 | row.menu(MUV_MT_CopyPasteUV_PasteUV.bl_idname, text="Paste") 53 | elif sc.muv_copy_paste_uv_mode == 'SEL_SEQ': 54 | row.menu(MUV_MT_CopyPasteUV_SelSeqCopyUV.bl_idname, 55 | text="Copy") 56 | row.menu(MUV_MT_CopyPasteUV_SelSeqPasteUV.bl_idname, 57 | text="Paste") 58 | box.prop(sc, "muv_copy_paste_uv_mode", expand=True) 59 | box.prop(sc, "muv_copy_paste_uv_copy_seams", text="Seams") 60 | box.prop(sc, "muv_copy_paste_uv_strategy", text="Strategy") 61 | 62 | box = layout.box() 63 | box.prop(sc, "muv_transfer_uv_enabled", text="Transfer UV") 64 | if sc.muv_transfer_uv_enabled: 65 | row = box.row(align=True) 66 | row.operator(MUV_OT_TransferUV_CopyUV.bl_idname, text="Copy") 67 | ops = row.operator(MUV_OT_TransferUV_PasteUV.bl_idname, 68 | text="Paste") 69 | ops.invert_normals = sc.muv_transfer_uv_invert_normals 70 | ops.copy_seams = sc.muv_transfer_uv_copy_seams 71 | row = box.row() 72 | row.prop(sc, "muv_transfer_uv_invert_normals", 73 | text="Invert Normals") 74 | row.prop(sc, "muv_transfer_uv_copy_seams", text="Seams") 75 | -------------------------------------------------------------------------------- /src/magic_uv/ui/view3d_copy_paste_uv_objectmode.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | import bpy 9 | 10 | from ..op.copy_paste_uv_object import ( 11 | MUV_MT_CopyPasteUVObject_CopyUV, 12 | MUV_MT_CopyPasteUVObject_PasteUV, 13 | ) 14 | from ..utils.bl_class_registry import BlClassRegistry 15 | from ..utils import compatibility as compat 16 | 17 | 18 | @BlClassRegistry() 19 | @compat.ChangeRegionType(region_type='TOOLS') 20 | class MUV_PT_View3D_Object_CopyPasteUV(bpy.types.Panel): 21 | """ 22 | Panel class: Copy/Paste UV on Property Panel on View3D 23 | """ 24 | 25 | bl_space_type = 'VIEW_3D' 26 | bl_region_type = 'UI' 27 | bl_label = "Copy/Paste UV" 28 | bl_category = "Edit" 29 | bl_context = 'objectmode' 30 | bl_options = {'DEFAULT_CLOSED'} 31 | 32 | def draw_header(self, _): 33 | layout = self.layout 34 | layout.label(text="", icon=compat.icon('IMAGE')) 35 | 36 | def draw(self, context): 37 | sc = context.scene 38 | layout = self.layout 39 | 40 | row = layout.row(align=True) 41 | row.menu(MUV_MT_CopyPasteUVObject_CopyUV.bl_idname, text="Copy") 42 | row.menu(MUV_MT_CopyPasteUVObject_PasteUV.bl_idname, text="Paste") 43 | layout.prop(sc, "muv_copy_paste_uv_object_copy_seams", 44 | text="Seams") 45 | -------------------------------------------------------------------------------- /src/magic_uv/ui/view3d_uv_mapping.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | import bpy 9 | 10 | from ..op.uvw import ( 11 | MUV_OT_UVW_BoxMap, 12 | MUV_OT_UVW_BestPlanerMap, 13 | ) 14 | from ..op.texture_projection import ( 15 | MUV_OT_TextureProjection, 16 | MUV_OT_TextureProjection_Project, 17 | ) 18 | from ..op.unwrap_constraint import MUV_OT_UnwrapConstraint 19 | from ..utils.bl_class_registry import BlClassRegistry 20 | from ..utils import compatibility as compat 21 | 22 | 23 | @BlClassRegistry() 24 | @compat.ChangeRegionType(region_type='TOOLS') 25 | class MUV_PT_View3D_UVMapping(bpy.types.Panel): 26 | """ 27 | Panel class: UV Mapping on Property Panel on View3D 28 | """ 29 | 30 | bl_space_type = 'VIEW_3D' 31 | bl_region_type = 'UI' 32 | bl_label = "UV Mapping" 33 | bl_category = "Edit" 34 | bl_context = 'mesh_edit' 35 | bl_options = {'DEFAULT_CLOSED'} 36 | 37 | def draw_header(self, _): 38 | layout = self.layout 39 | layout.label(text="", icon=compat.icon('IMAGE')) 40 | 41 | def draw(self, context): 42 | sc = context.scene 43 | layout = self.layout 44 | 45 | box = layout.box() 46 | box.prop(sc, "muv_unwrap_constraint_enabled", text="Unwrap Constraint") 47 | if sc.muv_unwrap_constraint_enabled: 48 | ops = box.operator(MUV_OT_UnwrapConstraint.bl_idname, 49 | text="Unwrap") 50 | ops.u_const = sc.muv_unwrap_constraint_u_const 51 | ops.v_const = sc.muv_unwrap_constraint_v_const 52 | row = box.row(align=True) 53 | row.prop(sc, "muv_unwrap_constraint_u_const", text="U-Constraint") 54 | row.prop(sc, "muv_unwrap_constraint_v_const", text="V-Constraint") 55 | 56 | box = layout.box() 57 | box.prop(sc, "muv_texture_projection_enabled", 58 | text="Texture Projection") 59 | if sc.muv_texture_projection_enabled: 60 | row = box.row() 61 | row.prop( 62 | sc, "muv_texture_projection_enable", 63 | text="Disable" 64 | if MUV_OT_TextureProjection.is_running(context) 65 | else "Enable", 66 | icon='RESTRICT_VIEW_OFF' 67 | if MUV_OT_TextureProjection.is_running(context) 68 | else 'RESTRICT_VIEW_ON') 69 | row.prop(sc, "muv_texture_projection_tex_image", text="") 70 | box.prop(sc, "muv_texture_projection_tex_transparency", 71 | text="Transparency") 72 | col = box.column(align=True) 73 | row = col.row() 74 | row.prop(sc, "muv_texture_projection_adjust_window", 75 | text="Adjust Window") 76 | if not sc.muv_texture_projection_adjust_window: 77 | sp = compat.layout_split(col, factor=0.5) 78 | sub = sp.column() 79 | sub.prop(sc, "muv_texture_projection_tex_scaling", 80 | text="Scaling") 81 | sp = compat.layout_split(sp, factor=1.0) 82 | sub = sp.column() 83 | sub.prop(sc, "muv_texture_projection_tex_translation", 84 | text="Translation") 85 | row = col.row() 86 | row.label(text="Rotation:") 87 | row.prop(sc, "muv_texture_projection_tex_rotation", text="") 88 | col.separator() 89 | col.prop(sc, "muv_texture_projection_apply_tex_aspect", 90 | text="Texture Aspect Ratio") 91 | col.prop(sc, "muv_texture_projection_assign_uvmap", 92 | text="Assign UVMap") 93 | box.operator( 94 | MUV_OT_TextureProjection_Project.bl_idname, 95 | text="Project") 96 | 97 | box = layout.box() 98 | box.prop(sc, "muv_uvw_enabled", text="UVW") 99 | if sc.muv_uvw_enabled: 100 | row = box.row(align=True) 101 | ops = row.operator(MUV_OT_UVW_BoxMap.bl_idname, text="Box") 102 | ops.assign_uvmap = sc.muv_uvw_assign_uvmap 103 | ops = row.operator(MUV_OT_UVW_BestPlanerMap.bl_idname, 104 | text="Best Planner") 105 | ops.assign_uvmap = sc.muv_uvw_assign_uvmap 106 | box.prop(sc, "muv_uvw_assign_uvmap", text="Assign UVMap") 107 | -------------------------------------------------------------------------------- /src/magic_uv/utils/__init__.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | if "bpy" in locals(): 9 | import importlib 10 | importlib.reload(bl_class_registry) 11 | importlib.reload(compatibility) 12 | importlib.reload(graph) 13 | importlib.reload(property_class_registry) 14 | else: 15 | from . import bl_class_registry 16 | from . import compatibility 17 | from . import graph 18 | from . import property_class_registry 19 | 20 | import bpy 21 | -------------------------------------------------------------------------------- /src/magic_uv/utils/bl_class_registry.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | import bpy 9 | 10 | from .. import common 11 | 12 | 13 | class BlClassRegistry: 14 | class_list = [] 15 | 16 | def __init__(self, *_, **kwargs): 17 | self.legacy = kwargs.get('legacy', False) 18 | 19 | def __call__(self, cls): 20 | if hasattr(cls, "bl_idname"): 21 | BlClassRegistry.add_class(cls.bl_idname, cls, self.legacy) 22 | elif hasattr(cls, "bl_context"): 23 | bl_idname = "{}{}{}{}".format(cls.bl_space_type, 24 | cls.bl_region_type, 25 | cls.bl_context, cls.bl_label) 26 | BlClassRegistry.add_class(bl_idname, cls, self.legacy) 27 | else: 28 | bl_idname = "{}{}{}".format(cls.bl_space_type, 29 | cls.bl_region_type, 30 | cls.bl_label) 31 | BlClassRegistry.add_class(bl_idname, cls, self.legacy) 32 | return cls 33 | 34 | @classmethod 35 | def add_class(cls, bl_idname, op_class, legacy): 36 | for class_ in cls.class_list: 37 | if (class_["bl_idname"] == bl_idname) and \ 38 | (class_["legacy"] == legacy): 39 | raise RuntimeError("{} is already registered" 40 | .format(bl_idname)) 41 | 42 | new_op = { 43 | "bl_idname": bl_idname, 44 | "class": op_class, 45 | "legacy": legacy, 46 | } 47 | cls.class_list.append(new_op) 48 | common.debug_print("{} is registered.".format(bl_idname)) 49 | 50 | @classmethod 51 | def register(cls): 52 | for class_ in cls.class_list: 53 | bpy.utils.register_class(class_["class"]) 54 | common.debug_print("{} is registered to Blender." 55 | .format(class_["bl_idname"])) 56 | 57 | @classmethod 58 | def unregister(cls): 59 | for class_ in cls.class_list: 60 | bpy.utils.unregister_class(class_["class"]) 61 | common.debug_print("{} is unregistered from Blender." 62 | .format(class_["bl_idname"])) 63 | 64 | @classmethod 65 | def cleanup(cls): 66 | cls.class_list = [] 67 | common.debug_print("Cleanup registry.") 68 | -------------------------------------------------------------------------------- /src/magic_uv/utils/compatibility.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | import bpy 9 | 10 | 11 | def check_version(major, minor, _): 12 | """ 13 | Check blender version 14 | """ 15 | 16 | if bpy.app.version[0] == major and bpy.app.version[1] == minor: 17 | return 0 18 | if bpy.app.version[0] > major: 19 | return 1 20 | if bpy.app.version[1] > minor: 21 | return 1 22 | return -1 23 | 24 | 25 | def make_annotations(cls): 26 | if check_version(2, 80, 0) < 0: 27 | return cls 28 | 29 | # make annotation from attributes 30 | props = {k: v 31 | for k, v in cls.__dict__.items() 32 | if isinstance(v, getattr(bpy.props, '_PropertyDeferred', tuple))} 33 | if props: 34 | if '__annotations__' not in cls.__dict__: 35 | setattr(cls, '__annotations__', {}) 36 | annotations = cls.__dict__['__annotations__'] 37 | for k, v in props.items(): 38 | annotations[k] = v 39 | delattr(cls, k) 40 | 41 | return cls 42 | 43 | 44 | class ChangeRegionType: 45 | def __init__(self, *_, **kwargs): 46 | self.region_type = kwargs.get('region_type', False) 47 | 48 | def __call__(self, cls): 49 | if check_version(2, 80, 0) >= 0: 50 | return cls 51 | 52 | cls.bl_region_type = self.region_type 53 | 54 | return cls 55 | 56 | 57 | def matmul(m1, m2): 58 | if check_version(2, 80, 0) < 0: 59 | return m1 * m2 60 | 61 | return m1 @ m2 62 | 63 | 64 | def layout_split(layout, factor=0.0, align=False): 65 | if check_version(2, 80, 0) < 0: 66 | return layout.split(percentage=factor, align=align) 67 | 68 | return layout.split(factor=factor, align=align) 69 | 70 | 71 | def get_user_preferences(context): 72 | if hasattr(context, "user_preferences"): 73 | return context.user_preferences 74 | 75 | return context.preferences 76 | 77 | 78 | def get_object_select(obj): 79 | if check_version(2, 80, 0) < 0: 80 | return obj.select 81 | 82 | return obj.select_get() 83 | 84 | 85 | def set_object_select(obj, select): 86 | if check_version(2, 80, 0) < 0: 87 | obj.select = select 88 | else: 89 | obj.select_set(select) 90 | 91 | 92 | def set_active_object(obj): 93 | if check_version(2, 80, 0) < 0: 94 | bpy.context.scene.objects.active = obj 95 | else: 96 | bpy.context.view_layer.objects.active = obj 97 | 98 | 99 | def get_active_object(context): 100 | if check_version(2, 80, 0) < 0: 101 | return context.scene.objects.active 102 | else: 103 | return context.view_layer.objects.active 104 | 105 | 106 | def object_has_uv_layers(obj): 107 | if check_version(2, 80, 0) < 0: 108 | return hasattr(obj.data, "uv_textures") 109 | else: 110 | return hasattr(obj.data, "uv_layers") 111 | 112 | 113 | def get_object_uv_layers(obj): 114 | if obj.type != 'MESH': 115 | return None 116 | if check_version(2, 80, 0) < 0: 117 | return obj.data.uv_textures 118 | else: 119 | return obj.data.uv_layers 120 | 121 | 122 | def icon(icon): 123 | if icon == 'IMAGE': 124 | if check_version(2, 80, 0) < 0: 125 | return 'IMAGE_COL' 126 | 127 | return icon 128 | 129 | 130 | def get_all_space_types(): 131 | if check_version(2, 80, 0) >= 0: 132 | return { 133 | 'CLIP_EDITOR': bpy.types.SpaceClipEditor, 134 | 'CONSOLE': bpy.types.SpaceConsole, 135 | 'DOPESHEET_EDITOR': bpy.types.SpaceDopeSheetEditor, 136 | 'FILE_BROWSER': bpy.types.SpaceFileBrowser, 137 | 'GRAPH_EDITOR': bpy.types.SpaceGraphEditor, 138 | 'IMAGE_EDITOR': bpy.types.SpaceImageEditor, 139 | 'INFO': bpy.types.SpaceInfo, 140 | 'NLA_EDITOR': bpy.types.SpaceNLA, 141 | 'NODE_EDITOR': bpy.types.SpaceNodeEditor, 142 | 'OUTLINER': bpy.types.SpaceOutliner, 143 | 'PROPERTIES': bpy.types.SpaceProperties, 144 | 'SEQUENCE_EDITOR': bpy.types.SpaceSequenceEditor, 145 | 'TEXT_EDITOR': bpy.types.SpaceTextEditor, 146 | 'USER_PREFERENCES': bpy.types.SpacePreferences, 147 | 'VIEW_3D': bpy.types.SpaceView3D, 148 | } 149 | else: 150 | return { 151 | 'VIEW_3D': bpy.types.SpaceView3D, 152 | 'TIMELINE': bpy.types.SpaceTimeline, 153 | 'GRAPH_EDITOR': bpy.types.SpaceGraphEditor, 154 | 'DOPESHEET_EDITOR': bpy.types.SpaceDopeSheetEditor, 155 | 'NLA_EDITOR': bpy.types.SpaceNLA, 156 | 'IMAGE_EDITOR': bpy.types.SpaceImageEditor, 157 | 'SEQUENCE_EDITOR': bpy.types.SpaceSequenceEditor, 158 | 'CLIP_EDITOR': bpy.types.SpaceClipEditor, 159 | 'TEXT_EDITOR': bpy.types.SpaceTextEditor, 160 | 'NODE_EDITOR': bpy.types.SpaceNodeEditor, 161 | 'LOGIC_EDITOR': bpy.types.SpaceLogicEditor, 162 | 'PROPERTIES': bpy.types.SpaceProperties, 163 | 'OUTLINER': bpy.types.SpaceOutliner, 164 | 'USER_PREFERENCES': bpy.types.SpaceUserPreferences, 165 | 'INFO': bpy.types.SpaceInfo, 166 | 'FILE_BROWSER': bpy.types.SpaceFileBrowser, 167 | 'CONSOLE': bpy.types.SpaceConsole, 168 | } 169 | -------------------------------------------------------------------------------- /src/magic_uv/utils/graph.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | 9 | class Node: 10 | def __init__(self, key, value=None): 11 | self.key = key 12 | self.value = value 13 | self.edges = [] 14 | 15 | def degree(self): 16 | return len(self.edges) 17 | 18 | def connected_nodes(self): 19 | return [e.other(self) for e in self.edges] 20 | 21 | 22 | class Edge: 23 | def __init__(self, node_1, node_2): 24 | self.node_1 = node_1 25 | self.node_2 = node_2 26 | 27 | def other(self, node): 28 | if self.node_1 == node and self.node_2 == node: 29 | raise RuntimeError("Loop edge in {} is not supported." 30 | .format(node.key)) 31 | if node not in (self.node_1, self.node_2): 32 | raise RuntimeError("Node {} does not belong this edge." 33 | .format(node.key)) 34 | if self.node_1 == node: 35 | return self.node_2 36 | return self.node_1 37 | 38 | 39 | class Graph: 40 | def __init__(self): 41 | self.edges = [] 42 | self.nodes = {} 43 | 44 | def add_node(self, node): 45 | if node.key in self.nodes: 46 | raise RuntimeError("Node '{}' is already registered." 47 | .format(node.key)) 48 | self.nodes[node.key] = node 49 | 50 | def add_edge(self, node_1, node_2): 51 | if node_1.key not in self.nodes: 52 | raise RuntimeError("Node '{}' is not registered." 53 | .format(node_1.key)) 54 | if node_2.key not in self.nodes: 55 | raise RuntimeError("Node '{}' is not registered." 56 | .format(node_2.key)) 57 | 58 | edge = Edge(node_1, node_2) 59 | self.edges.append(edge) 60 | node_1.edges.append(edge) 61 | node_2.edges.append(edge) 62 | 63 | def get_node(self, key): 64 | return self.nodes[key] 65 | 66 | 67 | def dump_graph(graph): 68 | print("=== Node ===") 69 | for _, node in graph.nodes.items(): 70 | print("Key: {}, Value {}".format(node.key, node.value)) 71 | 72 | print("=== Edge ===") 73 | for edge in graph.edges: 74 | print("{} - {}".format(edge.node_1.key, edge.node_2.key)) 75 | 76 | 77 | # VF2 algorithm 78 | # Ref: https://stackoverflow.com/questions/8176298/ 79 | # vf2-algorithm-steps-with-example 80 | # Ref: https://github.com/satemochi/saaaaah/blob/master/geometric_misc/ 81 | # isomorph/vf2/vf2.py 82 | def graph_is_isomorphic(graph_1, graph_2): 83 | def is_iso(pairs, matching_node, new_node): 84 | # Algorithm: 85 | # 1. The degree is same (It's faster). 86 | # 2. The connected node is same. 87 | if matching_node.degree() != new_node.degree(): 88 | return False 89 | 90 | matching_connected = [c.key for c in matching_node.connected_nodes()] 91 | new_connected = [c.key for c in new_node.connected_nodes()] 92 | 93 | for p in pairs: 94 | n1 = p[0] 95 | n2 = p[1] 96 | if n1 in matching_connected and n2 not in new_connected: 97 | return False 98 | if n1 not in matching_connected and n2 in new_connected: 99 | return False 100 | 101 | return True 102 | 103 | def dfs(graph_1, graph_2): 104 | def generate_pair(g1, g2, pairs): 105 | remove_1 = [p[0] for p in pairs] 106 | remove_2 = [p[1] for p in pairs] 107 | 108 | keys_1 = sorted(list(set(g1.nodes.keys()) - set(remove_1))) 109 | keys_2 = sorted(list(set(g2.nodes.keys()) - set(remove_2))) 110 | for k1 in keys_1: 111 | for k2 in keys_2: 112 | yield (k1, k2) 113 | 114 | pairs = [] 115 | stack = [generate_pair(graph_1, graph_2, pairs)] 116 | while stack: 117 | try: 118 | k1, k2 = next(stack[-1]) 119 | n1 = graph_1.get_node(k1) 120 | n2 = graph_2.get_node(k2) 121 | if is_iso(pairs, n1, n2): 122 | pairs.append([k1, k2]) 123 | stack.append(generate_pair(graph_1, graph_2, pairs)) 124 | if len(pairs) == len(graph_1.nodes): 125 | return True, pairs 126 | except StopIteration: 127 | stack.pop() 128 | diff = len(pairs) - len(stack) 129 | for _ in range(diff): 130 | pairs.pop() 131 | 132 | return False, [] 133 | 134 | # First, check simple condition. 135 | if len(graph_1.nodes) != len(graph_2.nodes): 136 | return False, {} 137 | if len(graph_1.edges) != len(graph_2.edges): 138 | return False, {} 139 | 140 | is_isomorphic, pairs = dfs(graph_1, graph_2) 141 | 142 | node_pairs = {} 143 | for pair in pairs: 144 | n1 = pair[0] 145 | n2 = pair[1] 146 | node_1 = graph_1.get_node(n1) 147 | node_2 = graph_2.get_node(n2) 148 | node_pairs[node_1] = node_2 149 | 150 | return is_isomorphic, node_pairs 151 | -------------------------------------------------------------------------------- /src/magic_uv/utils/property_class_registry.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: GPL-2.0-or-later 2 | 3 | __author__ = "Nutti " 4 | __status__ = "production" 5 | __version__ = "6.6" 6 | __date__ = "22 Apr 2022" 7 | 8 | from .. import common 9 | 10 | 11 | class PropertyClassRegistry: 12 | class_list = [] 13 | 14 | def __init__(self, *_, **kwargs): 15 | self.legacy = kwargs.get('legacy', False) 16 | 17 | def __call__(self, cls): 18 | PropertyClassRegistry.add_class(cls.idname, cls, self.legacy) 19 | return cls 20 | 21 | @classmethod 22 | def add_class(cls, idname, prop_class, legacy): 23 | for class_ in cls.class_list: 24 | if (class_["idname"] == idname) and (class_["legacy"] == legacy): 25 | raise RuntimeError("{} is already registered".format(idname)) 26 | 27 | new_op = { 28 | "idname": idname, 29 | "class": prop_class, 30 | "legacy": legacy, 31 | } 32 | cls.class_list.append(new_op) 33 | common.debug_print("{} is registered.".format(idname)) 34 | 35 | @classmethod 36 | def init_props(cls, scene): 37 | for class_ in cls.class_list: 38 | class_["class"].init_props(scene) 39 | common.debug_print("{} is initialized.".format(class_["idname"])) 40 | 41 | @classmethod 42 | def del_props(cls, scene): 43 | for class_ in cls.class_list: 44 | class_["class"].del_props(scene) 45 | common.debug_print("{} is cleared.".format(class_["idname"])) 46 | 47 | @classmethod 48 | def cleanup(cls): 49 | cls.class_list = [] 50 | common.debug_print("Cleanup registry.") 51 | -------------------------------------------------------------------------------- /tests/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | ENV MUV_CONSOLE_MODE true 4 | 5 | WORKDIR /root 6 | 7 | RUN env 8 | RUN apt-get update -y -qq 9 | RUN apt-get install -y \ 10 | blender \ 11 | wget \ 12 | python3 \ 13 | python3-pip \ 14 | zip 15 | 16 | RUN wget http://mirror.cs.umn.edu/blender.org/release/Blender2.77/blender-2.77-linux-glibc211-x86_64.tar.bz2 17 | RUN tar jxf blender-2.77-linux-glibc211-x86_64.tar.bz2 18 | -------------------------------------------------------------------------------- /tests/lint/pep8.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ $# -ne 1 ]; then 4 | echo "Usage: pep8.sh " 5 | exit 1 6 | fi 7 | 8 | tgt=${1} 9 | 10 | files=`find ${tgt} -name "*.py"` 11 | ignores=("__init__.py" "utils/compatibility.py" "lib/bglx.py") 12 | 13 | # pep8 14 | for file in ${files[@]}; do 15 | # ignore file in ignores 16 | found=0 17 | for ign in ${ignores[@]}; do 18 | if [ `echo "${file}" | grep "${ign}"` ]; then 19 | found=1 20 | fi 21 | done 22 | if [ ${found} -eq 1 ]; then 23 | continue 24 | fi 25 | echo "======= pep8 test "${file}" =======" 26 | which pycodestyle > /dev/null 27 | if [ $? -eq 0 ]; then 28 | pycodestyle ${file} --config pycodestyle 29 | else 30 | pep8 ${file} 31 | fi 32 | ret=`echo $?` 33 | if [ ${ret} -ne 0 ]; then 34 | exit 1 35 | fi 36 | done 37 | -------------------------------------------------------------------------------- /tests/lint/pylint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ $# -ne 1 ]; then 4 | echo "Usage: pylint.sh " 5 | exit 1 6 | fi 7 | 8 | tgt=${1} 9 | 10 | files=`find ${tgt} -name "*.py"` 11 | ignores=("__init__.py" "utils/compatibility.py" "lib/bglx.py") 12 | 13 | # pylint 14 | for file in ${files[@]}; do 15 | # ignore file in ignores 16 | found=0 17 | for ign in ${ignores[@]}; do 18 | if [ `echo "${file}" | grep "${ign}"` ]; then 19 | found=1 20 | fi 21 | done 22 | if [ ${found} -eq 1 ]; then 23 | continue 24 | fi 25 | echo "======= pylint test "${file}" =======" 26 | pylint ${file} 27 | ret=`echo $?` 28 | if [ ${ret} -ne 0 ]; then 29 | echo "Test failed (error code: "${ret}")" 30 | exit 1 31 | fi 32 | done 33 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/__init__.py: -------------------------------------------------------------------------------- 1 | from . import align_uv_test 2 | from . import align_uv_cursor_test 3 | from . import clip_uv_test 4 | from . import copy_paste_uv_test 5 | from . import copy_paste_uv_object_test 6 | from . import copy_paste_uv_uvedit_test 7 | from . import flip_rotate_uv_test 8 | from . import mirror_uv_test 9 | from . import move_uv_test 10 | from . import pack_uv_test 11 | from . import preserve_uv_aspect_test 12 | from . import select_uv_test 13 | from . import smooth_uv_test 14 | from . import texture_lock_test 15 | from . import texture_projection_test 16 | from . import texture_wrap_test 17 | from . import transfer_uv_test 18 | from . import unwrap_constraint_test 19 | from . import uv_bounding_box_test 20 | from . import uv_inspection_test 21 | from . import uv_sculpt_test 22 | from . import uvw_test 23 | from . import world_scale_uv_test 24 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/align_uv_cursor_test.py: -------------------------------------------------------------------------------- 1 | from . import common 2 | 3 | 4 | class TestAlignUVCursor(common.TestBase): 5 | module_name = "align_uv_cursor" 6 | idname = [ 7 | ('OPERATOR', 'uv.muv_align_uv_cursor'), 8 | ] 9 | 10 | # this test can not be done because area always NoneType in console run 11 | def test_nothing(self): 12 | pass 13 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/clip_uv_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import bpy 4 | 5 | from . import common 6 | from . import compatibility as compat 7 | 8 | 9 | class TestClipUV(common.TestBase): 10 | module_name = "clip_uv" 11 | idname = [ 12 | # Clip UV 13 | ('OPERATOR', 'uv.muv_clip_uv'), 14 | ] 15 | 16 | def setUpEachMethod(self): 17 | obj_name = "Cube" 18 | 19 | common.select_object_only(obj_name) 20 | compat.set_active_object(bpy.data.objects[obj_name]) 21 | bpy.ops.object.mode_set(mode='EDIT') 22 | 23 | bpy.context.scene.tool_settings.use_uv_select_sync = True 24 | 25 | def tearDownMethod(self): 26 | bpy.context.scene.tool_settings.use_uv_select_sync = False 27 | 28 | def test_ng_no_uv(self): 29 | # Warning: Object must have more than one UV map 30 | print("[TEST] (NG) No UV") 31 | bpy.ops.mesh.select_all(action='SELECT') 32 | result = bpy.ops.uv.muv_clip_uv() 33 | self.assertSetEqual(result, {'CANCELLED'}) 34 | 35 | def test_ok_default(self): 36 | print("[TEST] (OK) Default") 37 | bpy.ops.mesh.select_all(action='SELECT') 38 | bpy.ops.mesh.uv_texture_add() 39 | result = bpy.ops.uv.muv_clip_uv() 40 | self.assertSetEqual(result, {'FINISHED'}) 41 | 42 | def test_ok_user_specified(self): 43 | print("[TEST] (OK) User specified") 44 | bpy.ops.mesh.select_all(action='SELECT') 45 | bpy.ops.mesh.uv_texture_add() 46 | result = bpy.ops.uv.muv_clip_uv( 47 | clip_uv_range_max=(1.5, 2.0), 48 | clip_uv_range_min=(-1.5, -3.0) 49 | ) 50 | self.assertSetEqual(result, {'FINISHED'}) 51 | 52 | @unittest.skipIf(compat.check_version(2, 80, 0) < 0, 53 | "Not supported in <2.80") 54 | def test_ok_multiple_objects(self): 55 | print("[TEST] (OK) Multiple Objects") 56 | 57 | # Duplicate object. 58 | bpy.ops.object.mode_set(mode='OBJECT') 59 | obj_names = ["Cube", "Cube.001"] 60 | common.select_object_only(obj_names[0]) 61 | common.duplicate_object_without_uv() 62 | 63 | for name in obj_names: 64 | bpy.ops.object.mode_set(mode='OBJECT') 65 | common.select_object_only(name) 66 | compat.set_active_object(bpy.data.objects[name]) 67 | bpy.ops.object.mode_set(mode='EDIT') 68 | bpy.ops.mesh.uv_texture_add() 69 | bpy.ops.mesh.select_all(action='SELECT') 70 | 71 | # Select two objects. 72 | bpy.ops.object.mode_set(mode='OBJECT') 73 | compat.set_active_object(bpy.data.objects[obj_names[0]]) 74 | common.select_objects_only(obj_names) 75 | bpy.ops.object.mode_set(mode='EDIT') 76 | 77 | result = bpy.ops.uv.muv_clip_uv( 78 | clip_uv_range_max=(1.5, 2.0), 79 | clip_uv_range_min=(-1.5, -3.0) 80 | ) 81 | self.assertSetEqual(result, {'FINISHED'}) 82 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/common.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | 4 | import bpy 5 | import bmesh 6 | 7 | from . import compatibility as compat 8 | 9 | 10 | TESTEE_FILE = "testee.blend" 11 | 12 | 13 | def check_addon_enabled(mod): 14 | if compat.check_version(2, 80, 0) < 0: 15 | result = bpy.ops.wm.addon_enable(module=mod) 16 | else: 17 | result = bpy.ops.preferences.addon_enable(module=mod) 18 | assert (result == {'FINISHED'}), "Failed to enable add-on %s" % (mod) 19 | assert (mod in compat.get_user_preferences(bpy.context).addons.keys()), "Failed to enable add-on %s" % (mod) 20 | 21 | 22 | def check_addon_disabled(mod): 23 | if compat.check_version(2, 80, 0) < 0: 24 | result = bpy.ops.wm.addon_disable(module=mod) 25 | else: 26 | result = bpy.ops.preferences.addon_disable(module=mod) 27 | assert (result == {'FINISHED'}), "Failed to disable add-on %s" % (mod) 28 | assert (not mod in compat.get_user_preferences(bpy.context).addons.keys()), "Failed to disable add-on %s" % (mod) 29 | 30 | 31 | def operator_exists(idname): 32 | try: 33 | from bpy.ops import op_as_string 34 | op_as_string(idname) 35 | return True 36 | except: 37 | return False 38 | 39 | 40 | def menu_exists(idname): 41 | return idname in dir(bpy.types) 42 | 43 | 44 | def get_user_preferences(context): 45 | if hasattr(context, "user_preferences"): 46 | return context.user_preferences 47 | 48 | return context.preferences 49 | 50 | 51 | def set_object_select(obj, select): 52 | if compat.check_version(2, 80, 0) < 0: 53 | obj.select = select 54 | else: 55 | obj.select_set(select) 56 | 57 | 58 | def select_object_only(obj_name): 59 | for o in bpy.data.objects: 60 | if o.name == obj_name: 61 | set_object_select(o, True) 62 | else: 63 | set_object_select(o, False) 64 | 65 | 66 | def select_objects_only(obj_names): 67 | for o in bpy.data.objects: 68 | set_object_select(o, False) 69 | for name in obj_names: 70 | set_object_select(bpy.data.objects[name], True) 71 | 72 | 73 | def select_faces(obj, num_face, offset=0): 74 | bm = bmesh.from_edit_mesh(obj.data) 75 | selected_faces = [] 76 | for i, f in enumerate(bm.faces[offset:]): 77 | if i >= num_face: 78 | f.select = False 79 | else: 80 | f.select = True 81 | selected_faces.append(f) 82 | return selected_faces 83 | 84 | def select_and_active_faces(obj, num_face): 85 | bm = bmesh.from_edit_mesh(obj.data) 86 | return select_and_active_faces_bm(bm, num_face) 87 | 88 | 89 | def select_unlink_faces(obj, face, num_face): 90 | bm = bmesh.from_edit_mesh(obj.data) 91 | return select_unlink_faces_bm(bm, face, num_face) 92 | 93 | 94 | def select_and_active_faces_bm(bm, num_face): 95 | selected_face = [] 96 | for i, f in enumerate(bm.faces): 97 | if i >= num_face: 98 | f.select = False 99 | else: 100 | f.select = True 101 | bm.faces.active = f 102 | selected_face.append(f) 103 | return selected_face 104 | 105 | 106 | def select_unlink_faces_bm(bm, face, num_face): 107 | count = 0 108 | linked_faces = [] 109 | selected_faces = [] 110 | for e in face.edges: 111 | for lf in e.link_faces: 112 | linked_faces.append(lf) 113 | for f in bm.faces: 114 | if not f in linked_faces: 115 | f.select = True 116 | count = count + 1 117 | selected_faces.append(f) 118 | if count >= num_face: 119 | break 120 | return selected_faces 121 | 122 | 123 | def select_link_face(obj, num_face): 124 | bm = bmesh.from_edit_mesh(obj.data) 125 | count = 0 126 | selected_face = [] 127 | for f in bm.faces: 128 | if f.select: 129 | for e in f.edges: 130 | for lf in e.link_faces: 131 | if not lf.select: 132 | lf.select = True 133 | selected_face.append(lf) 134 | count = count + 1 135 | if count >= num_face: 136 | break 137 | if count >= num_face: 138 | break 139 | if count >= num_face: 140 | break 141 | return selected_face 142 | 143 | 144 | def add_face_select_history(obj, num_face, offset=0): 145 | bm = bmesh.from_edit_mesh(obj.data) 146 | bm.select_history.clear() 147 | for i, f in enumerate(bm.faces[offset:]): 148 | if i < num_face: 149 | bm.select_history.add(f) 150 | for hist in bm.select_history: 151 | hist.select = True 152 | 153 | 154 | def add_face_select_history_by_indices(obj, indices): 155 | bm = bmesh.from_edit_mesh(obj.data) 156 | bm.faces.ensure_lookup_table() 157 | bm.select_history.clear() 158 | for i in indices: 159 | bm.select_history.add(bm.faces[i]) 160 | for hist in bm.select_history: 161 | hist.select = True 162 | 163 | 164 | # This is a workaround for >2.80 because UV Map will be assigned 165 | # automatically while creating a object 166 | def delete_all_uv_maps(obj): 167 | mode_orig = bpy.context.active_object.mode 168 | bpy.ops.object.mode_set(mode='EDIT') 169 | 170 | bm = bmesh.from_edit_mesh(obj.data) 171 | for i in reversed(range(len(bm.loops.layers.uv))): 172 | bm.loops.layers.uv.remove(bm.loops.layers.uv[i]) 173 | bmesh.update_edit_mesh(obj.data) 174 | 175 | bpy.ops.object.mode_set(mode=mode_orig) 176 | 177 | 178 | def find_texture_nodes_from_material(mtrl): 179 | nodes = [] 180 | if not mtrl.node_tree: 181 | return nodes 182 | for node in mtrl.node_tree.nodes: 183 | tex_node_types = [ 184 | 'TEX_ENVIRONMENT', 185 | 'TEX_IMAGE', 186 | ] 187 | if node.type not in tex_node_types: 188 | continue 189 | if not node.image: 190 | continue 191 | nodes.append(node) 192 | 193 | return nodes 194 | 195 | 196 | def find_texture_nodes(obj): 197 | nodes = [] 198 | for slot in obj.material_slots: 199 | if not slot.material: 200 | continue 201 | nodes.extend(find_texture_nodes_from_material(slot.material)) 202 | 203 | return nodes 204 | 205 | 206 | def assign_new_image(obj, image_name): 207 | bpy.ops.image.new(name=image_name) 208 | img = bpy.data.images[image_name] 209 | if compat.check_version(2, 80, 0) < 0: 210 | bm = bmesh.from_edit_mesh(obj.data) 211 | tex_layer = bm.faces.layers.tex.verify() 212 | for f in bm.faces: 213 | f[tex_layer].image = img 214 | bmesh.update_edit_mesh(obj.data) 215 | else: 216 | node_tree = obj.active_material.node_tree 217 | output_node = node_tree.nodes["Material Output"] 218 | 219 | nodes = find_texture_nodes_from_material(obj.active_material) 220 | if len(nodes) >= 1: 221 | tex_node = nodes[0] 222 | else: 223 | tex_node = node_tree.nodes.new(type="ShaderNodeTexImage") 224 | tex_node.image = img 225 | node_tree.links.new(output_node.inputs["Surface"], tex_node.outputs["Color"]) 226 | 227 | 228 | def duplicate_object_without_uv(): 229 | bpy.ops.object.duplicate() 230 | delete_all_uv_maps(compat.get_active_object(bpy.context)) 231 | 232 | 233 | class TestBase(unittest.TestCase): 234 | 235 | package_name = "magic_uv" 236 | module_name = "" 237 | submodule_name = None 238 | idname = [] 239 | 240 | @classmethod 241 | def setUpClass(cls): 242 | if cls.submodule_name is not None: 243 | print("\n======== Module Test: {}.{} ({}) ========" 244 | .format(cls.package_name, cls.module_name, cls.submodule_name)) 245 | else: 246 | print("\n======== Module Test: {}.{} ========" 247 | .format(cls.package_name, cls.module_name)) 248 | try: 249 | bpy.ops.wm.read_factory_settings() 250 | check_addon_enabled(cls.package_name) 251 | for op in cls.idname: 252 | if op[0] == 'OPERATOR': 253 | assert operator_exists(op[1]), \ 254 | "Operator {} does not exist".format(op[1]) 255 | elif op[0] == 'MENU': 256 | assert menu_exists(op[1]), \ 257 | "Menu {} does not exist".format(op[1]) 258 | bpy.ops.wm.save_as_mainfile(filepath=TESTEE_FILE) 259 | except AssertionError as e: 260 | print(e) 261 | sys.exit(1) 262 | 263 | @classmethod 264 | def tearDownClass(cls): 265 | try: 266 | check_addon_disabled(cls.package_name) 267 | for op in cls.idname: 268 | if op[0] == 'OPERATOR': 269 | assert not operator_exists(op[1]), "Operator {} exists".format(op[1]) 270 | elif op[0] == 'MENU': 271 | assert not menu_exists(op[1]), "Menu %s exists".format(op[1]) 272 | except AssertionError as e: 273 | print(e) 274 | sys.exit(1) 275 | 276 | def setUp(self): 277 | bpy.ops.wm.open_mainfile(filepath=TESTEE_FILE) 278 | delete_all_uv_maps(compat.get_active_object(bpy.context)) 279 | self.setUpEachMethod() 280 | 281 | def setUpEachMethod(self): 282 | pass 283 | 284 | def tearDown(self): 285 | self.tearDownEachMethod() 286 | 287 | def tearDownEachMethod(self): 288 | pass 289 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/compatibility.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bgl 3 | import blf 4 | 5 | 6 | def check_version(major, minor, _): 7 | """ 8 | Check blender version 9 | """ 10 | 11 | if bpy.app.version[0] == major and bpy.app.version[1] == minor: 12 | return 0 13 | if bpy.app.version[0] > major: 14 | return 1 15 | if bpy.app.version[1] > minor: 16 | return 1 17 | return -1 18 | 19 | 20 | def make_annotations(cls): 21 | if check_version(2, 80, 0) < 0: 22 | return cls 23 | 24 | # make annotation from attributes 25 | props = {k: v for k, v in cls.__dict__.items() if isinstance(v, tuple)} 26 | if props: 27 | if '__annotations__' not in cls.__dict__: 28 | setattr(cls, '__annotations__', {}) 29 | annotations = cls.__dict__['__annotations__'] 30 | for k, v in props.items(): 31 | annotations[k] = v 32 | delattr(cls, k) 33 | 34 | return cls 35 | 36 | 37 | class ChangeRegionType: 38 | def __init__(self, *_, **kwargs): 39 | self.region_type = kwargs.get('region_type', False) 40 | 41 | def __call__(self, cls): 42 | if check_version(2, 80, 0) >= 0: 43 | return cls 44 | 45 | cls.bl_region_type = self.region_type 46 | 47 | return cls 48 | 49 | 50 | def matmul(m1, m2): 51 | if check_version(2, 80, 0) < 0: 52 | return m1 * m2 53 | 54 | return m1 @ m2 55 | 56 | 57 | def layout_split(layout, factor=0.0, align=False): 58 | if check_version(2, 80, 0) < 0: 59 | return layout.split(percentage=factor, align=align) 60 | 61 | return layout.split(factor=factor, align=align) 62 | 63 | 64 | def get_user_preferences(context): 65 | if hasattr(context, "user_preferences"): 66 | return context.user_preferences 67 | 68 | return context.preferences 69 | 70 | 71 | def get_object_select(obj): 72 | if check_version(2, 80, 0) < 0: 73 | return obj.select 74 | 75 | return obj.select_get() 76 | 77 | 78 | def set_active_object(obj): 79 | if check_version(2, 80, 0) < 0: 80 | bpy.context.scene.objects.active = obj 81 | else: 82 | bpy.context.view_layer.objects.active = obj 83 | 84 | 85 | def get_active_object(context): 86 | return context.active_object 87 | 88 | 89 | def object_has_uv_layers(obj): 90 | if check_version(2, 80, 0) < 0: 91 | return hasattr(obj.data, "uv_textures") 92 | else: 93 | return hasattr(obj.data, "uv_layers") 94 | 95 | 96 | def get_object_uv_layers(obj): 97 | if check_version(2, 80, 0) < 0: 98 | return obj.data.uv_textures 99 | else: 100 | return obj.data.uv_layers 101 | 102 | 103 | def icon(icon): 104 | if icon == 'IMAGE': 105 | if check_version(2, 80, 0) < 0: 106 | return 'IMAGE_COL' 107 | 108 | return icon 109 | 110 | 111 | def set_blf_font_color(font_id, r, g, b, a): 112 | if check_version(2, 80, 0) >= 0: 113 | blf.color(font_id, r, g, b, a) 114 | else: 115 | bgl.glColor4f(r, g, b, a) 116 | 117 | 118 | def set_blf_blur(font_id, radius): 119 | if check_version(2, 80, 0) < 0: 120 | blf.blur(font_id, radius) 121 | 122 | 123 | def get_all_space_types(): 124 | if check_version(2, 80, 0) >= 0: 125 | return { 126 | 'CLIP_EDITOR': bpy.types.SpaceClipEditor, 127 | 'CONSOLE': bpy.types.SpaceConsole, 128 | 'DOPESHEET_EDITOR': bpy.types.SpaceDopeSheetEditor, 129 | 'FILE_BROWSER': bpy.types.SpaceFileBrowser, 130 | 'GRAPH_EDITOR': bpy.types.SpaceGraphEditor, 131 | 'IMAGE_EDITOR': bpy.types.SpaceImageEditor, 132 | 'INFO': bpy.types.SpaceInfo, 133 | 'NLA_EDITOR': bpy.types.SpaceNLA, 134 | 'NODE_EDITOR': bpy.types.SpaceNodeEditor, 135 | 'OUTLINER': bpy.types.SpaceOutliner, 136 | 'PROPERTIES': bpy.types.SpaceProperties, 137 | 'SEQUENCE_EDITOR': bpy.types.SpaceSequenceEditor, 138 | 'TEXT_EDITOR': bpy.types.SpaceTextEditor, 139 | 'USER_PREFERENCES': bpy.types.SpacePreferences, 140 | 'VIEW_3D': bpy.types.SpaceView3D, 141 | } 142 | else: 143 | return { 144 | 'VIEW_3D': bpy.types.SpaceView3D, 145 | 'TIMELINE': bpy.types.SpaceTimeline, 146 | 'GRAPH_EDITOR': bpy.types.SpaceGraphEditor, 147 | 'DOPESHEET_EDITOR': bpy.types.SpaceDopeSheetEditor, 148 | 'NLA_EDITOR': bpy.types.SpaceNLA, 149 | 'IMAGE_EDITOR': bpy.types.SpaceImageEditor, 150 | 'SEQUENCE_EDITOR': bpy.types.SpaceSequenceEditor, 151 | 'CLIP_EDITOR': bpy.types.SpaceClipEditor, 152 | 'TEXT_EDITOR': bpy.types.SpaceTextEditor, 153 | 'NODE_EDITOR': bpy.types.SpaceNodeEditor, 154 | 'LOGIC_EDITOR': bpy.types.SpaceLogicEditor, 155 | 'PROPERTIES': bpy.types.SpaceProperties, 156 | 'OUTLINER': bpy.types.SpaceOutliner, 157 | 'USER_PREFERENCES': bpy.types.SpaceUserPreferences, 158 | 'INFO': bpy.types.SpaceInfo, 159 | 'FILE_BROWSER': bpy.types.SpaceFileBrowser, 160 | 'CONSOLE': bpy.types.SpaceConsole, 161 | } 162 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/copy_paste_uv_object_test.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | 4 | from . import common 5 | from . import compatibility as compat 6 | 7 | 8 | class TestCopyPasteUVObject(common.TestBase): 9 | module_name = "copy_paste_uv_object" 10 | idname = [ 11 | # Copy/Paste UV Coordinates (Among same objects) 12 | ('MENU', 'MUV_MT_CopyPasteUVObject_CopyUV'), 13 | ('OPERATOR', 'object.muv_copy_paste_uv_object_copy_uv'), 14 | ('MENU', 'MUV_MT_CopyPasteUVObject_PasteUV'), 15 | ('OPERATOR', 'object.muv_copy_paste_uv_object_paste_uv'), 16 | ] 17 | 18 | def setUpEachMethod(self): 19 | src_obj_name = "Cube" 20 | self.dest_obj_name = "Cube.001" 21 | self.uv_map = "UVMap" 22 | 23 | common.select_object_only(src_obj_name) 24 | common.duplicate_object_without_uv() 25 | common.select_object_only(src_obj_name) 26 | compat.set_active_object(bpy.data.objects[src_obj_name]) 27 | bpy.ops.object.mode_set(mode='OBJECT') 28 | 29 | def test_paste_uv_ng_not_copy_first(self): 30 | # Warning: Need copy UV at first 31 | print("[TEST] (NG) Not copy first") 32 | result = bpy.ops.object.muv_copy_paste_uv_object_paste_uv() 33 | self.assertSetEqual(result, {'CANCELLED'}) 34 | 35 | def test_copy_uv_ng_no_uv(self): 36 | # Warning: Object must have more than one UV map 37 | print("[TEST] (NG) No UV") 38 | result = bpy.ops.object.muv_copy_paste_uv_object_copy_uv() 39 | self.assertSetEqual(result, {'CANCELLED'}) 40 | 41 | def test_copy_uv_ok_default(self): 42 | print("[TEST] (OK) Default") 43 | bpy.ops.mesh.uv_texture_add() 44 | result = bpy.ops.object.muv_copy_paste_uv_object_copy_uv() 45 | self.assertSetEqual(result, {'FINISHED'}) 46 | 47 | def test_copy_uv_ok_all(self): 48 | print("[TEST] (OK) All") 49 | bpy.ops.mesh.uv_texture_add() 50 | result = bpy.ops.object.muv_copy_paste_uv_object_copy_uv(uv_map="__all") 51 | self.assertSetEqual(result, {'FINISHED'}) 52 | 53 | def test_copy_uv_ok_user_specified(self): 54 | print("[TEST] (OK) User specified UV") 55 | bpy.ops.mesh.uv_texture_add() 56 | result = bpy.ops.object.muv_copy_paste_uv_object_copy_uv(uv_map=self.uv_map) 57 | self.assertSetEqual(result, {'FINISHED'}) 58 | 59 | def __prepare_paste_uv_test(self): 60 | # Copy UV 61 | bpy.ops.mesh.uv_texture_add() 62 | result = bpy.ops.object.muv_copy_paste_uv_object_copy_uv() 63 | self.assertSetEqual(result, {'FINISHED'}) 64 | 65 | common.select_object_only(self.dest_obj_name) 66 | compat.set_active_object(bpy.data.objects[self.dest_obj_name]) 67 | self.active_obj = compat.get_active_object(bpy.context) 68 | bpy.ops.object.mode_set(mode='OBJECT') 69 | 70 | def test_paste_uv_ng_no_uv(self): 71 | # Warning: Object must have more than one UV map 72 | print("[TEST] (NG) No UV") 73 | self.__prepare_paste_uv_test() 74 | result = bpy.ops.object.muv_copy_paste_uv_object_paste_uv() 75 | self.assertSetEqual(result, {'CANCELLED'}) 76 | 77 | def test_paste_uv_ng_not_same_number_of_selected_face(self): 78 | # Warning: Number of faces is different from copied (src:6, dest:12) 79 | print("[TEST] (NG) Number of selected face is not same") 80 | self.__prepare_paste_uv_test() 81 | bpy.ops.mesh.uv_texture_add() 82 | bpy.ops.object.mode_set(mode='EDIT') 83 | bpy.ops.mesh.select_all(action='SELECT') 84 | bpy.ops.mesh.quads_convert_to_tris() 85 | bpy.ops.object.mode_set(mode='OBJECT') 86 | result = bpy.ops.object.muv_copy_paste_uv_object_paste_uv() 87 | self.assertSetEqual(result, {'CANCELLED'}) 88 | bpy.ops.object.mode_set(mode='EDIT') 89 | bpy.ops.mesh.select_all(action='SELECT') 90 | bpy.ops.mesh.tris_convert_to_quads() 91 | bpy.ops.object.mode_set(mode='OBJECT') 92 | 93 | def test_paste_uv_ok_default(self): 94 | print("[TEST] (OK) Default") 95 | self.__prepare_paste_uv_test() 96 | bpy.ops.mesh.uv_texture_add() 97 | result = bpy.ops.object.muv_copy_paste_uv_object_paste_uv() 98 | self.assertSetEqual(result, {'FINISHED'}) 99 | 100 | def test_paste_uv_ok_all(self): 101 | print("[TEST] (OK) Default") 102 | self.__prepare_paste_uv_test() 103 | bpy.ops.mesh.uv_texture_add() 104 | result = bpy.ops.object.muv_copy_paste_uv_object_paste_uv(uv_map="__all") 105 | self.assertSetEqual(result, {'FINISHED'}) 106 | 107 | def test_paste_uv_ok_new(self): 108 | print("[TEST] (OK) Default") 109 | self.__prepare_paste_uv_test() 110 | bpy.ops.mesh.uv_texture_add() 111 | result = bpy.ops.object.muv_copy_paste_uv_object_paste_uv(uv_map="__new") 112 | self.assertSetEqual(result, {'FINISHED'}) 113 | 114 | def test_paste_uv_ok_user_specified(self): 115 | print("[TEST] (OK) User specified UV") 116 | self.__prepare_paste_uv_test() 117 | bpy.ops.mesh.uv_texture_add() 118 | result = bpy.ops.object.muv_copy_paste_uv_object_paste_uv( 119 | uv_map=self.uv_map, 120 | copy_seams=False 121 | ) 122 | self.assertSetEqual(result, {'FINISHED'}) 123 | 124 | def test_paste_uv_ng_not_same_selected_face_size(self): 125 | # Warning: Some faces are different size 126 | print("[TEST] (NG) Selected face size is not same") 127 | self.__prepare_paste_uv_test() 128 | bpy.ops.mesh.uv_texture_add() 129 | bpy.ops.object.mode_set(mode='EDIT') 130 | bpy.ops.mesh.select_all(action='SELECT') 131 | bpy.ops.mesh.quads_convert_to_tris() 132 | bm = bmesh.from_edit_mesh(self.active_obj.data) 133 | bpy.ops.mesh.select_all(action='DESELECT') 134 | for i, f in enumerate(bm.faces): 135 | if i <= 5: 136 | f.select = True 137 | else: 138 | f.select = False 139 | bpy.ops.mesh.delete(type='FACE') 140 | bpy.ops.object.mode_set(mode='OBJECT') 141 | result = bpy.ops.object.muv_copy_paste_uv_object_paste_uv() 142 | self.assertSetEqual(result, {'CANCELLED'}) 143 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/copy_paste_uv_uvedit_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import bpy 4 | 5 | from . import common 6 | from . import compatibility as compat 7 | 8 | 9 | class TestCopyPasteUVUVEdit(common.TestBase): 10 | module_name = "copy_paste_uv_uvedit" 11 | idname = [ 12 | # Copy/Paste UV Coordinates on UV/Image Editor 13 | ('OPERATOR', 'uv.muv_copy_paste_uv_uvedit_copy_uv'), 14 | ('OPERATOR', 'uv.muv_copy_paste_uv_uvedit_paste_uv'), 15 | ('OPERATOR', 'uv.muv_copy_paste_uv_uvedit_copy_uv_island'), 16 | ('OPERATOR', 'uv.muv_copy_paste_uv_uvedit_paste_uv_island'), 17 | ] 18 | 19 | def setUpEachMethod(self): 20 | src_obj_name = "Cube" 21 | self.dest_obj_name = "Cube.001" 22 | 23 | common.select_object_only(src_obj_name) 24 | common.duplicate_object_without_uv() 25 | compat.set_active_object(bpy.data.objects[src_obj_name]) 26 | bpy.ops.object.mode_set(mode='EDIT') 27 | 28 | bpy.context.scene.tool_settings.use_uv_select_sync = True 29 | 30 | def tearDownMethod(self): 31 | bpy.context.scene.tool_settings.use_uv_select_sync = False 32 | 33 | def test_copy_uv_ok(self): 34 | print("[TEST] (OK)") 35 | bpy.ops.mesh.uv_texture_add() 36 | bpy.ops.mesh.select_all(action='SELECT') 37 | result = bpy.ops.uv.muv_copy_paste_uv_uvedit_copy_uv() 38 | self.assertSetEqual(result, {'FINISHED'}) 39 | 40 | def test_copy_uv_island_ok(self): 41 | print("[TEST] (OK) Copy UV Island") 42 | bpy.ops.mesh.uv_texture_add() 43 | bpy.ops.mesh.select_all(action='SELECT') 44 | result = bpy.ops.uv.muv_copy_paste_uv_uvedit_copy_uv_island() 45 | self.assertSetEqual(result, {'FINISHED'}) 46 | 47 | @unittest.skipIf(compat.check_version(2, 80, 0) < 0, 48 | "Not supported in <2.80") 49 | def test_ok_copy_uv_island_multiple_objects(self): 50 | print("[TEST] (OK) Copy UV Island Multiple Objects") 51 | 52 | # Duplicate object. 53 | bpy.ops.object.mode_set(mode='OBJECT') 54 | obj_names = ["Cube", "Cube.001"] 55 | common.select_object_only(obj_names[0]) 56 | bpy.ops.object.duplicate() 57 | 58 | for name in obj_names: 59 | bpy.ops.object.mode_set(mode='OBJECT') 60 | common.select_object_only(name) 61 | compat.set_active_object(bpy.data.objects[name]) 62 | bpy.ops.object.mode_set(mode='EDIT') 63 | bpy.ops.mesh.uv_texture_add() 64 | bpy.ops.mesh.select_all(action='SELECT') 65 | 66 | # Select two objects. 67 | bpy.ops.object.mode_set(mode='OBJECT') 68 | compat.set_active_object(bpy.data.objects[obj_names[0]]) 69 | common.select_objects_only(obj_names) 70 | bpy.ops.object.mode_set(mode='EDIT') 71 | 72 | result = bpy.ops.uv.muv_copy_paste_uv_uvedit_copy_uv_island() 73 | self.assertSetEqual(result, {'FINISHED'}) 74 | 75 | def __prepare_paste_uv_test(self): 76 | # Copy UV 77 | bpy.ops.mesh.uv_texture_add() 78 | bpy.ops.mesh.select_all(action='SELECT') 79 | result = bpy.ops.uv.muv_copy_paste_uv_uvedit_copy_uv() 80 | self.assertSetEqual(result, {'FINISHED'}) 81 | 82 | bpy.ops.object.mode_set(mode='OBJECT') 83 | common.select_object_only(self.dest_obj_name) 84 | compat.set_active_object(bpy.data.objects[self.dest_obj_name]) 85 | self.active_obj = compat.get_active_object(bpy.context) 86 | bpy.ops.object.mode_set(mode='EDIT') 87 | 88 | def __prepare_paste_uv_island_test(self): 89 | # Copy UV Island 90 | bpy.ops.mesh.select_all(action='SELECT') 91 | # bpy.ops.mesh.uv_texture_add() fails for this test. 92 | # The reason is not clear, but this failure can be avoided 93 | # by using bpy.ops.uv.smart_project(). 94 | bpy.ops.uv.smart_project() 95 | result = bpy.ops.uv.muv_copy_paste_uv_uvedit_copy_uv_island() 96 | self.assertSetEqual(result, {'FINISHED'}) 97 | 98 | def test_paste_uv_ok(self): 99 | print("[TEST] (OK)") 100 | self.__prepare_paste_uv_test() 101 | bpy.ops.mesh.uv_texture_add() 102 | bpy.ops.mesh.select_all(action='SELECT') 103 | result = bpy.ops.uv.muv_copy_paste_uv_uvedit_paste_uv() 104 | self.assertSetEqual(result, {'FINISHED'}) 105 | 106 | def test_paste_uv_island_ok(self): 107 | print("[TEST] (OK) Paste UV Island") 108 | self.__prepare_paste_uv_island_test() 109 | result = bpy.ops.uv.muv_copy_paste_uv_uvedit_paste_uv_island() 110 | self.assertSetEqual(result, {'FINISHED'}) 111 | 112 | def test_paste_uv_island_ok_with_unique_target(self): 113 | print("[TEST] (OK) Paste UV Island with Unique Target") 114 | self.__prepare_paste_uv_island_test() 115 | result = bpy.ops.uv.muv_copy_paste_uv_uvedit_paste_uv_island( 116 | unique_target=True 117 | ) 118 | self.assertSetEqual(result, {'FINISHED'}) 119 | 120 | @unittest.skipIf(compat.check_version(2, 80, 0) < 0, 121 | "Not supported in <2.80") 122 | def test_ok_paste_uv_island_multiple_objects(self): 123 | print("[TEST] (OK) Paste UV Island Multiple Objects") 124 | 125 | # Duplicate object. 126 | bpy.ops.object.mode_set(mode='OBJECT') 127 | obj_names = ["Cube", "Cube.001"] 128 | common.select_object_only(obj_names[0]) 129 | bpy.ops.object.duplicate() 130 | 131 | for name in obj_names: 132 | bpy.ops.object.mode_set(mode='OBJECT') 133 | common.select_object_only(name) 134 | compat.set_active_object(bpy.data.objects[name]) 135 | bpy.ops.object.mode_set(mode='EDIT') 136 | bpy.ops.mesh.uv_texture_add() 137 | bpy.ops.mesh.select_all(action='SELECT') 138 | 139 | # Select two objects. 140 | bpy.ops.object.mode_set(mode='OBJECT') 141 | compat.set_active_object(bpy.data.objects[obj_names[0]]) 142 | common.select_objects_only(obj_names) 143 | bpy.ops.object.mode_set(mode='EDIT') 144 | 145 | result = bpy.ops.uv.muv_copy_paste_uv_uvedit_copy_uv_island() 146 | self.assertSetEqual(result, {'FINISHED'}) 147 | result = bpy.ops.uv.muv_copy_paste_uv_uvedit_paste_uv_island( 148 | unique_target=True 149 | ) 150 | self.assertSetEqual(result, {'FINISHED'}) 151 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/flip_rotate_uv_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import bpy 4 | 5 | from . import common 6 | from . import compatibility as compat 7 | 8 | 9 | class TestFlipRotateUV(common.TestBase): 10 | module_name = "flip_rotate_uv" 11 | idname = [ 12 | # Flip/Rotate UVs 13 | ('OPERATOR', 'uv.muv_flip_rotate_uv'), 14 | ] 15 | 16 | def setUpEachMethod(self): 17 | obj_name = "Cube" 18 | 19 | common.select_object_only(obj_name) 20 | compat.set_active_object(bpy.data.objects[obj_name]) 21 | bpy.ops.object.mode_set(mode='EDIT') 22 | 23 | def test_ng_no_uv(self): 24 | # Warning: Object must have more than one UV map 25 | print("[TEST] (NG) No UV") 26 | result = bpy.ops.uv.muv_flip_rotate_uv() 27 | self.assertSetEqual(result, {'CANCELLED'}) 28 | 29 | def test_ng_no_selected_faces(self): 30 | # Warning: No faces are selected 31 | print("[TEST] (NG) No selected faces") 32 | bpy.ops.mesh.uv_texture_add() 33 | bpy.ops.mesh.select_all(action='DESELECT') 34 | result = bpy.ops.uv.muv_flip_rotate_uv() 35 | self.assertSetEqual(result, {'CANCELLED'}) 36 | 37 | def test_ok_default(self): 38 | print("[TEST] (OK) Default") 39 | bpy.ops.mesh.uv_texture_add() 40 | bpy.ops.mesh.select_all(action='SELECT') 41 | result = bpy.ops.uv.muv_flip_rotate_uv() 42 | self.assertSetEqual(result, {'FINISHED'}) 43 | 44 | def test_ok_user_specified(self): 45 | print("[TEST] (OK) User specified") 46 | bpy.ops.mesh.uv_texture_add() 47 | bpy.ops.mesh.select_all(action='SELECT') 48 | result = bpy.ops.uv.muv_flip_rotate_uv(flip=True, rotate=5, seams=False) 49 | self.assertSetEqual(result, {'FINISHED'}) 50 | 51 | @unittest.skipIf(compat.check_version(2, 80, 0) < 0, 52 | "Not supported in <2.80") 53 | def test_ok_multiple_objects(self): 54 | print("[TEST] (OK) Multiple Objects") 55 | 56 | # Duplicate object. 57 | bpy.ops.object.mode_set(mode='OBJECT') 58 | obj_names = ["Cube", "Cube.001"] 59 | common.select_object_only(obj_names[0]) 60 | common.duplicate_object_without_uv() 61 | 62 | for name in obj_names: 63 | bpy.ops.object.mode_set(mode='OBJECT') 64 | common.select_object_only(name) 65 | compat.set_active_object(bpy.data.objects[name]) 66 | bpy.ops.object.mode_set(mode='EDIT') 67 | bpy.ops.mesh.uv_texture_add() 68 | bpy.ops.mesh.select_all(action='SELECT') 69 | 70 | # Select two objects. 71 | bpy.ops.object.mode_set(mode='OBJECT') 72 | compat.set_active_object(bpy.data.objects[obj_names[0]]) 73 | common.select_objects_only(obj_names) 74 | bpy.ops.object.mode_set(mode='EDIT') 75 | 76 | result = bpy.ops.uv.muv_flip_rotate_uv(flip=True, rotate=5, seams=False) 77 | self.assertSetEqual(result, {'FINISHED'}) 78 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/mirror_uv_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import bpy 4 | 5 | from . import common 6 | from . import compatibility as compat 7 | 8 | 9 | class TestMirrorUV(common.TestBase): 10 | module_name = "mirror_uv" 11 | idname = [ 12 | # Mirror UV 13 | ('OPERATOR', 'uv.muv_mirror_uv'), 14 | ] 15 | 16 | def setUpEachMethod(self): 17 | obj_name = "Cube" 18 | 19 | common.select_object_only(obj_name) 20 | compat.set_active_object(bpy.data.objects[obj_name]) 21 | bpy.ops.object.mode_set(mode='EDIT') 22 | 23 | def test_ng_no_uv(self): 24 | # Warning: Object must have more than one UV map 25 | print("[TEST] (NG) No UV") 26 | bpy.ops.mesh.select_all(action='SELECT') 27 | result = bpy.ops.uv.muv_mirror_uv() 28 | self.assertSetEqual(result, {'CANCELLED'}) 29 | 30 | def test_ok_default(self): 31 | print("[TEST] (OK) Default") 32 | bpy.ops.mesh.select_all(action='SELECT') 33 | bpy.ops.mesh.uv_texture_add() 34 | result = bpy.ops.uv.muv_mirror_uv() 35 | self.assertSetEqual(result, {'FINISHED'}) 36 | 37 | def test_ok_user_specified_1(self): 38 | print("[TEST] (OK) User specified 1") 39 | bpy.ops.mesh.select_all(action='SELECT') 40 | bpy.ops.mesh.uv_texture_add() 41 | result = bpy.ops.uv.muv_mirror_uv(axis='Y', error=0.7, origin='GLOBAL') 42 | self.assertSetEqual(result, {'FINISHED'}) 43 | 44 | def test_ok_user_specified_2(self): 45 | print("[TEST] (OK) User specified 2") 46 | bpy.ops.mesh.select_all(action='SELECT') 47 | bpy.ops.mesh.uv_texture_add() 48 | result = bpy.ops.uv.muv_mirror_uv(axis='Y', error=19.4, origin='WORLD') 49 | self.assertSetEqual(result, {'FINISHED'}) 50 | 51 | @unittest.skipIf(compat.check_version(2, 80, 0) < 0, 52 | "Not supported in <2.80") 53 | def test_ok_multiple_objects(self): 54 | print("[TEST] (OK) Multiple Objects") 55 | 56 | # Duplicate object. 57 | bpy.ops.object.mode_set(mode='OBJECT') 58 | obj_names = ["Cube", "Cube.001"] 59 | common.select_object_only(obj_names[0]) 60 | common.duplicate_object_without_uv() 61 | 62 | for name in obj_names: 63 | bpy.ops.object.mode_set(mode='OBJECT') 64 | common.select_object_only(name) 65 | compat.set_active_object(bpy.data.objects[name]) 66 | bpy.ops.object.mode_set(mode='EDIT') 67 | bpy.ops.mesh.uv_texture_add() 68 | bpy.ops.mesh.select_all(action='SELECT') 69 | 70 | # Select two objects. 71 | bpy.ops.object.mode_set(mode='OBJECT') 72 | compat.set_active_object(bpy.data.objects[obj_names[0]]) 73 | common.select_objects_only(obj_names) 74 | bpy.ops.object.mode_set(mode='EDIT') 75 | 76 | result = bpy.ops.uv.muv_mirror_uv(axis='Y', error=19.4, origin='GLOBAL') 77 | self.assertSetEqual(result, {'FINISHED'}) 78 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/move_uv_test.py: -------------------------------------------------------------------------------- 1 | from . import common 2 | 3 | 4 | class TestMoveUV(common.TestBase): 5 | module_name = "move_uv" 6 | idname = [ 7 | # Move UV 8 | ('OPERATOR', "uv.muv_move_uv"), 9 | ] 10 | 11 | # modal operator can not invoke directly from cmdline 12 | def test_nothing(self): 13 | pass 14 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/pack_uv_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import bpy 4 | 5 | from . import common 6 | from . import compatibility as compat 7 | 8 | 9 | class TestPackUV(common.TestBase): 10 | module_name = "pack_uv" 11 | idname = [ 12 | # Pack UV (with same UV island packing) 13 | ('OPERATOR', 'uv.muv_pack_uv'), 14 | ] 15 | 16 | def setUpEachMethod(self): 17 | obj_name = "Cube" 18 | 19 | common.select_object_only(obj_name) 20 | compat.set_active_object(bpy.data.objects[obj_name]) 21 | bpy.ops.object.mode_set(mode='EDIT') 22 | 23 | def test_ng_no_uv(self): 24 | # Warning: Object must have more than one UV map 25 | print("[TEST] (NG) No UV") 26 | bpy.ops.mesh.select_all(action='SELECT') 27 | result = bpy.ops.uv.muv_pack_uv() 28 | self.assertSetEqual(result, {'CANCELLED'}) 29 | 30 | def test_ok_default(self): 31 | print("[TEST] (OK) Default") 32 | bpy.ops.mesh.select_all(action='SELECT') 33 | bpy.ops.mesh.uv_texture_add() 34 | if compat.check_version(2, 80, 0) < 0: 35 | # If <2.80, bpy.ops.uv.muv_pack_uv() will be failed due to the 36 | # mismatch among the islands without bpy.ops.uv.smart_project(). 37 | bpy.ops.uv.smart_project() 38 | result = bpy.ops.uv.muv_pack_uv() 39 | self.assertSetEqual(result, {'FINISHED'}) 40 | 41 | def test_ok_without_accurate_island_copy(self): 42 | print("[TEST] (OK) Default") 43 | bpy.ops.mesh.select_all(action='SELECT') 44 | bpy.ops.mesh.uv_texture_add() 45 | result = bpy.ops.uv.muv_pack_uv(accurate_island_copy=False) 46 | self.assertSetEqual(result, {'FINISHED'}) 47 | 48 | def test_ok_user_specified(self): 49 | print("[TEST] (OK) User specified") 50 | bpy.ops.mesh.select_all(action='SELECT') 51 | bpy.ops.mesh.uv_texture_add() 52 | result = bpy.ops.uv.muv_pack_uv( 53 | rotate=True, 54 | margin=0.03, 55 | allowable_center_deviation=(0.02, 0.05), 56 | allowable_size_deviation=(0.003, 0.0004), 57 | accurate_island_copy=True, 58 | stride=(1.0, -1.0), 59 | apply_pack_uv=False, 60 | ) 61 | self.assertSetEqual(result, {'FINISHED'}) 62 | 63 | @unittest.skipIf(compat.check_version(2, 80, 0) < 0, 64 | "Not supported in <2.80") 65 | def test_ok_multiple_objects(self): 66 | print("[TEST] (OK) Multiple Objects") 67 | 68 | # Duplicate object. 69 | bpy.ops.object.mode_set(mode='OBJECT') 70 | obj_names = ["Cube", "Cube.001"] 71 | common.select_object_only(obj_names[0]) 72 | common.duplicate_object_without_uv() 73 | 74 | for name in obj_names: 75 | bpy.ops.object.mode_set(mode='OBJECT') 76 | common.select_object_only(name) 77 | compat.set_active_object(bpy.data.objects[name]) 78 | bpy.ops.object.mode_set(mode='EDIT') 79 | bpy.ops.mesh.uv_texture_add() 80 | bpy.ops.mesh.select_all(action='SELECT') 81 | 82 | # Select two objects. 83 | bpy.ops.object.mode_set(mode='OBJECT') 84 | compat.set_active_object(bpy.data.objects[obj_names[0]]) 85 | common.select_objects_only(obj_names) 86 | bpy.ops.object.mode_set(mode='EDIT') 87 | 88 | result = bpy.ops.uv.muv_pack_uv( 89 | rotate=True, 90 | margin=0.03, 91 | allowable_center_deviation=(0.02, 0.05), 92 | allowable_size_deviation=(0.003, 0.0004), 93 | accurate_island_copy=True, 94 | stride=(1.0, -1.0), 95 | apply_pack_uv=False, 96 | ) 97 | self.assertSetEqual(result, {'FINISHED'}) 98 | 99 | @unittest.skipIf(compat.check_version(2, 80, 0) < 0, 100 | "Not supported in <2.80") 101 | def test_ok_multiple_objects_without_accurate_island_copy(self): 102 | print("[TEST] (OK) Multiple Objects") 103 | 104 | # Duplicate object. 105 | bpy.ops.object.mode_set(mode='OBJECT') 106 | obj_names = ["Cube", "Cube.001"] 107 | common.select_object_only(obj_names[0]) 108 | common.duplicate_object_without_uv() 109 | 110 | for name in obj_names: 111 | bpy.ops.object.mode_set(mode='OBJECT') 112 | common.select_object_only(name) 113 | compat.set_active_object(bpy.data.objects[name]) 114 | bpy.ops.object.mode_set(mode='EDIT') 115 | bpy.ops.mesh.uv_texture_add() 116 | bpy.ops.mesh.select_all(action='SELECT') 117 | 118 | # Select two objects. 119 | bpy.ops.object.mode_set(mode='OBJECT') 120 | compat.set_active_object(bpy.data.objects[obj_names[0]]) 121 | common.select_objects_only(obj_names) 122 | bpy.ops.object.mode_set(mode='EDIT') 123 | 124 | result = bpy.ops.uv.muv_pack_uv( 125 | rotate=True, 126 | margin=0.03, 127 | allowable_center_deviation=(0.02, 0.05), 128 | allowable_size_deviation=(0.003, 0.0004), 129 | accurate_island_copy=False, 130 | stride=(1.0, -1.0), 131 | apply_pack_uv=False, 132 | ) 133 | self.assertSetEqual(result, {'FINISHED'}) 134 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/preserve_uv_aspect_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import bpy 4 | import bmesh 5 | 6 | from . import common 7 | from . import compatibility as compat 8 | 9 | 10 | class TestPreserveUVAspect(common.TestBase): 11 | module_name = "preserve_uv_aspect" 12 | idname = [ 13 | # Preserve UV 14 | ('OPERATOR', 'uv.muv_preserve_uv_aspect'), 15 | ] 16 | 17 | def setUpEachMethod(self): 18 | obj_name = "Cube" 19 | self.dest_img_name = "Test" 20 | 21 | common.select_object_only(obj_name) 22 | compat.set_active_object(bpy.data.objects[obj_name]) 23 | bpy.ops.object.mode_set(mode='EDIT') 24 | 25 | def test_ng_no_uv(self): 26 | # Warning: Object must have more than one UV map 27 | print("[TEST] (NG) No UV") 28 | result = bpy.ops.uv.muv_preserve_uv_aspect(dest_img_name=self.dest_img_name) 29 | self.assertSetEqual(result, {'CANCELLED'}) 30 | 31 | def test_ok_default(self): 32 | print("[TEST] (OK) Default") 33 | active_obj = compat.get_active_object(bpy.context) 34 | common.assign_new_image(active_obj, self.dest_img_name) 35 | bpy.ops.mesh.uv_texture_add() 36 | result = bpy.ops.uv.muv_preserve_uv_aspect(dest_img_name=self.dest_img_name) 37 | self.assertSetEqual(result, {'FINISHED'}) 38 | 39 | def test_ok_user_specified(self): 40 | print("[TEST] (OK) user specified") 41 | active_obj = compat.get_active_object(bpy.context) 42 | common.assign_new_image(active_obj, self.dest_img_name) 43 | bpy.ops.mesh.uv_texture_add() 44 | result = bpy.ops.uv.muv_preserve_uv_aspect( 45 | dest_img_name=self.dest_img_name, 46 | origin='RIGHT_TOP' 47 | ) 48 | self.assertSetEqual(result, {'FINISHED'}) 49 | 50 | @unittest.skipIf(compat.check_version(2, 80, 0) < 0, 51 | "Not supported in <2.80") 52 | def test_ok_multiple_objects_same_images(self): 53 | print("[TEST] (OK) Multiple Objects (Same Images)") 54 | 55 | # Duplicate object. 56 | bpy.ops.object.mode_set(mode='OBJECT') 57 | obj_names = ["Cube", "Cube.001"] 58 | common.select_object_only(obj_names[0]) 59 | common.duplicate_object_without_uv() 60 | 61 | for name in obj_names: 62 | bpy.ops.object.mode_set(mode='OBJECT') 63 | common.select_object_only(name) 64 | compat.set_active_object(bpy.data.objects[name]) 65 | bpy.ops.object.mode_set(mode='EDIT') 66 | bpy.ops.mesh.uv_texture_add() 67 | common.assign_new_image(bpy.data.objects[name], self.dest_img_name) 68 | bpy.ops.mesh.select_all(action='SELECT') 69 | 70 | # Select two objects. 71 | bpy.ops.object.mode_set(mode='OBJECT') 72 | compat.set_active_object(bpy.data.objects[obj_names[0]]) 73 | common.select_objects_only(obj_names) 74 | bpy.ops.object.mode_set(mode='EDIT') 75 | 76 | result = bpy.ops.uv.muv_preserve_uv_aspect( 77 | dest_img_name=self.dest_img_name, 78 | origin='RIGHT_TOP' 79 | ) 80 | self.assertSetEqual(result, {'FINISHED'}) 81 | 82 | @unittest.skipIf(compat.check_version(2, 80, 0) < 0, 83 | "Not supported in <2.80") 84 | def test_ok_multiple_objects_different_images(self): 85 | print("[TEST] (OK) Multiple Objects (Different Images)") 86 | 87 | # Duplicate object. 88 | bpy.ops.object.mode_set(mode='OBJECT') 89 | obj_names = ["Cube", "Cube.001"] 90 | common.select_object_only(obj_names[0]) 91 | common.duplicate_object_without_uv() 92 | 93 | for name in obj_names: 94 | bpy.ops.object.mode_set(mode='OBJECT') 95 | common.select_object_only(name) 96 | compat.set_active_object(bpy.data.objects[name]) 97 | bpy.ops.object.mode_set(mode='EDIT') 98 | bpy.ops.mesh.uv_texture_add() 99 | common.assign_new_image(bpy.data.objects[name], "Img " + name) 100 | bpy.ops.mesh.select_all(action='SELECT') 101 | 102 | # Select two objects. 103 | bpy.ops.object.mode_set(mode='OBJECT') 104 | compat.set_active_object(bpy.data.objects[obj_names[0]]) 105 | common.select_objects_only(obj_names) 106 | bpy.ops.object.mode_set(mode='EDIT') 107 | 108 | result = bpy.ops.uv.muv_preserve_uv_aspect( 109 | dest_img_name="Img " + obj_names[0], 110 | origin='RIGHT_TOP' 111 | ) 112 | self.assertSetEqual(result, {'FINISHED'}) 113 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/smooth_uv_test.py: -------------------------------------------------------------------------------- 1 | from . import common 2 | 3 | 4 | class TestSmoothUV(common.TestBase): 5 | module_name = "smooth_uv" 6 | idname = [ 7 | # Smooth UV 8 | ('OPERATOR', 'uv.muv_smooth_uv'), 9 | ] 10 | 11 | # Smooth UV has a complicated condition to test 12 | def test_nothing(self): 13 | pass 14 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/texture_lock_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import bpy 4 | 5 | from . import common 6 | from . import compatibility as compat 7 | 8 | 9 | class TestTextureLock(common.TestBase): 10 | module_name = "texture_lock" 11 | submodule_name = "non_intr" 12 | idname = [ 13 | # Texture Lock 14 | ('OPERATOR', 'uv.muv_texture_lock_lock'), 15 | ('OPERATOR', 'uv.muv_texture_lock_unlock'), 16 | ] 17 | 18 | # can not test interactive mode 19 | def setUpEachMethod(self): 20 | obj_name = "Cube" 21 | 22 | common.select_object_only(obj_name) 23 | compat.set_active_object(bpy.data.objects[obj_name]) 24 | bpy.ops.object.mode_set(mode='EDIT') 25 | 26 | def test_lock_ng_no_uv(self): 27 | # Warning: Object must have more than one UV map 28 | print("[TEST] (NG) No UV (Lock)") 29 | bpy.ops.mesh.select_all(action='SELECT') 30 | result = bpy.ops.uv.muv_texture_lock_lock() 31 | self.assertSetEqual(result, {'CANCELLED'}) 32 | 33 | def test_unlock_ng_no_uv(self): 34 | # Warning: Object must have more than one UV map 35 | print("[TEST] (NG) No UV (Unlock)") 36 | bpy.ops.mesh.select_all(action='SELECT') 37 | result = bpy.ops.uv.muv_texture_lock_unlock() 38 | self.assertSetEqual(result, {'CANCELLED'}) 39 | 40 | def test_lock_ok_default(self): 41 | print("[TEST] (OK) Default") 42 | bpy.ops.mesh.uv_texture_add() 43 | bpy.ops.image.new(name='Test') 44 | result = bpy.ops.uv.muv_texture_lock_lock() 45 | self.assertSetEqual(result, {'FINISHED'}) 46 | 47 | result = bpy.ops.uv.muv_texture_lock_unlock() 48 | self.assertSetEqual(result, {'FINISHED'}) 49 | 50 | def test_lock_ok_with_connect(self): 51 | print("[TEST] (OK) With connect") 52 | bpy.ops.mesh.uv_texture_add() 53 | bpy.ops.image.new(name='Test') 54 | bpy.ops.uv.smart_project() # this needs because previous result corrupts UV and arise errors 55 | result = bpy.ops.uv.muv_texture_lock_lock() 56 | self.assertSetEqual(result, {'FINISHED'}) 57 | 58 | result = bpy.ops.uv.muv_texture_lock_unlock( 59 | connect=False 60 | ) 61 | self.assertSetEqual(result, {'FINISHED'}) 62 | 63 | @unittest.skipIf(compat.check_version(2, 80, 0) < 0, 64 | "Not supported in <2.80") 65 | def test_ok_multiple_objects(self): 66 | print("[TEST] (OK) Multiple Objects") 67 | 68 | # Duplicate object. 69 | bpy.ops.object.mode_set(mode='OBJECT') 70 | obj_names = ["Cube", "Cube.001"] 71 | common.select_object_only(obj_names[0]) 72 | common.duplicate_object_without_uv() 73 | 74 | for name in obj_names: 75 | bpy.ops.object.mode_set(mode='OBJECT') 76 | common.select_object_only(name) 77 | compat.set_active_object(bpy.data.objects[name]) 78 | bpy.ops.object.mode_set(mode='EDIT') 79 | bpy.ops.mesh.uv_texture_add() 80 | bpy.ops.image.new(name='Test') 81 | bpy.ops.uv.smart_project() # this needs because previous result corrupts UV and arise errors 82 | bpy.ops.mesh.select_all(action='SELECT') 83 | 84 | # Select two objects. 85 | bpy.ops.object.mode_set(mode='OBJECT') 86 | compat.set_active_object(bpy.data.objects[obj_names[0]]) 87 | common.select_objects_only(obj_names) 88 | bpy.ops.object.mode_set(mode='EDIT') 89 | 90 | result = bpy.ops.uv.muv_texture_lock_lock() 91 | self.assertSetEqual(result, {'FINISHED'}) 92 | 93 | result = bpy.ops.uv.muv_texture_lock_unlock( 94 | connect=False 95 | ) 96 | self.assertSetEqual(result, {'FINISHED'}) 97 | 98 | @unittest.skipIf(compat.check_version(2, 80, 0) < 0, 99 | "Not supported in <2.80") 100 | def test_ok_multiple_objects(self): 101 | print("[TEST] (NG) Different object list") 102 | 103 | # Duplicate object. 104 | bpy.ops.object.mode_set(mode='OBJECT') 105 | obj_names = ["Cube", "Cube.001"] 106 | common.select_object_only(obj_names[0]) 107 | common.duplicate_object_without_uv() 108 | 109 | for name in obj_names: 110 | bpy.ops.object.mode_set(mode='OBJECT') 111 | common.select_object_only(name) 112 | compat.set_active_object(bpy.data.objects[name]) 113 | bpy.ops.object.mode_set(mode='EDIT') 114 | bpy.ops.mesh.uv_texture_add() 115 | bpy.ops.image.new(name='Test') 116 | bpy.ops.uv.smart_project() # this needs because previous result corrupts UV and arise errors 117 | bpy.ops.mesh.select_all(action='SELECT') 118 | 119 | # Select two objects. 120 | bpy.ops.object.mode_set(mode='OBJECT') 121 | compat.set_active_object(bpy.data.objects[obj_names[0]]) 122 | common.select_objects_only(obj_names) 123 | bpy.ops.object.mode_set(mode='EDIT') 124 | 125 | result = bpy.ops.uv.muv_texture_lock_lock() 126 | self.assertSetEqual(result, {'FINISHED'}) 127 | 128 | # Select only one object. 129 | bpy.ops.object.mode_set(mode='OBJECT') 130 | compat.set_active_object(bpy.data.objects[obj_names[0]]) 131 | common.select_objects_only([obj_names[0]]) 132 | bpy.ops.object.mode_set(mode='EDIT') 133 | 134 | result = bpy.ops.uv.muv_texture_lock_unlock( 135 | connect=False 136 | ) 137 | self.assertSetEqual(result, {'CANCELLED'}) 138 | 139 | 140 | class TestTextureLockIntr(common.TestBase): 141 | module_name = "texture_lock" 142 | submodule_name = "intr" 143 | idname = [ 144 | # Texture Lock 145 | ('OPERATOR', 'uv.muv_texture_lock_intr'), 146 | ] 147 | 148 | # modal operator can not invoke directly from cmdline 149 | def test_nothing(self): 150 | pass 151 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/texture_projection_test.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from . import common 4 | from . import compatibility as compat 5 | 6 | 7 | class TestTextureProjection(common.TestBase): 8 | module_name = "texture_projection" 9 | idname = [ 10 | # Texture Lock 11 | ('OPERATOR', 'uv.muv_texture_projection'), 12 | ('OPERATOR', 'uv.muv_texture_projection_project'), 13 | ] 14 | 15 | # can not test interactive mode 16 | # - modal operator can not invoke directly from cmdline 17 | # can not test OK case 18 | # - context.region will be NoneType 19 | def setUpEachMethod(self): 20 | obj_name = "Cube" 21 | 22 | common.select_object_only(obj_name) 23 | compat.set_active_object(bpy.data.objects[obj_name]) 24 | bpy.ops.object.mode_set(mode='EDIT') 25 | 26 | def test_no_image(self): 27 | # Warning: No textures are selected 28 | print("[TEST] (NG) No Image") 29 | bpy.ops.mesh.select_all(action='SELECT') 30 | bpy.context.scene.muv_texture_projection_tex_image = 'None' 31 | result = bpy.ops.uv.muv_texture_projection_project() 32 | self.assertSetEqual(result, {'CANCELLED'}) 33 | 34 | def test_ng_no_uv(self): 35 | # Warning: Object must have more than one UV map 36 | print("[TEST] (NG) No UV") 37 | bpy.ops.image.new(name='Test') 38 | bpy.context.scene.muv_texture_projection_tex_image = 'Test' 39 | bpy.context.scene.muv_texture_projection_assign_uvmap = False 40 | result = bpy.ops.uv.muv_texture_projection_project() 41 | self.assertSetEqual(result, {'CANCELLED'}) 42 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/texture_wrap_test.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | 3 | from . import common 4 | from . import compatibility as compat 5 | 6 | 7 | class TestTextureWrap(common.TestBase): 8 | module_name = "texture_wrap" 9 | idname = [ 10 | # Texture Wrap 11 | ('OPERATOR', 'uv.muv_texture_wrap_refer'), 12 | ('OPERATOR', 'uv.muv_texture_wrap_set'), 13 | ] 14 | 15 | # "More than 1 vertex must be unshared" does rarely occur, 16 | # so skip this test 17 | def setUpEachMethod(self): 18 | obj_name = "Cube" 19 | 20 | common.select_object_only(obj_name) 21 | compat.set_active_object(bpy.data.objects[obj_name]) 22 | self.active_obj = compat.get_active_object(bpy.context) 23 | bpy.ops.object.mode_set(mode='EDIT') 24 | 25 | sc = bpy.context.scene 26 | sc.muv_texture_wrap_set_and_refer = False 27 | 28 | def test_refer_ng_no_uv(self): 29 | # Warning: Object must have more than one UV map 30 | print("[TEST] (NG) No UV") 31 | result = bpy.ops.uv.muv_texture_wrap_refer() 32 | self.assertSetEqual(result, {'CANCELLED'}) 33 | 34 | def test_set_ng_no_uv(self): 35 | # Warning: Object must have more than one UV map 36 | print("[TEST] (NG) No UV") 37 | result = bpy.ops.uv.muv_texture_wrap_set() 38 | self.assertSetEqual(result, {'CANCELLED'}) 39 | 40 | def test_refer_ng_not_select_one_face(self): 41 | # Warning: Must select only one face 42 | print("[TEST] (NG) Not select 1 face") 43 | bpy.ops.mesh.uv_texture_add() 44 | bpy.ops.mesh.select_all(action='DESELECT') 45 | result = bpy.ops.uv.muv_texture_wrap_refer() 46 | self.assertSetEqual(result, {'CANCELLED'}) 47 | 48 | def test_refer_ok(self): 49 | print("[TEST] (OK)") 50 | bpy.ops.mesh.uv_texture_add() 51 | bpy.ops.mesh.select_all(action='DESELECT') 52 | common.select_faces(self.active_obj, 1) 53 | result = bpy.ops.uv.muv_texture_wrap_refer() 54 | self.assertSetEqual(result, {'FINISHED'}) 55 | 56 | def __prepare_set_test(self): 57 | bpy.ops.mesh.uv_texture_add() 58 | bpy.ops.mesh.select_all(action='DESELECT') 59 | common.select_faces(self.active_obj, 1) 60 | result = bpy.ops.uv.muv_texture_wrap_refer() 61 | self.assertSetEqual(result, {'FINISHED'}) 62 | 63 | bpy.ops.mesh.select_all(action='DESELECT') 64 | 65 | def test_set_ng_not_select_one_face(self): 66 | # Warning: Must select only one face 67 | print("[TEST] (NG) Not select 1 face") 68 | self.__prepare_set_test() 69 | result = bpy.ops.uv.muv_texture_wrap_set() 70 | self.assertSetEqual(result, {'CANCELLED'}) 71 | 72 | def test_set_ng_not_select_different_face(self): 73 | # Warning: Must select different face 74 | print("[TEST] (NG) Not select different face") 75 | self.__prepare_set_test() 76 | common.select_faces(self.active_obj, 1) 77 | result = bpy.ops.uv.muv_texture_wrap_set() 78 | self.assertSetEqual(result, {'CANCELLED'}) 79 | 80 | def test_set_ok_default(self): 81 | print("[TEST] (OK) Default") 82 | self.__prepare_set_test() 83 | common.select_faces(self.active_obj, 1, 2) 84 | result = bpy.ops.uv.muv_texture_wrap_set() 85 | self.assertSetEqual(result, {'FINISHED'}) 86 | 87 | def test_set_ok_set_and_refer(self): 88 | sc = bpy.context.scene 89 | sc.muv_texture_wrap_set_and_refer = True 90 | 91 | self.__prepare_set_test() 92 | common.select_faces(self.active_obj, 1, 2) 93 | result = bpy.ops.uv.muv_texture_wrap_set() 94 | self.assertSetEqual(result, {'FINISHED'}) 95 | 96 | bpy.ops.mesh.select_all(action='DESELECT') 97 | common.select_faces(self.active_obj, 1, 3) 98 | result = bpy.ops.uv.muv_texture_wrap_set() 99 | self.assertSetEqual(result, {'FINISHED'}) 100 | 101 | def test_set_ng_not_share_2_vertices(self): 102 | # Warning: 2 vertices must be shared among faces 103 | print("[TEST] (NG) Not share 2 vertices") 104 | self.__prepare_set_test() 105 | if compat.check_version(2, 80, 0) < 0: 106 | common.select_faces(self.active_obj, 1, 1) 107 | else: 108 | common.select_faces(self.active_obj, 1, 3) 109 | result = bpy.ops.uv.muv_texture_wrap_set() 110 | self.assertSetEqual(result, {'CANCELLED'}) 111 | 112 | def test_set_ng_selseq_no_selected_face(self): 113 | sc = bpy.context.scene 114 | sc.muv_texture_wrap_selseq = True 115 | 116 | # Warning: Must select more than one face 117 | print("[TEST] (NG) No selected face") 118 | self.__prepare_set_test() 119 | result = bpy.ops.uv.muv_texture_wrap_set() 120 | self.assertSetEqual(result, {'CANCELLED'}) 121 | 122 | def test_set_ok_selseq(self): 123 | print("[TEST] (OK) Selection Sequence") 124 | self.__prepare_set_test() 125 | common.add_face_select_history(self.active_obj, 1, 2) 126 | result = bpy.ops.uv.muv_texture_wrap_set() 127 | self.assertSetEqual(result, {'FINISHED'}) 128 | 129 | def test_set_ng_different_object(self): 130 | sc = bpy.context.scene 131 | sc.muv_texture_wrap_selseq = False 132 | 133 | # Warning: Object must be same 134 | print("[TEST] (NG) Different object") 135 | self.__prepare_set_test() 136 | common.add_face_select_history(self.active_obj, 1, 2) 137 | result = bpy.ops.uv.muv_texture_wrap_set() 138 | self.assertSetEqual(result, {'FINISHED'}) 139 | 140 | bpy.ops.object.mode_set(mode='OBJECT') 141 | common.duplicate_object_without_uv() 142 | common.select_object_only("Cube.001") 143 | compat.set_active_object(bpy.data.objects["Cube.001"]) 144 | active_obj = compat.get_active_object(bpy.context) 145 | bpy.ops.object.mode_set(mode='EDIT') 146 | 147 | common.select_faces(active_obj, 1, 1) 148 | result = bpy.ops.uv.muv_texture_wrap_set() 149 | self.assertSetEqual(result, {'CANCELLED'}) 150 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/transfer_uv_test.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import bmesh 3 | 4 | from . import common 5 | from . import compatibility as compat 6 | 7 | 8 | class TestTransferUV(common.TestBase): 9 | module_name = "transfer_uv" 10 | idname = [ 11 | # Transfer UV 12 | ('OPERATOR', 'uv.muv_transfer_uv_copy_uv'), 13 | ('OPERATOR', 'uv.muv_transfer_uv_paste_uv'), 14 | ] 15 | 16 | # some test is complicated to reproduce 17 | # - there is no case more than 2 faces share one edge 18 | def setUpEachMethod(self): 19 | src_obj_name = "Cube" 20 | self.dest_obj_name = "Cube.001" 21 | 22 | common.select_object_only(src_obj_name) 23 | common.duplicate_object_without_uv() 24 | compat.set_active_object(bpy.data.objects[src_obj_name]) 25 | self.active_obj = compat.get_active_object(bpy.context) 26 | common.select_object_only(src_obj_name) 27 | bpy.ops.object.mode_set(mode='EDIT') 28 | 29 | def test_copy_uv_ng_no_uv(self): 30 | # Warning: Object must have more than one UV map 31 | print("[TEST] (NG) No UV") 32 | result = bpy.ops.uv.muv_transfer_uv_copy_uv() 33 | self.assertSetEqual(result, {'CANCELLED'}) 34 | 35 | def test_copy_uv_ng_not_select_two_faces(self): 36 | # Warning: Two faces must be selected 37 | print("[TEST] (NG) Not select two faces") 38 | bpy.ops.mesh.uv_texture_add() 39 | common.select_faces(self.active_obj, 1) 40 | result = bpy.ops.uv.muv_transfer_uv_copy_uv() 41 | self.assertSetEqual(result, {'CANCELLED'}) 42 | 43 | def test_copy_uv_ng_not_active_two_faces(self): 44 | # Warning: Two faces must be active 45 | print("[TEST] (NG) Not active two faces") 46 | bpy.ops.mesh.uv_texture_add() 47 | common.select_faces(self.active_obj, 2) 48 | bm = bmesh.from_edit_mesh(self.active_obj.data) 49 | for f in bm.faces: 50 | if not f.select: 51 | bm.faces.active = f 52 | break 53 | result = bpy.ops.uv.muv_transfer_uv_copy_uv() 54 | self.assertSetEqual(result, {'CANCELLED'}) 55 | 56 | def test_copy_uv_ng_not_share_one_edge(self): 57 | # Warning: Two faces should share one edge 58 | print("[TEST] (NG) Not share one edge") 59 | bpy.ops.mesh.uv_texture_add() 60 | bm = bmesh.from_edit_mesh(self.active_obj.data) 61 | selected_faces = common.select_and_active_faces_bm(bm, 1) 62 | common.select_unlink_faces_bm(bm, selected_faces[0], 1) 63 | result = bpy.ops.uv.muv_transfer_uv_copy_uv() 64 | self.assertSetEqual(result, {'CANCELLED'}) 65 | 66 | def test_copy_uv_ok(self): 67 | print("[TEST] (OK)") 68 | bpy.ops.mesh.uv_texture_add() 69 | common.select_and_active_faces(self.active_obj, 1) 70 | common.select_link_face(self.active_obj, 1) 71 | result = bpy.ops.uv.muv_transfer_uv_copy_uv() 72 | self.assertSetEqual(result, {'FINISHED'}) 73 | 74 | def __prepare_paste_uv_test(self): 75 | # Copy UV 76 | bpy.ops.mesh.uv_texture_add() 77 | common.select_and_active_faces(self.active_obj, 1) 78 | common.select_link_face(self.active_obj, 1) 79 | result = bpy.ops.uv.muv_transfer_uv_copy_uv() 80 | self.assertSetEqual(result, {'FINISHED'}) 81 | 82 | bpy.ops.object.mode_set(mode='OBJECT') 83 | common.select_object_only(self.dest_obj_name) 84 | compat.set_active_object(bpy.data.objects[self.dest_obj_name]) 85 | self.active_obj = compat.get_active_object(bpy.context) 86 | bpy.ops.object.mode_set(mode='EDIT') 87 | 88 | def test_paste_uv_ng_no_uv(self): 89 | # Warning: Object must have more than one UV map 90 | print("[TEST] (NG) No UV") 91 | self.__prepare_paste_uv_test() 92 | result = bpy.ops.uv.muv_transfer_uv_paste_uv() 93 | self.assertSetEqual(result, {'CANCELLED'}) 94 | 95 | def test_paste_uv_ng_not_select_two_faces(self): 96 | # Warning: Two faces must be selected 97 | print("[TEST] (NG) Not two face select") 98 | self.__prepare_paste_uv_test() 99 | bpy.ops.mesh.uv_texture_add() 100 | common.add_face_select_history(self.active_obj, 1) 101 | result = bpy.ops.uv.muv_transfer_uv_paste_uv() 102 | self.assertSetEqual(result, {'CANCELLED'}) 103 | 104 | def test_paste_uv_ng_not_share_one_edge(self): 105 | # Warning: Two faces should share one edge 106 | print("[TEST] (NG) Two faces should share one edge") 107 | self.__prepare_paste_uv_test() 108 | bpy.ops.mesh.uv_texture_add() 109 | if compat.check_version(2, 80, 0) < 0: 110 | common.add_face_select_history(self.active_obj, 2) 111 | else: 112 | common.add_face_select_history_by_indices(self.active_obj, [0, 3]) 113 | result = bpy.ops.uv.muv_transfer_uv_paste_uv() 114 | self.assertSetEqual(result, {'CANCELLED'}) 115 | 116 | def test_paste_uv_ok_default(self): 117 | print("[TEST] (OK) Default") 118 | self.__prepare_paste_uv_test() 119 | bpy.ops.mesh.uv_texture_add() 120 | bm = bmesh.from_edit_mesh(self.active_obj.data) 121 | bm.select_history.clear() 122 | selected_face = common.select_and_active_faces(self.active_obj, 1) 123 | bm.select_history.add(selected_face[0]) 124 | selected_face = common.select_link_face(self.active_obj, 1) 125 | bm.select_history.add(selected_face[0]) 126 | for hist in bm.select_history: 127 | hist.select = True 128 | result = bpy.ops.uv.muv_transfer_uv_paste_uv() 129 | self.assertSetEqual(result, {'FINISHED'}) 130 | 131 | def test_paste_uv_ok_user_specified(self): 132 | print("[TEST] (OK) User specified") 133 | self.__prepare_paste_uv_test() 134 | bpy.ops.mesh.uv_texture_add() 135 | bm = bmesh.from_edit_mesh(self.active_obj.data) 136 | bm.select_history.clear() 137 | selected_face = common.select_and_active_faces(self.active_obj, 1) 138 | bm.select_history.add(selected_face[0]) 139 | selected_face = common.select_link_face(self.active_obj, 1) 140 | bm.select_history.add(selected_face[0]) 141 | for hist in bm.select_history: 142 | hist.select = True 143 | result = bpy.ops.uv.muv_transfer_uv_paste_uv( 144 | invert_normals=True, 145 | copy_seams=False 146 | ) 147 | self.assertSetEqual(result, {'FINISHED'}) 148 | 149 | def test_paste_uv_ng_different_amount_of_faces(self): 150 | # Warning: Mesh has different amount of faces 151 | print("[TEST] (NG) Mesh has different amount of faces") 152 | self.__prepare_paste_uv_test() 153 | bpy.ops.mesh.uv_texture_add() 154 | bpy.ops.mesh.select_all(action='SELECT') 155 | bpy.ops.mesh.quads_convert_to_tris() 156 | bm = bmesh.from_edit_mesh(self.active_obj.data) 157 | bm.select_history.clear() 158 | selected_face = common.select_and_active_faces(self.active_obj, 1) 159 | bm.select_history.add(selected_face[0]) 160 | selected_face = common.select_link_face(self.active_obj, 1) 161 | bm.select_history.add(selected_face[0]) 162 | for hist in bm.select_history: 163 | hist.select = True 164 | result = bpy.ops.uv.muv_transfer_uv_paste_uv() 165 | self.assertSetEqual(result, {'CANCELLED'}) 166 | bpy.ops.mesh.select_all(action='SELECT') 167 | bpy.ops.mesh.tris_convert_to_quads() 168 | 169 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/unwrap_constraint_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import bpy 4 | 5 | from . import common 6 | from . import compatibility as compat 7 | 8 | 9 | class TestUnwrapConstraint(common.TestBase): 10 | module_name = "unwrap_constraint" 11 | idname = [ 12 | # Unwrap Constraint 13 | ('OPERATOR', 'uv.muv_unwrap_constraint'), 14 | ] 15 | 16 | def setUpEachMethod(self): 17 | obj_name = "Cube" 18 | 19 | common.select_object_only(obj_name) 20 | compat.set_active_object(bpy.data.objects[obj_name]) 21 | bpy.ops.object.mode_set(mode='EDIT') 22 | 23 | def test_ng_no_uv(self): 24 | print("[TEST] (NG) No UV") 25 | bpy.ops.mesh.select_all(action='SELECT') 26 | result = bpy.ops.uv.muv_unwrap_constraint() 27 | self.assertSetEqual(result, {'CANCELLED'}) 28 | 29 | def test_ok_default(self): 30 | print("[TEST] (OK) Default") 31 | bpy.ops.mesh.uv_texture_add() 32 | bpy.ops.mesh.select_all(action='SELECT') 33 | result = bpy.ops.uv.muv_unwrap_constraint() 34 | self.assertSetEqual(result, {'FINISHED'}) 35 | 36 | def test_ok_user_specified(self): 37 | print("[TEST] (OK) user specified") 38 | bpy.ops.mesh.uv_texture_add() 39 | bpy.ops.mesh.select_all(action='SELECT') 40 | result = bpy.ops.uv.muv_unwrap_constraint( 41 | method='CONFORMAL', 42 | fill_holes=False, 43 | correct_aspect=False, 44 | use_subsurf_data=True, 45 | margin=0.1, 46 | u_const=True, 47 | v_const=True 48 | ) 49 | self.assertSetEqual(result, {'FINISHED'}) 50 | 51 | @unittest.skipIf(compat.check_version(2, 80, 0) < 0, 52 | "Not supported in <2.80") 53 | def test_ok_multiple_objects(self): 54 | print("[TEST] (OK) Multiple Objects") 55 | 56 | # Duplicate object. 57 | bpy.ops.object.mode_set(mode='OBJECT') 58 | obj_names = ["Cube", "Cube.001"] 59 | common.select_object_only(obj_names[0]) 60 | common.duplicate_object_without_uv() 61 | 62 | for name in obj_names: 63 | bpy.ops.object.mode_set(mode='OBJECT') 64 | common.select_object_only(name) 65 | compat.set_active_object(bpy.data.objects[name]) 66 | bpy.ops.object.mode_set(mode='EDIT') 67 | bpy.ops.mesh.uv_texture_add() 68 | bpy.ops.mesh.select_all(action='SELECT') 69 | 70 | # Select two objects. 71 | bpy.ops.object.mode_set(mode='OBJECT') 72 | compat.set_active_object(bpy.data.objects[obj_names[0]]) 73 | common.select_objects_only(obj_names) 74 | bpy.ops.object.mode_set(mode='EDIT') 75 | 76 | result = bpy.ops.uv.muv_unwrap_constraint( 77 | method='CONFORMAL', 78 | fill_holes=False, 79 | correct_aspect=False, 80 | use_subsurf_data=True, 81 | margin=0.1, 82 | u_const=True, 83 | v_const=True 84 | ) 85 | self.assertSetEqual(result, {'FINISHED'}) 86 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/uv_bounding_box_test.py: -------------------------------------------------------------------------------- 1 | from . import common 2 | 3 | 4 | class TestUVBoundingBox(common.TestBase): 5 | module_name = "uv_bounding_box" 6 | idname = [ 7 | # UV Bounding Box 8 | ('OPERATOR', "uv.muv_uv_bounding_box"), 9 | ] 10 | 11 | # modal operator can not invoke directly from cmdline 12 | def test_nothing(self): 13 | pass 14 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/uv_inspection_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import bpy 4 | 5 | from . import common 6 | from . import compatibility as compat 7 | 8 | 9 | class TestUVInspection(common.TestBase): 10 | module_name = "uv_inspection" 11 | idname = [ 12 | # UV Inspection 13 | ('OPERATOR', "uv.muv_uv_inspection_render"), 14 | ('OPERATOR', "uv.muv_uv_inspection_update"), 15 | ] 16 | 17 | def setUpEachMethod(self): 18 | obj_name = "Cube" 19 | 20 | common.select_object_only(obj_name) 21 | compat.set_active_object(bpy.data.objects[obj_name]) 22 | bpy.ops.object.mode_set(mode='EDIT') 23 | 24 | sc = bpy.context.scene 25 | sc.muv_uv_inspection_show_overlapped = True 26 | sc.muv_uv_inspection_show_flipped = True 27 | sc.muv_uv_inspection_display_in_v3d = True 28 | sc.muv_uv_inspection_show_mode = 'FACE' 29 | 30 | def test_ok_update_single_object(self): 31 | print("[TEST] Single Object (OK)") 32 | bpy.ops.mesh.select_all(action='SELECT') 33 | bpy.ops.mesh.uv_texture_add() 34 | result = bpy.ops.uv.muv_uv_inspection_update() 35 | self.assertSetEqual(result, {'FINISHED'}) 36 | 37 | @unittest.skipIf(compat.check_version(2, 80, 0) < 0, 38 | "Not supported in <2.80") 39 | def test_ok_update_multiple_objects(self): 40 | print("[TEST] Multiple Object (OK)") 41 | 42 | # Duplicate object. 43 | bpy.ops.object.mode_set(mode='OBJECT') 44 | obj_names = ["Cube", "Cube.001"] 45 | common.select_object_only(obj_names[0]) 46 | common.duplicate_object_without_uv() 47 | 48 | for name in obj_names: 49 | bpy.ops.object.mode_set(mode='OBJECT') 50 | common.select_object_only(name) 51 | compat.set_active_object(bpy.data.objects[name]) 52 | bpy.ops.object.mode_set(mode='EDIT') 53 | bpy.ops.mesh.uv_texture_add() 54 | bpy.ops.mesh.select_all(action='SELECT') 55 | 56 | # Select two objects. 57 | bpy.ops.object.mode_set(mode='OBJECT') 58 | compat.set_active_object(bpy.data.objects[obj_names[0]]) 59 | common.select_objects_only(obj_names) 60 | bpy.ops.object.mode_set(mode='EDIT') 61 | 62 | result = bpy.ops.uv.muv_uv_inspection_update() 63 | self.assertSetEqual(result, {'FINISHED'}) 64 | 65 | 66 | class TestUVInspectionPaintUVIsland(common.TestBase): 67 | module_name = "uv_inspection" 68 | submodule_name = "paint_uv_island" 69 | idname = [ 70 | # UV Inspection (Paint UV Island) 71 | ('OPERATOR', "uv.muv_uv_inspection_paint_uv_island"), 72 | ] 73 | 74 | def setUpEachMethod(self): 75 | obj_name = "Cube" 76 | 77 | common.select_object_only(obj_name) 78 | compat.set_active_object(bpy.data.objects[obj_name]) 79 | bpy.ops.object.mode_set(mode='EDIT') 80 | 81 | def test_only_run(self): 82 | print("[TEST] (Only Run)") 83 | bpy.ops.mesh.select_all(action='SELECT') 84 | bpy.ops.mesh.uv_texture_add() 85 | result = bpy.ops.uv.muv_uv_inspection_paint_uv_island() 86 | # Paint UV Island needs 'IMAGE_EDITOR' space and 'VIEW_3D' space, 87 | # but we can not setup such environment in this test. 88 | self.assertSetEqual(result, {'CANCELLED'}) 89 | 90 | @unittest.skipIf(compat.check_version(2, 80, 0) < 0, 91 | "Not supported in <2.80") 92 | def test_multiple_objects_only_run(self): 93 | print("[TEST] Multiple Object (Only Run)") 94 | 95 | # Duplicate object. 96 | bpy.ops.object.mode_set(mode='OBJECT') 97 | obj_names = ["Cube", "Cube.001"] 98 | common.select_object_only(obj_names[0]) 99 | common.duplicate_object_without_uv() 100 | 101 | for name in obj_names: 102 | bpy.ops.object.mode_set(mode='OBJECT') 103 | common.select_object_only(name) 104 | compat.set_active_object(bpy.data.objects[name]) 105 | bpy.ops.object.mode_set(mode='EDIT') 106 | bpy.ops.mesh.uv_texture_add() 107 | bpy.ops.mesh.select_all(action='SELECT') 108 | 109 | # Select two objects. 110 | bpy.ops.object.mode_set(mode='OBJECT') 111 | compat.set_active_object(bpy.data.objects[obj_names[0]]) 112 | common.select_objects_only(obj_names) 113 | bpy.ops.object.mode_set(mode='EDIT') 114 | 115 | result = bpy.ops.uv.muv_uv_inspection_paint_uv_island() 116 | # Paint UV Island needs 'IMAGE_EDITOR' space and 'VIEW_3D' space, 117 | # but we can not setup such environment in this test. 118 | self.assertSetEqual(result, {'CANCELLED'}) 119 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/uv_sculpt_test.py: -------------------------------------------------------------------------------- 1 | from . import common 2 | 3 | 4 | class TestUVSculpt(common.TestBase): 5 | module_name = "uv_sculpt" 6 | idname = [ 7 | # UV Sculpt 8 | ('OPERATOR', "uv.muv_uv_sculpt"), 9 | ] 10 | 11 | # modal operator can not invoke directly from cmdline 12 | def test_nothing(self): 13 | pass 14 | -------------------------------------------------------------------------------- /tests/python/magic_uv_test/uvw_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | import bpy 4 | 5 | from . import common 6 | from . import compatibility as compat 7 | 8 | 9 | class TestUVWBox(common.TestBase): 10 | module_name = "uvw" 11 | submodule_name = "box" 12 | idname = [ 13 | # UVW 14 | ('OPERATOR', 'uv.muv_uvw_box_map'), 15 | ] 16 | 17 | def setUpEachMethod(self): 18 | obj_name = "Cube" 19 | 20 | common.select_object_only(obj_name) 21 | compat.set_active_object(bpy.data.objects[obj_name]) 22 | bpy.ops.object.mode_set(mode='EDIT') 23 | 24 | def test_ng_no_uv(self): 25 | # Warning: Object must have more than one UV map 26 | print("[TEST] (NG) No UV") 27 | bpy.ops.mesh.select_all(action='SELECT') 28 | result = bpy.ops.uv.muv_uvw_box_map(assign_uvmap=False) 29 | self.assertSetEqual(result, {'CANCELLED'}) 30 | 31 | def test_ok_default(self): 32 | print("[TEST] (OK) Default") 33 | bpy.ops.mesh.select_all(action='SELECT') 34 | result = bpy.ops.uv.muv_uvw_box_map() 35 | self.assertSetEqual(result, {'FINISHED'}) 36 | 37 | def test_ok_user_specified(self): 38 | print("[TEST] (OK) user specified") 39 | bpy.ops.mesh.select_all(action='SELECT') 40 | bpy.ops.mesh.uv_texture_add() 41 | result = bpy.ops.uv.muv_uvw_box_map( 42 | size=2.0, 43 | rotation=(0.2, 0.1, 0.4), 44 | offset=(1.2, 5.0, -20.0), 45 | tex_aspect=1.3, 46 | assign_uvmap=False, 47 | force_axis='Z', 48 | force_axis_tex_aspect_correction=1.2, 49 | force_axis_rotation=(0.2, 0.1, 0.4) 50 | ) 51 | self.assertSetEqual(result, {'FINISHED'}) 52 | 53 | @unittest.skipIf(compat.check_version(2, 80, 0) < 0, 54 | "Not supported in <2.80") 55 | def test_ok_multiple_objects(self): 56 | print("[TEST] Multiple Object (OK)") 57 | 58 | # Duplicate object. 59 | bpy.ops.object.mode_set(mode='OBJECT') 60 | obj_names = ["Cube", "Cube.001"] 61 | common.select_object_only(obj_names[0]) 62 | common.duplicate_object_without_uv() 63 | 64 | for name in obj_names: 65 | bpy.ops.object.mode_set(mode='OBJECT') 66 | common.select_object_only(name) 67 | compat.set_active_object(bpy.data.objects[name]) 68 | bpy.ops.object.mode_set(mode='EDIT') 69 | bpy.ops.mesh.uv_texture_add() 70 | bpy.ops.mesh.select_all(action='SELECT') 71 | 72 | # Select two objects. 73 | bpy.ops.object.mode_set(mode='OBJECT') 74 | compat.set_active_object(bpy.data.objects[obj_names[0]]) 75 | common.select_objects_only(obj_names) 76 | bpy.ops.object.mode_set(mode='EDIT') 77 | 78 | result = bpy.ops.uv.muv_uvw_box_map( 79 | size=2.0, 80 | rotation=(0.2, 0.1, 0.4), 81 | offset=(1.2, 5.0, -20.0), 82 | tex_aspect=1.3, 83 | assign_uvmap=False, 84 | force_axis='Z', 85 | force_axis_tex_aspect_correction=1.2, 86 | force_axis_rotation=(0.2, 0.1, 0.4) 87 | ) 88 | self.assertSetEqual(result, {'FINISHED'}) 89 | 90 | 91 | class TestUVWBestPlaner(common.TestBase): 92 | module_name = "uvw" 93 | submodule_name = "best_planer" 94 | idname = [ 95 | # UVW 96 | ('OPERATOR', 'uv.muv_uvw_best_planer_map'), 97 | ] 98 | 99 | def setUpEachMethod(self): 100 | obj_name = "Cube" 101 | 102 | common.select_object_only(obj_name) 103 | compat.set_active_object(bpy.data.objects[obj_name]) 104 | bpy.ops.object.mode_set(mode='EDIT') 105 | 106 | def test_ng_no_uv(self): 107 | # Warning: Object must have more than one UV map 108 | print("[TEST] (NG) No UV") 109 | bpy.ops.mesh.select_all(action='SELECT') 110 | result = bpy.ops.uv.muv_uvw_best_planer_map(assign_uvmap=False) 111 | self.assertSetEqual(result, {'CANCELLED'}) 112 | 113 | def test_ok_default(self): 114 | print("[TEST] (OK) Default") 115 | bpy.ops.mesh.select_all(action='SELECT') 116 | result = bpy.ops.uv.muv_uvw_best_planer_map() 117 | self.assertSetEqual(result, {'FINISHED'}) 118 | 119 | def test_ok_user_specified(self): 120 | print("[TEST] (OK) user specified") 121 | bpy.ops.mesh.select_all(action='SELECT') 122 | result = bpy.ops.uv.muv_uvw_best_planer_map( 123 | size=0.5, 124 | rotation=-0.4, 125 | offset=(-5.0, 10.0), 126 | tex_aspect=0.6, 127 | assign_uvmap=True 128 | ) 129 | self.assertSetEqual(result, {'FINISHED'}) 130 | 131 | @unittest.skipIf(compat.check_version(2, 80, 0) < 0, 132 | "Not supported in <2.80") 133 | def test_ok_multiple_objects(self): 134 | print("[TEST] Multiple Object (OK)") 135 | 136 | # Duplicate object. 137 | bpy.ops.object.mode_set(mode='OBJECT') 138 | obj_names = ["Cube", "Cube.001"] 139 | common.select_object_only(obj_names[0]) 140 | common.duplicate_object_without_uv() 141 | 142 | for name in obj_names: 143 | bpy.ops.object.mode_set(mode='OBJECT') 144 | common.select_object_only(name) 145 | compat.set_active_object(bpy.data.objects[name]) 146 | bpy.ops.object.mode_set(mode='EDIT') 147 | bpy.ops.mesh.select_all(action='SELECT') 148 | 149 | # Select two objects. 150 | bpy.ops.object.mode_set(mode='OBJECT') 151 | compat.set_active_object(bpy.data.objects[obj_names[0]]) 152 | common.select_objects_only(obj_names) 153 | bpy.ops.object.mode_set(mode='EDIT') 154 | 155 | result = bpy.ops.uv.muv_uvw_best_planer_map( 156 | size=0.5, 157 | rotation=-0.4, 158 | offset=(-5.0, 10.0), 159 | tex_aspect=0.6, 160 | assign_uvmap=True 161 | ) 162 | self.assertSetEqual(result, {'FINISHED'}) -------------------------------------------------------------------------------- /tests/python/run_tests.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | 4 | def test_main(): 5 | import os 6 | path = os.path.dirname(__file__) 7 | sys.path.append(path) 8 | 9 | import magic_uv_test 10 | 11 | test_cases = [ 12 | magic_uv_test.align_uv_test.TestAlignUVCircle, 13 | magic_uv_test.align_uv_test.TestAlignUVStraighten, 14 | magic_uv_test.align_uv_test.TestAlignUVAxis, 15 | magic_uv_test.align_uv_test.TestAlignUVSnapSetPointTargetToCursor, 16 | magic_uv_test.align_uv_test.TestAlignUVSnapSetPointTargetToVertexGroup, 17 | magic_uv_test.align_uv_test.TestAlignUVSnapToPoint, 18 | magic_uv_test.align_uv_test.TestAlignUVSnapSetEdgeTargetToEdgeCenter, 19 | magic_uv_test.align_uv_test.TestAlignUVSnapToEdge, 20 | magic_uv_test.align_uv_cursor_test.TestAlignUVCursor, 21 | magic_uv_test.clip_uv_test.TestClipUV, 22 | magic_uv_test.copy_paste_uv_test.TestCopyPasteUV, 23 | magic_uv_test.copy_paste_uv_test.TestCopyPasteUVSelseq, 24 | magic_uv_test.copy_paste_uv_object_test.TestCopyPasteUVObject, 25 | magic_uv_test.copy_paste_uv_uvedit_test.TestCopyPasteUVUVEdit, 26 | magic_uv_test.flip_rotate_uv_test.TestFlipRotateUV, 27 | magic_uv_test.mirror_uv_test.TestMirrorUV, 28 | magic_uv_test.move_uv_test.TestMoveUV, 29 | magic_uv_test.pack_uv_test.TestPackUV, 30 | magic_uv_test.preserve_uv_aspect_test.TestPreserveUVAspect, 31 | magic_uv_test.select_uv_test.TestSelectUVOverlapped, 32 | magic_uv_test.select_uv_test.TestSelectUVFlipped, 33 | magic_uv_test.select_uv_test.TestSelectUVZoomSelectedUV, 34 | magic_uv_test.smooth_uv_test.TestSmoothUV, 35 | magic_uv_test.texture_lock_test.TestTextureLock, 36 | magic_uv_test.texture_projection_test.TestTextureProjection, 37 | magic_uv_test.texture_wrap_test.TestTextureWrap, 38 | magic_uv_test.transfer_uv_test.TestTransferUV, 39 | magic_uv_test.unwrap_constraint_test.TestUnwrapConstraint, 40 | magic_uv_test.uv_bounding_box_test.TestUVBoundingBox, 41 | magic_uv_test.uv_inspection_test.TestUVInspection, 42 | magic_uv_test.uv_inspection_test.TestUVInspectionPaintUVIsland, 43 | magic_uv_test.uv_sculpt_test.TestUVSculpt, 44 | magic_uv_test.uvw_test.TestUVWBox, 45 | magic_uv_test.uvw_test.TestUVWBestPlaner, 46 | magic_uv_test.world_scale_uv_test.TestWorldScaleUVMeasure, 47 | magic_uv_test.world_scale_uv_test.TestWorldScaleUVApplyManual, 48 | magic_uv_test.world_scale_uv_test.TestWorldScaleUVApplyScalingDensity, 49 | magic_uv_test.world_scale_uv_test.TestWorldScaleUVProportionalToMesh, 50 | ] 51 | 52 | suite = unittest.TestSuite() 53 | for case in test_cases: 54 | suite.addTest(unittest.makeSuite(case)) 55 | ret = unittest.TextTestRunner().run(suite).wasSuccessful() 56 | sys.exit(not ret) 57 | 58 | 59 | if __name__ == "__main__": 60 | test_main() 61 | -------------------------------------------------------------------------------- /tools/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ $# -ne 2 ]; then 4 | echo "Usage: tools/install.sh " 5 | exit 1 6 | fi 7 | 8 | os=${1} 9 | version=${2} 10 | target="" 11 | 12 | if [ ${os} = "mac" ]; then 13 | target="${HOME}/Library/Application Support/Blender/${version}/scripts/addons/magic_uv" 14 | elif [ ${os} = "linux" ]; then 15 | target="${HOME}/.config/blender/${version}/scripts/addons/magic_uv" 16 | else 17 | echo "Invalid operating system." 18 | exit 1 19 | fi 20 | 21 | rm -rf "${target}" 22 | cp -r src/magic_uv "${target}" 23 | -------------------------------------------------------------------------------- /tools/muv_piemenu.py: -------------------------------------------------------------------------------- 1 | # 2 | 3 | # ##### BEGIN GPL LICENSE BLOCK ##### 4 | # 5 | # This program is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU General Public License 7 | # as published by the Free Software Foundation; either version 2 8 | # of the License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software Foundation, 17 | # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | # 19 | # ##### END GPL LICENSE BLOCK ##### 20 | 21 | import bpy 22 | 23 | __author__ = "Nutti " 24 | __status__ = "production" 25 | __version__ = "4.0" 26 | __date__ = "XX XXX 2015" 27 | 28 | 29 | addon_keymaps = [] 30 | 31 | class MUV_PieMenu(bpy.types.Menu): 32 | bl_idname = "pie.muv_cpuv" 33 | bl_label = "Magic UV" 34 | 35 | def draw(self, context): 36 | layout = self.layout 37 | pie = layout.menu_pie() 38 | pie.operator("uv.muv_cpuv_copy_uv", text="Copy UV") 39 | pie.operator("uv.muv_cpuv_paste_uv", text="Paste UV") 40 | pie.operator("uv.muv_cpuv_selseq_copy_uv", text="Copy UV (Selection Sequence)") 41 | pie.operator("uv.muv_cpuv_selseq_paste_uv", text="Paste UV (Selection Sequence)") 42 | pie.operator("uv.muv_fliprot", text="Flip/Rotate UV") 43 | pie.operator("uv.muv_transuv_copy", text="Transfer UV Copy") 44 | pie.operator("uv.muv_transuv_paste", text="Transfer UV Paste") 45 | 46 | 47 | def register(): 48 | bpy.utils.register_module(__name__) 49 | wm = bpy.context.window_manager 50 | kc = wm.keyconfigs.addon 51 | if kc: 52 | km = kc.keymaps.new(name="3D View", space_type="VIEW_3D") 53 | kmi = km.keymap_items.new('wm.call_menu_pie', 'U', 'PRESS', ctrl=True, alt=False, shift=False) 54 | kmi.properties.name = MUV_PieMenu.bl_idname 55 | addon_keymaps.append((km, kmi)) 56 | 57 | 58 | def unregister(): 59 | bpy.utils.unregister_module(__name__) 60 | for km, kmi in addon_keymaps: 61 | km.keymap_items.remove(kmi) 62 | addon_keymaps.clear() 63 | 64 | 65 | if __name__ == "__main__": 66 | register() 67 | --------------------------------------------------------------------------------