diff --git a/DevGoals.md b/DevGoals.md index 5f28270..b93a057 100644 --- a/DevGoals.md +++ b/DevGoals.md @@ -1 +1,1430 @@ - \ No newline at end of file +# 项目目标 + + 我想做一个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 + + SuperRigging:https://docs.pointart.net/ + + AnimCraft:https://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_() +``` \ No newline at end of file