Files
MetaFusion/scripts/ui/geometry.py
2025-05-08 23:57:22 +08:00

499 lines
23 KiB
Python
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.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Geometry UI Module for Plugin
几何模型UI模块 - 负责显示几何模型编辑界面和基础操作
基本功能:
- 模型拾取以及加载
- LOD模型分级过滤
- LOD模型创建
- 自动加载模型
- 标准化命名
- 自动分组
- 生成面部配件(睑毛,舌头,泪腺 等)
- 修复接缝(修复法线)
- 修复点序
"""
#========================================= IMPORT =========================================
from scripts.ui.Qt import QtWidgets, QtCore, QtGui
from scripts.ui.Qt.QtCompat import wrapInstance
from maya import OpenMayaUI as omui
import maya.cmds as cmds
import maya.mel as mel
import maya.utils as utils
import webbrowser
import subprocess
import importlib
import traceback
import locale
import sys
import os
from scripts.ui import ui_utils
from scripts.utils import utils_geometry
#========================================== CONFIG ========================================
import config
TOOL_NAME = config.TOOL_NAME
TOOL_VERSION = config.TOOL_VERSION
TOOL_AUTHOR = config.TOOL_AUTHOR
TOOL_YEAR = config.TOOL_YEAR
TOOL_MOD_FILENAME = config.TOOL_MOD_FILENAME
TOOL_LANG = config.TOOL_LANG
TOOL_WSCL_NAME = config.TOOL_WSCL_NAME
TOOL_HELP_URL = config.TOOL_HELP_URL
TOOL_PATH = config.TOOL_PATH
SCRIPTS_PATH = config.SCRIPTS_PATH
TOOL_MAIN_SCRIPT = config.TOOL_MAIN_SCRIPT
UI_PATH = config.UI_PATH
STYLE_FILE = config.STYLE_FILE
ICONS_PATH = config.ICONS_PATH
TOOL_ICON = config.TOOL_ICON
ASSETS_PATH = config.ASSETS_PATH
DNA_FILE_PATH = config.DNA_FILE_PATH
DNA_IMG_PATH = config.DNA_IMG_PATH
TOOL_COMMAND_ICON = config.TOOL_COMMAND_ICON
TOOL_WIDTH = config.TOOL_WIDTH
TOOL_HEIGHT = config.TOOL_HEIGHT
#========================================= LOCATION =======================================
from scripts.ui import localization
TEXT = localization.TEXT
class GeometryUI(ui_utils.BaseUI):
"""
几何模型UI类 - 负责显示几何模型编辑界面和基础操作
继承自BaseUI类实现几何模型相关的UI功能
"""
# 只继承BaseUI不自定义单例相关代码
def __init__(self, parent=None):
super().__init__(parent)
# 创建主控件
self.main_widget = QtWidgets.QWidget()
self.main_widget.setObjectName("geometryMainWidget")
# 初始化UI
self.create_widgets()
self.create_layouts()
self.create_connections()
# 更新UI文本
self.update_language()
#========================================= WIDGET =======================================
def create_widgets(self):
"""
创建几何模型UI控件
包括按钮、标签、列表等
"""
# 标题标签 - 使用HTML格式化标题
title_text = f"<h4 style='margin:0;padding:5px;'>{TEXT('geometry_title', '几何模型')}</h4>"
self.controls["title_label"] = QtWidgets.QLabel(title_text)
self.controls["title_label"].setObjectName("geometryTitleLabel")
self.controls["title_label"].setAlignment(QtCore.Qt.AlignCenter)
self.controls["title_label"].setMaximumHeight(30) # 限制标题高度
# 创建主容器
self.controls["main_container"] = QtWidgets.QWidget()
self.controls["main_container"].setObjectName("geometryMainContainer")
# 创建标签页控件
self.controls["tab_widget"] = QtWidgets.QTabWidget()
self.controls["tab_widget"].setObjectName("geometryTabWidget")
# 创建LOD标签页
self.create_lod_tabs()
# 创建底部功能按钮区域
self.create_bottom_buttons()
def create_lod_tabs(self):
"""
创建LOD标签页
包括LOD0~LOD7的标签页
"""
# 创建8个LOD标签页
lod_names = [f"LOD{i}" for i in range(8)]
# 定义部件名称和显示名称的映射
part_display_names = {
"Head": "头部",
"Teeth": "牙齿",
"Saliva": "牙龈",
"EyeLeft": "左眼",
"EyeRight": "右眼",
"Eyeshell": "红脸",
"Eyeslashes": "眉毛",
"EyesEdge": "眉毛",
"Body": "身体"
}
# 创建标签页和内容
for lod_name in lod_names:
# 创建标签页
tab = QtWidgets.QWidget()
tab.setObjectName(f"{lod_name}Tab")
# 创建标签页布局
tab_layout = QtWidgets.QVBoxLayout(tab)
tab_layout.setContentsMargins(5, 5, 5, 5)
tab_layout.setSpacing(5)
# 根据LOD级别限制显示的身体部位
if lod_name == "LOD0":
parts_to_show = ["Head", "Teeth", "Saliva", "EyeLeft", "EyeRight", "Eyeshell", "Eyeslashes", "EyesEdge", "Body"]
elif lod_name == "LOD1":
parts_to_show = ["Head", "Teeth", "Saliva", "EyeLeft", "EyeRight", "Eyeshell", "Eyeslashes", "EyesEdge", "Body"]
elif lod_name in ["LOD2", "LOD3"]:
parts_to_show = ["Head", "Teeth", "Saliva", "EyeLeft", "EyeRight", "Eyeshell", "Eyeslashes", "EyesEdge"]
elif lod_name == "LOD4":
parts_to_show = ["Head", "Teeth", "EyeLeft", "EyeRight", "Eyeshell"]
else: # LOD5, LOD6, LOD7
parts_to_show = ["Head", "Teeth", "EyeLeft", "EyeRight"]
# 为每个身体部位创建输入字段和加载按钮
for part in parts_to_show:
# 创建水平布局
part_layout = QtWidgets.QHBoxLayout()
part_layout.setSpacing(5)
# 创建标签
label = QtWidgets.QLabel(f"{part_display_names[part]}:")
label.setMinimumWidth(40)
label.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
# 输入框
line_edit = QtWidgets.QLineEdit()
line_edit.setPlaceholderText(TEXT("enter_model_name", "输入模型名称"))
line_edit.setObjectName(f"{lod_name}_{part}_input")
# 创建加载按钮(统一风格)
load_button = QtWidgets.QPushButton(TEXT("load", " 加 载 "))
load_button.setObjectName("LoadButton")
load_button.setIcon(QtGui.QIcon(os.path.join(ICONS_PATH, "loading.png")))
load_button.setIconSize(QtCore.QSize(16, 16))
load_button.setToolTip(TEXT("load_model", "加载模型"))
load_button.setFixedSize(105, 24) # 更紧凑,和输入框高度完全一致
load_button.setStyleSheet("""
QPushButton {
padding: 2px 2px;
margin: 0px 5px 0px 0px; /* 上右下左的边距,增加上边距避免与标签栏重叠 */
}
""")
# 将控件添加到布局
part_layout.addWidget(label)
part_layout.addWidget(line_edit, stretch=1)
part_layout.addWidget(load_button)
part_layout.setStretch(0, 0) # label 不拉伸
part_layout.setStretch(1, 1) # 输入框自适应
part_layout.setStretch(2, 0) # 按钮固定宽度
# 将布局添加到标签页布局
tab_layout.addLayout(part_layout)
# 保存控件引用
self.controls[f"{lod_name}_{part}_label"] = label
self.controls[f"{lod_name}_{part}_input"] = line_edit
self.buttons[f"{lod_name}_{part}_load"] = load_button
# 添加弹性空间
tab_layout.addStretch(1)
# === 添加LOD专属底部三大按钮 ===
lod_buttons_layout = QtWidgets.QHBoxLayout()
lod_buttons_layout.setContentsMargins(0, 0, 0, 0)
lod_buttons_layout.setSpacing(10)
btn_auto_load = QtWidgets.QPushButton(TEXT("auto_load_meshes", "自动加载模型"))
btn_auto_load.setIcon(ui_utils.load_icon("load_meshes.png"))
btn_auto_load.setIconSize(QtCore.QSize(20, 20))
btn_auto_load.setObjectName(f"{lod_name}_autoLoadMeshesButton")
btn_auto_load.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
btn_standardized = QtWidgets.QPushButton(TEXT("standardized_naming", "标准化命名"))
btn_standardized.setIcon(ui_utils.load_icon("standardized_naming.png"))
btn_standardized.setIconSize(QtCore.QSize(20, 20))
btn_standardized.setObjectName(f"{lod_name}_standardizedNamingButton")
btn_standardized.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
btn_auto_group = QtWidgets.QPushButton(TEXT("automatic_grouping", "自动分组"))
btn_auto_group.setIcon(ui_utils.load_icon("automatic_grouping.png"))
btn_auto_group.setIconSize(QtCore.QSize(20, 20))
btn_auto_group.setObjectName(f"{lod_name}_automaticGroupingButton")
btn_auto_group.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
lod_buttons_layout.addWidget(btn_auto_load, 1)
lod_buttons_layout.addWidget(btn_standardized, 1)
lod_buttons_layout.addWidget(btn_auto_group, 1)
tab_layout.addLayout(lod_buttons_layout)
# 保存引用(如需后续操作)
self.buttons[f"{lod_name}_auto_load_meshes"] = btn_auto_load
self.buttons[f"{lod_name}_standardized_naming"] = btn_standardized
self.buttons[f"{lod_name}_automatic_grouping"] = btn_auto_group
# 将标签页添加到标签页控件
self.controls["tab_widget"].addTab(tab, lod_name)
# 创建清理按钮
self.buttons["clear"] = QtWidgets.QPushButton(TEXT("clear", " 清 理 "))
self.buttons["clear"].setObjectName("clearButton")
self.buttons["clear"].setIcon(ui_utils.load_icon("delete.png"))
self.buttons["clear"].setIconSize(QtCore.QSize(16, 16))
self.buttons["clear"].setFixedSize(150, 28)
self.buttons["clear"].setToolTip(TEXT("clear_all_models", "清理所有模型"))
self.buttons["clear"].setStyleSheet("""
QPushButton {
padding: 2px 2px;
margin: 0px 50px 0px 0px; /* 上右下左的边距,增加上边距避免与标签栏重叠 */
}
""")
self.buttons["clear"].setSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed)
def create_bottom_buttons(self):
"""
创建底部功能按钮区域
包括模型工具组其中包含拓扑结构、选择LOD、创建LOD按钮等
"""
# 创建底部功能区域
self.controls["bottom_area"] = QtWidgets.QWidget()
self.controls["bottom_area"].setObjectName("geometryBottomArea")
self.controls["bottom_area"].setMinimumHeight(180)
self.controls["bottom_area"].setMaximumHeight(250)
# 创建底部区域布局
bottom_layout = QtWidgets.QVBoxLayout(self.controls["bottom_area"])
bottom_layout.setContentsMargins(5, 5, 5, 5)
bottom_layout.setSpacing(5)
# 创建模型工具区域
model_tools_group = QtWidgets.QGroupBox(TEXT("model_tools", "模型工具"))
model_tools_group.setObjectName("modelToolsGroup")
# 创建模型工具布局
model_tools_layout = QtWidgets.QGridLayout(model_tools_group)
model_tools_layout.setContentsMargins(5, 5, 5, 5)
model_tools_layout.setSpacing(5)
# 第一行放置拓扑结构、选择LOD和创建LOD按钮
# 创建三等分的水平布局
self.layouts["top_row_layout"] = QtWidgets.QHBoxLayout()
self.layouts["top_row_layout"].setContentsMargins(0, 0, 0, 0)
self.layouts["top_row_layout"].setSpacing(10) # 增加间距
# 创建三个容器小部件,每个占据一个均等部分
# 1. 拓扑结构部分
topology_container = QtWidgets.QWidget()
topology_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
topology_container_layout = QtWidgets.QHBoxLayout(topology_container)
topology_container_layout.setContentsMargins(5, 5, 5, 5)
topology_label = QtWidgets.QLabel(TEXT("topology_structure", "拓扑结构") + ":")
self.controls["topology_combo"] = QtWidgets.QComboBox()
self.controls["topology_combo"].setObjectName("topologyCombo")
self.controls["topology_combo"].addItem("MetaHuman")
topology_container_layout.addWidget(topology_label)
topology_container_layout.addWidget(self.controls["topology_combo"], 1) # 下拉框占据剩余空间
# 2. 选择LOD部分
lod_container = QtWidgets.QWidget()
lod_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
lod_container_layout = QtWidgets.QHBoxLayout(lod_container)
lod_container_layout.setContentsMargins(5, 5, 5, 5)
lod_label = QtWidgets.QLabel(TEXT("select_lod", "选择LOD") + ":")
self.controls["lod_combo"] = QtWidgets.QComboBox()
self.controls["lod_combo"].setObjectName("lodCombo")
self.controls["lod_combo"].addItem(TEXT("all", "全部"))
for i in range(8): # LOD0~LOD7
self.controls["lod_combo"].addItem(f"LOD{i}")
lod_container_layout.addWidget(lod_label)
lod_container_layout.addWidget(self.controls["lod_combo"], 1) # 下拉框占据剩余空间
# 3. 创建LOD按钮部分
button_container = QtWidgets.QWidget()
button_container.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Preferred)
button_container_layout = QtWidgets.QHBoxLayout(button_container)
button_container_layout.setContentsMargins(5, 5, 5, 5)
self.buttons["create_lod"] = QtWidgets.QPushButton(TEXT("create_lod", "创建LOD"))
self.buttons["create_lod"].setObjectName("createLodButton")
self.buttons["create_lod"].setIcon(ui_utils.load_icon("create_lod.png"))
self.buttons["create_lod"].setMinimumHeight(30)
# 设置按钮尺寸策略为水平扩展,使其宽度撑满容器
self.buttons["create_lod"].setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
button_container_layout.addWidget(self.buttons["create_lod"])
# 将三个容器添加到水平布局,均等分配空间
self.layouts["top_row_layout"].addWidget(topology_container, 1)
self.layouts["top_row_layout"].addWidget(lod_container, 1)
self.layouts["top_row_layout"].addWidget(button_container, 1)
# 将水平布局添加到模型工具布局
model_tools_layout.addLayout(self.layouts["top_row_layout"], 0, 0, 1, 5)
# 保留原有按钮行布局
size_policy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.layouts["buttons_row1_layout"] = QtWidgets.QHBoxLayout()
self.layouts["buttons_row1_layout"].setContentsMargins(0, 0, 0, 0)
self.layouts["buttons_row1_layout"].setSpacing(10)
self.layouts["buttons_row2_layout"] = QtWidgets.QHBoxLayout()
self.layouts["buttons_row2_layout"].setContentsMargins(0, 0, 0, 0)
self.layouts["buttons_row2_layout"].setSpacing(10)
self.layouts["buttons_row3_layout"] = QtWidgets.QHBoxLayout()
self.layouts["buttons_row3_layout"].setContentsMargins(0, 0, 0, 0)
self.layouts["buttons_row3_layout"].setSpacing(10)
# 第一行按钮
self.buttons["separate_model"] = QtWidgets.QPushButton(TEXT("separate_model", "模型分离"))
self.buttons["separate_model"].setObjectName("separateModelButton")
self.buttons["separate_model"].setIcon(ui_utils.load_icon("polySplitVertex.png"))
self.buttons["separate_model"].setMinimumHeight(30)
self.buttons["separate_model"].setSizePolicy(size_policy)
self.buttons["generate_face_components"] = QtWidgets.QPushButton(TEXT("generate_face_components", "生成面部配件"))
self.buttons["generate_face_components"].setObjectName("generateFaceComponentsButton")
self.buttons["generate_face_components"].setIcon(ui_utils.load_icon("meshes.png"))
self.buttons["generate_face_components"].setMinimumHeight(30)
self.buttons["generate_face_components"].setSizePolicy(size_policy)
# 添加第一行按钮到布局
self.layouts["buttons_row1_layout"].addWidget(self.buttons["separate_model"])
self.layouts["buttons_row1_layout"].addWidget(self.buttons["generate_face_components"])
# 第二行按钮
self.buttons["fix_normals"] = QtWidgets.QPushButton(TEXT("fix_normals", "修复法线"))
self.buttons["fix_normals"].setObjectName("fixNormalsButton")
self.buttons["fix_normals"].setIcon(ui_utils.load_icon("repair_normals.png"))
self.buttons["fix_normals"].setMinimumHeight(30)
self.buttons["fix_normals"].setSizePolicy(size_policy)
self.buttons["fix_vertex_order"] = QtWidgets.QPushButton(TEXT("fix_vertex_order", "修复点序"))
self.buttons["fix_vertex_order"].setObjectName("fixVertexOrderButton")
self.buttons["fix_vertex_order"].setIcon(ui_utils.load_icon("repair_vertex_order.png"))
self.buttons["fix_vertex_order"].setMinimumHeight(30)
self.buttons["fix_vertex_order"].setSizePolicy(size_policy)
# 添加第二行按钮到布局
self.layouts["buttons_row2_layout"].addWidget(self.buttons["fix_normals"])
self.layouts["buttons_row2_layout"].addWidget(self.buttons["fix_vertex_order"])
# 第三行按钮
self.buttons["fix_seams"] = QtWidgets.QPushButton(TEXT("fix_seams", "修复接缝"))
self.buttons["fix_seams"].setObjectName("fixSeamsButton")
self.buttons["fix_seams"].setIcon(ui_utils.load_icon("polyChipOff.png"))
self.buttons["fix_seams"].setMinimumHeight(30)
self.buttons["fix_seams"].setSizePolicy(size_policy)
self.buttons["optimize_scene"] = QtWidgets.QPushButton(TEXT("optimize_scene", "优化场景"))
self.buttons["optimize_scene"].setObjectName("optimizeSceneButton")
self.buttons["optimize_scene"].setIcon(ui_utils.load_icon("singlePerspLayout.png"))
self.buttons["optimize_scene"].setMinimumHeight(30)
self.buttons["optimize_scene"].setSizePolicy(size_policy)
# 添加第三行按钮到布局
self.layouts["buttons_row3_layout"].addWidget(self.buttons["fix_seams"])
self.layouts["buttons_row3_layout"].addWidget(self.buttons["optimize_scene"])
# 将按钮行添加到模型工具布局
model_tools_layout.addLayout(self.layouts["buttons_row1_layout"], 1, 0, 1, 5)
model_tools_layout.addLayout(self.layouts["buttons_row2_layout"], 2, 0, 1, 5)
model_tools_layout.addLayout(self.layouts["buttons_row3_layout"], 3, 0, 1, 5)
# 添加布局到底部区域
bottom_layout.addWidget(model_tools_group)
#========================================= LAYOUT =======================================
def create_layouts(self):
"""
创建几何模型UI布局
组织控件的排列和层次结构
"""
# 主布局
self.layouts["main_layout"] = QtWidgets.QVBoxLayout(self.main_widget)
self.layouts["main_layout"].setContentsMargins(0, 0, 0, 0)
self.layouts["main_layout"].setSpacing(0)
# 添加标题标签
self.layouts["main_layout"].addWidget(self.controls["title_label"])
# 将清理按钮直接添加到标签页控件的右上角
# 设置标签页控件的角落部件
self.controls["tab_widget"].setCornerWidget(self.buttons["clear"], QtCore.Qt.TopRightCorner)
# 添加标签页控件到主布局
self.layouts["main_layout"].addWidget(self.controls["tab_widget"])
# 添加底部功能按钮区域到主布局
self.layouts["main_layout"].addWidget(self.controls["bottom_area"])
#======================================= CONNECTION =====================================
def create_connections(self):
"""
连接信号和槽
设置UI控件的交互行为
"""
# 导入几何工具函数
from scripts.utils import utils_geometry
# 连接LOD标签页的加载按钮
lod_names = [f"LOD{i}" for i in range(8)]
# 定义部件名称和显示名称的映射
part_display_names = {
"Head": "头部",
"Teeth": "牙齿",
"Saliva": "牙龈",
"EyeLeft": "左眼",
"EyeRight": "右眼",
"Eyeshell": "红脸",
"Eyeslashes": "眉毛",
"EyesEdge": "眉毛",
"Body": "身体"
}
# 根据LOD级别限制连接的身体部位
for lod_name in lod_names:
if lod_name == "LOD0":
parts_to_connect = ["Head", "Teeth", "Saliva", "EyeLeft", "EyeRight", "Eyeshell", "Eyeslashes", "EyesEdge", "Body"]
elif lod_name == "LOD1":
parts_to_connect = ["Head", "Teeth", "Saliva", "EyeLeft", "EyeRight", "Eyeshell", "Eyeslashes", "EyesEdge", "Body"]
elif lod_name in ["LOD2", "LOD3"]:
parts_to_connect = ["Head", "Teeth", "Saliva", "EyeLeft", "EyeRight", "Eyeshell", "Eyeslashes", "EyesEdge"]
elif lod_name == "LOD4":
parts_to_connect = ["Head", "Teeth", "EyeLeft", "EyeRight", "Eyeshell"]
else: # LOD5, LOD6, LOD7
parts_to_connect = ["Head", "Teeth", "EyeLeft", "EyeRight"]
# 连接每个身体部位的加载按钮
for part in parts_to_connect:
# 创建一个闭包来保存当前的lod_name和part值
def create_load_callback(lod, part):
return lambda: utils_geometry.load_model_for_lod(lod, part)
# 连接加载按钮
self.buttons[f"{lod_name}_{part}_load"].clicked.connect(
create_load_callback(lod_name, part)
)
# 连接清理按钮
self.buttons["clear"].clicked.connect(utils_geometry.clean)
# 连接拓扑结构下拉框
self.controls["topology_combo"].currentIndexChanged.connect(utils_geometry.update_topology)
# 连接选择LOD下拉框
self.controls["lod_combo"].currentIndexChanged.connect(utils_geometry.select_lod)
# 连接创建LOD按钮
self.buttons["create_lod"].clicked.connect(utils_geometry.create_lod)
# 连接模型工具按钮
self.buttons["separate_model"].clicked.connect(utils_geometry.separate_model)
self.buttons["generate_face_components"].clicked.connect(utils_geometry.generate_face_components)
self.buttons["fix_normals"].clicked.connect(utils_geometry.fix_normals)
self.buttons["fix_vertex_order"].clicked.connect(utils_geometry.fix_vertex_order)
self.buttons["fix_seams"].clicked.connect(utils_geometry.fix_seams)
self.buttons["optimize_scene"].clicked.connect(utils_geometry.optimize_scene)