1034 lines
35 KiB
Python
1034 lines
35 KiB
Python
<<<<<<< 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) |