diff --git a/images/cursor-move-parallel.png b/images/cursor-move-parallel.png new file mode 100644 index 0000000000..cb18f43ed7 Binary files /dev/null and b/images/cursor-move-parallel.png differ 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 0000000000..e88742313f Binary files /dev/null and b/images/tool-move-parallel.png differ 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/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); } } } 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