From ee924720272e9231c97db601a0040b67adda9d7e Mon Sep 17 00:00:00 2001 From: Jeremy Hu Date: Tue, 2 Oct 2018 12:59:30 +0800 Subject: [PATCH] Add motion manager Added motions tab, hermite curve editor, quaternion overshoot blend and so on. --- ACKNOWLEDGEMENTS.html | 12 + dust3d.pro | 31 +- src/animationclipplayer.cpp | 59 +++ src/animationclipplayer.h | 29 ++ src/curveutil.cpp | 63 +++ src/curveutil.h | 43 ++ src/dust3dutil.cpp | 18 + src/dust3dutil.h | 1 + src/infolabel.cpp | 1 + src/interpolationgraphicswidget.cpp | 680 ++++++++++++++++++++++++++++ src/interpolationgraphicswidget.h | 302 ++++++++++++ src/jointnodetree.cpp | 13 + src/jointnodetree.h | 1 + src/motioneditwidget.cpp | 298 ++++++++++++ src/motioneditwidget.h | 56 +++ src/motionlistwidget.cpp | 315 +++++++++++++ src/motionlistwidget.h | 44 ++ src/motionmanagewidget.cpp | 76 ++++ src/motionmanagewidget.h | 25 + src/motionpreviewsgenerator.cpp | 153 +++++++ src/motionpreviewsgenerator.h | 45 ++ src/motionwidget.cpp | 104 +++++ src/motionwidget.h | 33 ++ src/poselistwidget.cpp | 17 + src/poselistwidget.h | 17 +- src/posemanagewidget.cpp | 17 + src/posemeshcreator.cpp | 4 +- src/posemeshcreator.h | 3 +- src/posepreviewmanager.cpp | 2 +- src/posepreviewsgenerator.cpp | 2 +- src/poser.cpp | 5 + src/poser.h | 1 + src/posewidget.cpp | 14 + src/posewidget.h | 6 +- src/riggenerator.cpp | 11 +- src/riggenerator.h | 2 + src/skeletondocument.cpp | 188 +++++++- src/skeletondocument.h | 39 +- src/skeletondocumentwindow.cpp | 17 + src/skeletondocumentwindow.h | 1 + src/skeletonsnapshot.h | 1 + src/skeletonxml.cpp | 92 +++- src/theme.cpp | 1 + src/theme.h | 1 + src/videoframeextractor.h | 41 -- 45 files changed, 2801 insertions(+), 83 deletions(-) create mode 100644 src/animationclipplayer.cpp create mode 100644 src/animationclipplayer.h create mode 100644 src/curveutil.cpp create mode 100644 src/curveutil.h create mode 100644 src/interpolationgraphicswidget.cpp create mode 100644 src/interpolationgraphicswidget.h create mode 100644 src/motioneditwidget.cpp create mode 100644 src/motioneditwidget.h create mode 100644 src/motionlistwidget.cpp create mode 100644 src/motionlistwidget.h create mode 100644 src/motionmanagewidget.cpp create mode 100644 src/motionmanagewidget.h create mode 100644 src/motionpreviewsgenerator.cpp create mode 100644 src/motionpreviewsgenerator.h create mode 100644 src/motionwidget.cpp create mode 100644 src/motionwidget.h delete mode 100644 src/videoframeextractor.h diff --git a/ACKNOWLEDGEMENTS.html b/ACKNOWLEDGEMENTS.html index ddb929cb..fea986ca 100644 --- a/ACKNOWLEDGEMENTS.html +++ b/ACKNOWLEDGEMENTS.html @@ -544,3 +544,15 @@ https://www.reddit.com/r/gamedev/comments/5iuf3h/i_am_writting_a_3d_monster_mode
     https://appimage.org/
 
+ +

Sam Hocevar

+
+    https://gamedev.stackexchange.com/questions/98246/quaternion-slerp-and-lerp-implementation-with-overshoot
+
+ +

David Rosen

