Merge branch 'curveeditor'
commit
b02ffedb9d
|
@ -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>
|
||||
|
||||
|
|
27
dust3d.pro
27
dust3d.pro
|
@ -200,6 +200,30 @@ HEADERS += src/meshweldseam.h
|
|||
SOURCES += src/advancesettingwidget.cpp
|
||||
HEADERS += src/advancesettingwidget.h
|
||||
|
||||
SOURCES += src/curveutil.cpp
|
||||
HEADERS += src/curveutil.h
|
||||
|
||||
SOURCES += src/interpolationgraphicswidget.cpp
|
||||
HEADERS += src/interpolationgraphicswidget.h
|
||||
|
||||
SOURCES += src/motioneditwidget.cpp
|
||||
HEADERS += src/motioneditwidget.h
|
||||
|
||||
SOURCES += src/motionmanagewidget.cpp
|
||||
HEADERS += src/motionmanagewidget.h
|
||||
|
||||
SOURCES += src/motionlistwidget.cpp
|
||||
HEADERS += src/motionlistwidget.h
|
||||
|
||||
SOURCES += src/motionwidget.cpp
|
||||
HEADERS += src/motionwidget.h
|
||||
|
||||
SOURCES += src/motionpreviewsgenerator.cpp
|
||||
HEADERS += src/motionpreviewsgenerator.h
|
||||
|
||||
SOURCES += src/animationclipplayer.cpp
|
||||
HEADERS += src/animationclipplayer.h
|
||||
|
||||
SOURCES += src/main.cpp
|
||||
|
||||
HEADERS += src/version.h
|
||||
|
@ -436,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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -56,3 +56,21 @@ QString unifiedWindowTitle(const QString &text)
|
|||
{
|
||||
return text + QObject::tr(" - ") + APP_NAME;
|
||||
}
|
||||
|
||||
// Sam Hocevar's answer
|
||||
// https://gamedev.stackexchange.com/questions/98246/quaternion-slerp-and-lerp-implementation-with-overshoot
|
||||
QQuaternion quaternionOvershootSlerp(const QQuaternion &q0, const QQuaternion &q1, float t)
|
||||
{
|
||||
// If t is too large, divide it by two recursively
|
||||
if (t > 1.0) {
|
||||
auto tmp = quaternionOvershootSlerp(q0, q1, t / 2);
|
||||
return tmp * q0.inverted() * tmp;
|
||||
}
|
||||
|
||||
// It’s easier to handle negative t this way
|
||||
if (t < 0.0)
|
||||
return quaternionOvershootSlerp(q1, q0, 1.0 - t);
|
||||
|
||||
return QQuaternion::slerp(q0, q1, t);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 ¤t = m_controlNodes[index];
|
||||
int newX = current.position.x();
|
||||
auto inTangent = current.outTangent;
|
||||
if (index + 1 >= (int)m_controlNodes.size()) {
|
||||
newX = (current.position.x() + scene()->sceneRect().right()) / 2;
|
||||
} else {
|
||||
const auto &next = m_controlNodes[index + 1];
|
||||
newX = (current.position.x() + next.position.x()) / 2;
|
||||
inTangent = (current.outTangent + next.inTangent) / 2;
|
||||
}
|
||||
auto outTangent = inTangent;
|
||||
|
||||
HermiteControlNode newNode(QVector2D(newX, current.position.y()), inTangent, outTangent);
|
||||
m_controlNodes.insert(m_controlNodes.begin() + index + 1, newNode);
|
||||
refresh();
|
||||
|
||||
emit controlNodesChanged();
|
||||
}
|
||||
|
||||
void InterpolationGraphicsWidget::addControlNodeBefore(int index)
|
||||
{
|
||||
if (index >= (int)m_controlNodes.size())
|
||||
return;
|
||||
|
||||
invalidateControlSelection();
|
||||
|
||||
const auto ¤t = m_controlNodes[index];
|
||||
int newX = current.position.x();
|
||||
auto inTangent = current.outTangent;
|
||||
if (index - 1 < 0) {
|
||||
newX = (current.position.x() + scene()->sceneRect().left()) / 2;
|
||||
} else {
|
||||
const auto &previous= m_controlNodes[index - 1];
|
||||
newX = (current.position.x() + previous.position.x()) / 2;
|
||||
inTangent = (current.inTangent + previous.outTangent) / 2;
|
||||
}
|
||||
auto outTangent = inTangent;
|
||||
|
||||
HermiteControlNode newNode(QVector2D(newX, current.position.y()), inTangent, outTangent);
|
||||
m_controlNodes.insert(m_controlNodes.begin() + index, newNode);
|
||||
refresh();
|
||||
|
||||
emit controlNodesChanged();
|
||||
}
|
||||
|
||||
void InterpolationGraphicsWidget::invalidateControlSelection()
|
||||
{
|
||||
m_currentHoveringControlIndex = -1;
|
||||
if (-1 != m_currentSelectedControlIndex)
|
||||
selectControlNode(-1);
|
||||
}
|
||||
|
||||
void InterpolationGraphicsWidget::invalidateKeyframeSelection()
|
||||
{
|
||||
m_currentHoveringKeyframeIndex = -1;
|
||||
if (-1 != m_currentSelectedKeyframeIndex)
|
||||
selectKeyframe(-1);
|
||||
}
|
||||
|
||||
void InterpolationGraphicsWidget::addControlNodeAtPosition(const QPointF &pos)
|
||||
{
|
||||
auto inTangent = QVector2D(0, 50);
|
||||
auto outTangent = inTangent;
|
||||
HermiteControlNode newNode(QVector2D(pos.x(), pos.y()), inTangent, outTangent);
|
||||
|
||||
invalidateControlSelection();
|
||||
|
||||
m_controlNodes.insert(m_controlNodes.end(), newNode);
|
||||
refresh();
|
||||
|
||||
emit controlNodesChanged();
|
||||
}
|
||||
|
||||
void InterpolationGraphicsWidget::hoverControlNode(int index)
|
||||
{
|
||||
if (index >= (int)m_controlNodes.size())
|
||||
return;
|
||||
|
||||
if (m_currentHoveringControlIndex == index)
|
||||
return;
|
||||
|
||||
m_currentHoveringControlIndex = index;
|
||||
}
|
||||
|
||||
void InterpolationGraphicsWidget::unhoverControlNode(int index)
|
||||
{
|
||||
if (index >= (int)m_controlNodes.size())
|
||||
return;
|
||||
|
||||
if (m_currentHoveringControlIndex == index) {
|
||||
m_currentHoveringControlIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void InterpolationGraphicsWidget::hoverKeyframe(int index)
|
||||
{
|
||||
if (index >= (int)m_keyframes.size())
|
||||
return;
|
||||
|
||||
if (m_currentHoveringKeyframeIndex == index)
|
||||
return;
|
||||
|
||||
m_currentHoveringKeyframeIndex = index;
|
||||
|
||||
QLabel *nameLabel = (QLabel *)m_keyframeNameItems[m_currentHoveringKeyframeIndex]->widget();
|
||||
nameLabel->setStyleSheet("QLabel {color : " + Theme::red.name() + ";}");
|
||||
}
|
||||
|
||||
void InterpolationGraphicsWidget::unhoverKeyframe(int index)
|
||||
{
|
||||
if (index >= (int)m_keyframes.size())
|
||||
return;
|
||||
|
||||
if (m_currentHoveringKeyframeIndex == index) {
|
||||
if (m_currentSelectedKeyframeIndex != m_currentHoveringKeyframeIndex) {
|
||||
QLabel *nameLabel = (QLabel *)m_keyframeNameItems[m_currentHoveringKeyframeIndex]->widget();
|
||||
nameLabel->setStyleSheet("QLabel {color : " + Theme::white.name() + ";}");
|
||||
}
|
||||
m_currentHoveringKeyframeIndex = -1;
|
||||
}
|
||||
}
|
||||
|
||||
void InterpolationGraphicsWidget::selectKeyframe(int index)
|
||||
{
|
||||
if (m_currentSelectedKeyframeIndex == index)
|
||||
return;
|
||||
|
||||
if (-1 != m_currentSelectedKeyframeIndex && m_currentSelectedKeyframeIndex < (int)m_keyframes.size()) {
|
||||
QLabel *nameLabel = (QLabel *)m_keyframeNameItems[m_currentSelectedKeyframeIndex]->widget();
|
||||
nameLabel->setStyleSheet("QLabel {color : " + Theme::white.name() + ";}");
|
||||
}
|
||||
|
||||
m_currentSelectedKeyframeIndex = index;
|
||||
|
||||
if (-1 != m_currentSelectedKeyframeIndex && m_currentSelectedKeyframeIndex < (int)m_keyframes.size()) {
|
||||
QLabel *nameLabel = (QLabel *)m_keyframeNameItems[m_currentSelectedKeyframeIndex]->widget();
|
||||
nameLabel->setStyleSheet("QLabel {color : " + Theme::red.name() + ";}");
|
||||
}
|
||||
}
|
||||
|
||||
void InterpolationGraphicsWidget::selectControlNode(int index)
|
||||
{
|
||||
if (m_currentSelectedControlIndex == index)
|
||||
return;
|
||||
|
||||
if (-1 != m_currentSelectedControlIndex && m_currentSelectedControlIndex < (int)m_controlNodes.size()) {
|
||||
m_anchorItems[m_currentSelectedControlIndex]->setChecked(false);
|
||||
m_handleItems[m_currentSelectedControlIndex].first->setChecked(false);
|
||||
m_handleItems[m_currentSelectedControlIndex].second->setChecked(false);
|
||||
}
|
||||
|
||||
m_currentSelectedControlIndex = index;
|
||||
|
||||
if (-1 != m_currentSelectedControlIndex && m_currentSelectedControlIndex < (int)m_controlNodes.size()) {
|
||||
m_anchorItems[m_currentSelectedControlIndex]->setChecked(true);
|
||||
m_handleItems[m_currentSelectedControlIndex].first->setChecked(true);
|
||||
m_handleItems[m_currentSelectedControlIndex].second->setChecked(true);
|
||||
}
|
||||
}
|
||||
|
||||
void InterpolationGraphicsWidget::showContextMenu(const QPoint &pos)
|
||||
{
|
||||
if (m_previewOnly)
|
||||
return;
|
||||
|
||||
QMenu contextMenu(this);
|
||||
|
||||
QAction addAction(tr("Add Control Node"), this);
|
||||
QAction addAfterAction(tr("Add Control Node After"), this);
|
||||
QAction addBeforeAction(tr("Add Control Node Before"), this);
|
||||
if (-1 != m_currentSelectedControlIndex) {
|
||||
connect(&addAfterAction, &QAction::triggered, this, [=]() {
|
||||
addControlNodeAfter(m_currentSelectedControlIndex);
|
||||
});
|
||||
contextMenu.addAction(&addAfterAction);
|
||||
|
||||
connect(&addBeforeAction, &QAction::triggered, this, [=]() {
|
||||
addControlNodeBefore(m_currentSelectedControlIndex);
|
||||
});
|
||||
contextMenu.addAction(&addBeforeAction);
|
||||
} else if (m_controlNodes.empty()) {
|
||||
connect(&addAction, &QAction::triggered, this, [=]() {
|
||||
addControlNodeAtPosition(mapToScene(pos));
|
||||
});
|
||||
contextMenu.addAction(&addAction);
|
||||
}
|
||||
|
||||
QAction deleteControlNodeAction(tr("Delete Control Node"), this);
|
||||
if (-1 != m_currentSelectedControlIndex) {
|
||||
connect(&deleteControlNodeAction, &QAction::triggered, this, [=]() {
|
||||
deleteControlNode(m_currentSelectedControlIndex);
|
||||
});
|
||||
contextMenu.addAction(&deleteControlNodeAction);
|
||||
}
|
||||
|
||||
QAction deleteKeyframeAction(tr("Delete Keyframe"), this);
|
||||
if (-1 != m_currentSelectedKeyframeIndex) {
|
||||
connect(&deleteKeyframeAction, &QAction::triggered, this, [=]() {
|
||||
deleteKeyframe(m_currentSelectedKeyframeIndex);
|
||||
});
|
||||
contextMenu.addAction(&deleteKeyframeAction);
|
||||
}
|
||||
|
||||
contextMenu.exec(mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void InterpolationGraphicsWidget::setPreviewOnly(bool previewOnly)
|
||||
{
|
||||
if (m_previewOnly == previewOnly)
|
||||
return;
|
||||
m_previewOnly = previewOnly;
|
||||
m_cursorItem->setVisible(!m_previewOnly);
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
}
|
|
@ -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
|
|
@ -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(©Action, &QAction::triggered, this, &MotionListWidget::copy);
|
||||
contextMenu.addAction(©Action);
|
||||
}
|
||||
|
||||
QAction pasteAction(tr("Paste"), this);
|
||||
if (m_document->hasPastableMotionsInClipboard()) {
|
||||
connect(&pasteAction, &QAction::triggered, m_document, &SkeletonDocument::paste);
|
||||
contextMenu.addAction(&pasteAction);
|
||||
}
|
||||
|
||||
QAction deleteAction(tr("Delete"), this);
|
||||
if (!motionIds.empty()) {
|
||||
connect(&deleteAction, &QAction::triggered, [=]() {
|
||||
for (const auto &motionId: motionIds)
|
||||
emit removeMotion(motionId);
|
||||
});
|
||||
contextMenu.addAction(&deleteAction);
|
||||
}
|
||||
|
||||
contextMenu.exec(mapToGlobal(pos));
|
||||
}
|
||||
|
||||
void MotionListWidget::resizeEvent(QResizeEvent *event)
|
||||
{
|
||||
QTreeWidget::resizeEvent(event);
|
||||
if (calculateColumnCount() != columnCount())
|
||||
reload();
|
||||
}
|
||||
|
||||
int MotionListWidget::calculateColumnCount()
|
||||
{
|
||||
if (nullptr == parentWidget())
|
||||
return 0;
|
||||
|
||||
int columns = parentWidget()->width() / Theme::motionPreviewImageSize;
|
||||
if (0 == columns)
|
||||
columns = 1;
|
||||
return columns;
|
||||
}
|
||||
|
||||
void MotionListWidget::reload()
|
||||
{
|
||||
removeAllContent();
|
||||
|
||||
int columns = calculateColumnCount();
|
||||
if (0 == columns)
|
||||
return;
|
||||
|
||||
int columnWidth = parentWidget()->width() / columns;
|
||||
|
||||
//qDebug() << "parentWidth:" << parentWidget()->width() << "columnWidth:" << columnWidth << "columns:" << columns;
|
||||
|
||||
setColumnCount(columns);
|
||||
for (int i = 0; i < columns; i++)
|
||||
setColumnWidth(i, columnWidth);
|
||||
|
||||
decltype(m_document->motionIdList.size()) motionIndex = 0;
|
||||
while (motionIndex < m_document->motionIdList.size()) {
|
||||
QTreeWidgetItem *item = new QTreeWidgetItem(this);
|
||||
item->setFlags((item->flags() | Qt::ItemIsEnabled) & ~(Qt::ItemIsSelectable) & ~(Qt::ItemIsEditable));
|
||||
for (int col = 0; col < columns && motionIndex < m_document->motionIdList.size(); col++, motionIndex++) {
|
||||
const auto &motionId = m_document->motionIdList[motionIndex];
|
||||
item->setSizeHint(col, QSize(columnWidth, MotionWidget::preferredHeight() + 2));
|
||||
item->setData(col, Qt::UserRole, motionId.toString());
|
||||
MotionWidget *widget = new MotionWidget(m_document, motionId);
|
||||
connect(widget, &MotionWidget::modifyMotion, this, &MotionListWidget::modifyMotion);
|
||||
setItemWidget(item, col, widget);
|
||||
widget->reload();
|
||||
widget->updateCheckedState(isMotionSelected(motionId));
|
||||
m_itemMap[motionId] = std::make_pair(item, col);
|
||||
}
|
||||
invisibleRootItem()->addChild(item);
|
||||
}
|
||||
}
|
||||
|
||||
void MotionListWidget::removeAllContent()
|
||||
{
|
||||
m_itemMap.clear();
|
||||
clear();
|
||||
}
|
||||
|
||||
bool MotionListWidget::mouseMove(QMouseEvent *event)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MotionListWidget::wheel(QWheelEvent *event)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MotionListWidget::mouseRelease(QMouseEvent *event)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MotionListWidget::mousePress(QMouseEvent *event)
|
||||
{
|
||||
if (event->button() == Qt::RightButton) {
|
||||
showContextMenu(mapFromGlobal(event->globalPos()));
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MotionListWidget::mouseDoubleClick(QMouseEvent *event)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool MotionListWidget::keyPress(QKeyEvent *event)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
void MotionListWidget::copy()
|
||||
{
|
||||
if (m_selectedMotionIds.empty() && m_currentSelectedMotionId.isNull())
|
||||
return;
|
||||
|
||||
std::set<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);
|
||||
}
|
|
@ -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
|
|
@ -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);
|
||||
}
|
|
@ -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
|
|
@ -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>> ¶meters)
|
||||
{
|
||||
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();
|
||||
}
|
|
@ -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>> ¶meters);
|
||||
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
|
|
@ -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);
|
||||
}
|
|
@ -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
|
|
@ -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();
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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>> ¶meters();
|
||||
virtual void commit();
|
||||
void reset();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 ¤tRiggedResultContext() const;
|
||||
bool currentRigSucceed() const;
|
||||
public slots:
|
||||
void removeNode(QUuid nodeId);
|
||||
void removeEdge(QUuid edgeId);
|
||||
|
@ -602,6 +631,11 @@ public slots:
|
|||
void removePose(QUuid poseId);
|
||||
void setPoseParameters(QUuid poseId, std::map<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;
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -137,6 +137,7 @@ private:
|
|||
QAction *m_showDebugDialogAction;
|
||||
QAction *m_showRigAction;
|
||||
QAction *m_showPosesAction;
|
||||
QAction *m_showMotionsAction;
|
||||
QAction *m_showAdvanceSettingAction;
|
||||
|
||||
QMenu *m_helpMenu;
|
||||
|
|
|
@ -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());
|
||||
};
|
||||
|
|
|
@ -96,20 +96,62 @@ void saveSkeletonToXmlStream(SkeletonSnapshot *snapshot, QXmlStreamWriter *write
|
|||
for (poseIterator = snapshot->poses.begin(); poseIterator != snapshot->poses.end(); poseIterator++) {
|
||||
std::map<QString, QString>::iterator poseAttributeIterator;
|
||||
writer->writeStartElement("pose");
|
||||
for (poseAttributeIterator = poseIterator->first.begin(); poseAttributeIterator != poseIterator->first.end(); poseAttributeIterator++) {
|
||||
writer->writeAttribute(poseAttributeIterator->first, poseAttributeIterator->second);
|
||||
}
|
||||
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;
|
||||
writer->writeStartElement("parameter");
|
||||
writer->writeAttribute("for", itemsIterator->first);
|
||||
for (parametersIterator = itemsIterator->second.begin(); parametersIterator != itemsIterator->second.end();
|
||||
parametersIterator++) {
|
||||
writer->writeAttribute(parametersIterator->first, parametersIterator->second);
|
||||
for (poseAttributeIterator = poseIterator->first.begin(); poseAttributeIterator != poseIterator->first.end(); poseAttributeIterator++) {
|
||||
writer->writeAttribute(poseAttributeIterator->first, poseAttributeIterator->second);
|
||||
}
|
||||
writer->writeStartElement("parameters");
|
||||
std::map<QString, std::map<QString, QString>>::iterator itemsIterator;
|
||||
for (itemsIterator = poseIterator->second.begin(); itemsIterator != poseIterator->second.end(); itemsIterator++) {
|
||||
std::map<QString, QString>::iterator parametersIterator;
|
||||
writer->writeStartElement("parameter");
|
||||
writer->writeAttribute("for", itemsIterator->first);
|
||||
for (parametersIterator = itemsIterator->second.begin(); parametersIterator != itemsIterator->second.end();
|
||||
parametersIterator++) {
|
||||
writer->writeAttribute(parametersIterator->first, parametersIterator->second);
|
||||
}
|
||||
writer->writeEndElement();
|
||||
}
|
||||
writer->writeEndElement();
|
||||
writer->writeEndElement();
|
||||
}
|
||||
writer->writeEndElement();
|
||||
|
||||
writer->writeStartElement("motions");
|
||||
std::vector<std::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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue