[calligra/krita-chili-kazakov] krita: First ready-for-testing version of the Transform Mask functionality

Dmitry Kazakov dimula73 at gmail.com
Mon Oct 27 16:32:12 UTC 2014


Git commit 2d16e0611550095db4ad674c1008ffb2d0e0c2a6 by Dmitry Kazakov.
Committed on 27/10/2014 at 15:31.
Pushed by dkazakov into branch 'krita-chili-kazakov'.

First ready-for-testing version of the Transform Mask functionality

1) Just create a Transform Mask and edit it with usual Transform Tool
2) All the transformation modes are supported.
2.1) Affine transforms (Free and Perspective) have dynamic preview. That
     is you can paint on a layer and the mask will be updated dynamically
2.1) The other types are updated "statically", that is after 3 sec after
     the last change in the original layer.

Question for testers: isn't this 3 sec delay too long? Probably we should
                      make it shorter?

3) The transformation is written into the mask after you click Apply button.
4) The quality of the affine preview and the final transform (after 3 sec
   delay) differs a bit. That is expected.
5) You can also apply a transform mask onto a Clone Layer, which will
   allow you painting in 3D-perspective dynamically.

Please test it in krita-chili-kazakov branch :)

CCMAIL:kimageshop at kde.org

M  +3    -0    krita/image/CMakeLists.txt
M  +4    -4    krita/image/kis_async_merger.cpp
M  +20   -14   krita/image/kis_base_rects_walker.h
A  +48   -0    krita/image/kis_cached_paint_device.h     [License: GPL (v2+)]
M  +4    -1    krita/image/kis_filter_mask.cpp
M  +2    -1    krita/image/kis_filter_mask.h
M  +19   -5    krita/image/kis_layer.cc
M  +3    -2    krita/image/kis_layer.h
M  +16   -35   krita/image/kis_mask.cc
M  +3    -2    krita/image/kis_mask.h
M  +88   -17   krita/image/kis_perspectivetransform_worker.cpp
M  +16   -5    krita/image/kis_perspectivetransform_worker.h
A  +41   -0    krita/image/kis_recalculate_transform_mask_job.cpp     [License: GPL (v2+)]
A  +38   -0    krita/image/kis_recalculate_transform_mask_job.h     [License: GPL (v2+)]
A  +253  -0    krita/image/kis_transform_mask.cpp     [License: GPL (v2+)]
C  +27   -19   krita/image/kis_transform_mask.h [from: krita/image/kis_filter_mask.h - 062% similarity]
A  +76   -0    krita/image/kis_transform_mask_params_interface.cpp     [License: GPL (v2+)]
A  +59   -0    krita/image/kis_transform_mask_params_interface.h     [License: GPL (v2+)]
A  +28   -0    krita/image/kis_transform_params.cpp     [License: GPL (v2+)]
A  +30   -0    krita/image/kis_transform_params.h     [License: GPL (v2+)]
M  +6    -1    krita/image/kis_transform_worker.cc
M  +1    -0    krita/image/kis_transform_worker.h
M  +4    -1    krita/image/kis_transparency_mask.cc
M  +2    -1    krita/image/kis_transparency_mask.h
M  +11   -3    krita/image/kis_types.h
M  +8    -0    krita/image/tests/CMakeLists.txt
M  +3    -2    krita/image/tests/kis_filter_mask_test.cpp
M  +1    -1    krita/image/tests/kis_mask_test.cpp
M  +3    -3    krita/image/tests/kis_paint_layer_test.cpp
A  +59   -0    krita/image/tests/kis_transform_mask_test.cpp     [License: GPL (v2+)]
A  +31   -0    krita/image/tests/kis_transform_mask_test.h     [License: GPL (v2+)]
M  +32   -0    krita/image/tests/kis_transform_worker_test.cpp
M  +2    -0    krita/image/tests/kis_transform_worker_test.h
M  +5    -3    krita/image/tests/kis_transparency_mask_test.cpp
M  +3    -0    krita/plugins/extensions/dockers/defaultdockers/kis_layer_box.cpp
M  +1    -0    krita/plugins/tools/tool_transform2/CMakeLists.txt
M  +3    -3    krita/plugins/tools/tool_transform2/kis_free_transform_strategy.cpp
M  +36   -3    krita/plugins/tools/tool_transform2/kis_tool_transform.cc
M  +2    -0    krita/plugins/tools/tool_transform2/kis_tool_transform.h
M  +2    -2    krita/plugins/tools/tool_transform2/kis_tool_transform_config_widget.cpp
A  +72   -0    krita/plugins/tools/tool_transform2/kis_transform_mask_adapter.cpp     [License: GPL (v2+)]
A  +47   -0    krita/plugins/tools/tool_transform2/kis_transform_mask_adapter.h     [License: GPL (v2+)]
M  +104  -0    krita/plugins/tools/tool_transform2/kis_transform_utils.cpp
M  +11   -0    krita/plugins/tools/tool_transform2/kis_transform_utils.h
M  +85   -112  krita/plugins/tools/tool_transform2/strokes/transform_stroke_strategy.cpp
M  +1    -0    krita/plugins/tools/tool_transform2/strokes/transform_stroke_strategy.h
M  +1    -1    krita/plugins/tools/tool_transform2/transform_transaction_properties.h
M  +2    -2    krita/sdk/tests/testutil.h
M  +14   -5    krita/ui/kis_mask_manager.cc
M  +2    -1    krita/ui/kis_mask_manager.h
M  +5    -0    krita/ui/kis_node_manager.cpp
M  +1    -1    krita/ui/widgets/kis_scratch_pad.cpp

http://commits.kde.org/calligra/2d16e0611550095db4ad674c1008ffb2d0e0c2a6

diff --git a/krita/image/CMakeLists.txt b/krita/image/CMakeLists.txt
index d84ace3..98f318f 100644
--- a/krita/image/CMakeLists.txt
+++ b/krita/image/CMakeLists.txt
@@ -132,6 +132,9 @@ set(kritaimage_LIB_SRCS
    kis_fill_painter.cc
    kis_filter_mask.cpp
    kis_filter_strategy.cc
+   kis_transform_mask.cpp
+   kis_transform_mask_params_interface.cpp
+   kis_recalculate_transform_mask_job.cpp
    kis_gradient_painter.cc
    kis_iterator_ng.cpp
    kis_async_merger.cpp
diff --git a/krita/image/kis_async_merger.cpp b/krita/image/kis_async_merger.cpp
index d2e4445..3211254 100644
--- a/krita/image/kis_async_merger.cpp
+++ b/krita/image/kis_async_merger.cpp
@@ -208,7 +208,7 @@ void KisAsyncMerger::startMerge(KisBaseRectsWalker &walker, bool notifyClones) {
                                                      m_currentProjection,
                                                      walker.cropRect());
             currentNode->accept(originalVisitor);
-            currentNode->updateProjection(applyRect);
+            currentNode->updateProjection(applyRect, KisMergeWalker::convertPositionToFilthy(item.m_position));
 
             continue;
         }
@@ -224,18 +224,18 @@ void KisAsyncMerger::startMerge(KisBaseRectsWalker &walker, bool notifyClones) {
         if(item.m_position & KisMergeWalker::N_FILTHY) {
             DEBUG_NODE_ACTION("Updating", "N_FILTHY", currentNode, applyRect);
             currentNode->accept(originalVisitor);
-            currentNode->updateProjection(applyRect);
+            currentNode->updateProjection(applyRect, KisMergeWalker::convertPositionToFilthy(item.m_position));
         }
         else if(item.m_position & KisMergeWalker::N_ABOVE_FILTHY) {
             DEBUG_NODE_ACTION("Updating", "N_ABOVE_FILTHY", currentNode, applyRect);
             if(dependOnLowerNodes(currentNode)) {
                 currentNode->accept(originalVisitor);
-                currentNode->updateProjection(applyRect);
+                currentNode->updateProjection(applyRect, KisMergeWalker::convertPositionToFilthy(item.m_position));
             }
         }
         else if(item.m_position & KisMergeWalker::N_FILTHY_PROJECTION) {
             DEBUG_NODE_ACTION("Updating", "N_FILTHY_PROJECTION", currentNode, applyRect);
-            currentNode->updateProjection(applyRect);
+            currentNode->updateProjection(applyRect, KisMergeWalker::convertPositionToFilthy(item.m_position));
         }
         else /*if(item.m_position & KisMergeWalker::N_BELOW_FILTHY)*/ {
             DEBUG_NODE_ACTION("Updating", "N_BELOW_FILTHY", currentNode, applyRect);
diff --git a/krita/image/kis_base_rects_walker.h b/krita/image/kis_base_rects_walker.h
index 293f12e..d9c34f1 100644
--- a/krita/image/kis_base_rects_walker.h
+++ b/krita/image/kis_base_rects_walker.h
@@ -58,7 +58,20 @@ public:
     };
 
     #define GRAPH_POSITION_MASK     0x07
-    #define POSITION_TO_FILTHY_MASK 0xF8
+
+    static inline KisNode::PositionToFilthy convertPositionToFilthy(NodePosition position) {
+        static const int positionToFilthyMask =
+            N_ABOVE_FILTHY |
+            N_FILTHY_PROJECTION |
+            N_FILTHY |
+            N_BELOW_FILTHY;
+
+        qint32 positionToFilthy = position & N_EXTRA ? N_FILTHY : position & positionToFilthyMask;
+        // We do not use N_FILTHY_ORIGINAL yet, so...
+        Q_ASSERT(positionToFilthy);
+
+        return static_cast<KisNode::PositionToFilthy>(positionToFilthy);
+    }
 
     struct CloneNotification {
         CloneNotification() {}
@@ -189,13 +202,6 @@ protected:
     virtual void startTrip(KisNodeSP startWith) = 0;
 
 protected:
-    static inline KisNode::PositionToFilthy getPositionToFilthy(qint32 position) {
-        qint32 positionToFilthy = position & POSITION_TO_FILTHY_MASK;
-        // We do not use N_FILTHY_ORIGINAL yet, so...
-        Q_ASSERT(!(positionToFilthy & N_FILTHY_ORIGINAL));
-
-        return static_cast<KisNode::PositionToFilthy>(positionToFilthy);
-    }
 
     static inline qint32 getGraphPosition(qint32 position) {
         return position & GRAPH_POSITION_MASK;
@@ -270,7 +276,7 @@ protected:
         if(!isLayer(node)) return;
 
         QRect currentChangeRect = node->changeRect(m_resultChangeRect,
-                                                   getPositionToFilthy(position));
+                                                   convertPositionToFilthy(position));
         currentChangeRect = cropThisRect(currentChangeRect);
 
         if(!m_changeRectVaries)
@@ -279,7 +285,7 @@ protected:
         m_resultChangeRect = currentChangeRect;
 
         m_resultUncroppedChangeRect = node->changeRect(m_resultUncroppedChangeRect,
-                                                       getPositionToFilthy(position));
+                                                       convertPositionToFilthy(position));
         registerCloneNotification(node, position);
     }
 
@@ -320,10 +326,10 @@ protected:
             //else /* Why push empty rect? */;
 
             m_resultAccessRect |= node->accessRect(m_lastNeedRect,
-                                                   getPositionToFilthy(position));
+                                                   convertPositionToFilthy(position));
 
             m_lastNeedRect = node->needRect(m_lastNeedRect,
-                                            getPositionToFilthy(position));
+                                            convertPositionToFilthy(position));
             m_lastNeedRect = cropThisRect(m_lastNeedRect);
             m_childNeedRect = m_lastNeedRect;
         }
@@ -332,10 +338,10 @@ protected:
                 pushJob(node, position, m_lastNeedRect);
 
                 m_resultAccessRect |= node->accessRect(m_lastNeedRect,
-                                                       getPositionToFilthy(position));
+                                                       convertPositionToFilthy(position));
 
                 m_lastNeedRect = node->needRect(m_lastNeedRect,
-                                                getPositionToFilthy(position));
+                                                convertPositionToFilthy(position));
                 m_lastNeedRect = cropThisRect(m_lastNeedRect);
             }
         }
