Merge branch 'curveeditor'

master
Jeremy Hu 2018-10-02 13:09:04 +08:00
commit b02ffedb9d
45 changed files with 2804 additions and 76 deletions

View File

@ -544,3 +544,15 @@ https://www.reddit.com/r/gamedev/comments/5iuf3h/i_am_writting_a_3d_monster_mode
<pre>
https://appimage.org/
</pre>
<h1>Sam Hocevar</h1>
<pre>
https://gamedev.stackexchange.com/questions/98246/quaternion-slerp-and-lerp-implementation-with-overshoot
</pre>
<h1>David Rosen</h1>
<pre>
An Indie Approach to Procedural Animation
http://www.gdcvault.com/play/1020583/Animation-Bootcamp-An-Indie-Approach
</pre>

View File

@ -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,6 +460,7 @@ macx {
GMP_LIBDIR = /usr/local/opt/gmp/lib
MPFR_INCLUDEDIR = /usr/local/opt/mpfr/include
MPFR_LIBDIR = /usr/local/opt/mpfr/lib
<<<<<<< HEAD
exists(/usr/local/opt/opencv) {
INCLUDEPATH += /usr/local/opt/opencv/include
@ -443,6 +468,8 @@ macx {
DEFINES += "USE_OPENCV=1"
}
=======
>>>>>>> curveeditor
}
unix:!macx {

View File

@ -0,0 +1,59 @@
#include "animationclipplayer.h"
AnimationClipPlayer::~AnimationClipPlayer()
{
clear();
}
void AnimationClipPlayer::updateFrameMeshes(std::vector<std::pair<float, MeshLoader *>> &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;
}

29
src/animationclipplayer.h Normal file
View File

@ -0,0 +1,29 @@
#ifndef ANIMATION_PLAYER_H
#define ANIMATION_PLAYER_H
#include <QObject>
#include <QTimer>
#include <QTime>
#include "meshloader.h"
class AnimationClipPlayer : public QObject
{
Q_OBJECT
signals:
void frameReadyToShow();
public:
~AnimationClipPlayer();
MeshLoader *takeFrameMesh();
void updateFrameMeshes(std::vector<std::pair<float, MeshLoader *>> &frameMeshes);
void clear();
private:
void freeFrames();
private:
MeshLoader *m_lastFrameMesh = nullptr;
int m_currentPlayIndex = 0;
private:
std::vector<std::pair<float, MeshLoader *>> m_frameMeshes;
QTime m_countForFrame;
QTimer m_timerForFrame;
};
#endif

63
src/curveutil.cpp Normal file
View File

@ -0,0 +1,63 @@
#include "curveutil.h"
void hermiteCurveToPainterPath(const std::vector<HermiteControlNode> &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<HermiteControlNode> &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;
}

43
src/curveutil.h Normal file
View File

@ -0,0 +1,43 @@
#ifndef CURVE_UTIL_H
#define CURVE_UTIL_H
#include <QPointF>
#include <vector>
#include <QPainterPath>
#include <QVector2D>
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<HermiteControlNode> &hermiteControlNodes,
QPainterPath &toPath);
QVector2D calculateHermiteInterpolation(const std::vector<HermiteControlNode> &hermiteControlNodes,
float knot);
#endif

View File

@ -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;
}
// Its easier to handle negative t this way
if (t < 0.0)
return quaternionOvershootSlerp(q1, q0, 1.0 - t);
return QQuaternion::slerp(q0, q1, t);
}

View File

@ -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

View File

@ -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);
}

View File

@ -0,0 +1,680 @@
#include <QGraphicsScene>
#include <QBrush>
#include <QGraphicsPathItem>
#include <QMenu>
#include <QDebug>
#include <QLabel>
#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<HermiteControlNode> &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<HermiteControlNode> &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<HermiteControlNode> &nodes)
{
invalidateControlSelection();
fromNormalizedControlNodes(nodes);
refresh();
}
void InterpolationGraphicsWidget::setKeyframes(const std::vector<std::pair<float, QString>> &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<HermiteControlNode> 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<HermiteControlNode> 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<HermiteControlNode> 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 &current = 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 &current = 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);
}

