From 1726d1805b8797e983c333b8c9445fe89d9f0928 Mon Sep 17 00:00:00 2001 From: Libor Pechacek Date: Wed, 29 Dec 2021 22:57:32 +0100 Subject: [PATCH 1/2] VirtualPath: Export tangent from findClosestPointTo() The tangent vector is essential for the move parallel tool where we want to calculate the distance of the cursor from the tangent line touching the path in question. Please note that we are generating a "pseudo tangent" for corners and line ends to enable sensible behavior during mouse drag events. --- src/core/objects/object.cpp | 2 +- src/core/path_coord.h | 8 +++++++- src/core/virtual_path.cpp | 16 ++++++++++++---- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/core/objects/object.cpp b/src/core/objects/object.cpp index 7e0804c7f0..be6f6d3114 100644 --- a/src/core/objects/object.cpp +++ b/src/core/objects/object.cpp @@ -1160,7 +1160,7 @@ ClosestPathCoord PathObject::findClosestPointTo( using distance_type = decltype(ClosestPathCoord::distance_squared); return std::accumulate(begin(path_parts), end(path_parts), - ClosestPathCoord { {}, std::numeric_limits::max() }, + ClosestPathCoord { {}, {}, std::numeric_limits::max() }, op); } diff --git a/src/core/path_coord.h b/src/core/path_coord.h index 8d093ff942..bea204ed80 100644 --- a/src/core/path_coord.h +++ b/src/core/path_coord.h @@ -158,11 +158,17 @@ class PathCoord /** - * The structure returned when looking for closest coordinates on a path. + * The structure returned when looking for the closest coordinates on a path. + * The tangent element is defined even on corners and line ends. Following the + * intuitive view, the tangent line touches the corner, dividing the plane into + * the part with the corner and the part without it. At the line ends, the + * "tangent" is the direction of the neighboring segment. The "pseudo tangent" + * is generated to define the direction for mouse drag actions. */ struct ClosestPathCoord { PathCoord path_coord; + MapCoordF tangent; double distance_squared; }; diff --git a/src/core/virtual_path.cpp b/src/core/virtual_path.cpp index cc9933d563..5b1f289d11 100644 --- a/src/core/virtual_path.cpp +++ b/src/core/virtual_path.cpp @@ -347,9 +347,11 @@ ClosestPathCoord VirtualPath::findClosestPointTo( size_type start_index, size_type end_index ) const { - Q_ASSERT(!path_coords.empty()); + Q_ASSERT(path_coords.size() > 1); - auto result = ClosestPathCoord { path_coords.front(), distance_bound_squared }; + auto result = ClosestPathCoord { path_coords.front(), + path_coords[1].pos - path_coords[0].pos, + distance_bound_squared }; // Find upper bound for distance. for (const auto& path_coord : path_coords) @@ -367,6 +369,7 @@ ClosestPathCoord VirtualPath::findClosestPointTo( result.path_coord = path_coord; } } + result.tangent = calculateTangent(result.path_coord.index); // Check between this coord and the next one. auto last = end(path_coords)-1; @@ -391,6 +394,7 @@ ClosestPathCoord VirtualPath::findClosestPointTo( if (to_coord.lengthSquared() < result.distance_squared) { result.distance_squared = to_coord.lengthSquared(); + result.tangent = tangent; result.path_coord = *pc; } continue; @@ -402,6 +406,7 @@ ClosestPathCoord VirtualPath::findClosestPointTo( if (coord.distanceSquaredTo(next_pos) < result.distance_squared) { result.distance_squared = coord.distanceSquaredTo(next_pos); + result.tangent = tangent; result.path_coord = *next_pc; } continue; @@ -425,13 +430,16 @@ ClosestPathCoord VirtualPath::findClosestPointTo( if (coords.flags[result.path_coord.index].isCurveStart()) { MapCoordF unused; + MapCoordF curve2_control_point1; PathCoord::splitBezierCurve(MapCoordF(coords.flags[result.path_coord.index]), MapCoordF(coords.flags[result.path_coord.index+1]), MapCoordF(coords.flags[result.path_coord.index+2]), MapCoordF(coords.flags[result.path_coord.index+3]), - result.path_coord.param, unused, unused, result.path_coord.pos, unused, unused); + result.path_coord.param, unused, unused, result.path_coord.pos, curve2_control_point1, unused); + result.tangent = curve2_control_point1 - result.path_coord.pos; } else { - result.path_coord.pos = pos + (next_pos - pos) * double(factor); + result.tangent = next_pos - pos; + result.path_coord.pos = pos + result.tangent * double(factor); } } } From be4003f3e976b58ff5b7a399d887cfb5555413ea Mon Sep 17 00:00:00 2001 From: Libor Pechacek Date: Wed, 29 Dec 2021 22:57:37 +0100 Subject: [PATCH 2/2] MoveParallelTool: Add the tool for parallel object moves The tool moves lines and area boundaries to create a larger or a smaller shape parallel to the original object. The tool reuses LineSymbol::shiftCoordinates() which currently does not handle shape degeneration. I.e., curve turns do not shrink into points but rather form a "negative" shape that is a mathematically exact image of the original curve but usually is not what the user expects. Closes GH-650 --- images/cursor-move-parallel.png | Bin 0 -> 1495 bytes images/svg/cursor-move-parallel.svg | 181 +++++++++++++++++++++ images/svg/tool-move-parallel.svg | 120 ++++++++++++++ images/tool-move-parallel.png | Bin 0 -> 974 bytes resources.qrc | 2 + src/CMakeLists.txt | 1 + src/gui/map/map_editor.cpp | 12 ++ src/gui/map/map_editor.h | 3 + src/tools/move_parallel_tool.cpp | 235 ++++++++++++++++++++++++++++ src/tools/move_parallel_tool.h | 116 ++++++++++++++ 10 files changed, 670 insertions(+) create mode 100644 images/cursor-move-parallel.png create mode 100644 images/svg/cursor-move-parallel.svg create mode 100644 images/svg/tool-move-parallel.svg create mode 100644 images/tool-move-parallel.png create mode 100644 src/tools/move_parallel_tool.cpp create mode 100644 src/tools/move_parallel_tool.h diff --git a/images/cursor-move-parallel.png b/images/cursor-move-parallel.png new file mode 100644 index 0000000000000000000000000000000000000000..cb18f43ed77ffecc30998dccb07e0f126b88d3cb GIT binary patch literal 1495 zcmV;|1t|K7P)MmPZDDY5WpcqA3=aSR1mH`!Wd&5 z{)7N=5fp~o%$n#N{>efi(u@QlHn?^~FFGL^m5WW1aiM{kcwsVel9&=hU34NA(2%eQ z7lMNk7lfJBCahTLDwWpr+r>&^Tq&qb&gD7J`~05od*6KD5kiO{v5Zf!BuUEB($cnN zOCU*7ltQ6Euh-WuTYycQHj$N;g%}YuDNh2E$vgJ%A8G zq@|^a`uh6S-QC>-I-Tx~*B(F!F}-Kcp0UEh!l;RfiQeT1Fe8Ncw5F!!QCL{mdu3&1 zKP)8(SPgsvdUzQUTa}!W}a+Gv2(sJXxpH>0c2V22=o-JRXm$tE-En zM~|{|=T1^nQ&FqcgZ~NOEBwl8wUUvM;nC~$c`pG@1de;XUVB?x8^y)NL`FuElaqtd zXvAzbGcq!Q)oN|=8^C9BuD`#Z?Ck8njYebiBEhME$!@oM8yXr&NJyZdpnx-H&S10I z0EU3$z&@WgB2a);KwnEs3){AB`_pJN1_uB~0w<=Xre+TxK1@VJ1Z8Dq3=9ka{04jp zyzRF*K!6oMIe@`nps1+G;s>k%bRLh#-qh5@`t|G4YPHyGHW|1A>;MAaHGYOL2RH%P z+-^4)FJ5Ht-n~zgl9C)nMMa6vz~g~#i^W2EdO9kViowA_fa^e3KqphQ(qw+I_tA!9J#>dAiOG`_|nl)?0ty{OeF)=aqLWqW- zprF|R&;%jGej&tuhr^LEG&Cgc+_~cxk|afigoMbsxw&`MYIThe!tXH*D1jU2&!1=A zx^*-+HzUjPIPm@gtIY+jOioV9O-)VY<>e6`9?sUSTRC{}AVLVCh>MGhT)^ZV;P?9a zdJ+>8=X(7k5FVf=3;1b#e4NV4N+Kd6&}cMVzI+*v$KwKi0Zs?v&KV`ivOJ;HYT3Mb zGY=j-@B&{1(60oJ+3oh(s;VlKN+q?mwMyA z0P~)n3k5jV+1bg)jT^at|Go@p0t7wR)6>I-4I4Ol@+7`%R(dJ0&-4SU)%r9xHWrJ; z0`PSJ_}QyhuM!;{O=o8(zT@}JSAo9)4Da5(o4UF>01Ke-qYDPkc64+Q6%|EKPY*z| zKkbVKs56;Nq@<+acDo%w>^!<}ySloFj*h0cw-?~_f|t%B;L0aYo=hhsB+N~)b{<{v z*w~mH6B9GnqVHcB?lb=8)TvYK+O-Q{(BA|D*-od^m6@4Ib8|C*`K2e_k9kaadHK(M zeSHq#Ge6%~0>74&lu%Jo0ssDJtX&fLbD&UvTM8U&YilDtJsr2({Rl{2a(IAHsSbz3 x9UmVba6bOe0Nbmps~v|99Rj$qWPKMx`~xg=@317TLNx#Y002ovPDHLkV1n(_rPBZa literal 0 HcmV?d00001 diff --git a/images/svg/cursor-move-parallel.svg b/images/svg/cursor-move-parallel.svg new file mode 100644 index 0000000000..3367f33601 --- /dev/null +++ b/images/svg/cursor-move-parallel.svg @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + image/svg+xml + + + 2021-03-26 + + + Libor Pecháček + + + https://github.com/OpenOrienteering/mapper + + + Move parallel + + + + + + + + + + + + + + + + + + diff --git a/images/svg/tool-move-parallel.svg b/images/svg/tool-move-parallel.svg new file mode 100644 index 0000000000..7bd9c1c9f4 --- /dev/null +++ b/images/svg/tool-move-parallel.svg @@ -0,0 +1,120 @@ + + + + + + + + + + + + + + + image/svg+xml + + + 2021-03-26 + + + Libor Pecháček + + + https://github.com/OpenOrienteering/mapper + + + Move parallel + + + + + + + + + + diff --git a/images/tool-move-parallel.png b/images/tool-move-parallel.png new file mode 100644 index 0000000000000000000000000000000000000000..e88742313f8ce4e583459ec32729345375de56d2 GIT binary patch literal 974 zcmV;<12O!GP)MmPZDDY5WpcqA3=aSR0@q1IK~z|U?blCe+hrKQ@y~+bI!gmX*O3|s zwJA(+Q7~F?f&;f=5rjf7qC*{uFfZQfMNqnn2>z*99D$;g5dWbf8xy1_Pd(T{{d1@w z7;^5%Ko-}|F;WbhIh5tS&Dcpl+a1QsiD?f!lv4-(D zVG@qwW&9`F(Z5|Z2yjVDNb||T`gj76VCbwKjWd@hzXq<;fn2&(U zZVH$Yw(`*yaVKK=Z@3d7EIz+fpbLQsd*}#cVeFS~~?S;FTsZZ=#47@P4a=qoboY3=a=KoXKRKNLs*Ap`*9s zq;SW(Ae`rkn3$M2n9t|Wc6D{#ouq(99KaN=3jLAzBAH61OixcA&gF6yEgs#?3OFr{ z)pHv6M?<>1yUonZD$PbUHoRD5lkw>SDXnwY4>~ zv$N}!O6Bt6;^O66tyWrHT`ljlfJD^mb#rrb^>Vp07*qoM6N<$f@C(m$^ZZW literal 0 HcmV?d00001 diff --git a/resources.qrc b/resources.qrc index a3fd91f570..4b55c51a5f 100644 --- a/resources.qrc +++ b/resources.qrc @@ -26,6 +26,7 @@ images/cursor-georeferencing-move.png images/cursor-hollow.png images/cursor-invisible.png + images/cursor-move-parallel.png images/cursor-rotate.png images/cursor-scale.png images/cut.png @@ -102,6 +103,7 @@ images/tool-fill-border.png images/tool-gps-display.png images/tool-measure.png + images/tool-move-parallel.png images/tool-rotate.png images/tool-rotate-pattern.png images/tool-scale.png diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a4b7d1454d..59cc2f44e3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -240,6 +240,7 @@ set(Mapper_Common_SRCS tools/object_selector.cpp tools/pan_tool.cpp tools/point_handles.cpp + tools/move_parallel_tool.cpp tools/rotate_tool.cpp tools/rotate_pattern_tool.cpp tools/scale_tool.cpp diff --git a/src/gui/map/map_editor.cpp b/src/gui/map/map_editor.cpp index 42af0eb846..18ad4f909a 100644 --- a/src/gui/map/map_editor.cpp +++ b/src/gui/map/map_editor.cpp @@ -158,6 +158,7 @@ #include "tools/edit_line_tool.h" #include "tools/fill_tool.h" #include "tools/pan_tool.h" +#include "tools/move_parallel_tool.h" #include "tools/rotate_pattern_tool.h" #include "tools/rotate_tool.h" #include "tools/scale_tool.h" @@ -936,6 +937,7 @@ void MapEditorController::assignKeyboardShortcuts() findAction("scaleobjects")->setShortcut(QKeySequence(tr("Z"))); findAction("cutobject")->setShortcut(QKeySequence(tr("K"))); findAction("cuthole")->setShortcut(QKeySequence(tr("H"))); + findAction("moveparallel")->setShortcut(QKeySequence(tr("Ctrl+Shift+M"))); findAction("measure")->setShortcut(QKeySequence(tr("M"))); findAction("booleanunion")->setShortcut(QKeySequence(tr("U"))); findAction("converttocurves")->setShortcut(QKeySequence(tr("N"))); @@ -1055,6 +1057,7 @@ void MapEditorController::createActions() cut_hole_menu->addAction(cut_hole_circle_act); cut_hole_menu->addAction(cut_hole_rectangle_act); + move_parallel_act = newToolAction("moveparallel", tr("Move parallel"), this, SLOT(moveParallelClicked()), "tool-move-parallel.png", QString{}, "toolbars.html#tool_move_parallel"); rotate_act = newToolAction("rotateobjects", tr("Rotate objects"), this, SLOT(rotateClicked()), "tool-rotate.png", QString{}, "toolbars.html#rotate"); rotate_pattern_act = newToolAction("rotatepatterns", tr("Rotate pattern"), this, SLOT(rotatePatternClicked()), "tool-rotate-pattern.png", QString{}, "toolbars.html#tool_rotate_pattern"); scale_act = newToolAction("scaleobjects", tr("Scale objects"), this, SLOT(scaleClicked()), "tool-scale.png", QString{}, "toolbars.html#scale"); @@ -1226,6 +1229,7 @@ void MapEditorController::createMenuAndToolbars() tools_menu->addAction(boolean_merge_holes_act); tools_menu->addAction(cut_tool_act); tools_menu->addMenu(cut_hole_menu); + tools_menu->addAction(move_parallel_act); tools_menu->addAction(rotate_act); tools_menu->addAction(rotate_pattern_act); tools_menu->addAction(scale_act); @@ -1363,6 +1367,7 @@ void MapEditorController::createMenuAndToolbars() cut_hole_button->setMenu(cut_hole_menu); toolbar_editing->addWidget(cut_hole_button); + toolbar_editing->addAction(move_parallel_act); toolbar_editing->addAction(rotate_act); toolbar_editing->addAction(rotate_pattern_act); toolbar_editing->addAction(scale_act); @@ -2616,6 +2621,8 @@ void MapEditorController::updateObjectDependentActions() cut_tool_act->setStatusTip(tr("Cut the selected objects into smaller parts.") + (cut_tool_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select at least one line or area object to activate this tool.")))); convert_to_curves_act->setEnabled(have_area || have_line); convert_to_curves_act->setStatusTip(tr("Turn paths made of straight segments into smooth bezier splines.") + (convert_to_curves_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select a path object to activate this tool.")))); + move_parallel_act->setEnabled(have_area || have_line); + move_parallel_act->setStatusTip(tr("Move lines and area borders in and out.") + (move_parallel_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select at least one line or area object to activate this tool.")))); simplify_path_act->setEnabled(have_area || have_line); simplify_path_act->setStatusTip(tr("Reduce the number of points in path objects while trying to retain their shape.") + (simplify_path_act->isEnabled() ? QString{} : QString(QLatin1Char(' ') + tr("Select a path object to activate this tool.")))); @@ -2719,6 +2726,11 @@ void MapEditorController::editLineToolClicked() setTool(new EditLineTool(this, edit_line_tool_act)); } +void MapEditorController::moveParallelClicked() +{ + setTool(new MoveParallelTool(this, move_parallel_act)); +} + void MapEditorController::drawPointClicked() { setTool(new DrawPointTool(this, draw_point_act)); diff --git a/src/gui/map/map_editor.h b/src/gui/map/map_editor.h index 9c72a2b43a..925fd49d0a 100644 --- a/src/gui/map/map_editor.h +++ b/src/gui/map/map_editor.h @@ -395,6 +395,8 @@ public slots: void editToolClicked(); /** Activates the line edit tool. */ void editLineToolClicked(); + /** Move parallel tool activation */ + void moveParallelClicked(); /** Activates the draw point tool. */ void drawPointClicked(); /** Activates the draw path tool. */ @@ -767,6 +769,7 @@ protected slots: QAction* edit_tool_act = {}; QAction* edit_line_tool_act = {}; + QAction* move_parallel_act = {}; QAction* draw_point_act = {}; QAction* draw_path_act = {}; QAction* draw_circle_act = {}; diff --git a/src/tools/move_parallel_tool.cpp b/src/tools/move_parallel_tool.cpp new file mode 100644 index 0000000000..aba4e81ccd --- /dev/null +++ b/src/tools/move_parallel_tool.cpp @@ -0,0 +1,235 @@ +/* + * Copyright 2021 Libor Pecháček + * + * This file is part of OpenOrienteering. + * + * OpenOrienteering is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenOrienteering is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenOrienteering. If not, see . + */ + + +#include "move_parallel_tool.h" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "core/map.h" +#include "core/map_view.h" +#include "core/objects/object.h" +#include "core/path_coord.h" +#include "core/renderables/renderable.h" +#include "core/symbols/combined_symbol.h" +#include "core/symbols/line_symbol.h" +#include "core/symbols/symbol.h" +#include "gui/map/map_widget.h" +#include "tools/tool.h" +#include "tools/tool_base.h" + + +class QPainter; + + +namespace OpenOrienteering { + + +MoveParallelTool::MoveParallelTool(MapEditorController* editor, QAction* tool_action) +: MapEditorToolBase(scaledToScreen(QCursor{ QPixmap(QString::fromLatin1(":/images/cursor-move-parallel.png")), 1, 1 }), + Other, editor, tool_action), + highlight_renderables(new MapRenderables(map())) +{ + // nothing +} + + +MoveParallelTool::~MoveParallelTool() +{ + // nothing +} + + +void MoveParallelTool::updateStatusText() +{ + if (editingInProgress()) + { + setStatusBarText(tr("Move distance: %1 mm") + .arg(QLocale().toString(qFabs(move_distance), 'f', 1))); + } + else + { + setStatusBarText({}); + } +} + + +void MoveParallelTool::objectSelectionChangedImpl() +{ + if (map()->getNumSelectedObjects() == 0) + deactivate(); + else + updateHoverState(); +} + + +/** \todo - Converge the updateHoverState() implementation across the + * different tools (cut, edit line, edit point). + */ +void MoveParallelTool::updateHoverState() +{ + auto new_hover_state = HoverState { HoverFlag::OverNothing }; + const PathObject* new_hover_object = {}; + + if (!map()->selectedObjects().empty()) + { + auto const click_tolerance_sq = qPow(0.001 * cur_map_widget->getMapView()->pixelToLength(clickTolerance()), 2); + auto best_distance_sq = std::numeric_limits::max(); + + for (auto* object : map()->selectedObjects()) + { + if (object->getType() == Object::Path) + { + auto* path = object->asPath(); + auto closest = path->findClosestPointTo(cur_pos_map); + + if (closest.distance_squared >= +0.0 && + closest.distance_squared < best_distance_sq && + closest.distance_squared < qMax(click_tolerance_sq, qPow(path->getSymbol()->calculateLargestLineExtent(), 2))) + { + new_hover_state = HoverFlag::OverPathEdge; + new_hover_object = path; + best_distance_sq = closest.distance_squared; + path_drag_point = closest.path_coord.pos; + path_normal_vector = closest.tangent.normalVector(); + path_normal_vector.normalize(); + } + } + } + } + + // Apply possible changes + if (new_hover_state != hover_state || + new_hover_object != hover_object) + { + highlight_renderables->removeRenderablesOfObject(highlight_object.get(), false); + + hover_state = new_hover_state; + hover_object = const_cast(new_hover_object); + + if (hover_state == HoverFlag::OverPathEdge && hover_object) + { + // Extract hover line + highlight_object = std::unique_ptr(hover_object->duplicate()); + highlight_object->setSymbol(map()->getCoveringCombinedLine(), true); + highlight_object->update(); + highlight_renderables->insertRenderablesOfObject(highlight_object.get()); + } + + effective_start_drag_distance = (hover_state == HoverFlag::OverNothing) ? startDragDistance() : 0; + updateDirtyRect(); + } +} + + +void MoveParallelTool::mouseMove() +{ + updateHoverState(); +} + + +void MoveParallelTool::dragStart() +{ + // Drag movement can start without prior mouse move event on the object. + // E.g. position cursor on above an object border, select the tool and + // immediately start a drag. This is why we update hover state here. + updateHoverState(); + + if (!hover_object) + return; + + orig_hover_object = std::unique_ptr(hover_object->duplicate()); + startEditing(hover_object); + updateStatusText(); +} + + +void MoveParallelTool::dragMove() +{ + if (!hover_object) + return; + + // taken from PathObject::findClosestPointOnBorder + MapCoordVector border_flags; + MapCoordVectorF border_coords; + move_distance = MapCoordF::dotProduct(path_normal_vector, cur_pos_map - path_drag_point); + + hover_object->clearCoordinates(); + highlight_renderables->removeRenderablesOfObject(highlight_object.get(), false); + highlight_object->clearCoordinates(); // TODO - can we share the path part vectors with hover object? + for (auto const& part : orig_hover_object->parts()) + { + LineSymbol::shiftCoordinates(part, -move_distance, 0, + LineSymbol::MiterJoin, border_flags, border_coords); + + auto start_new_part = true; + std::transform(begin(border_flags), end(border_flags), + begin(border_coords), begin(border_flags), + [this, &start_new_part](auto coord, auto const& coord_f) { + coord.setX(coord_f.x()); + coord.setY(coord_f.y()); + hover_object->addCoordinate(coord, start_new_part); + highlight_object->addCoordinate(coord, start_new_part); + start_new_part = false; + return coord; + }); + } + + highlight_object->update(); + highlight_renderables->insertRenderablesOfObject(highlight_object.get()); + updatePreviewObjectsAsynchronously(); + updateDirtyRect(); + updateStatusText(); +} + + +void MoveParallelTool::dragFinish() +{ + if (!hover_object) + return; + + finishEditing(); + updateStatusText(); +} + + +void MoveParallelTool::drawImpl(QPainter* painter, MapWidget* widget) +{ + auto num_selected_objects = map()->selectedObjects().size(); + if (num_selected_objects > 0) + { + drawSelectionOrPreviewObjects(painter, widget); + + if (!highlight_renderables->empty()) + map()->drawSelection(painter, true, widget, highlight_renderables.get(), true); + } +} + + +} // namespace OpenOrienteering diff --git a/src/tools/move_parallel_tool.h b/src/tools/move_parallel_tool.h new file mode 100644 index 0000000000..a96162a7e2 --- /dev/null +++ b/src/tools/move_parallel_tool.h @@ -0,0 +1,116 @@ +/* + * Copyright 2021 Libor Pecháček + * + * This file is part of OpenOrienteering. + * + * OpenOrienteering is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * OpenOrienteering is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with OpenOrienteering. If not, see . + */ + + +#ifndef OPENORIENTEERING_MOVE_PARALLEL_TOOL_H +#define OPENORIENTEERING_MOVE_PARALLEL_TOOL_H + +#include +#include +#include + +#include + +#include "core/map_coord.h" +#include "tools/edit_tool.h" +#include "tools/tool_base.h" + +class QAction; +class QPainter; + + +namespace OpenOrienteering { + +class MapEditorController; +class MapRenderables; +class MapWidget; +class PathObject; + +/** + * A tool to edit lines of PathObjects. + */ +class MoveParallelTool : public MapEditorToolBase +{ + using HoverFlag = EditTool::HoverFlag; + using HoverState = EditTool::HoverState; + +Q_OBJECT +public: + MoveParallelTool(MapEditorController* editor, QAction* tool_action); + ~MoveParallelTool(); + +protected: + void mouseMove() override; + void dragStart() override; + void dragMove() override; + void dragFinish() override; + void drawImpl(QPainter* painter, MapWidget* widget) override; + + void updateHoverState(); + + void updateStatusText() override; + void objectSelectionChangedImpl() override; + +private: + /** + * An object created for the current hover_line. + */ + std::unique_ptr highlight_object; + std::unique_ptr highlight_renderables; + + /** + * Provides general information on what is hovered over. + */ + HoverState hover_state = HoverFlag::OverNothing; + + /** + * Object which is hovered over (if any). + */ + PathObject* hover_object = {}; + + /** + * A copy of the original object which we shift. The new object is + * always a direct modification of the initial copy, so that we + * don't accumulate errors. + */ + std::unique_ptr orig_hover_object; + + /** + * Closest point on the dragged path. We use this point to calculate + * the object shift distance. + */ + MapCoordF path_drag_point; + + /** + * Normal vector at the closest point of the highlighted path. We use the + * normal vector for distance calsulation in dragMove(). + */ + MapCoordF path_normal_vector; + + /** + * The move distance is exposed from dragMove() for the purpose of + * status text updates. + */ + qreal move_distance = {}; +}; + + +} // namespace OpenOrienteering + +#endif