[graphics/krita] /: add liquify brushes "Relax" and "Affine Relax"; fixes messed up points, but preserves affine transforms (as much as possible...) normal "relax" only tries to maintain relative distortion, so may also un-shear, un-rotate etc; often you want one or the other so its two modes not one
Dmitry Kazakov <[email protected]>
| Newsgroups | gmane.comp.kde.cvs |
|---|---|
| Message-ID | <[email protected]> |
Git commit a85d83e532b79a2cb95cef95cabe2a5daa132d6f by Dmitry Kazakov, on behalf of Moritz Staudinger.
Committed on 07/08/2026 at 13:58.
Pushed by dkazakov into branch 'master'.
add liquify brushes "Relax" and "Affine Relax"; fixes messed up points, but preserves affine transforms (as much as possible...) normal "relax" only tries to maintain relative distortion, so may also un-shear, un-rotate etc; often you want one or the other so its two modes not one
M +43 -0 benchmarks/KisLiquifyTransformWorkerBenchmark.cpp
M +6 -0 benchmarks/KisLiquifyTransformWorkerBenchmark.h
M +4 -0 krita/pics/tool_transform/tool-transform-icons.qrc
M +233 -0 libs/image/kis_liquify_transform_worker.cpp
M +8 -0 libs/image/kis_liquify_transform_worker.h
M +48 -0 libs/image/tests/kis_liquify_transform_worker_test.cpp
M +2 -0 libs/image/tests/kis_liquify_transform_worker_test.h
M +15 -2 plugins/tools/tool_transform2/kis_liquify_paintop.cpp
M +41 -1 plugins/tools/tool_transform2/kis_liquify_properties.cpp
M +5 -0 plugins/tools/tool_transform2/kis_liquify_properties.h
M +22 -4 plugins/tools/tool_transform2/kis_tool_transform_config_widget.cpp
M +44 -0 plugins/tools/tool_transform2/wdg_tool_transform.ui
https://invent.kde.org/graphics/krita/-/commit/a85d83e532b79a2cb95cef95cabe2a5daa132d6f
diff --git a/benchmarks/KisLiquifyTransformWorkerBenchmark.cpp b/benchmarks/KisLiquifyTransformWorkerBenchmark.cpp
index e97aaa30218..110ee45d4b5 100644
--- a/benchmarks/KisLiquifyTransformWorkerBenchmark.cpp
+++ b/benchmarks/KisLiquifyTransformWorkerBenchmark.cpp
@@ -158,6 +158,29 @@ 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) {
+
+ for (int i = 0; i < bases.count(); i++) {
+ worker.translatePoints(bases[i], QPointF(50, 0), sigmas[i%sigmas.length()], useWashMode, flows[i%flows.length()]);
+ }
+
+ for (int i = 0; i < bases.count(); i++) {
+ 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);
+
+ 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()]);
+ }
+ }
+ }
} else if (operation == KisLiquifyTransformWorkerBenchmark::RunOnQImage) {
for (int i = 0; i < bases.count(); i++) {
worker.translatePoints(bases[i], QPointF(50, 0), sigmas[i%sigmas.length()], useWashMode, flows[i%flows.length()]);
@@ -291,6 +314,26 @@ void KisLiquifyTransformWorkerBenchmark::testPointsUndoWash()
testPoints(Undo, true);
}
+void KisLiquifyTransformWorkerBenchmark::testPointsRelaxSimpleBuildUp()
+{
+ testPoints(RelaxSimple, false);
+}
+
+void KisLiquifyTransformWorkerBenchmark::testPointsRelaxSimpleWash()
+{
+ testPoints(RelaxSimple, true);
+}
+
+void KisLiquifyTransformWorkerBenchmark::testPointsRelaxAffineBuildUp()
+{
+ testPoints(RelaxAffine, false);
+}
+
+void KisLiquifyTransformWorkerBenchmark::testPointsRelaxAffineWash()
+{
+ testPoints(RelaxAffine, true);
+}
+
void KisLiquifyTransformWorkerBenchmark::testRunOnDev()
{
testPoints(RunOnDev, true);
diff --git a/benchmarks/KisLiquifyTransformWorkerBenchmark.h b/benchmarks/KisLiquifyTransformWorkerBenchmark.h
index bc2cf180063..007d927b368 100644
--- a/benchmarks/KisLiquifyTransformWorkerBenchmark.h
+++ b/benchmarks/KisLiquifyTransformWorkerBenchmark.h
@@ -18,6 +18,8 @@ public:
Scale,
Rotate,
Undo,
+ RelaxSimple,
+ RelaxAffine,
RunOnQImage,
RunOnDev,
BenchmarkCopyConstructor,
@@ -42,6 +44,10 @@ private Q_SLOTS:
void testPointsUndoBuildUp();
void testPointsUndoWash();
+ void testPointsRelaxSimpleBuildUp();
+ void testPointsRelaxSimpleWash();
+ void testPointsRelaxAffineBuildUp();
+ void testPointsRelaxAffineWash();
void testRunOnDev();
void testRunOnQImage();
diff --git a/krita/pics/tool_transform/tool-transform-icons.qrc b/krita/pics/tool_transform/tool-transform-icons.qrc
index c512e37db99..065ca0f9f05 100644
--- a/krita/pics/tool_transform/tool-transform-icons.qrc
+++ b/krita/pics/tool_transform/tool-transform-icons.qrc
@@ -9,6 +9,8 @@
<file>dark_transform_icons_liquify_main.svg</file>
<file>dark_transform_icons_liquify_move.svg</file>
<file>dark_transform_icons_liquify_offset.svg</file>
+ <file>dark_transform_icons_liquify_relax_affine.svg</file>
+ <file>dark_transform_icons_liquify_relax.svg</file>
<file>dark_transform_icons_liquify_resize.svg</file>
<file>dark_transform_icons_liquify_rotate.svg</file>
<file>dark_transform_icons_liquify_rotateCCW.svg</file>
@@ -27,6 +29,8 @@
<file>light_transform_icons_liquify_main.svg</file>
<file>light_transform_icons_liquify_move.svg</file>
<file>light_transform_icons_liquify_offset.svg</file>
+ <file>light_transform_icons_liquify_relax_affine.svg</file>
+ <file>light_transform_icons_liquify_relax.svg</file>
<file>light_transform_icons_liquify_resize.svg</file>
<file>light_transform_icons_liquify_rotate.svg</file>
<file>light_transform_icons_liquify_rotateCCW.svg</file>
diff --git a/libs/image/kis_liquify_transform_worker.cpp b/libs/image/kis_liquify_transform_worker.cpp
index fec30b43a4c..4ee32aa3d4d 100644
--- a/libs/image/kis_liquify_transform_worker.cpp
+++ b/libs/image/kis_liquify_transform_worker.cpp
@@ -227,6 +227,239 @@ void KisLiquifyTransformWorker::undoPoints(const QPointF &base,
}
}
+namespace {
+
+struct PointUpdate
+{
+ int index = -1;
+ QPointF oldPosition;
+ QPointF newPosition;
+};
+
+bool solveLinear3x3(const qreal matrix[3][3], const qreal rhs[3], qreal result[3])
+{
+ 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;
+
+ 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;
+
+ for (int i = 0; i < 3; i++) {
+ result[i] = inverse[i][0] * rhs[0] + inverse[i][1] * rhs[1] + inverse[i][2] * rhs[2];
+ }
+
+ return true;
+}
+
+}
+
+void KisLiquifyTransformWorker::relaxPoints(const QPointF &base,
+ qreal amount,
+ qreal sigma)
+{
+ const qreal maxDistCoeff = 3.0;
+ const qreal maxDist = maxDistCoeff * sigma;
+
+ KIS_ASSERT_RECOVER_RETURN(m_d->originalPoints.size() ==
+ m_d->transformedPoints.size());
+
+ 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);
+
+ 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.
+ */
+ 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);
+ 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();
+
+ QPointF displacementSum;
+ int displacementCount = 0;
+
+ 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;
+
+ const int neighborIndex =
+ GridIterationTools::pointToIndex(QPoint(neighborCol, neighborRow),
+ m_d->gridSize);
+
+ displacementSum += m_d->transformedPoints[neighborIndex] -
+ m_d->originalPoints[neighborIndex];
+ displacementCount++;
+ }
+ }
+
+ if (!displacementCount) continue;
+
+ const QPointF oldDisplacement =
+ m_d->transformedPoints[index] - m_d->originalPoints[index];
+ const QPointF relaxedDisplacement = displacementSum / displacementCount;
+
+ const QPointF newPosition =
+ m_d->originalPoints[index] +
+ oldDisplacement * (1.0 - lambda) +
+ relaxedDisplacement * lambda;
+
+ updates << PointUpdate{index, m_d->transformedPoints[index], newPosition};
+ }
+
+ 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());
+
+ 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);
+
+ 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];
+
+ QPointF diff = m_d->transformedPoints[index] - base;
+ 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 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++;
+ }
+ }
+
+ qreal coeffX[3];
+ qreal coeffY[3];
+ if (sampleCount < 3 ||
+ !solveLinear3x3(matrix, rhsX, coeffX) ||
+ !solveLinear3x3(matrix, rhsY, coeffY)) {
+
+ continue;
+ }
+
+ const QPointF predictedPosition(coeffX[2], coeffY[2]);
+
+ const QPointF newPosition =
+ m_d->transformedPoints[index] * (1.0 - lambda) +
+ predictedPosition * lambda;
+
+ updates << PointUpdate{index, m_d->transformedPoints[index], newPosition};
+ }
+
+ 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);
+ }
+}
+
template <class ProcessOp>
void KisLiquifyTransformWorker::Private::
processTransformedPixelsBuildUp(ProcessOp op,
diff --git a/libs/image/kis_liquify_transform_worker.h b/libs/image/kis_liquify_transform_worker.h
index cb2471cd884..b52657f1395 100644
--- a/libs/image/kis_liquify_transform_worker.h
+++ b/libs/image/kis_liquify_transform_worker.h
@@ -60,6 +60,14 @@ public:
qreal amount,
qreal sigma);
+ void relaxPoints(const QPointF &base,
+ qreal amount,
+ qreal sigma);
+
+ void affineRelaxPoints(const QPointF &base,
+ qreal amount,
+ qreal sigma);
+
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 b5beea7d176..049782f9369 100644
--- a/libs/image/tests/kis_liquify_transform_worker_test.cpp
+++ b/libs/image/tests/kis_liquify_transform_worker_test.cpp
@@ -130,6 +130,54 @@ void KisLiquifyTransformWorkerTest::testPoints()
TestUtil::checkQImage(result, "liquify_transform_test", "liquify_dev", "unity");
}
+void KisLiquifyTransformWorkerTest::testRelaxPoints()
+{
+ 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];
+
+ worker.translatePoints(centerPoint, QPointF(30.0, 0.0), 8.0, false, 1.0);
+
+ const QPointF oldCenterDisplacement =
+ worker.transformedPoints()[centerIndex] - worker.originalPoints()[centerIndex];
+ worker.relaxPoints(worker.transformedPoints()[centerIndex], 1.0, 8.0);
+
+ const QPointF centerDisplacement =
+ worker.transformedPoints()[centerIndex] - worker.originalPoints()[centerIndex];
+
+ QVERIFY(centerDisplacement.x() < oldCenterDisplacement.x());
+ QVERIFY(centerDisplacement.x() > 0.0);
+ QVERIFY(qAbs(centerDisplacement.y()) < 1e-6);
+}
+
+void KisLiquifyTransformWorkerTest::testAffineRelaxPoints()
+{
+ 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];
+
+ worker.rotatePoints(centerPoint, M_PI / 2.0, 100.0, false, 1.0);
+ const QPointF affinePosition = worker.transformedPoints()[centerIndex];
+
+ worker.translatePoints(affinePosition, QPointF(25.0, 0.0), 8.0, false, 1.0);
+ const qreal corruptedDistance = kisDistance(worker.transformedPoints()[centerIndex], affinePosition);
+
+ worker.affineRelaxPoints(worker.transformedPoints()[centerIndex], 1.0, 8.0);
+ const qreal relaxedDistance = kisDistance(worker.transformedPoints()[centerIndex], affinePosition);
+
+ QVERIFY(relaxedDistance < corruptedDistance);
+}
+
void KisLiquifyTransformWorkerTest::testPointsQImage()
{
TestUtil::TestProgressBar bar;
diff --git a/libs/image/tests/kis_liquify_transform_worker_test.h b/libs/image/tests/kis_liquify_transform_worker_test.h
index 22543d74632..64392d6c60c 100644
--- a/libs/image/tests/kis_liquify_transform_worker_test.h
+++ b/libs/image/tests/kis_liquify_transform_worker_test.h
@@ -14,6 +14,8 @@ class KisLiquifyTransformWorkerTest : public QObject
Q_OBJECT
private Q_SLOTS:
void testPoints();
+ void testRelaxPoints();
+ void testAffineRelaxPoints();
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 3d2fcad239c..fcd906901c2 100644
--- a/plugins/tools/tool_transform2/kis_liquify_paintop.cpp
+++ b/plugins/tools/tool_transform2/kis_liquify_paintop.cpp
@@ -49,6 +49,8 @@ QPainterPath KisLiquifyPaintop::brushOutline(const KisLiquifyProperties &props,
switch (props.mode()) {
case KisLiquifyProperties::MOVE:
case KisLiquifyProperties::SCALE:
+ case KisLiquifyProperties::RELAX_SIMPLE:
+ case KisLiquifyProperties::RELAX_AFFINE:
break;
case KisLiquifyProperties::ROTATE: {
QPainterPath p;
@@ -132,8 +134,7 @@ KisSpacingInformation KisLiquifyPaintop::paintAt(const KisPaintInformation &pi)
const qreal spacing = m_d->props.spacing() * size;
const qreal reverseCoeff =
- m_d->props.mode() !=
- KisLiquifyProperties::UNDO &&
+ KisLiquifyProperties::supportsReverseDirection(m_d->props.mode()) &&
m_d->props.reverseDirection() ? -1.0 : 1.0;
const qreal amount = m_d->props.amountHasPressure() ?
pi.pressure() * reverseCoeff * m_d->props.amount():
@@ -173,6 +174,18 @@ KisSpacingInformation KisLiquifyPaintop::paintAt(const KisPaintInformation &pi)
amount,
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);
+
break;
case KisLiquifyProperties::N_MODES:
qFatal("Not supported mode");
diff --git a/plugins/tools/tool_transform2/kis_liquify_properties.cpp b/plugins/tools/tool_transform2/kis_liquify_properties.cpp
index 5a118cf6ae6..c0f98575b1b 100644
--- a/plugins/tools/tool_transform2/kis_liquify_properties.cpp
+++ b/plugins/tools/tool_transform2/kis_liquify_properties.cpp
@@ -57,6 +57,20 @@ bool KisLiquifyProperties::operator==(const KisLiquifyProperties &other) const
m_flow == other.m_flow;
}
+bool KisLiquifyProperties::supportsReverseDirection(LiquifyMode mode)
+{
+ return mode != UNDO &&
+ mode != RELAX_SIMPLE &&
+ mode != RELAX_AFFINE;
+}
+
+bool KisLiquifyProperties::supportsWashMode(LiquifyMode mode)
+{
+ return mode != UNDO &&
+ mode != RELAX_SIMPLE &&
+ mode != RELAX_AFFINE;
+}
+
QString liquifyModeString(KisLiquifyProperties::LiquifyMode mode)
{
QString result;
@@ -77,6 +91,12 @@ QString liquifyModeString(KisLiquifyProperties::LiquifyMode mode)
case KisLiquifyProperties::UNDO:
result = "Undo";
break;
+ case KisLiquifyProperties::RELAX_SIMPLE:
+ result = "RelaxSimple";
+ break;
+ case KisLiquifyProperties::RELAX_AFFINE:
+ result = "RelaxAffine";
+ break;
case KisLiquifyProperties::N_MODES:
qFatal("Unsupported mode");
}
@@ -84,6 +104,26 @@ QString liquifyModeString(KisLiquifyProperties::LiquifyMode mode)
return QString("LiquifyTool/%1").arg(result);
}
+qreal defaultAmountForMode(KisLiquifyProperties::LiquifyMode mode)
+{
+ switch (mode) {
+ case KisLiquifyProperties::RELAX_SIMPLE:
+ return 0.3;
+ case KisLiquifyProperties::RELAX_AFFINE:
+ return 1.0;
+ case KisLiquifyProperties::MOVE:
+ case KisLiquifyProperties::SCALE:
+ case KisLiquifyProperties::ROTATE:
+ case KisLiquifyProperties::OFFSET:
+ case KisLiquifyProperties::UNDO:
+ return 0.05;
+ case KisLiquifyProperties::N_MODES:
+ qFatal("Unsupported mode");
+ }
+
+ return 0.05;
+}
+
void KisLiquifyProperties::saveMode() const
{
KConfigGroup cfg =
@@ -108,7 +148,7 @@ void KisLiquifyProperties::loadMode()
KSharedConfig::openConfig()->group(liquifyModeString(m_mode));
m_size = cfg.readEntry("size", m_size);
- m_amount = cfg.readEntry("amount", m_amount);
+ m_amount = cfg.readEntry("amount", defaultAmountForMode(m_mode));
m_spacing = cfg.readEntry("spacing", m_spacing);
m_sizeHasPressure = cfg.readEntry("sizeHasPressure", m_sizeHasPressure);
m_amountHasPressure = cfg.readEntry("amountHasPressure", m_amountHasPressure);
diff --git a/plugins/tools/tool_transform2/kis_liquify_properties.h b/plugins/tools/tool_transform2/kis_liquify_properties.h
index 5e3a2b15b83..d14c98bb74a 100644
--- a/plugins/tools/tool_transform2/kis_liquify_properties.h
+++ b/plugins/tools/tool_transform2/kis_liquify_properties.h
@@ -24,6 +24,8 @@ public:
ROTATE,
OFFSET,
UNDO,
+ RELAX_SIMPLE,
+ RELAX_AFFINE,
N_MODES
};
@@ -55,6 +57,9 @@ public:
m_mode = value;
}
+ static bool supportsReverseDirection(LiquifyMode mode);
+ static bool supportsWashMode(LiquifyMode mode);
+
qreal size() const {
return m_size;
}
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 9a1183f5c39..a70dbb801d3 100644
--- a/plugins/tools/tool_transform2/kis_tool_transform_config_widget.cpp
+++ b/plugins/tools/tool_transform2/kis_tool_transform_config_widget.cpp
@@ -265,11 +265,15 @@ 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()));
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);
connect(liquifyModeMapper, SIGNAL(mapped(int)), SLOT(slotLiquifyModeChanged(int)));
connect(liquifyModeMapper, SIGNAL(mapped(int)), SLOT(notifyEditingFinished()));
@@ -278,6 +282,14 @@ 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"));
// Connect all edit boxes to the Editing Finished signal
connect(densityBox, SIGNAL(editingFinished()), this, SLOT(notifyEditingFinished()));
@@ -350,6 +362,8 @@ 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"));
@@ -394,10 +408,8 @@ void KisToolTransformConfigWidget::updateLiquifyControls()
KisLiquifyProperties::LiquifyMode mode =
static_cast<KisLiquifyProperties::LiquifyMode>(props->mode());
- bool canInverseDirection =
- mode != KisLiquifyProperties::UNDO;
-
- bool canUseWashMode = mode != KisLiquifyProperties::UNDO;
+ bool canInverseDirection = KisLiquifyProperties::supportsReverseDirection(mode);
+ bool canUseWashMode = KisLiquifyProperties::supportsWashMode(mode);
liquifyReverseDirectionChk->setEnabled(canInverseDirection);
liquifyFlowSlider->setEnabled(canUseWashMode && useWashMode);
@@ -657,6 +669,12 @@ 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);
+ break;
case KisLiquifyProperties::N_MODES:
qFatal("Unsupported mode");
}
diff --git a/plugins/tools/tool_transform2/wdg_tool_transform.ui b/plugins/tools/tool_transform2/wdg_tool_transform.ui
index 4dbce43450a..4247aafcbea 100644
--- a/plugins/tools/tool_transform2/wdg_tool_transform.ui
+++ b/plugins/tools/tool_transform2/wdg_tool_transform.ui
@@ -1940,6 +1940,50 @@
</property>
</widget>
</item>
+ <item>
+ <widget class="QToolButton" name="liquifyRelaxSimple">
+ <property name="sizePolicy">
+ <sizepolicy hsizetype="Expanding" vsizetype="Fixed">
+ <horstretch>0</horstretch>
+ <verstretch>0</verstretch>
+ </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>
+ </property>
+ <property name="checkable">
+ <bool>true</bool>
+ </property>
+ <property name="autoExclusive">
+ <bool>true</bool>
+ </property>
+ <property name="autoRaise">
+ <bool>true</bool>
+ </property>
+ </widget>
+ </item>
</layout>
</widget>
</item>