Files
Nexus/2025/scripts/userSetup.py
2025-12-05 08:08:44 +08:00

692 lines
28 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Nexus User Setup Script
Automatically sets up the Nexus plugin system in Maya upon startup.
Features:
- Auto-loads scripts, plugins, and shelves
- Sets up icon paths for UI elements
- Manages command port for external connections
- Clean exit with proper resource cleanup
- Maya 2018+ compatible
"""
import maya.cmds as cmds
import maya.mel as mel
import maya.utils
import atexit
import os
import sys
import re
# Silently try to open default commandPort to avoid startup error if it's already in use
try:
mel.eval('catchQuiet("commandPort -securityWarning -name \\"commandportDefault\\";");')
except Exception:
pass
# =============================================================================
# Configuration
# =============================================================================
# Shelves to load
SHELF_NAMES = ["Nexus_Manage", "Nexus_Modeling", "Nexus_Rigging", "Nexus_Animation", "Nexus_DevTools"]
# Tool packages configuration
TOOL_CONFIG = {
'scripts': [
{'name': 'animation_tools', 'path': 'animation_tools'},
{'name': 'modeling_tools', 'path': 'modeling_tools'},
{'name': 'rigging_tools', 'path': 'rigging_tools'},
{'name': 'manage_tools', 'path': 'manage_tools'},
],
'plugins': [
{'name': 'cv_manip.mll', 'path': 'modeling_tools/gs_curvetools/plugins/2025'},
{'name': 'ngskintools2.mll', 'path': 'rigging_tools/ngskintools2/plug-ins/2025'},
{'name': 'MGPicker_2025x64.mll', 'path': 'animation_tools/mgpicker/MGPicker_Program/Plug-ins'},
],
'icons': [
{'name': 'gs_curvetools', 'path': 'modeling_tools/gs_curvetools/icons'},
{'name': 'gs_toolbox', 'path': 'modeling_tools/gs_toolbox/icons'},
{'name': 'modit', 'path': 'modeling_tools/ModIt/Icons/Theme_Classic'},
{'name': 'ngskintools2', 'path': 'rigging_tools/ngskintools2/ui/images'},
{'name': 'mgpicker', 'path': 'animation_tools/mgpicker/MGPicker_Program/Icons'},
{'name': 'atools', 'path': 'animation_tools/atools/img'},
{'name': 'dwpicker', 'path': 'animation_tools/dwpicker/icons'},
{'name': 'studiolibrary', 'path': 'animation_tools/studiolibrary/studiolibrary/resource/icons'},
{'name': 'studiolibrary_maya', 'path': 'animation_tools/studiolibrary/studiolibrarymaya/icons'},
],
}
# Maya version compatibility check
MAYA_MIN_VERSION = 2018
# Debug control: set environment variable TOOL_DEBUG=1 for verbose logs
TOOL_DEBUG = os.environ.get('TOOL_DEBUG', '0') == '1'
# Global state - capture __file__ at module level before executeDeferred
try:
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
except NameError:
_SCRIPT_DIR = None
_ICONS_PATHS = {}
_LOADED_PLUGINS = {}
_COMMAND_PORT_OPENED = False
_ADDED_SCRIPT_PATHS = []
# =============================================================================
# Utility Functions
# =============================================================================
def _tool_log(msg):
"""Print debug message if TOOL_DEBUG is enabled"""
if TOOL_DEBUG:
print(msg)
def _tool_print(msg):
"""Print info message"""
print(msg)
def _get_script_dir():
"""Get the directory containing this script (with fallback)"""
global _SCRIPT_DIR
if _SCRIPT_DIR:
return _SCRIPT_DIR
# Try multiple methods to get script directory
try:
_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
except NameError:
try:
import inspect
_SCRIPT_DIR = os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))
except Exception:
# Last resort: use current working directory's scripts folder
_SCRIPT_DIR = os.getcwd()
return _SCRIPT_DIR
def _norm_path(path):
"""Normalize path with forward slashes"""
return os.path.normpath(path).replace('\\', '/')
def _resolve_relative_path(path, base_dir):
"""Resolve relative path against base directory"""
if os.path.isabs(path):
return path
return os.path.normpath(os.path.join(base_dir, path))
def _safe_mel_eval(cmd):
"""Execute MEL command safely, return None on error"""
try:
return mel.eval(cmd)
except Exception:
return None
def _check_maya_version():
"""Check if Maya version meets minimum requirements"""
try:
maya_version = int(cmds.about(version=True).split()[0])
if maya_version < MAYA_MIN_VERSION:
_tool_print(f"[Tool] Warning: Maya {maya_version} detected. Minimum supported version is {MAYA_MIN_VERSION}")
return False
_tool_log(f"[Tool] Maya version: {maya_version}")
return True
except Exception as e:
_tool_print(f"[Tool] Warning: Could not determine Maya version: {e}")
return True # Continue anyway
def _find_file_in_paths(filename, search_paths):
"""Find file in a list of search paths"""
for p in search_paths:
if not p:
continue
candidate = os.path.join(p, filename)
if os.path.exists(candidate):
return candidate
return None
def _verify_shelf_icon_references(search_paths):
"""Verify that shelf icon references can be resolved"""
if not TOOL_DEBUG:
return # Only verify in debug mode
try:
for shelf_name in SHELF_NAMES:
shelf_file = os.path.join(search_paths[-1] if search_paths else '', f'shelf_{shelf_name}.mel').replace('\\', '/')
if not os.path.exists(shelf_file):
shelf_file = os.path.join(os.path.dirname(_get_script_dir()), 'shelves', f'shelf_{shelf_name}.mel').replace('\\', '/')
if not os.path.exists(shelf_file):
continue
try:
with open(shelf_file, 'r', encoding='utf-8') as fh:
data = fh.read()
except:
continue
imgs = re.findall(r'-image\s+"([^"]+)"', data)
for img in imgs:
found = any(os.path.exists(os.path.join(p, img).replace('\\', '/')) for p in search_paths)
if not found:
_tool_log(f"[Tool] Warning: Icon '{img}' not found for shelf '{shelf_name}'")
except Exception as e:
_tool_log(f"[Tool] Icon verification error: {e}")
def _remove_shelf_config(shelf_name):
"""Remove shelf config from Maya prefs to force reload"""
try:
maya_version = cmds.about(version=True).split()[0]
maya_app_dir = os.environ.get('MAYA_APP_DIR', '')
if maya_app_dir:
shelf_config = os.path.join(maya_app_dir, maya_version, "prefs", "shelves", f"shelf_{shelf_name}.mel")
if os.path.exists(shelf_config):
os.remove(shelf_config)
_tool_log(f"[Tool] Removed cached shelf config: {shelf_name}")
except Exception as e:
_tool_log(f"[Tool] Could not remove shelf config: {e}")
# =============================================================================
# Setup Functions
# =============================================================================
def setup_icon_paths():
"""Configure icon paths in XBMLANGPATH environment variable"""
try:
script_dir = _get_script_dir()
icon_configs = TOOL_CONFIG.get('icons', [])
if not icon_configs:
_tool_log("[Tool] No icons configured")
return
# Build icon paths dictionary
icon_paths = {}
for cfg in icon_configs:
name = cfg.get('name', '')
path = cfg.get('path', '')
if not (name and path):
continue
full_path = _norm_path(_resolve_relative_path(path, script_dir))
if os.path.exists(full_path):
icon_paths[name] = full_path
else:
_tool_print(f"[Tool] Warning: Icon path not found: {full_path}")
# Check if already configured
global _ICONS_PATHS
if _ICONS_PATHS and _ICONS_PATHS == icon_paths:
_tool_log("[Tool] Icon paths already configured")
return
_ICONS_PATHS = icon_paths
# Update XBMLANGPATH
xbmlangpath = os.environ.get('XBMLANGPATH', '')
paths = [_norm_path(p.strip()) for p in xbmlangpath.split(os.pathsep) if p.strip()]
# Add icon paths to front
for icon_path in icon_paths.values():
if icon_path not in paths:
paths.insert(0, icon_path)
# Set environment variable
new_xbmlangpath = os.pathsep.join(paths)
os.environ['XBMLANGPATH'] = new_xbmlangpath
# Update MEL environment with proper escaping
try:
# Escape backslashes and quotes for MEL
safe_path = new_xbmlangpath.replace('\\', '/').replace('"', '\\"')
mel.eval(f'putenv "XBMLANGPATH" "{safe_path}";')
except Exception:
pass
_tool_log(f"[Tool] ✓ Icon paths configured: {len(icon_paths)} package(s)")
_verify_shelf_icon_references(paths)
except Exception as e:
_tool_print(f"[Tool] Error setting up icon paths: {e}")
def load_tool_shelves():
"""Load Nexus shelves into Maya"""
try:
# Determine shelf paths
shelf_paths = os.environ.get('MAYA_SHELF_PATH', '')
if not shelf_paths:
script_dir = _get_script_dir()
shelf_paths = os.path.join(os.path.dirname(script_dir), "shelves")
if not os.path.exists(shelf_paths):
_tool_print("[Tool] Shelf directory not found, skipping")
return
shelf_path_list = [p.strip() for p in shelf_paths.split(os.pathsep) if p.strip()]
# Load each shelf
loaded_count = 0
for shelf_name in SHELF_NAMES:
# Find shelf file
shelf_file_found = None
for shelf_path in shelf_path_list:
shelf_file = _norm_path(os.path.join(shelf_path, f"shelf_{shelf_name}.mel"))
if os.path.exists(shelf_file):
shelf_file_found = shelf_file
_tool_log(f"[Tool] Found shelf: {shelf_file}")
break
if not shelf_file_found:
_tool_print(f"[Tool] Shelf not found: shelf_{shelf_name}.mel")
continue
# Remove existing shelf
if cmds.shelfLayout(shelf_name, exists=True):
try:
cmds.deleteUI(shelf_name, layout=True)
_tool_log(f"[Tool] Removed existing shelf: {shelf_name}")
except Exception as e:
_tool_log(f"[Tool] Could not remove shelf: {e}")
# Remove cached config
_remove_shelf_config(shelf_name)
try:
# Disable shelf saving
_safe_mel_eval('optionVar -intValue "saveLastLoadedShelf" 0;')
# Create shelf layout using Python API instead of MEL
try:
# Get shelf parent using safe MEL evaluation
shelf_parent = mel.eval('global string $gShelfTopLevel; $temp = $gShelfTopLevel;')
cmds.setParent(shelf_parent)
cmds.shelfLayout(shelf_name, cellWidth=35, cellHeight=34)
except Exception as e:
_tool_log(f"[Tool] Error creating shelf layout: {e}")
raise
if not cmds.shelfLayout(shelf_name, exists=True):
raise RuntimeError(f"Failed to create shelf layout: {shelf_name}")
# Load shelf content with safe path handling
_safe_mel_eval(f'setParent {shelf_name};')
# Use forward slashes for cross-platform compatibility
safe_shelf_path = shelf_file_found.replace('\\', '/')
mel.eval(f'source "{safe_shelf_path}";')
mel.eval(f'shelf_{shelf_name}();')
# Verify loaded
buttons = cmds.shelfLayout(shelf_name, query=True, childArray=True) or []
_tool_print(f"[Tool] ✓ Shelf loaded: {shelf_name} ({len(buttons)} buttons)")
loaded_count += 1
# Remove config again to prevent saving
_remove_shelf_config(shelf_name)
except Exception as e:
_tool_print(f"[Tool] Error loading shelf {shelf_name}: {e}")
_tool_log(f"[Tool] ✓ Shelves: {loaded_count}/{len(SHELF_NAMES)} loaded")
except Exception as e:
_tool_print(f"[Tool] Error loading shelves: {e}")
def load_tool_plugins():
"""Load Nexus plugins"""
try:
script_dir = _get_script_dir()
plugin_configs = TOOL_CONFIG.get('plugins', [])
if not plugin_configs:
_tool_log("[Tool] No plugins configured")
return
loaded_count = 0
for cfg in plugin_configs:
plugin_name = cfg.get('name', '')
plugin_rel_path = cfg.get('path', '')
env_var = cfg.get('env_var', 'MAYA_PLUG_IN_PATH')
if not plugin_name:
continue
_tool_log(f"[Tool] Processing plugin: {plugin_name}")
# 首先检查配置的 path 参数
plugin_path = None
if plugin_rel_path:
# 构建完整路径script_dir/path/plugin_name
full_path = _norm_path(os.path.join(script_dir, plugin_rel_path, plugin_name))
if os.path.exists(full_path):
plugin_path = full_path
_tool_log(f"[Tool] Found plugin at configured path: {plugin_path}")
# 如果配置路径没找到,搜索环境变量路径
if not plugin_path:
plugin_path = _find_file_in_paths(plugin_name, os.environ.get(env_var, '').split(os.pathsep))
# 最后尝试脚本目录和父级 plug-ins 文件夹
if not plugin_path:
search_dirs = [
script_dir,
os.path.join(os.path.dirname(script_dir), 'plug-ins')
]
plugin_path = _find_file_in_paths(plugin_name, search_dirs)
if not plugin_path:
_tool_print(f"[Tool] Plugin not found: {plugin_name}")
continue
# Add plugin directory to environment
plugin_dir = _norm_path(os.path.dirname(plugin_path))
current_paths = [p for p in os.environ.get(env_var, '').split(os.pathsep) if p]
if plugin_dir not in current_paths:
os.environ[env_var] = os.pathsep.join([plugin_dir] + current_paths)
# Get plugin basename
plugin_basename = os.path.splitext(os.path.basename(plugin_path))[0]
# Check if already loaded
try:
already_loaded = bool(cmds.pluginInfo(plugin_basename, query=True, loaded=True))
except Exception:
already_loaded = False
if already_loaded:
_tool_log(f"[Tool] Plugin already loaded: {plugin_basename}")
_LOADED_PLUGINS[plugin_basename] = plugin_path
continue
# Load plugin
try:
cmds.loadPlugin(plugin_path, quiet=True)
_tool_print(f"[Tool] ✓ Plugin loaded: {plugin_basename}")
loaded_count += 1
_LOADED_PLUGINS[plugin_basename] = plugin_path
except Exception:
try:
# Fallback: load by basename
cmds.loadPlugin(plugin_basename, quiet=True)
_tool_print(f"[Tool] ✓ Plugin loaded: {plugin_basename}")
loaded_count += 1
_LOADED_PLUGINS[plugin_basename] = plugin_path
except Exception as e:
_tool_print(f"[Tool] Error loading plugin {plugin_basename}: {e}")
_tool_log(f"[Tool] ✓ Plugins: {loaded_count}/{len(plugin_configs)} loaded")
except Exception as e:
_tool_print(f"[Tool] Error loading plugins: {e}")
def load_tool_scripts():
"""Add Nexus script paths to sys.path"""
try:
script_dir = _get_script_dir()
script_configs = TOOL_CONFIG.get('scripts', [])
if not script_configs:
_tool_log("[Tool] No scripts configured")
return
global _ADDED_SCRIPT_PATHS
loaded = 0
for cfg in script_configs:
name = cfg.get('name', '')
path = cfg.get('path', '')
if not (name and path):
continue
full_path = os.path.normpath(os.path.join(script_dir, path))
if not os.path.exists(full_path):
_tool_print(f"[Tool] Script path not found: {full_path}")
continue
if full_path not in sys.path:
sys.path.insert(0, full_path)
_ADDED_SCRIPT_PATHS.append(full_path)
_tool_log(f"[Tool] Added script path: {name} -> {full_path}")
loaded += 1
_tool_log(f"[Tool] ✓ Script paths: {loaded}/{len(script_configs)} added")
except Exception as e:
_tool_print(f"[Tool] Error loading scripts: {e}")
def load_project_modules():
"""从 modules 目录加载 .mod 文件定义的插件"""
try:
# 获取当前 Maya 版本
maya_version = int(cmds.about(version=True).split()[0])
# 获取项目根目录
script_dir = _get_script_dir()
project_root = os.path.dirname(os.path.dirname(script_dir))
modules_dir = _norm_path(os.path.join(project_root, "modules"))
if not os.path.exists(modules_dir):
_tool_log(f"[Tool] Modules directory not found: {modules_dir}")
return
# 查找所有 .mod 文件
mod_files = [f for f in os.listdir(modules_dir) if f.endswith('.mod')]
if not mod_files:
_tool_log(f"[Tool] No .mod files found in: {modules_dir}")
return
_tool_print(f"[Tool] Found {len(mod_files)} module(s): {', '.join(mod_files)}")
# 解析 .mod 文件并加载插件
plugins_loaded = 0
for mod_file in mod_files:
mod_path = os.path.join(modules_dir, mod_file)
_tool_log(f"[Tool] Processing: {mod_file}")
try:
current_module_root = None
in_correct_version = False
with open(mod_path, 'r', encoding='utf-8') as f:
for line in f:
line = line.strip()
# 跳过注释和空行
if not line or line.startswith('#') or line.startswith('//'):
continue
# 解析模块定义行(以 + 开头)
if line.startswith('+'):
parts = line.split()
in_correct_version = False
current_module_root = None
if len(parts) >= 5:
# 检查版本匹配
for part in parts:
if part.startswith('MAYAVERSION:'):
required_version = int(part.split(':')[1])
if required_version == maya_version:
in_correct_version = True
# 获取模块根路径(最后一个参数)
current_module_root = parts[-1]
if current_module_root.startswith('..'):
current_module_root = _norm_path(os.path.join(modules_dir, current_module_root))
_tool_log(f"[Tool] Version matched for Maya {maya_version}: {current_module_root}")
break
# 只处理匹配版本的 MAYA_PLUG_IN_PATH
elif line.startswith('MAYA_PLUG_IN_PATH') and in_correct_version and current_module_root:
if '+:=' in line or '+=' in line:
plugin_path_relative = line.split('=')[-1].strip()
plugins_dir = _norm_path(os.path.join(current_module_root, plugin_path_relative))
if os.path.exists(plugins_dir):
_tool_log(f"[Tool] Checking plugin dir: {plugins_dir}")
for plugin_file in os.listdir(plugins_dir):
if plugin_file.endswith(('.mll', '.so', '.bundle', '.py')):
plugin_name = os.path.splitext(plugin_file)[0]
plugin_full_path = os.path.join(plugins_dir, plugin_file)
# 检查是否已加载
if plugin_name in _LOADED_PLUGINS:
continue
try:
is_loaded = cmds.pluginInfo(plugin_name, query=True, loaded=True)
except:
is_loaded = False
if not is_loaded:
try:
cmds.loadPlugin(plugin_full_path, quiet=True)
_tool_print(f"[Tool] ✓ Plugin loaded: {plugin_name} (from {mod_file})")
plugins_loaded += 1
_LOADED_PLUGINS[plugin_name] = plugin_full_path
except Exception as e:
_tool_print(f"[Tool] Failed to load {plugin_name}: {e}")
else:
_tool_log(f"[Tool] Plugin dir not found: {plugins_dir}")
except Exception as e:
_tool_print(f"[Tool] Error processing {mod_file}: {e}")
import traceback
traceback.print_exc()
if plugins_loaded > 0:
_tool_print(f"[Tool] ✓ Total {plugins_loaded} module plugin(s) loaded")
else:
_tool_print(f"[Tool] No module plugins loaded")
except Exception as e:
_tool_print(f"[Tool] Error loading modules: {e}")
import traceback
traceback.print_exc()
def setup_command_port():
"""Setup command port for external connections"""
global _COMMAND_PORT_OPENED
try:
# Check if already open
try:
port_exists = bool(mel.eval('commandPort -q -name "commandportDefault"'))
except Exception:
port_exists = False
if port_exists:
_tool_log("[Tool] Command port already open")
_COMMAND_PORT_OPENED = True
return
# Open command port
mel.eval('commandPort -securityWarning -name "commandportDefault";')
_tool_log("[Tool] ✓ Command port opened")
_COMMAND_PORT_OPENED = True
except Exception as e:
_tool_log(f"[Tool] Could not open command port: {e}")
# =============================================================================
# Main Initialization
# =============================================================================
def initialize_tool():
"""Main initialization function called on Maya startup"""
try:
print("=" * 80)
print("[Tool] Nexus Plugin System - Initializing...")
print("=" * 80)
# Check Maya version compatibility
if not _check_maya_version():
print("[Tool] Warning: Running on unsupported Maya version")
# 先设置图标路径,这样 shelf 加载时就能找到图标
setup_icon_paths()
# Load project modules
load_project_modules()
# Setup command port
setup_command_port()
# Load components in order
load_tool_scripts()
load_tool_plugins()
load_tool_shelves()
print("=" * 80)
print("[Tool] Nexus Plugin System - Ready!")
print("=" * 80)
except Exception as e:
print(f"[Tool] Initialization error: {e}")
import traceback
traceback.print_exc()
# Defer initialization until Maya is fully loaded
maya.utils.executeDeferred(initialize_tool)
# =============================================================================
# Cleanup on Exit
# =============================================================================
def cleanup_on_exit():
"""Cleanup resources when Maya exits"""
try:
_tool_log("[Tool] Cleanup initiated...")
# Close command port
global _COMMAND_PORT_OPENED
if _COMMAND_PORT_OPENED:
try:
if mel.eval('commandPort -q -name "commandportDefault"'):
mel.eval('commandPort -cl "commandportDefault"')
_tool_log("[Tool] ✓ Command port closed")
except Exception as e:
_tool_log(f"[Tool] Could not close command port: {e}")
# Unload plugins
if _LOADED_PLUGINS:
unloaded = 0
for plugin_basename, plugin_path in list(_LOADED_PLUGINS.items()):
try:
# Check if still loaded
try:
is_loaded = bool(cmds.pluginInfo(plugin_basename, query=True, loaded=True))
except Exception:
is_loaded = False
if is_loaded:
try:
cmds.unloadPlugin(plugin_basename, force=True)
_tool_log(f"[Tool] ✓ Plugin unloaded: {plugin_basename}")
unloaded += 1
except Exception as e:
_tool_log(f"[Tool] Could not unload plugin {plugin_basename}: {e}")
except Exception:
continue
if unloaded > 0:
_tool_log(f"[Tool] ✓ Plugins unloaded: {unloaded}/{len(_LOADED_PLUGINS)}")
# Clean up script paths from sys.path
global _ADDED_SCRIPT_PATHS
if _ADDED_SCRIPT_PATHS:
for path in _ADDED_SCRIPT_PATHS:
try:
if path in sys.path:
sys.path.remove(path)
except Exception:
pass
_tool_log(f"[Tool] ✓ Script paths cleaned: {len(_ADDED_SCRIPT_PATHS)}")
# Clear global state
_LOADED_PLUGINS.clear()
_ICONS_PATHS.clear()
_ADDED_SCRIPT_PATHS.clear()
_tool_log("[Tool] ✓ Cleanup complete")
except Exception as e:
_tool_log(f"[Tool] Cleanup error: {e}")
# Register cleanup function
atexit.register(cleanup_on_exit)