├── .gitignore ├── LICENSE ├── README.md ├── github_media ├── demovids │ ├── demo1a.gif │ ├── demo2a.gif │ ├── demo3.gif │ ├── demo4.gif │ └── demo5.gif └── oppanel_2201.png └── shape_key_tools ├── __init__.py ├── common.py ├── icons ├── arrow_branch.png ├── arrow_divide.png ├── arrow_join.png ├── arrow_merge.png └── line_diagonal.png └── ops ├── apply_modifiers_to_shape_keys.py ├── arbitrary_merge_blend_two.py ├── arbitrary_split_by_filter.py ├── internal_splitpair_preview_mesh.py ├── internal_viewport_visuals.py ├── pairs_merge_active.py ├── pairs_merge_all.py ├── pairs_split_active.py └── pairs_split_all.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | 132 | 133 | # Design-time materials 134 | Sources/ 135 | 136 | # Local deployment tools 137 | Install to Blender Scripts Folder.bat 138 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 TiberiumFusion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Shape Key Tools for Blender 2.79 4 | 5 | ### Tools for manipulating shape keys beyond Blender's limited abilities. 6 | 7 | * Split paired shape keys into left & right halves. Merge left & right shape key pairs together. 8 | * Combine shape keys with various blend modes and per-vertex filtering 9 | * Split shape keys with per-vertex filtering 10 | * Apply modifiers to meshes with shape keys 11 | 12 | "Officially" compatible with Blender 2.78-2.79, but it should work on older versions (possibly up to 2.75). 13 |
14 | ## Split & Merge Shape Key Pairs 15 | Split and merge shape key pairs, using a `MyShapeKeyL+MyShapeKeyR` naming convention. 16 | * Useful for separating and combining the left and right halves of expressions, such as eyebrow, eye, and mouth shapes. 17 | * Choose the world axis that defines the "left" and "right" sides of the model. 18 | * Crossfade split shape keys for a smooth transition between each half. 19 | 20 | ### Split and merge shape keys individually or all at once 21 | ![Demo1 gif](github_media/demovids/demo1a.gif) 22 | 23 | ### Fine-tune splitting with viewport guides and live preview 24 | ![Demo2 gif](github_media/demovids/demo2a.gif) 25 | 26 | 27 | ## Combine shape keys 28 | Combine two shapes keys together with a variety of Photoshop-like blending modes and vertex filtering options. 29 | * Useful when you don't want to fuss with the shape key panel and "New Shape Key From Mix", or when Blender's sole ability to additively blend all verts is not desirable. 30 | * Blend modes: `add`, `subtract`, `multiply`, `divide`, `overwrite`, `lerp` 31 | * Vertex filtering "masks" which verts are blended together, using the shape key's characteristics or the model's vertex groups. 32 | 33 | ![Demo3 gif](github_media/demovids/demo3.gif) 34 | 35 | 36 | ## Split shape keys 37 | Split off a new shape key from an existing shape key, using various options to filter the result on a per-vertex basis. 38 | * Useful for extracting a specific part of a shape key. 39 | * Vertex filtering "masks" which verts are split off, using the shape key's characteristics or the model's vertex groups. 40 | 41 | ![Demo4 gif](github_media/demovids/demo4.gif) 42 | 43 | 44 | ## Apply modifiers to meshes with shape keys 45 | Apply modifiers to meshes with shape keys, baking the modifier into both the base mesh and the shape key deltas. 46 | * Useful when you don't want to recreate 100 blend shapes just because Blender doesn't know how to deform shape keys. 47 | * Multiple modifiers can be applied at once, in stack order. 48 | * Only topology-preserving modifiers are compatible. Modifiers which change topology (incl. vertex count) cannot be applied. 49 | 50 | ![Demo5 gif](github_media/demovids/demo5.gif) 51 | -------------------------------------------------------------------------------- /github_media/demovids/demo1a.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiberiumFusion/BlenderShapeKeyTools/63a86145b5eb22e41a128d65ecfc2cb8f38b73af/github_media/demovids/demo1a.gif -------------------------------------------------------------------------------- /github_media/demovids/demo2a.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiberiumFusion/BlenderShapeKeyTools/63a86145b5eb22e41a128d65ecfc2cb8f38b73af/github_media/demovids/demo2a.gif -------------------------------------------------------------------------------- /github_media/demovids/demo3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiberiumFusion/BlenderShapeKeyTools/63a86145b5eb22e41a128d65ecfc2cb8f38b73af/github_media/demovids/demo3.gif -------------------------------------------------------------------------------- /github_media/demovids/demo4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiberiumFusion/BlenderShapeKeyTools/63a86145b5eb22e41a128d65ecfc2cb8f38b73af/github_media/demovids/demo4.gif -------------------------------------------------------------------------------- /github_media/demovids/demo5.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiberiumFusion/BlenderShapeKeyTools/63a86145b5eb22e41a128d65ecfc2cb8f38b73af/github_media/demovids/demo5.gif -------------------------------------------------------------------------------- /github_media/oppanel_2201.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiberiumFusion/BlenderShapeKeyTools/63a86145b5eb22e41a128d65ecfc2cb8f38b73af/github_media/oppanel_2201.png -------------------------------------------------------------------------------- /shape_key_tools/__init__.py: -------------------------------------------------------------------------------- 1 | # //////////////////////////////////////////////////////////////////////////////////////////////////// 2 | # // 3 | # // Shape Key Tools for Blender 2.79 4 | # // - Split/merge shape keys pairs (e.g. MyShapeKeyL+MyShapeKeyR) into left and right halves 5 | # // - Merge any two arbitrary shape keys with various blending modes 6 | # // - Split filtered deltas off from one shape key to form a new shape key 7 | # // - Apply modifiers to meshes with shape keys 8 | # // 9 | # //////////////////////////////////////////////////////////////////////////////////////////////////// 10 | 11 | bl_info = { 12 | "name": "Shape Key Tools", 13 | "author": "TiberiumFusion", 14 | "version": (2, 2, 1, 2), 15 | "blender": (2, 78, 0), # This is a guess... I think it was 2.77 or 2.78 that added some of the operators/api we need. Definitely no earlier than 2.75, since that is when support for custom icons was added. 16 | "location": "View3D (Object Mode) > Tool Shelf > Tools > Shape Key Tools", 17 | "description": "Tools for working with shape keys beyond Blender's limited abilities.", 18 | "wiki_url": "https://github.com/TiberiumFusion/BlenderShapeKeyTools", 19 | "tracker_url": "https://github.com/TiberiumFusion/BlenderShapeKeyTools/issues", 20 | "warning": "", 21 | "category": "Tools", 22 | } 23 | 24 | import sys, os, imp, types 25 | from types import SimpleNamespace 26 | 27 | import bpy, bpy.utils.previews 28 | from bpy.props import * 29 | from bpy.app.handlers import persistent 30 | 31 | from . import common 32 | 33 | 34 | # Container of our custom icons 35 | UiIconsExtra = None 36 | 37 | # Dictionary of registered operators, mapped by their Blender ID (e.g. "wm.shape_key_tools_split_active_pair") 38 | RegisteredOps = {} 39 | 40 | # Set by register() and unregister(), used by persistent application handlers to deactive as expected when the addon is unloaded 41 | AddonEnabled = False 42 | 43 | 44 | 45 | # 46 | #==================================================================================================== 47 | # Helpers 48 | #==================================================================================================== 49 | # 50 | 51 | ### Gets one of this addon's operator classes as specified by its bl_id 52 | def GetRegisteredOpClass(clsBlId): 53 | global RegisteredOps 54 | 55 | if (clsBlId in RegisteredOps): 56 | return RegisteredOps[clsBlId].OpClass # We have to manually track our registered operators because bpy.types. is always null for no good reason 57 | 58 | 59 | 60 | # 61 | #==================================================================================================== 62 | # Background modal operators 63 | #==================================================================================================== 64 | # 65 | 66 | ## 67 | ## Viewport-related background ops 68 | ## 69 | 70 | def StartViewportVisualizationOp(contextOverride=None): 71 | opCls = GetRegisteredOpClass("view3d.shape_key_tools_viewport_visuals") 72 | if (opCls.IsRunning == False): 73 | if (contextOverride != None): 74 | bpy.ops.view3d.shape_key_tools_viewport_visuals(contextOverride) 75 | else: 76 | bpy.ops.view3d.shape_key_tools_viewport_visuals() 77 | 78 | def StartSplitPairPreviewOp(contextOverride=None): 79 | opCls = GetRegisteredOpClass("view3d.shape_key_tools_splitpair_preview") 80 | if (opCls.InstanceInfo == None): 81 | if (contextOverride != None): 82 | bpy.ops.view3d.shape_key_tools_splitpair_preview(contextOverride) 83 | else: 84 | bpy.ops.view3d.shape_key_tools_splitpair_preview() 85 | 86 | 87 | ### Create a watcher for the blend file load event so we can start background ops if a blend file was saved with any enabled 88 | @persistent 89 | def BlendFileOpenedWatcher(dummy): 90 | try: # All of this is dangerous since we cannot assume anything about the user's actions or intent when this is raised 91 | if (AddonEnabled): 92 | scene = bpy.context.scene 93 | properties = scene.shape_key_tools_props 94 | 95 | runViewportVisualizer = properties.opt_shapepairs_splitmerge_viewportvisualize 96 | runSplitPairPreview = (properties.opt_shapepairs_splitmerge_preview_split_left or properties.opt_shapepairs_splitmerge_preview_split_right) 97 | 98 | overrideContext = None 99 | if (runViewportVisualizer or runSplitPairPreview): 100 | # Because there is no way to get the context of the viewport control, we have to make it ourselves... grrr 101 | # Reference: https://b3d.interplanety.org/en/context-override/ 102 | areas = [area for area in bpy.context.screen.areas if area.type == "VIEW_3D"] 103 | if (len(areas) > 0): 104 | area = areas[0] 105 | overrideContext = bpy.context.copy() 106 | overrideContext["window"] = bpy.context.window 107 | overrideContext["screen"] = bpy.context.screen 108 | overrideContext["scene"] = bpy.context.scene 109 | overrideContext["area"] = area 110 | overrideContext["region"] = area.regions[-1] 111 | overrideContext["space_data"] = area.spaces.active 112 | 113 | if (runViewportVisualizer): 114 | try: 115 | StartViewportVisualizationOp(overrideContext) 116 | except: 117 | pass 118 | 119 | if (runSplitPairPreview): 120 | try: 121 | StartSplitPairPreviewOp(overrideContext) 122 | except: 123 | pass 124 | except: 125 | pass 126 | bpy.app.handlers.load_post.append(BlendFileOpenedWatcher) 127 | 128 | 129 | 130 | # 131 | #==================================================================================================== 132 | # Top level properties 133 | #==================================================================================================== 134 | # 135 | 136 | class ShapeKeyTools_Properties(bpy.types.PropertyGroup): 137 | 138 | ## 139 | ## UI glue 140 | ## 141 | 142 | ### Subpanel expanders 143 | opt_gui_subpanel_expander_globalopts = BoolProperty(name="Global Options", default=False, description="These options are shared by all operations, but some operations may not use them. Read each operation's tooltip for more info!") 144 | opt_gui_subpanel_expander_shapepairs = BoolProperty(name="Split/Merge Pairs", default=True, description="Operations for splitting and merging shape key pairs (i.e. symmetrical shape keys, like facial expressions)") 145 | opt_gui_subpanel_expander_shapepairsopts = BoolProperty(name="Split/Merge Pairs Options", default=True, description="These options ONLY affect the 4 operations below") 146 | opt_gui_subpanel_expander_arbitrary = BoolProperty(name="Arbitrary Split/Merge Operations", default=True, description="General purpose split & merge operations") 147 | opt_gui_subpanel_expander_modifiers = BoolProperty(name="Modifer Operations", default=True, description="Operations involving shape keys and modifiers") 148 | opt_gui_subpanel_expander_info = BoolProperty(name="Info", default=True, description="Miscellaneous information on the active object's shape keys") 149 | 150 | ### Helpers for enabling/disabling controls based on other controls 151 | opt_gui_enabler_shapepairs_split_smoothdist = BoolProperty(name="", default=False, description="For internal addon use only.") 152 | 153 | 154 | ## 155 | ## Global options for all ops 156 | ## 157 | 158 | opt_global_enable_filterverts = BoolProperty( 159 | name = "Vertex Filter", 160 | description = "Filter shape key vertices by comparing them with the conditions below. Vertices that pass ALL conditions are considered RED. All other vertices are considered BLACK. Each operation may treat RED and BLACK vertices DIFFERENTLY, so read every tooltip!", 161 | default = False, 162 | ) 163 | 164 | opt_global_filterverts_distance_min_enable = BoolProperty( 165 | name = "Enable Minimum Delta", 166 | description = "Enables the 'Minimum Delta' filter condition", 167 | default = False, 168 | ) 169 | opt_global_filterverts_distance_min = FloatProperty( 170 | name = "Minimum Delta", 171 | description = "Vertex delta (difference in position from basis shape key) must be at least this distance (in local object space). By setting this to a low value, such as 0.1, you can filter out vertices with imperceptible deltas", 172 | min = 0.0, 173 | soft_min = 0.0, 174 | soft_max = 100.0, 175 | default = 0.1, 176 | precision = 6, 177 | step = 1, 178 | subtype = 'DISTANCE', 179 | unit = 'LENGTH', 180 | ) 181 | 182 | opt_global_filterverts_distance_max_enable = BoolProperty( 183 | name = "Enable Maximum Delta", 184 | description = "Enables the 'Maximum Delta' filter condition", 185 | default = False, 186 | ) 187 | opt_global_filterverts_distance_max = FloatProperty( 188 | name = "Maximum Delta", 189 | description = "Vertex delta (difference in position from basis shape key) must be no greater than this distance (in local object space). By setting this to a high value, such as 50, you can filter out vertices with extreme deltas", 190 | min = 0.0, 191 | soft_min = 0.0, 192 | soft_max = 10000.0, 193 | default = 10000.0, 194 | precision = 6, 195 | step = 1, 196 | subtype = 'DISTANCE', 197 | unit = 'LENGTH', 198 | ) 199 | 200 | opt_global_filterverts_vertexgroup_latestitems = None 201 | def getActiveObjectVertexGroups(self, context): 202 | vertexGroupsOrdered = [] 203 | for vg in context.object.vertex_groups: 204 | vertexGroupsOrdered.append((vg.index, vg.name)) 205 | def s(v): 206 | return v[0] 207 | vertexGroupsOrdered.sort(key=s) 208 | opt_global_filterverts_vertexgroup_latestitems = [(str(tuple[0]), tuple[1], tuple[1], "GROUP_VERTEX", tuple[0]) for tuple in vertexGroupsOrdered] 209 | return opt_global_filterverts_vertexgroup_latestitems 210 | opt_global_filterverts_vertexgroup_enable = BoolProperty( 211 | name = "Enable Vertex Group", 212 | description = "Enables the 'Vertex Group' filter condition", 213 | default = False, 214 | ) 215 | opt_global_filterverts_vertexgroup = EnumProperty( 216 | name = "Vertex Group", 217 | description = "Vertex must belong to the specified vertex group", 218 | items = getActiveObjectVertexGroups, 219 | ) 220 | 221 | # Creates a dictionary with the values of only the ENABLED vertex filter params 222 | def getEnabledVertexFilterParams(self): 223 | params = {} 224 | if (self.opt_global_filterverts_distance_min_enable): 225 | params["DeltaDistanceMin"] = self.opt_global_filterverts_distance_min 226 | if (self.opt_global_filterverts_distance_max_enable): 227 | params["DeltaDistanceMax"] = self.opt_global_filterverts_distance_max 228 | if (self.opt_global_filterverts_vertexgroup_enable): 229 | params["VertexGroupIndex"] = self.opt_global_filterverts_vertexgroup 230 | return params 231 | 232 | 233 | ## 234 | ## Local options for shape key pairs split & merge 235 | ## 236 | 237 | opt_shapepairs_split_axis = EnumProperty( 238 | name = "", 239 | description = "World axis for splitting/merging shape keys into 'left' and 'right' halves.", 240 | items = [ 241 | ("+X", "+X", "Split/merge shape keys into a +X half ('left') and a -X half ('right'), using the YZ world plane. Pick this if your character faces -Y.", "AXIS_SIDE", 1), 242 | ("+Y", "+Y", "Split/merge shape keys into a +Y half ('left') and a -Y half ('right'), using the XZ world plane. Pick this if your character faces +X.", "AXIS_FRONT", 2), 243 | ("+Z", "+Z", "Split/merge shape keys into a +Z half ('left') and a -Z half ('right'), using the XY world plane.", "AXIS_TOP", 3), 244 | ("-X", "-X", "Split/merge shape keys into a -X half ('left') and a +X half ('right'), using the YZ world plane. Pick this if your character faces +Y.", "AXIS_SIDE", 4), 245 | ("-Y", "-Y", "Split/merge shape keys into a -Y half ('left') and a +Y half ('right'), using the XZ world plane. Pick this if your character faces -X.", "AXIS_FRONT", 5), 246 | ("-Z", "-Z", "Split/merge shape keys into a -Z half ('left') and a +Z half ('right'), using the XY world plane.", "AXIS_TOP", 6), 247 | ] 248 | ) 249 | 250 | def inputShapePairsSplitModeChanged(self, context): 251 | if (self.opt_shapepairs_split_mode == "smooth"): 252 | self.opt_gui_enabler_shapepairs_split_smoothdist = True 253 | else: 254 | self.opt_gui_enabler_shapepairs_split_smoothdist = False 255 | opt_shapepairs_split_mode = EnumProperty( 256 | name = "", 257 | description = "Method for determing the per-side deltas when splitting shape keys into 'left' and 'right' halves.", 258 | items = [ 259 | ("sharp", "Sharp", "Sharp divide. No smoothing between the split left and right halves. Useful for shape keys that have distinct halves and do not connect in the center, like eye shapes."), 260 | ("smooth", "Smooth", "Smoothed divide. The left and right halves are crossfaded within the specified Smoothing Distance. Useful for shape keys that connect in the center, like mouth shapes."), 261 | ], 262 | update = inputShapePairsSplitModeChanged, 263 | ) 264 | 265 | opt_shapepairs_split_smoothdist = FloatProperty( 266 | name = "Smoothing Distance", 267 | description = "Only used by Smooth Split Mode. Radius (in worldspace) from the center of split axis that defines the region in which the left and right halves of the shape key are smoothed when split. Smoothing uses simple bezier interpolation", 268 | min = 0.0, 269 | soft_min = 0.0, 270 | soft_max = 100.0, 271 | default = 1.0, 272 | precision = 2, 273 | step = 0.1, 274 | subtype = 'DISTANCE', 275 | unit = 'LENGTH', 276 | ) 277 | 278 | opt_shapepairs_merge_mode = EnumProperty( 279 | name = "", 280 | description = "Method for combining left and right shape keys together.", 281 | items = [ 282 | ("additive", "Additive", "All deltas from both shape keys will be added together. This mode is suitable if you previously split the shape key with either the 'Smooth' or 'Sharp' mode."), 283 | ("overwrite", "Overwrite", "Only the deltas from the left side of the left shape key and the right side of the right shape key will be used. This mode is only suitable if you previously split the shape key with 'Sharp' mode."), 284 | ], 285 | ) 286 | 287 | def inputShapePairsVisualizeSplitMergeAxisChanged(self, context): 288 | StartViewportVisualizationOp() 289 | opt_shapepairs_splitmerge_viewportvisualize = BoolProperty( 290 | name = "Visualize Split/Merge Regions", 291 | description = "Draws an overlay in the viewport that represents the current split/merge parameters", 292 | default = False, 293 | update = inputShapePairsVisualizeSplitMergeAxisChanged, 294 | ) 295 | opt_shapepairs_splitmerge_viewportvisualize_show_splitplane = BoolProperty( 296 | name = "Show Split Plane", 297 | description = "Show the world plane that will bisect the shape keys", 298 | default = True, 299 | ) 300 | opt_shapepairs_splitmerge_viewportvisualize_show_splithalves = BoolProperty( 301 | name = "Show Halves", 302 | description = "Shows the left and right sides of the split plane", 303 | default = True, 304 | ) 305 | opt_shapepairs_splitmerge_viewportvisualize_show_smoothregion = BoolProperty( 306 | name = "Show Smoothing Region", 307 | description = "Shows the region where the split shape keys will be cross-blended when Split Mode is set to Smooth", 308 | default = True, 309 | ) 310 | 311 | def inputSplitPairPreviewLeftChanged(self, context): 312 | if (self.opt_shapepairs_splitmerge_preview_split_left): 313 | self.opt_shapepairs_splitmerge_preview_split_right = False 314 | StartSplitPairPreviewOp() 315 | opt_shapepairs_splitmerge_preview_split_left = BoolProperty( 316 | name = "L", 317 | description = "Live preview the result of the Split Active Shape Key operator. Change the split options with realtime feedback in the viewport", 318 | default = False, 319 | update = inputSplitPairPreviewLeftChanged, 320 | ) 321 | 322 | def inputSplitPairPreviewRightChanged(self, context): 323 | if (self.opt_shapepairs_splitmerge_preview_split_right): 324 | self.opt_shapepairs_splitmerge_preview_split_left = False 325 | StartSplitPairPreviewOp() 326 | opt_shapepairs_splitmerge_preview_split_right = BoolProperty( 327 | name = "R", 328 | description = "Live preview the result of the Split Active Shape Key operator. Change the split options with realtime feedback in the viewport", 329 | default = False, 330 | update = inputSplitPairPreviewRightChanged, 331 | ) 332 | 333 | 334 | 335 | # 336 | #==================================================================================================== 337 | # Ops panel 338 | #==================================================================================================== 339 | # 340 | 341 | class OBJECT_PT_ShapeKeyTools_Panel(bpy.types.Panel): 342 | bl_label = "Shape Key Tools" 343 | bl_idname = "OBJECT_PT_shape_key_tools_panel" 344 | bl_space_type = "VIEW_3D" 345 | bl_region_type = "TOOLS" 346 | bl_category = "Tools" 347 | bl_context = "objectmode" 348 | bl_options = {"DEFAULT_CLOSED"} 349 | 350 | @classmethod 351 | def poll(cls, context): 352 | if (context.object == None): 353 | return False 354 | if (context.object.type != "MESH"): 355 | return False 356 | return True 357 | 358 | def draw(self, context): 359 | global UiIconsExtra 360 | 361 | scene = context.scene 362 | properties = scene.shape_key_tools_props 363 | 364 | layout = self.layout 365 | 366 | obj = context.object 367 | 368 | if (obj == None or obj.type != "MESH"):# or not hasattr(obj.data.shape_keys, "key_blocks") or len(obj.data.shape_keys.key_blocks.keys()) <= 1): 369 | layout.label("Active object is invalid.") 370 | return 371 | 372 | ### Global options 373 | g0 = layout.box() 374 | g0Col = g0.column() 375 | g0Header = g0Col.row() 376 | g0Header.alignment = 'LEFT' 377 | g0Header.prop(properties, "opt_gui_subpanel_expander_globalopts", text="Common Options", icon=("TRIA_DOWN" if properties.opt_gui_subpanel_expander_globalopts else "TRIA_RIGHT"), emboss=False, expand=False) 378 | if (properties.opt_gui_subpanel_expander_globalopts): 379 | g0Body = g0Col.column() 380 | # Filter verts 381 | filterVerts = g0Body.box().column() 382 | filterVertsHeader = filterVerts.row() 383 | filterVertsHeader.alignment = 'EXPAND' 384 | filterVertsHeader.prop(properties, "opt_global_enable_filterverts") 385 | filterVertsHeader.label("", icon="FILTER") 386 | if (properties.opt_global_enable_filterverts): 387 | filterVertsBody = filterVerts.column() 388 | # Vertex group 389 | vg = filterVertsBody.box() 390 | vgCol = vg.column() 391 | vgCol.prop(properties, "opt_global_filterverts_vertexgroup_enable", text="Vertex Group:") 392 | vgValueCon = vgCol.row() 393 | vgValueCon.prop(properties, "opt_global_filterverts_vertexgroup", text="") 394 | vgValueCon.enabled = properties.opt_global_filterverts_vertexgroup_enable 395 | # Delta distance 396 | deltaDist = filterVertsBody.box() 397 | deltaDist.label("Delta Distance:") 398 | deltaDistCols = deltaDist.column_flow(columns=2, align=False) 399 | deltaDistMin = deltaDistCols.column() 400 | deltaDistMax = deltaDistCols.column() 401 | deltaDistMin.prop(properties, "opt_global_filterverts_distance_min_enable", text="Minimum:") 402 | deltaDistMinValueCon = deltaDistMin.row() 403 | deltaDistMinValueCon.prop(properties, "opt_global_filterverts_distance_min", text="") 404 | deltaDistMinValueCon.enabled = properties.opt_global_filterverts_distance_min_enable 405 | deltaDistMax.prop(properties, "opt_global_filterverts_distance_max_enable", text="Maximum:") 406 | deltaDistMaxValueCon = deltaDistMax.row() 407 | deltaDistMaxValueCon.prop(properties, "opt_global_filterverts_distance_max", text="") 408 | deltaDistMaxValueCon.enabled = properties.opt_global_filterverts_distance_max_enable 409 | 410 | ### Split/merge pairs 411 | g1 = layout.box() 412 | g1Col = g1.column() 413 | g1Header = g1Col.row() 414 | g1Header.alignment = 'LEFT' 415 | g1Header.prop(properties, "opt_gui_subpanel_expander_shapepairs", text="Split/Merge L+R Pairs", icon=("TRIA_DOWN" if properties.opt_gui_subpanel_expander_shapepairs else "TRIA_RIGHT"), emboss=False, expand=False) 416 | if (properties.opt_gui_subpanel_expander_shapepairs): 417 | g1Body = g1Col.column() 418 | # Options 419 | g1sg1 = g1Body.box() 420 | g1sg1Col = g1sg1.column() 421 | g1sg1Header = g1sg1Col.row() 422 | g1sg1Header.alignment = 'LEFT' 423 | g1sg1Header.prop(properties, "opt_gui_subpanel_expander_shapepairsopts", text="Options", icon=("TRIA_DOWN" if properties.opt_gui_subpanel_expander_shapepairsopts else "TRIA_RIGHT"), emboss=False, expand=False) 424 | if (properties.opt_gui_subpanel_expander_shapepairsopts): 425 | g1sg1Body = g1sg1Col.column() 426 | # Split axis 427 | g1sg1BodyRow1 = g1sg1Body.row() 428 | g1sg1BodyRow1.label("Split Axis:") 429 | g1sg1BodyRow1.prop(properties, "opt_shapepairs_split_axis", text="") 430 | # Split mode 431 | g1sg1BodyRow2 = g1sg1Body.row() 432 | g1sg1BodyRow2.label("Split Mode:") 433 | g1sg1BodyRow2.prop(properties, "opt_shapepairs_split_mode", text="") 434 | # Smoothing factor 435 | g1sg1BodyRow3 = g1sg1Body.row() 436 | g1sg1BodyRow3.prop(properties, "opt_shapepairs_split_smoothdist") 437 | g1sg1BodyRow3.enabled = properties.opt_gui_enabler_shapepairs_split_smoothdist 438 | # Merge mode 439 | g1sg1BodyRow4 = g1sg1Body.row() 440 | g1sg1BodyRow4.label("Merge Mode:") 441 | g1sg1BodyRow4.prop(properties, "opt_shapepairs_merge_mode", text="") 442 | ### Split/merge visualization 443 | # Master show/hide 444 | g1sg1BodyRow5 = g1sg1Body.row() 445 | g1sg1BodyRow5.prop(properties, "opt_shapepairs_splitmerge_viewportvisualize") 446 | if (properties.opt_shapepairs_splitmerge_viewportvisualize): 447 | # Show split plane 448 | g1sg1BodyRow6 = g1sg1Body.row() 449 | g1sg1BodyRow6.separator() 450 | g1sg1BodyRow6.prop(properties, "opt_shapepairs_splitmerge_viewportvisualize_show_splitplane") 451 | g1sg1BodyRow6.enabled = properties.opt_shapepairs_splitmerge_viewportvisualize 452 | # Show split halves 453 | g1sg1BodyRow7 = g1sg1Body.row() 454 | g1sg1BodyRow7.separator() 455 | g1sg1BodyRow7.prop(properties, "opt_shapepairs_splitmerge_viewportvisualize_show_splithalves") 456 | g1sg1BodyRow7.enabled = properties.opt_shapepairs_splitmerge_viewportvisualize 457 | # Show smoothing region 458 | g1sg1BodyRow8 = g1sg1Body.row() 459 | g1sg1BodyRow8.separator() 460 | g1sg1BodyRow8.enabled = properties.opt_shapepairs_splitmerge_viewportvisualize 461 | g1sg1BodyRow8a = g1sg1BodyRow8.row() 462 | g1sg1BodyRow8a.prop(properties, "opt_shapepairs_splitmerge_viewportvisualize_show_smoothregion") 463 | g1sg1BodyRow8a.enabled = properties.opt_gui_enabler_shapepairs_split_smoothdist 464 | # Split mesh preview 465 | g1sg1BodyRow9 = g1sg1Body.row() 466 | g1sg1BodyRow9.alignment = 'LEFT' 467 | g1sg1BodyRow9.label("Preview Split:") 468 | g1sg1BodyRow9.prop(properties, "opt_shapepairs_splitmerge_preview_split_left") 469 | g1sg1BodyRow9.prop(properties, "opt_shapepairs_splitmerge_preview_split_right") 470 | # Operators 471 | g1Body.operator("wm.shape_key_tools_split_active_pair", icon_value=UiIconsExtra["arrow_divide"].icon_id) 472 | g1Body.operator("wm.shape_key_tools_split_all_pairs", icon_value=UiIconsExtra["arrow_divide"].icon_id) 473 | g1Body.operator("wm.shape_key_tools_smartmerge_active", icon_value=UiIconsExtra["arrow_join"].icon_id) 474 | g1Body.operator("wm.shape_key_tools_smartmerge_all_pairs", icon_value=UiIconsExtra["arrow_join"].icon_id) 475 | 476 | ### Arbitary split/merge 477 | g2 = layout.box() 478 | g2Col = g2.column() 479 | g2Header = g2Col.row() 480 | g2Header.alignment = 'LEFT' 481 | g2Header.prop(properties, "opt_gui_subpanel_expander_arbitrary", icon=("TRIA_DOWN" if properties.opt_gui_subpanel_expander_arbitrary else "TRIA_RIGHT"), text="Arbitrary Split/Merge", emboss=False, expand=False) 482 | if (properties.opt_gui_subpanel_expander_arbitrary): 483 | g2Body = g2Col.column() 484 | # Operators 485 | splitByFilterCon = g2Body.row() 486 | splitByFilterCon.operator("wm.shape_key_tools_split_by_filter", icon_value=UiIconsExtra["arrow_branch"].icon_id) 487 | splitByFilterCon.enabled = properties.opt_global_enable_filterverts 488 | g2Body.operator("wm.shape_key_tools_combine_two", icon_value=UiIconsExtra["arrow_merge"].icon_id) 489 | 490 | ### Modifiers 491 | g3 = layout.box() 492 | g3Col = g3.column() 493 | g3Header = g3Col.row() 494 | g3Header.alignment = 'LEFT' 495 | g3Header.prop(properties, "opt_gui_subpanel_expander_modifiers", icon=("TRIA_DOWN" if properties.opt_gui_subpanel_expander_modifiers else "TRIA_RIGHT"), text="Shape Key Modifiers", emboss=False, expand=False) 496 | if (properties.opt_gui_subpanel_expander_modifiers): 497 | g3Body = g3Col.column() 498 | # Operators 499 | g3Body.operator("wm.shape_key_tools_apply_modifiers_to_shape_keys", icon="MODIFIER") 500 | 501 | ### Info 502 | g4 = layout.box() 503 | g4Col = g4.column() 504 | g4Header = g4Col.row() 505 | g4HeaderL = g4Header.row() 506 | g4HeaderR = g4Header.row() 507 | g4HeaderL.alignment = 'LEFT' 508 | g4HeaderR.alignment = 'RIGHT' 509 | infoHeaderText = "Info" 510 | if (obj): 511 | infoHeaderText = obj.name 512 | g4HeaderL.prop(properties, "opt_gui_subpanel_expander_info", icon=("TRIA_DOWN" if properties.opt_gui_subpanel_expander_info else "TRIA_RIGHT"), text=infoHeaderText, emboss=False, expand=False) 513 | g4HeaderR.label(text="", icon="QUESTION") 514 | if (properties.opt_gui_subpanel_expander_info): 515 | g4Body = g4Col.column() 516 | try: 517 | if (hasattr(obj.data.shape_keys, "key_blocks")): 518 | # Active shape key 519 | active = g4Body.box().column() 520 | activeRow = active.row() 521 | activeL = activeRow.row() 522 | activeR = activeRow.row() 523 | activeL.alignment = "LEFT" 524 | activeR.alignment = "RIGHT" 525 | activeL.label("Active") 526 | activeR.label(obj.data.shape_keys.key_blocks[obj.active_shape_key_index].name) 527 | if (obj.active_shape_key_index == 0): 528 | active.label("Incompatible with some ops", icon="ERROR") 529 | # Total shape keys 530 | total = g4Body.box().column() 531 | totalRow = total.row() 532 | totalL = totalRow.row() 533 | totalR = totalRow.row() 534 | totalL.alignment = "LEFT" 535 | totalR.alignment = "RIGHT" 536 | totalL.label("Total shape keys") 537 | totalR.label(str(len(obj.data.shape_keys.key_blocks))) 538 | # Merged L+R pair keys 539 | pairKeys = g4Body.box().column() 540 | pairKeysRow = pairKeys.row() 541 | pairKeysL = pairKeysRow.row() 542 | pairKeysR = pairKeysRow.row() 543 | pairKeysL.alignment = "LEFT" 544 | pairKeysR.alignment = "RIGHT" 545 | pairKeysL.label("L+R Pairs") 546 | pairCount = 0 547 | seen = {} 548 | for keyBlock in obj.data.shape_keys.key_blocks: 549 | if (not keyBlock.name in seen): 550 | seen[keyBlock.name] = True 551 | (splitLName, splitRName, usesPlusConvention) = common.FindShapeKeyPairSplitNames(keyBlock.name) 552 | if (usesPlusConvention): 553 | pairCount += 1 554 | pairKeysR.label(str(pairCount)) 555 | # Unmerged L+R pair keys 556 | unmergedPairKeys = g4Body.box().column() 557 | unmergedPairKeysRow = unmergedPairKeys.row() 558 | unmergedPairKeysL = unmergedPairKeysRow.row() 559 | unmergedPairKeysR = unmergedPairKeysRow.row() 560 | unmergedPairKeysL.alignment = "LEFT" 561 | unmergedPairKeysR.alignment = "RIGHT" 562 | unmergedPairKeysL.label("Unmerged L+R Pairs") 563 | unmergedPairCount = 0 564 | seen = {} 565 | for keyBlock in obj.data.shape_keys.key_blocks: 566 | if (not keyBlock.name in seen): 567 | seen[keyBlock.name] = True 568 | (firstName, expectedCompName, mergedName) = common.FindShapeKeyMergeNames(keyBlock.name) 569 | if (not expectedCompName in seen and expectedCompName in obj.data.shape_keys.key_blocks.keys()): 570 | seen[expectedCompName] = True 571 | unmergedPairCount += 1 572 | unmergedPairKeysR.label(str(unmergedPairCount)) 573 | else: 574 | g4Body.label("Object has no shape keys.") 575 | except Exception as e: 576 | g4Body.label(str(e), icon="CANCEL") 577 | raise 578 | 579 | 580 | 581 | def register(): 582 | global UiIconsExtra 583 | global RegisteredOps 584 | global AddonEnabled 585 | 586 | AddonEnabled = True 587 | 588 | # Custom icons 589 | UiIconsExtra = bpy.utils.previews.new() 590 | iconDir = os.path.join(os.path.dirname(__file__), "icons") 591 | for filename in os.listdir(iconDir): 592 | if (filename[-4:] == ".png"): 593 | UiIconsExtra.load(filename[:-4], os.path.join(iconDir, filename), "IMAGE") 594 | 595 | # Top level structure 596 | bpy.utils.register_module(__name__) 597 | bpy.types.Scene.shape_key_tools_props = PointerProperty(type=ShapeKeyTools_Properties) 598 | 599 | # Operators 600 | opsDir = os.path.join(os.path.dirname(__file__), "ops") 601 | for filename in os.listdir(opsDir): 602 | fullpath = os.path.join(opsDir, filename) 603 | if (os.path.isfile(fullpath)): # dont import the pycache folder 604 | script = imp.load_source("ops." + filename[:-3], fullpath) 605 | operatorClass = script.register() 606 | info = SimpleNamespace() 607 | info.Script = script 608 | info.OpClass = operatorClass 609 | RegisteredOps[operatorClass.bl_idname] = info 610 | 611 | def unregister(): 612 | global UiIconsExtra 613 | global RegisteredOps 614 | global AddonEnabled 615 | 616 | AddonEnabled = False 617 | 618 | bpy.utils.previews.remove(UiIconsExtra) 619 | 620 | del bpy.types.Scene.shape_key_tools_props 621 | bpy.utils.unregister_module(__name__) 622 | 623 | for blenderID in RegisteredOps: 624 | info = RegisteredOps[blenderID] 625 | info.Script.unregister() 626 | RegisteredOps = {} 627 | 628 | if __name__ == "__main__": 629 | register() 630 | -------------------------------------------------------------------------------- /shape_key_tools/common.py: -------------------------------------------------------------------------------- 1 | # //////////////////////////////////////////////////////////////////////////////////////////////////// 2 | # // 3 | # // Common Methods 4 | # // - Things that are used by multiple Operators/Panels/whatever 5 | # // 6 | # //////////////////////////////////////////////////////////////////////////////////////////////////// 7 | 8 | import sys, os, math, mathutils 9 | import bpy 10 | 11 | 12 | # 13 | #==================================================================================================== 14 | # Helpers 15 | #==================================================================================================== 16 | # 17 | 18 | ### Simple bezier interpolation for values in 0-1 19 | def InterpBezier(x): 20 | return (3.0 * x * x) - (2.0 * x * x * x) 21 | 22 | 23 | 24 | # 25 | #==================================================================================================== 26 | # Names 27 | #==================================================================================================== 28 | # 29 | 30 | ### Validates the provided shape key name as non-existent and modifies it with Blender's .001, .002, etc styling if it does exist 31 | def ValidateShapeKeyName(obj, name): 32 | if (not hasattr(obj.data.shape_keys, "key_blocks") or len(obj.data.shape_keys.key_blocks.keys()) == 0): # no shape keys 33 | return name, 0 34 | else: 35 | newName = name 36 | conflict = (newName in obj.data.shape_keys.key_blocks.keys()) 37 | numConflicts = 0 38 | while (conflict): 39 | numConflicts += 1 40 | if (numConflicts <= 999): 41 | newName = name + "." + "{:03d}".format(numConflicts) 42 | else: 43 | newName = name + "." + str(numConflicts) 44 | conflict = (newName in obj.data.shape_keys.key_blocks.keys()) 45 | return newName 46 | 47 | 48 | 49 | # 50 | #==================================================================================================== 51 | # Vertex Filtering 52 | #==================================================================================================== 53 | # 54 | 55 | ### Creates a vertex filtering kernel function per the provided parameters 56 | def CreateVertexFilterKernel(params): 57 | deltaDistanceMin = 0 58 | if ("DeltaDistanceMin" in params): 59 | deltaDistanceMin = params["DeltaDistanceMin"] 60 | 61 | deltaDistanceMax = sys.float_info.max 62 | if ("DeltaDistanceMax" in params): 63 | deltaDistanceMax = params["DeltaDistanceMax"] 64 | 65 | vertexGroupIndex = None 66 | if ("VertexGroupIndex" in params): 67 | vertexGroupIndex = int(params["VertexGroupIndex"], 10) # int() because blender requires a string identifier for EnumProperty value IDs (numbers cause silent errors) 68 | 69 | def filter(vertVGIndices, delta): 70 | return ( 71 | (delta.length >= deltaDistanceMin and delta.length <= deltaDistanceMax) 72 | and 73 | (vertexGroupIndex == None or vertexGroupIndex in vertVGIndices) 74 | ) 75 | 76 | return filter 77 | 78 | 79 | 80 | # 81 | #==================================================================================================== 82 | # Pair Split/Merge 83 | #==================================================================================================== 84 | # 85 | 86 | ### Given an existing shape key, determines the new names if this shape key was to be split into L and R halves 87 | # If validateWith = any object, the new names will be validated (and adjusted) for conflicts with existing shape keys 88 | # If validateWith = None, the ideal new names will be returned without modification 89 | def FindShapeKeyPairSplitNames(originalShapeKeyName, validateWith=None): 90 | newLeftName = None 91 | newRightName = None 92 | usesPairNameConvention = False 93 | if ('+' in originalShapeKeyName): 94 | nameCuts = originalShapeKeyName.split("+") 95 | if (nameCuts[0].lower()[-1] == "l" and nameCuts[1].lower()[-1] == "r"): 96 | newLeftName = nameCuts[0] 97 | newRightName = nameCuts[1] 98 | usesPairNameConvention = True 99 | elif (nameCuts[1].lower()[-1] == "l" and nameCuts[0].lower()[-1] == "r"): 100 | newLeftName = nameCuts[1] 101 | newRightName = nameCuts[0] 102 | usesPairNameConvention = True 103 | else: # shape key name has a + in it, but the string halves on either side of that + do not end in L and R 104 | newLeftName = originalShapeKeyName + "L" 105 | newRightName = originalShapeKeyName + "R" 106 | usesPairNameConvention = False 107 | else: 108 | newLeftName = originalShapeKeyName + "L" 109 | newRightName = originalShapeKeyName + "R" 110 | usesPairNameConvention = False 111 | 112 | if (validateWith): 113 | newLeftName = ValidateShapeKeyName(validateWith, newLeftName) 114 | newRightName = ValidateShapeKeyName(validateWith, newRightName) 115 | 116 | return (newLeftName, newRightName, usesPairNameConvention) 117 | 118 | 119 | ### Splits the active shape key on the specified object into separate left and right halves 120 | # Params: 121 | # - obj: The object who has the active shape key we are going to split 122 | # - optAxis: The world axis which determines which verts go into the "left" and "right" halves 123 | # - newLeftName: Name for the newly split-off left side shape key 124 | # - newRightName: Name for the newly split-off right side shape key 125 | # - (optional) deleteOriginal: If false, the original shape key will be kept instead of deleted 126 | # - (optional) smoothDistance: Distance in world space from the origin of the split axis to crossblend the split shape keys 127 | # - (optional) asyncProgressReporting: An object provided by __init__ for asynchronous operation (i.e. in a modal) 128 | def SplitPairActiveShapeKey(obj, optAxis, newLeftName, newRightName, smoothDistance=0, deleteOriginal=True, asyncProgressReporting=None): 129 | originalShapeKeyName = obj.active_shape_key.name 130 | originalShapeKeyIndex = obj.data.shape_keys.key_blocks.keys().index(originalShapeKeyName) 131 | 132 | # Basis shape key cannot be split (assume this is key 0) 133 | if (originalShapeKeyIndex == 0): 134 | raise Exception("You cannot split the basis shape key") 135 | 136 | # Create the two copies 137 | obj.shape_key_add(name=str(newLeftName), from_mix=True) 138 | newLeftShapeKeyIndex = len(obj.data.shape_keys.key_blocks) - 1 139 | obj.shape_key_add(name=str(newRightName), from_mix=True) 140 | newRightShapeKeyIndex = len(obj.data.shape_keys.key_blocks) - 1 141 | 142 | # Split axis factor 143 | axis = 0 144 | if (optAxis == "+X" or optAxis == "-X"): 145 | axis = 0 146 | elif (optAxis == "+Y" or optAxis == "-Y"): 147 | axis = 1 148 | elif (optAxis == "+Z" or optAxis == "-Z"): 149 | axis = 2 150 | axisFlip = 1 151 | if optAxis[0] == "-": 152 | axisFlip = -1 153 | 154 | # Async progress reporting 155 | reportAsyncProgress = False 156 | wm = None 157 | currentVert = 0 158 | totalVerts = 0 159 | if asyncProgressReporting: 160 | reportAsyncProgress = True 161 | wm = bpy.context.window_manager 162 | currentVert = asyncProgressReporting["CurrentVert"] 163 | totalVerts = asyncProgressReporting["TotalVerts"] 164 | 165 | basisShapeKeyVerts = obj.data.shape_keys.key_blocks[0].data 166 | originalShapeKeyVerts = obj.data.shape_keys.key_blocks[originalShapeKeyIndex].data 167 | leftShapeKeyVerts = obj.data.shape_keys.key_blocks[newLeftShapeKeyIndex].data 168 | rightShapeKeyVerts = obj.data.shape_keys.key_blocks[newRightShapeKeyIndex].data 169 | 170 | for vert in obj.data.vertices: 171 | if reportAsyncProgress: 172 | currentVert += 1 173 | if (currentVert % 100 == 0): # Only break for the UI thread every 100 verts. I'm not sure how much of a performance hit progress_update() incurs, but there's no need to call it faster than 60Hz. 174 | wm.progress_update(currentVert) 175 | 176 | basisVertPos = basisShapeKeyVerts[vert.index].co 177 | 178 | # The coordinate of the vert on the basis shape key determines whether it is a left (+aXis) or right (-aXis) vert 179 | axisSplitCoord = 0 180 | if (axis == 0): 181 | axisSplitCoord = basisVertPos.x 182 | elif (axis == 1): 183 | axisSplitCoord = basisVertPos.y 184 | elif (axis == 2): 185 | axisSplitCoord = basisVertPos.z 186 | axisSplitCoord *= axisFlip 187 | 188 | # if axisSplitCoord < 0: this vert is on the right side 189 | # if axisSplitCoord == 0: this vert is exactly on the middle of the split axis 190 | # if axisSplitCoord > 0: this vert is on the left side 191 | 192 | # Both the left and right shape keys are identical and start out with all deltas from both sides 193 | # So we are removing deltas from one side or the other instead of adding them in order to achieve the two split shape keys 194 | 195 | if (axisSplitCoord < 0): # Vert is on the right side 196 | if (axisSplitCoord < -smoothDistance or smoothDistance == 0): # Vert is outside of the smoothing radius or smoothing is disabled, so no crossfade 197 | leftShapeKeyVerts[vert.index].co = basisVertPos * 1 # Remove this (right side) delta from the left shape key 198 | else: # Vert is inside the smoothing radius, so factor the deltas for both the left and right shape keys to achieve the crossfade 199 | leftShapeKeyVerts[vert.index].co = basisVertPos * 1 # Remove this (right side) delta from the left shape key 200 | t = InterpBezier((smoothDistance - axisSplitCoord) / (2.0 * smoothDistance)) 201 | rightShapeKeyVerts[vert.index].co = basisVertPos.lerp(originalShapeKeyVerts[vert.index].co, t) 202 | leftShapeKeyVerts[vert.index].co = basisVertPos.lerp(originalShapeKeyVerts[vert.index].co, 1.0 - t) 203 | 204 | elif (axisSplitCoord >= 0): # Vert is on the left side (or center) 205 | if (axisSplitCoord > smoothDistance or smoothDistance == 0): # Vert is outside of the smoothing radius or smoothing is disabled, so no crossfade 206 | rightShapeKeyVerts[vert.index].co = basisVertPos * 1 # Remove this (left side) delta from the right shape key 207 | else: # Vert is inside the smoothing radius, so factor the deltas for both the left and right shape keys to achieve the crossfade 208 | t = InterpBezier((smoothDistance - axisSplitCoord) / (2.0 * smoothDistance)) 209 | leftShapeKeyVerts[vert.index].co = basisVertPos.lerp(originalShapeKeyVerts[vert.index].co, 1.0 - t) 210 | rightShapeKeyVerts[vert.index].co = basisVertPos.lerp(originalShapeKeyVerts[vert.index].co, t) 211 | 212 | # Move the two copies in the shape key list to sit after the original shape key 213 | while (newLeftShapeKeyIndex > originalShapeKeyIndex + 1): 214 | # Move left copy 215 | obj.active_shape_key_index = newLeftShapeKeyIndex 216 | bpy.ops.object.shape_key_move(type="UP") 217 | # Move right copy (will always be on the tail of the left copy) 218 | obj.active_shape_key_index = newRightShapeKeyIndex 219 | bpy.ops.object.shape_key_move(type="UP") 220 | newLeftShapeKeyIndex -= 1 221 | newRightShapeKeyIndex -= 1 222 | 223 | # Delete original shape key 224 | if (deleteOriginal): 225 | obj.active_shape_key_index = originalShapeKeyIndex 226 | bpy.ops.object.shape_key_remove() 227 | 228 | # Select the new L shape key 229 | obj.active_shape_key_index = obj.data.shape_keys.key_blocks.keys().index(newLeftName) 230 | 231 | # Update async progress reporting for delta verts processed 232 | if reportAsyncProgress: 233 | asyncProgressReporting["CurrentVert"] = currentVert 234 | 235 | 236 | ### Given an existing shape key, determines the expected name of the complementary shape key (the L for the R, or the R for the L) and the name of the final shape key if they two were merged 237 | # If validateWith = any object, the to-be-merged name will be validated (and adjusted) for conflicts with existing shape keys 238 | # If validateWith = None, the ideal to-be-merged name will be returned without modification 239 | def FindShapeKeyMergeNames(shapeKeyName, validateWith=None): 240 | expectedCompShapeKeyName = None 241 | mergedShapeKeyName = None 242 | if shapeKeyName[-1] == "L": 243 | expectedCompShapeKeyName = shapeKeyName[:-1] + "R" 244 | mergedShapeKeyName = shapeKeyName + "+" + expectedCompShapeKeyName 245 | if shapeKeyName[-1] == "R": 246 | expectedCompShapeKeyName = shapeKeyName[:-1] + "L" 247 | mergedShapeKeyName = expectedCompShapeKeyName + "+" + shapeKeyName 248 | 249 | if (validateWith): 250 | mergedShapeKeyName = ValidateShapeKeyName(validateWith, mergedShapeKeyName) 251 | 252 | return (shapeKeyName, expectedCompShapeKeyName, mergedShapeKeyName) 253 | 254 | 255 | ### Merges the specified shape key pair (two shape keys with names like "MyShapeKeyL" and "MyShapeKeyR") on the specified object into a single shape key 256 | # Params: 257 | # - obj: The object who has the two specified shape keys to be merged 258 | # - optAxis: The world axis which determines which verts belong to the "left" and "right" halves of the combined shape key 259 | # - shapeKeyLeftName: Name of the "left" side shape key to be merged 260 | # - shapeKeyRightName: Name of the "right" side shape key to be merged 261 | # - mergedShapeKeyName: Name of the soon-to-be merged shape key 262 | # - mode: Name of the mode to use for merging the left and right deltas 263 | # - (optional) deleteInputShapeKeys: Defaults to delete the left and right shape keys creating the new merged key 264 | # - (optional) asyncProgressReporting: An object provided by __init__ for asynchronous operation (i.e. in a modal) 265 | def MergeShapeKeyPair(obj, optAxis, shapeKeyLeftName, shapeKeyRightName, mergedShapeKeyName, mode, deleteInputShapeKeys=True, asyncProgressReporting=None): 266 | # Find the indices of the left and right shape keys 267 | leftShapeKeyIndex = obj.data.shape_keys.key_blocks.keys().index(shapeKeyLeftName) 268 | rightShapeKeyIndex = obj.data.shape_keys.key_blocks.keys().index(shapeKeyRightName) 269 | 270 | # Neither shape key can be the basis key (assume this is key 0) 271 | if (leftShapeKeyIndex == 0 or rightShapeKeyIndex == 0): 272 | raise Exception("The basis shape key cannot be merged.") 273 | 274 | key0 = obj.data.shape_keys.key_blocks[0] 275 | leftShapeKey = obj.data.shape_keys.key_blocks[leftShapeKeyIndex] 276 | rightShapeKey = obj.data.shape_keys.key_blocks[rightShapeKeyIndex] 277 | 278 | # If the two shape keys are relative to a key other than key 0 (should be the basis shape, assuming the user isn't being weird), change them now and restore the relative key at the end 279 | leftOldBasisKey = leftShapeKey.relative_key 280 | rightOldBasisKey = rightShapeKey.relative_key 281 | if (leftOldBasisKey != key0): 282 | leftShapeKey.relative_key = key0 283 | if (rightOldBasisKey != key0): 284 | rightShapeKey.relative_key = key0 285 | 286 | # Create a new shape key from the basis 287 | obj.active_shape_key_index = 0 288 | obj.shape_key_add(name=str(mergedShapeKeyName), from_mix=False) 289 | newShapeKeyIndex = len(obj.data.shape_keys.key_blocks) - 1 290 | newShapeKey = obj.data.shape_keys.key_blocks[newShapeKeyIndex] 291 | 292 | # Cherry pick which verts to bring into the new shape key from the -/+ sides of the left and right shape keys pair 293 | axis = 0 294 | if (optAxis == "+X" or optAxis == "-X"): 295 | axis = 0 296 | elif (optAxis == "+Y" or optAxis == "-Y"): 297 | axis = 1 298 | elif (optAxis == "+Z" or optAxis == "-Z"): 299 | axis = 2 300 | axisFlip = 1 301 | if optAxis[0] == "-": 302 | axisFlip = -1 303 | 304 | # Async progress reporting 305 | reportAsyncProgress = False 306 | wm = None 307 | currentVert = 0 308 | totalVerts = 0 309 | if asyncProgressReporting: 310 | reportAsyncProgress = True 311 | wm = bpy.context.window_manager 312 | currentVert = asyncProgressReporting["CurrentVert"] 313 | totalVerts = asyncProgressReporting["TotalVerts"] 314 | 315 | basisShapeKeyVerts = key0.data 316 | mergedShapeKeyVerts = newShapeKey.data 317 | leftShapeKeyVerts = leftShapeKey.data 318 | rightShapeKeyVerts = rightShapeKey.data 319 | 320 | for vert in obj.data.vertices: 321 | if reportAsyncProgress: 322 | currentVert += 1 323 | if (currentVert % 100 == 0): 324 | wm.progress_update(currentVert) 325 | 326 | baseVertPos = basisShapeKeyVerts[vert.index].co 327 | 328 | axisSplitCoord = 0 329 | if (axis == 0): 330 | axisSplitCoord = baseVertPos.x 331 | elif (axis == 1): 332 | axisSplitCoord = baseVertPos.y 333 | elif (axis == 2): 334 | axisSplitCoord = baseVertPos.z 335 | axisSplitCoord *= axisFlip 336 | 337 | if (mode == "overwrite"): 338 | # If the original vert is -aXis (right side), then we pick the flexed vert from the Right shape key 339 | if (axisSplitCoord < 0): 340 | mergedShapeKeyVerts[vert.index].co = rightShapeKeyVerts[vert.index].co * 1 341 | # If the original vert is +aXis (left side), then we pick the flexed vert from the Left shape key 342 | if (axisSplitCoord >= 0): 343 | mergedShapeKeyVerts[vert.index].co = leftShapeKeyVerts[vert.index].co * 1 344 | 345 | elif (mode == "additive"): 346 | # Add the deltas of both the left and right halves together 347 | leftDelta = leftShapeKeyVerts[vert.index].co - baseVertPos 348 | rightDelta = rightShapeKeyVerts[vert.index].co - baseVertPos 349 | mergedShapeKeyVerts[vert.index].co = baseVertPos + leftDelta + rightDelta 350 | 351 | # Restore relative_key for the two input shape keys if necessary 352 | if (leftOldBasisKey != key0): 353 | leftShapeKey.relative_key = leftOldBasisKey 354 | if (rightOldBasisKey != key0): 355 | rightShapeKey.relative_key = rightOldBasisKey 356 | 357 | # Set the relative_key for the new merged shape key to whatever the relative key was for the left shape key 358 | newShapeKey.relative_key = leftOldBasisKey 359 | 360 | # Move the new merged shape key in the shape key list to sit after the firstmost shape key of the pair in the shape key list 361 | originalShapeKeyIndex = min(leftShapeKeyIndex, rightShapeKeyIndex) 362 | while (newShapeKeyIndex > originalShapeKeyIndex + 1): 363 | # Move left copy 364 | obj.active_shape_key_index = newShapeKeyIndex 365 | bpy.ops.object.shape_key_move(type="UP") 366 | newShapeKeyIndex -= 1 367 | 368 | # Delete the left and right shape keys 369 | if (deleteInputShapeKeys): 370 | obj.active_shape_key_index = obj.data.shape_keys.key_blocks.keys().index(shapeKeyLeftName) 371 | bpy.ops.object.shape_key_remove() 372 | obj.active_shape_key_index = obj.data.shape_keys.key_blocks.keys().index(shapeKeyRightName) 373 | bpy.ops.object.shape_key_remove() 374 | 375 | # Reselect merged shape key 376 | obj.active_shape_key_index = obj.data.shape_keys.key_blocks.keys().index(mergedShapeKeyName) 377 | 378 | # Update async progress reporting for delta verts processed 379 | if reportAsyncProgress: 380 | asyncProgressReporting["CurrentVert"] = currentVert 381 | 382 | 383 | 384 | # 385 | #==================================================================================================== 386 | # Arbitrary Split/Merge 387 | #==================================================================================================== 388 | # 389 | 390 | ### Merges the two specified shape keys on the specified object into a single shape key, using various blend modes 391 | # Params: 392 | # - obj: The object who has the two specified shape keys to be merged 393 | # - shapeKey1Name: Name of the shape key to use as the lower layer in blending 394 | # - shapeKey2Name: Name of the shape key to use as the upper layer in blending 395 | # - destination: Either 1 (output to shape key 1), 2 (output to shape key 2), or a string (name of a new shape key to create) 396 | # - blendMode: Name of the blend mode 397 | # - (optional) blendModeParams: Dictionary of parameters specific to the chosen blend mode 398 | # - (optional) vertexFilterParams: Dictionary of parameters for vertex filtering. If None, vertex filtering is disabled. 399 | # - (optional) delete1OnFinish: If true, shape key 1 will be deleted after the merge is complete 400 | # - (optional) delete2OnFinish: If true, shape key 2 will be deleted after the merge is complete 401 | # - (optional) asyncProgressReporting: An object provided by __init__ for asynchronous operation (i.e. in a modal) 402 | def MergeAndBlendShapeKeys(obj, shapeKey1Name, shapeKey2Name, destination, blendMode, blendModeParams=None, vertexFilterParams=None, delete1OnFinish=False, delete2OnFinish=False, asyncProgressReporting=None): 403 | # New shape key from the basis (if we are outputting to a new shape key) 404 | newShapeKeyIndex = None 405 | if (isinstance(destination, str)): 406 | obj.active_shape_key_index = 0 407 | obj.shape_key_add(name=str(destination), from_mix=False) 408 | newShapeKeyIndex = len(obj.data.shape_keys.key_blocks) - 1 409 | 410 | # Find the indices of the source shape keys 411 | lowerShapeKeyIndex = obj.data.shape_keys.key_blocks.keys().index(shapeKey1Name) 412 | upperShapeKeyIndex = obj.data.shape_keys.key_blocks.keys().index(shapeKey2Name) 413 | 414 | key0 = obj.data.shape_keys.key_blocks[0] 415 | lowerShapeKey = obj.data.shape_keys.key_blocks[lowerShapeKeyIndex] 416 | upperShapeKey = obj.data.shape_keys.key_blocks[upperShapeKeyIndex] 417 | 418 | # If the two shape keys are relative to a key other than key 0 (should be the basis shape, assuming the user isn't being weird), change them now and restore the relative key at the end 419 | lowerOldBasisKey = lowerShapeKey.relative_key 420 | upperOldBasisKey = upperShapeKey.relative_key 421 | if (lowerOldBasisKey != key0): 422 | lowerShapeKey.relative_key = key0 423 | if (upperOldBasisKey != key0): 424 | upperShapeKey.relative_key = key0 425 | 426 | basisShapeKeyVerts = key0.data 427 | lowerShapeKeyVerts = lowerShapeKey.data 428 | upperShapeKeyVerts = upperShapeKey.data 429 | newShapeKeyVerts = None 430 | if (newShapeKeyIndex != None): 431 | newShapeKeyVerts = obj.data.shape_keys.key_blocks[newShapeKeyIndex].data 432 | 433 | destinationShapeKeyName = None 434 | destinationShapeKeyVerts = None 435 | if (destination == 1): 436 | destinationShapeKeyName = shapeKey1Name 437 | destinationShapeKeyVerts = lowerShapeKeyVerts 438 | elif (destination == 2): 439 | destinationShapeKeyName = shapeKey2Name 440 | destinationShapeKeyVerts = upperShapeKeyVerts 441 | else: 442 | destinationShapeKeyName = destination 443 | destinationShapeKeyVerts = newShapeKeyVerts 444 | 445 | 446 | ### Blend-mode-specific params 447 | blendModeLerp_Factor = None 448 | if (blendMode == "lerp"): 449 | blendModeLerp_Factor = min(max(0, blendModeParams["Factor"]), 1) 450 | 451 | 452 | ### Vertex filter kernel 453 | vertexFilterKernel = None 454 | doVertexFiltering = False 455 | if (vertexFilterParams != None): 456 | doVertexFiltering = True 457 | vertexFilterKernel = CreateVertexFilterKernel(vertexFilterParams) 458 | 459 | 460 | # Async progress reporting 461 | reportAsyncProgress = False 462 | wm = None 463 | currentVert = 0 464 | totalVerts = 0 465 | if asyncProgressReporting: 466 | reportAsyncProgress = True 467 | wm = bpy.context.window_manager 468 | currentVert = asyncProgressReporting["CurrentVert"] 469 | totalVerts = asyncProgressReporting["TotalVerts"] 470 | 471 | ### Iterate all the verts and combine the deltas as per the blend mode 472 | for vert in obj.data.vertices: 473 | if reportAsyncProgress: 474 | currentVert += 1 475 | if (currentVert % 100 == 0): 476 | wm.progress_update(currentVert) 477 | 478 | # Unfortunately, bpy does not expose relative position of each vert, so we have to calculate the deltas ourself 479 | basePos = basisShapeKeyVerts[vert.index].co 480 | lowerDelta = lowerShapeKeyVerts[vert.index].co - basePos 481 | upperDelta = upperShapeKeyVerts[vert.index].co - basePos 482 | 483 | # Filter the upper vert if vertex filtering is enabled 484 | vertPassesFilter = True # RED verts are True, BLACK verts are False. 485 | if (doVertexFiltering): 486 | vgIndices = [vg.group for vg in vert.groups] 487 | vertPassesFilter = vertexFilterKernel(vgIndices, upperDelta) 488 | 489 | ### Blend the upper shape key's delta with the lower shape key's delta 490 | if (vertPassesFilter): # We only incorporate RED verts into combined shape key 491 | newDelta = None 492 | 493 | # Additive 494 | if (blendMode == "add"): 495 | newDelta = lowerDelta + upperDelta 496 | 497 | # Subtractive 498 | elif (blendMode == "subtract"): 499 | newDelta = lowerDelta - upperDelta 500 | 501 | # Multiply 502 | elif (blendMode == "multiply"): 503 | newDelta = lowerDelta * upperDelta 504 | 505 | # Divide 506 | elif (blendMode == "divide"): 507 | newDelta = lowerDelta / upperDelta 508 | 509 | # Overwrite 510 | elif (blendMode == "over"): 511 | newDelta = upperDelta 512 | 513 | # Lerp 514 | elif (blendMode == "lerp"): 515 | newDelta = lowerDelta.lerp(upperDelta, blendModeLerp_Factor) 516 | 517 | # Update the destination shape key 518 | destinationShapeKeyVerts[vert.index].co = basePos + newDelta 519 | 520 | # If outputting to a new shape key, move the new merged shape key in the shape key list to sit after the upper shape key 521 | if (newShapeKeyIndex != None): 522 | while (newShapeKeyIndex > upperShapeKeyIndex + 1): 523 | obj.active_shape_key_index = newShapeKeyIndex 524 | bpy.ops.object.shape_key_move(type="UP") 525 | newShapeKeyIndex -= 1 526 | 527 | # Restore relative_key for the two input shape keys if necessary 528 | if (lowerOldBasisKey != key0): 529 | lowerShapeKey.relative_key = lowerOldBasisKey 530 | if (upperOldBasisKey != key0): 531 | upperShapeKey.relative_key = upperOldBasisKey 532 | 533 | # Delete the source shape keys if desired 534 | if (delete1OnFinish): 535 | obj.active_shape_key_index = obj.data.shape_keys.key_blocks.keys().index(shapeKey1Name) 536 | bpy.ops.object.shape_key_remove() 537 | if (delete2OnFinish): 538 | obj.active_shape_key_index = obj.data.shape_keys.key_blocks.keys().index(shapeKey2Name) 539 | bpy.ops.object.shape_key_remove() 540 | 541 | # Make the destination shape key active 542 | obj.active_shape_key_index = obj.data.shape_keys.key_blocks.keys().index(destinationShapeKeyName) 543 | 544 | # Update async progress reporting for delta verts processed 545 | if reportAsyncProgress: 546 | asyncProgressReporting["CurrentVert"] = currentVert 547 | 548 | 549 | ### Splits off a new shape key from the active shape key, using the Vertex Filter to determine which deltas go to which shape key 550 | # Params: 551 | # - obj: The object who has the two specified shape keys to be merged 552 | # - newShapeKeyName: Name of to-be-created new shape key 553 | # - mode: Name of the split mode to use 554 | # - vertexFilterParams: Dictionary of parameters for vertex filtering 555 | # - (optional) asyncProgressReporting: An object provided by __init__ for asynchronous operation (i.e. in a modal) 556 | def SplitFilterActiveShapeKey(obj, newShapeKeyName, mode, vertexFilterParams, asyncProgressReporting=None): 557 | if (vertexFilterParams == None): 558 | raise Exception("Vertex filter parameters must be specified.") 559 | 560 | sourceShapeKeyIndex = obj.data.shape_keys.key_blocks.keys().index(obj.active_shape_key.name) 561 | 562 | # New shape key from the basis 563 | obj.active_shape_key_index = 0 564 | obj.shape_key_add(name=str(newShapeKeyName), from_mix=False) 565 | newShapeKeyIndex = len(obj.data.shape_keys.key_blocks) - 1 566 | 567 | basisShapeKeyVerts = obj.data.shape_keys.key_blocks[0].data 568 | sourceShapeKeyVerts = obj.data.shape_keys.key_blocks[sourceShapeKeyIndex].data 569 | newShapeKeyVerts = obj.data.shape_keys.key_blocks[newShapeKeyIndex].data 570 | 571 | # Vertex filter kernel 572 | vertexFilterKernel = CreateVertexFilterKernel(vertexFilterParams) 573 | 574 | # Async progress reporting 575 | reportAsyncProgress = False 576 | wm = None 577 | currentVert = 0 578 | totalVerts = 0 579 | if asyncProgressReporting: 580 | reportAsyncProgress = True 581 | wm = bpy.context.window_manager 582 | currentVert = asyncProgressReporting["CurrentVert"] 583 | totalVerts = asyncProgressReporting["TotalVerts"] 584 | 585 | ### Update the verts of all the involved shape keys 586 | for vert in obj.data.vertices: 587 | if reportAsyncProgress: 588 | currentVert += 1 589 | if (currentVert % 100 == 0): 590 | wm.progress_update(currentVert) 591 | 592 | # Unfortunately, bpy does not expose relative position of each vert, so we have to calculate the deltas ourself 593 | basePos = basisShapeKeyVerts[vert.index].co 594 | sourcePos = sourceShapeKeyVerts[vert.index].co 595 | sourceDelta = sourcePos - basePos 596 | 597 | # Filter the vertex 598 | vgIndices = [vg.group for vg in vert.groups] 599 | vertPassesFilter = vertexFilterKernel(vgIndices, sourceDelta) # RED verts are True, BLACK verts are False. 600 | 601 | ### Change shape key verts depending on the operation mode 602 | # RED deltas make it into the new shape key. BLACK deltas do not (those verts revert to their basis pos defined in the basis shape key). 603 | if (vertPassesFilter): 604 | if (mode == "copy"): 605 | # Copy delta to new shape key and leave the original shape key unchanged 606 | newShapeKeyVerts[vert.index].co = sourcePos * 1 607 | 608 | elif (mode == "move"): 609 | # Copy delta to new shape key and neutralize the delta in the original shape key 610 | newShapeKeyVerts[vert.index].co = sourcePos * 1 611 | sourceShapeKeyVerts[vert.index].co = basePos * 1 612 | 613 | # Make the newly created shape key active 614 | obj.active_shape_key_index = newShapeKeyIndex 615 | # And move it to sit after original shape key 616 | while (newShapeKeyIndex > sourceShapeKeyIndex + 1): 617 | # Move left copy 618 | obj.active_shape_key_index = newShapeKeyIndex 619 | bpy.ops.object.shape_key_move(type="UP") 620 | newShapeKeyIndex -= 1 621 | 622 | # Update async progress reporting for delta verts processed 623 | if reportAsyncProgress: 624 | asyncProgressReporting["CurrentVert"] = currentVert 625 | 626 | -------------------------------------------------------------------------------- /shape_key_tools/icons/arrow_branch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiberiumFusion/BlenderShapeKeyTools/63a86145b5eb22e41a128d65ecfc2cb8f38b73af/shape_key_tools/icons/arrow_branch.png -------------------------------------------------------------------------------- /shape_key_tools/icons/arrow_divide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiberiumFusion/BlenderShapeKeyTools/63a86145b5eb22e41a128d65ecfc2cb8f38b73af/shape_key_tools/icons/arrow_divide.png -------------------------------------------------------------------------------- /shape_key_tools/icons/arrow_join.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiberiumFusion/BlenderShapeKeyTools/63a86145b5eb22e41a128d65ecfc2cb8f38b73af/shape_key_tools/icons/arrow_join.png -------------------------------------------------------------------------------- /shape_key_tools/icons/arrow_merge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiberiumFusion/BlenderShapeKeyTools/63a86145b5eb22e41a128d65ecfc2cb8f38b73af/shape_key_tools/icons/arrow_merge.png -------------------------------------------------------------------------------- /shape_key_tools/icons/line_diagonal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TiberiumFusion/BlenderShapeKeyTools/63a86145b5eb22e41a128d65ecfc2cb8f38b73af/shape_key_tools/icons/line_diagonal.png -------------------------------------------------------------------------------- /shape_key_tools/ops/apply_modifiers_to_shape_keys.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | import bpy 4 | from bpy.props import * 5 | 6 | from shape_key_tools import common 7 | 8 | 9 | class ShapeKeyTools_ApplyModifiersToShapeKeys_OptListItem(bpy.types.PropertyGroup): 10 | type = StringProperty() 11 | is_compatible = BoolProperty() 12 | do_apply = BoolProperty() 13 | 14 | is_visible_in_viewport = BoolProperty() 15 | def is_visible_in_viewport_readonly_get(self): 16 | return self.is_visible_in_viewport 17 | def is_visible_in_viewport_readonly_set(self, value): 18 | pass 19 | is_visible_in_viewport_readonly = BoolProperty( 20 | get = is_visible_in_viewport_readonly_get, 21 | set = is_visible_in_viewport_readonly_set, 22 | ) 23 | 24 | 25 | ModifierTypeInfo = { 26 | # modifier type name: (icon name, is compatible with this operation) 27 | "DATA_TRANSFER": ("MOD_DATA_TRANSFER", True), 28 | "MESH_CACHE": ("MOD_MESHDEFORM", False), 29 | "MESH_SEQUENCE_CACHE": ("MOD_MESHDEFORM", False), 30 | "NORMAL_EDIT": ("MOD_NORMALEDIT", False), 31 | "UV_PROJECT": ("MOD_UVPROJECT", True), 32 | "UV_WARP": ("MOD_UVPROJECT", True), 33 | "VERTEX_WEIGHT_EDIT": ("MOD_VERTEX_WEIGHT", True), 34 | "VERTEX_WEIGHT_MIX": ("MOD_VERTEX_WEIGHT", True), 35 | "VERTEX_WEIGHT_PROXIMITY": ("MOD_VERTEX_WEIGHT", True), 36 | 37 | "ARRAY": ("MOD_ARRAY", False), 38 | "BEVEL": ("MOD_BEVEL", False), 39 | "BOOLEAN": ("MOD_BOOLEAN", False), 40 | "BUILD": ("MOD_BUILD", False), 41 | "DECIMATE": ("MOD_DECIM", False), 42 | "EDGE_SPLIT": ("MOD_EDGESPLIT", False), 43 | "MASK": ("MOD_MASK", False), 44 | "MIRROR": ("MOD_MIRROR", False), 45 | "MULTIRES": ("MOD_MULTIRES", False), 46 | "REMESH": ("MOD_REMESH", False), 47 | "SCREW": ("MOD_SCREW", False), 48 | "SKIN": ("MOD_SKIN", False), 49 | "SOLIDIFY": ("MOD_SOLIDIFY", False), 50 | "SUBSURF": ("MOD_SUBSURF", False), 51 | "TRIANGULATE": ("MOD_TRIANGULATE", False), 52 | "WIREFRAME": ("MOD_WIREFRAME", False), 53 | 54 | "ARMATURE": ("MOD_ARMATURE", True), 55 | "CAST": ("MOD_CAST", True), 56 | "CORRECTIVE_SMOOTH": ("MOD_SMOOTH", True), 57 | "CURVE": ("MOD_CURVE", True), 58 | "DISPLACE": ("MOD_DISPLACE", True), 59 | "HOOK": ("HOOK", True), 60 | "LAPLACIANSMOOTH": ("MOD_SMOOTH", True), 61 | "LAPLACIANDEFORM": ("MOD_MESHDEFORM", True), 62 | "LATTICE": ("MOD_LATTICE", True), 63 | "MESH_DEFORM": ("MOD_MESHDEFORM", True), 64 | "SHRINKWRAP": ("MOD_SHRINKWRAP", True), 65 | "SIMPLE_DEFORM": ("MOD_SIMPLEDEFORM", True), 66 | "SMOOTH": ("MOD_SMOOTH", True), 67 | "SURFACE_DEFORM": ("MOD_MESHDEFORM", True), 68 | "WARP": ("MOD_WARP", True), 69 | "WAVE": ("MOD_WAVE", True), 70 | 71 | "CLOTH": ("MOD_CLOTH", False), 72 | "COLLISION": ("MOD_PHYSICS", False), 73 | "DYNAMIC_PAINT": ("MOD_DYNAMICPAINT", False), 74 | "EXPLODE": ("MOD_EXPLODE", False), 75 | "FLUID_SIMULATION": ("MOD_FLUIDSIM", False), 76 | "OCEAN": ("MOD_OCEAN", False), 77 | "PARTICLE_INSTANCE": ("MOD_PARTICLES", False), 78 | "PARTICLE_SYSTEM": ("MOD_PARTICLES", False), 79 | "SMOKE": ("MOD_SMOKE", False), 80 | "SOFT_BODY": ("MOD_SOFT", False), 81 | } 82 | 83 | 84 | class WM_OT_ShapeKeyTools_ApplyModifiersToShapeKeys(bpy.types.Operator): 85 | bl_idname = "wm.shape_key_tools_apply_modifiers_to_shape_keys" 86 | bl_label = "Apply Modifiers To Shape Keys" 87 | bl_description = "Applies compatible modifiers to the active mesh's base vertices and shape keys. Modifiers which change vertex count are incompatible. This operation does *not* use the Vertex Filter." 88 | bl_options = {"UNDO"} 89 | 90 | 91 | opt_modifiers = CollectionProperty( 92 | type = ShapeKeyTools_ApplyModifiersToShapeKeys_OptListItem, 93 | name = "Available Modifiers", 94 | description = "Choose which modifiers to apply to the base mesh and its shape keys." 95 | ) 96 | 97 | 98 | # report() doesnt print to console when running inside modal() for some weird reason 99 | # So we have to do that manually 100 | def preport(self, message, type="INFO"): 101 | print(message) 102 | self.report({type}, message) 103 | 104 | 105 | def draw(self, context): 106 | scene = context.scene 107 | properties = scene.shape_key_tools_props 108 | 109 | layout = self.layout 110 | topBody = layout.column() 111 | 112 | ### Usage info 113 | topBody.label("Apply modifiers (in stack order) to the base mesh and all of its shape keys.") 114 | topBody.label("This operation may take a very long time, especially for detailed meshes with many shape keys.") 115 | for optListItem in self.opt_modifiers: 116 | if (optListItem.is_compatible == False): 117 | topBody.box().label("Modifiers that change vertex count cannot be applied.", icon="ERROR") 118 | break 119 | 120 | ### List of modifiers 121 | gModifiers = topBody.box().column() 122 | # Modifier rows 123 | for optListItem in self.opt_modifiers: 124 | modifierTypeInfo = ModifierTypeInfo[optListItem.type] 125 | icon = modifierTypeInfo[0] 126 | cols = gModifiers.box().column_flow(columns=2, align=False) 127 | row1 = cols.row() 128 | row1.alignment = "LEFT" 129 | row1.prop(optListItem, "is_visible_in_viewport_readonly", icon="RESTRICT_VIEW_OFF", text="", emboss=True) 130 | row1.label(optListItem.name, icon=icon) 131 | row2 = cols.row() 132 | applyWrapper = row2.row() 133 | applyWrapper.alignment = "LEFT" 134 | if (optListItem.is_compatible == False): 135 | applyWrapper.enabled = False 136 | applyWrapper.prop(optListItem, "do_apply", text="Incompatible", icon="ERROR", emboss=False) 137 | else: 138 | applyWrapper.prop(optListItem, "do_apply", text="Apply Modifier", emboss=True) 139 | 140 | 141 | def validate(self, context): 142 | # This op requires an active object 143 | if (context.object == None or hasattr(context, "object") == False): 144 | return (False, "No object is selected.") 145 | 146 | obj = context.object 147 | 148 | # Object must be a mesh 149 | if (obj.type != "MESH"): 150 | return (False, "The active object ('" + obj.name + "', type: " + obj.type + ") is not a mesh.") 151 | 152 | # Object must have enough shape keys 153 | if (not hasattr(obj.data.shape_keys, "key_blocks") or len(obj.data.shape_keys.key_blocks.keys()) <= 1): 154 | return (False, "The active object must have at least 1 shape key (excluding the basis shape key).") 155 | 156 | return (True, None) 157 | 158 | def validateUser(self, context): 159 | (isValid, invalidReason) = self.validate(context) 160 | if (isValid): 161 | return True 162 | else: 163 | if self: 164 | self.report({'ERROR'}, invalidReason) 165 | return False 166 | 167 | @classmethod 168 | def poll(cls, context): 169 | (isValid, invalidReason) = cls.validate(None, context) 170 | return isValid 171 | 172 | 173 | ### Op helpers 174 | def deselectAll(self): 175 | for obj in bpy.data.objects: 176 | obj.select = False 177 | 178 | def activeSelect(self, context, obj): 179 | obj.select = True 180 | context.scene.objects.active = obj 181 | 182 | def singleSelect(self, context, obj): 183 | self.deselectAll() 184 | self.activeSelect(context, obj) 185 | 186 | 187 | ### Persistent op data 188 | _Timer = None 189 | 190 | _Obj = None 191 | _ModifierApplyOrder = [] 192 | _InvalidModifiers = {} 193 | _ShapeKeyDependencies = {} 194 | _ShapeKeyObj = None 195 | _CurWorkspaceObj = None 196 | _WorkStage = -1 197 | _WorkSubstage = -1 198 | _CurShapeKeyIndex = 0 199 | _TotalShapeKeys = 0 200 | _AnyWarnings = False 201 | 202 | 203 | def execute(self, context): 204 | scene = context.scene 205 | properties = scene.shape_key_tools_props 206 | 207 | if (self.validateUser(context) == False): 208 | return {'FINISHED'} 209 | 210 | obj = context.object 211 | 212 | ### Do as much here as possible before we get into the modal work stages 213 | # Many bpy.ops require a full UI update cycle when run in modal() but not when run in execute(). Both execute and modal are synchronous, so I don't see why this is the case... Blender is inconsistent and weird. What else is new? 214 | # Ultimately, it means we have to juggle and spread out the work over an excessive amount of modal events 215 | 216 | # The chosen modifiers will be applied in stack order 217 | chosenModifiers = [] 218 | for optListItem in self.opt_modifiers: 219 | if (optListItem.do_apply): 220 | chosenModifiers.append(optListItem.name) 221 | self._ModifierApplyOrder = [] 222 | for modifier in obj.modifiers: 223 | if (modifier.name in chosenModifiers): 224 | self._ModifierApplyOrder.append(modifier.name) 225 | 226 | if (len(self._ModifierApplyOrder) > 0): 227 | self._ModalWorkPacing = 0 228 | self._WorkStage = 0 229 | self._WorkSubstage = 0 230 | self._Obj = obj 231 | self._TotalShapeKeys = len(obj.data.shape_keys.key_blocks.keys()) # includes the basis shape key! 232 | self._CurShapeKeyIndex = 0 233 | self._InvalidModifiers = {} 234 | self._AnyWarnings = False 235 | 236 | # Flatten the shape key dependency tree by making all shape keys relative to the first shape key (which *should* be the basis, but Blender does not enforce this... nothing we can do, sadly) 237 | self._ShapeKeyDependencies = {} 238 | for keyBlock in obj.data.shape_keys.key_blocks: 239 | self._ShapeKeyDependencies[keyBlock.name] = keyBlock.relative_key.name # keep track of the dependencies so we can restore them later 240 | keyIndex = obj.data.shape_keys.key_blocks.keys().index(keyBlock.name) 241 | if (keyIndex > 0): 242 | keyBlock.relative_key = obj.data.shape_keys.key_blocks[0] # "it just works" because setting relative_key makes blender recalculate the shape key deltas to be relative to the new local basis shape key 243 | 244 | # Duplicate the active object so we can separate its shape keys from its base mesh and work on them independently 245 | self.singleSelect(context, obj) 246 | bpy.ops.object.duplicate() 247 | self._ShapeKeyObj = context.scene.objects.active 248 | self._ShapeKeyObj.name = "_DELETE_ME__ " + obj.name + "__SKALL" # bold name in case the op fails and this doesnt get deleted and the user sees it 249 | 250 | # Set the active object's active shape key to what we need for the next modal event 251 | obj.active_shape_key_index = self._TotalShapeKeys - 1 252 | 253 | # Preemptively report what will happen in the next modal stage so we can save a modal event right off the bat 254 | self.report({'INFO'}, "Applying modifiers to base mesh") 255 | 256 | # Begin the next stage of work in the modal events 257 | context.window_manager.modal_handler_add(self) 258 | self._Timer = context.window_manager.event_timer_add(0.1, context.window) 259 | context.window_manager.progress_begin(0, self._TotalShapeKeys + 0.1) 260 | context.window_manager.progress_update(0.1) 261 | 262 | self.report({'INFO'}, "Preparing to apply " + str(len(self._ModifierApplyOrder)) + " modifiers to the base mesh + all " + str(self._TotalShapeKeys - 1) + " shape keys") 263 | return {"RUNNING_MODAL"} 264 | 265 | else: 266 | return {"FINISHED"} 267 | 268 | # Work on one shape key per modal event 269 | def modal(self, context, event): 270 | if event.type == "TIMER": 271 | obj = self._Obj 272 | skObj = self._ShapeKeyObj 273 | workspaceObj = self._CurWorkspaceObj 274 | 275 | # This operation is complex and procedes through several work stages that carefully distributes the work in order to appease modal()'s weird behavior oddities 276 | if (self._WorkStage == 0): 277 | ### Very first modal event 278 | self.singleSelect(context, obj) 279 | 280 | # Remove all shape keys 281 | basisShapeKeyName = obj.data.shape_keys.key_blocks[0].name 282 | self.singleSelect(context, obj) 283 | for i in range(self._TotalShapeKeys): 284 | bpy.ops.object.shape_key_remove() 285 | 286 | # Apply the user's chosen modifiers to the base mesh 287 | for modifierName in self._ModifierApplyOrder: 288 | # Modifiers can be "disabled" on account of having invalid configuration (i.e. an Armature modifier without any armature object chosen) 289 | # https://github.com/blender/blender/blob/594f47ecd2d5367ca936cf6fc6ec8168c2b360d0/source/blender/editors/object/object_modifier.c#L677 290 | # This is not exposed to python, so there is no (clean) way of detecting these modifiers and warning the user 291 | # All we can really do is ignore it if it fails to apply and just keep going 292 | if (not modifierName in self._InvalidModifiers): 293 | try: 294 | bpy.ops.object.modifier_apply(apply_as="DATA", modifier=modifierName) 295 | except RuntimeError as e: 296 | self._InvalidModifiers[modifierName] = True 297 | self._AnyWarnings = True 298 | self.preport("[!!!!!] WARNING: Modifier '" + modifierName + "' failed to apply! Modifier will be skipped on shape keys. Modifier configuration is likely invalid. [!!!!!]", "WARNING") 299 | 300 | # Create the new basis shape key 301 | bpy.ops.object.shape_key_add() 302 | obj.data.shape_keys.key_blocks[0].name = basisShapeKeyName 303 | 304 | # On to the per-shape key work 305 | self._CurShapeKeyIndex = 1 # start the per-shape key work with the first "real" shape key (skip the basis shape key) 306 | self._WorkStage = 1 307 | self._WorkSubstage = 0 308 | context.window_manager.progress_update(1) 309 | 310 | elif (self._WorkStage == 1): 311 | ### Process each shape key in turn 312 | # Substages here allow for UI update cycles to appease the Blender deities and rid us of the voodoo that happens in modal() 313 | curShapeKeyName = skObj.data.shape_keys.key_blocks[self._CurShapeKeyIndex].name 314 | 315 | if (self._WorkSubstage == 0): 316 | # Notify for the shape key we are about to process 317 | self.preport("Applying modifiers to shape key " + str(self._CurShapeKeyIndex) + "/" + str(self._TotalShapeKeys - 1) + " '" + curShapeKeyName + "'") 318 | 319 | # Set the active shape key index to that shape key 320 | skObj.active_shape_key_index = skObj.data.shape_keys.key_blocks.keys().index(curShapeKeyName) 321 | 322 | self._WorkSubstage = 1 323 | 324 | elif (self._WorkSubstage == 1): 325 | # Idle to improve the odds that Blender will actually show the previous report() 326 | self._WorkSubstage = 2 327 | 328 | elif (self._WorkSubstage == 2): 329 | # Duplicate the shape key object 330 | self.singleSelect(context, skObj) 331 | bpy.ops.object.duplicate() 332 | workspaceObj = context.scene.objects.active 333 | workspaceObj.name = "_DELETE_ME__" + obj.name + "__SK_" + curShapeKeyName 334 | self._CurWorkspaceObj = workspaceObj 335 | 336 | # Move the shape key we are interested in to the end of the list 337 | self.singleSelect(context, workspaceObj) 338 | bpy.ops.object.shape_key_move(type="BOTTOM") 339 | 340 | # Set the active shape key in preparation for the next substage 341 | workspaceObj.active_shape_key_index = len(workspaceObj.data.shape_keys.key_blocks.keys()) - 2 342 | 343 | self._WorkSubstage = 3 344 | 345 | elif (self._WorkSubstage == 3): 346 | # Apply the active shape key to the base mesh 347 | self.singleSelect(context, workspaceObj) 348 | for i in range(len(workspaceObj.data.shape_keys.key_blocks.keys())): 349 | bpy.ops.object.shape_key_remove() 350 | 351 | # Apply the user's chosen modifiers to the base mesh 352 | for modifierName in self._ModifierApplyOrder: 353 | if (not modifierName in self._InvalidModifiers): 354 | try: 355 | bpy.ops.object.modifier_apply(apply_as="DATA", modifier=modifierName) 356 | except RuntimeError as e: 357 | self._InvalidModifiers[modifierName] = True 358 | self._AnyWarnings = True 359 | self.preport("[!!!!!] WARNING: Modifier '" + modifierName + "' failed to apply! Modifier configuration is likely invalid. [!!!!!]", "WARNING") 360 | 361 | # Add the workspace obj back to the source object as a new shape key 362 | self.singleSelect(context, workspaceObj) 363 | self.activeSelect(context, obj) 364 | bpy.ops.object.join_shapes() # always puts the new shape key at the end of the list 365 | newShapeKey = obj.data.shape_keys.key_blocks[len(obj.data.shape_keys.key_blocks.keys()) - 1] 366 | newShapeKey.name = curShapeKeyName 367 | 368 | # Copy the original shape key's pose parameters to the new shape key 369 | origShapeKey = skObj.data.shape_keys.key_blocks[skObj.data.shape_keys.key_blocks.keys().index(curShapeKeyName)] 370 | newShapeKey.slider_min = origShapeKey.slider_min 371 | newShapeKey.slider_max = origShapeKey.slider_max 372 | newShapeKey.value = origShapeKey.value 373 | newShapeKey.interpolation = origShapeKey.interpolation 374 | newShapeKey.vertex_group = origShapeKey.vertex_group # this is a string, not a VertexGroup 375 | newShapeKey.mute = origShapeKey.mute 376 | # relative_key will be set in the final work segment 377 | 378 | # Delete the workspace object 379 | self.singleSelect(context, workspaceObj) 380 | bpy.ops.object.delete() 381 | 382 | # On to the next shape key 383 | self._CurShapeKeyIndex += 1 384 | self._WorkSubstage = 0 385 | context.window_manager.progress_update(self._CurShapeKeyIndex) 386 | 387 | if (self._CurShapeKeyIndex > self._TotalShapeKeys - 1): 388 | self._WorkStage = 2 389 | self._WorkSubstage = 0 390 | #else: # Still have more shape keys to go 391 | 392 | elif (self._WorkStage == 2): 393 | ### Final things and tidying up 394 | self.singleSelect(context, obj) 395 | 396 | # Restore the blend shape dependencies 397 | for keyBlock in obj.data.shape_keys.key_blocks: 398 | relKey = None 399 | relKeyName = self._ShapeKeyDependencies[keyBlock.name] 400 | if (relKeyName in obj.data.shape_keys.key_blocks): 401 | relKeyIndex = obj.data.shape_keys.key_blocks.keys().index(relKeyName) 402 | keyBlock.relative_key = obj.data.shape_keys.key_blocks[relKeyIndex] 403 | # In my testing, the blend file must be saved and Blender restarted in order to later change the relative keys using the shape key panel 404 | 405 | # Delete the shape key object 406 | self.singleSelect(context, skObj) 407 | bpy.ops.object.delete() 408 | 409 | # Reselect the original object 410 | self.singleSelect(context, obj) 411 | 412 | # Done 413 | bpy.context.window_manager.progress_end() 414 | self.cancel(context) 415 | if (self._AnyWarnings): 416 | self.preport("Some modifiers failed to apply. Check console for details.", "ERROR") 417 | else: 418 | self.preport("All modifiers successfully applied.") 419 | return {"CANCELLED"} 420 | 421 | # Ensure the same object is selected at the end of every modal event so that the UI doesn't rapidly change 422 | self.singleSelect(context, obj) 423 | 424 | return {"PASS_THROUGH"} 425 | 426 | def cancel(self, context): 427 | if (self._Timer != None): 428 | context.window_manager.event_timer_remove(self._Timer) 429 | self._Timer = None 430 | 431 | 432 | def invoke(self, context, event): 433 | scene = context.scene 434 | properties = scene.shape_key_tools_props 435 | 436 | if (self.validateUser(context) == False): 437 | return {'FINISHED'} 438 | 439 | obj = context.object 440 | 441 | # Show all of the object's modifiers, but disable & mark the incompatible ones 442 | self.opt_modifiers.clear() 443 | for modifier in obj.modifiers: 444 | if (modifier.type in ModifierTypeInfo): 445 | optListItem = self.opt_modifiers.add() 446 | optListItem.name = modifier.name 447 | optListItem.do_apply = False 448 | 449 | optListItem.is_visible_in_viewport = modifier.show_viewport 450 | 451 | modifierTypeInfo = ModifierTypeInfo[modifier.type] 452 | optListItem.type = modifier.type 453 | optListItem.is_compatible = modifierTypeInfo[1] 454 | 455 | return context.window_manager.invoke_props_dialog(self, width=550) 456 | 457 | def register(): 458 | bpy.utils.register_class(ShapeKeyTools_ApplyModifiersToShapeKeys_OptListItem) 459 | bpy.utils.register_class(WM_OT_ShapeKeyTools_ApplyModifiersToShapeKeys) 460 | return WM_OT_ShapeKeyTools_ApplyModifiersToShapeKeys 461 | 462 | def unregister(): 463 | bpy.utils.unregister_class(WM_OT_ShapeKeyTools_ApplyModifiersToShapeKeys) 464 | bpy.utils.unregister_class(ShapeKeyTools_ApplyModifiersToShapeKeys_OptListItem) 465 | return WM_OT_ShapeKeyTools_ApplyModifiersToShapeKeys 466 | 467 | if (__name__ == "__main__"): 468 | register() 469 | -------------------------------------------------------------------------------- /shape_key_tools/ops/arbitrary_merge_blend_two.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | import bpy 4 | from bpy.props import * 5 | 6 | from shape_key_tools import common 7 | 8 | 9 | class WM_OT_ShapeKeyTools_OpCombineTwo(bpy.types.Operator): 10 | bl_idname = "wm.shape_key_tools_combine_two" 11 | bl_label = "Combine Two Shape Keys" 12 | bl_description = "Combines any two shape keys with various options for blending the two shape keys' deltas together. If the Vertex Filter is enabled, the deltas will be filtered before blending." 13 | bl_options = {"UNDO"} 14 | 15 | latest_shape_keys_items = None 16 | def getShapeKeys(self, context): 17 | shapeKeysOrdered = [] 18 | for shapeKeyBlock in context.object.data.shape_keys.key_blocks: 19 | index = context.object.data.shape_keys.key_blocks.keys().index(shapeKeyBlock.name) 20 | if (index > 0): # dont include the basis shape key 21 | shapeKeysOrdered.append((index, shapeKeyBlock.name)) 22 | def s(v): 23 | return v[0] 24 | shapeKeysOrdered.sort(key=s) 25 | latest_shape_keys_items = [(str(tuple[0]), tuple[1], tuple[1], "SHAPEKEY_DATA", tuple[0] - 1) for tuple in shapeKeysOrdered] 26 | return latest_shape_keys_items 27 | def inputShapeKeysChanged(self, context): 28 | if (self.opt_output_newname[:8] == "MERGED__" or self.opt_output_newname == "" or self.opt_output_newname == None): 29 | sk1Name = context.object.data.shape_keys.key_blocks[int(self.opt_shape_key_1, 10)].name 30 | sk2Name = context.object.data.shape_keys.key_blocks[int(self.opt_shape_key_2, 10)].name 31 | self.opt_output_newname = "MERGED__" + sk1Name + "__" + sk2Name 32 | opt_shape_key_1 = EnumProperty( 33 | name = "Shape Key 1 (Bottom Layer)", 34 | description = "One of the two shape keys that will be combined. In regards to the blending operation, this shape key exists on the 'layer' *below* Shape Key 2", 35 | items = getShapeKeys, 36 | update = inputShapeKeysChanged, 37 | ) 38 | opt_shape_key_2 = EnumProperty( 39 | name = "Shape Key 2 (Top Layer)", 40 | description = "One of the two shape keys that will be combined. In regards to the blending operation, this shape key exists on the 'layer' *above* Shape Key 1", 41 | items = getShapeKeys, 42 | update = inputShapeKeysChanged, 43 | ) 44 | 45 | opt_blend_mode = EnumProperty( 46 | name = "Blend Mode", 47 | description = "Method for compositing together the positions of the vertices in the two shape keys", 48 | items = [ 49 | ("add", "Additive", "Shape Key 2's deltas will be added to Shape Key 1's deltas. If the Vertex Filter is enabled, only RED deltas from Shape Key 2 will be added to Shape Key 1"), 50 | ("subtract", "Subtract", "Shape Key 2's deltas will be subtracted from Shape Key 1's deltas. If the Vertex Filter is enabled, only RED deltas from Shape Key 2 will be subtracted from Shape Key 1"), 51 | ("multiply", "Multiply", "Shape Key 1's deltas will be multiplied by Shape Key 2's deltas. If the Vertex Filter is enabled, only RED deltas from Shape Key 2 will be multiplied with Shape Key 1"), 52 | ("divide", "Divide", "Shape Key 1's deltas will be divided by Shape Key 2's deltas. If the Vertex Filter is enabled, only RED deltas from Shape Key 2 will be divided into Shape Key 1"), 53 | ("over", "Overwrite", "Shape Key 1's deltas will be replaced with Shape Key 2's deltas. If the Vertex Filter is enabled, only RED deltas from Shape Key 2 will replace their counterparts in Shape Key 1"), 54 | ("lerp", "Lerp", "Shape Key 1's deltas will be lerped towards Shape Key 2's deltas, using the user-specified lerp factor. If the Vertex Filter is enabled, only RED deltas from Shape Key 2 will be lerp'd into Shape Key 1"), 55 | ] 56 | ) 57 | opt_blend_lerp_factor = FloatProperty( 58 | name = "Lerp Blend Mode Factor", 59 | description = "Lerp factor, from 0 to 1. E.g. a value of 0.3 is 30% Shape Key 1 and 70% Shape Key 2", 60 | min = 0.0, 61 | max = 1.0, 62 | soft_min = 0.0, 63 | soft_max = 1.0, 64 | default = 0.5, 65 | precision = 6, 66 | step = 1, 67 | ) 68 | 69 | def optOutputChanged(self, context): 70 | if (self.opt_output == "replace1"): 71 | self.opt_delete_shapekey1_on_finish = False 72 | self.opt_delete_shapekey2_on_finish = True 73 | elif (self.opt_output == "replace2"): 74 | self.opt_delete_shapekey2_on_finish = False 75 | self.opt_delete_shapekey1_on_finish = True 76 | elif (self.opt_output == "new"): 77 | self.opt_delete_shapekey2_on_finish = False 78 | self.opt_delete_shapekey1_on_finish = False 79 | if (self.opt_output_newname[:8] == "MERGED__" or self.opt_output_newname == "" or self.opt_output_newname == None): 80 | sk1Name = context.object.data.shape_keys.key_blocks[int(self.opt_shape_key_1, 10)].name 81 | sk2Name = context.object.data.shape_keys.key_blocks[int(self.opt_shape_key_2, 10)].name 82 | self.opt_output_newname = "MERGED__" + sk1Name + "__" + sk2Name 83 | 84 | opt_output = EnumProperty( 85 | name = "Output", 86 | description = "Type of output for the newly combined shape key", 87 | items = [ 88 | ("replace1", "Replace Shape Key 1", "Output the combined shape key result to Shape Key 1."), 89 | ("replace2", "Replace Shape Key 2", "Output the combined shape key result to Shape Key 2."), 90 | ("new", "New Shape Key", "Create a new shape key for the result."), 91 | ], 92 | update = optOutputChanged 93 | ) 94 | opt_delete_shapekey1_on_finish = BoolProperty( 95 | name = "Delete Shape Key 1 On Finish", 96 | description = "Delete Shape Key 1 after combining", 97 | default = False, 98 | ) 99 | opt_delete_shapekey2_on_finish = BoolProperty( 100 | name = "Delete Shape Key 2 On Finish", 101 | description = "Delete Shape Key 2 after combining", 102 | default = True, 103 | ) 104 | opt_output_newname = StringProperty( 105 | name = "New Combined Shape Key Name", 106 | description = "Name for the new, combined shape key", 107 | ) 108 | 109 | def check(self, context): 110 | return True # To force redraws in the operator panel, which is does *not* occur by default 111 | 112 | def draw(self, context): 113 | scene = context.scene 114 | properties = scene.shape_key_tools_props 115 | 116 | layout = self.layout 117 | topBody = layout.column() 118 | 119 | ### Header area 120 | if (properties.opt_global_enable_filterverts): 121 | warnRow = topBody.row() 122 | topBody.label("The Vertex Filter is ENABLED and will affect this operation!", icon="ERROR") 123 | 124 | ### Layers and blend mode 125 | gLayers = topBody.box().column() 126 | gLayers.label("Shape Key Layers") 127 | gLayersInner = gLayers.box().column() 128 | # Header row 129 | header = gLayersInner.row().column_flow(columns=3, align=False) 130 | header.label("") # dummy 131 | header.label("") # dummy 132 | header.label("Blend Mode") 133 | # Top layer row 134 | layer2 = gLayersInner.row().column_flow(columns=3, align=False) 135 | layer2.label("Shape Key 2 (top layer)") 136 | layer2.prop(self, "opt_shape_key_2", text="") 137 | if (self.opt_blend_mode == "lerp"): 138 | blendModeRow = layer2.row().column_flow(columns=2, align=False) 139 | blendModeRow.prop(self, "opt_blend_mode", text="") 140 | blendModeRow.prop(self, "opt_blend_lerp_factor", text="") 141 | else: 142 | layer2.prop(self, "opt_blend_mode", text="") 143 | # Bottom layer row 144 | layer1 = gLayersInner.row().column_flow(columns=3, align=False) 145 | layer1.label("Shape Key 1 (bottom layer)") 146 | layer1.prop(self, "opt_shape_key_1", text="") 147 | layer1.label("") # dummy 148 | 149 | ### Output options 150 | gOutput = topBody.box().column() 151 | row = gOutput.row() 152 | # Type 153 | row.label("Output") 154 | row.prop(self, "opt_output", text="") 155 | # Type-specific options 156 | subcols = gOutput.row().column_flow(columns=2, align=False) 157 | subcolsLeft = subcols.column() 158 | subcolsLeft.label(" ") # dummy 159 | subcolsLeft.label(" ") # dummy 160 | subcolsRight = subcols.column() 161 | # Replace existing shape key 162 | rowCon1 = subcolsRight.row() 163 | rowCon1.prop(self, "opt_delete_shapekey1_on_finish", text="Delete Shape Key 1 when done") 164 | rowCon1.enabled = (self.opt_output == "replace2" or self.opt_output == "new") 165 | rowCon2 = subcolsRight.row() 166 | rowCon2.prop(self, "opt_delete_shapekey2_on_finish", text="Delete Shape Key 2 when done") 167 | rowCon2.enabled = (self.opt_output == "replace1" or self.opt_output == "new") 168 | # New shape key 169 | colCon1 = gOutput.column() 170 | if (self.opt_output == "new"): 171 | colCon1.label("New Shape Key Name:") 172 | colCon1.prop(self, "opt_output_newname", text="") 173 | 174 | 175 | def validate(self, context): 176 | # This op requires an active object 177 | if (context.object == None or hasattr(context, "object") == False): 178 | return (False, "No object is selected.") 179 | 180 | obj = context.object 181 | 182 | # Object must be a mesh 183 | if (obj.type != "MESH"): 184 | return (False, "The active object ('" + obj.name + "', type: " + obj.type + ") is not a mesh.") 185 | 186 | # Object must have enough shape keys 187 | if (not hasattr(obj.data.shape_keys, "key_blocks") or len(obj.data.shape_keys.key_blocks.keys()) <= 2): 188 | return (False, "The active object must have at least 2 shape keys (excluding the basis shape key).") 189 | 190 | return (True, None) 191 | 192 | def validateUser(self, context): 193 | (isValid, invalidReason) = self.validate(context) 194 | if (isValid): 195 | return True 196 | else: 197 | if self: 198 | self.report({'ERROR'}, invalidReason) 199 | return False 200 | 201 | @classmethod 202 | def poll(cls, context): 203 | (isValid, invalidReason) = cls.validate(None, context) 204 | return isValid 205 | 206 | 207 | def execute(self, context): 208 | scene = context.scene 209 | properties = scene.shape_key_tools_props 210 | 211 | if (self.validateUser(context) == False): 212 | return {'FINISHED'} 213 | 214 | obj = context.object 215 | 216 | if (self.opt_shape_key_1 == self.opt_shape_key_2): 217 | self.report({'ERROR'}, "You cannot combine a shape key with itself.") 218 | return {'FINISHED'} 219 | 220 | dest = self.opt_output_newname 221 | if (self.opt_output == "replace1"): 222 | dest = 1 223 | elif (self.opt_output == "replace2"): 224 | dest = 2 225 | 226 | # Build blend mode param dict 227 | blendModeParams = None 228 | if (self.opt_blend_mode == "lerp"): 229 | blendModeParams = { "Factor": self.opt_blend_lerp_factor } 230 | 231 | # Build vertex filter param dict 232 | vertexFilterParams = None 233 | if (properties.opt_global_enable_filterverts): 234 | vertexFilterParams = properties.getEnabledVertexFilterParams() 235 | 236 | # Blend and merge 237 | common.MergeAndBlendShapeKeys( 238 | obj, 239 | obj.data.shape_keys.key_blocks[int(self.opt_shape_key_1, 10)].name, 240 | obj.data.shape_keys.key_blocks[int(self.opt_shape_key_2, 10)].name, 241 | dest, 242 | self.opt_blend_mode, 243 | blendModeParams = blendModeParams, 244 | vertexFilterParams = vertexFilterParams, 245 | delete1OnFinish = self.opt_delete_shapekey1_on_finish, 246 | delete2OnFinish = self.opt_delete_shapekey2_on_finish, 247 | ) 248 | 249 | return{'FINISHED'} 250 | 251 | def invoke(self, context, event): 252 | scene = context.scene 253 | properties = scene.shape_key_tools_props 254 | 255 | if (self.validateUser(context) == False): 256 | return {'FINISHED'} 257 | 258 | obj = context.object 259 | 260 | # Set Shape Key 1 to the active shape key 261 | activeShapeKeyIndex = obj.data.shape_keys.key_blocks.keys().index(obj.active_shape_key.name) 262 | noShapeKeyWasActive = False 263 | if (activeShapeKeyIndex == 0): # no shape key active (idk how but it happens sometimes) 264 | noShapeKeyWasActive = True 265 | activeShapeKeyIndex = 1 # basis shape key active 266 | old = self.opt_shape_key_1 267 | self.opt_shape_key_1 = str(activeShapeKeyIndex) 268 | # If this changed the old value, then do some hueristics to auto pick an initial reasonable Shape Key 2 269 | if (self.opt_shape_key_1 != old or noShapeKeyWasActive): 270 | doNext = True 271 | 272 | # First see if Shape Key 1 is a L/R shape key with a corresponding opposite side shape key 273 | (firstShapeKey, expectedCompShapeKey, mergedShapeKey) = common.FindShapeKeyMergeNames(obj.active_shape_key.name) 274 | if (expectedCompShapeKey != None): 275 | if (expectedCompShapeKey in obj.data.shape_keys.key_blocks.keys()): 276 | self.opt_shape_key_2 = str(obj.data.shape_keys.key_blocks.keys().index(expectedCompShapeKey)) 277 | doNext = False 278 | 279 | # If no hueristic matched, default to the shape key immediately after Shape Key 1 (or immediately before, if Shape Key 1 is the very last one) (or the same, if only 1 non-basis shape key exists) 280 | if (doNext): 281 | sk1Index = int(self.opt_shape_key_1, 10) 282 | if (sk1Index == len(obj.data.shape_keys.key_blocks.keys()) - 1): 283 | self.opt_shape_key_2 = str(sk1Index - 1) 284 | elif (len(obj.data.shape_keys.key_blocks.keys()) <= 2): 285 | self.opt_shape_key_2 = str(sk1Index) 286 | else: 287 | self.opt_shape_key_2 = str(sk1Index + 1) 288 | 289 | # Launch the op 290 | return context.window_manager.invoke_props_dialog(self, width=600) 291 | 292 | 293 | def register(): 294 | bpy.utils.register_class(WM_OT_ShapeKeyTools_OpCombineTwo) 295 | return WM_OT_ShapeKeyTools_OpCombineTwo 296 | 297 | def unregister(): 298 | bpy.utils.unregister_class(WM_OT_ShapeKeyTools_OpCombineTwo) 299 | return WM_OT_ShapeKeyTools_OpCombineTwo 300 | 301 | if (__name__ == "__main__"): 302 | register() 303 | -------------------------------------------------------------------------------- /shape_key_tools/ops/arbitrary_split_by_filter.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | import bpy 4 | from bpy.props import * 5 | 6 | from shape_key_tools import common 7 | 8 | 9 | class WM_OT_ShapeKeyTools_OpSplitByFilter(bpy.types.Operator): 10 | bl_idname = "wm.shape_key_tools_split_by_filter" 11 | bl_label = "Split Active Shape Key" 12 | bl_description = "REQUIRES the Vertex Filter to be enabled! Splits the active mesh's active shape key into two shape keys, using the Vertex Filter to determine which deltas end up in the new shape key" 13 | bl_options = {"UNDO"} 14 | 15 | opt_mode = EnumProperty( 16 | name = "Split Mode", 17 | description = "Method for splitting off deltas from this shape key to a new shape key.", 18 | items = [ 19 | ("copy", "Copy", "RED deltas will be copied to the new shape key. The active shape key will remain unchanged."), 20 | ("move", "Move", "RED deltas will be copied to the new shape key, then those deltas will be removed from the active shape key (verts will be reverted to their basis position from the basis shape key)"), 21 | ] 22 | ) 23 | 24 | opt_new_shape_key_name = StringProperty( 25 | name = "New Shape Key Name", 26 | description = "Name for the newly split shape key", 27 | ) 28 | 29 | def draw(self, context): 30 | layout = self.layout 31 | topBody = layout.column() 32 | 33 | topBody.label("Split off a new shape key from the active shape key, using the Vertex Filter.") 34 | info = topBody.box().column() 35 | info.label("Verts with RED deltas will be preserved in the new shape key.") 36 | info.label("Verts with BLACK deltas will revert to their basis position in the new shape key.") 37 | 38 | optsBox = topBody.box().column() 39 | optsBox.prop(self, "opt_mode", text="Mode") 40 | optsBox.prop(self, "opt_new_shape_key_name", text="New Shape Key Name:") 41 | 42 | 43 | def validate(self, context): 44 | # This op requires an active object 45 | if (context.object == None or hasattr(context, "object") == False): 46 | return (False, "No object is selected.") 47 | 48 | obj = context.object 49 | 50 | # Object must be a mesh 51 | if (obj.type != "MESH"): 52 | return (False, "The active object ('" + obj.name + "', type: " + obj.type + ") is not a mesh.") 53 | 54 | # Object must have enough shape keys 55 | if (not hasattr(obj.data.shape_keys, "key_blocks") or len(obj.data.shape_keys.key_blocks.keys()) <= 1): 56 | return (False, "The active object must have at least 1 shape key (excluding the basis shape key).") 57 | 58 | # Active shape key cannot be the basis shape key 59 | activeShapeKeyIndex = obj.data.shape_keys.key_blocks.keys().index(obj.active_shape_key.name) 60 | if (activeShapeKeyIndex == 0): 61 | return (False, "You cannot split the basis shape key.") 62 | 63 | return (True, None) 64 | 65 | def validateUser(self, context): 66 | (isValid, invalidReason) = self.validate(context) 67 | if (isValid): 68 | return True 69 | else: 70 | if self: 71 | self.report({'ERROR'}, invalidReason) 72 | return False 73 | 74 | @classmethod 75 | def poll(cls, context): 76 | (isValid, invalidReason) = cls.validate(None, context) 77 | return isValid 78 | 79 | 80 | def execute(self, context): 81 | scene = context.scene 82 | properties = scene.shape_key_tools_props 83 | 84 | if (self.validateUser(context) == False): 85 | return {'FINISHED'} 86 | 87 | obj = context.object 88 | 89 | # Build the vertex filter parameter dict 90 | vertexFilterParams = None 91 | if (properties.opt_global_enable_filterverts): 92 | vertexFilterParams = properties.getEnabledVertexFilterParams() 93 | 94 | # Do the split 95 | common.SplitFilterActiveShapeKey(obj, self.opt_new_shape_key_name, self.opt_mode, vertexFilterParams) 96 | 97 | return {'FINISHED'} 98 | 99 | def invoke(self, context, event): 100 | scene = context.scene 101 | properties = scene.shape_key_tools_props 102 | 103 | if (self.validateUser(context) == False): 104 | return {'FINISHED'} 105 | 106 | obj = context.object 107 | 108 | # Default options 109 | self.opt_new_shape_key_name = obj.active_shape_key.name + "__SPLIT" 110 | 111 | return context.window_manager.invoke_props_dialog(self, width=450) 112 | 113 | 114 | def register(): 115 | bpy.utils.register_class(WM_OT_ShapeKeyTools_OpSplitByFilter) 116 | return WM_OT_ShapeKeyTools_OpSplitByFilter 117 | 118 | def unregister(): 119 | bpy.utils.unregister_class(WM_OT_ShapeKeyTools_OpSplitByFilter) 120 | return WM_OT_ShapeKeyTools_OpSplitByFilter 121 | 122 | if (__name__ == "__main__"): 123 | register() 124 | -------------------------------------------------------------------------------- /shape_key_tools/ops/internal_splitpair_preview_mesh.py: -------------------------------------------------------------------------------- 1 | import math, mathutils 2 | import bpy, bgl, blf 3 | 4 | 5 | ## 6 | ## Viewport drawing 7 | ## 8 | 9 | ### Applies default* 2D opengl drawing parameters 10 | #*default = what Blender's defaults** are 11 | #**which may or may not be wrong as the docs do not provide these, so they are figured through reasonable assumptions and empiric testing 12 | def OglDefaults(): 13 | bgl.glDisable(bgl.GL_BLEND) 14 | 15 | bgl.glDisable(bgl.GL_LINE_SMOOTH) 16 | bgl.glLineWidth(1) 17 | 18 | bgl.glDisable(bgl.GL_DEPTH_TEST) 19 | 20 | bgl.glColor4f(0.0, 0.0, 0.0, 1.0) 21 | 22 | 23 | ### Measures the to-be-drawn size of some 2d text 24 | def Measure2dText(text, size=24, dpi=96): 25 | blf.size(0, size, dpi) 26 | return blf.dimensions(0, text) 27 | 28 | ### Draws text in 2d with the specified viewportspace location, rotation, and alignment 29 | def Draw2dText(text, color, pos=None, rot=0, align=1, size=24, dpi=96): 30 | blf.size(0, size, dpi) 31 | textSize = blf.dimensions(0, text) 32 | 33 | # Alignment and translation are done together 34 | px = 0 35 | py = 0 36 | if (pos != None): 37 | px += pos[0] 38 | py += pos[1] 39 | #if (align == 1): # left align 40 | if (align == 0): # center align 41 | px += -(textSize[0] / 2) 42 | elif (align == -1): # right align 43 | px += -(textSize[0]) 44 | blf.position(0, px, py, 0) 45 | 46 | blf.enable(0, blf.ROTATION) 47 | blf.rotation(0, rot) 48 | 49 | bgl.glColor4f(*color) 50 | 51 | # For some unexplained reason, blf.draw() UNsets GL_BLEND if it is enabled, which undesirable and quite frankly very stupid 52 | restoreGlBlend = bgl.glIsEnabled(bgl.GL_BLEND) 53 | bgl.glDisable(bgl.GL_DEPTH_TEST) 54 | blf.draw(0, text) 55 | if (restoreGlBlend): 56 | bgl.glEnable(bgl.GL_BLEND) 57 | 58 | 59 | ## 60 | ## Main 2d draw callback 61 | ## 62 | 63 | def ViewportDraw2d(self, context): 64 | scene = context.scene 65 | properties = scene.shape_key_tools_props 66 | 67 | OglDefaults() 68 | 69 | bgl.glEnable(bgl.GL_BLEND) 70 | 71 | viewportBounds = bgl.Buffer(bgl.GL_INT, 4) 72 | bgl.glGetIntegerv(bgl.GL_VIEWPORT, viewportBounds) 73 | sW = viewportBounds[2] 74 | sH = viewportBounds[3] 75 | 76 | if (self.State == 1): # Warning message about the preview mesh init process 77 | line1 = "Initializing preview mesh..." 78 | line1Size = Measure2dText(line1, 18) 79 | 80 | line2 = "This may take a while for detailed meshes with many shape keys" 81 | line2Size = Measure2dText(line1, 12) 82 | 83 | Draw2dText(line1, (1, 0.95, 0.5, 1), (sW / 2, sH - 5 - line1Size[1]), 0, 0, 18) 84 | Draw2dText(line2, (1, 0.95, 0.6, 1), (sW / 2, sH - 5 - line1Size[1] - 10 - line2Size[1]), 0, 0, 12) 85 | 86 | elif (self.State == 2): # Live update mode 87 | if (self.ValidActiveShapeKey): 88 | line1 = "Previewing L/R Pair Split" 89 | line1Size = Measure2dText(line1, 18) 90 | 91 | line2 = "Commit this preview with Split Active Shape Key" 92 | line2Size = Measure2dText(line1, 12) 93 | 94 | Draw2dText(line1, (1, 1, 1, 1), (sW / 2, sH - 5 - line1Size[1]), 0, 0, 18) 95 | Draw2dText(line2, (1, 1, 1, 1), (sW / 2, sH - 5 - line1Size[1] - 10 - line2Size[1]), 0, 0, 12) 96 | else: 97 | line1 = "Previewing L/R Pair Split" 98 | line1Size = Measure2dText(line1, 18) 99 | 100 | line2 = "Mesh has no shape keys or the active shape key cannot be split" 101 | line2Size = Measure2dText(line1, 12) 102 | 103 | Draw2dText(line1, (1, 1, 1, 1), (sW / 2, sH - 5 - line1Size[1]), 0, 0, 18) 104 | Draw2dText(line2, (1, 0.15, 0.15, 1), (sW / 2, sH - 5 - line1Size[1] - 10 - line2Size[1]), 0, 0, 12) 105 | 106 | OglDefaults() 107 | 108 | 109 | 110 | ## 111 | ## Main operator 112 | ## 113 | 114 | # Param cls must be the __class__ object 115 | def Disable(cls, fromBlendFileChange=False): 116 | if (cls.InstanceInfo != None): 117 | opInstance = cls.InstanceInfo[0] 118 | opContext = cls.InstanceInfo[1] 119 | cls.InstanceInfo = None 120 | 121 | ## Selection helpers 122 | def deselectAll(): 123 | for obj in bpy.data.objects: 124 | obj.select = False 125 | def activeSelect(obj): 126 | obj.select = True 127 | opContext.scene.objects.active = obj 128 | def singleSelect(obj): 129 | deselectAll() 130 | activeSelect(obj) 131 | 132 | # Remove the modal timer 133 | if (opInstance._Timer != None): 134 | opContext.window_manager.event_timer_remove(opInstance._Timer) 135 | opInstance._Timer = None 136 | 137 | # Remove the 2d drawing callback 138 | if (opInstance._Drawing2dHandle != None): 139 | bpy.types.SpaceView3D.draw_handler_remove(opInstance._Drawing2dHandle, "WINDOW") 140 | opInstance._Drawing2dHandle = None 141 | 142 | # Remove the preview mesh object 143 | if (fromBlendFileChange == False): 144 | if (opInstance.PreviewMeshObject != None): 145 | opInstance.PreviewMeshObject.hide = False 146 | singleSelect(opInstance.PreviewMeshObject) 147 | bpy.ops.object.delete() 148 | opInstance.OriginalMeshObject.hide = False 149 | singleSelect(opInstance.OriginalMeshObject) 150 | 151 | 152 | 153 | ### Sets to false all properties which would cause this operator to run 154 | def UnsetEnablerProperties(context): 155 | scene = context.scene 156 | properties = scene.shape_key_tools_props 157 | 158 | properties.opt_shapepairs_splitmerge_preview_split_left = False 159 | properties.opt_shapepairs_splitmerge_preview_split_right = False 160 | 161 | 162 | 163 | ### bpy operator 164 | class VIEW_3D_OT_ShapeKeyTools_SplitPairPreview(bpy.types.Operator): 165 | bl_idname = "view3d.shape_key_tools_splitpair_preview" 166 | bl_label = "Shape Key Tools Split Pair Preview Mesh" 167 | bl_options = {'INTERNAL'} 168 | 169 | ## Statics 170 | InstanceInfo = None # Contains info on the currently running singleton instance of the operator. None when not running. 171 | 172 | ## Instance vars 173 | State = 0 174 | StateModals = 0 # Number of modal() calls that have occurred since the last state change 175 | 176 | OriginalMeshObject = None # The mesh which the preview mesh is representing 177 | PreviewMeshObject = None # The preview mesh object we are working with 178 | ValidActiveShapeKey = False # Only true when the mesh has >=2 shape keys and user's selected active shape key is not key 0 179 | LastActiveShapeKeyIndex = None # The index of the shape key on the original mesh that the user had active last time we checked 180 | LastUsedSplitParams = None # Dictionary of the pair split params last used to split the preview mesh's active shape key. None when the preview mesh is first initialized. 181 | 182 | 183 | ### Hook for when the current blend file is closing 184 | def BlendFilePreLoadWatcher(self, context): 185 | try: 186 | Disable(self.__class__, True) 187 | except: 188 | pass 189 | 190 | 191 | ### Returns true if this operator is valid to continue running, false otherwise 192 | def Validate(self, context): 193 | scene = context.scene 194 | properties = scene.shape_key_tools_props 195 | 196 | # Ensure the user still wants to see the preview 197 | if (properties.opt_shapepairs_splitmerge_preview_split_left == False and properties.opt_shapepairs_splitmerge_preview_split_right == False): 198 | return False 199 | 200 | # If the user has selected a different mesh, we can't assume things are in sync anymore 201 | 202 | 203 | return True 204 | 205 | 206 | ### Set a new state and reset the counter for modal() calls 207 | def ChangeState(self, newState): 208 | self.State = newState 209 | self.StateModals = 0 210 | 211 | 212 | ### Initial preview mesh setup 213 | def InitPreviewMesh(self, context): 214 | ## Selection helpers 215 | def deselectAll(): 216 | for obj in bpy.data.objects: 217 | obj.select = False 218 | def activeSelect(obj): 219 | obj.select = True 220 | context.scene.objects.active = obj 221 | def singleSelect(obj): 222 | deselectAll() 223 | activeSelect(obj) 224 | 225 | 226 | self.OriginalMeshObject = context.object 227 | 228 | 229 | ### The blend file might've been saved with the preview enabled and we're initializing from that and thus the preview mesh already exists in the scene 230 | # If so we will delete this old preview mesh and create a new one 231 | expectedPreviewMeshName = "zzz_DO_NOT_TOUCH__SHAPE_KEY_TOOLS__PREVIEW_SHAPE_KEY_SPLIT__" + self.OriginalMeshObject.name 232 | for obj in bpy.data.objects: 233 | if (obj.name == expectedPreviewMeshName): 234 | singleSelect(obj) 235 | bpy.ops.object.delete() 236 | 237 | 238 | ### Ensure the active mesh is selectable 239 | self.OriginalMeshObject.hide = False 240 | singleSelect(self.OriginalMeshObject) 241 | 242 | 243 | ### Duplicate the active mesh 244 | bpy.ops.object.duplicate() 245 | self.PreviewMeshObject = context.object 246 | 247 | # Rename it 248 | self.PreviewMeshObject.name = expectedPreviewMeshName 249 | 250 | # If it only has 0 or 1 shape keys (i.e. no real shape keys, only the basis shape key), or the active shape key is the key 0 (should be the basis), we wont preview anything 251 | self.ValidActiveShapeKey = (hasattr(self.PreviewMeshObject.data.shape_keys, "key_blocks") and len(self.PreviewMeshObject.data.shape_keys.key_blocks) >= 2 and self.PreviewMeshObject.active_shape_key_index > 0) 252 | if (self.ValidActiveShapeKey): 253 | # Ensure the active shape keys relative shape is key 0 254 | previewMeshActiveShape = self.PreviewMeshObject.data.shape_keys.key_blocks[self.PreviewMeshObject.active_shape_key_index] 255 | previewMeshActiveShape.relative_key = self.PreviewMeshObject.data.shape_keys.key_blocks[0] 256 | # Remove all shape keys except the active one and key 0 257 | bpy.ops.object.shape_key_move(type="BOTTOM") 258 | while (len(self.PreviewMeshObject.data.shape_keys.key_blocks) > 2): 259 | self.PreviewMeshObject.active_shape_key_index = len(self.PreviewMeshObject.data.shape_keys.key_blocks) - 2 260 | bpy.ops.object.shape_key_remove() 261 | self.PreviewMeshObject.active_shape_key_index = 1 262 | 263 | 264 | ### Reselect the original object 265 | singleSelect(self.OriginalMeshObject) 266 | 267 | # Hide it 268 | self.OriginalMeshObject.hide = True 269 | 270 | # The original object is selected, so the properties panel is showing all its shape keys and the user can select and manipulate them as expected 271 | # However, that object is hidden, so what they see instead in the viewport is the preview model 272 | 273 | 274 | ### Reset some tracked data 275 | self.LastActiveShapeKeyIndex = self.OriginalMeshObject.active_shape_key_index # This preview mesh will be valid so long as the user doesn't change the active shape key 276 | self.LastUsedSplitParams = None 277 | 278 | 279 | ### Keeps the preview mesh synchronized with the original mesh 280 | def UpdatePreviewMesh(self, context): 281 | scene = context.scene 282 | properties = scene.shape_key_tools_props 283 | 284 | ## Selection helpers 285 | def deselectAll(): 286 | for obj in bpy.data.objects: 287 | obj.select = False 288 | def activeSelect(obj): 289 | obj.select = True 290 | context.scene.objects.active = obj 291 | def singleSelect(obj): 292 | deselectAll() 293 | activeSelect(obj) 294 | 295 | # Ensure the preview mesh stays visible and the original mesh stays hidden 296 | self.PreviewMeshObject.hide = False 297 | self.OriginalMeshObject.hide = True 298 | 299 | # If the user selects something that isnt the original mesh or preview mesh, then we will end the preview and disable this operator 300 | if (not (context.object == self.OriginalMeshObject or context.object == self.PreviewMeshObject)): 301 | newSelection = context.object 302 | UnsetEnablerProperties(context) 303 | Disable(self.__class__) 304 | singleSelect(newSelection) 305 | return 306 | 307 | # If the user selects the preview mesh (i.e. most likely by clicking it in the viewport), change the selection back to the original mesh 308 | if (context.object == self.PreviewMeshObject): 309 | singleSelect(self.OriginalMeshObject) 310 | 311 | # If the user has changed the active shape key, we need to create a new preview mesh for it 312 | if (self.OriginalMeshObject.active_shape_key_index != self.LastActiveShapeKeyIndex): 313 | # Delete the existing preview mesh 314 | singleSelect(self.PreviewMeshObject) 315 | bpy.ops.object.delete() 316 | # Reselect the original mesh 317 | self.OriginalMeshObject.hide = False 318 | singleSelect(self.OriginalMeshObject) 319 | # Prepare to re-initialize a new preview mesh 320 | self.ChangeState(1) 321 | return 322 | 323 | ### If we have a valid active shape key to preview splitting, do that now 324 | if (self.ValidActiveShapeKey): 325 | # Update the split shape keys if needed 326 | needsUpdate = (self.LastUsedSplitParams == None) # If we have no record of the last split params, then we havent done the initial split yet 327 | if (needsUpdate == False): # Check if the user's chosen split/merge params have meaningfully changed and thus necessitate a new split to preview 328 | if (self.LastUsedSplitParams["opt_shapepairs_split_axis"] != properties.opt_shapepairs_split_axis): 329 | needsUpdate = True 330 | if (self.LastUsedSplitParams["opt_shapepairs_split_mode"] != properties.opt_shapepairs_split_mode): 331 | # sharp -> smooth w/ dist=0 doesnt need an update 332 | # smooth w/ dist 0 -> sharp doesnt need an update 333 | # all other situations need an update 334 | if (not ( 335 | (self.LastUsedSplitParams["opt_shapepairs_split_mode"] == "sharp" and properties.opt_shapepairs_split_mode == "smooth" and properties.opt_shapepairs_split_smoothdist == 0) 336 | or 337 | (self.LastUsedSplitParams["opt_shapepairs_split_mode"] == "smooth" and self.LastUsedSplitParams["opt_shapepairs_split_smoothdist"] == 0 and properties.opt_shapepairs_split_mode == "sharp") 338 | )): 339 | needsUpdate = True 340 | if (self.LastUsedSplitParams["opt_shapepairs_split_mode"] == "smooth" and properties.opt_shapepairs_split_mode == "smooth" and self.LastUsedSplitParams["opt_shapepairs_split_smoothdist"] != properties.opt_shapepairs_split_smoothdist): 341 | needsUpdate = True 342 | if (needsUpdate): 343 | # Select the preview mesh so we can operate on it 344 | singleSelect(self.PreviewMeshObject) 345 | # If old split keys still exist, delete them 346 | while (len(self.PreviewMeshObject.data.shape_keys.key_blocks) > 2): 347 | self.PreviewMeshObject.active_shape_key_index = len(self.PreviewMeshObject.data.shape_keys.key_blocks) - 1 348 | bpy.ops.object.shape_key_remove() 349 | # Split the active (and only) shape key. Unlike typical usage of this op, we want to keep the original shape key, and we dont want to turn off the split preview when it finishes 350 | bpy.ops.wm.shape_key_tools_split_active_pair(opt_delete_original=False, opt_clear_preview=False) 351 | 352 | # Update tracked params 353 | if (self.LastUsedSplitParams == None): 354 | self.LastUsedSplitParams = {} 355 | self.LastUsedSplitParams["opt_shapepairs_split_axis"] = properties.opt_shapepairs_split_axis 356 | self.LastUsedSplitParams["opt_shapepairs_split_mode"] = properties.opt_shapepairs_split_mode 357 | self.LastUsedSplitParams["opt_shapepairs_split_smoothdist"] = properties.opt_shapepairs_split_smoothdist 358 | 359 | # Reselect original mesh 360 | singleSelect(self.OriginalMeshObject) 361 | 362 | # The user can swap between previewing the left or right split key, so keep that synced 363 | if (properties.opt_shapepairs_splitmerge_preview_split_left): 364 | newIndex = len(self.PreviewMeshObject.data.shape_keys.key_blocks) - 2 365 | if (newIndex != self.PreviewMeshObject.active_shape_key_index): # because I think setting active_shape_key_index always trigger some expensive UI refreshes, even if the value is the same 366 | self.PreviewMeshObject.active_shape_key_index = newIndex 367 | elif (properties.opt_shapepairs_splitmerge_preview_split_right): 368 | newIndex = len(self.PreviewMeshObject.data.shape_keys.key_blocks) - 1 369 | if (newIndex != self.PreviewMeshObject.active_shape_key_index): 370 | self.PreviewMeshObject.active_shape_key_index = newIndex 371 | 372 | 373 | 374 | def modal(self, context, event): 375 | self.StateModals += 1 376 | 377 | if (self.Validate(context)): 378 | if (self.State == 1): # we need to reinit the preview, but first we're idling for 5 modal()s to give the drawing overlay a chance to warn the user about the lag that InitPreviewMesh() might incur 379 | if (self.StateModals > 5): 380 | self.InitPreviewMesh(context) 381 | self.ChangeState(2) 382 | 383 | elif (self.State == 2): # preview is set up and running, keep it updated 384 | self.UpdatePreviewMesh(context) 385 | 386 | return {'PASS_THROUGH'} 387 | else: 388 | Disable(self.__class__) 389 | return {'CANCELLED'} 390 | 391 | 392 | def execute(self, context): 393 | if (context.area.type == "VIEW_3D"): 394 | # Setup 2D drawing callback for the viewport 395 | self._Drawing2dHandle = bpy.types.SpaceView3D.draw_handler_add(ViewportDraw2d, (self, context), "WINDOW", "POST_PIXEL") 396 | # Since manipulating with many shape keys on complex meshes can hang the Blender UI for several seconds, we will draw some status text in the viewport so the user know what's going on while Blender is frozen 397 | 398 | # Opening a different blend file will stop this op before modal() has a chance to notice the blend file has changed 399 | # So we need to watch for that and clean up the drawing callback as needed 400 | bpy.app.handlers.load_pre.append(self.BlendFilePreLoadWatcher) 401 | 402 | self.__class__.InstanceInfo = (self, context) 403 | 404 | # Prep for initial operator setup so we can work with the user's currently selected mesh 405 | self.ChangeState(1) 406 | # We want to wait at least one or two rendered frames before running InitPreviewMesh so that the viewport drawing hook can warn the user about the potential incoming freeze 407 | # modal() will do this and call InitPreviewMesh() very shortly after this execute() 408 | 409 | context.window_manager.modal_handler_add(self) 410 | self._Timer = context.window_manager.event_timer_add(0.017, context.window) # See notes in internal_viewport_visuals.py on this 411 | 412 | return {'RUNNING_MODAL'} 413 | else: 414 | # If the viewport isn't available, disable the Properties which enable this operator 415 | print("Viewport not available. Split pair preview has been disabled.") 416 | UnsetEnablerProperties(context) 417 | return {'CANCELLED'} 418 | 419 | 420 | def register(): 421 | bpy.utils.register_class(VIEW_3D_OT_ShapeKeyTools_SplitPairPreview) 422 | return VIEW_3D_OT_ShapeKeyTools_SplitPairPreview 423 | 424 | def unregister(): 425 | Disable(VIEW_3D_OT_ShapeKeyTools_SplitPairPreview) 426 | bpy.utils.unregister_class(VIEW_3D_OT_ShapeKeyTools_SplitPairPreview) 427 | return VIEW_3D_OT_ShapeKeyTools_SplitPairPreview 428 | 429 | if __name__ == "__main__": 430 | register() 431 | -------------------------------------------------------------------------------- /shape_key_tools/ops/internal_viewport_visuals.py: -------------------------------------------------------------------------------- 1 | import math, mathutils 2 | import bpy, bgl, blf 3 | 4 | 5 | ## 6 | ## Debugging 7 | ## 8 | 9 | ### Gets the status of some ogl params that may be set through glEnable/Disable or otherwise may be controlled by client code or Blender internals and are potentially relevant to the rendering we are doing 10 | def GetGlParams(): 11 | def getInt1(param): 12 | out = bgl.Buffer(bgl.GL_INT, 1) 13 | bgl.glGetIntegerv(param, out) 14 | return out.to_list()[0] 15 | def getInt2(param): 16 | out = bgl.Buffer(bgl.GL_INT, 2) 17 | bgl.glGetIntegerv(param, out) 18 | return out.to_list() 19 | def getInt4(param): 20 | out = bgl.Buffer(bgl.GL_INT, 4) 21 | bgl.glGetIntegerv(param, out) 22 | return out.to_list() 23 | 24 | def getFloat1(param): 25 | out = bgl.Buffer(bgl.GL_FLOAT, 1) 26 | bgl.glGetIntegerv(param, out) 27 | return out.to_list()[0] 28 | def getFloat2(param): 29 | out = bgl.Buffer(bgl.GL_FLOAT, 2) 30 | bgl.glGetIntegerv(param, out) 31 | return out.to_list() 32 | def getFloat4(param): 33 | out = bgl.Buffer(bgl.GL_FLOAT, 4) 34 | bgl.glGetIntegerv(param, out) 35 | return out.to_list() 36 | def getFloat16(param): 37 | out = bgl.Buffer(bgl.GL_FLOAT, 16) 38 | bgl.glGetIntegerv(param, out) 39 | return out.to_list() 40 | 41 | params = { 42 | "GL_ACCUM_ALPHA_BITS": getInt1(bgl.GL_ACCUM_ALPHA_BITS), 43 | "GL_ACCUM_BLUE_BITS": getInt1(bgl.GL_ACCUM_BLUE_BITS), 44 | "GL_ACCUM_CLEAR_VALUE": getFloat4(bgl.GL_ACCUM_CLEAR_VALUE), 45 | "GL_ACCUM_GREEN_BITS": getInt1(bgl.GL_ACCUM_GREEN_BITS), 46 | "GL_ACCUM_RED_BITS": getInt1(bgl.GL_ACCUM_RED_BITS), 47 | "GL_ACTIVE_TEXTURE": getInt1(bgl.GL_ACTIVE_TEXTURE), 48 | "GL_ALIASED_POINT_SIZE_RANGE": getInt2(bgl.GL_ALIASED_POINT_SIZE_RANGE), 49 | "GL_ALIASED_LINE_WIDTH_RANGE": getInt2(bgl.GL_ALIASED_LINE_WIDTH_RANGE), 50 | "GL_ALPHA_BIAS": getFloat1(bgl.GL_ALPHA_BIAS), 51 | "GL_ALPHA_BITS": getInt1(bgl.GL_ALPHA_BITS), 52 | "GL_ALPHA_SCALE": getInt1(bgl.GL_ALPHA_SCALE), 53 | "GL_ALPHA_TEST": getInt1(bgl.GL_ALPHA_TEST), 54 | "GL_ALPHA_TEST_FUNC": getInt1(bgl.GL_ALPHA_TEST_FUNC), 55 | "GL_ALPHA_TEST_REF": getFloat1(bgl.GL_ALPHA_TEST_REF), 56 | "GL_ARRAY_BUFFER_BINDING": getInt1(bgl.GL_ARRAY_BUFFER_BINDING), 57 | "GL_ATTRIB_STACK_DEPTH": getInt1(bgl.GL_ATTRIB_STACK_DEPTH), 58 | "GL_AUTO_NORMAL": getInt1(bgl.GL_AUTO_NORMAL), 59 | "GL_AUX_BUFFERS": getInt1(bgl.GL_AUX_BUFFERS), 60 | "GL_BLEND": getInt1(bgl.GL_BLEND), 61 | #"GL_BLEND_COLOR": getFloat4(bgl.GL_BLEND_COLOR), # bgl.GL_BLEND_COLOR doesnt exist 62 | "GL_BLEND_DST_ALPHA": getInt1(bgl.GL_BLEND_DST_ALPHA), 63 | "GL_BLEND_DST_RGB": getInt1(bgl.GL_BLEND_DST_RGB), 64 | "GL_BLEND_EQUATION_RGB": getInt1(bgl.GL_BLEND_EQUATION_RGB), 65 | "GL_BLEND_EQUATION_ALPHA": getInt1(bgl.GL_BLEND_EQUATION_ALPHA), 66 | "GL_BLEND_SRC_ALPHA": getInt1(bgl.GL_BLEND_SRC_ALPHA), 67 | "GL_BLEND_SRC_RGB": getInt1(bgl.GL_BLEND_SRC_RGB), 68 | "GL_BLUE_BIAS": getFloat1(bgl.GL_BLUE_BIAS), 69 | "GL_BLUE_BITS": getInt1(bgl.GL_BLUE_BITS), 70 | "GL_BLUE_SCALE": getInt1(bgl.GL_BLUE_SCALE), 71 | "GL_CLIENT_ACTIVE_TEXTURE": getInt1(bgl.GL_CLIENT_ACTIVE_TEXTURE), 72 | "GL_CLIENT_ATTRIB_STACK_DEPTH": getInt1(bgl.GL_CLIENT_ATTRIB_STACK_DEPTH), 73 | "GL_COLOR_ARRAY": getInt1(bgl.GL_COLOR_ARRAY), 74 | "GL_COLOR_ARRAY_BUFFER_BINDING": getInt1(bgl.GL_COLOR_ARRAY_BUFFER_BINDING), 75 | "GL_COLOR_ARRAY_SIZE": getInt1(bgl.GL_COLOR_ARRAY_SIZE), 76 | "GL_COLOR_ARRAY_STRIDE": getInt1(bgl.GL_COLOR_ARRAY_STRIDE), 77 | "GL_COLOR_ARRAY_TYPE": getInt1(bgl.GL_COLOR_ARRAY_TYPE), 78 | "GL_COLOR_CLEAR_VALUE": getFloat4(bgl.GL_COLOR_CLEAR_VALUE), 79 | "GL_COLOR_LOGIC_OP": getInt1(bgl.GL_COLOR_LOGIC_OP), 80 | "GL_COLOR_MATERIAL": getInt1(bgl.GL_COLOR_MATERIAL), 81 | "GL_COLOR_MATERIAL_FACE": getInt1(bgl.GL_COLOR_MATERIAL_FACE), 82 | "GL_COLOR_MATERIAL_PARAMETER": getInt1(bgl.GL_COLOR_MATERIAL_PARAMETER), 83 | #"GL_COLOR_MATRIX": getFloat16(bgl.GL_COLOR_MATRIX), # bgl.GL_COLOR_MATRIX doesn't exist 84 | #"GL_COLOR_MATRIX_STACK_DEPTH": getInt1(bgl.GL_COLOR_MATRIX_STACK_DEPTH), # bgl.GL_COLOR_MATRIX_STACK_DEPTH doesn't exist 85 | "GL_COLOR_SUM": getInt1(bgl.GL_COLOR_SUM), 86 | #"GL_COLOR_TABLE": getInt1(bgl.GL_COLOR_TABLE), # bgl.GL_COLOR_TABLE doesn't exist 87 | "GL_COLOR_WRITEMASK": getInt4(bgl.GL_COLOR_WRITEMASK), 88 | #"GL_CONVOLUTION_1D": getInt1(bgl.GL_CONVOLUTION_1D), # bgl.GL_CONVOLUTION_1D doesn't exist 89 | #"GL_CONVOLUTION_2D": getInt1(bgl.GL_CONVOLUTION_2D), # bgl.GL_CONVOLUTION_2D doesn't exist 90 | "GL_CULL_FACE": getInt1(bgl.GL_CULL_FACE), 91 | "GL_CULL_FACE_MODE": getInt1(bgl.GL_CULL_FACE_MODE), 92 | "GL_CURRENT_COLOR": getFloat4(bgl.GL_CURRENT_COLOR), 93 | "GL_CURRENT_FOG_COORD": getFloat1(bgl.GL_CURRENT_FOG_COORD), 94 | "GL_CURRENT_INDEX": getInt1(bgl.GL_CURRENT_INDEX), 95 | "GL_CURRENT_NORMAL": getFloat4(bgl.GL_CURRENT_NORMAL), 96 | "GL_CURRENT_PROGRAM": getInt1(bgl.GL_CURRENT_PROGRAM), 97 | "GL_CURRENT_RASTER_COLOR": getFloat4(bgl.GL_CURRENT_RASTER_COLOR), 98 | "GL_CURRENT_RASTER_POSITION_VALID": getInt4(bgl.GL_CURRENT_RASTER_POSITION_VALID), 99 | "GL_CURRENT_RASTER_SECONDARY_COLOR": getFloat4(bgl.GL_CURRENT_RASTER_SECONDARY_COLOR), 100 | "GL_CURRENT_TEXTURE_COORDS": getFloat4(bgl.GL_CURRENT_TEXTURE_COORDS), 101 | "GL_DEPTH_BIAS": getFloat1(bgl.GL_DEPTH_BIAS), 102 | "GL_DEPTH_BITS": getInt1(bgl.GL_DEPTH_BITS), 103 | "GL_DEPTH_CLEAR_VALUE": getFloat1(bgl.GL_DEPTH_CLEAR_VALUE), 104 | "GL_DEPTH_SCALE": getInt1(bgl.GL_DEPTH_SCALE), 105 | "GL_DEPTH_TEST": getInt1(bgl.GL_DEPTH_TEST), 106 | "GL_DEPTH_WRITEMASK": getInt1(bgl.GL_DEPTH_WRITEMASK), 107 | "GL_DITHER": getInt1(bgl.GL_DITHER), 108 | "GL_DRAW_BUFFER": getInt1(bgl.GL_DRAW_BUFFER), 109 | "GL_EDGE_FLAG": getInt1(bgl.GL_EDGE_FLAG), 110 | "GL_EDGE_FLAG_ARRAY": getInt1(bgl.GL_EDGE_FLAG_ARRAY), 111 | "GL_EDGE_FLAG_ARRAY_BUFFER_BINDING": getInt1(bgl.GL_EDGE_FLAG_ARRAY_BUFFER_BINDING), 112 | "GL_EDGE_FLAG_ARRAY_STRIDE": getInt1(bgl.GL_EDGE_FLAG_ARRAY_STRIDE), 113 | "GL_ELEMENT_ARRAY_BUFFER_BINDING": getInt1(bgl.GL_ELEMENT_ARRAY_BUFFER_BINDING), 114 | "GL_FEEDBACK_BUFFER_SIZE": getInt1(bgl.GL_FEEDBACK_BUFFER_SIZE), 115 | "GL_FEEDBACK_BUFFER_TYPE": getInt1(bgl.GL_FEEDBACK_BUFFER_TYPE), 116 | "GL_FOG": getInt1(bgl.GL_FOG), 117 | "GL_FOG_COORD_ARRAY": getInt1(bgl.GL_FOG_COORD_ARRAY), 118 | "GL_FOG_COORD_ARRAY_BUFFER_BINDING": getInt1(bgl.GL_FOG_COORD_ARRAY_BUFFER_BINDING), 119 | "GL_FOG_COORD_ARRAY_STRIDE": getInt1(bgl.GL_FOG_COORD_ARRAY_STRIDE), 120 | "GL_FOG_COORD_ARRAY_TYPE": getInt1(bgl.GL_FOG_COORD_ARRAY_TYPE), 121 | "GL_FOG_COORD_SRC": getInt1(bgl.GL_FOG_COORD_SRC), 122 | "GL_FOG_COLOR": getFloat4(bgl.GL_FOG_COLOR), 123 | "GL_FOG_DENSITY": getInt1(bgl.GL_FOG_DENSITY), 124 | "GL_FOG_END": getInt1(bgl.GL_FOG_END), 125 | "GL_FOG_HINT": getInt1(bgl.GL_FOG_HINT), 126 | "GL_FOG_INDEX": getInt1(bgl.GL_FOG_INDEX), 127 | "GL_FOG_MODE": getInt1(bgl.GL_FOG_MODE), 128 | "GL_FOG_START": getInt1(bgl.GL_FOG_START), 129 | "GL_FRONT_FACE": getInt1(bgl.GL_FRONT_FACE), 130 | "GL_GREEN_BIAS": getFloat1(bgl.GL_GREEN_BIAS), 131 | "GL_GREEN_BITS": getInt1(bgl.GL_GREEN_BITS), 132 | "GL_GREEN_SCALE": getInt1(bgl.GL_GREEN_SCALE), 133 | #"GL_HISTOGRAM": getInt1(bgl.GL_HISTOGRAM), # bgl.GL_HISTOGRAM doesn't exist 134 | "GL_INDEX_ARRAY": getInt1(bgl.GL_INDEX_ARRAY), 135 | "GL_INDEX_ARRAY_BUFFER_BINDING": getInt1(bgl.GL_INDEX_ARRAY_BUFFER_BINDING), 136 | "GL_INDEX_ARRAY_STRIDE": getInt1(bgl.GL_INDEX_ARRAY_STRIDE), 137 | "GL_INDEX_ARRAY_TYPE": getInt1(bgl.GL_INDEX_ARRAY_TYPE), 138 | "GL_INDEX_BITS": getInt1(bgl.GL_INDEX_BITS), 139 | "GL_INDEX_CLEAR_VALUE": getInt1(bgl.GL_INDEX_CLEAR_VALUE), 140 | "GL_INDEX_LOGIC_OP": getInt1(bgl.GL_INDEX_LOGIC_OP), 141 | "GL_INDEX_MODE": getInt1(bgl.GL_INDEX_MODE), 142 | "GL_INDEX_OFFSET": getInt1(bgl.GL_INDEX_OFFSET), 143 | "GL_INDEX_SHIFT": getInt1(bgl.GL_INDEX_SHIFT), 144 | "GL_INDEX_WRITEMASK": getInt1(bgl.GL_INDEX_WRITEMASK), 145 | "GL_LIGHTING": getInt1(bgl.GL_LIGHTING), 146 | "GL_LIGHT_MODEL_AMBIENT": getFloat4(bgl.GL_LIGHT_MODEL_AMBIENT), 147 | "GL_LIGHT_MODEL_COLOR_CONTROL": getInt1(bgl.GL_LIGHT_MODEL_COLOR_CONTROL), 148 | "GL_LIGHT_MODEL_LOCAL_VIEWER": getInt1(bgl.GL_LIGHT_MODEL_LOCAL_VIEWER), 149 | "GL_LIGHT_MODEL_TWO_SIDE": getInt1(bgl.GL_LIGHT_MODEL_TWO_SIDE), 150 | "GL_LINE_SMOOTH": getInt1(bgl.GL_LINE_SMOOTH), 151 | "GL_LINE_SMOOTH_HINT": getInt1(bgl.GL_LINE_SMOOTH_HINT), 152 | "GL_LINE_STIPPLE": getInt1(bgl.GL_LINE_STIPPLE), 153 | "GL_LINE_STIPPLE_PATTERN": getInt1(bgl.GL_LINE_STIPPLE_PATTERN), 154 | "GL_LINE_STIPPLE_REPEAT": getInt1(bgl.GL_LINE_STIPPLE_REPEAT), 155 | "GL_LINE_WIDTH": getInt1(bgl.GL_LINE_WIDTH), 156 | "GL_LINE_WIDTH_GRANULARITY": getInt1(bgl.GL_LINE_WIDTH_GRANULARITY), 157 | "GL_LINE_WIDTH_RANGE": getInt2(bgl.GL_LINE_WIDTH_RANGE), 158 | "GL_LIST_BASE": getInt1(bgl.GL_LIST_BASE), 159 | "GL_LIST_INDEX": getInt1(bgl.GL_LIST_INDEX), 160 | "GL_LIST_MODE": getInt1(bgl.GL_LIST_MODE), 161 | "GL_LOGIC_OP_MODE": getInt1(bgl.GL_LOGIC_OP_MODE), 162 | "GL_LOGIC_OP_MODE": getInt1(bgl.GL_LOGIC_OP_MODE), 163 | "GL_MAP_COLOR": getInt1(bgl.GL_MAP_COLOR), 164 | "GL_MAP_STENCIL": getInt1(bgl.GL_MAP_STENCIL), 165 | "GL_MATRIX_MODE": getInt1(bgl.GL_MATRIX_MODE), 166 | "GL_MAX_CLIP_PLANES": getInt1(bgl.GL_MAX_CLIP_PLANES), 167 | "GL_MAX_LIGHTS": getInt1(bgl.GL_MAX_LIGHTS), 168 | "GL_MAX_MODELVIEW_STACK_DEPTH": getInt1(bgl.GL_MAX_MODELVIEW_STACK_DEPTH), 169 | "GL_MAX_PROJECTION_STACK_DEPTH": getInt1(bgl.GL_MAX_PROJECTION_STACK_DEPTH), 170 | "GL_MODELVIEW_MATRIX": getFloat16(bgl.GL_MODELVIEW_MATRIX), 171 | "GL_MODELVIEW_STACK_DEPTH": getInt1(bgl.GL_MODELVIEW_STACK_DEPTH), 172 | "GL_NAME_STACK_DEPTH": getInt1(bgl.GL_NAME_STACK_DEPTH), 173 | "GL_NORMAL_ARRAY": getInt1(bgl.GL_NORMAL_ARRAY), 174 | "GL_NORMAL_ARRAY_BUFFER_BINDING": getInt1(bgl.GL_NORMAL_ARRAY_BUFFER_BINDING), 175 | "GL_NORMAL_ARRAY_STRIDE": getInt1(bgl.GL_NORMAL_ARRAY_STRIDE), 176 | "GL_NORMAL_ARRAY_TYPE": getInt1(bgl.GL_NORMAL_ARRAY_TYPE), 177 | "GL_NORMALIZE": getInt1(bgl.GL_NORMALIZE), 178 | "GL_PERSPECTIVE_CORRECTION_HINT": getInt1(bgl.GL_PERSPECTIVE_CORRECTION_HINT), 179 | "GL_PERSPECTIVE_CORRECTION_HINT": getInt1(bgl.GL_PERSPECTIVE_CORRECTION_HINT), 180 | "GL_POLYGON_MODE": getInt1(bgl.GL_POLYGON_MODE), 181 | "GL_POLYGON_OFFSET_FACTOR": getFloat1(bgl.GL_POLYGON_OFFSET_FACTOR), 182 | "GL_POLYGON_OFFSET_UNITS": getFloat1(bgl.GL_POLYGON_OFFSET_UNITS), 183 | "GL_POLYGON_OFFSET_FILL": getInt1(bgl.GL_POLYGON_OFFSET_FILL), 184 | "GL_POLYGON_OFFSET_LINE": getInt1(bgl.GL_POLYGON_OFFSET_LINE), 185 | "GL_POLYGON_OFFSET_POINT": getInt1(bgl.GL_POLYGON_OFFSET_POINT), 186 | "GL_POLYGON_SMOOTH": getInt1(bgl.GL_POLYGON_SMOOTH), 187 | "GL_POLYGON_SMOOTH_HINT": getInt1(bgl.GL_POLYGON_SMOOTH_HINT), 188 | "GL_POLYGON_STIPPLE": getInt1(bgl.GL_POLYGON_STIPPLE), 189 | "GL_PROJECTION_MATRIX": getFloat16(bgl.GL_PROJECTION_MATRIX), 190 | "GL_PROJECTION_STACK_DEPTH": getInt1(bgl.GL_PROJECTION_STACK_DEPTH), 191 | "GL_READ_BUFFER": getInt1(bgl.GL_READ_BUFFER), 192 | "GL_RED_BIAS": getFloat1(bgl.GL_RED_BIAS), 193 | "GL_RED_BITS": getInt1(bgl.GL_RED_BITS), 194 | "GL_RED_SCALE": getInt1(bgl.GL_RED_SCALE), 195 | "GL_RED_SCALE": getInt1(bgl.GL_RED_SCALE), 196 | "GL_RENDER_MODE": getInt1(bgl.GL_RENDER_MODE), 197 | "GL_RESCALE_NORMAL": getInt1(bgl.GL_RESCALE_NORMAL), 198 | "GL_RGBA_MODE": getInt1(bgl.GL_RGBA_MODE), 199 | "GL_SAMPLE_BUFFERS": getInt1(bgl.GL_SAMPLE_BUFFERS), 200 | "GL_SAMPLE_COVERAGE_VALUE": getFloat1(bgl.GL_SAMPLE_COVERAGE_VALUE), 201 | "GL_SAMPLE_COVERAGE_INVERT": getInt1(bgl.GL_SAMPLE_COVERAGE_INVERT), 202 | "GL_SAMPLES": getInt1(bgl.GL_SAMPLES), 203 | "GL_SCISSOR_BOX": getInt4(bgl.GL_SCISSOR_BOX), 204 | "GL_SCISSOR_TEST": getInt1(bgl.GL_SCISSOR_TEST), 205 | "GL_SECONDARY_COLOR_ARRAY": getInt1(bgl.GL_SECONDARY_COLOR_ARRAY), 206 | "GL_SECONDARY_COLOR_ARRAY_BUFFER_BINDING": getInt1(bgl.GL_SECONDARY_COLOR_ARRAY_BUFFER_BINDING), 207 | "GL_SECONDARY_COLOR_ARRAY_SIZE": getInt1(bgl.GL_SECONDARY_COLOR_ARRAY_SIZE), 208 | "GL_SECONDARY_COLOR_ARRAY_STRIDE": getInt1(bgl.GL_SECONDARY_COLOR_ARRAY_STRIDE), 209 | "GL_SECONDARY_COLOR_ARRAY_TYPE": getInt1(bgl.GL_SECONDARY_COLOR_ARRAY_TYPE), 210 | "GL_SELECTION_BUFFER_SIZE": getInt1(bgl.GL_SELECTION_BUFFER_SIZE), 211 | "GL_SHADE_MODEL": getInt1(bgl.GL_SHADE_MODEL), 212 | "GL_SMOOTH_LINE_WIDTH_RANGE": getInt2(bgl.GL_SMOOTH_LINE_WIDTH_RANGE), 213 | "GL_SMOOTH_LINE_WIDTH_GRANULARITY": getInt1(bgl.GL_SMOOTH_LINE_WIDTH_GRANULARITY), 214 | "GL_STENCIL_BACK_FAIL": getInt1(bgl.GL_STENCIL_BACK_FAIL), 215 | "GL_STENCIL_BACK_FUNC": getInt1(bgl.GL_STENCIL_BACK_FUNC), 216 | "GL_STENCIL_BACK_PASS_DEPTH_FAIL": getInt1(bgl.GL_STENCIL_BACK_PASS_DEPTH_FAIL), 217 | "GL_STENCIL_BACK_PASS_DEPTH_PASS": getInt1(bgl.GL_STENCIL_BACK_PASS_DEPTH_PASS), 218 | "GL_STENCIL_BACK_REF": getInt1(bgl.GL_STENCIL_BACK_REF), 219 | "GL_STENCIL_BACK_VALUE_MASK": getInt1(bgl.GL_STENCIL_BACK_VALUE_MASK), 220 | "GL_STENCIL_BACK_WRITEMASK": getInt1(bgl.GL_STENCIL_BACK_WRITEMASK), 221 | "GL_STENCIL_BITS": getInt1(bgl.GL_STENCIL_BITS), 222 | "GL_STENCIL_CLEAR_VALUE": getInt1(bgl.GL_STENCIL_CLEAR_VALUE), 223 | "GL_STENCIL_FAIL": getInt1(bgl.GL_STENCIL_FAIL), 224 | "GL_STENCIL_FUNC": getInt1(bgl.GL_STENCIL_FUNC), 225 | "GL_STENCIL_PASS_DEPTH_FAIL": getInt1(bgl.GL_STENCIL_PASS_DEPTH_FAIL), 226 | "GL_STENCIL_PASS_DEPTH_PASS": getInt1(bgl.GL_STENCIL_PASS_DEPTH_PASS), 227 | "GL_STENCIL_REF": getInt1(bgl.GL_STENCIL_REF), 228 | "GL_STENCIL_TEST": getInt1(bgl.GL_STENCIL_TEST), 229 | "GL_STENCIL_VALUE_MASK": getInt1(bgl.GL_STENCIL_VALUE_MASK), 230 | "GL_STENCIL_WRITEMASK": getInt1(bgl.GL_STENCIL_WRITEMASK), 231 | "GL_TEXTURE_1D": getInt1(bgl.GL_TEXTURE_1D), 232 | "GL_TEXTURE_BINDING_1D": getInt1(bgl.GL_TEXTURE_BINDING_1D), 233 | "GL_TEXTURE_2D": getInt1(bgl.GL_TEXTURE_2D), 234 | "GL_TEXTURE_BINDING_2D": getInt1(bgl.GL_TEXTURE_BINDING_2D), 235 | "GL_TEXTURE_3D": getInt1(bgl.GL_TEXTURE_3D), 236 | "GL_TEXTURE_BINDING_3D": getInt1(bgl.GL_TEXTURE_BINDING_3D), 237 | "GL_TEXTURE_BINDING_CUBE_MAP": getInt1(bgl.GL_TEXTURE_BINDING_CUBE_MAP), 238 | "GL_TEXTURE_COORD_ARRAY": getInt1(bgl.GL_TEXTURE_COORD_ARRAY), 239 | "GL_TEXTURE_COORD_ARRAY_BUFFER_BINDING": getInt1(bgl.GL_TEXTURE_COORD_ARRAY_BUFFER_BINDING), 240 | "GL_TEXTURE_COORD_ARRAY_SIZE": getInt1(bgl.GL_TEXTURE_COORD_ARRAY_SIZE), 241 | "GL_TEXTURE_COORD_ARRAY_STRIDE": getInt1(bgl.GL_TEXTURE_COORD_ARRAY_STRIDE), 242 | "GL_TEXTURE_COORD_ARRAY_TYPE": getInt1(bgl.GL_TEXTURE_COORD_ARRAY_TYPE), 243 | "GL_TEXTURE_CUBE_MAP": getInt1(bgl.GL_TEXTURE_CUBE_MAP), 244 | "GL_TEXTURE_MATRIX": getFloat16(bgl.GL_TEXTURE_MATRIX), 245 | "GL_TEXTURE_STACK_DEPTH": getInt1(bgl.GL_TEXTURE_STACK_DEPTH), 246 | "GL_VERTEX_ARRAY": getInt1(bgl.GL_VERTEX_ARRAY), 247 | "GL_VERTEX_ARRAY_BUFFER_BINDING": getInt1(bgl.GL_VERTEX_ARRAY_BUFFER_BINDING), 248 | "GL_VERTEX_ARRAY_SIZE": getInt1(bgl.GL_VERTEX_ARRAY_SIZE), 249 | "GL_VERTEX_ARRAY_STRIDE": getInt1(bgl.GL_VERTEX_ARRAY_STRIDE), 250 | "GL_VERTEX_ARRAY_TYPE": getInt1(bgl.GL_VERTEX_ARRAY_TYPE), 251 | "GL_VERTEX_PROGRAM_POINT_SIZE": getInt1(bgl.GL_VERTEX_PROGRAM_POINT_SIZE), 252 | "GL_VERTEX_PROGRAM_TWO_SIDE": getInt1(bgl.GL_VERTEX_PROGRAM_TWO_SIDE), 253 | "GL_VIEWPORT": getInt4(bgl.GL_VIEWPORT), 254 | } 255 | 256 | for i in range(params["GL_MAX_CLIP_PLANES"]): 257 | id = "GL_CLIP_PLANE" + str(i) 258 | if (hasattr(bgl, id)): 259 | params[id] = getInt1(getattr(bgl, id)) 260 | 261 | for i in range(params["GL_MAX_LIGHTS"]): 262 | id = "GL_LIGHT" + str(i) 263 | if (hasattr(bgl, id)): 264 | params[id] = getInt1(getattr(bgl, id)) 265 | 266 | return params 267 | 268 | 269 | 270 | ## 271 | ## Helpers 272 | ## 273 | 274 | ### Checks whether or not the provided euler angles (in RADIANS) are aligned with any world axis 275 | def IsEulerOrtho(rX, rY, rZ): 276 | nRX = abs(rX) % (math.pi / 2) 277 | nRY = abs(rY) % (math.pi / 2) 278 | nRZ = abs(rZ) % (math.pi / 2) 279 | epsilon = 0.00001 # 1e-5 instead of 1e-6 to help account for quat<->euler conversion errors 280 | return (math.isclose(nRX, 0, abs_tol=epsilon) and math.isclose(nRY, 0, abs_tol=epsilon) and math.isclose(nRZ, 0, abs_tol=epsilon)) 281 | 282 | ### Gets the projected depth of a worldspace point in "window"space (because Blender exposes a so-called "perspective" matrix instead of a projection matrix) 283 | def GetPointSceneDepth(context, x, y, z): 284 | p = context.space_data.region_3d.perspective_matrix * mathutils.Vector((x, y, z, 1)) 285 | return p.w 286 | 287 | 288 | 289 | ## 290 | ## Viewport drawing 291 | ## 292 | 293 | ### Draws a simple line 294 | def DrawLine(color, start, end, width=1): 295 | bgl.glLineWidth(width) 296 | bgl.glColor4f(*color) 297 | bgl.glBegin(bgl.GL_LINES) 298 | bgl.glVertex3f(*start) 299 | bgl.glVertex3f(*end) 300 | bgl.glEnd() 301 | 302 | 303 | ### Draws a simple quad 304 | def DrawQuad(p1, p2, p3, p4, fillColor=None, outlineColor=None, outlineWidth=1): 305 | if (fillColor != None): 306 | bgl.glColor4f(*fillColor) 307 | bgl.glBegin(bgl.GL_QUADS) 308 | bgl.glVertex3f(*p1) 309 | bgl.glVertex3f(*p2) 310 | bgl.glVertex3f(*p3) 311 | bgl.glVertex3f(*p4) 312 | bgl.glEnd() 313 | if (outlineColor != None): 314 | bgl.glLineWidth(outlineWidth) 315 | bgl.glColor4f(*outlineColor) 316 | bgl.glBegin(bgl.GL_LINE_LOOP) 317 | bgl.glVertex3f(*p1) 318 | bgl.glVertex3f(*p2) 319 | bgl.glVertex3f(*p3) 320 | bgl.glVertex3f(*p4) 321 | bgl.glEnd() 322 | 323 | 324 | ### Draws a series of identical quads with a repeating constant worldspace offset from the previous quad 325 | def DrawQuadArray(p1, p2, p3, p4, offset=None, count=1, skipReference=False, fillColor=None, outlineColor=None, outlineWidth=1): 326 | # Reference quad 327 | if (skipReference != True): 328 | DrawQuad(p1, p2, p3, p4, fillColor, outlineColor, outlineWidth) 329 | # Array'd quads 330 | if (offset == None): 331 | offset = (0, 0, 0) 332 | if (count - 1 > 0): 333 | for i in range(count - 1): 334 | offX = offset[0] * (i + 1) 335 | offY = offset[1] * (i + 1) 336 | offZ = offset[2] * (i + 1) 337 | fp1 = (p1[0] + offX, p1[1] + offY, p1[2] + offZ) 338 | fp2 = (p2[0] + offX, p2[1] + offY, p2[2] + offZ) 339 | fp3 = (p3[0] + offX, p3[1] + offY, p3[2] + offZ) 340 | fp4 = (p4[0] + offX, p4[1] + offY, p4[2] + offZ) 341 | DrawQuad(fp1, fp2, fp3, fp4, fillColor, outlineColor, outlineWidth) 342 | 343 | 344 | ### Draws a simple cuboid 345 | def DrawCuboid(min, max, fillColor=None, outlineColor=None, outlineWidth=1): 346 | ### Build geometry 347 | verts = [ 348 | # 'Front' face (when looking at 0,0,0 from 100,0,0) 349 | (max[0], min[1], max[2]), # top left (as viewed with perspective projection) 350 | (max[0], max[1], max[2]), # top right 351 | (max[0], max[1], min[2]), # bottom right 352 | (max[0], min[1], min[2]), # bottom left 353 | 354 | # 'Left' face 355 | (min[0], min[1], max[2]), # top rear (same perspective pov) 356 | (max[0], min[1], max[2]), # top front 357 | (max[0], min[1], min[2]), # bottom front 358 | (min[0], min[1], min[2]), # bottom rear 359 | 360 | # 'Back' face 361 | (min[0], max[1], max[2]), # top right 362 | (min[0], min[1], max[2]), # top left 363 | (min[0], min[1], min[2]), # bottom left 364 | (min[0], max[1], min[2]), # bottom right 365 | 366 | # 'Right' face 367 | (max[0], max[1], max[2]), # top front 368 | (min[0], max[1], max[2]), # top rear 369 | (min[0], max[1], min[2]), # bottom rear 370 | (max[0], max[1], min[2]), # bottom front 371 | 372 | # 'Top' face 373 | (min[0], min[1], max[2]), # rear left 374 | (min[0], max[1], max[2]), # rear right 375 | (max[0], max[1], max[2]), # front right 376 | (max[0], min[1], max[2]), # front left 377 | 378 | # 'Bottom' face 379 | (min[0], max[1], min[2]), # rear right 380 | (min[0], min[1], min[2]), # rear left 381 | (max[0], min[1], min[2]), # front left 382 | (max[0], max[1], min[2]), # front right 383 | ] 384 | 385 | ### Draw it 386 | if (fillColor != None): 387 | bgl.glColor4f(*fillColor) 388 | bgl.glBegin(bgl.GL_QUADS) 389 | for v in verts: 390 | bgl.glVertex3f(*v) 391 | bgl.glEnd() 392 | if (outlineColor != None): 393 | bgl.glLineWidth(outlineWidth) 394 | bgl.glColor4f(*outlineColor) 395 | for i in range(int(len(verts) / 4)): 396 | bgl.glBegin(bgl.GL_LINE_LOOP) 397 | for t in range(4): 398 | bgl.glVertex3f(*verts[(i * 4) + t]) 399 | bgl.glEnd() 400 | 401 | 402 | ### Draws text in 3d with the specified worldspace location, rotation, and alignment 403 | def Draw3dText(text, color, worldpos=None, localpos=None, rotations=None, scale=None, align=1, size=24, dpi=96): 404 | blf.size(0, size, dpi) 405 | textSize = blf.dimensions(0, text) 406 | 407 | bgl.glMatrixMode(bgl.GL_MODELVIEW) 408 | bgl.glPushMatrix() 409 | 410 | if (scale != None): 411 | bgl.glScalef(*scale) 412 | 413 | if (rotations != None): 414 | for rot in rotations: 415 | bgl.glRotatef(rot[0], rot[1], rot[2], rot[3]) # stride: angle, axis (xyz) 416 | 417 | # Worldspace translation is done with bgl 418 | if (worldpos != None): 419 | bgl.glTranslatef(*worldpos) 420 | 421 | # Alignment and local translation is done with bfl 422 | lx = 0 423 | ly = 0 424 | lz = 0 425 | if (localpos != None): 426 | lx += localpos[0] 427 | ly += localpos[1] 428 | lz += localpos[2] 429 | #if (align == 1): # left align 430 | if (align == 0): # center align 431 | lx += -(textSize[0] / 2) 432 | elif (align == -1): # right align 433 | lx += -(textSize[0]) 434 | blf.position(0, lx, ly, lz) 435 | 436 | bgl.glColor4f(*color) 437 | 438 | # For some unexplained reason, blf.draw() UNsets GL_BLEND if it is enabled, which undesirable and quite frankly very stupid 439 | restoreGlBlend = bgl.glIsEnabled(bgl.GL_BLEND) 440 | blf.draw(0, text) 441 | if (restoreGlBlend): 442 | bgl.glEnable(bgl.GL_BLEND) 443 | 444 | bgl.glPopMatrix() 445 | 446 | 447 | ### Draws a fancy looking grid on a world axis aligned plane 448 | def DrawWorldPlaneGrid(self, context, fillColor, lineColor, plane, half=None): 449 | # We build the geometry in the xy plane and swizzle it for the others 450 | # And we use the user's chosen floor grid params (under Viewport -> Display) to scale the grid divisions 451 | 452 | ### Grid halves 453 | typeX = 0 # 0 = draw full grid, -1 = draw negative side only, 1 = draw positive side only 454 | typeY = 0 # ditto but for y dimen 455 | if (half == "-x"): 456 | typeX = -1 457 | typeY = 0 458 | elif (half == "+x"): 459 | typeX = 1 460 | typeY = 0 461 | elif (half == "-y"): 462 | typeX = 0 463 | typeY = -1 464 | elif (half == "+y"): 465 | typeX = 0 466 | typeY = 1 467 | 468 | ### Grid fill 469 | planeQuadSize = context.space_data.clip_end # Camera frustum farz 470 | pqMinX = -planeQuadSize 471 | pqMaxX = planeQuadSize 472 | pqMinY = -planeQuadSize 473 | pqMaxY = planeQuadSize 474 | if (typeX == -1): 475 | pqMaxX = 0 476 | if (typeX == 1): 477 | pqMinX = 0 478 | if (typeY == -1): 479 | pqMaxY = 0 480 | if (typeY == 1): 481 | pqMinY = 0 482 | planeQuad = [(pqMinX, pqMinY, 0), (pqMaxX, pqMinY, 0), (pqMaxX, pqMaxY, 0), (pqMinX, pqMaxY, 0)] 483 | 484 | ### Grid divisions 485 | 486 | # Side lengths of the grid squares 487 | bigDivSize = 100 * context.space_data.grid_scale 488 | smallDivSize = 10 * context.space_data.grid_scale 489 | 490 | bigGridLines = [] 491 | maxBigGridLines = 30 # Per pos/neg grid half (so real max total is this *2) 492 | bigGridLineCount = min(math.ceil(planeQuadSize / bigDivSize), maxBigGridLines * 2) 493 | for i in range(0, bigGridLineCount): 494 | grad = (i * bigDivSize) 495 | gridRadius = min(planeQuadSize, bigGridLineCount * bigDivSize) 496 | # Gradations along y axis, lines run along x 497 | vMinX = -gridRadius 498 | vMaxX = gridRadius 499 | if (typeX < 0): 500 | vMaxX = 0 501 | elif (typeX > 0): 502 | vMinX = 0 503 | if (typeY >= 0): 504 | bigGridLines.extend([(vMinX, grad, 0), (vMaxX, grad, 0)]) 505 | if (typeY <= 0): 506 | bigGridLines.extend([(vMinX, -grad, 0), (vMaxX, -grad, 0)]) 507 | # Gradations along x axis, lines run along y 508 | vMinY = -gridRadius 509 | vMaxY = gridRadius 510 | if (typeY < 0): 511 | vMaxY = 0 512 | elif (typeY > 0): 513 | vMinY = 0 514 | if (typeX >= 0): 515 | bigGridLines.extend([(grad, vMinY, 0), (grad, vMaxY, 0)]) 516 | if (typeX <= 0): 517 | bigGridLines.extend([(-grad, vMinY, 0), (-grad, vMaxY, 0)]) 518 | 519 | smallGridLines = [] 520 | maxSmallGridLines = maxBigGridLines * 10 521 | smallGridLineCount = min(math.ceil(planeQuadSize / smallDivSize), maxSmallGridLines * 2) 522 | for i in range(0, smallGridLineCount): 523 | grad = (i * smallDivSize) 524 | gridRadius = min(planeQuadSize, smallGridLineCount * smallDivSize) 525 | # Gradations along y axis, lines run along x 526 | vMinX = -gridRadius 527 | vMaxX = gridRadius 528 | if (typeX < 0): 529 | vMaxX = 0 530 | elif (typeX > 0): 531 | vMinX = 0 532 | if (typeY >= 0): 533 | smallGridLines.extend([(vMinX, grad, 0), (vMaxX, grad, 0)]) 534 | if (typeY <= 0): 535 | smallGridLines.extend([(vMinX, -grad, 0), (vMaxX, -grad, 0)]) 536 | # Gradations along x axis, lines run along y 537 | vMinY = -gridRadius 538 | vMaxY = gridRadius 539 | if (typeY < 0): 540 | vMaxY = 0 541 | elif (typeY > 0): 542 | vMinY = 0 543 | if (typeX >= 0): 544 | smallGridLines.extend([(grad, vMinY, 0), (grad, vMaxY, 0)]) 545 | if (typeX <= 0): 546 | smallGridLines.extend([(-grad, vMinY, 0), (-grad, vMaxY, 0)]) 547 | 548 | ### Plane switch 549 | if (plane == "xz" or plane == "zx"): 550 | # Grid fill 551 | newPlaneQuad = [] 552 | for v in planeQuad: 553 | newPlaneQuad.append((v[0], 0, v[1])) 554 | planeQuad = newPlaneQuad 555 | # Grid lines 556 | newBigGridLines = [] 557 | newSmallGridLines = [] 558 | for v in bigGridLines: 559 | newBigGridLines.append((v[0], 0, v[1])) 560 | for v in smallGridLines: 561 | newSmallGridLines.append((v[0], 0, v[1])) 562 | bigGridLines = newBigGridLines 563 | smallGridLines = newSmallGridLines 564 | elif (plane == "yz" or plane == "zy"): 565 | # Grid fill 566 | newPlaneQuad = [] 567 | for v in planeQuad: 568 | newPlaneQuad.append((0, v[0], v[1])) 569 | planeQuad = newPlaneQuad 570 | # Grid lines 571 | newBigGridLines = [] 572 | newSmallGridLines = [] 573 | for v in bigGridLines: 574 | newBigGridLines.append((0, v[0], v[1])) 575 | for v in smallGridLines: 576 | newSmallGridLines.append((0, v[0], v[1])) 577 | bigGridLines = newBigGridLines 578 | smallGridLines = newSmallGridLines 579 | 580 | ### Draw it 581 | # Fill 582 | if (fillColor != None): 583 | DrawQuad(planeQuad[0], planeQuad[1], planeQuad[2], planeQuad[3], fillColor) 584 | 585 | # Big grid divisions 586 | bgl.glLineWidth(4) 587 | bgl.glColor4f(*lineColor) 588 | bgl.glBegin(bgl.GL_LINES) 589 | for line in bigGridLines: 590 | bgl.glVertex3f(*line) 591 | bgl.glEnd() 592 | 593 | # Small grid divisions 594 | lineColor2 = (lineColor[0], lineColor[1], lineColor[2], lineColor[3] * 0.5) 595 | bgl.glLineWidth(2) 596 | bgl.glColor4f(*lineColor) 597 | bgl.glBegin(bgl.GL_LINES) 598 | for line in smallGridLines: 599 | bgl.glVertex3f(*line) 600 | bgl.glEnd() 601 | 602 | 603 | ### Applies default* opengl drawing parameters 604 | #*default = what Blender's defaults** are 605 | #**which may or may not be wrong as the docs do not provide these, so they are figured through reasonable assumptions and empiric testing 606 | def OglDefaults(): 607 | bgl.glDisable(bgl.GL_BLEND) 608 | 609 | bgl.glDisable(bgl.GL_LINE_SMOOTH) 610 | bgl.glLineWidth(1) 611 | 612 | bgl.glEnable(bgl.GL_DEPTH_TEST) 613 | bgl.glDepthMask(True) 614 | 615 | bgl.glDisable(bgl.GL_CULL_FACE) 616 | bgl.glCullFace(bgl.GL_BACK) 617 | 618 | bgl.glColor4f(0.0, 0.0, 0.0, 1.0) 619 | 620 | 621 | ## 622 | ## Main 3d draw callback 623 | ## 624 | 625 | def ViewportDraw(self, context): 626 | scene = context.scene 627 | properties = scene.shape_key_tools_props 628 | 629 | OglDefaults() 630 | 631 | bgl.glEnable(bgl.GL_BLEND) 632 | bgl.glEnable(bgl.GL_LINE_SMOOTH) 633 | 634 | 635 | ## 636 | ## Split/merge axis visualization 637 | ## 638 | 639 | gridFillColorSplit = (1.0, 0.9, 0.0, 0.2) 640 | gridLineColorSplit = (1.0, 0.9, 0.0, 0.3) 641 | 642 | gridFillColorLeft = (0.0, 1.0, 0.0, 0.2) 643 | gridLineColorLeft = (0.0, 1.0, 0.0, 0.25) 644 | 645 | gridFillColorRight = (1.0, 0.0, 0.0, 0.2) 646 | gridLineColorRight = (1.0, 0.0, 0.0, 0.25) 647 | 648 | gridLabelTextSize = 26 649 | 650 | smoothRegionFillColor = (0.0, 0.5, 1.0, 0.2) 651 | smoothRegionLineColor = (0.0, 0.5, 1.0, 0.3) 652 | 653 | smoothRegionIntervalFillColor = (0.0, 0.2, 1.0, 0.15) 654 | smoothRegionIntervalLineColor = (0.0, 0.2, 1.0, 0.35) 655 | 656 | splitAxis = properties.opt_shapepairs_split_axis 657 | splitMode = properties.opt_shapepairs_split_mode 658 | smoothingRadius = properties.opt_shapepairs_split_smoothdist 659 | 660 | drawSplitPlane = properties.opt_shapepairs_splitmerge_viewportvisualize_show_splitplane 661 | drawLRPlanes = properties.opt_shapepairs_splitmerge_viewportvisualize_show_splithalves 662 | drawSmoothRegion = properties.opt_shapepairs_splitmerge_viewportvisualize_show_smoothregion and splitMode == "smooth" 663 | 664 | viewportProjType = context.space_data.region_3d.view_perspective #https://blender.stackexchange.com/questions/181110/what-is-the-python-command-to-check-if-current-view-is-in-orthographic-or-perspe 665 | viewportIsOrtho = (viewportProjType == "ORTHO") 666 | 667 | # Because everything is translucent, we will read depth only (no write) and be very particular with the draw order 668 | # For this reason, all the geometry is split in two at 0,0,0 and we draw the halves with a basic depth sort 669 | 670 | def DrawSplitPlane(side): 671 | if (side == "pos"): 672 | if (splitAxis == "+X" or splitAxis == "-X"): 673 | DrawWorldPlaneGrid(self, context, gridFillColorSplit, gridLineColorSplit, "yz", "+x") 674 | elif (splitAxis == "+Y" or splitAxis == "-Y"): 675 | DrawWorldPlaneGrid(self, context, gridFillColorSplit, gridLineColorSplit, "xz", "+x") 676 | elif (splitAxis == "+Z" or splitAxis == "-Z"): 677 | DrawWorldPlaneGrid(self, context, gridFillColorSplit, gridLineColorSplit, "xy", "+x") 678 | elif (side == "neg"): 679 | if (splitAxis == "+X" or splitAxis == "-X"): 680 | DrawWorldPlaneGrid(self, context, gridFillColorSplit, gridLineColorSplit, "yz", "-x") 681 | elif (splitAxis == "+Y" or splitAxis == "-Y"): 682 | DrawWorldPlaneGrid(self, context, gridFillColorSplit, gridLineColorSplit, "xz", "-x") 683 | elif (splitAxis == "+Z" or splitAxis == "-Z"): 684 | DrawWorldPlaneGrid(self, context, gridFillColorSplit, gridLineColorSplit, "xy", "-x") 685 | 686 | def DrawSmoothingRegion(side): 687 | cuboidDepth = context.space_data.clip_end # Camera frustum farz 688 | intervalOffset = 3 * context.space_data.grid_scale 689 | intervalCount = 200 690 | 691 | # If the view is orthographic AND the camera is orthogonal, we can draw a simpler & better visualization of the smoothing cuboid region 692 | headOnView = False 693 | if (viewportIsOrtho): 694 | camAngles = context.space_data.region_3d.view_rotation.to_euler("XYZ") # RADIANS 695 | if (IsEulerOrtho(camAngles.x, camAngles.y, camAngles.z)): 696 | headOnView = True 697 | 698 | if (splitAxis == "+X" or splitAxis == "-X"): 699 | if (headOnView): 700 | # Main fill 701 | cuboidDepth *= 0.49 # In order to make the faces parallel with the ortho plane actually show up when viewed head on 702 | if (side == "pos"): 703 | DrawCuboid(min=(-smoothingRadius, 0, -cuboidDepth), max=(smoothingRadius, cuboidDepth, cuboidDepth), fillColor=smoothRegionIntervalFillColor, outlineColor=smoothRegionIntervalLineColor, outlineWidth=1) 704 | elif (side == "neg"): 705 | DrawCuboid(min=(-smoothingRadius, -cuboidDepth, -cuboidDepth), max=(smoothingRadius, 0, cuboidDepth), fillColor=smoothRegionIntervalFillColor, outlineColor=smoothRegionIntervalLineColor, outlineWidth=1) 706 | else: 707 | # Main fill 708 | if (side == "pos"): 709 | DrawCuboid(min=(-smoothingRadius, 0, -cuboidDepth), max=(smoothingRadius, cuboidDepth, cuboidDepth), fillColor=smoothRegionFillColor, outlineColor=smoothRegionLineColor, outlineWidth=1) 710 | elif (side == "neg"): 711 | DrawCuboid(min=(-smoothingRadius, -cuboidDepth, -cuboidDepth), max=(smoothingRadius, 0, cuboidDepth), fillColor=smoothRegionFillColor, outlineColor=smoothRegionLineColor, outlineWidth=1) 712 | # Longitude intervals 713 | tl = (-smoothingRadius, 0, cuboidDepth) 714 | tr = (smoothingRadius, 0, cuboidDepth) 715 | br = (smoothingRadius, 0, -cuboidDepth) 716 | bl = (-smoothingRadius, 0, -cuboidDepth) 717 | if (side == "pos"): 718 | DrawQuadArray(tl, tr, br, bl, offset=(0, intervalOffset, 0), count=intervalCount, fillColor=smoothRegionIntervalFillColor, outlineColor=smoothRegionIntervalLineColor, outlineWidth=1) 719 | elif (side == "neg"): 720 | DrawQuadArray(tl, tr, br, bl, offset=(0, -intervalOffset, 0), count=intervalCount, fillColor=smoothRegionIntervalFillColor, outlineColor=smoothRegionIntervalLineColor, outlineWidth=1) 721 | # Latitude intervals 722 | if (side == "neg"): 723 | tl = (-smoothingRadius, 0, 0) 724 | tr = (smoothingRadius, 0, 0) 725 | br = (smoothingRadius, -cuboidDepth, 0) 726 | bl = (-smoothingRadius, -cuboidDepth, 0) 727 | elif (side == "pos"): 728 | tl = (-smoothingRadius, cuboidDepth, 0) 729 | tr = (smoothingRadius, cuboidDepth, 0) 730 | br = (smoothingRadius, 0, 0) 731 | bl = (-smoothingRadius, 0, 0) 732 | DrawQuadArray(tl, tr, br, bl, offset=(0, 0, -intervalOffset), count=intervalCount, fillColor=smoothRegionIntervalFillColor, outlineColor=smoothRegionIntervalLineColor, outlineWidth=1) 733 | DrawQuadArray(tl, tr, br, bl, offset=(0, 0, intervalOffset), count=intervalCount, fillColor=smoothRegionIntervalFillColor, outlineColor=smoothRegionIntervalLineColor, outlineWidth=1) 734 | elif (splitAxis == "+Y" or splitAxis == "-Y"): 735 | if (headOnView): 736 | # Main fill 737 | cuboidDepth *= 0.49 # In order to make the faces parallel with the ortho plane actually show up when viewed head on 738 | if (side == "pos"): 739 | DrawCuboid(min=(0, -smoothingRadius, -cuboidDepth), max=(cuboidDepth, smoothingRadius, cuboidDepth), fillColor=smoothRegionIntervalFillColor, outlineColor=smoothRegionIntervalLineColor, outlineWidth=1) 740 | elif (side == "neg"): 741 | DrawCuboid(min=(-cuboidDepth, -smoothingRadius, -cuboidDepth), max=(0, smoothingRadius, cuboidDepth), fillColor=smoothRegionIntervalFillColor, outlineColor=smoothRegionIntervalLineColor, outlineWidth=1) 742 | else: 743 | # Main fill 744 | if (side == "pos"): 745 | DrawCuboid(min=(0, -smoothingRadius, -cuboidDepth), max=(cuboidDepth, smoothingRadius, cuboidDepth), fillColor=smoothRegionFillColor, outlineColor=smoothRegionLineColor, outlineWidth=1) 746 | elif (side == "neg"): 747 | DrawCuboid(min=(-cuboidDepth, -smoothingRadius, -cuboidDepth), max=(0, smoothingRadius, cuboidDepth), fillColor=smoothRegionFillColor, outlineColor=smoothRegionLineColor, outlineWidth=1) 748 | # Longitude intervals 749 | tl = (0, -smoothingRadius, cuboidDepth) 750 | tr = (0, smoothingRadius, cuboidDepth) 751 | br = (0, smoothingRadius, -cuboidDepth) 752 | bl = (0, -smoothingRadius, -cuboidDepth) 753 | if (side == "pos"): 754 | DrawQuadArray(tl, tr, br, bl, offset=(intervalOffset, 0, 0), count=intervalCount, fillColor=smoothRegionIntervalFillColor, outlineColor=smoothRegionIntervalLineColor, outlineWidth=1) 755 | if (side == "neg"): 756 | DrawQuadArray(tl, tr, br, bl, offset=(-intervalOffset, 0, 0), count=intervalCount, fillColor=smoothRegionIntervalFillColor, outlineColor=smoothRegionIntervalLineColor, outlineWidth=1) 757 | # Latitude intervals 758 | if (side == "neg"): 759 | tl = (0, -smoothingRadius, 0) 760 | tr = (0, smoothingRadius, 0) 761 | br = (-cuboidDepth, smoothingRadius, 0) 762 | bl = ( -cuboidDepth, -smoothingRadius, 0) 763 | elif (side == "pos"): 764 | tl = (cuboidDepth, -smoothingRadius, 0) 765 | tr = (cuboidDepth, smoothingRadius, 0) 766 | br = (0, smoothingRadius, 0) 767 | bl = (0, -smoothingRadius, 0) 768 | DrawQuadArray(tl, tr, br, bl, offset=(0, 0, -intervalOffset), count=intervalCount, fillColor=smoothRegionIntervalFillColor, outlineColor=smoothRegionIntervalLineColor, outlineWidth=1) 769 | DrawQuadArray(tl, tr, br, bl, offset=(0, 0, intervalOffset), count=intervalCount, fillColor=smoothRegionIntervalFillColor, outlineColor=smoothRegionIntervalLineColor, outlineWidth=1) 770 | elif (splitAxis == "+Z" or splitAxis == "-Z"): 771 | if (headOnView): 772 | # Main fill 773 | cuboidDepth *= 0.49 # In order to make the faces parallel with the ortho plane actually show up when viewed head on 774 | if (side == "pos"): 775 | DrawCuboid(min=(0, -cuboidDepth, -smoothingRadius), max=(cuboidDepth, cuboidDepth, smoothingRadius), fillColor=smoothRegionIntervalFillColor, outlineColor=smoothRegionIntervalLineColor, outlineWidth=1) 776 | elif (side == "neg"): 777 | DrawCuboid(min=(-cuboidDepth, -cuboidDepth, -smoothingRadius), max=(0, cuboidDepth, smoothingRadius), fillColor=smoothRegionIntervalFillColor, outlineColor=smoothRegionIntervalLineColor, outlineWidth=1) 778 | else: 779 | # Main fill 780 | if (side == "pos"): 781 | DrawCuboid(min=(0, -cuboidDepth, -smoothingRadius), max=(cuboidDepth, cuboidDepth, smoothingRadius), fillColor=smoothRegionFillColor, outlineColor=smoothRegionLineColor, outlineWidth=1) 782 | elif (side == "neg"): 783 | DrawCuboid(min=(-cuboidDepth, -cuboidDepth, -smoothingRadius), max=(0, cuboidDepth, smoothingRadius), fillColor=smoothRegionFillColor, outlineColor=smoothRegionLineColor, outlineWidth=1) 784 | # Longitude intervals 785 | tl = (0, cuboidDepth, -smoothingRadius) 786 | tr = (0, cuboidDepth, smoothingRadius) 787 | br = (0, -cuboidDepth, smoothingRadius) 788 | bl = (0, -cuboidDepth, -smoothingRadius) 789 | if (side == "neg"): 790 | DrawQuadArray(tl, tr, br, bl, offset=(-intervalOffset, 0, 0), count=intervalCount, fillColor=smoothRegionIntervalFillColor, outlineColor=smoothRegionIntervalLineColor, outlineWidth=1) 791 | elif (side == "pos"): 792 | DrawQuadArray(tl, tr, br, bl, offset=(intervalOffset, 0, 0), count=intervalCount, fillColor=smoothRegionIntervalFillColor, outlineColor=smoothRegionIntervalLineColor, outlineWidth=1) 793 | # Latitude intervals 794 | if (side == "neg"): 795 | tl = (0, 0, -smoothingRadius) 796 | tr = (0, 0, smoothingRadius) 797 | br = (-cuboidDepth, 0, smoothingRadius) 798 | bl = (-cuboidDepth, 0, -smoothingRadius) 799 | elif (side == "pos"): 800 | tl = (cuboidDepth, 0, -smoothingRadius) 801 | tr = (cuboidDepth, 0, smoothingRadius) 802 | br = (0, 0, smoothingRadius) 803 | bl = (0, 0, -smoothingRadius) 804 | DrawQuadArray(tl, tr, br, bl, offset=(0, -intervalOffset, 0), count=intervalCount, fillColor=smoothRegionIntervalFillColor, outlineColor=smoothRegionIntervalLineColor, outlineWidth=1) 805 | DrawQuadArray(tl, tr, br, bl, offset=(0, intervalOffset, 0), count=intervalCount, fillColor=smoothRegionIntervalFillColor, outlineColor=smoothRegionIntervalLineColor, outlineWidth=1) 806 | 807 | def DrawLeftPlane(): 808 | if (splitAxis == "+X"): 809 | DrawWorldPlaneGrid(self, context, gridFillColorLeft, gridLineColorLeft, "xz", "+x") 810 | elif (splitAxis == "-X"): 811 | DrawWorldPlaneGrid(self, context, gridFillColorLeft, gridLineColorLeft, "xz", "-x") 812 | elif (splitAxis == "+Y"): 813 | DrawWorldPlaneGrid(self, context, gridFillColorLeft, gridLineColorLeft, "yz", "+x") 814 | elif (splitAxis == "-Y"): 815 | DrawWorldPlaneGrid(self, context, gridFillColorLeft, gridLineColorLeft, "yz", "-x") 816 | elif (splitAxis == "+Z"): 817 | DrawWorldPlaneGrid(self, context, gridFillColorLeft, gridLineColorLeft, "zy", "+y") 818 | elif (splitAxis == "-Z"): 819 | DrawWorldPlaneGrid(self, context, gridFillColorLeft, gridLineColorLeft, "zy", "-y") 820 | 821 | def DrawRightPlane(): 822 | if (splitAxis == "+X"): 823 | DrawWorldPlaneGrid(self, context, gridFillColorRight, gridLineColorRight, "xz", "-x") 824 | elif (splitAxis == "-X"): 825 | DrawWorldPlaneGrid(self, context, gridFillColorRight, gridLineColorRight, "xz", "+x") 826 | elif (splitAxis == "+Y"): 827 | DrawWorldPlaneGrid(self, context, gridFillColorRight, gridLineColorRight, "yz", "-x") 828 | elif (splitAxis == "-Y"): 829 | DrawWorldPlaneGrid(self, context, gridFillColorRight, gridLineColorRight, "yz", "+x") 830 | elif (splitAxis == "+Z"): 831 | DrawWorldPlaneGrid(self, context, gridFillColorRight, gridLineColorRight, "zy", "-y") 832 | elif (splitAxis == "-Z"): 833 | DrawWorldPlaneGrid(self, context, gridFillColorRight, gridLineColorRight, "zy", "+y") 834 | 835 | def DrawLeftText(): 836 | bgl.glEnable(bgl.GL_CULL_FACE) 837 | if (splitAxis == "+X"): 838 | Draw3dText("Left", color=gridLineColorLeft, localpos=(5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(90, 1, 0, 0)], scale=None, align=1, size=gridLabelTextSize) 839 | Draw3dText("Left", color=gridLineColorLeft, localpos=(-5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(90, 1, 0, 0), (180, 0, 1, 0)], scale=None, align=-1, size=gridLabelTextSize) 840 | elif (splitAxis == "-X"): 841 | Draw3dText("Left", color=gridLineColorLeft, localpos=(-5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(90, 1, 0, 0)], scale=None, align=-1, size=gridLabelTextSize) 842 | Draw3dText("Left", color=gridLineColorLeft, localpos=(5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(90, 1, 0, 0), (180, 0, 1, 0)], scale=None, align=1, size=gridLabelTextSize) 843 | elif (splitAxis == "+Y"): 844 | Draw3dText("Left", color=gridLineColorLeft, localpos=(5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(90, 1, 0, 0), (90, 0, 1, 0)], scale=None, align=1, size=gridLabelTextSize) 845 | Draw3dText("Left", color=gridLineColorLeft, localpos=(-5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(90, 1, 0, 0), (90, 0, 1, 0), (180, 0, 1, 0)], scale=None, align=-1, size=gridLabelTextSize) 846 | elif (splitAxis == "-Y"): 847 | Draw3dText("Left", color=gridLineColorLeft, localpos=(-5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(90, 1, 0, 0), (90, 0, 1, 0)], scale=None, align=-1, size=gridLabelTextSize) 848 | Draw3dText("Left", color=gridLineColorLeft, localpos=(5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(90, 1, 0, 0), (90, 0, 1, 0), (180, 0, 1, 0)], scale=None, align=1, size=gridLabelTextSize) 849 | elif (splitAxis == "+Z"): 850 | Draw3dText("Left", color=gridLineColorLeft, localpos=(5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(-90, 0, 1, 0)], scale=None, align=1, size=gridLabelTextSize) 851 | Draw3dText("Left", color=gridLineColorLeft, localpos=(-5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(-90, 0, 1, 0), (180, 0, 1, 0)], scale=None, align=-1, size=gridLabelTextSize) 852 | elif (splitAxis == "-Z"): 853 | Draw3dText("Left", color=gridLineColorLeft, localpos=(-5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(-90, 0, 1, 0)], scale=None, align=-1, size=gridLabelTextSize) 854 | Draw3dText("Left", color=gridLineColorLeft, localpos=(5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(-90, 0, 1, 0), (180, 0, 1, 0)], scale=None, align=1, size=gridLabelTextSize) 855 | bgl.glDisable(bgl.GL_CULL_FACE) 856 | 857 | def DrawRightText(): 858 | bgl.glEnable(bgl.GL_CULL_FACE) 859 | if (splitAxis == "+X"): 860 | Draw3dText("Right", color=gridLineColorRight, localpos=(-5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(90, 1, 0, 0)], scale=None, align=-1, size=gridLabelTextSize) 861 | Draw3dText("Right", color=gridLineColorRight, localpos=(5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(90, 1, 0, 0), (180, 0, 1, 0)], scale=None, align=1, size=gridLabelTextSize) 862 | elif (splitAxis == "-X"): 863 | Draw3dText("Right", color=gridLineColorRight, localpos=(5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(90, 1, 0, 0)], scale=None, align=1, size=gridLabelTextSize) 864 | Draw3dText("Right", color=gridLineColorRight, localpos=(-5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(90, 1, 0, 0), (180, 0, 1, 0)], scale=None, align=-1, size=gridLabelTextSize) 865 | elif (splitAxis == "+Y"): 866 | Draw3dText("Right", color=gridLineColorRight, localpos=(-5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(90, 1, 0, 0), (90, 0, 1, 0)], scale=None, align=-1, size=gridLabelTextSize) 867 | Draw3dText("Right", color=gridLineColorRight, localpos=(5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(90, 1, 0, 0), (90, 0, 1, 0), (180, 0, 1, 0)], scale=None, align=1, size=gridLabelTextSize) 868 | elif (splitAxis == "-Y"): 869 | Draw3dText("Right", color=gridLineColorRight, localpos=(5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(90, 1, 0, 0), (90, 0, 1, 0)], scale=None, align=1, size=gridLabelTextSize) 870 | Draw3dText("Right", color=gridLineColorRight, localpos=(-5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(90, 1, 0, 0), (90, 0, 1, 0), (180, 0, 1, 0)], scale=None, align=-1, size=gridLabelTextSize) 871 | elif (splitAxis == "+Z"): 872 | Draw3dText("Right", color=gridLineColorRight, localpos=(-5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(-90, 0, 1, 0)], scale=None, align=-1, size=gridLabelTextSize) 873 | Draw3dText("Right", color=gridLineColorRight, localpos=(5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(-90, 0, 1, 0), (180, 0, 1, 0)], scale=None, align=1, size=gridLabelTextSize) 874 | elif (splitAxis == "-Z"): 875 | Draw3dText("Right", color=gridLineColorRight, localpos=(5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(-90, 0, 1, 0)], scale=None, align=1, size=gridLabelTextSize) 876 | Draw3dText("Right", color=gridLineColorRight, localpos=(-5, 5, 0), worldpos=(0, 0, 0.1), rotations=[(-90, 0, 1, 0), (180, 0, 1, 0)], scale=None, align=-1, size=gridLabelTextSize) 877 | bgl.glDisable(bgl.GL_CULL_FACE) 878 | 879 | ### Use axis depths to sort the geometry 880 | dTestDist = 0.1 881 | dNegX = (GetPointSceneDepth(context, -dTestDist, 0, 0), 1) # tuple with dummy 2nd value for unique hashing purposes 882 | dNegY = (GetPointSceneDepth(context, 0, -dTestDist, 0), 2) 883 | dNegZ = (GetPointSceneDepth(context, 0, 0, -dTestDist), 3) 884 | dPosX = (GetPointSceneDepth(context, dTestDist, 0, 0), 4) 885 | dPosY = (GetPointSceneDepth(context, 0, dTestDist, 0), 5) 886 | dPosZ = (GetPointSceneDepth(context, 0, 0, dTestDist), 6) 887 | 888 | ### Bind the draw ops to each axis direction 889 | # This is ugly as hell and the nested methods are significantly unperformant, but it makes this whole thing a lot more readable than something optimized for rendering 890 | drawOps = {} 891 | 892 | if (splitAxis == "+X" or splitAxis == "-X"): 893 | def nx(): 894 | if (drawLRPlanes): 895 | if (splitAxis == "+X"): 896 | DrawRightPlane() 897 | DrawRightText() 898 | elif (splitAxis == "-X"): 899 | DrawLeftPlane() 900 | DrawLeftText() 901 | drawOps[dNegX] = nx 902 | def px(): 903 | if (drawLRPlanes): 904 | if (splitAxis == "+X"): 905 | DrawLeftPlane() 906 | DrawLeftText() 907 | elif (splitAxis == "-X"): 908 | DrawRightPlane() 909 | DrawRightText() 910 | drawOps[dPosX] = px 911 | def ny(): 912 | if (drawSplitPlane): 913 | DrawSplitPlane(side="neg") 914 | if (drawSmoothRegion): 915 | DrawSmoothingRegion(side="neg") 916 | drawOps[dNegY] = ny 917 | def py(): 918 | if (drawSplitPlane): 919 | DrawSplitPlane(side="pos") 920 | if (drawSmoothRegion): 921 | DrawSmoothingRegion(side="pos") 922 | drawOps[dPosY] = py 923 | drawOps[dNegZ] = None 924 | drawOps[dPosZ] = None 925 | 926 | elif (splitAxis == "+Y" or splitAxis == "-Y"): 927 | def nx(): 928 | if (drawSplitPlane): 929 | DrawSplitPlane(side="neg") 930 | if (drawSmoothRegion): 931 | DrawSmoothingRegion(side="neg") 932 | drawOps[dNegX] = nx 933 | def px(): 934 | if (drawSplitPlane): 935 | DrawSplitPlane(side="pos") 936 | if (drawSmoothRegion): 937 | DrawSmoothingRegion(side="pos") 938 | drawOps[dPosX] = px 939 | def ny(): 940 | if (drawLRPlanes): 941 | if (splitAxis == "+Y"): 942 | DrawRightPlane() 943 | DrawRightText() 944 | elif (splitAxis == "-Y"): 945 | DrawLeftPlane() 946 | DrawLeftText() 947 | drawOps[dNegY] = ny 948 | def py(): 949 | if (drawLRPlanes): 950 | if (splitAxis == "+Y"): 951 | DrawLeftPlane() 952 | DrawLeftText() 953 | elif (splitAxis == "-Y"): 954 | DrawRightPlane() 955 | DrawRightText() 956 | drawOps[dPosY] = py 957 | drawOps[dNegZ] = None 958 | drawOps[dPosZ] = None 959 | 960 | elif (splitAxis == "+Z" or splitAxis == "-Z"): 961 | def nx(): 962 | if (drawSplitPlane): 963 | DrawSplitPlane(side="neg") 964 | if (drawSmoothRegion): 965 | DrawSmoothingRegion(side="neg") 966 | drawOps[dNegX] = nx 967 | def px(): 968 | if (drawSplitPlane): 969 | DrawSplitPlane(side="pos") 970 | if (drawSmoothRegion): 971 | DrawSmoothingRegion(side="pos") 972 | drawOps[dPosX] = px 973 | drawOps[dNegY] = None 974 | drawOps[dPosY] = None 975 | def nz(): 976 | if (drawLRPlanes): 977 | if (splitAxis == "+Z"): 978 | DrawRightPlane() 979 | DrawRightText() 980 | elif (splitAxis == "-Z"): 981 | DrawLeftPlane() 982 | DrawLeftText() 983 | drawOps[dNegZ] = nz 984 | def pz(): 985 | if (drawLRPlanes): 986 | if (splitAxis == "+Z"): 987 | DrawLeftPlane() 988 | DrawLeftText() 989 | elif (splitAxis == "-Z"): 990 | DrawRightPlane() 991 | DrawRightText() 992 | drawOps[dPosZ] = pz 993 | 994 | ### Sort and draw 995 | bgl.glDepthMask(False) 996 | 997 | depthOrder = list(drawOps.keys()) 998 | depthOrder.sort(key=lambda x: x[0], reverse=True) 999 | for depth in depthOrder: 1000 | op = drawOps[depth] 1001 | if (op != None): 1002 | op() 1003 | 1004 | bgl.glDepthMask(True) 1005 | 1006 | 1007 | ### Done with all drawing, restore defaults 1008 | OglDefaults() 1009 | 1010 | 1011 | ## 1012 | ## Main operator 1013 | ## 1014 | 1015 | ### Returns true if any of multiple properties require this operator to draw something 1016 | def ShouldDrawAnything(context): 1017 | scene = context.scene 1018 | properties = scene.shape_key_tools_props 1019 | 1020 | # Main addon panel poll check 1021 | if (bpy.types.OBJECT_PT_shape_key_tools_panel.poll(context) == False): 1022 | return False 1023 | 1024 | # Specific property checks that enable the viewport visuals 1025 | return (properties.opt_shapepairs_splitmerge_viewportvisualize) 1026 | 1027 | 1028 | ### Sets to false all properties which would cause ShouldDrawAnything() to return true 1029 | def UnsetDrawingEnablerProperties(context): 1030 | scene = context.scene 1031 | properties = scene.shape_key_tools_props 1032 | 1033 | properties.opt_shapepairs_splitmerge_viewportvisualize = False 1034 | 1035 | 1036 | ### Removes the drawing callback and considers the operator as disabled 1037 | # Param cls must be the __class__ object 1038 | def Disable(cls): 1039 | if (cls.DrawingHandle3d != None): 1040 | bpy.types.SpaceView3D.draw_handler_remove(cls.DrawingHandle3d, "WINDOW") 1041 | cls.DrawingHandle3d = None 1042 | cls.IsRunning = False 1043 | 1044 | 1045 | ### bpy operator 1046 | class VIEW_3D_OT_ShapeKeyTools_ViewportVisuals(bpy.types.Operator): 1047 | bl_idname = "view3d.shape_key_tools_viewport_visuals" 1048 | bl_label = "Shape Key Tools Viewport Visuals" 1049 | bl_options = {'INTERNAL'} 1050 | 1051 | ## Statics 1052 | IsRunning = False # True when an instance of this operator is running 1053 | DrawingHandle3d = None # bpy handle for the 3d viewport drawing callback 1054 | 1055 | 1056 | ### Hook for when the current blend file is closing 1057 | def BlendFilePreLoadWatcher(self, context): 1058 | try: 1059 | Disable(self.__class__) 1060 | except: 1061 | pass 1062 | 1063 | 1064 | def RemoveModalTimer(self, context): 1065 | if (self._Timer != None): 1066 | context.window_manager.event_timer_remove(self._Timer) 1067 | self._Timer = None 1068 | 1069 | 1070 | def modal(self, context, event): 1071 | if (self.__class__.IsRunning and ShouldDrawAnything(context)): 1072 | context.area.tag_redraw() # Ensure viewport refreshes 1073 | return {'PASS_THROUGH'} 1074 | else: 1075 | self.RemoveModalTimer(context) 1076 | Disable(self.__class__) 1077 | context.area.tag_redraw() # Ensure viewport refreshes 1078 | return {'CANCELLED'} 1079 | 1080 | 1081 | def execute(self, context): 1082 | if (context.area.type == "VIEW_3D"): 1083 | # Setup 3D drawing callback for the viewport 1084 | self.__class__.DrawingHandle3d = bpy.types.SpaceView3D.draw_handler_add(ViewportDraw, (self, context), "WINDOW", "POST_VIEW") 1085 | 1086 | # Opening a different blend file will stop this op before modal() has a chance to notice the blend file has changed 1087 | # So we need to watch for that and clean up the drawing callback as needed 1088 | bpy.app.handlers.load_pre.append(self.BlendFilePreLoadWatcher) 1089 | 1090 | self.__class__.IsRunning = True 1091 | 1092 | context.window_manager.modal_handler_add(self) 1093 | self._Timer = context.window_manager.event_timer_add(0.017, context.window) # This shouldn't be necessary, but it prevents weird UI behavior from occurring 1094 | # If this timer is not set: if the user clicks a control (like a checkbox) to disable the operator, then the entire UI will ignore mouse interaction until the user left clicks once anywhere in the window 1095 | # Specifially, the left mouse button is stuck in the down state during this time. Moving the cursor over things like checkboxes will uncheck them. 1096 | 1097 | return {'RUNNING_MODAL'} 1098 | else: 1099 | # If the viewport isn't available, disable the Properties which enable this operator 1100 | print("Viewport not available. Viewport visualization options have been disabled.") 1101 | UnsetDrawingEnablerProperties(context) 1102 | return {'CANCELLED'} 1103 | 1104 | 1105 | def register(): 1106 | bpy.utils.register_class(VIEW_3D_OT_ShapeKeyTools_ViewportVisuals) 1107 | return VIEW_3D_OT_ShapeKeyTools_ViewportVisuals 1108 | 1109 | def unregister(): 1110 | Disable(VIEW_3D_OT_ShapeKeyTools_ViewportVisuals) 1111 | bpy.utils.unregister_class(VIEW_3D_OT_ShapeKeyTools_ViewportVisuals) 1112 | return VIEW_3D_OT_ShapeKeyTools_ViewportVisuals 1113 | 1114 | if __name__ == "__main__": 1115 | register() 1116 | -------------------------------------------------------------------------------- /shape_key_tools/ops/pairs_merge_active.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | import bpy 4 | from bpy.props import * 5 | 6 | from shape_key_tools import common 7 | 8 | 9 | class WM_OT_ShapeKeyTools_OpMergeActive(bpy.types.Operator): 10 | bl_idname = "wm.shape_key_tools_smartmerge_active" 11 | bl_label = "Smart Merge Active Shape Key" 12 | bl_description = "Merges the active mesh's active shape key with its counterpart pair shape key. E.g. MyShapeKeyL will be merged with MyShapeKeyR into a single shape key named MyShapeKeyL+MyShapeKeyR. This operation does NOT use the Vertex Filter!" 13 | bl_options = {"UNDO"} 14 | 15 | 16 | def validate(self, context): 17 | # This op requires an active object 18 | if (context.object == None or hasattr(context, "object") == False): 19 | return (False, "No object is selected.") 20 | 21 | obj = context.object 22 | 23 | # Object must be a mesh 24 | if (obj.type != "MESH"): 25 | return (False, "The active object ('" + obj.name + "', type: " + obj.type + ") is not a mesh.") 26 | 27 | # Object must have enough shape keys 28 | if (not hasattr(obj.data.shape_keys, "key_blocks") or len(obj.data.shape_keys.key_blocks.keys()) <= 1): 29 | return (False, "The active object must have at least 1 shape key (excluding the basis shape key).") 30 | 31 | # Active shape key cannot be the basis shape key 32 | activeShapeKeyIndex = obj.data.shape_keys.key_blocks.keys().index(obj.active_shape_key.name) 33 | if (activeShapeKeyIndex == 0): 34 | return (False, "You cannot split the basis shape key.") 35 | 36 | # The active shape key's name must adhere to the MyShapeKeyL MyShapeKeyR naming convention 37 | (firstShapeKey, expectedCompShapeKeyName, mergedShapeKeyName) = common.FindShapeKeyMergeNames(obj.active_shape_key.name) 38 | if (expectedCompShapeKeyName == None): 39 | return (False, "The active shape key does not follow the MyShapeKeyL MyShapeKeyR naming convention. This operation only works on shape keys that end in L or R.") 40 | 41 | # A complementary shape key must exist 42 | if (not expectedCompShapeKeyName in obj.data.shape_keys.key_blocks.keys()): 43 | return (False, "No complementary shape key named '" + expectedCompShapeKeyName + "' exists.") 44 | 45 | return (True, None) 46 | 47 | def validateUser(self, context): 48 | (isValid, invalidReason) = self.validate(context) 49 | if (isValid): 50 | return True 51 | else: 52 | if self: 53 | self.report({'ERROR'}, invalidReason) 54 | return False 55 | 56 | @classmethod 57 | def poll(cls, context): 58 | (isValid, invalidReason) = cls.validate(None, context) 59 | return isValid 60 | 61 | 62 | def execute(self, context): 63 | scene = context.scene 64 | properties = scene.shape_key_tools_props 65 | 66 | if (self.validateUser(context) == False): 67 | return {'FINISHED'} 68 | 69 | obj = context.object 70 | 71 | # Find the name of the complementary shape key and the name of the to-be-merged shape key 72 | (firstShapeKey, expectedCompShapeKey, mergedShapeKey) = common.FindShapeKeyMergeNames(obj.active_shape_key.name, validateWith=obj) 73 | 74 | # Merge em 75 | if (firstShapeKey[-1] == "L"): 76 | common.MergeShapeKeyPair(obj, properties.opt_shapepairs_split_axis, firstShapeKey, expectedCompShapeKey, mergedShapeKey, properties.opt_shapepairs_merge_mode) 77 | else: 78 | common.MergeShapeKeyPair(obj, properties.opt_shapepairs_split_axis, expectedCompShapeKey, firstShapeKey, mergedShapeKey, properties.opt_shapepairs_merge_mode) 79 | self.report({'INFO'}, "Merged shape key '" + firstShapeKey + "' with '" + expectedCompShapeKey + "' to create new '" + mergedShapeKey + "'") 80 | 81 | return {'FINISHED'} 82 | 83 | 84 | def register(): 85 | bpy.utils.register_class(WM_OT_ShapeKeyTools_OpMergeActive) 86 | return WM_OT_ShapeKeyTools_OpMergeActive 87 | 88 | def unregister(): 89 | bpy.utils.unregister_class(WM_OT_ShapeKeyTools_OpMergeActive) 90 | return WM_OT_ShapeKeyTools_OpMergeActive 91 | 92 | if (__name__ == "__main__"): 93 | register() 94 | -------------------------------------------------------------------------------- /shape_key_tools/ops/pairs_merge_all.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | import bpy 4 | from bpy.props import * 5 | 6 | from shape_key_tools import common 7 | 8 | 9 | class WM_OT_ShapeKeyTools_OpMergeAllPairs(bpy.types.Operator): 10 | bl_idname = "wm.shape_key_tools_smartmerge_all_pairs" 11 | bl_label = "Smart Merge All Shape Keys" 12 | bl_description = "Merges all shape keys pairs on the active mesh into single left+right shape keys. Only shape keys that use the 'MyShapeKeyL' 'MyShapeKeyR' naming convention will be merged. This operation does NOT use the Vertex Filter!" 13 | bl_options = {"UNDO"} 14 | 15 | 16 | opt_run_async = BoolProperty( 17 | name = "Run as Modal", 18 | description = "When true, this modal operator runs normally (asynchronously). When false, this operator will block and run synchronously.", 19 | default = True, 20 | ) 21 | 22 | 23 | # report() doesnt print to console when running inside modal() for some weird reason 24 | # So we have to do that manually 25 | def preport(self, message): 26 | print(message) 27 | if (self.opt_run_async): 28 | self.report({'INFO'}, message) 29 | 30 | 31 | def validate(self, context): 32 | # This op requires an active object 33 | if (context.object == None or hasattr(context, "object") == False): 34 | return (False, "No object is selected.") 35 | 36 | obj = context.object 37 | 38 | # Object must be a mesh 39 | if (obj.type != "MESH"): 40 | return (False, "The active object ('" + obj.name + "', type: " + obj.type + ") is not a mesh.") 41 | 42 | # Object must have enough shape keys 43 | if (not hasattr(obj.data.shape_keys, "key_blocks") or len(obj.data.shape_keys.key_blocks.keys()) <= 2): 44 | return (False, "The active object must have at least 2 shape keys (excluding the basis shape key).") 45 | 46 | return (True, None) 47 | 48 | def validateUser(self, context): 49 | (isValid, invalidReason) = self.validate(context) 50 | if (isValid): 51 | return True 52 | else: 53 | if self: 54 | self.report({'ERROR'}, invalidReason) 55 | return False 56 | 57 | @classmethod 58 | def poll(cls, context): 59 | (isValid, invalidReason) = cls.validate(None, context) 60 | return isValid 61 | 62 | 63 | ### Persistent op data 64 | _Timer = None 65 | 66 | _Obj = None 67 | _MergeAxis = None 68 | _MergeMode = None 69 | _MergeBatch = [] 70 | _CurBatchNum = 0 71 | _CurVert = 0 72 | _TotalVerts = 0 73 | _ModalWorkPacing = 0 74 | 75 | 76 | def invoke(self, context, event): 77 | if (event.shift): 78 | return context.window_manager.invoke_props_dialog(self, width=500) 79 | else: 80 | return self.execute(context) 81 | 82 | 83 | def execute(self, context): 84 | scene = context.scene 85 | properties = scene.shape_key_tools_props 86 | 87 | if (self.validateUser(context) == False): 88 | return {'FINISHED'} 89 | 90 | obj = context.object 91 | 92 | # Merge all shape keys that have the MyShapeKeyL MyShapeKeyR naming convention AND have a complementary shape key to merge with 93 | # Example: "HappyL" and "HappyR" becomes "HappyL+HappyR" 94 | seen = {} 95 | self._MergeBatch = [] 96 | for keyBlock in obj.data.shape_keys.key_blocks: 97 | if (not keyBlock.name in seen): 98 | (firstShapeKey, expectedCompShapeKey, mergedShapeKey) = common.FindShapeKeyMergeNames(keyBlock.name, validateWith=obj) 99 | if (expectedCompShapeKey != None and expectedCompShapeKey in obj.data.shape_keys.key_blocks.keys() and not expectedCompShapeKey in seen): 100 | if (keyBlock.name[-1] == "L"): 101 | self._MergeBatch.append((firstShapeKey, expectedCompShapeKey, mergedShapeKey)) 102 | else: 103 | self._MergeBatch.append((expectedCompShapeKey, firstShapeKey, mergedShapeKey)) 104 | seen[firstShapeKey] = True 105 | seen[expectedCompShapeKey] = True 106 | 107 | # Prepare for the modal execution stage 108 | if (len(self._MergeBatch) > 0): 109 | self._Obj = obj 110 | self._MergeAxis = properties.opt_shapepairs_split_axis 111 | self._MergeMode = properties.opt_shapepairs_merge_mode 112 | self._CurBatchNum = 0 113 | self._CurVert = 0 114 | self._TotalVerts = len(obj.data.vertices) * len(self._MergeBatch) 115 | self._ModalWorkPacing = 0 116 | 117 | self.preport("Preparing to merge " + str(len(self._MergeBatch) * 2) + " of " + str(len(obj.data.shape_keys.key_blocks)) + " total shape keys") 118 | 119 | context.window_manager.progress_begin(0, self._TotalVerts) 120 | 121 | if (self.opt_run_async): 122 | context.window_manager.modal_handler_add(self) 123 | self._Timer = context.window_manager.event_timer_add(0.01, context.window) 124 | return {"RUNNING_MODAL"} 125 | else: 126 | modalComplete = None 127 | while (not modalComplete): 128 | modalComplete = self.modalStep(context) 129 | return {"FINISHED"} 130 | 131 | else: 132 | return {"FINISHED"} 133 | 134 | # Merge one shape key at a time per modal event 135 | def modal(self, context, event): 136 | if (event.type == "TIMER"): 137 | if (self.modalStep(context)): # modalStep only returns True when all work is done 138 | return {"CANCELLED"} 139 | else: 140 | return {"PASS_THROUGH"} 141 | 142 | return {"PASS_THROUGH"} 143 | 144 | def modalStep(self, context): 145 | obj = self._Obj 146 | 147 | (leftKey, rightKey, mergedName) = self._MergeBatch[self._CurBatchNum] 148 | 149 | # If we do work every modal() event (or even every 2nd or 3rd), the Blender UI will not update 150 | # So we always wait a few modal pulses after finishing the last work segment before doing the next work segment 151 | if (self._ModalWorkPacing == 0): # notify 152 | # The UI needs one full update cycle after self.report() to display it, so we do this one modal event *before* the actual work 153 | self.preport("Merging shape key pair " + str(self._CurBatchNum + 1) + "/" + str(len(self._MergeBatch)) + " '" + leftKey + "' and '" + rightKey + "' into '" + mergedName + "'") 154 | 155 | elif (self._ModalWorkPacing == 1): # work 156 | # Persistent parameters for all shape key merges 157 | obj = self._Obj 158 | axis = self._MergeAxis 159 | mergeMode = self._MergeMode 160 | 161 | # Create async progress reporting data so the merge method can report progress to the window manager's progress cursor 162 | asyncProgressReporting = { 163 | "CurrentVert": self._CurVert, 164 | "TotalVerts": self._TotalVerts, 165 | } 166 | 167 | # Merge the two victim shape keys 168 | common.MergeShapeKeyPair(obj, axis, leftKey, rightKey, mergedName, mergeMode, asyncProgressReporting=asyncProgressReporting) 169 | 170 | # Finalize this segment of the async work 171 | self._CurVert = asyncProgressReporting["CurrentVert"] 172 | self._CurBatchNum += 1 173 | 174 | if (self._CurBatchNum > len(self._MergeBatch) - 1): 175 | # All work completed 176 | bpy.context.window_manager.progress_end() 177 | self.cancel(context) 178 | self.preport("All shape keys pairs merged.") 179 | return True 180 | #else: # Need to do more work in the next modal 181 | 182 | #else: # rest 183 | 184 | self._ModalWorkPacing += 1 185 | if (self._ModalWorkPacing > 3): 186 | self._ModalWorkPacing = 0 187 | 188 | def cancel(self, context): 189 | if (self._Timer != None): 190 | context.window_manager.event_timer_remove(self._Timer) 191 | self._Timer = None 192 | 193 | 194 | def register(): 195 | bpy.utils.register_class(WM_OT_ShapeKeyTools_OpMergeAllPairs) 196 | return WM_OT_ShapeKeyTools_OpMergeAllPairs 197 | 198 | def unregister(): 199 | bpy.utils.unregister_class(WM_OT_ShapeKeyTools_OpMergeAllPairs) 200 | return WM_OT_ShapeKeyTools_OpMergeAllPairs 201 | 202 | if (__name__ == "__main__"): 203 | register() 204 | -------------------------------------------------------------------------------- /shape_key_tools/ops/pairs_split_active.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | import bpy 4 | from bpy.props import * 5 | 6 | from shape_key_tools import common 7 | 8 | 9 | class WM_OT_ShapeKeyTools_OpSplitActivePair(bpy.types.Operator): 10 | bl_idname = "wm.shape_key_tools_split_active_pair" 11 | bl_label = "Split Active Shape Key" 12 | bl_description = "Splits the active shape key on the active mesh into two separate shape keys. The left and right halves are determined by your chosen split axis. This operation does NOT use the Vertex Filter!" 13 | bl_options = {"UNDO"} 14 | 15 | 16 | opt_delete_original = BoolProperty( 17 | name = "Delete Original Shape Key", 18 | description = "Delete the original shape key after creating the two new split shape keys.", 19 | default = True, 20 | ) 21 | 22 | opt_clear_preview = BoolProperty( 23 | name = "Clear Live Preview if Enabled", 24 | description = "Disable the pair split preview after splitting if it is currently enabled.", 25 | default = True, 26 | ) 27 | 28 | 29 | def validate(self, context): 30 | # This op requires an active object 31 | if (context.object == None or hasattr(context, "object") == False): 32 | return (False, "No object is selected.") 33 | 34 | obj = context.object 35 | 36 | # Object must be a mesh 37 | if (obj.type != "MESH"): 38 | return (False, "The active object ('" + obj.name + "', type: " + obj.type + ") is not a mesh.") 39 | 40 | # Object must have enough shape keys 41 | if (not hasattr(obj.data.shape_keys, "key_blocks") or len(obj.data.shape_keys.key_blocks.keys()) <= 1): 42 | return (False, "The active object must have at least 1 shape key (excluding the basis shape key).") 43 | 44 | # Active shape key cannot be the basis shape key 45 | activeShapeKeyIndex = obj.data.shape_keys.key_blocks.keys().index(obj.active_shape_key.name) 46 | if (activeShapeKeyIndex == 0): 47 | return (False, "You cannot split the basis shape key.") 48 | 49 | return (True, None) 50 | 51 | def validateUser(self, context): 52 | (isValid, invalidReason) = self.validate(context) 53 | if (isValid): 54 | return True 55 | else: 56 | if self: 57 | self.report({'ERROR'}, invalidReason) 58 | return False 59 | 60 | @classmethod 61 | def poll(cls, context): 62 | (isValid, invalidReason) = cls.validate(None, context) 63 | return isValid 64 | 65 | 66 | def execute(self, context): 67 | scene = context.scene 68 | properties = scene.shape_key_tools_props 69 | 70 | if (self.validateUser(context) == False): 71 | return {'FINISHED'} 72 | 73 | obj = context.object 74 | 75 | # Determine the names for the two new (half) shape keys 76 | oldName = obj.active_shape_key.name 77 | (splitLName, splitRName, usesPlusConvention) = common.FindShapeKeyPairSplitNames(oldName, validateWith=obj) 78 | if (usesPlusConvention == False): # shape key name is not in MyShapeKeyL+MyShapeKeyR format 79 | self.report({'INFO'}, "Shape key '" + obj.active_shape_key.name + "' does not use the 'MyShapeKeyL+MyShapeKeyR' naming convention!") 80 | 81 | # Split the active shape key 82 | smoothingDistance = properties.opt_shapepairs_split_smoothdist 83 | if (properties.opt_shapepairs_split_mode == "sharp"): 84 | smoothingDistance = 0 85 | common.SplitPairActiveShapeKey(obj, properties.opt_shapepairs_split_axis, splitLName, splitRName, smoothingDistance, self.opt_delete_original) 86 | self.report({'INFO'}, "Split shape key '" + oldName + "' into left: '" + splitLName + "' and right: '" + splitRName + "'") 87 | 88 | # If the user was previewing this split, disable the preview now and make active the shape key side that was being previewed (L or R) 89 | if (self.opt_clear_preview): 90 | if (properties.opt_shapepairs_splitmerge_preview_split_left): 91 | obj.active_shape_key_index = obj.data.shape_keys.key_blocks.keys().index(splitLName) 92 | elif (properties.opt_shapepairs_splitmerge_preview_split_right): 93 | obj.active_shape_key_index = obj.data.shape_keys.key_blocks.keys().index(splitRName) 94 | properties.opt_shapepairs_splitmerge_preview_split_left = False 95 | properties.opt_shapepairs_splitmerge_preview_split_right = False 96 | 97 | return {'FINISHED'} 98 | 99 | 100 | def register(): 101 | bpy.utils.register_class(WM_OT_ShapeKeyTools_OpSplitActivePair) 102 | return WM_OT_ShapeKeyTools_OpSplitActivePair 103 | 104 | def unregister(): 105 | bpy.utils.unregister_class(WM_OT_ShapeKeyTools_OpSplitActivePair) 106 | return WM_OT_ShapeKeyTools_OpSplitActivePair 107 | 108 | if (__name__ == "__main__"): 109 | register() 110 | -------------------------------------------------------------------------------- /shape_key_tools/ops/pairs_split_all.py: -------------------------------------------------------------------------------- 1 | import sys, os 2 | 3 | import bpy 4 | from bpy.props import * 5 | 6 | from shape_key_tools import common 7 | 8 | 9 | class WM_OT_ShapeKeyTools_OpSplitAllPairs(bpy.types.Operator): 10 | bl_idname = "wm.shape_key_tools_split_all_pairs" 11 | bl_label = "Split All Paired Shape Keys" 12 | bl_description = "Splits ALL paired shape keys (i.e. shape keys with names like 'MyShapeKeyL+MyShapeKeyR') on the active mesh into two separate shape keys. The left and right halves are determined by your chosen split axis. This operation does NOT use the Vertex Filter!" 13 | bl_options = {"UNDO"} 14 | 15 | 16 | opt_run_async = BoolProperty( 17 | name = "Run as Modal", 18 | description = "When true, this modal operator runs normally (asynchronously). When false, this operator will block and run synchronously.", 19 | default = True, 20 | ) 21 | 22 | 23 | opt_delete_originals = BoolProperty( 24 | name = "Delete Original Shape Keys", 25 | description = "Delete the original shape keys after creating each pair of new split shape keys.", 26 | default = True, 27 | ) 28 | 29 | opt_clear_preview = BoolProperty( 30 | name = "Clear Live Preview if Enabled", 31 | description = "Disable the pair split preview after splitting if it is currently enabled.", 32 | default = True, 33 | ) 34 | 35 | 36 | # report() doesnt print to console when running inside modal() for some weird reason 37 | # So we have to do that manually 38 | def preport(self, message): 39 | print(message) 40 | if (self.opt_run_async): 41 | self.report({'INFO'}, message) 42 | 43 | 44 | def validate(self, context): 45 | # This op requires an active object 46 | if (context.object == None or hasattr(context, "object") == False): 47 | return (False, "No object is selected.") 48 | 49 | obj = context.object 50 | 51 | # Object must be a mesh 52 | if (obj.type != "MESH"): 53 | return (False, "The active object ('" + obj.name + "', type: " + obj.type + ") is not a mesh.") 54 | 55 | # Object must have enough shape keys 56 | if (not hasattr(obj.data.shape_keys, "key_blocks") or len(obj.data.shape_keys.key_blocks.keys()) <= 1): 57 | return (False, "The active object must have at least 1 shape key (excluding the basis shape key).") 58 | 59 | return (True, None) 60 | 61 | def validateUser(self, context): 62 | (isValid, invalidReason) = self.validate(context) 63 | if (isValid): 64 | return True 65 | else: 66 | if self: 67 | self.report({'ERROR'}, invalidReason) 68 | return False 69 | 70 | @classmethod 71 | def poll(cls, context): 72 | (isValid, invalidReason) = cls.validate(None, context) 73 | return isValid 74 | 75 | 76 | ### Persistent op data 77 | _Timer = None 78 | 79 | _Obj = None 80 | _SplitAxis = None 81 | _SmoothingDistance = 0 82 | _SplitBatch = [] 83 | _CurBatchNum = 0 84 | _CurVert = 0 85 | _TotalVerts = 0 86 | _ModalWorkPacing = 0 87 | 88 | 89 | def invoke(self, context, event): 90 | if (event.shift): 91 | return context.window_manager.invoke_props_dialog(self, width=500) 92 | else: 93 | return self.execute(context) 94 | 95 | 96 | def execute(self, context): 97 | scene = context.scene 98 | properties = scene.shape_key_tools_props 99 | 100 | if (self.validateUser(context) == False): 101 | return {'FINISHED'} 102 | 103 | obj = context.object 104 | 105 | # Split all shapekeys with the MyShapeKeyL MyShapeKeyR naming convention 106 | # Examples: 107 | # - "HappyL+HappyR" becomes HappyL and HappyR 108 | # - "HappyL+UnhappyR" becomes HappyL and UnhappyR (works, but bad names, cannot recombine later) 109 | # - "Happyl+happyR" becomes "Happyl" and "happyR" (works, but bad names, cannot recombine later) 110 | self._SplitBatch = [] 111 | for keyBlock in obj.data.shape_keys.key_blocks: 112 | (splitLName, splitRName, usesPlusConvention) = common.FindShapeKeyPairSplitNames(keyBlock.name, validateWith=obj) 113 | if (splitLName != None and splitRName != None and usesPlusConvention == True): 114 | self._SplitBatch.append((keyBlock.name, splitLName, splitRName)) 115 | 116 | # Prepare for the modal execution stage 117 | if (len(self._SplitBatch) > 0): 118 | self._Obj = obj 119 | self._SplitAxis = properties.opt_shapepairs_split_axis 120 | self._SmoothingDistance = properties.opt_shapepairs_split_smoothdist 121 | if (properties.opt_shapepairs_split_mode == "sharp"): 122 | self._SmoothingDistance = 0 123 | self._CurBatchNum = 0 124 | self._CurVert = 0 125 | self._TotalVerts = len(obj.data.vertices) * len(self._SplitBatch) 126 | self._ModalWorkPacing = 0 127 | 128 | # If the user was previewing this split, disable the preview now 129 | if (self.opt_clear_preview): 130 | properties.opt_shapepairs_splitmerge_preview_split_left = False 131 | properties.opt_shapepairs_splitmerge_preview_split_right = False 132 | 133 | self.preport("Preparing to split " + str(len(self._SplitBatch)) + " of " + str(len(obj.data.shape_keys.key_blocks)) + " total shape keys") 134 | 135 | context.window_manager.progress_begin(0, self._TotalVerts) 136 | 137 | if (self.opt_run_async): 138 | context.window_manager.modal_handler_add(self) 139 | self._Timer = context.window_manager.event_timer_add(0.01, context.window) 140 | return {"RUNNING_MODAL"} 141 | else: 142 | modalComplete = None 143 | while (not modalComplete): 144 | modalComplete = self.modalStep(context) 145 | return {"FINISHED"} 146 | 147 | else: 148 | return {"FINISHED"} 149 | 150 | # Split one shape key at a time per modal event 151 | def modal(self, context, event): 152 | if (event.type == "TIMER"): 153 | if (self.modalStep(context)): # modalStep only returns True when all work is done 154 | return {"CANCELLED"} 155 | else: 156 | return {"PASS_THROUGH"} 157 | 158 | return {"PASS_THROUGH"} 159 | 160 | def modalStep(self, context): 161 | obj = self._Obj 162 | 163 | (oldName, splitLName, splitRName) = self._SplitBatch[self._CurBatchNum] 164 | 165 | # If we do work every modal() event (or even every 2nd or 3rd), the Blender UI will not update 166 | # So we always wait a few modal pulses after finishing the last work segment before doing the next work segment 167 | if (self._ModalWorkPacing == 0): # notify 168 | # The UI needs one full update cycle after self.report() to display it, so we do this one modal event *before* the actual work 169 | self.preport("Splitting shape key " + str(self._CurBatchNum + 1) + "/" + str(len(self._SplitBatch)) + " '" + oldName + "' into left: '" + splitLName + "' and right: '" + splitRName + "'") 170 | 171 | elif (self._ModalWorkPacing == 1): # work 172 | # Persistent parameters for all shape key splits 173 | axis = self._SplitAxis 174 | smoothingDistance = self._SmoothingDistance 175 | 176 | # Create async progress reporting data so the split method can report progress to the window manager's progress cursor 177 | asyncProgressReporting = { 178 | "CurrentVert": self._CurVert, 179 | "TotalVerts": self._TotalVerts, 180 | } 181 | 182 | # Make the victim shape key active and split it 183 | obj.active_shape_key_index = obj.data.shape_keys.key_blocks.keys().index(oldName) 184 | common.SplitPairActiveShapeKey(obj, axis, splitLName, splitRName, smoothingDistance, self.opt_delete_originals, asyncProgressReporting=asyncProgressReporting) 185 | 186 | # Finalize this segment of the async work 187 | self._CurVert = asyncProgressReporting["CurrentVert"] 188 | self._CurBatchNum += 1 189 | 190 | if (self._CurBatchNum > len(self._SplitBatch) - 1): 191 | # All work completed 192 | bpy.context.window_manager.progress_end() 193 | self.cancel(context) 194 | self.preport("All shape keys pairs split.") 195 | return True 196 | 197 | #else: # Need to do more work in the next modal 198 | 199 | #else: # rest 200 | 201 | self._ModalWorkPacing += 1 202 | if (self._ModalWorkPacing > 3): 203 | self._ModalWorkPacing = 0 204 | 205 | def cancel(self, context): 206 | if (self._Timer != None): 207 | context.window_manager.event_timer_remove(self._Timer) 208 | self._Timer = None 209 | 210 | 211 | def register(): 212 | bpy.utils.register_class(WM_OT_ShapeKeyTools_OpSplitAllPairs) 213 | return WM_OT_ShapeKeyTools_OpSplitAllPairs 214 | 215 | def unregister(): 216 | bpy.utils.unregister_class(WM_OT_ShapeKeyTools_OpSplitAllPairs) 217 | return WM_OT_ShapeKeyTools_OpSplitAllPairs 218 | 219 | if (__name__ == "__main__"): 220 | register() 221 | --------------------------------------------------------------------------------