[graphics/krita] /: Rework liquify relax into restore shape tool

Dmitry Kazakov <[email protected]>
Newsgroups gmane.comp.kde.cvs
Message-ID <[email protected]>
Git commit 316008669085930a3e826f72427617da9dca6e9a by Dmitry Kazakov, on behalf of Moritz Staudinger.
Committed on 07/08/2026 at 13:58.
Pushed by dkazakov into branch 'master'.

Rework liquify relax into restore shape tool

M  +21   -17   benchmarks/KisLiquifyTransformWorkerBenchmark.cpp
M  +6    -6    benchmarks/KisLiquifyTransformWorkerBenchmark.h
M  +121  -171  libs/image/kis_liquify_transform_worker.cpp
M  +6    -7    libs/image/kis_liquify_transform_worker.h
M  +215  -24   libs/image/tests/kis_liquify_transform_worker_test.cpp
M  +5    -2    libs/image/tests/kis_liquify_transform_worker_test.h
M  +8    -12   plugins/tools/tool_transform2/kis_liquify_paintop.cpp
M  +44   -15   plugins/tools/tool_transform2/kis_liquify_properties.cpp
M  +29   -3    plugins/tools/tool_transform2/kis_liquify_properties.h
M  +120  -19   plugins/tools/tool_transform2/kis_tool_transform_config_widget.cpp
M  +6    -0    plugins/tools/tool_transform2/kis_tool_transform_config_widget.h
M  +197  -24   plugins/tools/tool_transform2/wdg_tool_transform.ui

https://invent.kde.org/graphics/krita/-/commit/316008669085930a3e826f72427617da9dca6e9a

diff --git a/benchmarks/KisLiquifyTransformWorkerBenchmark.cpp b/benchmarks/KisLiquifyTransformWorkerBenchmark.cpp
index 110ee45d4b5..43e002e3a60 100644
--- a/benchmarks/KisLiquifyTransformWorkerBenchmark.cpp
+++ b/benchmarks/KisLiquifyTransformWorkerBenchmark.cpp
@@ -158,8 +158,8 @@ void testPoints(KisLiquifyTransformWorkerBenchmark::Operation operation, bool us
                 worker.undoPoints(undoBases[i], flows[i%flows.length()], sigmas[i%sigmas.length()]);
             }
         }
-    } else if (operation == KisLiquifyTransformWorkerBenchmark::RelaxSimple ||
-               operation == KisLiquifyTransformWorkerBenchmark::RelaxAffine) {
+    } else if (operation == KisLiquifyTransformWorkerBenchmark::RestoreShape ||
+               operation == KisLiquifyTransformWorkerBenchmark::RestoreShapeWithStretch) {
 
         for (int i = 0; i < bases.count(); i++) {
             worker.translatePoints(bases[i], QPointF(50, 0), sigmas[i%sigmas.length()], useWashMode, flows[i%flows.length()]);
@@ -169,16 +169,20 @@ void testPoints(KisLiquifyTransformWorkerBenchmark::Operation operation, bool us
             worker.rotatePoints(bases[i], M_PI / 4, sigmas[i%sigmas.length()], useWashMode, flows[i%flows.length()]);
         }
 
-        QList<QPointF> relaxBases = preparePointsData((A + D)/2, (C+B)/2, (B+D)/2, (A+C)/2, oneWayBasesCount);
+        QList<QPointF> restoreShapeBases = preparePointsData((A + D)/2, (C+B)/2, (B+D)/2, (A+C)/2, oneWayBasesCount);
 
         QBENCHMARK {
 
-            for (int i = 0; i < relaxBases.count(); i++) {
-                if (operation == KisLiquifyTransformWorkerBenchmark::RelaxSimple) {
-                    worker.relaxPoints(relaxBases[i], flows[i%flows.length()], sigmas[i%sigmas.length()]);
-                } else {
-                    worker.affineRelaxPoints(relaxBases[i], flows[i%flows.length()], sigmas[i%sigmas.length()]);
-                }
+            for (int i = 0; i < restoreShapeBases.count(); i++) {
+                const bool preserveStretch =
+                    operation == KisLiquifyTransformWorkerBenchmark::RestoreShapeWithStretch;
+
+                worker.restoreShapePoints(restoreShapeBases[i],
+                                          flows[i%flows.length()],
+                                          sigmas[i%sigmas.length()],
+                                          true,
+                                          true,
+                                          preserveStretch);
             }
         }
     } else if (operation == KisLiquifyTransformWorkerBenchmark::RunOnQImage) {
@@ -314,24 +318,24 @@ void KisLiquifyTransformWorkerBenchmark::testPointsUndoWash()
     testPoints(Undo, true);
 }
 
-void KisLiquifyTransformWorkerBenchmark::testPointsRelaxSimpleBuildUp()
+void KisLiquifyTransformWorkerBenchmark::testPointsRestoreShapeBuildUp()
 {
-    testPoints(RelaxSimple, false);
+    testPoints(RestoreShape, false);
 }
 
-void KisLiquifyTransformWorkerBenchmark::testPointsRelaxSimpleWash()
+void KisLiquifyTransformWorkerBenchmark::testPointsRestoreShapeWash()
 {
-    testPoints(RelaxSimple, true);
+    testPoints(RestoreShape, true);
 }
 
-void KisLiquifyTransformWorkerBenchmark::testPointsRelaxAffineBuildUp()
+void KisLiquifyTransformWorkerBenchmark::testPointsRestoreShapeWithStretchBuildUp()
 {
-    testPoints(RelaxAffine, false);
+    testPoints(RestoreShapeWithStretch, false);
 }
 
-void KisLiquifyTransformWorkerBenchmark::testPointsRelaxAffineWash()
+void KisLiquifyTransformWorkerBenchmark::testPointsRestoreShapeWithStretchWash()
 {
-    testPoints(RelaxAffine, true);
+    testPoints(RestoreShapeWithStretch, true);
 }
 
 void KisLiquifyTransformWorkerBenchmark::testRunOnDev()
diff --git a/benchmarks/KisLiquifyTransformWorkerBenchmark.h b/benchmarks/KisLiquifyTransformWorkerBenchmark.h
index 007d927b368..14c7b45fff3 100644
--- a/benchmarks/KisLiquifyTransformWorkerBenchmark.h
+++ b/benchmarks/KisLiquifyTransformWorkerBenchmark.h
@@ -18,8 +18,8 @@ public:
         Scale,
         Rotate,
         Undo,
-        RelaxSimple,
-        RelaxAffine,
+        RestoreShape,
+        RestoreShapeWithStretch,
         RunOnQImage,
         RunOnDev,
         BenchmarkCopyConstructor,
@@ -44,10 +44,10 @@ private Q_SLOTS:
 
     void testPointsUndoBuildUp();
     void testPointsUndoWash();
-    void testPointsRelaxSimpleBuildUp();
-    void testPointsRelaxSimpleWash();
-    void testPointsRelaxAffineBuildUp();
-    void testPointsRelaxAffineWash();
+    void testPointsRestoreShapeBuildUp();
+    void testPointsRestoreShapeWash();
+    void testPointsRestoreShapeWithStretchBuildUp();
+    void testPointsRestoreShapeWithStretchWash();
 
     void testRunOnDev();
     void testRunOnQImage();
diff --git a/libs/image/kis_liquify_transform_worker.cpp b/libs/image/kis_liquify_transform_worker.cpp
index 4ee32aa3d4d..c9b06084297 100644
--- a/libs/image/kis_liquify_transform_worker.cpp
+++ b/libs/image/kis_liquify_transform_worker.cpp
@@ -7,6 +7,8 @@
 #include "kis_liquify_transform_worker.h"
 
 #include <KoColorSpace.h>
+#include <Eigen/Dense>
+#include <cmath>
 #include "kis_grid_interpolation_tools.h"
 #include "kis_dom_utils.h"
 #include "krita_utils.h"
@@ -236,40 +238,39 @@ struct PointUpdate
     QPointF newPosition;
 };
 
-bool solveLinear3x3(const qreal matrix[3][3], const qreal rhs[3], qreal result[3])
+struct RestoreShapeSample
 {
-    const qreal determinant =
-        matrix[0][0] * (matrix[1][1] * matrix[2][2] - matrix[1][2] * matrix[2][1]) -
-        matrix[0][1] * (matrix[1][0] * matrix[2][2] - matrix[1][2] * matrix[2][0]) +
-        matrix[0][2] * (matrix[1][0] * matrix[2][1] - matrix[1][1] * matrix[2][0]);
-
-    if (qAbs(determinant) < 1e-12) return false;
-
-    const qreal invDeterminant = 1.0 / determinant;
+    int index = -1;
+    qreal weight = 0.0;
+    qreal lambda = 0.0;
+};
 
-    qreal inverse[3][3];
-    inverse[0][0] =  (matrix[1][1] * matrix[2][2] - matrix[1][2] * matrix[2][1]) * invDeterminant;
-    inverse[0][1] = -(matrix[0][1] * matrix[2][2] - matrix[0][2] * matrix[2][1]) * invDeterminant;
-    inverse[0][2] =  (matrix[0][1] * matrix[1][2] - matrix[0][2] * matrix[1][1]) * invDeterminant;
-    inverse[1][0] = -(matrix[1][0] * matrix[2][2] - matrix[1][2] * matrix[2][0]) * invDeterminant;
-    inverse[1][1] =  (matrix[0][0] * matrix[2][2] - matrix[0][2] * matrix[2][0]) * invDeterminant;
-    inverse[1][2] = -(matrix[0][0] * matrix[1][2] - matrix[0][2] * matrix[1][0]) * invDeterminant;
-    inverse[2][0] =  (matrix[1][0] * matrix[2][1] - matrix[1][1] * matrix[2][0]) * invDeterminant;
-    inverse[2][1] = -(matrix[0][0] * matrix[2][1] - matrix[0][1] * matrix[2][0]) * invDeterminant;
-    inverse[2][2] =  (matrix[0][0] * matrix[1][1] - matrix[0][1] * matrix[1][0]) * invDeterminant;
+Eigen::Vector2d toEigenPoint(const QPointF &pt)
+{
+    return Eigen::Vector2d(pt.x(), pt.y());
+}
 
-    for (int i = 0; i < 3; i++) {
-        result[i] = inverse[i][0] * rhs[0] + inverse[i][1] * rhs[1] + inverse[i][2] * rhs[2];
-    }
+QPointF fromEigenPoint(const Eigen::Vector2d &pt)
+{
+    return QPointF(pt.x(), pt.y());
+}
 
-    return true;
+bool isFinite(const Eigen::Matrix2d &m)
+{
+    return std::isfinite(m(0, 0)) &&
+        std::isfinite(m(0, 1)) &&
+        std::isfinite(m(1, 0)) &&
+        std::isfinite(m(1, 1));
 }
 
 }
 
