From 038a833f0c3a7d55e4b707b4db86807a19ea8548 Mon Sep 17 00:00:00 2001 From: Timothy Slater Date: Fri, 2 Sep 2022 15:50:47 -0500 Subject: [PATCH] PixelPaint: Add Polygonal Select Tool Polygonal selection tool allows for the drawing of any arbitrary polygonal shape. It tracks clicked points in a vector, upon double clicking we finalize the polygon and generate the selection mask. The user can press the escape key during selection to cancel. The mask is generated as follows: - First we calculate the size of the bounding rect needed to hold the polygon - We add 2 pixels to height/width to allow us a 1 pixel border, the polygon will be centered in this bitmap - Draw the polygon into the bitmap via Gfx::Painter, making sure to connect final polygon point to the first to ensure an enclosed shape - Generate a selection mask the size of the bitmap, with all pixels initially selected - Perform a flood fill from (0,0) which is guaranteed to be outside the polygon - For every pixel reached by the flood fill, we clear the selected pixel from the selection mask - Finally we merge the selection mask like other selection tools. --- .../res/icons/pixelpaint/polygonal-select.png | Bin 0 -> 131 bytes .../Applications/PixelPaint/CMakeLists.txt | 1 + .../Applications/PixelPaint/ToolboxWidget.cpp | 2 + .../PixelPaint/Tools/PolygonalSelectTool.cpp | 225 ++++++++++++++++++ .../PixelPaint/Tools/PolygonalSelectTool.h | 41 ++++ 5 files changed, 269 insertions(+) create mode 100644 Base/res/icons/pixelpaint/polygonal-select.png create mode 100644 Userland/Applications/PixelPaint/Tools/PolygonalSelectTool.cpp create mode 100644 Userland/Applications/PixelPaint/Tools/PolygonalSelectTool.h diff --git a/Base/res/icons/pixelpaint/polygonal-select.png b/Base/res/icons/pixelpaint/polygonal-select.png new file mode 100644 index 0000000000000000000000000000000000000000..663a33578c9b5c30e4ab88fabea2f08afd420b92 GIT binary patch literal 131 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s7ygXeTLn>}1FAy;hTKUdi zZ)?vHEtR9JC)yTBcO)_@t8NqKYW{zuV&X)W+Z$Ic^i-Jigv%}Hk<4tHtFaDS4H_g{ j{3eTbq^KJFVq{SICBk>Wb3!u%0|SGntDnm{r-UW|#OW%> literal 0 HcmV?d00001 diff --git a/Userland/Applications/PixelPaint/CMakeLists.txt b/Userland/Applications/PixelPaint/CMakeLists.txt index 0e00b5c191..52f98d1375 100644 --- a/Userland/Applications/PixelPaint/CMakeLists.txt +++ b/Userland/Applications/PixelPaint/CMakeLists.txt @@ -62,6 +62,7 @@ set(SOURCES Tools/MoveTool.cpp Tools/PenTool.cpp Tools/PickerTool.cpp + Tools/PolygonalSelectTool.cpp Tools/RectangleSelectTool.cpp Tools/RectangleTool.cpp Tools/SprayTool.cpp diff --git a/Userland/Applications/PixelPaint/ToolboxWidget.cpp b/Userland/Applications/PixelPaint/ToolboxWidget.cpp index e47eac2386..a50cc0db56 100644 --- a/Userland/Applications/PixelPaint/ToolboxWidget.cpp +++ b/Userland/Applications/PixelPaint/ToolboxWidget.cpp @@ -16,6 +16,7 @@ #include "Tools/MoveTool.h" #include "Tools/PenTool.h" #include "Tools/PickerTool.h" +#include "Tools/PolygonalSelectTool.h" #include "Tools/RectangleSelectTool.h" #include "Tools/RectangleTool.h" #include "Tools/SprayTool.h" @@ -83,6 +84,7 @@ void ToolboxWidget::setup_tools() add_tool("zoom"sv, { 0, Key_Z }, make()); add_tool("rectangle-select"sv, { 0, Key_R }, make()); add_tool("wand-select"sv, { 0, Key_W }, make()); + add_tool("polygonal-select"sv, { Mod_Shift, Key_P }, make()); add_tool("guides"sv, { 0, Key_G }, make()); add_tool("clone"sv, { 0, Key_C }, make()); } diff --git a/Userland/Applications/PixelPaint/Tools/PolygonalSelectTool.cpp b/Userland/Applications/PixelPaint/Tools/PolygonalSelectTool.cpp new file mode 100644 index 0000000000..9815f579cb --- /dev/null +++ b/Userland/Applications/PixelPaint/Tools/PolygonalSelectTool.cpp @@ -0,0 +1,225 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "PolygonalSelectTool.h" +#include "../ImageEditor.h" +#include "../Layer.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace PixelPaint { + +void PolygonalSelectTool::flood_polygon_selection(Gfx::Bitmap& polygon_bitmap, Gfx::IntPoint polygon_delta) +{ + VERIFY(polygon_bitmap.bpp() == 32); + + // Create Mask which will track already-processed pixels. + Mask selection_mask = Mask::full(polygon_bitmap.rect().translated(polygon_delta)); + + auto pixel_reached = [&](Gfx::IntPoint location) { + selection_mask.set(Gfx::IntPoint(location.x(), location.y()).translated(polygon_delta), 0); + }; + + polygon_bitmap.flood_visit_from_point({ polygon_bitmap.width() - 1, polygon_bitmap.height() - 1 }, 0, move(pixel_reached)); + + selection_mask.shrink_to_fit(); + m_editor->image().selection().merge(selection_mask, m_merge_mode); +} + +void PolygonalSelectTool::process_polygon() +{ + // Determine minimum bounding box that can hold the polygon. + auto min_x_seen = m_polygon_points.at(0).x(); + auto max_x_seen = m_polygon_points.at(0).x(); + auto min_y_seen = m_polygon_points.at(0).y(); + auto max_y_seen = m_polygon_points.at(0).y(); + + for (auto point : m_polygon_points) { + if (point.x() < min_x_seen) + min_x_seen = point.x(); + if (point.x() > max_x_seen) + max_x_seen = point.x(); + if (point.y() < min_y_seen) + min_y_seen = point.y(); + if (point.y() > max_y_seen) + max_y_seen = point.y(); + } + + // We create a bitmap that is bigger by 1 pixel on each side (+2) and need to account for the 0 indexed + // pixel positions (+1) so we make the bitmap size the delta of x/y min/max + 3. + auto polygon_bitmap_or_error = Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRA8888, { (max_x_seen - min_x_seen) + 3, (max_y_seen - min_y_seen) + 3 }); + if (polygon_bitmap_or_error.is_error()) + return; + + auto polygon_bitmap = polygon_bitmap_or_error.release_value(); + + auto polygon_painter = Gfx::Painter(polygon_bitmap); + // We want to paint the polygon into the bitmap such that there is an empty 1px border all the way around it + // this ensures that we have a known pixel (0,0) that is outside the polygon. Since the coordinates are relative + // to the layer but the bitmap is cropped to the bounding rect of the polygon we need to offset our + // points by the the negative of min x/y. And because we want a 1 px offset to the right and down, we + 1 this. + auto polygon_bitmap_delta = Gfx::IntPoint(-min_x_seen + 1, -min_y_seen + 1); + polygon_painter.translate(polygon_bitmap_delta); + for (size_t i = 0; i < m_polygon_points.size() - 1; i++) { + polygon_painter.draw_line(m_polygon_points.at(i), m_polygon_points.at(i + 1), Color::Black); + } + polygon_painter.draw_line(m_polygon_points.at(m_polygon_points.size() - 1), m_polygon_points.at(0), Color::Black); + + // Delta to use for mapping the bitmap back to layer coordinates. -1 to account for the right and down offset. + auto bitmap_to_layer_delta = Gfx::IntPoint(min_x_seen + m_editor->active_layer()->location().x() - 1, min_y_seen + m_editor->active_layer()->location().y() - 1); + flood_polygon_selection(polygon_bitmap, bitmap_to_layer_delta); +} +void PolygonalSelectTool::on_mousedown(Layer*, MouseEvent& event) +{ + auto& image_event = event.image_event(); + if (image_event.button() != GUI::MouseButton::Primary) + return; + if (!m_selecting) { + m_polygon_points.clear(); + m_last_selecting_cursor_position = event.layer_event().position(); + } + + m_selecting = true; + + auto new_point = event.layer_event().position(); + + // This point matches the first point exactly. Consider this polygon finished. + if (m_polygon_points.size() > 0 && new_point == m_polygon_points.at(0)) { + m_selecting = false; + m_editor->image().selection().end_interactive_selection(); + process_polygon(); + m_editor->did_complete_action(tool_name()); + m_editor->update(); + return; + } + + // Avoid adding the same point multiple times if the user clicks again without moving the mouse. + if (m_polygon_points.size() > 0 && m_polygon_points.at(m_polygon_points.size() - 1) == new_point) + return; + + m_polygon_points.append(new_point); + m_editor->image().selection().begin_interactive_selection(); + + m_editor->update(); +} + +void PolygonalSelectTool::on_mousemove(Layer*, MouseEvent& event) +{ + if (m_selecting) + m_last_selecting_cursor_position = event.layer_event().position(); + m_editor->update(); +} + +void PolygonalSelectTool::on_doubleclick(Layer*, MouseEvent&) +{ + m_selecting = false; + m_editor->image().selection().end_interactive_selection(); + process_polygon(); + m_editor->did_complete_action(tool_name()); + m_editor->update(); +} + +void PolygonalSelectTool::on_second_paint(Layer const* layer, GUI::PaintEvent& event) +{ + if (!m_selecting) + return; + + GUI::Painter painter(*m_editor); + painter.add_clip_rect(event.rect()); + + painter.translate(editor_layer_location(*layer)); + + for (size_t i = 0; i < m_polygon_points.size() - 1; i++) { + auto preview_start = editor_stroke_position(m_polygon_points.at(i), 1); + auto preview_end = editor_stroke_position(m_polygon_points.at(i + 1), 1); + painter.draw_line(preview_start, preview_end, Color::Black, AK::max(m_editor->scale(), 1)); + } + + auto last_line_start = editor_stroke_position(m_polygon_points.at(m_polygon_points.size() - 1), 1); + auto last_line_stop = editor_stroke_position(m_last_selecting_cursor_position, 1); + painter.draw_line(last_line_start, last_line_stop, Color::Black, AK::max(m_editor->scale(), 1)); +} + +void PolygonalSelectTool::on_keydown(GUI::KeyEvent& key_event) +{ + Tool::on_keydown(key_event); + if (key_event.key() == KeyCode::Key_Escape) { + if (m_selecting) { + m_selecting = false; + m_polygon_points.clear(); + } else { + m_editor->image().selection().clear(); + } + } +} + +GUI::Widget* PolygonalSelectTool::get_properties_widget() +{ + if (m_properties_widget) + return m_properties_widget.ptr(); + + m_properties_widget = GUI::Widget::construct(); + m_properties_widget->set_layout(); + + auto& mode_container = m_properties_widget->add(); + mode_container.set_fixed_height(20); + mode_container.set_layout(); + + auto& mode_label = mode_container.add(); + mode_label.set_text("Mode:"); + mode_label.set_text_alignment(Gfx::TextAlignment::CenterLeft); + mode_label.set_fixed_size(80, 20); + + static constexpr auto s_merge_mode_names = [] { + Array names; + for (size_t i = 0; i < names.size(); i++) { + switch ((Selection::MergeMode)i) { + case Selection::MergeMode::Set: + names[i] = "Set"sv; + break; + case Selection::MergeMode::Add: + names[i] = "Add"sv; + break; + case Selection::MergeMode::Subtract: + names[i] = "Subtract"sv; + break; + case Selection::MergeMode::Intersect: + names[i] = "Intersect"sv; + break; + default: + break; + } + } + return names; + }(); + + auto& mode_combo = mode_container.add(); + mode_combo.set_only_allow_values_from_model(true); + mode_combo.set_model(*GUI::ItemListModel::create(s_merge_mode_names)); + mode_combo.set_selected_index((int)m_merge_mode); + mode_combo.on_change = [this](auto&&, GUI::ModelIndex const& index) { + VERIFY(index.row() >= 0); + VERIFY(index.row() < (int)Selection::MergeMode::__Count); + + m_merge_mode = (Selection::MergeMode)index.row(); + }; + + return m_properties_widget.ptr(); +} + +Gfx::IntPoint PolygonalSelectTool::point_position_to_preferred_cell(Gfx::FloatPoint const& position) const +{ + return position.to_type(); +} + +} diff --git a/Userland/Applications/PixelPaint/Tools/PolygonalSelectTool.h b/Userland/Applications/PixelPaint/Tools/PolygonalSelectTool.h new file mode 100644 index 0000000000..1b0f44286d --- /dev/null +++ b/Userland/Applications/PixelPaint/Tools/PolygonalSelectTool.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2022, the SerenityOS developers. + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "../Selection.h" +#include "Tool.h" +#include +#include + +namespace PixelPaint { + +class PolygonalSelectTool final : public Tool { +public: + PolygonalSelectTool() = default; + virtual ~PolygonalSelectTool() = default; + virtual void on_doubleclick(Layer*, MouseEvent& event) override; + virtual void on_mousedown(Layer*, MouseEvent& event) override; + virtual void on_mousemove(Layer*, MouseEvent& event) override; + virtual void on_keydown(GUI::KeyEvent&) override; + virtual void on_second_paint(Layer const*, GUI::PaintEvent&) override; + virtual GUI::Widget* get_properties_widget() override; + virtual Variant> cursor() override { return Gfx::StandardCursor::Crosshair; } + virtual Gfx::IntPoint point_position_to_preferred_cell(Gfx::FloatPoint const& position) const override; + +private: + virtual void flood_polygon_selection(Gfx::Bitmap&, Gfx::IntPoint); + virtual void process_polygon(); + virtual StringView tool_name() const override { return "Polygonal Select Tool"sv; } + + RefPtr m_properties_widget; + Selection::MergeMode m_merge_mode { Selection::MergeMode::Set }; + bool m_selecting { false }; + Gfx::IntPoint m_last_selecting_cursor_position; + Vector m_polygon_points {}; +}; + +}