Disallow the creation of invalid curve

but allow invalid initialized curves to be moved into a legal state.
Visualize invalid curve-segments by painting them in the errorColor

Fixes: QDS-2130
Change-Id: Ida44c3b4f5e3d113df7d1e8e7a2b965d26f43815
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
This commit is contained in:
Knud Dollereder
2020-09-25 13:08:23 +02:00
parent a3e92da8b8
commit 26ef972310
9 changed files with 207 additions and 9 deletions

View File

@@ -71,6 +71,7 @@ struct CurveItemStyleOption
{ {
double width = 1.0; double width = 1.0;
QColor color = QColor(0, 200, 0); QColor color = QColor(0, 200, 0);
QColor errorColor = QColor(200, 0, 0);
QColor selectionColor = QColor(200, 200, 200); QColor selectionColor = QColor(200, 200, 200);
QColor easingCurveColor = QColor(200, 0, 200); QColor easingCurveColor = QColor(200, 0, 200);
QColor lockedColor = QColor(50, 50, 50); QColor lockedColor = QColor(50, 50, 50);

View File

@@ -164,6 +164,29 @@ bool CurveSegment::isValid() const
return true; return true;
} }
bool CurveSegment::isLegal() const
{
if (!isValid())
return false;
if (interpolation() == Keyframe::Interpolation::Step)
return true;
if (interpolation() == Keyframe::Interpolation::Linear)
return true;
std::vector<double> ex = CubicPolynomial(m_left.position().x(),
m_left.rightHandle().x(),
m_right.leftHandle().x(),
m_right.position().x())
.extrema();
ex.erase(std::remove_if(ex.begin(), ex.end(), [](double val) { return val <= 0. || val >= 1.; }),
ex.end());
return ex.size() == 0;
}
bool CurveSegment::containsX(double x) const bool CurveSegment::containsX(double x) const
{ {
return m_left.position().x() <= x && m_right.position().x() >= x; return m_left.position().x() <= x && m_right.position().x() >= x;
@@ -500,6 +523,31 @@ void CurveSegment::setRight(const Keyframe &frame)
m_right = frame; m_right = frame;
} }
void CurveSegment::moveLeftTo(const QPointF &pos)
{
QPointF delta = pos - m_left.position();
if (m_left.hasLeftHandle())
m_left.setLeftHandle(m_left.leftHandle() + delta);
if (m_left.hasRightHandle())
m_left.setRightHandle(m_left.rightHandle() + delta);
m_left.setPosition(pos);
}
void CurveSegment::moveRightTo(const QPointF &pos)
{
QPointF delta = pos - m_right.position();
if (m_right.hasLeftHandle())
m_right.setLeftHandle(m_right.leftHandle() + delta);
if (m_right.hasRightHandle())
m_right.setRightHandle(m_right.rightHandle() + delta);
m_right.setPosition(pos);
}
void CurveSegment::setInterpolation(const Keyframe::Interpolation &interpol) void CurveSegment::setInterpolation(const Keyframe::Interpolation &interpol)
{ {
m_right.setInterpolation(interpol); m_right.setInterpolation(interpol);

View File

@@ -47,6 +47,8 @@ public:
bool isValid() const; bool isValid() const;
bool isLegal() const;
bool containsX(double x) const; bool containsX(double x) const;
Keyframe left() const; Keyframe left() const;
@@ -83,6 +85,10 @@ public:
void setRight(const Keyframe &frame); void setRight(const Keyframe &frame);
void moveLeftTo(const QPointF &pos);
void moveRightTo(const QPointF &pos);
void setInterpolation(const Keyframe::Interpolation &interpol); void setInterpolation(const Keyframe::Interpolation &interpol);
private: private:

View File

@@ -119,6 +119,8 @@ void CurveItem::paint(QPainter *painter, const QStyleOptionGraphicsItem *, QWidg
} else { } else {
if (locked()) if (locked())
pen.setColor(m_style.lockedColor); pen.setColor(m_style.lockedColor);
else if (!segment.isLegal())
pen.setColor(m_style.errorColor);
else if (isUnderMouse()) else if (isUnderMouse())
pen.setColor(m_style.hoverColor); pen.setColor(m_style.hoverColor);
else if (hasSelectedKeyframe()) else if (hasSelectedKeyframe())
@@ -178,6 +180,36 @@ bool CurveItem::hasEditableSegment(double time) const
return curve().segment(time).interpolation() != Keyframe::Interpolation::Easing; return curve().segment(time).interpolation() != Keyframe::Interpolation::Easing;
} }
bool CurveItem::isFirst(const KeyframeItem *key) const
{
if (m_keyframes.empty())
return false;
return m_keyframes.first() == key;
}
bool CurveItem::isLast(const KeyframeItem *key) const
{
if (m_keyframes.empty())
return false;
return m_keyframes.last() == key;
}
int CurveItem::indexOf(const KeyframeItem *key) const
{
if (m_keyframes.empty())
return -1;
int out = 0;
for (auto &&el : m_keyframes) {
if (el == key)
return out;
out++;
}
return -1;
}
unsigned int CurveItem::id() const unsigned int CurveItem::id() const
{ {
return m_id; return m_id;
@@ -283,6 +315,22 @@ QVector<HandleItem *> CurveItem::handles() const
return out; return out;
} }
CurveSegment CurveItem::segment(const KeyframeItem *keyframe, HandleItem::Slot slot) const
{
auto finder = [keyframe](KeyframeItem *item) { return item == keyframe; };
auto iter = std::find_if(m_keyframes.begin(), m_keyframes.end(), finder);
if (iter == m_keyframes.end())
return CurveSegment();
int index = static_cast<int>(std::distance(m_keyframes.begin(), iter));
if (slot == HandleItem::Slot::Left && index > 0)
return CurveSegment(m_keyframes[index - 1]->keyframe(), keyframe->keyframe());
else if (slot == HandleItem::Slot::Right && index < (m_keyframes.size() - 1))
return CurveSegment(keyframe->keyframe(), m_keyframes[index + 1]->keyframe());
return CurveSegment();
}
void CurveItem::restore() void CurveItem::restore()
{ {
if (m_keyframes.empty()) if (m_keyframes.empty())

View File

@@ -81,6 +81,12 @@ public:
bool hasEditableSegment(double time) const; bool hasEditableSegment(double time) const;
bool isFirst(const KeyframeItem *key) const;
bool isLast(const KeyframeItem *key) const;
int indexOf(const KeyframeItem *key) const;
unsigned int id() const; unsigned int id() const;
ValueType valueType() const; ValueType valueType() const;
@@ -99,6 +105,8 @@ public:
QVector<HandleItem *> handles() const; QVector<HandleItem *> handles() const;
CurveSegment segment(const KeyframeItem *keyframe, HandleItem::Slot slot) const;
void restore(); void restore();
void setDirty(bool dirty); void setDirty(bool dirty);

View File

@@ -24,6 +24,7 @@
****************************************************************************/ ****************************************************************************/
#include "handleitem.h" #include "handleitem.h"
#include "curvesegment.h"
#include "graphicsscene.h" #include "graphicsscene.h"
#include "keyframeitem.h" #include "keyframeitem.h"
#include "utils.h" #include "utils.h"
@@ -72,6 +73,14 @@ bool HandleItem::keyframeSelected() const
return false; return false;
} }
CurveSegment HandleItem::segment() const
{
if (KeyframeItem *parent = qgraphicsitem_cast<KeyframeItem *>(parentItem()))
return parent->segment(m_slot);
return CurveSegment();
}
KeyframeItem *HandleItem::keyframe() const KeyframeItem *HandleItem::keyframe() const
{ {
if (KeyframeItem *parent = qgraphicsitem_cast<KeyframeItem *>(parentItem())) if (KeyframeItem *parent = qgraphicsitem_cast<KeyframeItem *>(parentItem()))
@@ -146,17 +155,39 @@ void HandleItem::setStyle(const CurveEditorStyle &style)
QVariant HandleItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) QVariant HandleItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
{ {
if (change == ItemPositionChange) { if (change == ItemPositionChange) {
if (keyframe()) {
if (!scene())
return QGraphicsItem::itemChange(change, value);
if (KeyframeItem *keyItem = keyframe()) {
CurveSegment seg = segment();
if (!seg.isLegal())
return value;
QPointF pos = value.toPointF(); QPointF pos = value.toPointF();
QPointF relativePosition = keyItem->transform().inverted().map(pos);
if (m_slot == HandleItem::Slot::Left) { if (m_slot == HandleItem::Slot::Left) {
if (pos.x() > 0.0) if (pos.x() > 0.0)
pos.rx() = 0.0; pos.rx() = 0.0;
Keyframe key = seg.right();
key.setLeftHandle(key.position() + relativePosition);
seg.setRight(key);
} else if (m_slot == HandleItem::Slot::Right) { } else if (m_slot == HandleItem::Slot::Right) {
if (pos.x() < 0.0) if (pos.x() < 0.0)
pos.rx() = 0.0; pos.rx() = 0.0;
Keyframe key = seg.left();
key.setRightHandle(key.position() + relativePosition);
seg.setLeft(key);
} }
return QVariant(pos);
if (seg.isLegal())
m_validPos = pos;
return QVariant(m_validPos);
} }
} }
return QGraphicsItem::itemChange(change, value); return QGraphicsItem::itemChange(change, value);

View File

@@ -31,6 +31,7 @@
namespace DesignTools { namespace DesignTools {
class KeyframeItem; class KeyframeItem;
class CurveSegment;
class HandleItem : public SelectableItem class HandleItem : public SelectableItem
{ {
@@ -57,6 +58,8 @@ public:
bool keyframeSelected() const; bool keyframeSelected() const;
CurveSegment segment() const;
KeyframeItem *keyframe() const; KeyframeItem *keyframe() const;
Slot slot() const; Slot slot() const;
@@ -70,6 +73,8 @@ private:
Slot m_slot; Slot m_slot;
HandleItemStyleOption m_style; HandleItemStyleOption m_style;
QPointF m_validPos;
}; };
} // End namespace DesignTools. } // End namespace DesignTools.

View File

@@ -35,15 +35,22 @@ namespace DesignTools {
KeyframeItem::KeyframeItem(QGraphicsItem *parent) KeyframeItem::KeyframeItem(QGraphicsItem *parent)
: SelectableItem(parent) : SelectableItem(parent)
, m_transform()
, m_style()
, m_frame() , m_frame()
, m_left(nullptr)
, m_right(nullptr)
, m_validPos()
{} {}
KeyframeItem::KeyframeItem(const Keyframe &keyframe, QGraphicsItem *parent) KeyframeItem::KeyframeItem(const Keyframe &keyframe, QGraphicsItem *parent)
: SelectableItem(parent) : SelectableItem(parent)
, m_transform() , m_transform()
, m_style()
, m_frame() , m_frame()
, m_left(nullptr) , m_left(nullptr)
, m_right(nullptr) , m_right(nullptr)
, m_validPos()
{ {
setKeyframe(keyframe); setKeyframe(keyframe);
} }
@@ -137,6 +144,14 @@ HandleItem *KeyframeItem::rightHandle() const
return m_right; return m_right;
} }
CurveSegment KeyframeItem::segment(HandleItem::Slot slot) const
{
if (auto *curveItem = qgraphicsitem_cast<CurveItem *>(parentItem()))
return curveItem->segment(this, slot);
return CurveSegment();
}
QTransform KeyframeItem::transform() const QTransform KeyframeItem::transform() const
{ {
return m_transform; return m_transform;
@@ -364,19 +379,51 @@ void KeyframeItem::updateHandle(HandleItem *handle, bool emitChanged)
QVariant KeyframeItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value) QVariant KeyframeItem::itemChange(QGraphicsItem::GraphicsItemChange change, const QVariant &value)
{ {
if (change == ItemPositionChange) { if (change == ItemPositionChange) {
if (!scene())
return QGraphicsItem::itemChange(change, value);
CurveItem *curveItem = qgraphicsitem_cast<CurveItem *>(parentItem());
if (!curveItem)
return QGraphicsItem::itemChange(change, value);
CurveSegment lseg = segment(HandleItem::Slot::Left);
CurveSegment rseg = segment(HandleItem::Slot::Right);
auto legalLeft = [this, curveItem, &lseg]() {
if (curveItem->isFirst(this))
return true;
else
return lseg.isLegal();
};
auto legalRight = [this, curveItem, &rseg]() {
if (curveItem->isLast(this))
return true;
else
return rseg.isLegal();
};
bool ok; bool ok;
QPointF position = m_transform.inverted(&ok).map(value.toPointF()); QPointF position = m_transform.inverted(&ok).map(value.toPointF());
if (ok) { if (ok) {
position.setX(std::round(position.x())); position.setX(std::round(position.x()));
if (auto *curveItem = qgraphicsitem_cast<CurveItem *>(parentItem())) { if (curveItem->valueType() == ValueType::Integer)
if (curveItem->valueType() == ValueType::Integer) position.setY(std::round(position.y()));
position.setY(std::round(position.y())); else if (curveItem->valueType() == ValueType::Bool)
else if (curveItem->valueType() == ValueType::Bool) position.setY(position.y() > 0.5 ? 1.0 : 0.0);
position.setY(position.y() > 0.5 ? 1.0 : 0.0);
}
return QVariant(m_transform.map(position)); if (!legalLeft() || !legalRight()) {
return QVariant(m_transform.map(position));
} else {
lseg.moveRightTo(position);
rseg.moveLeftTo(position);
if (legalLeft() && legalRight())
m_validPos = position;
return QVariant(m_transform.map(m_validPos));
}
} }
} }

View File

@@ -78,6 +78,8 @@ public:
HandleItem *rightHandle() const; HandleItem *rightHandle() const;
CurveSegment segment(HandleItem::Slot slot) const;
QTransform transform() const; QTransform transform() const;
void setHandleVisibility(bool visible); void setHandleVisibility(bool visible);
@@ -127,6 +129,8 @@ private:
HandleItem *m_right; HandleItem *m_right;
QPointF m_validPos;
bool m_visibleOverride = true; bool m_visibleOverride = true;
}; };