diff --git a/dust3d.pro b/dust3d.pro index 18dd69fe..f7121bc1 100644 --- a/dust3d.pro +++ b/dust3d.pro @@ -179,8 +179,20 @@ HEADERS += src/posemeshcreator.h SOURCES += src/posepreviewmanager.cpp HEADERS += src/posepreviewmanager.h -SOURCES += src/tetrapodposeeditwidget.cpp -HEADERS += src/tetrapodposeeditwidget.h +SOURCES += src/poseeditwidget.cpp +HEADERS += src/poseeditwidget.h + +SOURCES += src/poselistwidget.cpp +HEADERS += src/poselistwidget.h + +SOURCES += src/posemanagewidget.cpp +HEADERS += src/posemanagewidget.h + +SOURCES += src/posepreviewsgenerator.cpp +HEADERS += src/posepreviewsgenerator.h + +SOURCES += src/posewidget.cpp +HEADERS += src/posewidget.h SOURCES += src/main.cpp diff --git a/src/aboutwidget.cpp b/src/aboutwidget.cpp index a2c80fe0..22babb8d 100644 --- a/src/aboutwidget.cpp +++ b/src/aboutwidget.cpp @@ -3,6 +3,7 @@ #include #include "aboutwidget.h" #include "version.h" +#include "dust3dutil.h" AboutWidget::AboutWidget() { @@ -16,5 +17,5 @@ AboutWidget::AboutWidget() setLayout(mainLayout); setFixedSize(QSize(350, 75)); - setWindowTitle(APP_NAME); + setWindowTitle(unifiedWindowTitle(tr("About"))); } diff --git a/src/aboutwidget.h b/src/aboutwidget.h index b81c54a6..5524f7f7 100644 --- a/src/aboutwidget.h +++ b/src/aboutwidget.h @@ -1,8 +1,8 @@ #ifndef ABOUT_WIDGET_H #define ABOUT_WIDGET_H -#include +#include -class AboutWidget : public QWidget +class AboutWidget : public QDialog { Q_OBJECT public: diff --git a/src/dust3dutil.cpp b/src/dust3dutil.cpp index f76ee467..664ae3cb 100644 --- a/src/dust3dutil.cpp +++ b/src/dust3dutil.cpp @@ -1,5 +1,6 @@ #include #include "dust3dutil.h" +#include "version.h" QString valueOfKeyInMapOrEmpty(const std::map &map, const QString &key) { @@ -51,3 +52,7 @@ QVector3D projectLineOnPlane(QVector3D line, QVector3D planeNormal) return line - verticalOffset; } +QString unifiedWindowTitle(const QString &text) +{ + return text + QObject::tr(" - ") + APP_NAME; +} diff --git a/src/dust3dutil.h b/src/dust3dutil.h index 1920c13f..f9d3792c 100644 --- a/src/dust3dutil.h +++ b/src/dust3dutil.h @@ -17,5 +17,6 @@ void qNormalizeAngle(int &angle); QVector3D pointInHermiteCurve(float t, QVector3D p0, QVector3D m0, QVector3D p1, QVector3D m1); float angleInRangle360BetweenTwoVectors(QVector3D a, QVector3D b, QVector3D planeNormal); QVector3D projectLineOnPlane(QVector3D line, QVector3D planeNormal); +QString unifiedWindowTitle(const QString &text); #endif diff --git a/src/exportpreviewwidget.cpp b/src/exportpreviewwidget.cpp index 8bf369d8..5ad41448 100644 --- a/src/exportpreviewwidget.cpp +++ b/src/exportpreviewwidget.cpp @@ -7,9 +7,10 @@ #include "aboutwidget.h" #include "version.h" #include "theme.h" +#include "dust3dutil.h" ExportPreviewWidget::ExportPreviewWidget(SkeletonDocument *document, QWidget *parent) : - QWidget(parent), + QDialog(parent), m_document(document), m_previewLabel(nullptr), m_spinnerWidget(nullptr) @@ -33,6 +34,7 @@ ExportPreviewWidget::ExportPreviewWidget(SkeletonDocument *document, QWidget *pa m_saveButton = new QPushButton(tr("Save")); connect(m_saveButton, &QPushButton::clicked, this, &ExportPreviewWidget::save); m_saveButton->hide(); + m_saveButton->setDefault(true); QComboBox *exportFormatSelectBox = new QComboBox; exportFormatSelectBox->addItem(tr("glTF")); @@ -59,11 +61,7 @@ ExportPreviewWidget::ExportPreviewWidget(SkeletonDocument *document, QWidget *pa renderLayout->setContentsMargins(20, 0, 20, 0); renderLayout->addWidget(m_textureRenderWidget); - QWidget *hrLightWidget = new QWidget; - hrLightWidget->setFixedHeight(1); - hrLightWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - hrLightWidget->setStyleSheet(QString("background-color: #565656;")); - hrLightWidget->setContentsMargins(0, 0, 0, 0); + QWidget *hrLightWidget = Theme::createHorizontalLineWidget(); QHBoxLayout *topLayout = new QHBoxLayout; topLayout->setSpacing(0); @@ -86,7 +84,7 @@ ExportPreviewWidget::ExportPreviewWidget(SkeletonDocument *document, QWidget *pa m_spinnerWidget->setNumberOfLines(12); m_spinnerWidget->hide(); - setWindowTitle(tr("Export") + tr(" - ") + APP_NAME); + setWindowTitle(unifiedWindowTitle(tr("Export"))); emit updateTexturePreview(); } diff --git a/src/exportpreviewwidget.h b/src/exportpreviewwidget.h index 366b884c..55fa94de 100644 --- a/src/exportpreviewwidget.h +++ b/src/exportpreviewwidget.h @@ -1,6 +1,6 @@ #ifndef EXPORT_PREVIEW_WIDGET_H #define EXPORT_PREVIEW_WIDGET_H -#include +#include #include #include #include @@ -9,7 +9,7 @@ #include "waitingspinnerwidget.h" #include "skeletondocument.h" -class ExportPreviewWidget : public QWidget +class ExportPreviewWidget : public QDialog { Q_OBJECT signals: diff --git a/src/logbrowserdialog.cpp b/src/logbrowserdialog.cpp index 4559c502..ae2a1ca6 100644 --- a/src/logbrowserdialog.cpp +++ b/src/logbrowserdialog.cpp @@ -12,6 +12,7 @@ #include #include #include "version.h" +#include "dust3dutil.h" LogBrowserDialog::LogBrowserDialog(QWidget *parent) : QDialog(parent) @@ -40,7 +41,7 @@ LogBrowserDialog::LogBrowserDialog(QWidget *parent) : resize(400, 300); - setWindowTitle(tr("Debug") + tr(" - ") + APP_NAME); + setWindowTitle(unifiedWindowTitle(tr("Debug"))); hide(); } diff --git a/src/meshgenerator.cpp b/src/meshgenerator.cpp index d8aa70a0..8b876f4b 100644 --- a/src/meshgenerator.cpp +++ b/src/meshgenerator.cpp @@ -434,7 +434,7 @@ void *MeshGenerator::combineComponentMesh(QString componentId, bool *inverse) auto findCachedMesh = m_cacheContext->componentCombinableMeshs.find(componentId); if (findCachedMesh != m_cacheContext->componentCombinableMeshs.end() && nullptr != findCachedMesh->second) { - qDebug() << "Component mesh cache used:" << componentId; + //qDebug() << "Component mesh cache used:" << componentId; return cloneCombinableMesh(findCachedMesh->second); } } @@ -519,7 +519,7 @@ void *MeshGenerator::combineComponentMesh(QString componentId, bool *inverse) } } if (seamVerticesNum > 0) { - qDebug() << "smoothSeamFactor:" << smoothSeamFactor << "seamVerticesIndicies.size():" << seamVerticesNum; + //qDebug() << "smoothSeamFactor:" << smoothSeamFactor << "seamVerticesIndicies.size():" << seamVerticesNum; meshlite_smooth_vertices(m_meshliteContext, meshIdForSmooth, smoothSeamFactor, seamVerticesIndicies, seamVerticesNum); } delete[] seamVerticesIndicies; diff --git a/src/tetrapodposeeditwidget.cpp b/src/poseeditwidget.cpp similarity index 58% rename from src/tetrapodposeeditwidget.cpp rename to src/poseeditwidget.cpp index 74f3acc7..0617f3ed 100644 --- a/src/tetrapodposeeditwidget.cpp +++ b/src/poseeditwidget.cpp @@ -1,21 +1,32 @@ #include +#include #include #include #include #include +#include +#include #include "theme.h" -#include "tetrapodposeeditwidget.h" +#include "poseeditwidget.h" #include "floatnumberwidget.h" +#include "version.h" -TetrapodPoseEditWidget::TetrapodPoseEditWidget(const SkeletonDocument *document, QWidget *parent) : - QWidget(parent), +PoseEditWidget::PoseEditWidget(const SkeletonDocument *document, QWidget *parent) : + QDialog(parent), m_document(document) { m_posePreviewManager = new PosePreviewManager(); connect(m_posePreviewManager, &PosePreviewManager::renderDone, [=]() { + if (m_closed) { + close(); + return; + } if (m_isPreviewDirty) updatePreview(); }); + connect(m_posePreviewManager, &PosePreviewManager::resultPreviewMeshChanged, [=]() { + m_previewWidget->updateMesh(m_posePreviewManager->takeResultPreviewMesh()); + }); std::map> buttons; buttons["Head"] = std::make_tuple(new QPushButton(tr("Head")), PopupWidgetType::PitchYawRoll); @@ -61,36 +72,117 @@ TetrapodPoseEditWidget::TetrapodPoseEditWidget(const SkeletonDocument *document, QFont buttonFont; buttonFont.setWeight(QFont::Light); - buttonFont.setPixelSize(7); + buttonFont.setPixelSize(9); buttonFont.setBold(false); for (const auto &item: buttons) { QString boneName = item.first; QPushButton *buttonWidget = std::get<0>(item.second); PopupWidgetType widgetType = std::get<1>(item.second); buttonWidget->setFont(buttonFont); - buttonWidget->setMaximumWidth(45); + buttonWidget->setMaximumWidth(55); connect(buttonWidget, &QPushButton::clicked, [this, boneName, widgetType]() { emit showPopupAngleDialog(boneName, widgetType, mapFromGlobal(QCursor::pos())); }); } - QVBoxLayout *layout = new QVBoxLayout; - layout->addLayout(marksContainerLayout); - layout->addLayout(lowerMarksContainerLayout); - layout->addStretch(); + m_previewWidget = new ModelWidget(this); + m_previewWidget->setMinimumSize(128, 128); + m_previewWidget->resize(384, 384); + m_previewWidget->move(-64, -64+22); - setLayout(layout); + QVBoxLayout *markButtonsLayout = new QVBoxLayout; + markButtonsLayout->addStretch(); + markButtonsLayout->addLayout(marksContainerLayout); + markButtonsLayout->addLayout(lowerMarksContainerLayout); + markButtonsLayout->addStretch(); - connect(m_document, &SkeletonDocument::resultRigChanged, this, &TetrapodPoseEditWidget::updatePreview); + QHBoxLayout *paramtersLayout = new QHBoxLayout; + paramtersLayout->setContentsMargins(256, 0, 0, 0); + paramtersLayout->addStretch(); + paramtersLayout->addLayout(markButtonsLayout); + paramtersLayout->addSpacing(20); + + m_nameEdit = new QLineEdit; + connect(m_nameEdit, &QLineEdit::textChanged, this, [=]() { + m_unsaved = true; + updateTitle(); + }); + QPushButton *saveButton = new QPushButton(tr("Save")); + connect(saveButton, &QPushButton::clicked, this, &PoseEditWidget::save); + saveButton->setDefault(true); + + QHBoxLayout *baseInfoLayout = new QHBoxLayout; + baseInfoLayout->addWidget(new QLabel(tr("Name"))); + baseInfoLayout->addWidget(m_nameEdit); + baseInfoLayout->addStretch(); + baseInfoLayout->addWidget(saveButton); + + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->addLayout(paramtersLayout); + mainLayout->addWidget(Theme::createHorizontalLineWidget()); + mainLayout->addLayout(baseInfoLayout); + + setLayout(mainLayout); + + connect(m_document, &SkeletonDocument::resultRigChanged, this, &PoseEditWidget::updatePreview); + connect(this, &PoseEditWidget::parametersAdjusted, this, &PoseEditWidget::updatePreview); + connect(this, &PoseEditWidget::parametersAdjusted, [=]() { + m_unsaved = true; + updateTitle(); + }); + connect(this, &PoseEditWidget::addPose, m_document, &SkeletonDocument::addPose); + connect(this, &PoseEditWidget::renamePose, m_document, &SkeletonDocument::renamePose); + connect(this, &PoseEditWidget::setPoseParameters, m_document, &SkeletonDocument::setPoseParameters); + + updatePreview(); + updateTitle(); } -TetrapodPoseEditWidget::~TetrapodPoseEditWidget() +void PoseEditWidget::reject() +{ + close(); +} + +void PoseEditWidget::closeEvent(QCloseEvent *event) +{ + if (m_unsaved && !m_closed) { + QMessageBox::StandardButton answer = QMessageBox::question(this, + APP_NAME, + tr("Do you really want to close while there are unsaved changes?"), + QMessageBox::Yes | QMessageBox::No); + if (answer != QMessageBox::Yes) { + event->ignore(); + return; + } + } + m_closed = true; + hide(); + if (m_openedMenuCount > 0) { + event->ignore(); + return; + } + if (m_posePreviewManager->isRendering()) { + event->ignore(); + return; + } + event->accept(); +} + +QSize PoseEditWidget::sizeHint() const +{ + return QSize(0, 350); +} + +PoseEditWidget::~PoseEditWidget() { delete m_posePreviewManager; } -void TetrapodPoseEditWidget::updatePreview() +void PoseEditWidget::updatePreview() { + if (m_closed) + return; + if (m_posePreviewManager->isRendering()) { m_isPreviewDirty = true; return; @@ -105,14 +197,37 @@ void TetrapodPoseEditWidget::updatePreview() return; } - delete m_poser; - m_poser = new TetrapodPoser(*rigBones); - m_poser->parameters() = m_parameters; - m_poser->commit(); - m_posePreviewManager->postUpdate(*m_poser, m_document->currentRiggedResultContext(), *rigWeights); + TetrapodPoser *poser = new TetrapodPoser(*rigBones); + poser->parameters() = m_parameters; + poser->commit(); + m_posePreviewManager->postUpdate(*poser, m_document->currentRiggedResultContext(), *rigWeights); + delete poser; } -void TetrapodPoseEditWidget::showPopupAngleDialog(QString boneName, PopupWidgetType popupWidgetType, QPoint pos) +void PoseEditWidget::setEditPoseId(QUuid poseId) +{ + if (m_poseId == poseId) + return; + + m_poseId = poseId; + updateTitle(); +} + +void PoseEditWidget::updateTitle() +{ + if (m_poseId.isNull()) { + setWindowTitle(unifiedWindowTitle(tr("New") + (m_unsaved ? "*" : ""))); + return; + } + const SkeletonPose *pose = m_document->findPose(m_poseId); + if (nullptr == pose) { + qDebug() << "Find pose failed:" << m_poseId; + return; + } + setWindowTitle(unifiedWindowTitle(pose->name + (m_unsaved ? "*" : ""))); +} + +void PoseEditWidget::showPopupAngleDialog(QString boneName, PopupWidgetType popupWidgetType, QPoint pos) { QMenu popupMenu; @@ -125,13 +240,13 @@ void TetrapodPoseEditWidget::showPopupAngleDialog(QString boneName, PopupWidgetT pitchWidget->setItemName(tr("Pitch")); pitchWidget->setRange(-180, 180); pitchWidget->setValue(valueOfKeyInMapOrEmpty(m_parameters[boneName], "pitch").toFloat()); - connect(pitchWidget, &FloatNumberWidget::valueChanged, [=](float value) { + connect(pitchWidget, &FloatNumberWidget::valueChanged, this, [=](float value) { m_parameters[boneName]["pitch"] = QString::number(value); - updatePreview(); + emit parametersAdjusted(); }); QPushButton *pitchEraser = new QPushButton(QChar(fa::eraser)); Theme::initAwesomeMiniButton(pitchEraser); - connect(pitchEraser, &QPushButton::clicked, [=]() { + connect(pitchEraser, &QPushButton::clicked, this, [=]() { pitchWidget->setValue(0.0); }); QHBoxLayout *pitchLayout = new QHBoxLayout; @@ -143,13 +258,13 @@ void TetrapodPoseEditWidget::showPopupAngleDialog(QString boneName, PopupWidgetT yawWidget->setItemName(tr("Yaw")); yawWidget->setRange(-180, 180); yawWidget->setValue(valueOfKeyInMapOrEmpty(m_parameters[boneName], "yaw").toFloat()); - connect(yawWidget, &FloatNumberWidget::valueChanged, [=](float value) { + connect(yawWidget, &FloatNumberWidget::valueChanged, this, [=](float value) { m_parameters[boneName]["yaw"] = QString::number(value); - updatePreview(); + emit parametersAdjusted(); }); QPushButton *yawEraser = new QPushButton(QChar(fa::eraser)); Theme::initAwesomeMiniButton(yawEraser); - connect(yawEraser, &QPushButton::clicked, [=]() { + connect(yawEraser, &QPushButton::clicked, this, [=]() { yawWidget->setValue(0.0); }); QHBoxLayout *yawLayout = new QHBoxLayout; @@ -161,13 +276,13 @@ void TetrapodPoseEditWidget::showPopupAngleDialog(QString boneName, PopupWidgetT rollWidget->setItemName(tr("Roll")); rollWidget->setRange(-180, 180); rollWidget->setValue(valueOfKeyInMapOrEmpty(m_parameters[boneName], "roll").toFloat()); - connect(rollWidget, &FloatNumberWidget::valueChanged, [=](float value) { + connect(rollWidget, &FloatNumberWidget::valueChanged, this, [=](float value) { m_parameters[boneName]["roll"] = QString::number(value); - updatePreview(); + emit parametersAdjusted(); }); QPushButton *rollEraser = new QPushButton(QChar(fa::eraser)); Theme::initAwesomeMiniButton(rollEraser); - connect(rollEraser, &QPushButton::clicked, [=]() { + connect(rollEraser, &QPushButton::clicked, this, [=]() { rollWidget->setValue(0.0); }); QHBoxLayout *rollLayout = new QHBoxLayout; @@ -179,13 +294,13 @@ void TetrapodPoseEditWidget::showPopupAngleDialog(QString boneName, PopupWidgetT intersectionWidget->setItemName(tr("Intersection")); intersectionWidget->setRange(-180, 180); intersectionWidget->setValue(valueOfKeyInMapOrEmpty(m_parameters[boneName], "intersection").toFloat()); - connect(intersectionWidget, &FloatNumberWidget::valueChanged, [=](float value) { + connect(intersectionWidget, &FloatNumberWidget::valueChanged, this, [=](float value) { m_parameters[boneName]["intersection"] = QString::number(value); - updatePreview(); + emit parametersAdjusted(); }); QPushButton *intersectionEraser = new QPushButton(QChar(fa::eraser)); Theme::initAwesomeMiniButton(intersectionEraser); - connect(intersectionEraser, &QPushButton::clicked, [=]() { + connect(intersectionEraser, &QPushButton::clicked, this, [=]() { intersectionWidget->setValue(0.0); }); QHBoxLayout *intersectionLayout = new QHBoxLayout; @@ -201,5 +316,40 @@ void TetrapodPoseEditWidget::showPopupAngleDialog(QString boneName, PopupWidgetT popupMenu.addAction(&action); + m_openedMenuCount++; popupMenu.exec(mapToGlobal(pos)); + m_openedMenuCount--; + + if (m_closed) + close(); +} + +void PoseEditWidget::setEditPoseName(QString name) +{ + m_nameEdit->setText(name); + updateTitle(); +} + +void PoseEditWidget::setEditParameters(std::map> parameters) +{ + m_parameters = parameters; + updatePreview(); +} + +void PoseEditWidget::clearUnsaveState() +{ + m_unsaved = false; + updateTitle(); +} + +void PoseEditWidget::save() +{ + if (m_poseId.isNull()) { + emit addPose(m_nameEdit->text(), m_parameters); + } else if (m_unsaved) { + m_unsaved = false; + emit renamePose(m_poseId, m_nameEdit->text()); + emit setPoseParameters(m_poseId, m_parameters); + } + close(); } diff --git a/src/poseeditwidget.h b/src/poseeditwidget.h new file mode 100644 index 00000000..2b5f419d --- /dev/null +++ b/src/poseeditwidget.h @@ -0,0 +1,56 @@ +#ifndef POSE_EDIT_WIDGET_H +#define POSE_EDIT_WIDGET_H +#include +#include +#include +#include +#include "posepreviewmanager.h" +#include "tetrapodposer.h" +#include "skeletondocument.h" +#include "modelwidget.h" + +enum class PopupWidgetType +{ + PitchYawRoll, + Intersection +}; + +class PoseEditWidget : public QDialog +{ + Q_OBJECT +signals: + void addPose(QString name, std::map> parameters); + void removePose(QUuid poseId); + void setPoseParameters(QUuid poseId, std::map> parameters); + void renamePose(QUuid poseId, QString name); + void parametersAdjusted(); +public: + PoseEditWidget(const SkeletonDocument *document, QWidget *parent=nullptr); + ~PoseEditWidget(); +public slots: + void updatePreview(); + void showPopupAngleDialog(QString boneName, PopupWidgetType popupWidgetType, QPoint pos); + void setEditPoseId(QUuid poseId); + void setEditPoseName(QString name); + void setEditParameters(std::map> parameters); + void updateTitle(); + void save(); + void clearUnsaveState(); +protected: + QSize sizeHint() const override; + void closeEvent(QCloseEvent *event) override; + void reject() override; +private: + const SkeletonDocument *m_document = nullptr; + PosePreviewManager *m_posePreviewManager = nullptr; + ModelWidget *m_previewWidget = nullptr; + bool m_isPreviewDirty = false; + bool m_closed = false; + std::map> m_parameters; + size_t m_openedMenuCount = 0; + QUuid m_poseId; + bool m_unsaved = false; + QLineEdit *m_nameEdit = nullptr; +}; + +#endif diff --git a/src/poselistwidget.cpp b/src/poselistwidget.cpp new file mode 100644 index 00000000..39ef14b7 --- /dev/null +++ b/src/poselistwidget.cpp @@ -0,0 +1,316 @@ +#include +#include +#include +#include +#include +#include "skeletonxml.h" +#include "poselistwidget.h" + +PoseListWidget::PoseListWidget(const SkeletonDocument *document, QWidget *parent) : + QTreeWidget(parent), + m_document(document) +{ + setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + setFocusPolicy(Qt::NoFocus); + setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + + setAutoScroll(false); + + setHeaderHidden(true); + + QPalette palette = this->palette(); + palette.setColor(QPalette::Window, Qt::transparent); + palette.setColor(QPalette::Base, Qt::transparent); + setPalette(palette); + + setStyleSheet("QTreeView {qproperty-indentation: 0;}"); + + setContentsMargins(0, 0, 0, 0); + + connect(document, &SkeletonDocument::poseListChanged, this, &PoseListWidget::reload); + connect(document, &SkeletonDocument::cleanup, this, &PoseListWidget::removeAllContent); + + connect(this, &PoseListWidget::removePose, document, &SkeletonDocument::removePose); + + setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, &QTreeWidget::customContextMenuRequested, this, &PoseListWidget::showContextMenu); + + reload(); +} + +void PoseListWidget::poseRemoved(QUuid poseId) +{ + if (m_currentSelectedPoseId == poseId) + m_currentSelectedPoseId = QUuid(); + m_selectedPoseIds.erase(poseId); + m_itemMap.erase(poseId); +} + +void PoseListWidget::updatePoseSelectState(QUuid poseId, bool selected) +{ + auto findItemResult = m_itemMap.find(poseId); + if (findItemResult == m_itemMap.end()) { + qDebug() << "Find pose item failed:" << poseId; + return; + } + PoseWidget *poseWidget = (PoseWidget *)itemWidget(findItemResult->second.first, findItemResult->second.second); + poseWidget->updateCheckedState(selected); +} + +void PoseListWidget::selectPose(QUuid poseId, bool multiple) +{ + if (multiple) { + if (!m_currentSelectedPoseId.isNull()) { + m_selectedPoseIds.insert(m_currentSelectedPoseId); + m_currentSelectedPoseId = QUuid(); + } + if (m_selectedPoseIds.find(poseId) != m_selectedPoseIds.end()) { + updatePoseSelectState(poseId, false); + m_selectedPoseIds.erase(poseId); + } else { + updatePoseSelectState(poseId, true); + m_selectedPoseIds.insert(poseId); + } + if (m_selectedPoseIds.size() > 1) { + return; + } + if (m_selectedPoseIds.size() == 1) + poseId = *m_selectedPoseIds.begin(); + else + poseId = QUuid(); + } + if (!m_selectedPoseIds.empty()) { + for (const auto &id: m_selectedPoseIds) { + updatePoseSelectState(id, false); + } + m_selectedPoseIds.clear(); + } + if (m_currentSelectedPoseId != poseId) { + if (!m_currentSelectedPoseId.isNull()) { + updatePoseSelectState(m_currentSelectedPoseId, false); + } + m_currentSelectedPoseId = poseId; + if (!m_currentSelectedPoseId.isNull()) { + updatePoseSelectState(m_currentSelectedPoseId, true); + } + } +} + +void PoseListWidget::mousePressEvent(QMouseEvent *event) +{ + QModelIndex itemIndex = indexAt(event->pos()); + QTreeView::mousePressEvent(event); + if (event->button() == Qt::LeftButton) { + bool multiple = QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ControlModifier); + if (itemIndex.isValid()) { + QTreeWidgetItem *item = itemFromIndex(itemIndex); + auto poseId = QUuid(item->data(itemIndex.column(), Qt::UserRole).toString()); + if (QGuiApplication::queryKeyboardModifiers().testFlag(Qt::ShiftModifier)) { + bool startAdd = false; + bool stopAdd = false; + std::vector waitQueue; + for (const auto &childId: m_document->poseIdList) { + if (m_shiftStartPoseId == childId || poseId == childId) { + if (startAdd) { + stopAdd = true; + } else { + startAdd = true; + } + } + if (startAdd) + waitQueue.push_back(childId); + if (stopAdd) + break; + } + if (stopAdd && !waitQueue.empty()) { + if (!m_selectedPoseIds.empty()) { + for (const auto &id: m_selectedPoseIds) { + updatePoseSelectState(id, false); + } + m_selectedPoseIds.clear(); + } + if (!m_currentSelectedPoseId.isNull()) { + m_currentSelectedPoseId = QUuid(); + } + for (const auto &waitId: waitQueue) { + selectPose(waitId, true); + } + } + return; + } else { + m_shiftStartPoseId = poseId; + } + selectPose(poseId, multiple); + return; + } + if (!multiple) + selectPose(QUuid()); + } +} + +bool PoseListWidget::isPoseSelected(QUuid poseId) +{ + return (m_currentSelectedPoseId == poseId || + m_selectedPoseIds.find(poseId) != m_selectedPoseIds.end()); +} + +void PoseListWidget::showContextMenu(const QPoint &pos) +{ + QMenu contextMenu(this); + + std::set unorderedPoseIds = m_selectedPoseIds; + if (!m_currentSelectedPoseId.isNull()) + unorderedPoseIds.insert(m_currentSelectedPoseId); + + std::vector poseIds; + for (const auto &cand: m_document->poseIdList) { + if (unorderedPoseIds.find(cand) != unorderedPoseIds.end()) + poseIds.push_back(cand); + } + + QAction modifyAction(tr("Modify"), this); + if (poseIds.size() == 1) { + connect(&modifyAction, &QAction::triggered, this, [=]() { + emit modifyPose(*poseIds.begin()); + }); + contextMenu.addAction(&modifyAction); + } + + QAction copyAction(tr("Copy"), this); + if (!poseIds.empty()) { + connect(©Action, &QAction::triggered, this, &PoseListWidget::copy); + contextMenu.addAction(©Action); + } + + QAction pasteAction(tr("Paste"), this); + if (m_document->hasPastablePosesInClipboard()) { + connect(&pasteAction, &QAction::triggered, m_document, &SkeletonDocument::paste); + contextMenu.addAction(&pasteAction); + } + + QAction deleteAction(tr("Delete"), this); + if (!poseIds.empty()) { + connect(&deleteAction, &QAction::triggered, [=]() { + for (const auto &poseId: poseIds) + emit removePose(poseId); + }); + contextMenu.addAction(&deleteAction); + } + + contextMenu.exec(mapToGlobal(pos)); +} + +void PoseListWidget::resizeEvent(QResizeEvent *event) +{ + QTreeWidget::resizeEvent(event); + if (calculateColumnCount() != columnCount()) + reload(); +} + +int PoseListWidget::calculateColumnCount() +{ + if (nullptr == parentWidget()) + return 0; + + int columns = parentWidget()->width() / Theme::posePreviewImageSize; + if (0 == columns) + columns = 1; + return columns; +} + +void PoseListWidget::reload() +{ + removeAllContent(); + + int columns = calculateColumnCount(); + if (0 == columns) + return; + + int columnWidth = parentWidget()->width() / columns; + + //qDebug() << "parentWidth:" << parentWidget()->width() << "columnWidth:" << columnWidth << "columns:" << columns; + + setColumnCount(columns); + for (int i = 0; i < columns; i++) + setColumnWidth(i, columnWidth); + + decltype(m_document->poseIdList.size()) poseIndex = 0; + while (poseIndex < m_document->poseIdList.size()) { + QTreeWidgetItem *item = new QTreeWidgetItem(this); + item->setFlags((item->flags() | Qt::ItemIsEditable | Qt::ItemIsEnabled) & ~(Qt::ItemIsSelectable)); + for (int col = 0; col < columns && poseIndex < m_document->poseIdList.size(); col++, poseIndex++) { + const auto &poseId = m_document->poseIdList[poseIndex]; + item->setSizeHint(col, QSize(columnWidth, PoseWidget::preferredHeight() + 2)); + item->setData(col, Qt::UserRole, poseId.toString()); + PoseWidget *widget = new PoseWidget(m_document, poseId); + connect(widget, &PoseWidget::modifyPose, this, &PoseListWidget::modifyPose); + widget->previewWidget()->setGraphicsFunctions(this); + setItemWidget(item, col, widget); + widget->reload(); + widget->updateCheckedState(isPoseSelected(poseId)); + m_itemMap[poseId] = std::make_pair(item, col); + } + invisibleRootItem()->addChild(item); + } +} + +void PoseListWidget::removeAllContent() +{ + m_itemMap.clear(); + clear(); +} + +bool PoseListWidget::mouseMove(QMouseEvent *event) +{ + return false; +} + +bool PoseListWidget::wheel(QWheelEvent *event) +{ + return false; +} + +bool PoseListWidget::mouseRelease(QMouseEvent *event) +{ + return false; +} + +bool PoseListWidget::mousePress(QMouseEvent *event) +{ + if (event->button() == Qt::RightButton) { + showContextMenu(mapFromGlobal(event->globalPos())); + return false; + } + return false; +} + +bool PoseListWidget::mouseDoubleClick(QMouseEvent *event) +{ + return false; +} + +bool PoseListWidget::keyPress(QKeyEvent *event) +{ + return false; +} + +void PoseListWidget::copy() +{ + if (m_selectedPoseIds.empty() && m_currentSelectedPoseId.isNull()) + return; + + std::set limitPoseIds = m_selectedPoseIds; + if (!m_currentSelectedPoseId.isNull()) + limitPoseIds.insert(m_currentSelectedPoseId); + + std::set emptySet; + + SkeletonSnapshot snapshot; + m_document->toSnapshot(&snapshot, emptySet, SkeletonDocumentToSnapshotFor::Poses, + limitPoseIds); + QString snapshotXml; + QXmlStreamWriter xmlStreamWriter(&snapshotXml); + saveSkeletonToXmlStream(&snapshot, &xmlStreamWriter); + QClipboard *clipboard = QApplication::clipboard(); + clipboard->setText(snapshotXml); +} diff --git a/src/poselistwidget.h b/src/poselistwidget.h new file mode 100644 index 00000000..40049182 --- /dev/null +++ b/src/poselistwidget.h @@ -0,0 +1,44 @@ +#ifndef POSE_LIST_WIDGET_H +#define POSE_LIST_WIDGET_H +#include +#include +#include "skeletondocument.h" +#include "posewidget.h" +#include "skeletongraphicswidget.h" + +class PoseListWidget : public QTreeWidget, public SkeletonGraphicsFunctions +{ + Q_OBJECT +signals: + void removePose(QUuid poseId); + void modifyPose(QUuid poseId); +public: + PoseListWidget(const SkeletonDocument *document, QWidget *parent=nullptr); + bool isPoseSelected(QUuid poseId); +public slots: + void reload(); + void removeAllContent(); + void poseRemoved(QUuid poseId); + void showContextMenu(const QPoint &pos); + void selectPose(QUuid poseId, bool multiple=false); + void copy(); +protected: + void resizeEvent(QResizeEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + bool mouseMove(QMouseEvent *event); + bool wheel(QWheelEvent *event); + bool mouseRelease(QMouseEvent *event); + bool mousePress(QMouseEvent *event); + bool mouseDoubleClick(QMouseEvent *event); + bool keyPress(QKeyEvent *event); +private: + int calculateColumnCount(); + void updatePoseSelectState(QUuid poseId, bool selected); + const SkeletonDocument *m_document = nullptr; + std::map> m_itemMap; + std::set m_selectedPoseIds; + QUuid m_currentSelectedPoseId; + QUuid m_shiftStartPoseId; +}; + +#endif diff --git a/src/posemanagewidget.cpp b/src/posemanagewidget.cpp new file mode 100644 index 00000000..a140717c --- /dev/null +++ b/src/posemanagewidget.cpp @@ -0,0 +1,62 @@ +#include +#include +#include +#include "posemanagewidget.h" +#include "theme.h" +#include "poseeditwidget.h" + +PoseManageWidget::PoseManageWidget(const SkeletonDocument *document, QWidget *parent) : + QWidget(parent), + m_document(document) +{ + QPushButton *addPoseButton = new QPushButton(Theme::awesome()->icon(fa::plus), tr("Add Pose...")); + + connect(addPoseButton, &QPushButton::clicked, this, &PoseManageWidget::showAddPoseDialog); + + QHBoxLayout *toolsLayout = new QHBoxLayout; + toolsLayout->addWidget(addPoseButton); + + m_poseListWidget = new PoseListWidget(document); + connect(m_poseListWidget, &PoseListWidget::modifyPose, this, &PoseManageWidget::showPoseDialog); + + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->addLayout(toolsLayout); + mainLayout->addWidget(m_poseListWidget); + + setLayout(mainLayout); +} + +PoseListWidget *PoseManageWidget::poseListWidget() +{ + return m_poseListWidget; +} + +QSize PoseManageWidget::sizeHint() const +{ + return QSize(Theme::sidebarPreferredWidth, 0); +} + +void PoseManageWidget::showAddPoseDialog() +{ + showPoseDialog(QUuid()); +} + +void PoseManageWidget::showPoseDialog(QUuid poseId) +{ + PoseEditWidget *poseEditWidget = new PoseEditWidget(m_document); + poseEditWidget->setAttribute(Qt::WA_DeleteOnClose); + if (!poseId.isNull()) { + const SkeletonPose *pose = m_document->findPose(poseId); + if (nullptr != pose) { + poseEditWidget->setEditPoseId(poseId); + poseEditWidget->setEditPoseName(pose->name); + poseEditWidget->setEditParameters(pose->parameters); + poseEditWidget->clearUnsaveState(); + } + } + poseEditWidget->show(); + connect(poseEditWidget, &QDialog::destroyed, [=]() { + emit unregisterDialog((QWidget *)poseEditWidget); + }); + emit registerDialog((QWidget *)poseEditWidget); +} diff --git a/src/posemanagewidget.h b/src/posemanagewidget.h new file mode 100644 index 00000000..2e27cffb --- /dev/null +++ b/src/posemanagewidget.h @@ -0,0 +1,26 @@ +#ifndef POSE_MANAGE_WIDGET_H +#define POSE_MANAGE_WIDGET_H +#include +#include "skeletondocument.h" +#include "poselistwidget.h" + +class PoseManageWidget : public QWidget +{ + Q_OBJECT +signals: + void registerDialog(QWidget *widget); + void unregisterDialog(QWidget *widget); +public: + PoseManageWidget(const SkeletonDocument *document, QWidget *parent=nullptr); + PoseListWidget *poseListWidget(); +protected: + virtual QSize sizeHint() const; +public slots: + void showAddPoseDialog(); + void showPoseDialog(QUuid poseId); +private: + const SkeletonDocument *m_document = nullptr; + PoseListWidget *m_poseListWidget = nullptr; +}; + +#endif diff --git a/src/posepreviewmanager.cpp b/src/posepreviewmanager.cpp index cb148544..245e0ee1 100644 --- a/src/posepreviewmanager.cpp +++ b/src/posepreviewmanager.cpp @@ -49,12 +49,11 @@ void PosePreviewManager::poseMeshReady() delete m_previewMesh; m_previewMesh = m_poseMeshCreator->takeResultMesh(); - emit resultPreviewMeshChanged(); - qDebug() << "Pose mesh generation done"; delete m_poseMeshCreator; m_poseMeshCreator = nullptr; + emit resultPreviewMeshChanged(); emit renderDone(); } diff --git a/src/posepreviewsgenerator.cpp b/src/posepreviewsgenerator.cpp new file mode 100644 index 00000000..ef772e65 --- /dev/null +++ b/src/posepreviewsgenerator.cpp @@ -0,0 +1,66 @@ +#include +#include +#include "posepreviewsgenerator.h" +#include "tetrapodposer.h" +#include "posemeshcreator.h" + +PosePreviewsGenerator::PosePreviewsGenerator(const std::vector *rigBones, + const std::map *rigWeights, + const MeshResultContext &meshResultContext) : + m_rigBones(*rigBones), + m_rigWeights(*rigWeights), + m_meshResultContext(new MeshResultContext(meshResultContext)) +{ +} + +PosePreviewsGenerator::~PosePreviewsGenerator() +{ + for (auto &item: m_previews) { + delete item.second; + } + delete m_meshResultContext; +} + +void PosePreviewsGenerator::addPose(QUuid poseId, const std::map> &pose) +{ + m_poses.push_back(std::make_pair(poseId, pose)); +} + +const std::set &PosePreviewsGenerator::generatedPreviewPoseIds() +{ + return m_generatedPoseIds; +} + +MeshLoader *PosePreviewsGenerator::takePreview(QUuid poseId) +{ + MeshLoader *resultMesh = m_previews[poseId]; + m_previews[poseId] = nullptr; + return resultMesh; +} + +void PosePreviewsGenerator::process() +{ + QElapsedTimer countTimeConsumed; + countTimeConsumed.start(); + + TetrapodPoser *poser = new TetrapodPoser(m_rigBones); + for (const auto &pose: m_poses) { + poser->parameters() = pose.second; + poser->commit(); + + PoseMeshCreator *poseMeshCreator = new PoseMeshCreator(*poser, *m_meshResultContext, m_rigWeights); + poseMeshCreator->createMesh(); + m_previews[pose.first] = poseMeshCreator->takeResultMesh(); + delete poseMeshCreator; + + poser->reset(); + + m_generatedPoseIds.insert(pose.first); + } + delete poser; + + qDebug() << "The pose previews generation took" << countTimeConsumed.elapsed() << "milliseconds"; + + this->moveToThread(QGuiApplication::instance()->thread()); + emit finished(); +} diff --git a/src/posepreviewsgenerator.h b/src/posepreviewsgenerator.h new file mode 100644 index 00000000..9acc2796 --- /dev/null +++ b/src/posepreviewsgenerator.h @@ -0,0 +1,35 @@ +#ifndef POSE_PREVIEWS_GENERATOR_H +#define POSE_PREVIEWS_GENERATOR_H +#include +#include +#include +#include +#include "meshloader.h" +#include "autorigger.h" +#include "meshresultcontext.h" + +class PosePreviewsGenerator : public QObject +{ + Q_OBJECT +public: + PosePreviewsGenerator(const std::vector *rigBones, + const std::map *rigWeights, + const MeshResultContext &meshResultContext); + ~PosePreviewsGenerator(); + void addPose(QUuid poseId, const std::map> &pose); + const std::set &generatedPreviewPoseIds(); + MeshLoader *takePreview(QUuid poseId); +signals: + void finished(); +public slots: + void process(); +private: + std::vector m_rigBones; + std::map m_rigWeights; + MeshResultContext *m_meshResultContext = nullptr; + std::vector>>> m_poses; + std::map m_previews; + std::set m_generatedPoseIds; +}; + +#endif diff --git a/src/poser.h b/src/poser.h index 953edbf8..42c49eb6 100644 --- a/src/poser.h +++ b/src/poser.h @@ -16,7 +16,7 @@ public: const std::vector &bones() const; const std::vector &resultNodes() const; std::map> ¶meters(); - void commit(); + virtual void commit(); void reset(); protected: std::vector m_bones; diff --git a/src/posewidget.cpp b/src/posewidget.cpp new file mode 100644 index 00000000..564408aa --- /dev/null +++ b/src/posewidget.cpp @@ -0,0 +1,93 @@ +#include +#include "posewidget.h" + +PoseWidget::PoseWidget(const SkeletonDocument *document, QUuid poseId) : + m_poseId(poseId), + m_document(document) +{ + setObjectName("PoseFrame"); + + m_previewWidget = new ModelWidget(this); + m_previewWidget->setFixedSize(Theme::posePreviewImageSize, Theme::posePreviewImageSize); + m_previewWidget->enableMove(false); + m_previewWidget->enableZoom(false); + + m_nameLabel = new QLabel; + m_nameLabel->setAlignment(Qt::AlignCenter); + m_nameLabel->setStyleSheet("background: qlineargradient(x1:0.5 y1:-15.5, x2:0.5 y2:1, stop:0 " + Theme::white.name() + ", stop:1 #252525);"); + + QFont nameFont; + nameFont.setWeight(QFont::Light); + nameFont.setPixelSize(9); + nameFont.setBold(false); + m_nameLabel->setFont(nameFont); + + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->setContentsMargins(0, 0, 0, 0); + mainLayout->addStretch(); + mainLayout->addWidget(m_nameLabel); + + setLayout(mainLayout); + + setFixedSize(Theme::posePreviewImageSize, PoseWidget::preferredHeight()); + + connect(document, &SkeletonDocument::poseNameChanged, this, &PoseWidget::updateName); + connect(document, &SkeletonDocument::posePreviewChanged, this, &PoseWidget::updatePreview); +} + +void PoseWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + m_previewWidget->move((width() - Theme::posePreviewImageSize) / 2, 0); +} + +int PoseWidget::preferredHeight() +{ + return Theme::posePreviewImageSize; +} + +void PoseWidget::reload() +{ + updatePreview(); + updateName(); +} + +void PoseWidget::updatePreview() +{ + const SkeletonPose *pose = m_document->findPose(m_poseId); + if (!pose) { + qDebug() << "Pose not found:" << m_poseId; + return; + } + MeshLoader *previewMesh = pose->takePreviewMesh(); + m_previewWidget->updateMesh(previewMesh); +} + +void PoseWidget::updateName() +{ + const SkeletonPose *pose = m_document->findPose(m_poseId); + if (!pose) { + qDebug() << "Pose not found:" << m_poseId; + return; + } + m_nameLabel->setText(pose->name); +} + +void PoseWidget::updateCheckedState(bool checked) +{ + if (checked) + setStyleSheet("#PoseFrame {border: 1px solid " + Theme::red.name() + ";}"); + else + setStyleSheet("#PoseFrame {border: 1px solid transparent;}"); +} + +ModelWidget *PoseWidget::previewWidget() +{ + return m_previewWidget; +} + +void PoseWidget::mouseDoubleClickEvent(QMouseEvent *event) +{ + QFrame::mouseDoubleClickEvent(event); + emit modifyPose(m_poseId); +} diff --git a/src/posewidget.h b/src/posewidget.h new file mode 100644 index 00000000..3ef7cb9b --- /dev/null +++ b/src/posewidget.h @@ -0,0 +1,32 @@ +#ifndef POSE_WIDGET_H +#define POSE_WIDGET_H +#include +#include +#include "skeletondocument.h" +#include "modelwidget.h" + +class PoseWidget : public QFrame +{ + Q_OBJECT +signals: + void modifyPose(QUuid poseId); +public: + PoseWidget(const SkeletonDocument *document, QUuid poseId); + static int preferredHeight(); + ModelWidget *previewWidget(); +protected: + void mouseDoubleClickEvent(QMouseEvent *event) override; +public slots: + void reload(); + void updatePreview(); + void updateName(); + void updateCheckedState(bool checked); + void resizeEvent(QResizeEvent *event) override; +private: + QUuid m_poseId; + const SkeletonDocument *m_document = nullptr; + ModelWidget *m_previewWidget = nullptr; + QLabel *m_nameLabel = nullptr; +}; + +#endif diff --git a/src/skeletondocument.cpp b/src/skeletondocument.cpp index 9e4f67bb..8a85aa94 100644 --- a/src/skeletondocument.cpp +++ b/src/skeletondocument.cpp @@ -51,7 +51,8 @@ SkeletonDocument::SkeletonDocument() : m_resultRigBones(nullptr), m_resultRigWeights(nullptr), m_isRigObsolete(false), - m_riggedResultContext(new MeshResultContext) + m_riggedResultContext(new MeshResultContext), + m_posePreviewsGenerator(nullptr) { } @@ -281,8 +282,6 @@ QUuid SkeletonDocument::createNode(float x, float y, float z, float radius, QUui nodeMap[node.id] = node; partMap[partId].nodeIds.push_back(node.id); - qDebug() << "Add node " << node.id << x << y << z; - emit nodeAdded(node.id); if (nullptr != fromNode) { @@ -306,6 +305,66 @@ QUuid SkeletonDocument::createNode(float x, float y, float z, float radius, QUui return node.id; } +void SkeletonDocument::addPose(QString name, std::map> parameters) +{ + QUuid newPoseId = QUuid::createUuid(); + auto &pose = poseMap[newPoseId]; + pose.id = newPoseId; + + pose.name = name; + pose.parameters = parameters; + pose.dirty = true; + + poseIdList.push_back(newPoseId); + + emit poseAdded(newPoseId); + emit poseListChanged(); + emit optionsChanged(); +} + +void SkeletonDocument::removePose(QUuid poseId) +{ + auto findPoseResult = poseMap.find(poseId); + if (findPoseResult == poseMap.end()) { + qDebug() << "Remove a none exist pose:" << poseId; + return; + } + poseIdList.erase(std::remove(poseIdList.begin(), poseIdList.end(), poseId), poseIdList.end()); + poseMap.erase(findPoseResult); + + emit poseListChanged(); + emit poseRemoved(poseId); + emit optionsChanged(); +} + +void SkeletonDocument::setPoseParameters(QUuid poseId, std::map> parameters) +{ + auto findPoseResult = poseMap.find(poseId); + if (findPoseResult == poseMap.end()) { + qDebug() << "Find pose failed:" << poseId; + return; + } + findPoseResult->second.parameters = parameters; + findPoseResult->second.dirty = true; + emit poseParametersChanged(poseId); + emit optionsChanged(); +} + +void SkeletonDocument::renamePose(QUuid poseId, QString name) +{ + auto findPoseResult = poseMap.find(poseId); + if (findPoseResult == poseMap.end()) { + qDebug() << "Find pose failed:" << poseId; + return; + } + if (findPoseResult->second.name == name) + return; + + findPoseResult->second.name = name; + emit poseNameChanged(poseId); + emit optionsChanged(); +} + const SkeletonEdge *SkeletonDocument::findEdgeByNodes(QUuid firstNodeId, QUuid secondNodeId) const { const SkeletonNode *firstNode = nullptr; @@ -446,6 +505,14 @@ const SkeletonComponent *SkeletonDocument::findComponent(QUuid componentId) cons return &it->second; } +const SkeletonPose *SkeletonDocument::findPose(QUuid poseId) const +{ + auto it = poseMap.find(poseId); + if (it == poseMap.end()) + return nullptr; + return &it->second; +} + void SkeletonDocument::scaleNodeByAddRadius(QUuid nodeId, float amount) { auto it = nodeMap.find(nodeId); @@ -675,128 +742,151 @@ void SkeletonDocument::markAllDirty() } } -void SkeletonDocument::toSnapshot(SkeletonSnapshot *snapshot, const std::set &limitNodeIds) const +void SkeletonDocument::toSnapshot(SkeletonSnapshot *snapshot, const std::set &limitNodeIds, + SkeletonDocumentToSnapshotFor forWhat, const std::set &limitPoseIds) const { - std::set limitPartIds; - std::set limitComponentIds; - for (const auto &nodeId: limitNodeIds) { - const SkeletonNode *node = findNode(nodeId); - if (!node) - continue; - const SkeletonPart *part = findPart(node->partId); - if (!part) - continue; - limitPartIds.insert(node->partId); - const SkeletonComponent *component = findComponent(part->componentId); - while (component) { - limitComponentIds.insert(component->id); - if (component->id.isNull()) - break; - component = findComponent(component->parentId); + if (SkeletonDocumentToSnapshotFor::Document == forWhat || + SkeletonDocumentToSnapshotFor::Nodes == forWhat) { + std::set limitPartIds; + std::set limitComponentIds; + for (const auto &nodeId: limitNodeIds) { + const SkeletonNode *node = findNode(nodeId); + if (!node) + continue; + const SkeletonPart *part = findPart(node->partId); + if (!part) + continue; + limitPartIds.insert(node->partId); + const SkeletonComponent *component = findComponent(part->componentId); + while (component) { + limitComponentIds.insert(component->id); + if (component->id.isNull()) + break; + component = findComponent(component->parentId); + } + } + for (const auto &partIt : partMap) { + if (!limitPartIds.empty() && limitPartIds.find(partIt.first) == limitPartIds.end()) + continue; + std::map part; + part["id"] = partIt.second.id.toString(); + part["visible"] = partIt.second.visible ? "true" : "false"; + part["locked"] = partIt.second.locked ? "true" : "false"; + part["subdived"] = partIt.second.subdived ? "true" : "false"; + part["disabled"] = partIt.second.disabled ? "true" : "false"; + part["xMirrored"] = partIt.second.xMirrored ? "true" : "false"; + part["zMirrored"] = partIt.second.zMirrored ? "true" : "false"; + part["rounded"] = partIt.second.rounded ? "true" : "false"; + part["wrapped"] = partIt.second.wrapped ? "true" : "false"; + part["dirty"] = partIt.second.dirty ? "true" : "false"; + if (partIt.second.hasColor) + part["color"] = partIt.second.color.name(); + if (partIt.second.deformThicknessAdjusted()) + part["deformThickness"] = QString::number(partIt.second.deformThickness); + if (partIt.second.deformWidthAdjusted()) + part["deformWidth"] = QString::number(partIt.second.deformWidth); + if (!partIt.second.name.isEmpty()) + part["name"] = partIt.second.name; + snapshot->parts[part["id"]] = part; + } + for (const auto &nodeIt: nodeMap) { + if (!limitNodeIds.empty() && limitNodeIds.find(nodeIt.first) == limitNodeIds.end()) + continue; + std::map node; + node["id"] = nodeIt.second.id.toString(); + node["radius"] = QString::number(nodeIt.second.radius); + node["x"] = QString::number(nodeIt.second.x); + node["y"] = QString::number(nodeIt.second.y); + node["z"] = QString::number(nodeIt.second.z); + node["partId"] = nodeIt.second.partId.toString(); + if (nodeIt.second.boneMark != SkeletonBoneMark::None) + node["boneMark"] = SkeletonBoneMarkToString(nodeIt.second.boneMark); + if (!nodeIt.second.name.isEmpty()) + node["name"] = nodeIt.second.name; + snapshot->nodes[node["id"]] = node; + } + for (const auto &edgeIt: edgeMap) { + if (edgeIt.second.nodeIds.size() != 2) + continue; + if (!limitNodeIds.empty() && + (limitNodeIds.find(edgeIt.second.nodeIds[0]) == limitNodeIds.end() || + limitNodeIds.find(edgeIt.second.nodeIds[1]) == limitNodeIds.end())) + continue; + std::map edge; + edge["id"] = edgeIt.second.id.toString(); + edge["from"] = edgeIt.second.nodeIds[0].toString(); + edge["to"] = edgeIt.second.nodeIds[1].toString(); + edge["partId"] = edgeIt.second.partId.toString(); + if (!edgeIt.second.name.isEmpty()) + edge["name"] = edgeIt.second.name; + snapshot->edges[edge["id"]] = edge; + } + for (const auto &componentIt: componentMap) { + if (!limitComponentIds.empty() && limitComponentIds.find(componentIt.first) == limitComponentIds.end()) + continue; + std::map component; + component["id"] = componentIt.second.id.toString(); + if (!componentIt.second.name.isEmpty()) + component["name"] = componentIt.second.name; + component["expanded"] = componentIt.second.expanded ? "true" : "false"; + component["inverse"] = componentIt.second.inverse ? "true" : "false"; + component["dirty"] = componentIt.second.dirty ? "true" : "false"; + if (componentIt.second.smoothAllAdjusted()) + component["smoothAll"] = QString::number(componentIt.second.smoothAll); + if (componentIt.second.smoothSeamAdjusted()) + component["smoothSeam"] = QString::number(componentIt.second.smoothSeam); + QStringList childIdList; + for (const auto &childId: componentIt.second.childrenIds) { + childIdList.append(childId.toString()); + } + QString children = childIdList.join(","); + if (!children.isEmpty()) + component["children"] = children; + QString linkData = componentIt.second.linkData(); + if (!linkData.isEmpty()) { + component["linkData"] = linkData; + component["linkDataType"] = componentIt.second.linkDataType(); + } + if (!componentIt.second.name.isEmpty()) + component["name"] = componentIt.second.name; + snapshot->components[component["id"]] = component; + } + if (limitComponentIds.empty() || limitComponentIds.find(QUuid()) != limitComponentIds.end()) { + QStringList childIdList; + for (const auto &childId: rootComponent.childrenIds) { + childIdList.append(childId.toString()); + } + QString children = childIdList.join(","); + if (!children.isEmpty()) + snapshot->rootComponent["children"] = children; } } - for (const auto &partIt : partMap) { - if (!limitPartIds.empty() && limitPartIds.find(partIt.first) == limitPartIds.end()) - continue; - std::map part; - part["id"] = partIt.second.id.toString(); - part["visible"] = partIt.second.visible ? "true" : "false"; - part["locked"] = partIt.second.locked ? "true" : "false"; - part["subdived"] = partIt.second.subdived ? "true" : "false"; - part["disabled"] = partIt.second.disabled ? "true" : "false"; - part["xMirrored"] = partIt.second.xMirrored ? "true" : "false"; - part["zMirrored"] = partIt.second.zMirrored ? "true" : "false"; - part["rounded"] = partIt.second.rounded ? "true" : "false"; - part["wrapped"] = partIt.second.wrapped ? "true" : "false"; - part["dirty"] = partIt.second.dirty ? "true" : "false"; - if (partIt.second.hasColor) - part["color"] = partIt.second.color.name(); - if (partIt.second.deformThicknessAdjusted()) - part["deformThickness"] = QString::number(partIt.second.deformThickness); - if (partIt.second.deformWidthAdjusted()) - part["deformWidth"] = QString::number(partIt.second.deformWidth); - if (!partIt.second.name.isEmpty()) - part["name"] = partIt.second.name; - snapshot->parts[part["id"]] = part; - } - for (const auto &nodeIt: nodeMap) { - if (!limitNodeIds.empty() && limitNodeIds.find(nodeIt.first) == limitNodeIds.end()) - continue; - std::map node; - node["id"] = nodeIt.second.id.toString(); - node["radius"] = QString::number(nodeIt.second.radius); - node["x"] = QString::number(nodeIt.second.x); - node["y"] = QString::number(nodeIt.second.y); - node["z"] = QString::number(nodeIt.second.z); - node["partId"] = nodeIt.second.partId.toString(); - if (nodeIt.second.boneMark != SkeletonBoneMark::None) - node["boneMark"] = SkeletonBoneMarkToString(nodeIt.second.boneMark); - if (!nodeIt.second.name.isEmpty()) - node["name"] = nodeIt.second.name; - snapshot->nodes[node["id"]] = node; - } - for (const auto &edgeIt: edgeMap) { - if (edgeIt.second.nodeIds.size() != 2) - continue; - if (!limitNodeIds.empty() && - (limitNodeIds.find(edgeIt.second.nodeIds[0]) == limitNodeIds.end() || - limitNodeIds.find(edgeIt.second.nodeIds[1]) == limitNodeIds.end())) - continue; - std::map edge; - edge["id"] = edgeIt.second.id.toString(); - edge["from"] = edgeIt.second.nodeIds[0].toString(); - edge["to"] = edgeIt.second.nodeIds[1].toString(); - edge["partId"] = edgeIt.second.partId.toString(); - if (!edgeIt.second.name.isEmpty()) - edge["name"] = edgeIt.second.name; - snapshot->edges[edge["id"]] = edge; - } - for (const auto &componentIt: componentMap) { - if (!limitComponentIds.empty() && limitComponentIds.find(componentIt.first) == limitComponentIds.end()) - continue; - std::map component; - component["id"] = componentIt.second.id.toString(); - if (!componentIt.second.name.isEmpty()) - component["name"] = componentIt.second.name; - component["expanded"] = componentIt.second.expanded ? "true" : "false"; - component["inverse"] = componentIt.second.inverse ? "true" : "false"; - component["dirty"] = componentIt.second.dirty ? "true" : "false"; - if (componentIt.second.smoothAllAdjusted()) - component["smoothAll"] = QString::number(componentIt.second.smoothAll); - if (componentIt.second.smoothSeamAdjusted()) - component["smoothSeam"] = QString::number(componentIt.second.smoothSeam); - QStringList childIdList; - for (const auto &childId: componentIt.second.childrenIds) { - childIdList.append(childId.toString()); + if (SkeletonDocumentToSnapshotFor::Document == forWhat || + SkeletonDocumentToSnapshotFor::Poses == forWhat) { + for (const auto &poseId: poseIdList) { + if (!limitPoseIds.empty() && limitPoseIds.find(poseId) == limitPoseIds.end()) + continue; + auto findPoseResult = poseMap.find(poseId); + if (findPoseResult == poseMap.end()) { + qDebug() << "Find pose failed:" << poseId; + continue; + } + auto &poseIt = *findPoseResult; + std::map pose; + pose["id"] = poseIt.second.id.toString(); + if (!poseIt.second.name.isEmpty()) + pose["name"] = poseIt.second.name; + snapshot->poses.push_back(std::make_pair(pose, poseIt.second.parameters)); } - QString children = childIdList.join(","); - if (!children.isEmpty()) - component["children"] = children; - QString linkData = componentIt.second.linkData(); - if (!linkData.isEmpty()) { - component["linkData"] = linkData; - component["linkDataType"] = componentIt.second.linkDataType(); - } - if (!componentIt.second.name.isEmpty()) - component["name"] = componentIt.second.name; - snapshot->components[component["id"]] = component; } - if (limitComponentIds.empty() || limitComponentIds.find(QUuid()) != limitComponentIds.end()) { - QStringList childIdList; - for (const auto &childId: rootComponent.childrenIds) { - childIdList.append(childId.toString()); - } - QString children = childIdList.join(","); - if (!children.isEmpty()) - snapshot->rootComponent["children"] = children; + if (SkeletonDocumentToSnapshotFor::Document == forWhat) { + std::map canvas; + canvas["originX"] = QString::number(originX); + canvas["originY"] = QString::number(originY); + canvas["originZ"] = QString::number(originZ); + canvas["rigType"] = RigTypeToString(rigType); + snapshot->canvas = canvas; } - - std::map canvas; - canvas["originX"] = QString::number(originX); - canvas["originY"] = QString::number(originY); - canvas["originZ"] = QString::number(originZ); - canvas["rigType"] = RigTypeToString(rigType); - snapshot->canvas = canvas; } void SkeletonDocument::addFromSnapshot(const SkeletonSnapshot &snapshot, bool fromPaste) @@ -921,11 +1011,11 @@ void SkeletonDocument::addFromSnapshot(const SkeletonSnapshot &snapshot, bool fr const auto &smoothSeamIt = componentKv.second.find("smoothSeam"); if (smoothSeamIt != componentKv.second.end()) component.setSmoothSeam(smoothSeamIt->second.toFloat()); - qDebug() << "Add component:" << component.id << " old:" << componentKv.first << "name:" << component.name; + //qDebug() << "Add component:" << component.id << " old:" << componentKv.first << "name:" << component.name; if ("partId" == linkDataType) { QUuid partId = oldNewIdMap[QUuid(linkData)]; component.linkToPartId = partId; - qDebug() << "Add part:" << partId << " from component:" << component.id; + //qDebug() << "Add part:" << partId << " from component:" << component.id; partMap[partId].componentId = component.id; if (inversePartIds.find(partId) != inversePartIds.end()) component.inverse = true; @@ -941,7 +1031,7 @@ void SkeletonDocument::addFromSnapshot(const SkeletonSnapshot &snapshot, bool fr QUuid componentId = oldNewIdMap[QUuid(childId)]; if (componentMap.find(componentId) == componentMap.end()) continue; - qDebug() << "Add root component:" << componentId; + //qDebug() << "Add root component:" << componentId; rootComponent.addChild(componentId); } } @@ -955,11 +1045,22 @@ void SkeletonDocument::addFromSnapshot(const SkeletonSnapshot &snapshot, bool fr QUuid childComponentId = oldNewIdMap[QUuid(childId)]; if (componentMap.find(childComponentId) == componentMap.end()) continue; - qDebug() << "Add child component:" << childComponentId << "to" << componentId; + //qDebug() << "Add child component:" << childComponentId << "to" << componentId; componentMap[componentId].addChild(childComponentId); componentMap[childComponentId].parentId = componentId; } } + for (const auto &poseIt: snapshot.poses) { + QUuid newPoseId = QUuid::createUuid(); + auto &newPose = poseMap[newPoseId]; + newPose.id = newPoseId; + const auto &poseAttributes = poseIt.first; + newPose.name = valueOfKeyInMapOrEmpty(poseAttributes, "name"); + newPose.parameters = poseIt.second; + oldNewIdMap[QUuid(valueOfKeyInMapOrEmpty(poseAttributes, "id"))] = newPoseId; + poseIdList.push_back(newPoseId); + emit poseAdded(newPoseId); + } for (const auto &nodeIt: newAddedNodeIds) { emit nodeAdded(nodeIt); @@ -989,6 +1090,9 @@ void SkeletonDocument::addFromSnapshot(const SkeletonSnapshot &snapshot, bool fr for (const auto &edgeIt: newAddedEdgeIds) { emit checkEdge(edgeIt); } + + if (!snapshot.poses.empty()) + emit poseListChanged(); } void SkeletonDocument::reset() @@ -1001,6 +1105,8 @@ void SkeletonDocument::reset() edgeMap.clear(); partMap.clear(); componentMap.clear(); + poseMap.clear(); + poseIdList.clear(); rootComponent = SkeletonComponent(); emit cleanup(); emit skeletonChanged(); @@ -1954,12 +2060,23 @@ void SkeletonDocument::paste() } } -bool SkeletonDocument::hasPastableContentInClipboard() const +bool SkeletonDocument::hasPastableNodesInClipboard() const { const QClipboard *clipboard = QApplication::clipboard(); const QMimeData *mimeData = clipboard->mimeData(); if (mimeData->hasText()) { - if (-1 != mimeData->text().left(1000).indexOf("partIdList")) + if (-1 != mimeData->text().left(1000).indexOf("mimeData(); + if (mimeData->hasText()) { + if (-1 != mimeData->text().right(1000).indexOf(" *rigBones = resultRigBones(); + const std::map *rigWeights = resultRigWeights(); + + if (nullptr == rigBones || nullptr == rigWeights) { + return; + } + + QThread *thread = new QThread; + m_posePreviewsGenerator = new PosePreviewsGenerator(rigBones, + rigWeights, *m_riggedResultContext); + bool hasDirtyPose = false; + for (auto &poseIt: poseMap) { + if (!poseIt.second.dirty) + continue; + m_posePreviewsGenerator->addPose(poseIt.first, poseIt.second.parameters); + poseIt.second.dirty = false; + hasDirtyPose = true; + } + if (!hasDirtyPose) { + delete m_posePreviewsGenerator; + m_posePreviewsGenerator = nullptr; + delete thread; + return; + } + + qDebug() << "Pose previews generating.."; + + m_posePreviewsGenerator->moveToThread(thread); + connect(thread, &QThread::started, m_posePreviewsGenerator, &PosePreviewsGenerator::process); + connect(m_posePreviewsGenerator, &PosePreviewsGenerator::finished, this, &SkeletonDocument::posePreviewsReady); + connect(m_posePreviewsGenerator, &PosePreviewsGenerator::finished, thread, &QThread::quit); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +void SkeletonDocument::posePreviewsReady() +{ + for (const auto &poseId: m_posePreviewsGenerator->generatedPreviewPoseIds()) { + auto pose = poseMap.find(poseId); + if (pose != poseMap.end()) { + MeshLoader *resultPartPreviewMesh = m_posePreviewsGenerator->takePreview(poseId); + pose->second.updatePreviewMesh(resultPartPreviewMesh); + emit posePreviewChanged(poseId); + } + } + + delete m_posePreviewsGenerator; + m_posePreviewsGenerator = nullptr; + + qDebug() << "Pose previews generation done"; + + generatePosePreviews(); +} diff --git a/src/skeletondocument.h b/src/skeletondocument.h index 12d48d12..ac7eea0d 100644 --- a/src/skeletondocument.h +++ b/src/skeletondocument.h @@ -20,6 +20,7 @@ #include "skeletonbonemark.h" #include "riggenerator.h" #include "rigtype.h" +#include "posepreviewsgenerator.h" class SkeletonNode { @@ -345,6 +346,43 @@ private: std::set m_childrenIdSet; }; +class SkeletonPose +{ +public: + SkeletonPose() + { + } + ~SkeletonPose() + { + delete m_previewMesh; + } + QUuid id; + QString name; + bool dirty = true; + std::map> parameters; + void updatePreviewMesh(MeshLoader *previewMesh) + { + delete m_previewMesh; + m_previewMesh = previewMesh; + } + MeshLoader *takePreviewMesh() const + { + if (nullptr == m_previewMesh) + return nullptr; + return new MeshLoader(*m_previewMesh); + } +private: + Q_DISABLE_COPY(SkeletonPose); + MeshLoader *m_previewMesh = nullptr; +}; + +enum class SkeletonDocumentToSnapshotFor +{ + Document = 0, + Nodes, + Poses +}; + class SkeletonDocument : public QObject { Q_OBJECT @@ -406,6 +444,12 @@ signals: void checkEdge(QUuid edgeId); void optionsChanged(); void rigTypeChanged(); + void poseAdded(QUuid poseId); + void poseRemoved(QUuid); + void poseListChanged(); + void poseNameChanged(QUuid poseId); + void poseParametersChanged(QUuid poseId); + void posePreviewChanged(QUuid poseId); public: // need initialize float originX; float originY; @@ -428,10 +472,14 @@ public: std::map nodeMap; std::map edgeMap; std::map componentMap; + std::map poseMap; + std::vector poseIdList; SkeletonComponent rootComponent; QImage turnaround; QImage preview; - void toSnapshot(SkeletonSnapshot *snapshot, const std::set &limitNodeIds=std::set()) const; + void toSnapshot(SkeletonSnapshot *snapshot, const std::set &limitNodeIds=std::set(), + SkeletonDocumentToSnapshotFor forWhat=SkeletonDocumentToSnapshotFor::Document, + const std::set &limitPoseIds=std::set()) const; void fromSnapshot(const SkeletonSnapshot &snapshot); void addFromSnapshot(const SkeletonSnapshot &snapshot, bool fromPaste=true); const SkeletonNode *findNode(QUuid nodeId) const; @@ -441,6 +489,7 @@ public: const SkeletonComponent *findComponent(QUuid componentId) const; const SkeletonComponent *findComponentParent(QUuid componentId) const; QUuid findComponentParentId(QUuid componentId) const; + const SkeletonPose *findPose(QUuid poseId) const; MeshLoader *takeResultMesh(); MeshLoader *takeResultTextureMesh(); MeshLoader *takeResultRigWeightMesh(); @@ -448,7 +497,8 @@ public: const std::map *resultRigWeights() const; void updateTurnaround(const QImage &image); void setSharedContextWidget(QOpenGLWidget *sharedContextWidget); - bool hasPastableContentInClipboard() const; + bool hasPastableNodesInClipboard() const; + bool hasPastablePosesInClipboard() const; bool undoable() const; bool redoable() const; bool isNodeEditable(QUuid nodeId) const; @@ -489,6 +539,8 @@ public slots: void ambientOcclusionTextureReady(); void generateRig(); void rigReady(); + void generatePosePreviews(); + void posePreviewsReady(); void setPartLockState(QUuid partId, bool locked); void setPartVisibleState(QUuid partId, bool visible); void setPartSubdivState(QUuid partId, bool subdived); @@ -543,6 +595,10 @@ public slots: void disableAllPositionRelatedLocks(); void toggleSmoothNormal(); void setRigType(RigType toRigType); + void addPose(QString name, std::map> parameters); + void removePose(QUuid poseId); + void setPoseParameters(QUuid poseId, std::map> parameters); + void renamePose(QUuid poseId, QString name); private: void splitPartByNode(std::vector> *groups, QUuid nodeId); void joinNodeAndNeiborsToGroup(std::vector *group, QUuid nodeId, std::set *visitMap, QUuid noUseEdgeId=QUuid()); @@ -583,6 +639,7 @@ private: // need initialize std::map *m_resultRigWeights; bool m_isRigObsolete; MeshResultContext *m_riggedResultContext; + PosePreviewsGenerator *m_posePreviewsGenerator; private: static unsigned long m_maxSnapshot; std::deque m_undoItems; diff --git a/src/skeletondocumentwindow.cpp b/src/skeletondocumentwindow.cpp index 0467d7d3..2573ebe7 100644 --- a/src/skeletondocumentwindow.cpp +++ b/src/skeletondocumentwindow.cpp @@ -30,7 +30,6 @@ #include "skeletonparttreewidget.h" #include "rigwidget.h" #include "markiconcreator.h" -#include "tetrapodposeeditwidget.h" int SkeletonDocumentWindow::m_modelRenderWidgetInitialX = 16; int SkeletonDocumentWindow::m_modelRenderWidgetInitialY = 16; @@ -55,7 +54,7 @@ void SkeletonDocumentWindow::showAcknowlegements() { if (!g_acknowlegementsWidget) { g_acknowlegementsWidget = new QTextBrowser; - g_acknowlegementsWidget->setWindowTitle(APP_NAME); + g_acknowlegementsWidget->setWindowTitle(unifiedWindowTitle(tr("Acknowlegements"))); g_acknowlegementsWidget->setMinimumSize(QSize(400, 300)); QFile file(":/ACKNOWLEDGEMENTS.html"); file.open(QFile::ReadOnly | QFile::Text); @@ -71,7 +70,7 @@ void SkeletonDocumentWindow::showContributors() { if (!g_contributorsWidget) { g_contributorsWidget = new QTextBrowser; - g_contributorsWidget->setWindowTitle(APP_NAME); + g_contributorsWidget->setWindowTitle(unifiedWindowTitle(tr("Contributors"))); g_contributorsWidget->setMinimumSize(QSize(400, 300)); QFile authors(":/AUTHORS"); authors.open(QFile::ReadOnly | QFile::Text); @@ -225,16 +224,21 @@ SkeletonDocumentWindow::SkeletonDocumentWindow() : updateRigWeightRenderWidget(); }); - //QDockWidget *animationDocker = new QDockWidget(tr("Animation"), this); - //animationDocker->setAllowedAreas(Qt::RightDockWidgetArea); - //TetrapodPoseEditWidget *tetrapodPoseEditWidget = new TetrapodPoseEditWidget(m_document, animationDocker); - //animationDocker->setWidget(tetrapodPoseEditWidget); - //AnimationListWidget *animationListWidget = new AnimationListWidget(m_document, animationDocker); - //animationDocker->setWidget(animationListWidget); - //addDockWidget(Qt::RightDockWidgetArea, animationDocker); + QDockWidget *poseDocker = new QDockWidget(tr("Poses"), this); + poseDocker->setAllowedAreas(Qt::RightDockWidgetArea); + PoseManageWidget *poseManageWidget = new PoseManageWidget(m_document, poseDocker); + poseDocker->setWidget(poseManageWidget); + connect(poseManageWidget, &PoseManageWidget::registerDialog, this, &SkeletonDocumentWindow::registerDialog); + connect(poseManageWidget, &PoseManageWidget::unregisterDialog, this, &SkeletonDocumentWindow::unregisterDialog); + addDockWidget(Qt::RightDockWidgetArea, poseDocker); + connect(poseDocker, &QDockWidget::topLevelChanged, [=](bool topLevel) { + Q_UNUSED(topLevel); + for (const auto &pose: m_document->poseMap) + emit m_document->posePreviewChanged(pose.first); + }); tabifyDockWidget(partTreeDocker, rigDocker); - //tabifyDockWidget(rigDocker, animationDocker); + tabifyDockWidget(rigDocker, poseDocker); partTreeDocker->raise(); @@ -447,7 +451,7 @@ SkeletonDocumentWindow::SkeletonDocumentWindow() : m_connectAction->setEnabled(m_graphicsWidget->hasTwoDisconnectedNodesSelection()); m_cutAction->setEnabled(m_graphicsWidget->hasSelection()); m_copyAction->setEnabled(m_graphicsWidget->hasSelection()); - m_pasteAction->setEnabled(m_document->hasPastableContentInClipboard()); + m_pasteAction->setEnabled(m_document->hasPastableNodesInClipboard()); m_flipHorizontallyAction->setEnabled(m_graphicsWidget->hasMultipleSelection()); m_flipVerticallyAction->setEnabled(m_graphicsWidget->hasMultipleSelection()); m_rotateClockwiseAction->setEnabled(m_graphicsWidget->hasMultipleSelection()); @@ -512,12 +516,22 @@ SkeletonDocumentWindow::SkeletonDocumentWindow() : }); m_windowMenu->addAction(m_showRigAction); - //m_showAnimationAction = new QAction(tr("Animation"), this); - //connect(m_showAnimationAction, &QAction::triggered, [=]() { - // animationDocker->show(); - // animationDocker->raise(); - //}); - //m_windowMenu->addAction(m_showAnimationAction); + QMenu *dialogsMenu = m_windowMenu->addMenu(tr("Dialogs")); + connect(dialogsMenu, &QMenu::aboutToShow, [=]() { + dialogsMenu->clear(); + if (this->m_dialogs.empty()) { + QAction *action = dialogsMenu->addAction(tr("None")); + action->setEnabled(false); + return; + } + for (const auto &dialog: this->m_dialogs) { + QAction *action = dialogsMenu->addAction(dialog->windowTitle()); + connect(action, &QAction::triggered, [=]() { + dialog->show(); + dialog->raise(); + }); + } + }); m_showDebugDialogAction = new QAction(tr("Debug"), this); connect(m_showDebugDialogAction, &QAction::triggered, g_logBrowser, &LogBrowser::showDialog); @@ -764,6 +778,16 @@ SkeletonDocumentWindow::SkeletonDocumentWindow() : connect(m_document, &SkeletonDocument::resultRigChanged, this, &SkeletonDocumentWindow::updateRigWeightRenderWidget); //connect(m_document, &SkeletonDocument::resultRigChanged, tetrapodPoseEditWidget, &TetrapodPoseEditWidget::updatePreview); + + connect(m_document, &SkeletonDocument::poseAdded, this, [=](QUuid poseId) { + Q_UNUSED(poseId); + m_document->generatePosePreviews(); + }); + connect(m_document, &SkeletonDocument::poseParametersChanged, this, [=](QUuid poseId) { + Q_UNUSED(poseId); + m_document->generatePosePreviews(); + }); + connect(m_document, &SkeletonDocument::resultRigChanged, m_document, &SkeletonDocument::generatePosePreviews); connect(this, &SkeletonDocumentWindow::initialized, m_document, &SkeletonDocument::uiReady); @@ -1058,13 +1082,13 @@ void SkeletonDocumentWindow::showExportPreview() { if (nullptr == m_exportPreviewWidget) { m_exportPreviewWidget = new ExportPreviewWidget(m_document, this); - m_exportPreviewWidget->setWindowFlags(Qt::Tool); connect(m_exportPreviewWidget, &ExportPreviewWidget::regenerate, m_document, &SkeletonDocument::regenerateMesh); connect(m_exportPreviewWidget, &ExportPreviewWidget::save, this, &SkeletonDocumentWindow::exportGltfResult); connect(m_document, &SkeletonDocument::resultMeshChanged, m_exportPreviewWidget, &ExportPreviewWidget::checkSpinner); connect(m_document, &SkeletonDocument::exportReady, m_exportPreviewWidget, &ExportPreviewWidget::checkSpinner); connect(m_document, &SkeletonDocument::resultTextureChanged, m_exportPreviewWidget, &ExportPreviewWidget::updateTexturePreview); connect(m_document, &SkeletonDocument::resultBakedTextureChanged, m_exportPreviewWidget, &ExportPreviewWidget::updateTexturePreview); + registerDialog(m_exportPreviewWidget); } if (m_document->isPostProcessResultObsolete()) { m_document->postProcess(); @@ -1145,3 +1169,13 @@ void SkeletonDocumentWindow::updateRigWeightRenderWidget() m_rigWidget->rigWeightRenderWidget()->update(); } } + +void SkeletonDocumentWindow::registerDialog(QWidget *widget) +{ + m_dialogs.push_back(widget); +} + +void SkeletonDocumentWindow::unregisterDialog(QWidget *widget) +{ + m_dialogs.erase(std::remove(m_dialogs.begin(), m_dialogs.end(), widget), m_dialogs.end()); +} diff --git a/src/skeletondocumentwindow.h b/src/skeletondocumentwindow.h index 6b27b7f3..ce84740a 100644 --- a/src/skeletondocumentwindow.h +++ b/src/skeletondocumentwindow.h @@ -12,6 +12,7 @@ #include "exportpreviewwidget.h" #include "rigwidget.h" #include "skeletonbonemark.h" +#include "posemanagewidget.h" class SkeletonGraphicsWidget; @@ -56,6 +57,8 @@ public slots: void updateZlockButtonState(); void updateRadiusLockButtonState(); void updateRigWeightRenderWidget(); + void registerDialog(QWidget *widget); + void unregisterDialog(QWidget *widget); private: void initLockButton(QPushButton *button); void setCurrentFilename(const QString &filename); @@ -65,12 +68,14 @@ private: bool m_firstShow; bool m_documentSaved; ExportPreviewWidget *m_exportPreviewWidget; + std::vector m_dialogs; private: QString m_currentFilename; ModelWidget *m_modelRenderWidget; SkeletonGraphicsWidget *m_graphicsWidget; RigWidget *m_rigWidget; + PoseManageWidget *m_poseManageWidget; QMenu *m_fileMenu; QAction *m_newWindowAction; @@ -129,7 +134,6 @@ private: QAction *m_showPartsListAction; QAction *m_showDebugDialogAction; QAction *m_showRigAction; - QAction *m_showAnimationAction; QMenu *m_helpMenu; QAction *m_viewSourceAction; diff --git a/src/skeletongraphicswidget.cpp b/src/skeletongraphicswidget.cpp index b3ecbc68..7ded0a0e 100644 --- a/src/skeletongraphicswidget.cpp +++ b/src/skeletongraphicswidget.cpp @@ -158,7 +158,7 @@ void SkeletonGraphicsWidget::showContextMenu(const QPoint &pos) } QAction pasteAction(tr("Paste"), this); - if (m_document->hasPastableContentInClipboard()) { + if (m_document->hasPastableNodesInClipboard()) { connect(&pasteAction, &QAction::triggered, m_document, &SkeletonDocument::paste); contextMenu.addAction(&pasteAction); } @@ -1193,7 +1193,6 @@ bool SkeletonGraphicsWidget::mousePress(QMouseEvent *event) m_lastAddedX = unifiedMainPos.x(); m_lastAddedY = unifiedMainPos.y(); m_lastAddedZ = unifiedSidePos.x(); - qDebug() << "Emit add node " << m_lastAddedX << m_lastAddedY << m_lastAddedZ; emit addNode(unifiedMainPos.x(), unifiedMainPos.y(), unifiedSidePos.x(), sceneRadiusToUnified(m_cursorNodeItem->radius()), nullptr == m_addFromNodeItem ? QUuid() : m_addFromNodeItem->id()); emit groupOperationAdded(); return true; @@ -2158,7 +2157,7 @@ void SkeletonGraphicsWidget::copy() if (nodeIdSet.empty()) return; SkeletonSnapshot snapshot; - m_document->toSnapshot(&snapshot, nodeIdSet); + m_document->toSnapshot(&snapshot, nodeIdSet, SkeletonDocumentToSnapshotFor::Nodes); QString snapshotXml; QXmlStreamWriter xmlStreamWriter(&snapshotXml); saveSkeletonToXmlStream(&snapshot, &xmlStreamWriter); diff --git a/src/skeletonparttreewidget.cpp b/src/skeletonparttreewidget.cpp index e8d14dbb..7b40e6d1 100644 --- a/src/skeletonparttreewidget.cpp +++ b/src/skeletonparttreewidget.cpp @@ -281,7 +281,7 @@ void SkeletonPartTreeWidget::showContextMenu(const QPoint &pos) ModelWidget *previewWidget = new ModelWidget; previewWidget->enableMove(false); previewWidget->enableZoom(false); - previewWidget->setFixedSize(Theme::previewImageSize, Theme::previewImageSize); + previewWidget->setFixedSize(Theme::partPreviewImageSize, Theme::partPreviewImageSize); previewWidget->setXRotation(partWidget->previewWidget()->xRot()); previewWidget->setYRotation(partWidget->previewWidget()->yRot()); previewWidget->setZRotation(partWidget->previewWidget()->zRot()); @@ -289,7 +289,7 @@ void SkeletonPartTreeWidget::showContextMenu(const QPoint &pos) layout->addWidget(previewWidget); } else { QLabel *previewLabel = new QLabel; - previewLabel->setFixedHeight(Theme::previewImageSize); + previewLabel->setFixedHeight(Theme::partPreviewImageSize); previewLabel->setStyleSheet("QLabel {color: " + Theme::red.name() + "}"); if (nullptr != component) { previewLabel->setText(component->name); diff --git a/src/skeletonpartwidget.cpp b/src/skeletonpartwidget.cpp index 24013026..c8b3f2bf 100644 --- a/src/skeletonpartwidget.cpp +++ b/src/skeletonpartwidget.cpp @@ -55,7 +55,7 @@ SkeletonPartWidget::SkeletonPartWidget(const SkeletonDocument *document, QUuid p m_previewWidget = new ModelWidget; m_previewWidget->enableMove(false); m_previewWidget->enableZoom(false); - m_previewWidget->setFixedSize(Theme::previewImageSize, Theme::previewImageSize); + m_previewWidget->setFixedSize(Theme::partPreviewImageSize, Theme::partPreviewImageSize); QWidget *hrLightWidget = new QWidget; hrLightWidget->setFixedHeight(1); @@ -99,7 +99,7 @@ SkeletonPartWidget::SkeletonPartWidget(const SkeletonDocument *document, QUuid p QWidget *backgroundWidget = new QWidget; backgroundWidget->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Minimum); - backgroundWidget->setFixedSize(preferredSize().width(), Theme::previewImageSize); + backgroundWidget->setFixedSize(preferredSize().width(), Theme::partPreviewImageSize); backgroundWidget->setObjectName("background"); m_backgroundWidget = backgroundWidget; backgroundWidget->setLayout(previewAndToolsLayout); @@ -238,7 +238,7 @@ ModelWidget *SkeletonPartWidget::previewWidget() QSize SkeletonPartWidget::preferredSize() { - return QSize(Theme::miniIconSize + Theme::previewImageSize + Theme::miniIconSize * 4 + 5 + 2, Theme::previewImageSize + 6); + return QSize(Theme::miniIconSize + Theme::partPreviewImageSize + Theme::miniIconSize * 4 + 5 + 2, Theme::partPreviewImageSize + 6); } void SkeletonPartWidget::updateAllButtons() diff --git a/src/skeletonpartwidget.h b/src/skeletonpartwidget.h index d1bee77e..589beae6 100644 --- a/src/skeletonpartwidget.h +++ b/src/skeletonpartwidget.h @@ -47,7 +47,7 @@ public: static QSize preferredSize(); ModelWidget *previewWidget(); protected: - void mouseDoubleClickEvent(QMouseEvent *event); + void mouseDoubleClickEvent(QMouseEvent *event) override; public slots: void showDeformSettingPopup(const QPoint &pos); void showColorSettingPopup(const QPoint &pos); diff --git a/src/skeletonsnapshot.h b/src/skeletonsnapshot.h index 6c8af42e..c73c427a 100644 --- a/src/skeletonsnapshot.h +++ b/src/skeletonsnapshot.h @@ -15,6 +15,7 @@ public: std::map> parts; std::map> components; std::map rootComponent; + std::vector, std::map>>> poses; // std::pair public: void resolveBoundingBox(QRectF *mainProfile, QRectF *sideProfile, const QString &partId=QString()); }; diff --git a/src/skeletonxml.cpp b/src/skeletonxml.cpp index db2a0f60..9320c421 100644 --- a/src/skeletonxml.cpp +++ b/src/skeletonxml.cpp @@ -91,6 +91,29 @@ void saveSkeletonToXmlStream(SkeletonSnapshot *snapshot, QXmlStreamWriter *write writer->writeEndElement(); } + writer->writeStartElement("poses"); + std::vector, std::map>>>::iterator poseIterator; + for (poseIterator = snapshot->poses.begin(); poseIterator != snapshot->poses.end(); poseIterator++) { + std::map::iterator poseAttributeIterator; + writer->writeStartElement("pose"); + for (poseAttributeIterator = poseIterator->first.begin(); poseAttributeIterator != poseIterator->first.end(); poseAttributeIterator++) { + writer->writeAttribute(poseAttributeIterator->first, poseAttributeIterator->second); + } + std::map>::iterator itemsIterator; + for (itemsIterator = poseIterator->second.begin(); itemsIterator != poseIterator->second.end(); itemsIterator++) { + std::map::iterator parametersIterator; + writer->writeStartElement("parameter"); + writer->writeAttribute("for", itemsIterator->first); + for (parametersIterator = itemsIterator->second.begin(); parametersIterator != itemsIterator->second.end(); + parametersIterator++) { + writer->writeAttribute(parametersIterator->first, parametersIterator->second); + } + writer->writeEndElement(); + } + writer->writeEndElement(); + } + writer->writeEndElement(); + writer->writeEndElement(); writer->writeEndDocument(); @@ -100,10 +123,14 @@ void loadSkeletonFromXmlStream(SkeletonSnapshot *snapshot, QXmlStreamReader &rea { std::stack componentStack; std::vector elementNameStack; + std::pair, std::map>> currentPose; while (!reader.atEnd()) { reader.readNext(); - if (!reader.isStartElement() && !reader.isEndElement()) + if (!reader.isStartElement() && !reader.isEndElement()) { + if (!reader.name().toString().isEmpty()) + qDebug() << "Skip xml element:" << reader.name().toString() << " tokenType:" << reader.tokenType(); continue; + } QString baseName = reader.name().toString(); if (reader.isStartElement()) elementNameStack.push_back(baseName); @@ -114,6 +141,7 @@ void loadSkeletonFromXmlStream(SkeletonSnapshot *snapshot, QXmlStreamReader &rea QString fullName = nameItems.join("."); if (reader.isEndElement()) elementNameStack.pop_back(); + //qDebug() << (reader.isStartElement() ? "<" : ">") << "fullName:" << fullName << "baseName:" << baseName; if (reader.isStartElement()) { if (fullName == "canvas") { foreach(const QXmlStreamAttribute &attr, reader.attributes()) { @@ -172,10 +200,29 @@ void loadSkeletonFromXmlStream(SkeletonSnapshot *snapshot, QXmlStreamReader &rea if (!parentChildrenIds.isEmpty()) parentChildrenIds += ","; parentChildrenIds += componentId; + } else if (fullName == "canvas.poses.pose") { + QString poseId = reader.attributes().value("id").toString(); + if (poseId.isEmpty()) + continue; + currentPose = decltype(currentPose)(); + foreach(const QXmlStreamAttribute &attr, reader.attributes()) { + currentPose.first[attr.name().toString()] = attr.value().toString(); + } + } else if (fullName == "canvas.poses.pose.parameter") { + QString forWhat = reader.attributes().value("for").toString(); + if (forWhat.isEmpty()) + continue; + foreach(const QXmlStreamAttribute &attr, reader.attributes()) { + if ("for" == attr.name().toString()) + continue; + currentPose.second[forWhat][attr.name().toString()] = attr.value().toString(); + } } } else if (reader.isEndElement()) { if (fullName.startsWith("canvas.components.component")) { componentStack.pop(); + } else if (fullName == "canvas.poses.pose") { + snapshot->poses.push_back(currentPose); } } } diff --git a/src/tetrapodposeeditwidget.h b/src/tetrapodposeeditwidget.h deleted file mode 100644 index 0e650cbf..00000000 --- a/src/tetrapodposeeditwidget.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef TETRAPOD_POSE_EDIT_WIDGET_H -#define TETRAPOD_POSE_EDIT_WIDGET_H -#include -#include -#include -#include "posepreviewmanager.h" -#include "tetrapodposer.h" -#include "skeletondocument.h" - -enum class PopupWidgetType -{ - PitchYawRoll, - Intersection -}; - -class TetrapodPoseEditWidget : public QWidget -{ - Q_OBJECT -public: - TetrapodPoseEditWidget(const SkeletonDocument *document, QWidget *parent=nullptr); - ~TetrapodPoseEditWidget(); -public slots: - void updatePreview(); - void showPopupAngleDialog(QString boneName, PopupWidgetType popupWidgetType, QPoint pos); -private: - const SkeletonDocument *m_document = nullptr; - PosePreviewManager *m_posePreviewManager = nullptr; - TetrapodPoser *m_poser = nullptr; - bool m_isPreviewDirty = false; - std::map> m_parameters; -}; - -#endif diff --git a/src/theme.cpp b/src/theme.cpp index 9054f2d0..1649a3aa 100644 --- a/src/theme.cpp +++ b/src/theme.cpp @@ -33,8 +33,9 @@ int Theme::toolIconFontSize = 16; int Theme::toolIconSize = 24; int Theme::miniIconFontSize = 9; int Theme::miniIconSize = 15; -int Theme::previewImageSize = (Theme::miniIconSize * 3); -int Theme::previewImageRenderSize = 75; +int Theme::partPreviewImageSize = (Theme::miniIconSize * 3); +int Theme::posePreviewImageSize = 75; +int Theme::sidebarPreferredWidth = 200; QtAwesome *Theme::awesome() { @@ -128,3 +129,22 @@ void Theme::initAwesomeToolButton(QPushButton *button) Theme::initAwesomeToolButtonWithoutFont(button); } +QWidget *Theme::createHorizontalLineWidget() +{ + QWidget *hrLightWidget = new QWidget; + hrLightWidget->setFixedHeight(1); + hrLightWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + hrLightWidget->setStyleSheet(QString("background-color: #565656;")); + hrLightWidget->setContentsMargins(0, 0, 0, 0); + return hrLightWidget; +} + +QWidget *Theme::createVerticalLineWidget() +{ + QWidget *hrLightWidget = new QWidget; + hrLightWidget->setFixedWidth(1); + hrLightWidget->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Expanding); + hrLightWidget->setStyleSheet(QString("background-color: #565656;")); + hrLightWidget->setContentsMargins(0, 0, 0, 0); + return hrLightWidget; +} diff --git a/src/theme.h b/src/theme.h index f54218f1..1c00ea8b 100644 --- a/src/theme.h +++ b/src/theme.h @@ -30,12 +30,15 @@ public: static std::map nextSideColorNameMap; static std::map sideColorNameToColorMap; static QtAwesome *awesome(); + static QWidget *createHorizontalLineWidget(); + static QWidget *createVerticalLineWidget(); static int toolIconFontSize; static int toolIconSize; - static int previewImageRenderSize; - static int previewImageSize; + static int posePreviewImageSize; + static int partPreviewImageSize; static int miniIconFontSize; static int miniIconSize; + static int sidebarPreferredWidth; public: static void initAwesomeButton(QPushButton *button); static void initAwesomeLabel(QLabel *label);