diff --git a/krita/image/kis_cached_paint_device.h b/krita/image/kis_cached_paint_device.h
new file mode 100644
index 0000000..8ee478e
--- /dev/null
+++ b/krita/image/kis_cached_paint_device.h
@@ -0,0 +1,48 @@
+/*
+ *  Copyright (c) 2014 Dmitry Kazakov <dimula73 at gmail.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef __KIS_CACHED_PAINT_DEVICE_H
+#define __KIS_CACHED_PAINT_DEVICE_H
+
+#include "tiles3/kis_lockless_stack.h"
+
+
+class KisCachedPaintDevice
+{
+public:
+    KisPaintDeviceSP getDevice(KisPaintDeviceSP prototype) {
+        KisPaintDeviceSP device;
+
+        if(!m_stack.pop(device)) {
+            device = new KisPaintDevice(prototype->colorSpace());
+        }
+
+        device->prepareClone(prototype);
+        return device;
+    }
+
+    void putDevice(KisPaintDeviceSP device) {
+        device->clear();
+        m_stack.push(device);
+    }
+
+private:
+    KisLocklessStack<KisPaintDeviceSP> m_stack;
+};
+
+#endif /* __KIS_CACHED_PAINT_DEVICE_H */
diff --git a/krita/image/kis_filter_mask.cpp b/krita/image/kis_filter_mask.cpp
index 1e43dec..a5e81f5 100644
--- a/krita/image/kis_filter_mask.cpp
+++ b/krita/image/kis_filter_mask.cpp
@@ -69,8 +69,11 @@ void KisFilterMask::setFilter(KisFilterConfiguration * filterConfig)
 
 QRect KisFilterMask::decorateRect(KisPaintDeviceSP &src,
                                   KisPaintDeviceSP &dst,
-                                  const QRect & rc) const
+                                  const QRect & rc,
+                                  PositionToFilthy parentPos) const
 {
+    Q_UNUSED(parentPos);
+
     KisSafeFilterConfigurationSP filterConfig = filter();
 
     Q_ASSERT(nodeProgressProxy());
diff --git a/krita/image/kis_filter_mask.h b/krita/image/kis_filter_mask.h
index e3c093d..4095c7b 100644
--- a/krita/image/kis_filter_mask.h
+++ b/krita/image/kis_filter_mask.h
@@ -60,7 +60,8 @@ public:
 
     QRect decorateRect(KisPaintDeviceSP &src,
                        KisPaintDeviceSP &dst,
-                       const QRect & rc) const;
+                       const QRect & rc,
+                       PositionToFilthy parentPos) const;
 
     QRect changeRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const;
     QRect needRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const;
diff --git a/krita/image/kis_layer.cc b/krita/image/kis_layer.cc
index 83aba8d..89e52cb 100644
--- a/krita/image/kis_layer.cc
+++ b/krita/image/kis_layer.cc
@@ -389,7 +389,8 @@ QRect KisLayer::masksNeedRect(const QList<KisEffectMaskSP> &masks,
 
 QRect KisLayer::applyMasks(const KisPaintDeviceSP source,
                            const KisPaintDeviceSP destination,
-                           const QRect &requestedRect) const
+                           const QRect &requestedRect,
+                           PositionToFilthy pos) const
 {
     Q_ASSERT(source);
     Q_ASSERT(destination);
@@ -434,7 +435,11 @@ QRect KisLayer::applyMasks(const KisPaintDeviceSP source,
             }
 
             foreach(const KisEffectMaskSP& mask, masks) {
-                mask->apply(destination, applyRects.pop());
+                const QRect maskApplyRect = applyRects.pop();
+                const QRect maskNeedRect =
+                    applyRects.isEmpty() ? needRect : applyRects.top();
+
+                mask->apply(destination, maskApplyRect, maskNeedRect, pos);
             }
             Q_ASSERT(applyRects.isEmpty());
         } else {
@@ -445,10 +450,19 @@ QRect KisLayer::applyMasks(const KisPaintDeviceSP source,
              */
 
             KisPaintDeviceSP tempDevice = new KisPaintDevice(colorSpace());
+            tempDevice->prepareClone(source);
             copyOriginalToProjection(source, tempDevice, needRect);
 
+            QRect maskApplyRect = applyRects.pop();
+            QRect maskNeedRect = needRect;
+
             foreach(const KisEffectMaskSP& mask, masks) {
-                mask->apply(tempDevice, applyRects.pop());
+                mask->apply(tempDevice, maskApplyRect, maskNeedRect, pos);
+
+                if (!applyRects.isEmpty()) {
+                    maskNeedRect = maskApplyRect;
+                    maskApplyRect = applyRects.pop();
+                }
             }
             Q_ASSERT(applyRects.isEmpty());
 
@@ -461,7 +475,7 @@ QRect KisLayer::applyMasks(const KisPaintDeviceSP source,
     return changeRect;
 }
 
-QRect KisLayer::updateProjection(const QRect& rect)
+QRect KisLayer::updateProjection(const QRect& rect, PositionToFilthy pos)
 {
     QRect updatedRect = rect;
     KisPaintDeviceSP originalDevice = original();
@@ -478,7 +492,7 @@ QRect KisLayer::updateProjection(const QRect& rect)
                 m_d->safeProjection.getDeviceLazy(originalDevice);
 
             updatedRect = applyMasks(originalDevice, projection,
-                                     updatedRect);
+                                     updatedRect, pos);
         }
     }
 
diff --git a/krita/image/kis_layer.h b/krita/image/kis_layer.h
index eb36e26..6ab0d52 100644
--- a/krita/image/kis_layer.h
+++ b/krita/image/kis_layer.h
@@ -83,7 +83,7 @@ public:
      * Ask the layer to assemble its data & apply all the effect masks
      * to it.
      */
-    virtual QRect updateProjection(const QRect& rect);
+    virtual QRect updateProjection(const QRect& rect, PositionToFilthy pos);
 
     virtual bool needProjection() const;
 
@@ -275,7 +275,8 @@ protected:
 
     QRect applyMasks(const KisPaintDeviceSP source,
                      const KisPaintDeviceSP destination,
-                     const QRect &requestedRect) const;
+                     const QRect &requestedRect,
+                     PositionToFilthy pos) const;
 
 private:
     struct Private;
diff --git a/krita/image/kis_mask.cc b/krita/image/kis_mask.cc
index 7145c75..3e7b7bc 100644
--- a/krita/image/kis_mask.cc
+++ b/krita/image/kis_mask.cc
@@ -38,35 +38,14 @@
 #include "kis_image.h"
 #include "kis_layer.h"
 
-#include "tiles3/kis_lockless_stack.h"
+#include "kis_cached_paint_device.h"
 
