├── .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 | 
22 |
23 | ### Fine-tune splitting with viewport guides and live preview
24 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------