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 }