Commit 1746390a authored by Dmitry Kazakov's avatar Dmitry Kazakov

Implemented the first really-hard-to-use version fo the Liquify tool

It's usability is still weird, but at least it works now :)
parent 9c8df73c
......@@ -23,6 +23,8 @@
#include "kis_paintop.h"
#include "kis_distance_information.h"
#include "kis_algebra_2d.h"
struct KisPaintInformation::Private {
......@@ -182,15 +184,6 @@ KisPaintInformation KisPaintInformation::fromXML(const QDomElement& e)
rotation, tangentialPressure, perspective, time);
}
void KisPaintInformation::paintAt(KisPaintOp *op, KisDistanceInformation *distanceInfo)
{
d->registerDistanceInfo(distanceInfo);
KisSpacingInformation spacingInfo = op->paintAt(*this);
d->unregisterDistanceInfo();
distanceInfo->registerPaintedDab(*this, spacingInfo);
}
const QPointF& KisPaintInformation::pos() const
{
return d->pos;
......@@ -253,6 +246,22 @@ qreal KisPaintInformation::drawingAngle() const
return atan2(diff.y(), diff.x());
}
QPointF KisPaintInformation::drawingDirectionVector() const
{
if (d->drawingAngleOverride) {
qreal angle = *d->drawingAngleOverride;
return QPointF(cos(angle), sin(angle));
}
if (!d->currentDistanceInfo || !d->currentDistanceInfo->hasLastDabInformation()) {
qWarning() << "KisPaintInformation::drawingDirectionVector()" << "Cannot access Distance Info last dab data";
return QPointF(1.0, 0.0);
}
QPointF diff(pos() - d->currentDistanceInfo->lastPosition());
return KisAlgebra2D::normalize(diff);
}
qreal KisPaintInformation::drawingDistance() const
{
if (!d->currentDistanceInfo || !d->currentDistanceInfo->hasLastDabInformation()) {
......
......@@ -25,6 +25,7 @@
#include "kis_global.h"
#include "kis_vec.h"
#include "krita_export.h"
#include "kis_distance_information.h"
class QDomDocument;
class QDomElement;
......@@ -91,7 +92,17 @@ public:
~KisPaintInformation();
void paintAt(KisPaintOp *op, KisDistanceInformation *distanceInfo);
template <class PaintOp>
void paintAt(PaintOp &op, KisDistanceInformation *distanceInfo) {
KisSpacingInformation spacingInfo;
{
DistanceInformationRegistrar r = registerDistanceInformation(distanceInfo);
spacingInfo = op.paintAt(*this);
}
distanceInfo->registerPaintedDab(*this, spacingInfo);
}
const QPointF& pos() const;
void setPos(const QPointF& p);
......@@ -122,6 +133,14 @@ public:
*/
qreal drawingAngle() const;
/**
* Current brush direction vector computed from the cursor movement
*
* WARNING: this method is available *only* inside paintAt() call,
* that is when the distance information is registered.
*/
QPointF drawingDirectionVector() const;
/**
* Current brush speed computed from the cursor movement
*
......
......@@ -42,6 +42,8 @@
#include "kis_vec.h"
#include "kis_perspective_math.h"
#include "kis_fixed_paint_device.h"
#include "kis_paintop_utils.h"
#define BEZIER_FLATNESS_THRESHOLD 0.5
#include <kis_distance_information.h>
......@@ -67,11 +69,6 @@ struct KisPaintOp::Private {
bool fanCornersEnabled;
qreal fanCornersStep;
bool paintFan(const KisPaintInformation &pi1,
const KisPaintInformation &pi2,
KisDistanceInformation *currentDistance);
};
......@@ -169,66 +166,17 @@ void KisPaintOp::paintLine(const KisPaintInformation &pi1,
const KisPaintInformation &pi2,
KisDistanceInformation *currentDistance)
{
QPointF end = pi2.pos();
KisPaintInformation pi = pi1;
QPointF pt = pi1.pos();
qreal t = 0.0;
while ((t = currentDistance->getNextPointPosition(pt, end)) >= 0.0) {
pt = pt + t * (end - pt);
pi = KisPaintInformation::mix(pt, t, pi, pi2);
if (d->fanCornersEnabled &&
currentDistance->hasLastPaintInformation()) {
d->paintFan(currentDistance->lastPaintInformation(),
pi,
currentDistance);
}
/**
* A bit complicated part to ensure the registration
* of the distance information is done in right order
*/
pi.paintAt(this, currentDistance);
}
KisPaintOpUtils::paintLine(*this, pi1, pi2, currentDistance,
d->fanCornersEnabled,
d->fanCornersStep);
}
bool KisPaintOp::Private::paintFan(const KisPaintInformation &pi1,
const KisPaintInformation &pi2,
KisDistanceInformation *currentDistance)
{
const qreal angleStep = fanCornersStep;
const qreal initialAngle = currentDistance->lastDrawingAngle();
const qreal finalAngle = pi2.drawingAngleSafe(*currentDistance);
const qreal fullDistance = shortestAngularDistance(initialAngle,
pi2.drawingAngleSafe(*currentDistance));
qreal lastAngle = initialAngle;
int i = 0;
while (shortestAngularDistance(lastAngle, finalAngle) > angleStep) {
lastAngle = incrementInDirection(lastAngle, angleStep, finalAngle);
qreal t = angleStep * i++ / fullDistance;
QPointF pt = pi1.pos() + t * (pi2.pos() - pi1.pos());
KisPaintInformation pi = KisPaintInformation::mix(pt, t, pi1, pi2);
pi.overrideDrawingAngle(lastAngle);
pi.paintAt(q, currentDistance);
}
return i;
}
void KisPaintOp::paintAt(const KisPaintInformation& info, KisDistanceInformation *currentDistance)
{
Q_ASSERT(currentDistance);
KisPaintInformation pi(info);
pi.paintAt(this, currentDistance);
pi.paintAt(*this, currentDistance);
}
KisPainter* KisPaintOp::painter() const
......
/*
* Copyright (c) 2014 Dmitry Kazakov <dimula73@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#ifndef __KIS_PAINTOP_UTILS_H
#define __KIS_PAINTOP_UTILS_H
#include "kis_global.h"
#include "kis_paint_information.h"
namespace KisPaintOpUtils {
template <class PaintOp>
bool paintFan(PaintOp &op,
const KisPaintInformation &pi1,
const KisPaintInformation &pi2,
KisDistanceInformation *currentDistance,
qreal fanCornersStep)
{
const qreal angleStep = fanCornersStep;
const qreal initialAngle = currentDistance->lastDrawingAngle();
const qreal finalAngle = pi2.drawingAngleSafe(*currentDistance);
const qreal fullDistance = shortestAngularDistance(initialAngle,
pi2.drawingAngleSafe(*currentDistance));
qreal lastAngle = initialAngle;
int i = 0;
while (shortestAngularDistance(lastAngle, finalAngle) > angleStep) {
lastAngle = incrementInDirection(lastAngle, angleStep, finalAngle);
qreal t = angleStep * i++ / fullDistance;
QPointF pt = pi1.pos() + t * (pi2.pos() - pi1.pos());
KisPaintInformation pi = KisPaintInformation::mix(pt, t, pi1, pi2);
pi.overrideDrawingAngle(lastAngle);
pi.paintAt(op, currentDistance);
}
return i;
}
template <class PaintOp>
void paintLine(PaintOp &op,
const KisPaintInformation &pi1,
const KisPaintInformation &pi2,
KisDistanceInformation *currentDistance,
bool fanCornersEnabled,
qreal fanCornersStep)
{
QPointF end = pi2.pos();
KisPaintInformation pi = pi1;
QPointF pt = pi1.pos();
qreal t = 0.0;
while ((t = currentDistance->getNextPointPosition(pt, end)) >= 0.0) {
pt = pt + t * (end - pt);
pi = KisPaintInformation::mix(pt, t, pi, pi2);
if (fanCornersEnabled &&
currentDistance->hasLastPaintInformation()) {
paintFan(op,
currentDistance->lastPaintInformation(),
pi,
currentDistance,
fanCornersStep);
}
/**
* A bit complicated part to ensure the registration
* of the distance information is done in right order
*/
pi.paintAt(op, currentDistance);
}
}
}
#endif /* __KIS_PAINTOP_UTILS_H */
......@@ -67,6 +67,13 @@ qreal norm(const T &a)
return std::sqrt(pow2(a.x()) + pow2(a.y()));
}
template <class Point>
Point normalize(const Point &a)
{
const qreal length = norm(a);
return (1.0 / length) * a;
}
template <class T>
T leftUnitNormal(const T &a)
{
......
......@@ -267,8 +267,8 @@ struct QImagePolygonOp
QPoint srcPointI = srcPoint.toPoint();
QPoint dstPointI = dstPoint.toPoint();
srcPointI = KisAlgebra2D::clampPoint(srcPointI, m_dstImageRect);
dstPointI = KisAlgebra2D::clampPoint(dstPointI, m_srcImageRect);
if (!m_dstImageRect.contains(srcPointI)) continue;
if (!m_srcImageRect.contains(dstPointI)) continue;
m_dstImage.setPixel(srcPointI, m_srcImage.pixel(dstPointI));
}
......
......@@ -22,19 +22,16 @@
struct KisLiquifyTransformWorker::Private
{
Private(KisPaintDeviceSP _dev,
Private(const QRect &_srcBounds,
KoUpdater *_progress,
int _pixelPrecision)
: dev(_dev),
: srcBounds(_srcBounds),
progress(_progress),
pixelPrecision(_pixelPrecision)
{
}
KisPaintDeviceSP dev;
QImage srcImage;
QPointF srcImageOffset;
const QRect srcBounds;
QVector<QPointF> originalPoints;
QVector<QPointF> transformedPoints;
......@@ -53,23 +50,18 @@ struct KisLiquifyTransformWorker::Private
qreal sigma);
};
KisLiquifyTransformWorker::KisLiquifyTransformWorker(KisPaintDeviceSP dev,
KisLiquifyTransformWorker::KisLiquifyTransformWorker(const QRect &srcBounds,
KoUpdater *progress,
int pixelPrecision)
: m_d(new Private(dev, progress, pixelPrecision))
: m_d(new Private(srcBounds, progress, pixelPrecision))
{
// TODO: implement 'progress' stuff
m_d->preparePoints();
}
KisLiquifyTransformWorker::KisLiquifyTransformWorker(const QImage &srcImage,
const QPointF &srcImageOffset,
KoUpdater *progress,
int pixelPrecision)
: m_d(new Private(0, progress, pixelPrecision))
KisLiquifyTransformWorker::KisLiquifyTransformWorker(const KisLiquifyTransformWorker &rhs)
: m_d(new Private(*rhs.m_d.data()))
{
m_d->srcImage = srcImage;
m_d->srcImageOffset = srcImageOffset;
m_d->preparePoints();
}
KisLiquifyTransformWorker::~KisLiquifyTransformWorker()
......@@ -122,9 +114,6 @@ struct AllPointsFetcherOp
void KisLiquifyTransformWorker::Private::preparePoints()
{
QRect srcBounds = dev ? dev->region().boundingRect() :
QRectF(srcImageOffset, srcImage.size()).toAlignedRect();
gridSize =
GridIterationTools::calcGridSize(srcBounds, pixelPrecision);
......@@ -321,14 +310,14 @@ struct KisLiquifyTransformWorker::Private::MapIndexesOp {
};
void KisLiquifyTransformWorker::run()
void KisLiquifyTransformWorker::run(KisPaintDeviceSP device)
{
KisPaintDeviceSP srcDev = new KisPaintDevice(*m_d->dev.data());
m_d->dev->clear();
KisPaintDeviceSP srcDev = new KisPaintDevice(*device.data());
device->clear();
using namespace GridIterationTools;
PaintDevicePolygonOp polygonOp(srcDev, m_d->dev);
PaintDevicePolygonOp polygonOp(srcDev, device);
Private::MapIndexesOp indexesOp(m_d.data());
iterateThroughGrid<AlwaysCompletePolygonPolicy>(polygonOp, indexesOp,
m_d->gridSize,
......@@ -336,26 +325,51 @@ void KisLiquifyTransformWorker::run()
m_d->transformedPoints);
}
QImage KisLiquifyTransformWorker::runOnQImage(QPointF *newOffset)
#include <boost/function.hpp>
#include <boost/bind.hpp>
#include <QTransform>
typedef boost::function<QPointF (const QPointF&)> PointMapFunction;
PointMapFunction bindPointMapTransform(const QTransform &transform) {
typedef QPointF (QTransform::*MapFuncType)(const QPointF&) const;
return boost::bind(static_cast<MapFuncType>(&QTransform::map), &transform, _1);
}
QImage KisLiquifyTransformWorker::runOnQImage(const QImage &srcImage,
const QPointF &srcImageOffset,
const QTransform &imageToThumbTransform,
QPointF *newOffset)
{
KIS_ASSERT_RECOVER(m_d->originalPoints.size() == m_d->transformedPoints.size()) {
return QImage();
}
KIS_ASSERT_RECOVER(!m_d->srcImage.isNull()) {
KIS_ASSERT_RECOVER(!srcImage.isNull()) {
return QImage();
}
KIS_ASSERT_RECOVER(m_d->srcImage.format() == QImage::Format_ARGB32) {
KIS_ASSERT_RECOVER(srcImage.format() == QImage::Format_ARGB32) {
return QImage();
}
QVector<QPointF> originalPointsLocal(m_d->originalPoints);
QVector<QPointF> transformedPointsLocal(m_d->transformedPoints);
PointMapFunction mapFunc = bindPointMapTransform(imageToThumbTransform);
std::transform(originalPointsLocal.begin(), originalPointsLocal.end(),
originalPointsLocal.begin(), mapFunc);
std::transform(transformedPointsLocal.begin(), transformedPointsLocal.end(),
transformedPointsLocal.begin(), mapFunc);
QRectF dstBounds;
foreach (const QPointF &pt, m_d->transformedPoints) {
foreach (const QPointF &pt, transformedPointsLocal) {
KisAlgebra2D::accumulateBounds(pt, &dstBounds);
}
const QRectF srcBounds(m_d->srcImageOffset, m_d->srcImage.size());
const QRectF srcBounds(srcImageOffset, srcImage.size());
dstBounds |= srcBounds;
QPointF dstQImageOffset = dstBounds.topLeft();
......@@ -363,17 +377,16 @@ QImage KisLiquifyTransformWorker::runOnQImage(QPointF *newOffset)
QRect dstBoundsI = dstBounds.toAlignedRect();
QImage dstImage(dstBoundsI.size(), m_d->srcImage.format());
QImage dstImage(dstBoundsI.size(), srcImage.format());
dstImage.fill(0);
GridIterationTools::QImagePolygonOp polygonOp(m_d->srcImage, dstImage, m_d->srcImageOffset, dstQImageOffset);
GridIterationTools::QImagePolygonOp polygonOp(srcImage, dstImage, srcImageOffset, dstQImageOffset);
Private::MapIndexesOp indexesOp(m_d.data());
GridIterationTools::iterateThroughGrid
<GridIterationTools::AlwaysCompletePolygonPolicy>(polygonOp, indexesOp,
m_d->gridSize,
m_d->originalPoints,
m_d->transformedPoints);
originalPointsLocal,
transformedPointsLocal);
return dstImage;
}
......@@ -28,14 +28,11 @@ class QImage;
class KRITAIMAGE_EXPORT KisLiquifyTransformWorker
{
public:
KisLiquifyTransformWorker(KisPaintDeviceSP dev,
KisLiquifyTransformWorker(const QRect &srcBounds,
KoUpdater *progress,
int pixelPrecision = 8);
KisLiquifyTransformWorker(const QImage &srcImage,
const QPointF &srcImageOffset,
KoUpdater *progress,
int pixelPrecision = 8);
KisLiquifyTransformWorker(const KisLiquifyTransformWorker &rhs);
~KisLiquifyTransformWorker();
......@@ -61,8 +58,11 @@ public:
const QVector<QPointF>& originalPoints() const;
QVector<QPointF>& transformedPoints();
void run();
QImage runOnQImage(QPointF *newOffset);
void run(KisPaintDeviceSP device);
QImage runOnQImage(const QImage &srcImage,
const QPointF &srcImageOffset,
const QTransform &imageToThumbTransform,
QPointF *newOffset);
private:
struct Private;
......
......@@ -126,66 +126,6 @@ void testCage(bool clockwise, bool unityTransform, bool benchmarkPrepareOnly = f
}
}
QPointF translatePoint(const QPointF &src,
const QPointF &base,
const QPointF &offset,
qreal sigma)
{
qreal dist = KisAlgebra2D::norm(src - base);
if (dist > 3 * sigma) return src;
const qreal lambda = exp(-pow2(dist) / 2 / pow2(sigma));
return src + lambda * offset;
}
QPointF scalePoint(const QPointF &src,
const QPointF &base,
qreal scale,
qreal sigma)
{
QPointF diff = src - base;
qreal dist = KisAlgebra2D::norm(diff);
if (dist > 3 * sigma) return src;
const qreal lambda = exp(-pow2(dist) / 2 / pow2(sigma));
return base + (1.0 + scale * lambda) * diff;
}
QPointF rotatePoint(const QPointF &src,
const QPointF &base,
qreal angle,
qreal sigma)
{
QPointF diff = src - base;
qreal dist = KisAlgebra2D::norm(diff);
if (dist > 4 * sigma) return src;
const qreal lambda = exp(-pow2(dist) / 2 / pow2(sigma));
angle *= lambda;
qreal sinA = std::sin(angle);
qreal cosA = std::cos(angle);
qreal x = cosA * diff.x() + sinA * diff.y();
qreal y = -sinA * diff.x() + cosA * diff.y();
QPointF result = base + QPointF(x, y);
return result;
}
QPointF undoPoint(const QPointF &src,
const QPointF &base,
const QPointF &refPoint,
qreal sigma)
{
QPointF diff = src - base;
qreal dist = KisAlgebra2D::norm(diff);
if (dist > 3 * sigma) return src;
const qreal lambda = exp(-pow2(dist) / 2 / pow2(sigma));
return refPoint * lambda + src * (1.0 - lambda);
}
void KisLiquifyTransformWorkerTest::testPoints()
{
TestUtil::TestProgressBar bar;
......@@ -200,7 +140,7 @@ void KisLiquifyTransformWorkerTest::testPoints()
const int pixelPrecision = 8;
KisLiquifyTransformWorker worker(dev,
KisLiquifyTransformWorker worker(dev->exactBounds(),
updater,
pixelPrecision);
......@@ -231,10 +171,59 @@ void KisLiquifyTransformWorkerTest::testPoints()
50);
}
worker.run();
worker.run(dev);
QImage result = dev->convertToQImage(0);
TestUtil::checkQImage(result, "liquify_transform_test", "liquify_dev", "unity");
}
void KisLiquifyTransformWorkerTest::testPointsQImage()
{
TestUtil::TestProgressBar bar;
KoProgressUpdater pu(&bar);
KoUpdaterPtr updater = pu.startSubtask();
const KoColorSpace *cs = KoColorSpaceRegistry::instance()->rgb8();
QImage image(TestUtil::fetchDataFileLazy("test_transform_quality_second.png"));
KisPaintDeviceSP dev = new KisPaintDevice(cs);
dev->convertFromQImage(image, 0);
const int pixelPrecision = 8;
KisLiquifyTransformWorker worker(dev->exactBounds(),
updater,
pixelPrecision);
worker.translatePoints(QPointF(100,100),
QPointF(50, 0),
50);
QRect rc = dev->exactBounds();
dev->setX(50);
dev->setY(50);
worker.run(dev);
rc |= dev->exactBounds();
QImage resultDev = dev->convertToQImage(0, rc.x(), rc.y(), rc.width(), rc.height());
TestUtil::checkQImage(resultDev, "liquify_transform_test", "liquify_qimage", "refDevice");