View File

@ -0,0 +1,302 @@
#ifndef INTERPOLATION_GRAPHICS_WIDGET_H
#define INTERPOLATION_GRAPHICS_WIDGET_H
#include <QGraphicsView>
#include <QBrush>
#include <QPen>
#include <QGraphicsItemGroup>
#include <QGraphicsEllipseItem>
#include <QGraphicsLineItem>
#include <QPointF>
#include <QResizeEvent>
#include <QGraphicsSceneHoverEvent>
#include <QGraphicsProxyWidget>
#include <QGraphicsWidget>
#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<HermiteControlNode> &snapshot);
void fromNormalizedControlNodes(const std::vector<HermiteControlNode> &snapshot);
float cursorKnot() const;
public slots:
void setControlNodes(const std::vector<HermiteControlNode> &nodes);
void setKeyframes(const std::vector<std::pair<float, QString>> &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<HermiteControlNode> m_controlNodes;
std::vector<std::pair<float, QString>> m_keyframes;
QGraphicsPathItem *m_pathItem = nullptr;
std::vector<InterpolationGraphicsCircleItem *> m_anchorItems;
std::vector<std::pair<InterpolationGraphicsEdgeItem *, InterpolationGraphicsEdgeItem *>> m_lineItems;
std::vector<std::pair<InterpolationGraphicsCircleItem *, InterpolationGraphicsCircleItem *>> m_handleItems;
std::vector<QGraphicsLineItem *> m_gridRowLineItems;
std::vector<QGraphicsLineItem *> m_gridColumnLineItems;
std::vector<InterpolationGraphicsKeyframeItem *> m_keyframeKnotItems;
std::vector<QGraphicsProxyWidget *> 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

View File

@ -1,4 +1,5 @@
#include "jointnodetree.h"
#include "dust3dutil.h"
const std::vector<JointNode> &JointNodeTree::nodes() const
{
@ -72,3 +73,15 @@ JointNodeTree::JointNodeTree(const std::vector<AutoRiggerBone> *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;
}

View File

@ -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<JointNode> m_boneNodes;
};

298
src/motioneditwidget.cpp Normal file
View File

@ -0,0 +1,298 @@
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QGridLayout>
#include <QMessageBox>
#include <QDebug>
#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<HermiteControlNode> 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<HermiteControlNode> controlNodes)
{
m_graphicsWidget->setControlNodes(controlNodes);
}
void MotionEditWidget::syncKeyframesToGraphicsView()
{
std::vector<std::pair<float, QString>> 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<std::pair<float, QUuid>> 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<float, QUuid> &first, const std::pair<float, QUuid> &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<AutoRiggerBone> *rigBones = m_document->resultRigBones();
const std::map<int, AutoRiggerVertexWeights> *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<HermiteControlNode> 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();
}

56
src/motioneditwidget.h Normal file
View File

@ -0,0 +1,56 @@
#ifndef MOTION_EDIT_WIDGET_H
#define MOTION_EDIT_WIDGET_H
#include <QDialog>
#include <QLineEdit>
#include <QCloseEvent>
#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<HermiteControlNode> controlNodes, std::vector<std::pair<float, QUuid>> keyframes);
void setMotionControlNodes(QUuid motionId, std::vector<HermiteControlNode> controlNodes);
void setMotionKeyframes(QUuid motionId, std::vector<std::pair<float, QUuid>> 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<HermiteControlNode> controlNodes);
void setEditMotionKeyframes(std::vector<std::pair<float, QUuid>> 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<std::pair<float, QUuid>> m_keyframes;
bool m_unsaved = false;
bool m_closed = false;
bool m_isPreviewsObsolete = false;
MotionPreviewsGenerator *m_previewsGenerator = nullptr;
AnimationClipPlayer *m_clipPlayer = nullptr;
};
#endif

315
src/motionlistwidget.cpp Normal file
View File

@ -0,0 +1,315 @@
#include <QGuiApplication>
#include <QMenu>
#include <QXmlStreamWriter>
#include <QClipboard>
#include <QApplication>
#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<QUuid> 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<QUuid> unorderedMotionIds = m_selectedMotionIds;
if (!m_currentSelectedMotionId.isNull())
unorderedMotionIds.insert(m_currentSelectedMotionId);
std::vector<QUuid> 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(&copyAction, &QAction::triggered, this, &MotionListWidget::copy);
contextMenu.addAction(&copyAction);
}
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<QUuid> limitMotionIds = m_selectedMotionIds;
if (!m_currentSelectedMotionId.isNull())
limitMotionIds.insert(m_currentSelectedMotionId);
std::set<QUuid> 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);
}

