[Bf-extensions-cvs] [046114b9] master: Rigify: add utility and operator classes for the upcoming face rigs.

Alexander Gavrilov noreply at git.blender.org
Tue Jul 6 20:57:57 CEST 2021


Commit: 046114b96a1c369886a55de0bf958bf7ac2ae5a1
Author: Alexander Gavrilov
Date:   Tue Jul 6 21:50:41 2021 +0300
Branches: master
https://developer.blender.org/rBA046114b96a1c369886a55de0bf958bf7ac2ae5a1

Rigify: add utility and operator classes for the upcoming face rigs.

- LazyRef utility class that provides a hashable field reference.
- NodeMerger plugin for grouping abstract points by distance.
- pose.rigify_copy_single_parameter operator for copying a single
  property to all selected rigs that inherit from a specific class
  (intended to be used via property panel buttons).

===================================================================

M	rigify/__init__.py
A	rigify/operators/__init__.py
A	rigify/operators/copy_mirror_parameters.py
M	rigify/rig_lists.py
M	rigify/utils/misc.py
M	rigify/utils/naming.py
A	rigify/utils/node_merger.py

===================================================================

diff --git a/rigify/__init__.py b/rigify/__init__.py
index e91fca51..1bb633f6 100644
--- a/rigify/__init__.py
+++ b/rigify/__init__.py
@@ -64,6 +64,7 @@ initial_load_order = [
     'rig_ui_template',
     'generate',
     'rot_mode',
+    'operators',
     'ui',
 ]
 
@@ -449,6 +450,7 @@ def register():
     ui.register()
     feature_set_list.register()
     metarig_menu.register()
+    operators.register()
 
     # Classes.
     for cls in classes:
@@ -597,6 +599,7 @@ def unregister():
     clear_rigify_parameters()
 
     # Sub-modules.
+    operators.unregister()
     metarig_menu.unregister()
     ui.unregister()
     feature_set_list.unregister()
