オランのまったりな
開発日記
VirtualClassを利用してPymelのタイプを拡張
Sorry, failed to load main article image
Bantukul Olarn
    
Nov 29 2017 00:23
  

どなたもPymelのタイプを拡張したいと思えばひとまずクラスを継承すればいい。。たぶん。。と考えてしまうでしょう。

しかしPymelノード生成の仕組みでは下記の様な継承を書いてみたら期待した通の結果があまり出てこないと思います。

# -*- coding: utf-8 -*-
import pymel.core as pm

class TestExtension(pm.nt.Transform):
    pass

def main():

    # this node creation will fail
    trn = TestExtension(name="testtest")
    print(trn, type(trn))
   
main()

しかしPymelタイプ拡張のために用意されてあったVirtualClassという機能を利用すれば作ることができる。


VirtualClass仕様ドキュメントのまとめ


- Virtualタイプを実装するのに、テスト対象のMayaノードがそのVirtualClassに属すべきかどうかを判断するための_isVirtual()というclass/static メソッドを用意する必要がある。

- 必須ではなかったが、Pynode生成前後の処理やその最中の引数Dict調整の為に_preCreate(), _create() と _postCreate() class/staticメソッドを使うこともできる。

- 拡張タイプは定義した後、PymelのNode factoryに登録しなければPymelが認識してくれません。


各VirtualClassメソッドの簡単説明:


_isVirtual ()

pm.ls等を実行した際、そのノードのベースタイプに対する登録したすれべのVirtualタイプにこの関数が呼ばれる。現在のノードがVirtualタイプかどうかTrue/Falseを返すの関数です。一番先にTrueと返すVirtualタイプにそのノードがそのVirtualタイプだとみなす。すべての_isVirtual()テストが失敗すれば、そのノードが以前通りベースタイプのままとなる。


パラメーター: MObject, このisVirtualが属するクラスのstr名

戻り値:bool


* この関数はPymelノードが生成される前に呼ばれるため、この中のコードはMayaApi、CmdsまたはMelコマンドしか使えません。

* テストが行われる度にこの関数は(テスト対象ノードx登録したVirtualタイプ)回呼ばれますので、パフォーマンスに影響が大きい


_preCreate()

ノードが生成される前に呼ばれる関数。

生成される前に、ここでkwargsと_postCreateへ渡す(別の)kwargsを調整できる。

戻り値が dict1, dict2 2つのdictの場合、2個目のdictが_postCreateの引数として渡される。


パラメーター: kwargs

戻り値①:kwargs

戻り値②:kwargs, kwargs_for_postcreate


_create()

今回のサンプルコードには使われていないですが、既定のノード生成のコマンドをここで調整できる。

戻り値:strのノード名又はそのノードのclass.__init__が処理できるデータ


_postCreate()

ノードが生成された後に呼ばれる。

ここでノードにカスタムアトリビュート追加、設定、リネームすることなどができる。

パラメーター: kwargs (_preCreateで渡されるなら) 又は None

戻り値:None


さて上記の関数でタイプを拡張した例を作ってみよう。この拡張タイプは __vtypeid__[タイプ名] という文字列アトリビュート(Mayaノードの)があるかどうかとその中身でVirtualタイプを判断する。タイプ判断は_isVirtualで行い、新規ノードのアトリビュート追加は_postCreateで行われる。

Pymelの継承可能の最低限Virtualタイプ
# -*- coding: utf-8 -*-
from __future__ import print_function, absolute_import, division

import pymel.core as pm
from pymel.internal.factories import virtualClasses
import pprint

# -------------------------------------------------------------------------------

