MetaFusion/DevGoals.md

39 KiB
Raw Blame History

项目目标

我想做一个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

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

#!/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

/* 全局 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

#!/usr/bin/env python
# -*- coding: utf-8 -*-

from . import *

│ └── data.py

#!/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

#!/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_()