-void KisLiquifyTransformWorker::relaxPoints(const QPointF &base,
-                                            qreal amount,
-                                            qreal sigma)
+void KisLiquifyTransformWorker::restoreShapePoints(const QPointF &base,
+                                                   qreal amount,
+                                                   qreal sigma,
+                                                   bool preserveRotation,
+                                                   bool preserveScale,
+                                                   bool preserveStretch)
 {
     const qreal maxDistCoeff = 3.0;
     const qreal maxDist = maxDistCoeff * sigma;
@@ -284,179 +285,128 @@ void KisLiquifyTransformWorker::relaxPoints(const QPointF &base,
     QVector<int> indexes;
     m_d->transformedPointsContainer.findAllInRange(indexes, base, maxDist);
 
-    QVector<PointUpdate> updates;
-    updates.reserve(indexes.count());
-
-    /*
-     * Blur the displacement field, not the transformed point positions.
-     * Repeatedly blurring positions would pull all coordinates in the dab
-     * toward their centroid, effectively compressing the grid like a black
-     * hole. Blurring displacements keeps the original grid structure and
-     * only reduces local disagreement in how neighboring points moved.
-     *
-     * Store all updates first so every point is relaxed against the same
-     * pre-dab state; otherwise the result would depend on iteration order.
-     */
+    QVector<RestoreShapeSample> samples;
+    samples.reserve(indexes.count());
+
+    qreal weightSum = 0.0;
+    Eigen::Vector2d originalCentroid(0.0, 0.0);
+    Eigen::Vector2d transformedCentroid(0.0, 0.0);
+
     for (int i = 0; i < indexes.count(); i++) {
         const int index = indexes[i];
 
-        QPointF diff = m_d->transformedPoints[index] - base;
-        qreal dist = KisAlgebra2D::norm(diff);
+        const QPointF diff = m_d->transformedPoints[index] - base;
+        const qreal dist = KisAlgebra2D::norm(diff);
         if (dist > maxDist) continue;
 
-        qreal lambda = exp(-0.5 * pow2(dist / sigma));
-        lambda *= amount;
-
-        const int col = index % m_d->gridSize.width();
-        const int row = index / m_d->gridSize.width();
+        const qreal weight = exp(-0.5 * pow2(dist / sigma));
+        const qreal lambda = qBound<qreal>(0.0, weight * amount, 1.0);
+        if (lambda <= 0.0) continue;
 
-        QPointF displacementSum;
-        int displacementCount = 0;
+        samples << RestoreShapeSample{index, weight, lambda};
 
-        for (int neighborRow = qMax(0, row - 1);
-             neighborRow <= qMin(m_d->gridSize.height() - 1, row + 1);
-             neighborRow++) {
-            for (int neighborCol = qMax(0, col - 1);
-                 neighborCol <= qMin(m_d->gridSize.width() - 1, col + 1);
-                 neighborCol++) {
-
-                if (neighborCol == col && neighborRow == row) continue;
+        originalCentroid += weight * toEigenPoint(m_d->originalPoints[index]);
+        transformedCentroid += weight * toEigenPoint(m_d->transformedPoints[index]);
+        weightSum += weight;
+    }
 
-                const int neighborIndex =
-                    GridIterationTools::pointToIndex(QPoint(neighborCol, neighborRow),
-                                                     m_d->gridSize);
+    if (samples.count() < 3 || weightSum <= 1e-12) return;
 
-                displacementSum += m_d->transformedPoints[neighborIndex] -
-                    m_d->originalPoints[neighborIndex];
-                displacementCount++;
-            }
-        }
+    originalCentroid /= weightSum;
+    transformedCentroid /= weightSum;
 
-        if (!displacementCount) continue;
+    Eigen::Matrix2d originalMoment = Eigen::Matrix2d::Zero();
+    Eigen::Matrix2d transformedOriginalMoment = Eigen::Matrix2d::Zero();
 
-        const QPointF oldDisplacement =
-            m_d->transformedPoints[index] - m_d->originalPoints[index];
-        const QPointF relaxedDisplacement = displacementSum / displacementCount;
+    for (int i = 0; i < samples.count(); i++) {
+        const RestoreShapeSample &sample = samples[i];
 
-        const QPointF newPosition =
-            m_d->originalPoints[index] +
-            oldDisplacement * (1.0 - lambda) +
-            relaxedDisplacement * lambda;
+        const Eigen::Vector2d originalOffset =
+            toEigenPoint(m_d->originalPoints[sample.index]) - originalCentroid;
+        const Eigen::Vector2d transformedOffset =
+            toEigenPoint(m_d->transformedPoints[sample.index]) - transformedCentroid;
 
-        updates << PointUpdate{index, m_d->transformedPoints[index], newPosition};
+        originalMoment += sample.weight * originalOffset * originalOffset.transpose();
+        transformedOriginalMoment += sample.weight * transformedOffset * originalOffset.transpose();
     }
 
-    for (int i = 0; i < updates.count(); i++) {
-        const PointUpdate &update = updates[i];
-        m_d->transformedPoints[update.index] = update.newPosition;
-        m_d->transformedPointsContainer.movePoint(update.index, update.oldPosition, update.newPosition);
-    }
-}
-
-void KisLiquifyTransformWorker::affineRelaxPoints(const QPointF &base,
-                                                  qreal amount,
-                                                  qreal sigma)
-{
-    const qreal maxDistCoeff = 3.0;
-    const qreal maxDist = maxDistCoeff * sigma;
-    const int neighborhoodRadius = 2;
-
-    KIS_ASSERT_RECOVER_RETURN(m_d->originalPoints.size() ==
-                              m_d->transformedPoints.size());
+    if (qAbs(originalMoment.determinant()) < 1e-12) return;
 
-    QRectF clipRect(base.x() - maxDist, base.y() - maxDist,
-                    2 * maxDist, 2 * maxDist);
-    m_d->accumulatedBrushStrokes |= kisGrowRect(clipRect, m_d->pixelPrecision);
-
-    QVector<int> indexes;
-    m_d->transformedPointsContainer.findAllInRange(indexes, base, maxDist);
+    const Eigen::Matrix2d affine =
+        transformedOriginalMoment * originalMoment.inverse();
 
-    QVector<PointUpdate> updates;
-    updates.reserve(indexes.count());
-
-    /*
-     * Fit a local affine transform from neighboring original points to their
-     * transformed positions. This preserves broad local translation, rotation,
-     * scale and shear. A pure displacement blur would slowly damp rotations
-     * because a rotated area has different displacement vectors at different
-     * points, even when the rotation itself is perfectly smooth.
-     *
-     * Like simple relax, collect updates first so each fit samples the same
-     * pre-dab deformation grid.
-     */
-    for (int i = 0; i < indexes.count(); i++) {
-        const int index = indexes[i];
+    if (!isFinite(affine)) return;
 
-        QPointF diff = m_d->transformedPoints[index] - base;
-        qreal dist = KisAlgebra2D::norm(diff);
-        if (dist > maxDist) continue;
+    Eigen::JacobiSVD<Eigen::Matrix2d> svd(affine, Eigen::ComputeFullU | Eigen::ComputeFullV);
+    Eigen::Matrix2d rotation = svd.matrixU() * svd.matrixV().transpose();
 
-        qreal lambda = exp(-0.5 * pow2(dist / sigma));
-        lambda *= amount;
+    if (rotation.determinant() < 0.0) {
+        Eigen::Matrix2d u = svd.matrixU();
+        u.col(1) *= -1.0;
+        rotation = u * svd.matrixV().transpose();
+    }
 
-        const int col = index % m_d->gridSize.width();
-        const int row = index / m_d->gridSize.width();
-        const QPointF originalCenter = m_d->originalPoints[index];
-
-        qreal matrix[3][3] = {};
-        qreal rhsX[3] = {};
-        qreal rhsY[3] = {};
-        int sampleCount = 0;
-
-        for (int neighborRow = qMax(0, row - neighborhoodRadius);
-             neighborRow <= qMin(m_d->gridSize.height() - 1, row + neighborhoodRadius);
-             neighborRow++) {
-            for (int neighborCol = qMax(0, col - neighborhoodRadius);
-                 neighborCol <= qMin(m_d->gridSize.width() - 1, col + neighborhoodRadius);
-                 neighborCol++) {
-
-                if (neighborCol == col && neighborRow == row) continue;
-
-                const int neighborIndex =
-                    GridIterationTools::pointToIndex(QPoint(neighborCol, neighborRow),
-                                                     m_d->gridSize);
-                const QPointF originalOffset =
-                    m_d->originalPoints[neighborIndex] - originalCenter;
-                const qreal fitPoint[3] = {originalOffset.x(), originalOffset.y(), 1.0};
-
-                const qreal gridDistance = KisAlgebra2D::norm(originalOffset);
-                const qreal fitSigma = qMax<qreal>(m_d->pixelPrecision, 1.0);
-                const qreal weight = exp(-0.5 * pow2(gridDistance / (neighborhoodRadius * fitSigma)));
-
-                for (int r = 0; r < 3; r++) {
-                    for (int c = 0; c < 3; c++) {
-                        matrix[r][c] += weight * fitPoint[r] * fitPoint[c];
-                    }
-                    rhsX[r] += weight * fitPoint[r] * m_d->transformedPoints[neighborIndex].x();
-                    rhsY[r] += weight * fitPoint[r] * m_d->transformedPoints[neighborIndex].y();
-                }
-
-                sampleCount++;
-            }
-        }
+    if (!isFinite(rotation)) return;
 
-        qreal coeffX[3];
-        qreal coeffY[3];
-        if (sampleCount < 3 ||
-            !solveLinear3x3(matrix, rhsX, coeffX) ||
-            !solveLinear3x3(matrix, rhsY, coeffY)) {
+    const Eigen::Vector2d singularValues = svd.singularValues();
+    const qreal uniformScale =
+        preserveScale ? qMax<qreal>(1e-6, 0.5 * (singularValues.x() + singularValues.y())) : 1.0;
 
-            continue;
-        }
+    Eigen::Matrix2d targetLinear = Eigen::Matrix2d::Identity();
+    if (preserveRotation) {
+        targetLinear = rotation;
+    }
+    targetLinear *= uniformScale;
+
+    if (preserveStretch) {
+        const Eigen::Matrix2d stretchInRotationSpace = rotation.transpose() * affine;
+        qreal stretchX = qMax<qreal>(1e-6, qAbs(stretchInRotationSpace(0, 0)));
+        qreal stretchY = qMax<qreal>(1e-6, qAbs(stretchInRotationSpace(1, 1)));
+        const qreal stretchNormalization =
+            preserveScale ? uniformScale : qMax<qreal>(1e-6, std::sqrt(stretchX * stretchY));
+
+        Eigen::Matrix2d stretch = Eigen::Matrix2d::Identity();
+        stretch(0, 0) = stretchX / stretchNormalization;
+        stretch(1, 1) = stretchY / stretchNormalization;
+        targetLinear *= stretch;
+    }
 
-        const QPointF predictedPosition(coeffX[2], coeffY[2]);
+    QVector<PointUpdate> updates;
+    updates.reserve(samples.count());
+
+    Eigen::Vector2d updatedCentroid(0.0, 0.0);
+    qreal updatedWeightSum = 0.0;
+
+    for (int i = 0; i < samples.count(); i++) {
+        const RestoreShapeSample &sample = samples[i];
+        const Eigen::Vector2d oldPosition = toEigenPoint(m_d->transformedPoints[sample.index]);
+        const Eigen::Vector2d originalOffset =
+            toEigenPoint(m_d->originalPoints[sample.index]) - originalCentroid;
+        const Eigen::Vector2d targetPosition =
+            transformedCentroid + targetLinear * originalOffset;
+        const Eigen::Vector2d newPosition =
+            oldPosition * (1.0 - sample.lambda) + targetPosition * sample.lambda;
+
+        updates << PointUpdate{sample.index,
+                               m_d->transformedPoints[sample.index],
+                               fromEigenPoint(newPosition)};
+
+        updatedCentroid += sample.weight * newPosition;
+        updatedWeightSum += sample.weight;
+    }
 
-        const QPointF newPosition =
-            m_d->transformedPoints[index] * (1.0 - lambda) +
-            predictedPosition * lambda;
+    if (updatedWeightSum <= 1e-12) return;
+    updatedCentroid /= updatedWeightSum;
 
-        updates << PointUpdate{index, m_d->transformedPoints[index], newPosition};
-    }
+    const QPointF centroidCorrection = fromEigenPoint(transformedCentroid - updatedCentroid);
 
     for (int i = 0; i < updates.count(); i++) {
-        const PointUpdate &update = updates[i];
+        PointUpdate &update = updates[i];
+        update.newPosition += centroidCorrection;
         m_d->transformedPoints[update.index] = update.newPosition;
-        m_d->transformedPointsContainer.movePoint(update.index, update.oldPosition, update.newPosition);
+        m_d->transformedPointsContainer.movePoint(update.index,
+                                                 update.oldPosition,
+                                                 update.newPosition);
     }
 }
 
diff --git a/libs/image/kis_liquify_transform_worker.h b/libs/image/kis_liquify_transform_worker.h
index b52657f1395..6101dd233cc 100644
--- a/libs/image/kis_liquify_transform_worker.h
+++ b/libs/image/kis_liquify_transform_worker.h
@@ -60,13 +60,12 @@ public:
                     qreal amount,
                     qreal sigma);
 
