1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-31 22:38:13 +00:00

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.
This commit is contained in:
Timothy Slater 2022-09-07 07:46:16 -05:00 committed by Andreas Kling
parent dc5402f61e
commit 3d542b0c38
5 changed files with 281 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 132 B

View file

@ -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

View file

@ -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<RectangleSelectTool>());
add_tool("wand-select"sv, { 0, Key_W }, make<WandSelectTool>());
add_tool("polygonal-select"sv, { Mod_Shift, Key_P }, make<PolygonalSelectTool>());
add_tool("lasso-select"sv, { 0, Key_L }, make<LassoSelectTool>());
add_tool("guides"sv, { 0, Key_G }, make<GuideTool>());
add_tool("clone"sv, { 0, Key_C }, make<CloneTool>());
}

View file

@ -0,0 +1,231 @@
/*
* Copyright (c) 2022, Timothy Slater <tslater2006@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "LassoSelectTool.h"
#include "../ImageEditor.h"
#include "../Layer.h"
#include <AK/Queue.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/Button.h>
#include <LibGUI/ComboBox.h>
#include <LibGUI/ItemListModel.h>
#include <LibGUI/Label.h>
#include <LibGUI/Model.h>
#include <LibGUI/Painter.h>
#include <LibGUI/ValueSlider.h>
#include <LibGfx/BitmapMixer.h>
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<int>::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<GUI::VerticalBoxLayout>();
auto& mode_container = m_properties_widget->add<GUI::Widget>();
mode_container.set_fixed_height(20);
mode_container.set_layout<GUI::HorizontalBoxLayout>();
auto& mode_label = mode_container.add<GUI::Label>();
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<StringView, (int)Selection::MergeMode::__Count> 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<GUI::ComboBox>();
mode_combo.set_only_allow_values_from_model(true);
mode_combo.set_model(*GUI::ItemListModel<StringView, decltype(s_merge_mode_names)>::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();
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright (c) 2022, Timothy Slater <tslater2006@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#pragma once
#include "../Selection.h"
#include "Tool.h"
#include <AK/Vector.h>
#include <LibGUI/Widget.h>
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<GUI::Widget> m_properties_widget;
Selection::MergeMode m_merge_mode { Selection::MergeMode::Set };
Gfx::IntPoint m_start_position;
Gfx::IntPoint m_most_recent_position;
RefPtr<Gfx::Bitmap> m_selection_bitmap;
RefPtr<Gfx::Bitmap> m_preview_bitmap;
Gfx::IntPoint m_top_left;
Gfx::IntPoint m_bottom_right;
bool m_selecting { false };
};
}