File indexing completed on 2025-02-02 04:22:31

0001 /*
0002  *
0003  *  SPDX-FileCopyrightText: 2007 Sven Langkamp <sven.langkamp@gmail.com>
0004  *
0005  *  SPDX-License-Identifier: GPL-2.0-or-later
0006  */
0007 
0008 #include "kis_tool_measure.h"
0009 
0010 #include <math.h>
0011 
0012 #include <QPainter>
0013 #include <QLayout>
0014 #include <QWidget>
0015 #include <QLabel>
0016 #include <QPainterPath>
0017 #include <kcombobox.h>
0018 
0019 #include <kis_debug.h>
0020 #include <klocalizedstring.h>
0021 
0022 #include "kis_algebra_2d.h"
0023 #include "kis_image.h"
0024 #include "kis_cursor.h"
0025 #include "KoPointerEvent.h"
0026 #include "KoCanvasBase.h"
0027 #include <KoViewConverter.h>
0028 #include "krita_utils.h"
0029 #include "kis_floating_message.h"
0030 #include "kis_canvas2.h"
0031 #include "KisViewManager.h"
0032 #include <KisOptimizedBrushOutline.h>
0033 
0034 #define INNER_RADIUS 50
0035 
0036 KisToolMeasureOptionsWidget::KisToolMeasureOptionsWidget(QWidget* parent, KisImageWSP image)
0037         : QWidget(parent),
0038         m_resolution(image->xRes()),
0039         m_unit(KoUnit::Pixel)
0040 {
0041     m_distance = 0.0;
0042 
0043     QGridLayout* optionLayout = new QGridLayout(this);
0044     Q_CHECK_PTR(optionLayout);
0045     optionLayout->setMargin(0);
0046 
0047     optionLayout->addWidget(new QLabel(i18n("Distance:"), this), 0, 0);
0048     optionLayout->addWidget(new QLabel(i18n("Angle:"), this), 1, 0);
0049 
0050     m_distanceLabel = new QLabel(this);
0051     m_distanceLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
0052     optionLayout->addWidget(m_distanceLabel, 0, 1);
0053 
0054     m_angleLabel = new QLabel(this);
0055     m_angleLabel->setAlignment(Qt::AlignRight | Qt::AlignVCenter);
0056     optionLayout->addWidget(m_angleLabel, 1, 1);
0057 
0058     KComboBox* unitBox = new KComboBox(this);
0059     unitBox->addItems(KoUnit::listOfUnitNameForUi(KoUnit::ListAll));
0060     connect(unitBox, SIGNAL(currentIndexChanged(int)), this, SLOT(slotUnitChanged(int)));
0061     unitBox->setCurrentIndex(m_unit.indexInListForUi(KoUnit::ListAll));
0062 
0063     optionLayout->addWidget(unitBox, 0, 2);
0064     optionLayout->addItem(new QSpacerItem(1, 1, QSizePolicy::Fixed, QSizePolicy::Expanding), 2, 0, 1, 2);
0065 
0066     connect(image, SIGNAL(sigResolutionChanged(double, double)), this, SLOT(slotResolutionChanged(double, double)));
0067 }
0068 
0069 void KisToolMeasureOptionsWidget::slotSetDistance(double distance)
0070 {
0071     m_distance = distance;
0072     updateDistance();
0073 }
0074 
0075 void KisToolMeasureOptionsWidget::slotSetAngle(double angle)
0076 {
0077     m_angleLabel->setText(i18nc("angle value in degrees", "%1°", KritaUtils::prettyFormatReal(angle)));
0078 }
0079 
0080 void KisToolMeasureOptionsWidget::slotUnitChanged(int index)
0081 {
0082     m_unit = KoUnit::fromListForUi(index, KoUnit::ListAll, m_resolution);
0083     updateDistance();
0084 }
0085 
0086 void KisToolMeasureOptionsWidget::slotResolutionChanged(double xRes, double /*yRes*/)
0087 {
0088     m_resolution = xRes;
0089     updateDistance();
0090 }
0091 
0092 void KisToolMeasureOptionsWidget::updateDistance()
0093 {
0094     double distance = m_distance / m_resolution;
0095     m_distanceLabel->setText(KritaUtils::prettyFormatReal(m_unit.toUserValue(distance)));
0096 }
0097 
0098 
0099 KisToolMeasure::KisToolMeasure(KoCanvasBase * canvas)
0100     : KisTool(canvas, KisCursor::crossCursor())
0101 {
0102 }
0103 
0104 KisToolMeasure::~KisToolMeasure()
0105 {
0106 }
0107 QPointF KisToolMeasure::lockedAngle(QPointF pos)
0108 {
0109     const QPointF lineVector = pos - m_startPos;
0110     qreal lineAngle = normalizeAngle(std::atan2(lineVector.y(), lineVector.x()));
0111 
0112     const qreal ANGLE_BETWEEN_CONSTRAINED_LINES = (2 * M_PI) / 24;
0113 
0114     const quint32 constrainedLineIndex = static_cast<quint32>((lineAngle / ANGLE_BETWEEN_CONSTRAINED_LINES) + 0.5);
0115     const qreal constrainedLineAngle = constrainedLineIndex * ANGLE_BETWEEN_CONSTRAINED_LINES;
0116 
0117     const qreal lineLength = KisAlgebra2D::norm(lineVector);
0118 
0119     const QPointF constrainedLineVector(lineLength * std::cos(constrainedLineAngle), lineLength * std::sin(constrainedLineAngle));
0120 
0121     const QPointF result = m_startPos + constrainedLineVector;
0122 
0123     return result;
0124 }
0125 
0126 void KisToolMeasure::paint(QPainter& gc, const KoViewConverter &converter)
0127 {
0128     QPen old = gc.pen();
0129     QPen pen(Qt::SolidLine);
0130     gc.setPen(pen);
0131 
0132     QPainterPath elbowPath;
0133     elbowPath.moveTo(m_endPos);
0134     elbowPath.lineTo(m_startPos);
0135 
0136     QPointF offset = (m_baseLineVec * INNER_RADIUS).toPoint();
0137     QPointF diff = m_endPos-m_startPos;
0138 
0139     bool switch_elbow = QPointF::dotProduct(diff, offset) > 0.0;
0140     if(switch_elbow) {
0141         elbowPath.lineTo(m_startPos + offset);
0142     } else {
0143         elbowPath.lineTo(m_startPos - offset);
0144     }
0145 
0146     if (distance() >= INNER_RADIUS) {
0147         QRectF rectangle(m_startPos.x() - INNER_RADIUS, m_startPos.y() - INNER_RADIUS, 2*INNER_RADIUS, 2*INNER_RADIUS);
0148         
0149         double det = diff.x() * m_baseLineVec.y() - diff.y() * m_baseLineVec.x();
0150         int startAngle = -atan2(m_baseLineVec.y(), m_baseLineVec.x()) / (2*M_PI) * 360;
0151         int spanAngle = switch_elbow ? -angle() : angle();
0152 
0153         if(!switch_elbow) {
0154             startAngle+=180;
0155             startAngle%=360;
0156         }
0157 
0158         if(det > 0) {
0159             spanAngle = -spanAngle;
0160         }
0161 
0162         elbowPath.arcTo(rectangle, startAngle, spanAngle);
0163     }
0164 
0165     // The opengl renderer doesn't take the QPainter's transform, so the path is scaled here
0166     qreal sx, sy;
0167     converter.zoom(&sx, &sy);
0168     QTransform transf;
0169     transf.scale(sx / currentImage()->xRes(), sy / currentImage()->yRes());
0170     paintToolOutline(&gc, transf.map(elbowPath));
0171 
0172     gc.setPen(old);
0173 }
0174 void KisToolMeasure::showDistanceAngleOnCanvas()
0175 {
0176     KisCanvas2 *kisCanvas = qobject_cast<KisCanvas2*>(canvas());
0177     QString message = i18nc("%1=distance %2=unit of distance %3=angle in degrees", "%1 %2\n%3°",
0178                             m_optionsWidget->m_distanceLabel->text(),
0179                             m_optionsWidget->m_unit.symbol(),
0180                             QString::number(angle(),'f',1));
0181     kisCanvas->viewManager()->showFloatingMessage(message, QIcon(), 2000, KisFloatingMessage::High);
0182 }
0183 void KisToolMeasure::beginPrimaryAction(KoPointerEvent *event)
0184 {
0185     setMode(KisTool::PAINT_MODE);
0186 
0187     // Erase old temporary lines
0188     canvas()->updateCanvas(convertToPt(boundingRect()));
0189 
0190     m_startPos = convertToPixelCoord(event);
0191     m_endPos = m_startPos;
0192     m_baseLineVec = QVector2D(1.0f, 0.0f);
0193 
0194     emit sigDistanceChanged(0.0);
0195     emit sigAngleChanged(0.0);
0196 }
0197 
0198 void KisToolMeasure::continuePrimaryAction(KoPointerEvent *event)
0199 {
0200     CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE);
0201 
0202     // Erase old temporary lines
0203     canvas()->updateCanvas(convertToPt(boundingRect()));
0204 
0205     QPointF pos = convertToPixelCoord(event);
0206 
0207     if (event->modifiers() & Qt::AltModifier) {
0208         QPointF trans = pos - m_endPos;
0209         m_startPos += trans;
0210         m_endPos += trans;
0211     } else if(event->modifiers() & Qt::ShiftModifier){
0212         m_endPos = lockedAngle(pos);
0213     } else {
0214         m_endPos = pos;
0215     }
0216 
0217     if(!(event->modifiers() & Qt::ControlModifier)) {
0218         m_chooseBaseLineVec = false;
0219     } else if(!m_chooseBaseLineVec) {
0220         m_chooseBaseLineVec = true;
0221         m_baseLineVec = QVector2D(m_endPos-m_startPos).normalized();
0222     }
0223 
0224     canvas()->updateCanvas(convertToPt(boundingRect()));
0225     emit sigDistanceChanged(distance());
0226     emit sigAngleChanged(angle());
0227     showDistanceAngleOnCanvas();
0228 }
0229 
0230 void KisToolMeasure::endPrimaryAction(KoPointerEvent *event)
0231 {
0232     CHECK_MODE_SANITY_OR_RETURN(KisTool::PAINT_MODE);
0233 
0234     Q_UNUSED(event);
0235     setMode(KisTool::HOVER_MODE);
0236 }
0237 
0238 QWidget* KisToolMeasure::createOptionWidget()
0239 {
0240     if (!currentImage())
0241         return nullptr;
0242     m_optionsWidget = new KisToolMeasureOptionsWidget(nullptr, currentImage());
0243 
0244     // See https://bugs.kde.org/show_bug.cgi?id=316896
0245     QWidget *specialSpacer = new QWidget(m_optionsWidget);
0246     specialSpacer->setObjectName("SpecialSpacer");
0247     specialSpacer->setFixedSize(0, 0);
0248     m_optionsWidget->layout()->addWidget(specialSpacer);
0249 
0250     m_optionsWidget->setObjectName(toolId() + " option widget");
0251     connect(this, SIGNAL(sigDistanceChanged(double)), m_optionsWidget, SLOT(slotSetDistance(double)));
0252     connect(this, SIGNAL(sigAngleChanged(double)), m_optionsWidget, SLOT(slotSetAngle(double)));
0253     m_optionsWidget->setFixedHeight(m_optionsWidget->sizeHint().height());
0254     return m_optionsWidget;
0255 }
0256 
0257 double KisToolMeasure::angle()
0258 {
0259     double dot = QVector2D::dotProduct(QVector2D(m_endPos-m_startPos).normalized(), m_baseLineVec);
0260     return acos(qAbs(dot)) / (2*M_PI)*360;
0261 }
0262 
0263 double KisToolMeasure::distance()
0264 {
0265     return QVector2D(m_endPos - m_startPos).length();
0266 }
0267 
0268 QRectF KisToolMeasure::boundingRect()
0269 {
0270     QRectF bound;
0271     bound.setTopLeft(m_startPos);
0272     bound.setBottomRight(m_endPos);
0273     bound = bound.united(QRectF(m_startPos.x() - INNER_RADIUS, m_startPos.y() - INNER_RADIUS, 2 * INNER_RADIUS, 2 * INNER_RADIUS));
0274     return bound.normalized();
0275 }