MetaFusion/scripts/dna_viewer/ui/app.py
2025-01-13 23:36:50 +08:00

1034 lines
35 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<<<<<<< Updated upstream
=======
#!/usr/bin/env python
# -*- coding: utf-8 -*-
>>>>>>> Stashed changes
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()
<<<<<<< Updated upstream
TOOL_NAME = "Delos"
WINDOW_TITLE = "DNA Viewer"
WINDOW_OBJECT = "dnaviewer"
TOOL_HELP_URL = f"http://10.72.61.59:3000/ArtGroup/{TOOL_NAME}/wiki"
=======
TOOL_NAME = "DNA Viewer"
WINDOW_TITLE = "DNA Viewer"
WINDOW_OBJECT = "dnaviewer"
TOOL_HELP_URL = f"https://gitea.cgnico.com/CGNICO/MetaFusion/wiki"
>>>>>>> Stashed changes
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 image path
default_preview = os.path.join(self.img_path, "Preview.png").replace("\\", "/")
for file in os.listdir(self.dna_path):
if file.endswith('.dna'):
name = os.path.splitext(file)[0]
dna_file = os.path.join(self.dna_path, file).replace("\\", "/")
# Find corresponding image, if not found use default image
img_file = None
for ext in ['.jpg', '.png', '.jpeg']:
img_path = os.path.join(self.img_path, f"{name}{ext}").replace("\\", "/")
if os.path.exists(img_path):
img_file = img_path
break
<<<<<<< Updated upstream
# If no corresponding image is found, use default preview image
=======
>>>>>>> Stashed changes
if not img_file and os.path.exists(default_preview):
img_file = default_preview
self.dna_files[name] = {
'dna_path': dna_file,
'img_path': img_file
}
def update_grid(self):
"""Update DNA grid"""
for i in reversed(range(self.flow_layout.count())):
self.flow_layout.itemAt(i).widget().deleteLater()
for name, info in self.dna_files.items():
dna_btn = self.create_dna_button(name, info)
self.flow_layout.addWidget(dna_btn)
def create_dna_button(self, name, info):
"""Create DNA button"""
btn = QPushButton()
btn.setFixedSize(180, 120)
layout = QVBoxLayout(btn)
layout.setContentsMargins(4, 4, 4, 4)
# Icon label
icon_label = QLabel()
icon_label.setAlignment(Qt.AlignCenter)
pixmap = QtGui.QPixmap(info['img_path'])
scaled_pixmap = pixmap.scaled(90, 90, Qt.KeepAspectRatio, Qt.SmoothTransformation)
icon_label.setPixmap(scaled_pixmap)
# Text label
text_label = QLabel(name)
text_label.setAlignment(Qt.AlignCenter)
text_label.setStyleSheet("color: #FFFFFF;")
layout.addWidget(icon_label)
layout.addWidget(text_label)
btn.setStyleSheet("""
QPushButton {
background-color: #303030;
border: 2px solid #202020;
border-radius: 6px;
}
QPushButton:hover {
background-color: #404040;
border: 2px solid #303030;
}
""")
btn.clicked.connect(lambda: self.dna_selected.emit(info['dna_path']))
return btn
class MeshTreeList(QWidget):
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)