This commit is contained in:
2025-05-02 00:14:28 +08:00
commit 6f27dc11e3
132 changed files with 28609 additions and 0 deletions

741
scripts/ReloadModules.py Normal file
View File

@@ -0,0 +1,741 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Tool - Module Reload Tool
Provides module reloading functionality for the plugin, supporting hot updates
用法说明:
1. 在Maya中直接运行:
import scripts.ReloadModules
scripts.ReloadModules.main()
2. 从命令行重新加载所有模块:
import scripts.ReloadModules
scripts.ReloadModules.reload_all()
3. 重新加载特定模块:
import scripts.ReloadModules
scripts.ReloadModules.ModuleReloader.reload_module('module_name')
4. 在开发过程中使用:
- 当修改了代码后,运行此模块可以热更新所有更改
- 无需重启Maya即可测试新功能
- 支持UI和命令行两种方式
注意事项:
- 某些深度依赖的模块可能需要手动重启Maya
- 如果遇到导入错误,请检查模块路径是否正确
- 重新加载可能不会影响已经创建的对象实例
"""
import sys
import importlib
import os
import traceback
import shutil
from maya import cmds
# 设置工具路径
TOOL_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if TOOL_PATH not in sys.path:
sys.path.insert(0, TOOL_PATH)
# 设置脚本路径
SCRIPTS_PATH = os.path.dirname(os.path.abspath(__file__))
if SCRIPTS_PATH not in sys.path:
sys.path.insert(0, SCRIPTS_PATH)
# 导入主配置 - 多级尝试导入策略
def import_main_config():
"""尝试多种方式导入主配置文件"""
global TOOL_NAME, TOOL_VERSION, TOOL_AUTHOR, TOOL_PATH, SCRIPTS_PATH
global UI_PATH, STYLE_FILE, ICONS_PATH, ASSETS_PATH, DNA_FILE_PATH, Config
# 尝试直接导入
try:
import config
importlib.reload(config) # 确保获取最新配置
# 从主配置文件导入路径和设置
TOOL_NAME = config.TOOL_NAME
TOOL_VERSION = config.TOOL_VERSION
TOOL_AUTHOR = config.TOOL_AUTHOR
TOOL_PATH = config.TOOL_PATH
SCRIPTS_PATH = config.SCRIPTS_PATH
UI_PATH = config.UI_PATH
STYLE_FILE = config.STYLE_FILE
ICONS_PATH = config.ICONS_PATH
ASSETS_PATH = config.ASSETS_PATH
DNA_FILE_PATH = config.DNA_FILE_PATH
print(f"成功从主配置文件导入配置")
return True
except ImportError as e:
print(f"直接导入主配置文件失败: {str(e)}")
# 尝试从上级目录导入
parent_dir = os.path.dirname(TOOL_PATH)
if parent_dir not in sys.path:
sys.path.insert(0, parent_dir)
try:
import config
importlib.reload(config) # 确保获取最新配置
# 从主配置文件导入路径和设置
TOOL_NAME = config.TOOL_NAME
TOOL_VERSION = config.TOOL_VERSION
TOOL_AUTHOR = config.TOOL_AUTHOR
TOOL_PATH = config.TOOL_PATH
SCRIPTS_PATH = config.SCRIPTS_PATH
UI_PATH = config.UI_PATH
STYLE_FILE = config.STYLE_FILE
ICONS_PATH = config.ICONS_PATH
ASSETS_PATH = config.ASSETS_PATH
DNA_FILE_PATH = config.DNA_FILE_PATH
print(f"成功从上级目录导入主配置文件")
return True
except ImportError as e:
print(f"从上级目录导入主配置文件失败: {str(e)}")
if parent_dir in sys.path:
sys.path.remove(parent_dir)
return False
# 尝试导入主配置
if not import_main_config():
print("警告: 无法导入主配置文件,使用默认配置")
# 使用默认路径
TOOL_NAME = "Delos"
TOOL_VERSION = "Alpha v1.0.0"
TOOL_AUTHOR = "Virtuos Games"
TOOL_PATH = TOOL_PATH
UI_PATH = os.path.join(SCRIPTS_PATH, 'ui')
STYLE_FILE = os.path.join(SCRIPTS_PATH, 'ui', 'style.qss')
ICONS_PATH = os.path.join(TOOL_PATH, 'icons')
ASSETS_PATH = os.path.join(TOOL_PATH, 'assets')
DNA_FILE_PATH = os.path.join(TOOL_PATH, 'assets', 'dna')
# 尝试导入Config模块
try:
from scripts.utils import Config
except ImportError:
try:
from utils import Config
except ImportError:
try:
import Config
except ImportError:
print("警告: 无法导入Config模块")
Config = None
# 创建默认Config对象
class DefaultConfig:
# 使用TOOL_PATH而不是ROOT_DIR
DNA_CONFIG = {
'DNA_PATH': os.path.join(TOOL_PATH, 'assets', 'dna'),
'DNA_FILE_PATH': os.path.join(TOOL_PATH, 'assets', 'dna', 'default.dna'),
'DNA_VERSION': 'DNAv4',
'LOD_LEVELS': [0, 1, 2, 3, 4],
'DEFAULT_MESH_INDICES': [0],
'GUI_PATH': os.path.join(TOOL_PATH, 'ui'),
'ASSEMBLE_SCRIPT': os.path.join(TOOL_PATH, 'scripts', 'assemble.py')
}
DNA_LIB_PATHS = [
os.path.join(TOOL_PATH, 'lib'),
os.path.join(TOOL_PATH, 'lib', 'windows' if sys.platform == 'win32' else 'linux')
]
Config = DefaultConfig()
# 尝试导入 Qt 模块
# 首先尝试使用项目中的 Qt.py 兼容层
try:
# 添加父目录到路径中以确保可以导入 Qt.py
parent_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
if parent_dir not in sys.path:
sys.path.append(parent_dir)
# 尝试导入 Qt.py
from Qt import QtWidgets, QtCore, QtGui
from Qt.QtCompat import wrapInstance
# 获取 Maya 主窗口
def getMayaMainWindow():
from maya import OpenMayaUI as omui
mainWindowPtr = omui.MQtUtil.mainWindow()
return wrapInstance(int(mainWindowPtr), QtWidgets.QWidget)
HAS_QT = True
print("Successfully imported Qt modules using Qt.py compatibility layer")
except ImportError:
# 如果无法导入 Qt.py尝试直接导入 PySide2/PySide6
try:
from PySide2 import QtWidgets, QtCore, QtGui
from shiboken2 import wrapInstance
from maya import OpenMayaUI as omui
def getMayaMainWindow():
mainWindowPtr = omui.MQtUtil.mainWindow()
return wrapInstance(int(mainWindowPtr), QtWidgets.QWidget)
HAS_QT = True
print("Successfully imported Qt modules using PySide2")
except ImportError:
try:
from PySide6 import QtWidgets, QtCore, QtGui
from shiboken6 import wrapInstance
from maya import OpenMayaUI as omui
def getMayaMainWindow():
mainWindowPtr = omui.MQtUtil.mainWindow()
return wrapInstance(int(mainWindowPtr), QtWidgets.QWidget)
HAS_QT = True
print("Successfully imported Qt modules using PySide6")
except ImportError:
# 如果所有导入尝试都失败,设置标志并显示警告
HAS_QT = False
cmds.warning("Failed to import Qt modules. UI functionality will be limited to command line only.")
def clean_pycache(root_dir):
"""Delete all __pycache__ directories and .pyc files under the given root directory
Args:
root_dir (str): Root directory to search for __pycache__ folders and .pyc files
Returns:
int: Number of __pycache__ directories and .pyc files removed
"""
count = 0
try:
# 首先确保目录存在
if not os.path.exists(root_dir) or not os.path.isdir(root_dir):
print(f"Warning: Directory does not exist or is not a directory: {root_dir}")
return count
print(f"Cleaning __pycache__ in directory: {root_dir}")
# 收集所有需要删除的路径,避免在遍历时修改目录结构
pycache_dirs = []
pyc_files = []
# 首先收集所有 __pycache__ 目录和 .pyc 文件
for root, dirs, files in os.walk(root_dir):
# 收集 __pycache__ 目录
if "__pycache__" in dirs:
pycache_path = os.path.join(root, "__pycache__")
pycache_dirs.append(pycache_path)
# 收集 .pyc 文件
for file in files:
if file.endswith(".pyc"):
file_path = os.path.join(root, file)
pyc_files.append(file_path)
# 优先使用批处理命令删除 __pycache__ 目录(更可靠)
if os.name == 'nt': # Windows
# 创建临时批处理文件
temp_bat = os.path.join(os.environ.get('TEMP', '.'), 'clean_pycache_temp.bat')
with open(temp_bat, 'w') as f:
f.write('@echo off\n')
# 添加删除 __pycache__ 目录的命令
for pycache_path in pycache_dirs:
f.write(f'if exist "{pycache_path}" rd /s /q "{pycache_path}"\n')
# 添加删除 .pyc 文件的命令
for file_path in pyc_files:
f.write(f'if exist "{file_path}" del /f /q "{file_path}"\n')
# 执行批处理文件
os.system(f'"{temp_bat}"')
# 删除临时批处理文件
try:
os.remove(temp_bat)
except:
pass
# 验证是否删除成功
remaining_pycache = 0
for pycache_path in pycache_dirs:
if not os.path.exists(pycache_path):
count += 1
else:
remaining_pycache += 1
remaining_pyc = 0
for file_path in pyc_files:
if not os.path.exists(file_path):
count += 1
else:
remaining_pyc += 1
if remaining_pycache > 0 or remaining_pyc > 0:
print(f"Warning: {remaining_pycache} __pycache__ directories and {remaining_pyc} .pyc files could not be removed")
else: # Unix/Linux
# 使用系统命令删除 __pycache__ 目录
for pycache_path in pycache_dirs:
if os.system(f'rm -rf "{pycache_path}"') == 0:
print(f"Removed __pycache__ directory: {pycache_path}")
count += 1
else:
print(f"Failed to remove {pycache_path}")
# 使用系统命令删除 .pyc 文件
for file_path in pyc_files:
if os.system(f'rm -f "{file_path}"') == 0:
print(f"Removed .pyc file: {file_path}")
count += 1
else:
print(f"Failed to remove {file_path}")
except Exception as error:
print(f"Error cleaning __pycache__ directories: {str(error)}")
print(f"Removed {count} files/directories: {len(pycache_dirs)} __pycache__ directories and {len(pyc_files)} .pyc files")
traceback.print_exc()
return count
class ModuleReloader(object):
"""Class for reloading modules in the Plugin"""
@staticmethod
def get_package_modules(package_name):
"""
Get all modules in a package
Args:
package_name (str): Name of the package
Returns:
list: List of module names
"""
package_modules = []
try:
# 尝试导入包
package = importlib.import_module(package_name)
package_path = os.path.dirname(package.__file__)
# 遍历包目录查找模块
for root, dirs, files in os.walk(package_path):
for file in files:
if file.endswith('.py') and file != '__init__.py':
# 计算相对路径
rel_path = os.path.relpath(os.path.join(root, file), package_path)
# 转换为模块路径格式
module_path = os.path.splitext(rel_path)[0].replace(os.sep, '.')
# 构建完整模块名
module_name = f"{package_name}.{module_path}"
package_modules.append(module_name)
# 添加子包
for dir_name in dirs:
init_file = os.path.join(root, dir_name, '__init__.py')
if os.path.exists(init_file):
rel_path = os.path.relpath(os.path.join(root, dir_name), package_path)
module_name = f"{package_name}.{rel_path.replace(os.sep, '.')}"
package_modules.append(module_name)
return package_modules
except Exception as e:
print(f"错误: 无法导入包 {package_name}: {str(e)}")
traceback.print_exc()
return []
@staticmethod
def reload_module(module_name):
"""Reload a specific module
Args:
module_name (str): Name of the module to reload
Returns:
bool: True if successful, False otherwise
"""
try:
# 尝试导入模块
try:
module = __import__(module_name, fromlist=["*"])
except ImportError:
# 如果失败,尝试将点替换为斜杠
module_path = module_name.replace('.', '/')
if os.path.exists(os.path.join(TOOL_PATH, module_path + '.py')):
sys_path_modified = False
if TOOL_PATH not in sys.path:
sys.path.insert(0, TOOL_PATH)
sys_path_modified = True
module = __import__(module_name, fromlist=["*"])
if sys_path_modified:
sys.path.remove(TOOL_PATH)
else:
raise ImportError(f"Module {module_name} not found")
# 重新加载模块
importlib.reload(module)
print(f"成功重新加载模块: {module_name}")
return True
except Exception as e:
print(f"重新加载模块 {module_name} 时出错: {str(e)}")
traceback.print_exc()
return False
@classmethod
def reload_all_modules(cls):
"""
Reload all Plugin modules
Returns:
dict: Results of reloading each module
"""
# 首先声明全局变量
global TOOL_NAME, TOOL_VERSION, TOOL_AUTHOR, TOOL_PATH, SCRIPTS_PATH
global UI_PATH, STYLE_FILE, ICONS_PATH, ASSETS_PATH, DNA_FILE_PATH
results = {}
# 清理缓存文件
clean_pycache(TOOL_PATH)
# 首先尝试重新加载主配置文件
try:
# 尝试直接导入主配置
if 'config' in sys.modules:
config_module = sys.modules['config']
importlib.reload(config_module)
print("成功重新加载主配置文件")
results['config'] = True
TOOL_NAME = config_module.TOOL_NAME
TOOL_VERSION = config_module.TOOL_VERSION
TOOL_AUTHOR = config_module.TOOL_AUTHOR
TOOL_PATH = config_module.TOOL_PATH
SCRIPTS_PATH = config_module.SCRIPTS_PATH
UI_PATH = config_module.UI_PATH
STYLE_FILE = config_module.STYLE_FILE
ICONS_PATH = config_module.ICONS_PATH
ASSETS_PATH = config_module.ASSETS_PATH
DNA_FILE_PATH = config_module.DNA_FILE_PATH
else:
# 尝试重新导入
import_main_config()
results['config'] = True
except Exception as e:
print(f"重新加载主配置文件时出错: {str(e)}")
results['config'] = False
# 定义要重新加载的模块
modules_to_reload = [
"scripts.utils.Config", # 首先重新加载配置
"scripts.utils",
"scripts.ui",
"scripts.Main"
]
# 获取所有子模块
all_modules = []
for module in modules_to_reload:
all_modules.append(module)
try:
submodules = cls.get_package_modules(module)
if submodules:
all_modules.extend(submodules)
except Exception as e:
print(f"获取模块 {module} 的子模块时出错: {str(e)}")
# 去除重复项并排序
all_modules = sorted(list(set(all_modules)))
# 按照依赖关系对模块进行排序
# 确保配置模块先重新加载
priority_modules = []
normal_modules = []
for module in all_modules:
if "Config" in module or module.endswith(".config"):
priority_modules.append(module)
else:
normal_modules.append(module)
# 先重新加载优先级模块,然后是普通模块
for module in priority_modules + normal_modules:
results[module] = cls.reload_module(module)
return results
def reload_all():
"""Reload all Plugin modules and display results"""
print("\n" + "-"*50)
print("Reloading Plugin modules...")
print("-"*50)
# 获取插件根目录 - 使用config中定义的TOOL_PATH
try:
import config
TOOL_PATH = config.TOOL_PATH
except ImportError:
# 如果无法导入config则使用相对路径
TOOL_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
print(f"Plugin root directory: {TOOL_PATH}")
# 清理 __pycache__ 目录
print("Cleaning __pycache__ directories...")
pycache_count = clean_pycache(TOOL_PATH)
print(f"Removed {pycache_count} __pycache__ directories and .pyc files")
# Reload modules
results = ModuleReloader.reload_all_modules()
success_count = sum(1 for success in results.values() if success)
total_count = len(results)
print("\nReload Summary:")
print(f"Successfully reloaded {success_count} of {total_count} modules")
if success_count < total_count:
print("\nFailed modules:")
for module, success in results.items():
if not success:
print(f" - {module}")
print("-"*50)
return results
def show_reload_ui():
"""Show a UI for reloading modules"""
# 检查是否有Qt模块可用
if not 'HAS_QT' in globals() or not HAS_QT:
cmds.warning("Qt modules not available. Falling back to command line reload.")
return reload_all()
try:
# 创建对话框
dialog = QtWidgets.QDialog(getMayaMainWindow())
dialog.setWindowTitle("Plugin Reload UI")
dialog.setMinimumWidth(450)
dialog.setWindowFlags(dialog.windowFlags() ^ QtCore.Qt.WindowContextHelpButtonHint)
# 创建布局
layout = QtWidgets.QVBoxLayout(dialog)
# 创建信息标签
info_label = QtWidgets.QLabel("Reload Plugin modules, no need to restart Maya.")
layout.addWidget(info_label)
# 创建模块选择区域
module_group = QtWidgets.QGroupBox("Select Modules to Reload")
module_layout = QtWidgets.QVBoxLayout(module_group)
# 创建全选复选框
all_modules_cb = QtWidgets.QCheckBox("All Modules")
all_modules_cb.setChecked(True)
module_layout.addWidget(all_modules_cb)
# 创建模块类别复选框
module_cbs = {}
module_categories = [
("Core Modules", [
"scripts",
"scripts.Main",
"scripts.ReloadModules"
]
),
("UI Modules", [
"scripts.ui",
"scripts.ui.toolbar",
"scripts.ui.geometry",
"scripts.ui.rigging",
"scripts.ui.behaviour",
"scripts.ui.definition"
]
),
("Utility Modules", [
"scripts.utils",
"scripts.utils.utils_toolbar",
"scripts.utils.utils_geometry",
"scripts.utils.utils_rigging",
"scripts.utils.utils_behaviour",
"scripts.utils.utils_definition"
]
),
("Config", ["config"])
]
for category, modules in module_categories:
category_cb = QtWidgets.QCheckBox(category)
category_cb.setChecked(True)
category_cb.setEnabled(False)
module_layout.addWidget(category_cb)
module_cbs[category] = (category_cb, modules)
# 全选/取消全选逻辑
def toggle_all_modules():
checked = all_modules_cb.isChecked()
for category, (cb, _) in module_cbs.items():
cb.setChecked(checked)
cb.setEnabled(not checked)
all_modules_cb.toggled.connect(toggle_all_modules)
layout.addWidget(module_group)
# 创建操作按钮区域
button_layout = QtWidgets.QHBoxLayout()
# 创建重载按钮
reload_button = QtWidgets.QPushButton("Reload Selected Modules")
reload_button.setMinimumHeight(30)
button_layout.addWidget(reload_button)
# 创建清理缓存按钮
clean_button = QtWidgets.QPushButton("Clean Caches Only")
clean_button.setMinimumHeight(30)
button_layout.addWidget(clean_button)
layout.addLayout(button_layout)
# 创建结果文本区域
results_text = QtWidgets.QTextEdit()
results_text.setReadOnly(True)
results_text.setMinimumHeight(300)
layout.addWidget(results_text)
# 创建关闭按钮
close_button = QtWidgets.QPushButton("Close")
close_button.setMinimumHeight(30)
close_button.clicked.connect(dialog.close)
layout.addWidget(close_button)
# 重载并更新UI的函数
def reload_and_update_ui(text_widget):
text_widget.clear()
text_widget.append("Reloading modules...\n")
# 获取插件根目录 - 使用config中定义的TOOL_PATH
try:
import config
TOOL_PATH = config.TOOL_PATH
except ImportError:
# 如果无法导入config则使用相对路径
TOOL_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
text_widget.append(f"Plugin root directory: {TOOL_PATH}")
# 清理 __pycache__ 目录
text_widget.append("Cleaning __pycache__ directories...")
pycache_count = clean_pycache(TOOL_PATH)
text_widget.append(f"Removed {pycache_count} __pycache__ directories and .pyc files\n")
# 确定要重载的模块
results = {}
if all_modules_cb.isChecked():
# 重载所有模块
results = ModuleReloader.reload_all_modules()
else:
# 重载选定的模块类别
selected_categories = []
for category, (cb, modules) in module_cbs.items():
if cb.isChecked():
selected_categories.extend(modules)
if not selected_categories:
text_widget.append("No module was selected, the operation was canceled.")
return
# 获取所有模块
all_modules = []
for package in selected_categories:
if package.startswith("scripts."):
all_modules.extend(ModuleReloader.get_package_modules(package))
else:
all_modules.append(package)
# 重载选定的模块
results = {}
for module in all_modules:
results[module] = ModuleReloader.reload_module(module)
# 更新UI显示结果
success_count = sum(1 for success in results.values() if success)
total_count = len(results)
text_widget.append(f"Successfully reloaded {success_count}/{total_count} modules\n")
# 显示成功重载的模块
text_widget.append("Successfully reloaded modules:")
for module, success in results.items():
if success:
text_widget.append(f" - {module}")
# 显示失败的模块(如果有)
if success_count < total_count:
text_widget.append("\nFailed to reload modules:")
for module, success in results.items():
if not success:
text_widget.append(f" - {module}")
# 提示用户重载完成
text_widget.append("\nReload operation completed!")
# 仅清理缓存的函数
def clean_cache_only(text_widget):
text_widget.clear()
text_widget.append("Cleaning caches only...\n")
# 获取插件根目录
try:
import config
TOOL_PATH = config.TOOL_PATH
except ImportError:
TOOL_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
text_widget.append(f"Plugin root directory: {TOOL_PATH}")
# 清理 __pycache__ 目录
pycache_count = clean_pycache(TOOL_PATH)
text_widget.append(f"Removed {pycache_count} __pycache__ directories and .pyc files")
text_widget.append("\nCache cleaning operation completed!")
# 连接按钮信号
reload_button.clicked.connect(lambda: reload_and_update_ui(results_text))
clean_button.clicked.connect(lambda: clean_cache_only(results_text))
# 显示对话框
dialog.show()
except Exception as e:
traceback.print_exc()
cmds.warning(f"Failed to show UI: {str(e)}. Falling back to command line reload.")
return reload_all()
# Main function to be called from Maya
def main():
"""Main function to be called from Maya"""
try:
if HAS_QT:
show_reload_ui()
else:
reload_all()
print("Modules reloaded successfully!")
return True
except Exception as e:
print(f"Error during module reload: {str(e)}")
traceback.print_exc()
return False
# Allow running this script directly
if __name__ == "__main__":
main()