-struct KisMask::Private {
-    class CachedPaintDevice {
-    public:
-        KisPaintDeviceSP getDevice(KisPaintDeviceSP prototype) {
-            KisPaintDeviceSP device;
-
-            if(!m_stack.pop(device)) {
-                device = new KisPaintDevice(prototype->colorSpace());
-            }
-
-            device->prepareClone(prototype);
-            return device;
-        }
-
-        void putDevice(KisPaintDeviceSP device) {
-            device->clear();
-            m_stack.push(device);
-        }
-
-    private:
-        KisLocklessStack<KisPaintDeviceSP> m_stack;
-    };
 
+struct KisMask::Private {
     Private(KisMask *_q) : q(_q) {}
 
     mutable KisSelectionSP selection;
-    CachedPaintDevice paintDeviceCache;
+    KisCachedPaintDevice paintDeviceCache;
     KisMask *q;
 
     /**
@@ -231,30 +210,32 @@ void KisMask::select(const QRect & rc, quint8 selectedness)
 
 QRect KisMask::decorateRect(KisPaintDeviceSP &src,
                             KisPaintDeviceSP &dst,
-                            const QRect & rc) const
+                            const QRect & rc,
+                            PositionToFilthy parentPos) const
 {
     Q_UNUSED(src);
     Q_UNUSED(dst);
+    Q_UNUSED(parentPos);
     Q_ASSERT_X(0, "KisMask::decorateRect", "Should be overridden by successors");
     return rc;
 }
 
-void KisMask::apply(KisPaintDeviceSP projection, const QRect & rc) const
+void KisMask::apply(KisPaintDeviceSP projection, const QRect &applyRect, const QRect &needRect, PositionToFilthy parentPos) const
 {
     if (selection()) {
 
-        m_d->selection->updateProjection(rc);
+        m_d->selection->updateProjection(applyRect);
 
-        if(!extent().intersects(rc))
+        if(!extent().intersects(applyRect))
             return;
 
         KisPaintDeviceSP cacheDevice = m_d->paintDeviceCache.getDevice(projection);
 
-        QRect updatedRect = decorateRect(projection, cacheDevice, rc);
+        QRect updatedRect = decorateRect(projection, cacheDevice, applyRect, parentPos);
 
         KisPainter gc(projection);
-        gc.setCompositeOp(compositeOp());
-        gc.setOpacity(opacity());
+        // masks don't have any compositioning
+        gc.setCompositeOp(COMPOSITE_COPY);
         gc.setSelection(m_d->selection);
         gc.bitBlt(updatedRect.topLeft(), cacheDevice, updatedRect);
 
@@ -262,11 +243,11 @@ void KisMask::apply(KisPaintDeviceSP projection, const QRect & rc) const
 
     } else {
         KisPaintDeviceSP cacheDevice = m_d->paintDeviceCache.getDevice(projection);
-        cacheDevice->makeCloneFromRough(projection, rc);
-        projection->clear(rc);
 
-        // FIXME: how about opacity and compositeOp?
-        decorateRect(cacheDevice, projection, rc);
+        cacheDevice->makeCloneFromRough(projection, needRect);
+        projection->clear(needRect);
+
+        decorateRect(cacheDevice, projection, applyRect, parentPos);
 
         m_d->paintDeviceCache.putDevice(cacheDevice);
     }
diff --git a/krita/image/kis_mask.h b/krita/image/kis_mask.h
index 561c72b..4fb7ef5 100644
--- a/krita/image/kis_mask.h
+++ b/krita/image/kis_mask.h
@@ -178,10 +178,11 @@ protected:
      * Apply the effect the projection using the mask as a selection.
      * Made public in KisEffectMask
      */
-    virtual void apply(KisPaintDeviceSP projection, const QRect & rc) const;
+    void apply(KisPaintDeviceSP projection, const QRect & applyRect, const QRect & needRect, PositionToFilthy parentPos) const;
     virtual QRect decorateRect(KisPaintDeviceSP &src,
                                KisPaintDeviceSP &dst,
-                               const QRect & rc) const;
+                               const QRect & rc,
+                               PositionToFilthy parentPos) const;
 
 private:
 
diff --git a/krita/image/kis_perspectivetransform_worker.cpp b/krita/image/kis_perspectivetransform_worker.cpp
index 7839099..8d0a98b 100644
--- a/krita/image/kis_perspectivetransform_worker.cpp
+++ b/krita/image/kis_perspectivetransform_worker.cpp
@@ -24,6 +24,12 @@
 #include <QMatrix4x4>
 #include <QTransform>
 #include <QVector3D>
+#include <QPolygonF>
+
+#include <KoProgressUpdater.h>
+#include <KoUpdater.h>
+#include <KoColor.h>
+#include <KoCompositeOpRegistry.h>
 
 #include "kis_paint_device.h"
 #include "kis_perspective_math.h"
@@ -33,10 +39,8 @@
 #include <kis_iterator_ng.h>
 #include "krita_utils.h"
 #include "kis_progress_update_helper.h"
+#include "kis_painter.h"
 
-#include <KoProgressUpdater.h>
-#include <KoUpdater.h>
-#include <KoColor.h>
 
 KisPerspectiveTransformWorker::KisPerspectiveTransformWorker(KisPaintDeviceSP dev, QPointF center, double aX, double aY, double distance, KoUpdaterPtr progress)
         : m_dev(dev), m_progressUpdater(progress)
@@ -60,21 +64,38 @@ KisPerspectiveTransformWorker::KisPerspectiveTransformWorker(KisPaintDeviceSP de
     init(transform);
 }
 
-void KisPerspectiveTransformWorker::init(const QTransform &transform)
+void KisPerspectiveTransformWorker::fillParams(const QRectF &srcRect,
+                                               const QRect &dstBaseClipRect,
+                                               QRegion *dstRegion,
+                                               QPolygonF *dstClipPolygon)
 {
-    QPolygon bounds(m_dev->exactBounds());
-    QPolygon newBounds = transform.map(bounds);
+    QPolygonF bounds = srcRect;
+    QPolygonF newBounds = m_forwardTransform.map(bounds);
+
+    newBounds = newBounds.intersected(QRectF(dstBaseClipRect));
+
+    QPainterPath path;
+    path.addPolygon(newBounds);
+    *dstRegion = KritaUtils::splitPath(path);
+    *dstClipPolygon = newBounds;
+}
 
+void KisPerspectiveTransformWorker::init(const QTransform &transform)
+{
     m_isIdentity = transform.isIdentity();
 
-    if (!m_isIdentity && transform.isInvertible()) {
-        m_newTransform = transform.inverted();
+    m_forwardTransform = transform;
+    m_backwardTransform = transform.inverted();
+
+    if (m_dev) {
         m_srcRect = m_dev->exactBounds();
-        newBounds = newBounds.intersected(m_dev->defaultBounds()->bounds());
 
-        QPainterPath path;
-        path.addPolygon(newBounds);
-        m_dstRegion = KritaUtils::splitPath(path);
+        QPolygonF dstClipPolygonUnused;
+
+        fillParams(m_srcRect,
+                   m_dev->defaultBounds()->bounds(),
+                   &m_dstRegion,
+                   &dstClipPolygonUnused);
     }
 }
 
@@ -82,11 +103,16 @@ KisPerspectiveTransformWorker::~KisPerspectiveTransformWorker()
 {
 }
 
+void KisPerspectiveTransformWorker::setForwardTransform(const QTransform &transform)
+{
+    init(transform);
+}
+
 void KisPerspectiveTransformWorker::run()
 {
-    KisProgressUpdateHelper progressHelper(m_progressUpdater, 100, m_dstRegion.rectCount());
-    if (m_isIdentity) return;
+    KIS_ASSERT_RECOVER_RETURN(m_dev);
 
+    if (m_isIdentity) return;
 
     KisPaintDeviceSP cloneDevice = new KisPaintDevice(*m_dev.data());
 
@@ -94,7 +120,11 @@ void KisPerspectiveTransformWorker::run()
     // shared with cloneDevice
     m_dev->clear();
 
-    KisRandomSubAccessorSP srcAcc = cloneDevice->createRandomSubAccessor();
+    KIS_ASSERT_RECOVER_NOOP(!m_isIdentity);
+
+    KisProgressUpdateHelper progressHelper(m_progressUpdater, 100, m_dstRegion.rectCount());
+
+    KisRandomSubAccessorSP srcAcc = m_dev->createRandomSubAccessor();
     KisRandomAccessorSP accessor = m_dev->createRandomAccessorNG(0, 0);
 
     foreach(const QRect &rect, m_dstRegion.rects()) {
@@ -102,19 +132,60 @@ void KisPerspectiveTransformWorker::run()
             for (int x = rect.x(); x < rect.x() + rect.width(); ++x) {
 
                 QPointF dstPoint(x, y);
-                QPointF srcPoint = m_newTransform.map(dstPoint);
+                QPointF srcPoint = m_backwardTransform.map(dstPoint);
 
                 if (m_srcRect.contains(srcPoint)) {
                     accessor->moveTo(dstPoint.x(), dstPoint.y());
                     srcAcc->moveTo(srcPoint.x(), srcPoint.y());
                     srcAcc->sampledOldRawData(accessor->rawData());
                 }
+            }
+        }
+        progressHelper.step();
+    }
+}
+
+void KisPerspectiveTransformWorker::runPartialDst(KisPaintDeviceSP srcDev,
+                                                  KisPaintDeviceSP dstDev,
+                                                  const QRect &dstRect)
+{
+    if (m_isIdentity) {
+        KisPainter gc(dstDev);
+        gc.setCompositeOp(COMPOSITE_COPY);
+        gc.bitBltOldData(dstRect.topLeft(), srcDev, dstRect);
+        return;
+    }
 
+    QRectF srcClipRect = srcDev->defaultBounds()->bounds();
 
+    KisProgressUpdateHelper progressHelper(m_progressUpdater, 100, dstRect.height());
+
+    KisRandomSubAccessorSP srcAcc = srcDev->createRandomSubAccessor();
+    KisRandomAccessorSP accessor = dstDev->createRandomAccessorNG(0, 0);
+
+    for (int y = dstRect.y(); y < dstRect.y() + dstRect.height(); ++y) {
+        for (int x = dstRect.x(); x < dstRect.x() + dstRect.width(); ++x) {
+
+            QPointF dstPoint(x, y);
+            QPointF srcPoint = m_backwardTransform.map(dstPoint);
+
+            if (srcClipRect.contains(srcPoint)) {
+                accessor->moveTo(dstPoint.x(), dstPoint.y());
+                srcAcc->moveTo(srcPoint.x(), srcPoint.y());
+                srcAcc->sampledOldRawData(accessor->rawData());
             }
         }
         progressHelper.step();
     }
+
 }
 
-#include "kis_perspectivetransform_worker.moc"
+QTransform KisPerspectiveTransformWorker::forwardTransform() const
+{
+    return m_forwardTransform;
+}
+
+QTransform KisPerspectiveTransformWorker::backwardTransform() const
+{
+    return m_backwardTransform;
+}
diff --git a/krita/image/kis_perspectivetransform_worker.h b/krita/image/kis_perspectivetransform_worker.h
index 9d78295..d0455be 100644
--- a/krita/image/kis_perspectivetransform_worker.h
+++ b/krita/image/kis_perspectivetransform_worker.h
@@ -30,11 +30,8 @@
 #include <KoUpdater.h>
 
 
-class KRITAIMAGE_EXPORT KisPerspectiveTransformWorker : public QObject
+class KRITAIMAGE_EXPORT KisPerspectiveTransformWorker
 {
-
-    Q_OBJECT
-
 public:
     KisPerspectiveTransformWorker(KisPaintDeviceSP dev, QPointF center, double aX, double aY, double distance, KoUpdaterPtr progress);
     KisPerspectiveTransformWorker(KisPaintDeviceSP dev, const QTransform &transform, KoUpdaterPtr progress);
@@ -42,16 +39,30 @@ public:
     ~KisPerspectiveTransformWorker();
 
     void run();
+    void runPartialDst(KisPaintDeviceSP srcDev,
+                       KisPaintDeviceSP dstDev,
+                       const QRect &dstRect);
+
+    void setForwardTransform(const QTransform &transform);
+
+    QTransform forwardTransform() const;
+    QTransform backwardTransform() const;
 
 private:
     void init(const QTransform &transform);
 
+    void fillParams(const QRectF &srcRect,
+                    const QRect &dstBaseClipRect,
+                    QRegion *dstRegion,
+                    QPolygonF *dstClipPolygon);
+
 private:
     KisPaintDeviceSP m_dev;
     KoUpdaterPtr m_progressUpdater;
     QRegion m_dstRegion;
     QRectF m_srcRect;
-    QTransform m_newTransform;
+    QTransform m_backwardTransform;
+    QTransform m_forwardTransform;
     bool m_isIdentity;
 };
 
diff --git a/krita/image/kis_recalculate_transform_mask_job.cpp b/krita/image/kis_recalculate_transform_mask_job.cpp
new file mode 100644
index 0000000..55d4967
--- /dev/null
+++ b/krita/image/kis_recalculate_transform_mask_job.cpp
@@ -0,0 +1,41 @@
+/*
+ *  Copyright (c) 2014 Dmitry Kazakov <dimula73 at gmail.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "kis_recalculate_transform_mask_job.h"
+
+#include "kis_transform_mask.h"
+
+
+KisRecalculateTransformMaskJob::KisRecalculateTransformMaskJob(KisTransformMaskSP mask)
+    : m_mask(mask)
+{
+}
+
+bool KisRecalculateTransformMaskJob::overrides(const KisSpontaneousJob *_otherJob)
+{
+    const KisRecalculateTransformMaskJob *otherJob =
+        dynamic_cast<const KisRecalculateTransformMaskJob*>(_otherJob);
+
+    return otherJob && otherJob->m_mask == m_mask;
+}
+
+void KisRecalculateTransformMaskJob::run()
+{
+    m_mask->recaclulateStaticImage();
+    m_mask->setDirty();
+}
diff --git a/krita/image/kis_recalculate_transform_mask_job.h b/krita/image/kis_recalculate_transform_mask_job.h
new file mode 100644
index 0000000..1faf4f9
--- /dev/null
+++ b/krita/image/kis_recalculate_transform_mask_job.h
@@ -0,0 +1,38 @@
+/*
+ *  Copyright (c) 2014 Dmitry Kazakov <dimula73 at gmail.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef __KIS_RECALCULATE_TRANSFORM_MASK_JOB_H
+#define __KIS_RECALCULATE_TRANSFORM_MASK_JOB_H
+
+#include "kis_types.h"
+#include "kis_spontaneous_job.h"
+
+
+class KRITAIMAGE_EXPORT KisRecalculateTransformMaskJob : public KisSpontaneousJob
+{
+public:
+    KisRecalculateTransformMaskJob(KisTransformMaskSP mask);
+
+    bool overrides(const KisSpontaneousJob *otherJob);
+    void run();
+
+private:
+    KisTransformMaskSP m_mask;
+};
+
+#endif /* __KIS_RECALCULATE_TRANSFORM_MASK_JOB_H */
diff --git a/krita/image/kis_transform_mask.cpp b/krita/image/kis_transform_mask.cpp
new file mode 100644
index 0000000..f15bd51
--- /dev/null
+++ b/krita/image/kis_transform_mask.cpp
@@ -0,0 +1,253 @@
+/*
+ *  Copyright (c) 2007 Boudewijn Rempt <boud at valdyas.org>
+ *
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include <KoIcon.h>
+#include <KoCompositeOpRegistry.h>
+
+#include "kis_layer.h"
+#include "kis_transform_mask.h"
+#include "filter/kis_filter.h"
+#include "filter/kis_filter_configuration.h"
+#include "filter/kis_filter_registry.h"
+#include "kis_selection.h"
+#include "kis_processing_information.h"
+#include "kis_node.h"
+#include "kis_node_visitor.h"
+#include "kis_processing_visitor.h"
+#include "kis_node_progress_proxy.h"
+#include "kis_transaction.h"
+#include "kis_painter.h"
+
+#include <KoUpdater.h>
+#include "kis_perspectivetransform_worker.h"
+#include "kis_transform_mask_params_interface.h"
+#include "kis_recalculate_transform_mask_job.h"
+#include "kis_signal_compressor.h"
+
+#define UPDATE_DELAY 3000 /*ms */
+
+struct KisTransformMask::Private
+{
+    Private()
+        : worker(0, QTransform(), 0),
+          staticCacheValid(false),
+          recalculatingStaticImage(false),
+          updateSignalCompressor(UPDATE_DELAY, KisSignalCompressor::POSTPONE)
+    {
+    }
+
+    Private(const Private &rhs)
+        : worker(rhs.worker),
+          params(rhs.params),
+          staticCacheValid(false),
+          recalculatingStaticImage(rhs.recalculatingStaticImage),
+          updateSignalCompressor(UPDATE_DELAY, KisSignalCompressor::POSTPONE)
+    {
+    }
+
+    KisPerspectiveTransformWorker worker;
+    KisTransformMaskParamsInterfaceSP params;
+
+    bool staticCacheValid;
+    bool recalculatingStaticImage;
+    KisPaintDeviceSP staticCacheDevice;
+
+    KisSignalCompressor updateSignalCompressor;
+};
+
+
+KisTransformMask::KisTransformMask()
+    : KisEffectMask(),
+      m_d(new Private)
+{
+    setTransformParams(
+        KisTransformMaskParamsInterfaceSP(
+            new KisDumbTransformMaskParams()));
+
+    connect(&m_d->updateSignalCompressor, SIGNAL(timeout()), SLOT(slotDelayedStaticUpdate()));
+}
+
+KisTransformMask::~KisTransformMask()
+{
+}
+
+KisTransformMask::KisTransformMask(const KisTransformMask& rhs)
+    : KisEffectMask(rhs),
+      m_d(new Private(*rhs.m_d))
+{
+    connect(&m_d->updateSignalCompressor, SIGNAL(timeout()), SLOT(slotDelayedStaticUpdate()));
+}
+
+KisPaintDeviceSP KisTransformMask::paintDevice() const
+{
+    return 0;
+}
+
+QIcon KisTransformMask::icon() const
+{
+    return koIcon("edit-cut");
+}
+
+void KisTransformMask::setTransformParams(KisTransformMaskParamsInterfaceSP params)
+{
+    KIS_ASSERT_RECOVER(params) {
+        params = KisTransformMaskParamsInterfaceSP(
+            new KisDumbTransformMaskParams());
+    }
+
+    QTransform affineTransform;
+    if (params->isAffine()) {
+        affineTransform = params->finalAffineTransform();
+    }
+    m_d->worker.setForwardTransform(affineTransform);
+
+    m_d->params = params;
+    m_d->updateSignalCompressor.stop();
+}
+
+KisTransformMaskParamsInterfaceSP KisTransformMask::transformParams() const
+{
+    return m_d->params;
+}
+
+void KisTransformMask::slotDelayedStaticUpdate()
+{
+    KisLayerSP parentLayer = dynamic_cast<KisLayer*>(parent().data());
+    KIS_ASSERT_RECOVER_RETURN(parentLayer);
+
+    KisImageSP image = parentLayer->image();
+    if (image) {
+        image->addSpontaneousJob(new KisRecalculateTransformMaskJob(this));
+    }
+}
+
+void KisTransformMask::recaclulateStaticImage()
+{
+    /**
+     * Note: this function must be called from within the scheduler's
+     * context. We are accessing parent's updateProjection(), which
+     * is not entirely safe.
+     */
+
+    KisLayerSP parentLayer = dynamic_cast<KisLayer*>(parent().data());
+    KIS_ASSERT_RECOVER_RETURN(parentLayer);
+
+    if (!m_d->staticCacheDevice) {
+        m_d->staticCacheDevice =
+            new KisPaintDevice(parentLayer->original()->colorSpace());
+    }
+
+    m_d->recalculatingStaticImage = true;
+    parentLayer->updateProjection(parentLayer->exactBounds(), N_FILTHY_PROJECTION);
+    m_d->recalculatingStaticImage = false;
+
+    m_d->staticCacheValid = true;
+}
+
+QRect KisTransformMask::decorateRect(KisPaintDeviceSP &src,
+                                     KisPaintDeviceSP &dst,
+                                     const QRect & rc,
+                                     PositionToFilthy parentPos) const
+{
+    Q_ASSERT(nodeProgressProxy());
+    Q_ASSERT_X(src != dst, "KisTransformMask::decorateRect",
+               "src must be != dst, because we cant create transactions "
+               "during merge, as it breaks reentrancy");
+
+    KIS_ASSERT_RECOVER(m_d->params) { return rc; }
+
+    if (m_d->params->isHidden()) return rc;
+
+    if (parentPos != N_FILTHY_PROJECTION) {
+        m_d->staticCacheValid = false;
+        m_d->updateSignalCompressor.start();
+    }
+
+    if (m_d->recalculatingStaticImage) {
+        m_d->params->transformDevice(const_cast<KisTransformMask*>(this), src, m_d->staticCacheDevice);
+        dst->makeCloneFrom(m_d->staticCacheDevice, m_d->staticCacheDevice->extent());
+    } else if (!m_d->staticCacheValid && m_d->params->isAffine()) {
+        m_d->worker.runPartialDst(src, dst, rc);
+    } else {
+        KisPainter gc(dst);
+        gc.setCompositeOp(COMPOSITE_COPY);
+        gc.bitBlt(rc.topLeft(), m_d->staticCacheDevice, rc);
+    }
+
+    return rc;
+}
+
+bool KisTransformMask::accept(KisNodeVisitor &v)
+{
+    return v.visit(this);
+}
+
+void KisTransformMask::accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter)
+{
+    return visitor.visit(this, undoAdapter);
+}
+
+QRect KisTransformMask::changeRect(const QRect &rect, PositionToFilthy pos) const
+{
+    Q_UNUSED(pos);
+
+    /**
+     * FIXME: This check of the emptiness should be done
+     * on the higher/lower level
+     */
+    if (rect.isEmpty()) return rect;
+    if (!m_d->params->isAffine()) return rect;
+
+    QRect changeRect = m_d->worker.forwardTransform()
+        .mapRect(QRectF(rect)).toAlignedRect();
+
+    KisNodeSP parentNode;
+    KisPaintDeviceSP parentOriginal;
+
+    if ((parentNode = parent()) &&
+        (parentOriginal = parentNode->original())) {
+
+        QRect backwardRect = m_d->worker.backwardTransform().mapRect(rect);
+        QRegion backwardRegion(backwardRect);
+        backwardRegion -= parentOriginal->defaultBounds()->bounds();
+
+        backwardRegion = m_d->worker.forwardTransform().map(backwardRegion);
+
+        // FIXME: d-oh... please fix me and use region instead :(
+        changeRect |= backwardRegion.boundingRect();
+    }
+
+    return changeRect;
+}
+
+QRect KisTransformMask::needRect(const QRect& rect, PositionToFilthy pos) const
+{
+    Q_UNUSED(pos);
+
+    /**
+     * FIXME: This check of the emptiness should be done
+     * on the higher/lower level
+     */
+    if (rect.isEmpty()) return rect;
+    if (!m_d->params->isAffine()) return rect;
+
+    return kisGrowRect(m_d->worker.backwardTransform().mapRect(rect), 2);
+}
+
+#include "kis_transform_mask.moc"
diff --git a/krita/image/kis_filter_mask.h b/krita/image/kis_transform_mask.h
similarity index 62%
copy from krita/image/kis_filter_mask.h
copy to krita/image/kis_transform_mask.h
index e3c093d..8a61cfa 100644
--- a/krita/image/kis_filter_mask.h
+++ b/krita/image/kis_transform_mask.h
@@ -15,24 +15,19 @@
  *  along with this program; if not, write to the Free Software
  *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  */