44
src/motionlistwidget.h Normal file
View File

@ -0,0 +1,44 @@
#ifndef MOTION_LIST_WIDGET_H
#define MOTION_LIST_WIDGET_H
#include <QTreeWidget>
#include <map>
#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<QUuid, std::pair<QTreeWidgetItem *, int>> m_itemMap;
std::set<QUuid> m_selectedMotionIds;
QUuid m_currentSelectedMotionId;
QUuid m_shiftStartMotionId;
};
#endif

View File

@ -0,0 +1,76 @@
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QPushButton>
#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);
}

25
src/motionmanagewidget.h Normal file
View File

@ -0,0 +1,25 @@
#ifndef MOTION_MANAGE_WIDGET_H
#define MOTION_MANAGE_WIDGET_H
#include <QWidget>
#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

View File

@ -0,0 +1,153 @@
#include <QGuiApplication>
#include <QElapsedTimer>
#include "motionpreviewsgenerator.h"
#include "tetrapodposer.h"
#include "posemeshcreator.h"
MotionPreviewsGenerator::MotionPreviewsGenerator(const std::vector<AutoRiggerBone> *rigBones,
const std::map<int, AutoRiggerVertexWeights> *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<QString, std::map<QString, QString>> &parameters)
{
m_poses[poseId] = parameters;
}
void MotionPreviewsGenerator::addMotionToLibrary(const QUuid &motionId, const std::vector<HermiteControlNode> &controlNodes,
const std::vector<std::pair<float, QUuid>> &keyframes)
{
m_motions[motionId] = {controlNodes, keyframes};
}
void MotionPreviewsGenerator::addPreviewRequirement(const QUuid &motionId)
{
m_requiredMotionIds.insert(motionId);
}
const std::set<QUuid> &MotionPreviewsGenerator::requiredMotionIds()
{
return m_requiredMotionIds;
}
const std::set<QUuid> &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<HermiteControlNode> &controlNodes = findMotionResult->second.first;
std::vector<std::pair<float, QUuid>> 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<std::map<QString, std::map<QString, QString>>> 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<std::pair<float, MeshLoader *>> 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();
}

View File

@ -0,0 +1,45 @@
#ifndef MOTION_PREVIEWS_GENERATOR_H
#define MOTION_PREVIEWS_GENERATOR_H
#include <QObject>
#include <vector>
#include <map>
#include <set>
#include "meshloader.h"
#include "autorigger.h"
#include "curveutil.h"
#include "jointnodetree.h"
class MotionPreviewsGenerator : public QObject
{
Q_OBJECT
public:
MotionPreviewsGenerator(const std::vector<AutoRiggerBone> *rigBones,
const std::map<int, AutoRiggerVertexWeights> *rigWeights,
const MeshResultContext &meshResultContext);
~MotionPreviewsGenerator();
void addPoseToLibrary(const QUuid &poseId, const std::map<QString, std::map<QString, QString>> &parameters);
void addMotionToLibrary(const QUuid &motionId, const std::vector<HermiteControlNode> &controlNodes,
const std::vector<std::pair<float, QUuid>> &keyframes);
void addPreviewRequirement(const QUuid &motionId);
std::vector<std::pair<float, MeshLoader *>> takeResultPreviewMeshs(const QUuid &motionId);
const std::set<QUuid> &requiredMotionIds();
const std::set<QUuid> &generatedMotionIds();
void generateForMotion(const QUuid &motionId);
void generate();
signals:
void finished();
public slots:
void process();
private:
std::vector<AutoRiggerBone> m_rigBones;
std::map<int, AutoRiggerVertexWeights> m_rigWeights;
MeshResultContext m_meshResultContext;
std::map<QUuid, std::map<QString, std::map<QString, QString>>> m_poses;
std::map<QUuid, std::pair<std::vector<HermiteControlNode>, std::vector<std::pair<float, QUuid>>>> m_motions;
std::set<QUuid> m_requiredMotionIds;
std::set<QUuid> m_generatedMotionIds;
std::map<QUuid, std::vector<std::pair<float, MeshLoader *>>> m_resultPreviewMeshs;
int m_fps = 24;
};
#endif

