├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── __init__.py ├── character_ui.py ├── operators ├── __init__.py ├── attribute_groups.py ├── attributes.py ├── edit_outfit_piece.py ├── edit_visibility.py ├── fix_new_id.py ├── format_outfit_piece_name.py ├── generate_character_ui_script.py ├── links.py ├── move_unassigned_objects.py ├── parent_to_character.py ├── tooltip.py ├── use_as_cage.py └── use_as_driver.py ├── panels ├── __init__.py ├── attributes.py ├── body.py ├── generate.py ├── main.py ├── miscellaneous.py ├── outfits.py ├── physics.py └── rig_layers.py ├── template.blend └── template.blend1 /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. Linux,Windows,...] 28 | - Blender: [2.79,2.92,...] 29 | 30 | **Additional context** 31 | Add any other context about the problem here. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | __pycache__ -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | nextr3d@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Character-UI 2 | Blender add-on for creating simple yet functional UIs for your characters. 3 | 4 | ![showcase](https://imgur.com/DtfJjEt.png) 5 | ### Installation 6 | 7 | Download the whole repository as a ZIP file, if you want the newest version [or get the newest stable version from the Releases page](https://github.com/nextr3d/Character-UI/releases/) and install it as a normal add-on in Blender Preferences 8 | 9 | ![install](https://i.imgur.com/wx7GDAn.png) 10 | 11 | ### Usage 12 | You will find the add-on in the Sidebar, if hidden press **N** in the 3D Viewport and locate the **Character-UI** tab 13 | ![panel](https://imgur.com/yqzoCnB.png) 14 | 15 | **[For detailed guide on how to use the add-on read the wiki](https://github.com/nextr3d/Character-UI/wiki)** 16 | ## Issues 17 | 18 | If you find a bug or have a feature in mind I should add feel free to [add a new issue](https://github.com/nextr3d/Character-UI/issues/new/choose). -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from . import operators 2 | from . import panels 3 | import importlib 4 | bl_info = { 5 | "name": "Character-UI", 6 | "description": "Addon for creating simple yet functional menus for your characters", 7 | "author": "nextr", 8 | "version": (1, 4, 0), 9 | "blender": (4, 3, 0), 10 | "location": "View3d > Sidebar > Character-UI", 11 | "category": 'Interface', 12 | "doc_url": "https://github.com/nextr3d/Character-UI/wiki", 13 | "tracker_url": "https://github.com/nextr3d/Character-UI/issues" 14 | } 15 | 16 | modules = [ 17 | panels, 18 | operators 19 | ] 20 | 21 | def register(): 22 | for m in modules: 23 | importlib.reload(m) 24 | if hasattr(m, 'register'): 25 | m.register() 26 | 27 | 28 | def unregister(): 29 | # Apparently it's better to unregister modules in the reversed order 30 | for m in reversed(modules): 31 | if hasattr(m, "unregister"): 32 | m.unregister() 33 | 34 | 35 | if __name__ == "__main__": 36 | register() 37 | -------------------------------------------------------------------------------- /character_ui.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | import time 3 | import re 4 | from bpy.types import Operator, Panel, PropertyGroup, Object 5 | from bpy.utils import register_class, unregister_class 6 | from bpy.props import EnumProperty, BoolProperty, StringProperty, IntProperty, FloatVectorProperty 7 | 8 | """ 9 | available variables 10 | character_id 11 | character_id_key 12 | rig_layers_key 13 | links_key 14 | custom_label 15 | """ 16 | # script variables 17 | custom_prefix = "CharacterUI_" 18 | attributes_key = "%satt_%s" % (custom_prefix, character_id) 19 | 20 | bl_info = { 21 | "name": "Character UI Script", 22 | "description": "Script rendering UI for your character", 23 | "author": "nextr", 24 | "version": (5, 4, 0), 25 | "blender": (4, 3, 0), 26 | } 27 | 28 | 29 | class CharacterUI(PropertyGroup): 30 | @staticmethod 31 | def initialize(): 32 | t = time.time() 33 | ch = CharacterUIUtils.get_character() 34 | key = "%s%s" % (custom_prefix, character_id) 35 | if ch: 36 | print("Building UI for %s" % (ch.name)) 37 | if key not in ch: 38 | ch[key] = {} 39 | if "body_object" in ch.data and ch.data["body_object"]: 40 | CharacterUI.remove_visibility_drivers_drivers(ch) 41 | print("Removed drivers") 42 | CharacterUI.setup_visibility_drivers(ch) 43 | if "hair_collection" in ch.data and ch.data["hair_collection"]: 44 | CharacterUI.build_hair(ch, key) 45 | if "outfits_collection" in ch.data and ch.data["outfits_collection"]: 46 | CharacterUI.build_outfits(ch, key) 47 | print("Finished building UI in %s" % (time.time()-t)) 48 | 49 | @classmethod 50 | def build_outfits(self, ch, key): 51 | "Builds outfit selector for the UI" 52 | data = getattr(ch, key) 53 | 54 | outfits = ch.data["outfits_collection"].children.keys() 55 | options = self.create_enum_options(outfits, "Show outfit: ") 56 | default = 0 57 | if "outfits_enum" in data: 58 | default = data["outfits_enum"] 59 | try: 60 | self.ui_setup_enum("outfits_enum", CharacterUI.update_hair_by_outfit, 61 | "Outfits", "Changes outfits", options, default) 62 | except: 63 | pass 64 | self.ui_build_outfit_buttons(ch, key) 65 | print("Finished building outfits") 66 | 67 | @classmethod 68 | def remove_visibility_drivers_drivers(self, ch): 69 | "removes drivers from modifiers" 70 | for key in ["character_ui_masks", "character_ui_shape_keys"]: 71 | if key in ch.data: 72 | for item in ch.data[key]: 73 | if "name" not in item: 74 | name = item["modifier"] if "modifier" in item else item["shape_key"] 75 | item["name"] = name 76 | if "modifier" in item: 77 | del item["modifier"] 78 | elif "shape_key" in item: 79 | del item["shape_key"] 80 | if item["name"] in ch.data["body_object"].modifiers: 81 | if key == "character_ui_masks": 82 | ch.data["body_object"].modifiers[item["name"] 83 | ].driver_remove("show_viewport") 84 | ch.data["body_object"].modifiers[item["name"] 85 | ].driver_remove("show_render") 86 | else: 87 | ch.data["body_object"].data.shape_keys.key_blocks[item["name"]].driver_remove( 88 | "value") 89 | 90 | @classmethod 91 | def ui_build_outfit_buttons(self, ch, key): 92 | "Builds individual button for outfit pieces, their locks and creates drivers" 93 | data = getattr(ch, key) 94 | index = 0 95 | for collection in ch.data["outfits_collection"].children: 96 | objects = collection.objects 97 | for o in objects: 98 | default = False 99 | default_lock = False 100 | name = o.name_full.replace( 101 | '.', "-").replace(" ", "_")+"_outfit_toggle" 102 | if name in data and name+"_lock" in data: 103 | default = data[name] 104 | default_lock = data[name+"_lock"] 105 | 106 | toggle_label = o.name_full 107 | 108 | if "chui_outfit_piece_settings" in o: 109 | if "prefix" in o["chui_outfit_piece_settings"]: 110 | prefix = o["chui_outfit_piece_settings"]["prefix"] 111 | if prefix != "" and prefix in toggle_label and toggle_label.index(prefix) == 0: 112 | toggle_label = toggle_label[len(prefix):] 113 | 114 | self.ui_setup_toggle( 115 | name, None, toggle_label, "Toggles outfit piece on and off", default) 116 | self.ui_setup_toggle( 117 | name+"_lock", None, "", "Locks the outfit piece to be visible even when changing outfits", default_lock) 118 | variables = [{"name": "chui_outfit", "path": "%s.outfits_enum" % ( 119 | key)}, {"name": "chui_object", "path": "%s.%s" % (key, name)}] 120 | lock_expression = "chui_lock==1" 121 | expression = "not (chui_object == 1 and (chui_outfit ==%i or chui_lock==1))" % ( 122 | index) 123 | 124 | is_top_child = False 125 | if o.parent: 126 | # parent is in different collection so it has to 127 | if not o.users_collection[0] == o.parent.users_collection[0]: 128 | is_top_child = True 129 | else: 130 | is_top_child = True 131 | 132 | if is_top_child: 133 | variables.append( 134 | {"name": "chui_lock", "path": "%s.%s_lock" % (key, name)}) 135 | else: 136 | expression = "not (chui_object == 1 and chui_parent == 0)" 137 | variables.append( 138 | {"name": "chui_parent", "path": "hide_viewport", "driver_id": o.parent}) 139 | 140 | CharacterUIUtils.create_driver( 141 | ch, o, 'hide_viewport', expression, variables) 142 | CharacterUIUtils.create_driver( 143 | ch, o, 'hide_render', expression, variables) 144 | 145 | index += 1 146 | 147 | @classmethod 148 | def setup_visibility_drivers(self, ch): 149 | for key in ["character_ui_masks", "character_ui_shape_keys"]: 150 | if key in ch.data and "body_object" in ch.data: 151 | if ch.data["body_object"]: 152 | body = ch.data["body_object"] 153 | for item in ch.data[key]: 154 | expression = "" 155 | variables_viewport = [] 156 | variables_render = [] 157 | if type(item["driver_id"]) == Object: 158 | new_items = [item["driver_id"]] 159 | item["driver_id"] = new_items 160 | 161 | for (i, o) in enumerate(item["driver_id"]): 162 | expression = "%schui_object_%i==0%s" % ( 163 | expression, i, " or " if i < len(item["driver_id"]) - 1 else "") 164 | variables_viewport.append({"name": "chui_object_%i" % ( 165 | i), "path": "hide_viewport", "driver_id": o}) 166 | variables_render.append({"name": "chui_object_%i" % ( 167 | i), "path": "hide_render", "driver_id": o}) 168 | 169 | if key == "character_ui_masks": 170 | if "name" not in item: 171 | name = item["modifier"] 172 | item["name"] = name 173 | del item["modifier"] 174 | CharacterUIUtils.create_driver( 175 | None, body.modifiers[item["name"]], "show_viewport", expression, variables_viewport) 176 | CharacterUIUtils.create_driver( 177 | None, body.modifiers[item["name"]], "show_render", expression, variables_render) 178 | else: 179 | if "name" not in item: 180 | name = item["shape_key"] 181 | item["name"] = name 182 | del item["shape_key"] 183 | CharacterUIUtils.create_driver( 184 | None, body.data.shape_keys.key_blocks[item["name"]], "value", expression, variables_render) 185 | 186 | @classmethod 187 | def build_hair(self, ch, key): 188 | data = getattr(ch, key) 189 | default_value = 0 190 | if 'hair_lock' in data: 191 | default_value = data['hair_lock'] 192 | self.ui_setup_toggle( 193 | "hair_lock", None, "", "Locks hair so it's not changed by the outfit", default_value) 194 | hair_collection = ch.data["hair_collection"] 195 | items = [*hair_collection.children, *hair_collection.objects] 196 | names = [o.name for o in items] 197 | 198 | def create_hair_drivers(target, index): 199 | CharacterUIUtils.create_driver(ch, target, 'hide_viewport', "characterui_hair!=%i" % ( 200 | index), [{"name": "characterui_hair", "path": "%s.hair_enum" % (key)}]) 201 | CharacterUIUtils.create_driver(ch, target, 'hide_render', "characterui_hair!=%i" % ( 202 | index), [{"name": "characterui_hair", "path": "%s.hair_enum" % (key)}]) 203 | 204 | def recursive_hair(hair_items, index=-1): 205 | for i in enumerate(hair_items): 206 | if hasattr(i[1], "type"): 207 | create_hair_drivers(i[1], i[0] if index < 0 else index) 208 | else: 209 | recursive_hair([*i[1].children, *i[1].objects], i[0]) 210 | 211 | recursive_hair(items) 212 | default = 0 213 | if "hair_enum" in data: 214 | default = data["hair_enum"] 215 | try: 216 | self.ui_setup_enum('hair_enum', None, "Hairstyle", "Switch between different hairdos", 217 | self.create_enum_options(names, "Enables: "), default) 218 | except: 219 | pass 220 | print("Finished building hair") 221 | 222 | @classmethod 223 | def ui_setup_toggle(self, property_name, update_function, name='Name', description='Empty description', default=False): 224 | "method for easier creation of toggles (buttons)" 225 | 226 | props = CharacterUIUtils.get_props_from_character() 227 | props[property_name] = default 228 | 229 | prop = BoolProperty( 230 | name=name, 231 | description=description, 232 | update=update_function, 233 | default=default 234 | ) 235 | setattr(self, property_name, prop) 236 | 237 | @classmethod 238 | def ui_setup_enum(self, property_name, update_function, name="Name", description="Empty description", items=[], default=0): 239 | "method for easier creation of enums (selects)" 240 | props = CharacterUIUtils.get_props_from_character() 241 | props[property_name] = default 242 | 243 | prop = EnumProperty( 244 | name=name, 245 | description=description, 246 | items=items, 247 | update=update_function, 248 | default='OP'+str(default) 249 | ) 250 | setattr(self, property_name, prop) 251 | 252 | @staticmethod 253 | def create_enum_options(array, description_prefix="Empty description for:"): 254 | "method for creating options for blender UI enums" 255 | items = [] 256 | for array_item in array: 257 | items.append(("OP"+str(array.index(array_item)), 258 | array_item, description_prefix+" "+array_item)) 259 | return items 260 | 261 | @staticmethod 262 | def update_hair_by_outfit(self, context): 263 | ch = CharacterUIUtils.get_character() 264 | if ch: 265 | props = CharacterUIUtils.get_props_from_character() 266 | outfit_name = ch.data["outfits_collection"].children[props["outfits_enum"]].name 267 | if "hair_collection" in ch.data and ch.data["hair_collection"]: 268 | if not props["hair_lock"]: 269 | hairstyles = [*ch.data["hair_collection"].children, 270 | *ch.data["hair_collection"].objects] 271 | for hairstyle in enumerate(hairstyles): 272 | if outfit_name in hairstyle[1].name: 273 | props["hair_enum"] = hairstyle[0] 274 | 275 | 276 | class CharacterUIUtils: 277 | @staticmethod 278 | def get_character(): 279 | for o in bpy.data.objects: 280 | if str(type(o.data)) != "": # empties... 281 | if character_id_key in o.data: 282 | if o.data[character_id_key] == character_id: 283 | return o 284 | return False 285 | 286 | @staticmethod 287 | def get_props_from_character(): 288 | ch = CharacterUIUtils.get_character() 289 | return getattr(ch, "%s%s" % (custom_prefix, character_id)) 290 | 291 | @staticmethod 292 | def create_driver(driver_id, driver_target, driver_path, driver_expression, variables): 293 | "TODO: same exact code is in the add-on, make it that it's only once in the whole codebase" 294 | driver_target.driver_remove(driver_path) 295 | driver = driver_target.driver_add(driver_path) 296 | 297 | def setup_driver(driver, addition_path=""): 298 | driver.type = "SCRIPTED" 299 | driver.expression = driver_expression 300 | for variable in variables: 301 | local_driver_id = variable["driver_id"] if "driver_id" in variable else driver_id 302 | var = driver.variables.new() 303 | var.name = variable["name"] 304 | var.targets[0].id_type = local_driver_id.rna_type.name.upper() 305 | var.targets[0].id = local_driver_id 306 | var.targets[0].data_path = "%s%s" % ( 307 | variable["path"], addition_path) 308 | if type(driver) == list: 309 | for d in enumerate(driver): 310 | setup_driver(d[1].driver, "[%i]" % (d[0])) 311 | else: 312 | setup_driver(driver.driver) 313 | 314 | @staticmethod 315 | def safe_render(parent, data, prop, **kwargs): 316 | if hasattr(data, prop): 317 | parent.prop(data, prop, **kwargs) 318 | 319 | @staticmethod 320 | def render_outfit_piece(o, element, props, is_child=False): 321 | "recursively render outfit piece buttons" 322 | row = element.row(align=True) 323 | name = o.name.replace(".", "-").replace(" ", "_")+"_outfit_toggle" 324 | if o.data: 325 | CharacterUIUtils.safe_render(row, props, name, toggle=True, icon="DOWNARROW_HLT" if (props[name] and ("settings" in o.data or len( 326 | o.children))) else ("RIGHTARROW" if not props[name] and ("settings" in o.data or len(o.children)) else "NONE")) 327 | else: 328 | CharacterUIUtils.safe_render(row, props, name, toggle=True, icon="DOWNARROW_HLT" if (props[name] and ( 329 | len(o.children))) else ("RIGHTARROW" if not props[name] and (len(o.children)) else "NONE")) 330 | 331 | if not is_child: 332 | CharacterUIUtils.safe_render( 333 | row, props, name+"_lock", icon="LOCKED" if props[name+"_lock"] else "UNLOCKED") 334 | 335 | if not o.data: 336 | if len(o.children) and props[name]: 337 | settings_box = element.box() 338 | settings_box.label(text="Items", icon="MOD_CLOTH") 339 | for child in o.children: 340 | child_name = child.name.replace(" ", "_")+"_outfit_toggle" 341 | if hasattr(props, child_name): 342 | CharacterUIUtils.render_outfit_piece( 343 | child, settings_box, props, True) 344 | 345 | return 346 | 347 | if (len(o.children) or "settings" in o.data) and props[name]: 348 | if len(o.children): 349 | settings_box = element.box() 350 | settings_box.label(text="Items", icon="MOD_CLOTH") 351 | for child in o.children: 352 | child_name = child.name.replace(" ", "_")+"_outfit_toggle" 353 | if hasattr(props, child_name): 354 | CharacterUIUtils.render_outfit_piece( 355 | child, settings_box, props, True) 356 | 357 | @staticmethod 358 | def render_attributes(layout, groups, panel_name): 359 | for g in groups: 360 | render = True 361 | if "visibility" in g: 362 | expression = g["visibility"]["expression"] 363 | for var in g["visibility"]["variables"]: 364 | expression = expression.replace( 365 | str(var["variable"]), str(var["data_path"])) 366 | render = eval(expression) 367 | if render: 368 | box = layout.box() 369 | header_row = box.row(align=True) 370 | expanded_op = header_row.operator("character_ui_script.expand_attribute_group_%s" % ( 371 | character_id.lower()), emboss=False, text="", icon="DOWNARROW_HLT" if g["expanded"] else "RIGHTARROW") 372 | expanded_op.panel_name = panel_name 373 | expanded_op.group_name = g["name"] 374 | try: 375 | header_row.label(text=g["name"].replace( 376 | "_", " "), icon=g["icon"]) 377 | except: 378 | header_row.label(text=g["name"].replace("_", " ")) 379 | 380 | if g["expanded"]: 381 | for a in g["attributes"]: 382 | render_attribute = True 383 | if "visibility" in a: 384 | if "expression" in a["visibility"]: 385 | expression_a = a["visibility"]["expression"] 386 | for var in a["visibility"]["variables"]: 387 | expression_a = expression_a.replace( 388 | var["variable"], var["data_path"]) 389 | try: 390 | render_attribute = eval(expression_a) 391 | except: 392 | render_attribute = True 393 | if render_attribute: 394 | row = box.row(align=True) 395 | delimiter = '][' if '][' in a['path'] else '.' 396 | offset = 1 if '][' in a['path'] else 0 397 | prop = a['path'][a['path'].rindex(delimiter)+1:] 398 | path = a['path'][:a['path'].rindex( 399 | delimiter)+offset] 400 | 401 | toggle = a["toggle"] if "invert_checkbox" in a else False 402 | invert_checkbox = a["invert_checkbox"] if "invert_checkbox" in a else False 403 | slider = a["slider"] if "slider" in a else False 404 | icon = a["icon"] if "icon" in a else "NONE" 405 | if a['name']: 406 | try: 407 | row.prop(eval( 408 | path), prop, text=a['name'], invert_checkbox=invert_checkbox, toggle=toggle, slider=slider, icon=icon) 409 | except: 410 | print("couldn't render ", path, " prop") 411 | else: 412 | try: 413 | row.prop(eval( 414 | path), prop, invert_checkbox=invert_checkbox, toggle=toggle, slider=slider, icon=icon) 415 | except: 416 | print("couldn't render %s prop"(path)) 417 | 418 | @staticmethod 419 | def create_unique_ids(panels, operators): 420 | for p in panels: 421 | unique_panel = type( 422 | "%s_%s" % (p.bl_idname, character_id), (p,), {'bl_idname': "%s_%s" % ( 423 | p.bl_idname, character_id), 'bl_label': p.bl_label, 'bl_parent_id': "%s_%s" % (p.bl_parent_id, character_id) if hasattr(p, "bl_parent_id") else ""} 424 | ) 425 | register_class(unique_panel) 426 | for o in operators: 427 | name = "%s_%s" % (o.bl_idname, character_id.lower()) 428 | unique_operator = type(name, (o,), {"bl_idname": name}) 429 | register_class(unique_operator) 430 | 431 | @staticmethod 432 | def render_cages(layout, cages, panel=1): 433 | for c in cages: 434 | if c[1] == "OP%i" % (panel): 435 | _addName = "" 436 | for m in c[0].modifiers: 437 | if (m.type == "CLOTH" or m.type == "SOFT_BODY"): 438 | box = layout.box() 439 | if m.type == "CLOTH" : _addName = "cloth" 440 | else: _addName = "soft body" 441 | box.label(text=c[0].name + " - " + _addName + " modifier") 442 | row = box.row(align=True) 443 | row.prop(m, "show_viewport") 444 | row.prop(m, "show_render") 445 | box.prop(m.point_cache, "frame_start") 446 | box.prop(m.point_cache, "frame_end") 447 | icon = "TRASH" 448 | text = "Delete Bake" 449 | if not m.point_cache.is_baked: 450 | icon = "PHYSICS" 451 | text = "Bake" 452 | box.operator("character_ui.bake_%s" % ( 453 | character_id.lower()), text=text, icon=icon).object_name = c[0].name 454 | 455 | 456 | class VIEW3D_PT_characterUI(Panel): 457 | "Main panel" 458 | bl_space_type = 'VIEW_3D' 459 | bl_region_type = 'UI' 460 | bl_category = custom_label 461 | 462 | @classmethod 463 | def poll(self, context): 464 | if always_show: 465 | return True 466 | 467 | ch = CharacterUIUtils.get_character() 468 | if ch: 469 | return ch == context.object 470 | return False 471 | 472 | 473 | class VIEW3D_PT_outfits(VIEW3D_PT_characterUI): 474 | "Panel for outfits, settings and attributes regarding the outfits of the character" 475 | bl_label = "Outfits" 476 | bl_idname = "VIEW3D_PT_outfits" 477 | 478 | def draw(self, context): 479 | layout = self.layout 480 | ch = CharacterUIUtils.get_character() 481 | props = CharacterUIUtils.get_props_from_character() 482 | if ch and props: 483 | if ch.data["outfits_collection"]: 484 | outfits = ch.data["outfits_collection"] 485 | if len(outfits.children) > 1: 486 | CharacterUIUtils.safe_render(layout, props, "outfits_enum") 487 | box = layout.box() 488 | for o in outfits.children[props['outfits_enum']].objects: 489 | is_top_child = True # True because if no parent then it's the top child 490 | if not o.parent == None and not o.parent == ch: 491 | # parent is in different collection so it has to 492 | is_top_child = not o.users_collection[0] == o.parent.users_collection[0] 493 | if is_top_child: 494 | CharacterUIUtils.render_outfit_piece(o, box, props) 495 | 496 | locked_pieces = {} 497 | 498 | for i, c in enumerate(outfits.children): 499 | pieces = [] 500 | for o in c.objects: 501 | if i != props["outfits_enum"]: 502 | name = o.name.replace(" ", "_")+"_outfit_toggle" 503 | if props[name+"_lock"]: 504 | pieces.append(o) 505 | if len(pieces): 506 | locked_pieces[c.name] = pieces 507 | 508 | for n, pcs in locked_pieces.items(): 509 | box.label(text=n) 510 | for p in pcs: 511 | CharacterUIUtils.render_outfit_piece(p, box, props) 512 | if attributes_key in ch: 513 | if "outfits" in ch[attributes_key]: 514 | attributes_box = layout.box() 515 | attributes_box.label(text="Attributes") 516 | CharacterUIUtils.render_attributes( 517 | attributes_box, ch[attributes_key]["outfits"], "outfits") 518 | 519 | 520 | class VIEW3D_PT_body(VIEW3D_PT_characterUI): 521 | "Body panel" 522 | bl_label = "Body" 523 | bl_idname = "VIEW3D_PT_body" 524 | 525 | @classmethod 526 | def poll(self, context): 527 | ch = CharacterUIUtils.get_character() 528 | if ch: 529 | render = False 530 | if attributes_key in ch: 531 | if "body" in ch[attributes_key]: 532 | render = len(ch[attributes_key]["body"]) > 0 533 | if ch.data["hair_collection"] and not render: 534 | if (len(ch.data["hair_collection"].children) + len(ch.data["hair_collection"].objects)) > 1: 535 | render = True 536 | if "character_ui_cages" in ch.data and not render: 537 | if "cages" in ch.data["character_ui_cages"]: 538 | out = list(filter(lambda x: "OP2" in x, 539 | ch.data["character_ui_cages"]["cages"])) 540 | render = len(out) > 0 541 | return render and (ch == context.object or always_show) 542 | return False 543 | 544 | def draw(self, context): 545 | layout = self.layout 546 | ch = CharacterUIUtils.get_character() 547 | if ch: 548 | props = CharacterUIUtils.get_props_from_character() 549 | if ch.data["hair_collection"]: 550 | if (len(ch.data["hair_collection"].children) + len(ch.data["hair_collection"].objects)) > 1: 551 | hair_row = layout.row(align=True) 552 | CharacterUIUtils.safe_render(hair_row, props, "hair_enum") 553 | if hasattr(props, "hair_lock") and hasattr(props, "hair_enum"): 554 | CharacterUIUtils.safe_render( 555 | hair_row, props, "hair_lock", icon="LOCKED" if props.hair_lock else "UNLOCKED", toggle=True) 556 | if attributes_key in ch: 557 | if "body" in ch[attributes_key]: 558 | if len(ch[attributes_key]["body"]): 559 | attributes_box = layout.box() 560 | attributes_box.label(text="Attributes") 561 | CharacterUIUtils.render_attributes( 562 | attributes_box, ch[attributes_key]["body"], "body") 563 | 564 | 565 | class VIEW3D_PT_physics_body_panel(VIEW3D_PT_characterUI): 566 | "Physics Sub-Panel" 567 | bl_label = "Physics" 568 | bl_idname = "VIEW3D_PT_physics_body_panel" 569 | bl_parent_id = "VIEW3D_PT_body" 570 | 571 | @classmethod 572 | def poll(self, context): 573 | ch = CharacterUIUtils.get_character() 574 | if ch: 575 | if "character_ui_cages" in ch.data: 576 | if "cages" in ch.data["character_ui_cages"]: 577 | out = list(filter(lambda x: "OP2" in x, 578 | ch.data["character_ui_cages"]["cages"])) 579 | return len(out) > 0 580 | 581 | return False 582 | 583 | def draw(self, context): 584 | layout = self.layout 585 | ch = CharacterUIUtils.get_character() 586 | CharacterUIUtils.render_cages( 587 | layout, ch.data["character_ui_cages"]["cages"], 2) 588 | 589 | 590 | class VIEW3D_PT_physics_outfits_panel(VIEW3D_PT_characterUI): 591 | "Physics Sub-Panel" 592 | bl_label = "Physics" 593 | bl_idname = "VIEW3D_PT_physics_outfits_panel" 594 | bl_parent_id = "VIEW3D_PT_outfits" 595 | 596 | @classmethod 597 | def poll(self, context): 598 | ch = CharacterUIUtils.get_character() 599 | if ch: 600 | if "character_ui_cages" in ch.data: 601 | if "cages" in ch.data["character_ui_cages"]: 602 | out = list(filter(lambda x: "OP1" in x, 603 | ch.data["character_ui_cages"]["cages"])) 604 | return len(out) > 0 605 | return False 606 | 607 | def draw(self, context): 608 | layout = self.layout 609 | ch = CharacterUIUtils.get_character() 610 | CharacterUIUtils.render_cages( 611 | layout, ch.data["character_ui_cages"]["cages"], 1) 612 | 613 | 614 | class VIEW3D_PT_physics_misc_panel(VIEW3D_PT_characterUI): 615 | "Physics Sub-Panel" 616 | bl_label = "Physics" 617 | bl_idname = "VIEW3D_PT_physics_misc_panel" 618 | bl_parent_id = "VIEW3D_PT_miscellaneous" 619 | 620 | @classmethod 621 | def poll(self, context): 622 | ch = CharacterUIUtils.get_character() 623 | if ch: 624 | if "character_ui_cages" in ch.data: 625 | if "cages" in ch.data["character_ui_cages"]: 626 | out = list(filter(lambda x: "OP3" in x, 627 | ch.data["character_ui_cages"]["cages"])) 628 | return len(out) > 0 629 | return False 630 | 631 | def draw(self, context): 632 | layout = self.layout 633 | ch = CharacterUIUtils.get_character() 634 | CharacterUIUtils.render_cages( 635 | layout, ch.data["character_ui_cages"]["cages"], 3) 636 | 637 | 638 | class VIEW3D_PT_rig_layers(VIEW3D_PT_characterUI): 639 | "Panel for rig layers, settings and attributes regarding the rig" 640 | bl_label = "Rig" 641 | bl_idname = "VIEW3D_PT_rig_layers" 642 | 643 | @classmethod 644 | def poll(self, context): 645 | ch = CharacterUIUtils.get_character() 646 | if ch: 647 | if ch == context.active_object or always_show: 648 | if ch.type == "ARMATURE": 649 | return True 650 | 651 | if attributes_key in ch: 652 | if "rig" in ch[attributes_key]: 653 | if len(ch[attributes_key]["rig"]): 654 | return True 655 | return False 656 | 657 | def draw(self, context): 658 | box = self.layout.column().box() 659 | ch = CharacterUIUtils.get_character() 660 | if ch: 661 | if ch.type == "ARMATURE": 662 | box.label(text="Layers") 663 | box.template_bone_collection_tree() 664 | 665 | if attributes_key in ch: 666 | if "rig" in ch[attributes_key]: 667 | attributes_box = self.layout.box() 668 | attributes_box.label(text="Attributes") 669 | CharacterUIUtils.render_attributes( 670 | attributes_box, ch[attributes_key]["rig"], "rig") 671 | 672 | 673 | class VIEW3D_PT_miscellaneous(VIEW3D_PT_characterUI): 674 | "Panel for things which don't belong anywhere" 675 | bl_label = "Miscellaneous" 676 | bl_idname = "VIEW3D_PT_miscellaneous" 677 | 678 | @classmethod 679 | def poll(self, context): 680 | ch = CharacterUIUtils.get_character() 681 | if ch: 682 | if ch == context.active_object or always_show: 683 | if attributes_key in ch: 684 | if "misc" in ch[attributes_key]: 685 | if len(ch[attributes_key]["misc"]): 686 | return True 687 | if "character_ui_cages" in ch.data: 688 | if "cages" in ch.data["character_ui_cages"]: 689 | out = list(filter(lambda x: "OP3" in x, 690 | ch.data["character_ui_cages"]["cages"])) 691 | return len(out) > 0 692 | 693 | return False 694 | 695 | def draw(self, context): 696 | layout = self.layout 697 | ch = CharacterUIUtils.get_character() 698 | if attributes_key in ch: 699 | if "misc" in ch[attributes_key]: 700 | attributes_box = self.layout.box() 701 | attributes_box.label(text="Attributes") 702 | CharacterUIUtils.render_attributes( 703 | attributes_box, ch[attributes_key]["misc"], "misc") 704 | 705 | 706 | class VIEW3D_PT_links(VIEW3D_PT_characterUI): 707 | "Panel containing links and build info of the UI" 708 | bl_label = "Info" 709 | bl_idname = "VIEW3D_PT_links" 710 | 711 | def draw(self, context): 712 | layout = self.layout 713 | layout.separator() 714 | col = layout.column() 715 | data = CharacterUIUtils.get_character().data 716 | if links_key in data: 717 | for section in data[links_key].to_dict(): 718 | box = col.box() 719 | box.label(text=section) 720 | column = box.column(align=True) 721 | for link in data[links_key][section].to_dict(): 722 | try: 723 | column.operator( 724 | "wm.url_open", text=link, icon=data[links_key][section][link][0]).url = data[links_key][section][link][1] 725 | except: 726 | column.operator( 727 | "wm.url_open", text=link).url = data[links_key][section][link][1] 728 | box_model_info = layout.box() 729 | box_model_info.label(text=custom_label, icon="ARMATURE_DATA") 730 | if "character_ui_generation_date" in data: 731 | box_model_info.label(text="UI Generation date: %s" % ( 732 | data["character_ui_generation_date"]), icon="TIME") 733 | if "character_ui_char_version" in data: 734 | if len(data["character_ui_char_version"]): 735 | box_model_info.label(text="Version: %s" % ( 736 | data["character_ui_char_version"]), icon="BLENDER") 737 | 738 | box_ui_info = layout.box() 739 | box_ui_info.label(text="UI", icon="MENU_PANEL") 740 | box_ui_info.label(text='Character-UI v%s%s' % (".".join(str(i) 741 | for i in bl_info["version"]), '-%s' % (bl_info["branch"]) if "branch" in bl_info else ""), icon='SETTINGS') 742 | box_ui_info.operator( 743 | "wm.url_open", text="UI bugs/suggestions").url = "https://github.com/nextr3d/Character-UI/issues/new/choose" 744 | box_ui_info.operator( 745 | "wm.url_open", text="Download Character-UI add-on").url = "https://github.com/nextr3d/Character-UI" 746 | 747 | 748 | class OPS_OT_ExpandAttributeGroup(Operator): 749 | "Expands or Contracts attribute group" 750 | bl_idname = "character_ui_script.expand_attribute_group" 751 | bl_label = "Expand/Contract" 752 | bl_description = "Expands or Contracts Attribute Group" 753 | 754 | panel_name: StringProperty() 755 | group_name: StringProperty() 756 | 757 | def execute(self, context): 758 | ch = CharacterUIUtils.get_character() 759 | if ch: 760 | if attributes_key in ch: 761 | if self.panel_name in ch[attributes_key]: 762 | for i in range(len(ch[attributes_key][self.panel_name])): 763 | g = ch[attributes_key][self.panel_name][i] 764 | if g["name"] == self.group_name: 765 | g["expanded"] = not g["expanded"] 766 | ch[attributes_key][self.panel_name][i] = g 767 | 768 | return {'FINISHED'} 769 | 770 | 771 | class OPS_PT_BakePhysics(bpy.types.Operator): 772 | bl_idname = "character_ui.bake" 773 | bl_description = "Bake Physics" 774 | bl_label = "Bake" 775 | 776 | object_name: bpy.props.StringProperty() 777 | 778 | def execute(self, context): 779 | for m in bpy.data.objects[self.object_name].modifiers: 780 | if (m.type == "CLOTH" or m.type == "SOFT_BODY") and not m.point_cache.is_baked: 781 | if not m.show_viewport: 782 | self.report( 783 | {'WARNING'}, "Modifier is not visible in the viewport, baking will have no effect!") 784 | else: 785 | override = { 786 | 'scene': context.scene, 'active_object': bpy.data.objects[self.object_name], 'point_cache': m.point_cache} 787 | bpy.ops.ptcache.bake(override, bake=True) 788 | self.report( 789 | {'INFO'}, "Done baking physics for: "+self.object_name) 790 | elif (m.type == "CLOTH" or m.type == "SOFT_BODY") and m.point_cache.is_baked: 791 | override = {'scene': context.scene, 792 | 'active_object': bpy.data.objects[self.object_name], 'point_cache': m.point_cache} 793 | bpy.ops.ptcache.free_bake(override) 794 | self.report( 795 | {'INFO'}, "Removed physics cache for: "+self.object_name) 796 | return {'FINISHED'} 797 | 798 | 799 | classes = [ 800 | CharacterUI 801 | ] 802 | panels = [ 803 | VIEW3D_PT_outfits, 804 | VIEW3D_PT_rig_layers, 805 | VIEW3D_PT_body, 806 | VIEW3D_PT_physics_body_panel, 807 | VIEW3D_PT_physics_outfits_panel, 808 | VIEW3D_PT_miscellaneous, 809 | VIEW3D_PT_physics_misc_panel, 810 | VIEW3D_PT_links 811 | ] 812 | operators = [ 813 | OPS_OT_ExpandAttributeGroup, 814 | OPS_PT_BakePhysics 815 | ] 816 | 817 | 818 | def register(): 819 | for c in classes: 820 | register_class(c) 821 | 822 | bpy.app.handlers.load_post.append( 823 | CharacterUIUtils.create_unique_ids(panels, operators)) 824 | setattr(bpy.types.Object, "%s%s" % (custom_prefix, character_id), 825 | bpy.props.PointerProperty(type=CharacterUI)) 826 | 827 | CharacterUI.initialize() 828 | 829 | 830 | def unregister(): 831 | for c in reversed(classes): 832 | unregister_class(c) 833 | delattr(bpy.types.Object, "%s%s" % (custom_prefix, character_id)) 834 | 835 | 836 | if __name__ in ['__main__', 'builtins']: 837 | # __main__ when executed through the editor 838 | # builtins when executed after generation of the script 839 | register() 840 | -------------------------------------------------------------------------------- /operators/__init__.py: -------------------------------------------------------------------------------- 1 | from . import links 2 | from . import tooltip 3 | from . import attributes 4 | from . import fix_new_id 5 | from . import use_as_cage 6 | from . import use_as_driver 7 | from . import edit_visibility 8 | from . import attribute_groups 9 | from . import edit_outfit_piece 10 | from . import parent_to_character 11 | from . import move_unassigned_objects 12 | from . import format_outfit_piece_name 13 | from . import generate_character_ui_script 14 | import importlib 15 | 16 | modules = [ 17 | links, 18 | tooltip, 19 | attributes, 20 | fix_new_id, 21 | use_as_cage, 22 | use_as_driver, 23 | edit_visibility, 24 | attribute_groups, 25 | edit_outfit_piece, 26 | parent_to_character, 27 | move_unassigned_objects, 28 | format_outfit_piece_name, 29 | generate_character_ui_script 30 | ] 31 | 32 | 33 | def register(): 34 | for m in modules: 35 | importlib.reload(m) 36 | m.register() 37 | 38 | 39 | def unregister(): 40 | for m in reversed(modules): 41 | m.unregister() 42 | -------------------------------------------------------------------------------- /operators/attribute_groups.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Operator) 3 | from bpy.props import (StringProperty, BoolProperty) 4 | from bpy.utils import (register_class, unregister_class) 5 | 6 | 7 | class OPS_OT_AddNewAttributeGroup(Operator): 8 | bl_idname = "character_ui.add_new_attribute_group" 9 | bl_label = "Add new Attribute Group" 10 | bl_description = "Creates new attribute group" 11 | bl_options = {"INTERNAL"} 12 | 13 | panel_name: StringProperty() 14 | 15 | def execute(self, context): 16 | ch = context.scene.character_ui_object 17 | if ch: 18 | rig_id = ch.data[context.scene.character_ui_object_id] 19 | attributes_key = "CharacterUI_att_%s" % (rig_id) 20 | if attributes_key not in ch: 21 | ch[attributes_key] = {} 22 | 23 | def add_group(att): 24 | att.append({"name": 'Group_'+str(len(att)), 25 | "attributes": [], "expanded": True}) 26 | return att 27 | if self.panel_name in ch[attributes_key] and len(ch[attributes_key][self.panel_name]): 28 | ch[attributes_key][self.panel_name] = add_group( 29 | ch[attributes_key][self.panel_name]) 30 | else: 31 | ch[attributes_key][self.panel_name] = add_group([]) 32 | self.report({'INFO'}, "Added new attribute group") 33 | return {"FINISHED"} 34 | 35 | 36 | class OPS_OT_ExpandAttributeGroup(Operator): 37 | "Expands or contracts attribute group" 38 | bl_idname = "character_ui.expand_attribute_group" 39 | bl_label = "Expand/Contract" 40 | bl_description = "Expands or Contracts Attribute Group" 41 | bl_options = {"INTERNAL"} 42 | 43 | panel_name: StringProperty() 44 | group_name: StringProperty() 45 | 46 | def execute(self, context): 47 | ch = context.scene.character_ui_object 48 | if ch: 49 | rig_id = ch.data[context.scene.character_ui_object_id] 50 | attributes_key = "CharacterUI_att_%s" % (rig_id) 51 | if self.panel_name in ch[attributes_key]: 52 | for i in range(len(ch[attributes_key][self.panel_name])): 53 | g = ch[attributes_key][self.panel_name][i] 54 | if g["name"] == self.group_name: 55 | g["expanded"] = not g["expanded"] 56 | ch[attributes_key][self.panel_name][i] = g 57 | 58 | return {'FINISHED'} 59 | 60 | 61 | class OPS_OT_EditAttributeGroup(Operator): 62 | "Edits settings of attribute groups" 63 | bl_idname = "character_ui.edit_attribute_group" 64 | bl_label = "Edit attribute group" 65 | bl_description = "Edit attribute group" 66 | bl_options = {"INTERNAL"} 67 | 68 | panel_name: StringProperty() 69 | group_name: StringProperty() 70 | new_group_name: StringProperty(name="Group Name") 71 | group_icon: StringProperty( 72 | name="Icon", description="Name of the Icon for the group. Enable the built-in addon Icon Viewer to see all of the available icons.") 73 | 74 | def invoke(self, context, event): 75 | ch = context.scene.character_ui_object 76 | if ch: 77 | rig_id = ch.data[context.scene.character_ui_object_id] 78 | attributes_key = "CharacterUI_att_%s" % (rig_id) 79 | if self.panel_name in ch[attributes_key]: 80 | for g in ch[attributes_key][self.panel_name]: 81 | if g["name"] == self.group_name: 82 | self.new_group_name = g["name"].replace("_", " ") 83 | self.group_icon = g["icon"] if "icon" in g else "" 84 | 85 | return context.window_manager.invoke_props_dialog(self, width=350) 86 | return None 87 | 88 | def draw(self, context): 89 | self.layout.prop(self, "new_group_name") 90 | row = self.layout.row(align=True) 91 | row.prop(self, "group_icon") 92 | row.operator("character_ui.tooltip", text="", icon="QUESTION").tooltip_id = "icons" 93 | try: 94 | self.layout.label(text="- Icon Preview", icon=self.group_icon) 95 | except: 96 | self.layout.label(text="This icon does not exist - Icon Preview") 97 | 98 | def execute(self, context): 99 | ch = context.scene.character_ui_object 100 | if ch: 101 | rig_id = ch.data[context.scene.character_ui_object_id] 102 | attributes_key = "CharacterUI_att_%s" % (rig_id) 103 | if self.panel_name in ch[attributes_key]: 104 | index = -1 105 | for i in range(len(ch[attributes_key][self.panel_name])): 106 | if self.group_name == self.new_group_name.replace(" ", "_") and ch[attributes_key][self.panel_name][i]["name"] == self.new_group_name.replace(" ", "_"): 107 | ch[attributes_key][self.panel_name][i]["icon"] = self.group_icon 108 | self.report({'INFO'}, "Updated group's icon") 109 | return {'FINISHED'} 110 | if ch[attributes_key][self.panel_name][i]["name"] == self.new_group_name.replace(" ", "_"): 111 | self.report( 112 | {'INFO'}, "No changes saved, duplicated name for one panel") 113 | return {'CANCELLED'} 114 | if ch[attributes_key][self.panel_name][i]["name"] == self.group_name: 115 | index = i 116 | 117 | ch[attributes_key][self.panel_name][index]["name"] = self.new_group_name.replace( 118 | " ", "_") 119 | ch[attributes_key][self.panel_name][index]["icon"] = self.group_icon 120 | self.report({'INFO'}, "Updated Attribute Group") 121 | return {'FINISHED'} 122 | else: 123 | self.report({'ERROR'}, "No active object.") 124 | return {'CANCELLED'} 125 | 126 | 127 | class OPS_OT_RemoveAttributeGroup(Operator): 128 | "Removes attribute group from the UI" 129 | bl_idname = 'character_ui.remove_attribute_group' 130 | bl_label = 'Remove attribute group from the UI' 131 | bl_description = "Removes attribute group from the UI and all of the attributes inside and other synced attributes too" 132 | bl_options = {"INTERNAL"} 133 | 134 | group_name: StringProperty() 135 | panel_name: StringProperty() 136 | 137 | def invoke(self, context, event): 138 | return context.window_manager.invoke_confirm(self, event) 139 | 140 | def execute(self, context): 141 | ch = context.scene.character_ui_object 142 | if ch: 143 | rig_id = ch.data[context.scene.character_ui_object_id] 144 | attributes_key = "CharacterUI_att_%s" % (rig_id) 145 | if self.panel_name in ch[attributes_key]: 146 | att = ch[attributes_key][self.panel_name] 147 | new_groups = [] 148 | for g in att: 149 | if g["name"] != self.group_name: 150 | new_groups.append(g) 151 | ch[attributes_key][self.panel_name] = new_groups 152 | self.report({"INFO"}, 'Removed Attribute Group') 153 | return {'FINISHED'} 154 | 155 | 156 | class OPS_OT_AttributeGroupChangePosition(Operator): 157 | bl_idname = 'character_ui.attribute_group_change_position' 158 | bl_label = "Change attribute group's position in the list" 159 | bl_description = "Changes position of the attribute group in the current list" 160 | bl_options = {"INTERNAL"} 161 | 162 | group_name: StringProperty() 163 | panel_name: StringProperty() 164 | direction: BoolProperty() # True moves up, False move down 165 | 166 | def execute(self, context): 167 | ch = context.scene.character_ui_object 168 | if ch: 169 | rig_id = ch.data[context.scene.character_ui_object_id] 170 | attributes_key = "CharacterUI_att_%s" % (rig_id) 171 | if self.panel_name in ch[attributes_key]: 172 | att = ch[attributes_key][self.panel_name] 173 | i = 0 174 | for a in enumerate(att): 175 | if a[1]['name'] == self.group_name: 176 | i = a[0] 177 | if self.direction and i-1 >= 0: # move attribute group up in the list 178 | prev = att[i-1] 179 | att[i-1] = att[i] 180 | att[i] = prev 181 | self.report( 182 | {'INFO'}, "Moved attribute group up in the list") 183 | elif not self.direction and i+1 < len(att): 184 | next = att[i+1] 185 | att[i+1] = att[i] 186 | att[i] = next 187 | self.report( 188 | {'INFO'}, "Moved attribute group down in the list") 189 | ch[attributes_key][self.panel_name] = att 190 | return {'FINISHED'} 191 | 192 | 193 | classes = [ 194 | OPS_OT_AddNewAttributeGroup, 195 | OPS_OT_ExpandAttributeGroup, 196 | OPS_OT_EditAttributeGroup, 197 | OPS_OT_RemoveAttributeGroup, 198 | OPS_OT_AttributeGroupChangePosition 199 | ] 200 | 201 | 202 | def register(): 203 | for c in classes: 204 | register_class(c) 205 | 206 | 207 | def unregister(): 208 | for c in reversed(classes): 209 | unregister_class(c) 210 | -------------------------------------------------------------------------------- /operators/attributes.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Operator) 3 | from bpy.props import (PointerProperty, StringProperty, BoolProperty) 4 | from bpy.utils import (register_class, unregister_class) 5 | 6 | 7 | class OPS_OT_AddNewAttribute(Operator): 8 | bl_idname = "character_ui.add_new_attribute" 9 | bl_label = "Select object to trigger shape key change for:" 10 | bl_description = "Sets the shape key to be toggled on/off based on an outfit piece" 11 | bl_options = {"INTERNAL"} 12 | 13 | panel_name: StringProperty() 14 | group_name: StringProperty() 15 | parent_path: StringProperty() 16 | 17 | def execute(self, context): 18 | ch = context.scene.character_ui_object 19 | if ch: 20 | try: 21 | bpy.ops.ui.copy_data_path_button(full_path=True) 22 | except: 23 | self.report( 24 | {'WARNING'}, "Couldn't get path, invalid selection!") 25 | return {'CANCELLED'} 26 | 27 | path = context.window_manager.clipboard 28 | 29 | button_prop = context.button_prop 30 | name = False 31 | try: 32 | name = button_prop.name 33 | except: 34 | name = False 35 | rig_id = ch.data[context.scene.character_ui_object_id] 36 | attributes_key = "CharacterUI_att_%s" % (rig_id) 37 | 38 | if self.parent_path not in ["", " "]: # syncing attribtues 39 | driver_id = eval( 40 | self.parent_path[:self.parent_path.index(']')+1]) 41 | driver_id_path = self.parent_path[self.parent_path.index( 42 | ']')+2:] 43 | driver_path = path[path.rindex('.')+1:] 44 | parent_prop = eval( 45 | self.parent_path[:self.parent_path.rindex(".")]) 46 | prop = eval(path[:path.rindex('.')]) 47 | if parent_prop.bl_rna == prop.bl_rna: 48 | err = CharacterUIAttributesOperatorsUtils.create_driver( 49 | driver_id, prop, driver_path, "chui_value", [{"name": "chui_value", "path": driver_id_path}]) 50 | if err: 51 | self.report( 52 | {'ERROR'}, "Could not create driver and sync the attributes!") 53 | return {"CANCELLED"} 54 | name = "Default Value" 55 | for g in ch[attributes_key][self.panel_name]: 56 | if g["name"] == self.group_name: 57 | for att in g["attributes"]: 58 | if att["path"] == self.parent_path: 59 | if hasattr(att["synced"], "append"): 60 | synced = att["synced"] 61 | synced.append(path) 62 | att["synced"] = synced 63 | else: 64 | att["synced"] = [path] 65 | if att["name"]: 66 | name = att["name"] 67 | self.report({"INFO"}, "Synced %s to %s" % 68 | (prop.name, name)) 69 | else: 70 | self.report( 71 | {"ERROR"}, "Attributes have different data types!") 72 | return{"CANCELLED"} 73 | else: # adding new attribute 74 | for g in ch[attributes_key][self.panel_name]: 75 | if g["name"] == self.group_name: 76 | if hasattr(g["attributes"], "append"): 77 | att = g["attributes"] 78 | att.append( 79 | {"name": name, "path": path, "synced": []}) 80 | g["attributes"] = att 81 | else: 82 | g["attributes"] = [ 83 | {"name": name, "path": path, "synced": []}] 84 | 85 | self.panel_name = "" 86 | self.parent_path = "" 87 | return {'FINISHED'} 88 | 89 | 90 | class OPS_OT_RemoveAttribute(Operator): 91 | bl_idname = 'character_ui.remove_attribute' 92 | bl_label = 'Remove attribute from the UI' 93 | bl_description = "Removes attribute from the UI and other synced attributes too" 94 | bl_options = {"INTERNAL"} 95 | 96 | path: StringProperty() 97 | panel_name: StringProperty() 98 | group_name: StringProperty() 99 | 100 | def invoke(self, context, event): 101 | return context.window_manager.invoke_confirm(self, event) 102 | 103 | def execute(self, context): 104 | ch = context.scene.character_ui_object 105 | if ch: 106 | rig_id = ch.data[context.scene.character_ui_object_id] 107 | attributes_key = "CharacterUI_att_%s" % (rig_id) 108 | if self.panel_name in ch[attributes_key]: 109 | for g in ch[attributes_key][self.panel_name]: 110 | if g["name"] == self.group_name: 111 | att = g["attributes"] 112 | new_att = [] 113 | for a in att: 114 | if a['path'] != self.path: 115 | new_att.append(a) 116 | g["attributes"] = new_att 117 | self.report({"INFO"}, 'Removed property') 118 | return {'FINISHED'} 119 | 120 | 121 | class OPS_OT_AttributeChangePosition(Operator): 122 | bl_idname = 'character_ui.attribute_change_position' 123 | bl_label = "Change attributes position in the list" 124 | bl_description = "Changes position of the attribute in the current list" 125 | bl_options = {"INTERNAL"} 126 | 127 | path: StringProperty() 128 | panel_name: StringProperty() 129 | direction: BoolProperty() # True moves up, False move down 130 | group_name: StringProperty() 131 | 132 | def execute(self, context): 133 | ch = context.scene.character_ui_object 134 | if ch: 135 | rig_id = ch.data[context.scene.character_ui_object_id] 136 | attributes_key = "CharacterUI_att_%s" % (rig_id) 137 | if self.panel_name in ch[attributes_key]: 138 | for g in ch[attributes_key][self.panel_name]: 139 | if g["name"] == self.group_name: 140 | att = g["attributes"] 141 | i = 0 142 | for a in enumerate(att): 143 | if a[1]['path'] == self.path: 144 | i = a[0] 145 | if self.direction and i-1 >= 0: # move attribute up in the list 146 | prev = att[i-1] 147 | att[i-1] = att[i] 148 | att[i] = prev 149 | self.report( 150 | {'INFO'}, "Moved attribute up in the list") 151 | elif not self.direction and i+1 < len(att): 152 | next = att[i+1] 153 | att[i+1] = att[i] 154 | att[i] = next 155 | self.report( 156 | {'INFO'}, "Moved attribute down in the list") 157 | g["attributes"] = att 158 | return {'FINISHED'} 159 | 160 | 161 | class CharacterUIAttributesOperatorsUtils(): 162 | @staticmethod 163 | def create_driver(driver_id, driver_target, driver_path, driver_expression, variables): 164 | "TODO: same exact code is in the add-on, make it that it's only once in the whole codebase" 165 | 166 | try: 167 | driver_target.driver_remove(driver_path) 168 | driver = driver_target.driver_add(driver_path) 169 | except: 170 | return True 171 | 172 | def setup_driver(driver, addition_path=""): 173 | driver.type = "SCRIPTED" 174 | driver.expression = driver_expression 175 | for variable in variables: 176 | var = driver.variables.new() 177 | var.name = variable["name"] 178 | var.targets[0].id_type = driver_id.rna_type.name.upper() 179 | var.targets[0].id = variable["driver_id"] if "driver_id" in variable else driver_id 180 | var.targets[0].data_path = "%s%s" % ( 181 | variable["path"], addition_path) 182 | if type(driver) == list: 183 | for d in enumerate(driver): 184 | setup_driver(d[1].driver, "[%i]" % (d[0])) 185 | else: 186 | setup_driver(driver.driver) 187 | 188 | @staticmethod 189 | def sync_attribute_to_parent(attributes, parent_path, path, prop): 190 | "adds data path " 191 | for i in range(len(attributes)): 192 | if attributes[i]['path'] == parent_path: 193 | if 'synced' in attributes[i]: 194 | # this thing is so unnecessary but I couldn't find a better solution, no matter what I did I couldn't add new attributes 195 | syn = attributes[i]['synced'] 196 | if not syn: 197 | syn = [] 198 | syn.append({"path": path, "prop": prop}) 199 | attributes[i]['synced'] = syn 200 | else: 201 | syn = [] # here is it the same as few lines up 202 | syn.append({"path": path, "prop": prop}) 203 | attributes[i]['synced'] = syn 204 | return attributes 205 | 206 | 207 | class OPS_OT_EditAttribute(Operator): 208 | bl_idname = "character_ui.edit_attribute" 209 | bl_label = 'Edit attribute' 210 | bl_description = 'Edit attribute' 211 | bl_options = {"INTERNAL"} 212 | 213 | path: StringProperty(name="Path", description="RNA path of the attribute") 214 | panel_name: StringProperty() 215 | group_name: StringProperty() 216 | attribute_name: StringProperty(name="Name") 217 | invert_checkbox: BoolProperty(description="Forces checkbox to be inverted") 218 | toggle: BoolProperty(description="Style checkbox as a toggle") 219 | slider: BoolProperty(description="Use slider widget for numeric values") 220 | emboss: BoolProperty(description="Draw the button itself, not just the icon/text") 221 | icon: StringProperty(description="Override automatic icon of the item") 222 | 223 | def invoke(self, context, event): 224 | ch = context.scene.character_ui_object 225 | rig_id = ch.data[context.scene.character_ui_object_id] 226 | attributes_key = "CharacterUI_att_%s" % (rig_id) 227 | if attributes_key in ch: 228 | if self.panel_name in ch[attributes_key]: 229 | for g in ch[attributes_key][self.panel_name]: 230 | if self.group_name == g["name"]: 231 | for att in g["attributes"]: 232 | if att["path"] == self.path: 233 | self.attribute_name = att["name"] if att["name"] else "Default Value" 234 | self.invert_checkbox = att["invert_checkbox"] if "invert_checkbox" in att else False 235 | self.toggle = att["toggle"] if "toggle" in att else False 236 | self.slider = att["slider"] if "slider" in att else False 237 | self.emboss = att["emboss"] if "emboss" in att else True 238 | self.icon = ("" if att["icon"] == "NONE" else att["icon"]) if "icon" in att else "" 239 | return context.window_manager.invoke_props_dialog(self, width=750) 240 | 241 | def draw(self, context): 242 | ch = context.scene.character_ui_object 243 | rig_id = ch.data[context.scene.character_ui_object_id] 244 | attributes_key = "CharacterUI_att_%s" % (rig_id) 245 | if attributes_key in ch: 246 | if self.panel_name in ch[attributes_key]: 247 | for g in ch[attributes_key][self.panel_name]: 248 | if self.group_name == g["name"]: 249 | for att in g["attributes"]: 250 | if att["path"] == self.path: 251 | prop_exists = True 252 | try: 253 | eval(att["path"]) 254 | except: 255 | prop_exists = False 256 | 257 | if prop_exists: 258 | layout = self.layout 259 | layout.prop(self, "attribute_name") 260 | layout.label(text=self.path) 261 | style_box = layout.box() 262 | style_box.label(text="Style") 263 | row = style_box.row() 264 | 265 | row.prop(self, "invert_checkbox", text="Invert checkbox", toggle=True) 266 | row.prop(self, "toggle", text="Toggle", toggle=True) 267 | row.prop(self, "slider", text="Slider", toggle=True) 268 | row.prop(self, "emboss", text="Emboss", toggle=True) 269 | icon_row = style_box.row(align=True) 270 | icon_row.prop(self, "icon", text="Icon") 271 | icon_row.operator("character_ui.tooltip", text="", icon="QUESTION").tooltip_id = "icons" 272 | 273 | try: 274 | style_box.label( 275 | text="- Icon Preview", icon=self.icon) 276 | except: 277 | style_box.label( 278 | text="This icon does not exist - Icon Preview") 279 | 280 | if "synced" in att and len(att["synced"]): 281 | synced_box = layout.box() 282 | synced_box.label( 283 | text="Synced attributes", icon="LINK_BLEND") 284 | for s in att["synced"]: 285 | synced_row = synced_box.row() 286 | synced_row.label(text=s) 287 | remove_op = synced_row.operator( 288 | OPS_OT_RemoveSyncedAttribute.bl_idname, icon="X") 289 | remove_op.path = s 290 | remove_op.parent_path = self.path 291 | remove_op.panel_name = self.panel_name 292 | remove_op.group_name = self.group_name 293 | else: 294 | layout = self.layout 295 | layout.label( 296 | text="Invalid attribute", icon="ERROR") 297 | op = layout.operator( 298 | OPS_OT_RemoveAttribute.bl_idname, icon="X", text="Remove attribute") 299 | op.path = self.path 300 | op.panel_name = self.panel_name 301 | op.group_name = self.group_name 302 | 303 | def execute(self, context): 304 | ch = context.scene.character_ui_object 305 | rig_id = ch.data[context.scene.character_ui_object_id] 306 | attributes_key = "CharacterUI_att_%s" % (rig_id) 307 | if attributes_key in ch: 308 | if self.panel_name in ch[attributes_key]: 309 | for g in ch[attributes_key][self.panel_name]: 310 | if self.group_name == g["name"]: 311 | for att in g["attributes"]: 312 | if att["path"] == self.path: 313 | if self.attribute_name not in ["", " "]: 314 | att["name"] = self.attribute_name 315 | att["invert_checkbox"] = self.invert_checkbox 316 | att["toggle"] = self.toggle 317 | att["slider"] = self.slider 318 | if self.icon not in ["", " "]: 319 | att["icon"] = self.icon 320 | else: 321 | att["icon"] = "NONE" 322 | att["emboss"] = self.emboss 323 | 324 | return{"FINISHED"} 325 | 326 | 327 | class OPS_OT_RemoveSyncedAttribute(Operator): 328 | bl_idname = "character_ui.remove_synced_attribute" 329 | bl_label = "" 330 | bl_description = "Removes synced attribute" 331 | bl_options = {"INTERNAL"} 332 | 333 | path: StringProperty() 334 | parent_path: StringProperty() 335 | panel_name: StringProperty() 336 | group_name: StringProperty() 337 | 338 | def execute(self, context): 339 | ch = context.scene.character_ui_object 340 | rig_id = ch.data[context.scene.character_ui_object_id] 341 | attributes_key = "CharacterUI_att_%s" % (rig_id) 342 | if attributes_key in ch: 343 | if self.panel_name in ch[attributes_key]: 344 | for g in ch[attributes_key][self.panel_name]: 345 | if self.group_name == g["name"]: 346 | for att in g["attributes"]: 347 | if att["path"] == self.parent_path: 348 | prop = eval(self.path[:self.path.rindex('.')]) 349 | driver_path = self.path[self.path.rindex('.')+1:] 350 | prop.driver_remove(driver_path) 351 | att["synced"] = [item for item in att["synced"] if item != self.path] 352 | return{"FINISHED"} 353 | 354 | 355 | classes = [ 356 | OPS_OT_AddNewAttribute, 357 | OPS_OT_RemoveAttribute, 358 | OPS_OT_AttributeChangePosition, 359 | OPS_OT_EditAttribute, 360 | OPS_OT_RemoveSyncedAttribute 361 | ] 362 | 363 | 364 | def register(): 365 | for c in classes: 366 | register_class(c) 367 | 368 | 369 | def unregister(): 370 | for c in reversed(classes): 371 | unregister_class(c) 372 | -------------------------------------------------------------------------------- /operators/edit_outfit_piece.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Operator) 3 | from bpy.props import (PointerProperty, StringProperty, EnumProperty) 4 | from bpy.utils import (register_class, unregister_class) 5 | from . import format_outfit_piece_name 6 | 7 | class OPS_OT_EditOutfitPiece(Operator): 8 | bl_idname = "character_ui.edit_outfit_piece" 9 | bl_label = "Edit Outfit Piece" 10 | bl_description = "Set outfit piece properties" 11 | bl_options = {"INTERNAL"} 12 | 13 | prefix : StringProperty(name="Prefix", description="If the outfit piece contains this prefix it will remove it for the buttons in the generated UI.") 14 | 15 | 16 | def invoke(self, context, event): 17 | outfit_collection = context.scene.character_ui_outfits_collection.children[context.scene.character_ui_active_outfit_index] 18 | outfit_piece_index = context.scene.character_ui_active_outfit_piece_index 19 | if outfit_piece_index < len(outfit_collection.objects): 20 | outfit_piece = outfit_collection.objects[outfit_piece_index] 21 | context.scene.character_ui_outfit_piece_parent = outfit_piece.parent 22 | if "chui_outfit_piece_settings" in outfit_piece: 23 | if "prefix" in outfit_piece["chui_outfit_piece_settings"]: 24 | self.prefix = outfit_piece["chui_outfit_piece_settings"]["prefix"] 25 | return context.window_manager.invoke_props_dialog(self, width=350) 26 | return {"FINISHED"} 27 | 28 | 29 | def draw(self, context): 30 | layout = self.layout 31 | row = layout.row(align=True) 32 | row.prop(context.scene, "character_ui_outfit_piece_parent") 33 | row.operator("character_ui.tooltip", text="", icon="QUESTION").tooltip_id = "outfit_piece_parent" 34 | 35 | toggle_box = layout.box() 36 | toggle_box.label(text="UI toggle style") 37 | toggle_box.prop(self, "prefix") 38 | 39 | 40 | def execute(self, context): 41 | outfit_collection = context.scene.character_ui_outfits_collection.children[context.scene.character_ui_active_outfit_index] 42 | outfit_piece = outfit_collection.objects[context.scene.character_ui_active_outfit_piece_index] 43 | outfit_piece.parent = context.scene.character_ui_outfit_piece_parent 44 | format_outfit_piece_name.format_name(outfit_piece, self.prefix) 45 | return {"FINISHED"} 46 | 47 | 48 | classes = [ 49 | OPS_OT_EditOutfitPiece 50 | ] 51 | 52 | 53 | def register(): 54 | bpy.types.Scene.character_ui_outfit_piece_parent = PointerProperty( 55 | name="Parent", 56 | description="Parent of the outfit piece", 57 | type=bpy.types.Object) 58 | for c in classes: 59 | register_class(c) 60 | 61 | 62 | def unregister(): 63 | del bpy.types.Scene.character_ui_outfit_piece_parent 64 | for c in reversed(classes): 65 | unregister_class(c) 66 | -------------------------------------------------------------------------------- /operators/edit_visibility.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Operator, PropertyGroup) 3 | from bpy.props import (PointerProperty, StringProperty, 4 | BoolProperty, CollectionProperty, IntProperty) 5 | from bpy.utils import (register_class, unregister_class) 6 | 7 | 8 | class VisibilityVariableItem(PropertyGroup): 9 | variable: StringProperty(name="Variable") 10 | data_path: StringProperty(name="Data Path") 11 | 12 | 13 | class OPS_OT_EditVisibilityVariables(Operator): 14 | bl_idname = "character_ui.edit_visibility_variables" 15 | bl_label = "" 16 | bl_description = "Edits visibility" 17 | bl_options = {"INTERNAL"} 18 | 19 | path: StringProperty(name="Path", description="RNA path of the attribute") 20 | panel_name: StringProperty() 21 | group_name: StringProperty() 22 | variables: CollectionProperty(type=VisibilityVariableItem) 23 | expression: StringProperty(name="Expression") 24 | 25 | def execute(self, context): 26 | ch = context.scene.character_ui_object 27 | if ch: 28 | rig_id = ch.data[context.scene.character_ui_object_id] 29 | attributes_key = "CharacterUI_att_%s" % (rig_id) 30 | if self.panel_name in ch[attributes_key]: 31 | for g in ch[attributes_key][self.panel_name]: 32 | if g["name"] == self.group_name: 33 | if self.path == "": 34 | if "visibility" in g: 35 | if self.expression not in ["", " "]: 36 | g["visibility"]["expression"] = self.expression 37 | new_vars = [] 38 | for var in context.scene.character_ui_variables: 39 | if var.variable not in ["", " "] and var.data_path not in ["", " "]: 40 | new_vars.append({"variable": var.variable, "data_path": var.data_path}) 41 | g["visibility"]["variables"] = new_vars 42 | else: 43 | del g["visibility"] 44 | else: 45 | for att in g["attributes"]: 46 | if self.path == att["path"]: 47 | if "visibility" in att: 48 | if self.expression not in ["", " "]: 49 | att["visibility"]["expression"] = self.expression 50 | new_vars = [] 51 | for var in context.scene.character_ui_variables: 52 | if var.variable not in ["", " "] and var.data_path not in ["", " "]: 53 | new_vars.append({"variable": var.variable, "data_path": var.data_path}) 54 | att["visibility"]["variables"] = new_vars 55 | else: 56 | del att["visibility"] 57 | return {"FINISHED"} 58 | def invoke(self, context, event): 59 | ch = context.scene.character_ui_object 60 | context.scene.character_ui_variables.clear() 61 | if ch: 62 | rig_id = ch.data[context.scene.character_ui_object_id] 63 | self.expression = "" 64 | attributes_key = "CharacterUI_att_%s" % (rig_id) 65 | if self.panel_name in ch[attributes_key]: 66 | for g in ch[attributes_key][self.panel_name]: 67 | if g["name"] == self.group_name: 68 | if self.path == "": 69 | if "visibility" in g: 70 | self.expression = g["visibility"]["expression"] 71 | for var in g["visibility"]["variables"]: 72 | collection_var = context.scene.character_ui_variables.add() 73 | collection_var.variable = var["variable"] 74 | collection_var.data_path = var["data_path"] 75 | else: 76 | for att in g["attributes"]: 77 | if self.path == att["path"]: 78 | if "visibility" in att: 79 | if "expression" in att["visibility"]: 80 | self.expression = att["visibility"]["expression"] 81 | for var in att["visibility"]["variables"]: 82 | collection_var = context.scene.character_ui_variables.add() 83 | collection_var.variable = var["variable"] 84 | collection_var.data_path = var["data_path"] 85 | 86 | return context.window_manager.invoke_props_dialog(self, width=450) 87 | 88 | def draw(self, context): 89 | layout = self.layout 90 | expression_row = layout.row(align=True) 91 | expression_row.prop(self, "expression") 92 | expression_row.operator("character_ui.tooltip", text="", icon="QUESTION").tooltip_id = "character_ui_expression" 93 | for var in enumerate(context.scene.character_ui_variables): 94 | box = layout.box() 95 | box.prop(var[1], "variable") 96 | box.prop(var[1], "data_path") 97 | remove_op = box.operator(OPS_OT_RemoveVariable.bl_idname, icon="X") 98 | remove_op.panel_name = self.panel_name 99 | remove_op.group_name = self.group_name 100 | remove_op.path = self.path 101 | remove_op.var_id = var[0] 102 | 103 | add_var = layout.operator(OPS_OT_AddNewVariable.bl_idname) 104 | add_var.panel_name = self.panel_name 105 | add_var.group_name = self.group_name 106 | add_var.path = self.path 107 | 108 | 109 | class OPS_OT_AddNewVariable(Operator): 110 | bl_idname = "character_ui.add_new_variable" 111 | bl_label = "Add new variable" 112 | bl_description = "Adds new variable" 113 | bl_options = {"INTERNAL"} 114 | 115 | path: StringProperty(name="Path", description="RNA path of the attribute") 116 | panel_name: StringProperty() 117 | group_name: StringProperty() 118 | 119 | def execute(self, context): 120 | ch = context.scene.character_ui_object 121 | if ch: 122 | rig_id = ch.data[context.scene.character_ui_object_id] 123 | attributes_key = "CharacterUI_att_%s" % (rig_id) 124 | if self.panel_name in ch[attributes_key]: 125 | for g in ch[attributes_key][self.panel_name]: 126 | if g["name"] == self.group_name: 127 | if self.path == "": 128 | if "visibility" in g: 129 | variables = g["visibility"]["variables"] 130 | try: 131 | variables.append( 132 | {"variable": "var", "data_path": ""}) 133 | except: 134 | variables.to_list().append( 135 | {"variable": "var", "data_path": ""}) 136 | g["visibility"]["variables"] = variables 137 | else: 138 | g["visibility"] = {"expression": "", "variables": [ 139 | {"variable": "var", "data_path": ""}]} 140 | new_var = context.scene.character_ui_variables.add() 141 | new_var.variable = "var" 142 | new_var.data_path = "" 143 | else: 144 | for att in g["attributes"]: 145 | if self.path == att["path"]: 146 | if "visibility" in att: 147 | variables = [] 148 | if "variables" in att["visibility"]: 149 | variables = att["visibility"]["variables"] 150 | try: 151 | variables.append( 152 | {"variable": "var", "data_path": ""}) 153 | except: 154 | variables.to_list().append( 155 | {"variable": "var", "data_path": ""}) 156 | else: 157 | variables.append({"variable": "var", "data_path": ""}) 158 | att["visibility"]["variables"] = variables 159 | else: 160 | att["visibility"] = {"expression": "", "variables": [ 161 | {"variable": "var", "data_path": ""}]} 162 | new_var = context.scene.character_ui_variables.add() 163 | new_var.variable = "var" 164 | new_var.data_path = "" 165 | 166 | return {"FINISHED"} 167 | 168 | 169 | class OPS_OT_RemoveVariable(Operator): 170 | bl_idname = "character_ui.remove_variable" 171 | bl_label = "Remove variable" 172 | bl_description = "Removes variable" 173 | bl_options = {"INTERNAL"} 174 | 175 | path: StringProperty(name="Path", description="RNA path of the attribute") 176 | panel_name: StringProperty() 177 | group_name: StringProperty() 178 | var_id: IntProperty() 179 | 180 | def execute(self, context): 181 | ch = context.scene.character_ui_object 182 | if ch: 183 | rig_id = ch.data[context.scene.character_ui_object_id] 184 | attributes_key = "CharacterUI_att_%s" % (rig_id) 185 | if self.panel_name in ch[attributes_key]: 186 | for g in ch[attributes_key][self.panel_name]: 187 | if g["name"] == self.group_name: 188 | if self.path == "": 189 | if "visibility" in g: 190 | context.scene.character_ui_variables.remove( 191 | self.var_id) 192 | new_vars = g["visibility"]["variables"] 193 | if self.var_id < len(new_vars): 194 | del new_vars[self.var_id] 195 | g["visibility"]["variables"] = new_vars 196 | else: 197 | for att in g["attributes"]: 198 | if self.path == att["path"]: 199 | if "visibility" in att: 200 | context.scene.character_ui_variables.remove( 201 | self.var_id) 202 | new_vars = att["visibility"]["variables"] 203 | if self.var_id < len(new_vars): 204 | del new_vars[self.var_id] 205 | att["visibility"]["variables"] = new_vars 206 | return {"FINISHED"} 207 | 208 | classes = [ 209 | VisibilityVariableItem, 210 | OPS_OT_AddNewVariable, 211 | OPS_OT_EditVisibilityVariables, 212 | OPS_OT_RemoveVariable 213 | ] 214 | 215 | 216 | def register(): 217 | for c in classes: 218 | register_class(c) 219 | bpy.types.Scene.character_ui_variables = CollectionProperty( 220 | type=VisibilityVariableItem) 221 | 222 | 223 | def unregister(): 224 | del bpy.types.Scene.character_ui_variables 225 | for c in reversed(classes): 226 | unregister_class(c) 227 | -------------------------------------------------------------------------------- /operators/fix_new_id.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Operator) 3 | from bpy.utils import (register_class, unregister_class) 4 | 5 | 6 | class OPS_OT_FixNewId(Operator): 7 | bl_idname = "character_ui.fix_new_id" 8 | bl_label = "Fix" 9 | bl_description = "After new id has been generated outside of Character UI's control you need to fix the character." 10 | bl_options = {"INTERNAL"} 11 | 12 | def execute(self, context): 13 | ch = context.scene.character_ui_object 14 | if "hair_collection" not in ch.data: 15 | ch.data["hair_collection"] = context.scene.character_ui_hair_collection 16 | if "outfits_collection" not in ch.data: 17 | ch.data["outfits_collection"] = context.scene.character_ui_outfits_collection 18 | if "character_ui_cages" not in ch.data: 19 | ch.data["character_ui_cages"] = {} 20 | if "collection" not in ch.data["character_ui_cages"]: 21 | ch.data["character_ui_cages"]["collection"] = context.scene.character_ui_physics_collection 22 | 23 | character_id_key = context.scene.character_ui_object_id 24 | character_id = ch.data[character_id_key] 25 | context.scene.character_ui_object_id_value = character_id 26 | self.report({"INFO"}, "Fixed character") 27 | 28 | return {"FINISHED"} 29 | 30 | 31 | classes = [ 32 | OPS_OT_FixNewId 33 | ] 34 | 35 | 36 | def register(): 37 | for c in classes: 38 | register_class(c) 39 | 40 | 41 | def unregister(): 42 | for c in reversed(classes): 43 | unregister_class(c) 44 | -------------------------------------------------------------------------------- /operators/format_outfit_piece_name.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Operator) 3 | from bpy.props import (PointerProperty, StringProperty, BoolProperty) 4 | from bpy.utils import (register_class, unregister_class) 5 | 6 | 7 | class OPS_OT_FormatOutfitPieceName(Operator): 8 | bl_idname = "character_ui.format_outfit_piece_name" 9 | bl_label = "Format Outfit Piece name" 10 | bl_description = "Format the label of outfit piece" 11 | bl_options = {"INTERNAL"} 12 | 13 | 14 | prefix : StringProperty(name="Prefix", description="If the outfit piece contains this prefix it will remove it for the buttons in the generated UI.") 15 | whole_outfit : BoolProperty(default=True) 16 | 17 | def invoke(self, context, event): 18 | if self.whole_outfit: 19 | return context.window_manager.invoke_props_dialog(self, width=350) 20 | return 21 | 22 | def execute(self, context): 23 | outfit_collection = context.scene.character_ui_outfits_collection.children[context.scene.character_ui_active_outfit_index] 24 | if self.whole_outfit: 25 | for o in outfit_collection.objects: 26 | format_name(o, self.prefix) 27 | l = len(outfit_collection.objects) 28 | self.report({"INFO"}, "Changed format of %i outfit piece%s"%(l, "s" if l > 1 else "")) 29 | 30 | return {"FINISHED"} 31 | def draw(self, context): 32 | self.layout.prop(self, "prefix"); 33 | 34 | 35 | def format_name(outfit_piece, prefix): 36 | if "chui_outfit_piece_settings"not in outfit_piece: 37 | outfit_piece["chui_outfit_piece_settings"] = {} 38 | outfit_piece["chui_outfit_piece_settings"]["prefix"] = prefix 39 | 40 | 41 | classes = [ 42 | OPS_OT_FormatOutfitPieceName 43 | ] 44 | 45 | 46 | def register(): 47 | for c in classes: 48 | register_class(c) 49 | 50 | 51 | def unregister(): 52 | for c in reversed(classes): 53 | unregister_class(c) 54 | -------------------------------------------------------------------------------- /operators/generate_character_ui_script.py: -------------------------------------------------------------------------------- 1 | import os 2 | import bpy 3 | from datetime import datetime 4 | from bpy.types import (Operator) 5 | from bpy.props import (StringProperty, BoolProperty) 6 | from bpy.utils import (register_class, unregister_class) 7 | 8 | 9 | class OPS_OT_GenerateScript(Operator): 10 | "Generates script, executes it and adds it to the character's custom props" 11 | bl_idname = 'characterui_generate.generate_script' 12 | bl_label = 'Generate UI' 13 | bl_description = 'Generates the UI script' 14 | bl_options = {"INTERNAL"} 15 | 16 | character_id: StringProperty() 17 | character_id_key: StringProperty() 18 | rig_layers_key: StringProperty() 19 | always_show: BoolProperty() 20 | custom_label: StringProperty() 21 | 22 | def invoke(self, context, event): 23 | return context.window_manager.invoke_confirm(self, event) 24 | 25 | def execute(self, context): 26 | if self.character_id: 27 | text = load_ui_script(context, self.character_id, 28 | self.character_id_key, self.rig_layers_key, self.always_show, self.custom_label) 29 | for o in bpy.data.objects: 30 | if str(type(o.data)) != "": 31 | if self.character_id_key in o.data: 32 | if o.data[self.character_id_key] == self.character_id: 33 | o.data["character_ui_generation_date"] = datetime.today().strftime('%Y-%m-%d-%H:%M:%S') 34 | if "character_ui_char_version" not in o.data: 35 | o.data["character_ui_char_version"] = "" 36 | o.data["character_ui_textfile"] = text 37 | self.report({"INFO"}, "Generated script") 38 | else: 39 | self.report({"ERROR"}, "You need to generate rig id!") 40 | return {"FAILED"} 41 | return {'FINISHED'} 42 | 43 | 44 | def load_ui_script(context, character_id, character_id_key, rig_layers_key, always_show, custom_label): 45 | 46 | file_name = "%s.py" % (character_id) 47 | text = bpy.data.texts.get(file_name) 48 | # check if file already exists 49 | if not text: 50 | text = bpy.data.texts.new(name=file_name) 51 | text.use_fake_user = False 52 | 53 | text.clear() # clear text 54 | text.use_module = True # enable Register 55 | 56 | file_path = os.path.dirname(os.path.realpath(__file__)) 57 | 58 | readfile = open(os.path.join(file_path, "../character_ui.py"), 'r') 59 | for line in readfile: 60 | text.write(line) 61 | if line == "# script variables\n": 62 | text.write("character_id_key=\"%s\"\n" % (character_id_key)) 63 | text.write("character_id=\"%s\"\n" % (character_id)) 64 | text.write("rig_layers_key=\"%s\"\n" % (rig_layers_key)) 65 | text.write("links_key=\"%s\"\n" % (context.scene.character_ui_links_key)) 66 | text.write("custom_label=\"%s\"\n" % (custom_label)) 67 | text.write("always_show=%s\n" % (str(always_show))) 68 | 69 | readfile.close() 70 | 71 | # Run UI script 72 | exec(text.as_string(), {}) 73 | return text 74 | 75 | 76 | def register(): 77 | register_class(OPS_OT_GenerateScript) 78 | 79 | 80 | def unregister(): 81 | unregister_class(OPS_OT_GenerateScript) 82 | -------------------------------------------------------------------------------- /operators/links.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Operator) 3 | from bpy.props import (StringProperty) 4 | from bpy.utils import (register_class, unregister_class) 5 | 6 | 7 | class OPS_OT_AddLink(Operator): 8 | bl_idname = "character_ui.add_link" 9 | bl_label = "Add new link" 10 | bl_description = "Adds new link" 11 | bl_options = {"INTERNAL"} 12 | 13 | link_section: StringProperty() 14 | link_text: StringProperty(name="Button Text") 15 | link_icon: StringProperty(name="Button Icon") 16 | link_url: StringProperty(name="URL Address") 17 | 18 | def execute(self, context): 19 | ch = context.scene.character_ui_object 20 | key = context.scene.character_ui_links_key 21 | if ch and key: 22 | if key in ch.data: 23 | if self.link_section in ch.data[key]: 24 | s = ch.data[key][self.link_section] 25 | if self.link_text != s: 26 | s[self.link_text] = (self.link_icon, self.link_url) 27 | else: 28 | self.report({"WARNING"}, "Duplicate link name") 29 | return {"CANCELLED"} 30 | return {"FINISHED"} 31 | 32 | def invoke(self, context, event): 33 | return context.window_manager.invoke_props_dialog(self, width=350) 34 | 35 | def draw(self, context): 36 | icon_row = self.layout.row(align=True) 37 | icon_row.prop(self, 'link_icon') 38 | icon_row.operator("character_ui.tooltip", text="", icon="QUESTION").tooltip_id = "icons" 39 | try: 40 | self.layout.label(text="- Icon Preview", icon=self.link_icon) 41 | except: 42 | self.layout.label(text="This icon does not exist - Icon Preview") 43 | self.layout.prop(self, 'link_text') 44 | self.layout.prop(self, 'link_url') 45 | 46 | 47 | class OPS_OT_RemoveLink(Operator): 48 | bl_idname = "character_ui.remove_link" 49 | bl_label = "Remove link" 50 | bl_description = "Removes link" 51 | bl_options = {"INTERNAL"} 52 | 53 | link_section: StringProperty() 54 | link: StringProperty() 55 | 56 | def invoke(self, context, event): 57 | return context.window_manager.invoke_confirm(self, event) 58 | 59 | def execute(self, context): 60 | ch = context.scene.character_ui_object 61 | key = context.scene.character_ui_links_key 62 | if ch and key: 63 | if key in ch.data: 64 | if self.link_section in ch.data[key]: 65 | del ch.data[key][self.link_section][self.link] 66 | self.report({"INFO"}, "Removed Link") 67 | return {"FINISHED"} 68 | 69 | 70 | class OPS_OT_EnableLinks(Operator): 71 | bl_idname = "character_ui.enable_links" 72 | bl_label = "Enable Links Sections" 73 | bl_description = "Adds custom property to the rig which allows you to create links sections containing links to your social media, websites, etc." 74 | 75 | def execute(self, context): 76 | ch = context.scene.character_ui_object 77 | key = context.scene.character_ui_links_key 78 | if ch and key: 79 | if key not in ["", " "]: 80 | ch.data[key] = {} 81 | self.report({"INFO"}, "Enabled Links Sections") 82 | return {"FINISHED"} 83 | self.report({"ERROR"}, "Could not enable links") 84 | return {"CANCELLED"} 85 | 86 | 87 | class OPS_OT_RemoveLinksSection(Operator): 88 | bl_idname = 'character_ui.remove_links_section' 89 | bl_label = "Remove section" 90 | bl_description = "Removes links section" 91 | bl_options = {"INTERNAL"} 92 | 93 | link_section: StringProperty() 94 | 95 | def invoke(self, context, event): 96 | return context.window_manager.invoke_confirm(self, event) 97 | 98 | def execute(self, context): 99 | ch = context.scene.character_ui_object 100 | key = context.scene.character_ui_links_key 101 | if ch and key: 102 | if key in ch.data: 103 | if self.link_section != "": # remove section 104 | new_sections = {} 105 | for s in ch.data[key].to_dict(): 106 | if s != self.link_section: 107 | new_sections[s] = ch.data[key][s] 108 | ch.data[key] = new_sections 109 | self.report({"INFO"}, "Removed links section") 110 | return {"FINISHED"} 111 | 112 | 113 | class OPS_OT_AddLinksSection(Operator): 114 | bl_idname = "character_ui.add_links_section" 115 | bl_label = "Add Links Section" 116 | bl_description = "Adds links section" 117 | 118 | link_section_name: StringProperty(name="Name") 119 | 120 | def execute(self, context): 121 | ch = context.scene.character_ui_object 122 | key = context.scene.character_ui_links_key 123 | if ch and key: 124 | if key in ch.data: 125 | if self.link_section_name != "" and self.link_section_name not in ch.data[key]: 126 | ch.data[key][self.link_section_name] = {} 127 | self.report({"INFO"}, "Added links section") 128 | else: 129 | self.report( 130 | {"WARNING"}, "Section with this name already exists!") 131 | return {"CANCELLED"} 132 | return {"FINISHED"} 133 | return {"CANCELED"} 134 | 135 | def invoke(self, context, event): 136 | return context.window_manager.invoke_props_dialog(self, width=350) 137 | 138 | def draw(self, context): 139 | self.layout.prop(self, 'link_section_name') 140 | 141 | 142 | class OPS_OT_EditLinksSection(Operator): 143 | bl_idname = "character_ui.edit_links_section" 144 | bl_label = "Edit Links Section" 145 | bl_description = "Edits links section" 146 | 147 | link_section: StringProperty() 148 | link_section_name: StringProperty(name="New Name") 149 | 150 | def invoke(self, context, event): 151 | self.link_section_name = self.link_section 152 | return context.window_manager.invoke_props_dialog(self, width=350) 153 | 154 | def draw(self, context): 155 | self.layout.prop(self, 'link_section_name') 156 | 157 | def execute(self, context): 158 | ch = context.scene.character_ui_object 159 | key = context.scene.character_ui_links_key 160 | if ch and key: 161 | if key in ch.data: 162 | if self.link_section_name not in ch.data[key].to_dict(): 163 | new_sections = {} 164 | for s in ch.data[key].to_dict(): 165 | if s != self.link_section: 166 | new_sections[s] = ch.data[key][s] 167 | else: 168 | new_sections[self.link_section_name] = ch.data[key][s] 169 | ch.data[key] = new_sections 170 | self.report({"INFO"}, "Updated section") 171 | else: 172 | self.report( 173 | {"ERROR"}, "Section with this name already exists!") 174 | return {"CANCELLED"} 175 | return {"FINISHED"} 176 | 177 | 178 | classes = [ 179 | OPS_OT_AddLink, 180 | OPS_OT_RemoveLink, 181 | OPS_OT_EnableLinks, 182 | OPS_OT_AddLinksSection, 183 | OPS_OT_EditLinksSection, 184 | OPS_OT_RemoveLinksSection 185 | ] 186 | 187 | 188 | def register(): 189 | for c in classes: 190 | register_class(c) 191 | 192 | 193 | def unregister(): 194 | for c in reversed(classes): 195 | unregister_class(c) 196 | -------------------------------------------------------------------------------- /operators/move_unassigned_objects.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Operator) 3 | from bpy.props import (PointerProperty, StringProperty, BoolProperty) 4 | from bpy.utils import (register_class, unregister_class) 5 | 6 | 7 | class OPS_OT_MoveLooseObjects(Operator): 8 | bl_idname = "character_ui.move_unassigned_objects" 9 | bl_label = "Move unassigned objects" 10 | bl_description = "Moves objects from the main outfits collection to outfit collection" 11 | bl_options = {"INTERNAL"} 12 | 13 | outfit_name : StringProperty(name="New Outfit Name", description="Name of the outfit") 14 | existing_new : BoolProperty(name="New Outfit", description="Create new collection with the set name", default=False) 15 | 16 | def invoke(self, context, event): 17 | return context.window_manager.invoke_props_dialog(self, width=450) 18 | 19 | def draw(self, context): 20 | layout = self.layout 21 | layout.prop(self, "existing_new", toggle=True) 22 | if self.existing_new: 23 | layout.prop(self, "outfit_name") 24 | else: 25 | layout.prop(context.scene, "character_ui_unassigned_objects_outfit") 26 | 27 | 28 | 29 | def execute(self, context): 30 | outfits_collection = context.scene.character_ui_outfits_collection 31 | if self.outfit_name not in ["", " "] or context.scene.character_ui_unassigned_objects_outfit: 32 | coll = None 33 | if self.existing_new: 34 | coll = bpy.data.collections.new(self.outfit_name) 35 | context.scene.character_ui_outfits_collection.children.link(coll) 36 | else: 37 | coll = context.scene.character_ui_unassigned_objects_outfit 38 | loose_objects = outfits_collection.objects 39 | for o in loose_objects: 40 | outfits_collection.objects.unlink(o) 41 | coll.objects.link(o) 42 | self.report({"INFO"}, "Created %s and moved loose objects"%(coll.name)) 43 | return {"FINISHED"} 44 | 45 | def poll(self, obj): 46 | return obj.name in self.character_ui_outfits_collection.children 47 | classes = [ 48 | OPS_OT_MoveLooseObjects 49 | ] 50 | 51 | 52 | def register(): 53 | bpy.types.Scene.character_ui_unassigned_objects_outfit = PointerProperty( 54 | name="Outfit", 55 | description="Select existing outfit which will receive unassigned objects", 56 | type=bpy.types.Collection, 57 | poll=poll 58 | ) 59 | for c in classes: 60 | register_class(c) 61 | 62 | 63 | def unregister(): 64 | del bpy.types.Scene.character_ui_unassigned_objects_outfit 65 | for c in reversed(classes): 66 | unregister_class(c) 67 | -------------------------------------------------------------------------------- /operators/parent_to_character.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Operator) 3 | from bpy.props import (PointerProperty, StringProperty, EnumProperty) 4 | from bpy.utils import (register_class, unregister_class) 5 | 6 | 7 | class OPS_OT_ParentToCharacter(Operator): 8 | bl_idname = "character_ui.parent_to_character" 9 | bl_label = "Parent to the Character UI Object" 10 | bl_description = "Parents all of the outfit pieces from the current outfit to the Character UI Object." 11 | bl_options = {"INTERNAL"} 12 | 13 | 14 | def execute(self, context): 15 | outfit_collection = context.scene.character_ui_outfits_collection.children[context.scene.character_ui_active_outfit_index] 16 | chr = context.scene.character_ui_object 17 | no_parent = 0 18 | for o in outfit_collection.objects: 19 | if not o.parent: 20 | no_parent += 1 21 | o.parent = chr 22 | self.report({'INFO'}, "Parented all (%i) of outfit pieces to the character (%s)"%(no_parent, chr.name)) 23 | return {"FINISHED"} 24 | 25 | 26 | classes = [ 27 | OPS_OT_ParentToCharacter 28 | ] 29 | 30 | 31 | def register(): 32 | for c in classes: 33 | register_class(c) 34 | 35 | 36 | def unregister(): 37 | for c in reversed(classes): 38 | unregister_class(c) 39 | -------------------------------------------------------------------------------- /operators/tooltip.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Operator) 3 | from bpy.props import (StringProperty) 4 | from bpy.utils import (register_class, unregister_class) 5 | 6 | tooltip_texts = { 7 | "character_ui_object": { 8 | "header": "Character UI Object", 9 | "body": "Select one object to which the UI gets bind to. Depending on the type of the object you will get different options.\nFor example only Armatures will have access to the Rig Layers Panel", 10 | "doc_url": ["https://github.com/nextr3d/Character-UI/wiki/Add-on#character-ui-setup"] 11 | }, 12 | "character_ui_masks": { 13 | "header": "Masks", 14 | "body": "Add objects to a modifier which will be used to show or hide the modifier. These modifiers are taken from the body object.\nEach modifier can contain multiple objects and gets enabled when at least one of the added objects is visible.", 15 | "doc_url": ["https://github.com/nextr3d/Character-UI/wiki/Add-on#character-ui-body"] 16 | }, 17 | "character_ui_attributes": { 18 | "header": "Attribute Groups and Attributes", 19 | "body": "You can add many controls to the UI so the end user doesn't have to look for them. Very useful for controlling materials from one place instead of going to the material you want to edit.\nAll attributes have to be in a group. By right clicking any controls in the UI you can either add them or synchronize them to already existing attribute so they will get the same value as does the primary attribute.", 20 | "doc_url": ["https://github.com/nextr3d/Character-UI/wiki/Add-on#character-ui-attributes"] 21 | }, 22 | "icons": { 23 | "header": "Icons", 24 | "body": "Blender has a set of built-in icons which you can use. You can find these icons by enabling the Icon viewer add-on.", 25 | "doc_url": ["https://docs.blender.org/manual/en/dev/addons/development/icon_viewer.html", "Blender Manual"] 26 | }, 27 | "character_ui_expression": { 28 | "header": "Visibility Expressions", 29 | "body": "You can drive the visibility of the UI element based on some expression. Usefull when you want to show material settings of an outfit piece but only when the outfit piece is visible.\nExpressions behave exactly same as do driver expressions.\nFor example\n 'var1==0 and var2==1' \nwill show the UI element only when data from var1 Data Path equal to 0 and from var2 Data Path equal to 1.", 30 | "doc_url": ["https://github.com/nextr3d/Character-UI/wiki/Add-on#visibility"] 31 | }, 32 | "character_ui_physics": { 33 | "header": "Physics", 34 | "body": "Lists all of the objects with a cloth modifier from the Physics collection. By clicking the buttons you can set in which panel you want the physics settings to be rendered in.\nNone prevents the settings from rendering.", 35 | "doc_url": ["https://github.com/nextr3d/Character-UI/wiki/Add-on#character-ui-physics"] 36 | }, 37 | "character_ui_version": { 38 | "header": "Version", 39 | "body": "Custom property stored on the Character UI Object indicating the version of the character. If left empty it won't render in the UI.", 40 | "doc_url": ["https://github.com/nextr3d/Character-UI/wiki/Add-on#character-ui-generate"] 41 | }, 42 | "character_ui_generation_date": { 43 | "header": "UI Generation Date", 44 | "body": "Custom property stored on the Character UI Object indicating when the UI was generated.\n(Can be tweaked by finding the custom property on the Character UI Object if needed. It will get overwritten by next generation)", 45 | "doc_url": ["https://github.com/nextr3d/Character-UI/wiki/Add-on#character-ui-generate"] 46 | }, 47 | "character_ui_rig_layers": { 48 | "header": "Rig Layers", 49 | "body": "You can name Rig Layers to make it more clear what each Rig Layer contains. Like Hair, Torso, Arms,...\nRig Layers are stored the same as CloudRig Rig Layers so if you set the Rig Layers Key to:\nrigify_layers\nit will get them from the CloudRig settings and vice versa.\nEnabling Rig Layer but keeping the name empty will result in a layer with a label 'Layer [index of the layer]'.\nSetting UI Row to the same number will show them one one row in the UI.", 50 | "doc_url": ["https://github.com/nextr3d/Character-UI/wiki/Add-on#character-ui-rig-layers"] 51 | }, 52 | "character_ui_shape_keys": { 53 | "header": "Shape Keys", 54 | "body": "Add objects to a shape key which will be used to show or hide the shape key. These shape keys are taken from the body object.\nEach shape key can contain multiple objects and gets enabled when at least one of the added objects is visible.", 55 | "doc_url": ["https://github.com/nextr3d/Character-UI/wiki/Add-on#character-ui-body"] 56 | }, 57 | "outfit_piece_parent": { 58 | "header": "Outfit piece parent", 59 | "body": "By default parent of each outfit piece should be the Character UI Object so you can grab it and move it and everything follows.\nIf you set the parent to be another object it will be only visible when the parent is visible.\nThis is useful for example when your character has pants and a belt, then it makes no sense to have the belt without pants but pants can be without the belt.", 60 | }, 61 | "outfits": { 62 | "header": "Outfits", 63 | "body": "Each outfit is a collection containing multiple objects. Each object represents one outfit piece and will have a button in the UI.\nThe main outfits collection can't contain any \"unassigned\" objects, if it does, the add-on will notify you and offer one click fix.\nOutfit collections can't (for now) contain other collections.", 64 | }, 65 | "cant_contain_directly": { 66 | "header": "Master Collection", 67 | "body": "To make scenes more organized the add-on requires that all of the outfits are contained in one Master Collection.\nThis means that you have one collection (named Suzanne Outfits for example) and this one is set in the outfits box and this collection contains only other collections and each one is threated as a separate outfit and these collection contain only objects.", 68 | }, 69 | } 70 | 71 | 72 | class OPS_OT_Tooltip(Operator): 73 | 74 | bl_idname = "character_ui.tooltip" 75 | bl_label = "" 76 | bl_description = "Click to learn more" 77 | bl_options = {"INTERNAL"} 78 | tooltip_id: StringProperty() 79 | 80 | def invoke(self, context, event): 81 | return context.window_manager.invoke_props_dialog(self, width=550) 82 | 83 | @classmethod 84 | def draw_label_with_linebreak(self, layout, text, alert=False, align_split=False): 85 | """ Attempt to simulate a proper textbox by only displaying as many 86 | characters in a single label as fits in the UI. 87 | This only works well on specific UI zoom levels. 88 | Code taken from: https://gitlab.com/blender/CloudRig 89 | """ 90 | 91 | if text == "": 92 | return 93 | col = layout.column(align=True) 94 | col.alert = alert 95 | if align_split: 96 | split = col.split(factor=0.2) 97 | split.row() 98 | col = split.row().column() 99 | paragraphs = text.split("\n") 100 | 101 | max_line_length = 95 102 | if align_split: 103 | max_line_length *= 0.95 104 | for p in paragraphs: 105 | 106 | lines = [""] 107 | for word in p.split(" "): 108 | if len(lines[-1]) + len(word)+1 > max_line_length: 109 | lines.append("") 110 | lines[-1] += word + " " 111 | 112 | for line in lines: 113 | col.label(text=line) 114 | return col 115 | 116 | def draw(self, context): 117 | layout = self.layout 118 | if self.tooltip_id in tooltip_texts: 119 | tooltip_text = tooltip_texts[self.tooltip_id] 120 | header_row = layout.row() 121 | header_row.alert = True 122 | header_row.label(text=tooltip_text["header"]) 123 | self.draw_label_with_linebreak(layout, tooltip_text["body"]) 124 | if "doc_url" in tooltip_text: 125 | layout.operator("wm.url_open", text=tooltip_text["doc_url"][1] if len(tooltip_text["doc_url"]) == 2 else "GitHub Wiki").url = tooltip_text["doc_url"][0] 126 | else: 127 | layout.label(text="Could not find a documentation", icon="ERROR") 128 | 129 | def execute(self, context): 130 | return {"FINISHED"} 131 | 132 | 133 | classes = [ 134 | OPS_OT_Tooltip 135 | ] 136 | 137 | 138 | def register(): 139 | for c in classes: 140 | register_class(c) 141 | 142 | 143 | def unregister(): 144 | for c in reversed(classes): 145 | unregister_class(c) 146 | -------------------------------------------------------------------------------- /operators/use_as_cage.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Operator) 3 | from bpy.props import (PointerProperty, StringProperty, EnumProperty) 4 | from bpy.utils import (register_class, unregister_class) 5 | 6 | 7 | class OPS_OT_UseAsCage(Operator): 8 | bl_idname = "character_ui.use_as_cage" 9 | bl_label = "Select object to be used as physics cage" 10 | bl_description = "Options for object which could be used as mesh deform cage" 11 | bl_options = {"INTERNAL"} 12 | 13 | cage: StringProperty() 14 | panel: EnumProperty(name="Panel", items=[("OP1", "Outfits", "Toggles in the Outfits Panel"), ("OP2", "Body", "Toggles in the Body Panel"), ( 15 | "OP3", "Miscellaneous", "Toggles in the MIscellanesou Panel"), ("OP4", "None", "Not visible in the UI")]) 16 | custom_label: StringProperty( 17 | name="Custom UI Label", description="Set a custom label which will be show instead of the object's name") 18 | 19 | def invoke(self, context, event): 20 | ch = context.scene.character_ui_object 21 | self.panel = "OP4" 22 | self.custom_label = self.cage 23 | if "character_ui_cages" in ch.data: 24 | if "cages" in ch.data["character_ui_cages"]: 25 | for c in ch.data["character_ui_cages"]["cages"]: 26 | if c[0].name == self.cage: 27 | self.panel = c[1] 28 | if len(c) >= 3: 29 | self.custom_label = c[2] 30 | return context.window_manager.invoke_props_dialog(self, width=350) 31 | 32 | def draw(self, context): 33 | layout = self.layout 34 | layout.label(text=self.cage) 35 | layout.prop(self, "custom_label") 36 | layout.prop(self, "panel") 37 | 38 | def execute(self, context): 39 | ch = context.scene.character_ui_object 40 | if ch: 41 | if "cages" in ch.data["character_ui_cages"]: 42 | new_cages = [] 43 | add_new = True 44 | for c in ch.data["character_ui_cages"]["cages"]: 45 | if c[0].name == self.cage and self.panel != "OP4": 46 | new_cages.append( 47 | (bpy.data.objects[self.cage], self.panel, self.custom_label)) 48 | add_new = False 49 | elif c[0].name != self.cage: 50 | new_cages.append(c) 51 | if add_new and self.panel != "OP4": 52 | new_cages.append( 53 | (bpy.data.objects[self.cage], self.panel, self.custom_label)) 54 | ch.data["character_ui_cages"]["cages"] = new_cages 55 | else: 56 | ch.data["character_ui_cages"]["cages"] = [ 57 | (bpy.data.objects[self.cage], self.panel, self.custom_label)] 58 | return {"FINISHED"} 59 | 60 | 61 | classes = [ 62 | OPS_OT_UseAsCage 63 | ] 64 | 65 | 66 | def register(): 67 | for c in classes: 68 | register_class(c) 69 | 70 | 71 | def unregister(): 72 | for c in reversed(classes): 73 | unregister_class(c) 74 | -------------------------------------------------------------------------------- /operators/use_as_driver.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Operator, Object) 3 | from bpy.props import (PointerProperty, StringProperty) 4 | from bpy.utils import (register_class, unregister_class) 5 | 6 | 7 | class OPS_OT_AddObjectAsDriver(Operator): 8 | bl_idname = "character_ui.add_object_as_driver" 9 | bl_label = "Add object" 10 | bl_description = "Adds selected object to the mask modifer" 11 | bl_options = {"INTERNAL"} 12 | 13 | modifier: StringProperty() 14 | shape_key: StringProperty() 15 | 16 | @classmethod 17 | def poll(self, context): 18 | return context.scene.character_ui_driver_object != None 19 | 20 | def execute(self, context): 21 | ch = context.scene.character_ui_object 22 | if ch: 23 | found = False 24 | key = "" 25 | name = "" 26 | if self.modifier: 27 | key = "character_ui_masks" 28 | name = self.modifier 29 | if self.shape_key: 30 | key = "character_ui_shape_keys" 31 | name = self.shape_key 32 | driver = context.scene.character_ui_driver_object 33 | if key not in ch.data: 34 | ch.data[key] = [] 35 | for item in ch.data[key]: 36 | if "name" in item and item["name"] == name: 37 | new_items = [] 38 | try: 39 | new_items = item["driver_id"] 40 | except: 41 | new_items = item["driver_id"].to_list() 42 | if driver not in new_items: 43 | new_items.append(driver) 44 | item["driver_id"] = new_items 45 | else: 46 | self.report({"WARNING"}, "This object has already been added.") 47 | return {"CANCELLED"} 48 | found = True 49 | elif "modifier" in item and item["modifier"] == name: 50 | old_item = item["driver_id"] 51 | item["driver_id"] = [old_item] 52 | item["name"] = name 53 | del item["modifier"] 54 | 55 | elif "shape_key" in item and item["shape_key"] == name: 56 | old_item = item["driver_id"] 57 | item["driver_id"] = [old_item] 58 | item["name"] = name 59 | del item["shape_key"] 60 | 61 | if not found: 62 | items = [] 63 | try: 64 | items = ch.data[key].to_list() 65 | except: 66 | items = ch.data[key] 67 | items.append({"name": name, 68 | "driver_id": [driver]}) 69 | ch.data[key] = items 70 | self.report({'INFO'}, "Added %s to %s" % (driver.name, name)) 71 | context.scene.character_ui_driver_object = None 72 | 73 | return {'FINISHED'} 74 | 75 | 76 | class OPS_OT_RemoveObjectAsDriver(Operator): 77 | bl_idname = "character_ui.remove_object_as_driver" 78 | bl_label = "Remove object" 79 | bl_description = "Removes object from the mask modifier" 80 | bl_options = {"INTERNAL"} 81 | 82 | modifier: StringProperty() 83 | shape_key: StringProperty() 84 | removed_object: StringProperty() 85 | 86 | def execute(self, context): 87 | ch = context.scene.character_ui_object 88 | if ch: 89 | key = "" 90 | name = "" 91 | if self.modifier: 92 | key = "character_ui_masks" 93 | name = self.modifier 94 | if self.shape_key: 95 | key = "character_ui_shape_keys" 96 | name = self.shape_key 97 | for item in ch.data[key]: 98 | if item["name"] == name: 99 | if type(item["driver_id"]) == Object: 100 | item["driver_id"] = [] 101 | else: 102 | new_drivers = [] 103 | for o in item["driver_id"]: 104 | if o.name != self.removed_object: 105 | new_drivers.append(o) 106 | item["driver_id"] = new_drivers 107 | self.report({'INFO'}, "Removed %s from %s" % (self.removed_object, name)) 108 | 109 | return {'FINISHED'} 110 | 111 | 112 | class OPS_OT_UseAsDriver(Operator): 113 | bl_idname = "character_ui.use_as_driver" 114 | bl_label = "Select object to trigger visibility change for:" 115 | bl_description = "" 116 | bl_options = {"INTERNAL"} 117 | 118 | modifier: StringProperty() 119 | shape_key: StringProperty() 120 | 121 | def invoke(self, context, event): 122 | context.scene.character_ui_driver_object = None 123 | return context.window_manager.invoke_props_dialog(self, width=450) 124 | 125 | def draw(self, context): 126 | self.layout.label(text=self.modifier) 127 | box = self.layout.box() 128 | box.label(text="Objects", icon="OUTLINER_OB_MESH") 129 | ch = context.scene.character_ui_object 130 | if ch: 131 | key = "" 132 | name = "" 133 | driver_object = context.scene.character_ui_driver_object 134 | if self.modifier: 135 | key = "character_ui_masks" 136 | name = self.modifier 137 | if self.shape_key: 138 | key = "character_ui_shape_keys" 139 | name = self.shape_key 140 | 141 | if key in ch.data: 142 | for item in ch.data[key]: 143 | if "name" in item: 144 | if item["name"] == name: 145 | for o in item["driver_id"]: 146 | row = box.row() 147 | row.label(text=o.name) 148 | rem = row.operator(OPS_OT_RemoveObjectAsDriver.bl_idname, icon="X") 149 | if self.modifier: 150 | rem.modifier = self.modifier 151 | if self.shape_key: 152 | rem.shape_key = self.shape_key 153 | rem.removed_object = o.name 154 | if "modifier" in item or "shape_key" in item: 155 | # for legacy reasons so it doesn't break compatibility with older versions 156 | alert = box.row() 157 | alert.alert = True 158 | alert.label(text="Found old storing of data! Data will be updated to match current version", icon="ERROR") 159 | row = self.layout.row(align=True) 160 | row.prop(context.scene, 'character_ui_driver_object') 161 | remove_op = row.operator(OPS_OT_AddObjectAsDriver.bl_idname, text="", icon="ADD") 162 | if self.modifier: 163 | remove_op.modifier = self.modifier 164 | if self.shape_key: 165 | remove_op.shape_key = self.shape_key 166 | 167 | def execute(self, context): 168 | return {"FINISHED"} 169 | 170 | 171 | classes = [ 172 | OPS_OT_UseAsDriver, 173 | OPS_OT_AddObjectAsDriver, 174 | OPS_OT_RemoveObjectAsDriver 175 | ] 176 | 177 | 178 | def register(): 179 | bpy.types.Scene.character_ui_driver_object = PointerProperty(type=Object, name="Outfit piece") 180 | for c in classes: 181 | register_class(c) 182 | 183 | 184 | def unregister(): 185 | del bpy.types.Scene.character_ui_driver_object 186 | for c in reversed(classes): 187 | unregister_class(c) 188 | -------------------------------------------------------------------------------- /panels/__init__.py: -------------------------------------------------------------------------------- 1 | from . import main 2 | from . import body 3 | from . import outfits 4 | from . import physics 5 | from . import generate 6 | from . import rig_layers 7 | from . import attributes 8 | from . import miscellaneous 9 | import importlib 10 | 11 | 12 | modules = [ 13 | main, 14 | body, 15 | outfits, 16 | rig_layers, 17 | attributes, 18 | physics, 19 | miscellaneous, 20 | generate 21 | ] 22 | 23 | 24 | def register(): 25 | for m in modules: 26 | importlib.reload(m) 27 | m.register() 28 | 29 | 30 | def unregister(): 31 | for m in reversed(modules): 32 | m.unregister() 33 | -------------------------------------------------------------------------------- /panels/attributes.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Panel, PropertyGroup, Operator, Menu) 3 | from bpy.props import (PointerProperty, StringProperty, 4 | BoolProperty, IntProperty) 5 | from bpy.utils import (register_class, unregister_class) 6 | 7 | 8 | class VIEW3D_PT_character_ui_attributes(Panel): 9 | bl_space_type = "VIEW_3D" 10 | bl_region_type = "UI" 11 | bl_category = "Character-UI" 12 | bl_label = "Character UI Attributes" 13 | bl_options = {'HEADER_LAYOUT_EXPAND'} 14 | 15 | def draw_header(self, context): 16 | self.layout.label(text="") 17 | row = self.layout.row(align=True) 18 | row.operator("character_ui.tooltip", text="", icon="QUESTION").tooltip_id = "character_ui_attributes" 19 | 20 | @classmethod 21 | def poll(self, context): 22 | ch = context.scene.character_ui_object 23 | if not ch: 24 | return False 25 | rig_id_key = context.scene.character_ui_object_id 26 | return rig_id_key and rig_id_key in ch.data 27 | 28 | def draw(self, context): 29 | pass 30 | 31 | 32 | class VIEW3D_PT_character_ui_attributes_body(Panel): 33 | bl_space_type = "VIEW_3D" 34 | bl_region_type = "UI" 35 | bl_label = "Body Panel" 36 | bl_idname = "VIEW3D_PT_character_ui_attributes_body" 37 | bl_parent_id = "VIEW3D_PT_character_ui_attributes" 38 | 39 | @classmethod 40 | def poll(self, context): 41 | return CharacterUIAttributesUtils.render_body_attributes_panels(context) 42 | 43 | def draw(self, context): 44 | CharacterUIAttributesUtils.render_attributes_group_panel( 45 | context, "body", self.layout) 46 | 47 | 48 | class VIEW3D_PT_character_ui_attributes_outfits(Panel): 49 | bl_space_type = "VIEW_3D" 50 | bl_region_type = "UI" 51 | bl_label = "Outfits Panel" 52 | bl_idname = "VIEW3D_PT_character_ui_attributes_outfits" 53 | bl_parent_id = "VIEW3D_PT_character_ui_attributes" 54 | 55 | @classmethod 56 | def poll(self, context): 57 | return CharacterUIAttributesUtils.render_outfits_attributes_panels(context) 58 | 59 | def draw(self, context): 60 | CharacterUIAttributesUtils.render_attributes_group_panel( 61 | context, "outfits", self.layout) 62 | 63 | 64 | class VIEW3D_PT_character_ui_attributes_rig(Panel): 65 | bl_space_type = "VIEW_3D" 66 | bl_region_type = "UI" 67 | bl_label = "Rig Panel" 68 | bl_idname = "VIEW3D_PT_character_ui_attributes_rig" 69 | bl_parent_id = "VIEW3D_PT_character_ui_attributes" 70 | 71 | @classmethod 72 | def poll(self, context): 73 | return CharacterUIAttributesUtils.render_rig_attributes_panels(context) 74 | 75 | def draw(self, context): 76 | CharacterUIAttributesUtils.render_attributes_group_panel( 77 | context, "rig", self.layout) 78 | 79 | 80 | class VIEW3D_PT_character_ui_attributes_miscellaneous(Panel): 81 | bl_space_type = "VIEW_3D" 82 | bl_region_type = "UI" 83 | bl_label = "Miscellaneous Panel" 84 | bl_idname = "VIEW3D_PT_character_ui_attributes_miscellaneous" 85 | bl_parent_id = "VIEW3D_PT_character_ui_attributes" 86 | 87 | def draw(self, context): 88 | CharacterUIAttributesUtils.render_attributes_group_panel( 89 | context, "misc", self.layout) 90 | 91 | 92 | class CharacterUIAttributesUtils: 93 | @staticmethod 94 | def render_right_click_menu_operators(self, context): 95 | ch = context.scene.character_ui_object 96 | if ch: 97 | layout = self.layout 98 | layout.separator() 99 | layout.label(text="Character-UI Attributes") 100 | layout.menu(WM_MT_add_new_attribute.bl_idname) 101 | layout.menu(WM_MT_sync_attribute_panel.bl_idname) 102 | 103 | @staticmethod 104 | def render_attributes_group_panel(context, panel_name, layout): 105 | ch = context.scene.character_ui_object 106 | rig_id = ch.data[context.scene.character_ui_object_id] 107 | attributes_key = "CharacterUI_att_%s" % (rig_id) 108 | layout.operator( 109 | "character_ui.add_new_attribute_group").panel_name = panel_name 110 | if attributes_key in ch: 111 | if panel_name in ch[attributes_key]: 112 | for g in ch[attributes_key][panel_name]: 113 | box = layout.box() 114 | header_row = box.row(align=True) 115 | expand_op = header_row.operator( 116 | "character_ui.expand_attribute_group", text="", icon="DOWNARROW_HLT" if g["expanded"] else "RIGHTARROW", emboss=False) 117 | expand_op.panel_name = panel_name 118 | expand_op.group_name = g["name"] 119 | try: 120 | header_row.label(text=g["name"].replace( 121 | "_", " "), icon=g["icon"]) 122 | except: 123 | header_row.label(text=g["name"].replace("_", " ")) 124 | # edit group operator 125 | edit_op = header_row.operator( 126 | "character_ui.edit_attribute_group", text="", icon="PREFERENCES") 127 | edit_op.panel_name = panel_name 128 | edit_op.group_name = g["name"] 129 | # edit group operator 130 | # visibility operator 131 | visibility_op = header_row.operator( 132 | "character_ui.edit_visibility_variables", text="", icon="HIDE_OFF") 133 | visibility_op.panel_name = panel_name 134 | visibility_op.group_name = g["name"] 135 | # visibility operator 136 | # move up group operator 137 | move_up_op = header_row.operator( 138 | "character_ui.attribute_group_change_position", text="", icon="TRIA_UP") 139 | move_up_op.panel_name = panel_name 140 | move_up_op.group_name = g["name"] 141 | move_up_op.direction = True 142 | # move up group operator 143 | # move down group operator 144 | move_up_op = header_row.operator( 145 | "character_ui.attribute_group_change_position", text="", icon="TRIA_DOWN") 146 | move_up_op.panel_name = panel_name 147 | move_up_op.group_name = g["name"] 148 | move_up_op.direction = False 149 | # move down group operator 150 | # delete group operator 151 | delete_op = header_row.operator( 152 | "character_ui.remove_attribute_group", text="", icon="X") 153 | delete_op.panel_name = panel_name 154 | delete_op.group_name = g["name"] 155 | # delete group operator 156 | 157 | if g["expanded"]: 158 | for p in g["attributes"]: 159 | delimiter = "][" if "][" in p["path"] else "." 160 | offset = 1 if "][" in p["path"] else 0 161 | prop = p["path"][p["path"].rindex(delimiter)+1:] 162 | path = p["path"][:p["path"].rindex( 163 | delimiter)+offset] 164 | prop_exists = True 165 | toggle = p["toggle"] if "invert_checkbox" in p else False 166 | invert_checkbox = p["invert_checkbox"] if "invert_checkbox" in p else False 167 | slider = p["slider"] if "slider" in p else False 168 | emboss = p["emboss"] if "emboss" in p else True 169 | icon = p["icon"] if "icon" in p else "NONE" 170 | try: 171 | eval(p["path"]) 172 | except: 173 | prop_exists = False 174 | 175 | row = box.row(align=True) 176 | 177 | if not prop_exists: 178 | row.label(text="Invalid attribute", 179 | icon="ERROR") 180 | else: 181 | if p["name"]: 182 | try: 183 | row.prop(eval(path), prop, text=p["name"], invert_checkbox=invert_checkbox, 184 | toggle=toggle, slider=slider, icon=icon, emboss=emboss) 185 | except: 186 | continue 187 | 188 | else: 189 | try: 190 | row.prop(eval(path), prop, invert_checkbox=invert_checkbox, 191 | toggle=toggle, slider=slider, icon=icon, emboss=emboss) 192 | except: 193 | continue 194 | 195 | if not prop_exists: 196 | row.label(text="Invalid attribute", 197 | icon="ERROR") 198 | 199 | op_edit = row.operator( 200 | "character_ui.edit_attribute", icon="PREFERENCES", text="") 201 | op_edit.path = p["path"] 202 | op_edit.panel_name = panel_name 203 | op_edit.group_name = g["name"] 204 | 205 | a_visibility_op = row.operator( 206 | "character_ui.edit_visibility_variables", text="", icon="HIDE_OFF") 207 | a_visibility_op.panel_name = panel_name 208 | a_visibility_op.group_name = g["name"] 209 | a_visibility_op.path = p["path"] 210 | 211 | op_up = row.operator( 212 | "character_ui.attribute_change_position", icon="TRIA_UP", text="") 213 | op_up.direction = True 214 | op_up.path = p["path"] 215 | op_up.panel_name = panel_name 216 | op_up.group_name = g["name"] 217 | 218 | op_down = row.operator( 219 | "character_ui.attribute_change_position", icon="TRIA_DOWN", text="") 220 | op_down.direction = False 221 | op_down.path = p["path"] 222 | op_down.panel_name = panel_name 223 | op_down.group_name = g["name"] 224 | 225 | op = row.operator( 226 | "character_ui.remove_attribute", icon="X", text="") 227 | op.path = p["path"] 228 | op.panel_name = panel_name 229 | op.group_name = g["name"] 230 | 231 | @staticmethod 232 | def render_attribute_groups_menu(layout, context, panel_name): 233 | ch = context.scene.character_ui_object 234 | ch_id = ch.data[context.scene.character_ui_object_id] 235 | key = "CharacterUI_att_%s" % (ch_id) 236 | if key in ch: 237 | if panel_name in ch[key]: 238 | for g in ch[key][panel_name]: 239 | op = layout.operator( 240 | "character_ui.add_new_attribute", text=g["name"].replace("_", " ")) 241 | op.panel_name = panel_name 242 | op.group_name = g["name"] 243 | op.parent_path = "" 244 | 245 | @staticmethod 246 | def render_attributes_in_menu(layout, context, panel_name): 247 | ch = context.scene.character_ui_object 248 | ch_id = ch.data[context.scene.character_ui_object_id] 249 | attributes_key = "CharacterUI_att_%s" % (ch_id) 250 | if attributes_key in ch: 251 | if panel_name in ch[attributes_key]: 252 | for g in ch[attributes_key][panel_name]: 253 | layout.separator() 254 | layout.label(text=g["name"].replace("_", " ")) 255 | layout.separator() 256 | for p in g["attributes"]: 257 | name = "Default Value" 258 | if p["name"]: 259 | name = p["name"] 260 | op = layout.operator( 261 | "character_ui.add_new_attribute", text=name) 262 | op.parent_path = p["path"] 263 | op.panel_name = panel_name 264 | op.group_name = g["name"] 265 | return layout 266 | 267 | @staticmethod 268 | def render_rig_attributes_panels(context): 269 | ch = context.scene.character_ui_object 270 | if ch: 271 | return ch.type == "ARMATURE" 272 | return False 273 | 274 | @staticmethod 275 | def render_outfits_attributes_panels(context): 276 | ch = context.scene.character_ui_object 277 | if ch: 278 | if context.scene.character_ui_object_id in ch.data and "outfits_collection" in ch.data: 279 | if ch.data["outfits_collection"]: 280 | return True 281 | return False 282 | 283 | @staticmethod 284 | def render_body_attributes_panels(context): 285 | ch = context.scene.character_ui_object 286 | if ch: 287 | if ("body_object" in ch.data and ch.data["body_object"]) or ( "hair_collection" in ch.data and ch.data["hair_collection"]): 288 | return True 289 | return False 290 | 291 | 292 | class WM_MT_button_context(Menu): 293 | bl_label = "Add to UI" 294 | 295 | def draw(self, context): 296 | pass 297 | 298 | 299 | class WM_MT_add_new_attribute(Menu): 300 | bl_label = "Add New Attribute" 301 | bl_idname = "WM_MT_add_new_attribute_menu" 302 | 303 | def draw(self, context): 304 | layout = self.layout 305 | layout.menu(WM_MT_add_new_attribute_outfits_menu.bl_idname, 306 | text="Outfits Panel") 307 | layout.menu(WM_MT_add_new_attribute_body_menu.bl_idname, 308 | text="Body Panel") 309 | layout.menu(WM_MT_add_new_attribute_rig_menu.bl_idname, 310 | text="Rig Panel") 311 | layout.menu(WM_MT_add_new_attribute_miscellaneous_menu.bl_idname, 312 | text="Miscellaneous Panel") 313 | 314 | 315 | class WM_MT_add_new_attribute_outfits_menu(Menu): 316 | bl_label = "no attribute name entered!" 317 | bl_idname = "WM_MT_add_new_attribute_outfits_menu" 318 | 319 | @classmethod 320 | def poll(self, context): 321 | return CharacterUIAttributesUtils.render_outfits_attributes_panels(context) 322 | 323 | def draw(self, context): 324 | self.layout.label(text="Attribute Groups for Outfits Panel") 325 | CharacterUIAttributesUtils.render_attribute_groups_menu( 326 | self.layout, context, "outfits") 327 | 328 | 329 | class WM_MT_add_new_attribute_body_menu(Menu): 330 | bl_label = "no attribute name entered!" 331 | bl_idname = "WM_MT_add_new_attribute_body_menu" 332 | 333 | @classmethod 334 | def poll(self, context): 335 | return CharacterUIAttributesUtils.render_body_attributes_panels(context) 336 | 337 | def draw(self, context): 338 | self.layout.label(text="Attribute Groups for Body Panel") 339 | CharacterUIAttributesUtils.render_attribute_groups_menu( 340 | self.layout, context, "body") 341 | 342 | 343 | class WM_MT_add_new_attribute_rig_menu(Menu): 344 | bl_label = "no attribute name entered!" 345 | bl_idname = "WM_MT_add_new_attribute_rig_menu" 346 | 347 | @classmethod 348 | def poll(self, context): 349 | return CharacterUIAttributesUtils.render_rig_attributes_panels(context) 350 | 351 | def draw(self, context): 352 | self.layout.label(text="Attribute Groups for Body Panel") 353 | CharacterUIAttributesUtils.render_attribute_groups_menu( 354 | self.layout, context, "rig") 355 | 356 | 357 | class WM_MT_add_new_attribute_miscellaneous_menu(Menu): 358 | bl_label = "no attribute name entered!" 359 | bl_idname = "WM_MT_add_new_attribute_miscellaneous_menu" 360 | 361 | def draw(self, context): 362 | self.layout.label(text="Attribute Groups for Body Panel") 363 | CharacterUIAttributesUtils.render_attribute_groups_menu( 364 | self.layout, context, "misc") 365 | 366 | 367 | class WM_MT_sync_attribute_panel(Menu): 368 | bl_label = "Sync To Attribute" 369 | bl_idname = "WM_MT_sync_attribute_panel" 370 | 371 | def draw(self, context): 372 | layout = self.layout 373 | layout.menu(WM_MT_sync_attribute_outfits_menu.bl_idname, 374 | text="Outfits Panel") 375 | layout.menu(WM_MT_sync_attribute_body_menu.bl_idname, 376 | text="Body Panel") 377 | layout.menu(WM_MT_sync_attribute_rig_menu.bl_idname, text="Rig Panel") 378 | layout.menu(WM_MT_sync_attribute_miscellaneous_menu.bl_idname, 379 | text="Miscellaneous Panel") 380 | 381 | 382 | class WM_MT_sync_attribute_outfits_menu(Menu): 383 | bl_label = "no attribute name entered!" 384 | bl_idname = "WM_MT_sync_attribute_outfits_menu" 385 | 386 | def draw(self, context): 387 | self.layout.label(text="Attributes for Rig Layers Panel") 388 | CharacterUIAttributesUtils.render_attributes_in_menu( 389 | self.layout, context, "outfits") 390 | 391 | 392 | class WM_MT_sync_attribute_body_menu(Menu): 393 | bl_label = "no attribute name entered!" 394 | bl_idname = "WM_MT_sync_attribute_body_menu" 395 | 396 | def draw(self, context): 397 | self.layout.label(text="Attributes for Rig Layers Panel") 398 | CharacterUIAttributesUtils.render_attributes_in_menu( 399 | self.layout, context, "body") 400 | 401 | 402 | class WM_MT_sync_attribute_rig_menu(Menu): 403 | bl_label = "no attribute name entered!" 404 | bl_idname = "WM_MT_sync_attribute_rig_menu" 405 | 406 | def draw(self, context): 407 | self.layout.label(text="Attributes for Rig Layers Panel") 408 | CharacterUIAttributesUtils.render_attributes_in_menu( 409 | self.layout, context, "rig") 410 | 411 | 412 | class WM_MT_sync_attribute_miscellaneous_menu(Menu): 413 | bl_label = "no attribute name entered!" 414 | bl_idname = "WM_MT_sync_attribute_miscellaneous_menu" 415 | 416 | def draw(self, context): 417 | self.layout.label(text="Attributes for Rig Layers Panel") 418 | CharacterUIAttributesUtils.render_attributes_in_menu( 419 | self.layout, context, "misc") 420 | 421 | 422 | classes = [ 423 | WM_MT_button_context, 424 | WM_MT_add_new_attribute, 425 | WM_MT_add_new_attribute_outfits_menu, 426 | WM_MT_add_new_attribute_body_menu, 427 | WM_MT_add_new_attribute_rig_menu, 428 | WM_MT_add_new_attribute_miscellaneous_menu, 429 | VIEW3D_PT_character_ui_attributes, 430 | VIEW3D_PT_character_ui_attributes_outfits, 431 | VIEW3D_PT_character_ui_attributes_body, 432 | VIEW3D_PT_character_ui_attributes_rig, 433 | VIEW3D_PT_character_ui_attributes_miscellaneous, 434 | WM_MT_sync_attribute_panel, 435 | WM_MT_sync_attribute_outfits_menu, 436 | WM_MT_sync_attribute_body_menu, 437 | WM_MT_sync_attribute_rig_menu, 438 | WM_MT_sync_attribute_miscellaneous_menu 439 | 440 | ] 441 | 442 | 443 | def register(): 444 | for c in classes: 445 | register_class(c) 446 | 447 | bpy.types.WM_MT_button_context.append( 448 | CharacterUIAttributesUtils.render_right_click_menu_operators) 449 | 450 | 451 | def unregister(): 452 | bpy.types.WM_MT_button_context.remove( 453 | CharacterUIAttributesUtils.render_right_click_menu_operators) 454 | 455 | for c in reversed(classes): 456 | unregister_class(c) 457 | -------------------------------------------------------------------------------- /panels/body.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Panel) 3 | from bpy.props import (IntProperty) 4 | from bpy.utils import (register_class, unregister_class) 5 | 6 | 7 | class VIEW3D_PT_character_ui_body(Panel): 8 | bl_space_type = 'VIEW_3D' 9 | bl_region_type = 'UI' 10 | bl_category = "Character-UI" 11 | bl_label = "Character UI Body" 12 | bl_idname = "VIEW3D_PT_character_ui_body" 13 | bl_options = {"DEFAULT_CLOSED"} 14 | 15 | @classmethod 16 | def poll(self, context): 17 | body = context.scene.character_ui_object_body 18 | return body and hasattr(body, "modifiers") 19 | 20 | def draw(self, context): 21 | pass 22 | 23 | 24 | class VIEW3D_PT_character_ui_shape_keys(Panel): 25 | bl_space_type = 'VIEW_3D' 26 | bl_region_type = 'UI' 27 | bl_label = "Shape Keys" 28 | bl_idname = "VIEW3D_PT_character_ui_shape_keys" 29 | bl_parent_id = "VIEW3D_PT_character_ui_body" 30 | bl_options = {"HEADER_LAYOUT_EXPAND", "DEFAULT_CLOSED"} 31 | 32 | @classmethod 33 | def poll(self, context): 34 | ch = context.scene.character_ui_object 35 | body = ch.data["body_object"] 36 | return context.scene.character_ui_object_body.type == "MESH" and hasattr(body.data.shape_keys, "key_blocks") 37 | 38 | def draw_header(self, context): 39 | self.layout.label(text="") 40 | row = self.layout.row(align=True) 41 | row.operator("character_ui.tooltip", text="", icon="QUESTION").tooltip_id = "character_ui_shape_keys" 42 | 43 | def draw(self, context): 44 | layout = self.layout 45 | ch = context.scene.character_ui_object 46 | body = ch.data["body_object"] 47 | layout.template_list("MESH_UL_shape_keys", "", ch.data["body_object"].data.shape_keys, "key_blocks", 48 | context.scene, "character_ui_active_shape_key_index") 49 | shape_key_name = body.data.shape_keys.key_blocks[context.scene.character_ui_active_shape_key_index].name 50 | op = layout.operator("character_ui.use_as_driver", text="Use %s as deformer" % (shape_key_name), emboss=True) 51 | op.shape_key = shape_key_name 52 | 53 | 54 | class VIEW3D_PT_character_ui_masks(Panel): 55 | bl_space_type = 'VIEW_3D' 56 | bl_region_type = 'UI' 57 | bl_label = "Masks" 58 | bl_idname = "VIEW3D_PT_character_ui_masks" 59 | bl_parent_id = "VIEW3D_PT_character_ui_body" 60 | bl_options = {'HEADER_LAYOUT_EXPAND', "DEFAULT_CLOSED"} 61 | 62 | @classmethod 63 | def poll(self, context): 64 | return context.scene.character_ui_object_body.type == "MESH" 65 | 66 | def draw_header(self, context): 67 | self.layout.label(text="") 68 | row = self.layout.row(align=True) 69 | row.operator("character_ui.tooltip", text="", icon="QUESTION").tooltip_id = "character_ui_masks" 70 | 71 | def draw(self, context): 72 | pass 73 | 74 | 75 | class VIEW3D_PT_character_ui_masks_masks(Panel): 76 | bl_space_type = 'VIEW_3D' 77 | bl_region_type = 'UI' 78 | bl_label = "Masks modifiers" 79 | bl_idname = "VIEW3D_PT_character_ui_masks_masks" 80 | bl_parent_id = "VIEW3D_PT_character_ui_masks" 81 | 82 | def draw(self, context): 83 | layout = self.layout 84 | ch = context.scene.character_ui_object 85 | body = ch.data["body_object"] 86 | for m in body.modifiers: 87 | if m.type in ["MASK", "VERTEX_WEIGHT_MIX"]: 88 | op = layout.operator("character_ui.use_as_driver", text=m.name) 89 | op.modifier = m.name 90 | 91 | 92 | class VIEW3D_PT_character_ui_masks_other(Panel): 93 | bl_space_type = 'VIEW_3D' 94 | bl_region_type = 'UI' 95 | bl_label = "Other modifiers" 96 | bl_idname = "VIEW3D_PT_character_ui_masks_other" 97 | bl_parent_id = "VIEW3D_PT_character_ui_masks" 98 | 99 | def draw(self, context): 100 | layout = self.layout 101 | ch = context.scene.character_ui_object 102 | body = ch.data["body_object"] 103 | for m in body.modifiers: 104 | if m.type not in ["MASK", "VERTEX_WEIGHT_MIX"]: 105 | op = layout.operator("character_ui.use_as_driver", text=m.name) 106 | op.modifier = m.name 107 | 108 | 109 | classes = [ 110 | VIEW3D_PT_character_ui_body, 111 | VIEW3D_PT_character_ui_masks, 112 | VIEW3D_PT_character_ui_shape_keys, 113 | VIEW3D_PT_character_ui_masks_masks, 114 | VIEW3D_PT_character_ui_masks_other 115 | ] 116 | 117 | 118 | def register(): 119 | bpy.types.Scene.character_ui_active_shape_key_index = IntProperty() 120 | for c in classes: 121 | register_class(c) 122 | 123 | 124 | def unregister(): 125 | del bpy.types.Scene.character_ui_active_shape_key_index 126 | for c in reversed(classes): 127 | unregister_class(c) 128 | -------------------------------------------------------------------------------- /panels/generate.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | import bpy 4 | from bpy.types import (Panel, PropertyGroup, Operator) 5 | from bpy.props import (PointerProperty, StringProperty, BoolProperty) 6 | from bpy.utils import (register_class, unregister_class) 7 | 8 | 9 | class OPS_OT_GenerateID(Operator): 10 | bl_idname = 'characterui_generate.generate_id' 11 | bl_label = 'Generate random ID' 12 | bl_description = 'Generates random ID to identify the character, if one exists it will be overwritten!' 13 | 14 | def invoke(self, context, event): 15 | return context.window_manager.invoke_confirm(self, event) 16 | 17 | def execute(self, context): 18 | data = context.scene.character_ui_object.data 19 | # use character_id as the default key 20 | key = context.scene.character_ui_object_id if context.scene.character_ui_object_id else "character_id" 21 | data[key] = ''.join(random.SystemRandom().choice( 22 | string.ascii_letters + string.digits) for _ in range(16)) 23 | self.report({"INFO"}, "Generated new ID: %s" % (data[key])) 24 | return {"FINISHED"} 25 | 26 | 27 | class VIEW3D_PT_character_ui_generate(Panel): 28 | bl_space_type = 'VIEW_3D' 29 | bl_region_type = 'UI' 30 | bl_category = "Character-UI" 31 | bl_label = "Character UI Generate" 32 | 33 | def draw(self, context): 34 | layout = self.layout 35 | box = layout.box() 36 | if context.scene.character_ui_object: 37 | o = context.scene.character_ui_object 38 | box.label(text="Generate UI for %s" % (o.name)) 39 | box.prop(context.scene, "character_ui_object_id") 40 | 41 | row = box.row() 42 | row.operator(OPS_OT_GenerateID.bl_idname) 43 | if context.scene.character_ui_object_id in o.data: 44 | character_id_key = context.scene.character_ui_object_id 45 | character_id = o.data[character_id_key] 46 | rig_layers_key = context.scene.character_ui_rig_layers_key 47 | always_show = context.scene.character_ui_always_show 48 | 49 | row.label(text="Rig ID: %s" % (character_id)) 50 | if character_id != context.scene.character_ui_object_id_value: 51 | box.operator("character_ui.fix_new_id") 52 | visual_box = box.box() 53 | visual_box.label(text="UI Settings") 54 | row = visual_box.row(align=True) 55 | 56 | row.prop(context.scene, "character_ui_custom_label") 57 | row.prop(context.scene, "character_ui_always_show", toggle=True) 58 | if "character_ui_generation_date" in o.data: 59 | generation_row = visual_box.row(align=True) 60 | column_disabled = generation_row.column() 61 | column_disabled.enabled = False 62 | column_disabled.prop(o.data, '["character_ui_generation_date"]', text="UI Generation date", icon="TIME") 63 | generation_row.operator("character_ui.tooltip", text="", icon="QUESTION").tooltip_id = "character_ui_generation_date" 64 | 65 | if "character_ui_char_version" in o.data: 66 | version_row = visual_box.row(align=True) 67 | version_row.prop(o.data, '["character_ui_char_version"]', text="Version", icon="BLENDER") 68 | version_row.operator("character_ui.tooltip", text="", icon="QUESTION").tooltip_id = "character_ui_version" 69 | 70 | op = box.operator("characterui_generate.generate_script") 71 | op.character_id = character_id 72 | op.character_id_key = character_id_key 73 | op.rig_layers_key = rig_layers_key 74 | op.always_show = always_show 75 | custom_label = context.scene.character_ui_custom_label 76 | op.custom_label = custom_label if custom_label not in ["", " "] else context.scene.character_ui_object.name 77 | 78 | else: 79 | box.label(text="You have to select an object!", icon="ERROR") 80 | def character_ui_id_update(self, context): 81 | o = context.scene.character_ui_object 82 | key = context.scene.character_ui_object_id 83 | context.scene.character_ui_object_id_value = o.data[key] 84 | 85 | classes = ( 86 | OPS_OT_GenerateID, 87 | VIEW3D_PT_character_ui_generate 88 | ) 89 | 90 | 91 | def register(): 92 | bpy.types.Scene.character_ui_object_id = StringProperty( 93 | name="Custom Property Name", 94 | description="Custom Property used for storing the Character UI ID, if your character has a unique ID you can use it too", 95 | default="character_id", 96 | update=character_ui_id_update 97 | ) 98 | bpy.types.Scene.character_ui_object_id_value = StringProperty() 99 | bpy.types.Scene.character_ui_custom_label = StringProperty( 100 | name="Label", 101 | description="Text used as the label for the tab in the Sidebar" 102 | ) 103 | bpy.types.Scene.character_ui_always_show = BoolProperty( 104 | name="Always Show", 105 | default=False, 106 | description="Always show the UI panel instead of hiding it when the character is not selected" 107 | ) 108 | for c in classes: 109 | register_class(c) 110 | 111 | 112 | def unregister(): 113 | del bpy.types.Scene.character_ui_object_id 114 | del bpy.types.Scene.character_ui_custom_label 115 | del bpy.types.Scene.character_ui_always_show 116 | 117 | for c in reversed(classes): 118 | unregister_class(c) 119 | -------------------------------------------------------------------------------- /panels/main.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Panel, PropertyGroup) 3 | from bpy.props import (PointerProperty) 4 | from bpy.utils import (register_class, unregister_class) 5 | 6 | 7 | class CharacterUIMainUpdates: 8 | @staticmethod 9 | def update_character_ui_object(self, context): 10 | if context.scene.character_ui_object: 11 | o = context.scene.character_ui_object 12 | CharacterUIMainUpdates.update_character_ui_object_collections(context, o) 13 | CharacterUIMainUpdates.update_character_ui_object_rig_layers(context, o) 14 | context.scene.character_ui_object_body = o.data["body_object"] 15 | context.scene.character_ui_custom_label = o.name 16 | if "cloud_rig" in o: 17 | context.scene.character_ui_object_id = "rig_id" 18 | else: 19 | context.scene.character_ui_object_body = None 20 | 21 | @staticmethod 22 | def update_character_ui_object_collections(context, o): 23 | outfits = None 24 | hair = None 25 | body = None 26 | physics = None 27 | 28 | if "hair_collection" in o.data: 29 | hair = o.data["hair_collection"] 30 | if "outfits_collection" in o.data: 31 | outfits = o.data["outfits_collection"] 32 | if "body_object" in o.data: 33 | body = o.data["body_object"] 34 | if "character_ui_cages" in o.data: 35 | if "collection" in o.data["character_ui_cages"]: 36 | physics = o.data["character_ui_cages"]["collection"] 37 | 38 | context.scene.character_ui_hair_collection = hair 39 | context.scene.character_ui_outfits_collection = outfits 40 | context.scene.character_ui_object_body = body 41 | context.scene.character_ui_physics_collection = physics 42 | 43 | @staticmethod 44 | def update_character_ui_object_rig_layers(context, o): 45 | key = context.scene.character_ui_rig_layers_key 46 | for i in range(31): 47 | visible = False 48 | name = "" 49 | row = i + 1 50 | if key in o: 51 | if len(o[key][i]["name"][:1]) > 0: 52 | visible = not o[key][i]["name"][:1] == "$" 53 | name = o[key][i]["name"] if visible else o[key][i]["name"][1:] 54 | row = o[key][i]["row"] + 1 55 | 56 | context.scene["character_ui_row_visible_%i" % (i)] = visible 57 | context.scene["character_ui_row_name_%i" % (i)] = name 58 | context.scene["character_ui_row_index_%i" % (i)] = row 59 | 60 | @staticmethod 61 | def update_collections(self, context): 62 | if context.scene.character_ui_object: 63 | o = context.scene.character_ui_object 64 | o.data["outfits_collection"] = context.scene.character_ui_outfits_collection 65 | o.data["hair_collection"] = context.scene.character_ui_hair_collection 66 | 67 | @staticmethod 68 | def update_objects(self, context): 69 | if context.scene.character_ui_object: 70 | o = context.scene.character_ui_object 71 | o.data["body_object"] = context.scene.character_ui_object_body 72 | 73 | @staticmethod 74 | def update_physics_collection(self, context): 75 | ch = context.scene.character_ui_object 76 | if ch: 77 | if "character_ui_cages" not in ch.data: 78 | ch.data["character_ui_cages"] = {"collection": None} 79 | ch.data["character_ui_cages"]["collection"] = context.scene.character_ui_physics_collection 80 | 81 | 82 | class VIEW3D_PT_character_ui_main(Panel): 83 | bl_space_type = 'VIEW_3D' 84 | bl_region_type = 'UI' 85 | bl_category = "Character-UI" 86 | bl_label = "Character UI Setup" 87 | 88 | def draw(self, context): 89 | layout = self.layout 90 | box = layout.box() 91 | o_row = box.row(align=True) 92 | o_row.prop(context.scene, "character_ui_object") 93 | o_row.operator("character_ui.tooltip", text="", icon="QUESTION").tooltip_id = "character_ui_object" 94 | ch = context.scene.character_ui_object 95 | if ch: 96 | if ch.type != "ARMATURE": 97 | box.label(text="Object is not an armature", icon="ERROR") 98 | if context.scene.character_ui_object_id in ch.data: 99 | collections = box.box() 100 | collections.label(text="Collections") 101 | collections.prop( 102 | context.scene, "character_ui_outfits_collection") 103 | collections.prop(context.scene, "character_ui_hair_collection") 104 | collections.prop( 105 | context.scene, "character_ui_physics_collection") 106 | objects = box.box() 107 | objects.label(text="Objects") 108 | objects.prop(context.scene, "character_ui_object_body") 109 | else: 110 | box.label( 111 | text="You must generate the ID first before you can continue!", icon="ERROR") 112 | 113 | 114 | def register(): 115 | bpy.types.Scene.character_ui_object = PointerProperty( 116 | name="Character UI Object", 117 | description="Which object is going to be used as the main Character UI object", 118 | type=bpy.types.Object, 119 | update=CharacterUIMainUpdates.update_character_ui_object 120 | ) 121 | bpy.types.Scene.character_ui_object_body = PointerProperty( 122 | name="Body", 123 | description="Which object is the Character body, leave blank if no body", 124 | type=bpy.types.Object, 125 | update=CharacterUIMainUpdates.update_objects 126 | ) 127 | bpy.types.Scene.character_ui_outfits_collection = PointerProperty( 128 | name="Outfits", 129 | description="Collection holding all of the outfits, for consistency should have the charactes name in it as a prefix for example", 130 | type=bpy.types.Collection, 131 | update=CharacterUIMainUpdates.update_collections 132 | ) 133 | bpy.types.Scene.character_ui_hair_collection = PointerProperty( 134 | name="Hair", 135 | description="Collection holding all of the hair styles, for consistency should have the charactes name in it as a prefix for example", 136 | type=bpy.types.Collection, 137 | update=CharacterUIMainUpdates.update_collections 138 | ) 139 | bpy.types.Scene.character_ui_physics_collection = PointerProperty( 140 | name="Physics", 141 | description="Collection holding all of the mesh deform cages, for consistency should have the characters name in it as a prefix for example", 142 | type=bpy.types.Collection, 143 | update=CharacterUIMainUpdates.update_physics_collection 144 | ) 145 | register_class(VIEW3D_PT_character_ui_main) 146 | 147 | 148 | def unregister(): 149 | del bpy.types.Scene.character_ui_object 150 | del bpy.types.Scene.character_ui_object_body 151 | del bpy.types.Scene.character_ui_outfits_collection 152 | del bpy.types.Scene.character_ui_hair_collection 153 | del bpy.types.Scene.character_ui_physics_collection 154 | 155 | unregister_class(VIEW3D_PT_character_ui_main) 156 | -------------------------------------------------------------------------------- /panels/miscellaneous.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Panel, PropertyGroup, Operator) 3 | from bpy.props import (PointerProperty, StringProperty, 4 | BoolProperty, IntProperty) 5 | from bpy.utils import (register_class, unregister_class) 6 | 7 | 8 | class VIEW3D_PT_character_ui_miscellaneous(Panel): 9 | bl_space_type = 'VIEW_3D' 10 | bl_region_type = 'UI' 11 | bl_category = "Character-UI" 12 | bl_label = "Character UI Miscellaneous" 13 | bl_options = {"DEFAULT_CLOSED"} 14 | 15 | @classmethod 16 | def poll(self, context): 17 | ch = context.scene.character_ui_object 18 | return ch 19 | 20 | def draw(self, context): 21 | layout = self.layout 22 | 23 | 24 | class VIEW3D_PT_character_ui_links_panel(Panel): 25 | bl_space_type = 'VIEW_3D' 26 | bl_region_type = 'UI' 27 | bl_label = "Links" 28 | bl_idname = "VIEW3D_PT_character_ui_links_panel" 29 | bl_parent_id = "VIEW3D_PT_character_ui_miscellaneous" 30 | 31 | def draw(self, context): 32 | layout = self.layout 33 | layout.prop(context.scene, "character_ui_links_key") 34 | ch = context.scene.character_ui_object 35 | key = context.scene.character_ui_links_key 36 | if ch: 37 | if key and key in ch.data: 38 | box = layout.box() 39 | box.label(text="Links", icon="URL") 40 | if len(ch.data[key]): 41 | for s in ch.data[key].to_dict(): 42 | section_box = box.box() 43 | row = section_box.row(align=True) 44 | row.label(text=s) 45 | row.operator("character_ui.edit_links_section", 46 | text="", icon="PREFERENCES").link_section = s 47 | row.operator("character_ui.remove_links_section", 48 | text="", icon="TRASH").link_section = s 49 | for l in ch.data[key][s].to_dict(): 50 | link_row = section_box.row(align=True) 51 | try: 52 | link_row.label( 53 | text=l, icon=ch.data[key][s][l][0]) 54 | except: 55 | link_row.label(text=l) 56 | url = ch.data[key][s][l][1] 57 | link_row.operator( 58 | "wm.url_open", text=url).url = url 59 | remove_link = link_row.operator( 60 | "character_ui.remove_link", text="", icon="X") 61 | remove_link.link_section = s 62 | remove_link.link = l 63 | section_box.operator( 64 | "character_ui.add_link", text="Add link", icon="PLUS").link_section = s 65 | box.operator("character_ui.add_links_section", 66 | text="Add Links Section") 67 | else: 68 | layout.operator("character_ui.enable_links", icon="PLUS") 69 | 70 | 71 | classes = [ 72 | VIEW3D_PT_character_ui_miscellaneous, 73 | VIEW3D_PT_character_ui_links_panel 74 | ] 75 | 76 | 77 | def register(): 78 | bpy.types.Scene.character_ui_links_key = StringProperty(name="Links Key", 79 | description="Under which custom property the links are stored", 80 | default="character_ui_links") 81 | for c in classes: 82 | register_class(c) 83 | 84 | 85 | def unregister(): 86 | del bpy.types.Scene.character_ui_links_key 87 | for c in reversed(classes): 88 | unregister_class(c) 89 | -------------------------------------------------------------------------------- /panels/outfits.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Panel) 3 | from bpy.props import (IntProperty) 4 | from bpy.utils import (register_class, unregister_class) 5 | 6 | 7 | class VIEW3D_PT_character_ui_outfits(Panel): 8 | bl_space_type = 'VIEW_3D' 9 | bl_region_type = 'UI' 10 | bl_category = "Character-UI" 11 | bl_label = "Character UI Outfits" 12 | bl_idname = "VIEW3D_PT_character_ui_outfits" 13 | bl_options = {"DEFAULT_CLOSED"} 14 | 15 | @classmethod 16 | def poll(self, context): 17 | collection = context.scene.character_ui_outfits_collection 18 | return collection 19 | 20 | def draw(self, context): 21 | layout = self.layout 22 | outfits_collection = context.scene.character_ui_outfits_collection 23 | loose_objects = outfits_collection.objects 24 | if len(loose_objects): 25 | loose_objects_box = layout.box() 26 | row = loose_objects_box.row() 27 | row.alert = True 28 | row.label(text="%s can't contain objects directly!"%(outfits_collection.name)) 29 | row.operator("character_ui.tooltip", text="", icon="QUESTION").tooltip_id = "cant_contain_directly" 30 | loose_objects_box.operator("character_ui.move_unassigned_objects") 31 | 32 | active_outfit = context.scene.character_ui_active_outfit_index 33 | if active_outfit < len(outfits_collection.children): 34 | 35 | outfits_box = layout.box() 36 | outfits_label_row = outfits_box.row(align=True) 37 | outfits_label_row.label(text="Outfits") 38 | outfits_label_row.operator("character_ui.tooltip", text="", icon="QUESTION").tooltip_id = "outfits" 39 | outfits_box.template_list("UI_UL_list", "character_ui_outfits", outfits_collection, "children", context.scene, "character_ui_active_outfit_index") 40 | 41 | outfit_pieces_box = layout.box() 42 | outfit_pieces_box.label(text="Outfit Pieces") 43 | outfit_pieces_row = outfit_pieces_box.row() 44 | outfit_pieces_row.template_list("UI_UL_list", "character_ui_outfit_pieces", outfits_collection.children[active_outfit], "objects", context.scene, "character_ui_active_outfit_piece_index") 45 | outfit_pieces_row.operator("character_ui.edit_outfit_piece", text="", icon="PREFERENCES") 46 | outfit_pieces_box.operator("character_ui.parent_to_character") 47 | outfit_pieces_box.operator("character_ui.format_outfit_piece_name") 48 | 49 | 50 | 51 | 52 | 53 | classes = [ 54 | VIEW3D_PT_character_ui_outfits 55 | ] 56 | 57 | 58 | def register(): 59 | bpy.types.Scene.character_ui_active_outfit_index = IntProperty(name="Active outfit") 60 | bpy.types.Scene.character_ui_active_outfit_piece_index = IntProperty(name="Active outfit piece") 61 | for c in classes: 62 | register_class(c) 63 | 64 | 65 | def unregister(): 66 | del bpy.types.Scene.character_ui_active_outfit_index 67 | del bpy.types.Scene.character_ui_active_outfit_piece_index 68 | for c in reversed(classes): 69 | unregister_class(c) 70 | -------------------------------------------------------------------------------- /panels/physics.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Panel, PropertyGroup) 3 | from bpy.props import (PointerProperty) 4 | from bpy.utils import (register_class, unregister_class) 5 | 6 | 7 | class VIEW3D_PT_character_ui_physics(Panel): 8 | bl_space_type = 'VIEW_3D' 9 | bl_region_type = 'UI' 10 | bl_category = "Character-UI" 11 | bl_label = "Character UI Physics" 12 | bl_options = {'HEADER_LAYOUT_EXPAND', 'DEFAULT_CLOSED'} 13 | 14 | @classmethod 15 | def poll(self, context): 16 | collection = context.scene.character_ui_physics_collection 17 | return collection != None 18 | 19 | def draw_header(self, context): 20 | self.layout.label(text="") 21 | row = self.layout.row(align=True) 22 | row.operator("character_ui.tooltip", text="", icon="QUESTION").tooltip_id = "character_ui_physics" 23 | 24 | def draw(self, context): 25 | layout = self.layout 26 | collection = context.scene.character_ui_physics_collection 27 | 28 | def render_meshes(items): 29 | for i in items: 30 | if hasattr(i, "type"): 31 | if i.type == "MESH": 32 | for m in i.modifiers: 33 | if m.type == "CLOTH" or m.type == "SOFT_BODY": 34 | op = layout.operator( 35 | "character_ui.use_as_cage", text=i.name) 36 | op.cage = i.name 37 | 38 | else: 39 | render_meshes([*i.children, *i.objects]) 40 | objects_to_render = [*collection.children, *collection.objects] 41 | if len(objects_to_render): 42 | render_meshes(objects_to_render) 43 | else: 44 | layout.label( 45 | text="Collection has no objects or children collections", icon="ERROR") 46 | 47 | 48 | classes = [ 49 | VIEW3D_PT_character_ui_physics 50 | ] 51 | 52 | 53 | def register(): 54 | for c in classes: 55 | register_class(c) 56 | 57 | 58 | def unregister(): 59 | for c in reversed(classes): 60 | unregister_class(c) 61 | -------------------------------------------------------------------------------- /panels/rig_layers.py: -------------------------------------------------------------------------------- 1 | import bpy 2 | from bpy.types import (Panel, PropertyGroup, Operator) 3 | from bpy.props import (PointerProperty, StringProperty, 4 | BoolProperty, IntProperty) 5 | from bpy.utils import (register_class, unregister_class) 6 | 7 | 8 | class CharacterUIRigLayerUpdates(): 9 | @staticmethod 10 | def update_rig_layer_key(self, context): 11 | key = context.scene.character_ui_rig_layers_key 12 | o = context.scene.character_ui_object 13 | for i in range(32): 14 | visible = False 15 | name = "" 16 | row = i + 1 17 | if key in o.data: 18 | if len(o.data[key][i]["name"][:1]) > 0: 19 | visible = not o.data[key][i]["name"][:1] == "$" 20 | name = o.data[key][i]["name"] if visible else o.data[key][i]["name"][1:] 21 | row = o.data[key][i]["row"] + 1 22 | 23 | context.scene["character_ui_row_visible_%i" % (i)] = visible 24 | context.scene["character_ui_row_name_%i" % (i)] = name 25 | context.scene["character_ui_row_index_%i" % (i)] = row 26 | 27 | 28 | class VIEW3D_PT_character_ui_rig_layers(Panel): 29 | bl_space_type = 'VIEW_3D' 30 | bl_region_type = 'UI' 31 | bl_category = "Character-UI" 32 | bl_label = "Character UI Rig Layers" 33 | bl_options = {'HEADER_LAYOUT_EXPAND', 'DEFAULT_CLOSED'} 34 | 35 | def draw_header(self, context): 36 | self.layout.label(text="") 37 | row = self.layout.row(align=True) 38 | row.operator("character_ui.tooltip", text="", icon="QUESTION").tooltip_id = "character_ui_rig_layers" 39 | 40 | @classmethod 41 | def poll(self, context): 42 | ch = context.scene.character_ui_object 43 | return ch and ch.type == "ARMATURE" 44 | 45 | def draw(self, context): 46 | layout = self.layout 47 | box = layout.box() 48 | box.label(text="Rig Layers") 49 | ch = context.scene.character_ui_object 50 | if ch: 51 | if ch.type == "ARMATURE": 52 | active_bcoll = ch.data.collections.active 53 | 54 | row = layout.row() 55 | row.template_bone_collection_tree() 56 | 57 | col = row.column(align=True) 58 | col.operator("armature.collection_add", icon='ADD', text="") 59 | col.operator("armature.collection_remove", icon='REMOVE', text="") 60 | 61 | col.separator() 62 | 63 | col.menu("ARMATURE_MT_collection_context_menu", icon='DOWNARROW_HLT', text="") 64 | 65 | if active_bcoll: 66 | col.separator() 67 | col.operator("armature.collection_move", icon='TRIA_UP', text="").direction = 'UP' 68 | col.operator("armature.collection_move", icon='TRIA_DOWN', text="").direction = 'DOWN' 69 | 70 | row = layout.row() 71 | 72 | sub = row.row(align=True) 73 | sub.operator("armature.collection_assign", text="Assign") 74 | sub.operator("armature.collection_unassign", text="Remove") 75 | 76 | sub = row.row(align=True) 77 | sub.operator("armature.collection_select", text="Select") 78 | sub.operator("armature.collection_deselect", text="Deselect") 79 | 80 | else: 81 | box.label(text="Object is not an armature", icon="ERROR") 82 | else: 83 | box.label(text="You have to select an object!", icon="ERROR") 84 | 85 | 86 | def character_ui_generate_rig_layers(self, context): 87 | "generates UI rig layers for the UI" 88 | ch = context.scene.character_ui_object 89 | key = context.scene.character_ui_rig_layers_key 90 | if ch and key: 91 | ch.data[key] = [] 92 | layers = [] 93 | for i in range(32): 94 | row = i 95 | if "character_ui_row_index_%i" % (i) in context.scene: 96 | row = context.scene["character_ui_row_index_%i" % (i)] - 1 97 | name = "Layer "+str(i+1) 98 | if "character_ui_row_name_%i" % (i) in context.scene: 99 | if context.scene["character_ui_row_name_%i" % (i)] not in ["", " "]: 100 | name = context.scene["character_ui_row_name_%i" % (i)] 101 | 102 | if "character_ui_row_visible_%i" % (i) in context.scene: 103 | if not context.scene["character_ui_row_visible_%i" % (i)]: 104 | name = "$%s" % (name) 105 | else: 106 | name = "$%s" % (name) 107 | 108 | layers.insert(i, {'name': name, "row": row}) 109 | ch.data[key] = layers 110 | 111 | 112 | classes = [ 113 | VIEW3D_PT_character_ui_rig_layers 114 | ] 115 | 116 | 117 | def register(): 118 | bpy.types.Scene.character_ui_rig_layers_key = StringProperty( 119 | name="Rig Layers Key", default="rig_layers", update=CharacterUIRigLayerUpdates.update_rig_layer_key) 120 | for i in range(32): 121 | setattr(bpy.types.Scene, "character_ui_row_visible_%i" % (i), 122 | BoolProperty(name="", update=character_ui_generate_rig_layers)) 123 | setattr(bpy.types.Scene, "character_ui_row_name_%i" % ( 124 | i), StringProperty(name="", update=character_ui_generate_rig_layers)) 125 | setattr(bpy.types.Scene, "character_ui_row_index_%i" % (i), IntProperty( 126 | name="UI Row", min=1, max=32, default=i+1, update=character_ui_generate_rig_layers)) 127 | 128 | for c in classes: 129 | register_class(c) 130 | 131 | 132 | def unregister(): 133 | del bpy.types.Scene.character_ui_rig_layers_key 134 | for i in range(32): 135 | delattr(bpy.types.Scene, "character_ui_row_visible_%i" % (i)) 136 | delattr(bpy.types.Scene, "character_ui_row_name_%i" % (i)) 137 | delattr(bpy.types.Scene, "character_ui_row_index_%i" % (i)) 138 | 139 | for c in reversed(classes): 140 | unregister_class(c) 141 | -------------------------------------------------------------------------------- /template.blend: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextr3d/Character-UI/376ebbbbb88235e556ee8302f55422dc957236d6/template.blend -------------------------------------------------------------------------------- /template.blend1: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nextr3d/Character-UI/376ebbbbb88235e556ee8302f55422dc957236d6/template.blend1 --------------------------------------------------------------------------------