diff --git a/scripts/DNA_Viewer/ui/__init__.py b/scripts/DNA_Viewer/ui/__init__.py index e69de29..8b13789 100644 --- a/scripts/DNA_Viewer/ui/__init__.py +++ b/scripts/DNA_Viewer/ui/__init__.py @@ -0,0 +1 @@ + diff --git a/scripts/DNA_Viewer/ui/app.py b/scripts/DNA_Viewer/ui/app.py index 64342c1..1ea56bb 100644 --- a/scripts/DNA_Viewer/ui/app.py +++ b/scripts/DNA_Viewer/ui/app.py @@ -1,3 +1,6 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + import logging import os import webbrowser @@ -5,13 +8,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, @@ -22,7 +26,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 @@ -35,13 +42,14 @@ def show() -> None: DnaViewerWindow.show_window() -WINDOW_OBJECT = "dnaviewer" +TOOL_NAME = "MetaFusion" WINDOW_TITLE = "DNA Viewer" -HELP_URL = "https://epicgames.github.io/MetaHuman-DNA-Calibration/" +WINDOW_OBJECT = "dnaviewer" +TOOL_HELP_URL = f"https://gitea.cgnico.com/CGNICO/MetaFusion/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 @@ -55,61 +63,273 @@ MARGIN_BODY_LEFT = 0 MARGIN_BODY_TOP = 0 MARGIN_BODY_RIGHT = 0 +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("\\", "/") +DATA_PATH = os.path.join(TOOL_PATH, "data").replace("\\", "/") +DNA_PATH = os.path.join(DATA_PATH, "dna").replace("\\", "/") +IMG_PATH = os.path.join(DATA_PATH, "img").replace("\\", "/") +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("\\", "/") + + +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 = 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("\\", "/") + 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 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 = 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 = 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) @@ -120,44 +340,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( @@ -175,23 +371,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) @@ -227,61 +412,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 @@ -308,13 +438,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 @@ -329,8 +456,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() @@ -342,28 +467,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) @@ -389,8 +507,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() @@ -399,9 +515,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(), @@ -413,7 +529,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()) @@ -428,8 +543,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: @@ -445,15 +558,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 @@ -461,8 +565,6 @@ class DnaViewerWindow(QMainWindow): @staticmethod def activate_window() -> None: - """Shows window if minimized""" - try: DnaViewerWindow._instance.show() @@ -502,16 +604,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()) @@ -519,13 +611,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, @@ -534,42 +620,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, @@ -580,13 +691,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") @@ -594,10 +698,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, @@ -606,13 +708,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) @@ -637,27 +732,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)", @@ -665,12 +746,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(): @@ -682,29 +757,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: @@ -732,27 +790,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 @@ -765,24 +814,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, @@ -796,13 +827,6 @@ class DnaViewerWindow(QMainWindow): return widget def create_gui_selector(self) -> FileChooser: - """ - Creates the gui selector widget - - @rtype: FileChooser - @returns: Gui selector widget - """ - return self.create_file_chooser( "Gui path:", "GUI file to load. Required by RigLogic", @@ -811,13 +835,6 @@ class DnaViewerWindow(QMainWindow): ) def create_aas_selector(self) -> FileChooser: - """ - Creates and adds the additional assemble script selector widget - - @rtype: FileChooser - @returns: Additional assemble script selector widget - """ - return self.create_file_chooser( "Additional assemble script path:", "Additional assemble script to use. Required by RigLogic", @@ -826,13 +843,6 @@ class DnaViewerWindow(QMainWindow): ) def create_analog_gui_selector(self) -> FileChooser: - """ - Creates and adds the analog gui selector widget - - @rtype: FileChooser - @returns: Analog gui selector widget - """ - return self.create_file_chooser( "Analog gui path:", "Analog GUI file to load. Required by RigLogic", @@ -841,7 +851,6 @@ class DnaViewerWindow(QMainWindow): ) def create_build_options(self) -> QWidget: - """Creates and adds the widget containing the build options checkboxes""" widget = QWidget() layout = QVBoxLayout(widget) layout.setContentsMargins( @@ -876,10 +885,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( @@ -919,6 +928,7 @@ class DnaViewerWindow(QMainWindow): ) layout.addStretch() + widget.setMaximumHeight(150) return widget def enable_additional_build_options(self, enable: bool) -> None: @@ -936,28 +946,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) @@ -968,13 +957,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(): @@ -986,16 +968,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) @@ -1004,16 +976,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) @@ -1023,23 +985,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()) @@ -1047,15 +995,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 = ( @@ -1063,8 +1006,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) + self.rig_logic_cb.setEnabled(enabled) \ No newline at end of file diff --git a/scripts/MetaFusion.py b/scripts/MetaFusion.py index 5f28270..b267b5c 100644 --- a/scripts/MetaFusion.py +++ b/scripts/MetaFusion.py @@ -1 +1,322 @@ - \ No newline at end of file +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +#===================================== IMPORTS ===================================== +# Standard library imports +import os +import sys +import webbrowser +import locale + +# Qt imports +from PySide2 import QtWidgets, QtCore, QtGui +from shiboken2 import wrapInstance + +# Maya imports +from maya import OpenMayaUI as omui +import maya.cmds as cmds +import maya.mel as mel + +#===================================== IMPORT MODULES ===================================== +# Standard library imports +import BodyPrep +import BatchImport +import DNA_Viewer + +#===================================== CONSTANTS ===================================== +# Tool info +TOOL_NAME = "MetaFusion" +TOOL_VERSION = "Beta v1.0.0" +TOOL_AUTHOR = "CGNICO" +TOOL_WSCL_NAME = "MetaFusionWorkSpaceControl" +TOOL_HELP_URL = f"https://gitea.cgnico.com/CGNICO/MetaFusion/wiki" +DEFAULT_WINDOW_SIZE = (500, 800) + + +TOOL_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__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("\\", "/") + +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("\\", "/") +OUTPUT_PATH = os.path.join(DATA_PATH, "output").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}") + + +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"MH4_PATH: {MH4_PATH}") +print(f"MH4_DNA_PATH: {MH4_DNA_PATH}") +print(f"OUTPUT_PATH: {OUTPUT_PATH}") +print(f"SAVE_PATH: {SAVE_PATH}") +print(f"MAYA_VERSION: {MAYA_VERSION}") +print(f"PLUGIN_PATH: {PLUGIN_PATH}") + +#===================================== LANGUAGE SETTINGS ===================================== +TOOL_LANG = 'en_US' +SUPPORTED_LANGUAGES = ['en_US', 'zh_CN'] + +LANG = { + "en_US": { + "MetaFusion": "MetaFusion" + }, + "zh_CN": { + "MetaFusion": "MetaFusion" + } +} + +#===================================== UTILITY FUNCTIONS ===================================== +def get_system_encoding(): + encoding = sys.getdefaultencoding() + if encoding.lower() == 'ascii': + encoding = locale.getpreferredencoding() + return encoding + +def maya_main_window(): + main_window_ptr = omui.MQtUtil.mainWindow() + return wrapInstance(int(main_window_ptr), QtWidgets.QWidget) + +#===================================== UI COMPONENTS ===================================== +class MainButton(QtWidgets.QPushButton): + DEFAULT_COLORS = {"normal": "#D0D0D0", "hover": "#E0E0E0", "pressed": "#C0C0C0"} + + def __init__(self, text="", icon=None, color=None, hover_color=None, pressed_color=None): + super().__init__(text) + if icon: + self.setIcon(icon) + self.setIconSize(QtCore.QSize(24, 24)) + self.setMinimumHeight(30) + 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): + return f""" + QPushButton {{ + background-color: {normal}; + color: #303030; + border-radius: 10px; + padding: 5px; + font-weight: bold; + text-align: center; + }} + QPushButton:hover {{ + background-color: {hover}; + }} + QPushButton:pressed {{ + background-color: {pressed}; + }} + """ + +class BottomButton(QtWidgets.QPushButton): + def __init__(self, text="", icon=None): + super().__init__(text) + self.setMinimumHeight(20) + self.setStyleSheet(self._generate_style_sheet()) + self.setFont(QtGui.QFont("Microsoft YaHei", 10)) + + @staticmethod + def _generate_style_sheet(): + return """ + QPushButton { + background-color: transparent; + border: none; + color: gray; + font-weight: bold; + } + QPushButton:hover { + color: black; + } + """ + +#===================================== MAIN WINDOW ===================================== +class MainWindow(QtWidgets.QWidget): + + instance = None + + def __init__(self, parent=maya_main_window()): + super(MainWindow, self).__init__(parent) + self.setWindowTitle(f"{TOOL_NAME} - {TOOL_VERSION}") + self.setObjectName(TOOL_PATH) + self.setWindowFlags(QtCore.Qt.Window) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred) + self.setMinimumSize(300, 800) + + self.create_widgets() + self.create_layouts() + self.create_connections() + + if os.path.exists(TOOL_ICON): + self.setWindowIcon(QtGui.QIcon(TOOL_ICON)) + else: + print(f"WARNING: Icon file not found: {TOOL_ICON}") + + @classmethod + def show_window(cls): + try: + if cmds.workspaceControl(TOOL_WSCL_NAME, exists=True): + cmds.deleteUI(TOOL_WSCL_NAME, control=True) + if cls.instance is not None: + try: + cls.instance.close() + cls.instance.deleteLater() + except Exception: + pass + cls.instance = cls() + cls.instance.dock_to_maya() + return cls.instance + except Exception as e: + print(f"Error showing {TOOL_NAME} window: {e}") + return None + + def dock_to_maya(self): + if cmds.workspaceControl(TOOL_WSCL_NAME, exists=True): + cmds.deleteUI(TOOL_WSCL_NAME) + try: + workspace_control = cmds.workspaceControl(TOOL_WSCL_NAME, label=TOOL_NAME, floating=True, retain=True, resizeWidth=True, initialWidth=500, minimumWidth=500) + cmds.workspaceControl(TOOL_WSCL_NAME, e=True, resizeWidth=True) + cmds.control(self.objectName(), e=True, p=workspace_control) + cmds.evalDeferred(lambda: cmds.workspaceControl(TOOL_WSCL_NAME, e=True, resizeWidth=True)) + except Exception as e: + print(f"Error creating workspace control: {e}") + +#===================================== UI COMPONENTS ===================================== + def create_widgets(self): + 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") + 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") + 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") + + self.help_btn = BottomButton(LANG[TOOL_LANG]["Help"]) + self.help_btn.setToolTip(LANG[TOOL_LANG]["Help"]) + self.help_btn.setFixedSize(100, 20) + self.lang_btn = BottomButton(LANG[TOOL_LANG]["ZH" if TOOL_LANG == 'en_US' else "EN"]) + 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) + content_layout = QtWidgets.QVBoxLayout() + content_layout.setContentsMargins(5, 5, 5, 5) + prepare_group = QtWidgets.QGroupBox(LANG[TOOL_LANG]["Prepare"]) + prepare_layout = QtWidgets.QVBoxLayout(prepare_group) + prepare_layout.addWidget(self.body_prepare_btn) + content_layout.addWidget(prepare_group) + dna_edit_group = QtWidgets.QGroupBox(LANG[TOOL_LANG]["DNA Edit"]) + dna_edit_layout = QtWidgets.QVBoxLayout(dna_edit_group) + dna_edit_layout.addWidget(self.dna_viewer_btn) + content_layout.addWidget(dna_edit_group) + import_group = QtWidgets.QGroupBox(LANG[TOOL_LANG]["Import"]) + import_layout = QtWidgets.QVBoxLayout(import_group) + import_layout.addWidget(self.batch_import_btn) + + content_layout.addWidget(import_group) + main_layout.addLayout(content_layout) + + # Bottom layout + main_layout.addStretch() + bottom_layout = QtWidgets.QHBoxLayout() + bottom_layout.setContentsMargins(5, 0, 5, 5) + icon_label = QtWidgets.QLabel() + if os.path.exists(TOOL_ICON): + icon = QtGui.QPixmap(TOOL_ICON).scaled(24, 24, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation) + icon_label.setPixmap(icon) + version_label = QtWidgets.QLabel(f"{TOOL_VERSION}") + version_label.setStyleSheet("color: gray; font-size: 12px;") + bottom_layout.addWidget(icon_label) + bottom_layout.addWidget(version_label) + bottom_layout.addStretch() + bottom_layout.addWidget(self.help_btn) + bottom_layout.addWidget(self.lang_btn) + main_layout.addLayout(bottom_layout) + + def create_connections(self): + # Connect function buttons + self.body_prepare_btn.clicked.connect(self.run_body_prepare) + self.dna_viewer_btn.clicked.connect(self.run_dna_viewer) + self.batch_import_btn.clicked.connect(self.run_batch_import) + # Existing connections + self.help_btn.clicked.connect(self.help) + self.lang_btn.clicked.connect(self.switch_language) + +#===================================== FUNCTIONS ===================================== + #===================================== MAIN FUNCTIONS ===================================== + def run_body_prepare(self): + BodyPrep.run() + + def run_dna_viewer(self): + DNA_Viewer.show() + + def run_batch_import(self): + BatchImport.run() + + #===================================== BOTTOM LAYOUT ===================================== + def help(self): + webbrowser.open(TOOL_HELP_URL) + + def switch_language(self): + global TOOL_LANG + TOOL_LANG = 'en_US' if TOOL_LANG == 'zh_CN' else 'zh_CN' + self.lang_btn.setText("ZH" if TOOL_LANG == 'en_US' else "EN") + self.retranslate_ui() + + QtWidgets.QToolTip.showText( + self.lang_btn.mapToGlobal(QtCore.QPoint(0, -30)), + "Language switched" if TOOL_LANG == 'en_US' else "语言已切换", + self.lang_btn + ) + + def retranslate_ui(self): + self.body_prepare_btn.setText(LANG[TOOL_LANG]["Body Prepare"]) + self.dna_viewer_btn.setText(LANG[TOOL_LANG]["Open DNA Viewer"]) + self.batch_import_btn.setText(LANG[TOOL_LANG]["Batch Import"]) + for button in [self.body_prepare_btn, self.batch_import_btn, self.dna_viewer_btn]: + button.setFont(QtGui.QFont("Microsoft YaHei", 10)) + + self.help_btn.setText(LANG[TOOL_LANG]["Help"]) + self.lang_btn.setText("ZH" if TOOL_LANG == 'en_US' else "EN") + for button in [self.help_btn, self.lang_btn]: + button.setFont(QtGui.QFont("Microsoft YaHei", 10)) + + def on_dna_selected(self, dna_path): + self.dna_file_input.setText(dna_path) + print(f"Selected DNA file: {dna_path}") + + def on_dna_file_changed(self): + dna_path = self.dna_file_input.text() + print(f"DNA file path updated: {dna_path}") + +#===================================== LAUNCH FUNCTIONS ===================================== +def show(): + return MainWindow.show_window() \ No newline at end of file