1
Fork 0
mirror of https://github.com/RGBCube/serenity synced 2025-05-31 11:18:11 +00:00
serenity/Userland/Applications/PixelPaint/Tools/EllipseTool.cpp
Timothy Slater 0d7d759095 PixelPaint: Limit editing tools to selection
This effectively creates a double-buffer for tools to use when modifying
the layer's bitmap (content or mask). Once the changes have been made
the tool reports to the layer that it has made changes along with a Rect
of the changed region. The layer will then merge the changes from the
scratch image to the real bitmap. This merge is done as follows: If a
given pixel is inside the selected region, the pixel from the scratch
bitmap is copied to the real bitmap. If the pixel is not inside the
selected region, the pixel from the real bitmap is copied to the scratch
bitmap.

As an optimization, when there is no selection active, the new method
for getting the scratch bitmap will return the real bitmap and no
merging will need to take place.
2022-10-24 23:46:22 +01:00

220 lines
8.5 KiB
C++

/*
* Copyright (c) 2018-2020, Andreas Kling <kling@serenityos.org>
* Copyright (c) 2021, Mustafa Quraish <mustafa@serenityos.org>
* Copyright (c) 2022, the SerenityOS developers.
*
* SPDX-License-Identifier: BSD-2-Clause
*/
#include "EllipseTool.h"
#include "../ImageEditor.h"
#include "../Layer.h"
#include <LibGUI/Action.h>
#include <LibGUI/BoxLayout.h>
#include <LibGUI/CheckBox.h>
#include <LibGUI/Label.h>
#include <LibGUI/Menu.h>
#include <LibGUI/Painter.h>
#include <LibGUI/RadioButton.h>
#include <LibGUI/TextBox.h>
#include <LibGUI/ValueSlider.h>
#include <LibGfx/AntiAliasingPainter.h>
#include <LibGfx/Rect.h>
namespace PixelPaint {
void EllipseTool::draw_using(GUI::Painter& painter, Gfx::IntPoint const& start_position, Gfx::IntPoint const& end_position, int thickness)
{
Gfx::IntRect ellipse_intersecting_rect;
if (m_draw_mode == DrawMode::FromCenter) {
auto delta = end_position - start_position;
ellipse_intersecting_rect = Gfx::IntRect::from_two_points(start_position - delta, end_position);
} else {
ellipse_intersecting_rect = Gfx::IntRect::from_two_points(start_position, end_position);
}
Gfx::AntiAliasingPainter aa_painter { painter };
switch (m_fill_mode) {
case FillMode::Outline:
if (m_antialias_enabled) {
aa_painter.draw_ellipse(ellipse_intersecting_rect, m_editor->color_for(m_drawing_button), thickness);
} else {
// For some reason for non-AA draw_ellipse() the ellipse is outside of the rect (unlike all other ellipse drawing functions).
// Scale the ellipse rect by sqrt(2) to get an ellipse arc that appears as if it was inside of the rect.
// Ie. reduce the size by a factor of 1 - sqrt(1/2)
auto shrink_width = ellipse_intersecting_rect.width() * (1 - AK::Sqrt1_2<float>);
auto shrink_height = ellipse_intersecting_rect.height() * (1 - AK::Sqrt1_2<float>);
ellipse_intersecting_rect.shrink(shrink_width, shrink_height);
painter.draw_ellipse_intersecting(ellipse_intersecting_rect, m_editor->color_for(m_drawing_button), thickness);
}
break;
case FillMode::Fill:
if (m_antialias_enabled)
aa_painter.fill_ellipse(ellipse_intersecting_rect, m_editor->color_for(m_drawing_button));
else
painter.fill_ellipse(ellipse_intersecting_rect, m_editor->color_for(m_drawing_button));
break;
default:
VERIFY_NOT_REACHED();
}
}
void EllipseTool::on_mousedown(Layer* layer, MouseEvent& event)
{
if (!layer)
return;
auto& layer_event = event.layer_event();
if (layer_event.button() != GUI::MouseButton::Primary && layer_event.button() != GUI::MouseButton::Secondary)
return;
if (m_drawing_button != GUI::MouseButton::None)
return;
m_drawing_button = layer_event.button();
m_ellipse_start_position = layer_event.position();
m_ellipse_end_position = layer_event.position();
m_editor->update();
}
void EllipseTool::on_mouseup(Layer* layer, MouseEvent& event)
{
if (!layer)
return;
if (event.layer_event().button() == m_drawing_button) {
GUI::Painter painter(layer->get_scratch_edited_bitmap());
draw_using(painter, m_ellipse_start_position, m_ellipse_end_position, m_thickness);
m_drawing_button = GUI::MouseButton::None;
layer->did_modify_bitmap(layer->get_scratch_edited_bitmap().rect());
m_editor->update();
m_editor->did_complete_action(tool_name());
}
}
void EllipseTool::on_mousemove(Layer*, MouseEvent& event)
{
if (m_drawing_button == GUI::MouseButton::None)
return;
m_draw_mode = event.layer_event().alt() ? DrawMode::FromCenter : DrawMode::FromCorner;
if (event.layer_event().shift())
m_ellipse_end_position = m_ellipse_start_position.end_point_for_aspect_ratio(event.layer_event().position(), 1.0);
else if (m_aspect_ratio.has_value())
m_ellipse_end_position = m_ellipse_start_position.end_point_for_aspect_ratio(event.layer_event().position(), m_aspect_ratio.value());
else
m_ellipse_end_position = event.layer_event().position();
m_editor->update();
}
void EllipseTool::on_second_paint(Layer const* layer, GUI::PaintEvent& event)
{
if (!layer || m_drawing_button == GUI::MouseButton::None)
return;
GUI::Painter painter(*m_editor);
painter.add_clip_rect(event.rect());
painter.translate(editor_layer_location(*layer));
auto preview_start = m_editor->content_to_frame_position(m_ellipse_start_position).to_type<int>();
auto preview_end = m_editor->content_to_frame_position(m_ellipse_end_position).to_type<int>();
draw_using(painter, preview_start, preview_end, AK::max(m_thickness * m_editor->scale(), 1));
}
void EllipseTool::on_keydown(GUI::KeyEvent& event)
{
Tool::on_keydown(event);
if (event.key() == Key_Escape && m_drawing_button != GUI::MouseButton::None) {
m_drawing_button = GUI::MouseButton::None;
m_editor->update();
event.accept();
}
}
GUI::Widget* EllipseTool::get_properties_widget()
{
if (!m_properties_widget) {
m_properties_widget = GUI::Widget::construct();
m_properties_widget->set_layout<GUI::VerticalBoxLayout>();
auto& thickness_container = m_properties_widget->add<GUI::Widget>();
thickness_container.set_fixed_height(20);
thickness_container.set_layout<GUI::HorizontalBoxLayout>();
auto& thickness_label = thickness_container.add<GUI::Label>("Thickness:");
thickness_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
thickness_label.set_fixed_size(80, 20);
auto& thickness_slider = thickness_container.add<GUI::ValueSlider>(Orientation::Horizontal, "px");
thickness_slider.set_range(1, 10);
thickness_slider.set_value(m_thickness);
thickness_slider.on_change = [&](int value) {
m_thickness = value;
};
set_primary_slider(&thickness_slider);
auto& mode_container = m_properties_widget->add<GUI::Widget>();
mode_container.set_fixed_height(70);
mode_container.set_layout<GUI::HorizontalBoxLayout>();
auto& mode_label = mode_container.add<GUI::Label>("Mode:");
mode_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
auto& mode_radio_container = mode_container.add<GUI::Widget>();
mode_radio_container.set_layout<GUI::VerticalBoxLayout>();
auto& outline_mode_radio = mode_radio_container.add<GUI::RadioButton>("Outline");
auto& fill_mode_radio = mode_radio_container.add<GUI::RadioButton>("Fill");
auto& aa_enable_checkbox = mode_radio_container.add<GUI::CheckBox>("Anti-alias");
aa_enable_checkbox.on_checked = [&](bool checked) {
m_antialias_enabled = checked;
};
outline_mode_radio.on_checked = [&](bool checked) {
if (checked)
m_fill_mode = FillMode::Outline;
};
fill_mode_radio.on_checked = [&](bool checked) {
if (checked)
m_fill_mode = FillMode::Fill;
};
aa_enable_checkbox.set_checked(false);
outline_mode_radio.set_checked(true);
auto& aspect_container = m_properties_widget->add<GUI::Widget>();
aspect_container.set_fixed_height(20);
aspect_container.set_layout<GUI::HorizontalBoxLayout>();
auto& aspect_label = aspect_container.add<GUI::Label>("Aspect Ratio:");
aspect_label.set_text_alignment(Gfx::TextAlignment::CenterLeft);
aspect_label.set_fixed_size(80, 20);
m_aspect_w_textbox = aspect_container.add<GUI::TextBox>();
m_aspect_w_textbox->set_fixed_height(20);
m_aspect_w_textbox->set_fixed_width(25);
m_aspect_w_textbox->on_change = [&] {
auto x = m_aspect_w_textbox->text().to_int().value_or(0);
auto y = m_aspect_h_textbox->text().to_int().value_or(0);
if (x > 0 && y > 0) {
m_aspect_ratio = (float)x / (float)y;
} else {
m_aspect_ratio = {};
}
};
auto& multiply_label = aspect_container.add<GUI::Label>("x");
multiply_label.set_text_alignment(Gfx::TextAlignment::Center);
multiply_label.set_fixed_size(10, 20);
m_aspect_h_textbox = aspect_container.add<GUI::TextBox>();
m_aspect_h_textbox->set_fixed_height(20);
m_aspect_h_textbox->set_fixed_width(25);
m_aspect_h_textbox->on_change = [&] { m_aspect_w_textbox->on_change(); };
}
return m_properties_widget.ptr();
}
}