class TypedGrp(pm.nt.Transform):
    @classmethod
    def _isVirtual(cls, obj, name):
        # obj is either an MObject or an MDagPath, depending on whether this class is a subclass of DependNode or DagNode, respectively.
        # we use MFnDependencyNode below because it works with either and we only need to test attribute existence.
        fn = pm.api.MFnDependencyNode(obj)
        try:
            # NOTE: MFnDependencyNode.hasAttribute fails if the attribute does not exist, so we have to try/except it.
            # the _jointClassID is stored on subclass of CustomJointBase
            test_result = fn.hasAttribute(cls._gen_typeid(cls._typeID))
            print("is virtual class", obj, name, test_result)
            # first call:
            # is virtual class <maya.OpenMaya.MObject; proxy of <Swig Object of type 'MObject *' at 0x0000020E12093C00> > samplegroup False
            # seocnd call:
            # is virtual class <maya.OpenMaya.MObject; proxy of <Swig Object of type 'MObject *' at 0x0000020E11F76690> > None True
            return test_result
        except:
            pass

        print("is not virtual class", obj, name)
        return False

    @classmethod
    def _preCreateVirtual(cls, **kwargs):
        print("precreatevirtual")
        postKwargs = {"a_key": "some_value"}
        return kwargs, postKwargs

    @classmethod
    def _postCreateVirtual(cls, newNode, **kwargs):
        print("postcreatevirtual")
        print("argument recieved in post create virtual", newNode, kwargs)
        # argument recieved in post create virtual samplegroup {'a_key': 'some_value'}
        # add the identifying attribute. the attribute name will be set on subclasses of this class
        newNode.addAttr(cls._gen_typeid(cls._typeID), dataType="string")

    @classmethod
    def _gen_typeid(cls, name):
        return u"__vtypeid__{0}".format(name)


class A_GRP(TypedGrp):
    _typeID = 'TYPE_A'


class B_GRP(TypedGrp):
    _typeID = 'TYPE_B'


def main():
    try:
        print("registering virtual class")
        virtualClasses.register(A_GRP, nameRequired=False)

        a_grp1 = A_GRP(name="samplegroup")

        print("newly created nodes, ", a_grp1)
    finally:
        print("unregistering virtual class")
        virtualClasses.unregister(A_GRP)

main()
完全に既定タイプをVirtualで置換する例

今回は _isVirtual() をTrueに固定することで、ノードが必ずVirtualタイプとみなされ規定タイプと完全に置換。

# -*- coding: utf-8 -*-
from __future__ import print_function, absolute_import, division

import pymel.core as pm
from pymel.internal.factories import virtualClasses
import pprint


class GlobalPymelTransformClassReplacement(pm.nt.Transform):
    @classmethod
    def _isVirtual(cls, obj, name):
        # virtual class determination always return true = this class replaces vanilla Transform class
        # in all cases
        return True

    @classmethod
    def _preCreateVirtual(cls, **kwargs):
        postKwargs = {}
        return kwargs, postKwargs

    @classmethod
    def _postCreateVirtual(cls, newNode, **kwargs):
        pass


def main():
    try:
        virtualClasses.register(GlobalPymelTransformClassReplacement, nameRequired=False)

        a_grp1 = GlobalPymelTransformClassReplacement(name="samplegroup")
        vanilla_grp = pm.nt.Transform(name="normaltransformnode")

        print("newly created nodes, ", [a_grp1, vanilla_grp])
        print("pm.ls by custom transform type")
        pprint.pprint(pm.ls(type=GlobalPymelTransformClassReplacement))
        print("pm.ls by the original Transform type still returns everything as custom transform type")
        pprint.pprint(pm.ls(type=pm.nt.Transform))

    finally:
        virtualClasses.unregister(GlobalPymelTransformClassReplacement)
応用例

isinstanceでVirtualと普通のタイプを比較してみる。

今までのサンプルが文字列アトリビュートでタイプ判断していたが今回はノード名Prefixで判断するようにしたVirtualタイプも作る。

# -*- coding: utf-8 -*-
from __future__ import print_function, absolute_import, division

import pymel.core as pm
from pymel.internal.factories import virtualClasses
import pprint

