[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 ¢er,
+ const QTransform &linearTransform,
+ const QPointF &translation)
+{
+ return center + translation + linearTransform.map(pt - center);
+}
+
+void setTransformedPoints(KisLiquifyTransformWorker *worker,
+ const QPointF ¢er,
+ 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">