Module kawa_scripts.shapekeys
Useful tools for Shape Keys
Most of the functions available as operators from UI:
In "Shape Keys" area in "Properties" editor window:
In Object context menu in "3D Viewport" window at Object-mode:
In Vertex context menu in "3D Viewport" window at Mesh-Edit-mode:
Expand source code
# Kawashirov's Scripts (c) 2021 by Sergey V. Kawashirov
#
# Kawashirov's Scripts is licensed under a
# Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.
#
# You should have received a copy of the license along with this
# work. If not, see <http://creativecommons.org/licenses/by-nc-sa/3.0/>.
#
#
"""
Useful tools for Shape Keys
**Most of the functions available as operators from UI:**
In "*Shape Keys*" area in "*Properties*" editor window:
![menu_shapekeys.png](https://i.imgur.com/hqzhUqy.png)
In Object context menu in "*3D Viewport*" window at Object-mode:
![menu_object.png](https://i.imgur.com/I8DAm2u.png)
In Vertex context menu in "*3D Viewport*" window at Mesh-Edit-mode:
![menu_vertex.png](https://i.imgur.com/CZtWPHN.png)
"""
import bpy as _bpy
from bpy import context as _C
from ._internals import log as _log
from ._internals import KawaOperator as _KawaOperator
from . import _doc
import typing as _typing
if _typing.TYPE_CHECKING:
from typing import *
from bpy.types import *
def ensure_len_match(mesh: 'Mesh', shape_key: 'ShapeKey', op: 'Operator' = None):
"""
Ensure `len(mesh.vertices) == len(shape_key.data)`. Helps to detect corrupted Mesh/Key datablocks.
"""
len_vts = len(mesh.vertices)
len_skd = len(shape_key.data)
if len_vts == len_skd:
return True
_log.error("Size of {0} ({1}) and size of {2} ({3}) does not match! Is shape key corrupted?"
.format(repr(mesh.vertices), len_vts, repr(shape_key.data), len_skd), op=op)
return False
def _mesh_have_shapekeys(mesh: 'Mesh', n: int = 1):
return mesh is not None and mesh.shape_keys is not None and len(mesh.shape_keys.key_blocks) >= n
def _obj_have_shapekeys(obj: 'Object', n: int = 1):
return obj is not None and obj.type == 'MESH' and _mesh_have_shapekeys(obj.data, n=n)
def _mesh_selection_to_vertices(mesh: 'Mesh'):
for p in mesh.polygons:
if p.select:
for i in p.vertices:
mesh.vertices[i].select = True
for e in mesh.edges:
if e.select:
for i in e.vertices:
mesh.vertices[i].select = True
class OperatorSelectVerticesAffectedByShapeKey(_KawaOperator):
"""
**Select Vertices Affected by Active Shape Key.**
"""
bl_idname = "kawa.select_vertices_affected_by_shape_key"
bl_label = "Select Vertices Affected by Active Shape Key"
bl_description = "Select Vertices Affected by Active Shape Key."
bl_options = {'REGISTER', 'UNDO'}
epsilon: _bpy.props.FloatProperty(
name="Epsilon",
description="Selection precision in local space",
min=1e-07,
default=1e-06,
max=1,
precision=6,
unit='LENGTH'
)
@classmethod
def poll(cls, context: 'Context'):
obj = cls.get_active_obj(context)
if not obj or obj.type != 'MESH':
return False # Требуется активный меш-объект
if not obj.active_shape_key or obj.active_shape_key_index == 0:
return False # Требуется что бы был активный не первый шейпкей
if context.mode != 'EDIT_MESH' and context.mode != 'OBJECT':
return False # Требуется режим OBJECT или EDIT_MESH
return True
def invoke(self, context: 'Context', event):
wm = context.window_manager
# return wm.invoke_props_popup(self, event)
# return {'RUNNING_MODAL'}
return wm.invoke_props_dialog(self)
def execute(self, context: 'Context'):
# Рофл в том, что операции над мешью надо проводить вне эдит-мода
_bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
obj = self.get_active_obj(context)
mesh = obj.data # type: Mesh
shape_key = obj.active_shape_key
reference = mesh.shape_keys.reference_key
match_skd = ensure_len_match(mesh, shape_key, op=self)
match_ref = ensure_len_match(mesh, reference, op=self)
if not match_skd or not match_ref:
return {'CANCELLED'}
for p in mesh.polygons:
p.select = False
for e in mesh.edges:
e.select = False
counter = 0
for i in range(len(mesh.vertices)):
mesh.vertices[i].select = (shape_key.data[i].co - reference.data[i].co).magnitude > self.epsilon
counter += 1
_log.info("Selected {0} vertices affected by {1} in {2}".format(counter, repr(shape_key), repr(obj)))
_bpy.ops.object.mode_set_with_submode(mode='EDIT', toggle=False, mesh_select_mode={'VERT'})
return {'FINISHED'}
class OperatorRevertSelectedInActiveToBasis(_KawaOperator):
"""
**Revert selected vertices in edit-mode to Reference Shape Key (Basis) in active Shape Key.**
"""
bl_idname = "kawa.revert_selected_shape_keys_in_active_to_basis"
bl_label = "REVERT SELECTED Vertices in ACTIVE Shape Key to BASIS"
bl_description = "Revert selected vertices in edit-mode to Reference Shape Key (Basis) in active Shape Key."
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: 'Context'):
obj = cls.get_active_obj(context)
if not obj or obj.type != 'MESH':
return False # Требуется активный меш-объект
if not obj.active_shape_key or obj.active_shape_key_index == 0:
return False # Требуется что бы был активный не первый шейпкей
if context.mode != 'EDIT_MESH':
return False # Требуется режим EDIT_MESH
return True
def execute(self, context: 'Context'):
obj = self.get_active_obj(context)
# Рофл в том, что операции над мешью надо проводить вне эдит-мода
_bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
mesh = obj.data # type: Mesh
shape_key = obj.active_shape_key
reference = mesh.shape_keys.reference_key
match_skd = ensure_len_match(mesh, shape_key, op=self)
match_ref = ensure_len_match(mesh, reference, op=self)
if not match_skd or not match_ref:
return {'CANCELLED'}
_mesh_selection_to_vertices(mesh)
for i in range(len(mesh.vertices)):
if mesh.vertices[i].select:
shape_key.data[i].co = reference.data[i].co.copy()
_bpy.ops.object.mode_set_with_submode(mode='EDIT', toggle=False, mesh_select_mode={'VERT'})
return {'FINISHED'}
class OperatorRevertSelectedInAllToBasis(_KawaOperator):
"""
**Revert selected vertices in edit-mode to Reference Shape Key (Basis) in every Shape Key.**
"""
bl_idname = "kawa.revert_selected_shape_keys_in_all_to_basis"
bl_label = "REVERT SELECTED Vertices in ALL Shape Keys to BASIS"
bl_description = "Revert selected vertices in edit-mode to Reference Shape Key (Basis) in every Shape Key."
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: 'Context'):
obj = cls.get_active_obj(context)
if not obj or obj.type != 'MESH':
return False # Требуется активный меш-объект
data = obj.data # type: Mesh
if data.shape_keys is None or len(data.shape_keys.key_blocks) < 2:
return False # Требуется что бы было 2 или более шейпкея
if context.mode != 'EDIT_MESH':
return False # Требуется режим EDIT_MESH
return True
def execute(self, context: 'Context'):
obj = self.get_active_obj(context)
# Рофл в том, что операции над мешью надо проводить вне эдит-мода
_bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
mesh = obj.data # type: Mesh
reference = mesh.shape_keys.reference_key
if not ensure_len_match(mesh, reference, op=self):
return {'CANCELLED'}
_mesh_selection_to_vertices(mesh)
for shape_key in mesh.shape_keys.key_blocks:
if shape_key == reference:
continue
if not ensure_len_match(mesh, shape_key, op=self):
continue
for i in range(len(mesh.vertices)):
if mesh.vertices[i].select:
shape_key.data[i].co = reference.data[i].co.copy()
_bpy.ops.object.mode_set_with_submode(mode='EDIT', toggle=False, mesh_select_mode={'VERT'})
return {'FINISHED'}
class OperatorApplySelectedInActiveToBasis(_KawaOperator):
"""
Same as `OperatorApplyActiveToBasis`, but only for selected vertices in edit-mode.
See also: `apply_active_to_basis`.
"""
bl_idname = "kawa.apply_selected_shape_keys_in_active_to_basis"
bl_label = "APPLY SELECTED Vertices in ACTIVE Shape Key to Basis"
# bl_description at the end of file.
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: 'Context'):
obj = cls.get_active_obj(context)
if not obj or obj.type != 'MESH':
return False # Требуется активный меш-объект
if not obj.active_shape_key or obj.active_shape_key_index == 0:
return False # Требуется что бы был активный не первый шейпкей
if context.mode != 'EDIT_MESH':
return False # Требуется режим EDIT_MESH
return True
def execute(self, context: 'Context'):
obj = self.get_active_obj(context)
# Рофл в том, что операции над мешью надо проводить вне эдит-мода
_bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
mesh = obj.data # type: Mesh
active_key = obj.active_shape_key
ref_key = mesh.shape_keys.reference_key
match_active = ensure_len_match(mesh, active_key, op=self)
match_ref = ensure_len_match(mesh, ref_key, op=self)
if not match_active or not match_ref:
return {'CANCELLED'}
_mesh_selection_to_vertices(mesh)
for i in range(len(mesh.vertices)):
if mesh.vertices[i].select:
ref_key.data[i].co = active_key.data[i].co.copy()
_bpy.ops.object.mode_set_with_submode(mode='EDIT', toggle=False, mesh_select_mode={'VERT'})
return {'FINISHED'}
class OperatorApplySelectedInActiveToAll(_KawaOperator):
"""
Same as `OperatorApplyActiveToAll`, but only for selected vertices in edit-mode.
See also: `apply_active_to_all`.
"""
bl_idname = "kawa.apply_selected_shape_keys_in_active_to_all"
bl_label = "APPLY SELECTED Vertices in ACTIVE Shape Key to ALL Others"
# bl_description at the end of file.
bl_options = {'REGISTER', 'UNDO'}
@classmethod
def poll(cls, context: 'Context'):
obj = cls.get_active_obj(context)
if not obj or obj.type != 'MESH':
return False # Требуется активный меш-объект
if not obj.active_shape_key or obj.active_shape_key_index == 0:
return False # Требуется что бы был активный не первый шейпкей
if context.mode != 'EDIT_MESH':
return False # Требуется режим EDIT_MESH
return True
def execute(self, context: 'Context'):
obj = self.get_active_obj(context)
# Рофл в том, что операции над мешью надо проводить вне эдит-мода
_bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
mesh = obj.data # type: Mesh
active_key = obj.active_shape_key
ref_key = mesh.shape_keys.reference_key
match_active = ensure_len_match(mesh, active_key, op=self)
match_ref = ensure_len_match(mesh, ref_key, op=self)
if not match_active or not match_ref:
return {'CANCELLED'}
_mesh_selection_to_vertices(mesh)
for other_key in mesh.shape_keys.key_blocks:
if other_key == active_key or other_key == ref_key:
continue
if not ensure_len_match(mesh, other_key, op=self):
continue
for i in range(len(mesh.vertices)):
if mesh.vertices[i].select:
other_offset = other_key.data[i].co - ref_key.data[i].co
active_offset = active_key.data[i].co - ref_key.data[i].co
other_key.data[i].co = ref_key.data[i].co + other_offset + active_offset
for i in range(len(mesh.vertices)):
if mesh.vertices[i].select:
ref_key.data[i].co = active_key.data[i].co.copy()
_bpy.ops.object.mode_set_with_submode(mode='EDIT', toggle=False, mesh_select_mode={'VERT'})
return {'FINISHED'}
#
# Object-mode operators
def apply_active_to_basis(obj: 'Object', keep_reverted=True, op: 'Operator' = None):
"""
**Applies positions of active Shape Key to Reference ShapeKey (Basis).**
Positions (shapes) will be transferred from active Shape Key to Reference ShapeKey (Basis).
Other Shape Keys keep their positions (shapes).
If `keep_reverted` then old positions from Reference ShapeKey (Basis) will be transferred to active Shape Key,
so active Shape Key act as reverted. ` (Reverted)` will be added to it's name.
If not `keep_reverted` then active Shape Key will be deleted.
Returns: True if succeeded, False otherwise.
Available as operator `OperatorApplyActiveToBasis`.
See also: `apply_active_to_all`, `OperatorApplySelectedInActiveToBasis`.
"""
# No context control
mesh = obj.data # type: Mesh
active_key = obj.active_shape_key
ref_key = mesh.shape_keys.reference_key
match_active = ensure_len_match(mesh, active_key, op=op)
match_ref = ensure_len_match(mesh, ref_key, op=op)
if not match_active or not match_ref:
return False
for i in range(len(mesh.vertices)):
v = ref_key.data[i].co.copy()
ref_key.data[i].co = active_key.data[i].co.copy()
active_key.data[i].co = v
if keep_reverted:
active_key.name += ' (Reverted)'
else:
obj.active_shape_key_index = 0
obj.shape_key_remove(active_key)
return True
class OperatorApplyActiveToBasis(_KawaOperator):
"""
Operator of `apply_active_to_basis`.
See also: `OperatorApplySelectedInActiveToBasis`.
"""
bl_idname = "kawa.apply_active_shape_keys_to_basis"
bl_label = "APPLY ACTIVE Shape Key to Basis"
bl_description = "\n".join((
"Positions (shapes) will be transferred from active Shape Key to Reference ShapeKey (Basis).",
"Other Shape Keys keep their positions (shapes).",
))
bl_options = {'REGISTER', 'UNDO'}
keep_reverted: _bpy.props.BoolProperty(
name="Keep Reverted Shape Key",
default=True,
)
@classmethod
def poll(cls, context: 'Context'):
obj = cls.get_active_obj(context)
if not obj or obj.type != 'MESH':
return False # Требуется активный меш-объект
if not obj.active_shape_key or obj.active_shape_key_index == 0:
return False # Требуется что бы был активный не первый шейпкей
if context.mode != 'OBJECT':
return False # Требуется режим OBJECT
return True
def invoke(self, context: 'Context', event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: 'Context'):
return {'FINISHED'} if apply_active_to_basis(self.get_active_obj(context), keep_reverted=self.keep_reverted, op=self) else {'CANCELLED'}
def apply_active_to_all(obj: 'Object', keep_reverted=False, op: 'Operator' = None):
"""
**Applies offsets of active Shape Key to every other shape key.**
Same as `apply_active_to_basis`, but other Shape Keys will be also edited.
It's like changing whole base mesh with all it's shape keys.
If `keep_reverted` then old positions from Reference ShapeKey (Basis) will be moved to active Shape Key,
so active Shape Key act as reverted. ` (Reverted)` will be added to it's name.
If not `keep_reverted` then active Shape Key will be deleted.
Returns: True if succeeded, False otherwise.
Available as operator `OperatorApplyActiveToAll`.
See also: `OperatorApplySelectedInActiveToAll`.
"""
# No context control
mesh = obj.data # type: Mesh
active_key = obj.active_shape_key
ref_key = mesh.shape_keys.reference_key
match_active = ensure_len_match(mesh, active_key, op=op)
match_ref = ensure_len_match(mesh, ref_key, op=op)
if not match_active or not match_ref:
return False
for other_key in mesh.shape_keys.key_blocks:
if other_key == active_key or other_key == ref_key:
continue
if not ensure_len_match(mesh, other_key, op=op):
continue
for i in range(len(mesh.vertices)):
other_offset = other_key.data[i].co - ref_key.data[i].co
active_offset = active_key.data[i].co - ref_key.data[i].co
other_key.data[i].co = ref_key.data[i].co + other_offset + active_offset
for i in range(len(mesh.vertices)):
v = ref_key.data[i].co.copy()
ref_key.data[i].co = active_key.data[i].co.copy()
active_key.data[i].co = v
if keep_reverted:
active_key.name += ' (Reverted)'
else:
obj.active_shape_key_index = 0
obj.shape_key_remove(active_key)
return True
class OperatorApplyActiveToAll(_KawaOperator):
"""
Operator of `apply_active_to_all`.
See also: `OperatorApplySelectedInActiveToAll`.
"""
bl_idname = "kawa.apply_active_shape_keys_to_all"
bl_label = "APPLY ACTIVE Shape Key to ALL Others"
bl_description = "Same as {}, but other Shape Keys will be also edited.".format(
repr(OperatorApplyActiveToBasis.bl_label))
bl_options = {'REGISTER', 'UNDO'}
keep_reverted: _bpy.props.BoolProperty(
name="Keep Reverted Shape Key",
default=False,
)
@classmethod
def poll(cls, context: 'Context'):
obj = cls.get_active_obj(context)
if not obj or obj.type != 'MESH':
return False # Требуется активный меш-объект
if not obj.active_shape_key or obj.active_shape_key_index == 0:
return False # Требуется что бы был активный не первый шейпкей
if context.mode != 'OBJECT':
return False # Требуется режим OBJECT
return True
def invoke(self, context: 'Context', event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: 'Context'):
return {'FINISHED'} if apply_active_to_all(self.get_active_obj(context), keep_reverted=self.keep_reverted, op=self) else {'CANCELLED'}
def cleanup_active(obj: 'Object', epsilon: 'float', op: 'Operator' = None) -> 'int':
"""
**Removes micro-offsets in active Shape Key.**
If position of a vertex differs from position in Reference Shape Key (Basis) for `epsilon` or less,
then it's position will be reverted (to be the same as in Reference Shape Key)
Returns: number of changed vertices.
Available as operator `OperatorCleanupActive`
"""
if not _obj_have_shapekeys(obj, n=2):
return 0
mesh = obj.data # type: Mesh
active_key = obj.active_shape_key
ref_key = mesh.shape_keys.reference_key
if active_key == ref_key:
return 0
match_active = ensure_len_match(mesh, active_key, op=op)
match_ref = ensure_len_match(mesh, ref_key, op=op)
if not match_active or not match_ref:
return 0
changed = 0
for i in range(len(mesh.vertices)):
if (ref_key.data[i].co - active_key.data[i].co).magnitude <= epsilon:
active_key.data[i].co = ref_key.data[i].co.copy()
changed += 1
return changed
class OperatorCleanupActive(_KawaOperator):
"""
Operator of `cleanup_active`
"""
bl_idname = "kawa.cleanup_active_shape_key"
bl_label = "Remove Mirco-offsets in ACTIVE Shape Key"
bl_description = "\n".join((
"If position of a vertex differs from position in Reference Shape Key (Basis) for `epsilon` or less,",
"then it's position will be reverted (to be the same as in Reference Shape Key)",
))
bl_options = {'REGISTER', 'UNDO'}
epsilon: _bpy.props.FloatProperty(
name="Epsilon",
description="Threshold in local space",
min=1e-07,
default=1e-04,
max=1,
precision=6,
unit='LENGTH'
)
@classmethod
def poll(cls, context: 'Context'):
obj = cls.get_active_obj(context)
if not obj or obj.type != 'MESH':
return False # Требуется активный меш-объект
if not obj.active_shape_key or obj.active_shape_key_index == 0:
return False # Требуется что бы был активный не первый шейпкей
if context.mode != 'OBJECT':
return False # Требуется режим OBJECT
return True
def invoke(self, context: 'Context', event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: 'Context'):
changed = cleanup_active(self.get_active_obj(context), self.epsilon, op=self)
_log.info("Cleaned {0} vertices.".format(changed), op=self)
return {'FINISHED'} if changed > 0 else {'CANCELLED'}
def cleanup_all(objs: 'Iterable[Object]', epsilon: float, op: 'Operator' = None) -> 'Tuple[int, int, int]':
"""
**Removes micro-offsets in all Shape Keys.**
Same as `cleanup_active`, but for every shape key (except reference one) for every object.
Returns: (number of changed vertices, number of changed shape keys, number of changed meshes).
Available as operator `OperatorCleanupAll`
"""
objs = list(obj for obj in objs if _obj_have_shapekeys(obj, n=2)) # type: List[Object]
meshes = set()
vertices_changed, shapekeys_changed, meshes_changed = 0, 0, 0
for obj in objs:
mesh = obj.data # type: Mesh
if mesh in meshes:
continue # Уже трогали
meshes.add(mesh)
last_shape_key_index = obj.active_shape_key_index
try:
mesh_changed = False
for shape_key_index in range(1, len(mesh.shape_keys.key_blocks)):
obj.active_shape_key_index = shape_key_index
vc = cleanup_active(obj, epsilon, op=op)
if vc > 0:
vertices_changed += vc
shapekeys_changed += 1
mesh_changed = True
if mesh_changed:
meshes_changed += 1
finally:
obj.active_shape_key_index = last_shape_key_index
return vertices_changed, shapekeys_changed, meshes_changed
class OperatorCleanupAll(_KawaOperator):
"""
Operator of `cleanup_all`
"""
bl_idname = "kawa.cleanup_all_shape_keys"
bl_label = "Remove Mirco-offsets in ALL Shape Keys"
bl_description = "Same as {}, but for every shape key (except reference one) for every object".format(
repr(OperatorCleanupActive.bl_label))
bl_options = {'REGISTER', 'UNDO'}
epsilon: _bpy.props.FloatProperty(
name="Epsilon",
description="Threshold in local space",
min=1e-07,
default=1e-04,
max=1,
precision=6,
unit='LENGTH'
)
@classmethod
def poll(cls, context: 'Context'):
if context.mode != 'OBJECT':
return False # Требуется режим OBJECT
if not any(True for obj in cls.get_selected_objs(context) if _obj_have_shapekeys(obj, n=2)):
return False # Должны быть выбраны Меш-объекты c 2 или более шейпами
return True
def invoke(self, context: 'Context', event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: 'Context'):
selected = list(self.get_selected_objs(context))
if len(selected) < 1:
_log.warning("No mesh-objects with multiple shape keys selected.", op=self)
return {'CANCELLED'}
vertices_cleaned, shapekeys_cleaned, objects_cleaned = cleanup_all(selected, self.epsilon, op=self)
_log.info("Cleaned {0} Vertices in {1} Shape Keys in {2} Meshes from micro-offsets (<{3}).".format(
vertices_cleaned, shapekeys_cleaned, objects_cleaned, float(self.epsilon)), op=self)
return {'FINISHED'} if vertices_cleaned > 0 else {'CANCELLED'}
def remove_empty(objs: 'Iterable[Object]', epsilon: float, op: 'Operator' = None) -> 'Tuple[int, int]':
"""
**Removes empty Shape Keys from Mesh-objects.**
Shape Key is empty, if positions of **every** vertex differ from Reference Shape Key (Basis) for `epsilon` or less.
Returns: (number of removed Shape Keys, number of meshes changed)
Available as operator `OperatorRemoveEmpty`
"""
objs = list(obj for obj in objs if _obj_have_shapekeys(obj, n=2)) # type: List[Object]
removed_shapekeys, changed_meshes = 0, 0
meshes = set()
for obj in objs:
mesh = obj.data # type: Mesh
if mesh in meshes:
continue # Уже трогали
meshes.add(mesh)
key = mesh.shape_keys # type: Key
reference = key.reference_key
empty_keys = set() # type: Set[str]
for shape_key in key.key_blocks:
if shape_key == reference:
continue # Базис не удаялется
match1 = ensure_len_match(mesh, reference, op=op)
match2 = ensure_len_match(mesh, shape_key, op=op)
if not match1 or not match2:
continue
# Имеются ли различия между шейпами?
if not any((shape_key.data[i].co - reference.data[i].co).magnitude > epsilon for i in range(len(mesh.vertices))):
empty_keys.add(shape_key.name)
# _log.info("Found {0} empty shape keys in mesh {1}: {2}, removing...".format(len(empty_keys), repr(mesh), repr(empty_keys)), op=op)
if len(empty_keys) < 1:
continue
for empty_key in empty_keys:
# На всякий случай удаляю по прямому пути, вдруг там что-то перестраивается в процессе удаления.
# Не спроста же Key-блоки можно редактировать только через Object-блоки
obj.shape_key_remove(obj.data.shape_keys.key_blocks[empty_key])
removed_shapekeys += len(empty_keys)
changed_meshes += 1
return removed_shapekeys, changed_meshes
class OperatorRemoveEmpty(_KawaOperator):
"""
Operator of `remove_empty`.
"""
bl_idname = "kawa.remove_empty_shape_keys"
bl_label = "Remove Empty Shape Keys"
bl_description = "\n".join((
"Shape Key is empty, if positions of EVERY vertex differ ",
"from Reference Shape Key (Basis) for Epsilon or less.",
))
bl_options = {'REGISTER', 'UNDO'}
epsilon: _bpy.props.FloatProperty(
name="Epsilon",
description="Selection precision in local space",
min=1e-07,
default=1e-06,
max=1,
precision=6,
unit='LENGTH'
)
@classmethod
def poll(cls, context: 'Context'):
if context.mode != 'OBJECT':
return False # Требуется режим OBJECT
if not any(True for obj in cls.get_selected_objs(context) if _obj_have_shapekeys(obj, n=2)):
return False # Должны быть выбраны какие-то Меш-объекты
return True
def invoke(self, context: 'Context', event):
return context.window_manager.invoke_props_dialog(self)
def execute(self, context: 'Context'):
objs = self.get_selected_objs(context)
removed_shapekeys, changed_meshes = remove_empty(objs, self.epsilon, op=self)
_log.info("Total {0} shape keys removed from {1} Meshes.".format(removed_shapekeys, changed_meshes), op=self)
return {'FINISHED'} if removed_shapekeys > 0 else {'CANCELLED'}
OperatorApplySelectedInActiveToBasis.bl_description = \
"Same as {}, but only for selected vertices in edit-mode.".format(repr(OperatorApplyActiveToBasis.bl_label))
OperatorApplySelectedInActiveToAll.bl_description = \
"Same as {}, but only for selected vertices in edit-mode.".format(repr(OperatorApplyActiveToAll.bl_label))
classes = (
# Edit-mode
OperatorSelectVerticesAffectedByShapeKey,
#
OperatorRevertSelectedInActiveToBasis,
OperatorRevertSelectedInAllToBasis,
#
OperatorApplySelectedInActiveToBasis,
OperatorApplySelectedInActiveToAll,
#
# Object-mode
OperatorApplyActiveToBasis,
OperatorApplyActiveToAll,
#
OperatorCleanupActive,
OperatorCleanupAll,
OperatorRemoveEmpty,
)
__pdoc__ = dict()
_doc.process_blender_classes(__pdoc__, classes)
Functions
def apply_active_to_all(obj: Object, keep_reverted=False, op: Operator = None)
-
Applies offsets of active Shape Key to every other shape key. Same as
apply_active_to_basis()
, but other Shape Keys will be also edited. It's like changing whole base mesh with all it's shape keys. Ifkeep_reverted
then old positions from Reference ShapeKey (Basis) will be moved to active Shape Key, so active Shape Key act as reverted.(Reverted)
will be added to it's name. If notkeep_reverted
then active Shape Key will be deleted.Returns: True if succeeded, False otherwise.
Available as operator
OperatorApplyActiveToAll
. See also:OperatorApplySelectedInActiveToAll
.Expand source code
def apply_active_to_all(obj: 'Object', keep_reverted=False, op: 'Operator' = None): """ **Applies offsets of active Shape Key to every other shape key.** Same as `apply_active_to_basis`, but other Shape Keys will be also edited. It's like changing whole base mesh with all it's shape keys. If `keep_reverted` then old positions from Reference ShapeKey (Basis) will be moved to active Shape Key, so active Shape Key act as reverted. ` (Reverted)` will be added to it's name. If not `keep_reverted` then active Shape Key will be deleted. Returns: True if succeeded, False otherwise. Available as operator `OperatorApplyActiveToAll`. See also: `OperatorApplySelectedInActiveToAll`. """ # No context control mesh = obj.data # type: Mesh active_key = obj.active_shape_key ref_key = mesh.shape_keys.reference_key match_active = ensure_len_match(mesh, active_key, op=op) match_ref = ensure_len_match(mesh, ref_key, op=op) if not match_active or not match_ref: return False for other_key in mesh.shape_keys.key_blocks: if other_key == active_key or other_key == ref_key: continue if not ensure_len_match(mesh, other_key, op=op): continue for i in range(len(mesh.vertices)): other_offset = other_key.data[i].co - ref_key.data[i].co active_offset = active_key.data[i].co - ref_key.data[i].co other_key.data[i].co = ref_key.data[i].co + other_offset + active_offset for i in range(len(mesh.vertices)): v = ref_key.data[i].co.copy() ref_key.data[i].co = active_key.data[i].co.copy() active_key.data[i].co = v if keep_reverted: active_key.name += ' (Reverted)' else: obj.active_shape_key_index = 0 obj.shape_key_remove(active_key) return True
def apply_active_to_basis(obj: Object, keep_reverted=True, op: Operator = None)
-
Applies positions of active Shape Key to Reference ShapeKey (Basis). Positions (shapes) will be transferred from active Shape Key to Reference ShapeKey (Basis). Other Shape Keys keep their positions (shapes). If
keep_reverted
then old positions from Reference ShapeKey (Basis) will be transferred to active Shape Key, so active Shape Key act as reverted.(Reverted)
will be added to it's name. If notkeep_reverted
then active Shape Key will be deleted.Returns: True if succeeded, False otherwise.
Available as operator
OperatorApplyActiveToBasis
. See also:apply_active_to_all()
,OperatorApplySelectedInActiveToBasis
.Expand source code
def apply_active_to_basis(obj: 'Object', keep_reverted=True, op: 'Operator' = None): """ **Applies positions of active Shape Key to Reference ShapeKey (Basis).** Positions (shapes) will be transferred from active Shape Key to Reference ShapeKey (Basis). Other Shape Keys keep their positions (shapes). If `keep_reverted` then old positions from Reference ShapeKey (Basis) will be transferred to active Shape Key, so active Shape Key act as reverted. ` (Reverted)` will be added to it's name. If not `keep_reverted` then active Shape Key will be deleted. Returns: True if succeeded, False otherwise. Available as operator `OperatorApplyActiveToBasis`. See also: `apply_active_to_all`, `OperatorApplySelectedInActiveToBasis`. """ # No context control mesh = obj.data # type: Mesh active_key = obj.active_shape_key ref_key = mesh.shape_keys.reference_key match_active = ensure_len_match(mesh, active_key, op=op) match_ref = ensure_len_match(mesh, ref_key, op=op) if not match_active or not match_ref: return False for i in range(len(mesh.vertices)): v = ref_key.data[i].co.copy() ref_key.data[i].co = active_key.data[i].co.copy() active_key.data[i].co = v if keep_reverted: active_key.name += ' (Reverted)' else: obj.active_shape_key_index = 0 obj.shape_key_remove(active_key) return True
def cleanup_active(obj: Object, epsilon: float, op: Operator = None) ‑> int
-
Removes micro-offsets in active Shape Key. If position of a vertex differs from position in Reference Shape Key (Basis) for
epsilon
or less, then it's position will be reverted (to be the same as in Reference Shape Key)Returns: number of changed vertices.
Available as operator
OperatorCleanupActive
Expand source code
def cleanup_active(obj: 'Object', epsilon: 'float', op: 'Operator' = None) -> 'int': """ **Removes micro-offsets in active Shape Key.** If position of a vertex differs from position in Reference Shape Key (Basis) for `epsilon` or less, then it's position will be reverted (to be the same as in Reference Shape Key) Returns: number of changed vertices. Available as operator `OperatorCleanupActive` """ if not _obj_have_shapekeys(obj, n=2): return 0 mesh = obj.data # type: Mesh active_key = obj.active_shape_key ref_key = mesh.shape_keys.reference_key if active_key == ref_key: return 0 match_active = ensure_len_match(mesh, active_key, op=op) match_ref = ensure_len_match(mesh, ref_key, op=op) if not match_active or not match_ref: return 0 changed = 0 for i in range(len(mesh.vertices)): if (ref_key.data[i].co - active_key.data[i].co).magnitude <= epsilon: active_key.data[i].co = ref_key.data[i].co.copy() changed += 1 return changed
def cleanup_all(objs: Iterable[Object], epsilon: float, op: Operator = None) ‑> Tuple[int, int, int]
-
Removes micro-offsets in all Shape Keys. Same as
cleanup_active()
, but for every shape key (except reference one) for every object.Returns: (number of changed vertices, number of changed shape keys, number of changed meshes).
Available as operator
OperatorCleanupAll
Expand source code
def cleanup_all(objs: 'Iterable[Object]', epsilon: float, op: 'Operator' = None) -> 'Tuple[int, int, int]': """ **Removes micro-offsets in all Shape Keys.** Same as `cleanup_active`, but for every shape key (except reference one) for every object. Returns: (number of changed vertices, number of changed shape keys, number of changed meshes). Available as operator `OperatorCleanupAll` """ objs = list(obj for obj in objs if _obj_have_shapekeys(obj, n=2)) # type: List[Object] meshes = set() vertices_changed, shapekeys_changed, meshes_changed = 0, 0, 0 for obj in objs: mesh = obj.data # type: Mesh if mesh in meshes: continue # Уже трогали meshes.add(mesh) last_shape_key_index = obj.active_shape_key_index try: mesh_changed = False for shape_key_index in range(1, len(mesh.shape_keys.key_blocks)): obj.active_shape_key_index = shape_key_index vc = cleanup_active(obj, epsilon, op=op) if vc > 0: vertices_changed += vc shapekeys_changed += 1 mesh_changed = True if mesh_changed: meshes_changed += 1 finally: obj.active_shape_key_index = last_shape_key_index return vertices_changed, shapekeys_changed, meshes_changed
def ensure_len_match(mesh: Mesh, shape_key: ShapeKey, op: Operator = None)
-
Ensure
len(mesh.vertices) == len(shape_key.data)
. Helps to detect corrupted Mesh/Key datablocks.Expand source code
def ensure_len_match(mesh: 'Mesh', shape_key: 'ShapeKey', op: 'Operator' = None): """ Ensure `len(mesh.vertices) == len(shape_key.data)`. Helps to detect corrupted Mesh/Key datablocks. """ len_vts = len(mesh.vertices) len_skd = len(shape_key.data) if len_vts == len_skd: return True _log.error("Size of {0} ({1}) and size of {2} ({3}) does not match! Is shape key corrupted?" .format(repr(mesh.vertices), len_vts, repr(shape_key.data), len_skd), op=op) return False
def remove_empty(objs: Iterable[Object], epsilon: float, op: Operator = None) ‑> Tuple[int, int]
-
Removes empty Shape Keys from Mesh-objects. Shape Key is empty, if positions of every vertex differ from Reference Shape Key (Basis) for
epsilon
or less.Returns: (number of removed Shape Keys, number of meshes changed)
Available as operator
OperatorRemoveEmpty
Expand source code
def remove_empty(objs: 'Iterable[Object]', epsilon: float, op: 'Operator' = None) -> 'Tuple[int, int]': """ **Removes empty Shape Keys from Mesh-objects.** Shape Key is empty, if positions of **every** vertex differ from Reference Shape Key (Basis) for `epsilon` or less. Returns: (number of removed Shape Keys, number of meshes changed) Available as operator `OperatorRemoveEmpty` """ objs = list(obj for obj in objs if _obj_have_shapekeys(obj, n=2)) # type: List[Object] removed_shapekeys, changed_meshes = 0, 0 meshes = set() for obj in objs: mesh = obj.data # type: Mesh if mesh in meshes: continue # Уже трогали meshes.add(mesh) key = mesh.shape_keys # type: Key reference = key.reference_key empty_keys = set() # type: Set[str] for shape_key in key.key_blocks: if shape_key == reference: continue # Базис не удаялется match1 = ensure_len_match(mesh, reference, op=op) match2 = ensure_len_match(mesh, shape_key, op=op) if not match1 or not match2: continue # Имеются ли различия между шейпами? if not any((shape_key.data[i].co - reference.data[i].co).magnitude > epsilon for i in range(len(mesh.vertices))): empty_keys.add(shape_key.name) # _log.info("Found {0} empty shape keys in mesh {1}: {2}, removing...".format(len(empty_keys), repr(mesh), repr(empty_keys)), op=op) if len(empty_keys) < 1: continue for empty_key in empty_keys: # На всякий случай удаляю по прямому пути, вдруг там что-то перестраивается в процессе удаления. # Не спроста же Key-блоки можно редактировать только через Object-блоки obj.shape_key_remove(obj.data.shape_keys.key_blocks[empty_key]) removed_shapekeys += len(empty_keys) changed_meshes += 1 return removed_shapekeys, changed_meshes
Classes
class OperatorApplyActiveToAll
-
Operator of
apply_active_to_all()
. See also:OperatorApplySelectedInActiveToAll
.ID name:
kawa.apply_active_shape_keys_to_all
.Label:
APPLY ACTIVE Shape Key to ALL Others
.Description:
Same as 'APPLY ACTIVE Shape Key to Basis', but other Shape Keys will be also edited.
.Expand source code
class OperatorApplyActiveToAll(_KawaOperator): """ Operator of `apply_active_to_all`. See also: `OperatorApplySelectedInActiveToAll`. """ bl_idname = "kawa.apply_active_shape_keys_to_all" bl_label = "APPLY ACTIVE Shape Key to ALL Others" bl_description = "Same as {}, but other Shape Keys will be also edited.".format( repr(OperatorApplyActiveToBasis.bl_label)) bl_options = {'REGISTER', 'UNDO'} keep_reverted: _bpy.props.BoolProperty( name="Keep Reverted Shape Key", default=False, ) @classmethod def poll(cls, context: 'Context'): obj = cls.get_active_obj(context) if not obj or obj.type != 'MESH': return False # Требуется активный меш-объект if not obj.active_shape_key or obj.active_shape_key_index == 0: return False # Требуется что бы был активный не первый шейпкей if context.mode != 'OBJECT': return False # Требуется режим OBJECT return True def invoke(self, context: 'Context', event): return context.window_manager.invoke_props_dialog(self) def execute(self, context: 'Context'): return {'FINISHED'} if apply_active_to_all(self.get_active_obj(context), keep_reverted=self.keep_reverted, op=self) else {'CANCELLED'}
Ancestors
- kawa_scripts._internals.KawaOperator
- bpy.types.Operator
- bpy.types.bpy_struct
Class variables
var keep_reverted : None
class OperatorApplyActiveToBasis
-
Operator of
apply_active_to_basis()
. See also:OperatorApplySelectedInActiveToBasis
.ID name:
kawa.apply_active_shape_keys_to_basis
.Label:
APPLY ACTIVE Shape Key to Basis
.Description:
Positions (shapes) will be transferred from active Shape Key to Reference ShapeKey (Basis). Other Shape Keys keep their positions (shapes).
.Expand source code
class OperatorApplyActiveToBasis(_KawaOperator): """ Operator of `apply_active_to_basis`. See also: `OperatorApplySelectedInActiveToBasis`. """ bl_idname = "kawa.apply_active_shape_keys_to_basis" bl_label = "APPLY ACTIVE Shape Key to Basis" bl_description = "\n".join(( "Positions (shapes) will be transferred from active Shape Key to Reference ShapeKey (Basis).", "Other Shape Keys keep their positions (shapes).", )) bl_options = {'REGISTER', 'UNDO'} keep_reverted: _bpy.props.BoolProperty( name="Keep Reverted Shape Key", default=True, ) @classmethod def poll(cls, context: 'Context'): obj = cls.get_active_obj(context) if not obj or obj.type != 'MESH': return False # Требуется активный меш-объект if not obj.active_shape_key or obj.active_shape_key_index == 0: return False # Требуется что бы был активный не первый шейпкей if context.mode != 'OBJECT': return False # Требуется режим OBJECT return True def invoke(self, context: 'Context', event): return context.window_manager.invoke_props_dialog(self) def execute(self, context: 'Context'): return {'FINISHED'} if apply_active_to_basis(self.get_active_obj(context), keep_reverted=self.keep_reverted, op=self) else {'CANCELLED'}
Ancestors
- kawa_scripts._internals.KawaOperator
- bpy.types.Operator
- bpy.types.bpy_struct
Class variables
var keep_reverted : None
class OperatorApplySelectedInActiveToAll
-
Same as
OperatorApplyActiveToAll
, but only for selected vertices in edit-mode. See also:apply_active_to_all()
.ID name:
kawa.apply_selected_shape_keys_in_active_to_all
.Label:
APPLY SELECTED Vertices in ACTIVE Shape Key to ALL Others
.Description:
Same as 'APPLY ACTIVE Shape Key to ALL Others', but only for selected vertices in edit-mode.
.Expand source code
class OperatorApplySelectedInActiveToAll(_KawaOperator): """ Same as `OperatorApplyActiveToAll`, but only for selected vertices in edit-mode. See also: `apply_active_to_all`. """ bl_idname = "kawa.apply_selected_shape_keys_in_active_to_all" bl_label = "APPLY SELECTED Vertices in ACTIVE Shape Key to ALL Others" # bl_description at the end of file. bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context: 'Context'): obj = cls.get_active_obj(context) if not obj or obj.type != 'MESH': return False # Требуется активный меш-объект if not obj.active_shape_key or obj.active_shape_key_index == 0: return False # Требуется что бы был активный не первый шейпкей if context.mode != 'EDIT_MESH': return False # Требуется режим EDIT_MESH return True def execute(self, context: 'Context'): obj = self.get_active_obj(context) # Рофл в том, что операции над мешью надо проводить вне эдит-мода _bpy.ops.object.mode_set(mode='OBJECT', toggle=False) mesh = obj.data # type: Mesh active_key = obj.active_shape_key ref_key = mesh.shape_keys.reference_key match_active = ensure_len_match(mesh, active_key, op=self) match_ref = ensure_len_match(mesh, ref_key, op=self) if not match_active or not match_ref: return {'CANCELLED'} _mesh_selection_to_vertices(mesh) for other_key in mesh.shape_keys.key_blocks: if other_key == active_key or other_key == ref_key: continue if not ensure_len_match(mesh, other_key, op=self): continue for i in range(len(mesh.vertices)): if mesh.vertices[i].select: other_offset = other_key.data[i].co - ref_key.data[i].co active_offset = active_key.data[i].co - ref_key.data[i].co other_key.data[i].co = ref_key.data[i].co + other_offset + active_offset for i in range(len(mesh.vertices)): if mesh.vertices[i].select: ref_key.data[i].co = active_key.data[i].co.copy() _bpy.ops.object.mode_set_with_submode(mode='EDIT', toggle=False, mesh_select_mode={'VERT'}) return {'FINISHED'}
Ancestors
- kawa_scripts._internals.KawaOperator
- bpy.types.Operator
- bpy.types.bpy_struct
class OperatorApplySelectedInActiveToBasis
-
Same as
OperatorApplyActiveToBasis
, but only for selected vertices in edit-mode. See also:apply_active_to_basis()
.ID name:
kawa.apply_selected_shape_keys_in_active_to_basis
.Label:
APPLY SELECTED Vertices in ACTIVE Shape Key to Basis
.Description:
Same as 'APPLY ACTIVE Shape Key to Basis', but only for selected vertices in edit-mode.
.Expand source code
class OperatorApplySelectedInActiveToBasis(_KawaOperator): """ Same as `OperatorApplyActiveToBasis`, but only for selected vertices in edit-mode. See also: `apply_active_to_basis`. """ bl_idname = "kawa.apply_selected_shape_keys_in_active_to_basis" bl_label = "APPLY SELECTED Vertices in ACTIVE Shape Key to Basis" # bl_description at the end of file. bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context: 'Context'): obj = cls.get_active_obj(context) if not obj or obj.type != 'MESH': return False # Требуется активный меш-объект if not obj.active_shape_key or obj.active_shape_key_index == 0: return False # Требуется что бы был активный не первый шейпкей if context.mode != 'EDIT_MESH': return False # Требуется режим EDIT_MESH return True def execute(self, context: 'Context'): obj = self.get_active_obj(context) # Рофл в том, что операции над мешью надо проводить вне эдит-мода _bpy.ops.object.mode_set(mode='OBJECT', toggle=False) mesh = obj.data # type: Mesh active_key = obj.active_shape_key ref_key = mesh.shape_keys.reference_key match_active = ensure_len_match(mesh, active_key, op=self) match_ref = ensure_len_match(mesh, ref_key, op=self) if not match_active or not match_ref: return {'CANCELLED'} _mesh_selection_to_vertices(mesh) for i in range(len(mesh.vertices)): if mesh.vertices[i].select: ref_key.data[i].co = active_key.data[i].co.copy() _bpy.ops.object.mode_set_with_submode(mode='EDIT', toggle=False, mesh_select_mode={'VERT'}) return {'FINISHED'}
Ancestors
- kawa_scripts._internals.KawaOperator
- bpy.types.Operator
- bpy.types.bpy_struct
class OperatorCleanupActive
-
Operator of
cleanup_active()
ID name:
kawa.cleanup_active_shape_key
.Label:
Remove Mirco-offsets in ACTIVE Shape Key
.Description:
If position of a vertex differs from position in Reference Shape Key (Basis) for
epsilonor less, then it's position will be reverted (to be the same as in Reference Shape Key)
.Expand source code
class OperatorCleanupActive(_KawaOperator): """ Operator of `cleanup_active` """ bl_idname = "kawa.cleanup_active_shape_key" bl_label = "Remove Mirco-offsets in ACTIVE Shape Key" bl_description = "\n".join(( "If position of a vertex differs from position in Reference Shape Key (Basis) for `epsilon` or less,", "then it's position will be reverted (to be the same as in Reference Shape Key)", )) bl_options = {'REGISTER', 'UNDO'} epsilon: _bpy.props.FloatProperty( name="Epsilon", description="Threshold in local space", min=1e-07, default=1e-04, max=1, precision=6, unit='LENGTH' ) @classmethod def poll(cls, context: 'Context'): obj = cls.get_active_obj(context) if not obj or obj.type != 'MESH': return False # Требуется активный меш-объект if not obj.active_shape_key or obj.active_shape_key_index == 0: return False # Требуется что бы был активный не первый шейпкей if context.mode != 'OBJECT': return False # Требуется режим OBJECT return True def invoke(self, context: 'Context', event): return context.window_manager.invoke_props_dialog(self) def execute(self, context: 'Context'): changed = cleanup_active(self.get_active_obj(context), self.epsilon, op=self) _log.info("Cleaned {0} vertices.".format(changed), op=self) return {'FINISHED'} if changed > 0 else {'CANCELLED'}
Ancestors
- kawa_scripts._internals.KawaOperator
- bpy.types.Operator
- bpy.types.bpy_struct
Class variables
var epsilon : None
class OperatorCleanupAll
-
Operator of
cleanup_all()
ID name:
kawa.cleanup_all_shape_keys
.Label:
Remove Mirco-offsets in ALL Shape Keys
.Description:
Same as 'Remove Mirco-offsets in ACTIVE Shape Key', but for every shape key (except reference one) for every object
.Expand source code
class OperatorCleanupAll(_KawaOperator): """ Operator of `cleanup_all` """ bl_idname = "kawa.cleanup_all_shape_keys" bl_label = "Remove Mirco-offsets in ALL Shape Keys" bl_description = "Same as {}, but for every shape key (except reference one) for every object".format( repr(OperatorCleanupActive.bl_label)) bl_options = {'REGISTER', 'UNDO'} epsilon: _bpy.props.FloatProperty( name="Epsilon", description="Threshold in local space", min=1e-07, default=1e-04, max=1, precision=6, unit='LENGTH' ) @classmethod def poll(cls, context: 'Context'): if context.mode != 'OBJECT': return False # Требуется режим OBJECT if not any(True for obj in cls.get_selected_objs(context) if _obj_have_shapekeys(obj, n=2)): return False # Должны быть выбраны Меш-объекты c 2 или более шейпами return True def invoke(self, context: 'Context', event): return context.window_manager.invoke_props_dialog(self) def execute(self, context: 'Context'): selected = list(self.get_selected_objs(context)) if len(selected) < 1: _log.warning("No mesh-objects with multiple shape keys selected.", op=self) return {'CANCELLED'} vertices_cleaned, shapekeys_cleaned, objects_cleaned = cleanup_all(selected, self.epsilon, op=self) _log.info("Cleaned {0} Vertices in {1} Shape Keys in {2} Meshes from micro-offsets (<{3}).".format( vertices_cleaned, shapekeys_cleaned, objects_cleaned, float(self.epsilon)), op=self) return {'FINISHED'} if vertices_cleaned > 0 else {'CANCELLED'}
Ancestors
- kawa_scripts._internals.KawaOperator
- bpy.types.Operator
- bpy.types.bpy_struct
Class variables
var epsilon : None
class OperatorRemoveEmpty
-
Operator of
remove_empty()
.ID name:
kawa.remove_empty_shape_keys
.Label:
Remove Empty Shape Keys
.Description:
Shape Key is empty, if positions of EVERY vertex differ from Reference Shape Key (Basis) for Epsilon or less.
.Expand source code
class OperatorRemoveEmpty(_KawaOperator): """ Operator of `remove_empty`. """ bl_idname = "kawa.remove_empty_shape_keys" bl_label = "Remove Empty Shape Keys" bl_description = "\n".join(( "Shape Key is empty, if positions of EVERY vertex differ ", "from Reference Shape Key (Basis) for Epsilon or less.", )) bl_options = {'REGISTER', 'UNDO'} epsilon: _bpy.props.FloatProperty( name="Epsilon", description="Selection precision in local space", min=1e-07, default=1e-06, max=1, precision=6, unit='LENGTH' ) @classmethod def poll(cls, context: 'Context'): if context.mode != 'OBJECT': return False # Требуется режим OBJECT if not any(True for obj in cls.get_selected_objs(context) if _obj_have_shapekeys(obj, n=2)): return False # Должны быть выбраны какие-то Меш-объекты return True def invoke(self, context: 'Context', event): return context.window_manager.invoke_props_dialog(self) def execute(self, context: 'Context'): objs = self.get_selected_objs(context) removed_shapekeys, changed_meshes = remove_empty(objs, self.epsilon, op=self) _log.info("Total {0} shape keys removed from {1} Meshes.".format(removed_shapekeys, changed_meshes), op=self) return {'FINISHED'} if removed_shapekeys > 0 else {'CANCELLED'}
Ancestors
- kawa_scripts._internals.KawaOperator
- bpy.types.Operator
- bpy.types.bpy_struct
Class variables
var epsilon : None
class OperatorRevertSelectedInActiveToBasis
-
Revert selected vertices in edit-mode to Reference Shape Key (Basis) in active Shape Key.
ID name:
kawa.revert_selected_shape_keys_in_active_to_basis
.Label:
REVERT SELECTED Vertices in ACTIVE Shape Key to BASIS
.Description:
Revert selected vertices in edit-mode to Reference Shape Key (Basis) in active Shape Key.
.Expand source code
class OperatorRevertSelectedInActiveToBasis(_KawaOperator): """ **Revert selected vertices in edit-mode to Reference Shape Key (Basis) in active Shape Key.** """ bl_idname = "kawa.revert_selected_shape_keys_in_active_to_basis" bl_label = "REVERT SELECTED Vertices in ACTIVE Shape Key to BASIS" bl_description = "Revert selected vertices in edit-mode to Reference Shape Key (Basis) in active Shape Key." bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context: 'Context'): obj = cls.get_active_obj(context) if not obj or obj.type != 'MESH': return False # Требуется активный меш-объект if not obj.active_shape_key or obj.active_shape_key_index == 0: return False # Требуется что бы был активный не первый шейпкей if context.mode != 'EDIT_MESH': return False # Требуется режим EDIT_MESH return True def execute(self, context: 'Context'): obj = self.get_active_obj(context) # Рофл в том, что операции над мешью надо проводить вне эдит-мода _bpy.ops.object.mode_set(mode='OBJECT', toggle=False) mesh = obj.data # type: Mesh shape_key = obj.active_shape_key reference = mesh.shape_keys.reference_key match_skd = ensure_len_match(mesh, shape_key, op=self) match_ref = ensure_len_match(mesh, reference, op=self) if not match_skd or not match_ref: return {'CANCELLED'} _mesh_selection_to_vertices(mesh) for i in range(len(mesh.vertices)): if mesh.vertices[i].select: shape_key.data[i].co = reference.data[i].co.copy() _bpy.ops.object.mode_set_with_submode(mode='EDIT', toggle=False, mesh_select_mode={'VERT'}) return {'FINISHED'}
Ancestors
- kawa_scripts._internals.KawaOperator
- bpy.types.Operator
- bpy.types.bpy_struct
class OperatorRevertSelectedInAllToBasis
-
Revert selected vertices in edit-mode to Reference Shape Key (Basis) in every Shape Key.
ID name:
kawa.revert_selected_shape_keys_in_all_to_basis
.Label:
REVERT SELECTED Vertices in ALL Shape Keys to BASIS
.Description:
Revert selected vertices in edit-mode to Reference Shape Key (Basis) in every Shape Key.
.Expand source code
class OperatorRevertSelectedInAllToBasis(_KawaOperator): """ **Revert selected vertices in edit-mode to Reference Shape Key (Basis) in every Shape Key.** """ bl_idname = "kawa.revert_selected_shape_keys_in_all_to_basis" bl_label = "REVERT SELECTED Vertices in ALL Shape Keys to BASIS" bl_description = "Revert selected vertices in edit-mode to Reference Shape Key (Basis) in every Shape Key." bl_options = {'REGISTER', 'UNDO'} @classmethod def poll(cls, context: 'Context'): obj = cls.get_active_obj(context) if not obj or obj.type != 'MESH': return False # Требуется активный меш-объект data = obj.data # type: Mesh if data.shape_keys is None or len(data.shape_keys.key_blocks) < 2: return False # Требуется что бы было 2 или более шейпкея if context.mode != 'EDIT_MESH': return False # Требуется режим EDIT_MESH return True def execute(self, context: 'Context'): obj = self.get_active_obj(context) # Рофл в том, что операции над мешью надо проводить вне эдит-мода _bpy.ops.object.mode_set(mode='OBJECT', toggle=False) mesh = obj.data # type: Mesh reference = mesh.shape_keys.reference_key if not ensure_len_match(mesh, reference, op=self): return {'CANCELLED'} _mesh_selection_to_vertices(mesh) for shape_key in mesh.shape_keys.key_blocks: if shape_key == reference: continue if not ensure_len_match(mesh, shape_key, op=self): continue for i in range(len(mesh.vertices)): if mesh.vertices[i].select: shape_key.data[i].co = reference.data[i].co.copy() _bpy.ops.object.mode_set_with_submode(mode='EDIT', toggle=False, mesh_select_mode={'VERT'}) return {'FINISHED'}
Ancestors
- kawa_scripts._internals.KawaOperator
- bpy.types.Operator
- bpy.types.bpy_struct
class OperatorSelectVerticesAffectedByShapeKey
-
Select Vertices Affected by Active Shape Key.
ID name:
kawa.select_vertices_affected_by_shape_key
.Label:
Select Vertices Affected by Active Shape Key
.Description:
Select Vertices Affected by Active Shape Key.
.Expand source code
class OperatorSelectVerticesAffectedByShapeKey(_KawaOperator): """ **Select Vertices Affected by Active Shape Key.** """ bl_idname = "kawa.select_vertices_affected_by_shape_key" bl_label = "Select Vertices Affected by Active Shape Key" bl_description = "Select Vertices Affected by Active Shape Key." bl_options = {'REGISTER', 'UNDO'} epsilon: _bpy.props.FloatProperty( name="Epsilon", description="Selection precision in local space", min=1e-07, default=1e-06, max=1, precision=6, unit='LENGTH' ) @classmethod def poll(cls, context: 'Context'): obj = cls.get_active_obj(context) if not obj or obj.type != 'MESH': return False # Требуется активный меш-объект if not obj.active_shape_key or obj.active_shape_key_index == 0: return False # Требуется что бы был активный не первый шейпкей if context.mode != 'EDIT_MESH' and context.mode != 'OBJECT': return False # Требуется режим OBJECT или EDIT_MESH return True def invoke(self, context: 'Context', event): wm = context.window_manager # return wm.invoke_props_popup(self, event) # return {'RUNNING_MODAL'} return wm.invoke_props_dialog(self) def execute(self, context: 'Context'): # Рофл в том, что операции над мешью надо проводить вне эдит-мода _bpy.ops.object.mode_set(mode='OBJECT', toggle=False) obj = self.get_active_obj(context) mesh = obj.data # type: Mesh shape_key = obj.active_shape_key reference = mesh.shape_keys.reference_key match_skd = ensure_len_match(mesh, shape_key, op=self) match_ref = ensure_len_match(mesh, reference, op=self) if not match_skd or not match_ref: return {'CANCELLED'} for p in mesh.polygons: p.select = False for e in mesh.edges: e.select = False counter = 0 for i in range(len(mesh.vertices)): mesh.vertices[i].select = (shape_key.data[i].co - reference.data[i].co).magnitude > self.epsilon counter += 1 _log.info("Selected {0} vertices affected by {1} in {2}".format(counter, repr(shape_key), repr(obj))) _bpy.ops.object.mode_set_with_submode(mode='EDIT', toggle=False, mesh_select_mode={'VERT'}) return {'FINISHED'}
Ancestors
- kawa_scripts._internals.KawaOperator
- bpy.types.Operator
- bpy.types.bpy_struct
Class variables
var epsilon : None