diff --git a/Base/res/icons/pixelpaint/lasso-select.png b/Base/res/icons/pixelpaint/lasso-select.png new file mode 100644 index 0000000000..f0eb62a188 Binary files /dev/null and b/Base/res/icons/pixelpaint/lasso-select.png differ 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 }; +}; + +}