This commit is contained in:
Jeffreytsai1004 2025-01-12 22:29:32 +08:00
parent c2324147ae
commit d9a19aa282

View File

@ -1,6 +1,3 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import os
import webbrowser
@ -8,14 +5,7 @@ from typing import Callable, List
from maya import cmds
from maya.cmds import confirmDialog
from PySide2.QtCore import (
QCoreApplication,
Qt,
Signal,
QRect,
QPoint,
QSize,
)
from PySide2.QtCore import QCoreApplication, Qt
from PySide2.QtWidgets import (
QApplication,
QCheckBox,
@ -32,11 +22,7 @@ from PySide2.QtWidgets import (
QTreeWidgetItemIterator,
QVBoxLayout,
QWidget,
QLayout,
QScrollArea,
QGroupBox,
)
from PySide2 import QtGui
from .. import DNA, build_rig
from ..builder.config import RigConfig
@ -70,232 +56,14 @@ MARGIN_BODY_TOP = 0
MARGIN_BODY_RIGHT = 0
class FlowLayout(QLayout):
def __init__(self, parent=None):
super().__init__(parent)
self.itemList = []
self.spacing_x = 5
self.spacing_y = 5
def addItem(self, item):
self.itemList.append(item)
def count(self):
return len(self.itemList)
def itemAt(self, index):
if 0 <= index < len(self.itemList):
return self.itemList[index]
return None
def takeAt(self, index):
if 0 <= index < len(self.itemList):
return self.itemList.pop(index)
return None
def expandingDirections(self):
return Qt.Orientations(Qt.Orientation(0))
def hasHeightForWidth(self):
return True
def heightForWidth(self, width):
height = self.doLayout(QRect(0, 0, width, 0), True)
return height
def setGeometry(self, rect):
super().setGeometry(rect)
self.doLayout(rect, False)
def sizeHint(self):
return self.minimumSize()
def minimumSize(self):
size = QSize()
for item in self.itemList:
size = size.expandedTo(item.minimumSize())
return size
def doLayout(self, rect, testOnly):
x = rect.x()
y = rect.y()
lineHeight = 0
for item in self.itemList:
widget = item.widget()
spaceX = self.spacing_x
spaceY = self.spacing_y
nextX = x + item.sizeHint().width() + spaceX
if nextX - spaceX > rect.right() and lineHeight > 0:
x = rect.x()
y = y + lineHeight + spaceY
nextX = x + item.sizeHint().width() + spaceX
lineHeight = 0
if not testOnly:
item.setGeometry(QRect(QPoint(x, y), item.sizeHint()))
x = nextX
lineHeight = max(lineHeight, item.sizeHint().height())
return y + lineHeight - rect.y()
class DNABrowserWidget(QWidget):
dna_selected = Signal(str)
def __init__(self, dna_path, img_path, parent=None):
super().__init__(parent)
self.dna_path = dna_path
self.img_path = img_path
self.setup_ui()
self.scan_dna_files()
self.update_grid()
self.calculate_and_set_height()
def calculate_and_set_height(self):
container_width = self.flow_widget.width() or 800
button_width = (container_width - 40) // 3
button_height = button_width
self.setFixedHeight(button_height * 3 + 10 * 4)
def setup_ui(self):
self.main_layout = QVBoxLayout(self)
self.main_layout.setContentsMargins(0, 0, 0, 0)
self.flow_widget = QWidget()
self.flow_layout = FlowLayout(self.flow_widget)
self.flow_layout.setSpacing(5)
self.scroll_area = QScrollArea()
self.scroll_area.setWidgetResizable(True)
self.scroll_area.setWidget(self.flow_widget)
self.setup_scroll_area_style()
self.main_layout.addWidget(self.scroll_area)
def setup_scroll_area_style(self):
self.scroll_area.setStyleSheet("""
QScrollArea {
border: none;
background-color: transparent;
}
QScrollBar:vertical {
border: none;
background: #F0F0F0;
width: 8px;
margin: 0px;
}
QScrollBar::handle:vertical {
background: #CCCCCC;
border-radius: 4px;
min-height: 20px;
}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
height: 0px;
}
""")
def scan_dna_files(self):
self.dna_files = {}
if not os.path.exists(self.dna_path):
cmds.warning(f"DNA path not found: {self.dna_path}")
return
if not os.path.exists(self.img_path):
cmds.warning(f"Image path not found: {self.img_path}")
return
for file in os.listdir(self.dna_path):
if file.endswith('.dna'):
name = os.path.splitext(file)[0]
dna_file = os.path.join(self.dna_path, file).replace("\\", "/")
img_file = None
for ext in ['.jpg', '.png', '.jpeg']:
img_path = os.path.join(self.img_path, f"{name}{ext}").replace("\\", "/")
if os.path.exists(img_path):
img_file = img_path
break
self.dna_files[name] = {
'dna_path': dna_file,
'img_path': img_file
}
def update_grid(self):
for i in reversed(range(self.flow_layout.count())):
item = self.flow_layout.takeAt(i)
if item.widget():
item.widget().deleteLater()
container_width = self.flow_widget.width() or 800
button_width = (container_width - 40) // 3
button_height = button_width
for name, info in sorted(self.dna_files.items()):
self.flow_layout.addWidget(self.create_dna_button(name, info, button_width, button_height))
def create_dna_button(self, name, info, width, height):
btn = QPushButton()
btn.setFixedSize(width, height)
layout = QVBoxLayout(btn)
layout.setContentsMargins(10, 10, 10, 10)
layout.setSpacing(5)
icon_label = QLabel()
icon_label.setAlignment(Qt.AlignCenter)
icon_size = height - 40
if info['img_path']:
icon_label.setPixmap(
QtGui.QPixmap(info['img_path']).scaled(
icon_size,
icon_size,
Qt.KeepAspectRatio,
Qt.SmoothTransformation
)
)
else:
icon_label.setText("No Image")
icon_label.setStyleSheet("color: #FFFFFF; font-size: 12px;")
text_label = QLabel(name)
text_label.setAlignment(Qt.AlignCenter)
text_label.setStyleSheet("""
color: #FFFFFF;
font-size: 12px;
font-weight: bold;
""")
layout.addWidget(icon_label, 1)
layout.addWidget(text_label)
btn.setStyleSheet("""
QPushButton {
background-color: #303030;
border: 2px solid #202020;
border-radius: 10px;
padding: 8px;
color: #FFFFFF;
}
QPushButton:hover {
background-color: #404040;
border: 2px solid #505050;
}
QPushButton:pressed {
background-color: #202020;
border: 2px solid #606060;
}
""")
btn.setProperty('dna_path', info['dna_path'])
btn.clicked.connect(lambda: self.on_dna_selected(info['dna_path']))
return btn
def on_dna_selected(self, dna_path):
self.dna_selected.emit(dna_path)
class MeshTreeList(QWidget):
"""
A custom widget that lists out meshes with checkboxes next to them, so these meshes can be selected to be processed. The meshes are grouped by LOD
@type mesh_tree: QWidget
@param mesh_tree: The widget that contains the meshes to be selected in a tree list
"""
def __init__(self, main_window: "DnaViewerWindow") -> None:
super().__init__()
self.main_window = main_window
@ -322,23 +90,26 @@ class MeshTreeList(QWidget):
MARGIN_BOTTOM,
)
buttons_layout = QHBoxLayout()
self.btn_select_all = QPushButton("Select all meshes")
self.btn_select_all.setEnabled(False)
self.btn_select_all.clicked.connect(self.select_all)
buttons_layout.addWidget(self.btn_select_all)
layout_holder.addWidget(self.btn_select_all)
self.btn_deselect_all = QPushButton("Deselect all meshes")
self.btn_deselect_all.setEnabled(False)
self.btn_deselect_all.clicked.connect(self.deselect_all)
buttons_layout.addWidget(self.btn_deselect_all)
layout_holder.addLayout(buttons_layout)
layout_holder.addWidget(self.btn_deselect_all)
self.setLayout(layout_holder)
def create_mesh_tree(self) -> QWidget:
"""
Creates the mesh tree list widget
@rtype: QWidget
@returns: The created widget
"""
mesh_tree = QTreeWidget()
mesh_tree.setHeaderHidden(True)
mesh_tree.itemChanged.connect(self.tree_item_changed)
@ -531,15 +302,12 @@ class DnaViewerWindow(QMainWindow):
progress_bar: QProgressBar = None
dna: DNA = None
def __init__(self, parent=None) -> None:
def __init__(self, parent: QWidget = None) -> None:
super().__init__(parent)
# 设置默认路径
self.default_paths = {
'gui_path': os.path.normpath("data/gui.ma"),
'analog_gui_path': os.path.normpath("data/analog_gui.ma"),
'additional_script_path': os.path.normpath("data/additional_assemble_script.py")
}
self.body: QVBoxLayout = None
self.header: QHBoxLayout = None
self.build_options: QWidget = None
self.extra_build_options: QWidget = None
self.setup_window()
self.create_ui()
@ -621,22 +389,43 @@ class DnaViewerWindow(QMainWindow):
return True
def process(self) -> None:
"""处理DNA构建时使用默认路径"""
options = {
"joints": self.joints_cb.isChecked(),
"blendShapes": self.blend_shapes_cb.isChecked(),
"skinCluster": self.skin_cb.isChecked(),
"rigLogic": self.rig_logic_cb.isChecked(),
"ctrlAttributes": self.ctrl_attrs_cb.isChecked(),
"animatedMapAttributes": self.anim_attrs_cb.isChecked(),
"meshNameToBlendShape": self.mesh_name_cb.isChecked(),
"keyFrame": self.key_frame_cb.isChecked(),
"guiPath": self.default_paths['gui_path'],
"analogGuiPath": self.default_paths['analog_gui_path'],
"analogAssembleScript": self.default_paths['additional_script_path']
}
"""Start the build process of creation of scene from provided configuration from the UI"""
# ... 其余处理代码保持不变
process = True
if cmds.file(q=True, modified=True):
process = self.show_message_dialog()
if process:
self.set_progress(text="Processing in progress...", value=0)
config = RigConfig(
meshes=self.mesh_tree_list.get_selected_meshes(),
gui_path=self.select_gui_path.get_file_path(),
analog_gui_path=self.select_analog_gui_path.get_file_path(),
aas_path=self.select_aas_path.get_file_path(),
add_rig_logic=self.add_rig_logic(),
add_joints=self.add_joints(),
add_blend_shapes=self.add_blend_shapes(),
add_skin_cluster=self.add_skin_cluster(),
add_ctrl_attributes_on_root_joint=self.add_ctrl_attributes_on_root_joint(),
add_animated_map_attributes_on_root_joint=self.add_animated_map_attributes_on_root_joint(),
add_mesh_name_to_blend_shape_channel_name=self.add_mesh_name_to_blend_shape_channel_name(),
add_key_frames=self.add_key_frames(),
)
self.main_widget.setEnabled(False)
try:
self.set_progress(value=33)
self.dna = DNA(self.select_dna_path.get_file_path())
self.set_progress(value=66)
build_rig(dna=self.dna, config=config)
self.set_progress(text="Processing completed", value=100)
except Exception as e:
self.set_progress(text="Processing failed", value=100)
logging.error(e)
confirmDialog(message=e, button=["ok"], icon="critical")
self.main_widget.setEnabled(True)
def set_progress(self, text: str = None, value: int = None) -> None:
"""Setting text and/or value to progress bar"""
@ -701,16 +490,16 @@ class DnaViewerWindow(QMainWindow):
return self.is_checked(self.rig_logic_cb)
def add_ctrl_attributes_on_root_joint(self) -> bool:
return self.is_checked(self.ctrl_attrs_cb)
return self.is_checked(self.ctrl_attributes_on_root_joint_cb)
def add_animated_map_attributes_on_root_joint(self) -> bool:
return self.is_checked(self.anim_attrs_cb)
return self.is_checked(self.animated_map_attributes_on_root_joint_cb)
def add_mesh_name_to_blend_shape_channel_name(self) -> bool:
return self.is_checked(self.mesh_name_cb)
return self.is_checked(self.mesh_name_to_blend_shape_channel_name_cb)
def add_key_frames(self) -> bool:
return self.is_checked(self.key_frame_cb)
return self.is_checked(self.key_frames_cb)
def is_checked(self, checkbox: QCheckBox) -> bool:
"""
@ -745,10 +534,6 @@ class DnaViewerWindow(QMainWindow):
MARGIN_BOTTOM,
)
self.body.setSpacing(SPACING)
# 创建但隐藏文件输入控件
self.create_file_inputs()
self.create_dna_selector()
self.mesh_tree_list = self.create_mesh_selector()
self.build_options = self.create_build_options()
@ -760,8 +545,12 @@ class DnaViewerWindow(QMainWindow):
widget = QWidget()
layout = QHBoxLayout(widget)
layout.addWidget(tab)
self.body.addWidget(widget)
self.select_gui_path = self.create_gui_selector()
self.select_analog_gui_path = self.create_analog_gui_selector()
self.select_aas_path = self.create_aas_selector()
self.process_btn = self.create_process_btn()
self.progress_bar = self.create_progress_bar()
@ -817,69 +606,48 @@ class DnaViewerWindow(QMainWindow):
)
def create_dna_selector(self) -> QWidget:
"""
Creates and adds the DNA selector widget
@rtype: QWidget
@returns: The created DNA selector widget
"""
widget = QWidget()
layout = QVBoxLayout()
layout.setSpacing(5)
dna_group = QGroupBox("DNA")
dna_layout = QVBoxLayout(dna_group)
dna_layout.setContentsMargins(5, 5, 5, 5)
dna_layout.setSpacing(10)
self.dna_browser = DNABrowserWidget(
os.path.join(os.path.dirname(__file__), "..", "..", "..", "data", "dna"),
os.path.join(os.path.dirname(__file__), "..", "..", "..", "data", "img")
)
self.dna_browser.dna_selected.connect(self.on_dna_browser_selected)
dna_layout.addWidget(self.dna_browser)
self.select_dna_path = self.create_dna_chooser()
dna_layout.addWidget(self.select_dna_path)
self.load_dna_btn = self.create_load_dna_button(self.select_dna_path)
dna_layout.addWidget(self.load_dna_btn)
self.select_dna_path.fc_text_field.textChanged.connect(
lambda: self.on_dna_selected(self.select_dna_path)
)
layout.addWidget(dna_group)
layout = QVBoxLayout()
layout.addWidget(self.select_dna_path)
layout.addWidget(self.load_dna_btn)
layout.setContentsMargins(
MARGIN_HEADER_LEFT,
MARGIN_HEADER_TOP,
MARGIN_HEADER_RIGHT,
MARGIN_HEADER_BOTTOM,
)
self.setMinimumWidth(900)
widget.setLayout(layout)
self.body.addWidget(widget)
return widget
def on_dna_browser_selected(self, dna_path: str) -> None:
"""
Handle DNA selection from DNA Browser
"""
if dna_path and os.path.exists(dna_path):
self.select_dna_path.fc_text_field.setText(dna_path)
self.on_dna_selected(self.select_dna_path)
self.body.addWidget(widget)
return widget
def on_dna_selected(self, input: FileChooser) -> None:
"""
Handle DNA selection from file input
The method that gets called when a DNA file gets selected
@type input: FileChooser
@param input: The file chooser object corresponding to the DNA selector widget
"""
dna_path = input.get_file_path()
enabled = dna_path is not None and os.path.exists(dna_path)
enabled = input.get_file_path() is not None
self.load_dna_btn.setEnabled(enabled)
self.process_btn.setEnabled(False)
# Update DNA path variable
if enabled:
global DNA_File
DNA_File = dna_path
def create_dna_chooser(self) -> FileChooser:
"""
Creates and adds the DNA chooser widget
@ -1085,29 +853,26 @@ class DnaViewerWindow(QMainWindow):
self.joints_cb = self.create_checkbox(
"joints",
"Add joints to rig",
"Add joints to rig. Requires: DNA to be loaded",
layout,
self.on_joints_changed,
enabled=True,
)
self.blend_shapes_cb = self.create_checkbox(
"blend shapes",
"Add blend shapes to rig",
"Add blend shapes to rig. Requires: DNA to be loaded and at least one mesh to be check",
layout,
self.on_generic_changed,
enabled=True,
)
self.skin_cb = self.create_checkbox(
"skin cluster",
"Add skin cluster to rig",
"Add skin cluster to rig. Requires: DNA to be loaded and at least one mesh and joints to be checked",
layout,
self.on_generic_changed,
)
self.rig_logic_cb = self.create_checkbox(
"rig logic",
"Add rig logic to rig",
"Add RigLogic to rig. Requires: DNA to be loaded, all meshes to be checked, joints, skin, blend shapes to be checked, also gui, analog gui and additional assemble script must be set",
layout,
self.on_generic_changed,
)
layout.addStretch()
@ -1124,28 +889,28 @@ class DnaViewerWindow(QMainWindow):
MARGIN_BOTTOM,
)
self.ctrl_attrs_cb = self.create_checkbox(
self.ctrl_attributes_on_root_joint_cb = self.create_checkbox(
"ctrl attributes on root joint",
"ctrl attributes on root joint",
layout,
enabled=True,
checked=True,
)
self.anim_attrs_cb = self.create_checkbox(
self.animated_map_attributes_on_root_joint_cb = self.create_checkbox(
"animated map attributes on root joint",
"animated map attributes on root joint",
layout,
enabled=True,
checked=True,
)
self.mesh_name_cb = self.create_checkbox(
self.mesh_name_to_blend_shape_channel_name_cb = self.create_checkbox(
"mesh name to blend shape channel name",
"mesh name to blend shape channel name",
layout,
enabled=True,
checked=True,
)
self.key_frame_cb = self.create_checkbox(
self.key_frames_cb = self.create_checkbox(
"key frames",
"Add keyframes to rig",
layout,
@ -1157,10 +922,10 @@ class DnaViewerWindow(QMainWindow):
return widget
def enable_additional_build_options(self, enable: bool) -> None:
self.ctrl_attrs_cb.setEnabled(enable)
self.anim_attrs_cb.setEnabled(enable)
self.mesh_name_cb.setEnabled(enable)
self.key_frame_cb.setEnabled(enable)
self.ctrl_attributes_on_root_joint_cb.setEnabled(enable)
self.animated_map_attributes_on_root_joint_cb.setEnabled(enable)
self.mesh_name_to_blend_shape_channel_name_cb.setEnabled(enable)
self.key_frames_cb.setEnabled(enable)
def create_checkbox(
self,
@ -1303,39 +1068,3 @@ class DnaViewerWindow(QMainWindow):
and self.select_aas_path.get_file_path() is not None
)
self.rig_logic_cb.setEnabled(enabled)
def create_file_inputs(self) -> None:
"""Creates the file input widgets but keeps them hidden"""
# GUI Path - 隐藏但保持功能
self.select_gui_path = self.create_file_chooser(
"GUI Path:",
"GUI file to load",
"Select a GUI file",
"Maya ASCII (*.ma)",
self.on_generic_changed,
)
self.select_gui_path.hide() # 隐藏控件
self.select_gui_path.fc_text_field.setText(self.default_paths['gui_path'])
# Analog GUI Path - 隐藏但保持功能
self.select_analog_gui_path = self.create_file_chooser(
"Analog GUI Path:",
"Analog GUI file to load",
"Select an analog GUI file",
"Maya ASCII (*.ma)",
self.on_generic_changed,
)
self.select_analog_gui_path.hide() # 隐藏控件
self.select_analog_gui_path.fc_text_field.setText(self.default_paths['analog_gui_path'])
# Additional Script Path - 隐藏但保持功能
self.select_aas_path = self.create_file_chooser(
"Additional Script Path:",
"Additional assembly script to load",
"Select a Python file",
"Python files (*.py)",
self.on_generic_changed,
)
self.select_aas_path.hide() # 隐藏控件
self.select_aas_path.fc_text_field.setText(self.default_paths['additional_script_path'])