Powered by Google Translate
日本語
English
简体中文
繁體中文
한국어
Français
Deutsch
Español
Italiano
Português
Русский
Bahasa Indonesia
ไทย
Tiếng Việt
Bahasa Melayu
العربية
हिन्दी
Türkçe
Nederlands
Polski
Українська
Powered by Google Translate

Mayaスクリプト(13):スクリプト実行ツール "StmScriptLauncher"

こんにちは、Steemです。


 今回はズバリ「スクリプトを使うためのスクリプト」を紹介します。


「スクリプトをダウンロードしたはいいけど、シェルフに登録するのが面倒… 」


「このスクリプトは困ったときにしか使わないからシェルフに登録する必要はないけど、その度に名前を調べてコマンド打ち込むのも手間…」

 

そんなときのためのスクリプトです。

 機能説明

Ul 

今回、動画はありません。

 UI:クリックで拡大

 操作説明

  • スクリプトの保存先にあたる既定のフォルダを参照し、スクリプトをリスト表示します。
    • テキストを入力すると、ファイル名でフィルターします。
    • 半角スペースでAND検索が可能です。 
  • Source ボタン:
    •  スクリプトエディタ内 "Source Script" ボタンに相当します。
    • 内部に実行文まで記述がある場合、これを押すだけでスクリプトは実行されます。
  • Call ボタン:
    •  コマンドの呼び出しのみを行います。
    • ファイル名と異なる名前のコマンドを呼び出したい場合はボタンの上にある欄にコマンド名を入力して実行します。
  • Source / Call ボタン:
    •  スクリプト内部に実行文があるかどうかを自動で判別し、実行文があれば Source を、なければ Call を行います。
    • リスト項目のダブルクリックでもこのボタンと同じ処理を行います。
  •  プレビューパネル
    •  スクリプトの内容を確認できます。 

 

Source / Call 実行時の優先度としては

  1.  内部に実行文が見つかれば Source
  2.  内部に実行文が見つからず、かつコマンド名が入力されていればその名前で Call 実行
  3.  内部に実行文が見つからず、かつコマンド名が入力されていなければスクリプトファイルの名前で Call 実行

という順番です。

 

 そのため、

  • 通常はSource / Call ボタンで実行
  • 実行文はないが Source のみ行いたい場合(保存したばかりのスクリプトをMayaに読み込みたい場合など)は Source ボタンを使用
  • Call のみ行いたい場合(内部に実行文が含まれているがそれとは異なる部分を呼び出したいときなど)はコマンド名を入力した上で Call ボタンを使用

 といった使い方をします。

 (とりあえずはじめは Source / Call ボタンを使用して問題ないかと思います。)

 

 また、実行文を有するかどうかの判別は実験的な機能となっており、パターンによっては判別に失敗する可能性もあるのでご了承ください。(もしエラー・空振りが発生したら単体の Source ボタン・Call ボタンを使用してください。)

 

なお、スクリプトの保存先として参照するフォルダは以下の4つです。

  • ドキュメント > maya > scripts
  • ドキュメント > maya > (バージョン) > (言語) > scripts
  • ドキュメント > maya > (バージョン) > scripts
  • Mayaインストールディレクトリ(バージョン別) > scripts 

 コード

(このスクリプトのニーズを加味して?)ファイルとして保存して呼び出す必要のないように、ここにそのままスクリプトを載せてしまいます。

スクリプトエディタにそのまま貼り付けて実行可能です。


 以下の手順でシェルフに追加もできます。