-    void relaxPoints(const QPointF &base,
-                     qreal amount,
-                     qreal sigma);
-
-    void affineRelaxPoints(const QPointF &base,
-                           qreal amount,
-                           qreal sigma);
+    void restoreShapePoints(const QPointF &base,
+                            qreal amount,
+                            qreal sigma,
+                            bool preserveRotation,
+                            bool preserveScale,
+                            bool preserveStretch);
 
     const QVector<QPointF>& originalPoints() const;
     QVector<QPointF>& transformedPoints();
diff --git a/libs/image/tests/kis_liquify_transform_worker_test.cpp b/libs/image/tests/kis_liquify_transform_worker_test.cpp
index 049782f9369..179e05f78e3 100644
--- a/libs/image/tests/kis_liquify_transform_worker_test.cpp
+++ b/libs/image/tests/kis_liquify_transform_worker_test.cpp
@@ -11,6 +11,9 @@
 #include <KoColor.h>
 #include <KoProgressUpdater.h>
 #include <KoUpdater.h>
+#include <QDomDocument>
+#include <QScopedPointer>
+#include <cmath>
 
 #include <testutil.h>
 #include <kis_liquify_transform_worker.h>
@@ -76,6 +79,83 @@ KisLiquifyTransformWorker* getWorkerFromXml(QString filename)
 
 }
 
