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