・スクリプトエディタに全文ペースト(※Pythonタブを使用

・貼り付けたスクリプトを全選択(Ctrl+A)

・シェルフに中ボタンドラッグ

 

#=== [StmScriptLauncher] === # Function: A script for previewing and executing other scripts. # Last Update: 2026.04 # To report errors, please contact me via my website: [Steem's CG Developin']. # ( link: https://steems-cg-developin.com/ ) #========================================== import maya.cmds as cmds import maya.mel as mel import os import glob import re import ast import importlib import sys class StmScriptLauncher: def __init__(self): self.window_id = "scriptLauncherWin" self.history_var = "scriptLauncher_history" self.script_data = {} version = cmds.about(v=True) lang = cmds.about(uiLanguage=True) user_docs = cmds.internalVar(userAppDir=True) maya_install_dir = os.environ.get('MAYA_LOCATION') search_paths = [ os.path.join(user_docs, "scripts"), os.path.join(user_docs, version, lang, "scripts"), os.path.join(user_docs, version, "scripts"), os.path.join(maya_install_dir, "scripts") ] self.collect_scripts(search_paths) self.create_ui() def collect_scripts(self, paths): for path in paths: if not os.path.exists(path): continue files = glob.glob(os.path.join(path, "*.mel")) + glob.glob(os.path.join(path, "*.py")) for f in files: file_name = os.path.basename(f) if file_name not in self.script_data: self.script_data[file_name] = f.replace("\\", "/") def create_ui(self): if cmds.window(self.window_id, exists=True): cmds.deleteUI(self.window_id) self.win = cmds.window(self.window_id, title="StmScriptLauncher", widthHeight=(800, 600)) pane = cmds.paneLayout(configuration='vertical2', separatorThickness=5) left_col = cmds.columnLayout(adjustableColumn=True, rowSpacing=5, columnOffset=("both", 5)) cmds.text(label="Search (AND):", align="left", height=20) self.search_field = cmds.textField(textChangedCommand=self.refresh_list) self.scroll_list = cmds.textScrollList( numberOfRows=15, allowMultiSelection=False, selectCommand=self.on_search_select, doubleClickCommand=self.execute_source_call ) cmds.text(label="History (Recent 5):", align="left", height=20) self.history_list = cmds.textScrollList( numberOfRows=5, allowMultiSelection=False, selectCommand=self.on_history_select, doubleClickCommand=self.execute_source_call ) cmds.text(label="Custom Command (Optional):", align="left", height=20) self.custom_cmd_field = cmds.textField(placeholderText="Function name") cmds.rowLayout(numberOfColumns=3, adjustableColumn=3, columnWidth3=[60, 60, 70]) cmds.button(label="Source", command=self.execute_source, height=40) cmds.button(label="Call", command=self.execute_call, height=40) cmds.button(label="Source / Call", command=self.execute_source_call, height=40, backgroundColor=(0.3, 0.5, 0.3)) cmds.setParent('..') cmds.setParent('..') right_form = cmds.formLayout(numberOfDivisions=100) preview_label = cmds.text(label="Code Preview:", align="left", height=20) self.preview_field = cmds.scrollField(editable=False, wordWrap=False, font="fixedWidthFont") cmds.formLayout(right_form, edit=True, attachForm=[ (preview_label, 'top', 0), (preview_label, 'left', 5), (preview_label, 'right', 5), (self.preview_field, 'left', 5), (self.preview_field, 'right', 8), (self.preview_field, 'bottom', 8) ], attachControl=[(self.preview_field, 'top', 5, preview_label)] ) cmds.setParent('..') self.refresh_list() self.refresh_history_ui() cmds.showWindow(self.window_id) def update_history_var(self, script_name): if not script_name: return current_history = [] if cmds.optionVar(exists=self.history_var): current_history = cmds.optionVar(q=self.history_var) current_history = list(current_history) if isinstance(current_history, (list, tuple)) else ([current_history] if current_history else []) if script_name in current_history: current_history.remove(script_name) current_history.insert(0, script_name) current_history = current_history[:5] cmds.optionVar(clearArray=self.history_var) for item in current_history: cmds.optionVar(stringValueAppend=(self.history_var, item)) self.refresh_history_ui() def refresh_history_ui(self): cmds.textScrollList(self.history_list, edit=True, removeAll=True) if cmds.optionVar(exists=self.history_var): history = cmds.optionVar(q=self.history_var) if history: cmds.textScrollList(self.history_list, edit=True, append=history) def read_file_safe(self, path): encodings = ['utf-8', 'cp932', 'euc-jp', 'utf-16'] for enc in encodings: try: with open(path, "r", encoding=enc) as f: return f.read() except (UnicodeDecodeError, LookupError): continue with open(path, "r", encoding="utf-8", errors="replace") as f: return f.read() def refresh_list(self, *args): search_query = cmds.textField(self.search_field, q=True, text=True).lower() keywords = search_query.split() cmds.textScrollList(self.scroll_list, edit=True, removeAll=True) sorted_names = sorted(self.script_data.keys()) for name in sorted_names: if all(kw in name.lower() for kw in keywords): cmds.textScrollList(self.scroll_list, edit=True, append=name) def update_preview(self, *args): selected = cmds.textScrollList(self.scroll_list, q=True, selectItem=True) if not selected: selected = cmds.textScrollList(self.history_list, q=True, selectItem=True) if not selected: cmds.scrollField(self.preview_field, edit=True, text="") return path = self.script_data[selected[0]] content = self.read_file_safe(path) cmds.scrollField(self.preview_field, edit=True, text=content) def on_search_select(self): cmds.textScrollList(self.history_list, edit=True, deselectAll=True) self.update_preview() def on_history_select(self): cmds.textScrollList(self.scroll_list, edit=True, deselectAll=True) self.update_preview() def get_active_selection(self): sel = cmds.textScrollList(self.scroll_list, q=True, selectItem=True) if not sel: sel = cmds.textScrollList(self.history_list, q=True, selectItem=True) return sel[0] if sel else None def has_mel_execution(self, file_path): if not os.path.exists(file_path): return 0 content = self.read_file_safe(file_path) content = re.sub(r'//.*', lambda m: " " * len(m.group()), content) content = re.sub(r'/\*.*?\*/', lambda m: " " * len(m.group()), content, flags=re.DOTALL) defined_procs = set(re.findall(r'\bproc\s+([a-zA-Z_]\w*)', content)) chars = list(content) n = len(chars) i = 0 proc_start_pattern = re.compile(r'\bproc\s+\w+') print(f"\n{'='*80}\n[DEBUG: SCOPE ANALYSIS] {os.path.basename(file_path)}\n{'='*80}") while i < n: match = proc_start_pattern.match(content, i) if match: actual_start = i prefix_area = content[max(0, i-60):i] boundary = max(prefix_area.rfind(';'), prefix_area.rfind('\n')) + 1 prefix_candidate = prefix_area[boundary:].strip() if prefix_candidate: actual_start = (max(0, i-60) + boundary) + prefix_area[boundary:].find(prefix_candidate) start_line = content.count('\n', 0, actual_start) + 1 brace_level = 0 in_proc_body = False search_idx = i proc_name = "Unknown" while search_idx < n: if chars[search_idx] == '{': brace_level = 1 in_proc_body = True for k in range(actual_start, search_idx + 1): chars[k] = ' ' i = search_idx + 1 break search_idx += 1 if in_proc_body: while i < n and brace_level > 0: if chars[i] == '{': brace_level += 1 elif chars[i] == '}': brace_level -= 1 if brace_level == 0: end_line = content.count('\n', 0, i) + 1 print(f"Found Proc: {proc_name:<30 ---="" .join="" 1="" 80="" be="" break="" case="" catch="" chars="" clean_content.splitlines="" clean_content="" code="" continue="" debug_lines="" default="" do="" else="" end_line="" float="" for="" global="" i="" if="" in="" int="" investigated="" join="" line.strip="" line="" lines="" matrix="" n="" o="" outside="" print="" procedures="" remains="" reserved_words="{" return="" source="" start_line="" string="" switch="" to="" tokens="re.sub(r" vector="" void="" while="">,\n\t\r\"\'`]', ' ', clean_content).split() found_any = 0 seen = set() for word in tokens: if (word in reserved_words or word.startswith('$') or word[0].isdigit() or len(word) <= 1 or word in seen): continue seen.add(word) if word in defined_procs: print(f"[MATCH] Local Call: '{word}'") found_any = 1 continue try: res = mel.eval(f'whatIs "{word}"').lower() if any(t in res for t in ['command', 'procedure', 'script']): print(f"[MATCH] {res.capitalize()}: '{word}'") found_any = 1 except: continue return 1 if found_any else 0 def has_python_execution(self, file_path): # determine whether a Python script contains an executable statement print(f"\n{'='*60}") print(f"[TARGET]: {file_path}") if not os.path.exists(file_path): print("[ERROR] File not found.") return 0 try: with open(file_path, 'r', encoding='utf-8', errors='ignore') as f: source = f.read() tree = ast.parse(source) except Exception as e: print(f"[ERROR] Syntax or Read Error: {e}") return 0 found_execution = 0 print(f"[LOG] Analyzing top-level nodes...") for node in tree.body: if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef, ast.Import, ast.ImportFrom)): continue if isinstance(node, ast.Expr) and isinstance(node.value, (ast.Str, ast.Constant)): continue if isinstance(node, ast.If): test = node.test if isinstance(test, ast.Compare) and isinstance(test.left, ast.Name) and test.left.id == '__name__': if isinstance(test.ops[0], ast.Eq): comp = test.comparators[0] val = comp.value if isinstance(comp, ast.Constant) else getattr(comp, 's', None) if val == '__main__': print("[SKIP] True main block detected.") continue found_execution = 1 print(f"[MATCH!] Actual execution node: {ast.dump(node)}") break print(f"[RESULT]: {'1 (Execution Found)' if found_execution else '0 (Definitions Only)'}") print(f"{'='*60}\n") return found_execution def execute_source(self, *args): selected = self.get_active_selection() if not selected: return self.update_history_var(selected) file_name = selected full_path = self.script_data[file_name] print(f"\n# --- Sourcing: {file_name} --- #") try: if file_name.endswith(".mel"): mel.eval(f'source "{full_path}";') elif file_name.endswith(".py"): code = self.read_file_safe(full_path) exec(code, globals()) except Exception as e: cmds.warning("Source failed. See Script Editor for details.") print(f"# --- ERROR: {e} --- #") def execute_call(self, *args): selected = self.get_active_selection() if not selected: return self.update_history_var(selected) file_name = selected module_name = os.path.splitext(file_name)[0] input_cmd = cmds.textField(self.custom_cmd_field, q=True, text=True).strip() exec_name = input_cmd.replace("()", "") if input_cmd else module_name try: if file_name.endswith(".mel"): exists_val = mel.eval(f'exists "{exec_name}"') what_is_val = mel.eval(f'whatIs "{exec_name}"') if exists_val and ("procedure" in what_is_val.lower() or "script" in what_is_val.lower()): print(f"\n# --- Calling: {exec_name} --- #") mel.eval(f'{exec_name}();') elif exists_val and what_is_val == "command": print(f"\n# --- Calling: {exec_name} --- #") mel.eval(f'{exec_name};') else: print(f"Target '{exec_name}' not found in current session.") elif file_name.endswith(".py"): found = False if exec_name in globals(): func = globals()[exec_name] if callable(func): func() found = True print(f"# Success: Found '{exec_name}' in globals.") if not found: try: if module_name in sys.modules: module = sys.modules[module_name] else: module = importlib.import_module(module_name) if hasattr(module, exec_name): func = getattr(module, exec_name) if callable(func): func() print(f"# Success: {module_name}.{exec_name}() called.") found = True else: if callable(module): module() found = True except Exception as e: print(f"# Error: Failed to import or execute '{module_name}': {e}") if not found: print(f"# Warning: Target '{exec_name}' not found in current session or module '{module_name}'.") except Exception as e: cmds.warning("Call failed. See Script Editor for details.") print(f"# --- ERROR: {e} --- #") def execute_source_call(self, *args): selected = self.get_active_selection() if not selected: return self.update_history_var(selected) file_name = selected full_path = self.script_data[file_name] module_name = os.path.splitext(file_name)[0] input_cmd = cmds.textField(self.custom_cmd_field, q=True, text=True).strip() exec_name = input_cmd.replace("()", "") if input_cmd else module_name try: if file_name.endswith(".mel"): exists_val = mel.eval(f'exists "{exec_name}"') what_is_val = mel.eval(f'whatIs "{exec_name}"') hasExecution = self.has_mel_execution(full_path) if hasExecution: print(f"# --- Sourcing: {file_name} --- #") mel.eval(f'source "{full_path}";') elif exists_val and ("procedure" in what_is_val.lower() or "script" in what_is_val.lower()): print(f"\n# --- Calling: {exec_name} --- #") mel.eval(f'{exec_name}();') elif exists_val and what_is_val == "Command": print(f"\n# --- Calling: {exec_name} --- #") mel.eval(f'{exec_name};') elif file_name.endswith(".py"): found = False hasExecution = self.has_python_execution(full_path) if hasExecution: print(f"# --- Sourcing: {file_name} --- #") code = self.read_file_safe(full_path) exec(code, globals()) else: if exec_name in globals(): func = globals()[exec_name] if callable(func): print(f"\n# --- Calling: {exec_name} --- #") func() found = True if not found: try: if module_name in sys.modules: module = sys.modules[module_name] else: module = importlib.import_module(module_name) if hasattr(module, exec_name): func = getattr(module, exec_name) if callable(func): print(f"\n# --- Calling: {module_name}.{exec_name}() --- #") func() found = True else: if callable(module): module() found = True except Exception as e: print(f"# Error: Failed to import or execute '{module_name}': {e}") if not found: print(f"# Warning: Target '{exec_name}' not found in current session or module '{module_name}'.") except Exception as e: cmds.warning("Call failed. See Script Editor for details.") print(f"# --- ERROR: {e} --- #") launcher = StmScriptLauncher()

  

もちろんスクリプトエディタから適宜ファイルとして保存することも可能です。

追加説明

Tooltip 表示について

 シェルフに登録したそのままの状態でシェルフアイコンにカーソルを載せると、このスクリプト全文がTooltipとして表示されてしまいます。

アイコンを右クリックしてShelf Editorを開き、 ToolTip の内容をシンプルなものに変更することを推奨します。


Shelf Editor:

マーキングメニューへ追加する

 もしマーキングメニューに入れたくなったら、少々手間ですが

  •  StmScriptLauncher.py の名前でスクリプトを保存
  •  MEL化した以下のスクリプトをシェルフに登録
    • python("import StmScriptLauncher;StmScriptLauncher.StmScriptLauncher(); None");
  • マーキングメニューに追加

という手順を踏むのが最も簡潔かと思います。 

更新

  • テキストボックスを追加しました。
    • ここに関数名・プロシージャ名を入力してSource and Call を実行することで、ファイル名と異なるファイル内部のコマンドを実行できます。
    • (入力する関数名に"()"を付ける必要はありません。)
  • プレビュー上の日本語の文字化けを防ぐ処理を加えました。 
    • UTF-8、Shift-JIS (cp932)、EUC-JP、UTF-16 これらを順に試します。 

  • "Source", "Call", "Source / Call" の三つのボタンを設置
    •  "Source / Call" ボタン実行時、スクリプト内部に実行文があるかどうかを自動判別する形式に変更。
    • Source ボタン・Call ボタンはそれぞれ
      • Source のみを実行したい場合(保存したばかりのスクリプトを読み込みたいときなど)
      • Call のみを実行したい場合(内部に実行文があるがそれとは別の関数を呼び出したいときなど)

      に用います。

  • その他UI要素の改善

 

  • ヒストリー表示フィールドを追加。
    •  最後に使用したスクリプトを5つまで保存して表示します。

 

  •  Pythonの実行文判定ロジックを強化しました。

 

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


このスクリプトについて詳細に解説した記事をnoteにアップしています。(約20000字)(主に自分に向けた解説でもあります。)

有料ですが、ご興味あれば(応援だけでも!)是非ご覧下さい。

https://note.com/steems_note/n/n11b15e2d6d15


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

お問い合わせはこちらまでお願いします。

連絡用フォーム