From 3d542b0c38ee06cb4d1c1fe7b11706d0f4bd69cf Mon Sep 17 00:00:00 2001 From: Timothy Slater Date: Wed, 7 Sep 2022 07:46:16 -0500 Subject: [PATCH] PixelPaint: Add lasso selection tool Lasso selection works by allowing the user to draw an arbitrary shape much like the pen tool and ensuring the shape is closed by connecting the start/end points when the user is done drawing. Everything inside the shape becomes the selection. Selection is determined via an outer flood fill. We begin a flood fill from a point that is guaranteed to be outside of the drawn shape, and anything the fill doesn't touch is determined to be the selection region. --- Base/res/icons/pixelpaint/lasso-select.png | Bin 0 -> 132 bytes .../Applications/PixelPaint/CMakeLists.txt | 1 + .../Applications/PixelPaint/ToolboxWidget.cpp | 2 + .../PixelPaint/Tools/LassoSelectTool.cpp | 231 ++++++++++++++++++ .../PixelPaint/Tools/LassoSelectTool.h | 47 ++++ 5 files changed, 281 insertions(+) create mode 100644 Base/res/icons/pixelpaint/lasso-select.png create mode 100644 Userland/Applications/PixelPaint/Tools/LassoSelectTool.cpp create mode 100644 Userland/Applications/PixelPaint/Tools/LassoSelectTool.h diff --git a/Base/res/icons/pixelpaint/lasso-select.png b/Base/res/icons/pixelpaint/lasso-select.png new file mode 100644 index 0000000000000000000000000000000000000000..f0eb62a188020ee1d362b70cd40ec17834514dac GIT binary patch literal 132 zcmeAS@N?(olHy`uVBq!ia0y~yU=RRd4mJh`2Kmqb6B!s7yggkULn>~aoxD+yL4kw$ z@89-g_nbMVJ84d|=_s`2*ZUb@Dw6cFr_o~VNd}Ja{{4)LRl^x3buI}%@X=9b?(Ml3 lb_g6lwWIzZ(}@YZRhuOFf8Vux#lXP8;OXk;vd$@?2>?7$E~o$i literal 0 HcmV?d00001 diff --git a/Userland/Applications/PixelPaint/CMakeLists.txt b/Userland/Applications/PixelPaint/CMakeLists.txt index 8869981104..a423b52e37 100644 --- a/Userland/Applications/PixelPaint/CMakeLists.txt +++ b/Userland/Applications/PixelPaint/CMakeLists.txt @@ -58,6 +58,7 @@ set(SOURCES Tools/EllipseTool.cpp Tools/EraseTool.cpp Tools/GuideTool.cpp + Tools/LassoSelectTool.cpp Tools/LineTool.cpp Tools/MoveTool.cpp Tools/PenTool.cpp diff --git a/Userland/Applications/PixelPaint/ToolboxWidget.cpp b/Userland/Applications/PixelPaint/ToolboxWidget.cpp index a50cc0db56..9f8a2d31f4 100644 --- a/Userland/Applications/PixelPaint/ToolboxWidget.cpp +++ b/Userland/Applications/PixelPaint/ToolboxWidget.cpp @@ -12,6 +12,7 @@ #include "Tools/EllipseTool.h" #include "Tools/EraseTool.h" #include "Tools/GuideTool.h" +#include "Tools/LassoSelectTool.h" #include "Tools/LineTool.h" #include "Tools/MoveTool.h" #include "Tools/PenTool.h" @@ -85,6 +86,7 @@ void ToolboxWidget::setup_tools() 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("lasso-select"sv, { 0, Key_L }, make()); add_tool("guides"sv, { 0, Key_G }, make()); add_tool("clone"sv, { 0, Key_C }, make()); } diff --git a/Userland/Applications/PixelPaint/Tools/LassoSelectTool.cpp b/Userland/Applications/PixelPaint/Tools/LassoSelectTool.cpp new file mode 100644 index 0000000000..d945c0c621 --- /dev/null +++ b/Userland/Applications/PixelPaint/Tools/LassoSelectTool.cpp @@ -0,0 +1,231 @@ +/* + * Copyright (c) 2022, Timothy Slater + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include "LassoSelectTool.h" +#include "../ImageEditor.h" +#include "../Layer.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace PixelPaint { + +void LassoSelectTool::on_mousedown(Layer* layer, MouseEvent& event) +{ + if (!layer) + return; + + auto& layer_event = event.layer_event(); + if (!layer->rect().contains(layer_event.position())) + return; + + auto selection_bitmap_result = Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRA8888, layer->content_bitmap().size()); + if (selection_bitmap_result.is_error()) + return; + + auto preview_bitmap_result = Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRA8888, layer->content_bitmap().size()); + if (preview_bitmap_result.is_error()) + return; + + m_selection_bitmap = selection_bitmap_result.release_value(); + m_preview_bitmap = preview_bitmap_result.release_value(); + m_start_position = layer_event.position(); + m_most_recent_position = layer_event.position(); + m_top_left = m_start_position; + m_bottom_right = m_start_position; + + m_selection_bitmap->set_pixel(m_most_recent_position, Gfx::Color::Black); + + m_selecting = true; + + m_editor->image().selection().begin_interactive_selection(); +} + +void LassoSelectTool::on_mousemove(Layer* layer, MouseEvent& event) +{ + if (!m_selecting) + return; + + auto& layer_event = event.layer_event(); + auto new_position = layer_event.position(); + if (!layer->rect().contains(new_position)) + return; + + if (new_position == m_most_recent_position) + return; + + // tracking the bounding box for cropping the selection bitmap at the end + if (new_position.x() < m_top_left.x()) + m_top_left.set_x(new_position.x()); + if (new_position.y() < m_top_left.y()) + m_top_left.set_y(new_position.y()); + if (new_position.x() > m_bottom_right.x()) + m_bottom_right.set_x(new_position.x()); + if (new_position.y() > m_bottom_right.y()) + m_bottom_right.set_y(new_position.y()); + + auto preview_painter = Gfx::Painter(*m_preview_bitmap); + auto preview_start = editor_stroke_position(m_most_recent_position, 1); + auto preview_end = editor_stroke_position(new_position, 1); + preview_painter.draw_line(preview_start, preview_end, Gfx::Color::Black, AK::max(1 * m_editor->scale(), 1)); + + auto selection_painter = Gfx::Painter(*m_selection_bitmap); + selection_painter.draw_line(m_most_recent_position, new_position, Gfx::Color::Black); + + m_most_recent_position = new_position; +} + +void LassoSelectTool::on_mouseup(Layer*, MouseEvent&) +{ + if (!m_selecting) + return; + + if (m_selection_bitmap.is_null()) + return; + + m_selecting = false; + m_bottom_right.translate_by(1); + + if (m_most_recent_position != m_start_position) { + auto selection_painter = Gfx::Painter(*m_selection_bitmap); + selection_painter.draw_line(m_most_recent_position, m_start_position, Gfx::Color::Black, 1); + } + + auto cropped_selection_result = m_selection_bitmap->cropped(Gfx::Rect::from_two_points(m_top_left, m_bottom_right)); + if (cropped_selection_result.is_error()) + return; + auto cropped_selection = cropped_selection_result.release_value(); + + // We create a bitmap that is bigger by 1 pixel on each side + auto lasso_bitmap_or_error = Gfx::Bitmap::try_create(Gfx::BitmapFormat::BGRA8888, { (m_bottom_right.x() - m_top_left.x()) + 2, (m_bottom_right.y() - m_top_left.y()) + 2 }); + if (lasso_bitmap_or_error.is_error()) + return; + + auto lasso_bitmap = lasso_bitmap_or_error.release_value(); + + auto lasso_painter = Gfx::Painter(lasso_bitmap); + + // We want to paint the lasso into the bitmap such that there is an empty 1px border on each side + // this ensures that we have a known pixel (0,0) that is outside the lasso. + // Because we want a 1 px offset to the right and down, we blit the cropped selection bitmap starting at (1,1). + lasso_painter.blit({ 1, 1 }, cropped_selection, cropped_selection->rect()); + + // 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(m_top_left.x() + m_editor->active_layer()->location().x() - 1, m_top_left.y() + m_editor->active_layer()->location().y() - 1); + flood_lasso_selection(lasso_bitmap, bitmap_to_layer_delta); +} + +void LassoSelectTool::flood_lasso_selection(Gfx::Bitmap& lasso_bitmap, Gfx::IntPoint lasso_delta) +{ + VERIFY(lasso_bitmap.bpp() == 32); + + // Create Mask which will track already-processed pixels + Mask selection_mask = Mask::full(lasso_bitmap.rect().translated(lasso_delta)); + + auto pixel_reached = [&](Gfx::IntPoint location) { + selection_mask.set(Gfx::IntPoint(location.x(), location.y()).translated(lasso_delta), 0); + }; + + lasso_bitmap.flood_visit_from_point({ 0, 0 }, 0, move(pixel_reached)); + + selection_mask.shrink_to_fit(); + selection_mask.bounding_rect().translate_by(m_editor->active_layer()->location()); + m_editor->image().selection().merge(selection_mask, m_merge_mode); +} + +void LassoSelectTool::on_second_paint(Layer const* layer, GUI::PaintEvent& event) +{ + + if (!m_selecting) + return; + + if (m_preview_bitmap.is_null()) + return; + + GUI::Painter painter(*m_editor); + painter.add_clip_rect(event.rect()); + + painter.blit(layer->location(), *m_preview_bitmap, m_preview_bitmap->rect()); +} + +bool LassoSelectTool::on_keydown(GUI::KeyEvent const& key_event) +{ + Tool::on_keydown(key_event); + if (key_event.key() == KeyCode::Key_Escape) { + if (m_selecting) { + m_selecting = false; + m_selection_bitmap.clear(); + m_preview_bitmap.clear(); + return true; + } + } + + return Tool::on_keydown(key_event); +} + +GUI::Widget* LassoSelectTool::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(); +} + +} diff --git a/Userland/Applications/PixelPaint/Tools/LassoSelectTool.h b/Userland/Applications/PixelPaint/Tools/LassoSelectTool.h new file mode 100644 index 0000000000..af2730571f --- /dev/null +++ b/Userland/Applications/PixelPaint/Tools/LassoSelectTool.h @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2022, Timothy Slater + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#pragma once + +#include "../Selection.h" +#include "Tool.h" + +#include +#include + +namespace PixelPaint { + +class LassoSelectTool final : public Tool { +public: + LassoSelectTool() = default; + virtual ~LassoSelectTool() = default; + + virtual void on_mousedown(Layer*, MouseEvent& event) override; + virtual void on_mouseup(Layer*, MouseEvent& event) override; + virtual void on_mousemove(Layer*, MouseEvent& event) override; + virtual bool on_keydown(GUI::KeyEvent const&) override; + virtual void on_second_paint(Layer const*, GUI::PaintEvent&) override; + virtual GUI::Widget* get_properties_widget() override; + +private: + virtual StringView tool_name() const override { return "Lasso Select Tool"sv; } + void flood_lasso_selection(Gfx::Bitmap&, Gfx::IntPoint); + + RefPtr m_properties_widget; + Selection::MergeMode m_merge_mode { Selection::MergeMode::Set }; + + Gfx::IntPoint m_start_position; + Gfx::IntPoint m_most_recent_position; + RefPtr m_selection_bitmap; + RefPtr m_preview_bitmap; + + Gfx::IntPoint m_top_left; + Gfx::IntPoint m_bottom_right; + + bool m_selecting { false }; +}; + +}