Skip to content

Commit 67f7b69

Browse files
committed
Support declaring new callback methods via stage decorators.
Making generic rig 'template' classes that are intended to be subclassed in specific rigs involves splitting operations done in each stage into multiple methods that can be overridden separately. The main callback thus ends up simply calling a sequence of other methods. To make such code cleaner it's better to allow registering those methods as new callbacks that would be automatically called by the system. This can be done via decorators. A new metaclass used for all rig and generate plugin classes builds and validates a table of all decorated methods, and allows calling them all together with the main callback.
1 parent cb41b0f commit 67f7b69

File tree

3 files changed

+140
-6
lines changed

3 files changed

+140
-6
lines changed

base_generate.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ def __run_object_stage(self, method_name):
171171
self.stage = method_name
172172

173173
for rig in [*self.rig_list, *self.plugin_list]:
174-
getattr(rig, method_name)()
174+
rig.rigify_invoke_stage(method_name)
175175

176176
assert(self.context.active_object == self.obj)
177177
assert(self.obj.mode == 'OBJECT')
@@ -186,7 +186,7 @@ def __run_edit_stage(self, method_name):
186186
self.stage = method_name
187187

188188
for rig in [*self.rig_list, *self.plugin_list]:
189-
getattr(rig, method_name)()
189+
rig.rigify_invoke_stage(method_name)
190190

191191
assert(self.context.active_object == self.obj)
192192
assert(self.obj.mode == 'EDIT')
@@ -221,15 +221,15 @@ def invoke_generate_bones(self):
221221
self.stage = 'generate_bones'
222222

223223
for rig in self.rig_list:
224-
rig.generate_bones()
224+
rig.rigify_invoke_stage('generate_bones')
225225

226226
assert(self.context.active_object == self.obj)
227227
assert(self.obj.mode == 'EDIT')
228228

229229
self.__auto_register_bones(self.obj.data.edit_bones, rig)
230230

231231
for plugin in self.plugin_list:
232-
plugin.generate_bones()
232+
plugin.rigify_invoke_stage('generate_bones')
233233

234234
assert(self.context.active_object == self.obj)
235235
assert(self.obj.mode == 'EDIT')

base_rig.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424

2525
from .utils.bones import BoneDict, BoneUtilityMixin
2626
from .utils.mechanism import MechanismUtilityMixin
27+
from .utils.metaclass import BaseStagedClass
2728

2829
# Only export certain symbols via 'from base_rig import *'
2930
__all__ = ['BaseRig', 'RigUtility']
@@ -32,7 +33,7 @@
3233
# Base Rig
3334
#=============================================
3435