+namespace {
+
+KisLiquifyTransformWorker* cloneWorkerWithRebuiltSpatialIndex(const KisLiquifyTransformWorker &worker)
+{
+    QDomDocument doc;
+    QDomElement root = doc.createElement("data");
+    doc.appendChild(root);
+    worker.toXML(&root);
+    return KisLiquifyTransformWorker::fromXML(root);
+}
+
+QPointF mapPoint(const QPointF &pt,
+                 const QPointF &center,
+                 const QTransform &linearTransform,
+                 const QPointF &translation)
+{
+    return center + translation + linearTransform.map(pt - center);
+}
+
+void setTransformedPoints(KisLiquifyTransformWorker *worker,
+                          const QPointF &center,
+                          const QTransform &linearTransform,
+                          const QPointF &translation)
+{
+    for (int i = 0; i < worker->transformedPoints().size(); i++) {
+        worker->transformedPoints()[i] =
+            mapPoint(worker->originalPoints()[i], center, linearTransform, translation);
+    }
+}
+
+qreal maxDistance(const QVector<QPointF> &lhs, const QVector<QPointF> &rhs)
+{
+    KIS_ASSERT(lhs.size() == rhs.size());
+
+    qreal maxDistance = 0.0;
+    for (int i = 0; i < lhs.size(); i++) {
+        maxDistance = qMax(maxDistance, kisDistance(lhs[i], rhs[i]));
+    }
+
+    return maxDistance;
+}
+
+qreal averageDistance(const QVector<QPointF> &lhs, const QVector<QPointF> &rhs)
+{
+    KIS_ASSERT(lhs.size() == rhs.size());
+
+    qreal distance = 0.0;
+    for (int i = 0; i < lhs.size(); i++) {
+        distance += kisDistance(lhs[i], rhs[i]);
+    }
+
+    return distance / lhs.size();
+}
+
+QPointF weightedTransformedCentroid(KisLiquifyTransformWorker *worker,
+                                    const QPointF &base,
+                                    qreal sigma)
+{
+    const qreal maxDist = 3.0 * sigma;
+    QPointF centroid;
+    qreal weightSum = 0.0;
+
+    Q_FOREACH (const QPointF &pt, worker->transformedPoints()) {
+        const QPointF diff = pt - base;
+        const qreal dist = KisAlgebra2D::norm(diff);
+        if (dist > maxDist) continue;
+
+        const qreal weight = exp(-0.5 * pow2(dist / sigma));
+        centroid += pt * weight;
+        weightSum += weight;
+    }
+
+    return centroid / weightSum;
+}
+
+}
+
 
 void KisLiquifyTransformWorkerTest::testPoints()
 {
@@ -130,52 +210,163 @@ void KisLiquifyTransformWorkerTest::testPoints()
     TestUtil::checkQImage(result, "liquify_transform_test", "liquify_dev", "unity");
 }
 
-void KisLiquifyTransformWorkerTest::testRelaxPoints()
+void KisLiquifyTransformWorkerTest::testRestoreShapePreservesSimilarity()
+{
+    const QRect bounds(0, 0, 64, 64);
+    const int pixelPrecision = 8;
+
+    KisLiquifyTransformWorker worker(bounds, 0, pixelPrecision);
+
+    const QPointF centerPoint(32.0, 32.0);
+    const QPointF translation(17.0, -6.0);
+
+    QTransform transform;
+    transform.rotateRadians(M_PI / 5.0);
+    transform.scale(1.4, 1.4);
+
+    setTransformedPoints(&worker, centerPoint, transform, translation);
+
+    QScopedPointer<KisLiquifyTransformWorker> indexedWorker(
+        cloneWorkerWithRebuiltSpatialIndex(worker));
+
+    const QVector<QPointF> before = indexedWorker->transformedPoints();
+    indexedWorker->restoreShapePoints(centerPoint + translation, 1.0, 100.0, true, true, false);
+
+    QVERIFY(maxDistance(before, indexedWorker->transformedPoints()) < 1e-4);
+}
+
+void KisLiquifyTransformWorkerTest::testRestoreShapeRestoresRotationAndScale()
 {
     const QRect bounds(0, 0, 64, 64);
     const int pixelPrecision = 8;
 
     KisLiquifyTransformWorker worker(bounds, 0, pixelPrecision);
 
-    const QPoint centerCell(4, 4);
-    const int centerIndex = worker.pointToIndex(centerCell);
-    const QPointF centerPoint = worker.originalPoints()[centerIndex];
+    const QPointF centerPoint(32.0, 32.0);
+    const QPointF translation(17.0, -6.0);
 
-    worker.translatePoints(centerPoint, QPointF(30.0, 0.0), 8.0, false, 1.0);
+    QTransform transform;
+    transform.rotateRadians(M_PI / 4.0);
+    transform.scale(1.8, 1.8);
 
-    const QPointF oldCenterDisplacement =
-        worker.transformedPoints()[centerIndex] - worker.originalPoints()[centerIndex];
-    worker.relaxPoints(worker.transformedPoints()[centerIndex], 1.0, 8.0);
+    setTransformedPoints(&worker, centerPoint, transform, translation);
 
-    const QPointF centerDisplacement =
-        worker.transformedPoints()[centerIndex] - worker.originalPoints()[centerIndex];
+    QScopedPointer<KisLiquifyTransformWorker> indexedWorker(
+        cloneWorkerWithRebuiltSpatialIndex(worker));
 
-    QVERIFY(centerDisplacement.x() < oldCenterDisplacement.x());
-    QVERIFY(centerDisplacement.x() > 0.0);
-    QVERIFY(qAbs(centerDisplacement.y()) < 1e-6);
+    const QPoint sampleCell(5, 4);
+    const int sampleIndex = indexedWorker->pointToIndex(sampleCell);
+    const QPointF expectedRestoredPosition =
+        centerPoint + translation + (indexedWorker->originalPoints()[sampleIndex] - centerPoint);
+
+    const qreal beforeDistance =
+        kisDistance(indexedWorker->transformedPoints()[sampleIndex], expectedRestoredPosition);
+
+    indexedWorker->restoreShapePoints(centerPoint + translation, 1.0, 100.0, false, false, false);
+
+    const qreal afterDistance =
+        kisDistance(indexedWorker->transformedPoints()[sampleIndex], expectedRestoredPosition);
+
+    QVERIFY(afterDistance < beforeDistance);
 }
 
-void KisLiquifyTransformWorkerTest::testAffineRelaxPoints()
+void KisLiquifyTransformWorkerTest::testRestoreShapeRestoresScalePreservingRotation()
 {
     const QRect bounds(0, 0, 64, 64);
     const int pixelPrecision = 8;
 
     KisLiquifyTransformWorker worker(bounds, 0, pixelPrecision);
 
-    const QPoint centerCell(4, 4);
-    const int centerIndex = worker.pointToIndex(centerCell);
-    const QPointF centerPoint = worker.originalPoints()[centerIndex];
+    const QPointF centerPoint(32.0, 32.0);
+    const QPointF translation(17.0, -6.0);
+
+    QTransform fullTransform;
+    fullTransform.rotateRadians(M_PI / 4.0);
+    fullTransform.scale(1.8, 1.8);
+
+    QTransform rotationOnlyTransform;
+    rotationOnlyTransform.rotateRadians(M_PI / 4.0);
+
+    setTransformedPoints(&worker, centerPoint, fullTransform, translation);
+
+    QScopedPointer<KisLiquifyTransformWorker> indexedWorker(
+        cloneWorkerWithRebuiltSpatialIndex(worker));
+
+    const QPoint sampleCell(5, 4);
+    const int sampleIndex = indexedWorker->pointToIndex(sampleCell);
+    const QPointF expectedRestoredPosition =
+        mapPoint(indexedWorker->originalPoints()[sampleIndex],
+                 centerPoint,
+                 rotationOnlyTransform,
+                 translation);
+
+    const qreal beforeDistance =
+        kisDistance(indexedWorker->transformedPoints()[sampleIndex], expectedRestoredPosition);
+
+    indexedWorker->restoreShapePoints(centerPoint + translation, 1.0, 100.0, true, false, false);
+
+    const qreal afterDistance =
+        kisDistance(indexedWorker->transformedPoints()[sampleIndex], expectedRestoredPosition);
+
+    QVERIFY(afterDistance < beforeDistance);
+}
+
+void KisLiquifyTransformWorkerTest::testRestoreShapePreservesCentroid()
+{
+    const QRect bounds(0, 0, 64, 64);
+    const int pixelPrecision = 8;
+
+    KisLiquifyTransformWorker worker(bounds, 0, pixelPrecision);
+
+    const QPointF centerPoint(32.0, 32.0);
+    const QPointF translation(9.0, 12.0);
+    const QPointF base = centerPoint + translation;
+    const qreal sigma = 60.0;
+
+    QTransform transform(1.8, 0.2,
+                         0.6, 0.7,
+                         0.0, 0.0);
+    setTransformedPoints(&worker, centerPoint, transform, translation);
+
+    QScopedPointer<KisLiquifyTransformWorker> indexedWorker(
+        cloneWorkerWithRebuiltSpatialIndex(worker));
+
+    const QPointF beforeCentroid =
+        weightedTransformedCentroid(indexedWorker.data(), base, sigma);
+    indexedWorker->restoreShapePoints(base, 1.0, sigma, false, false, false);
+    const QPointF afterCentroid =
+        weightedTransformedCentroid(indexedWorker.data(), base, sigma);
+
+    QVERIFY(kisDistance(beforeCentroid, afterCentroid) < 1e-4);
+}
+
+void KisLiquifyTransformWorkerTest::testRestoreShapeCanPreserveStretch()
+{
+    const QRect bounds(0, 0, 64, 64);
+    const int pixelPrecision = 8;
+
+    KisLiquifyTransformWorker worker(bounds, 0, pixelPrecision);
+
+    const QPointF centerPoint(32.0, 32.0);
+    const QPointF translation(9.0, 12.0);
+    const QPointF base = centerPoint + translation;
+
+    QTransform transform;
+    transform.scale(2.0, 0.5);
+    setTransformedPoints(&worker, centerPoint, transform, translation);
 
-    worker.rotatePoints(centerPoint, M_PI / 2.0, 100.0, false, 1.0);
-    const QPointF affinePosition = worker.transformedPoints()[centerIndex];
+    QScopedPointer<KisLiquifyTransformWorker> restoreStretchWorker(
+        cloneWorkerWithRebuiltSpatialIndex(worker));
+    QScopedPointer<KisLiquifyTransformWorker> restoreShapeWorker(
+        cloneWorkerWithRebuiltSpatialIndex(worker));
 
-    worker.translatePoints(affinePosition, QPointF(25.0, 0.0), 8.0, false, 1.0);
-    const qreal corruptedDistance = kisDistance(worker.transformedPoints()[centerIndex], affinePosition);
+    const QVector<QPointF> before = restoreStretchWorker->transformedPoints();
 
-    worker.affineRelaxPoints(worker.transformedPoints()[centerIndex], 1.0, 8.0);
-    const qreal relaxedDistance = kisDistance(worker.transformedPoints()[centerIndex], affinePosition);
+    restoreStretchWorker->restoreShapePoints(base, 1.0, 100.0, false, true, true);
+    restoreShapeWorker->restoreShapePoints(base, 1.0, 100.0, false, true, false);
 
-    QVERIFY(relaxedDistance < corruptedDistance);
+    QVERIFY(averageDistance(before, restoreStretchWorker->transformedPoints()) <
+            averageDistance(before, restoreShapeWorker->transformedPoints()));
 }
 
 void KisLiquifyTransformWorkerTest::testPointsQImage()
