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 logging
import os import os
import webbrowser import webbrowser
@ -8,14 +5,7 @@ from typing import Callable, List
from maya import cmds from maya import cmds
from maya.cmds import confirmDialog from maya.cmds import confirmDialog
from PySide2.QtCore import ( from PySide2.QtCore import QCoreApplication, Qt
QCoreApplication,
Qt,
Signal,
QRect,
QPoint,
QSize,
)
from PySide2.QtWidgets import ( from PySide2.QtWidgets import (
QApplication, QApplication,
QCheckBox, QCheckBox,
@ -32,11 +22,7 @@ from PySide2.QtWidgets import (
QTreeWidgetItemIterator, QTreeWidgetItemIterator,
QVBoxLayout, QVBoxLayout,
QWidget, QWidget,
QLayout,
QScrollArea,
QGroupBox,
) )
from PySide2 import QtGui
from .. import DNA, build_rig from .. import DNA, build_rig
from ..builder.config import RigConfig from ..builder.config import RigConfig
@ -70,232 +56,14 @@ MARGIN_BODY_TOP = 0
MARGIN_BODY_RIGHT = 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): 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: def __init__(self, main_window: "DnaViewerWindow") -> None:
super().__init__() super().__init__()
self.main_window = main_window self.main_window = main_window
@ -322,23 +90,26 @@ class MeshTreeList(QWidget):
MARGIN_BOTTOM, MARGIN_BOTTOM,
) )
buttons_layout = QHBoxLayout()
self.btn_select_all = QPushButton("Select all meshes") self.btn_select_all = QPushButton("Select all meshes")
self.btn_select_all.setEnabled(False) self.btn_select_all.setEnabled(False)
self.btn_select_all.clicked.connect(self.select_all) 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 = QPushButton("Deselect all meshes")
self.btn_deselect_all.setEnabled(False) self.btn_deselect_all.setEnabled(False)
self.btn_deselect_all.clicked.connect(self.deselect_all) self.btn_deselect_all.clicked.connect(self.deselect_all)
buttons_layout.addWidget(self.btn_deselect_all) layout_holder.addWidget(self.btn_deselect_all)
layout_holder.addLayout(buttons_layout)
self.setLayout(layout_holder) self.setLayout(layout_holder)
def create_mesh_tree(self) -> QWidget: def create_mesh_tree(self) -> QWidget:
"""
Creates the mesh tree list widget
@rtype: QWidget
@returns: The created widget
"""
mesh_tree = QTreeWidget() mesh_tree = QTreeWidget()
mesh_tree.setHeaderHidden(True) mesh_tree.setHeaderHidden(True)
mesh_tree.itemChanged.connect(self.tree_item_changed) mesh_tree.itemChanged.connect(self.tree_item_changed)
@ -531,16 +302,13 @@ class DnaViewerWindow(QMainWindow):
progress_bar: QProgressBar = None progress_bar: QProgressBar = None
dna: DNA = None dna: DNA = None
def __init__(self, parent=None) -> None: def __init__(self, parent: QWidget = None) -> None:
super().__init__(parent) super().__init__(parent)
self.body: QVBoxLayout = None
# 设置默认路径 self.header: QHBoxLayout = None
self.default_paths = { self.build_options: QWidget = None
'gui_path': os.path.normpath("data/gui.ma"), self.extra_build_options: QWidget = None
'analog_gui_path': os.path.normpath("data/analog_gui.ma"),
'additional_script_path': os.path.normpath("data/additional_assemble_script.py")
}
self.setup_window() self.setup_window()
self.create_ui() self.create_ui()
@ -621,22 +389,43 @@ class DnaViewerWindow(QMainWindow):
return True return True
def process(self) -> None: def process(self) -> None:
"""处理DNA构建时使用默认路径""" """Start the build process of creation of scene from provided configuration from the UI"""
options = {
"joints": self.joints_cb.isChecked(), process = True
"blendShapes": self.blend_shapes_cb.isChecked(), if cmds.file(q=True, modified=True):
"skinCluster": self.skin_cb.isChecked(), process = self.show_message_dialog()
"rigLogic": self.rig_logic_cb.isChecked(),
"ctrlAttributes": self.ctrl_attrs_cb.isChecked(), if process:
"animatedMapAttributes": self.anim_attrs_cb.isChecked(), self.set_progress(text="Processing in progress...", value=0)
"meshNameToBlendShape": self.mesh_name_cb.isChecked(), config = RigConfig(
"keyFrame": self.key_frame_cb.isChecked(), meshes=self.mesh_tree_list.get_selected_meshes(),
"guiPath": self.default_paths['gui_path'], gui_path=self.select_gui_path.get_file_path(),
"analogGuiPath": self.default_paths['analog_gui_path'], analog_gui_path=self.select_analog_gui_path.get_file_path(),
"analogAssembleScript": self.default_paths['additional_script_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: def set_progress(self, text: str = None, value: int = None) -> None:
"""Setting text and/or value to progress bar""" """Setting text and/or value to progress bar"""
@ -701,16 +490,16 @@ class DnaViewerWindow(QMainWindow):
return self.is_checked(self.rig_logic_cb) return self.is_checked(self.rig_logic_cb)
def add_ctrl_attributes_on_root_joint(self) -> bool: 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: 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: 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: 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: def is_checked(self, checkbox: QCheckBox) -> bool:
""" """
@ -745,10 +534,6 @@ class DnaViewerWindow(QMainWindow):
MARGIN_BOTTOM, MARGIN_BOTTOM,
) )
self.body.setSpacing(SPACING) self.body.setSpacing(SPACING)
# 创建但隐藏文件输入控件
self.create_file_inputs()
self.create_dna_selector() self.create_dna_selector()
self.mesh_tree_list = self.create_mesh_selector() self.mesh_tree_list = self.create_mesh_selector()
self.build_options = self.create_build_options() self.build_options = self.create_build_options()
@ -760,8 +545,12 @@ class DnaViewerWindow(QMainWindow):
widget = QWidget() widget = QWidget()
layout = QHBoxLayout(widget) layout = QHBoxLayout(widget)
layout.addWidget(tab) layout.addWidget(tab)
self.body.addWidget(widget) 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.process_btn = self.create_process_btn()
self.progress_bar = self.create_progress_bar() self.progress_bar = self.create_progress_bar()
@ -817,68 +606,47 @@ class DnaViewerWindow(QMainWindow):
) )
def create_dna_selector(self) -> QWidget: def create_dna_selector(self) -> QWidget:
"""
Creates and adds the DNA selector widget
@rtype: QWidget
@returns: The created DNA selector widget
"""
widget = QWidget() 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() 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) 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( self.select_dna_path.fc_text_field.textChanged.connect(
lambda: self.on_dna_selected(self.select_dna_path) 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( layout.setContentsMargins(
MARGIN_HEADER_LEFT, MARGIN_HEADER_LEFT,
MARGIN_HEADER_TOP, MARGIN_HEADER_TOP,
MARGIN_HEADER_RIGHT, MARGIN_HEADER_RIGHT,
MARGIN_HEADER_BOTTOM, MARGIN_HEADER_BOTTOM,
) )
self.setMinimumWidth(900)
widget.setLayout(layout) widget.setLayout(layout)
self.body.addWidget(widget)
return widget
def on_dna_browser_selected(self, dna_path: str) -> None: self.body.addWidget(widget)
"""
Handle DNA selection from DNA Browser return widget
"""
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)
def on_dna_selected(self, input: FileChooser) -> None: 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.load_dna_btn.setEnabled(enabled)
self.process_btn.setEnabled(False) self.process_btn.setEnabled(False)
# Update DNA path variable
if enabled:
global DNA_File
DNA_File = dna_path
def create_dna_chooser(self) -> FileChooser: def create_dna_chooser(self) -> FileChooser:
""" """
@ -1085,29 +853,26 @@ class DnaViewerWindow(QMainWindow):
self.joints_cb = self.create_checkbox( self.joints_cb = self.create_checkbox(
"joints", "joints",
"Add joints to rig", "Add joints to rig. Requires: DNA to be loaded",
layout, layout,
self.on_joints_changed, self.on_joints_changed,
enabled=True,
) )
self.blend_shapes_cb = self.create_checkbox( self.blend_shapes_cb = self.create_checkbox(
"blend shapes", "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, layout,
self.on_generic_changed, self.on_generic_changed,
enabled=True,
) )
self.skin_cb = self.create_checkbox( self.skin_cb = self.create_checkbox(
"skin cluster", "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, layout,
self.on_generic_changed, self.on_generic_changed,
) )
self.rig_logic_cb = self.create_checkbox( self.rig_logic_cb = self.create_checkbox(
"rig logic", "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, layout,
self.on_generic_changed,
) )
layout.addStretch() layout.addStretch()
@ -1124,28 +889,28 @@ class DnaViewerWindow(QMainWindow):
MARGIN_BOTTOM, 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",
"ctrl attributes on root joint", "ctrl attributes on root joint",
layout, layout,
enabled=True, enabled=True,
checked=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",
"animated map attributes on root joint", "animated map attributes on root joint",
layout, layout,
enabled=True, enabled=True,
checked=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",
"mesh name to blend shape channel name", "mesh name to blend shape channel name",
layout, layout,
enabled=True, enabled=True,
checked=True, checked=True,
) )
self.key_frame_cb = self.create_checkbox( self.key_frames_cb = self.create_checkbox(
"key frames", "key frames",
"Add keyframes to rig", "Add keyframes to rig",
layout, layout,
@ -1157,10 +922,10 @@ class DnaViewerWindow(QMainWindow):
return widget return widget
def enable_additional_build_options(self, enable: bool) -> None: def enable_additional_build_options(self, enable: bool) -> None:
self.ctrl_attrs_cb.setEnabled(enable) self.ctrl_attributes_on_root_joint_cb.setEnabled(enable)
self.anim_attrs_cb.setEnabled(enable) self.animated_map_attributes_on_root_joint_cb.setEnabled(enable)
self.mesh_name_cb.setEnabled(enable) self.mesh_name_to_blend_shape_channel_name_cb.setEnabled(enable)
self.key_frame_cb.setEnabled(enable) self.key_frames_cb.setEnabled(enable)
def create_checkbox( def create_checkbox(
self, self,
@ -1303,39 +1068,3 @@ class DnaViewerWindow(QMainWindow):
and self.select_aas_path.get_file_path() is not None and self.select_aas_path.get_file_path() is not None
) )
self.rig_logic_cb.setEnabled(enabled) 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'])