Krita Source Code Documentation
Loading...
Searching...
No Matches
kis_liquify_transform_worker.cpp
Go to the documentation of this file.
1/*
2 * SPDX-FileCopyrightText: 2014 Dmitry Kazakov <dimula73@gmail.com>
3 *
4 * SPDX-License-Identifier: GPL-2.0-or-later
5 */
6
8
9#include <KoColorSpace.h>
11#include "kis_dom_utils.h"
12#include "krita_utils.h"
13#include "KisSpatialContainer.h"
14
15
17{
18 Private(const QRect &_srcBounds,
19 KoUpdater *_progress,
20 int _pixelPrecision)
21 : srcBounds(_srcBounds),
22 progress(_progress),
23 pixelPrecision(_pixelPrecision)
24 , originalPointsContainer(_srcBounds)
25 , transformedPointsContainer(_srcBounds)
26 {
27 }
28
29 QRect srcBounds;
30
33
36
38
41 QSize gridSize;
42
44
45 struct MapIndexesOp;
46
47 template <class ProcessOp>
49 const QPointF &base,
50 qreal sigma);
51
52 template <class ProcessOp>
54 const QPointF &base,
55 qreal sigma,
56 qreal flow);
57
58 template <class ProcessOp>
59 void processTransformedPixels(ProcessOp op,
60 const QPointF &base,
61 qreal sigma,
62 bool useWashMode,
63 qreal flow);
64};
65
67 KoUpdater *progress,
68 int pixelPrecision)
69 : m_d(new Private(srcBounds, progress, pixelPrecision))
70{
72
73 // TODO: implement 'progress' stuff
74 m_d->preparePoints();
75}
76
81
85
87{
88 bool result =
89 m_d->srcBounds == other.m_d->srcBounds &&
90 m_d->pixelPrecision == other.m_d->pixelPrecision &&
91 m_d->gridSize == other.m_d->gridSize &&
92 m_d->originalPoints.size() == other.m_d->originalPoints.size() &&
93 m_d->transformedPoints.size() == other.m_d->transformedPoints.size();
94
95 if (!result) return false;
96
97 const qreal eps = 1e-6;
98
99 result =
100 KisAlgebra2D::fuzzyPointCompare(m_d->originalPoints, other.m_d->originalPoints, eps) &&
101 KisAlgebra2D::fuzzyPointCompare(m_d->transformedPoints, other.m_d->transformedPoints, eps);
102
103 return result;
104}
105
107{
108 const qreal eps = 1e-6;
109 return KisAlgebra2D::fuzzyPointCompare(m_d->originalPoints, m_d->transformedPoints, eps);
110}
111
113{
114 return GridIterationTools::pointToIndex(cellPt, m_d->gridSize);
115}
116
118{
119 return m_d->gridSize;
120}
121
123{
124 return m_d->originalPoints;
125}
126
128{
129 return m_d->transformedPoints;
130}
131
133{
134 AllPointsFetcherOp(QRectF srcRect) : m_srcRect(srcRect) {}
135
136 inline void processPoint(int col, int row,
137 int prevCol, int prevRow,
138 int colIndex, int rowIndex) {
139
140 Q_UNUSED(prevCol);
141 Q_UNUSED(prevRow);
142 Q_UNUSED(colIndex);
143 Q_UNUSED(rowIndex);
144
145 QPointF pt(col, row);
146 m_points << pt;
147 }
148
149 inline void nextLine() {
150 }
151
153 QRectF m_srcRect;
154};
155
156void KisLiquifyTransformWorker::Private::preparePoints()
157{
158 gridSize =
159 GridIterationTools::calcGridSize(srcBounds, pixelPrecision);
160
161 AllPointsFetcherOp pointsOp(srcBounds);
162 GridIterationTools::processGrid(pointsOp, srcBounds, pixelPrecision);
163
164 const int numPoints = pointsOp.m_points.size();
165
166 KIS_ASSERT_RECOVER_RETURN(numPoints == gridSize.width() * gridSize.height());
167
168 originalPoints = pointsOp.m_points;
169 transformedPoints = pointsOp.m_points;
170
171 originalPointsContainer.initializeWithGridPoints(srcBounds, pixelPrecision);
172 transformedPointsContainer.initializeWithGridPoints(srcBounds, pixelPrecision);
173
174}
175
176void KisLiquifyTransformWorker::translate(const QPointF &offset)
177{
178 KIS_ASSERT_RECOVER_RETURN(m_d->originalPoints.size() ==
179 m_d->transformedPoints.size());
180
181 // TODO: make it within Spatial Container, either a hidden offset, or just offsetting all points at once
182 // and benchmark
183 for (int i = 0; i < m_d->transformedPoints.count(); i++) {
184 m_d->originalPointsContainer.movePoint(i, m_d->originalPoints[i], m_d->originalPoints[i] + offset);
185 m_d->transformedPointsContainer.movePoint(i, m_d->transformedPoints[i], m_d->transformedPoints[i] + offset);
186
187 m_d->originalPoints[i] += offset;
188 m_d->transformedPoints[i] += offset;
189 }
190}
191
193{
194 // TODO: make it within Spatial Container, either a hidden offset, or just offsetting all points at once
195 // and benchmark
196 for (int i = 0; i < m_d->transformedPoints.count(); i++) {
197 m_d->transformedPointsContainer.movePoint(i, m_d->transformedPoints[i], m_d->transformedPoints[i] + offset);
198 m_d->transformedPoints[i] += offset;
199 }
200}
201
203 qreal amount,
204 qreal sigma)
205{
206 const qreal maxDistCoeff = 3.0;
207 const qreal maxDist = maxDistCoeff * sigma;
208
209 KIS_ASSERT_RECOVER_RETURN(m_d->originalPoints.size() ==
210 m_d->transformedPoints.size());
211
212 QVector<int> indexes;
213 m_d->transformedPointsContainer.findAllInRange(indexes, base, maxDist);
214 for (int i = 0; i < indexes.count(); i++) {
215
216 QPointF diff = m_d->transformedPoints[indexes[i]] - base;
217 qreal dist = KisAlgebra2D::norm(diff);
218 qreal lambda = exp(-0.5 * pow2(dist / sigma));
219 lambda *= amount;
220
221 QPointF oldPosition = m_d->transformedPoints[indexes[i]];
222 m_d->transformedPoints[indexes[i]] = m_d->originalPoints[indexes[i]] * lambda + m_d->transformedPoints[indexes[i]] * (1.0 - lambda);
223
224 m_d->transformedPointsContainer.movePoint(indexes[i], oldPosition, m_d->transformedPoints[indexes[i]]);
225 }
226}
227
228template <class ProcessOp>
229void KisLiquifyTransformWorker::Private::
230processTransformedPixelsBuildUp(ProcessOp op,
231 const QPointF &base,
232 qreal sigma)
233{
234 const qreal maxDist = ProcessOp::maxDistCoeff * sigma;
235 QRectF clipRect(base.x() - maxDist, base.y() - maxDist,
236 2 * maxDist, 2 * maxDist);
237
238 accumulatedBrushStrokes |= clipRect;
239
240 QVector<int> indexes;
241 transformedPointsContainer.findAllInRange(indexes, base, maxDist);
242
243 for (int i = 0; i < indexes.count(); i++) {
244
245 QPointF diff = transformedPoints[indexes[i]] - base;
246 qreal dist = KisAlgebra2D::norm(diff);
247 if (dist > maxDist) continue;
248
249 const qreal lambda = exp(-0.5 * pow2(dist / sigma));
250 QPointF oldPosition = transformedPoints[indexes[i]];
251 transformedPoints[indexes[i]] = op(transformedPoints[indexes[i]], base, diff, lambda);
252
253
254 transformedPointsContainer.movePoint(indexes[i], oldPosition, transformedPoints[indexes[i]]);
255
256 }
257}
258
259template <class ProcessOp>
260void KisLiquifyTransformWorker::Private::
261processTransformedPixelsWash(ProcessOp op,
262 const QPointF &base,
263 qreal sigma,
264 qreal flow)
265{
266 const qreal maxDist = ProcessOp::maxDistCoeff * sigma;
267 QRectF clipRect(base.x() - maxDist, base.y() - maxDist,
268 2 * maxDist, 2 * maxDist);
269
270 accumulatedBrushStrokes |= clipRect;
271
272 KIS_ASSERT_RECOVER_RETURN(originalPoints.size() ==
273 transformedPoints.size());
274
275 // TODO: remove the originalPointsContainer entirely, and use GridIterationTools to figure out indexes instead
276 // and add unit tests for it
277
278 QVector<int> indexes;
279 originalPointsContainer.findAllInRange(indexes, base, maxDist);
280 for (int i = 0; i < indexes.count(); i++) {
281
282 QPointF diff = originalPoints[indexes[i]] - base;
283 qreal dist = KisAlgebra2D::norm(diff);
284
285 const qreal lambda = exp(-0.5 * pow2(dist / sigma));
286 QPointF dstPt = op(originalPoints[indexes[i]], base, diff, lambda);
287
288 if (kisDistance(dstPt, originalPoints[indexes[i]]) > kisDistance(transformedPoints[indexes[i]], originalPoints[indexes[i]])) {
289 QPointF oldPosition = transformedPoints[indexes[i]];
290 transformedPoints[indexes[i]] = (1.0 - flow) * transformedPoints[indexes[i]] + flow * dstPt;
291
292 transformedPointsContainer.movePoint(indexes[i], oldPosition, transformedPoints[indexes[i]]);
293 }
294 }
295}
296
297template <class ProcessOp>
298void KisLiquifyTransformWorker::Private::
299processTransformedPixels(ProcessOp op,
300 const QPointF &base,
301 qreal sigma,
302 bool useWashMode,
303 qreal flow)
304{
305 if (useWashMode) {
306 processTransformedPixelsWash(op, base, sigma, flow);
307 } else {
308 processTransformedPixelsBuildUp(op, base, sigma);
309 }
310}
311
313{
314 TranslateOp(const QPointF &offset) : m_offset(offset) {}
315
316 QPointF operator() (const QPointF &pt,
317 const QPointF &base,
318 const QPointF &diff,
319 qreal lambda)
320 {
321 Q_UNUSED(base);
322 Q_UNUSED(diff);
323 return pt + lambda * m_offset;
324 }
325
326 static const qreal maxDistCoeff;
327
328 QPointF m_offset;
329};
330
331const qreal TranslateOp::maxDistCoeff = 3.0;
332
334{
335 ScaleOp(qreal scale) : m_scale(scale) {}
336
337 QPointF operator() (const QPointF &pt,
338 const QPointF &base,
339 const QPointF &diff,
340 qreal lambda)
341 {
342 Q_UNUSED(pt);
343 Q_UNUSED(diff);
344 return base + (1.0 + m_scale * lambda) * diff;
345 }
346
347 static const qreal maxDistCoeff;
348
349 qreal m_scale;
350};
351
352const qreal ScaleOp::maxDistCoeff = 3.0;
353
355{
356 RotateOp(qreal angle) : m_angle(angle) {}
357
358 QPointF operator() (const QPointF &pt,
359 const QPointF &base,
360 const QPointF &diff,
361 qreal lambda)
362 {
363 Q_UNUSED(pt);
364
365 const qreal angle = m_angle * lambda;
366 const qreal sinA = std::sin(angle);
367 const qreal cosA = std::cos(angle);
368
369 qreal x = cosA * diff.x() + sinA * diff.y();
370 qreal y = -sinA * diff.x() + cosA * diff.y();
371
372 return base + QPointF(x, y);
373 }
374
375 static const qreal maxDistCoeff;
376
377 qreal m_angle;
378};
379
380const qreal RotateOp::maxDistCoeff = 3.0;
381
383 const QPointF &offset,
384 qreal sigma,
385 bool useWashMode,
386 qreal flow)
387{
388 TranslateOp op(offset);
389 m_d->processTransformedPixels(op, base, sigma, useWashMode, flow);
390}
391
393 qreal scale,
394 qreal sigma,
395 bool useWashMode,
396 qreal flow)
397{
398 ScaleOp op(scale);
399 m_d->processTransformedPixels(op, base, sigma, useWashMode, flow);
400}
401
403 qreal angle,
404 qreal sigma,
405 bool useWashMode,
406 qreal flow)
407{
408 RotateOp op(angle);
409 m_d->processTransformedPixels(op, base, sigma, useWashMode, flow);
410}
411
413{
414 KIS_SAFE_ASSERT_RECOVER_RETURN(*srcDevice->colorSpace() == *dstDevice->colorSpace());
415
416 dstDevice->clear();
417
418 using namespace GridIterationTools;
419 QRect correctSubGrid = calculateCorrectSubGrid(m_d->srcBounds, m_d->pixelPrecision, m_d->accumulatedBrushStrokes, m_d->gridSize);
420
421 PaintDevicePolygonOp polygonOp(srcDevice, dstDevice);
422 RegularGridIndexesOp indexesOp(m_d->gridSize);
423
424 bool canMergeRects = GridIterationTools::canProcessRectsInRandomOrder(indexesOp, m_d->transformedPoints, correctSubGrid);
425 polygonOp.setCanMergeRects(canMergeRects);
426
427 iterateThroughGrid<AlwaysCompletePolygonPolicy>(polygonOp, indexesOp,
428 m_d->gridSize,
429 m_d->originalPoints,
430 m_d->transformedPoints,
431 correctSubGrid);
432 QList<QRectF> areasToCopy = cutOutSubgridFromBounds(correctSubGrid, m_d->srcBounds, m_d->gridSize, m_d->originalPoints);
433 for (int i = 0; i < areasToCopy.length(); i++) {
434 polygonOp.fastCopyArea(areasToCopy[i].toRect(), false);
435 }
436}
437
439{
440 const qreal margin = 0.05;
441 QRect resultRect = m_d->transformedPointsContainer.exactBounds().toRect();
442 return KisAlgebra2D::blowRect(resultRect | rc, margin);
443}
444
445QRect KisLiquifyTransformWorker::approxNeedRect(const QRect &rc, const QRect &fullBounds)
446{
447 Q_UNUSED(rc);
448 return fullBounds;
449}
450
452{
453 return m_d->accumulatedBrushStrokes;
454}
455
457{
458 KIS_SAFE_ASSERT_RECOVER_RETURN(t.type() <= QTransform::TxScale);
459
460 m_d->srcBounds = t.mapRect(m_d->srcBounds);
461
462 // TODO: do it within Spatial Container
463 for (int i = 0; i < m_d->transformedPoints.count(); i++) {
464 m_d->originalPointsContainer.movePoint(i, m_d->originalPoints[i], t.map(m_d->originalPoints[i]));
465 m_d->transformedPointsContainer.movePoint(i, m_d->transformedPoints[i], t.map(m_d->transformedPoints[i]));
466
467 m_d->originalPoints[i] = t.map(m_d->originalPoints[i]);
468 m_d->transformedPoints[i] = t.map(m_d->transformedPoints[i]);
469 }
470}
471
472#include <functional>
473#include <QTransform>
474
475using PointMapFunction = std::function<QPointF (const QPointF&)>;
476
477
478PointMapFunction bindPointMapTransform(const QTransform &transform) {
479 using namespace std::placeholders;
480
481 typedef QPointF (QTransform::*MapFuncType)(const QPointF&) const;
482 return std::bind(static_cast<MapFuncType>(&QTransform::map), &transform, _1);
483}
484
485QImage KisLiquifyTransformWorker::runOnQImage(const QImage &srcImage,
486 const QPointF &srcImageOffset,
487 const QTransform &imageToThumbTransform,
488 QPointF *newOffset)
489{
490 KIS_ASSERT_RECOVER(m_d->originalPoints.size() == m_d->transformedPoints.size()) {
491 return QImage();
492 }
493
494 KIS_ASSERT_RECOVER(!srcImage.isNull()) {
495 return QImage();
496 }
497
498 KIS_ASSERT_RECOVER(srcImage.format() == QImage::Format_ARGB32) {
499 return QImage();
500 }
501
502 QVector<QPointF> originalPointsLocal(m_d->originalPoints);
503 QVector<QPointF> transformedPointsLocal(m_d->transformedPoints);
504
505 PointMapFunction mapFunc = bindPointMapTransform(imageToThumbTransform);
506
507 std::transform(originalPointsLocal.begin(), originalPointsLocal.end(),
508 originalPointsLocal.begin(), mapFunc);
509
510 std::transform(transformedPointsLocal.begin(), transformedPointsLocal.end(),
511 transformedPointsLocal.begin(), mapFunc);
512
513 QRectF dstBounds;
514 Q_FOREACH (const QPointF &pt, transformedPointsLocal) {
515 KisAlgebra2D::accumulateBounds(pt, &dstBounds);
516 }
517
518 const QRectF srcBounds(srcImageOffset, srcImage.size());
519 dstBounds |= srcBounds;
520
521 QPointF dstQImageOffset = dstBounds.topLeft();
522 *newOffset = dstQImageOffset;
523
524 QRect dstBoundsI = dstBounds.toAlignedRect();
525
526 QImage dstImage(dstBoundsI.size(), srcImage.format());
527 dstImage.fill(0);
528
529 GridIterationTools::QImagePolygonOp polygonOp(srcImage, dstImage, srcImageOffset, dstQImageOffset);
531
532
533 QRect correctSubGrid = GridIterationTools::calculateCorrectSubGrid(m_d->srcBounds, m_d->pixelPrecision, m_d->accumulatedBrushStrokes, m_d->gridSize);
534 bool canMergeRects = GridIterationTools::canProcessRectsInRandomOrder(indexesOp, m_d->transformedPoints, correctSubGrid);
535 polygonOp.setCanMergeRects(canMergeRects);
536
537
538 GridIterationTools::iterateThroughGrid<GridIterationTools::AlwaysCompletePolygonPolicy>(polygonOp, indexesOp,
539 m_d->gridSize,
540 originalPointsLocal,
541 transformedPointsLocal,
542 correctSubGrid);
543
544
545 QList<QRectF> areasToCopy = GridIterationTools::cutOutSubgridFromBounds(correctSubGrid, m_d->srcBounds, m_d->gridSize, m_d->originalPoints);
546 polygonOp.setCanMergeRects(false);
547 for (int i = 0; i < areasToCopy.length(); i++) {
548 polygonOp.fastCopyArea(imageToThumbTransform.map(QPolygonF(areasToCopy[i])));
549 }
550 return dstImage;
551}
552
553void KisLiquifyTransformWorker::toXML(QDomElement *e) const
554{
555 QDomDocument doc = e->ownerDocument();
556 QDomElement liqEl = doc.createElement("liquify_points");
557 e->appendChild(liqEl);
558
559 KisDomUtils::saveValue(&liqEl, "srcBounds", m_d->srcBounds);
560 KisDomUtils::saveValue(&liqEl, "originalPoints", m_d->originalPoints);
561 KisDomUtils::saveValue(&liqEl, "transformedPoints", m_d->transformedPoints);
562 KisDomUtils::saveValue(&liqEl, "pixelPrecision", m_d->pixelPrecision);
563 KisDomUtils::saveValue(&liqEl, "gridSize", m_d->gridSize);
564}
565
567{
568 QDomElement liquifyEl;
569
570 QRect srcBounds;
573 int pixelPrecision;
574 QSize gridSize;
575
576 bool result = false;
577
578
579 result =
580 KisDomUtils::findOnlyElement(e, "liquify_points", &liquifyEl) &&
581
582 KisDomUtils::loadValue(liquifyEl, "srcBounds", &srcBounds) &&
583 KisDomUtils::loadValue(liquifyEl, "originalPoints", &originalPoints) &&
584 KisDomUtils::loadValue(liquifyEl, "transformedPoints", &transformedPoints) &&
585 KisDomUtils::loadValue(liquifyEl, "pixelPrecision", &pixelPrecision) &&
586 KisDomUtils::loadValue(liquifyEl, "gridSize", &gridSize);
587
588 if (!result) {
589 warnKrita << "WARNING: Failed to load liquify worker from XML";
590 return new KisLiquifyTransformWorker(QRect(0,0,1024, 1024), 0, 8);
591 }
592
595
596 const int numPoints = originalPoints.size();
597
598 if (numPoints != transformedPoints.size() ||
599 numPoints != worker->m_d->originalPoints.size() ||
600 gridSize != worker->m_d->gridSize) {
601 warnKrita << "WARNING: Inconsistent number of points!";
602 warnKrita << ppVar(originalPoints.size());
605 warnKrita << ppVar(worker->m_d->originalPoints.size());
606 warnKrita << ppVar(worker->m_d->transformedPoints.size());
607 warnKrita << ppVar(worker->m_d->gridSize);
608
609 return worker;
610 }
611
612 QRectF changedRect = QRectF();
613
614 for (int i = 0; i < numPoints; i++) {
615 worker->m_d->originalPoints[i] = originalPoints[i];
616 worker->m_d->transformedPoints[i] = transformedPoints[i];
619 }
620 }
621
622 worker->m_d->transformedPointsContainer.initializeWith(worker->m_d->transformedPoints);
623 worker->m_d->originalPointsContainer.initializeWith(worker->m_d->originalPoints);
624
625 worker->m_d->accumulatedBrushStrokes = changedRect;
626
627
628 return worker;
629}
virtual void clear()
const KoColorSpace * colorSpace() const
#define KIS_ASSERT_RECOVER(cond)
Definition kis_assert.h:55
#define KIS_SAFE_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:128
#define KIS_ASSERT_RECOVER_RETURN(cond)
Definition kis_assert.h:75
const qreal eps
#define warnKrita
Definition kis_debug.h:87
#define ppVar(var)
Definition kis_debug.h:155
qreal kisDistance(const QPointF &pt1, const QPointF &pt2)
Definition kis_global.h:190
T pow2(const T &x)
Definition kis_global.h:166
std::function< QPointF(const QPointF &)> PointMapFunction
PointMapFunction bindPointMapTransform(const QTransform &transform)
bool canProcessRectsInRandomOrder(IndexesOp &indexesOp, const QVector< QPointF > &transformedPoints, QSize grid)
QList< QRectF > cutOutSubgridFromBounds(QRect subGrid, QRect srcBounds, const QSize &gridSize, const QVector< QPointF > &originalPoints)
QRect calculateCorrectSubGrid(QRect originalBoundsForGrid, int pixelPrecision, QRectF currentBounds, QSize gridSize)
int pointToIndex(const QPoint &cellPt, const QSize &gridSize)
void processGrid(ProcessCell &cellOp, const QRect &srcBounds, const int pixelPrecision)
QSize calcGridSize(const QRect &srcBounds, const int pixelPrecision)
Rect blowRect(const Rect &rect, qreal coeff)
void accumulateBounds(const Point &pt, Rect *bounds)
qreal norm(const T &a)
bool fuzzyPointCompare(const QPointF &p1, const QPointF &p2)
void saveValue(QDomElement *parent, const QString &tag, const QSize &size)
bool findOnlyElement(const QDomElement &parent, const QString &tag, QDomElement *el, QStringList *errorMessages)
bool loadValue(const QDomElement &e, float *v)
void processPoint(int col, int row, int prevCol, int prevRow, int colIndex, int rowIndex)
void rotatePoints(const QPointF &base, qreal angle, qreal sigma, bool useWashMode, qreal flow)
Private(const QRect &_srcBounds, KoUpdater *_progress, int _pixelPrecision)
KisLiquifyTransformWorker(const QRect &srcBounds, KoUpdater *progress, int pixelPrecision=8)
void translate(const QPointF &offset)
void undoPoints(const QPointF &base, qreal amount, qreal sigma)
void translateDstSpace(const QPointF &offset)
QImage runOnQImage(const QImage &srcImage, const QPointF &srcImageOffset, const QTransform &imageToThumbTransform, QPointF *newOffset)
void transformSrcAndDst(const QTransform &t)
void scalePoints(const QPointF &base, qreal scale, qreal sigma, bool useWashMode, qreal flow)
void processTransformedPixelsWash(ProcessOp op, const QPointF &base, qreal sigma, qreal flow)
static KisLiquifyTransformWorker * fromXML(const QDomElement &e)
const QScopedPointer< Private > m_d
void processTransformedPixelsBuildUp(ProcessOp op, const QPointF &base, qreal sigma)
void processTransformedPixels(ProcessOp op, const QPointF &base, qreal sigma, bool useWashMode, qreal flow)
bool operator==(const KisLiquifyTransformWorker &other) const
void translatePoints(const QPointF &base, const QPointF &offset, qreal sigma, bool useWashMode, qreal flow)
QRect approxNeedRect(const QRect &rc, const QRect &fullBounds)
void run(KisPaintDeviceSP srcDevice, KisPaintDeviceSP dstDevice)
QPointF operator()(const QPointF &pt, const QPointF &base, const QPointF &diff, qreal lambda)
static const qreal maxDistCoeff
static const qreal maxDistCoeff
QPointF operator()(const QPointF &pt, const QPointF &base, const QPointF &diff, qreal lambda)
TranslateOp(const QPointF &offset)
QPointF operator()(const QPointF &pt, const QPointF &base, const QPointF &diff, qreal lambda)
static const qreal maxDistCoeff