diff --git a/libs/image/tests/kis_liquify_transform_worker_test.h b/libs/image/tests/kis_liquify_transform_worker_test.h
index 64392d6c60c..ceba7ef015b 100644
--- a/libs/image/tests/kis_liquify_transform_worker_test.h
+++ b/libs/image/tests/kis_liquify_transform_worker_test.h
@@ -14,8 +14,11 @@ class KisLiquifyTransformWorkerTest : public QObject
     Q_OBJECT
 private Q_SLOTS:
     void testPoints();
-    void testRelaxPoints();
-    void testAffineRelaxPoints();
+    void testRestoreShapePreservesSimilarity();
+    void testRestoreShapeRestoresRotationAndScale();
+    void testRestoreShapeRestoresScalePreservingRotation();
+    void testRestoreShapePreservesCentroid();
+    void testRestoreShapeCanPreserveStretch();
     void testPointsQImage();
     void testIdentityTransform();
 
diff --git a/plugins/tools/tool_transform2/kis_liquify_paintop.cpp b/plugins/tools/tool_transform2/kis_liquify_paintop.cpp
index fcd906901c2..c0cfd078ee4 100644
--- a/plugins/tools/tool_transform2/kis_liquify_paintop.cpp
+++ b/plugins/tools/tool_transform2/kis_liquify_paintop.cpp
@@ -49,8 +49,7 @@ QPainterPath KisLiquifyPaintop::brushOutline(const KisLiquifyProperties &props,
     switch (props.mode()) {
     case KisLiquifyProperties::MOVE:
     case KisLiquifyProperties::SCALE:
-    case KisLiquifyProperties::RELAX_SIMPLE:
-    case KisLiquifyProperties::RELAX_AFFINE:
+    case KisLiquifyProperties::RESTORE_SHAPE:
         break;
     case KisLiquifyProperties::ROTATE: {
         QPainterPath p;
@@ -175,16 +174,13 @@ KisSpacingInformation KisLiquifyPaintop::paintAt(const KisPaintInformation &pi)
                                 size);
 
         break;
-    case KisLiquifyProperties::RELAX_SIMPLE:
-        m_d->worker->relaxPoints(pi.pos(),
-                                 amount,
-                                 size);
-
-        break;
-    case KisLiquifyProperties::RELAX_AFFINE:
-        m_d->worker->affineRelaxPoints(pi.pos(),
-                                       amount,
-                                       size);
+    case KisLiquifyProperties::RESTORE_SHAPE:
+        m_d->worker->restoreShapePoints(pi.pos(),
+                                        amount,
+                                        size,
+                                        m_d->props.preserveShapeRotation(),
+                                        m_d->props.preserveShapeScale(),
+                                        m_d->props.preserveShapeStretch());
 
         break;
     case KisLiquifyProperties::N_MODES:
diff --git a/plugins/tools/tool_transform2/kis_liquify_properties.cpp b/plugins/tools/tool_transform2/kis_liquify_properties.cpp
index c0f98575b1b..347c4f5022e 100644
--- a/plugins/tools/tool_transform2/kis_liquify_properties.cpp
+++ b/plugins/tools/tool_transform2/kis_liquify_properties.cpp
@@ -26,6 +26,9 @@ KisLiquifyProperties::KisLiquifyProperties(const KisLiquifyProperties &rhs)
     m_reverseDirection = rhs.m_reverseDirection;
     m_useWashMode = rhs.m_useWashMode;
     m_flow = rhs.m_flow;
+    m_preserveShapeRotation = rhs.m_preserveShapeRotation;
+    m_preserveShapeScale = rhs.m_preserveShapeScale;
+    m_preserveShapeStretch = rhs.m_preserveShapeStretch;
 }
 
 KisLiquifyProperties &KisLiquifyProperties::operator=(const KisLiquifyProperties &rhs)
@@ -39,6 +42,9 @@ KisLiquifyProperties &KisLiquifyProperties::operator=(const KisLiquifyProperties
     m_reverseDirection = rhs.m_reverseDirection;
     m_useWashMode = rhs.m_useWashMode;
     m_flow = rhs.m_flow;
+    m_preserveShapeRotation = rhs.m_preserveShapeRotation;
+    m_preserveShapeScale = rhs.m_preserveShapeScale;
+    m_preserveShapeStretch = rhs.m_preserveShapeStretch;
 
     return *this;
 }
@@ -54,21 +60,22 @@ bool KisLiquifyProperties::operator==(const KisLiquifyProperties &other) const
         m_amountHasPressure == other.m_amountHasPressure &&
         m_reverseDirection == other.m_reverseDirection &&
         m_useWashMode == other.m_useWashMode &&
-        m_flow == other.m_flow;
+        m_flow == other.m_flow &&
+        m_preserveShapeRotation == other.m_preserveShapeRotation &&
+        m_preserveShapeScale == other.m_preserveShapeScale &&
+        m_preserveShapeStretch == other.m_preserveShapeStretch;
 }
 
 bool KisLiquifyProperties::supportsReverseDirection(LiquifyMode mode)
 {
     return mode != UNDO &&
-        mode != RELAX_SIMPLE &&
-        mode != RELAX_AFFINE;
+        mode != RESTORE_SHAPE;
 }
 
 bool KisLiquifyProperties::supportsWashMode(LiquifyMode mode)
 {
     return mode != UNDO &&
-        mode != RELAX_SIMPLE &&
-        mode != RELAX_AFFINE;
+        mode != RESTORE_SHAPE;
 }
 
 QString liquifyModeString(KisLiquifyProperties::LiquifyMode mode)
@@ -91,11 +98,8 @@ QString liquifyModeString(KisLiquifyProperties::LiquifyMode mode)
     case KisLiquifyProperties::UNDO:
         result = "Undo";
         break;
-    case KisLiquifyProperties::RELAX_SIMPLE:
-        result = "RelaxSimple";
-        break;
-    case KisLiquifyProperties::RELAX_AFFINE:
-        result = "RelaxAffine";
+    case KisLiquifyProperties::RESTORE_SHAPE:
+        result = "RestoreShape";
         break;
     case KisLiquifyProperties::N_MODES:
         qFatal("Unsupported mode");
