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