MetaFusion/scripts/DNA_Viewer/ui/app.py
2025-01-14 00:17:20 +08:00

1010 lines
35 KiB
Python

#!/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)