@@ -107,10 +111,8 @@ QString liquifyModeString(KisLiquifyProperties::LiquifyMode mode)
 qreal defaultAmountForMode(KisLiquifyProperties::LiquifyMode mode)
 {
     switch (mode) {
-    case KisLiquifyProperties::RELAX_SIMPLE:
-        return 0.3;
-    case KisLiquifyProperties::RELAX_AFFINE:
-        return 1.0;
+    case KisLiquifyProperties::RESTORE_SHAPE:
+        return 0.03;
     case KisLiquifyProperties::MOVE:
     case KisLiquifyProperties::SCALE:
     case KisLiquifyProperties::ROTATE:
@@ -137,6 +139,9 @@ void KisLiquifyProperties::saveMode() const
     cfg.writeEntry("reverseDirection", m_reverseDirection);
     cfg.writeEntry("useWashMode", m_useWashMode);
     cfg.writeEntry("flow", m_flow);
+    cfg.writeEntry("preserveShapeRotation", m_preserveShapeRotation);
+    cfg.writeEntry("preserveShapeScale", m_preserveShapeScale);
+    cfg.writeEntry("preserveShapeStretch", m_preserveShapeStretch);
 
     KConfigGroup globalCfg =  KSharedConfig::openConfig()->group("LiquifyTool");
     globalCfg.writeEntry("mode", (int)m_mode);
@@ -155,12 +160,22 @@ void KisLiquifyProperties::loadMode()
     m_reverseDirection = cfg.readEntry("reverseDirection", m_reverseDirection);
     m_useWashMode = cfg.readEntry("useWashMode", m_useWashMode);
     m_flow = cfg.readEntry("flow", m_flow);
+    m_preserveShapeRotation = cfg.readEntry("preserveShapeRotation", m_preserveShapeRotation);
+    m_preserveShapeScale = cfg.readEntry("preserveShapeScale", m_preserveShapeScale);
+    m_preserveShapeStretch = cfg.readEntry("preserveShapeStretch", m_preserveShapeStretch);
 }
 
 void KisLiquifyProperties::loadAndResetMode()
 {
     KConfigGroup globalCfg =  KSharedConfig::openConfig()->group("LiquifyTool");
-    m_mode = (LiquifyMode) globalCfg.readEntry("mode", (int)m_mode);
+    const int loadedMode = globalCfg.readEntry("mode", (int)m_mode);
+    if (loadedMode >= 0 && loadedMode < N_MODES) {
+        m_mode = (LiquifyMode) loadedMode;
+    } else if (loadedMode == N_MODES) {
+        m_mode = RESTORE_SHAPE;
+    } else {
+        m_mode = MOVE;
+    }
 
     loadMode();
 }
@@ -180,6 +195,9 @@ void KisLiquifyProperties::toXML(QDomElement *e) const
     KisDomUtils::saveValue(&liqEl, "reverseDirection", m_reverseDirection);
     KisDomUtils::saveValue(&liqEl, "useWashMode", m_useWashMode);
     KisDomUtils::saveValue(&liqEl, "flow", m_flow);
+    KisDomUtils::saveValue(&liqEl, "preserveShapeRotation", m_preserveShapeRotation);
+    KisDomUtils::saveValue(&liqEl, "preserveShapeScale", m_preserveShapeScale);
+    KisDomUtils::saveValue(&liqEl, "preserveShapeStretch", m_preserveShapeStretch);
 }
 
 KisLiquifyProperties KisLiquifyProperties::fromXML(const QDomElement &e)
@@ -205,10 +223,18 @@ KisLiquifyProperties KisLiquifyProperties::fromXML(const QDomElement &e)
 
     if (result && newMode >= 0 && newMode < N_MODES) {
         props.m_mode = (LiquifyMode) newMode;
+    } else if (result && newMode == N_MODES) {
+        props.m_mode = RESTORE_SHAPE;
     } else {
         result = false;
     }
 
+    if (result) {
+        KisDomUtils::loadValue(liqEl, "preserveShapeRotation", &props.m_preserveShapeRotation);
+        KisDomUtils::loadValue(liqEl, "preserveShapeScale", &props.m_preserveShapeScale);
+        KisDomUtils::loadValue(liqEl, "preserveShapeStretch", &props.m_preserveShapeStretch);
+    }
+
     return props;
 }
 
@@ -225,6 +251,9 @@ QDebug operator<<(QDebug dbg, const KisLiquifyProperties &props)
     dbg.space() << "\n    " << ppVar(props.reverseDirection());
     dbg.space() << "\n    " << ppVar(props.useWashMode());
     dbg.space() << "\n    " << ppVar(props.flow());
+    dbg.space() << "\n    " << ppVar(props.preserveShapeRotation());
+    dbg.space() << "\n    " << ppVar(props.preserveShapeScale());
+    dbg.space() << "\n    " << ppVar(props.preserveShapeStretch());
     dbg.space() << "\n    );\n";
     return dbg.nospace();
 }
diff --git a/plugins/tools/tool_transform2/kis_liquify_properties.h b/plugins/tools/tool_transform2/kis_liquify_properties.h
index d14c98bb74a..cd933bc6b31 100644
--- a/plugins/tools/tool_transform2/kis_liquify_properties.h
+++ b/plugins/tools/tool_transform2/kis_liquify_properties.h
@@ -24,8 +24,7 @@ public:
         ROTATE,
         OFFSET,
         UNDO,
-        RELAX_SIMPLE,
-        RELAX_AFFINE,
+        RESTORE_SHAPE,
 
         N_MODES
     };
@@ -39,7 +38,10 @@ public:
           m_amountHasPressure(false),
           m_reverseDirection(false),
           m_useWashMode(false),
-          m_flow(0.2)
+          m_flow(0.2),
+          m_preserveShapeRotation(true),
+          m_preserveShapeScale(true),
+          m_preserveShapeStretch(false)
     {
     }
 
@@ -124,6 +126,27 @@ public:
         m_flow = value;
     }
 
+    bool preserveShapeRotation() const {
+        return m_preserveShapeRotation;
+    }
+    void setPreserveShapeRotation(bool value) {
+        m_preserveShapeRotation = value;
+    }
+
+    bool preserveShapeScale() const {
+        return m_preserveShapeScale;
+    }
+    void setPreserveShapeScale(bool value) {
+        m_preserveShapeScale = value;
+    }
+
+    bool preserveShapeStretch() const {
+        return m_preserveShapeStretch;
+    }
+    void setPreserveShapeStretch(bool value) {
+        m_preserveShapeStretch = value;
+    }
+
     void saveMode() const;
     void loadMode();
 
@@ -143,6 +166,9 @@ private:
 
     bool m_useWashMode;
     qreal m_flow;
+    bool m_preserveShapeRotation;
+    bool m_preserveShapeScale;
+    bool m_preserveShapeStretch;
 };
 
 QDebug KRITATOOLTRANSFORM_EXPORT operator<<(QDebug dbg, const KisLiquifyProperties &properties);