diff --git a/rigify/operators/__init__.py b/rigify/operators/__init__.py
new file mode 100644
index 00000000..4263c8dd
--- /dev/null
+++ b/rigify/operators/__init__.py
@@ -0,0 +1,47 @@
+# ====================== BEGIN GPL LICENSE BLOCK ======================
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ======================= END GPL LICENSE BLOCK ========================
+
+# <pep8 compliant>
+
+import importlib
+
+
+# Submodules to load during register
+submodules = (
+    'copy_mirror_parameters',
+)
+
+loaded_submodules = []
+
+
+def register():
+    # Lazily load modules to make reloading easier. Loading this way
+    # hides the sub-modules and their dependencies from initial_load_order.
+    loaded_submodules[:] = [
+        importlib.import_module(__name__ + '.' + name) for name in submodules
+    ]
+
+    for mod in loaded_submodules:
+        mod.register()
+
+
+def unregister():
+    for mod in reversed(loaded_submodules):
+        mod.unregister()
+
+    loaded_submodules.clear()
diff --git a/rigify/operators/copy_mirror_parameters.py b/rigify/operators/copy_mirror_parameters.py
new file mode 100644
index 00000000..95818ba8
--- /dev/null
+++ b/rigify/operators/copy_mirror_parameters.py
@@ -0,0 +1,129 @@
+# ====================== BEGIN GPL LICENSE BLOCK ======================
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ======================= END GPL LICENSE BLOCK ========================
+
+# <pep8 compliant>
+
+import bpy
+import importlib
+
+from ..utils.naming import Side, get_name_base_and_sides, mirror_name
+
+from ..utils.rig import get_rigify_type
+from ..rig_lists import get_rig_class
+
+
+# =============================================
+# Single parameter copy button
+
+class POSE_OT_rigify_copy_single_parameter(bpy.types.Operator):
+    bl_idname = "pose.rigify_copy_single_parameter"
+    bl_label = "Copy Option To Selected Rigs"
+    bl_description = "Copy this property value to all selected rigs of the appropriate type"
+    bl_options = {'UNDO', 'INTERNAL'}
+
+    property_name: bpy.props.StringProperty(name='Property Name')
+    mirror_bone: bpy.props.BoolProperty(name='Mirror As Bone Name')
+
+    module_name: bpy.props.StringProperty(name='Module Name')
+    class_name: bpy.props.StringProperty(name='Class Name')
+
+    @classmethod
+    def poll(cls, context):
+        return (
+            context.active_object and context.active_object.type == 'ARMATURE'
+            and context.active_pose_bone
+            and context.active_object.data.get('rig_id') is None
+            and get_rigify_type(context.active_pose_bone)
+        )
+
+    def invoke(self, context, event):
+        return context.window_manager.invoke_confirm(self, event)
+
+    def execute(self, context):
+        try:
+            module = importlib.import_module(self.module_name)
+            filter_rig_class = getattr(module, self.class_name)
+        except (KeyError, AttributeError, ImportError):
+            self.report(
+                {'ERROR'}, f"Cannot find class {self.class_name} in {self.module_name}")
+            return {'CANCELLED'}
+
+        active_pbone = context.active_pose_bone
+        active_split = get_name_base_and_sides(active_pbone.name)
+
+        value = getattr(active_pbone.rigify_parameters, self.property_name)
+        num_copied = 0
+
+        # Copy to different bones of appropriate rig types
+        for sel_pbone in context.selected_pose_bones:
+            rig_type = get_rigify_type(sel_pbone)
+
+            if rig_type and sel_pbone != active_pbone:
+                rig_class = get_rig_class(rig_type)
+
+                if rig_class and issubclass(rig_class, filter_rig_class):
+                    new_value = value
+
+                    # If mirror requested and copying to a different side bone, mirror the value
+                    if self.mirror_bone and active_split.side != Side.MIDDLE and value:
+                        sel_split = get_name_base_and_sides(sel_pbone.name)
+
+                        if sel_split.side == -active_split.side:
+                            new_value = mirror_name(value)
+
+                    # Assign the final value
+                    setattr(sel_pbone.rigify_parameters,
+                            self.property_name, new_value)
+                    num_copied += 1
+
+        if num_copied:
+            self.report({'INFO'}, f"Copied the value to {num_copied} bones.")
+            return {'FINISHED'}
+        else:
+            self.report({'WARNING'}, "No suitable selected bones to copy to.")
+            return {'CANCELLED'}
+
+
+def make_copy_parameter_button(layout, property_name, *, base_class, mirror_bone=False):
+    """Displays a button that copies the property to selected rig of the specified base type."""
+    props = layout.operator(
+        POSE_OT_rigify_copy_single_parameter.bl_idname, icon='DUPLICATE', text='')
+    props.property_name = property_name
+    props.mirror_bone = mirror_bone
+    props.module_name = base_class.__module__
+    props.class_name = base_class.__name__
+
+
+# =============================================
+# Registration
+
+classes = (
+    POSE_OT_rigify_copy_single_parameter,
+)
+
+
+def register():
+    from bpy.utils import register_class
+    for cls in classes:
+        register_class(cls)
+
+
+def unregister():
+    from bpy.utils import unregister_class
+    for cls in classes:
+        unregister_class(cls)
diff --git a/rigify/rig_lists.py b/rigify/rig_lists.py
index 49cc9545..ae7d9d00 100644
--- a/rigify/rig_lists.py
+++ b/rigify/rig_lists.py
@@ -80,6 +80,12 @@ def get_rigs(base_dir, base_path, *, path=[], feature_set=feature_set_list.DEFAU
 rigs = {}
 implementation_rigs = {}
 
+def get_rig_class(name):
+    try:
+        return rigs[name]["module"].Rig
+    except (KeyError, AttributeError):
+        return None
+
 def get_internal_rigs():
     global rigs, implementation_rigs
 
diff --git a/rigify/utils/misc.py b/rigify/utils/misc.py
index 20fd6a08..e4ba55f2 100644
--- a/rigify/utils/misc.py
+++ b/rigify/utils/misc.py
@@ -153,17 +153,60 @@ def map_apply(func, *inputs):
 
 
 #=============================================
-# Misc
+# Lazy references
 #=============================================
 
 
 def force_lazy(value):
+    """If the argument is callable, invokes it without arguments. Otherwise returns the argument as is."""
     if callable(value):
         return value()
     else:
         return value
 
 
+class LazyRef:
+    """Hashable lazy reference. When called, evaluates (foo, 'a', 'b'...) as foo('a','b')
+    if foo is callable. Otherwise the remaining arguments are used as attribute names or
+    keys, like foo.a.b or foo.a[b] etc."""
+
+    def __init__(self, first, *args):
+        self.first = first
+        self.args = tuple(args)
+        self.first_hashable = first.__hash__ is not None
+
+    def __repr__(self):
+        return 'LazyRef{}'.format(tuple(self.first, *self.args))
+
+    def __eq__(self, other):
+        return (
+            isinstance(other, LazyRef) and
+            (self.first == other.first if self.first_hashable else self.first is other.first) and
+            self.args == other.args
+        )
+
+    def __hash__(self):
+        return (hash(self.first) if self.first_hashable else hash(id(self.first))) ^ hash(self.args)
+
+    def __call__(self):
+        first = self.first
+        if callable(first):
+            return first(*self.args)
+
+        for item in self.args:
+            if isinstance(first, (dict, list)):
+                first = first[item]
+            else:
+                first = getattr(first, item)
+
+        return first
+
+
+#=============================================
+# Misc
+#=============================================
+
+
 def copy_attributes(a, b):
     keys = dir(a)
     for key in keys:
diff --git a/rigify/utils/naming.py b/rigify/utils/naming.py
index 6c54b988..a713e407 100644
--- a/rigify/utils/naming.py
+++ b/rigify/utils/naming.py
@@ -162,6 +162,9 @@ class SideZ(enum.IntEnum):
         return combine_name(parts, side_z=new_side)
 
 
+NameSides = collections.namedtuple('NameSides', ['base', 'side', 'side_z'])
+
+
 def get_name_side(name):
    

@@ Diff output truncated at 10240 characters. @@



More information about the Bf-extensions-cvs mailing list