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