File indexing completed on 2025-02-23 04:08:58

0001 /*
0002  *  SPDX-FileCopyrightText: 2010 Dmitry Kazakov <dimula73@gmail.com>
0003  *  SPDX-FileCopyrightText: 2011 Silvio Heinrich <plassy@web.de>
0004  *
0005  *  SPDX-License-Identifier: GPL-2.0-or-later
0006  */
0007 
0008 #include <cmath>
0009 
0010 #include "kis_coordinates_converter.h"
0011 
0012 #include <QtMath>
0013 #include <QTransform>
0014 #include <KoViewConverter.h>
0015 
0016 #include <kis_config.h>
0017 #include <kis_image.h>
0018 
0019 
0020 struct KisCoordinatesConverter::Private {
0021     Private():
0022         isXAxisMirrored(false),
0023         isYAxisMirrored(false),
0024         isRotating(false),
0025         isNativeGesture(false),
0026         rotationAngle(0.0),
0027         rotationBaseAngle(0.0),
0028         devicePixelRatio(1.0)
0029     {
0030     }
0031 
0032     KisImageWSP image;
0033 
0034     bool isXAxisMirrored;
0035     bool isYAxisMirrored;
0036     bool isRotating;
0037     bool isNativeGesture;
0038     qreal rotationAngle;
0039     qreal rotationBaseAngle;
0040     QSizeF canvasWidgetSize;
0041     qreal devicePixelRatio;
0042     QPointF documentOffset;
0043 
0044     QTransform flakeToWidget;
0045     QTransform rotationBaseTransform;
0046     QTransform imageToDocument;
0047     QTransform documentToFlake;
0048     QTransform widgetToViewport;
0049 };
0050 
0051 /**
0052  * When vastScrolling value is less than 0.5 it is possible
0053  * that the whole scrolling area (viewport) will be smaller than
0054  * the size of the widget. In such cases the image should be
0055  * centered in the widget. Previously we used a special parameter
0056  * documentOrigin for this purpose, now the value for this
0057  * centering is calculated dynamically, helping the offset to
0058  * center the image inside the widget
0059  *
0060  * Note that the correction is null when the size of the document
0061  * plus vast scrolling reserve is larger than the widget. This
0062  * is always true for vastScrolling parameter > 0.5.
0063  */
0064 
0065 QPointF KisCoordinatesConverter::centeringCorrection() const
0066 {
0067     KisConfig cfg(true);
0068 
0069     QSize documentSize = imageRectInWidgetPixels().toAlignedRect().size();
0070     QPointF dPoint(documentSize.width(), documentSize.height());
0071     QPointF wPoint(m_d->canvasWidgetSize.width(), m_d->canvasWidgetSize.height());
0072 
0073     QPointF minOffset = -cfg.vastScrolling() * wPoint;
0074     QPointF maxOffset = dPoint - wPoint + cfg.vastScrolling() * wPoint;
0075 
0076     QPointF range = maxOffset - minOffset;
0077 
0078     range.rx() = qMin(range.x(), (qreal)0.0);
0079     range.ry() = qMin(range.y(), (qreal)0.0);
0080 
0081     range /= 2;
0082 
0083     return -range;
0084 }
0085 
0086 /**
0087  * The document offset and the position of the top left corner of the
0088  * image must always coincide, that is why we need to correct them to
0089  * and fro.
0090  *
0091  * When we change zoom level, the calculation of the new offset is
0092  * done by KoCanvasControllerWidget, that is why we just passively fix
0093  * the flakeToWidget transform to conform the offset and wait until
0094  * the canvas controller will recenter us.
0095  *
0096  * But when we do our own transformations of the canvas, like rotation
0097  * and mirroring, we cannot rely on the centering of the canvas
0098  * controller and we do it ourselves. Then we just set new offset and
0099  * return its value to be set in the canvas controller explicitly.
0100  */
0101 
0102 void KisCoordinatesConverter::correctOffsetToTransformation()
0103 {
0104     m_d->documentOffset = snapToDevicePixel(-(imageRectInWidgetPixels().topLeft() -
0105           centeringCorrection()));
0106 }
0107 
0108 void KisCoordinatesConverter::correctTransformationToOffset()
0109 {
0110     QPointF topLeft = imageRectInWidgetPixels().topLeft();
0111     QPointF diff = (-topLeft) - m_d->documentOffset;
0112     diff += centeringCorrection();
0113     m_d->flakeToWidget *= QTransform::fromTranslate(diff.x(), diff.y());
0114 }
0115 
0116 void KisCoordinatesConverter::recalculateTransformations()
0117 {
0118     if(!m_d->image) return;
0119 
0120     m_d->imageToDocument = QTransform::fromScale(1 / m_d->image->xRes(),
0121                                                  1 / m_d->image->yRes());
0122 
0123     qreal zoomX, zoomY;
0124     KoZoomHandler::zoom(&zoomX, &zoomY);
0125     m_d->documentToFlake = QTransform::fromScale(zoomX, zoomY);
0126 
0127     correctTransformationToOffset();
0128 
0129     QRectF irect = imageRectInWidgetPixels();
0130     QRectF wrect = QRectF(QPoint(0,0), m_d->canvasWidgetSize);
0131     QRectF rrect = irect & wrect;
0132 
0133     QTransform reversedTransform = flakeToWidgetTransform().inverted();
0134     QRectF     canvasBounds      = reversedTransform.mapRect(rrect);
0135     QPointF    offset            = canvasBounds.topLeft();
0136 
0137     m_d->widgetToViewport = reversedTransform * QTransform::fromTranslate(-offset.x(), -offset.y());
0138 }
0139 
0140 
0141 KisCoordinatesConverter::KisCoordinatesConverter()
0142     : m_d(new Private) { }
0143 
0144 KisCoordinatesConverter::~KisCoordinatesConverter()
0145 {
0146     delete m_d;
0147 }
0148 
0149 QSizeF KisCoordinatesConverter::getCanvasWidgetSize() const
0150 {
0151     return m_d->canvasWidgetSize;
0152 }
0153 
0154 void KisCoordinatesConverter::setCanvasWidgetSize(QSizeF size)
0155 {
0156     m_d->canvasWidgetSize = size;
0157     recalculateTransformations();
0158 }
0159 
0160 void KisCoordinatesConverter::setDevicePixelRatio(qreal value)
0161 {
0162     m_d->devicePixelRatio = value;
0163 }
0164 
0165 void KisCoordinatesConverter::setImage(KisImageWSP image)
0166 {
0167     m_d->image = image;
0168     recalculateTransformations();
0169 }
0170 
0171 void KisCoordinatesConverter::setDocumentOffset(const QPointF& offset)
0172 {
0173     QPointF diff = m_d->documentOffset - offset;
0174 
0175     m_d->documentOffset = offset;
0176     m_d->flakeToWidget *= QTransform::fromTranslate(diff.x(), diff.y());
0177     recalculateTransformations();
0178 }
0179 
0180 qreal KisCoordinatesConverter::devicePixelRatio() const
0181 {
0182     return m_d->devicePixelRatio;
0183 }
0184 
0185 QPoint KisCoordinatesConverter::documentOffset() const
0186 {
0187     return QPoint(int(m_d->documentOffset.x()), int(m_d->documentOffset.y()));
0188 }
0189 
0190 qreal KisCoordinatesConverter::rotationAngle() const
0191 {
0192     return m_d->rotationAngle;
0193 }
0194 
0195 void KisCoordinatesConverter::setZoom(qreal zoom)
0196 {
0197     KoZoomHandler::setZoom(zoom);
0198     recalculateTransformations();
0199 }
0200 
0201 qreal KisCoordinatesConverter::effectiveZoom() const
0202 {
0203     qreal scaleX, scaleY;
0204     this->imageScale(&scaleX, &scaleY);
0205 
0206     if (scaleX != scaleY) {
0207         qWarning() << "WARNING: Zoom is not isotropic!"  << ppVar(scaleX) << ppVar(scaleY) << ppVar(qFuzzyCompare(scaleX, scaleY));
0208     }
0209 
0210     // zoom by average of x and y
0211     return 0.5 * (scaleX + scaleY);
0212 }
0213 
0214 qreal KisCoordinatesConverter::effectivePhysicalZoom() const
0215 {
0216     qreal scaleX, scaleY;
0217     this->imagePhysicalScale(&scaleX, &scaleY);
0218 
0219     if (scaleX != scaleY) {
0220         qWarning() << "WARNING: Zoom is not isotropic!"  << ppVar(scaleX) << ppVar(scaleY) << ppVar(qFuzzyCompare(scaleX, scaleY));
0221     }
0222 
0223     // zoom by average of x and y
0224     return 0.5 * (scaleX + scaleY);
0225 }
0226 
0227 void KisCoordinatesConverter::enableNatureGestureFlag()
0228 {
0229     m_d->isNativeGesture = true;
0230 }
0231 
0232 void KisCoordinatesConverter::beginRotation()
0233 {
0234     KIS_SAFE_ASSERT_RECOVER_NOOP(!m_d->isRotating);
0235 
0236     // Save the current transformation and angle to use as the base of the ongoing rotation.
0237     m_d->rotationBaseTransform = m_d->flakeToWidget;
0238     m_d->rotationBaseAngle = m_d->rotationAngle;
0239     m_d->isRotating = true;
0240 }
0241 
0242 void KisCoordinatesConverter::endRotation()
0243 {
0244     KIS_SAFE_ASSERT_RECOVER_NOOP(m_d->isRotating);
0245 
0246     m_d->isNativeGesture = false;
0247     m_d->isRotating = false;
0248 }
0249 
0250 QPoint KisCoordinatesConverter::rotate(QPointF center, qreal angle)
0251 {
0252     QTransform rot;
0253     rot.rotate(angle);
0254 
0255     if (!m_d->isNativeGesture && m_d->isRotating)
0256     {
0257         // Modal (begin/end) rotation. Transform from the stable base.
0258         m_d->flakeToWidget = m_d->rotationBaseTransform;
0259         m_d->rotationAngle = std::fmod(m_d->rotationBaseAngle + angle, 360.0);
0260     }
0261     else
0262     {
0263         // Immediate rotation, directly applied to the canvas transformation.
0264         m_d->rotationAngle = std::fmod(m_d->rotationAngle + angle, 360.0);
0265     }
0266 
0267     m_d->flakeToWidget *= QTransform::fromTranslate(-center.x(),-center.y());
0268     m_d->flakeToWidget *= rot;
0269     m_d->flakeToWidget *= QTransform::fromTranslate(center.x(), center.y());
0270 
0271     correctOffsetToTransformation();
0272     recalculateTransformations();
0273 
0274     return m_d->documentOffset.toPoint();
0275 }
0276 
0277 QPoint KisCoordinatesConverter::mirror(QPointF center, bool mirrorXAxis, bool mirrorYAxis)
0278 {
0279     bool keepOrientation = false; // XXX: Keep here for now, maybe some day we can restore the parameter again.
0280 
0281     bool       doXMirroring = m_d->isXAxisMirrored ^ mirrorXAxis;
0282     bool       doYMirroring = m_d->isYAxisMirrored ^ mirrorYAxis;
0283     qreal      scaleX       = doXMirroring ? -1.0 : 1.0;
0284     qreal      scaleY       = doYMirroring ? -1.0 : 1.0;
0285     QTransform mirror       = QTransform::fromScale(scaleX, scaleY);
0286 
0287     QTransform rot;
0288     rot.rotate(m_d->rotationAngle);
0289 
0290     m_d->flakeToWidget *= QTransform::fromTranslate(-center.x(),-center.y());
0291 
0292     if (keepOrientation) {
0293         m_d->flakeToWidget *= rot.inverted();
0294     }
0295 
0296     m_d->flakeToWidget *= mirror;
0297 
0298     if (keepOrientation) {
0299         m_d->flakeToWidget *= rot;
0300     }
0301 
0302     m_d->flakeToWidget *= QTransform::fromTranslate(center.x(),center.y());
0303 
0304 
0305     if (!keepOrientation && (doXMirroring ^ doYMirroring)) {
0306         m_d->rotationAngle = -m_d->rotationAngle;
0307     }
0308 
0309     m_d->isXAxisMirrored = mirrorXAxis;
0310     m_d->isYAxisMirrored = mirrorYAxis;
0311 
0312     correctOffsetToTransformation();
0313     recalculateTransformations();
0314 
0315     return m_d->documentOffset.toPoint();
0316 }
0317 
0318 bool KisCoordinatesConverter::xAxisMirrored() const
0319 {
0320     return m_d->isXAxisMirrored;
0321 }
0322 
0323 bool KisCoordinatesConverter::yAxisMirrored() const
0324 {
0325     return m_d->isYAxisMirrored;
0326 }
0327 
0328 QPoint KisCoordinatesConverter::resetRotation(QPointF center)
0329 {
0330     QTransform rot;
0331     rot.rotate(-m_d->rotationAngle);
0332 
0333     m_d->flakeToWidget *= QTransform::fromTranslate(-center.x(), -center.y());
0334     m_d->flakeToWidget *= rot;
0335     m_d->flakeToWidget *= QTransform::fromTranslate(center.x(), center.y());
0336     m_d->rotationAngle = 0.0;
0337 
0338     correctOffsetToTransformation();
0339     recalculateTransformations();
0340 
0341     return m_d->documentOffset.toPoint();
0342 }
0343 
0344 QTransform KisCoordinatesConverter::imageToWidgetTransform() const {
0345     return m_d->imageToDocument * m_d->documentToFlake * m_d->flakeToWidget;
0346 }
0347 
0348 QTransform KisCoordinatesConverter::imageToDocumentTransform() const {
0349     return m_d->imageToDocument;
0350 }
0351 
0352 QTransform KisCoordinatesConverter::documentToFlakeTransform() const {
0353     return m_d->documentToFlake;
0354 }
0355 
0356 QTransform KisCoordinatesConverter::flakeToWidgetTransform() const {
0357     return m_d->flakeToWidget;
0358 }
0359 
0360 QTransform KisCoordinatesConverter::documentToWidgetTransform() const {
0361     return m_d->documentToFlake * m_d->flakeToWidget;
0362 }
0363 
0364 QTransform KisCoordinatesConverter::viewportToWidgetTransform() const {
0365     return m_d->widgetToViewport.inverted();
0366 }
0367 
0368 QTransform KisCoordinatesConverter::imageToViewportTransform() const {
0369     return m_d->imageToDocument * m_d->documentToFlake * m_d->flakeToWidget * m_d->widgetToViewport;
0370 }
0371 
0372 void KisCoordinatesConverter::getQPainterCheckersInfo(QTransform *transform,
0373                                                       QPointF *brushOrigin,
0374                                                       QPolygonF *polygon,
0375                                                       const bool scrollCheckers) const
0376 {
0377     /**
0378      * Qt has different rounding for QPainter::drawRect/drawImage.
0379      * The image is rounded mathematically, while rect in aligned
0380      * to the next integer. That causes transparent line appear on
0381      * the canvas.
0382      *
0383      * See: https://bugreports.qt.nokia.com/browse/QTBUG-22827
0384      */
0385 
0386     QRectF imageRect = imageRectInViewportPixels();
0387     imageRect.adjust(0,0,-0.5,-0.5);
0388 
0389     if (scrollCheckers) {
0390         *transform = viewportToWidgetTransform();
0391         *polygon = imageRect;
0392         *brushOrigin = imageToViewport(QPointF(0,0));
0393     }
0394     else {
0395         *transform = QTransform();
0396         *polygon = viewportToWidgetTransform().map(imageRect);
0397         *brushOrigin = QPoint(0,0);
0398     }
0399 }
0400 
0401 void KisCoordinatesConverter::getOpenGLCheckersInfo(const QRectF &viewportRect,
0402                                                     QTransform *textureTransform,
0403                                                     QTransform *modelTransform,
0404                                                     QRectF *textureRect,
0405                                                     QRectF *modelRect,
0406                                                     const bool scrollCheckers) const
0407 {
0408     if(scrollCheckers) {
0409         *textureTransform = QTransform();
0410         *textureRect = QRectF(0, 0, viewportRect.width(),viewportRect.height());
0411     }
0412     else {
0413         *textureTransform = viewportToWidgetTransform();
0414         *textureRect = viewportRect;
0415     }
0416 
0417     *modelTransform = viewportToWidgetTransform();
0418     *modelRect = viewportRect;
0419 }
0420 
0421 QPointF KisCoordinatesConverter::imageCenterInWidgetPixel() const
0422 {
0423     if(!m_d->image)
0424         return QPointF();
0425 
0426     QPolygonF poly = imageToWidget(QPolygon(m_d->image->bounds()));
0427     return (poly[0] + poly[1] + poly[2] + poly[3]) / 4.0;
0428 }
0429 
0430 
0431 // these functions return a bounding rect if the canvas is rotated
0432 
0433 QRectF KisCoordinatesConverter::imageRectInWidgetPixels() const
0434 {
0435     if(!m_d->image) return QRectF();
0436     return imageToWidget(m_d->image->bounds());
0437 }
0438 
0439 QRectF KisCoordinatesConverter::imageRectInViewportPixels() const
0440 {
0441     if(!m_d->image) return QRectF();
0442     return imageToViewport(m_d->image->bounds());
0443 }
0444 
0445 QRect KisCoordinatesConverter::imageRectInImagePixels() const
0446 {
0447     if(!m_d->image) return QRect();
0448     return m_d->image->bounds();
0449 }
0450 
0451 QRectF KisCoordinatesConverter::imageRectInDocumentPixels() const
0452 {
0453     if(!m_d->image) return QRectF();
0454     return imageToDocument(m_d->image->bounds());
0455 }
0456 
0457 QSizeF KisCoordinatesConverter::imageSizeInFlakePixels() const
0458 {
0459     if(!m_d->image) return QSizeF();
0460 
0461     qreal scaleX, scaleY;
0462     imageScale(&scaleX, &scaleY);
0463     QSize imageSize = m_d->image->size();
0464 
0465     return QSizeF(imageSize.width() * scaleX, imageSize.height() * scaleY);
0466 }
0467 
0468 QRectF KisCoordinatesConverter::widgetRectInFlakePixels() const
0469 {
0470     return widgetToFlake(QRectF(QPoint(0,0), m_d->canvasWidgetSize));
0471 }
0472 
0473 QRectF KisCoordinatesConverter::widgetRectInImagePixels() const
0474 {
0475     return widgetToImage(QRectF(QPoint(0,0), m_d->canvasWidgetSize));
0476 }
0477 
0478 QPointF KisCoordinatesConverter::flakeCenterPoint() const
0479 {
0480     QRectF widgetRect = widgetRectInFlakePixels();
0481     return QPointF(widgetRect.left() + widgetRect.width() / 2,
0482                    widgetRect.top() + widgetRect.height() / 2);
0483 }
0484 
0485 QPointF KisCoordinatesConverter::widgetCenterPoint() const
0486 {
0487     return QPointF(m_d->canvasWidgetSize.width() / 2.0, m_d->canvasWidgetSize.height() / 2.0);
0488 }
0489 
0490 void KisCoordinatesConverter::imageScale(qreal *scaleX, qreal *scaleY) const
0491 {
0492     if(!m_d->image) {
0493         *scaleX = 1.0;
0494         *scaleY = 1.0;
0495         return;
0496     }
0497 
0498     // get the x and y zoom level of the canvas
0499     qreal zoomX, zoomY;
0500     KoZoomHandler::zoom(&zoomX, &zoomY);
0501 
0502     // Get the KisImage resolution
0503     qreal resX = m_d->image->xRes();
0504     qreal resY = m_d->image->yRes();
0505 
0506     // Compute the scale factors
0507     *scaleX = zoomX / resX;
0508     *scaleY = zoomY / resY;
0509 }
0510 
0511 void KisCoordinatesConverter::imagePhysicalScale(qreal *scaleX, qreal *scaleY) const
0512 {
0513     imageScale(scaleX, scaleY);
0514     *scaleX *= m_d->devicePixelRatio;
0515     *scaleY *= m_d->devicePixelRatio;
0516 }
0517 
0518 /**
0519  * @brief Adjust a given pair of coordinates to the nearest device pixel
0520  *        according to the value of `devicePixelRatio`.
0521  * @param point a point in logical pixel space
0522  * @return The point in logical pixel space but adjusted to the nearest device
0523  *         pixel
0524  */
0525 
0526 QPointF KisCoordinatesConverter::snapToDevicePixel(const QPointF &point) const
0527 {
0528     QPoint devicePixel = (point * m_d->devicePixelRatio).toPoint();
0529     // These adjusted coords will be in logical pixel but is aligned in device
0530     // pixel space for pixel-perfect rendering.
0531     return QPointF(devicePixel) / m_d->devicePixelRatio;
0532 }
0533 
0534 QTransform KisCoordinatesConverter::viewToWidget() const
0535 {
0536     return flakeToWidgetTransform();
0537 }
0538 
0539 QTransform KisCoordinatesConverter::widgetToView() const
0540 {
0541     return flakeToWidgetTransform().inverted();
0542 }