diff --git a/scripts/dna_viewer/ui/app.py b/scripts/dna_viewer/ui/app.py index e15ca0a..64342c1 100644 --- a/scripts/dna_viewer/ui/app.py +++ b/scripts/dna_viewer/ui/app.py @@ -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,16 +302,13 @@ 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,68 +606,47 @@ 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: """ @@ -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'])