1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-07-25 19:37:35 +00:00

PixelPaint: Ensure the selection is always within image bounds

This commit is contained in:
Tim Ledbetter 2023-03-29 18:00:59 +01:00 committed by Sam Atkins
parent fc137a7827
commit f132751fae
8 changed files with 97 additions and 55 deletions

View file

@ -1,5 +1,6 @@
/*
* Copyright (c) 2022-2023, the SerenityOS developers.
* Copyright (c) 2023, Tim Ledbetter <timledbetter@gmail.com>
*
* SPDX-License-Identifier: BSD-2-Clause
*/
@ -24,14 +25,16 @@ void PolygonalSelectTool::flood_polygon_selection(Gfx::Bitmap& polygon_bitmap, G
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 mask_rect = Gfx::IntRect(polygon_delta, polygon_bitmap.size()).intersected(m_editor->image().rect());
auto selection_mask = Mask::full(mask_rect);
auto pixel_reached = [&](Gfx::IntPoint location) {
selection_mask.set(Gfx::IntPoint(location.x(), location.y()).translated(polygon_delta), 0);
auto point_to_set = location.translated(polygon_delta);
if (mask_rect.contains(point_to_set))
selection_mask.set(point_to_set, 0);
};
polygon_bitmap.flood_visit_from_point({ polygon_bitmap.width() - 1, polygon_bitmap.height() - 1 }, 0, move(pixel_reached));
polygon_bitmap.flood_visit_from_point({ 0, 0 }, 0, move(pixel_reached));
selection_mask.shrink_to_fit();
m_editor->image().selection().merge(selection_mask, m_merge_mode);
}
@ -39,60 +42,65 @@ void PolygonalSelectTool::flood_polygon_selection(Gfx::Bitmap& polygon_bitmap, G
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();
auto top_left = m_polygon_points.at(0);
auto bottom_right = m_polygon_points.at(0);
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();
if (point.x() < top_left.x())
top_left.set_x(point.x());
if (point.x() > bottom_right.x())
bottom_right.set_x(point.x());
if (point.y() < top_left.y())
top_left.set_y(point.y());
if (point.y() > bottom_right.y())
bottom_right.set_y(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::create(Gfx::BitmapFormat::BGRA8888, { (max_x_seen - min_x_seen) + 3, (max_y_seen - min_y_seen) + 3 });
top_left.translate_by(-1);
auto polygon_rect = Gfx::IntRect::from_two_points(top_left, bottom_right);
auto image_rect = m_editor->image().rect();
if (!polygon_rect.intersects(image_rect)) {
m_editor->image().selection().merge(Gfx::IntRect {}, m_merge_mode);
return;
}
if (m_polygon_points.last() != m_polygon_points.first())
m_polygon_points.append(m_polygon_points.first());
// 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.
auto bitmap_rect = polygon_rect.inflated(2, 2);
// FIXME: It should be possible to limit the size of the polygon bitmap to the size of the canvas, as that is
// the maximum possible size of the selection.
auto polygon_bitmap_or_error = Gfx::Bitmap::create(Gfx::BitmapFormat::BGRA8888, bitmap_rect.size());
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);
Gfx::Painter polygon_painter(polygon_bitmap);
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);
auto line_start = m_polygon_points.at(i) - top_left;
auto line_end = m_polygon_points.at(i + 1) - top_left;
polygon_painter.draw_line(line_start, line_end, 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);
flood_polygon_selection(polygon_bitmap, top_left);
}
void PolygonalSelectTool::on_mousedown(Layer*, MouseEvent& event)
{
auto& image_event = event.image_event();
auto const& 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_last_selecting_cursor_position = image_event.position();
}
m_selecting = true;
auto new_point = event.layer_event().position();
if (!m_polygon_points.is_empty() && event.layer_event().shift())
auto new_point = image_event.position();
if (!m_polygon_points.is_empty() && image_event.shift())
new_point = Tool::constrain_line_angle(m_polygon_points.last(), new_point);
// This point matches the first point exactly. Consider this polygon finished.
@ -120,10 +128,11 @@ void PolygonalSelectTool::on_mousemove(Layer*, MouseEvent& event)
if (!m_selecting)
return;
if (event.layer_event().shift())
m_last_selecting_cursor_position = Tool::constrain_line_angle(m_polygon_points.last(), event.layer_event().position());
auto const& image_event = event.image_event();
if (image_event.shift())
m_last_selecting_cursor_position = Tool::constrain_line_angle(m_polygon_points.last(), image_event.position());
else
m_last_selecting_cursor_position = event.layer_event().position();
m_last_selecting_cursor_position = image_event.position();
m_editor->update();
}
@ -137,7 +146,7 @@ void PolygonalSelectTool::on_doubleclick(Layer*, MouseEvent&)
m_editor->update();
}
void PolygonalSelectTool::on_second_paint(Layer const* layer, GUI::PaintEvent& event)
void PolygonalSelectTool::on_second_paint(Layer const*, GUI::PaintEvent& event)
{
if (!m_selecting)
return;
@ -145,8 +154,6 @@ void PolygonalSelectTool::on_second_paint(Layer const* layer, GUI::PaintEvent& e
GUI::Painter painter(*m_editor);
painter.add_clip_rect(event.rect());
painter.translate(editor_layer_location(*layer));
auto draw_preview_lines = [&](auto color, auto thickness) {
for (size_t i = 0; i < m_polygon_points.size() - 1; i++) {
auto preview_start = editor_stroke_position(m_polygon_points.at(i), 1);