Krita Source Code Documentation
Loading...
Searching...
No Matches
kis_free_transform_strategy.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 <QPointF>
10#include <QPainter>
11#include <QPainterPath>
12#include <QMatrix4x4>
13
14#include <KoResourcePaths.h>
15
17#include "tool_transform_args.h"
19#include "krita_utils.h"
20#include "kis_cursor.h"
21#include "kis_transform_utils.h"
23#include "kis_algebra_2d.h"
24
25
26namespace {
27enum StrokeFunction {
28 ROTATE = 0,
29 MOVE,
30 RIGHTSCALE,
31 TOPRIGHTSCALE,
32 TOPSCALE,
33 TOPLEFTSCALE,
34 LEFTSCALE,
35 BOTTOMLEFTSCALE,
36 BOTTOMSCALE,
37 BOTTOMRIGHTSCALE,
38 BOTTOMSHEAR,
39 RIGHTSHEAR,
40 TOPSHEAR,
41 LEFTSHEAR,
42 MOVECENTER,
43 PERSPECTIVE,
44 ROTATEBOUNDS
45};
46}
47
49{
51 const KisCoordinatesConverter *_converter,
52 ToolTransformArgs &_currentArgs,
54 : q(_q),
55 converter(_converter),
56 currentArgs(_currentArgs),
57 transaction(_transaction),
58 imageTooBig(false),
59 isTransforming(false)
60 {
69
70 shearCursorPixmap.load(":/shear_cursor.png");
71 rotateHandlesCursor = QCursor(QPixmap(":/rotate_cursor_handles.xpm"));
72 }
73
75
77
79
84
85
88
91
92 QTransform handlesTransform;
93
95
96 StrokeFunction function {MOVE};
97
98 struct HandlePoints {
99 QPointF topLeft;
100 QPointF topMiddle;
101 QPointF topRight;
102
103 QPointF middleLeft;
105 QPointF middleRight;
106
107 QPointF bottomLeft;
109 QPointF bottomRight;
110 };
112
113 QRectF bounds;
114 QTransform boundsTransform; // Transforms bounds into original image space (rotates by boundsRotation)
115
116 QTransform transform;
117
118 QCursor scaleCursors[8]; // cursors for the 8 directions
121
122 bool imageTooBig {false};
123
125 QPointF clickPos;
126 QTransform clickTransform;
127
128 bool isTransforming {false};
129
130 QCursor getScaleCursor(const QPointF &handlePt);
131 QCursor getShearCursor(const QPointF &start, const QPointF &end);
134 void recalculateBounds();
135};
136
138 KoSnapGuide *snapGuide,
139 ToolTransformArgs &currentArgs,
141 : KisSimplifiedActionPolicyStrategy(converter, snapGuide),
142 m_d(new Private(this, converter, currentArgs, transaction))
143{
144}
145
149
150namespace {
151QPointF middleLeft(const QRectF &rect)
152{
153 return (rect.topLeft() + rect.bottomLeft()) * 0.5;
154}
155QPointF middleRight(const QRectF &rect)
156{
157 return (rect.topRight() + rect.bottomRight()) * 0.5;
158}
159QPointF bottomMiddle(const QRectF &rect)
160{
161 return (rect.bottomLeft() + rect.bottomRight()) * 0.5;
162}
163QPointF topMiddle(const QRectF &rect)
164{
165 return (rect.topLeft() + rect.topRight()) * 0.5;
166}
167}
168
170{
171 const QPolygonF &convexHull = transaction.convexHull();
172 if (!convexHull.isEmpty()) {
173 bounds = boundsTransform.inverted().map(convexHull).boundingRect();
174 } else {
175 bounds = boundsTransform.inverted().mapRect(transaction.originalRect());
176 }
177}
178
179
181{
182 QTransform boundsFullTransform = boundsTransform * transform;
183 transformedHandles.topLeft = boundsFullTransform.map(bounds.topLeft());
184 transformedHandles.topMiddle = boundsFullTransform.map(topMiddle(bounds));
185 transformedHandles.topRight = boundsFullTransform.map(bounds.topRight());
186
187 transformedHandles.middleLeft = boundsFullTransform.map(middleLeft(bounds));
188 transformedHandles.middleRight = boundsFullTransform.map(middleRight(bounds));
189
190 transformedHandles.bottomLeft = boundsFullTransform.map(bounds.bottomLeft());
191 transformedHandles.bottomMiddle = boundsFullTransform.map(bottomMiddle(bounds));
192 transformedHandles.bottomRight = boundsFullTransform.map(bounds.bottomRight());
193
194 transformedHandles.rotationCenter = transform.map(currentArgs.originalCenter() + currentArgs.rotationCenterOffset());
195}
196
197void KisFreeTransformStrategy::setTransformFunction(const QPointF &mousePos, bool perspectiveModifierActive, bool shiftModifierActive, bool altModifierActive)
198{
199 Q_UNUSED(shiftModifierActive);
200
201 if (perspectiveModifierActive && !m_d->transaction.shouldAvoidPerspectiveTransform()) {
202 m_d->function = PERSPECTIVE;
203 return;
204 }
205
206 QTransform boundsFullTransform = m_d->boundsTransform * m_d->transform;
207 QPolygonF transformedPolygon = boundsFullTransform.map(QPolygonF(m_d->bounds));
208 qreal handleRadius = KisTransformUtils::effectiveHandleGrabRadius(m_d->converter);
209 qreal rotationHandleRadius = KisTransformUtils::effectiveHandleGrabRadius(m_d->converter);
210
211
212 StrokeFunction defaultFunction;
213 if (transformedPolygon.containsPoint(mousePos, Qt::OddEvenFill))
214 defaultFunction = MOVE;
215 else if (m_d->transaction.boundsRotationAllowed() && altModifierActive)
216 defaultFunction = ROTATEBOUNDS;
217 else
218 defaultFunction = ROTATE;
220 handleChooser(mousePos, defaultFunction);
221
222 handleChooser.addFunction(m_d->transformedHandles.topMiddle,
223 handleRadius, TOPSCALE);
224 handleChooser.addFunction(m_d->transformedHandles.topRight,
225 handleRadius, TOPRIGHTSCALE);
226 handleChooser.addFunction(m_d->transformedHandles.middleRight,
227 handleRadius, RIGHTSCALE);
228
229 handleChooser.addFunction(m_d->transformedHandles.bottomRight,
230 handleRadius, BOTTOMRIGHTSCALE);
231 handleChooser.addFunction(m_d->transformedHandles.bottomMiddle,
232 handleRadius, BOTTOMSCALE);
233 handleChooser.addFunction(m_d->transformedHandles.bottomLeft,
234 handleRadius, BOTTOMLEFTSCALE);
235 handleChooser.addFunction(m_d->transformedHandles.middleLeft,
236 handleRadius, LEFTSCALE);
237 handleChooser.addFunction(m_d->transformedHandles.topLeft,
238 handleRadius, TOPLEFTSCALE);
239 handleChooser.addFunction(m_d->transformedHandles.rotationCenter,
240 rotationHandleRadius, MOVECENTER);
241
242 m_d->function = handleChooser.function();
243
244 if (m_d->function == ROTATE || m_d->function == MOVE) {
245 QRectF bounds = m_d->bounds;
246 QPointF t = boundsFullTransform.inverted().map(mousePos);
247
248 if (t.x() >= bounds.left() && t.x() <= bounds.right()) {
249 if (fabs(t.y() - bounds.top()) <= handleRadius)
250 m_d->function = TOPSHEAR;
251 if (fabs(t.y() - bounds.bottom()) <= handleRadius)
252 m_d->function = BOTTOMSHEAR;
253 }
254 if (t.y() >= bounds.top() && t.y() <= bounds.bottom()) {
255 if (fabs(t.x() - bounds.left()) <= handleRadius)
256 m_d->function = LEFTSHEAR;
257 if (fabs(t.x() - bounds.right()) <= handleRadius)
258 m_d->function = RIGHTSHEAR;
259 }
260 }
261}
262
264{
265 return true;
266}
267
269{
270 QPointF handlePtInWidget = converter->imageToWidget(handlePt);
271 QPointF centerPtInWidget = converter->imageToWidget(currentArgs.transformedCenter());
272
273 QPointF direction = handlePtInWidget - centerPtInWidget;
274 qreal angle = atan2(direction.y(), direction.x());
275 angle = normalizeAngle(angle);
276
277 int octant = qRound(angle * 4. / M_PI) % 8;
278 return scaleCursors[octant];
279}
280
281QCursor KisFreeTransformStrategy::Private::getShearCursor(const QPointF &start, const QPointF &end)
282{
283 QPointF startPtInWidget = converter->imageToWidget(start);
284 QPointF endPtInWidget = converter->imageToWidget(end);
285 QPointF direction = endPtInWidget - startPtInWidget;
286
287 qreal angle = atan2(-direction.y(), direction.x());
288 return QCursor(shearCursorPixmap.transformed(QTransform().rotateRadians(-angle)));
289}
290
292{
293 QCursor cursor;
294
295 switch (m_d->function) {
296 case MOVE:
297 cursor = KisCursor::moveCursor();
298 break;
299 case ROTATEBOUNDS:
300 cursor = m_d->rotateHandlesCursor;
301 break;
302 case ROTATE:
303 cursor = KisCursor::rotateCursor();
304 break;
305 case PERSPECTIVE:
306 //TODO: find another cursor for perspective
307 cursor = KisCursor::rotateCursor();
308 break;
309 case RIGHTSCALE:
310 cursor = m_d->getScaleCursor(m_d->transformedHandles.middleRight);
311 break;
312 case TOPSCALE:
313 cursor = m_d->getScaleCursor(m_d->transformedHandles.topMiddle);
314 break;
315 case LEFTSCALE:
316 cursor = m_d->getScaleCursor(m_d->transformedHandles.middleLeft);
317 break;
318 case BOTTOMSCALE:
319 cursor = m_d->getScaleCursor(m_d->transformedHandles.bottomMiddle);
320 break;
321 case TOPRIGHTSCALE:
322 cursor = m_d->getScaleCursor(m_d->transformedHandles.topRight);
323 break;
324 case BOTTOMLEFTSCALE:
325 cursor = m_d->getScaleCursor(m_d->transformedHandles.bottomLeft);
326 break;
327 case TOPLEFTSCALE:
328 cursor = m_d->getScaleCursor(m_d->transformedHandles.topLeft);
329 break;
330 case BOTTOMRIGHTSCALE:
331 cursor = m_d->getScaleCursor(m_d->transformedHandles.bottomRight);
332 break;
333 case MOVECENTER:
334 cursor = KisCursor::handCursor();
335 break;
336 case BOTTOMSHEAR:
337 cursor = m_d->getShearCursor(m_d->transformedHandles.bottomLeft, m_d->transformedHandles.bottomRight);
338 break;
339 case RIGHTSHEAR:
340 cursor = m_d->getShearCursor(m_d->transformedHandles.bottomRight, m_d->transformedHandles.topRight);
341 break;
342 case TOPSHEAR:
343 cursor = m_d->getShearCursor(m_d->transformedHandles.topRight, m_d->transformedHandles.topLeft);
344 break;
345 case LEFTSHEAR:
346 cursor = m_d->getShearCursor(m_d->transformedHandles.topLeft, m_d->transformedHandles.bottomLeft);
347 break;
348 }
349
350 return cursor;
351}
352
354{
355 gc.save();
356
357 gc.setOpacity(m_d->transaction.basePreviewOpacity());
358 gc.setTransform(m_d->paintingTransform, true);
359 gc.drawImage(m_d->paintingOffset, originalImage());
360
361 gc.restore();
362
363 // Draw Handles
364
365 QRectF handleRect =
367 m_d->handlesTransform,
368 m_d->bounds, 0, 0);
369
370 qreal rX = 1;
371 qreal rY = 1;
372 QRectF rotationCenterRect =
374 m_d->handlesTransform,
375 m_d->bounds,
376 &rX,
377 &rY);
378
379 QPainterPath handles;
380
381 handles.moveTo(m_d->bounds.topLeft());
382 handles.lineTo(m_d->bounds.topRight());
383 handles.lineTo(m_d->bounds.bottomRight());
384 handles.lineTo(m_d->bounds.bottomLeft());
385 handles.lineTo(m_d->bounds.topLeft());
386
387 handles.addRect(handleRect.translated(m_d->bounds.topLeft()));
388 handles.addRect(handleRect.translated(m_d->bounds.topRight()));
389 handles.addRect(handleRect.translated(m_d->bounds.bottomLeft()));
390 handles.addRect(handleRect.translated(m_d->bounds.bottomRight()));
391 handles.addRect(handleRect.translated(middleLeft(m_d->bounds)));
392 handles.addRect(handleRect.translated(middleRight(m_d->bounds)));
393 handles.addRect(handleRect.translated(topMiddle(m_d->bounds)));
394 handles.addRect(handleRect.translated(bottomMiddle(m_d->bounds)));
395
396 QPointF rotationCenter = m_d->boundsTransform.inverted().map(m_d->currentArgs.originalCenter() + m_d->currentArgs.rotationCenterOffset());
397 QPointF dx(rX + 3, 0);
398 QPointF dy(0, rY + 3);
399 handles.addEllipse(rotationCenterRect.translated(rotationCenter));
400 handles.moveTo(rotationCenter - dx);
401 handles.lineTo(rotationCenter + dx);
402 handles.moveTo(rotationCenter - dy);
403 handles.lineTo(rotationCenter + dy);
404
405 gc.save();
406
407 if (m_d->isTransforming) {
408 gc.setOpacity(0.1);
409 }
410
411 //gc.setTransform(m_d->handlesTransform, true); <-- don't do like this!
412 QPainterPath mappedHandles = m_d->handlesTransform.map(handles);
413
414 QPen pen[2];
415 pen[0].setWidth(decorationThickness());
416 pen[0].setCosmetic(true);
417 pen[1].setWidth(decorationThickness() * 2);
418 pen[1].setCosmetic(true);
419 pen[1].setColor(Qt::lightGray);
420
421 for (int i = 1; i >= 0; --i) {
422 gc.setPen(pen[i]);
423 gc.drawPath(mappedHandles);
424 }
425
426 gc.restore();
427}
428
430{
431 m_d->recalculateTransformations();
432}
433
435{
436 m_d->clickArgs = m_d->currentArgs;
437 m_d->clickPos = pt;
438
440 m_d->clickTransform = m.finalTransform();
441
442 if (m_d->function == ROTATEBOUNDS) {
444 }
445
446 return true;
447}
448
450 bool shiftModifierActive,
451 bool altModifierActive)
452{
453 // Note: "shiftModifierActive" just tells us if the shift key is being pressed
454 // Note: "altModifierActive" just tells us if the alt key is being pressed
455
456 m_d->isTransforming = true;
457 const QPointF anchorPoint = m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset();
458
459 switch (m_d->function) {
460 case MOVE: {
461 QPointF diff = mousePos - m_d->clickPos;
462
463 if (shiftModifierActive) {
464
466 QTransform t = m.S * m.projectedP;
467 QPointF originalDiff = t.inverted().map(diff);
468
469 if (qAbs(originalDiff.x()) >= qAbs(originalDiff.y())) {
470 originalDiff.setY(0);
471 } else {
472 originalDiff.setX(0);
473 }
474
475 diff = t.map(originalDiff);
476
477 }
478
479 m_d->currentArgs.setTransformedCenter(m_d->clickArgs.transformedCenter() + diff);
480
481 break;
482 }
483 case ROTATEBOUNDS:
484 {
485 const KisTransformUtils::MatricesPack clickM(m_d->clickArgs);
486 const QTransform clickT = clickM.finalTransform();
487
488 const QPointF rotationCenter = m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset();
489 const QPointF clickMouseImagePos = clickT.inverted().map(m_d->clickPos) - rotationCenter;
490 const QPointF mouseImagePosClickSpace = clickT.inverted().map(mousePos) - rotationCenter;
491
492 const qreal a1 = atan2(clickMouseImagePos.y(), clickMouseImagePos.x());
493 const qreal a2 = atan2(mouseImagePosClickSpace.y(), mouseImagePosClickSpace.x());
494
495 const qreal theta = a2 - a1;
496 m_d->currentArgs.setBoundsRotation(m_d->clickArgs.boundsRotation() + theta);
497
498 // Find new scale/shear/rotation for the rotated bounds
499 QTransform newBR; newBR.rotateRadians(m_d->currentArgs.boundsRotation());
500 QTransform clickZ; clickZ.rotateRadians(m_d->clickArgs.aZ());
501 // newM.BRI * newM.SC * newM.S * newZ = clickM.BRI * clickM.SC * clickM.S * clickZ
502 // newM.SC * newM.S * newZ = newM.BR * clickM.BRI * clickM.SC * clickM.S * clickZ
503 QTransform desired = newBR * clickM.BRI * clickM.SC * clickM.S * clickZ;
505 if (dm.isValid()) {
506 m_d->currentArgs.setScaleX(dm.scaleX);
507 m_d->currentArgs.setScaleY(dm.scaleY);
508 m_d->currentArgs.setShearX(dm.shearXY);
509 m_d->currentArgs.setShearY(0);
510 m_d->currentArgs.setAZ(kisDegreesToRadians(dm.angle));
511 }
512
513 // Snap with shift key
514 // if (shiftModifierActive) {
515 // const qreal angle = m_d->currentArgs.boundsRotation();
516 // const qreal snapAngle = M_PI_4 / 6.0; // 7.5 degrees
517 // qint32 angleIndex = static_cast<qint32>((angle / snapAngle) + 0.5);
518 // m_d->currentArgs.setBoundsRotation(angleIndex * snapAngle);
519 // }
520 }
521 break;
522 case ROTATE:
523 {
524 const KisTransformUtils::MatricesPack clickM(m_d->clickArgs);
525 const QTransform clickT = clickM.finalTransform();
526
527 const QPointF rotationCenter = m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset();
528 const QPointF clickMouseImagePos = clickT.inverted().map(m_d->clickPos) - rotationCenter;
529 const QPointF mouseImagePosClickSpace = clickT.inverted().map(mousePos) - rotationCenter;
530
531 const qreal a1 = atan2(clickMouseImagePos.y(), clickMouseImagePos.x());
532 const qreal a2 = atan2(mouseImagePosClickSpace.y(), mouseImagePosClickSpace.x());
533
538 const qreal theta = KisAlgebra2D::signZZ(clickM.SC.determinant()) * (a2 - a1);
539
540 // Snap with shift key
541 if (shiftModifierActive) {
542 const qreal snapAngle = M_PI_4 / 6.0; // 7.5 degrees
543 qint32 thetaIndex = static_cast<qint32>((theta / snapAngle) + 0.5);
544 m_d->currentArgs.setAZ(thetaIndex * snapAngle);
545 }
546 else {
547 const qreal clickAngle = m_d->clickArgs.aZ();
548 const qreal targetAngle = m_d->clickArgs.aZ() + theta;
549 qreal shortestDistance = shortestAngularDistance(clickAngle, targetAngle);
550 const bool clockwise = (theta <= M_PI && theta >= 0) || (theta < -M_PI);
551 shortestDistance = clockwise ? shortestDistance : shortestDistance * -1;
552
553 m_d->currentArgs.setAZ(m_d->clickArgs.aZ() + shortestDistance);
554 }
555
556 KisTransformUtils::MatricesPack m(m_d->currentArgs);
557 QTransform t = m.finalTransform();
558 QPointF newRotationCenter = t.map(m_d->currentArgs.originalCenter() + m_d->currentArgs.rotationCenterOffset());
559 QPointF oldRotationCenter = clickT.map(m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset());
560
561 m_d->currentArgs.setTransformedCenter(m_d->currentArgs.transformedCenter() + oldRotationCenter - newRotationCenter);
562 }
563 break;
564 case PERSPECTIVE:
565 {
566 QPointF diff = mousePos - m_d->clickPos;
567 double thetaX = - diff.y() * M_PI / m_d->transaction.originalHalfHeight() / 2 / fabs(m_d->currentArgs.scaleY());
568 m_d->currentArgs.setAX(normalizeAngle(m_d->clickArgs.aX() + thetaX));
569
570 qreal sign = qAbs(m_d->currentArgs.aX() - M_PI) < M_PI / 2 ? -1.0 : 1.0;
571 double thetaY = sign * diff.x() * M_PI / m_d->transaction.originalHalfWidth() / 2 / fabs(m_d->currentArgs.scaleX());
572 m_d->currentArgs.setAY(normalizeAngle(m_d->clickArgs.aY() + thetaY));
573
574 KisTransformUtils::MatricesPack m(m_d->currentArgs);
575 QTransform t = m.finalTransform();
576 QPointF newRotationCenter = t.map(m_d->currentArgs.originalCenter() + m_d->currentArgs.rotationCenterOffset());
577
578 KisTransformUtils::MatricesPack clickM(m_d->clickArgs);
579 QTransform clickT = clickM.finalTransform();
580 QPointF oldRotationCenter = clickT.map(m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset());
581
582 m_d->currentArgs.setTransformedCenter(m_d->currentArgs.transformedCenter() + oldRotationCenter - newRotationCenter);
583 }
584 break;
585 case TOPSCALE:
586 case BOTTOMSCALE: {
587 QPointF staticPoint;
588 QPointF movingPoint;
589
590 if (m_d->function == TOPSCALE) {
591 staticPoint = m_d->boundsTransform.map(bottomMiddle(m_d->bounds));
592 movingPoint = m_d->boundsTransform.map(topMiddle(m_d->bounds));
593 } else {
594 staticPoint = m_d->boundsTransform.map(topMiddle(m_d->bounds));
595 movingPoint = m_d->boundsTransform.map(bottomMiddle(m_d->bounds));
596 }
597
598 QPointF staticPointInView = m_d->clickTransform.map(staticPoint);
599 const QPointF movingPointInView = m_d->clickTransform.map(movingPoint);
600
601 const QPointF projNormVector =
602 KisAlgebra2D::normalize(movingPointInView - staticPointInView);
603
604 const qreal projLength =
605 KisAlgebra2D::dotProduct(mousePos - staticPointInView, projNormVector);
606
607 const QPointF targetMovingPointInView = staticPointInView + projNormVector * projLength;
608
609 // override scale static point if it is locked
610 if ((m_d->clickArgs.transformAroundRotationCenter() ^ altModifierActive) &&
611 !qFuzzyCompare(anchorPoint.y(), movingPoint.y())) {
612
613 staticPoint = anchorPoint;
614 staticPointInView = m_d->clickTransform.map(staticPoint);
615 }
616
617 GSL::ScaleResult1D result =
618 GSL::calculateScaleY(m_d->currentArgs,
619 staticPoint,
620 staticPointInView,
621 movingPoint,
622 targetMovingPointInView);
623
624 if (!result.isValid) {
625 break;
626 }
627
628 if (shiftModifierActive || m_d->currentArgs.keepAspectRatio()) {
629 qreal aspectRatio = m_d->clickArgs.scaleX() / m_d->clickArgs.scaleY();
630 m_d->currentArgs.setScaleX(aspectRatio * result.scale);
631 }
632
633 m_d->currentArgs.setScaleY(result.scale);
634 m_d->currentArgs.setTransformedCenter(result.transformedCenter);
635 break;
636 }
637
638 case LEFTSCALE:
639 case RIGHTSCALE: {
640 QPointF staticPoint;
641 QPointF movingPoint;
642
643 if (m_d->function == LEFTSCALE) {
644 staticPoint = m_d->boundsTransform.map(middleRight(m_d->bounds));
645 movingPoint = m_d->boundsTransform.map(middleLeft(m_d->bounds));
646 } else {
647 staticPoint = m_d->boundsTransform.map(middleLeft(m_d->bounds));
648 movingPoint = m_d->boundsTransform.map(middleRight(m_d->bounds));
649 }
650
651 QPointF staticPointInView = m_d->clickTransform.map(staticPoint);
652 const QPointF movingPointInView = m_d->clickTransform.map(movingPoint);
653
654 const QPointF projNormVector =
655 KisAlgebra2D::normalize(movingPointInView - staticPointInView);
656
657 const qreal projLength =
658 KisAlgebra2D::dotProduct(mousePos - staticPointInView, projNormVector);
659
660 const QPointF targetMovingPointInView = staticPointInView + projNormVector * projLength;
661
662 // override scale static point if it is locked
663 if ((m_d->currentArgs.transformAroundRotationCenter() ^ altModifierActive) &&
664 !qFuzzyCompare(anchorPoint.x(), movingPoint.x())) {
665
666 staticPoint = anchorPoint;
667 staticPointInView = m_d->clickTransform.map(staticPoint);
668 }
669
670 GSL::ScaleResult1D result =
671 GSL::calculateScaleX(m_d->currentArgs,
672 staticPoint,
673 staticPointInView,
674 movingPoint,
675 targetMovingPointInView);
676
677 if (!result.isValid) {
678 break;
679 }
680
681 if (shiftModifierActive || m_d->currentArgs.keepAspectRatio()) {
682 qreal aspectRatio = m_d->clickArgs.scaleY() / m_d->clickArgs.scaleX();
683 m_d->currentArgs.setScaleY(aspectRatio * result.scale);
684 }
685
686 m_d->currentArgs.setScaleX(result.scale);
687 m_d->currentArgs.setTransformedCenter(result.transformedCenter);
688 break;
689 }
690 case TOPRIGHTSCALE:
691 case BOTTOMRIGHTSCALE:
692 case TOPLEFTSCALE:
693 case BOTTOMLEFTSCALE: {
694 QPointF staticPoint;
695 QPointF movingPoint;
696
697 if (m_d->function == TOPRIGHTSCALE) {
698 staticPoint = m_d->boundsTransform.map(m_d->bounds.bottomLeft());
699 movingPoint = m_d->boundsTransform.map(m_d->bounds.topRight());
700 } else if (m_d->function == BOTTOMRIGHTSCALE) {
701 staticPoint = m_d->boundsTransform.map(m_d->bounds.topLeft());
702 movingPoint = m_d->boundsTransform.map(m_d->bounds.bottomRight());
703 } else if (m_d->function == TOPLEFTSCALE) {
704 staticPoint = m_d->boundsTransform.map(m_d->bounds.bottomRight());
705 movingPoint = m_d->boundsTransform.map(m_d->bounds.topLeft());
706 } else {
707 staticPoint = m_d->boundsTransform.map(m_d->bounds.topRight());
708 movingPoint = m_d->boundsTransform.map(m_d->bounds.bottomLeft());
709 }
710
711 // override scale static point if it is locked
712 if ((m_d->currentArgs.transformAroundRotationCenter() ^ altModifierActive) &&
713 !(qFuzzyCompare(anchorPoint.x(), movingPoint.x()) ||
714 qFuzzyCompare(anchorPoint.y(), movingPoint.y()))) {
715
716 staticPoint = anchorPoint;
717 }
718
719 QPointF staticPointInView = m_d->clickTransform.map(staticPoint);
720 QPointF movingPointInView = mousePos;
721
722 if (shiftModifierActive || m_d->currentArgs.keepAspectRatio()) {
723 QPointF refDiff = m_d->clickTransform.map(movingPoint) - staticPointInView;
724 QPointF realDiff = mousePos - staticPointInView;
725 realDiff = kisProjectOnVector(refDiff, realDiff);
726
727 movingPointInView = staticPointInView + realDiff;
728 }
729
730 const bool isAffine =
731 qFuzzyIsNull(m_d->currentArgs.aX()) &&
732 qFuzzyIsNull(m_d->currentArgs.aY());
733
734 GSL::ScaleResult2D result =
735 !isAffine ?
736 GSL::calculateScale2D(m_d->currentArgs,
737 staticPoint,
738 staticPointInView,
739 movingPoint,
740 movingPointInView) :
741 GSL::calculateScale2DAffine(m_d->currentArgs,
742 staticPoint,
743 staticPointInView,
744 movingPoint,
745 movingPointInView);
746
747 if (result.isValid) {
748 m_d->currentArgs.setScaleX(result.scaleX);
749 m_d->currentArgs.setScaleY(result.scaleY);
750 m_d->currentArgs.setTransformedCenter(result.transformedCenter);
751 }
752
753 break;
754 }
755 case MOVECENTER: {
756
757 QPointF pt;
758 if (altModifierActive) {
759 pt = (m_d->boundsTransform * m_d->transform).inverted().map(mousePos);
760 pt = KisTransformUtils::clipInRect(pt, m_d->bounds);
761 pt = m_d->boundsTransform.map(pt);
762 } else {
763 pt = m_d->transform.inverted().map(mousePos);
764 }
765
766 QPointF newRotationCenterOffset = pt - m_d->currentArgs.originalCenter();
767
768 if (shiftModifierActive) {
769 if (qAbs(newRotationCenterOffset.x()) > qAbs(newRotationCenterOffset.y())) {
770 newRotationCenterOffset.ry() = 0;
771 } else {
772 newRotationCenterOffset.rx() = 0;
773 }
774 }
775
776 m_d->currentArgs.setRotationCenterOffset(newRotationCenterOffset);
778 }
779 break;
780 case TOPSHEAR:
781 case BOTTOMSHEAR: {
783
784 QPointF oldStaticPoint = m.finalTransform().map(m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset());
785
786 QTransform backwardT = (m.S * m.projectedP).inverted();
787 QPointF diff = backwardT.map(mousePos - m_d->clickPos);
788
789 qreal sign = m_d->function == BOTTOMSHEAR ? 1.0 : -1.0;
790
791 // get the dx pixels corresponding to the current shearX factor
792 qreal dx = sign * m_d->clickArgs.shearX() * m_d->clickArgs.scaleY() * (m_d->bounds.height() / 2.0); // get the dx pixels corresponding to the current shearX factor
793 dx += diff.x();
794
795 // calculate the new shearX factor
796 m_d->currentArgs.setShearX(sign * dx / m_d->currentArgs.scaleY() / (m_d->bounds.height() / 2.0)); // calculate the new shearX factor
797
798 KisTransformUtils::MatricesPack currentM(m_d->currentArgs);
799 QTransform t = currentM.finalTransform();
800 QPointF newStaticPoint = t.map(m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset());
801 m_d->currentArgs.setTransformedCenter(m_d->currentArgs.transformedCenter() + oldStaticPoint - newStaticPoint);
802 break;
803 }
804
805 case LEFTSHEAR:
806 case RIGHTSHEAR: {
808
809 QPointF oldStaticPoint = m.finalTransform().map(m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset());
810
811 QTransform backwardT = (m.S * m.projectedP).inverted();
812 QPointF diff = backwardT.map(mousePos - m_d->clickPos);
813
814 qreal sign = m_d->function == RIGHTSHEAR ? 1.0 : -1.0;
815
816 // get the dx pixels corresponding to the current shearX factor
817 qreal dy = sign * m_d->clickArgs.shearY() * m_d->clickArgs.scaleX() * (m_d->bounds.width() / 2.0);
818 dy += diff.y();
819
820 // calculate the new shearY factor
821 m_d->currentArgs.setShearY(sign * dy / m_d->clickArgs.scaleX() / (m_d->bounds.width() / 2.0));
822
823 KisTransformUtils::MatricesPack currentM(m_d->currentArgs);
824 QTransform t = currentM.finalTransform();
825 QPointF newStaticPoint = t.map(m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset());
826 m_d->currentArgs.setTransformedCenter(m_d->currentArgs.transformedCenter() + oldStaticPoint - newStaticPoint);
827 break;
828 }
829 }
830
831 m_d->recalculateTransformations();
832}
833
835{
836 bool shouldSave = !m_d->imageTooBig;
837 m_d->isTransforming = false;
838
839 if (m_d->imageTooBig) {
840 m_d->currentArgs = m_d->clickArgs;
841 m_d->recalculateTransformations();
842 }
843
844 return shouldSave;
845}
846
848{
849 KisTransformUtils::MatricesPack m(currentArgs);
850 QTransform sanityCheckMatrix = m.TS * m.SC * m.S * m.projectedP;
851
856 KIS_ASSERT_RECOVER_NOOP(sanityCheckMatrix.map(currentArgs.originalCenter()).manhattanLength() < 1e-4);
857
858 transform = m.finalTransform();
859 boundsTransform = m.BRI.inverted();
860 recalculateBounds();
861
862 QTransform viewScaleTransform = converter->imageToDocumentTransform() * converter->documentToFlakeTransform();
863 handlesTransform = boundsTransform * transform * viewScaleTransform;
864
865 QTransform tl = QTransform::fromTranslate(transaction.originalTopLeft().x(), transaction.originalTopLeft().y());
866 paintingTransform = tl.inverted() * q->thumbToImageTransform() * tl * transform * viewScaleTransform;
867 paintingOffset = transaction.originalTopLeft();
868
869 // check whether image is too big to be displayed or not
870 imageTooBig = KisTransformUtils::checkImageTooBig(transaction.originalRect(), m, currentArgs.cameraPos().z());
871
872 // recalculate cached handles position
873 recalculateTransformedHandles();
874
875 Q_EMIT q->requestShowImageTooBig(imageTooBig);
876 Q_EMIT q->requestImageRecalculation();
877}
static QCursor sizeBDiagCursor()
Definition kis_cursor.cc:74
static QCursor sizeHorCursor()
Definition kis_cursor.cc:69
static QCursor handCursor()
static QCursor rotateCursor()
static QCursor moveCursor()
static QCursor sizeFDiagCursor()
Definition kis_cursor.cc:79
static QCursor sizeVerCursor()
Definition kis_cursor.cc:64
void paint(QPainter &gc) override
QCursor getCurrentCursor() const override
bool beginPrimaryAction(const QPointF &pt) override
const QScopedPointer< Private > m_d
void continuePrimaryAction(const QPointF &pt, bool shiftModifierActive, bool altModifierActive) override
void setTransformFunction(const QPointF &mousePos, bool perspectiveModifierActive, bool shiftModifierActive, bool altModifierActive) override
void requestResetRotationCenterButtons()
KisFreeTransformStrategy(const KisCoordinatesConverter *converter, KoSnapGuide *snapGuide, ToolTransformArgs &currentArgs, TransformTransactionProperties &transaction)
bool addFunction(const QPointF &pt, qreal radius, Function function)
static qreal effectiveHandleGrabRadius(const KisCoordinatesConverter *converter)
static QPointF clipInRect(QPointF p, QRectF r)
static const int handleVisualRadius
static QRectF handleRect(qreal radius, const QTransform &t, const QRectF &limitingRect, qreal *dOutX, qreal *dOutY)
static bool checkImageTooBig(const QRectF &bounds, const MatricesPack &m, qreal cameraHeight)
static const int rotationHandleVisualRadius
static bool qFuzzyIsNull(half h)
#define KIS_ASSERT_RECOVER_NOOP(cond)
Definition kis_assert.h:97
#define bounds(x, a, b)
qreal shortestAngularDistance(qreal a, qreal b)
Definition kis_global.h:140
T kisDegreesToRadians(T degrees)
Definition kis_global.h:176
std::enable_if< std::is_floating_point< T >::value, T >::type normalizeAngle(T a)
Definition kis_global.h:121
QPointF kisProjectOnVector(const QPointF &base, const QPointF &v)
Definition kis_global.h:256
#define M_PI
Definition kis_global.h:111
ScaleResult2D calculateScale2DAffine(const ToolTransformArgs &args, const QPointF &staticPointSrc, const QPointF &staticPointDst, const QPointF &movingPointSrc, const QPointF &movingPointDst)
ScaleResult2D calculateScale2D(const ToolTransformArgs &args, const QPointF &staticPointSrc, const QPointF &staticPointDst, const QPointF &movingPointSrc, const QPointF &movingPointDst)
ScaleResult1D calculateScaleX(const ToolTransformArgs &args, const QPointF &staticPointSrc, const QPointF &staticPointDst, const QPointF &movingPointSrc, const QPointF &movingPointDst)
ScaleResult1D calculateScaleY(const ToolTransformArgs &args, const QPointF &staticPointSrc, const QPointF &staticPointDst, const QPointF &movingPointSrc, const QPointF &movingPointDst)
Point normalize(const Point &a)
PointTypeTraits< T >::value_type dotProduct(const T &a, const T &b)
Private(KisFreeTransformStrategy *_q, const KisCoordinatesConverter *_converter, ToolTransformArgs &_currentArgs, TransformTransactionProperties &_transaction)
TransformTransactionProperties & transaction
QCursor getScaleCursor(const QPointF &handlePt)
const KisCoordinatesConverter * converter
standard members ///
QCursor getShearCursor(const QPointF &start, const QPointF &end)
StrokeFunction function
custom members ///