MetaFusion/DevGoals.md

1430 lines
39 KiB
Markdown
Raw 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.

# 项目目标
我想做一个Maya的Metahuman自定义的插件
语言基于Python
Maya版本2022, 2023, 2024, 2025
## 项目描述
本项目是一个Maya插件主要功能是提供与MetaHuman相同拓扑的模型或者自定义的3d模型以来完成自定义绑定编辑DNA校准骨骼位置保存DNA载入DNA导出fbx保存DNA文件, 编辑BlendShape,等功能。
## 这个插件主要功能:
提供与MetaHuman相同拓扑的模型或者自定义的3d模型以来完成自定义绑定编辑DNA校准骨骼位置保存DNA载入DNA导出fbx保存DNA文件, 编辑BlendShape,等功能。
## 注意Reference路径不参与参见功能实现只作为参考。Reference只作为参考可以从中拷贝必要的文件到当前项目中
## 代码实现:
根据Maya和Python版本来获取plugin的路径并尽可能使用PySide编写UI要保证PySide的通用性使用单独的ccs文件来定义定义样式。
根据Maya不同的版本来定义PySide的UI定义版本通用性参考MSLiveLink。
## 参考代码:
DNA\_Calibration中主要参考DNA编辑等功能更SuperRigigng主要参考UI样式并获取对应的功能的实现逻辑MSLiveLink主要参开DNA编辑和文件处理方式。
## 产品功能对标:
DNA Calibration Document : https://epicgames.github.io/MetaHuman-DNA-Calibration/index.html
MetaHuman-DNA-Calibration 代码https://github.com/EpicGames/MetaHuman-DNA-Calibration
SuperRigginghttps://docs.pointart.net/
AnimCrafthttps://geekdaxue.co/read/animcraft@cn/
## 代码初始结构:
.
├── config
│ └── \_\_init\_\_.py
│ └── data.py
├── plugins
│ └── Linux
│ │ ├── 2022
│ │ │ ├── \_py3dnacalib.so
│ │ │ ├── dnacalib.py
│ │ │ ├── libdnacalib.so.6
│ │ │ ├── libembeddedRL4.so
│ │ │ ├── libembeddedRL4.so.8
│ │ │ ├── libembeddedRL4.so.8.0.8
│ │ │ ├── MayaUE4RBFPlugin2022.mll
│ │ │ ├── MayaUERBFPlugin.mll
│ │ ├── 2023
│ │ │ ├── \_py3dnacalib.so
│ │ │ ├── dnacalib.py
│ │ │ ├── libdnacalib.so.6
│ │ │ ├── libembeddedRL4.so
│ │ │ ├── libembeddedRL4.so.8
│ │ │ ├── libembeddedRL4.so.8.0.8
│ │ │ ├── MayaUE4RBFPlugin2023.mll
│ │ │ ├── MayaUERBFPlugin.mll
│ │ ├── 2024
│ │ │ ├── \_py3dnacalib.so
│ │ │ ├── dnacalib.py
│ │ │ ├── libdnacalib.so.6
│ │ │ ├── libembeddedRL4.so
│ │ │ ├── MayaUERBFPlugin.mll
│ │ ├── 2025
│ │ │ ├── \_py3dnacalib.so
│ │ │ ├── dnacalib.py
│ │ │ ├── embeddedRL4.so
│ │ │ ├── libdnacalib.so.6
│ │ │ ├── MayaUERBFPlugin.mll
│ │ ├── pydna
│ │ │ ├── python3
│ │ │ │ ├── \_py3dna.so
│ │ │ │ ├── dna.py
│ │ │ │ ├── libdna.so.7.1.0
│ │ │ ├── python311
│ │ │ │ ├── \_py3dna.so
│ │ │ │ ├── dna.py
│ │ │ │ ├── libdna.so.7
│ │ │ ├── python397
│ │ │ │ ├── \_py3dna.so
│ │ │ │ ├── dna.py
│ │ │ │ ├── libdna.so.7.1.0
│ │ │ ├── python3108
│ │ │ │ ├── \_py3dna.so
│ │ │ │ ├── dna.py
│ │ │ │ ├── libdna.so.7.1.0
│ └── Windows
│ │ ├── 2022
│ │ │ ├── \_py3dnacalib.pyd
│ │ │ ├── dnacalib.dll
│ │ │ ├── dnacalib.py
│ │ │ ├── embeddedRL4.mll
│ │ │ ├── MayaUE4RBFPlugin2022.mll
│ │ │ ├── MayaUERBFPlugin.mll
│ │ ├── 2023
│ │ │ ├── \_py3dnacalib.pyd
│ │ │ ├── dnacalib.dll
│ │ │ ├── dnacalib.py
│ │ │ ├── embeddedRL4.mll
│ │ │ ├── MayaUE4RBFPlugin2023.mll
│ │ │ ├── MayaUERBFPlugin.mll
│ │ ├── 2024
│ │ │ ├── \_py3dnacalib.pyd
│ │ │ ├── dnacalib.dll
│ │ │ ├── dnacalib.py
│ │ │ ├── embeddedRL4.mll
│ │ │ ├── MayaUERBFPlugin.mll
│ │ ├── 2025
│ │ │ ├── \_py3dnacalib.pyd
│ │ │ ├── dnacalib.dll
│ │ │ ├── dnacalib.py
│ │ │ ├── embeddedRL4.mll
│ │ │ ├── MayaUERBFPlugin.mll
│ │ ├── pydna
│ │ │ ├── python3
│ │ │ │ ├── \_py3dna.pyd
│ │ │ │ ├── dna.dll
│ │ │ │ ├── dna.py
│ │ │ ├── python311
│ │ │ │ ├── \_py3dna.pyd
│ │ │ │ ├── \_py3dna9\_4\_3.pyd
│ │ │ │ ├── dna.dll
│ │ │ │ ├── dna.py
│ │ │ │ ├── dna9\_4\_3.dll
│ │ │ │ ├── polyalloc1\_3\_12.dll
│ │ │ │ ├── statuscode1\_2\_6.dll
│ │ │ │ ├── trio4\_0\_16.dll
│ │ │ ├── python397
│ │ │ │ ├── \_py3dna.pyd
│ │ │ │ ├── dna.dll
│ │ │ │ ├── dna.py
│ │ │ ├── python3108
│ │ │ │ ├── \_py3dna.pyd
│ │ │ │ ├── dna.dll
│ │ │ │ ├── dna.py
│ │ │ ├── \_\_init\_\_.py
├── resources
│ ├── icons
│ │ ├── ARKit52.png
│ │ ├── automatic\_grouping.png
│ │ ├── backward.png
│ │ ├── behaviour.png
│ │ ├── blendRaw.png
│ │ ├── blendShape\_current.png
│ │ ├── blendShape.png
│ │ ├── change\_password.png
│ │ ├── chinese.png
│ │ ├── clone\_blendShape.png
│ │ ├── clothing\_weight.png
│ │ ├── color.png
│ │ ├── CommandButton.png
│ │ ├── configuration.png
│ │ ├── connect.png
│ │ ├── controller.png
│ │ ├── copy\_skin.png
│ │ ├── copy.png
│ │ ├── create\_body\_ctrl.png
│ │ ├── create\_lod.png
│ │ ├── ctrl\_hide.png
│ │ ├── definition.png
│ │ ├── delete.png
│ │ ├── detector.png
│ │ ├── disconnect.png
│ │ ├── english.png
│ │ ├── exit.png
│ │ ├── export\_skin.png
│ │ ├── export.png
│ │ ├── expression.png
│ │ ├── expressions\_blend.png
│ │ ├── expressions\_current.png
│ │ ├── expressions.png
│ │ ├── forward.png
│ │ ├── help.png
│ │ ├── import\_body\_anim.png
│ │ ├── import\_face\_anim.png
│ │ ├── import\_skin.png
│ │ ├── import.png
│ │ ├── joint.png
│ │ ├── load\_meshes.png
│ │ ├── loading.png
│ │ ├── locator.png
│ │ ├── lock.png
│ │ ├── mark.png
│ │ ├── meshes.png
│ │ ├── message.png
│ │ ├── MetaFusionLogo.png
│ │ ├── mirror.png
│ │ ├── mirrorL.png
│ │ ├── mirrorR.png
│ │ ├── motion\_apply.png
│ │ ├── open\_camera.png
│ │ ├── open.png
│ │ ├── pause.png
│ │ ├── play.png
│ │ ├── plus.png
│ │ ├── pose\_A\_To\_T.png
│ │ ├── pose\_T\_To\_A.png
│ │ ├── presets.png
│ │ ├── psd.png
│ │ ├── rebuildTargets.png
│ │ ├── reduce.png
│ │ ├── rename.png
│ │ ├── repair\_normals.png
│ │ ├── repair\_vertex\_order.png
│ │ ├── reset.png
│ │ ├── resetname.png
│ │ ├── return.png
│ │ ├── save\_new.png
│ │ ├── save.png
│ │ ├── search.png
│ │ ├── set\_current.png
│ │ ├── set\_no.png
│ │ ├── set\_ok.png
│ │ ├── set\_range.png
│ │ ├── settings.png
│ │ ├── standardized\_naming.png
│ │ ├── stop.png
│ │ ├── supplement\_meshes.png
│ │ ├── symmetry.png
│ │ ├── target.png
│ │ ├── transfer\_maps.png
│ │ ├── unmark\_all.png
│ │ ├── unreal.png
│ │ ├── update.png
│ │ ├── user\_login.png
│ │ ├── visible.png
│ │ ├── warning.png
│ ├── styles
│ │ ├── style.qss
├── scripts
│ ├── ui
│ ├── MetaFusion.py
├── CleanPycache.bat
├── Install.mel
├── Install.py
├── README.md
## 重要代码内容:
├── Install.mel
```Python
global proc install()
{
string $scriptPath = `whatIs install`;
string $dirPath = `substring $scriptPath 25 (size($scriptPath))`;
$dirPath = `dirname $dirPath`;
string $pythonPath = $dirPath + "/Install.py";
$pythonPath = substituteAllString($pythonPath, "\\", "/");
string $pythonCmd = "import os, sys\n";
$pythonCmd += "INSTALL_PATH = r'" + $pythonPath + "'\n";
$pythonCmd += "sys.path.append(os.path.dirname(INSTALL_PATH))\n";
$pythonCmd += "import Install\n";
$pythonCmd += "Install.main()\n";
python($pythonCmd);
}
install();
```
├── Install.py
```Python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#===================================== 1. Module Imports =====================================
# Standard library imports
import os
import sys
import webbrowser
# Maya imports
import maya.mel as mel
import maya.cmds as cmds
import maya.OpenMayaUI as omui
# Qt imports
from PySide2 import QtWidgets, QtGui, QtCore
from shiboken2 import wrapInstance
# Custom imports
from config import data
QtCore, QtGui, QtWidgets = data.Qt()
#===================================== 2. Global Variables =====================================
ROOT_PATH = data.ROOT_PATH
TOOL_NAME = data.TOOL_NAME
TOOL_VERSION = data.TOOL_VERSION
TOOL_AUTHOR = data.TOOL_AUTHOR
TOOL_LANG = data.TOOL_LANG
TOOL_WSCL_NAME = data.TOOL_WSCL_NAME
TOOL_HELP_URL = data.TOOL_HELP_URL
SCRIPTS_PATH = data.SCRIPTS_PATH
ICONS_PATH = data.ICONS_PATH
TOOL_MAIN_SCRIPT = data.TOOL_MAIN_SCRIPT
TOOL_MOD_FILENAME = data.TOOL_MOD_FILENAME
TOOL_ICON = data.TOOL_ICON
TOOL_COMMAND_ICON = data.TOOL_COMMAND_ICON
#===================================== 3. Utility Functions =====================================
def maya_main_window():
"""Get Maya main window as QWidget"""
main_window_ptr = omui.MQtUtil.mainWindow()
return wrapInstance(int(main_window_ptr), QtWidgets.QWidget)
def ensure_directory(directory_path):
"""Ensure directory exists, create if not"""
if not os.path.exists(directory_path):
os.makedirs(directory_path)
print(f"Created directory: {directory_path}")
return directory_path
def get_maya_modules_dir():
"""Get Maya modules directory path"""
maya_app_dir = cmds.internalVar(userAppDir=True)
return ensure_directory(os.path.join(maya_app_dir, "modules"))
#===================================== 4. UI Component Classes =====================================
class SetButton(QtWidgets.QPushButton):
"""Custom styled button for installation interface"""
def __init__(self, text):
super(SetButton, self).__init__(text)
#===================================== 5. Main Window Class =====================================
class InstallDialog(QtWidgets.QDialog):
def __init__(self, parent=maya_main_window()):
super(InstallDialog, self).__init__(parent)
self.load_stylesheet()
self.setup_ui()
def load_stylesheet(self):
"""加载 QSS 样式文件"""
try:
style_file = data.TOOL_STYLE_FILE
if os.path.exists(style_file):
with open(style_file, 'r') as f:
self.setStyleSheet(f.read())
else:
print(f"Warning: Style file not found: {style_file}")
except Exception as e:
print(f"Error loading stylesheet: {e}")
def setup_ui(self):
"""Initialize and setup UI components"""
self.setWindowTitle(f"{TOOL_NAME} Installation")
self.setFixedSize(220, 120)
self.setup_window_icon()
self.create_widgets()
self.create_layouts()
self.create_connections()
def setup_window_icon(self):
"""Setup window icon if available"""
if os.path.exists(TOOL_ICON):
self.setWindowIcon(QtGui.QIcon(TOOL_ICON))
else:
print(f"Warning: Icon file not found: {TOOL_ICON}")
#----------------- 5.1 UI Methods -----------------
def create_widgets(self):
self.new_shelf_toggle = QtWidgets.QCheckBox(f"{TOOL_NAME} Installation")
self.install_button = SetButton("Install " + TOOL_NAME)
self.uninstall_button = SetButton("Uninstall " + TOOL_NAME)
def create_layouts(self):
main_layout = QtWidgets.QVBoxLayout(self)
main_layout.setContentsMargins(10, 2, 10, 5)
main_layout.setSpacing(5)
header_layout = QtWidgets.QHBoxLayout()
header_layout.setSpacing(5)
welcome_label = QtWidgets.QLabel("Welcome to " + TOOL_NAME + "!")
welcome_label.setStyleSheet("font-size: 11px; padding: 0px; margin: 0px;")
header_layout.addWidget(welcome_label)
header_layout.addStretch()
main_layout.addLayout(header_layout)
main_layout.addWidget(self.install_button)
main_layout.addWidget(self.uninstall_button)
self.install_button.setFixedHeight(30)
self.uninstall_button.setFixedHeight(30)
def create_connections(self):
self.install_button.clicked.connect(self.install)
self.uninstall_button.clicked.connect(self.uninstall)
def create_styled_message_box(self, title, text):
msg_box = QtWidgets.QMessageBox(self)
msg_box.setWindowTitle(title)
msg_box.setText(text)
msg_box.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No)
msg_box.setStyleSheet(self.styleSheet())
return msg_box
#----------------- 5.2 Event Handler Methods -----------------
def event(self, event):
if event.type() == QtCore.QEvent.EnterWhatsThisMode:
QtWidgets.QWhatsThis.leaveWhatsThisMode()
self.open_help_url()
return True
return QtWidgets.QDialog.event(self, event)
def closeEvent(self, event):
"""Handle window close event"""
try:
super(InstallDialog, self).closeEvent(event)
except Exception as e:
print(f"Error closing window: {e}")
event.accept()
def helpEvent(self, event):
self.open_help_url()
event.accept()
#----------------- 5.3 Utility Methods -----------------
def open_help_url(self):
webbrowser.open(TOOL_HELP_URL)
QtWidgets.QApplication.restoreOverrideCursor()
def get_script_path(self):
maya_script = mel.eval('getenv("MAYA_SCRIPT_NAME")')
if maya_script and os.path.exists(maya_script):
return os.path.dirname(maya_script)
for sys_path in sys.path:
install_path = os.path.join(sys_path, "install.py")
if os.path.exists(install_path):
return os.path.dirname(install_path)
return os.getcwd()
#----------------- 5.4 Installation Methods -----------------
def install(self):
"""Handle install request with error handling"""
if not self._validate_paths():
return
msg_box = self.create_styled_message_box(
"Confirm Installation",
f"Are you sure you want to install {TOOL_NAME}?"
)
if msg_box.exec_() == QtWidgets.QMessageBox.Yes:
try:
self.install_tool()
self.close()
except Exception as e:
error_msg = f"Error during installation: {e}"
print(error_msg)
QtWidgets.QMessageBox.critical(self, "Error", error_msg)
def uninstall(self, *args):
"""Handle uninstall request"""
msg_box = self.create_styled_message_box(
"Confirm Uninstallation",
f"Are you sure you want to uninstall {TOOL_NAME}?"
)
if msg_box.exec_() == QtWidgets.QMessageBox.Yes:
try:
self.uninstall_tool()
self.close()
except Exception as e:
error_msg = f"Error during uninstallation: {e}"
print(error_msg)
QtWidgets.QMessageBox.critical(self, "Error", error_msg)
else:
print("Uninstallation cancelled")
def create_mod_file(self):
"""Create or update the .mod file for Maya"""
modules_dir = get_maya_modules_dir()
mod_content = f"""+ {TOOL_NAME} {TOOL_VERSION} {ROOT_PATH}
scripts: {SCRIPTS_PATH}
"""
mod_file_path = os.path.join(modules_dir, TOOL_MOD_FILENAME)
self._write_mod_file(mod_file_path, mod_content)
def _write_mod_file(self, file_path, content):
"""Helper method to write .mod file"""
try:
with open(file_path, "w") as f:
f.write(content)
print(f"Successfully created/updated: {file_path}")
except Exception as e:
error_msg = f"Error writing .mod file: {e}"
print(error_msg)
QtWidgets.QMessageBox.critical(self, "Error", error_msg)
def uninstall_mod_file(self):
modules_dir = get_maya_modules_dir()
mod_file_path = os.path.join(modules_dir, TOOL_MOD_FILENAME)
if os.path.exists(mod_file_path):
try:
os.remove(mod_file_path)
print(f"{TOOL_NAME}.mod file deleted")
except Exception as e:
print(f"Error deleting {TOOL_NAME}.mod file: {e}")
def clean_existing_buttons(self):
if cmds.shelfLayout(TOOL_NAME, exists=True):
buttons = cmds.shelfLayout(TOOL_NAME, query=True, childArray=True) or []
for btn in buttons:
if cmds.shelfButton(btn, query=True, exists=True):
label = cmds.shelfButton(btn, query=True, label=True)
if label == TOOL_NAME:
cmds.deleteUI(btn)
print(f"Deleted existing {TOOL_NAME} button: {btn}")
def install_tool(self):
"""Install the tool to Maya"""
if not os.path.exists(SCRIPTS_PATH):
print(f"Error: Scripts path does not exist: {SCRIPTS_PATH}")
return
if not os.path.exists(TOOL_MAIN_SCRIPT):
print(f"Error: Main script file not found: {TOOL_MAIN_SCRIPT}")
return
# Add scripts path to Python path
if SCRIPTS_PATH not in sys.path:
sys.path.insert(0, SCRIPTS_PATH)
# Create shelf and button
self._create_shelf_button()
self.create_mod_file()
# Switch to the newly created shelf
try:
cmds.shelfTabLayout("ShelfLayout", edit=True, selectTab=TOOL_NAME)
print(f"Switched to {TOOL_NAME} shelf")
except Exception as e:
print(f"Error switching to {TOOL_NAME} shelf: {e}")
self._show_install_success_message()
def _create_shelf_button(self):
"""Create shelf button for the tool"""
shelf_layout = mel.eval('$tmpVar=$gShelfTopLevel')
# Create shelf if not exists
if not cmds.shelfLayout(TOOL_NAME, exists=True):
cmds.shelfLayout(TOOL_NAME, parent=shelf_layout)
# Clean existing buttons
self.clean_existing_buttons()
# Create new button
icon_path = TOOL_ICON if os.path.exists(TOOL_ICON) else TOOL_COMMAND_ICON
command = self._get_shelf_button_command()
cmds.shelfButton(
parent=TOOL_NAME,
image1=icon_path,
label=TOOL_NAME,
command=command,
sourceType="python",
annotation=f"{TOOL_NAME} {TOOL_VERSION}",
noDefaultPopup=True,
style="iconOnly"
)
def _get_shelf_button_command(self):
"""Get the command string for shelf button"""
return f"""
import sys
import os
SCRIPTS_PATH = r'{SCRIPTS_PATH}'
if SCRIPTS_PATH not in sys.path:
sys.path.insert(0, SCRIPTS_PATH)
os.chdir(SCRIPTS_PATH)
try:
import {TOOL_NAME}
{TOOL_NAME}.show()
except ImportError as e:
print("Error importing {TOOL_NAME}:", str(e))
print("Scripts path:", SCRIPTS_PATH)
print("sys.path:", sys.path)
print("Contents of Scripts folder:", os.listdir(SCRIPTS_PATH))
"""
def uninstall_tool(self):
"""Uninstall the tool from Maya"""
window_name = f"{TOOL_NAME}Window"
dock_name = f"{TOOL_NAME}WindowDock"
shelf_file = f"shelf_{TOOL_NAME}.mel"
if cmds.window(window_name, exists=True):
try:
cmds.deleteUI(window_name)
except Exception as e:
print(f"Error closing {TOOL_NAME} window: {e}")
if cmds.dockControl(dock_name, exists=True):
try:
cmds.deleteUI(dock_name)
except Exception as e:
print(f"Error closing docked {TOOL_NAME} window: {e}")
self.uninstall_mod_file()
# Get the current shelf before removing it
current_shelf = cmds.shelfTabLayout("ShelfLayout", query=True, selectTab=True)
# Delete Shelves and Buttons
if cmds.shelfLayout(TOOL_NAME, exists=True):
try:
cmds.deleteUI(TOOL_NAME, layout=True)
except Exception as e:
print(f"Error deleting {TOOL_NAME} shelf: {e}")
self._clean_all_shelf_buttons()
# Remove from Python path
if SCRIPTS_PATH in sys.path:
sys.path.remove(SCRIPTS_PATH)
# Deleting Shelf Files
shelf_path = os.path.join(
cmds.internalVar(userAppDir=True),
cmds.about(version=True),
"prefs",
"shelves",
f"shelf_{TOOL_NAME}.mel"
)
if os.path.exists(shelf_path):
try:
os.remove(shelf_path)
except Exception as e:
print(f"Error deleting shelf file: {e}")
# If the current tool shelf is a deleted tool shelf, switch to another tool shelf
if current_shelf == TOOL_NAME:
shelves = cmds.shelfTabLayout("ShelfLayout", query=True, childArray=True)
if shelves and len(shelves) > 0:
cmds.shelfTabLayout("ShelfLayout", edit=True, selectTab=shelves[0])
self._show_uninstall_success_message()
def _clean_all_shelf_buttons(self):
"""Clean up all shelf buttons related to the tool"""
all_shelves = cmds.shelfTabLayout("ShelfLayout", query=True, childArray=True) or []
for shelf in all_shelves:
shelf_buttons = cmds.shelfLayout(shelf, query=True, childArray=True) or []
for btn in shelf_buttons:
if cmds.shelfButton(btn, query=True, exists=True):
if cmds.shelfButton(btn, query=True, label=True) == TOOL_NAME:
cmds.deleteUI(btn)
def _show_uninstall_success_message(self):
"""Show uninstallation success message"""
msg_box = QtWidgets.QMessageBox()
msg_box.setWindowTitle("Uninstallation Successful")
msg_box.setText(f"{TOOL_NAME} has been successfully uninstalled!")
msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok)
msg_box.setWindowIcon(QtGui.QIcon(TOOL_ICON))
msg_box.setStyleSheet(self.styleSheet())
msg_box.exec_()
def _show_install_success_message(self):
msg_box = QtWidgets.QMessageBox()
msg_box.setWindowTitle("Installation Successful")
msg_box.setText(f"{TOOL_NAME} has been successfully installed!")
msg_box.setStandardButtons(QtWidgets.QMessageBox.Ok)
msg_box.setWindowIcon(QtGui.QIcon(TOOL_ICON))
msg_box.setStyleSheet(self.styleSheet())
msg_box.exec_()
def _validate_paths(self):
"""Validate all required paths exist"""
paths = {
"Root": ROOT_PATH,
"Scripts": SCRIPTS_PATH,
"Icons": ICONS_PATH
}
for name, path in paths.items():
if not os.path.exists(path):
error_msg = f"Error: {name} path does not exist: {path}"
print(error_msg)
QtWidgets.QMessageBox.critical(self, "Error", error_msg)
return False
return True
def _log(self, message, error=False):
"""Log messages with timestamp"""
from datetime import datetime
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
log_message = f"[{timestamp}] {message}"
print(log_message)
if error:
QtWidgets.QMessageBox.critical(self, "Error", message)
def _load_mel_shelf(self):
"""Load mel shelf file with error handling"""
try:
mel.eval(f'loadNewShelf "shelf_{TOOL_NAME}.mel"')
except Exception as e:
self._log(f"Error loading shelf file: {e}", error=True)
#===================================== 6. Main Function =====================================
def main():
"""Main entry point for the installer"""
try:
dialog = InstallDialog()
dialog.show()
except Exception as e:
print(f"Error launching installer: {e}")
return -1
return dialog
if __name__ == "__main__":
main()
```
│ ├── styles
│ │ ├── style.qss
```Python
/* 全局 QPushButton 样式 */
QPushButton {
background-color: #D0D0D0;
color: #303030;
border-radius: 10px;
padding: 5px;
font-weight: bold;
min-width: 80px;
}
QPushButton:hover {
background-color: #E0E0E0;
}
QPushButton:pressed {
background-color: #C0C0C0;
}
/* 单独的消息按钮样式可选 */
.messageButton {
background-color: #B0B0B0;
color: #303030;
border-radius: 10px;
padding: 5px;
font-weight: bold;
min-width: 80px;
}
.messageButton:hover {
background-color: #C0C0C0;
}
.messageButton:pressed {
background-color: #A0A0A0;
}
/* MetaFusion 深色主题样式 */
/* 主窗口样式 */
QMainWindow {
background-color: #333333;
color: #CCCCCC;
}
/* 菜单栏样式 */
QMenuBar {
background-color: #333333;
color: #CCCCCC;
border-bottom: 1px solid #222222;
}
QMenuBar::item {
background-color: transparent;
padding: 4px 8px;
}
QMenuBar::item:selected {
background-color: #444444;
}
/* 工具栏样式 */
QToolBar {
background-color: #333333;
border: none;
padding: 3px;
}
QToolButton {
background-color: transparent;
border: 1px solid transparent;
border-radius: 2px;
padding: 4px;
margin: 1px;
}
QToolButton:hover {
background-color: #444444;
border: 1px solid #555555;
}
/* 标签页样式 */
QTabWidget::pane {
border: 1px solid #222222;
background-color: #333333;
}
QTabBar::tab {
background-color: #2A2A2A;
color: #CCCCCC;
padding: 5px 10px;
border: 1px solid #222222;
min-width: 80px;
}
QTabBar::tab:selected {
background-color: #333333;
border-bottom: none;
}
QTabBar::tab:hover:not(:selected) {
background-color: #3A3A3A;
}
/* 列表和树形控件样式 */
QTreeView, QListView {
background-color: #2A2A2A;
border: 1px solid #222222;
color: #CCCCCC;
}
QTreeView::item:hover, QListView::item:hover {
background-color: #3A3A3A;
}
QTreeView::item:selected, QListView::item:selected {
background-color: #444444;
}
/* 输入框样式 */
QLineEdit {
background-color: #2A2A2A;
border: 1px solid #222222;
border-radius: 2px;
color: #CCCCCC;
padding: 3px;
}
/* 下拉框样式 */
QComboBox {
background-color: #2A2A2A;
border: 1px solid #222222;
border-radius: 2px;
color: #CCCCCC;
padding: 3px;
min-width: 100px;
}
QComboBox::drop-down {
border: none;
width: 20px;
}
QComboBox::down-arrow {
border-image: url(:/resources/icons/down_arrow.png);
width: 12px;
height: 12px;
}
/* 按钮样式 */
QPushButton {
background-color: #2A2A2A;
border: 1px solid #222222;
border-radius: 2px;
color: #CCCCCC;
padding: 5px 15px;
min-width: 80px;
}
QPushButton:hover {
background-color: #3A3A3A;
border: 1px solid #444444;
}
QPushButton:pressed {
background-color: #222222;
}
/* 滚动条样式 */
QScrollBar:vertical {
background: #2A2A2A;
width: 10px;
margin: 0;
}
QScrollBar::handle:vertical {
background: #444444;
min-height: 20px;
border-radius: 5px;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
height: 0px;
}
QScrollBar:horizontal {
background: #2A2A2A;
height: 10px;
margin: 0;
}
QScrollBar::handle:horizontal {
background: #444444;
min-width: 20px;
border-radius: 5px;
}
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
width: 0px;
}
/* 分组框样式 */
QGroupBox {
border: 1px solid #222222;
border-radius: 3px;
margin-top: 6px;
padding-top: 6px;
color: #CCCCCC;
}
QGroupBox::title {
left: 7px;
padding: 0px 3px;
}
/* 状态栏样式 */
QStatusBar {
background-color: #333333;
color: #CCCCCC;
}
/* 工具提示样式 */
QToolTip {
background-color: #2A2A2A;
border: 1px solid #222222;
color: #CCCCCC;
padding: 3px;
}
```
├── config
│ └── \_\_init\_\_.py
```Python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from . import *
```
│ └── data.py
```Python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
import maya.cmds as cmds
#===================================== 2. Global Variables =====================================
try:
ROOT_PATH = os.path.dirname(INSTALL_PATH).replace("\\", "/")
except NameError:
# __file__ 在 config 中,所以返回上一级目录即项目根目录
ROOT_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))).replace("\\", "/")
TOOL_NAME = "MetaFusion"
TOOL_VERSION = "Beta v1.0.0"
TOOL_AUTHOR = "Virtuos"
TOOL_LANG = 'en_US'
TOOL_WSCL_NAME = f"{TOOL_NAME}WorkSpaceControl"
TOOL_HELP_URL = f"https://gitea.cgnico.com/CGNICO/{TOOL_NAME}/wiki"
#===================================== 3. Paths =====================================
# PATHS
SCRIPTS_PATH = os.path.join(ROOT_PATH, "scripts").replace("\\", "/")
ICONS_PATH = os.path.join(ROOT_PATH, "resources", "icons").replace("\\", "/")
STYLES_PATH = os.path.join(ROOT_PATH, "resources", "styles").replace("\\", "/")
MAYA_VERSION = cmds.about(version=True)
SYSTEM_OS = cmds.about(os=True)
if MAYA_VERSION in ["2022", "2023", "2024", "2025"]:
PLUGIN_PATH = os.path.join(ROOT_PATH, "plugins", SYSTEM_OS, MAYA_VERSION).replace("\\", "/")
else:
print(f"MetaFusion is not supported on Maya {MAYA_VERSION}")
PYTHON_VERSION = sys.version_info.major
if PYTHON_VERSION == 3:
PYDNA_PATH = os.path.join(ROOT_PATH, "plugins", SYSTEM_OS, "python3").replace("\\", "/")
elif PYTHON_VERSION == 3.11:
PYDNA_PATH = os.path.join(ROOT_PATH, "plugins", SYSTEM_OS, "python311").replace("\\", "/")
elif PYTHON_VERSION == 3.9:
PYDNA_PATH = os.path.join(ROOT_PATH, "plugins", SYSTEM_OS, "python397").replace("\\", "/")
elif PYTHON_VERSION == 3.10:
PYDNA_PATH = os.path.join(ROOT_PATH, "plugins", SYSTEM_OS, "python3108").replace("\\", "/")
else:
print(f"MetaFusion is not supported on Python {PYTHON_VERSION}")
#===================================== 3. Files =====================================
# FILES
TOOL_MAIN_SCRIPT = os.path.join(SCRIPTS_PATH, f"{TOOL_NAME}.py").replace("\\", "/")
TOOL_STYLE_FILE = os.path.join(STYLES_PATH, "style.qss").replace("\\", "/")
TOOL_ICON = os.path.join(ICONS_PATH, f"{TOOL_NAME}Logo.png").replace("\\", "/")
TOOL_COMMAND_ICON = os.path.join(ICONS_PATH, "CommandButton.png").replace("\\", "/")
TOOL_MOD_FILENAME = f"{TOOL_NAME}.mod"
#===================================== 4. Qt =====================================
# Qt
def Qt():
try:
from PySide import QtCore, QtGui, QtWidgets
return QtCore, QtGui, QtWidgets
except ImportError:
try:
from PySide2 import QtCore, QtGui, QtWidgets
return QtCore, QtGui, QtWidgets
except ImportError:
try:
from PySide3 import QtCore, QtGui, QtWidgets
return QtCore, QtGui, QtWidgets
except ImportError:
try:
from PySide4 import QtCore, QtGui, QtWidgets
return QtCore, QtGui, QtWidgets
except ImportError:
try:
from PySide5 import QtCore, QtGui, QtWidgets
return QtCore, QtGui, QtWidgets
except ImportError:
try:
from PySide6 import QtCore, QtGui, QtWidgets
return QtCore, QtGui, QtWidgets
except ImportError:
print("未找到 Qt 模块")
return None, None, None
```
├── scripts
│ ├── MetaFusion.py
```Python
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import sys
from PySide2 import QtCore, QtGui, QtWidgets
# 添加项目根目录到 Python 路径
ROOT_DIR = os.path.dirname(os.path.dirname(__file__))
if ROOT_DIR not in sys.path:
sys.path.insert(0, ROOT_DIR)
from config import data
QtCore, QtGui, QtWidgets = data.Qt()
#===================================== 2. Global Variables =====================================
TOOL_NAME = data.TOOL_NAME
TOOL_VERSION = data.TOOL_VERSION
TOOL_AUTHOR = data.TOOL_AUTHOR
TOOL_LANG = data.TOOL_LANG
TOOL_WSCL_NAME = data.TOOL_WSCL_NAME
TOOL_HELP_URL = data.TOOL_HELP_URL
SCRIPTS_PATH = data.SCRIPTS_PATH
ICONS_PATH = data.ICONS_PATH
TOOL_MAIN_SCRIPT = data.TOOL_MAIN_SCRIPT
TOOL_MOD_FILENAME = data.TOOL_MOD_FILENAME
TOOL_ICON = data.TOOL_ICON
TOOL_COMMAND_ICON = data.TOOL_COMMAND_ICON
main_window = None
class MetaFusionWindow(QtWidgets.QMainWindow):
def __init__(self, parent=None):
super(MetaFusionWindow, self).__init__(parent)
self.setWindowTitle("MetaFusion")
self.resize(800, 600)
# 加载样式表
self.load_stylesheet()
# 创建UI
self.setup_ui()
def load_stylesheet(self):
"""加载QSS样式表"""
style_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), "resources", "styles", "style.qss")
if os.path.exists(style_file):
with open(style_file, 'r', encoding='utf-8') as f:
self.setStyleSheet(f.read())
def setup_ui(self):
"""设置UI结构"""
# 创建中心部件
central_widget = QtWidgets.QWidget()
self.setCentralWidget(central_widget)
# 创建主布局
main_layout = QtWidgets.QVBoxLayout(central_widget)
# 创建菜单栏
self.create_menu_bar()
# 创建工具栏
self.create_tool_bar()
# 创建标签页
self.create_tabs()
def create_menu_bar(self):
"""创建菜单栏"""
menubar = self.menuBar()
# 文件菜单
file_menu = menubar.addMenu("文件")
file_menu.addAction("新建")
file_menu.addAction("打开")
file_menu.addAction("保存")
file_menu.addSeparator()
file_menu.addAction("退出")
# 编辑菜单
edit_menu = menubar.addMenu("编辑")
edit_menu.addAction("撤销")
edit_menu.addAction("重做")
# 帮助菜单
help_menu = menubar.addMenu("帮助")
help_menu.addAction("关于")
def create_tool_bar(self):
"""创建工具栏"""
toolbar = self.addToolBar("主工具栏")
toolbar.setMovable(False)
# 添加工具栏按钮
toolbar.addAction(QtGui.QIcon(":/icons/new.png"), "新建")
toolbar.addAction(QtGui.QIcon(":/icons/open.png"), "打开")
toolbar.addAction(QtGui.QIcon(":/icons/save.png"), "保存")
def create_tabs(self):
"""创建标签页"""
self.tab_widget = QtWidgets.QTabWidget()
self.centralWidget().layout().addWidget(self.tab_widget)
# 创建四个主要标签页
self.model_tab = QtWidgets.QWidget()
self.rig_tab = QtWidgets.QWidget()
self.adjust_tab = QtWidgets.QWidget()
self.define_tab = QtWidgets.QWidget()
# 添加标签页到标签页控件
self.tab_widget.addTab(self.model_tab, "模型")
self.tab_widget.addTab(self.rig_tab, "绑定")
self.tab_widget.addTab(self.adjust_tab, "调整")
self.tab_widget.addTab(self.define_tab, "定义")
# 设置各个标签页的内容
self.setup_model_tab()
self.setup_rig_tab()
self.setup_adjust_tab()
self.setup_define_tab()
def setup_model_tab(self):
"""设置模型标签页内容"""
layout = QtWidgets.QVBoxLayout(self.model_tab)
# 在这里添加模型标签页的具体控件
def setup_rig_tab(self):
"""设置绑定标签页内容"""
layout = QtWidgets.QVBoxLayout(self.rig_tab)
# 在这里添加绑定标签页的具体控件
def setup_adjust_tab(self):
"""设置调整标签页内容"""
layout = QtWidgets.QVBoxLayout(self.adjust_tab)
# 在这里添加调整标签页的具体控件
def setup_define_tab(self):
"""设置定义标签页内容"""
layout = QtWidgets.QVBoxLayout(self.define_tab)
# 在这里添加定义标签页的具体控件
def show():
"""显示主窗口"""
global main_window
try:
main_window.close()
except:
pass
main_window = MetaFusionWindow()
main_window.show()
return main_window
if __name__ == "__main__":
app = QtWidgets.QApplication([])
window = show()
app.exec_()
```