diff --git a/plugins/tools/tool_transform2/kis_tool_transform_config_widget.cpp b/plugins/tools/tool_transform2/kis_tool_transform_config_widget.cpp
index a70dbb801d3..6728de562e9 100644
--- a/plugins/tools/tool_transform2/kis_tool_transform_config_widget.cpp
+++ b/plugins/tools/tool_transform2/kis_tool_transform_config_widget.cpp
@@ -265,15 +265,13 @@ KisToolTransformConfigWidget::KisToolTransformConfigWidget(TransformTransactionP
     connect(liquifyRotate, SIGNAL(toggled(bool)), liquifyModeMapper, SLOT(map()));
     connect(liquifyOffset, SIGNAL(toggled(bool)), liquifyModeMapper, SLOT(map()));
     connect(liquifyUndo, SIGNAL(toggled(bool)), liquifyModeMapper, SLOT(map()));
-    connect(liquifyRelaxSimple, SIGNAL(toggled(bool)), liquifyModeMapper, SLOT(map()));
-    connect(liquifyRelaxAffine, SIGNAL(toggled(bool)), liquifyModeMapper, SLOT(map()));
+    connect(liquifyRestoreShape, SIGNAL(toggled(bool)), liquifyModeMapper, SLOT(map()));
     liquifyModeMapper->setMapping(liquifyMove, (int)KisLiquifyProperties::MOVE);
     liquifyModeMapper->setMapping(liquifyScale, (int)KisLiquifyProperties::SCALE);
     liquifyModeMapper->setMapping(liquifyRotate, (int)KisLiquifyProperties::ROTATE);
     liquifyModeMapper->setMapping(liquifyOffset, (int)KisLiquifyProperties::OFFSET);
     liquifyModeMapper->setMapping(liquifyUndo, (int)KisLiquifyProperties::UNDO);
-    liquifyModeMapper->setMapping(liquifyRelaxSimple, (int)KisLiquifyProperties::RELAX_SIMPLE);
-    liquifyModeMapper->setMapping(liquifyRelaxAffine, (int)KisLiquifyProperties::RELAX_AFFINE);
+    liquifyModeMapper->setMapping(liquifyRestoreShape, (int)KisLiquifyProperties::RESTORE_SHAPE);
     connect(liquifyModeMapper, SIGNAL(mapped(int)), SLOT(slotLiquifyModeChanged(int)));
     connect(liquifyModeMapper, SIGNAL(mapped(int)), SLOT(notifyEditingFinished()));
 
@@ -282,14 +280,69 @@ KisToolTransformConfigWidget::KisToolTransformConfigWidget(TransformTransactionP
     liquifyRotate->setToolTip(i18nc("@info:tooltip", "Rotate: twirl image under cursor"));
     liquifyOffset->setToolTip(i18nc("@info:tooltip", "Offset: shift the image to the right of the stroke direction"));
     liquifyUndo->setToolTip(i18nc("@info:tooltip", "Undo: erase actions of other tools"));
-    liquifyRelaxSimple->setToolTip(i18nc("@info:tooltip",
-                                          "Relax: smooth liquify distortions by averaging neighboring movements\n"
-                                          "Preserves: local average movement\n"
-                                          "Removes: local movement differences, bumps and jitter"));
-    liquifyRelaxAffine->setToolTip(i18nc("@info:tooltip",
-                                          "Shape-preserving Relax: smooth liquify distortions while preserving local rotation, scale and shear\n"
-                                          "Preserves: local translation, rotation, scale and shear\n"
-                                          "Removes: local non-affine distortions, pinches and swirls"));
+    liquifyRestoreShape->setToolTip(i18nc("@info:tooltip",
+                                           "Restore Shape: reduce local liquify stretching and bunching while keeping the affected area centered"));
+
+    m_restoreShapeRotationButtons = new QButtonGroup(this);
+    m_restoreShapeRotationButtons->addButton(liquifyRestoreShapeRotationOriginal, 0);
+    m_restoreShapeRotationButtons->addButton(liquifyRestoreShapeRotationPreserve, 1);
+    connect(m_restoreShapeRotationButtons, SIGNAL(idClicked(int)), this, SLOT(liquifyPreserveShapeRotationChanged(int)));
+    connect(m_restoreShapeRotationButtons, SIGNAL(idClicked(int)), this, SLOT(notifyEditingFinished()));
+
+    m_restoreShapeScaleButtons = new QButtonGroup(this);
+    m_restoreShapeScaleButtons->addButton(liquifyRestoreShapeScaleOriginal, 0);
+    m_restoreShapeScaleButtons->addButton(liquifyRestoreShapeScalePreserve, 1);
+    connect(m_restoreShapeScaleButtons, SIGNAL(idClicked(int)), this, SLOT(liquifyPreserveShapeScaleChanged(int)));
+    connect(m_restoreShapeScaleButtons, SIGNAL(idClicked(int)), this, SLOT(notifyEditingFinished()));
+
+    m_restoreShapeStretchButtons = new QButtonGroup(this);
+    m_restoreShapeStretchButtons->addButton(liquifyRestoreShapeStretchOriginal, 0);
+    m_restoreShapeStretchButtons->addButton(liquifyRestoreShapeStretchPreserve, 1);
+    connect(m_restoreShapeStretchButtons, SIGNAL(idClicked(int)), this, SLOT(liquifyPreserveShapeStretchChanged(int)));
+    connect(m_restoreShapeStretchButtons, SIGNAL(idClicked(int)), this, SLOT(notifyEditingFinished()));
+
+    liquifyRestoreShapeRotationOriginal->setToolTip(i18nc("@info:tooltip", "Rotate the affected area back toward its original orientation"));
+    liquifyRestoreShapeRotationPreserve->setToolTip(i18nc("@info:tooltip", "Keep the current rotation of the affected area"));
+    liquifyRestoreShapeScaleOriginal->setToolTip(i18nc("@info:tooltip", "Scale the affected area back toward its original size"));
+    liquifyRestoreShapeScalePreserve->setToolTip(i18nc("@info:tooltip", "Keep the current overall scale of the affected area"));
+    liquifyRestoreShapeStretchOriginal->setToolTip(i18nc("@info:tooltip", "Remove simple stretch or squash from the affected area"));
+    liquifyRestoreShapeStretchPreserve->setToolTip(i18nc("@info:tooltip", "Keep simple stretch or squash while removing shear-like distortion"));
+
+    const auto setRestoreShapeButtonStyle = [] (QToolButton *button, bool isLeftButton) {
+        button->setStyleSheet(QString::fromLatin1(
+            "QToolButton {"
+            " border: 1px solid palette(mid);"
+            " background-color: palette(button);"
+            " color: palette(button-text);"
+            " padding: 4px 10px;"
+            " %1"
+            "}"
+            "QToolButton:hover {"
+            " background-color: palette(light);"
+            "}"
+            "QToolButton:pressed {"
+            " background-color: palette(midlight);"
+            "}"
+            "QToolButton:checked {"
+            " background-color: palette(highlight);"
+            " color: palette(highlighted-text);"
+            " border-color: palette(highlight);"
+            "}"
+            "QToolButton:checked:hover {"
+            " background-color: palette(highlight);"
+            " color: palette(highlighted-text);"
+            "}")
+            .arg(isLeftButton ?
+                 QLatin1String("border-top-right-radius: 0px; border-bottom-right-radius: 0px;") :
+                 QLatin1String("border-top-left-radius: 0px; border-bottom-left-radius: 0px;")));
+    };
+
+    setRestoreShapeButtonStyle(liquifyRestoreShapeRotationOriginal, true);
+    setRestoreShapeButtonStyle(liquifyRestoreShapeRotationPreserve, false);
+    setRestoreShapeButtonStyle(liquifyRestoreShapeScaleOriginal, true);
+    setRestoreShapeButtonStyle(liquifyRestoreShapeScalePreserve, false);
+    setRestoreShapeButtonStyle(liquifyRestoreShapeStretchOriginal, true);
+    setRestoreShapeButtonStyle(liquifyRestoreShapeStretchPreserve, false);
 
     // Connect all edit boxes to the Editing Finished signal
     connect(densityBox, SIGNAL(editingFinished()), this, SLOT(notifyEditingFinished()));
@@ -362,8 +415,7 @@ void KisToolTransformConfigWidget::slotUpdateIcons()
     liquifyRotate->setIcon(KisIconUtils::loadIcon("transform_icons_liquify_rotate"));
     liquifyOffset->setIcon(KisIconUtils::loadIcon("transform_icons_liquify_offset"));
     liquifyUndo->setIcon(KisIconUtils::loadIcon("transform_icons_liquify_erase"));
-    liquifyRelaxSimple->setIcon(KisIconUtils::loadIcon("transform_icons_liquify_relax"));
-    liquifyRelaxAffine->setIcon(KisIconUtils::loadIcon("transform_icons_liquify_relax_affine"));
+    liquifyRestoreShape->setIcon(KisIconUtils::loadIcon("transform_icons_liquify_relax"));
 
 
 
@@ -403,6 +455,12 @@ void KisToolTransformConfigWidget::updateLiquifyControls()
     liquifySizePressureBox->setChecked(props->sizeHasPressure());
     liquifyAmountPressureBox->setChecked(props->amountHasPressure());
     liquifyReverseDirectionChk->setChecked(props->reverseDirection());
+    liquifyRestoreShapeRotationOriginal->setChecked(!props->preserveShapeRotation());
+    liquifyRestoreShapeRotationPreserve->setChecked(props->preserveShapeRotation());
+    liquifyRestoreShapeScaleOriginal->setChecked(!props->preserveShapeScale());
+    liquifyRestoreShapeScalePreserve->setChecked(props->preserveShapeScale());
+    liquifyRestoreShapeStretchOriginal->setChecked(!props->preserveShapeStretch());
+    liquifyRestoreShapeStretchPreserve->setChecked(props->preserveShapeStretch());
 
 
     KisLiquifyProperties::LiquifyMode mode =
@@ -410,10 +468,20 @@ void KisToolTransformConfigWidget::updateLiquifyControls()
 
     bool canInverseDirection = KisLiquifyProperties::supportsReverseDirection(mode);
     bool canUseWashMode = KisLiquifyProperties::supportsWashMode(mode);
+    bool isRestoreShapeMode = mode == KisLiquifyProperties::RESTORE_SHAPE;
 
     liquifyReverseDirectionChk->setEnabled(canInverseDirection);
     liquifyFlowSlider->setEnabled(canUseWashMode && useWashMode);
     buildupModeComboBox->setEnabled(canUseWashMode);
+    lblLiquifyShapeRotation->setVisible(isRestoreShapeMode);
+    liquifyRestoreShapeRotationOriginal->setVisible(isRestoreShapeMode);
+    liquifyRestoreShapeRotationPreserve->setVisible(isRestoreShapeMode);
+    lblLiquifyShapeScale->setVisible(isRestoreShapeMode);
+    liquifyRestoreShapeScaleOriginal->setVisible(isRestoreShapeMode);
+    liquifyRestoreShapeScalePreserve->setVisible(isRestoreShapeMode);
+    lblLiquifyShapeStretch->setVisible(isRestoreShapeMode);
+    liquifyRestoreShapeStretchOriginal->setVisible(isRestoreShapeMode);
+    liquifyRestoreShapeStretchPreserve->setVisible(isRestoreShapeMode);
 
     const qreal maxAmount = canUseWashMode ? 5.0 : 1.0;
     liquifyAmountSlider->setRange(0.0, maxAmount, 2);
@@ -543,6 +611,42 @@ void KisToolTransformConfigWidget::liquifyReverseDirectionChanged(bool value)
     notifyConfigChanged(false);
 }
 
+void KisToolTransformConfigWidget::liquifyPreserveShapeRotationChanged(int value)
+{
+    if (m_uiSlotsBlocked) return;
+
+    ToolTransformArgs *config = m_transaction->currentConfig();
+    KisLiquifyProperties *props =
+        config->liquifyProperties();
+
+    props->setPreserveShapeRotation(value == 1);
+    notifyConfigChanged(false);
+}
+
+void KisToolTransformConfigWidget::liquifyPreserveShapeScaleChanged(int value)
+{
+    if (m_uiSlotsBlocked) return;
+
+    ToolTransformArgs *config = m_transaction->currentConfig();
+    KisLiquifyProperties *props =
+        config->liquifyProperties();
+
+    props->setPreserveShapeScale(value == 1);
+    notifyConfigChanged(false);
+}
+
+void KisToolTransformConfigWidget::liquifyPreserveShapeStretchChanged(int value)
+{
+    if (m_uiSlotsBlocked) return;
+
+    ToolTransformArgs *config = m_transaction->currentConfig();
+    KisLiquifyProperties *props =
+        config->liquifyProperties();
+
+    props->setPreserveShapeStretch(value == 1);
+    notifyConfigChanged(false);
+}
+
 void KisToolTransformConfigWidget::updateConfig(const ToolTransformArgs &config)
 {
     blockUiSlots();
@@ -669,11 +773,8 @@ void KisToolTransformConfigWidget::updateConfig(const ToolTransformArgs &config)
         case KisLiquifyProperties::UNDO:
             liquifyUndo->setChecked(true);
             break;
-        case KisLiquifyProperties::RELAX_SIMPLE:
-            liquifyRelaxSimple->setChecked(true);
-            break;
-        case KisLiquifyProperties::RELAX_AFFINE:
-            liquifyRelaxAffine->setChecked(true);
+        case KisLiquifyProperties::RESTORE_SHAPE:
+            liquifyRestoreShape->setChecked(true);
             break;
         case KisLiquifyProperties::N_MODES:
             qFatal("Unsupported mode");
diff --git a/plugins/tools/tool_transform2/kis_tool_transform_config_widget.h b/plugins/tools/tool_transform2/kis_tool_transform_config_widget.h
index f0c5287ecde..c3e24df3380 100644
--- a/plugins/tools/tool_transform2/kis_tool_transform_config_widget.h
+++ b/plugins/tools/tool_transform2/kis_tool_transform_config_widget.h
@@ -100,6 +100,9 @@ public Q_SLOTS:
     void liquifySizePressureChanged(bool value);
     void liquifyAmountPressureChanged(bool value);
     void liquifyReverseDirectionChanged(bool value);
+    void liquifyPreserveShapeRotationChanged(int value);
+    void liquifyPreserveShapeScaleChanged(int value);
+    void liquifyPreserveShapeStretchChanged(int value);
 
     void slotLiquifyModeChanged(int value);
 
@@ -139,6 +142,9 @@ private:
     TransformTransactionProperties *m_transaction;
     QPointF m_handleDir[9];
     QButtonGroup *m_rotationCenterButtons;
+    QButtonGroup *m_restoreShapeRotationButtons;
+    QButtonGroup *m_restoreShapeScaleButtons;
+    QButtonGroup *m_restoreShapeStretchButtons;
     int m_notificationsBlocked;
     int m_uiSlotsBlocked;
     double m_scaleRatio;
diff --git a/plugins/tools/tool_transform2/wdg_tool_transform.ui b/plugins/tools/tool_transform2/wdg_tool_transform.ui
index 4247aafcbea..25fdd1b644e 100644
--- a/plugins/tools/tool_transform2/wdg_tool_transform.ui
+++ b/plugins/tools/tool_transform2/wdg_tool_transform.ui
@@ -1941,7 +1941,7 @@
              </widget>
             </item>
             <item>
-             <widget class="QToolButton" name="liquifyRelaxSimple">
+             <widget class="QToolButton" name="liquifyRestoreShape">
               <property name="sizePolicy">
                <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
                 <horstretch>0</horstretch>
@@ -1949,29 +1949,7 @@
                </sizepolicy>
               </property>
               <property name="text">
-               <string>Relax</string>
-              </property>
-              <property name="checkable">
-               <bool>true</bool>
-              </property>
-              <property name="autoExclusive">
-               <bool>true</bool>
-              </property>
-              <property name="autoRaise">
-               <bool>true</bool>
-              </property>
-             </widget>
-            </item>
-            <item>
-             <widget class="QToolButton" name="liquifyRelaxAffine">
-              <property name="sizePolicy">
-               <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
-                <horstretch>0</horstretch>
-                <verstretch>0</verstretch>
-               </sizepolicy>
-              </property>
-              <property name="text">
-               <string>Affine Relax</string>
+               <string>Restore Shape</string>
               </property>
               <property name="checkable">
                <bool>true</bool>
@@ -2115,6 +2093,201 @@
              </property>
             </widget>
            </item>
+           <item row="6" column="0">
+            <widget class="QLabel" name="lblLiquifyShapeRotation">
+             <property name="text">
+              <string>Rotation:</string>
+             </property>
+            </widget>
+           </item>
+           <item row="6" column="1" colspan="2">
+            <layout class="QHBoxLayout" name="horizontalLayoutLiquifyRotation">
+             <property name="spacing">
+              <number>0</number>
+             </property>
+             <item>
+              <widget class="QToolButton" name="liquifyRestoreShapeRotationOriginal">
+               <property name="minimumSize">
+                <size>
+                 <width>135</width>
+                 <height>0</height>
+                </size>
+               </property>
+               <property name="text">
+                <string>To original</string>
+               </property>
+               <property name="checkable">
+                <bool>true</bool>
+               </property>
+               <property name="autoRaise">
+                <bool>true</bool>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <widget class="QToolButton" name="liquifyRestoreShapeRotationPreserve">
+               <property name="minimumSize">
+                <size>
+                 <width>135</width>
+                 <height>0</height>
+                </size>
+               </property>
+               <property name="text">
+                <string>Preserve</string>
+               </property>
+               <property name="checkable">
+                <bool>true</bool>
+               </property>
+               <property name="autoRaise">
+                <bool>true</bool>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <spacer name="horizontalSpacerLiquifyRotation">
+               <property name="orientation">
+                <enum>Qt::Horizontal</enum>
+               </property>
+               <property name="sizeHint" stdset="0">
+                <size>
+                 <width>0</width>
+                 <height>20</height>
+                </size>
+               </property>
+              </spacer>
+             </item>
+            </layout>
+           </item>
+           <item row="7" column="0">
+            <widget class="QLabel" name="lblLiquifyShapeScale">
+             <property name="text">
+              <string>Scale:</string>
+             </property>
+            </widget>
+           </item>
+           <item row="7" column="1" colspan="2">
+            <layout class="QHBoxLayout" name="horizontalLayoutLiquifyScale">
+             <property name="spacing">
+              <number>0</number>
+             </property>
+             <item>
+              <widget class="QToolButton" name="liquifyRestoreShapeScaleOriginal">
+               <property name="minimumSize">
+                <size>
+                 <width>135</width>
+                 <height>0</height>
+                </size>
+               </property>
+               <property name="text">
+                <string>To original</string>
+               </property>
+               <property name="checkable">
+                <bool>true</bool>
+               </property>
+               <property name="autoRaise">
+                <bool>true</bool>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <widget class="QToolButton" name="liquifyRestoreShapeScalePreserve">
+               <property name="minimumSize">
+                <size>
+                 <width>135</width>
+                 <height>0</height>
+                </size>
+               </property>
+               <property name="text">
+                <string>Preserve</string>
+               </property>
+               <property name="checkable">
+                <bool>true</bool>
+               </property>
+               <property name="autoRaise">
+                <bool>true</bool>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <spacer name="horizontalSpacerLiquifyScale">
+               <property name="orientation">
+                <enum>Qt::Horizontal</enum>
+               </property>
+               <property name="sizeHint" stdset="0">
+                <size>
+                 <width>0</width>
+                 <height>20</height>
+                </size>
+               </property>
+              </spacer>
+             </item>
+            </layout>
+           </item>
+           <item row="8" column="0">
+            <widget class="QLabel" name="lblLiquifyShapeStretch">
+             <property name="text">
+              <string>Stretch:</string>
+             </property>
+            </widget>
+           </item>
+           <item row="8" column="1" colspan="2">
+            <layout class="QHBoxLayout" name="horizontalLayoutLiquifyStretch">
+             <property name="spacing">
+              <number>0</number>
+             </property>
+             <item>
+              <widget class="QToolButton" name="liquifyRestoreShapeStretchOriginal">
+               <property name="minimumSize">
+                <size>
+                 <width>135</width>
+                 <height>0</height>
+                </size>
+               </property>
+               <property name="text">
+                <string>To original</string>
+               </property>
+               <property name="checkable">
+                <bool>true</bool>
+               </property>
+               <property name="autoRaise">
+                <bool>true</bool>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <widget class="QToolButton" name="liquifyRestoreShapeStretchPreserve">
+               <property name="minimumSize">
+                <size>
+                 <width>135</width>
+                 <height>0</height>
+                </size>
+               </property>
+               <property name="text">
+                <string>Preserve</string>
+               </property>
+               <property name="checkable">
+                <bool>true</bool>
+               </property>
+               <property name="autoRaise">
+                <bool>true</bool>
+               </property>
+              </widget>
+             </item>
+             <item>
+              <spacer name="horizontalSpacerLiquifyStretch">
+               <property name="orientation">
+                <enum>Qt::Horizontal</enum>
+               </property>
+               <property name="sizeHint" stdset="0">
+                <size>
+                 <width>0</width>
+                 <height>20</height>
+                </size>
+               </property>
+              </spacer>
+             </item>
+            </layout>
+           </item>
            <item row="2" column="0">
             <widget class="QLabel" name="lblAmount">
              <property name="sizePolicy">
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.