Warning, file /graphics/glaxnimate/src/gui/tools/draw_tool.cpp was not indexed or was modified since last indexation (in which case cross-reference links may be missing, inaccurate or erroneous).

0001 /*
0002  * SPDX-FileCopyrightText: 2019-2023 Mattia Basaglia <dev@dragon.best>
0003  *
0004  * SPDX-License-Identifier: GPL-3.0-or-later
0005  */
0006 
0007 #include "draw_tool.hpp"
0008 
0009 #include <QShortcut>
0010 
0011 #include "model/shapes/path.hpp"
0012 #include "app/settings/keyboard_shortcuts.hpp"
0013 #include "glaxnimate_app.hpp"
0014 #include "math/geom.hpp"
0015 #include "math/vector.hpp"
0016 
0017 class glaxnimate::gui::tools::DrawTool::Private
0018 {
0019 public:
0020     class ToolUndoStack
0021     {
0022     public:
0023 
0024         void push(QUndoCommand* command, SelectionManager* window)
0025         {
0026             if ( !undo_stack )
0027             {
0028                 undo_stack = std::make_unique<QUndoStack>();
0029                 window->undo_group().addStack(undo_stack.get());
0030                 window->undo_group().setActiveStack(undo_stack.get());
0031             }
0032 
0033             undo_stack->push(command);
0034         }
0035 
0036         void undo(SelectionManager* window)
0037         {
0038             if ( undo_stack )
0039             {
0040                 if ( undo_stack->index() == 0 )
0041                     clear(window);
0042                 else
0043                     undo_stack->undo();
0044             }
0045         }
0046 
0047         void redo(SelectionManager*)
0048         {
0049             if ( undo_stack )
0050                 undo_stack->redo();
0051         }
0052 
0053         void clear(SelectionManager* window)
0054         {
0055             if ( undo_stack )
0056             {
0057                 window->undo_group().removeStack(undo_stack.get());
0058                 window->undo_group().setActiveStack(&window->document()->undo_stack());
0059                 undo_stack.reset();
0060             }
0061         }
0062 
0063     private:
0064         std::unique_ptr<QUndoStack> undo_stack;
0065     };
0066 
0067     class BezierUndoCommand : public QUndoCommand
0068     {
0069     public:
0070         BezierUndoCommand(const QString& name, math::bezier::Bezier* target, math::bezier::Bezier&& after )
0071             : QUndoCommand(name),
0072               target(target),
0073               before(*target),
0074               after(std::move(after))
0075         {}
0076 
0077         void undo() override
0078         {
0079             *target = before;
0080         }
0081 
0082         void redo() override
0083         {
0084             *target = after;
0085         }
0086 
0087         math::bezier::Bezier* target;
0088         math::bezier::Bezier before;
0089         math::bezier::Bezier after;
0090     };
0091 
0092     void create(const Event& event, DrawTool* tool);
0093     void adjust_point_type(Qt::KeyboardModifiers mod);
0094     void clear(bool hard, SelectionManager* window);
0095     void add_extension_points(model::Path* owner);
0096     void remove_extension_points(model::AnimatedProperty<math::bezier::Bezier>* property);
0097     void recursive_add_selection(graphics::DocumentScene * scene, model::VisualNode* node);
0098     void recursive_remove_selection(graphics::DocumentScene * scene, model::VisualNode* node);
0099     bool within_join_distance(const glaxnimate::gui::tools::MouseEvent& event, const QPointF& scene_pos);
0100     void prepare_draw(const glaxnimate::gui::tools::MouseEvent& event);
0101 
0102     void push_command(const QString& name, SelectionManager* window, math::bezier::Bezier&& after)
0103     {
0104         undo_stack.push(new BezierUndoCommand(name, &bezier, std::move(after)), window);
0105     }
0106 
0107     struct ExtendPathData
0108     {
0109         model::AnimatedProperty<math::bezier::Bezier>* property = nullptr;
0110         model::Path* owner = nullptr;
0111         bool at_end = true;
0112 
0113         QPointF pos() const
0114         {
0115             QPointF p = at_end ? property->get().back().pos : property->get()[0].pos;
0116             if ( owner )
0117                 p = owner->transform_matrix(owner->time()).map(p);
0118             return p;
0119         }
0120     };
0121 
0122     QPointF best_point(const glaxnimate::gui::tools::MouseEvent& event)
0123     {
0124         if ( bezier.size() < 2 || !(event.modifiers() & Qt::ControlModifier) )
0125             return event.scene_pos;
0126 
0127         const QPointF& ref = bezier.points()[bezier.size() - 2].pos;
0128         QPointF best = math::line_closest_point(ref, ref + QPointF(10, 0), event.scene_pos);
0129         auto best_dist = math::distance(best, event.scene_pos);
0130         static const std::array<QPointF, 3> offsets{QPointF(0, 10), QPointF(10, 10), QPointF(10, -10)};
0131         for ( const auto& off : offsets )
0132         {
0133             QPointF p = math::line_closest_point(ref, ref + off, event.scene_pos);
0134             auto dist = math::distance(p, event.scene_pos);
0135             if ( dist < best_dist )
0136             {
0137                 best = p;
0138                 best_dist = dist;
0139             }
0140         }
0141 
0142         return best;
0143     }
0144 
0145     math::bezier::Bezier bezier;
0146     bool dragging = false;
0147     math::bezier::PointType point_type = math::bezier::Symmetrical;
0148     qreal join_radius = 5;
0149     bool joining = false;
0150 
0151     ExtendPathData extend;
0152     std::vector<ExtendPathData> extension_points;
0153 
0154     ToolUndoStack undo_stack;
0155 };
0156 
0157 glaxnimate::gui::tools::Autoreg<glaxnimate::gui::tools::DrawTool> glaxnimate::gui::tools::DrawTool::autoreg{max_priority};
0158 
0159 void glaxnimate::gui::tools::DrawTool::Private::create(const glaxnimate::gui::tools::Event& event, DrawTool* tool)
0160 {
0161     if ( !bezier.empty() )
0162     {
0163 #ifdef Q_OS_ANDROID
0164         if ( bezier.closed() )
0165 #endif
0166         bezier.points().pop_back();
0167 
0168 
0169         // use symmetrical length while drawing but for editing having smooth nodes is nicer
0170         // the user can always change them back
0171         for ( auto & point : bezier )
0172         {
0173             if ( point.type == math::bezier::PointType::Symmetrical )
0174             {
0175                 if ( math::fuzzy_compare(point.pos, point.tan_in) && math::fuzzy_compare(point.pos, point.tan_out) )
0176                     point.type = math::bezier::PointType::Corner;
0177                 else
0178                     point.type = math::bezier::PointType::Smooth;
0179             }
0180         }
0181 
0182         if ( extend.property )
0183         {
0184             command::UndoMacroGuard guard(i18n("Extend Path"), event.window->document());
0185 
0186             if ( !extend.at_end )
0187                 bezier.reverse();
0188 
0189             extend.property->extend(bezier, extend.at_end);
0190 
0191             if ( bezier.closed() )
0192             {
0193                 if ( extend.owner )
0194                     extend.owner->closed.set_undoable(true);
0195                 remove_extension_points(extend.property);
0196             }
0197         }
0198         else
0199         {
0200             auto shape = std::make_unique<model::Path>(event.window->document());
0201             shape->shape.set(bezier);
0202             if ( bezier.closed() )
0203                 shape->closed.set(true);
0204 
0205             add_extension_points(shape.get());
0206 
0207             tool->create_shape(i18n("Draw Shape"), event, std::move(shape));
0208         }
0209 
0210     }
0211     clear(false, event.window);
0212     event.repaint();
0213 }
0214 
0215 void glaxnimate::gui::tools::DrawTool::Private::add_extension_points(model::Path* owner)
0216 {
0217     if ( !owner->closed.get() )
0218     {
0219         extension_points.push_back(ExtendPathData{
0220             &owner->shape,
0221             owner,
0222             true
0223         });
0224         extension_points.push_back(ExtendPathData{
0225             &owner->shape,
0226             owner,
0227             false
0228         });
0229     }
0230 }
0231 
0232 void glaxnimate::gui::tools::DrawTool::Private::remove_extension_points(model::AnimatedProperty<math::bezier::Bezier>* property)
0233 {
0234     extension_points.erase(
0235         std::remove_if(extension_points.begin(), extension_points.end(), [property](const ExtendPathData& p){
0236             return p.property == property;
0237         }),
0238         extension_points.end()
0239     );
0240 }
0241 
0242 void glaxnimate::gui::tools::DrawTool::Private::adjust_point_type(Qt::KeyboardModifiers mod)
0243 {
0244     if ( mod & Qt::ShiftModifier )
0245         point_type = math::bezier::Corner;
0246     else if ( mod & Qt::ControlModifier )
0247         point_type = math::bezier::Smooth;
0248     else
0249         point_type = math::bezier::Symmetrical;
0250 
0251     if ( !bezier.empty() )
0252     {
0253         bezier.points().back().type = point_type;
0254 //         bezier.points().back().drag_tan_out(bezier.points().back().tan_out);
0255     }
0256 }
0257 
0258 void glaxnimate::gui::tools::DrawTool::key_press(const glaxnimate::gui::tools::KeyEvent& event)
0259 {
0260     if ( d->bezier.empty() )
0261         return;
0262 
0263     if ( event.key() == Qt::Key_Delete || event.key() == Qt::Key_Backspace || event.key() == Qt::Key_Back )
0264     {
0265         remove_last(event.window);
0266         event.accept();
0267         event.repaint();
0268     }
0269     else if ( event.key() == Qt::Key_Shift || event.key() == Qt::Key_Control )
0270     {
0271         d->adjust_point_type(event.modifiers());
0272         event.accept();
0273         event.repaint();
0274     }
0275     else if ( event.key() == Qt::Key_Enter || event.key() == Qt::Key_Return )
0276     {
0277         d->create(event, this);
0278         event.accept();
0279     }
0280     else if ( event.key() == Qt::Key_Escape )
0281     {
0282         d->clear(false, event.window);
0283         event.repaint();
0284     }
0285 }
0286 
0287 void glaxnimate::gui::tools::DrawTool::Private::clear(bool hard, SelectionManager* window)
0288 {
0289     undo_stack.clear(window);
0290     dragging = false;
0291     bezier.clear();
0292     point_type = math::bezier::Symmetrical;
0293     joining = false;
0294     extend = {};
0295     if ( hard )
0296         extension_points.clear();
0297 }
0298 
0299 void glaxnimate::gui::tools::DrawTool::remove_last(SelectionManager* window)
0300 {
0301     if ( d->bezier.empty() )
0302         return;
0303 
0304 #ifndef Q_OS_ANDROID
0305     int back = 2;
0306 #else
0307     int back = 1;
0308 #endif
0309 
0310     if ( d->bezier.size() <= back )
0311     {
0312         d->clear(false, window);
0313     }
0314     else
0315     {
0316         auto bezier = d->bezier;
0317         bezier.points().erase(bezier.points().end() - back);
0318         d->push_command(i18n("Delete curve point"), window, std::move(bezier));
0319     }
0320 
0321 }
0322 
0323 
0324 bool glaxnimate::gui::tools::DrawTool::Private::within_join_distance(const glaxnimate::gui::tools::MouseEvent& event, const QPointF& scene_pos)
0325 {
0326     return math::length(event.pos() - event.view->mapFromScene(scene_pos)) <= join_radius;
0327 }
0328 
0329 void glaxnimate::gui::tools::DrawTool::Private::prepare_draw(const glaxnimate::gui::tools::MouseEvent& event)
0330 {
0331     for ( const auto& point : extension_points )
0332     {
0333         if ( within_join_distance(event, point.pos()) )
0334         {
0335             extend = point;
0336             bezier = point.property->get();
0337             if ( !point.at_end )
0338                 bezier.reverse();
0339             bezier.points().back().type = math::bezier::Corner;
0340             bezier.points()[0].type = math::bezier::Corner;
0341             return;
0342         }
0343     }
0344 
0345     bezier.push_back(math::bezier::Point(event.scene_pos, event.scene_pos, event.scene_pos, math::bezier::Corner));
0346 }
0347 
0348 glaxnimate::gui::tools::DrawTool::DrawTool()
0349     : d(std::make_unique<Private>())
0350 {
0351 }
0352 
0353 glaxnimate::gui::tools::DrawTool::~DrawTool()
0354 {
0355 }
0356 
0357 void glaxnimate::gui::tools::DrawTool::mouse_press(const glaxnimate::gui::tools::MouseEvent& event)
0358 {
0359     if ( event.button() != Qt::LeftButton )
0360         return;
0361 
0362     if ( d->bezier.empty() )
0363     {
0364         d->prepare_draw(event);
0365     }
0366 #ifdef Q_OS_ANDROID
0367     else
0368     {
0369         QPointF pos = d->best_point(event);
0370         d->bezier.points().push_back(math::bezier::Point(pos, pos, pos, d->point_type));
0371         event.repaint();
0372     }
0373 #endif
0374 
0375     d->dragging = true;
0376 
0377 }
0378 
0379 void glaxnimate::gui::tools::DrawTool::mouse_move(const glaxnimate::gui::tools::MouseEvent& event)
0380 {
0381     if ( d->bezier.empty() )
0382         return;
0383 
0384     if ( d->dragging )
0385     {
0386         d->bezier.points().back().drag_tan_out(event.scene_pos);
0387     }
0388     else if ( d->bezier.size() > 2 && d->within_join_distance(event, d->bezier.points().front().pos) )
0389     {
0390         d->joining = true;
0391 #ifndef Q_OS_ANDROID
0392         d->bezier.points().back().translate_to(d->bezier.points().front().pos);
0393 #endif
0394     }
0395     else
0396     {
0397         d->joining = false;
0398 #ifndef Q_OS_ANDROID
0399         d->bezier.points().back().translate_to(d->best_point(event));
0400 #endif
0401     }
0402 }
0403 
0404 void glaxnimate::gui::tools::DrawTool::mouse_release(const glaxnimate::gui::tools::MouseEvent& event)
0405 {
0406     if ( !d->dragging )
0407         return;
0408 
0409     if ( event.button() == Qt::LeftButton )
0410     {
0411         d->dragging = false;
0412 
0413         if ( d->joining )
0414         {
0415             d->bezier.points().front().tan_in = d->bezier.points().back().tan_in;
0416             d->bezier.set_closed(true);
0417             d->create(event, this);
0418         }
0419 #ifndef Q_OS_ANDROID
0420         else
0421         {
0422             auto bezier = d->bezier;
0423             QPointF pos = d->best_point(event);
0424             bezier.points().push_back(math::bezier::Point(pos, pos, pos, d->point_type));
0425             d->push_command(i18n("Add curve point"), event.window, std::move(bezier));
0426             event.repaint();
0427         }
0428 #endif
0429     }
0430 }
0431 
0432 void glaxnimate::gui::tools::DrawTool::mouse_double_click(const glaxnimate::gui::tools::MouseEvent& event)
0433 {
0434     d->create(event, this);
0435     event.accept();
0436 }
0437 
0438 void glaxnimate::gui::tools::DrawTool::paint(const glaxnimate::gui::tools::PaintEvent& event)
0439 {
0440     QPen pen(event.palette.highlight(), 1);
0441     pen.setCosmetic(true);
0442     qreal view_radius = d->join_radius;
0443 
0444     if ( !d->bezier.empty() )
0445     {
0446         QPainterPath path;
0447         d->bezier.add_to_painter_path(path);
0448         draw_shape(event, event.view->mapFromScene(path));
0449 
0450         event.painter->setPen(pen);
0451         event.painter->setBrush(Qt::NoBrush);
0452 
0453 #ifdef Q_OS_ANDROID
0454         if ( d->bezier.size() >= 1 )
0455 #else
0456         if ( d->bezier.size() > 1 )
0457 #endif
0458         {
0459             QPointF center = event.view->mapFromScene(d->bezier.points().front().pos);
0460 
0461             if ( d->joining )
0462             {
0463                 event.painter->setBrush(event.palette.highlightedText());
0464                 event.painter->drawEllipse(center, view_radius*1.5, view_radius*1.5);
0465                 event.painter->setBrush(Qt::NoBrush);
0466             }
0467             else
0468             {
0469                 event.painter->drawEllipse(center, view_radius, view_radius);
0470             }
0471         }
0472 
0473 #ifndef Q_OS_ANDROID
0474         if ( d->dragging )
0475 #endif
0476         {
0477             pen.setWidth(2);
0478             event.painter->setPen(pen);
0479 
0480             QPolygonF poly;
0481             if ( d->bezier.size() > 1 )
0482                 poly.push_back(event.view->mapFromScene(d->bezier.points().back().tan_in));
0483             QPointF center = event.view->mapFromScene(d->bezier.points().back().pos);
0484             poly.push_back(center);
0485             poly.push_back(event.view->mapFromScene(d->bezier.points().back().tan_out));
0486             event.painter->drawPolyline(poly);
0487             event.painter->drawEllipse(center, view_radius, view_radius);
0488         }
0489     }
0490     else
0491     {
0492         event.painter->setPen(pen);
0493         event.painter->setBrush(event.palette.brush(QPalette::Active, QPalette::HighlightedText));
0494         bool got_one = false;
0495         for ( const auto& point : d->extension_points )
0496         {
0497             QPointF center = event.view->mapFromScene(point.pos());
0498             qreal mult = 1;
0499             if ( !got_one && math::length(event.view->mapFromGlobal(QCursor::pos()) - center) <= d->join_radius )
0500             {
0501                 mult = 1.5;
0502                 got_one = true;
0503             }
0504             event.painter->drawEllipse(center, view_radius * mult, view_radius * mult);
0505         }
0506     }
0507 }
0508 
0509 void glaxnimate::gui::tools::DrawTool::enable_event(const Event& ev)
0510 {
0511     d->clear(false, ev.window);
0512     ev.repaint();
0513 }
0514 
0515 void glaxnimate::gui::tools::DrawTool::disable_event(const Event& event)
0516 {
0517     d->create(event, this);
0518     d->clear(true, event.window);
0519 }
0520 
0521 void glaxnimate::gui::tools::DrawTool::on_selected(graphics::DocumentScene * scene, model::VisualNode * node)
0522 {
0523     d->recursive_add_selection(scene, node);
0524 }
0525 
0526 void glaxnimate::gui::tools::DrawTool::on_deselected(graphics::DocumentScene * scene, model::VisualNode* node)
0527 {
0528     d->recursive_remove_selection(scene, node);
0529 }
0530 
0531 void glaxnimate::gui::tools::DrawTool::Private::recursive_add_selection(graphics::DocumentScene * scene, model::VisualNode* node)
0532 {
0533     auto meta = node->metaObject();
0534     if ( meta->inherits(&model::Path::staticMetaObject) )
0535     {
0536         add_extension_points(static_cast<model::Path*>(node));
0537     }
0538     else if ( meta->inherits(&model::Group::staticMetaObject) )
0539     {
0540         for ( const auto& sub : static_cast<model::Group*>(node)->shapes )
0541             recursive_add_selection(scene, sub.get());
0542     }
0543 }
0544 
0545 void glaxnimate::gui::tools::DrawTool::Private::recursive_remove_selection(graphics::DocumentScene * scene, model::VisualNode* node)
0546 {
0547     auto meta = node->metaObject();
0548     if ( meta->inherits(&model::Path::staticMetaObject) )
0549     {
0550         remove_extension_points(&static_cast<model::Path*>(node)->shape);
0551     }
0552     else if ( meta->inherits(&model::Group::staticMetaObject) )
0553     {
0554         for ( const auto& sub : static_cast<model::Group*>(node)->shapes )
0555             recursive_remove_selection(scene, sub.get());
0556     }
0557 }
0558 
0559 void glaxnimate::gui::tools::DrawTool::initialize(const Event& event)
0560 {
0561     d->join_radius = 5 * GlaxnimateApp::handle_size_multiplier();
0562     Q_UNUSED(event);
0563 }