diff --git a/plugins/tools/tool_transform2/kis_free_transform_strategy.cpp b/plugins/tools/tool_transform2/kis_free_transform_strategy.cpp index c6791f7397..9fd5578c6d 100644 --- a/plugins/tools/tool_transform2/kis_free_transform_strategy.cpp +++ b/plugins/tools/tool_transform2/kis_free_transform_strategy.cpp @@ -1,723 +1,731 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_free_transform_strategy.h" #include #include #include #include #include "kis_coordinates_converter.h" #include "tool_transform_args.h" #include "transform_transaction_properties.h" #include "krita_utils.h" #include "kis_cursor.h" #include "kis_transform_utils.h" #include "kis_free_transform_strategy_gsl_helpers.h" #include "kis_algebra_2d.h" enum StrokeFunction { ROTATE = 0, MOVE, RIGHTSCALE, TOPRIGHTSCALE, TOPSCALE, TOPLEFTSCALE, LEFTSCALE, BOTTOMLEFTSCALE, BOTTOMSCALE, BOTTOMRIGHTSCALE, BOTTOMSHEAR, RIGHTSHEAR, TOPSHEAR, LEFTSHEAR, MOVECENTER, PERSPECTIVE }; struct KisFreeTransformStrategy::Private { Private(KisFreeTransformStrategy *_q, const KisCoordinatesConverter *_converter, ToolTransformArgs &_currentArgs, TransformTransactionProperties &_transaction) : q(_q), converter(_converter), currentArgs(_currentArgs), transaction(_transaction), - imageTooBig(false) + imageTooBig(false), + isTransforming(false) { scaleCursors[0] = KisCursor::sizeHorCursor(); scaleCursors[1] = KisCursor::sizeFDiagCursor(); scaleCursors[2] = KisCursor::sizeVerCursor(); scaleCursors[3] = KisCursor::sizeBDiagCursor(); scaleCursors[4] = KisCursor::sizeHorCursor(); scaleCursors[5] = KisCursor::sizeFDiagCursor(); scaleCursors[6] = KisCursor::sizeVerCursor(); scaleCursors[7] = KisCursor::sizeBDiagCursor(); shearCursorPixmap.load(":/shear_cursor.png"); } KisFreeTransformStrategy *q; /// standard members /// const KisCoordinatesConverter *converter; ////// ToolTransformArgs ¤tArgs; ////// TransformTransactionProperties &transaction; QTransform thumbToImageTransform; QImage originalImage; QTransform paintingTransform; QPointF paintingOffset; QTransform handlesTransform; /// custom members /// StrokeFunction function; struct HandlePoints { QPointF topLeft; QPointF topMiddle; QPointF topRight; QPointF middleLeft; QPointF rotationCenter; QPointF middleRight; QPointF bottomLeft; QPointF bottomMiddle; QPointF bottomRight; }; HandlePoints transformedHandles; QTransform transform; QCursor scaleCursors[8]; // cursors for the 8 directions QPixmap shearCursorPixmap; bool imageTooBig; ToolTransformArgs clickArgs; QPointF clickPos; + bool isTransforming; QCursor getScaleCursor(const QPointF &handlePt); QCursor getShearCursor(const QPointF &start, const QPointF &end); void recalculateTransformations(); void recalculateTransformedHandles(); }; KisFreeTransformStrategy::KisFreeTransformStrategy(const KisCoordinatesConverter *converter, KoSnapGuide *snapGuide, ToolTransformArgs ¤tArgs, TransformTransactionProperties &transaction) : KisSimplifiedActionPolicyStrategy(converter, snapGuide), m_d(new Private(this, converter, currentArgs, transaction)) { } KisFreeTransformStrategy::~KisFreeTransformStrategy() { } void KisFreeTransformStrategy::Private::recalculateTransformedHandles() { transformedHandles.topLeft = transform.map(transaction.originalTopLeft()); transformedHandles.topMiddle = transform.map(transaction.originalMiddleTop()); transformedHandles.topRight = transform.map(transaction.originalTopRight()); transformedHandles.middleLeft = transform.map(transaction.originalMiddleLeft()); transformedHandles.rotationCenter = transform.map(currentArgs.originalCenter() + currentArgs.rotationCenterOffset()); transformedHandles.middleRight = transform.map(transaction.originalMiddleRight()); transformedHandles.bottomLeft = transform.map(transaction.originalBottomLeft()); transformedHandles.bottomMiddle = transform.map(transaction.originalMiddleBottom()); transformedHandles.bottomRight = transform.map(transaction.originalBottomRight()); } void KisFreeTransformStrategy::setTransformFunction(const QPointF &mousePos, bool perspectiveModifierActive) { if (perspectiveModifierActive && !m_d->transaction.shouldAvoidPerspectiveTransform()) { m_d->function = PERSPECTIVE; return; } QPolygonF transformedPolygon = m_d->transform.map(QPolygonF(m_d->transaction.originalRect())); qreal handleRadius = KisTransformUtils::effectiveHandleGrabRadius(m_d->converter); qreal rotationHandleRadius = KisTransformUtils::effectiveHandleGrabRadius(m_d->converter); StrokeFunction defaultFunction = transformedPolygon.containsPoint(mousePos, Qt::OddEvenFill) ? MOVE : ROTATE; KisTransformUtils::HandleChooser handleChooser(mousePos, defaultFunction); handleChooser.addFunction(m_d->transformedHandles.topMiddle, handleRadius, TOPSCALE); handleChooser.addFunction(m_d->transformedHandles.topRight, handleRadius, TOPRIGHTSCALE); handleChooser.addFunction(m_d->transformedHandles.middleRight, handleRadius, RIGHTSCALE); handleChooser.addFunction(m_d->transformedHandles.bottomRight, handleRadius, BOTTOMRIGHTSCALE); handleChooser.addFunction(m_d->transformedHandles.bottomMiddle, handleRadius, BOTTOMSCALE); handleChooser.addFunction(m_d->transformedHandles.bottomLeft, handleRadius, BOTTOMLEFTSCALE); handleChooser.addFunction(m_d->transformedHandles.middleLeft, handleRadius, LEFTSCALE); handleChooser.addFunction(m_d->transformedHandles.topLeft, handleRadius, TOPLEFTSCALE); handleChooser.addFunction(m_d->transformedHandles.rotationCenter, rotationHandleRadius, MOVECENTER); m_d->function = handleChooser.function(); if (m_d->function == ROTATE || m_d->function == MOVE) { QRectF originalRect = m_d->transaction.originalRect(); QPointF t = m_d->transform.inverted().map(mousePos); if (t.x() >= originalRect.left() && t.x() <= originalRect.right()) { if (fabs(t.y() - originalRect.top()) <= handleRadius) m_d->function = TOPSHEAR; if (fabs(t.y() - originalRect.bottom()) <= handleRadius) m_d->function = BOTTOMSHEAR; } if (t.y() >= originalRect.top() && t.y() <= originalRect.bottom()) { if (fabs(t.x() - originalRect.left()) <= handleRadius) m_d->function = LEFTSHEAR; if (fabs(t.x() - originalRect.right()) <= handleRadius) m_d->function = RIGHTSHEAR; } } } QCursor KisFreeTransformStrategy::Private::getScaleCursor(const QPointF &handlePt) { QPointF handlePtInWidget = converter->imageToWidget(handlePt); QPointF centerPtInWidget = converter->imageToWidget(currentArgs.transformedCenter()); QPointF direction = handlePtInWidget - centerPtInWidget; qreal angle = atan2(direction.y(), direction.x()); angle = normalizeAngle(angle); int octant = qRound(angle * 4. / M_PI) % 8; return scaleCursors[octant]; } QCursor KisFreeTransformStrategy::Private::getShearCursor(const QPointF &start, const QPointF &end) { QPointF startPtInWidget = converter->imageToWidget(start); QPointF endPtInWidget = converter->imageToWidget(end); QPointF direction = endPtInWidget - startPtInWidget; qreal angle = atan2(-direction.y(), direction.x()); return QCursor(shearCursorPixmap.transformed(QTransform().rotateRadians(-angle))); } QCursor KisFreeTransformStrategy::getCurrentCursor() const { QCursor cursor; switch (m_d->function) { case MOVE: cursor = KisCursor::moveCursor(); break; case ROTATE: cursor = KisCursor::rotateCursor(); break; case PERSPECTIVE: //TODO: find another cursor for perspective cursor = KisCursor::rotateCursor(); break; case RIGHTSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.middleRight); break; case TOPSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.topMiddle); break; case LEFTSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.middleLeft); break; case BOTTOMSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.bottomMiddle); break; case TOPRIGHTSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.topRight); break; case BOTTOMLEFTSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.bottomLeft); break; case TOPLEFTSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.topLeft); break; case BOTTOMRIGHTSCALE: cursor = m_d->getScaleCursor(m_d->transformedHandles.bottomRight); break; case MOVECENTER: cursor = KisCursor::handCursor(); break; case BOTTOMSHEAR: cursor = m_d->getShearCursor(m_d->transformedHandles.bottomLeft, m_d->transformedHandles.bottomRight); break; case RIGHTSHEAR: cursor = m_d->getShearCursor(m_d->transformedHandles.bottomRight, m_d->transformedHandles.topRight); break; case TOPSHEAR: cursor = m_d->getShearCursor(m_d->transformedHandles.topRight, m_d->transformedHandles.topLeft); break; case LEFTSHEAR: cursor = m_d->getShearCursor(m_d->transformedHandles.topLeft, m_d->transformedHandles.bottomLeft); break; } return cursor; } void KisFreeTransformStrategy::paint(QPainter &gc) { gc.save(); gc.setOpacity(m_d->transaction.basePreviewOpacity()); gc.setTransform(m_d->paintingTransform, true); gc.drawImage(m_d->paintingOffset, originalImage()); gc.restore(); // Draw Handles QRectF handleRect = KisTransformUtils::handleRect(KisTransformUtils::handleVisualRadius, m_d->handlesTransform, m_d->transaction.originalRect(), 0, 0); qreal rX = 1; qreal rY = 1; QRectF rotationCenterRect = KisTransformUtils::handleRect(KisTransformUtils::rotationHandleVisualRadius, m_d->handlesTransform, m_d->transaction.originalRect(), &rX, &rY); QPainterPath handles; handles.moveTo(m_d->transaction.originalTopLeft()); handles.lineTo(m_d->transaction.originalTopRight()); handles.lineTo(m_d->transaction.originalBottomRight()); handles.lineTo(m_d->transaction.originalBottomLeft()); handles.lineTo(m_d->transaction.originalTopLeft()); handles.addRect(handleRect.translated(m_d->transaction.originalTopLeft())); handles.addRect(handleRect.translated(m_d->transaction.originalTopRight())); handles.addRect(handleRect.translated(m_d->transaction.originalBottomLeft())); handles.addRect(handleRect.translated(m_d->transaction.originalBottomRight())); handles.addRect(handleRect.translated(m_d->transaction.originalMiddleLeft())); handles.addRect(handleRect.translated(m_d->transaction.originalMiddleRight())); handles.addRect(handleRect.translated(m_d->transaction.originalMiddleTop())); handles.addRect(handleRect.translated(m_d->transaction.originalMiddleBottom())); QPointF rotationCenter = m_d->currentArgs.originalCenter() + m_d->currentArgs.rotationCenterOffset(); QPointF dx(rX + 3, 0); QPointF dy(0, rY + 3); handles.addEllipse(rotationCenterRect.translated(rotationCenter)); handles.moveTo(rotationCenter - dx); handles.lineTo(rotationCenter + dx); handles.moveTo(rotationCenter - dy); handles.lineTo(rotationCenter + dy); gc.save(); + if (m_d->isTransforming) { + gc.setOpacity(0.1); + } + //gc.setTransform(m_d->handlesTransform, true); <-- don't do like this! QPainterPath mappedHandles = m_d->handlesTransform.map(handles); QPen pen[2]; pen[0].setWidth(1); pen[1].setWidth(2); pen[1].setColor(Qt::lightGray); for (int i = 1; i >= 0; --i) { gc.setPen(pen[i]); gc.drawPath(mappedHandles); } gc.restore(); } void KisFreeTransformStrategy::externalConfigChanged() { m_d->recalculateTransformations(); } bool KisFreeTransformStrategy::beginPrimaryAction(const QPointF &pt) { m_d->clickArgs = m_d->currentArgs; m_d->clickPos = pt; return true; } void KisFreeTransformStrategy::continuePrimaryAction(const QPointF &mousePos, bool shiftModifierActive, bool altModifierActive) { // Note: "shiftModifierActive" just tells us if the shift key is being pressed // Note: "altModifierActive" just tells us if the alt key is being pressed + m_d->isTransforming = true; const QPointF anchorPoint = m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset(); switch (m_d->function) { case MOVE: { QPointF diff = mousePos - m_d->clickPos; if (shiftModifierActive) { KisTransformUtils::MatricesPack m(m_d->clickArgs); QTransform t = m.S * m.projectedP; QPointF originalDiff = t.inverted().map(diff); if (qAbs(originalDiff.x()) >= qAbs(originalDiff.y())) { originalDiff.setY(0); } else { originalDiff.setX(0); } diff = t.map(originalDiff); } m_d->currentArgs.setTransformedCenter(m_d->clickArgs.transformedCenter() + diff); break; } case ROTATE: { const KisTransformUtils::MatricesPack clickM(m_d->clickArgs); const QTransform clickT = clickM.finalTransform(); const QPointF rotationCenter = m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset(); const QPointF clickMouseImagePos = clickT.inverted().map(m_d->clickPos) - rotationCenter; const QPointF mouseImagePos = clickT.inverted().map(mousePos) - rotationCenter; const qreal a1 = atan2(clickMouseImagePos.y(), clickMouseImagePos.x()); const qreal a2 = atan2(mouseImagePos.y(), mouseImagePos.x()); const qreal theta = KisAlgebra2D::signZZ(clickT.determinant()) * (a2 - a1); // Snap with shift key if (shiftModifierActive) { const qreal snapAngle = M_PI_4 / 6.0; // fifteen degrees qint32 thetaIndex = static_cast((theta / snapAngle) + 0.5); m_d->currentArgs.setAZ(normalizeAngle(thetaIndex * snapAngle)); } else { m_d->currentArgs.setAZ(normalizeAngle(m_d->clickArgs.aZ() + theta)); } KisTransformUtils::MatricesPack m(m_d->currentArgs); QTransform t = m.finalTransform(); QPointF newRotationCenter = t.map(m_d->currentArgs.originalCenter() + m_d->currentArgs.rotationCenterOffset()); QPointF oldRotationCenter = clickT.map(m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset()); m_d->currentArgs.setTransformedCenter(m_d->currentArgs.transformedCenter() + oldRotationCenter - newRotationCenter); } break; case PERSPECTIVE: { QPointF diff = mousePos - m_d->clickPos; double thetaX = - diff.y() * M_PI / m_d->transaction.originalHalfHeight() / 2 / fabs(m_d->currentArgs.scaleY()); m_d->currentArgs.setAX(normalizeAngle(m_d->clickArgs.aX() + thetaX)); qreal sign = qAbs(m_d->currentArgs.aX() - M_PI) < M_PI / 2 ? -1.0 : 1.0; double thetaY = sign * diff.x() * M_PI / m_d->transaction.originalHalfWidth() / 2 / fabs(m_d->currentArgs.scaleX()); m_d->currentArgs.setAY(normalizeAngle(m_d->clickArgs.aY() + thetaY)); KisTransformUtils::MatricesPack m(m_d->currentArgs); QTransform t = m.finalTransform(); QPointF newRotationCenter = t.map(m_d->currentArgs.originalCenter() + m_d->currentArgs.rotationCenterOffset()); KisTransformUtils::MatricesPack clickM(m_d->clickArgs); QTransform clickT = clickM.finalTransform(); QPointF oldRotationCenter = clickT.map(m_d->clickArgs.originalCenter() + m_d->clickArgs.rotationCenterOffset()); m_d->currentArgs.setTransformedCenter(m_d->currentArgs.transformedCenter() + oldRotationCenter - newRotationCenter); } break; case TOPSCALE: case BOTTOMSCALE: { QPointF staticPoint; QPointF movingPoint; qreal extraSign; if (m_d->function == TOPSCALE) { staticPoint = m_d->transaction.originalMiddleBottom(); movingPoint = m_d->transaction.originalMiddleTop(); extraSign = -1.0; } else { staticPoint = m_d->transaction.originalMiddleTop(); movingPoint = m_d->transaction.originalMiddleBottom(); extraSign = 1.0; } // override scale static point if it is locked if ((m_d->currentArgs.transformAroundRotationCenter() ^ altModifierActive) && !qFuzzyCompare(anchorPoint.y(), movingPoint.y())) { staticPoint = anchorPoint; } QPointF mouseImagePos = m_d->transform.inverted().map(mousePos); qreal sign = mouseImagePos.y() <= staticPoint.y() ? -extraSign : extraSign; m_d->currentArgs.setScaleY(sign * m_d->currentArgs.scaleY()); QPointF staticPointInView = m_d->transform.map(staticPoint); qreal dist = kisDistance(staticPointInView, mousePos); GSL::ScaleResult1D result = GSL::calculateScaleY(m_d->currentArgs, staticPoint, staticPointInView, movingPoint, dist); if (shiftModifierActive || m_d->currentArgs.keepAspectRatio()) { qreal aspectRatio = m_d->clickArgs.scaleX() / m_d->clickArgs.scaleY(); m_d->currentArgs.setScaleX(aspectRatio * result.scale); } m_d->currentArgs.setScaleY(result.scale); m_d->currentArgs.setTransformedCenter(result.transformedCenter); break; } case LEFTSCALE: case RIGHTSCALE: { QPointF staticPoint; QPointF movingPoint; qreal extraSign; if (m_d->function == LEFTSCALE) { staticPoint = m_d->transaction.originalMiddleRight(); movingPoint = m_d->transaction.originalMiddleLeft(); extraSign = -1.0; } else { staticPoint = m_d->transaction.originalMiddleLeft(); movingPoint = m_d->transaction.originalMiddleRight(); extraSign = 1.0; } // override scale static point if it is locked if ((m_d->currentArgs.transformAroundRotationCenter() ^ altModifierActive) && !qFuzzyCompare(anchorPoint.x(), movingPoint.x())) { staticPoint = anchorPoint; } QPointF mouseImagePos = m_d->transform.inverted().map(mousePos); qreal sign = mouseImagePos.x() <= staticPoint.x() ? -extraSign : extraSign; m_d->currentArgs.setScaleX(sign * m_d->currentArgs.scaleX()); QPointF staticPointInView = m_d->transform.map(staticPoint); qreal dist = kisDistance(staticPointInView, mousePos); GSL::ScaleResult1D result = GSL::calculateScaleX(m_d->currentArgs, staticPoint, staticPointInView, movingPoint, dist); if (shiftModifierActive || m_d->currentArgs.keepAspectRatio()) { qreal aspectRatio = m_d->clickArgs.scaleY() / m_d->clickArgs.scaleX(); m_d->currentArgs.setScaleY(aspectRatio * result.scale); } m_d->currentArgs.setScaleX(result.scale); m_d->currentArgs.setTransformedCenter(result.transformedCenter); break; } case TOPRIGHTSCALE: case BOTTOMRIGHTSCALE: case TOPLEFTSCALE: case BOTTOMLEFTSCALE: { QPointF staticPoint; QPointF movingPoint; if (m_d->function == TOPRIGHTSCALE) { staticPoint = m_d->transaction.originalBottomLeft(); movingPoint = m_d->transaction.originalTopRight(); } else if (m_d->function == BOTTOMRIGHTSCALE) { staticPoint = m_d->transaction.originalTopLeft(); movingPoint = m_d->transaction.originalBottomRight(); } else if (m_d->function == TOPLEFTSCALE) { staticPoint = m_d->transaction.originalBottomRight(); movingPoint = m_d->transaction.originalTopLeft(); } else { staticPoint = m_d->transaction.originalTopRight(); movingPoint = m_d->transaction.originalBottomLeft(); } // override scale static point if it is locked if ((m_d->currentArgs.transformAroundRotationCenter() ^ altModifierActive) && !(qFuzzyCompare(anchorPoint.x(), movingPoint.x()) || qFuzzyCompare(anchorPoint.y(), movingPoint.y()))) { staticPoint = anchorPoint; } QPointF staticPointInView = m_d->transform.map(staticPoint); QPointF movingPointInView = mousePos; if (shiftModifierActive || m_d->currentArgs.keepAspectRatio()) { KisTransformUtils::MatricesPack m(m_d->clickArgs); QTransform t = m.finalTransform(); QPointF refDiff = t.map(movingPoint) - staticPointInView; QPointF realDiff = mousePos - staticPointInView; realDiff = kisProjectOnVector(refDiff, realDiff); movingPointInView = staticPointInView + realDiff; } GSL::ScaleResult2D result = GSL::calculateScale2D(m_d->currentArgs, staticPoint, staticPointInView, movingPoint, movingPointInView); m_d->currentArgs.setScaleX(result.scaleX); m_d->currentArgs.setScaleY(result.scaleY); m_d->currentArgs.setTransformedCenter(result.transformedCenter); break; } case MOVECENTER: { QPointF pt = m_d->transform.inverted().map(mousePos); pt = KisTransformUtils::clipInRect(pt, m_d->transaction.originalRect()); QPointF newRotationCenterOffset = pt - m_d->currentArgs.originalCenter(); if (shiftModifierActive) { if (qAbs(newRotationCenterOffset.x()) > qAbs(newRotationCenterOffset.y())) { newRotationCenterOffset.ry() = 0; } else { newRotationCenterOffset.rx() = 0; } } m_d->currentArgs.setRotationCenterOffset(newRotationCenterOffset); emit requestResetRotationCenterButtons(); } break; case TOPSHEAR: case BOTTOMSHEAR: { KisTransformUtils::MatricesPack m(m_d->clickArgs); QTransform backwardT = (m.S * m.projectedP).inverted(); QPointF diff = backwardT.map(mousePos - m_d->clickPos); qreal sign = m_d->function == BOTTOMSHEAR ? 1.0 : -1.0; // get the dx pixels corresponding to the current shearX factor qreal dx = sign * m_d->clickArgs.shearX() * m_d->clickArgs.scaleY() * m_d->transaction.originalHalfHeight(); // get the dx pixels corresponding to the current shearX factor dx += diff.x(); // calculate the new shearX factor m_d->currentArgs.setShearX(sign * dx / m_d->currentArgs.scaleY() / m_d->transaction.originalHalfHeight()); // calculate the new shearX factor break; } case LEFTSHEAR: case RIGHTSHEAR: { KisTransformUtils::MatricesPack m(m_d->clickArgs); QTransform backwardT = (m.S * m.projectedP).inverted(); QPointF diff = backwardT.map(mousePos - m_d->clickPos); qreal sign = m_d->function == RIGHTSHEAR ? 1.0 : -1.0; // get the dx pixels corresponding to the current shearX factor qreal dy = sign * m_d->clickArgs.shearY() * m_d->clickArgs.scaleX() * m_d->transaction.originalHalfWidth(); dy += diff.y(); // calculate the new shearY factor m_d->currentArgs.setShearY(sign * dy / m_d->clickArgs.scaleX() / m_d->transaction.originalHalfWidth()); break; } } m_d->recalculateTransformations(); } bool KisFreeTransformStrategy::endPrimaryAction() { bool shouldSave = !m_d->imageTooBig; + m_d->isTransforming = false; if (m_d->imageTooBig) { m_d->currentArgs = m_d->clickArgs; m_d->recalculateTransformations(); } return shouldSave; } void KisFreeTransformStrategy::Private::recalculateTransformations() { KisTransformUtils::MatricesPack m(currentArgs); QTransform sanityCheckMatrix = m.TS * m.SC * m.S * m.projectedP; /** * The center of the original image should still * stay the origin of CS */ KIS_ASSERT_RECOVER_NOOP(sanityCheckMatrix.map(currentArgs.originalCenter()).manhattanLength() < 1e-4); transform = m.finalTransform(); QTransform viewScaleTransform = converter->imageToDocumentTransform() * converter->documentToFlakeTransform(); handlesTransform = transform * viewScaleTransform; QTransform tl = QTransform::fromTranslate(transaction.originalTopLeft().x(), transaction.originalTopLeft().y()); paintingTransform = tl.inverted() * q->thumbToImageTransform() * tl * transform * viewScaleTransform; paintingOffset = transaction.originalTopLeft(); // check whether image is too big to be displayed or not imageTooBig = KisTransformUtils::checkImageTooBig(transaction.originalRect(), m); // recalculate cached handles position recalculateTransformedHandles(); emit q->requestShowImageTooBig(imageTooBig); } diff --git a/plugins/tools/tool_transform2/kis_perspective_transform_strategy.cpp b/plugins/tools/tool_transform2/kis_perspective_transform_strategy.cpp index 9c23c4d1e3..0cff5ed2a6 100644 --- a/plugins/tools/tool_transform2/kis_perspective_transform_strategy.cpp +++ b/plugins/tools/tool_transform2/kis_perspective_transform_strategy.cpp @@ -1,683 +1,692 @@ /* * Copyright (c) 2014 Dmitry Kazakov * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "kis_perspective_transform_strategy.h" #include #include #include #include #include #include "kis_coordinates_converter.h" #include "tool_transform_args.h" #include "transform_transaction_properties.h" #include "krita_utils.h" #include "kis_cursor.h" #include "kis_transform_utils.h" #include "kis_free_transform_strategy_gsl_helpers.h" enum StrokeFunction { DRAG_HANDLE = 0, DRAG_X_VANISHING_POINT, DRAG_Y_VANISHING_POINT, MOVE, NONE }; struct KisPerspectiveTransformStrategy::Private { Private(KisPerspectiveTransformStrategy *_q, const KisCoordinatesConverter *_converter, ToolTransformArgs &_currentArgs, TransformTransactionProperties &_transaction) : q(_q), converter(_converter), currentArgs(_currentArgs), transaction(_transaction), - imageTooBig(false) + imageTooBig(false), + isTransforming(false) { } KisPerspectiveTransformStrategy *q; /// standard members /// const KisCoordinatesConverter *converter; ////// ToolTransformArgs ¤tArgs; ////// TransformTransactionProperties &transaction; QTransform thumbToImageTransform; QImage originalImage; QTransform paintingTransform; QPointF paintingOffset; QTransform handlesTransform; /// custom members /// StrokeFunction function; struct HandlePoints { bool xVanishingExists; bool yVanishingExists; QPointF xVanishing; QPointF yVanishing; }; HandlePoints transformedHandles; QTransform transform; QVector srcCornerPoints; QVector dstCornerPoints; int currentDraggingCornerPoint; bool imageTooBig; QPointF clickPos; ToolTransformArgs clickArgs; + bool isTransforming; QCursor getScaleCursor(const QPointF &handlePt); QCursor getShearCursor(const QPointF &start, const QPointF &end); void recalculateTransformations(); void recalculateTransformedHandles(); void transformIntoArgs(const Eigen::Matrix3f &t); QTransform transformFromArgs(); }; KisPerspectiveTransformStrategy::KisPerspectiveTransformStrategy(const KisCoordinatesConverter *converter, KoSnapGuide *snapGuide, ToolTransformArgs ¤tArgs, TransformTransactionProperties &transaction) : KisSimplifiedActionPolicyStrategy(converter, snapGuide), m_d(new Private(this, converter, currentArgs, transaction)) { } KisPerspectiveTransformStrategy::~KisPerspectiveTransformStrategy() { } void KisPerspectiveTransformStrategy::Private::recalculateTransformedHandles() { srcCornerPoints.clear(); srcCornerPoints << transaction.originalTopLeft(); srcCornerPoints << transaction.originalTopRight(); srcCornerPoints << transaction.originalBottomLeft(); srcCornerPoints << transaction.originalBottomRight(); dstCornerPoints.clear(); Q_FOREACH (const QPointF &pt, srcCornerPoints) { dstCornerPoints << transform.map(pt); } QMatrix4x4 realMatrix(transform); QVector4D v; v = QVector4D(1, 0, 0, 0); v = realMatrix * v; transformedHandles.xVanishingExists = !qFuzzyCompare(v.w(), 0); transformedHandles.xVanishing = v.toVector2DAffine().toPointF(); v = QVector4D(0, 1, 0, 0); v = realMatrix * v; transformedHandles.yVanishingExists = !qFuzzyCompare(v.w(), 0); transformedHandles.yVanishing = v.toVector2DAffine().toPointF(); } void KisPerspectiveTransformStrategy::setTransformFunction(const QPointF &mousePos, bool perspectiveModifierActive) { Q_UNUSED(perspectiveModifierActive); QPolygonF transformedPolygon = m_d->transform.map(QPolygonF(m_d->transaction.originalRect())); StrokeFunction defaultFunction = transformedPolygon.containsPoint(mousePos, Qt::OddEvenFill) ? MOVE : NONE; KisTransformUtils::HandleChooser handleChooser(mousePos, defaultFunction); qreal handleRadius = KisTransformUtils::effectiveHandleGrabRadius(m_d->converter); if (!m_d->transformedHandles.xVanishing.isNull()) { handleChooser.addFunction(m_d->transformedHandles.xVanishing, handleRadius, DRAG_X_VANISHING_POINT); } if (!m_d->transformedHandles.yVanishing.isNull()) { handleChooser.addFunction(m_d->transformedHandles.yVanishing, handleRadius, DRAG_Y_VANISHING_POINT); } m_d->currentDraggingCornerPoint = -1; for (int i = 0; i < m_d->dstCornerPoints.size(); i++) { if (handleChooser.addFunction(m_d->dstCornerPoints[i], handleRadius, DRAG_HANDLE)) { m_d->currentDraggingCornerPoint = i; } } m_d->function = handleChooser.function(); } QCursor KisPerspectiveTransformStrategy::getCurrentCursor() const { QCursor cursor; switch (m_d->function) { case NONE: cursor = KisCursor::arrowCursor(); break; case MOVE: cursor = KisCursor::moveCursor(); break; case DRAG_HANDLE: case DRAG_X_VANISHING_POINT: case DRAG_Y_VANISHING_POINT: cursor = KisCursor::pointingHandCursor(); break; } return cursor; } void KisPerspectiveTransformStrategy::paint(QPainter &gc) { gc.save(); gc.setOpacity(m_d->transaction.basePreviewOpacity()); gc.setTransform(m_d->paintingTransform, true); gc.drawImage(m_d->paintingOffset, originalImage()); gc.restore(); // Draw Handles QPainterPath handles; handles.moveTo(m_d->transaction.originalTopLeft()); handles.lineTo(m_d->transaction.originalTopRight()); handles.lineTo(m_d->transaction.originalBottomRight()); handles.lineTo(m_d->transaction.originalBottomLeft()); handles.lineTo(m_d->transaction.originalTopLeft()); auto addHandleRectFunc = [&](const QPointF &pt) { handles.addRect( KisTransformUtils::handleRect(KisTransformUtils::handleVisualRadius, m_d->handlesTransform, m_d->transaction.originalRect(), pt) .translated(pt)); }; addHandleRectFunc(m_d->transaction.originalTopLeft()); addHandleRectFunc(m_d->transaction.originalTopRight()); addHandleRectFunc(m_d->transaction.originalBottomLeft()); addHandleRectFunc(m_d->transaction.originalBottomRight()); gc.save(); + if (m_d->isTransforming) { + gc.setOpacity(0.1); + } + /** * WARNING: we cannot install a transform to paint the handles here! * * There is a bug in Qt that prevents painting of cosmetic-pen * brushes in openGL mode when a TxProject matrix is active on * a QPainter. So just convert it manually. * * https://bugreports.qt-project.org/browse/QTBUG-42658 */ //gc.setTransform(m_d->handlesTransform, true); <-- don't do like this! QPainterPath mappedHandles = m_d->handlesTransform.map(handles); QPen pen[2]; pen[0].setWidth(1); pen[1].setWidth(2); pen[1].setColor(Qt::lightGray); for (int i = 1; i >= 0; --i) { gc.setPen(pen[i]); gc.drawPath(mappedHandles); } gc.restore(); { // painting perspective handles QPainterPath perspectiveHandles; QRectF handleRect = KisTransformUtils::handleRect(KisTransformUtils::handleVisualRadius, QTransform(), m_d->transaction.originalRect(), 0, 0); if (m_d->transformedHandles.xVanishingExists) { QRectF rc = handleRect.translated(m_d->transformedHandles.xVanishing); perspectiveHandles.addEllipse(rc); } if (m_d->transformedHandles.yVanishingExists) { QRectF rc = handleRect.translated(m_d->transformedHandles.yVanishing); perspectiveHandles.addEllipse(rc); } if (!perspectiveHandles.isEmpty()) { gc.save(); gc.setTransform(m_d->converter->imageToWidgetTransform()); gc.setBrush(Qt::red); for (int i = 1; i >= 0; --i) { gc.setPen(pen[i]); gc.drawPath(perspectiveHandles); } gc.restore(); } } } void KisPerspectiveTransformStrategy::externalConfigChanged() { m_d->recalculateTransformations(); } bool KisPerspectiveTransformStrategy::beginPrimaryAction(const QPointF &pt) { Q_UNUSED(pt); if (m_d->function == NONE) return false; m_d->clickPos = pt; m_d->clickArgs = m_d->currentArgs; return true; } Eigen::Matrix3f getTransitionMatrix(const QVector &sp) { Eigen::Matrix3f A; Eigen::Vector3f v3; A << sp[0].x() , sp[1].x() , sp[2].x() ,sp[0].y() , sp[1].y() , sp[2].y() , 1 , 1 , 1; v3 << sp[3].x() , sp[3].y() , 1; Eigen::Vector3f coeffs = A.colPivHouseholderQr().solve(v3); A.col(0) *= coeffs(0); A.col(1) *= coeffs(1); A.col(2) *= coeffs(2); return A; } QTransform toQTransform(const Eigen::Matrix3f &m) { return QTransform(m(0,0), m(1,0), m(2,0), m(0,1), m(1,1), m(2,1), m(0,2), m(1,2), m(2,2)); } Eigen::Matrix3f fromQTransform(const QTransform &t) { Eigen::Matrix3f m; m << t.m11() , t.m21() , t.m31() ,t.m12() , t.m22() , t.m32() ,t.m13() , t.m23() , t.m33(); return m; } Eigen::Matrix3f fromTranslate(const QPointF &pt) { Eigen::Matrix3f m; m << 1 , 0 , pt.x() ,0 , 1 , pt.y() ,0 , 0 , 1; return m; } Eigen::Matrix3f fromScale(qreal sx, qreal sy) { Eigen::Matrix3f m; m << sx , 0 , 0 ,0 , sy , 0 ,0 , 0 , 1; return m; } Eigen::Matrix3f fromShear(qreal sx, qreal sy) { Eigen::Matrix3f m; m << 1 , sx , 0 ,sy , sx*sy + 1, 0 ,0 , 0 , 1; return m; } void KisPerspectiveTransformStrategy::Private::transformIntoArgs(const Eigen::Matrix3f &t) { Eigen::Matrix3f TS = fromTranslate(-currentArgs.originalCenter()); Eigen::Matrix3f m = t * TS.inverse(); qreal tX = m(0,2) / m(2,2); qreal tY = m(1,2) / m(2,2); Eigen::Matrix3f T = fromTranslate(QPointF(tX, tY)); m = T.inverse() * m; // TODO: implement matrix decomposition as described here // https://www.w3.org/TR/css-transforms-1/#decomposing-a-3d-matrix // For now use an extremely hackish approximation if (m(0,1) != 0.0 && m(0,0) != 0.0 && m(2,2) != 0.0) { const qreal factor = (m(1,1) / m(0,1) - m(1,0) / m(0,0)); qreal scaleX = m(0,0) / m(2,2); qreal scaleY = m(0,1) / m(2,2) * factor; Eigen::Matrix3f SC = fromScale(scaleX, scaleY); qreal shearX = 1.0 / factor; qreal shearY = m(1,0) / m(0,0); Eigen::Matrix3f S = fromShear(shearX, shearY); currentArgs.setScaleX(scaleX); currentArgs.setScaleY(scaleY); currentArgs.setShearX(shearX); currentArgs.setShearY(shearY); m = m * SC.inverse(); m = m * S.inverse(); m /= m(2,2); } else { currentArgs.setScaleX(1.0); currentArgs.setScaleY(1.0); currentArgs.setShearX(0.0); currentArgs.setShearY(0.0); } currentArgs.setTransformedCenter(QPointF(tX, tY)); currentArgs.setFlattenedPerspectiveTransform(toQTransform(m)); } QTransform KisPerspectiveTransformStrategy::Private::transformFromArgs() { KisTransformUtils::MatricesPack m(currentArgs); return m.finalTransform(); } QVector4D fromQPointF(const QPointF &pt) { return QVector4D(pt.x(), pt.y(), 0, 1.0); } QPointF toQPointF(const QVector4D &v) { return v.toVector2DAffine().toPointF(); } void KisPerspectiveTransformStrategy::continuePrimaryAction(const QPointF &mousePos, bool shiftModifierActve, bool altModifierActive) { Q_UNUSED(shiftModifierActve); Q_UNUSED(altModifierActive); + m_d->isTransforming = true; + switch (m_d->function) { case NONE: break; case MOVE: { QPointF diff = mousePos - m_d->clickPos; m_d->currentArgs.setTransformedCenter( m_d->clickArgs.transformedCenter() + diff); break; } case DRAG_HANDLE: { KIS_ASSERT_RECOVER_RETURN(m_d->currentDraggingCornerPoint >=0); m_d->dstCornerPoints[m_d->currentDraggingCornerPoint] = mousePos; Eigen::Matrix3f A = getTransitionMatrix(m_d->srcCornerPoints); Eigen::Matrix3f B = getTransitionMatrix(m_d->dstCornerPoints); Eigen::Matrix3f result = B * A.inverse(); m_d->transformIntoArgs(result); break; } case DRAG_X_VANISHING_POINT: case DRAG_Y_VANISHING_POINT: { QMatrix4x4 m(m_d->transform); QPointF tl = m_d->transaction.originalTopLeft(); QPointF tr = m_d->transaction.originalTopRight(); QPointF bl = m_d->transaction.originalBottomLeft(); QPointF br = m_d->transaction.originalBottomRight(); QVector4D v(1,0,0,0); QVector4D otherV(0,1,0,0); if (m_d->function == DRAG_X_VANISHING_POINT) { v = QVector4D(1,0,0,0); otherV = QVector4D(0,1,0,0); } else { v = QVector4D(0,1,0,0); otherV = QVector4D(1,0,0,0); } QPointF tl_dst = toQPointF(m * fromQPointF(tl)); QPointF tr_dst = toQPointF(m * fromQPointF(tr)); QPointF bl_dst = toQPointF(m * fromQPointF(bl)); QPointF br_dst = toQPointF(m * fromQPointF(br)); QPointF v_dst = toQPointF(m * v); QPointF otherV_dst = toQPointF(m * otherV); QVector srcPoints; QVector dstPoints; QPointF far1_src; QPointF far2_src; QPointF near1_src; QPointF near2_src; QPointF far1_dst; QPointF far2_dst; QPointF near1_dst; QPointF near2_dst; if (m_d->function == DRAG_X_VANISHING_POINT) { // topLeft (far) --- topRight (near) --- vanishing if (kisSquareDistance(v_dst, tl_dst) > kisSquareDistance(v_dst, tr_dst)) { far1_src = tl; far2_src = bl; near1_src = tr; near2_src = br; far1_dst = tl_dst; far2_dst = bl_dst; near1_dst = tr_dst; near2_dst = br_dst; // topRight (far) --- topLeft (near) --- vanishing } else { far1_src = tr; far2_src = br; near1_src = tl; near2_src = bl; far1_dst = tr_dst; far2_dst = br_dst; near1_dst = tl_dst; near2_dst = bl_dst; } } else /* if (m_d->function == DRAG_Y_VANISHING_POINT) */{ // topLeft (far) --- bottomLeft (near) --- vanishing if (kisSquareDistance(v_dst, tl_dst) > kisSquareDistance(v_dst, bl_dst)) { far1_src = tl; far2_src = tr; near1_src = bl; near2_src = br; far1_dst = tl_dst; far2_dst = tr_dst; near1_dst = bl_dst; near2_dst = br_dst; // bottomLeft (far) --- topLeft (near) --- vanishing } else { far1_src = bl; far2_src = br; near1_src = tl; near2_src = tr; far1_dst = bl_dst; far2_dst = br_dst; near1_dst = tl_dst; near2_dst = tr_dst; } } QLineF l0(far1_dst, mousePos); QLineF l1(far2_dst, mousePos); QLineF l2(otherV_dst, near1_dst); l0.intersect(l2, &near1_dst); l1.intersect(l2, &near2_dst); srcPoints << far1_src; srcPoints << far2_src; srcPoints << near1_src; srcPoints << near2_src; dstPoints << far1_dst; dstPoints << far2_dst; dstPoints << near1_dst; dstPoints << near2_dst; Eigen::Matrix3f A = getTransitionMatrix(srcPoints); Eigen::Matrix3f B = getTransitionMatrix(dstPoints); Eigen::Matrix3f result = B * A.inverse(); m_d->transformIntoArgs(result); break; } } m_d->recalculateTransformations(); } bool KisPerspectiveTransformStrategy::endPrimaryAction() { bool shouldSave = !m_d->imageTooBig; + m_d->isTransforming = false; if (m_d->imageTooBig) { m_d->currentArgs = m_d->clickArgs; m_d->recalculateTransformations(); } return shouldSave; } void KisPerspectiveTransformStrategy::Private::recalculateTransformations() { transform = transformFromArgs(); QTransform viewScaleTransform = converter->imageToDocumentTransform() * converter->documentToFlakeTransform(); handlesTransform = transform * viewScaleTransform; QTransform tl = QTransform::fromTranslate(transaction.originalTopLeft().x(), transaction.originalTopLeft().y()); paintingTransform = tl.inverted() * q->thumbToImageTransform() * tl * transform * viewScaleTransform; paintingOffset = transaction.originalTopLeft(); // check whether image is too big to be displayed or not const qreal maxScale = 20.0; imageTooBig = false; if (qAbs(currentArgs.scaleX()) > maxScale || qAbs(currentArgs.scaleY()) > maxScale) { imageTooBig = true; } else { QVector points; points << transaction.originalRect().topLeft(); points << transaction.originalRect().topRight(); points << transaction.originalRect().bottomRight(); points << transaction.originalRect().bottomLeft(); for (int i = 0; i < points.size(); i++) { points[i] = transform.map(points[i]); } for (int i = 0; i < points.size(); i++) { const QPointF &pt = points[i]; const QPointF &prev = points[(i - 1 + 4) % 4]; const QPointF &next = points[(i + 1) % 4]; const QPointF &other = points[(i + 2) % 4]; QLineF l1(pt, other); QLineF l2(prev, next); QPointF intersection; l1.intersect(l2, &intersection); qreal maxDistance = kisSquareDistance(pt, other); if (kisSquareDistance(pt, intersection) > maxDistance || kisSquareDistance(other, intersection) > maxDistance) { imageTooBig = true; break; } const qreal thresholdDistance = 0.02 * l2.length(); if (kisDistanceToLine(pt, l2) < thresholdDistance) { imageTooBig = true; break; } } } // recalculate cached handles position recalculateTransformedHandles(); emit q->requestShowImageTooBig(imageTooBig); }