🔌

【Maya】ペーストの挙動をなんとかするためにプラグインを書いた

2020/12/21に公開

これはMaya Advent Calender 2020の空き枠埋め用に書いていた記事です。
無事全ての枠が埋まって完走できそうでしたので、サクッと公開することにしました。
参加された皆様おつかれさまでした(まだ終わってないけど)

https://qiita.com/advent-calendar/2020/maya


で掲題の件なんですが、Mayaのペースト評判悪くないですか。
なにがどうなんだというと

  1. コピペが実質「エクスポートとインポート」
  2. ペーストすると「pasted_」とつけられたノードが大量に読み込まれる
  3. うっかり文字列入力欄へペーストするつもりで発動してしまうとストレス(または見落とす)
  4. それがさらに巨大ファイルだったりするとストレスがマッハ

……といったあたりがヘイトを集めています。

今年のアドカレでも12/10の枠で言及されていますし、それ以外だとDF Talkの過去記事(なんと2013年!)に見出すことができます。

考えられる対策として、
pasted_が大量発生した後だともうリネーム処理をちくちく回すくらいしかなくて面白くないので(?)、ペースト時にほんとにペーストするのかどうか確認ダイアログを出すことにします。

MSceneMessageのコールバックを使ったプラグイン化

コールバックについての記事を最近よく見かけるので、してみむとてすなり。

python プラグイン

my_plugin.py
import maya.OpenMaya as om

def some_func():
    print( 'なにか' )

def initializePlugin(mobject):
    print( 'なにか' )

def uninitializePlugin(mobject):
    print( 'なにか' )

mobjectを引数に取るinitializePlugin関数と、
mobjectを引数に取るuninitializePlugin関数が含まれている .py ファイルが
pythonパスに置かれると、プラグインマネージャからプラグインとして読み込めます。
……上のスクリプトは実際にはこんなホネホネな内容で書きませんので、あくまでもイメージを掴むための例です。

initializePlugin関数はプラグインを有効にしたとき(チェックを入れたとき)に実行され、
uninitializePlugin関数はその逆で、プラグインが無効になるときに実行されます。

コールバック

なにかのついでになにかをさせる、
という仕組みというかお約束というかをcallbackと呼びます。
界隈が違えば hook という呼び方もするようです。
callbackとhookが呼び方が違うだけで同じものなのか、似ていて微妙に違うのか、全然違うのかは
いまひとつよくわかりません。Shotgun関連ではこういうののことをhookと呼びます。

「MSceneMessage callback」といったワードで検索すると、
MayaのAPIにはどういうコールバックが用意されているかのリファレンスが出てくると思います。

これとか > https://help.autodesk.com/view/MAYAUL/2020/ENU/?guid=__cpp_ref_class_m_scene_message_html

例として、
新規シーン時に発動させたい「kBeforeNew」「kAfterNew」や、リファレンス時の「kBeforeReference」「kAfterReference」、さらにインポート時・エクスポート時・Plugin読み込み・解除時……など各種イベントが発生したタイミングで「ついでに〇〇させる」を仕込めます。

また、
コールバックを登録するための関数として、「addCallback」「addCheckCallback」「addStringArrayCallback」...などの姿が確認できます。

うまいこと使えると夢が広がりそうです。

あわせてみる

initializePluginでコールバックを登録し、uninitializePluginで登録したコールバックを除去することを考えてみます。

my_plugin.py
import maya.OpenMaya as om
from maya.OpenMaya import MSceneMessage

IDs = []

def some_func():
    print( 'なにか' )

def initializePlugin(mobject):
    result_id = MSceneMessage.addCallback(MSceneMessage.kBeforeNew,some_func)
    global IDs
    IDs.append(result_id)

def uninitializePlugin(mobject):
    if not IDs is []:
        for callback_id in IDs:
            MSceneMessage.removeCallback(callback_id)

こちらで用意した処理「some_func」をコールバックとして登録しました。
addCallbackに渡したのは「MSceneMessage.kBeforeNew」なので、「新規シーンする前に」ついでにsome_funcが呼ばれます。

addCallback関数は、成功するとコールバックIDを返します。
これは終了処理に使うので、必ず控えておく必要があります。
そしてuninitializePlugin関数内では、removeCallbackを使ってコールバックを除去します。
先ほど控えたコールバックIDを渡すと、そのIDのコールバックが除去されます。

  • initializePluginでコールバック追加 &コールバックID取得
  • uninitializePluginで、removeCallbackにコールバックID渡して除去

これが必須です。

ペーストのイベント?

BeforeNewやAfterReferenceのように「BeforeCopy」「AfterPaste」みたいなのがあればわかりやすいのですが、ありません。