class TypedTransformBase(pm.nt.Transform):
    _default_name = "TypedTransform"
    @classmethod
    def _isVirtual(cls, obj, name):
        dag_node = pm.api.MFnDependencyNode(obj)
        try:
            return dag_node.hasAttribute(cls._gen_typeid(cls._typeID))
        except:
            pass
        return False

    @classmethod
    def _preCreateVirtual(cls, **kwargs ):
        # give a default name if no name supplied
        if 'name' not in kwargs and 'n' not in kwargs:
            kwargs['name'] = cls._default_name
        postKwargs = {}
        return kwargs, postKwargs

    @classmethod
    def _postCreateVirtual(cls, newNode, **kwargs ):
        newNode.addAttr( cls._gen_typeid(cls._typeID), dataType = "string")

    @classmethod
    def _gen_typeid(cls, name):
        return u"__vtypeid__{0}".format(name)

class ATransform(TypedTransformBase):
    _typeID = 'A_SET'

class BTransform(TypedTransformBase):
    _typeID = 'B_SET'

class PrefixTransformBase(pm.nt.Transform):
    _prefix = "REPLACEME"

    @classmethod
    def _isVirtual(cls, obj, name):
        # assume no namespace and no '|' character!
        dag_node = pm.api.MFnDependencyNode(obj)
        return dag_node.name().startswith(cls._prefix)

    @classmethod
    def _postCreateVirtual(cls, newNode, **kwargs ):
        # assumes no namespace and no '|' character!
        newNode.rename(cls._prefix+newNode.name())

class APrefixTrn(PrefixTransformBase):
    _prefix = "A_TRN_"

class BPrefixTrn(PrefixTransformBase):
    _prefix = "B_TRN_"

def main():
    try:

        virtualClasses.register(ATransform, nameRequired=False)
        virtualClasses.register(BTransform, nameRequired=False)
        virtualClasses.register(APrefixTrn, nameRequired=False)
        virtualClasses.register(BPrefixTrn, nameRequired=False)


        # attribute based virtual class example
        a_grp1 = ATransform()
        a_grp2 = ATransform()
        b_grp = BTransform()
        print("is a_grp1 an instace of ATransform?", isinstance(a_grp1, ATransform))
        print("is a_grp2 also an instance of ATransform?", isinstance(a_grp2, ATransform))
        print("is a_grp1 a derived class instance of TypedTransformBase?",
              issubclass(type(a_grp1), TypedTransformBase))
        print("Is b_grp an isinstance of ATransform?", isinstance(b_grp, ATransform))
        print("Is b_grp BTransform isinstance?", isinstance(b_grp, BTransform))

        # name prefix based virtual class example
        c_grp1 = APrefixTrn(name="samplename")
        c_grp2 = BPrefixTrn(name="anothersamplename")



        print("newly created prefix transform: ", c_grp1, c_grp2)
        print(type(n) for n in [c_grp1,c_grp2])

        # Manually naming a normal Transform object so it would be recognized as APrefixTrn
        c_grp3 = pm.nt.Transform()
        print("vanilla transform type.., ", c_grp3, type(c_grp3))
        c_grp3.rename("B_TRN_manuallyrenamed")
        # Renaming c_grp3 to BPrefixTrn type manually does not updates the pymel wrapper type
        print("renamed c_grp3:", c_grp3, type(c_grp3))
        # But would show up correctly if it were listed again (pymel wrapper has been recreated)
        c_grp3 = pm.ls(c_grp3)[0]
        print("re-listed c_grp3, ", c_grp3, type(c_grp3))

        print("listing all transform based classes")
        pprint.pprint(pm.ls(type=pm.nt.Transform))
        '''
        output:

        [nt.APrefixTrn(u'A_TRN_samplename'),
         nt.BPrefixTrn(u'B_TRN_anothersamplename'),
         nt.BPrefixTrn(u'B_TRN_manuallyrenamed'),
         nt.ATransform(u'TypedTransform'),
         nt.ATransform(u'TypedTransform1'),
         nt.BTransform(u'TypedTransform2'),
         nt.Transform(u'front'),
         nt.Transform(u'persp'),
         nt.Transform(u'side'),
         nt.Transform(u'top')]
        '''

    finally:
        virtualClasses.unregister(ATransform)
        virtualClasses.unregister(BTransform)
        virtualClasses.unregister(APrefixTrn)
        virtualClasses.unregister(BPrefixTrn)

Tags: