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 }