35-
class GenerateCallbackMixin(object):
36+
class GenerateCallbackMixin(BaseStagedClass):
3637
"""
3738
Standard set of callback methods to redefine.
3839
Shared between BaseRig and GeneratorPlugin.
@@ -43,7 +44,27 @@ class GenerateCallbackMixin(object):
4344
Switching modes is not allowed in rigs for performance
4445
reasons. Place code in the appropriate callbacks to use
4546
the mode set by the main engine.
47+
48+
Before each callback, all other methods decorated with
49+
@stage_<method_name> are called, for instance:
50+
51+
@stage_generate_bones
52+
def foo(self):
53+
print('first')
54+
55+
def generate_bones(self):
56+
print('second')
57+
58+
Will print 'first', then 'second'. However, the order
59+
in which different @stage_generate_bones methods in the
60+
same rig will be called is not specified.
61+
62+
When overriding such methods in a subclass the appropriate
63+
decorator should be repeated for code clarity reasons;
64+
a warning is printed if this is not done.
4665
"""
66+
DEFINE_STAGES = True
67+
4768
def initialize(self):
4869
"""
4970
Initialize processing after all rig classes are constructed.
@@ -203,3 +224,11 @@ def register_new_bone(self, new_name, old_name=None):
203224
self.owner.register_new_bone(new_name, old_name)
204225

205226

227+
#=============================================
228+
# Rig Stage Decorators
229+
#=============================================
230+
231+
# Generate and export @stage_... decorators for all valid stages
232+
for name, decorator in GenerateCallbackMixin.make_stage_decorators():
233+
globals()[name] = decorator
234+
__all__.append(name)

utils/metaclass.py

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,118 @@
2020

2121
import collections
2222

23+
from types import FunctionType
24+
25+
26+
#=============================================
27+
# Class With Stages
28+
#=============================================
29+
30+
31+
def rigify_stage(stage):
32+
"""Decorates the method with the specified stage."""
33+
def process(method):
34+
if not isinstance(method, FunctionType):
35+
raise ValueError("Stage decorator must be applied to a method definition")
36+
method._rigify_stage = stage
37+
return method
38+
return process
39+
40+
41+
class StagedMetaclass(type):
42+
"""
43+
Metaclass for rigs that manages assignment of methods to stages via @stage_* decorators.
44+
45+
Using 'DEFINE_STAGES = True' inside the class definition will register all non-system
46+
method names from that definition as valid stages. After that, subclasses can
47+
register methods to those stages, to be called via rigify_invoke_stage.
48+
"""
49+
def __new__(metacls, class_name, bases, namespace, **kwds):
50+
staged_bases = [base for base in bases if isinstance(base, StagedMetaclass)]
51+
52+
# Compute the set of inherited stages
53+
stages = set().union(*[base._rigify_stages for base in staged_bases])
54+
55+
# Add methods from current class if requested
56+
if 'DEFINE_STAGES' in namespace:
57+
del namespace['DEFINE_STAGES']
58+
59+
for name, item in namespace.items():
60+
if name[0] != '_' and isinstance(item, FunctionType):
61+
stages.add(name)
62+
63+
# Create the class
64+
result = type.__new__(metacls, class_name, bases, dict(namespace))
65+
66+
# Compute the inherited stage to method mapping
67+
stage_map = collections.defaultdict(collections.OrderedDict)
68+
method_map = {}
69+
70+
for base in staged_bases:
71+
for stage_name, methods in base._rigify_stage_map.items():
72+
for method_name, method_class in methods.items():
73+
# Check consistency of inherited stage assignment to methods
74+
if method_name in method_map:
75+
if method_map[method_name] != stage_name:
76+
print("RIGIFY CLASS %s (%s): method '%s' has inherited both @stage_%s and @stage_%s\n" %
77+
(class_name, result.__module__, method_name, method_map[method_name], stage_name))
78+
else:
79+
method_map[method_name] = stage_name
80+
81+
stage_map[stage_name][method_name] = method_class
82+
83+
# Scan newly defined methods for stage decorations
84+
for method_name, item in namespace.items():
85+
if isinstance(item, FunctionType):
86+
stage = getattr(item, '_rigify_stage', None)
87+
88+
# Ensure that decorators aren't lost when redefining methods
89+
if method_name in method_map:
90+
if not stage:
91+
stage = method_map[method_name]
92+
print("RIGIFY CLASS %s (%s): missing stage decorator on method '%s' (should be @stage_%s)" %
93+
(class_name, result.__module__, method_name, stage))
94+
# Check that the method is assigned to only one stage
95+
elif stage != method_map[method_name]:
96+
print("RIGIFY CLASS %s (%s): method '%s' has decorator @stage_%s, but inherited base has @stage_%s" %
97+
(class_name, result.__module__, method_name, stage, method_map[method_name]))
98+
99+
# Assign the method to the stage, verifying that it's valid
100+
if stage:
101+
if stage not in stages:
102+
raise ValueError("Invalid stage name '%s' for method '%s' in class %s (%s)" %
103+
(stage, method_name, class_name, result.__module__))
104+
else:
105+
stage_map[stage][method_name] = result
106+
107+
result._rigify_stages = frozenset(stages)
108+
result._rigify_stage_map = stage_map
109+
110+
return result
111+
112+
def make_stage_decorators(self):
113+
return [('stage_'+name, rigify_stage(name)) for name in self._rigify_stages]
114+
115+
116+
class BaseStagedClass(object, metaclass=StagedMetaclass):
117+
def rigify_invoke_stage(self, stage):
118+
"""Call all methods decorated with the given stage, followed by the callback."""
119+
cls = self.__class__
120+
assert(isinstance(cls, StagedMetaclass))
121+
assert(stage in cls._rigify_stages)
122+
123+
for method_name in cls._rigify_stage_map[stage]:
124+
getattr(self, method_name)()
125+
126+
getattr(self, stage)()
127+
23128

24129
#=============================================
25130
# Per-owner singleton class
26131
#=============================================
27132

28133

29-
class SingletonPluginMetaclass(type):
134+
class SingletonPluginMetaclass(StagedMetaclass):
30135
"""Metaclass for maintaining one instance per owner object per constructor arg set."""
31136
def __call__(cls, owner, *constructor_args):
32137
key = (cls, *constructor_args)

0 commit comments

Comments
 (0)