diff --git a/Install.py b/Install.py index 644e0d2..88a7b19 100644 --- a/Install.py +++ b/Install.py @@ -29,8 +29,8 @@ SCRIPTS_PATH = os.path.join(ROOT_PATH, "scripts").replace("\\", "/") ICONS_PATH = os.path.join(ROOT_PATH, "icons").replace("\\", "/") TOOL_ICON = os.path.join(ICONS_PATH, "logo.png").replace("\\", "/") DEFAULT_ICON = "commandButton.png" -TOOL_HELP_URL = f"https://gitea.cgnico.com/CGNICO/MetaFusion/wiki" -TOOL_WSCL_NAME = "MetaFusionWorkSpaceControl" +TOOL_HELP_URL = f"http://10.72.61.59:3000/ArtGroup/{TOOL_NAME}/wiki" +TOOL_WSCL_NAME = "ToolBoxWorkSpaceControl" MOD_FILE_NAME = f"{TOOL_NAME}.mod" MAIN_SCRIPT_NAME = f"{TOOL_NAME}.py" diff --git a/data/dna/Custom.dna b/data/dna/Custom.dna new file mode 100644 index 0000000..d1664b0 Binary files /dev/null and b/data/dna/Custom.dna differ diff --git a/icons/color.png b/icons/color.png index ccd431a..ef128cc 100644 Binary files a/icons/color.png and b/icons/color.png differ diff --git a/icons/command.png b/icons/command.png new file mode 100644 index 0000000..ae1de7a Binary files /dev/null and b/icons/command.png differ diff --git a/icons/expressions_blend.png b/icons/expressions_blend.png index b6d5fa5..5099338 100644 Binary files a/icons/expressions_blend.png and b/icons/expressions_blend.png differ diff --git a/icons/expressions_current.png b/icons/expressions_current.png index 408ef97..4b33b04 100644 Binary files a/icons/expressions_current.png and b/icons/expressions_current.png differ diff --git a/icons/logo.png b/icons/logo.png index 03f837a..a485d7f 100644 Binary files a/icons/logo.png and b/icons/logo.png differ diff --git a/icons/set_current.png b/icons/set_current.png index a99c328..7d2df58 100644 Binary files a/icons/set_current.png and b/icons/set_current.png differ diff --git a/icons/set_ok.png b/icons/set_ok.png index ad4c004..1971006 100644 Binary files a/icons/set_ok.png and b/icons/set_ok.png differ diff --git a/scripts/MetaFusion.py b/scripts/MetaFusion.py index 33382c3..4affee0 100644 --- a/scripts/MetaFusion.py +++ b/scripts/MetaFusion.py @@ -21,8 +21,7 @@ import maya.mel as mel # Standard library imports import BodyPrep import BatchImport -import dna_viewer - +import DNA_Viewer #===================================== CONSTANTS ===================================== # Tool info TOOL_NAME = "MetaFusion" @@ -31,7 +30,7 @@ TOOL_AUTHOR = "Virtuos" # UI Constants TOOL_WSCL_NAME = "MetaFusionWorkSpaceControl" TOOL_HELP_URL = f"http://10.72.61.59:3000/ArtGroup/{TOOL_NAME}/wiki" -DEFAULT_WINDOW_SIZE = (450, 800) +DEFAULT_WINDOW_SIZE = (500, 800) # Paths TOOL_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))).replace("\\", "/") @@ -56,7 +55,6 @@ PLUGIN_PATH = os.path.join(TOOL_PATH, "plugins", f"{MAYA_VERSION}").replace("\\" if not os.path.exists(PLUGIN_PATH): cmds.warning(f"Plugin path not found: {PLUGIN_PATH}") -# 打印上面的所有变量 print(f"TOOL_PATH: {TOOL_PATH}") print(f"SCRIPTS_PATH: {SCRIPTS_PATH}") print(f"ICONS_PATH: {ICONS_PATH}") @@ -69,6 +67,7 @@ print(f"MAP_PATH: {MAP_PATH}") print(f"MASKS_PATH: {MASKS_PATH}") print(f"SHADERS_PATH: {SHADERS_PATH}") + #===================================== LANGUAGE SETTINGS ===================================== TOOL_LANG = 'en_US' SUPPORTED_LANGUAGES = ['en_US', 'zh_CN'] @@ -144,7 +143,12 @@ class MainButton(QtWidgets.QPushButton): self.setIcon(icon) self.setIconSize(QtCore.QSize(24, 24)) self.setMinimumHeight(30) - self.setStyleSheet(self._generate_style_sheet(color or self.DEFAULT_COLORS["normal"], hover_color or self.DEFAULT_COLORS["hover"], pressed_color or self.DEFAULT_COLORS["pressed"])) + colors = { + "normal": color or self.DEFAULT_COLORS["normal"], + "hover": hover_color or self.DEFAULT_COLORS["hover"], + "pressed": pressed_color or self.DEFAULT_COLORS["pressed"] + } + self.setStyleSheet(self._generate_style_sheet(**colors)) @staticmethod def _generate_style_sheet(normal, hover, pressed): @@ -192,6 +196,8 @@ class MainWindow(QtWidgets.QWidget): instance = None def __init__(self, parent=maya_main_window()): + self.load_required_plugins() + super(MainWindow, self).__init__(parent) self.setWindowTitle(f"{TOOL_NAME} - {TOOL_VERSION}") self.setObjectName(TOOL_PATH) @@ -209,6 +215,29 @@ class MainWindow(QtWidgets.QWidget): else: print(f"WARNING: Icon file not found: {TOOL_ICON}") + def load_required_plugins(self): + try: + if PLUGIN_PATH not in os.environ.get('MAYA_PLUG_IN_PATH', ''): + if 'MAYA_PLUG_IN_PATH' in os.environ: + os.environ['MAYA_PLUG_IN_PATH'] = f"{PLUGIN_PATH};{os.environ['MAYA_PLUG_IN_PATH']}" + else: + os.environ['MAYA_PLUG_IN_PATH'] = PLUGIN_PATH + + required_plugins = ['embeddedRL4.mll'] + for plugin in required_plugins: + plugin_path = os.path.join(PLUGIN_PATH, plugin) + if os.path.exists(plugin_path): + try: + if not cmds.pluginInfo(plugin, query=True, loaded=True): + cmds.loadPlugin(plugin_path) + print(f"Successfully loaded plugin: {plugin}") + except Exception as e: + cmds.warning(f"Failed to load plugin {plugin}: {str(e)}") + else: + cmds.warning(f"Plugin not found: {plugin_path}") + except Exception as e: + cmds.warning(f"Error loading plugins: {str(e)}") + @classmethod def show_window(cls): try: @@ -241,8 +270,8 @@ class MainWindow(QtWidgets.QWidget): floating=True, retain=True, resizeWidth=True, - initialWidth=450, - minimumWidth=450 + initialWidth=500, + minimumWidth=500 ) cmds.workspaceControl(TOOL_WSCL_NAME, e=True, resizeWidth=True) cmds.control(self.objectName(), e=True, p=workspace_control) @@ -252,7 +281,10 @@ class MainWindow(QtWidgets.QWidget): #===================================== UI COMPONENTS ===================================== def create_widgets(self): - # Create function buttons + # DNA Edit group + self.dna_edit_btn = MainButton(LANG[TOOL_LANG]["DNA Edit"]) + self.dna_viewer_btn = MainButton(LANG[TOOL_LANG]["Open DNA Viewer"], color="#B8E6B3", hover_color="#C4F2BF", pressed_color="#A3D99E") + # Prepare group self.prepare_btn = MainButton(LANG[TOOL_LANG]["Prepare"]) self.body_prepare_btn = MainButton(LANG[TOOL_LANG]["Body Prepare"], color="#FFEBA1", hover_color="#FFF5B3", pressed_color="#FFE68A") @@ -261,11 +293,7 @@ class MainWindow(QtWidgets.QWidget): self.import_btn = MainButton(LANG[TOOL_LANG]["Import"]) self.batch_import_btn = MainButton(LANG[TOOL_LANG]["Batch Import"], color="#A7C6ED", hover_color="#B2D3F0", pressed_color="#8BB8E0") - # DNA Edit group - self.dna_edit_btn = MainButton(LANG[TOOL_LANG]["DNA Edit"]) - self.dna_viewer_btn = MainButton(LANG[TOOL_LANG]["Open DNA Viewer"], color="#B8E6B3", hover_color="#C4F2BF", pressed_color="#A3D99E") - - # Bottom buttons + # Bottom buttons (existing code) self.help_btn = BottomButton(LANG[TOOL_LANG]["Help"]) self.help_btn.setToolTip(LANG[TOOL_LANG]["Help"]) self.help_btn.setFixedSize(100, 20) @@ -274,6 +302,9 @@ class MainWindow(QtWidgets.QWidget): self.lang_btn.setToolTip(LANG[TOOL_LANG]["Switch Language"]) self.lang_btn.setFixedSize(30, 20) + for button in [self.help_btn, self.lang_btn]: + button.setFont(QtGui.QFont("Microsoft YaHei", 10)) + def create_layouts(self): main_layout = QtWidgets.QVBoxLayout(self) main_layout.setContentsMargins(2, 2, 2, 2) @@ -303,7 +334,7 @@ class MainWindow(QtWidgets.QWidget): main_layout.addLayout(content_layout) main_layout.addStretch() - # Bottom layout + # Bottom layout (existing code) bottom_layout = QtWidgets.QHBoxLayout() bottom_layout.setContentsMargins(5, 0, 5, 5) @@ -329,7 +360,7 @@ class MainWindow(QtWidgets.QWidget): self.batch_import_btn.clicked.connect(self.run_batch_import) self.dna_viewer_btn.clicked.connect(self.run_dna_viewer) - # Bottom buttons + # Existing connections self.help_btn.clicked.connect(self.help) self.lang_btn.clicked.connect(self.switch_language) @@ -347,8 +378,9 @@ class MainWindow(QtWidgets.QWidget): # DNA Edit group def run_dna_viewer(self): - import dna_viewer - dna_viewer.show() + import DNA_Viewer + DNA_Viewer.show() + #===================================== BOTTOM LAYOUT ===================================== def help(self): @@ -369,6 +401,7 @@ class MainWindow(QtWidgets.QWidget): def retranslate_ui(self): # Update function button translations + self.load_dna_btn.setText(LANG[TOOL_LANG]["Load DNA"]) self.body_prepare_btn.setText(LANG[TOOL_LANG]["Body Prepare"]) self.batch_import_btn.setText(LANG[TOOL_LANG]["Batch Import"]) self.dna_viewer_btn.setText(LANG[TOOL_LANG]["Open DNA Viewer"]) @@ -389,6 +422,21 @@ class MainWindow(QtWidgets.QWidget): ]: button.setFont(QtGui.QFont("Microsoft YaHei", 10)) + self.dna_file_label.setText(LANG[TOOL_LANG]["DNA File:"]) + + def on_dna_selected(self, dna_path): + """当DNA被选中时""" + global DNA_File + DNA_File = dna_path + self.dna_file_input.setText(DNA_File) + print(f"Selected DNA file: {DNA_File}") + + def on_dna_file_changed(self): + """当DNA文件输入框内容改变时""" + global DNA_File + DNA_File = self.dna_file_input.text() + print(f"DNA file path updated: {DNA_File}") + #===================================== LAUNCH FUNCTIONS ===================================== def show(): return MainWindow.show_window() diff --git a/scripts/dna_viewer/ui/app.py b/scripts/dna_viewer/ui/app.py index 3b0f371..03b99fd 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,13 +5,14 @@ from typing import Callable, List from maya import cmds from maya.cmds import confirmDialog -from PySide2.QtCore import QCoreApplication, Qt +from PySide2.QtCore import Qt, QSize, QRect, QPoint, QCoreApplication from PySide2.QtWidgets import ( QApplication, QCheckBox, QGridLayout, QHBoxLayout, QLabel, + QLayout, QMainWindow, QMessageBox, QProgressBar, @@ -25,7 +23,10 @@ from PySide2.QtWidgets import ( QTreeWidgetItemIterator, QVBoxLayout, QWidget, + QScrollArea ) +from PySide2.QtGui import QIcon, QPixmap +from PySide2 import QtCore, QtGui from .. import DNA, build_rig from ..builder.config import RigConfig @@ -38,12 +39,14 @@ def show() -> None: DnaViewerWindow.show_window() -WINDOW_OBJECT = "dnaviewer" +TOOL_NAME = "Delos" WINDOW_TITLE = "DNA Viewer" +WINDOW_OBJECT = "dnaviewer" +TOOL_HELP_URL = f"http://10.72.61.59:3000/ArtGroup/{TOOL_NAME}/wiki" SPACING = 6 WINDOW_SIZE_WIDTH_MIN = 800 WINDOW_SIZE_WIDTH_MAX = 1200 -WINDOW_SIZE_HEIGHT_MIN = 800 +WINDOW_SIZE_HEIGHT_MIN = 1000 WINDOW_SIZE_HEIGHT_MAX = 1000 MARGIN_LEFT = 8 MARGIN_TOP = 8 @@ -57,113 +60,284 @@ MARGIN_BODY_LEFT = 0 MARGIN_BODY_TOP = 0 MARGIN_BODY_RIGHT = 0 -#===================================== CONSTANTS ===================================== -# Tool info -TOOL_NAME = "MetaFusion" -TOOL_VERSION = "Beta v1.0.0" -TOOL_AUTHOR = "Virtuos" -# UI Constants -TOOL_WSCL_NAME = "MetaFusionWorkSpaceControl" -HELP_URL = f"http://10.72.61.59:3000/ArtGroup/{TOOL_NAME}/wiki" -DEFAULT_WINDOW_SIZE = (450, 800) - -# Paths -TOOL_PATH = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))).replace("\\", "/")) +TOOL_PATH = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))).replace("\\", "/") SCRIPTS_PATH = os.path.join(TOOL_PATH, "scripts").replace("\\", "/") -ICONS_PATH = os.path.join(TOOL_PATH, "icons").replace("\\", "/") -TOOL_ICON = os.path.join(ICONS_PATH, "logo.png").replace("\\", "/") - -# Metahuman paths DATA_PATH = os.path.join(TOOL_PATH, "data").replace("\\", "/") DNA_PATH = os.path.join(DATA_PATH, "dna").replace("\\", "/") -BODY_PATH = os.path.join(DATA_PATH, "body").replace("\\", "/") IMG_PATH = os.path.join(DATA_PATH, "img").replace("\\", "/") -MAP_PATH = os.path.join(DATA_PATH, "map").replace("\\", "/") -MASKS_PATH = os.path.join(DATA_PATH, "masks").replace("\\", "/") -SHADERS_PATH = os.path.join(DATA_PATH, "shaders").replace("\\", "/") -MH4_PATH = os.path.join(DATA_PATH, "mh4").replace("\\", "/") -MH4_DNA_PATH = os.path.join(MH4_PATH, "dna").replace("\\", "/") -OUT_PATH = os.path.join(DATA_PATH, "out").replace("\\", "/") -SAVE_PATH = os.path.join(DATA_PATH, "save").replace("\\", "/") -MAYA_VERSION = cmds.about(version=True) -PLUGIN_PATH = os.path.join(TOOL_PATH, "plugins", f"{MAYA_VERSION}").replace("\\", "/") -if not os.path.exists(PLUGIN_PATH): - cmds.warning(f"Plugin path not found: {PLUGIN_PATH}") +SOURCE_PATH = os.path.join(DATA_PATH, "source").replace("\\", "/") +GUI_PATH = os.path.join(SOURCE_PATH, "gui.ma").replace("\\", "/") +ANALOG_GUI_PATH = os.path.join(SOURCE_PATH, "analog_gui.ma").replace("\\", "/") +ADDITIONAL_ASSEMBLE_SCRIPT_PATH = os.path.join(SOURCE_PATH, "additional_assemble_script.py").replace("\\", "/") -GUI_PATH = os.path.join(DATA_PATH, "source", "gui.ma").replace("\\", "/") -ANALOG_GUI_PATH = os.path.join(DATA_PATH, "source", "analog_gui.ma").replace("\\", "/") -AAS_PATH = os.path.join(DATA_PATH, "source", "additional_assemble_script.py").replace("\\", "/") -print(f"TOOL_PATH: {TOOL_PATH}") -print(f"SCRIPTS_PATH: {SCRIPTS_PATH}") -print(f"ICONS_PATH: {ICONS_PATH}") -print(f"TOOL_ICON: {TOOL_ICON}") -print(f"DATA_PATH: {DATA_PATH}") -print(f"DNA_PATH: {DNA_PATH}") -print(f"BODY_PATH: {BODY_PATH}") -print(f"IMG_PATH: {IMG_PATH}") -print(f"MAP_PATH: {MAP_PATH}") -print(f"MASKS_PATH: {MASKS_PATH}") -print(f"SHADERS_PATH: {SHADERS_PATH}") -print(f"GUI_PATH: {GUI_PATH}") -print(f"ANALOG_GUI_PATH: {ANALOG_GUI_PATH}") -print(f"AAS_PATH: {AAS_PATH}") +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 index >= 0 and index < len(self.itemList): + return self.itemList[index] + return None + + def takeAt(self, index): + if index >= 0 and 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(FlowLayout, self).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()) + size += QSize(2 * self.margin(), 2 * self.margin()) + return size + + def doLayout(self, rect, testOnly): + x = rect.x() + y = rect.y() + lineHeight = 0 + for item in self.itemList: + wid = 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 DNABrowser(QWidget): + dna_selected = QtCore.Signal(str) # Signal: when DNA is selected + + 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() + + 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(10) + + self.scroll_area = QScrollArea() + self.scroll_area.setWidgetResizable(True) + self.scroll_area.setWidget(self.flow_widget) + self.scroll_area.setFixedHeight(350) + self.scroll_area.setStyleSheet(""" + QScrollArea { + border: 1px solid #404040; + background-color: #303030; + } + """) + + self.main_layout.addWidget(self.scroll_area) + + def scan_dna_files(self): + """Scan DNA folder and create index""" + self.dna_files = {} + if not os.path.exists(self.dna_path): + cmds.warning(f"DNA path not found: {self.dna_path}") + return + + # Default preview image path + default_preview = os.path.join(self.img_path, "Preview.png").replace("\\", "/") + + 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("\\", "/") + + # Find corresponding image, if not found use default image + 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 + + # If no corresponding image is found, use default preview image + if not img_file and os.path.exists(default_preview): + img_file = default_preview + + self.dna_files[name] = { + 'dna_path': dna_file, + 'img_path': img_file + } + + def update_grid(self): + """Update DNA grid""" + for i in reversed(range(self.flow_layout.count())): + self.flow_layout.itemAt(i).widget().deleteLater() + + for name, info in self.dna_files.items(): + dna_btn = self.create_dna_button(name, info) + self.flow_layout.addWidget(dna_btn) + + def create_dna_button(self, name, info): + """Create DNA button""" + btn = QPushButton() + btn.setFixedSize(180, 120) + + layout = QVBoxLayout(btn) + layout.setContentsMargins(4, 4, 4, 4) + + # Icon label + icon_label = QLabel() + icon_label.setAlignment(Qt.AlignCenter) + pixmap = QtGui.QPixmap(info['img_path']) + scaled_pixmap = pixmap.scaled(90, 90, Qt.KeepAspectRatio, Qt.SmoothTransformation) + icon_label.setPixmap(scaled_pixmap) + + # Text label + text_label = QLabel(name) + text_label.setAlignment(Qt.AlignCenter) + text_label.setStyleSheet("color: #FFFFFF;") + + layout.addWidget(icon_label) + layout.addWidget(text_label) + + btn.setStyleSheet(""" + QPushButton { + background-color: #303030; + border: 2px solid #202020; + border-radius: 6px; + } + QPushButton:hover { + background-color: #404040; + border: 2px solid #303030; + } + """) + + btn.clicked.connect(lambda: self.dna_selected.emit(info['dna_path'])) + return btn 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 - label = QLabel("Meshes:") + # Layout + self.main_layout = QVBoxLayout() + self.main_layout.setContentsMargins( + MARGIN_BODY_LEFT, + MARGIN_BODY_TOP, + MARGIN_BODY_RIGHT, + MARGIN_BOTTOM + ) + self.setLayout(self.main_layout) + + # Widgets + self.title_label = QLabel("Meshes:") + self.scroll_area = QScrollArea() + self.tree_container = QWidget() self.mesh_tree = self.create_mesh_tree() - - layout = QGridLayout() - layout.addWidget(self.mesh_tree, 0, 0, 4, 1) - layout.setContentsMargins( - MARGIN_BODY_LEFT, - MARGIN_BODY_TOP, - MARGIN_BODY_RIGHT, - MARGIN_BOTTOM, - ) - - layout_holder = QVBoxLayout() - layout_holder.addWidget(label) - layout_holder.addLayout(layout) - layout_holder.setContentsMargins( - MARGIN_BODY_LEFT, - MARGIN_BODY_TOP, - MARGIN_BODY_RIGHT, - MARGIN_BOTTOM, - ) - - self.btn_select_all = QPushButton("Select all meshes") - self.btn_select_all.setEnabled(False) - self.btn_select_all.clicked.connect(self.select_all) - layout_holder.addWidget(self.btn_select_all) - + self.btn_select_all = QPushButton("Select all meshes") self.btn_deselect_all = QPushButton("Deselect all meshes") - self.btn_deselect_all.setEnabled(False) - self.btn_deselect_all.clicked.connect(self.deselect_all) - layout_holder.addWidget(self.btn_deselect_all) - self.setLayout(layout_holder) + # Setup scroll area + self.scroll_area.setWidgetResizable(True) + self.scroll_area.setMinimumHeight(150) + self.scroll_area.setStyleSheet(""" + QScrollArea { + border: 1px solid #404040; + background-color: #303030; + } + """) + + # Setup tree container + self.tree_layout = QVBoxLayout(self.tree_container) + self.tree_layout.setContentsMargins(0, 0, 0, 0) + self.tree_layout.addWidget(self.mesh_tree) + self.scroll_area.setWidget(self.tree_container) + + # Setup buttons + self.btn_select_all.setEnabled(False) + self.btn_deselect_all.setEnabled(False) + self.button_layout = QHBoxLayout() + self.button_layout.addWidget(self.btn_select_all) + self.button_layout.addWidget(self.btn_deselect_all) + + # Layout assembly + self.main_layout.addWidget(self.title_label) + self.main_layout.addWidget(self.scroll_area) + self.main_layout.addLayout(self.button_layout) + + # Connections + self.btn_select_all.clicked.connect(self.select_all) + self.btn_deselect_all.clicked.connect(self.deselect_all) + + # ==================================================== DNA Browser ==================================================== + 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 + } + print(f"DNA file: {name}") + print(f" DNA path: {dna_file}") + print(f" Image path: {img_file}") + print(f" Image exists: {bool(img_file and os.path.exists(img_file))}") + + 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) @@ -174,44 +348,20 @@ class MeshTreeList(QWidget): def fill_mesh_list( self, lod_count: int, names: List[str], indices_names: List[List[int]] ) -> None: - """ - Fills the mesh list with the meshes, and groups them by lods - - @type lod_count: int - @param lod_count: The LOD count - - @type names: List[str] - @param names: The names and indices of all the meshes - - @type indices_names: List[List[int] - @param indices_names: The names and indices of all the meshes - """ - self.mesh_tree.clear() - for i in range(lod_count): parent = QTreeWidgetItem(self.mesh_tree) parent.setText(0, f"LOD {i}") parent.setFlags(parent.flags() | Qt.ItemIsTristate | Qt.ItemIsUserCheckable) - meshes_in_lod = indices_names[i] - for mesh_index in meshes_in_lod: child = QTreeWidgetItem(parent) child.setFlags(child.flags() | Qt.ItemIsUserCheckable) child.setText(0, f"{names[mesh_index]}") child.setCheckState(0, Qt.Unchecked) - self.mesh_tree.setItemExpanded(parent, True) def get_selected_meshes(self) -> List[int]: - """ - Gets the selected meshes from the tree widget - - @rtype: List[int] - @returns: The list of mesh indices that are selected - """ - meshes = [] iterator = QTreeWidgetItemIterator( @@ -229,23 +379,12 @@ class MeshTreeList(QWidget): return meshes def select_all(self) -> None: - """ - Selects all meshes in the tree widget - """ self.iterate_over_items(Qt.Checked) def deselect_all(self) -> None: - """ - Deselects all meshes in the tree widget - """ - self.iterate_over_items(Qt.Unchecked) def iterate_over_items(self, state: Qt.CheckState) -> None: - """ - Deselects all meshes in the tree widget - """ - item = self.mesh_tree.invisibleRootItem() for index in range(item.childCount()): child = item.child(index) @@ -281,61 +420,6 @@ class MeshTreeList(QWidget): class DnaViewerWindow(QMainWindow): - """ - UI Window - - Attributes - ---------- - - @type select_dna_path: FileChooser - @param select_dna_path: The FileChooser widget for getting the DNA path - - @type load_dna_btn: QPushButton - @param load_dna_btn: The button that starts loading in the DNA - - @type mesh_tree_list: QWidget - @param mesh_tree_list: The widget that contains the meshes to be selected in a tree list - - @type joints_cb: QCheckBox - @param joints_cb: The checkbox that represents if joints should be added - - @type blend_shapes_cb: QCheckBox - @param blend_shapes_cb: The checkbox that represents if blend shapes should be added - - @type skin_cb: QCheckBox - @param skin_cb: The checkbox that represents if skin should be added - - @type rig_logic_cb: QCheckBox - @param rig_logic_cb: The checkbox that represents if rig logic should be added - - @type ctrl_attributes_on_root_joint_cb: QCheckBox - @param ctrl_attributes_on_root_joint_cb: The checkbox that represents if control attributes on joint should be added - - @type animated_map_attributes_on_root_joint_cb: QCheckBox - @param animated_map_attributes_on_root_joint_cb: The checkbox that represents if animated maps attributes on root joint should be added - - @type mesh_name_to_blend_shape_channel_name_cb: QCheckBox - @param mesh_name_to_blend_shape_channel_name_cb: The checkbox that represents if mesh names to blend shapes channel name should be added - - @type key_frames_cb: QCheckBox - @param key_frames_cb: The checkbox that represents if key frames should be added - - @type select_gui_path: FileChooser - @param select_gui_path: The FileChooser widget for getting the gui path - - @type select_analog_gui_path: FileChooser - @param select_analog_gui_path: The FileChooser widget for getting the analog gui path - - @type select_aas_path: FileChooser - @param select_aas_path: The FileChooser widget for getting the additional assemble script path - - @type process_btn: QPushButton - @param process_btn: The button that starts creating the scene and character - - @type progress_bar: QProgressBar - @param progress_bar: The progress bar that shows the building progress - """ - _instance: "DnaViewerWindow" = None main_widget: QWidget = None select_dna_path: FileChooser = None @@ -362,13 +446,10 @@ class DnaViewerWindow(QMainWindow): self.header: QHBoxLayout = None self.build_options: QWidget = None self.extra_build_options: QWidget = None - self.setup_window() self.create_ui() def setup_window(self) -> None: - """A method for setting up the window""" - self.setWindowFlags( self.windowFlags() | Qt.WindowTitleHint @@ -383,8 +464,6 @@ class DnaViewerWindow(QMainWindow): self.setFocusPolicy(Qt.StrongFocus) def create_ui(self) -> None: - """Fills the window with UI elements""" - self.main_widget = self.create_main_widget() self.setCentralWidget(self.main_widget) self.set_size() @@ -396,28 +475,21 @@ class DnaViewerWindow(QMainWindow): return file.read() def create_main_widget(self) -> QWidget: - """ - Creates the widget containing the UI elements - - @rtype: QtWidgets.QWidget - @returns: the main widget - """ - - header = self.create_header() - body = self.create_body() - widget = QWidget() layout = QVBoxLayout(widget) + + header = self.create_header() layout.addLayout(header) layout.addWidget(QHLine()) + + body = self.create_body() layout.addLayout(body) + layout.setContentsMargins(MARGIN_LEFT, MARGIN_TOP, MARGIN_RIGHT, MARGIN_BOTTOM) layout.setSpacing(SPACING) return widget def set_size(self) -> None: - """Sets the window size""" - self.setMaximumSize(WINDOW_SIZE_WIDTH_MAX, WINDOW_SIZE_HEIGHT_MAX) self.setMinimumSize(WINDOW_SIZE_WIDTH_MIN, WINDOW_SIZE_HEIGHT_MIN) self.resize(WINDOW_SIZE_WIDTH_MIN, WINDOW_SIZE_HEIGHT_MIN) @@ -443,8 +515,6 @@ class DnaViewerWindow(QMainWindow): return True def process(self) -> None: - """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() @@ -453,9 +523,9 @@ class DnaViewerWindow(QMainWindow): 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(), + gui_path=GUI_PATH, + analog_gui_path=ANALOG_GUI_PATH, + aas_path=ADDITIONAL_ASSEMBLE_SCRIPT_PATH, add_rig_logic=self.add_rig_logic(), add_joints=self.add_joints(), add_blend_shapes=self.add_blend_shapes(), @@ -467,7 +537,6 @@ class DnaViewerWindow(QMainWindow): ) self.main_widget.setEnabled(False) - try: self.set_progress(value=33) self.dna = DNA(self.select_dna_path.get_file_path()) @@ -482,8 +551,6 @@ class DnaViewerWindow(QMainWindow): self.main_widget.setEnabled(True) def set_progress(self, text: str = None, value: int = None) -> None: - """Setting text and/or value to progress bar""" - if text is not None: self.progress_bar.setFormat(text) if value is not None: @@ -499,15 +566,6 @@ class DnaViewerWindow(QMainWindow): @staticmethod def maya_main_window() -> QWidget: - """ - Gets the MayaWindow instance - - @throws RuntimeError - - @rtype: QtWidgets.QWidget - @returns: main window instance - """ - for obj in QApplication.topLevelWidgets(): if obj.objectName() == "MayaWindow": return obj @@ -515,8 +573,6 @@ class DnaViewerWindow(QMainWindow): @staticmethod def activate_window() -> None: - """Shows window if minimized""" - try: DnaViewerWindow._instance.show() @@ -556,16 +612,6 @@ class DnaViewerWindow(QMainWindow): return self.is_checked(self.key_frames_cb) def is_checked(self, checkbox: QCheckBox) -> bool: - """ - Returns if the provided checkbox is checked and enabled - - @type checkbox: QCheckBox - @param checkbox: The checkbox thats value needs to be checked and enabled - - @rtype: bool - @returns: The flag representing if the checkbox is checked and enabled - """ - return ( checkbox is not None and bool(checkbox.isEnabled()) @@ -573,13 +619,7 @@ class DnaViewerWindow(QMainWindow): ) def create_body(self) -> QVBoxLayout: - """ - Creates the main body layout and adds needed widgets - - @rtype: QVBoxLayout - @returns: The created vertical box layout with the widgets added - """ - + """Create body layout""" self.body = QVBoxLayout() self.body.setContentsMargins( MARGIN_BODY_LEFT, @@ -588,42 +628,67 @@ class DnaViewerWindow(QMainWindow): MARGIN_BOTTOM, ) self.body.setSpacing(SPACING) + + # Add DNA browser + self.dna_browser = DNABrowser(DNA_PATH, IMG_PATH, self) + self.dna_browser.dna_selected.connect(self.on_dna_browser_selected) + self.body.addWidget(self.dna_browser) + + # DNA selector self.create_dna_selector() + self.mesh_tree_list = self.create_mesh_selector() + self.build_options = self.create_build_options() self.extra_build_options = self.create_extra_build_options() - tab = QTabWidget(self) tab.addTab(self.build_options, "Build options") tab.addTab(self.extra_build_options, "Extra options") + 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.select_gui_path = FileChooser("", "", self) + self.select_gui_path.fc_text_field.setText(GUI_PATH) + self.select_gui_path.hide() + + self.select_analog_gui_path = FileChooser("", "", self) + self.select_analog_gui_path.fc_text_field.setText(ANALOG_GUI_PATH) + self.select_analog_gui_path.hide() + + self.select_aas_path = FileChooser("", "", self) + self.select_aas_path.fc_text_field.setText(ADDITIONAL_ASSEMBLE_SCRIPT_PATH) + self.select_aas_path.hide() + self.process_btn = self.create_process_btn() self.progress_bar = self.create_progress_bar() - + return self.body + def on_dna_browser_selected(self, dna_path: str) -> None: + """When DNA browser selects a DNA file, update the input field""" + if self.select_dna_path: + self.select_dna_path.fc_text_field.setText(dna_path) + self.on_dna_selected(self.select_dna_path) + + def on_dna_path_changed(self, text: str) -> None: + """When DNA file input field content changes""" + if os.path.exists(text) and text.endswith('.dna'): + self.on_dna_selected(self.select_dna_path) + def create_header(self) -> QHBoxLayout: - """ - Creates and adds to the header widget - - @rtype: QHBoxLayout - @returns: The created horizontal box layout with the widgets added - """ - self.header = QHBoxLayout() + label = QLabel("v" + __version__) + btn = self.create_help_btn() + self.header.addWidget(label) self.header.addStretch(1) self.header.addWidget(btn) + self.header.setContentsMargins( MARGIN_HEADER_LEFT, MARGIN_HEADER_TOP, @@ -634,13 +699,6 @@ class DnaViewerWindow(QMainWindow): return self.header def create_help_btn(self) -> QWidget: - """ - Creates the help button widget - - @rtype: QHBoxLayout - @returns: The created horizontal box layout with the widgets added - """ - btn = QPushButton(self) btn.setText(" ? ") btn.setToolTip("Help") @@ -648,10 +706,8 @@ class DnaViewerWindow(QMainWindow): return btn def on_help(self) -> None: - """The method that gets called when the help button is clicked""" - - if HELP_URL: - webbrowser.open(HELP_URL) + if TOOL_HELP_URL: + webbrowser.open(TOOL_HELP_URL) else: QMessageBox.about( self, @@ -660,13 +716,6 @@ 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() self.select_dna_path = self.create_dna_chooser() self.load_dna_btn = self.create_load_dna_button(self.select_dna_path) @@ -691,27 +740,13 @@ class DnaViewerWindow(QMainWindow): return widget def on_dna_selected(self, input: FileChooser) -> None: - """ - 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 - """ - enabled = input.get_file_path() is not None self.load_dna_btn.setEnabled(enabled) self.process_btn.setEnabled(False) def create_dna_chooser(self) -> FileChooser: - """ - Creates and adds the DNA chooser widget - - @rtype: FileChooser - @returns: Dna chooser widget - """ - return self.create_file_chooser( - "Path:", + "DNA File:", "DNA file to load. Required by all gui elements", "Select a DNA file", "DNA files (*.dna)", @@ -719,12 +754,6 @@ class DnaViewerWindow(QMainWindow): ) def on_dna_changed(self, state: int) -> None: # pylint: disable=unused-argument - """ - Method that gets called when the checkbox is changed - - @type state: int - @param state: The changed state of the checkbox - """ enabled = False if self.dna: if self.dna.path == self.select_dna_path.get_file_path(): @@ -736,29 +765,12 @@ class DnaViewerWindow(QMainWindow): self.process_btn.setEnabled(enabled) def create_load_dna_button(self, dna_input: FileChooser) -> QWidget: - """ - Creates and adds the load DNA button widget - - @type input: FileChooser - @param input: The file chooser object corresponding to the DNA selector widget - - @rtype: QWidget - @returns: The created load DNA button widget - """ - btn = QPushButton("Load DNA") btn.setEnabled(False) btn.clicked.connect(lambda: self.on_load_dna_clicked(dna_input)) return btn def on_load_dna_clicked(self, input: FileChooser) -> None: - """ - 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 - """ - self.main_widget.setEnabled(False) QCoreApplication.processEvents() try: @@ -786,27 +798,18 @@ class DnaViewerWindow(QMainWindow): self.main_widget.setEnabled(True) def get_mesh_names(self) -> List[str]: - """Reads in the meshes of the definition""" names: List[str] = [] for index in range(self.dna.get_mesh_count()): names.append(self.dna.get_mesh_name(index)) return names def get_lod_indices_names(self) -> List[List[int]]: - """Reads in the meshes of the definition""" lod_indices: List[List[int]] = [] for index in range(self.dna.get_lod_count()): lod_indices.append(self.dna.get_mesh_indices_for_lod(index)) return lod_indices def create_mesh_selector(self) -> MeshTreeList: - """ - Creates and adds a mesh tree list where the entries are grouped by lods, this is used for selecting the meses that need to be processed - - @rtype: MeshTreeList - @returns: The created mesh tree list widget - """ - widget = MeshTreeList(self) self.body.addWidget(widget) return widget @@ -819,24 +822,6 @@ class DnaViewerWindow(QMainWindow): filter: str, on_changed: Callable[[int], None] = None, ) -> FileChooser: - """ - Creates a file chooser widget that is used for selecting file paths - - @type label: str - @param label: The label in the FileDialog that pops up - - @type hint: str - @param hint: The label in the FileDialog that pops up - - @type caption: str - @param caption: The caption in the FileDialog that pops up - - @type filter: str - @param filter: The file filter that is used in the FileDialog - - @rtype: FileChooser - @returns: The created file chooser object - """ widget = FileChooser( label, @@ -850,43 +835,30 @@ class DnaViewerWindow(QMainWindow): return widget def create_gui_selector(self) -> FileChooser: - """Creates the hidden gui selector widget with default path""" - widget = self.create_file_chooser( + return self.create_file_chooser( "Gui path:", - "GUI file to load", - "Select a GUI file", - "Maya ASCII (*.ma)", + "GUI file to load. Required by RigLogic", + "Select the gui file", + "gui files (*.ma)", ) - widget.hide() - widget.fc_text_field.setText(GUI_PATH) - return widget - - def create_analog_gui_selector(self) -> FileChooser: - """Creates the hidden analog gui selector widget with default path""" - widget = self.create_file_chooser( - "Analog gui path:", - "Analog GUI file to load", - "Select an analog GUI file", - "Maya ASCII (*.ma)", - ) - widget.hide() - widget.fc_text_field.setText(ANALOG_GUI_PATH) - return widget def create_aas_selector(self) -> FileChooser: - """Creates the hidden additional assemble script selector widget with default path""" - widget = self.create_file_chooser( - "Additional Script Path:", - "Additional assembly script to load", - "Select a Python file", - "Python files (*.py)", + return self.create_file_chooser( + "Additional assemble script path:", + "Additional assemble script to use. Required by RigLogic", + "Select the aas file", + "python script (*.py)", + ) + + def create_analog_gui_selector(self) -> FileChooser: + return self.create_file_chooser( + "Analog gui path:", + "Analog GUI file to load. Required by RigLogic", + "Select the analog gui file", + "analog gui files (*.ma)", ) - widget.hide() - widget.fc_text_field.setText(AAS_PATH) - return widget def create_build_options(self) -> QWidget: - """Creates and adds the widget containing the build options checkboxes""" widget = QWidget() layout = QVBoxLayout(widget) layout.setContentsMargins( @@ -921,10 +893,10 @@ class DnaViewerWindow(QMainWindow): ) layout.addStretch() + widget.setMaximumHeight(150) return widget def create_extra_build_options(self) -> QWidget: - """Creates and adds the widget containing the extra build options checkboxes""" widget = QWidget() layout = QVBoxLayout(widget) layout.setContentsMargins( @@ -964,6 +936,7 @@ class DnaViewerWindow(QMainWindow): ) layout.addStretch() + widget.setMaximumHeight(150) return widget def enable_additional_build_options(self, enable: bool) -> None: @@ -981,28 +954,7 @@ class DnaViewerWindow(QMainWindow): checked: bool = False, enabled: bool = False, ) -> QCheckBox: - """ - Adds a checkbox with given parameters and connects them to the on_changed method - - @type label: str - @param label: The label of the checkbox - - @type hint: str - @param hint: The hint of the checkbox - - @type on_changed: Callable[[int], None] - @param on_changed: The method that will get called when the checkbox changes states - - @type checked: bool - @param checked: The value representing if the checkbox is checked after creation - - @type enabled: bool - @param enabled: The value representing if the checkbox is enabled after creation - - @rtype: QCheckBox - @returns: the created checkbox object - """ - + checkbox = QCheckBox(label, self) checkbox.setChecked(checked) checkbox.setEnabled(enabled) @@ -1013,13 +965,6 @@ class DnaViewerWindow(QMainWindow): return checkbox def on_joints_changed(self, state: int) -> None: - """ - Method that gets called when the joints checkbox is changed - - @type state: int - @param state: The changed state of the checkbox - """ - if self.joints_cb.isChecked(): self.process_btn.setEnabled(True) if self.mesh_tree_list.get_selected_meshes(): @@ -1031,16 +976,6 @@ class DnaViewerWindow(QMainWindow): self.on_generic_changed(state) def create_process_btn(self) -> QPushButton: - """ - Creates and adds a process button - - @type window: QMainWindow - @param window: The instance of the window object - - @rtype: QPushButton - @returns: The created process button - """ - btn = QPushButton("Process") btn.setEnabled(False) btn.clicked.connect(self.process) @@ -1049,16 +984,6 @@ class DnaViewerWindow(QMainWindow): return btn def create_progress_bar(self) -> QProgressBar: - """ - Creates and adds progress bar - - @type window: QMainWindow - @param window: The instance of the window object - - @rtype: QProgressBar - @returns: The created progress bar - """ - progress = QProgressBar(self) progress.setRange(0, 100) progress.setValue(0) @@ -1068,23 +993,9 @@ class DnaViewerWindow(QMainWindow): return progress def on_generic_changed(self, state: int) -> None: # pylint: disable=unused-argument - """ - Method that gets called when the checkbox is changed - - @type state: int - @param state: The changed state of the checkbox - """ - self.set_riglogic_cb_enabled() def is_enabled_and_checked(self, check_box: QCheckBox) -> bool: - """ - Method that checks if check box is enabled in same time - - @type check_box: QCheckBox - @param check_box: The checkbox instance to check - """ - return ( check_box is not None and bool(check_box.isEnabled()) @@ -1092,15 +1003,10 @@ class DnaViewerWindow(QMainWindow): ) def set_riglogic_cb_enabled(self) -> None: - """Method that sets enable state of riglogic check box""" - all_total_meshes = False if self.dna and self.is_enabled_and_checked(self.blend_shapes_cb): - if ( - len(self.mesh_tree_list.get_selected_meshes()) - == self.dna.get_mesh_count() - ): + if len(self.mesh_tree_list.get_selected_meshes()) == self.dna.get_mesh_count(): all_total_meshes = True enabled = ( @@ -1108,8 +1014,5 @@ class DnaViewerWindow(QMainWindow): and self.is_enabled_and_checked(self.blend_shapes_cb) and all_total_meshes and self.is_enabled_and_checked(self.skin_cb) - and self.select_gui_path.get_file_path() is not None - and self.select_analog_gui_path.get_file_path() is not None - and self.select_aas_path.get_file_path() is not None ) self.rig_logic_cb.setEnabled(enabled)