+
+    An Indie Approach to Procedural Animation
+    http://www.gdcvault.com/play/1020583/Animation-Bootcamp-An-Indie-Approach
+
+ diff --git a/dust3d.pro b/dust3d.pro index f0c20d94..b6fb8524 100644 --- a/dust3d.pro +++ b/dust3d.pro @@ -200,6 +200,30 @@ HEADERS += src/meshweldseam.h SOURCES += src/advancesettingwidget.cpp HEADERS += src/advancesettingwidget.h +SOURCES += src/curveutil.cpp +HEADERS += src/curveutil.h + +SOURCES += src/interpolationgraphicswidget.cpp +HEADERS += src/interpolationgraphicswidget.h + +SOURCES += src/motioneditwidget.cpp +HEADERS += src/motioneditwidget.h + +SOURCES += src/motionmanagewidget.cpp +HEADERS += src/motionmanagewidget.h + +SOURCES += src/motionlistwidget.cpp +HEADERS += src/motionlistwidget.h + +SOURCES += src/motionwidget.cpp +HEADERS += src/motionwidget.h + +SOURCES += src/motionpreviewsgenerator.cpp +HEADERS += src/motionpreviewsgenerator.h + +SOURCES += src/animationclipplayer.cpp +HEADERS += src/animationclipplayer.h + SOURCES += src/main.cpp HEADERS += src/version.h @@ -436,13 +460,6 @@ macx { GMP_LIBDIR = /usr/local/opt/gmp/lib MPFR_INCLUDEDIR = /usr/local/opt/mpfr/include MPFR_LIBDIR = /usr/local/opt/mpfr/lib - - exists(/usr/local/opt/opencv) { - INCLUDEPATH += /usr/local/opt/opencv/include - LIBS += -L/usr/local/opt/opencv/lib -lopencv_core -lopencv_videoio -lopencv_imgproc - - DEFINES += "USE_OPENCV=1" - } } unix:!macx { diff --git a/src/animationclipplayer.cpp b/src/animationclipplayer.cpp new file mode 100644 index 00000000..aad3a3ad --- /dev/null +++ b/src/animationclipplayer.cpp @@ -0,0 +1,59 @@ +#include "animationclipplayer.h" + +AnimationClipPlayer::~AnimationClipPlayer() +{ + clear(); +} + +void AnimationClipPlayer::updateFrameMeshes(std::vector> &frameMeshes) +{ + clear(); + + m_frameMeshes = frameMeshes; + frameMeshes.clear(); + + m_currentPlayIndex = 0; + m_countForFrame.restart(); + + if (!m_frameMeshes.empty()) + m_timerForFrame.singleShot(0, this, &AnimationClipPlayer::frameReadyToShow); +} + +void AnimationClipPlayer::clear() +{ + freeFrames(); + delete m_lastFrameMesh; + m_lastFrameMesh = nullptr; +} + +void AnimationClipPlayer::freeFrames() +{ + for (auto &it: m_frameMeshes) { + delete it.second; + } + m_frameMeshes.clear(); +} + +MeshLoader *AnimationClipPlayer::takeFrameMesh() +{ + if (m_currentPlayIndex >= (int)m_frameMeshes.size()) { + if (nullptr != m_lastFrameMesh) + return new MeshLoader(*m_lastFrameMesh); + return nullptr; + } + int millis = m_frameMeshes[m_currentPlayIndex].first * 1000 - m_countForFrame.elapsed(); + if (millis > 0) { + m_timerForFrame.singleShot(millis, this, &AnimationClipPlayer::frameReadyToShow); + if (nullptr != m_lastFrameMesh) + return new MeshLoader(*m_lastFrameMesh); + return nullptr; + } + m_currentPlayIndex = (m_currentPlayIndex + 1) % m_frameMeshes.size(); + m_countForFrame.restart(); + + MeshLoader *mesh = new MeshLoader(*m_frameMeshes[m_currentPlayIndex].second); + m_timerForFrame.singleShot(m_frameMeshes[m_currentPlayIndex].first * 1000, this, &AnimationClipPlayer::frameReadyToShow); + delete m_lastFrameMesh; + m_lastFrameMesh = new MeshLoader(*mesh); + return mesh; +} diff --git a/src/animationclipplayer.h b/src/animationclipplayer.h new file mode 100644 index 00000000..095d9742 --- /dev/null +++ b/src/animationclipplayer.h @@ -0,0 +1,29 @@ +#ifndef ANIMATION_PLAYER_H +#define ANIMATION_PLAYER_H +#include +#include +#include +#include "meshloader.h" + +class AnimationClipPlayer : public QObject +{ + Q_OBJECT +signals: + void frameReadyToShow(); +public: + ~AnimationClipPlayer(); + MeshLoader *takeFrameMesh(); + void updateFrameMeshes(std::vector> &frameMeshes); + void clear(); +private: + void freeFrames(); +private: + MeshLoader *m_lastFrameMesh = nullptr; + int m_currentPlayIndex = 0; +private: + std::vector> m_frameMeshes; + QTime m_countForFrame; + QTimer m_timerForFrame; +}; + +#endif diff --git a/src/curveutil.cpp b/src/curveutil.cpp new file mode 100644 index 00000000..c0e3432f --- /dev/null +++ b/src/curveutil.cpp @@ -0,0 +1,63 @@ +#include "curveutil.h" + +void hermiteCurveToPainterPath(const std::vector &hermiteControlNodes, + QPainterPath &toPath) +{ + if (!hermiteControlNodes.empty()) { + QPainterPath path(QPointF(hermiteControlNodes[0].position.x(), + hermiteControlNodes[0].position.y())); + for (size_t i = 1; i < hermiteControlNodes.size(); i++) { + const auto &beginHermite = hermiteControlNodes[i - 1]; + const auto &endHermite = hermiteControlNodes[i]; + BezierControlNode bezier(beginHermite, endHermite); + path.cubicTo(QPointF(bezier.handles[0].x(), bezier.handles[0].y()), + QPointF(bezier.handles[1].x(), bezier.handles[1].y()), + QPointF(bezier.endpoint.x(), bezier.endpoint.y())); + } + toPath = path; + } +} + +QVector2D calculateHermiteInterpolation(const std::vector &hermiteControlNodes, + float knot) +{ + if (hermiteControlNodes.size() < 2) + return QVector2D(); + + int startControlIndex = 0; + int stopControlIndex = hermiteControlNodes.size() - 1; + for (size_t i = 0; i < hermiteControlNodes.size(); i++) { + if (hermiteControlNodes[i].position.x() > knot) + break; + startControlIndex = i; + } + for (int i = (int)hermiteControlNodes.size() - 1; i >= 0; i--) { + if (hermiteControlNodes[i].position.x() < knot) + break; + stopControlIndex = i; + } + if (startControlIndex >= stopControlIndex) + return QVector2D(); + + const auto &startControlNode = hermiteControlNodes[startControlIndex]; + const auto &stopControlNode = hermiteControlNodes[stopControlIndex]; + + if (startControlNode.position.x() >= stopControlNode.position.x()) + return startControlNode.position; + + float length = (float)(stopControlNode.position.x() - startControlNode.position.x()); + float t = (knot - startControlNode.position.x()) / length; + float t2 = t * t; + float t3 = t2 * t; + float h1 = 2 * t3 - 3 * t2 + 1; + float h2 = -2 * t3 + 3 * t2; + float h3 = t3 - 2 * t2 + t; + float h4 = t3 - t2; + + QVector2D interpolatedPosition = h1 * startControlNode.position + + h2 * stopControlNode.position + + h3 * startControlNode.outTangent + + h4 * stopControlNode.inTangent; + + return interpolatedPosition; +} diff --git a/src/curveutil.h b/src/curveutil.h new file mode 100644 index 00000000..2e4b35ce --- /dev/null +++ b/src/curveutil.h @@ -0,0 +1,43 @@ +#ifndef CURVE_UTIL_H +#define CURVE_UTIL_H +#include +#include +#include +#include + +class HermiteControlNode +{ +public: + QVector2D position; + QVector2D inTangent; + QVector2D outTangent; + + HermiteControlNode(const QVector2D &p, const QVector2D &tin, const QVector2D &tout) + { + position = p; + inTangent = tin; + outTangent = tout; + } +}; + +class BezierControlNode +{ +public: + QVector2D endpoint; + QVector2D handles[2]; + + BezierControlNode(const HermiteControlNode &beginHermite, + const HermiteControlNode &endHermite) + { + endpoint = endHermite.position; + handles[0] = beginHermite.position + beginHermite.outTangent / 3; + handles[1] = endHermite.position - endHermite.inTangent / 3; + } +}; + +void hermiteCurveToPainterPath(const std::vector &hermiteControlNodes, + QPainterPath &toPath); +QVector2D calculateHermiteInterpolation(const std::vector &hermiteControlNodes, + float knot); + +#endif diff --git a/src/dust3dutil.cpp b/src/dust3dutil.cpp index 664ae3cb..f745b1dc 100644 --- a/src/dust3dutil.cpp +++ b/src/dust3dutil.cpp @@ -56,3 +56,21 @@ QString unifiedWindowTitle(const QString &text) { return text + QObject::tr(" - ") + APP_NAME; } + +// Sam Hocevar's answer +// https://gamedev.stackexchange.com/questions/98246/quaternion-slerp-and-lerp-implementation-with-overshoot +QQuaternion quaternionOvershootSlerp(const QQuaternion &q0, const QQuaternion &q1, float t) +{ + // If t is too large, divide it by two recursively + if (t > 1.0) { + auto tmp = quaternionOvershootSlerp(q0, q1, t / 2); + return tmp * q0.inverted() * tmp; + } + + // It’s easier to handle negative t this way + if (t < 0.0) + return quaternionOvershootSlerp(q1, q0, 1.0 - t); + + return QQuaternion::slerp(q0, q1, t); +} + diff --git a/src/dust3dutil.h b/src/dust3dutil.h index f9d3792c..e4f069de 100644 --- a/src/dust3dutil.h +++ b/src/dust3dutil.h @@ -18,5 +18,6 @@ QVector3D pointInHermiteCurve(float t, QVector3D p0, QVector3D m0, QVector3D p1, float angleInRangle360BetweenTwoVectors(QVector3D a, QVector3D b, QVector3D planeNormal); QVector3D projectLineOnPlane(QVector3D line, QVector3D planeNormal); QString unifiedWindowTitle(const QString &text); +QQuaternion quaternionOvershootSlerp(const QQuaternion &q0, const QQuaternion &q1, float t); #endif diff --git a/src/infolabel.cpp b/src/infolabel.cpp index 782b7c20..dc806512 100644 --- a/src/infolabel.cpp +++ b/src/infolabel.cpp @@ -15,6 +15,7 @@ InfoLabel::InfoLabel(const QString &text, QWidget *parent) : QHBoxLayout *mainLayout = new QHBoxLayout; mainLayout->addWidget(m_icon); mainLayout->addWidget(m_label); + mainLayout->addStretch(); setLayout(mainLayout); } diff --git a/src/interpolationgraphicswidget.cpp b/src/interpolationgraphicswidget.cpp new file mode 100644 index 00000000..bf8f8560 --- /dev/null +++ b/src/interpolationgraphicswidget.cpp @@ -0,0 +1,680 @@ +#include +#include +#include +#include +#include +#include +#include "interpolationgraphicswidget.h" +#include "theme.h" + +const float InterpolationGraphicsWidget::m_anchorRadius = 5; +const float InterpolationGraphicsWidget::m_handleRadius = 7; +const int InterpolationGraphicsWidget::m_gridColumns = 20; +const int InterpolationGraphicsWidget::m_gridRows = 10; +const int InterpolationGraphicsWidget::m_sceneWidth = 640; +const int InterpolationGraphicsWidget::m_sceneHeight = 480; +const float InterpolationGraphicsWidget::m_tangentMagnitudeScaleFactor = 9; + +InterpolationGraphicsWidget::InterpolationGraphicsWidget(QWidget *parent) : + QGraphicsView(parent) +{ + setRenderHint(QPainter::Antialiasing); + + setObjectName("interpolationGraphics"); + setStyleSheet("#interpolationGraphics {background: transparent}"); + + setScene(new QGraphicsScene()); + scene()->setSceneRect(QRectF(0, 0, InterpolationGraphicsWidget::m_sceneWidth, InterpolationGraphicsWidget::m_sceneHeight)); + + QColor curveColor = Theme::green; + QColor gridColor = QColor(0x19, 0x19, 0x19); + + QPen curvePen; + curvePen.setColor(curveColor); + curvePen.setWidth(3); + + QPen gridPen; + gridPen.setColor(gridColor); + gridPen.setWidth(0); + + m_cursorItem = new InterpolationGraphicsCursorItem; + m_cursorItem->setFlag(QGraphicsItem::ItemSendsScenePositionChanges, !m_previewOnly); + m_cursorItem->setRect(0, 0, 0, InterpolationGraphicsWidget::m_sceneHeight); + m_cursorItem->setFlags(m_cursorItem->flags() & ~(Qt::ItemIsEditable | Qt::ItemIsEnabled | Qt::ItemIsSelectable)); + m_cursorItem->setZValue(-1); + scene()->addItem(m_cursorItem); + + if (m_gridEnabled) { + m_gridRowLineItems.resize(m_gridRows + 1); + float rowHeight = scene()->sceneRect().height() / m_gridRows; + for (int row = 0; row <= m_gridRows; row++) { + m_gridRowLineItems[row] = new QGraphicsLineItem(); + m_gridRowLineItems[row]->setFlags(m_gridRowLineItems[row]->flags() & ~(Qt::ItemIsEditable | Qt::ItemIsEnabled | Qt::ItemIsSelectable)); + m_gridRowLineItems[row]->setPen(gridPen); + float y = scene()->sceneRect().top() + row * rowHeight; + m_gridRowLineItems[row]->setLine(scene()->sceneRect().left(), y, scene()->sceneRect().right(), y); + scene()->addItem(m_gridRowLineItems[row]); + } + m_gridColumnLineItems.resize(m_gridColumns + 1); + float columnWidth = scene()->sceneRect().width() / m_gridColumns; + for (int column = 0; column <= m_gridColumns; column++) { + m_gridColumnLineItems[column] = new QGraphicsLineItem(); + m_gridColumnLineItems[column]->setFlags(m_gridColumnLineItems[column]->flags() & ~(Qt::ItemIsEditable | Qt::ItemIsEnabled | Qt::ItemIsSelectable)); + m_gridColumnLineItems[column]->setPen(gridPen); + float x = scene()->sceneRect().left() + column * columnWidth; + m_gridColumnLineItems[column]->setLine(x, scene()->sceneRect().top(), x, scene()->sceneRect().bottom()); + scene()->addItem(m_gridColumnLineItems[column]); + } + } + + m_pathItem = new QGraphicsPathItem; + m_pathItem->setPen(curvePen); + scene()->addItem(m_pathItem); +} + +void InterpolationGraphicsWidget::toNormalizedControlNodes(std::vector &snapshot) +{ + snapshot = m_controlNodes; + for (auto &item: snapshot) { + item.position.setX(item.position.x() / scene()->sceneRect().width()); + item.position.setY(item.position.y() / scene()->sceneRect().height()); + item.inTangent.setX(item.inTangent.x() / scene()->sceneRect().width()); + item.inTangent.setY(item.inTangent.y() / scene()->sceneRect().height()); + item.outTangent.setX(item.outTangent.x() / scene()->sceneRect().width()); + item.outTangent.setY(item.outTangent.y() / scene()->sceneRect().height()); + item.inTangent *= InterpolationGraphicsWidget::m_tangentMagnitudeScaleFactor; + item.outTangent *= InterpolationGraphicsWidget::m_tangentMagnitudeScaleFactor; + } +} + +void InterpolationGraphicsWidget::fromNormalizedControlNodes(const std::vector &snapshot) +{ + m_controlNodes = snapshot; + for (auto &item: m_controlNodes) { + item.position.setX(item.position.x() * scene()->sceneRect().width()); + item.position.setY(item.position.y() * scene()->sceneRect().height()); + item.inTangent.setX(item.inTangent.x() * scene()->sceneRect().width()); + item.inTangent.setY(item.inTangent.y() * scene()->sceneRect().height()); + item.outTangent.setX(item.outTangent.x() * scene()->sceneRect().width()); + item.outTangent.setY(item.outTangent.y() * scene()->sceneRect().height()); + item.inTangent /= InterpolationGraphicsWidget::m_tangentMagnitudeScaleFactor; + item.outTangent /= InterpolationGraphicsWidget::m_tangentMagnitudeScaleFactor; + } + refresh(); +} + +void InterpolationGraphicsWidget::resizeEvent(QResizeEvent *event) +{ + QGraphicsView::resizeEvent(event); + if (scene()) + fitInView(scene()->sceneRect(), Qt::KeepAspectRatio); +} + +void InterpolationGraphicsWidget::setControlNodes(const std::vector &nodes) +{ + invalidateControlSelection(); + fromNormalizedControlNodes(nodes); + refresh(); +} + +void InterpolationGraphicsWidget::setKeyframes(const std::vector> &keyframes) +{ + invalidateKeyframeSelection(); + m_keyframes = keyframes; + refresh(); +} + +void InterpolationGraphicsWidget::refreshControlAnchor(int index) +{ + if (index >= (int)m_controlNodes.size()) + return; + + if (nullptr == m_anchorItems[index]) { + m_anchorItems[index] = new InterpolationGraphicsCircleItem(m_anchorRadius); + m_anchorItems[index]->setFlag(QGraphicsItem::ItemIsMovable, !m_previewOnly); + m_anchorItems[index]->setFlag(QGraphicsItem::ItemSendsScenePositionChanges, !m_previewOnly); + connect(m_anchorItems[index]->proxy(), &InterpolationGraphicsProxyObject::itemMoved, this, [=](QPointF point) { + QVector2D newControlPosition = QVector2D(point.x(), point.y()); + if (index - 1 >= 0 && newControlPosition.x() < m_controlNodes[index - 1].position.x()) + newControlPosition.setX(m_controlNodes[index - 1].position.x()); + else if (index + 1 <= (int)m_controlNodes.size() - 1 && newControlPosition.x() > m_controlNodes[index + 1].position.x()) + newControlPosition.setX(m_controlNodes[index + 1].position.x()); + m_controlNodes[index].position = newControlPosition; + refreshCurve(); + refreshControlNode(index); + refreshKeyframes(); + emit controlNodesChanged(); + }); + connect(m_anchorItems[index]->proxy(), &InterpolationGraphicsProxyObject::itemHoverEnter, this, [=]() { + hoverControlNode(index); + }); + connect(m_anchorItems[index]->proxy(), &InterpolationGraphicsProxyObject::itemHoverLeave, this, [=]() { + unhoverControlNode(index); + }); + scene()->addItem(m_anchorItems[index]); + } + + const auto &controlNode = m_controlNodes[index]; + m_anchorItems[index]->setOrigin(QPointF(controlNode.position.x(), controlNode.position.y())); +} + +void InterpolationGraphicsWidget::refreshControlEdges(int index) +{ + if (index >= (int)m_controlNodes.size()) + return; + + if (nullptr == m_lineItems[index].first) { + Q_ASSERT(nullptr == m_lineItems[index].second); + m_lineItems[index] = std::make_pair(new InterpolationGraphicsEdgeItem(), + new InterpolationGraphicsEdgeItem()); + scene()->addItem(m_lineItems[index].first); + scene()->addItem(m_lineItems[index].second); + } + + const auto &controlNode = m_controlNodes[index]; + + QVector2D inHandlePosition = controlNode.position + controlNode.inTangent; + m_lineItems[index].first->setLine(controlNode.position.x(), controlNode.position.y(), + inHandlePosition.x(), inHandlePosition.y()); + + QVector2D outHandlePosition = controlNode.position - controlNode.outTangent; + m_lineItems[index].second->setLine(controlNode.position.x(), controlNode.position.y(), + outHandlePosition.x(), outHandlePosition.y()); +} + +void InterpolationGraphicsWidget::refreshControlHandles(int index) +{ + if (index >= (int)m_controlNodes.size()) + return; + + if (nullptr == m_handleItems[index].first) { + Q_ASSERT(nullptr == m_handleItems[index].second); + m_handleItems[index] = std::make_pair(new InterpolationGraphicsCircleItem(m_handleRadius, false), + new InterpolationGraphicsCircleItem(m_handleRadius, false)); + m_handleItems[index].first->setFlag(QGraphicsItem::ItemIsMovable, !m_previewOnly); + m_handleItems[index].first->setFlag(QGraphicsItem::ItemSendsScenePositionChanges, !m_previewOnly); + m_handleItems[index].second->setFlag(QGraphicsItem::ItemIsMovable, !m_previewOnly); + m_handleItems[index].second->setFlag(QGraphicsItem::ItemSendsScenePositionChanges, !m_previewOnly); + connect(m_handleItems[index].first->proxy(), &InterpolationGraphicsProxyObject::itemMoved, this, [=](QPointF point) { + auto newHandlePosition = QVector2D(point.x(), point.y()); + m_controlNodes[index].inTangent = newHandlePosition - m_controlNodes[index].position; + refreshCurve(); + refreshControlEdges(index); + refreshKeyframes(); + emit controlNodesChanged(); + }); + connect(m_handleItems[index].second->proxy(), &InterpolationGraphicsProxyObject::itemMoved, this, [=](QPointF point) { + auto newHandlePosition = QVector2D(point.x(), point.y()); + m_controlNodes[index].outTangent = m_controlNodes[index].position - newHandlePosition; + refreshCurve(); + refreshControlEdges(index); + refreshKeyframes(); + emit controlNodesChanged(); + }); + connect(m_handleItems[index].first->proxy(), &InterpolationGraphicsProxyObject::itemHoverEnter, this, [=]() { + hoverControlNode(index); + }); + connect(m_handleItems[index].second->proxy(), &InterpolationGraphicsProxyObject::itemHoverEnter, this, [=]() { + hoverControlNode(index); + }); + connect(m_handleItems[index].first->proxy(), &InterpolationGraphicsProxyObject::itemHoverLeave, this, [=]() { + unhoverControlNode(index); + }); + connect(m_handleItems[index].second->proxy(), &InterpolationGraphicsProxyObject::itemHoverLeave, this, [=]() { + unhoverControlNode(index); + }); + scene()->addItem(m_handleItems[index].first); + scene()->addItem(m_handleItems[index].second); + } + + const auto &controlNode = m_controlNodes[index]; + + QVector2D inHandlePosition = controlNode.position + controlNode.inTangent; + m_handleItems[index].first->setOrigin(QPointF(inHandlePosition.x(), inHandlePosition.y())); + + QVector2D outHandlePosition = controlNode.position - controlNode.outTangent; + m_handleItems[index].second->setOrigin(QPointF(outHandlePosition.x(), outHandlePosition.y())); +} + +void InterpolationGraphicsWidget::refreshControlNode(int index) +{ + if (index >= (int)m_controlNodes.size()) + return; + + refreshControlAnchor(index); + refreshControlEdges(index); + refreshControlHandles(index); +} + +void InterpolationGraphicsWidget::refreshCurve() +{ + QPainterPath path; + std::vector controlNodes; + toNormalizedControlNodes(controlNodes); + for (auto &item: controlNodes) { + item.position.setX(item.position.x() * scene()->sceneRect().width()); + item.position.setY(item.position.y() * scene()->sceneRect().height()); + item.inTangent.setX(item.inTangent.x() * scene()->sceneRect().width()); + item.inTangent.setY(item.inTangent.y() * scene()->sceneRect().height()); + item.outTangent.setX(item.outTangent.x() * scene()->sceneRect().width()); + item.outTangent.setY(item.outTangent.y() * scene()->sceneRect().height()); + } + hermiteCurveToPainterPath(controlNodes, path); + m_pathItem->setPath(path); +} + +void InterpolationGraphicsWidget::refreshKeyframeKnot(int index) +{ + if (index >= (int)m_keyframes.size()) + return; + + std::vector controlNodes; + toNormalizedControlNodes(controlNodes); + if (controlNodes.size() < 2) + return; + + if (scene()->sceneRect().width() <= 0) + return; + + const auto &knot = m_keyframes[index].first; + QVector2D interpolatedPosition = calculateHermiteInterpolation(controlNodes, knot); + + if (nullptr == m_keyframeKnotItems[index]) { + m_keyframeKnotItems[index] = new InterpolationGraphicsKeyframeItem; + scene()->addItem(m_keyframeKnotItems[index]); + } + + QPointF knotPos = QPointF(interpolatedPosition.x() * scene()->sceneRect().width(), + interpolatedPosition.y() * scene()->sceneRect().height()); + m_keyframeKnotItems[index]->setPos(knotPos); +} + +void InterpolationGraphicsWidget::refreshKeyframeLabel(int index) +{ + if (index >= (int)m_keyframes.size()) + return; + + std::vector controlNodes; + toNormalizedControlNodes(controlNodes); + if (controlNodes.size() < 2) + return; + + if (scene()->sceneRect().width() <= 0) + return; + + const auto &knot = m_keyframes[index].first; + QVector2D interpolatedPosition = calculateHermiteInterpolation(controlNodes, knot); + + if (nullptr == m_keyframeNameItems[index]) { + InterpolationGraphicsLabelParentWidget *parentWidget = new InterpolationGraphicsLabelParentWidget; + parentWidget->setFlag(QGraphicsItem::ItemIsMovable, !m_previewOnly); + parentWidget->setFlag(QGraphicsItem::ItemSendsScenePositionChanges, !m_previewOnly); + parentWidget->setAutoFillBackground(true); + parentWidget->setZValue(1); + scene()->addItem(parentWidget); + + QLabel *nameLabel = new QLabel; + nameLabel->setFocusPolicy(Qt::NoFocus); + m_keyframeNameItems[index] = scene()->addWidget(nameLabel); + m_keyframeNameItems[index]->setParentItem(parentWidget); + connect(parentWidget->proxy(), &InterpolationGraphicsProxyObject::itemMoved, this, [=](QPointF point) { + QVector2D newKnotPosition = QVector2D(point.x(), point.y()); + float knot = point.x() / scene()->sceneRect().width(); + if (index - 1 >= 0 && knot < m_keyframes[index - 1].first) + newKnotPosition.setX(m_keyframes[index - 1].first * scene()->sceneRect().width()); + else if (index + 1 <= (int)m_keyframes.size() - 1 && knot > m_keyframes[index + 1].first) + newKnotPosition.setX(m_keyframes[index + 1].first * scene()->sceneRect().width()); + knot = newKnotPosition.x() / scene()->sceneRect().width(); + m_keyframes[index].first = knot; + refreshKeyframe(index); + emit keyframeKnotChanged(index, knot); + }); + connect(parentWidget->proxy(), &InterpolationGraphicsProxyObject::itemHoverEnter, this, [=]() { + hoverKeyframe(index); + }); + connect(parentWidget->proxy(), &InterpolationGraphicsProxyObject::itemHoverLeave, this, [=]() { + unhoverKeyframe(index); + }); + } + + QPointF knotPos = QPointF(interpolatedPosition.x() * scene()->sceneRect().width(), + interpolatedPosition.y() * scene()->sceneRect().height()); + + QLabel *nameLabel = (QLabel *)m_keyframeNameItems[index]->widget(); + nameLabel->setText(m_keyframes[index].second); + nameLabel->adjustSize(); + QPointF textPos(knotPos.x(), knotPos.y()); + InterpolationGraphicsLabelParentWidget *parentWidget = (InterpolationGraphicsLabelParentWidget *)(m_keyframeNameItems[index]->parentItem()); + QSize sizeHint = nameLabel->sizeHint(); + nameLabel->resize(sizeHint); + parentWidget->resize(sizeHint); + parentWidget->setPosWithoutEmitSignal(textPos); +} + +void InterpolationGraphicsWidget::refreshKeyframe(int index) +{ + refreshKeyframeKnot(index); + refreshKeyframeLabel(index); +} + +void InterpolationGraphicsWidget::refresh() +{ + refreshCurve(); + + for (size_t i = m_controlNodes.size(); i < m_anchorItems.size(); i++) + delete m_anchorItems[i]; + m_anchorItems.resize(m_controlNodes.size()); + + for (size_t i = m_controlNodes.size(); i < m_lineItems.size(); i++) { + delete m_lineItems[i].first; + delete m_lineItems[i].second; + } + m_lineItems.resize(m_controlNodes.size()); + + for (size_t i = m_controlNodes.size(); i < m_handleItems.size(); i++) { + delete m_handleItems[i].first; + delete m_handleItems[i].second; + } + m_handleItems.resize(m_controlNodes.size()); + + for (size_t i = 0; i < m_controlNodes.size(); i++) + refreshControlNode(i); + + for (size_t i = m_keyframes.size(); i < m_keyframeKnotItems.size(); i++) { + delete m_keyframeKnotItems[i]; + delete m_keyframeNameItems[i]->parentItem(); + } + m_keyframeKnotItems.resize(m_keyframes.size()); + m_keyframeNameItems.resize(m_keyframes.size()); + + refreshKeyframes(); + + fitInView(scene()->sceneRect(), Qt::KeepAspectRatio); +} + +float InterpolationGraphicsWidget::cursorKnot() const +{ + return m_cursorItem->pos().x() / scene()->sceneRect().width(); +} + +void InterpolationGraphicsWidget::refreshKeyframes() +{ + for (size_t i = 0; i < m_keyframes.size(); i++) + refreshKeyframe(i); +} + +void InterpolationGraphicsWidget::mouseMoveEvent(QMouseEvent *event) +{ + QGraphicsView::mouseMoveEvent(event); +} + +void InterpolationGraphicsWidget::mouseReleaseEvent(QMouseEvent *event) +{ + QGraphicsView::mouseReleaseEvent(event); +} + +void InterpolationGraphicsWidget::mousePressEvent(QMouseEvent *event) +{ + QGraphicsView::mousePressEvent(event); + + if (m_previewOnly) + return; + + if (event->button() == Qt::LeftButton) { + auto cursorPos = mapToScene(event->pos()); + cursorPos.setY(0); + m_cursorItem->updateCursorPosition(cursorPos); + } + + if (event->button() == Qt::LeftButton || + event->button() == Qt::RightButton) { + selectControlNode(m_currentHoveringControlIndex); + selectKeyframe(m_currentHoveringKeyframeIndex); + } + + if (event->button() == Qt::RightButton) { + showContextMenu(mapFromGlobal(event->globalPos())); + } +} + +void InterpolationGraphicsWidget::deleteControlNode(int index) +{ + if (index >= (int)m_controlNodes.size()) + return; + + invalidateControlSelection(); + + m_controlNodes.erase(m_controlNodes.begin() + index); + refresh(); + + emit controlNodesChanged(); +} + +void InterpolationGraphicsWidget::deleteKeyframe(int index) +{ + if (index >= (int)m_keyframes.size()) + return; + + emit removeKeyframe(index); +} + +void InterpolationGraphicsWidget::addControlNodeAfter(int index) +{ + if (index >= (int)m_controlNodes.size()) + return; + + invalidateControlSelection(); + + const auto ¤t = m_controlNodes[index]; + int newX = current.position.x(); + auto inTangent = current.outTangent; + if (index + 1 >= (int)m_controlNodes.size()) { + newX = (current.position.x() + scene()->sceneRect().right()) / 2; + } else { + const auto &next = m_controlNodes[index + 1]; + newX = (current.position.x() + next.position.x()) / 2; + inTangent = (current.outTangent + next.inTangent) / 2; + } + auto outTangent = inTangent; + + HermiteControlNode newNode(QVector2D(newX, current.position.y()), inTangent, outTangent); + m_controlNodes.insert(m_controlNodes.begin() + index + 1, newNode); + refresh(); + + emit controlNodesChanged(); +} + +void InterpolationGraphicsWidget::addControlNodeBefore(int index) +{ + if (index >= (int)m_controlNodes.size()) + return; + + invalidateControlSelection(); + + const auto ¤t = m_controlNodes[index]; + int newX = current.position.x(); + auto inTangent = current.outTangent; + if (index - 1 < 0) { + newX = (current.position.x() + scene()->sceneRect().left()) / 2; + } else { + const auto &previous= m_controlNodes[index - 1]; + newX = (current.position.x() + previous.position.x()) / 2; + inTangent = (current.inTangent + previous.outTangent) / 2; + } + auto outTangent = inTangent; + + HermiteControlNode newNode(QVector2D(newX, current.position.y()), inTangent, outTangent); + m_controlNodes.insert(m_controlNodes.begin() + index, newNode); + refresh(); + + emit controlNodesChanged(); +} + +void InterpolationGraphicsWidget::invalidateControlSelection() +{ + m_currentHoveringControlIndex = -1; + if (-1 != m_currentSelectedControlIndex) + selectControlNode(-1); +} + +void InterpolationGraphicsWidget::invalidateKeyframeSelection() +{ + m_currentHoveringKeyframeIndex = -1; + if (-1 != m_currentSelectedKeyframeIndex) + selectKeyframe(-1); +} + +void InterpolationGraphicsWidget::addControlNodeAtPosition(const QPointF &pos) +{ + auto inTangent = QVector2D(0, 50); + auto outTangent = inTangent; + HermiteControlNode newNode(QVector2D(pos.x(), pos.y()), inTangent, outTangent); + + invalidateControlSelection(); + + m_controlNodes.insert(m_controlNodes.end(), newNode); + refresh(); + + emit controlNodesChanged(); +} + +void InterpolationGraphicsWidget::hoverControlNode(int index) +{ + if (index >= (int)m_controlNodes.size()) + return; + + if (m_currentHoveringControlIndex == index) + return; + + m_currentHoveringControlIndex = index; +} + +void InterpolationGraphicsWidget::unhoverControlNode(int index) +{ + if (index >= (int)m_controlNodes.size()) + return; + + if (m_currentHoveringControlIndex == index) { + m_currentHoveringControlIndex = -1; + } +} + +void InterpolationGraphicsWidget::hoverKeyframe(int index) +{ + if (index >= (int)m_keyframes.size()) + return; + + if (m_currentHoveringKeyframeIndex == index) + return; + + m_currentHoveringKeyframeIndex = index; + + QLabel *nameLabel = (QLabel *)m_keyframeNameItems[m_currentHoveringKeyframeIndex]->widget(); + nameLabel->setStyleSheet("QLabel {color : " + Theme::red.name() + ";}"); +} + +void InterpolationGraphicsWidget::unhoverKeyframe(int index) +{ + if (index >= (int)m_keyframes.size()) + return; + + if (m_currentHoveringKeyframeIndex == index) { + if (m_currentSelectedKeyframeIndex != m_currentHoveringKeyframeIndex) { + QLabel *nameLabel = (QLabel *)m_keyframeNameItems[m_currentHoveringKeyframeIndex]->widget(); + nameLabel->setStyleSheet("QLabel {color : " + Theme::white.name() + ";}"); + } + m_currentHoveringKeyframeIndex = -1; + } +} + +void InterpolationGraphicsWidget::selectKeyframe(int index) +{ + if (m_currentSelectedKeyframeIndex == index) + return; + + if (-1 != m_currentSelectedKeyframeIndex && m_currentSelectedKeyframeIndex < (int)m_keyframes.size()) { + QLabel *nameLabel = (QLabel *)m_keyframeNameItems[m_currentSelectedKeyframeIndex]->widget(); + nameLabel->setStyleSheet("QLabel {color : " + Theme::white.name() + ";}"); + } + + m_currentSelectedKeyframeIndex = index; + + if (-1 != m_currentSelectedKeyframeIndex && m_currentSelectedKeyframeIndex < (int)m_keyframes.size()) { + QLabel *nameLabel = (QLabel *)m_keyframeNameItems[m_currentSelectedKeyframeIndex]->widget(); + nameLabel->setStyleSheet("QLabel {color : " + Theme::red.name() + ";}"); + } +} + +void InterpolationGraphicsWidget::selectControlNode(int index) +{ + if (m_currentSelectedControlIndex == index) + return; + + if (-1 != m_currentSelectedControlIndex && m_currentSelectedControlIndex < (int)m_controlNodes.size()) { + m_anchorItems[m_currentSelectedControlIndex]->setChecked(false); + m_handleItems[m_currentSelectedControlIndex].first->setChecked(false); + m_handleItems[m_currentSelectedControlIndex].second->setChecked(false); + } + + m_currentSelectedControlIndex = index; + + if (-1 != m_currentSelectedControlIndex && m_currentSelectedControlIndex < (int)m_controlNodes.size()) { + m_anchorItems[m_currentSelectedControlIndex]->setChecked(true); + m_handleItems[m_currentSelectedControlIndex].first->setChecked(true); + m_handleItems[m_currentSelectedControlIndex].second->setChecked(true); + } +} + +void InterpolationGraphicsWidget::showContextMenu(const QPoint &pos) +{ + if (m_previewOnly) + return; + + QMenu contextMenu(this); + + QAction addAction(tr("Add Control Node"), this); + QAction addAfterAction(tr("Add Control Node After"), this); + QAction addBeforeAction(tr("Add Control Node Before"), this); + if (-1 != m_currentSelectedControlIndex) { + connect(&addAfterAction, &QAction::triggered, this, [=]() { + addControlNodeAfter(m_currentSelectedControlIndex); + }); + contextMenu.addAction(&addAfterAction); + + connect(&addBeforeAction, &QAction::triggered, this, [=]() { + addControlNodeBefore(m_currentSelectedControlIndex); + }); + contextMenu.addAction(&addBeforeAction); + } else if (m_controlNodes.empty()) { + connect(&addAction, &QAction::triggered, this, [=]() { + addControlNodeAtPosition(mapToScene(pos)); + }); + contextMenu.addAction(&addAction); + } + + QAction deleteControlNodeAction(tr("Delete Control Node"), this); + if (-1 != m_currentSelectedControlIndex) { + connect(&deleteControlNodeAction, &QAction::triggered, this, [=]() { + deleteControlNode(m_currentSelectedControlIndex); + }); + contextMenu.addAction(&deleteControlNodeAction); + } + + QAction deleteKeyframeAction(tr("Delete Keyframe"), this); + if (-1 != m_currentSelectedKeyframeIndex) { + connect(&deleteKeyframeAction, &QAction::triggered, this, [=]() { + deleteKeyframe(m_currentSelectedKeyframeIndex); + }); + contextMenu.addAction(&deleteKeyframeAction); + } + + contextMenu.exec(mapToGlobal(pos)); +} + +void InterpolationGraphicsWidget::setPreviewOnly(bool previewOnly) +{ + if (m_previewOnly == previewOnly) + return; + m_previewOnly = previewOnly; + m_cursorItem->setVisible(!m_previewOnly); +} diff --git a/src/interpolationgraphicswidget.h b/src/interpolationgraphicswidget.h new file mode 100644 index 00000000..041b003f --- /dev/null +++ b/src/interpolationgraphicswidget.h @@ -0,0 +1,302 @@ +#ifndef INTERPOLATION_GRAPHICS_WIDGET_H +#define INTERPOLATION_GRAPHICS_WIDGET_H +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "curveutil.h" +#include "theme.h" + +class InterpolationGraphicsProxyObject : public QObject +{ + Q_OBJECT +signals: + void itemMoved(QPointF point); + void itemHoverEnter(); + void itemHoverLeave(); +}; + +class InterpolationGraphicsCircleItem : public QGraphicsEllipseItem +{ +public: + InterpolationGraphicsCircleItem(float toRadius, bool limitMoveRange=true) : + m_limitMoveRange(limitMoveRange) + { + setAcceptHoverEvents(true); + setRect(QRectF(-toRadius, -toRadius, toRadius * 2, toRadius * 2)); + updateAppearance(); + } + ~InterpolationGraphicsCircleItem() + { + delete m_object; + } + InterpolationGraphicsProxyObject *proxy() + { + return m_object; + } + void setOrigin(QPointF point) + { + m_disableEmitSignal = true; + setPos(point.x(), point.y()); + m_disableEmitSignal = false; + } + QPointF origin() + { + return QPointF(pos().x(), pos().y()); + } + float radius() + { + return rect().width() / 2; + } + void setChecked(bool checked) + { + m_checked = checked; + updateAppearance(); + } +protected: + virtual QVariant itemChange(GraphicsItemChange change, const QVariant &value) override + { + switch (change) { + case ItemPositionHasChanged: + { + QPointF newPos = value.toPointF(); + if (m_limitMoveRange) { + QRectF rect = scene()->sceneRect(); + if (!rect.contains(newPos)) { + newPos.setX(qMin(rect.right(), qMax(newPos.x(), rect.left()))); + newPos.setY(qMin(rect.bottom(), qMax(newPos.y(), rect.top()))); + } + } + if (!m_disableEmitSignal) + emit m_object->itemMoved(newPos); + return newPos; + } + break; + default: + break; + }; + return QGraphicsItem::itemChange(change, value); + } + virtual void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override + { + QGraphicsItem::hoverEnterEvent(event); + m_hovered = true; + updateAppearance(); + emit m_object->itemHoverEnter(); + } + virtual void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override + { + QGraphicsItem::hoverLeaveEvent(event); + m_hovered = false; + updateAppearance(); + emit m_object->itemHoverLeave(); + } +private: + InterpolationGraphicsProxyObject *m_object = new InterpolationGraphicsProxyObject(); + bool m_hovered = false; + bool m_checked = false; + bool m_limitMoveRange = true; + bool m_disableEmitSignal = false; + void updateAppearance() + { + QColor color = Theme::white; + if (m_hovered || m_checked) { + setBrush(color); + } else { + setBrush(Qt::transparent); + } + QPen pen; + pen.setWidth(0); + pen.setColor(color); + setPen(pen); + } +}; + +class InterpolationGraphicsEdgeItem : public QGraphicsLineItem +{ +public: + InterpolationGraphicsEdgeItem() + { + QPen linePen; + linePen.setColor(Theme::white); + linePen.setWidth(0); + setPen(linePen); + } +}; + +class InterpolationGraphicsLabelParentWidget : public QGraphicsWidget +{ +public: + InterpolationGraphicsLabelParentWidget() + { + setAcceptHoverEvents(true); + } + ~InterpolationGraphicsLabelParentWidget() + { + delete m_object; + } + InterpolationGraphicsProxyObject *proxy() + { + return m_object; + } + void setPosWithoutEmitSignal(QPointF pos) + { + m_disableEmitSignal = true; + setPos(pos); + m_disableEmitSignal = false; + } +protected: + virtual QVariant itemChange(GraphicsItemChange change, const QVariant &value) override + { + switch (change) { + case ItemPositionHasChanged: + { + QPointF newPos = value.toPointF(); + QRectF rect = scene()->sceneRect(); + if (!rect.contains(newPos)) { + newPos.setX(qMin(rect.right(), qMax(newPos.x(), rect.left()))); + newPos.setY(qMin(rect.bottom(), qMax(newPos.y(), rect.top()))); + } + if (!m_disableEmitSignal) + emit m_object->itemMoved(newPos); + return newPos; + } + break; + default: + break; + }; + return QGraphicsItem::itemChange(change, value); + } + virtual void hoverEnterEvent(QGraphicsSceneHoverEvent *event) override + { + QGraphicsWidget::hoverEnterEvent(event); + emit m_object->itemHoverEnter(); + } + virtual void hoverLeaveEvent(QGraphicsSceneHoverEvent *event) override + { + QGraphicsWidget::hoverLeaveEvent(event); + emit m_object->itemHoverLeave(); + } +private: + InterpolationGraphicsProxyObject *m_object = new InterpolationGraphicsProxyObject(); + bool m_disableEmitSignal = false; +}; + +class InterpolationGraphicsCursorItem : public QGraphicsRectItem +{ +public: + InterpolationGraphicsCursorItem() + { + QPen linePen; + linePen.setColor(Theme::red); + linePen.setWidth(0); + setPen(linePen); + } + void updateCursorPosition(const QPointF &pos) + { + QPointF newPos = pos; + QRectF rect = scene()->sceneRect(); + if (!rect.contains(newPos)) { + newPos.setX(qMin(rect.right(), qMax(newPos.x(), rect.left()))); + newPos.setY(qMin(rect.bottom(), qMax(newPos.y(), rect.top()))); + } + setPos(newPos); + } +}; + +class InterpolationGraphicsKeyframeItem : public QGraphicsRectItem +{ +public: + InterpolationGraphicsKeyframeItem() + { + QPen linePen; + linePen.setColor(Theme::red); + linePen.setWidth(0); + setPen(linePen); + + setBrush(QBrush(Theme::red)); + + setRect(-4, -4, 8, 8); + } +}; + +class InterpolationGraphicsWidget : public QGraphicsView +{ + Q_OBJECT +signals: + void controlNodesChanged(); + void keyframeKnotChanged(int index, float knot); + void removeKeyframe(int index); +public: + InterpolationGraphicsWidget(QWidget *parent=nullptr); + void toNormalizedControlNodes(std::vector &snapshot); + void fromNormalizedControlNodes(const std::vector &snapshot); + float cursorKnot() const; +public slots: + void setControlNodes(const std::vector &nodes); + void setKeyframes(const std::vector> &keyframes); + void refresh(); + void refreshControlNode(int index); + void refreshControlAnchor(int index); + void refreshControlEdges(int index); + void refreshControlHandles(int index); + void refreshCurve(); + void refreshKeyframe(int index); + void refreshKeyframeKnot(int index); + void refreshKeyframeLabel(int index); + void refreshKeyframes(); + void showContextMenu(const QPoint &pos); + void deleteControlNode(int index); + void deleteKeyframe(int index); + void addControlNodeAfter(int index); + void addControlNodeBefore(int index); + void addControlNodeAtPosition(const QPointF &pos); + void hoverControlNode(int index); + void unhoverControlNode(int index); + void selectControlNode(int index); + void selectKeyframe(int index); + void hoverKeyframe(int index); + void unhoverKeyframe(int index); + void invalidateControlSelection(); + void invalidateKeyframeSelection(); + void setPreviewOnly(bool previewOnly); +protected: + virtual void resizeEvent(QResizeEvent *event) override; + virtual void mouseMoveEvent(QMouseEvent *event) override; + virtual void mouseReleaseEvent(QMouseEvent *event) override; + virtual void mousePressEvent(QMouseEvent *event) override; +private: + std::vector m_controlNodes; + std::vector> m_keyframes; + QGraphicsPathItem *m_pathItem = nullptr; + std::vector m_anchorItems; + std::vector> m_lineItems; + std::vector> m_handleItems; + std::vector m_gridRowLineItems; + std::vector m_gridColumnLineItems; + std::vector m_keyframeKnotItems; + std::vector m_keyframeNameItems; + InterpolationGraphicsCursorItem *m_cursorItem = nullptr; + bool m_gridEnabled = true; + int m_currentHoveringControlIndex = -1; + int m_currentSelectedControlIndex = -1; + int m_currentHoveringKeyframeIndex = -1; + int m_currentSelectedKeyframeIndex = -1; + bool m_previewOnly = false; + static const float m_anchorRadius; + static const float m_handleRadius; + static const int m_gridColumns; + static const int m_gridRows; + static const int m_sceneWidth; + static const int m_sceneHeight; + static const float m_tangentMagnitudeScaleFactor; +}; + +#endif diff --git a/src/jointnodetree.cpp b/src/jointnodetree.cpp index 0dd31d01..383b47bf 100644 --- a/src/jointnodetree.cpp +++ b/src/jointnodetree.cpp @@ -1,4 +1,5 @@ #include "jointnodetree.h" +#include "dust3dutil.h" const std::vector &JointNodeTree::nodes() const { @@ -72,3 +73,15 @@ JointNodeTree::JointNodeTree(const std::vector *resultRigBones) node.inverseBindMatrix = node.transformMatrix.inverted(); } } + +JointNodeTree JointNodeTree::slerp(const JointNodeTree &first, const JointNodeTree &second, float t) +{ + JointNodeTree slerpResult = first; + for (decltype(first.nodes().size()) i = 0; i < first.nodes().size() && i < second.nodes().size(); i++) { + slerpResult.updateRotation(i, + quaternionOvershootSlerp(first.nodes()[i].rotation, second.nodes()[i].rotation, t)); + } + slerpResult.recalculateTransformMatrices(); + return slerpResult; +} + diff --git a/src/jointnodetree.h b/src/jointnodetree.h index 41ee9a81..b7484ac6 100644 --- a/src/jointnodetree.h +++ b/src/jointnodetree.h @@ -25,6 +25,7 @@ public: void updateRotation(int index, QQuaternion rotation); void reset(); void recalculateTransformMatrices(); + static JointNodeTree slerp(const JointNodeTree &first, const JointNodeTree &second, float t); private: std::vector m_boneNodes; }; diff --git a/src/motioneditwidget.cpp b/src/motioneditwidget.cpp new file mode 100644 index 00000000..2c1bead0 --- /dev/null +++ b/src/motioneditwidget.cpp @@ -0,0 +1,298 @@ +#include +#include +#include +#include +#include +#include "motioneditwidget.h" +#include "dust3dutil.h" +#include "poselistwidget.h" +#include "version.h" + +MotionEditWidget::MotionEditWidget(const SkeletonDocument *document, QWidget *parent) : + QDialog(parent), + m_document(document) +{ + m_clipPlayer = new AnimationClipPlayer; + + m_graphicsWidget = new InterpolationGraphicsWidget(this); + m_graphicsWidget->setMinimumSize(256, 128); + + connect(m_graphicsWidget, &InterpolationGraphicsWidget::controlNodesChanged, this, &MotionEditWidget::setUnsavedState); + connect(m_graphicsWidget, &InterpolationGraphicsWidget::controlNodesChanged, this, &MotionEditWidget::generatePreviews); + connect(m_graphicsWidget, &InterpolationGraphicsWidget::removeKeyframe, this, [=](int index) { + m_keyframes.erase(m_keyframes.begin() + index); + syncKeyframesToGraphicsView(); + setUnsavedState(); + generatePreviews(); + }); + connect(m_graphicsWidget, &InterpolationGraphicsWidget::keyframeKnotChanged, this, [=](int index, float knot) { + m_keyframes[index].first = knot; + setUnsavedState(); + generatePreviews(); + }); + + m_previewWidget = new ModelWidget(this); + m_previewWidget->setMinimumSize(128, 128); + m_previewWidget->resize(384, 384); + m_previewWidget->move(-64, 0); + + connect(m_clipPlayer, &AnimationClipPlayer::frameReadyToShow, this, [=]() { + m_previewWidget->updateMesh(m_clipPlayer->takeFrameMesh()); + }); + + PoseListWidget *poseListWidget = new PoseListWidget(document); + poseListWidget->setCornerButtonVisible(true); + poseListWidget->setHasContextMenu(false); + QWidget *poseListContainerWidget = new QWidget; + QGridLayout *poseListLayoutForContainer = new QGridLayout; + poseListLayoutForContainer->addWidget(poseListWidget); + poseListContainerWidget->setLayout(poseListLayoutForContainer); + + poseListContainerWidget->resize(512, Theme::posePreviewImageSize); + + connect(poseListWidget, &PoseListWidget::cornerButtonClicked, this, [=](QUuid poseId) { + addKeyframe(m_graphicsWidget->cursorKnot(), poseId); + }); + + //QLabel *interpolationTypeItemName = new QLabel(tr("Interpolation:")); + //QLabel *interpolationTypeValue = new QLabel(tr("Spring")); + + //QPushButton *interpolationTypeButton = new QPushButton(); + //interpolationTypeButton->setText(tr("Browse...")); + + //QHBoxLayout *interpolationButtonsLayout = new QHBoxLayout; + //interpolationButtonsLayout->addStretch(); + //interpolationButtonsLayout->addWidget(interpolationTypeItemName); + //interpolationButtonsLayout->addWidget(interpolationTypeValue); + //interpolationButtonsLayout->addWidget(interpolationTypeButton); + //interpolationButtonsLayout->addStretch(); + + QVBoxLayout *motionEditLayout = new QVBoxLayout; + motionEditLayout->addWidget(poseListContainerWidget); + motionEditLayout->addWidget(Theme::createHorizontalLineWidget()); + motionEditLayout->addWidget(m_graphicsWidget); + //motionEditLayout->addLayout(interpolationButtonsLayout); + + QHBoxLayout *topLayout = new QHBoxLayout; + topLayout->setContentsMargins(256, 0, 0, 0); + topLayout->addWidget(Theme::createVerticalLineWidget()); + topLayout->addLayout(motionEditLayout); + + m_nameEdit = new QLineEdit; + connect(m_nameEdit, &QLineEdit::textChanged, this, &MotionEditWidget::setUnsavedState); + QPushButton *saveButton = new QPushButton(tr("Save")); + connect(saveButton, &QPushButton::clicked, this, &MotionEditWidget::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(topLayout); + mainLayout->addWidget(Theme::createHorizontalLineWidget()); + mainLayout->addLayout(baseInfoLayout); + + setLayout(mainLayout); + + connect(this, &MotionEditWidget::addMotion, m_document, &SkeletonDocument::addMotion); + connect(this, &MotionEditWidget::renameMotion, m_document, &SkeletonDocument::renameMotion); + connect(this, &MotionEditWidget::setMotionControlNodes, m_document, &SkeletonDocument::setMotionControlNodes); + connect(this, &MotionEditWidget::setMotionKeyframes, m_document, &SkeletonDocument::setMotionKeyframes); + + updateTitle(); +} + +MotionEditWidget::~MotionEditWidget() +{ + delete m_clipPlayer; +} + +QSize MotionEditWidget::sizeHint() const +{ + return QSize(800, 600); +} + +void MotionEditWidget::reject() +{ + close(); +} + +void MotionEditWidget::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 (nullptr != m_previewsGenerator) { + event->ignore(); + return; + } + event->accept(); +} + +void MotionEditWidget::save() +{ + std::vector controlNodes; + m_graphicsWidget->toNormalizedControlNodes(controlNodes); + if (m_motionId.isNull()) { + emit addMotion(m_nameEdit->text(), controlNodes, m_keyframes); + } else if (m_unsaved) { + emit renameMotion(m_motionId, m_nameEdit->text()); + emit setMotionControlNodes(m_motionId, controlNodes); + emit setMotionKeyframes(m_motionId, m_keyframes); + } + m_unsaved = false; + close(); +} + +void MotionEditWidget::clearUnsaveState() +{ + m_unsaved = false; + updateTitle(); +} + +void MotionEditWidget::setUnsavedState() +{ + m_unsaved = true; + updateTitle(); +} + +void MotionEditWidget::setEditMotionId(QUuid motionId) +{ + if (m_motionId == motionId) + return; + + m_motionId = motionId; + updateTitle(); +} + +void MotionEditWidget::setEditMotionName(QString name) +{ + m_nameEdit->setText(name); + updateTitle(); +} + +void MotionEditWidget::updateTitle() +{ + if (m_motionId.isNull()) { + setWindowTitle(unifiedWindowTitle(tr("New") + (m_unsaved ? "*" : ""))); + return; + } + const SkeletonMotion *motion = m_document->findMotion(m_motionId); + if (nullptr == motion) { + qDebug() << "Find motion failed:" << m_motionId; + return; + } + setWindowTitle(unifiedWindowTitle(motion->name + (m_unsaved ? "*" : ""))); +} + +void MotionEditWidget::setEditMotionControlNodes(std::vector controlNodes) +{ + m_graphicsWidget->setControlNodes(controlNodes); +} + +void MotionEditWidget::syncKeyframesToGraphicsView() +{ + std::vector> keyframesForGraphicsView; + for (const auto &frame: m_keyframes) { + QString poseName; + const SkeletonPose *pose = m_document->findPose(frame.second); + if (nullptr == pose) { + qDebug() << "Find pose failed:" << frame.second; + } else { + poseName = pose->name; + } + keyframesForGraphicsView.push_back({frame.first, poseName}); + } + m_graphicsWidget->setKeyframes(keyframesForGraphicsView); +} + +void MotionEditWidget::setEditMotionKeyframes(std::vector> keyframes) +{ + m_keyframes = keyframes; + syncKeyframesToGraphicsView(); +} + +void MotionEditWidget::addKeyframe(float knot, QUuid poseId) +{ + m_keyframes.push_back({knot, poseId}); + std::sort(m_keyframes.begin(), m_keyframes.end(), + [](const std::pair &first, const std::pair &second) { + return first.first < second.first; + }); + syncKeyframesToGraphicsView(); + setUnsavedState(); + generatePreviews(); +} + +void MotionEditWidget::updateKeyframeKnot(int index, float knot) +{ + if (index < 0 || index >= (int)m_keyframes.size()) + return; + m_keyframes[index].first = knot; + setUnsavedState(); + generatePreviews(); +} + +void MotionEditWidget::generatePreviews() +{ + if (nullptr != m_previewsGenerator) { + m_isPreviewsObsolete = true; + return; + } + + m_isPreviewsObsolete = false; + + const std::vector *rigBones = m_document->resultRigBones(); + const std::map *rigWeights = m_document->resultRigWeights(); + + if (nullptr == rigBones || nullptr == rigWeights) { + return; + } + + m_previewsGenerator = new MotionPreviewsGenerator(rigBones, rigWeights, + m_document->currentRiggedResultContext()); + for (const auto &pose: m_document->poseMap) + m_previewsGenerator->addPoseToLibrary(pose.first, pose.second.parameters); + //for (const auto &motion: m_document->motionMap) + // m_previewsGenerator->addMotionToLibrary(motion.first, motion.second.controlNodes, motion.second.keyframes); + std::vector controlNodes; + m_graphicsWidget->toNormalizedControlNodes(controlNodes); + m_previewsGenerator->addMotionToLibrary(QUuid(), controlNodes, m_keyframes); + m_previewsGenerator->addPreviewRequirement(QUuid()); + QThread *thread = new QThread; + m_previewsGenerator->moveToThread(thread); + connect(thread, &QThread::started, m_previewsGenerator, &MotionPreviewsGenerator::process); + connect(m_previewsGenerator, &MotionPreviewsGenerator::finished, this, &MotionEditWidget::previewsReady); + connect(m_previewsGenerator, &MotionPreviewsGenerator::finished, thread, &QThread::quit); + connect(thread, &QThread::finished, thread, &QThread::deleteLater); + thread->start(); +} + +void MotionEditWidget::previewsReady() +{ + auto resultPreviewMeshs = m_previewsGenerator->takeResultPreviewMeshs(QUuid()); + m_clipPlayer->updateFrameMeshes(resultPreviewMeshs); + + delete m_previewsGenerator; + m_previewsGenerator = nullptr; + + if (m_closed) { + close(); + return; + } + + if (m_isPreviewsObsolete) + generatePreviews(); +} diff --git a/src/motioneditwidget.h b/src/motioneditwidget.h new file mode 100644 index 00000000..109a4f67 --- /dev/null +++ b/src/motioneditwidget.h @@ -0,0 +1,56 @@ +#ifndef MOTION_EDIT_WIDGET_H +#define MOTION_EDIT_WIDGET_H +#include +#include +#include +#include "skeletondocument.h" +#include "interpolationgraphicswidget.h" +#include "modelwidget.h" +#include "curveutil.h" +#include "motionpreviewsgenerator.h" +#include "animationclipplayer.h" + +class MotionEditWidget : public QDialog +{ + Q_OBJECT +signals: + void addMotion(QString name, std::vector controlNodes, std::vector> keyframes); + void setMotionControlNodes(QUuid motionId, std::vector controlNodes); + void setMotionKeyframes(QUuid motionId, std::vector> keyframes); + void renameMotion(QUuid motionId, QString name); +public: + MotionEditWidget(const SkeletonDocument *document, QWidget *parent=nullptr); + ~MotionEditWidget(); +protected: + void closeEvent(QCloseEvent *event) override; + void reject() override; + QSize sizeHint() const override; +public slots: + void updateTitle(); + void save(); + void clearUnsaveState(); + void setEditMotionId(QUuid poseId); + void setEditMotionName(QString name); + void setEditMotionControlNodes(std::vector controlNodes); + void setEditMotionKeyframes(std::vector> keyframes); + void setUnsavedState(); + void addKeyframe(float knot, QUuid poseId); + void syncKeyframesToGraphicsView(); + void generatePreviews(); + void previewsReady(); + void updateKeyframeKnot(int index, float knot); +private: + const SkeletonDocument *m_document = nullptr; + InterpolationGraphicsWidget *m_graphicsWidget = nullptr; + ModelWidget *m_previewWidget = nullptr; + QUuid m_motionId; + QLineEdit *m_nameEdit = nullptr; + std::vector> m_keyframes; + bool m_unsaved = false; + bool m_closed = false; + bool m_isPreviewsObsolete = false; + MotionPreviewsGenerator *m_previewsGenerator = nullptr; + AnimationClipPlayer *m_clipPlayer = nullptr; +}; + +#endif diff --git a/src/motionlistwidget.cpp b/src/motionlistwidget.cpp new file mode 100644 index 00000000..856ea12d --- /dev/null +++ b/src/motionlistwidget.cpp @@ -0,0 +1,315 @@ +#include +#include +#include +#include +#include +#include "skeletonxml.h" +#include "motionlistwidget.h" + +MotionListWidget::MotionListWidget(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::motionListChanged, this, &MotionListWidget::reload); + connect(document, &SkeletonDocument::cleanup, this, &MotionListWidget::removeAllContent); + + connect(this, &MotionListWidget::removeMotion, document, &SkeletonDocument::removeMotion); + + setContextMenuPolicy(Qt::CustomContextMenu); + connect(this, &QTreeWidget::customContextMenuRequested, this, &MotionListWidget::showContextMenu); + + reload(); +} + +void MotionListWidget::motionRemoved(QUuid motionId) +{ + if (m_currentSelectedMotionId == motionId) + m_currentSelectedMotionId = QUuid(); + m_selectedMotionIds.erase(motionId); + m_itemMap.erase(motionId); +} + +void MotionListWidget::updateMotionSelectState(QUuid motionId, bool selected) +{ + auto findItemResult = m_itemMap.find(motionId); + if (findItemResult == m_itemMap.end()) { + qDebug() << "Find motion item failed:" << motionId; + return; + } + MotionWidget *motionWidget = (MotionWidget *)itemWidget(findItemResult->second.first, findItemResult->second.second); + motionWidget->updateCheckedState(selected); +} + +void MotionListWidget::selectMotion(QUuid motionId, bool multiple) +{ + if (multiple) { + if (!m_currentSelectedMotionId.isNull()) { + m_selectedMotionIds.insert(m_currentSelectedMotionId); + m_currentSelectedMotionId = QUuid(); + } + if (m_selectedMotionIds.find(motionId) != m_selectedMotionIds.end()) { + updateMotionSelectState(motionId, false); + m_selectedMotionIds.erase(motionId); + } else { + updateMotionSelectState(motionId, true); + m_selectedMotionIds.insert(motionId); + } + if (m_selectedMotionIds.size() > 1) { + return; + } + if (m_selectedMotionIds.size() == 1) + motionId = *m_selectedMotionIds.begin(); + else + motionId = QUuid(); + } + if (!m_selectedMotionIds.empty()) { + for (const auto &id: m_selectedMotionIds) { + updateMotionSelectState(id, false); + } + m_selectedMotionIds.clear(); + } + if (m_currentSelectedMotionId != motionId) { + if (!m_currentSelectedMotionId.isNull()) { + updateMotionSelectState(m_currentSelectedMotionId, false); + } + m_currentSelectedMotionId = motionId; + if (!m_currentSelectedMotionId.isNull()) { + updateMotionSelectState(m_currentSelectedMotionId, true); + } + } +} + +void MotionListWidget::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 motionId = 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->motionIdList) { + if (m_shiftStartMotionId == childId || motionId == childId) { + if (startAdd) { + stopAdd = true; + } else { + startAdd = true; + } + } + if (startAdd) + waitQueue.push_back(childId); + if (stopAdd) + break; + } + if (stopAdd && !waitQueue.empty()) { + if (!m_selectedMotionIds.empty()) { + for (const auto &id: m_selectedMotionIds) { + updateMotionSelectState(id, false); + } + m_selectedMotionIds.clear(); + } + if (!m_currentSelectedMotionId.isNull()) { + m_currentSelectedMotionId = QUuid(); + } + for (const auto &waitId: waitQueue) { + selectMotion(waitId, true); + } + } + return; + } else { + m_shiftStartMotionId = motionId; + } + selectMotion(motionId, multiple); + return; + } + if (!multiple) + selectMotion(QUuid()); + } +} + +bool MotionListWidget::isMotionSelected(QUuid motionId) +{ + return (m_currentSelectedMotionId == motionId || + m_selectedMotionIds.find(motionId) != m_selectedMotionIds.end()); +} + +void MotionListWidget::showContextMenu(const QPoint &pos) +{ + QMenu contextMenu(this); + + std::set unorderedMotionIds = m_selectedMotionIds; + if (!m_currentSelectedMotionId.isNull()) + unorderedMotionIds.insert(m_currentSelectedMotionId); + + std::vector motionIds; + for (const auto &cand: m_document->motionIdList) { + if (unorderedMotionIds.find(cand) != unorderedMotionIds.end()) + motionIds.push_back(cand); + } + + QAction modifyAction(tr("Modify"), this); + if (motionIds.size() == 1) { + connect(&modifyAction, &QAction::triggered, this, [=]() { + emit modifyMotion(*motionIds.begin()); + }); + contextMenu.addAction(&modifyAction); + } + + QAction copyAction(tr("Copy"), this); + if (!motionIds.empty()) { + connect(©Action, &QAction::triggered, this, &MotionListWidget::copy); + contextMenu.addAction(©Action); + } + + QAction pasteAction(tr("Paste"), this); + if (m_document->hasPastableMotionsInClipboard()) { + connect(&pasteAction, &QAction::triggered, m_document, &SkeletonDocument::paste); + contextMenu.addAction(&pasteAction); + } + + QAction deleteAction(tr("Delete"), this); + if (!motionIds.empty()) { + connect(&deleteAction, &QAction::triggered, [=]() { + for (const auto &motionId: motionIds) + emit removeMotion(motionId); + }); + contextMenu.addAction(&deleteAction); + } + + contextMenu.exec(mapToGlobal(pos)); +} + +void MotionListWidget::resizeEvent(QResizeEvent *event) +{ + QTreeWidget::resizeEvent(event); + if (calculateColumnCount() != columnCount()) + reload(); +} + +int MotionListWidget::calculateColumnCount() +{ + if (nullptr == parentWidget()) + return 0; + + int columns = parentWidget()->width() / Theme::motionPreviewImageSize; + if (0 == columns) + columns = 1; + return columns; +} + +void MotionListWidget::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->motionIdList.size()) motionIndex = 0; + while (motionIndex < m_document->motionIdList.size()) { + QTreeWidgetItem *item = new QTreeWidgetItem(this); + item->setFlags((item->flags() | Qt::ItemIsEnabled) & ~(Qt::ItemIsSelectable) & ~(Qt::ItemIsEditable)); + for (int col = 0; col < columns && motionIndex < m_document->motionIdList.size(); col++, motionIndex++) { + const auto &motionId = m_document->motionIdList[motionIndex]; + item->setSizeHint(col, QSize(columnWidth, MotionWidget::preferredHeight() + 2)); + item->setData(col, Qt::UserRole, motionId.toString()); + MotionWidget *widget = new MotionWidget(m_document, motionId); + connect(widget, &MotionWidget::modifyMotion, this, &MotionListWidget::modifyMotion); + setItemWidget(item, col, widget); + widget->reload(); + widget->updateCheckedState(isMotionSelected(motionId)); + m_itemMap[motionId] = std::make_pair(item, col); + } + invisibleRootItem()->addChild(item); + } +} + +void MotionListWidget::removeAllContent() +{ + m_itemMap.clear(); + clear(); +} + +bool MotionListWidget::mouseMove(QMouseEvent *event) +{ + return false; +} + +bool MotionListWidget::wheel(QWheelEvent *event) +{ + return false; +} + +bool MotionListWidget::mouseRelease(QMouseEvent *event) +{ + return false; +} + +bool MotionListWidget::mousePress(QMouseEvent *event) +{ + if (event->button() == Qt::RightButton) { + showContextMenu(mapFromGlobal(event->globalPos())); + return false; + } + return false; +} + +bool MotionListWidget::mouseDoubleClick(QMouseEvent *event) +{ + return false; +} + +bool MotionListWidget::keyPress(QKeyEvent *event) +{ + return false; +} + +void MotionListWidget::copy() +{ + if (m_selectedMotionIds.empty() && m_currentSelectedMotionId.isNull()) + return; + + std::set limitMotionIds = m_selectedMotionIds; + if (!m_currentSelectedMotionId.isNull()) + limitMotionIds.insert(m_currentSelectedMotionId); + + std::set emptySet; + + SkeletonSnapshot snapshot; + m_document->toSnapshot(&snapshot, emptySet, SkeletonDocumentToSnapshotFor::Motions, + emptySet, limitMotionIds); + QString snapshotXml; + QXmlStreamWriter xmlStreamWriter(&snapshotXml); + saveSkeletonToXmlStream(&snapshot, &xmlStreamWriter); + QClipboard *clipboard = QApplication::clipboard(); + clipboard->setText(snapshotXml); +} diff --git a/src/motionlistwidget.h b/src/motionlistwidget.h new file mode 100644 index 00000000..1d85381c --- /dev/null +++ b/src/motionlistwidget.h @@ -0,0 +1,44 @@ +#ifndef MOTION_LIST_WIDGET_H +#define MOTION_LIST_WIDGET_H +#include +#include +#include "skeletondocument.h" +#include "motionwidget.h" +#include "skeletongraphicswidget.h" + +class MotionListWidget : public QTreeWidget, public SkeletonGraphicsFunctions +{ + Q_OBJECT +signals: + void removeMotion(QUuid motionId); + void modifyMotion(QUuid motionId); +public: + MotionListWidget(const SkeletonDocument *document, QWidget *parent=nullptr); + bool isMotionSelected(QUuid motionId); +public slots: + void reload(); + void removeAllContent(); + void motionRemoved(QUuid motionId); + void showContextMenu(const QPoint &pos); + void selectMotion(QUuid motionId, bool multiple=false); + void copy(); +protected: + void resizeEvent(QResizeEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + bool mouseMove(QMouseEvent *event) override; + bool wheel(QWheelEvent *event) override; + bool mouseRelease(QMouseEvent *event) override; + bool mousePress(QMouseEvent *event) override; + bool mouseDoubleClick(QMouseEvent *event) override; + bool keyPress(QKeyEvent *event) override; +private: + int calculateColumnCount(); + void updateMotionSelectState(QUuid motionId, bool selected); + const SkeletonDocument *m_document = nullptr; + std::map> m_itemMap; + std::set m_selectedMotionIds; + QUuid m_currentSelectedMotionId; + QUuid m_shiftStartMotionId; +}; + +#endif diff --git a/src/motionmanagewidget.cpp b/src/motionmanagewidget.cpp new file mode 100644 index 00000000..f38e1e82 --- /dev/null +++ b/src/motionmanagewidget.cpp @@ -0,0 +1,76 @@ +#include +#include +#include +#include "motionmanagewidget.h" +#include "motioneditwidget.h" +#include "theme.h" +#include "infolabel.h" + +MotionManageWidget::MotionManageWidget(const SkeletonDocument *document, QWidget *parent) : + QWidget(parent), + m_document(document) +{ + QPushButton *addMotionButton = new QPushButton(Theme::awesome()->icon(fa::plus), tr("Add Motion...")); + addMotionButton->hide(); + + connect(addMotionButton, &QPushButton::clicked, this, &MotionManageWidget::showAddMotionDialog); + + QHBoxLayout *toolsLayout = new QHBoxLayout; + toolsLayout->addWidget(addMotionButton); + + m_motionListWidget = new MotionListWidget(document); + connect(m_motionListWidget, &MotionListWidget::modifyMotion, this, &MotionManageWidget::showMotionDialog); + + InfoLabel *infoLabel = new InfoLabel; + infoLabel->setText(tr("Missing Rig")); + infoLabel->show(); + + connect(m_document, &SkeletonDocument::resultRigChanged, this, [=]() { + if (m_document->currentRigSucceed()) { + infoLabel->hide(); + addMotionButton->show(); + } else { + infoLabel->show(); + addMotionButton->hide(); + } + }); + + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->addWidget(infoLabel); + mainLayout->addLayout(toolsLayout); + mainLayout->addWidget(m_motionListWidget); + + setLayout(mainLayout); +} + +QSize MotionManageWidget::sizeHint() const +{ + return QSize(Theme::sidebarPreferredWidth, 0); +} + +void MotionManageWidget::showAddMotionDialog() +{ + showMotionDialog(QUuid()); +} + +void MotionManageWidget::showMotionDialog(QUuid motionId) +{ + MotionEditWidget *motionEditWidget = new MotionEditWidget(m_document); + motionEditWidget->setAttribute(Qt::WA_DeleteOnClose); + if (!motionId.isNull()) { + const SkeletonMotion *motion = m_document->findMotion(motionId); + if (nullptr != motion) { + motionEditWidget->setEditMotionId(motionId); + motionEditWidget->setEditMotionName(motion->name); + motionEditWidget->setEditMotionControlNodes(motion->controlNodes); + motionEditWidget->setEditMotionKeyframes(motion->keyframes); + motionEditWidget->clearUnsaveState(); + motionEditWidget->generatePreviews(); + } + } + motionEditWidget->show(); + connect(motionEditWidget, &QDialog::destroyed, [=]() { + emit unregisterDialog((QWidget *)motionEditWidget); + }); + emit registerDialog((QWidget *)motionEditWidget); +} diff --git a/src/motionmanagewidget.h b/src/motionmanagewidget.h new file mode 100644 index 00000000..b752622e --- /dev/null +++ b/src/motionmanagewidget.h @@ -0,0 +1,25 @@ +#ifndef MOTION_MANAGE_WIDGET_H +#define MOTION_MANAGE_WIDGET_H +#include +#include "skeletondocument.h" +#include "motionlistwidget.h" + +class MotionManageWidget : public QWidget +{ + Q_OBJECT +signals: + void registerDialog(QWidget *widget); + void unregisterDialog(QWidget *widget); +public: + MotionManageWidget(const SkeletonDocument *document, QWidget *parent=nullptr); +protected: + virtual QSize sizeHint() const; +public slots: + void showAddMotionDialog(); + void showMotionDialog(QUuid motionId); +private: + const SkeletonDocument *m_document = nullptr; + MotionListWidget *m_motionListWidget = nullptr; +}; + +#endif diff --git a/src/motionpreviewsgenerator.cpp b/src/motionpreviewsgenerator.cpp new file mode 100644 index 00000000..1b0dfbd7 --- /dev/null +++ b/src/motionpreviewsgenerator.cpp @@ -0,0 +1,153 @@ +#include +#include +#include "motionpreviewsgenerator.h" +#include "tetrapodposer.h" +#include "posemeshcreator.h" + +MotionPreviewsGenerator::MotionPreviewsGenerator(const std::vector *rigBones, + const std::map *rigWeights, + const MeshResultContext &meshResultContext) : + m_rigBones(*rigBones), + m_rigWeights(*rigWeights), + m_meshResultContext(meshResultContext) +{ +} + +MotionPreviewsGenerator::~MotionPreviewsGenerator() +{ + for (auto &item: m_resultPreviewMeshs) { + for (auto &subItem: item.second) { + delete subItem.second; + } + } +} + +void MotionPreviewsGenerator::addPoseToLibrary(const QUuid &poseId, const std::map> ¶meters) +{ + m_poses[poseId] = parameters; +} + +void MotionPreviewsGenerator::addMotionToLibrary(const QUuid &motionId, const std::vector &controlNodes, + const std::vector> &keyframes) +{ + m_motions[motionId] = {controlNodes, keyframes}; +} + +void MotionPreviewsGenerator::addPreviewRequirement(const QUuid &motionId) +{ + m_requiredMotionIds.insert(motionId); +} + +const std::set &MotionPreviewsGenerator::requiredMotionIds() +{ + return m_requiredMotionIds; +} + +const std::set &MotionPreviewsGenerator::generatedMotionIds() +{ + return m_generatedMotionIds; +} + +void MotionPreviewsGenerator::generateForMotion(const QUuid &motionId) +{ + auto findMotionResult = m_motions.find(motionId); + if (findMotionResult == m_motions.end()) + return; + const std::vector &controlNodes = findMotionResult->second.first; + std::vector> keyframes = findMotionResult->second.second; + if (keyframes.empty()) + return; + + auto firstFrame = keyframes[0]; + auto lastFrame = keyframes[keyframes.size() - 1]; + + if (keyframes[0].first > 0) { + // Insert the last keyframe as start frame + keyframes.insert(keyframes.begin(), {0, lastFrame.second}); + } + + if (keyframes[keyframes.size() - 1].first < 1) { + // Insert the first keyframe as stop frame + keyframes.push_back({1.0, firstFrame.second}); + } + + std::vector>> keyframesParameters; + for (const auto &item: keyframes) { + const auto &poseId = item.second; + keyframesParameters.push_back(m_poses[poseId]); + } + + auto findLboundKeyframes = [=, &keyframes](float knot) { + for (decltype(keyframes.size()) i = 0; i < keyframes.size(); i++) { + //qDebug() << "Compare knot:" << knot << "keyframe[" << i << "] knot:" << keyframes[i].first; + if (knot >= keyframes[i].first && i + 1 < keyframes.size() && knot <= keyframes[i + 1].first) + return (int)i; + } + return -1; + }; + + TetrapodPoser *poser = new TetrapodPoser(m_rigBones); + float interval = 1.0 / m_fps; + float lastKnot = 0; + for (float knot = 0; knot <= 1; knot += interval) { + int firstKeyframeIndex = findLboundKeyframes(knot); + if (-1 == firstKeyframeIndex) { + continue; + } + poser->parameters() = keyframesParameters[firstKeyframeIndex]; + poser->commit(); + auto firstKeyframeJointNodeTree = poser->resultJointNodeTree(); + poser->reset(); + poser->parameters() = keyframesParameters[firstKeyframeIndex + 1]; + poser->commit(); + auto secondKeyframeJointNodeTree = poser->resultJointNodeTree(); + float firstKeyframeKnot = keyframes[firstKeyframeIndex].first; + float secondKeyframeKnot = keyframes[firstKeyframeIndex + 1].first; + QVector2D firstKeyframePosition = calculateHermiteInterpolation(controlNodes, firstKeyframeKnot); + QVector2D secondKeyframePosition = calculateHermiteInterpolation(controlNodes, secondKeyframeKnot); + QVector2D currentFramePosition = calculateHermiteInterpolation(controlNodes, knot); + float length = secondKeyframePosition.y() - firstKeyframePosition.y(); + if (qFuzzyIsNull(length)) + continue; + float t = (currentFramePosition.y() - firstKeyframePosition.y()) / length; + auto resultJointNodeTree = JointNodeTree::slerp(firstKeyframeJointNodeTree, secondKeyframeJointNodeTree, t); + + PoseMeshCreator *poseMeshCreator = new PoseMeshCreator(resultJointNodeTree.nodes(), m_meshResultContext, m_rigWeights); + poseMeshCreator->createMesh(); + m_resultPreviewMeshs[motionId].push_back({(knot - lastKnot) * 2, poseMeshCreator->takeResultMesh()}); + lastKnot = knot; + delete poseMeshCreator; + } + delete poser; +} + +std::vector> MotionPreviewsGenerator::takeResultPreviewMeshs(const QUuid &motionId) +{ + auto findResult = m_resultPreviewMeshs.find(motionId); + if (findResult == m_resultPreviewMeshs.end()) + return {}; + auto result = findResult->second; + m_resultPreviewMeshs.erase(findResult); + return result; +} + +void MotionPreviewsGenerator::generate() +{ + for (const auto &motionId: m_requiredMotionIds) { + generateForMotion(motionId); + m_generatedMotionIds.insert(motionId); + } +} + +void MotionPreviewsGenerator::process() +{ + QElapsedTimer countTimeConsumed; + countTimeConsumed.start(); + + generate(); + + qDebug() << "The motion previews generation took" << countTimeConsumed.elapsed() << "milliseconds"; + + this->moveToThread(QGuiApplication::instance()->thread()); + emit finished(); +} diff --git a/src/motionpreviewsgenerator.h b/src/motionpreviewsgenerator.h new file mode 100644 index 00000000..f3b6c23a --- /dev/null +++ b/src/motionpreviewsgenerator.h @@ -0,0 +1,45 @@ +#ifndef MOTION_PREVIEWS_GENERATOR_H +#define MOTION_PREVIEWS_GENERATOR_H +#include +#include +#include +#include +#include "meshloader.h" +#include "autorigger.h" +#include "curveutil.h" +#include "jointnodetree.h" + +class MotionPreviewsGenerator : public QObject +{ + Q_OBJECT +public: + MotionPreviewsGenerator(const std::vector *rigBones, + const std::map *rigWeights, + const MeshResultContext &meshResultContext); + ~MotionPreviewsGenerator(); + void addPoseToLibrary(const QUuid &poseId, const std::map> ¶meters); + void addMotionToLibrary(const QUuid &motionId, const std::vector &controlNodes, + const std::vector> &keyframes); + void addPreviewRequirement(const QUuid &motionId); + std::vector> takeResultPreviewMeshs(const QUuid &motionId); + const std::set &requiredMotionIds(); + const std::set &generatedMotionIds(); + void generateForMotion(const QUuid &motionId); + void generate(); +signals: + void finished(); +public slots: + void process(); +private: + std::vector m_rigBones; + std::map m_rigWeights; + MeshResultContext m_meshResultContext; + std::map>> m_poses; + std::map, std::vector>>> m_motions; + std::set m_requiredMotionIds; + std::set m_generatedMotionIds; + std::map>> m_resultPreviewMeshs; + int m_fps = 24; +}; + +#endif diff --git a/src/motionwidget.cpp b/src/motionwidget.cpp new file mode 100644 index 00000000..48242c1b --- /dev/null +++ b/src/motionwidget.cpp @@ -0,0 +1,104 @@ +#include +#include "motionwidget.h" + +MotionWidget::MotionWidget(const SkeletonDocument *document, QUuid motionId) : + m_motionId(motionId), + m_document(document) +{ + setObjectName("MotionFrame"); + + m_previewWidget = new InterpolationGraphicsWidget(this); + m_previewWidget->setFixedSize(Theme::motionPreviewImageSize, Theme::motionPreviewImageSize); + m_previewWidget->setPreviewOnly(true); + + 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::motionPreviewImageSize, MotionWidget::preferredHeight()); + + connect(document, &SkeletonDocument::motionNameChanged, this, &MotionWidget::updateName); + connect(document, &SkeletonDocument::motionControlNodesChanged, this, &MotionWidget::updatePreview); + connect(document, &SkeletonDocument::motionKeyframesChanged, this, &MotionWidget::updatePreview); +} + +void MotionWidget::resizeEvent(QResizeEvent *event) +{ + QWidget::resizeEvent(event); + m_previewWidget->move((width() - Theme::motionPreviewImageSize) / 2, 0); +} + +int MotionWidget::preferredHeight() +{ + return Theme::motionPreviewImageSize; +} + +void MotionWidget::reload() +{ + updatePreview(); + updateName(); +} + +void MotionWidget::updatePreview() +{ + const SkeletonMotion *motion = m_document->findMotion(m_motionId); + if (!motion) { + qDebug() << "Motion not found:" << m_motionId; + return; + } + std::vector> keyframesForGraphicsView; + for (const auto &frame: motion->keyframes) { + QString poseName; + const SkeletonPose *pose = m_document->findPose(frame.second); + if (nullptr == pose) { + qDebug() << "Find pose failed:" << frame.second; + } else { + poseName = pose->name; + } + keyframesForGraphicsView.push_back({frame.first, poseName}); + } + m_previewWidget->setControlNodes(motion->controlNodes); + m_previewWidget->setKeyframes(keyframesForGraphicsView); +} + +void MotionWidget::updateName() +{ + const SkeletonMotion *motion = m_document->findMotion(m_motionId); + if (!motion) { + qDebug() << "Motion not found:" << m_motionId; + return; + } + m_nameLabel->setText(motion->name); +} + +void MotionWidget::updateCheckedState(bool checked) +{ + if (checked) + setStyleSheet("#MotionFrame {border: 1px solid " + Theme::red.name() + ";}"); + else + setStyleSheet("#MotionFrame {border: 1px solid transparent;}"); +} + +InterpolationGraphicsWidget *MotionWidget::previewWidget() +{ + return m_previewWidget; +} + +void MotionWidget::mouseDoubleClickEvent(QMouseEvent *event) +{ + QFrame::mouseDoubleClickEvent(event); + emit modifyMotion(m_motionId); +} diff --git a/src/motionwidget.h b/src/motionwidget.h new file mode 100644 index 00000000..b972f6bc --- /dev/null +++ b/src/motionwidget.h @@ -0,0 +1,33 @@ +#ifndef MOTION_WIDGET_H +#define MOTION_WIDGET_H +#include +#include +#include +#include "skeletondocument.h" +#include "interpolationgraphicswidget.h" + +class MotionWidget : public QFrame +{ + Q_OBJECT +signals: + void modifyMotion(QUuid motionId); +public: + MotionWidget(const SkeletonDocument *document, QUuid motionId); + static int preferredHeight(); + InterpolationGraphicsWidget *previewWidget(); +protected: + void mouseDoubleClickEvent(QMouseEvent *event) override; + void resizeEvent(QResizeEvent *event) override; +public slots: + void reload(); + void updatePreview(); + void updateName(); + void updateCheckedState(bool checked); +private: + QUuid m_motionId; + const SkeletonDocument *m_document = nullptr; + InterpolationGraphicsWidget *m_previewWidget = nullptr; + QLabel *m_nameLabel = nullptr; +}; + +#endif diff --git a/src/poselistwidget.cpp b/src/poselistwidget.cpp index 14cd2944..2d47bf01 100644 --- a/src/poselistwidget.cpp +++ b/src/poselistwidget.cpp @@ -55,6 +55,9 @@ void PoseListWidget::updatePoseSelectState(QUuid poseId, bool selected) } PoseWidget *poseWidget = (PoseWidget *)itemWidget(findItemResult->second.first, findItemResult->second.second); poseWidget->updateCheckedState(selected); + if (m_cornerButtonVisible) { + poseWidget->setCornerButtonVisible(selected); + } } void PoseListWidget::selectPose(QUuid poseId, bool multiple) @@ -156,6 +159,9 @@ bool PoseListWidget::isPoseSelected(QUuid poseId) void PoseListWidget::showContextMenu(const QPoint &pos) { + if (!m_hasContextMenu) + return; + QMenu contextMenu(this); std::set unorderedPoseIds = m_selectedPoseIds; @@ -244,6 +250,7 @@ void PoseListWidget::reload() item->setData(col, Qt::UserRole, poseId.toString()); PoseWidget *widget = new PoseWidget(m_document, poseId); connect(widget, &PoseWidget::modifyPose, this, &PoseListWidget::modifyPose); + connect(widget, &PoseWidget::cornerButtonClicked, this, &PoseListWidget::cornerButtonClicked); widget->previewWidget()->setGraphicsFunctions(this); setItemWidget(item, col, widget); widget->reload(); @@ -254,6 +261,16 @@ void PoseListWidget::reload() } } +void PoseListWidget::setCornerButtonVisible(bool visible) +{ + m_cornerButtonVisible = visible; +} + +void PoseListWidget::setHasContextMenu(bool hasContextMenu) +{ + m_hasContextMenu = hasContextMenu; +} + void PoseListWidget::removeAllContent() { m_itemMap.clear(); diff --git a/src/poselistwidget.h b/src/poselistwidget.h index 40049182..7042091b 100644 --- a/src/poselistwidget.h +++ b/src/poselistwidget.h @@ -12,6 +12,7 @@ class PoseListWidget : public QTreeWidget, public SkeletonGraphicsFunctions signals: void removePose(QUuid poseId); void modifyPose(QUuid poseId); + void cornerButtonClicked(QUuid poseId); public: PoseListWidget(const SkeletonDocument *document, QWidget *parent=nullptr); bool isPoseSelected(QUuid poseId); @@ -22,15 +23,17 @@ public slots: void showContextMenu(const QPoint &pos); void selectPose(QUuid poseId, bool multiple=false); void copy(); + void setCornerButtonVisible(bool visible); + void setHasContextMenu(bool hasContextMenu); 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); + bool mouseMove(QMouseEvent *event) override; + bool wheel(QWheelEvent *event) override; + bool mouseRelease(QMouseEvent *event) override; + bool mousePress(QMouseEvent *event) override; + bool mouseDoubleClick(QMouseEvent *event) override; + bool keyPress(QKeyEvent *event) override; private: int calculateColumnCount(); void updatePoseSelectState(QUuid poseId, bool selected); @@ -39,6 +42,8 @@ private: std::set m_selectedPoseIds; QUuid m_currentSelectedPoseId; QUuid m_shiftStartPoseId; + bool m_cornerButtonVisible = false; + bool m_hasContextMenu = true; }; #endif diff --git a/src/posemanagewidget.cpp b/src/posemanagewidget.cpp index a140717c..8388429f 100644 --- a/src/posemanagewidget.cpp +++ b/src/posemanagewidget.cpp @@ -4,12 +4,14 @@ #include "posemanagewidget.h" #include "theme.h" #include "poseeditwidget.h" +#include "infolabel.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...")); + addPoseButton->hide(); connect(addPoseButton, &QPushButton::clicked, this, &PoseManageWidget::showAddPoseDialog); @@ -19,7 +21,22 @@ PoseManageWidget::PoseManageWidget(const SkeletonDocument *document, QWidget *pa m_poseListWidget = new PoseListWidget(document); connect(m_poseListWidget, &PoseListWidget::modifyPose, this, &PoseManageWidget::showPoseDialog); + InfoLabel *infoLabel = new InfoLabel; + infoLabel->setText(tr("Missing Rig")); + infoLabel->show(); + + connect(m_document, &SkeletonDocument::resultRigChanged, this, [=]() { + if (m_document->currentRigSucceed()) { + infoLabel->hide(); + addPoseButton->show(); + } else { + infoLabel->show(); + addPoseButton->hide(); + } + }); + QVBoxLayout *mainLayout = new QVBoxLayout; + mainLayout->addWidget(infoLabel); mainLayout->addLayout(toolsLayout); mainLayout->addWidget(m_poseListWidget); diff --git a/src/posemeshcreator.cpp b/src/posemeshcreator.cpp index 683db135..a22d99e0 100644 --- a/src/posemeshcreator.cpp +++ b/src/posemeshcreator.cpp @@ -2,10 +2,10 @@ #include "posemeshcreator.h" #include "skinnedmeshcreator.h" -PoseMeshCreator::PoseMeshCreator(const Poser &poser, +PoseMeshCreator::PoseMeshCreator(const std::vector &resultNodes, const MeshResultContext &meshResultContext, const std::map &resultWeights) : - m_resultNodes(poser.resultNodes()), + m_resultNodes(resultNodes), m_meshResultContext(meshResultContext), m_resultWeights(resultWeights) { diff --git a/src/posemeshcreator.h b/src/posemeshcreator.h index 99f7878d..e34f1799 100644 --- a/src/posemeshcreator.h +++ b/src/posemeshcreator.h @@ -1,7 +1,6 @@ #ifndef POSE_MESH_CREATOR_H #define POSE_MESH_CREATOR_H #include -#include "poser.h" #include "meshloader.h" #include "jointnodetree.h" #include "meshresultcontext.h" @@ -12,7 +11,7 @@ class PoseMeshCreator : public QObject signals: void finished(); public: - PoseMeshCreator(const Poser &poser, + PoseMeshCreator(const std::vector &resultNodes, const MeshResultContext &meshResultContext, const std::map &resultWeights); ~PoseMeshCreator(); diff --git a/src/posepreviewmanager.cpp b/src/posepreviewmanager.cpp index 245e0ee1..15b5eade 100644 --- a/src/posepreviewmanager.cpp +++ b/src/posepreviewmanager.cpp @@ -26,7 +26,7 @@ bool PosePreviewManager::postUpdate(const Poser &poser, qDebug() << "Pose mesh generating.."; QThread *thread = new QThread; - m_poseMeshCreator = new PoseMeshCreator(poser, meshResultContext, resultWeights); + m_poseMeshCreator = new PoseMeshCreator(poser.resultNodes(), meshResultContext, resultWeights); m_poseMeshCreator->moveToThread(thread); connect(thread, &QThread::started, m_poseMeshCreator, &PoseMeshCreator::process); connect(m_poseMeshCreator, &PoseMeshCreator::finished, this, &PosePreviewManager::poseMeshReady); diff --git a/src/posepreviewsgenerator.cpp b/src/posepreviewsgenerator.cpp index ef772e65..bb9fa2ca 100644 --- a/src/posepreviewsgenerator.cpp +++ b/src/posepreviewsgenerator.cpp @@ -48,7 +48,7 @@ void PosePreviewsGenerator::process() poser->parameters() = pose.second; poser->commit(); - PoseMeshCreator *poseMeshCreator = new PoseMeshCreator(*poser, *m_meshResultContext, m_rigWeights); + PoseMeshCreator *poseMeshCreator = new PoseMeshCreator(poser->resultNodes(), *m_meshResultContext, m_rigWeights); poseMeshCreator->createMesh(); m_previews[pose.first] = poseMeshCreator->takeResultMesh(); delete poseMeshCreator; diff --git a/src/poser.cpp b/src/poser.cpp index 8c462b10..398a6949 100644 --- a/src/poser.cpp +++ b/src/poser.cpp @@ -40,6 +40,11 @@ const std::vector &Poser::resultNodes() const return m_jointNodeTree.nodes(); } +const JointNodeTree &Poser::resultJointNodeTree() const +{ + return m_jointNodeTree; +} + void Poser::commit() { m_jointNodeTree.recalculateTransformMatrices(); diff --git a/src/poser.h b/src/poser.h index 42c49eb6..4e879847 100644 --- a/src/poser.h +++ b/src/poser.h @@ -15,6 +15,7 @@ public: int findBoneIndex(const QString &name); const std::vector &bones() const; const std::vector &resultNodes() const; + const JointNodeTree &resultJointNodeTree() const; std::map> ¶meters(); virtual void commit(); void reset(); diff --git a/src/posewidget.cpp b/src/posewidget.cpp index 564408aa..622462bc 100644 --- a/src/posewidget.cpp +++ b/src/posewidget.cpp @@ -35,6 +35,20 @@ PoseWidget::PoseWidget(const SkeletonDocument *document, QUuid poseId) : connect(document, &SkeletonDocument::posePreviewChanged, this, &PoseWidget::updatePreview); } +void PoseWidget::setCornerButtonVisible(bool visible) +{ + if (nullptr == m_cornerButton) { + m_cornerButton = new QPushButton(this); + m_cornerButton->move(Theme::posePreviewImageSize - Theme::miniIconSize - 2, 2); + Theme::initAwesomeMiniButton(m_cornerButton); + m_cornerButton->setText(QChar(fa::plussquare)); + connect(m_cornerButton, &QPushButton::clicked, this, [=]() { + emit cornerButtonClicked(m_poseId); + }); + } + m_cornerButton->setVisible(visible); +} + void PoseWidget::resizeEvent(QResizeEvent *event) { QWidget::resizeEvent(event); diff --git a/src/posewidget.h b/src/posewidget.h index 3ef7cb9b..526021e2 100644 --- a/src/posewidget.h +++ b/src/posewidget.h @@ -2,6 +2,7 @@ #define POSE_WIDGET_H #include #include +#include #include "skeletondocument.h" #include "modelwidget.h" @@ -10,23 +11,26 @@ class PoseWidget : public QFrame Q_OBJECT signals: void modifyPose(QUuid poseId); + void cornerButtonClicked(QUuid poseId); public: PoseWidget(const SkeletonDocument *document, QUuid poseId); static int preferredHeight(); ModelWidget *previewWidget(); protected: void mouseDoubleClickEvent(QMouseEvent *event) override; + void resizeEvent(QResizeEvent *event) override; public slots: void reload(); void updatePreview(); void updateName(); void updateCheckedState(bool checked); - void resizeEvent(QResizeEvent *event) override; + void setCornerButtonVisible(bool visible); private: QUuid m_poseId; const SkeletonDocument *m_document = nullptr; ModelWidget *m_previewWidget = nullptr; QLabel *m_nameLabel = nullptr; + QPushButton *m_cornerButton = nullptr; }; #endif diff --git a/src/riggenerator.cpp b/src/riggenerator.cpp index 56d34805..623a8800 100644 --- a/src/riggenerator.cpp +++ b/src/riggenerator.cpp @@ -46,6 +46,11 @@ MeshLoader *RigGenerator::takeResultMesh() return resultMesh; } +bool RigGenerator::isSucceed() +{ + return m_isSucceed; +} + const std::vector &RigGenerator::missingMarkNames() { return m_missingMarkNames; @@ -95,9 +100,9 @@ void RigGenerator::process() std::get<0>(marks.second) / std::get<1>(marks.second), std::get<2>(marks.second)); } - bool rigSucceed = m_autoRigger->rig(); + m_isSucceed = m_autoRigger->rig(); - if (rigSucceed) { + if (m_isSucceed) { qDebug() << "Rig succeed"; } else { qDebug() << "Rig failed"; @@ -111,7 +116,7 @@ void RigGenerator::process() // Blend vertices colors according to bone weights std::vector inputVerticesColors(m_meshResultContext->vertices.size()); - if (rigSucceed) { + if (m_isSucceed) { const auto &resultWeights = m_autoRigger->resultWeights(); const auto &resultBones = m_autoRigger->resultBones(); diff --git a/src/riggenerator.h b/src/riggenerator.h index ae8c8090..5aaeeadd 100644 --- a/src/riggenerator.h +++ b/src/riggenerator.h @@ -19,6 +19,7 @@ public: const std::vector &missingMarkNames(); const std::vector &errorMarkNames(); MeshResultContext *takeMeshResultContext(); + bool isSucceed(); signals: void finished(); public slots: @@ -31,6 +32,7 @@ private: std::map *m_resultWeights = nullptr; std::vector m_missingMarkNames; std::vector m_errorMarkNames; + bool m_isSucceed = false; }; #endif diff --git a/src/skeletondocument.cpp b/src/skeletondocument.cpp index 9189d2d7..ba33da4f 100644 --- a/src/skeletondocument.cpp +++ b/src/skeletondocument.cpp @@ -53,7 +53,8 @@ SkeletonDocument::SkeletonDocument() : m_resultRigWeights(nullptr), m_isRigObsolete(false), m_riggedResultContext(new MeshResultContext), - m_posePreviewsGenerator(nullptr) + m_posePreviewsGenerator(nullptr), + m_currentRigSucceed(false) { } @@ -326,6 +327,80 @@ void SkeletonDocument::addPose(QString name, std::map controlNodes, std::vector> keyframes) +{ + QUuid newMotionId = QUuid::createUuid(); + auto &motion = motionMap[newMotionId]; + motion.id = newMotionId; + + motion.name = name; + motion.controlNodes = controlNodes; + motion.keyframes = keyframes; + motion.dirty = true; + + motionIdList.push_back(newMotionId); + + emit motionAdded(newMotionId); + emit motionListChanged(); + emit optionsChanged(); +} + +void SkeletonDocument::removeMotion(QUuid motionId) +{ + auto findMotionResult = motionMap.find(motionId); + if (findMotionResult == motionMap.end()) { + qDebug() << "Remove a none exist motion:" << motionId; + return; + } + motionIdList.erase(std::remove(motionIdList.begin(), motionIdList.end(), motionId), motionIdList.end()); + motionMap.erase(findMotionResult); + + emit motionListChanged(); + emit motionRemoved(motionId); + emit optionsChanged(); +} + +void SkeletonDocument::setMotionControlNodes(QUuid motionId, std::vector controlNodes) +{ + auto findMotionResult = motionMap.find(motionId); + if (findMotionResult == motionMap.end()) { + qDebug() << "Find motion failed:" << motionId; + return; + } + findMotionResult->second.controlNodes = controlNodes; + findMotionResult->second.dirty = true; + emit motionControlNodesChanged(motionId); + emit optionsChanged(); +} + +void SkeletonDocument::setMotionKeyframes(QUuid motionId, std::vector> keyframes) +{ + auto findMotionResult = motionMap.find(motionId); + if (findMotionResult == motionMap.end()) { + qDebug() << "Find motion failed:" << motionId; + return; + } + findMotionResult->second.keyframes = keyframes; + findMotionResult->second.dirty = true; + emit motionKeyframesChanged(motionId); + emit optionsChanged(); +} + +void SkeletonDocument::renameMotion(QUuid motionId, QString name) +{ + auto findMotionResult = motionMap.find(motionId); + if (findMotionResult == motionMap.end()) { + qDebug() << "Find motion failed:" << motionId; + return; + } + if (findMotionResult->second.name == name) + return; + + findMotionResult->second.name = name; + emit motionNameChanged(motionId); + emit optionsChanged(); +} + void SkeletonDocument::removePose(QUuid poseId) { auto findPoseResult = poseMap.find(poseId); @@ -517,6 +592,14 @@ const SkeletonPose *SkeletonDocument::findPose(QUuid poseId) const return &it->second; } +const SkeletonMotion *SkeletonDocument::findMotion(QUuid motionId) const +{ + auto it = motionMap.find(motionId); + if (it == motionMap.end()) + return nullptr; + return &it->second; +} + void SkeletonDocument::scaleNodeByAddRadius(QUuid nodeId, float amount) { auto it = nodeMap.find(nodeId); @@ -747,7 +830,7 @@ void SkeletonDocument::markAllDirty() } void SkeletonDocument::toSnapshot(SkeletonSnapshot *snapshot, const std::set &limitNodeIds, - SkeletonDocumentToSnapshotFor forWhat, const std::set &limitPoseIds) const + SkeletonDocumentToSnapshotFor forWhat, const std::set &limitPoseIds, const std::set &limitMotionIds) const { if (SkeletonDocumentToSnapshotFor::Document == forWhat || SkeletonDocumentToSnapshotFor::Nodes == forWhat) { @@ -883,6 +966,43 @@ void SkeletonDocument::toSnapshot(SkeletonSnapshot *snapshot, const std::setposes.push_back(std::make_pair(pose, poseIt.second.parameters)); } } + if (SkeletonDocumentToSnapshotFor::Document == forWhat || + SkeletonDocumentToSnapshotFor::Motions == forWhat) { + for (const auto &motionId: motionIdList) { + if (!limitMotionIds.empty() && limitMotionIds.find(motionId) == limitMotionIds.end()) + continue; + auto findMotionResult = motionMap.find(motionId); + if (findMotionResult == motionMap.end()) { + qDebug() << "Find motion failed:" << motionId; + continue; + } + auto &motionIt = *findMotionResult; + std::map motion; + motion["id"] = motionIt.second.id.toString(); + if (!motionIt.second.name.isEmpty()) + motion["name"] = motionIt.second.name; + std::vector> controlNodesAttributes; + std::vector> keyframesAttributes; + for (const auto &controlNode: motionIt.second.controlNodes) { + std::map attributes; + attributes["x"] = QString::number(controlNode.position.x()); + attributes["y"] = QString::number(controlNode.position.y()); + attributes["inTangentX"] = QString::number(controlNode.inTangent.x()); + attributes["inTangentY"] = QString::number(controlNode.inTangent.y()); + attributes["outTangentX"] = QString::number(controlNode.outTangent.x()); + attributes["outTangentY"] = QString::number(controlNode.outTangent.y()); + controlNodesAttributes.push_back(attributes); + } + for (const auto &keyframe: motionIt.second.keyframes) { + std::map attributes; + attributes["knot"] = QString::number(keyframe.first); + attributes["linkDataType"] = "poseId"; + attributes["linkData"] = keyframe.second.toString(); + keyframesAttributes.push_back(attributes); + } + snapshot->motions.push_back(std::make_tuple(motion, controlNodesAttributes, keyframesAttributes)); + } + } if (SkeletonDocumentToSnapshotFor::Document == forWhat) { std::map canvas; canvas["originX"] = QString::number(originX); @@ -1065,6 +1185,44 @@ void SkeletonDocument::addFromSnapshot(const SkeletonSnapshot &snapshot, bool fr poseIdList.push_back(newPoseId); emit poseAdded(newPoseId); } + for (const auto &motionIt: snapshot.motions) { + QUuid newMotionId = QUuid::createUuid(); + auto &newMotion = motionMap[newMotionId]; + newMotion.id = newMotionId; + const auto &motionAttributes = std::get<0>(motionIt); + newMotion.name = valueOfKeyInMapOrEmpty(motionAttributes, "name"); + for (const auto &attributes: std::get<1>(motionIt)) { + float x = valueOfKeyInMapOrEmpty(attributes, "x").toFloat(); + float y = valueOfKeyInMapOrEmpty(attributes, "y").toFloat(); + float inTangentX = valueOfKeyInMapOrEmpty(attributes, "inTangentX").toFloat(); + float inTangentY = valueOfKeyInMapOrEmpty(attributes, "inTangentY").toFloat(); + float outTangentX = valueOfKeyInMapOrEmpty(attributes, "outTangentX").toFloat(); + float outTangentY = valueOfKeyInMapOrEmpty(attributes, "outTangentY").toFloat(); + HermiteControlNode hermite(QVector2D(x, y), + QVector2D(inTangentX, inTangentY), QVector2D(outTangentX, outTangentY)); + newMotion.controlNodes.push_back(hermite); + } + for (const auto &attributes: std::get<2>(motionIt)) { + float knot = valueOfKeyInMapOrEmpty(attributes, "knot").toFloat(); + QString linkDataType = valueOfKeyInMapOrEmpty(attributes, "linkDataType"); + if ("poseId" != linkDataType) { + qDebug() << "Encounter unknown linkDataType:" << linkDataType; + continue; + } + QUuid linkToPoseId = QUuid(valueOfKeyInMapOrEmpty(attributes, "linkData")); + auto findPoseResult = poseMap.find(linkToPoseId); + if (findPoseResult != poseMap.end()) { + newMotion.keyframes.push_back({knot, findPoseResult->first}); + } else { + auto findInOldNewIdMapResult = oldNewIdMap.find(linkToPoseId); + if (findInOldNewIdMapResult != oldNewIdMap.end()) + newMotion.keyframes.push_back({knot, findInOldNewIdMapResult->second}); + } + } + oldNewIdMap[QUuid(valueOfKeyInMapOrEmpty(motionAttributes, "id"))] = newMotionId; + motionIdList.push_back(newMotionId); + emit motionAdded(newMotionId); + } for (const auto &nodeIt: newAddedNodeIds) { emit nodeAdded(nodeIt); @@ -1097,6 +1255,8 @@ void SkeletonDocument::addFromSnapshot(const SkeletonSnapshot &snapshot, bool fr if (!snapshot.poses.empty()) emit poseListChanged(); + if (!snapshot.motions.empty()) + emit motionListChanged(); } void SkeletonDocument::reset() @@ -1111,6 +1271,8 @@ void SkeletonDocument::reset() componentMap.clear(); poseMap.clear(); poseIdList.clear(); + motionMap.clear(); + motionIdList.clear(); rootComponent = SkeletonComponent(); emit cleanup(); emit skeletonChanged(); @@ -2076,7 +2238,7 @@ bool SkeletonDocument::hasPastableNodesInClipboard() const const QClipboard *clipboard = QApplication::clipboard(); const QMimeData *mimeData = clipboard->mimeData(); if (mimeData->hasText()) { - if (-1 != mimeData->text().left(1000).indexOf("text().indexOf("mimeData(); if (mimeData->hasText()) { - if (-1 != mimeData->text().right(1000).indexOf("text().indexOf("mimeData(); + if (mimeData->hasText()) { + if (-1 != mimeData->text().indexOf("isSucceed(); + delete m_resultRigWeightMesh; m_resultRigWeightMesh = m_rigGenerator->takeResultMesh(); @@ -2466,6 +2641,11 @@ const MeshResultContext &SkeletonDocument::currentRiggedResultContext() const return *m_riggedResultContext; } +bool SkeletonDocument::currentRigSucceed() const +{ + return m_currentRigSucceed; +} + void SkeletonDocument::generatePosePreviews() { if (nullptr != m_posePreviewsGenerator) { diff --git a/src/skeletondocument.h b/src/skeletondocument.h index 00df114a..599fabd6 100644 --- a/src/skeletondocument.h +++ b/src/skeletondocument.h @@ -21,6 +21,7 @@ #include "riggenerator.h" #include "rigtype.h" #include "posepreviewsgenerator.h" +#include "curveutil.h" class SkeletonNode { @@ -377,11 +378,27 @@ private: MeshLoader *m_previewMesh = nullptr; }; +class SkeletonMotion +{ +public: + SkeletonMotion() + { + } + QUuid id; + QString name; + bool dirty = true; + std::vector controlNodes; + std::vector> keyframes; //std::pair +private: + Q_DISABLE_COPY(SkeletonMotion); +}; + enum class SkeletonDocumentToSnapshotFor { Document = 0, Nodes, - Poses + Poses, + Motions }; class SkeletonDocument : public QObject @@ -451,6 +468,12 @@ signals: void poseNameChanged(QUuid poseId); void poseParametersChanged(QUuid poseId); void posePreviewChanged(QUuid poseId); + void motionAdded(QUuid motionId); + void motionRemoved(QUuid motionId); + void motionListChanged(); + void motionNameChanged(QUuid motionId); + void motionControlNodesChanged(QUuid motionId); + void motionKeyframesChanged(QUuid motionId); public: // need initialize float originX; float originY; @@ -476,12 +499,15 @@ public: std::map componentMap; std::map poseMap; std::vector poseIdList; + std::map motionMap; + std::vector motionIdList; SkeletonComponent rootComponent; QImage turnaround; QImage preview; void toSnapshot(SkeletonSnapshot *snapshot, const std::set &limitNodeIds=std::set(), SkeletonDocumentToSnapshotFor forWhat=SkeletonDocumentToSnapshotFor::Document, - const std::set &limitPoseIds=std::set()) const; + const std::set &limitPoseIds=std::set(), + const std::set &limitMotionIds=std::set()) const; void fromSnapshot(const SkeletonSnapshot &snapshot); void addFromSnapshot(const SkeletonSnapshot &snapshot, bool fromPaste=true); const SkeletonNode *findNode(QUuid nodeId) const; @@ -492,6 +518,7 @@ public: const SkeletonComponent *findComponentParent(QUuid componentId) const; QUuid findComponentParentId(QUuid componentId) const; const SkeletonPose *findPose(QUuid poseId) const; + const SkeletonMotion *findMotion(QUuid motionId) const; MeshLoader *takeResultMesh(); MeshLoader *takeResultTextureMesh(); MeshLoader *takeResultRigWeightMesh(); @@ -501,6 +528,7 @@ public: void setSharedContextWidget(QOpenGLWidget *sharedContextWidget); bool hasPastableNodesInClipboard() const; bool hasPastablePosesInClipboard() const; + bool hasPastableMotionsInClipboard() const; bool undoable() const; bool redoable() const; bool isNodeEditable(QUuid nodeId) const; @@ -515,6 +543,7 @@ public: const std::vector &resultRigMissingMarkNames() const; const std::vector &resultRigErrorMarkNames() const; const MeshResultContext ¤tRiggedResultContext() const; + bool currentRigSucceed() const; public slots: void removeNode(QUuid nodeId); void removeEdge(QUuid edgeId); @@ -602,6 +631,11 @@ public slots: void removePose(QUuid poseId); void setPoseParameters(QUuid poseId, std::map> parameters); void renamePose(QUuid poseId, QString name); + void addMotion(QString name, std::vector controlNodes, std::vector> keyframes); + void removeMotion(QUuid motionId); + void setMotionControlNodes(QUuid motionId, std::vector controlNodes); + void setMotionKeyframes(QUuid motionId, std::vector> keyframes); + void renameMotion(QUuid motionId, QString name); private: void splitPartByNode(std::vector> *groups, QUuid nodeId); void joinNodeAndNeiborsToGroup(std::vector *group, QUuid nodeId, std::set *visitMap, QUuid noUseEdgeId=QUuid()); @@ -643,6 +677,7 @@ private: // need initialize bool m_isRigObsolete; MeshResultContext *m_riggedResultContext; PosePreviewsGenerator *m_posePreviewsGenerator; + bool m_currentRigSucceed; private: static unsigned long m_maxSnapshot; std::deque m_undoItems; diff --git a/src/skeletondocumentwindow.cpp b/src/skeletondocumentwindow.cpp index 37dd010c..683183c7 100644 --- a/src/skeletondocumentwindow.cpp +++ b/src/skeletondocumentwindow.cpp @@ -30,6 +30,7 @@ #include "skeletonparttreewidget.h" #include "rigwidget.h" #include "markiconcreator.h" +#include "motionmanagewidget.h" int SkeletonDocumentWindow::m_modelRenderWidgetInitialX = 16; int SkeletonDocumentWindow::m_modelRenderWidgetInitialY = 16; @@ -238,8 +239,17 @@ SkeletonDocumentWindow::SkeletonDocumentWindow() : emit m_document->posePreviewChanged(pose.first); }); + QDockWidget *motionDocker = new QDockWidget(tr("Motions"), this); + motionDocker->setAllowedAreas(Qt::RightDockWidgetArea); + MotionManageWidget *motionManageWidget = new MotionManageWidget(m_document, motionDocker); + motionDocker->setWidget(motionManageWidget); + connect(motionManageWidget, &MotionManageWidget::registerDialog, this, &SkeletonDocumentWindow::registerDialog); + connect(motionManageWidget, &MotionManageWidget::unregisterDialog, this, &SkeletonDocumentWindow::unregisterDialog); + addDockWidget(Qt::RightDockWidgetArea, motionDocker); + tabifyDockWidget(partTreeDocker, rigDocker); tabifyDockWidget(rigDocker, poseDocker); + tabifyDockWidget(poseDocker, motionDocker); partTreeDocker->raise(); @@ -524,6 +534,13 @@ SkeletonDocumentWindow::SkeletonDocumentWindow() : }); m_windowMenu->addAction(m_showPosesAction); + m_showMotionsAction = new QAction(tr("Motions"), this); + connect(m_showMotionsAction, &QAction::triggered, [=]() { + motionDocker->show(); + motionDocker->raise(); + }); + m_windowMenu->addAction(m_showMotionsAction); + QMenu *dialogsMenu = m_windowMenu->addMenu(tr("Dialogs")); connect(dialogsMenu, &QMenu::aboutToShow, [=]() { dialogsMenu->clear(); diff --git a/src/skeletondocumentwindow.h b/src/skeletondocumentwindow.h index 47538e29..b0b8d8e2 100644 --- a/src/skeletondocumentwindow.h +++ b/src/skeletondocumentwindow.h @@ -137,6 +137,7 @@ private: QAction *m_showDebugDialogAction; QAction *m_showRigAction; QAction *m_showPosesAction; + QAction *m_showMotionsAction; QAction *m_showAdvanceSettingAction; QMenu *m_helpMenu; diff --git a/src/skeletonsnapshot.h b/src/skeletonsnapshot.h index c73c427a..a6154b2d 100644 --- a/src/skeletonsnapshot.h +++ b/src/skeletonsnapshot.h @@ -16,6 +16,7 @@ public: std::map> components; std::map rootComponent; std::vector, std::map>>> poses; // std::pair + std::vector, std::vector>, std::vector>>> motions; // std::tuple public: void resolveBoundingBox(QRectF *mainProfile, QRectF *sideProfile, const QString &partId=QString()); }; diff --git a/src/skeletonxml.cpp b/src/skeletonxml.cpp index 9320c421..7a943933 100644 --- a/src/skeletonxml.cpp +++ b/src/skeletonxml.cpp @@ -96,20 +96,62 @@ void saveSkeletonToXmlStream(SkeletonSnapshot *snapshot, QXmlStreamWriter *write 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); + for (poseAttributeIterator = poseIterator->first.begin(); poseAttributeIterator != poseIterator->first.end(); poseAttributeIterator++) { + writer->writeAttribute(poseAttributeIterator->first, poseAttributeIterator->second); + } + writer->writeStartElement("parameters"); + 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->writeStartElement("motions"); + std::vector, std::vector>, std::vector>>>::iterator motionIterator; + for (motionIterator = snapshot->motions.begin(); motionIterator != snapshot->motions.end(); motionIterator++) { + std::map::iterator motionAttributeIterator; + writer->writeStartElement("motion"); + for (motionAttributeIterator = std::get<0>(*motionIterator).begin(); motionAttributeIterator != std::get<0>(*motionIterator).end(); motionAttributeIterator++) { + writer->writeAttribute(motionAttributeIterator->first, motionAttributeIterator->second); + } + writer->writeStartElement("controlNodes"); + { + std::vector>::iterator itemsIterator; + for (itemsIterator = std::get<1>(*motionIterator).begin(); itemsIterator != std::get<1>(*motionIterator).end(); itemsIterator++) { + std::map::iterator attributesIterator; + writer->writeStartElement("controlNode"); + for (attributesIterator = itemsIterator->begin(); attributesIterator != itemsIterator->end(); + attributesIterator++) { + writer->writeAttribute(attributesIterator->first, attributesIterator->second); + } + writer->writeEndElement(); + } + } + writer->writeEndElement(); + writer->writeStartElement("keyframes"); + { + std::vector>::iterator itemsIterator; + for (itemsIterator = std::get<2>(*motionIterator).begin(); itemsIterator != std::get<2>(*motionIterator).end(); itemsIterator++) { + std::map::iterator attributesIterator; + writer->writeStartElement("keyframe"); + for (attributesIterator = itemsIterator->begin(); attributesIterator != itemsIterator->end(); + attributesIterator++) { + writer->writeAttribute(attributesIterator->first, attributesIterator->second); + } + writer->writeEndElement(); + } } writer->writeEndElement(); - } writer->writeEndElement(); } writer->writeEndElement(); @@ -124,6 +166,7 @@ void loadSkeletonFromXmlStream(SkeletonSnapshot *snapshot, QXmlStreamReader &rea std::stack componentStack; std::vector elementNameStack; std::pair, std::map>> currentPose; + std::tuple, std::vector>, std::vector>> currentMotion; while (!reader.atEnd()) { reader.readNext(); if (!reader.isStartElement() && !reader.isEndElement()) { @@ -208,7 +251,8 @@ void loadSkeletonFromXmlStream(SkeletonSnapshot *snapshot, QXmlStreamReader &rea foreach(const QXmlStreamAttribute &attr, reader.attributes()) { currentPose.first[attr.name().toString()] = attr.value().toString(); } - } else if (fullName == "canvas.poses.pose.parameter") { + } else if (fullName == "canvas.poses.pose.parameter" || + fullName == "canvas.poses.pose.parameters.parameter") { QString forWhat = reader.attributes().value("for").toString(); if (forWhat.isEmpty()) continue; @@ -217,12 +261,34 @@ void loadSkeletonFromXmlStream(SkeletonSnapshot *snapshot, QXmlStreamReader &rea continue; currentPose.second[forWhat][attr.name().toString()] = attr.value().toString(); } + } else if (fullName == "canvas.motions.motion") { + QString motionId = reader.attributes().value("id").toString(); + if (motionId.isEmpty()) + continue; + currentMotion = decltype(currentMotion)(); + foreach(const QXmlStreamAttribute &attr, reader.attributes()) { + std::get<0>(currentMotion)[attr.name().toString()] = attr.value().toString(); + } + } else if (fullName == "canvas.motions.motion.controlNodes.controlNode") { + std::map attributes; + foreach(const QXmlStreamAttribute &attr, reader.attributes()) { + attributes[attr.name().toString()] = attr.value().toString(); + } + std::get<1>(currentMotion).push_back(attributes); + } else if (fullName == "canvas.motions.motion.keyframes.keyframe") { + std::map attributes; + foreach(const QXmlStreamAttribute &attr, reader.attributes()) { + attributes[attr.name().toString()] = attr.value().toString(); + } + std::get<2>(currentMotion).push_back(attributes); } } else if (reader.isEndElement()) { if (fullName.startsWith("canvas.components.component")) { componentStack.pop(); } else if (fullName == "canvas.poses.pose") { snapshot->poses.push_back(currentPose); + } else if (fullName == "canvas.motions.motion") { + snapshot->motions.push_back(currentMotion); } } } diff --git a/src/theme.cpp b/src/theme.cpp index 1649a3aa..b09c80d0 100644 --- a/src/theme.cpp +++ b/src/theme.cpp @@ -35,6 +35,7 @@ int Theme::miniIconFontSize = 9; int Theme::miniIconSize = 15; int Theme::partPreviewImageSize = (Theme::miniIconSize * 3); int Theme::posePreviewImageSize = 75; +int Theme::motionPreviewImageSize = 75; int Theme::sidebarPreferredWidth = 200; QtAwesome *Theme::awesome() diff --git a/src/theme.h b/src/theme.h index 1c00ea8b..b9e0f810 100644 --- a/src/theme.h +++ b/src/theme.h @@ -36,6 +36,7 @@ public: static int toolIconSize; static int posePreviewImageSize; static int partPreviewImageSize; + static int motionPreviewImageSize; static int miniIconFontSize; static int miniIconSize; static int sidebarPreferredWidth; diff --git a/src/videoframeextractor.h b/src/videoframeextractor.h deleted file mode 100644 index 7e7050cb..00000000 --- a/src/videoframeextractor.h +++ /dev/null @@ -1,41 +0,0 @@ -#ifndef VIDEO_FRAME_EXTRACTOR_H -#define VIDEO_FRAME_EXTRACTOR_H -#include -#include -#include - -struct VideoFrame -{ - QImage image; - QImage thumbnail; - qint64 position; - float durationSeconds = 0; -}; - -class VideoFrameExtractor : public QObject -{ - Q_OBJECT -public: - VideoFrameExtractor(const QString &fileName, const QString &realPath, QTemporaryFile *fileHandle, float thumbnailHeight, int maxFrames=(10 * 60)); - ~VideoFrameExtractor(); - const QString &fileName(); - const QString &realPath(); - QTemporaryFile *fileHandle(); - void extract(); - std::vector *takeResultFrames(); -signals: - void finished(); -public slots: - void process(); -private: - void release(); -private: - QString m_fileName; - QString m_realPath; - QTemporaryFile *m_fileHandle = nullptr; - std::vector *m_resultFrames = nullptr; - int m_maxFrames; - float m_thumbnailHeight = 0; -}; - -#endif