-#ifndef _KIS_FILTER_MASK_
-#define _KIS_FILTER_MASK_
+#ifndef _KIS_TRANSFORM_MASK_
+#define _KIS_TRANSFORM_MASK_
+
+#include <QScopedPointer>
 
 #include "kis_types.h"
 #include "kis_effect_mask.h"
 
-#include "kis_node_filter_interface.h"
-
-class KisFilterConfiguration;
-
 /**
-   An filter mask is a single channel mask that applies a particular
-   filter to the layer the mask belongs to. It differs from an
-   adjustment layer in that it only works on its parent layer, while
-   adjustment layers work on all layers below it in its layer group.
+   Transform a layer according to a matrix transform
 */
 
-class KRITAIMAGE_EXPORT KisFilterMask : public KisEffectMask, public KisNodeFilterInterface
+class KRITAIMAGE_EXPORT KisTransformMask : public KisEffectMask
 {
     Q_OBJECT
 
@@ -41,29 +36,42 @@ public:
     /**
      * Create an empty filter mask.
      */
-    KisFilterMask();
+    KisTransformMask();
 
-    virtual ~KisFilterMask();
+    virtual ~KisTransformMask();
 
     QIcon icon() const;
 
     KisNodeSP clone() const {
-        return KisNodeSP(new KisFilterMask(*this));
+        return KisNodeSP(new KisTransformMask(*this));
     }
 
+    KisPaintDeviceSP paintDevice() const;
+
     bool accept(KisNodeVisitor &v);
     void accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter);
 
-    KisFilterMask(const KisFilterMask& rhs);
-
-    void setFilter(KisFilterConfiguration *filterConfig);
+    KisTransformMask(const KisTransformMask& rhs);
 
     QRect decorateRect(KisPaintDeviceSP &src,
                        KisPaintDeviceSP &dst,
-                       const QRect & rc) const;
+                       const QRect & rc,
+                       PositionToFilthy parentPos) const;
 
     QRect changeRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const;
     QRect needRect(const QRect &rect, PositionToFilthy pos = N_FILTHY) const;
+
+    void setTransformParams(KisTransformMaskParamsInterfaceSP params);
+    KisTransformMaskParamsInterfaceSP transformParams() const;
+
+    void recaclulateStaticImage();
+
+private slots:
+    void slotDelayedStaticUpdate();
+
+private:
+    struct Private;
+    const QScopedPointer<Private> m_d;
 };
 
