#pragma once #include #include #include #include #include #include #include #include #include #include #include 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 / 3.0) * head_length, std::cos(angle + std::numbers::pi_v / 3.0) * head_length ); const QPointF head_b = arrow_tip + QPointF( std::sin(angle + std::numbers::pi_v - std::numbers::pi_v / 3.0) * head_length, std::cos(angle + std::numbers::pi_v - std::numbers::pi_v / 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