104
src/motionwidget.cpp Normal file
View File

@ -0,0 +1,104 @@
#include <QVBoxLayout>
#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<std::pair<float, QString>> 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);
}

33
src/motionwidget.h Normal file
View File

@ -0,0 +1,33 @@
#ifndef MOTION_WIDGET_H
#define MOTION_WIDGET_H
#include <QFrame>
#include <QLabel>
#include <QIcon>
#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

View File

@ -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<QUuid> 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();

View File

@ -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<QUuid> m_selectedPoseIds;
QUuid m_currentSelectedPoseId;
QUuid m_shiftStartPoseId;
bool m_cornerButtonVisible = false;
bool m_hasContextMenu = true;
};
#endif

View File

@ -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);

View File

@ -2,10 +2,10 @@
#include "posemeshcreator.h"
#include "skinnedmeshcreator.h"
PoseMeshCreator::PoseMeshCreator(const Poser &poser,
PoseMeshCreator::PoseMeshCreator(const std::vector<JointNode> &resultNodes,
const MeshResultContext &meshResultContext,
const std::map<int, AutoRiggerVertexWeights> &resultWeights) :
m_resultNodes(poser.resultNodes()),
m_resultNodes(resultNodes),
m_meshResultContext(meshResultContext),
m_resultWeights(resultWeights)
{

View File

@ -1,7 +1,6 @@
#ifndef POSE_MESH_CREATOR_H
#define POSE_MESH_CREATOR_H
#include <QObject>
#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<JointNode> &resultNodes,
const MeshResultContext &meshResultContext,
const std::map<int, AutoRiggerVertexWeights> &resultWeights);
~PoseMeshCreator();

View File

@ -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);

View File

@ -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;

View File

@ -40,6 +40,11 @@ const std::vector<JointNode> &Poser::resultNodes() const
return m_jointNodeTree.nodes();
}
const JointNodeTree &Poser::resultJointNodeTree() const
{
return m_jointNodeTree;
}
void Poser::commit()
{
m_jointNodeTree.recalculateTransformMatrices();

View File

@ -15,6 +15,7 @@ public:
int findBoneIndex(const QString &name);
const std::vector<AutoRiggerBone> &bones() const;
const std::vector<JointNode> &resultNodes() const;
const JointNodeTree &resultJointNodeTree() const;
std::map<QString, std::map<QString, QString>> &parameters();
virtual void commit();
void reset();

View File

@ -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);

View File

@ -2,6 +2,7 @@
#define POSE_WIDGET_H
#include <QFrame>
#include <QLabel>
#include <QIcon>
#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

View File

