#!/usr/bin/env python # -*- coding: utf-8 -*- import logging import os import webbrowser from typing import Callable, List from maya import cmds from maya.cmds import confirmDialog from PySide2.QtCore import Qt, QSize, QRect, QPoint, QCoreApplication from PySide2.QtWidgets import ( QApplication, QCheckBox, QGridLayout, QHBoxLayout, QLabel, QLayout, QMainWindow, QMessageBox, QProgressBar, QPushButton, QTabWidget, QTreeWidget, QTreeWidgetItem, 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 from ..dnalib.layer import Layer from ..version import __version__ from .widgets import FileChooser, QHLine def show() -> None: DnaViewerWindow.show_window() TOOL_NAME = "MetaFusion" WINDOW_TITLE = "DNA Viewer" 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 = 1000 WINDOW_SIZE_HEIGHT_MAX = 1000 MARGIN_LEFT = 8 MARGIN_TOP = 8 MARGIN_RIGHT = 8 MARGIN_BOTTOM = 8 MARGIN_HEADER_LEFT = 0 MARGIN_HEADER_TOP = 0 MARGIN_HEADER_RIGHT = 0 MARGIN_HEADER_BOTTOM = 0 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): def __init__(self, main_window: "DnaViewerWindow") -> None: super().__init__() self.main_window = main_window # 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() self.btn_select_all = QPushButton("Select all meshes") self.btn_deselect_all = QPushButton("Deselect all meshes") # 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: mesh_tree = QTreeWidget() mesh_tree.setHeaderHidden(True) mesh_tree.itemChanged.connect(self.tree_item_changed) mesh_tree.setStyleSheet("background-color: #505050") mesh_tree.setToolTip("Select mesh or meshes to add to rig") return mesh_tree def fill_mesh_list( self, lod_count: int, names: List[str], indices_names: List[List[int]] ) -> None: 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]: meshes = [] iterator = QTreeWidgetItemIterator( self.mesh_tree, QTreeWidgetItemIterator.Checked ) while iterator.value(): item = iterator.value() mesh_name = item.text(0) mesh_index = self.main_window.dna.get_mesh_id_from_mesh_name(mesh_name) if mesh_index is not None: meshes.append(mesh_index) iterator += 1 return meshes def select_all(self) -> None: self.iterate_over_items(Qt.Checked) def deselect_all(self) -> None: self.iterate_over_items(Qt.Unchecked) def iterate_over_items(self, state: Qt.CheckState) -> None: item = self.mesh_tree.invisibleRootItem() for index in range(item.childCount()): child = item.child(index) child.setCheckState(0, state) def tree_item_changed(self) -> None: """The method that gets called when a tree item gets its value changed""" meshes = self.get_selected_meshes() if meshes: self.main_window.skin_cb.setEnabled(self.main_window.joints_cb.checkState()) self.main_window.blend_shapes_cb.setEnabled(True) self.main_window.process_btn.setEnabled(True) self.main_window.rig_logic_cb.setEnabled(False) if len(meshes) == self.main_window.dna.get_mesh_count(): self.main_window.rig_logic_cb.setEnabled( self.main_window.joints_cb.checkState() and self.main_window.blend_shapes_cb.checkState() and self.main_window.skin_cb.checkState() and self.main_window.select_gui_path.get_file_path() is not None and self.main_window.select_analog_gui_path.get_file_path() is not None and self.main_window.select_aas_path.get_file_path() is not None ) else: self.main_window.skin_cb.setEnabled(False) self.main_window.blend_shapes_cb.setEnabled(False) self.main_window.process_btn.setEnabled( self.main_window.joints_cb.checkState() ) class DnaViewerWindow(QMainWindow): _instance: "DnaViewerWindow" = None main_widget: QWidget = None select_dna_path: FileChooser = None load_dna_btn: QPushButton = None mesh_tree_list: QWidget = None joints_cb: QCheckBox = None blend_shapes_cb: QCheckBox = None skin_cb: QCheckBox = None rig_logic_cb: QCheckBox = None ctrl_attributes_on_root_joint_cb: QCheckBox = None animated_map_attributes_on_root_joint_cb: QCheckBox = None mesh_name_to_blend_shape_channel_name_cb: QCheckBox = None key_frames_cb: QCheckBox = None select_gui_path: FileChooser = None select_analog_gui_path: FileChooser = None select_aas_path: FileChooser = None process_btn: QPushButton = None progress_bar: QProgressBar = None dna: DNA = None def __init__(self, parent: QWidget = None) -> None: super().__init__(parent) 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() def setup_window(self) -> None: self.setWindowFlags( self.windowFlags() | Qt.WindowTitleHint | Qt.WindowMaximizeButtonHint | Qt.WindowMinimizeButtonHint | Qt.WindowCloseButtonHint ) self.setAttribute(Qt.WA_DeleteOnClose) self.setObjectName(WINDOW_OBJECT) self.setWindowTitle(WINDOW_TITLE) self.setWindowFlags(Qt.Window) self.setFocusPolicy(Qt.StrongFocus) def create_ui(self) -> None: self.main_widget = self.create_main_widget() self.setCentralWidget(self.main_widget) self.set_size() self.setStyleSheet(self.load_css()) def load_css(self) -> str: css = os.path.join(os.path.dirname(__file__), "app.css") with open(css, encoding="utf-8") as file: return file.read() def create_main_widget(self) -> QWidget: 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: 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) def show_message_dialog(self) -> bool: dlg = QMessageBox() dlg.setIcon(QMessageBox.Warning) dlg.setWindowTitle("Warning") dlg.setText( "Unsaved changes exists.\nSave changes and create new scene, discard changes, and create new scene or cancel procesing." ) dlg.setStandardButtons( QMessageBox.Save | QMessageBox.Discard | QMessageBox.Cancel ) button = dlg.exec_() if button == QMessageBox.Save: cmds.SaveScene() return not cmds.file(q=True, modified=True) if button == QMessageBox.Cancel: return False return True def process(self) -> None: 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=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(), 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: if text is not None: self.progress_bar.setFormat(text) if value is not None: self.progress_bar.setValue(value) @staticmethod def show_window() -> None: if DnaViewerWindow._instance is None: DnaViewerWindow._instance = DnaViewerWindow( parent=DnaViewerWindow.maya_main_window() ) DnaViewerWindow.activate_window() @staticmethod def maya_main_window() -> QWidget: for obj in QApplication.topLevelWidgets(): if obj.objectName() == "MayaWindow": return obj raise RuntimeError("Could not find MayaWindow instance") @staticmethod def activate_window() -> None: try: DnaViewerWindow._instance.show() if DnaViewerWindow._instance.windowState() & Qt.WindowMinimized: DnaViewerWindow._instance.setWindowState(Qt.WindowActive) DnaViewerWindow._instance.raise_() DnaViewerWindow._instance.activateWindow() except RuntimeError as e: logging.info(e) if str(e).rstrip().endswith("already deleted."): DnaViewerWindow._instance = None DnaViewerWindow.show_window() def add_joints(self) -> bool: return self.is_checked(self.joints_cb) def add_blend_shapes(self) -> bool: return self.is_checked(self.blend_shapes_cb) def add_skin_cluster(self) -> bool: return self.is_checked(self.skin_cb) def add_rig_logic(self) -> bool: return self.is_checked(self.rig_logic_cb) def add_ctrl_attributes_on_root_joint(self) -> bool: 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.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_to_blend_shape_channel_name_cb) def add_key_frames(self) -> bool: return self.is_checked(self.key_frames_cb) def is_checked(self, checkbox: QCheckBox) -> bool: return ( checkbox is not None and bool(checkbox.isEnabled()) and checkbox.checkState() == Qt.CheckState.Checked ) def create_body(self) -> QVBoxLayout: """Create body layout""" self.body = QVBoxLayout() self.body.setContentsMargins( MARGIN_BODY_LEFT, MARGIN_BODY_TOP, MARGIN_BODY_RIGHT, 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 = 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: 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, MARGIN_HEADER_RIGHT, MARGIN_HEADER_BOTTOM, ) self.header.setSpacing(SPACING) return self.header def create_help_btn(self) -> QWidget: btn = QPushButton(self) btn.setText(" ? ") btn.setToolTip("Help") btn.clicked.connect(self.on_help) return btn def on_help(self) -> None: if TOOL_HELP_URL: webbrowser.open(TOOL_HELP_URL) else: QMessageBox.about( self, "About", "Sorry, this application does not have documentation yet.", ) def create_dna_selector(self) -> QWidget: widget = QWidget() self.select_dna_path = self.create_dna_chooser() self.load_dna_btn = self.create_load_dna_button(self.select_dna_path) self.select_dna_path.fc_text_field.textChanged.connect( lambda: self.on_dna_selected(self.select_dna_path) ) 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, ) widget.setLayout(layout) self.body.addWidget(widget) return widget def on_dna_selected(self, input: FileChooser) -> None: 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: return self.create_file_chooser( "DNA File:", "DNA file to load. Required by all gui elements", "Select a DNA file", "DNA files (*.dna)", self.on_dna_changed, ) def on_dna_changed(self, state: int) -> None: # pylint: disable=unused-argument enabled = False if self.dna: if self.dna.path == self.select_dna_path.get_file_path(): enabled = True self.load_dna_btn.setEnabled(enabled) self.mesh_tree_list.btn_select_all.setEnabled(enabled) self.mesh_tree_list.btn_deselect_all.setEnabled(enabled) self.process_btn.setEnabled(enabled) def create_load_dna_button(self, dna_input: FileChooser) -> QWidget: 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: self.main_widget.setEnabled(False) QCoreApplication.processEvents() try: dna_file_path = input.get_file_path() if dna_file_path: self.dna = DNA(dna_file_path, [Layer.definition]) lod_count = self.dna.get_lod_count() names = self.get_mesh_names() indices_names = self.get_lod_indices_names() self.mesh_tree_list.fill_mesh_list(lod_count, names, indices_names) self.joints_cb.setEnabled(True) self.enable_additional_build_options(True) self.process_btn.setEnabled(False) self.mesh_tree_list.btn_select_all.setEnabled(True) self.mesh_tree_list.btn_deselect_all.setEnabled(True) except Exception as e: dlg = QMessageBox() dlg.setIcon(QMessageBox.Warning) dlg.setWindowTitle("Error") dlg.setText(str(e)) dlg.setStandardButtons(QMessageBox.Ok) dlg.exec_() self.main_widget.setEnabled(True) def get_mesh_names(self) -> List[str]: 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]]: 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: widget = MeshTreeList(self) self.body.addWidget(widget) return widget def create_file_chooser( self, label: str, hint: str, caption: str, filter: str, on_changed: Callable[[int], None] = None, ) -> FileChooser: widget = FileChooser( label, hint, self, dialog_caption=caption, dialog_filter=filter, on_changed=on_changed or self.on_generic_changed, ) self.body.addWidget(widget) return widget def create_gui_selector(self) -> FileChooser: return self.create_file_chooser( "Gui path:", "GUI file to load. Required by RigLogic", "Select the gui file", "gui files (*.ma)", ) def create_aas_selector(self) -> FileChooser: 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)", ) def create_build_options(self) -> QWidget: widget = QWidget() layout = QVBoxLayout(widget) layout.setContentsMargins( MARGIN_BODY_LEFT, MARGIN_BODY_TOP, MARGIN_BODY_RIGHT, MARGIN_BOTTOM, ) self.joints_cb = self.create_checkbox( "joints", "Add joints to rig. Requires: DNA to be loaded", layout, self.on_joints_changed, ) self.blend_shapes_cb = self.create_checkbox( "blend shapes", "Add blend shapes to rig. Requires: DNA to be loaded and at least one mesh to be check", layout, self.on_generic_changed, ) self.skin_cb = self.create_checkbox( "skin cluster", "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 RigLogic to rig. Requires: DNA to be loaded, all meshes to be checked, joints, skin, blend shapes to be checked, also gui, analog gui and additional assemble script must be set", layout, ) layout.addStretch() widget.setMaximumHeight(150) return widget def create_extra_build_options(self) -> QWidget: widget = QWidget() layout = QVBoxLayout(widget) layout.setContentsMargins( MARGIN_BODY_LEFT, MARGIN_BODY_TOP, MARGIN_BODY_RIGHT, MARGIN_BOTTOM, ) 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.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_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_frames_cb = self.create_checkbox( "key frames", "Add keyframes to rig", layout, enabled=True, checked=True, ) layout.addStretch() widget.setMaximumHeight(150) return widget def enable_additional_build_options(self, enable: bool) -> None: 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, label: str, hint: str, layout: QHBoxLayout, on_changed: Callable[[int], None] = None, checked: bool = False, enabled: bool = False, ) -> QCheckBox: checkbox = QCheckBox(label, self) checkbox.setChecked(checked) checkbox.setEnabled(enabled) checkbox.setToolTip(hint) if on_changed: checkbox.stateChanged.connect(on_changed) layout.addWidget(checkbox) return checkbox def on_joints_changed(self, state: int) -> None: if self.joints_cb.isChecked(): self.process_btn.setEnabled(True) if self.mesh_tree_list.get_selected_meshes(): self.skin_cb.setEnabled(True) else: self.skin_cb.setEnabled(False) if not self.mesh_tree_list.get_selected_meshes(): self.process_btn.setEnabled(False) self.on_generic_changed(state) def create_process_btn(self) -> QPushButton: btn = QPushButton("Process") btn.setEnabled(False) btn.clicked.connect(self.process) self.body.addWidget(btn) return btn def create_progress_bar(self) -> QProgressBar: progress = QProgressBar(self) progress.setRange(0, 100) progress.setValue(0) progress.setTextVisible(True) progress.setFormat("") self.body.addWidget(progress) return progress def on_generic_changed(self, state: int) -> None: # pylint: disable=unused-argument self.set_riglogic_cb_enabled() def is_enabled_and_checked(self, check_box: QCheckBox) -> bool: return ( check_box is not None and bool(check_box.isEnabled()) and bool(check_box.isChecked()) ) def set_riglogic_cb_enabled(self) -> None: 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(): all_total_meshes = True enabled = ( self.is_enabled_and_checked(self.joints_cb) and self.is_enabled_and_checked(self.blend_shapes_cb) and all_total_meshes and self.is_enabled_and_checked(self.skin_cb) ) self.rig_logic_cb.setEnabled(enabled)