冒頭に例としてあげた記事でも判明している通り、Mayaのコピペは実質エクスポート&インポートなので、コピー用・ペースト用のイベントを用意してもらうなんて贅沢が許される身分ではないのです。

インポートやエクスポートならばイベントが用意されているので、これを使うことにします。

クリップボードの場所を特定する

いまは「ペースト時に確認ダイアログを出す」処理を考えているわけですが、使うのはインポート時のイベントです。
インポートのたびに確認されたら、普通にインポートしたいときに鬱陶しそうです。
条件をつけて分けたい。

Mayaのペーストは、コピー(実質エクスポート)時に書き出されたシーンをインポートすることで実現されています。
ではその読み込まれるシーンの場所は?

os.environ.get('TEMP')

これで取得できるTEMPフォルダに、 maya_sceneClipBoard.拡張子 みたいな名前で置かれています。

  • インポートしようとしたら、コールバック発動
  • インポートしようとしてるファイルが TEMP にあったら「ペーストしようとしてる」と判断

これでいきたいと思います。

コールバック登録用関数について

さきほど書いたスクリプトでは普通の「addCallback」を使いましたが、今回は「addCheckFileCallback」を使うことにします。

# MSceneMessage.addCallback
MSceneMessage.addCheckFileCallback

これを使うと、登録したコールバックに対して

  • いま処理しようとしてるファイルを渡す
  • Falseを渡したら処理を中断する

ということができます。

def some_func(retCode, fileObject, clientData):
    filename_raw = fileObject.rawFullName()
    print('いまインポートしようとしてるファイル >',filename_raw)
    
    om.MScriptUtil.setBool(retCode, True)
    # om.MScriptUtil.setBool(retCode, False)    < インポートを中断

def initializePlugin(mobject):
    result_id = MSceneMessage.addCheckFileCallback(MSceneMessage.kBeforeImportCheck,some_func)
    global IDs
    IDs.append(result_id)

addCheckFileCallback に渡す関数(ここでは「some_func」)の中で MScriptUtil.setBool にFalseを渡すと操作中断、Trueを渡すと続行です。
ここではkBeforeImportCheckというイベントに対してsome_funcを登録していますので、「インポートする 前に チェック」→「setBoolにTrue/Falseを渡して中断or続行」という内容が書けます。
setBool の第一引数に retCode というのを渡していますが、これはコールバックに登録された関数の第一引数として渡されてくるもので、その関数の結果を受け渡すためのものです。
この辺りに書いてあります。

https://help.autodesk.com/view/MAYAUL/2020/ENU/?guid=__cpp_ref_class_m_message_html

これで材料は揃いました。
あとは、インポート時に渡されるfileObjectのパスを TEMP のパスと見比べて、
ペーストと判断できたら「ペーストしますか?」確認を出し、
キャンセルだったらインポートを中断する、
という処理にすれば完成です。

できた

こんな感じにしてみました。

paste_alert.py
import os

from maya import cmds
import maya.OpenMaya as om
from maya.OpenMaya import MSceneMessage

IDs = []

def doIt(retCode, fileObject, clientData):
    raw_path = fileObject.rawFullName()
    dirname = os.path.dirname(raw_path)
    if os.environ.get('TEMP').replace('\\','/') != dirname:
        # TEMPからじゃない普通のimportの場合 > 中断しない
        om.MScriptUtil.setBool(retCode, True)
        return

    result = cmds.confirmDialog( title=u'確認',
                                    message=u'ペーストを実行しますか?', 
                                    button=['Continue','Cancel'], 
                                    defaultButton='Cancel', 
                                    cancelButton='Cancel', 
                                    dismissString='Cancel' )
    if result == 'Continue':
        om.MScriptUtil.setBool(retCode, True)
    else:
        print('Paste cancelled...')
        om.MScriptUtil.setBool(retCode, False)



def initializePlugin(mobject):
    result_id_bic = MSceneMessage.addCheckFileCallback(MSceneMessage.kBeforeImportCheck,doIt)
    global IDs
    IDs.append(result_id_bic)

def uninitializePlugin(mobject):
    if not IDs is []:
        for callback_id in IDs:
            MSceneMessage.removeCallback(callback_id)

これで、ペースト時にインポートが発動しそうになった場合には
確認ダイアログ
こういう感じで引き止めてくれます。

ちなみに、「ctl+Vを監視」して出しているわけではないため、
同じ条件が当てはまる類似操作(直にMayaのビューポートへTEMPにあるクリップボードファイルを投げつけてインポート、とか)でもこれが出ます。

まとめ

プラグインとして作ったので、機能を外したいときにはunloadすればいいし、
なにかミスっていてもスクリプト内にreloadを仕込んでおくとかしなくてもunload - loadで更新されるので開発的にも楽でした。
軽率にプラグイン作っていこう。

Discussion