@ -46,6 +46,11 @@ MeshLoader *RigGenerator::takeResultMesh()
return resultMesh;
}
bool RigGenerator::isSucceed()
{
return m_isSucceed;
}
const std::vector<QString> &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<QColor> inputVerticesColors(m_meshResultContext->vertices.size());
if (rigSucceed) {
if (m_isSucceed) {
const auto &resultWeights = m_autoRigger->resultWeights();
const auto &resultBones = m_autoRigger->resultBones();

View File

@ -19,6 +19,7 @@ public:
const std::vector<QString> &missingMarkNames();
const std::vector<QString> &errorMarkNames();
MeshResultContext *takeMeshResultContext();
bool isSucceed();
signals:
void finished();
public slots:
@ -31,6 +32,7 @@ private:
std::map<int, AutoRiggerVertexWeights> *m_resultWeights = nullptr;
std::vector<QString> m_missingMarkNames;
std::vector<QString> m_errorMarkNames;
bool m_isSucceed = false;
};
#endif

View File

@ -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<QString, std::map<QString,
emit optionsChanged();
}
void SkeletonDocument::addMotion(QString name, std::vector<HermiteControlNode> controlNodes, std::vector<std::pair<float, QUuid>> 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<HermiteControlNode> 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<std::pair<float, QUuid>> 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<QUuid> &limitNodeIds,
SkeletonDocumentToSnapshotFor forWhat, const std::set<QUuid> &limitPoseIds) const
SkeletonDocumentToSnapshotFor forWhat, const std::set<QUuid> &limitPoseIds, const std::set<QUuid> &limitMotionIds) const
{
if (SkeletonDocumentToSnapshotFor::Document == forWhat ||
SkeletonDocumentToSnapshotFor::Nodes == forWhat) {
@ -883,6 +966,43 @@ void SkeletonDocument::toSnapshot(SkeletonSnapshot *snapshot, const std::set<QUu
snapshot->poses.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<QString, QString> motion;
motion["id"] = motionIt.second.id.toString();
if (!motionIt.second.name.isEmpty())
motion["name"] = motionIt.second.name;
std::vector<std::map<QString, QString>> controlNodesAttributes;
std::vector<std::map<QString, QString>> keyframesAttributes;
for (const auto &controlNode: motionIt.second.controlNodes) {
std::map<QString, QString> 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<QString, QString> 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<QString, QString> 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("<node "))
if (-1 != mimeData->text().indexOf("<node "))
return true;
}
return false;
@ -2087,7 +2249,18 @@ bool SkeletonDocument::hasPastablePosesInClipboard() const
const QClipboard *clipboard = QApplication::clipboard();
const QMimeData *mimeData = clipboard->mimeData();
if (mimeData->hasText()) {
if (-1 != mimeData->text().right(1000).indexOf("<pose "))
if (-1 != mimeData->text().indexOf("<pose "))
return true;
}
return false;
}
bool SkeletonDocument::hasPastableMotionsInClipboard() const
{
const QClipboard *clipboard = QApplication::clipboard();
const QMimeData *mimeData = clipboard->mimeData();
if (mimeData->hasText()) {
if (-1 != mimeData->text().indexOf("<motion "))
return true;
}
return false;
@ -2378,6 +2551,8 @@ void SkeletonDocument::generateRig()
void SkeletonDocument::rigReady()
{
m_currentRigSucceed = m_rigGenerator->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) {

View File

@ -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<HermiteControlNode> controlNodes;
std::vector<std::pair<float, QUuid>> keyframes; //std::pair<timeslot:0~1, poseId>
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<QUuid, SkeletonComponent> componentMap;
std::map<QUuid, SkeletonPose> poseMap;
std::vector<QUuid> poseIdList;
std::map<QUuid, SkeletonMotion> motionMap;
std::vector<QUuid> motionIdList;
SkeletonComponent rootComponent;
QImage turnaround;
QImage preview;
void toSnapshot(SkeletonSnapshot *snapshot, const std::set<QUuid> &limitNodeIds=std::set<QUuid>(),
SkeletonDocumentToSnapshotFor forWhat=SkeletonDocumentToSnapshotFor::Document,
const std::set<QUuid> &limitPoseIds=std::set<QUuid>()) const;
const std::set<QUuid> &limitPoseIds=std::set<QUuid>(),
const std::set<QUuid> &limitMotionIds=std::set<QUuid>()) 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<QString> &resultRigMissingMarkNames() const;
const std::vector<QString> &resultRigErrorMarkNames() const;
const MeshResultContext &currentRiggedResultContext() 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<QString, std::map<QString, QString>> parameters);
void renamePose(QUuid poseId, QString name);
void addMotion(QString name, std::vector<HermiteControlNode> controlNodes, std::vector<std::pair<float, QUuid>> keyframes);
void removeMotion(QUuid motionId);
void setMotionControlNodes(QUuid motionId, std::vector<HermiteControlNode> controlNodes);
void setMotionKeyframes(QUuid motionId, std::vector<std::pair<float, QUuid>> keyframes);
void renameMotion(QUuid motionId, QString name);
private:
void splitPartByNode(std::vector<std::vector<QUuid>> *groups, QUuid nodeId);
void joinNodeAndNeiborsToGroup(std::vector<QUuid> *group, QUuid nodeId, std::set<QUuid> *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<SkeletonHistoryItem> m_undoItems;

View File

@ -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();

View File

@ -137,6 +137,7 @@ private:
QAction *m_showDebugDialogAction;
QAction *m_showRigAction;
QAction *m_showPosesAction;
QAction *m_showMotionsAction;
QAction *m_showAdvanceSettingAction;
QMenu *m_helpMenu;

View File

@ -16,6 +16,7 @@ public:
std::map<QString, std::map<QString, QString>> components;
std::map<QString, QString> rootComponent;
std::vector<std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>>> poses; // std::pair<Pose attributes, Bone attributes>
std::vector<std::tuple<std::map<QString, QString>, std::vector<std::map<QString, QString>>, std::vector<std::map<QString, QString>>>> motions; // std::tuple<Motion attributes, controlNodes, keyframes>
public:
void resolveBoundingBox(QRectF *mainProfile, QRectF *sideProfile, const QString &partId=QString());
};

View File

@ -99,6 +99,7 @@ void saveSkeletonToXmlStream(SkeletonSnapshot *snapshot, QXmlStreamWriter *write
for (poseAttributeIterator = poseIterator->first.begin(); poseAttributeIterator != poseIterator->first.end(); poseAttributeIterator++) {
writer->writeAttribute(poseAttributeIterator->first, poseAttributeIterator->second);
}
writer->writeStartElement("parameters");
std::map<QString, std::map<QString, QString>>::iterator itemsIterator;
for (itemsIterator = poseIterator->second.begin(); itemsIterator != poseIterator->second.end(); itemsIterator++) {
std::map<QString, QString>::iterator parametersIterator;
@ -111,6 +112,47 @@ void saveSkeletonToXmlStream(SkeletonSnapshot *snapshot, QXmlStreamWriter *write
writer->writeEndElement();
}
writer->writeEndElement();
writer->writeEndElement();
}
writer->writeEndElement();
writer->writeStartElement("motions");
std::vector<std::tuple<std::map<QString, QString>, std::vector<std::map<QString, QString>>, std::vector<std::map<QString, QString>>>>::iterator motionIterator;
for (motionIterator = snapshot->motions.begin(); motionIterator != snapshot->motions.end(); motionIterator++) {
std::map<QString, QString>::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<std::map<QString, QString>>::iterator itemsIterator;
for (itemsIterator = std::get<1>(*motionIterator).begin(); itemsIterator != std::get<1>(*motionIterator).end(); itemsIterator++) {
std::map<QString, QString>::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<std::map<QString, QString>>::iterator itemsIterator;
for (itemsIterator = std::get<2>(*motionIterator).begin(); itemsIterator != std::get<2>(*motionIterator).end(); itemsIterator++) {
std::map<QString, QString>::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<QString> componentStack;
std::vector<QString> elementNameStack;
std::pair<std::map<QString, QString>, std::map<QString, std::map<QString, QString>>> currentPose;
std::tuple<std::map<QString, QString>, std::vector<std::map<QString, QString>>, std::vector<std::map<QString, QString>>> 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<QString, QString> 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<QString, QString> 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);
}
}
}

View File

@ -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()

View File

@ -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;

View File

@ -1,41 +0,0 @@
#ifndef VIDEO_FRAME_EXTRACTOR_H
#define VIDEO_FRAME_EXTRACTOR_H
#include <QObject>
#include <QImage>
#include <QTemporaryFile>
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<VideoFrame> *takeResultFrames();
signals:
void finished();
public slots:
void process();
private:
void release();
private:
QString m_fileName;
QString m_realPath;
QTemporaryFile *m_fileHandle = nullptr;
std::vector<VideoFrame> *m_resultFrames = nullptr;
int m_maxFrames;
float m_thumbnailHeight = 0;
};
#endif