-#endif //_KIS_FILTER_MASK_
+#endif //_KIS_TRANSFORM_MASK_
diff --git a/krita/image/kis_transform_mask_params_interface.cpp b/krita/image/kis_transform_mask_params_interface.cpp
new file mode 100644
index 0000000..0cc0ed1
--- /dev/null
+++ b/krita/image/kis_transform_mask_params_interface.cpp
@@ -0,0 +1,76 @@
+/*
+ *  Copyright (c) 2014 Dmitry Kazakov <dimula73 at gmail.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "kis_transform_mask_params_interface.h"
+
+#include <QTransform>
+
+
+KisTransformMaskParamsInterface::~KisTransformMaskParamsInterface()
+{
+}
+
+///////////////// KisDumbTransformMaskParams ////////////////////////////
+
+struct KisDumbTransformMaskParams::Private
+{
+    Private() : isHidden(false) {}
+
+    QTransform transform;
+    bool isHidden;
+};
+
+KisDumbTransformMaskParams::KisDumbTransformMaskParams()
+    : m_d(new Private)
+{
+}
+
+KisDumbTransformMaskParams::KisDumbTransformMaskParams(const QTransform &transform)
+    : m_d(new Private)
+{
+    m_d->isHidden = false;
+    m_d->transform = transform;
+}
+
+KisDumbTransformMaskParams::KisDumbTransformMaskParams(bool isHidden)
+    : m_d(new Private)
+{
+    m_d->isHidden = isHidden;
+}
+
+QTransform KisDumbTransformMaskParams::finalAffineTransform() const
+{
+    return m_d->transform;
+}
+
+bool KisDumbTransformMaskParams::isAffine() const
+{
+    return true;
+}
+
+bool KisDumbTransformMaskParams::isHidden() const
+{
+    return m_d->isHidden;
+}
+
+void KisDumbTransformMaskParams::transformDevice(KisNodeSP node, KisPaintDeviceSP src, KisPaintDeviceSP dst) const
+{
+    Q_UNUSED(node);
+    Q_UNUSED(src);
+    Q_UNUSED(dst);
+}
diff --git a/krita/image/kis_transform_mask_params_interface.h b/krita/image/kis_transform_mask_params_interface.h
new file mode 100644
index 0000000..ada0935
--- /dev/null
+++ b/krita/image/kis_transform_mask_params_interface.h
@@ -0,0 +1,59 @@
+/*
+ *  Copyright (c) 2014 Dmitry Kazakov <dimula73 at gmail.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef __KIS_TRANSFORM_MASK_PARAMS_INTERFACE_H
+#define __KIS_TRANSFORM_MASK_PARAMS_INTERFACE_H
+
+#include "krita_export.h"
+#include "kis_types.h"
+
+#include <QScopedPointer>
+
+
+class QTransform;
+
+class KRITAIMAGE_EXPORT KisTransformMaskParamsInterface
+{
+public:
+    virtual ~KisTransformMaskParamsInterface();
+
+    virtual QTransform finalAffineTransform() const = 0;
+    virtual bool isAffine() const = 0;
+    virtual bool isHidden() const = 0;
+
+    virtual void transformDevice(KisNodeSP node, KisPaintDeviceSP src, KisPaintDeviceSP dst) const = 0;
+};
+
+class KRITAIMAGE_EXPORT KisDumbTransformMaskParams : public KisTransformMaskParamsInterface
+{
+public:
+    KisDumbTransformMaskParams();
+    KisDumbTransformMaskParams(const QTransform &transform);
+    KisDumbTransformMaskParams(bool isHidden);
+
+    QTransform finalAffineTransform() const;
+    bool isAffine() const;
+    bool isHidden() const;
+    void transformDevice(KisNodeSP node, KisPaintDeviceSP src, KisPaintDeviceSP dst) const;
+
+private:
+    struct Private;
+    const QScopedPointer<Private> m_d;
+};
+
+#endif /* __KIS_TRANSFORM_MASK_PARAMS_INTERFACE_H */
diff --git a/krita/image/kis_transform_params.cpp b/krita/image/kis_transform_params.cpp
new file mode 100644
index 0000000..f290806
--- /dev/null
+++ b/krita/image/kis_transform_params.cpp
@@ -0,0 +1,28 @@
+/*
+ *  Copyright (c) 2014 Dmitry Kazakov <dimula73 at gmail.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "kis_transform_params.h"
+
+
+KisTransformParams::KisTransformParams()
+{
+}
+
+KisTransformParams::~KisTransformParams()
+{
+}
diff --git a/krita/image/kis_transform_params.h b/krita/image/kis_transform_params.h
new file mode 100644
index 0000000..d59d095
--- /dev/null
+++ b/krita/image/kis_transform_params.h
@@ -0,0 +1,30 @@
+/*
+ *  Copyright (c) 2014 Dmitry Kazakov <dimula73 at gmail.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef __KIS_TRANSFORM_PARAMS_H
+#define __KIS_TRANSFORM_PARAMS_H
+
+
+class KisTransformParams
+{
+public:
+    KisTransformParams();
+    ~KisTransformParams();
+};
+
+#endif /* __KIS_TRANSFORM_PARAMS_H */
diff --git a/krita/image/kis_transform_worker.cc b/krita/image/kis_transform_worker.cc
index a602670..beee995 100644
--- a/krita/image/kis_transform_worker.cc
+++ b/krita/image/kis_transform_worker.cc
@@ -233,6 +233,11 @@ void swapValues(T *a, T *b) {
 
 bool KisTransformWorker::run()
 {
+    return runPartial(m_dev->exactBounds());
+}
+
+bool KisTransformWorker::runPartial(const QRect &processRect)
+{
     /* Check for nonsense and let the user know, this helps debugging.
     Otherwise the program will crash at a later point, in a very obscure way, probably by division by zero */
     Q_ASSERT_X(m_xscale != 0, "KisTransformer::run() validation step", "xscale == 0");
@@ -240,7 +245,7 @@ bool KisTransformWorker::run()
     // Fallback safety line in case Krita is compiled without ASSERTS
     if (m_xscale == 0 || m_yscale == 0) return false;
 
-    m_boundRect = m_dev->exactBounds();
+    m_boundRect = processRect;
 
     if (m_boundRect.isNull()) {
         if (!m_progressUpdater.isNull()) {
diff --git a/krita/image/kis_transform_worker.h b/krita/image/kis_transform_worker.h
index 3775b61..c536dce 100644
--- a/krita/image/kis_transform_worker.h
+++ b/krita/image/kis_transform_worker.h
@@ -91,6 +91,7 @@ public:
 
     // returns false if interrupted
     bool run();
+    bool runPartial(const QRect &processRect);
 
     /**
      * Returns a matrix of the transformation executed by the worker.
diff --git a/krita/image/kis_transparency_mask.cc b/krita/image/kis_transparency_mask.cc
index ce9ce43..01f41c5 100644
--- a/krita/image/kis_transparency_mask.cc
+++ b/krita/image/kis_transparency_mask.cc
@@ -46,8 +46,11 @@ KisTransparencyMask::~KisTransparencyMask()
 
 QRect KisTransparencyMask::decorateRect(KisPaintDeviceSP &src,
                                         KisPaintDeviceSP &dst,
-                                        const QRect & rc) const
+                                        const QRect & rc,
+                                        PositionToFilthy parentPos) const
 {
+    Q_UNUSED(parentPos);
+
     if (src != dst) {
         KisPainter gc(dst);
         gc.setCompositeOp(src->colorSpace()->compositeOp(COMPOSITE_COPY));
diff --git a/krita/image/kis_transparency_mask.h b/krita/image/kis_transparency_mask.h
index f9bda97..56b4531 100644
--- a/krita/image/kis_transparency_mask.h
+++ b/krita/image/kis_transparency_mask.h
@@ -49,7 +49,8 @@ public:
     }
 
     QRect decorateRect(KisPaintDeviceSP &src, KisPaintDeviceSP &dst,
-                       const QRect & rc) const;
+                       const QRect & rc,
+                       PositionToFilthy parentPos) const;
     QIcon icon() const;
     bool accept(KisNodeVisitor &v);
     void accept(KisProcessingVisitor &visitor, KisUndoAdapter *undoAdapter);
diff --git a/krita/image/kis_types.h b/krita/image/kis_types.h
index 8a277cc..20a0b7a 100644
--- a/krita/image/kis_types.h
+++ b/krita/image/kis_types.h
@@ -27,6 +27,9 @@ class KisWeakSharedPtr;
 template<class T>
 class KisSharedPtr;
 
+template<class T> class QSharedPointer;
+template<class T> class QWeakPointer;
+
 template <class T>
 uint qHash(KisSharedPtr<T> ptr) {
     return qHash(ptr.data());
@@ -80,6 +83,14 @@ class KisFilterMask;
 typedef KisSharedPtr<KisFilterMask> KisFilterMaskSP;
 typedef KisWeakSharedPtr<KisFilterMask> KisFilterMaskWSP;
 
+class KisTransformMask;
+typedef KisSharedPtr<KisTransformMask> KisTransformMaskSP;
+typedef KisWeakSharedPtr<KisTransformMask> KisTransformMaskWSP;
+
+class KisTransformMaskParamsInterface;
+typedef QSharedPointer<KisTransformMaskParamsInterface> KisTransformMaskParamsInterfaceSP;
+typedef QWeakPointer<KisTransformMaskParamsInterface> KisTransformMaskParamsInterfaceWSP;
+
 class KisTransparencyMask;
 typedef KisSharedPtr<KisTransparencyMask> KisTransparencyMaskSP;
 typedef KisWeakSharedPtr<KisTransparencyMask> KisTransparencyMaskWSP;
@@ -201,9 +212,6 @@ typedef QPointer<KoUpdater> KoUpdaterPtr;
 class KisProcessingVisitor;
 typedef KisSharedPtr<KisProcessingVisitor> KisProcessingVisitorSP;
 
-template<class T> class QSharedPointer;
-template<class T> class QWeakPointer;
-
 class KUndo2Command;
 typedef QSharedPointer<KUndo2Command> KUndo2CommandSP;
 
diff --git a/krita/image/tests/CMakeLists.txt b/krita/image/tests/CMakeLists.txt
index f585687..8075b38 100644
--- a/krita/image/tests/CMakeLists.txt
+++ b/krita/image/tests/CMakeLists.txt
@@ -137,6 +137,14 @@ target_link_libraries(KisFilterMaskTest  ${KDE4_KDEUI_LIBS} kritaimage ${QT_QTTE
 
 ########### next target ###############
 
+set(kis_transform_mask_test_SRCS kis_transform_mask_test.cpp )
+
+kde4_add_unit_test(KisTransformMaskTest TESTNAME krita-image-KisTransformMaskTest ${kis_transform_mask_test_SRCS})
+
+target_link_libraries(KisTransformMaskTest  ${KDE4_KDEUI_LIBS} kritaimage ${QT_QTTEST_LIBRARY})
+
+########### next target ###############
+
 set(kis_paint_layer_test_SRCS kis_paint_layer_test.cpp )
 
 kde4_add_unit_test(KisPaintLayerTest TESTNAME krita-image-KisPaintLayerTest ${kis_paint_layer_test_SRCS})
diff --git a/krita/image/tests/kis_filter_mask_test.cpp b/krita/image/tests/kis_filter_mask_test.cpp
index d652006..86b52f9 100644
--- a/krita/image/tests/kis_filter_mask_test.cpp
+++ b/krita/image/tests/kis_filter_mask_test.cpp
@@ -76,7 +76,8 @@ void KisFilterMaskTest::testProjectionNotSelected()
     mask->initSelection(layer);
     mask->createNodeProgressProxy();
     mask->select(qimage.rect(), MIN_SELECTED);
-    mask->apply(projection, QRect(0, 0, qimage.width(), qimage.height()));
+
+    mask->apply(projection, qimage.rect(), qimage.rect(), KisNode::N_FILTHY);
 
     QPoint errpoint;
     if (!TestUtil::compareQImages(errpoint, qimage, projection->convertToQImage(0, 0, 0, qimage.width(), qimage.height()))) {
@@ -110,7 +111,7 @@ void KisFilterMaskTest::testProjectionSelected()
 
     mask->initSelection(layer);
     mask->select(qimage.rect(), MAX_SELECTED);
-    mask->apply(projection, QRect(0, 0, qimage.width(), qimage.height()));
+    mask->apply(projection, qimage.rect(), qimage.rect(), KisNode::N_FILTHY);
     QCOMPARE(mask->exactBounds(), QRect(0, 0, IMAGE_WIDTH, IMAGE_HEIGHT));
 
     QPoint errpoint;
diff --git a/krita/image/tests/kis_mask_test.cpp b/krita/image/tests/kis_mask_test.cpp
index 5bb4eff..942719a 100644
--- a/krita/image/tests/kis_mask_test.cpp
+++ b/krita/image/tests/kis_mask_test.cpp
@@ -98,7 +98,7 @@ void KisMaskTest::testCropUpdateBySelection()
 
     mask->initSelection(sel, p.layer);
 
-    mask->apply(p.layer->projection(), updateRect);
+    mask->apply(p.layer->projection(), updateRect, updateRect, KisNode::N_FILTHY);
     // Here we crash! :)
 
     /**
diff --git a/krita/image/tests/kis_paint_layer_test.cpp b/krita/image/tests/kis_paint_layer_test.cpp
index 420cf6c..ebf757f 100644
--- a/krita/image/tests/kis_paint_layer_test.cpp
+++ b/krita/image/tests/kis_paint_layer_test.cpp
@@ -59,7 +59,7 @@ void KisPaintLayerTest::testProjection()
     Q_ASSERT(layer->hasEffectMasks());
 
     // And now we're going to update the projection, but nothing is dirty yet
-    layer->updateProjection(qimage.rect());
+    layer->updateProjection(qimage.rect(), KisNode::N_FILTHY);
 
     // Which also means that the projection is no longer the paint device
     QVERIFY(layer->paintDevice().data() != layer->projection().data());
@@ -68,7 +68,7 @@ void KisPaintLayerTest::testProjection()
     layer->setDirty(qimage.rect());
 
     // And now we're going to update the projection, but nothing is dirty yet
-    layer->updateProjection(qimage.rect());
+    layer->updateProjection(qimage.rect(), KisNode::N_FILTHY);
 
     // Which also means that the projection is no longer the paint device
     QVERIFY(layer->paintDevice().data() != layer->projection().data());
@@ -78,7 +78,7 @@ void KisPaintLayerTest::testProjection()
     QVERIFY(layer->projection().data() != 0);
 
     // The selection is initially empty, so after an update, all pixels are still visible
-    layer->updateProjection(qimage.rect());
+    layer->updateProjection(qimage.rect(), KisNode::N_FILTHY);
 
     // We've inverted the mask, so now nothing is seen
     KisSequentialConstIterator it(layer->projection(), qimage.rect());
diff --git a/krita/image/tests/kis_transform_mask_test.cpp b/krita/image/tests/kis_transform_mask_test.cpp
new file mode 100644
index 0000000..b1719d2
--- /dev/null
+++ b/krita/image/tests/kis_transform_mask_test.cpp
@@ -0,0 +1,59 @@
+/*
+ *  Copyright (c) 2014 Dmitry Kazakov <dimula73 at gmail.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "kis_transform_mask_test.h"
+
+#include <qtest_kde.h>
+#include "kis_transform_mask.h"
+#include "kis_transform_mask_params_interface.h"
+
+#include "testutil.h"
+
+
+void KisTransformMaskTest::test()
+{
+    QImage refImage(TestUtil::fetchDataFileLazy("test_transform_quality.png"));
+    TestUtil::MaskParent p(refImage.rect());
+
+    p.layer->paintDevice()->convertFromQImage(refImage, 0);
+
+    KisTransformMaskSP mask = new KisTransformMask();
+
+    p.image->addNode(mask, p.layer);
+
+    QTransform transform =
+        QTransform::fromTranslate(100.0, 0.0) *
+        QTransform::fromScale(2.0, 1.0);
+
+    mask->setTransformParams(KisTransformMaskParamsInterfaceSP(
+                                 new KisDumbTransformMaskParams(transform)));
+
+    p.layer->setDirty(QRect(160, 160, 150, 300));
+    p.layer->setDirty(QRect(310, 160, 150, 300));
+    p.layer->setDirty(QRect(460, 160, 150, 300));
+    p.layer->setDirty(QRect(610, 160, 150, 300));
+    p.layer->setDirty(QRect(760, 160, 150, 300));
+
+    p.image->waitForDone();
+
+    QImage result = p.layer->projection()->convertToQImage(0);
+    TestUtil::checkQImage(result, "transform_mask_test", "partial", "single");
+
+}
+
+QTEST_KDEMAIN(KisTransformMaskTest, GUI)
diff --git a/krita/image/tests/kis_transform_mask_test.h b/krita/image/tests/kis_transform_mask_test.h
new file mode 100644
index 0000000..32afb01
--- /dev/null
+++ b/krita/image/tests/kis_transform_mask_test.h
@@ -0,0 +1,31 @@
+/*
+ *  Copyright (c) 2014 Dmitry Kazakov <dimula73 at gmail.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef __KIS_TRANSFORM_MASK_TEST_H
+#define __KIS_TRANSFORM_MASK_TEST_H
+
+#include <QtTest/QtTest>
+
+class KisTransformMaskTest : public QObject
+{
+    Q_OBJECT
+private slots:
+    void test();
+};
+
+#endif /* __KIS_TRANSFORM_MASK_TEST_H */
diff --git a/krita/image/tests/kis_transform_worker_test.cpp b/krita/image/tests/kis_transform_worker_test.cpp
index 5ee16a3..a6e4ec0 100644
--- a/krita/image/tests/kis_transform_worker_test.cpp
+++ b/krita/image/tests/kis_transform_worker_test.cpp
@@ -951,5 +951,37 @@ void KisTransformWorkerTest::generateTestImages()
     }
 }
 
+#include "kis_perspectivetransform_worker.h"
+
+
+void KisTransformWorkerTest::testPartialProcessing()
+{
+    TestUtil::TestProgressBar bar;
+    KoProgressUpdater pu(&bar);
+    KoUpdaterPtr updater = pu.startSubtask();
+
+    const KoColorSpace * cs = KoColorSpaceRegistry::instance()->rgb8();
+    QImage image(TestUtil::fetchDataFileLazy("test_transform_quality.png"));
+    KisPaintDeviceSP dev = new KisPaintDevice(cs);
+    dev->convertFromQImage(image, 0);
+
+    KisTransaction t(dev);
+
+    QTransform transform = QTransform::fromScale(2.0, 1.1);
+    transform.shear(1.1, 0);
+    transform.rotateRadians(M_PI / 18);
+
+    KisPerspectiveTransformWorker tw(0, transform, updater);
+    tw.runPartialDst(dev, dev, QRect(1200, 1200, 150, 150));
+    tw.runPartialDst(dev, dev, QRect(1350, 1200, 150, 150));
+    tw.runPartialDst(dev, dev, QRect(1200, 1350, 150, 150));
+    tw.runPartialDst(dev, dev, QRect(1350, 1350, 150, 150));
+
+    t.end();
+
+    QImage result = dev->convertToQImage(0);
+    TestUtil::checkQImage(result, "transform_test", "partial", "single");
+}
+
 QTEST_KDEMAIN(KisTransformWorkerTest, GUI)
 #include "kis_transform_worker_test.moc"
diff --git a/krita/image/tests/kis_transform_worker_test.h b/krita/image/tests/kis_transform_worker_test.h
index d44c910..3f82def 100644
--- a/krita/image/tests/kis_transform_worker_test.h
+++ b/krita/image/tests/kis_transform_worker_test.h
@@ -62,6 +62,8 @@ private slots:
     void benchmarkShear();
     void benchmarkScaleRotateShear();
 
+    void testPartialProcessing();
+
 private:
     void generateTestImages();
 };
diff --git a/krita/image/tests/kis_transparency_mask_test.cpp b/krita/image/tests/kis_transparency_mask_test.cpp
index 3fae211..c565632 100644
--- a/krita/image/tests/kis_transparency_mask_test.cpp
+++ b/krita/image/tests/kis_transparency_mask_test.cpp
@@ -67,10 +67,12 @@ void KisTransparencyMaskTest::testApply()
     KisTransparencyMaskSP mask;
 
 
+    QRect applyRect(0, 0, 200, 100);
+
     // Everything is selected
     initImage(image, layer, dev, mask);
     mask->initSelection(layer);
-    mask->apply(dev, QRect(0, 0, 200, 100));
+    mask->apply(dev, applyRect, applyRect, KisNode::N_FILTHY);
     QImage qimage = dev->convertToQImage(0, 0, 0, 200, 100);
 
     if (!TestUtil::compareQImages(errpoint,
@@ -83,7 +85,7 @@ void KisTransparencyMaskTest::testApply()
     initImage(image, layer, dev, mask);
     mask->initSelection(layer);
     mask->selection()->pixelSelection()->invert();
-    mask->apply(dev, QRect(0, 0, 200, 100));
+    mask->apply(dev, applyRect, applyRect, KisNode::N_FILTHY);
     qimage = dev->convertToQImage(0, 0, 0, 200, 100);
 
     if (!TestUtil::compareQImages(errpoint,
@@ -96,7 +98,7 @@ void KisTransparencyMaskTest::testApply()
     mask->initSelection(layer);
     mask->selection()->pixelSelection()->invert();
     mask->select(QRect(50, 0, 100, 100));
-    mask->apply(dev, QRect(0, 0, 200, 100));
+    mask->apply(dev, applyRect, applyRect, KisNode::N_FILTHY);
     qimage = dev->convertToQImage(0, 0, 0, 200, 100);
 
     if (!TestUtil::compareQImages(errpoint,
diff --git a/krita/plugins/extensions/dockers/defaultdockers/kis_layer_box.cpp b/krita/plugins/extensions/dockers/defaultdockers/kis_layer_box.cpp
index c834f4b..2970fa0 100644
--- a/krita/plugins/extensions/dockers/defaultdockers/kis_layer_box.cpp
+++ b/krita/plugins/extensions/dockers/defaultdockers/kis_layer_box.cpp
@@ -365,6 +365,7 @@ void KisLayerBox::setCanvas(KoCanvasBase *canvas)
         m_newLayerMenu->addSeparator();
         addActionToMenu(m_newLayerMenu, "add_new_transparency_mask");
         addActionToMenu(m_newLayerMenu, "add_new_filter_mask");
+        addActionToMenu(m_newLayerMenu, "add_new_transform_mask");
         addActionToMenu(m_newLayerMenu, "add_new_selection_mask");
     }
 
@@ -489,6 +490,7 @@ void KisLayerBox::slotContextMenuRequested(const QPoint &pos, const QModelIndex
         addActionToMenu(convertToMenu, "convert_to_paint_layer");
         addActionToMenu(convertToMenu, "convert_to_transparency_mask");
         addActionToMenu(convertToMenu, "convert_to_filter_mask");
+        addActionToMenu(convertToMenu, "convert_to_transform_mask");
         addActionToMenu(convertToMenu, "convert_to_selection_mask");
 
         addActionToMenu(&menu, "isolate_layer");
@@ -496,6 +498,7 @@ void KisLayerBox::slotContextMenuRequested(const QPoint &pos, const QModelIndex
     menu.addSeparator();
     addActionToMenu(&menu, "add_new_transparency_mask");
     addActionToMenu(&menu, "add_new_filter_mask");
+    addActionToMenu(&menu, "add_new_transform_mask");
     addActionToMenu(&menu, "add_new_selection_mask");
     menu.addSeparator();
     menu.addAction(m_selectOpaque);
diff --git a/krita/plugins/tools/tool_transform2/CMakeLists.txt b/krita/plugins/tools/tool_transform2/CMakeLists.txt
index 14d3d7e..7e22ace 100644
--- a/krita/plugins/tools/tool_transform2/CMakeLists.txt
+++ b/krita/plugins/tools/tool_transform2/CMakeLists.txt
@@ -1,6 +1,7 @@
 set(kritatooltransform_PART_SRCS
     tool_transform.cc
     tool_transform_args.cc
+    kis_transform_mask_adapter.cpp
     tool_transform_changes_tracker.cpp
     kis_tool_transform.cc
     kis_tool_transform_config_widget.cpp
diff --git a/krita/plugins/tools/tool_transform2/kis_free_transform_strategy.cpp b/krita/plugins/tools/tool_transform2/kis_free_transform_strategy.cpp
index 4166647..dd154f7 100644
--- a/krita/plugins/tools/tool_transform2/kis_free_transform_strategy.cpp
+++ b/krita/plugins/tools/tool_transform2/kis_free_transform_strategy.cpp
@@ -332,7 +332,7 @@ void KisFreeTransformStrategy::paint(QPainter &gc)
     handles.addRect(handleRect.translated(m_d->transaction.originalMiddleTop()));
     handles.addRect(handleRect.translated(m_d->transaction.originalMiddleBottom()));
 
-    QPointF rotationCenter = m_d->transaction.originalCenter() + m_d->currentArgs.rotationCenterOffset();
+    QPointF rotationCenter = m_d->currentArgs.originalCenter() + m_d->currentArgs.rotationCenterOffset();
     QPointF dx(r + 3, 0);
     QPointF dy(0, r + 3);
     handles.addEllipse(rotationCenterRect.translated(rotationCenter));
@@ -577,7 +577,7 @@ void KisFreeTransformStrategy::continuePrimaryAction(const QPointF &mousePos, bo
         QPointF pt = m_d->transform.inverted().map(mousePos);
         pt = KisTransformUtils::clipInRect(pt, m_d->transaction.originalRect());
 
-        QPointF newRotationCenterOffset = pt - m_d->transaction.originalCenter();
+        QPointF newRotationCenterOffset = pt - m_d->currentArgs.originalCenter();
 
         if (specialModifierActive) {
             if (qAbs(newRotationCenterOffset.x()) > qAbs(newRotationCenterOffset.y())) {
@@ -652,7 +652,7 @@ void KisFreeTransformStrategy::Private::recalculateTransformations()
      * The center of the original image should still
      * stay the the origin of CS
      */
-    KIS_ASSERT_RECOVER_NOOP(sanityCheckMatrix.map(transaction.originalCenter()).manhattanLength() < 1e-4);
+    KIS_ASSERT_RECOVER_NOOP(sanityCheckMatrix.map(currentArgs.originalCenter()).manhattanLength() < 1e-4);
 
     transform = m.finalTransform();
 
diff --git a/krita/plugins/tools/tool_transform2/kis_tool_transform.cc b/krita/plugins/tools/tool_transform2/kis_tool_transform.cc
index 498de63..084312c 100644
--- a/krita/plugins/tools/tool_transform2/kis_tool_transform.cc
+++ b/krita/plugins/tools/tool_transform2/kis_tool_transform.cc
@@ -82,6 +82,9 @@
 #include "kis_free_transform_strategy.h"
 #include "kis_perspective_transform_strategy.h"
 
+#include "kis_transform_mask.h"
+#include "kis_transform_mask_adapter.h"
+
 #include "strokes/transform_stroke_strategy.h"
 
 KisToolTransform::KisToolTransform(KoCanvasBase * canvas)
@@ -630,6 +633,29 @@ void KisToolTransform::setWarpPointDensity( int density )
     m_optionsWidget->slotSetWarpDensity(density);
 }
 
+bool KisToolTransform::tryInitTransformModeFromNode(KisNodeSP node)
+{
+    bool result = false;
+
+    if (KisTransformMaskSP mask =
+        dynamic_cast<KisTransformMask*>(node.data())) {
+
+        KisTransformMaskParamsInterfaceSP savedParams =
+            mask->transformParams();
+
+        KisTransformMaskAdapter *adapter =
+            dynamic_cast<KisTransformMaskAdapter*>(savedParams.data());
+
+        if (adapter) {
+            m_currentArgs = adapter->savedArgs();
+            initGuiAfterTransformMode();
+            result = true;
+        }
+    }
+
+    return result;
+}
+
 void KisToolTransform::initTransformMode(ToolTransformArgs::TransformMode mode)
 {
     // NOTE: we are requesting an old value of m_currentArgs variable
@@ -638,8 +664,8 @@ void KisToolTransform::initTransformMode(ToolTransformArgs::TransformMode mode)
     QString filterId = m_currentArgs.filterId();
 
     m_currentArgs = ToolTransformArgs();
-    m_currentArgs.setOriginalCenter(m_transaction.originalCenter());
-    m_currentArgs.setTransformedCenter(m_transaction.originalCenter());
+    m_currentArgs.setOriginalCenter(m_transaction.originalCenterGeometric());
+    m_currentArgs.setTransformedCenter(m_transaction.originalCenterGeometric());
 
     if (mode == ToolTransformArgs::FREE_TRANSFORM) {
         m_currentArgs.setMode(ToolTransformArgs::FREE_TRANSFORM);
@@ -657,6 +683,11 @@ void KisToolTransform::initTransformMode(ToolTransformArgs::TransformMode mode)
         m_currentArgs.setMode(ToolTransformArgs::PERSPECTIVE_4POINT);
     }
 
+    initGuiAfterTransformMode();
+}
+
+void KisToolTransform::initGuiAfterTransformMode()
+{
     currentStrategy()->externalConfigChanged();
     outlineChanged();
     updateOptionWidget();
@@ -814,7 +845,9 @@ void KisToolTransform::startStroke(ToolTransformArgs::TransformMode mode)
     initThumbnailImage(previewDevice);
     updateSelectionPath();
 
-    initTransformMode(mode);
+    if (!tryInitTransformModeFromNode(currentNode)) {
+        initTransformMode(mode);
+    }
 
     m_strokeData = StrokeData(image()->startStroke(strategy));
     clearDevices(m_transaction.rootNode(), m_workRecursively);
diff --git a/krita/plugins/tools/tool_transform2/kis_tool_transform.h b/krita/plugins/tools/tool_transform2/kis_tool_transform.h
index a4df787..0dbb35f 100644
--- a/krita/plugins/tools/tool_transform2/kis_tool_transform.h
+++ b/krita/plugins/tools/tool_transform2/kis_tool_transform.h
@@ -232,7 +232,9 @@ private:
     void commitChanges();
 
 
+    bool tryInitTransformModeFromNode(KisNodeSP node);
     void initTransformMode(ToolTransformArgs::TransformMode mode);
+    void initGuiAfterTransformMode();
 
     void initThumbnailImage(KisPaintDeviceSP previewDevice);
     void updateSelectionPath();
diff --git a/krita/plugins/tools/tool_transform2/kis_tool_transform_config_widget.cpp b/krita/plugins/tools/tool_transform2/kis_tool_transform_config_widget.cpp
index 841e0a2..362e340 100644
--- a/krita/plugins/tools/tool_transform2/kis_tool_transform_config_widget.cpp
+++ b/krita/plugins/tools/tool_transform2/kis_tool_transform_config_widget.cpp
@@ -844,8 +844,8 @@ void KisToolTransformConfigWidget::setDefaultWarpPoints(int pointsPerLine)
 
     if (nbPoints == 1) {
         //there is actually no grid
-        origPoints[0] = m_transaction->originalCenter();
-        transfPoints[0] = m_transaction->originalCenter();
+        origPoints[0] = m_transaction->originalCenterGeometric();
+        transfPoints[0] = m_transaction->originalCenterGeometric();
     }
     else if (nbPoints > 1) {
         gridSpaceX = m_transaction->originalRect().width() / (pointsPerLine - 1);
diff --git a/krita/plugins/tools/tool_transform2/kis_transform_mask_adapter.cpp b/krita/plugins/tools/tool_transform2/kis_transform_mask_adapter.cpp
new file mode 100644
index 0000000..6ec5275
--- /dev/null
+++ b/krita/plugins/tools/tool_transform2/kis_transform_mask_adapter.cpp
@@ -0,0 +1,72 @@
+/*
+ *  Copyright (c) 2014 Dmitry Kazakov <dimula73 at gmail.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#include "kis_transform_mask_adapter.h"
+
+#include <QTransform>
+
+#include "tool_transform_args.h"
+#include "kis_transform_utils.h"
+
+
+struct KisTransformMaskAdapter::Private
+{
+    ToolTransformArgs args;
+};
+
+
+KisTransformMaskAdapter::KisTransformMaskAdapter(const ToolTransformArgs &args)
+    : m_d(new Private)
+{
+    m_d->args = args;
+}
+
+KisTransformMaskAdapter::~KisTransformMaskAdapter()
+{
+}
+
+QTransform KisTransformMaskAdapter::finalAffineTransform() const
+{
+    KisTransformUtils::MatricesPack m(m_d->args);
+    return m.finalTransform();
+}
+
+bool KisTransformMaskAdapter::isAffine() const
+{
+    return m_d->args.mode() == ToolTransformArgs::FREE_TRANSFORM ||
+        m_d->args.mode() == ToolTransformArgs::PERSPECTIVE_4POINT;
+}
+
+bool KisTransformMaskAdapter::isHidden() const
+{
+    return false;
+}
+
+void KisTransformMaskAdapter::transformDevice(KisNodeSP node, KisPaintDeviceSP src, KisPaintDeviceSP dst) const
+{
+    dst->makeCloneFrom(src, src->extent());
+
+    KisProcessingVisitor::ProgressHelper helper(node);
+    KisTransformUtils::transformDevice(m_d->args, dst, &helper);
+}
+
+const ToolTransformArgs& KisTransformMaskAdapter::savedArgs() const
+{
+    return m_d->args;
+}
+
diff --git a/krita/plugins/tools/tool_transform2/kis_transform_mask_adapter.h b/krita/plugins/tools/tool_transform2/kis_transform_mask_adapter.h
new file mode 100644
index 0000000..5963310
--- /dev/null
+++ b/krita/plugins/tools/tool_transform2/kis_transform_mask_adapter.h
@@ -0,0 +1,47 @@
+/*
+ *  Copyright (c) 2014 Dmitry Kazakov <dimula73 at gmail.com>
+ *
+ *  This program is free software; you can redistribute it and/or modify
+ *  it under the terms of the GNU General Public License as published by
+ *  the Free Software Foundation; either version 2 of the License, or
+ *  (at your option) any later version.
+ *
+ *  This program is distributed in the hope that it will be useful,
+ *  but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ *  GNU General Public License for more details.
+ *
+ *  You should have received a copy of the GNU General Public License
+ *  along with this program; if not, write to the Free Software
+ *  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+
+#ifndef __KIS_TRANSFORM_MASK_ADAPTER_H
+#define __KIS_TRANSFORM_MASK_ADAPTER_H
+
+#include <QScopedPointer>
+#include "kis_transform_mask_params_interface.h"
+
+class ToolTransformArgs;
+
+
+class KisTransformMaskAdapter : public KisTransformMaskParamsInterface
+{
+public:
+    KisTransformMaskAdapter(const ToolTransformArgs &args);
+    ~KisTransformMaskAdapter();
+
+    QTransform finalAffineTransform() const;
+    bool isAffine() const;
+    bool isHidden() const;
+
+    void transformDevice(KisNodeSP node, KisPaintDeviceSP src, KisPaintDeviceSP dst) const;
+
+    const ToolTransformArgs& savedArgs() const;
+
+private:
+    struct Private;
+    const QScopedPointer<Private> m_d;
+};
+
+#endif /* __KIS_TRANSFORM_MASK_ADAPTER_H */
diff --git a/krita/plugins/tools/tool_transform2/kis_transform_utils.cpp b/krita/plugins/tools/tool_transform2/kis_transform_utils.cpp
index cf037fd..5f908ec 100644
--- a/krita/plugins/tools/tool_transform2/kis_transform_utils.cpp
+++ b/krita/plugins/tools/tool_transform2/kis_transform_utils.cpp
@@ -148,3 +148,107 @@ bool KisTransformUtils::checkImageTooBig(const QRectF &bounds, const MatricesPac
 
     return imageTooBig;
 }
+
+#include <kis_transform_worker.h>
+#include <kis_perspectivetransform_worker.h>
+#include <kis_warptransform_worker.h>
+#include <kis_cage_transform_worker.h>
+#include <kis_liquify_transform_worker.h>
+
+KisTransformWorker KisTransformUtils::createTransformWorker(const ToolTransformArgs &config,
+                                                            KisPaintDeviceSP device,
+                                                            KoUpdaterPtr updater,
+                                                            QVector3D *transformedCenter /* OUT */)
+{
+    {
+        KisTransformWorker t(0,
+                             config.scaleX(), config.scaleY(),
+                             config.shearX(), config.shearY(),
+                             config.originalCenter().x(),
+                             config.originalCenter().y(),
+                             config.aZ(),
+                             0, // set X and Y translation
+                             0, // to null for calculation
+                             0,
+                             config.filter());
+
+        *transformedCenter = QVector3D(t.transform().map(config.originalCenter()));
+    }
+
+    QPointF translation = config.transformedCenter() - (*transformedCenter).toPointF();
+
+    KisTransformWorker transformWorker(device,
+                                       config.scaleX(), config.scaleY(),
+                                       config.shearX(), config.shearY(),
+                                       config.originalCenter().x(),
+                                       config.originalCenter().y(),
+                                       config.aZ(),
+                                       (int)(translation.x()),
+                                       (int)(translation.y()),
+                                       updater,
+                                       config.filter());
+
+    return transformWorker;
+}
+
+void KisTransformUtils::transformDevice(const ToolTransformArgs &config,
+                                        KisPaintDeviceSP device,
+                                        KisProcessingVisitor::ProgressHelper *helper)
+{
+    if (config.mode() == ToolTransformArgs::WARP) {
+        KoUpdaterPtr updater = helper->updater();
+
+        KisWarpTransformWorker worker(config.warpType(),
+                                      device,
+                                      config.origPoints(),
+                                      config.transfPoints(),
+                                      config.alpha(),
+                                      updater);
+        worker.run();
+    } else if (config.mode() == ToolTransformArgs::CAGE) {
+        KoUpdaterPtr updater = helper->updater();
+
+        KisCageTransformWorker worker(device,
+                                      config.origPoints(),
+                                      updater,
+                                      8);
+
+        worker.prepareTransform();
+        worker.setTransformedCage(config.transfPoints());
+        worker.run();
+    } else if (config.mode() == ToolTransformArgs::LIQUIFY) {
+        KoUpdaterPtr updater = helper->updater();
+        //FIXME:
+        Q_UNUSED(updater);
+
+        config.liquifyWorker()->run(device);
+    } else {
+        QVector3D transformedCenter;
+        KoUpdaterPtr updater1 = helper->updater();
+        KoUpdaterPtr updater2 = helper->updater();
+
+        KisTransformWorker transformWorker =
+            createTransformWorker(config, device, updater1, &transformedCenter);
+
+        transformWorker.run();
+
+        if (config.mode() == ToolTransformArgs::FREE_TRANSFORM) {
+            KisPerspectiveTransformWorker perspectiveWorker(device,
+                                                            config.transformedCenter(),
+                                                            config.aX(),
+                                                            config.aY(),
+                                                            config.cameraPos().z(),
+                                                            updater2);
+            perspectiveWorker.run();
+        } else if (config.mode() == ToolTransformArgs::PERSPECTIVE_4POINT) {
+            QTransform T =
+                QTransform::fromTranslate(config.transformedCenter().x(),
+                                          config.transformedCenter().y());
+
+            KisPerspectiveTransformWorker perspectiveWorker(device,
+                                                            T.inverted() * config.flattenedPerspectiveTransform() * T,
+                                                            updater2);
+            perspectiveWorker.run();
+        }
+    }
+}
diff --git a/krita/plugins/tools/tool_transform2/kis_transform_utils.h b/krita/plugins/tools/tool_transform2/kis_transform_utils.h
index 97e52a8..ab788d1 100644
--- a/krita/plugins/tools/tool_transform2/kis_transform_utils.h
+++ b/krita/plugins/tools/tool_transform2/kis_transform_utils.h
@@ -25,8 +25,10 @@
 
 #include <QTransform>
 #include <QMatrix4x4>
+#include <kis_processing_visitor.h>
 
 class ToolTransformArgs;
+class KisTransformWorker;
 
 class KisTransformUtils
 {
@@ -73,6 +75,15 @@ public:
     };
 
     static bool checkImageTooBig(const QRectF &bounds, const MatricesPack &m);
+
+    static KisTransformWorker createTransformWorker(const ToolTransformArgs &config,
+                                                    KisPaintDeviceSP device,
+                                                    KoUpdaterPtr updater,
+                                                    QVector3D *transformedCenter /* OUT */);
+
+    static void transformDevice(const ToolTransformArgs &config,
+                                KisPaintDeviceSP device,
+                                KisProcessingVisitor::ProgressHelper *helper);
 };
 
 #endif /* __KIS_TRANSFORM_UTILS_H */
diff --git a/krita/plugins/tools/tool_transform2/strokes/transform_stroke_strategy.cpp b/krita/plugins/tools/tool_transform2/strokes/transform_stroke_strategy.cpp
index 6fb1e17..28391a5 100644
--- a/krita/plugins/tools/tool_transform2/strokes/transform_stroke_strategy.cpp
+++ b/krita/plugins/tools/tool_transform2/strokes/transform_stroke_strategy.cpp
@@ -29,10 +29,47 @@
 #include <kis_transaction.h>
 #include <kis_painter.h>
 #include <kis_transform_worker.h>
-#include <kis_perspectivetransform_worker.h>
-#include <kis_warptransform_worker.h>
-#include <kis_cage_transform_worker.h>
-#include <kis_liquify_transform_worker.h>
+#include <kis_transform_mask.h>
+#include "kis_transform_mask_adapter.h"
+#include "kis_transform_utils.h"
+
+
+
+class ModifyTransformMaskCommand : public KUndo2Command {
+public:
+    ModifyTransformMaskCommand(KisTransformMaskSP mask, KisTransformMaskParamsInterfaceSP params)
+        : m_mask(mask),
+          m_params(params),
+          m_oldParams(m_mask->transformParams())
+        {
+        }
+
+    void redo() {
+        m_mask->setTransformParams(m_params);
+
+        /**
+         * NOTE: this code "duplicates" the functionality provided
+         * by KisRecalculateTransformMaskJob, but there is not much
+         * reason for starting a separate stroke when a transformation
+         * has happened
+         */
+
+        m_mask->recaclulateStaticImage();
+        m_mask->setDirty();
+    }
+
+    void undo() {
+        m_mask->setTransformParams(m_oldParams);
+
+        m_mask->recaclulateStaticImage();
+        m_mask->setDirty();
+    }
+
+private:
+    KisTransformMaskSP m_mask;
+    KisTransformMaskParamsInterfaceSP m_params;
+    KisTransformMaskParamsInterfaceSP m_oldParams;
+};
 
 
 TransformStrokeStrategy::TransformStrokeStrategy(KisNodeSP rootNode,
@@ -42,7 +79,22 @@ TransformStrokeStrategy::TransformStrokeStrategy(KisNodeSP rootNode,
       m_selection(selection)
 {
     if (rootNode->childCount() || !rootNode->paintDevice()) {
-        m_previewDevice = createDeviceCache(rootNode->projection());
+        KisPaintDeviceSP device;
+
+        if (dynamic_cast<KisTransformMask*>(rootNode.data())) {
+            KisNodeSP parentNode = rootNode->parent();
+            device = parentNode->paintDevice();
+
+            if (!device) {
+                device = parentNode->projection();
+            }
+
+        } else {
+            device = rootNode->projection();
+        }
+
+        m_previewDevice = createDeviceCache(device);
+
     } else {
         m_previewDevice = createDeviceCache(rootNode->paintDevice());
         putDeviceCache(rootNode->paintDevice(), m_previewDevice);
@@ -101,42 +153,6 @@ KisPaintDeviceSP TransformStrokeStrategy::getDeviceCache(KisPaintDeviceSP src)
     return cache;
 }
 
-KisTransformWorker createTransformWorker(const ToolTransformArgs &config,
-                                         KisPaintDeviceSP device,
-                                         KoUpdaterPtr updater,
-                                         QVector3D *transformedCenter /* OUT */)
-{
-    {
-        KisTransformWorker t(0,
-                             config.scaleX(), config.scaleY(),
-                             config.shearX(), config.shearY(),
-                             config.originalCenter().x(),
-                             config.originalCenter().y(),
-                             config.aZ(),
-                             0, // set X and Y translation
-                             0, // to null for calculation
-                             0,
-                             config.filter());
-
-        *transformedCenter = QVector3D(t.transform().map(config.originalCenter()));
-    }
-
-    QPointF translation = config.transformedCenter() - (*transformedCenter).toPointF();
-
-    KisTransformWorker transformWorker(device,
-                                       config.scaleX(), config.scaleY(),
-                                       config.shearX(), config.shearY(),
-                                       config.originalCenter().x(),
-                                       config.originalCenter().y(),
-                                       config.aZ(),
-                                       (int)(translation.x()),
-                                       (int)(translation.y()),
-                                       updater,
-                                       config.filter());
-
-    return transformWorker;
-}
-
 bool TransformStrokeStrategy::checkBelongsToSelection(KisPaintDeviceSP device) const
 {
     return m_selection &&
@@ -172,21 +188,31 @@ void TransformStrokeStrategy::doStrokeCallback(KisStrokeJobData *data)
             } if (KisExternalLayer *extLayer =
                   dynamic_cast<KisExternalLayer*>(td->node.data())) {
 
-                if (td->config.mode() == ToolTransformArgs::WARP) {
-                    qWarning() << "Warp transform of an external layer is not supported:" << extLayer->name();
-                } else {
+                if (td->config.mode() == ToolTransformArgs::FREE_TRANSFORM ||
+                    td->config.mode() == ToolTransformArgs::PERSPECTIVE_4POINT) {
+
                     if (td->config.aX() || td->config.aY()) {
                         qWarning() << "Perspective transform of an external layer is not supported:" << extLayer->name();
                     }
 
                     QVector3D transformedCenter;
-                    KisTransformWorker w = createTransformWorker(td->config, 0, 0, &transformedCenter);
+                    KisTransformWorker w = KisTransformUtils::createTransformWorker(td->config, 0, 0, &transformedCenter);
                     QTransform t = w.transform();
 
                     runAndSaveCommand(KUndo2CommandSP(extLayer->transform(t)),
                                       KisStrokeJobData::CONCURRENT,
-                                      KisStrokeJobData::NORMAL);;
+                                      KisStrokeJobData::NORMAL);
                 }
+
+            } else if (KisTransformMask *transformMask =
+                       dynamic_cast<KisTransformMask*>(td->node.data())) {
+
+                runAndSaveCommand(KUndo2CommandSP(
+                                      new ModifyTransformMaskCommand(transformMask,
+                                                                     KisTransformMaskParamsInterfaceSP(
+                                                                         new KisTransformMaskAdapter(td->config)))),
+                                  KisStrokeJobData::CONCURRENT,
+                                  KisStrokeJobData::NORMAL);
             }
         } else if (m_selection) {
 
@@ -197,9 +223,9 @@ void TransformStrokeStrategy::doStrokeCallback(KisStrokeJobData *data)
             KisTransaction transaction(m_selection->pixelSelection());
 
             KisProcessingVisitor::ProgressHelper helper(td->node);
-            transformDevice(td->config,
-                            m_selection->pixelSelection(),
-                            &helper);
+            KisTransformUtils::transformDevice(td->config,
+                                               m_selection->pixelSelection(),
+                                               &helper);
 
             runAndSaveCommand(KUndo2CommandSP(transaction.endAndTake()),
                               KisStrokeJobData::CONCURRENT,
@@ -212,6 +238,15 @@ void TransformStrokeStrategy::doStrokeCallback(KisStrokeJobData *data)
                 putDeviceCache(device, createDeviceCache(device));
             }
             clearSelection(device);
+        } else if (KisTransformMask *transformMask =
+                   dynamic_cast<KisTransformMask*>(csd->node.data())) {
+
+            runAndSaveCommand(KUndo2CommandSP(
+                                  new ModifyTransformMaskCommand(transformMask,
+                                                                 KisTransformMaskParamsInterfaceSP(
+                                                                     new KisDumbTransformMaskParams(true)))),
+                                  KisStrokeJobData::SEQUENTIAL,
+                                  KisStrokeJobData::NORMAL);
         }
     } else {
         KisStrokeStrategyUndoCommandBased::doStrokeCallback(data);
@@ -240,7 +275,7 @@ void TransformStrokeStrategy::transformAndMergeDevice(const ToolTransformArgs &c
 {
     KoUpdaterPtr mergeUpdater = src != dst ? helper->updater() : 0;
 
-    transformDevice(config, src, helper);
+    KisTransformUtils::transformDevice(config, src, helper);
     if (src != dst) {
         QRect mergeRect = src->extent();
         KisPainter painter(dst);
@@ -249,65 +284,3 @@ void TransformStrokeStrategy::transformAndMergeDevice(const ToolTransformArgs &c
         painter.end();
     }
 }
-
-void TransformStrokeStrategy::transformDevice(const ToolTransformArgs &config,
-                                              KisPaintDeviceSP device,
-                                              KisProcessingVisitor::ProgressHelper *helper)
-{
-    if (config.mode() == ToolTransformArgs::WARP) {
-        KoUpdaterPtr updater = helper->updater();
-
-        KisWarpTransformWorker worker(config.warpType(),
-                                      device,
-                                      config.origPoints(),
-                                      config.transfPoints(),
-                                      config.alpha(),
-                                      updater);
-        worker.run();
-    } else if (config.mode() == ToolTransformArgs::CAGE) {
-        KoUpdaterPtr updater = helper->updater();
-
-        KisCageTransformWorker worker(device,
-                                      config.origPoints(),
-                                      updater,
-                                      8);
-
-        worker.prepareTransform();
-        worker.setTransformedCage(config.transfPoints());
-        worker.run();
-    } else if (config.mode() == ToolTransformArgs::LIQUIFY) {
-        KoUpdaterPtr updater = helper->updater();
-        //FIXME:
-        Q_UNUSED(updater);
-
-        config.liquifyWorker()->run(device);
-    } else {
-        QVector3D transformedCenter;
-        KoUpdaterPtr updater1 = helper->updater();
-        KoUpdaterPtr updater2 = helper->updater();
-
-        KisTransformWorker transformWorker =
-            createTransformWorker(config, device, updater1, &transformedCenter);
-
-        transformWorker.run();
-
-        if (config.mode() == ToolTransformArgs::FREE_TRANSFORM) {
-            KisPerspectiveTransformWorker perspectiveWorker(device,
-                                                            config.transformedCenter(),
-                                                            config.aX(),
-                                                            config.aY(),
-                                                            config.cameraPos().z(),
-                                                            updater2);
-            perspectiveWorker.run();
-        } else if (config.mode() == ToolTransformArgs::PERSPECTIVE_4POINT) {
-            QTransform T =
-                QTransform::fromTranslate(config.transformedCenter().x(),
-                                          config.transformedCenter().y());
-
-            KisPerspectiveTransformWorker perspectiveWorker(device,
-                                                            T.inverted() * config.flattenedPerspectiveTransform() * T,
-                                                            updater2);
-            perspectiveWorker.run();
-        }
-    }
-}
diff --git a/krita/plugins/tools/tool_transform2/strokes/transform_stroke_strategy.h b/krita/plugins/tools/tool_transform2/strokes/transform_stroke_strategy.h
index 308336b..466fb86 100644
--- a/krita/plugins/tools/tool_transform2/strokes/transform_stroke_strategy.h
+++ b/krita/plugins/tools/tool_transform2/strokes/transform_stroke_strategy.h
@@ -106,6 +106,7 @@ private:
     QHash<KisPaintDevice*, KisPaintDeviceSP> m_devicesCacheHash;
 
     KisPaintDeviceSP m_previewDevice;
+    KisTransformMaskSP writeToTransformMask;
 };
 
 #endif /* __TRANSFORM_STROKE_STRATEGY_H */
diff --git a/krita/plugins/tools/tool_transform2/transform_transaction_properties.h b/krita/plugins/tools/tool_transform2/transform_transaction_properties.h
index 71be742..82dc2b8 100644
--- a/krita/plugins/tools/tool_transform2/transform_transaction_properties.h
+++ b/krita/plugins/tools/tool_transform2/transform_transaction_properties.h
@@ -52,7 +52,7 @@ TransformTransactionProperties(const QRectF &originalRect, ToolTransformArgs *cu
         return m_originalRect;
     }
 
-    QPointF originalCenter() const {
+    QPointF originalCenterGeometric() const {
         return m_originalRect.center();
     }
 
diff --git a/krita/sdk/tests/testutil.h b/krita/sdk/tests/testutil.h
index 51060cd..651a18f 100644
--- a/krita/sdk/tests/testutil.h
+++ b/krita/sdk/tests/testutil.h
@@ -402,8 +402,8 @@ namespace TestUtil {
 
 struct MaskParent
 {
-    MaskParent()
-        : imageRect(0,0,512,512) {
+    MaskParent(const QRect &_imageRect = QRect(0,0,512,512))
+        : imageRect(_imageRect) {
         const KoColorSpace * cs = KoColorSpaceRegistry::instance()->rgb8();
         image = new KisImage(new KisSurrogateUndoStore(), imageRect.width(), imageRect.height(), cs, "test image");
         layer = new KisPaintLayer(image, "paint1", OPACITY_OPAQUE_U8);
diff --git a/krita/ui/kis_mask_manager.cc b/krita/ui/kis_mask_manager.cc
index 266d4d2..ec702d5 100644
--- a/krita/ui/kis_mask_manager.cc
+++ b/krita/ui/kis_mask_manager.cc
@@ -33,6 +33,7 @@
 #include <kis_clone_layer.h>
 #include <kis_group_layer.h>
 #include <kis_filter_mask.h>
+#include <kis_transform_mask.h>
 #include <kis_transparency_mask.h>
 #include <kis_selection_mask.h>
 #include <kis_effect_mask.h>
@@ -140,7 +141,7 @@ void KisMaskManager::adjustMaskPosition(KisNodeSP node, KisNodeSP activeNode, bo
     }
 }
 
-void KisMaskManager::createMaskCommon(KisMaskSP mask, KisNodeSP activeNode, KisPaintDeviceSP copyFrom, const KUndo2MagicString& macroName, const QString &nodeType, const QString &nodeName)
+void KisMaskManager::createMaskCommon(KisMaskSP mask, KisNodeSP activeNode, KisPaintDeviceSP copyFrom, const KUndo2MagicString& macroName, const QString &nodeType, const QString &nodeName, bool suppressSelection)
 {
     m_commandsAdapter->beginMacro(macroName);
 
@@ -151,10 +152,12 @@ void KisMaskManager::createMaskCommon(KisMaskSP mask, KisNodeSP activeNode, KisP
     KisLayerSP parentLayer = dynamic_cast<KisLayer*>(parent.data());
     Q_ASSERT(parentLayer);
 
-    if (copyFrom) {
-        mask->initSelection(copyFrom, parentLayer);
-    } else {
-        mask->initSelection(m_view->selection(), parentLayer);
+    if (!suppressSelection) {
+        if (copyFrom) {
+            mask->initSelection(copyFrom, parentLayer);
+        } else {
+            mask->initSelection(m_view->selection(), parentLayer);
+        }
     }
 
     //counting number of KisSelectionMask
@@ -221,6 +224,12 @@ void KisMaskManager::createFilterMask(KisNodeSP activeNode, KisPaintDeviceSP cop
     }
 }
 
+void KisMaskManager::createTransformMask(KisNodeSP activeNode)
+{
+    KisTransformMaskSP mask = new KisTransformMask();
+    createMaskCommon(mask, activeNode, 0, kundo2_i18n("Add Transform Mask"), "KisTransformMask", i18n("Transform Mask"), true);
+}
+
 void KisMaskManager::duplicateMask()
 {
     if (!m_activeMask) return;
diff --git a/krita/ui/kis_mask_manager.h b/krita/ui/kis_mask_manager.h
index 9e8abec..4eb444f 100644
--- a/krita/ui/kis_mask_manager.h
+++ b/krita/ui/kis_mask_manager.h
@@ -115,10 +115,11 @@ private:
     void activateMask(KisMaskSP mask);
 
     void adjustMaskPosition(KisNodeSP node, KisNodeSP activeNode, bool avoidActiveNode, KisNodeSP &parent, KisNodeSP &above);
-    void createMaskCommon(KisMaskSP mask, KisNodeSP activeNode, KisPaintDeviceSP copyFrom, const KUndo2MagicString &macroName, const QString &nodeType, const QString &nodeName);
+    void createMaskCommon(KisMaskSP mask, KisNodeSP activeNode, KisPaintDeviceSP copyFrom, const KUndo2MagicString &macroName, const QString &nodeType, const QString &nodeName, bool suppressSelection = false);
 
     void createSelectionMask(KisNodeSP activeNode, KisPaintDeviceSP copyFrom);
     void createFilterMask(KisNodeSP activeNode, KisPaintDeviceSP copyFrom, bool quiet = false);
+    void createTransformMask(KisNodeSP activeNode);
     void createTransparencyMask(KisNodeSP activeNode, KisPaintDeviceSP copyFrom);
 
     KisView2 * m_view;
diff --git a/krita/ui/kis_node_manager.cpp b/krita/ui/kis_node_manager.cpp
index 75a3ece..d462bd3 100644
--- a/krita/ui/kis_node_manager.cpp
+++ b/krita/ui/kis_node_manager.cpp
@@ -262,6 +262,9 @@ void KisNodeManager::setup(KActionCollection * actionCollection, KisActionManage
     NEW_MASK_ACTION("add_new_filter_mask", i18n("&Filter Mask..."),
                     "KisFilterMask", koIcon("bookmarks"));
 
+    NEW_MASK_ACTION("add_new_transform_mask", i18n("&Transform Mask..."),
+                    "KisTransformMask", koIcon("bookmarks"));
+
     NEW_MASK_ACTION("add_new_selection_mask", i18n("&Local Selection"),
                     "KisSelectionMask", koIcon("edit-paste"));
 
@@ -446,6 +449,8 @@ void KisNodeManager::createNode(const QString & nodeType, bool quiet)
         m_d->maskManager->createTransparencyMask(activeNode, 0);
     } else if (nodeType == "KisFilterMask") {
         m_d->maskManager->createFilterMask(activeNode, 0, quiet);
+    } else if (nodeType == "KisTransformMask") {
+        m_d->maskManager->createTransformMask(activeNode);
     } else if (nodeType == "KisSelectionMask") {
         m_d->maskManager->createSelectionMask(activeNode, 0);
     } else if (nodeType == "KisFileLayer") {
diff --git a/krita/ui/widgets/kis_scratch_pad.cpp b/krita/ui/widgets/kis_scratch_pad.cpp
index 6c85e67..bdc3c20 100644
--- a/krita/ui/widgets/kis_scratch_pad.cpp
+++ b/krita/ui/widgets/kis_scratch_pad.cpp
@@ -294,7 +294,7 @@ void KisScratchPad::paintEvent ( QPaintEvent * event ) {
 
     QPointF offset = alignedImageRect.topLeft();
 
-    m_paintLayer->updateProjection(alignedImageRect);
+    m_paintLayer->updateProjection(alignedImageRect, KisNode::N_FILTHY);
     KisPaintDeviceSP projection = m_paintLayer->projection();
 
     QImage image = projection->convertToQImage(m_displayProfile,


More information about the kimageshop mailing list