Files
wayland-shot/src/model/Annotation.h
T
2026-05-19 14:53:37 +02:00

166 lines
4.5 KiB
C++

#pragma once
#include <QColor>
#include <QFont>
#include <QLineF>
#include <QPainter>
#include <QPainterPath>
#include <QPointF>
#include <QRectF>
#include <QString>
#include <QUuid>
#include <cmath>
#include <numbers>
namespace ws::model {
enum class Tool {
Select,
Arrow,
Line,
Rectangle,
Text,
};
enum class AnnotationKind {
Arrow,
Line,
Rectangle,
Text,
};
struct AnnotationStyle {
QColor color = QColor("#ff5a36");
int stroke_width = 4;
QFont font = QFont(QStringLiteral("Noto Sans"), 18, QFont::DemiBold);
};
struct Annotation {
QUuid id = QUuid::createUuid();
AnnotationKind kind = AnnotationKind::Line;
AnnotationStyle style;
QLineF line;
QRectF rect;
QPointF text_anchor;
QString text;
[[nodiscard]] QRectF bounds() const
{
switch (kind) {
case AnnotationKind::Arrow:
case AnnotationKind::Line:
return QRectF(line.p1(), line.p2()).normalized().adjusted(
-style.stroke_width * 1.5,
-style.stroke_width * 1.5,
style.stroke_width * 1.5,
style.stroke_width * 1.5
);
case AnnotationKind::Rectangle:
return rect.normalized().adjusted(
-style.stroke_width,
-style.stroke_width,
style.stroke_width,
style.stroke_width
);
case AnnotationKind::Text: {
QFontMetricsF metrics(style.font);
const QRectF text_rect = metrics.boundingRect(text);
return QRectF(text_anchor + QPointF(0.0, text_rect.top()), text_rect.size())
.adjusted(-6.0, -6.0, 6.0, 6.0);
}
}
return {};
}
void translate(const QPointF& delta)
{
switch (kind) {
case AnnotationKind::Arrow:
case AnnotationKind::Line:
line.translate(delta);
break;
case AnnotationKind::Rectangle:
rect.translate(delta);
break;
case AnnotationKind::Text:
text_anchor += delta;
break;
}
}
[[nodiscard]] bool hit_test(const QPointF& point, qreal tolerance = 8.0) const
{
switch (kind) {
case AnnotationKind::Arrow:
case AnnotationKind::Line: {
QPainterPath path;
path.moveTo(line.p1());
path.lineTo(line.p2());
QPainterPathStroker stroker;
stroker.setWidth(style.stroke_width + tolerance);
return stroker.createStroke(path).contains(point);
}
case AnnotationKind::Rectangle:
return bounds().contains(point);
case AnnotationKind::Text:
return bounds().contains(point);
}
return false;
}
void paint(QPainter& painter, bool selected) const
{
painter.save();
QPen pen(style.color, style.stroke_width, Qt::SolidLine, Qt::RoundCap, Qt::RoundJoin);
painter.setPen(pen);
painter.setBrush(Qt::NoBrush);
switch (kind) {
case AnnotationKind::Line:
painter.drawLine(line);
break;
case AnnotationKind::Arrow: {
painter.drawLine(line);
const double angle = std::atan2(-line.dy(), line.dx());
constexpr qreal head_length = 16.0;
const QPointF arrow_tip = line.p2();
const QPointF head_a = arrow_tip + QPointF(
std::sin(angle + std::numbers::pi_v<double> / 3.0) * head_length,
std::cos(angle + std::numbers::pi_v<double> / 3.0) * head_length
);
const QPointF head_b = arrow_tip + QPointF(
std::sin(angle + std::numbers::pi_v<double> - std::numbers::pi_v<double> / 3.0) * head_length,
std::cos(angle + std::numbers::pi_v<double> - std::numbers::pi_v<double> / 3.0) * head_length
);
painter.setBrush(style.color);
painter.drawPolygon(QPolygonF {arrow_tip, head_a, head_b});
break;
}
case AnnotationKind::Rectangle:
painter.drawRect(rect.normalized());
break;
case AnnotationKind::Text:
painter.setFont(style.font);
painter.drawText(text_anchor, text);
break;
}
if (selected) {
QPen selection_pen(QColor("#0ea5e9"), 2, Qt::DashLine);
painter.setPen(selection_pen);
painter.setBrush(Qt::NoBrush);
painter.drawRect(bounds());
}
painter.restore();
}
};
} // namespace ws::model