LibGfx: Implement filling paths

There are some imperfections with intersecting edges (because the main
algorithm used is scanline, and that is not geared towards drawing
complex shapes), however, it behaves mostly fine for normal use :^)
This commit is contained in:
AnotherTest 2020-05-06 11:55:12 +04:30 committed by Andreas Kling
parent 4d20cf57db
commit f54b41f748
Notes: sideshowbarker 2024-07-19 06:56:14 +09:00
5 changed files with 274 additions and 8 deletions

View file

@ -37,7 +37,7 @@ class FloatRect;
class FloatPoint {
public:
FloatPoint() {}
FloatPoint() { }
FloatPoint(float x, float y)
: m_x(x)
, m_y(y)
@ -112,6 +112,13 @@ public:
}
FloatPoint operator+(const FloatPoint& other) const { return { m_x + other.m_x, m_y + other.m_y }; }
FloatPoint& operator/=(float factor)
{
m_x /= factor;
m_y /= factor;
return *this;
}
String to_string() const { return String::format("[%g,%g]", x(), y()); }
bool is_null() const { return !m_x && !m_y; }

View file

@ -31,6 +31,7 @@
#include <AK/Assertions.h>
#include <AK/Function.h>
#include <AK/Memory.h>
#include <AK/QuickSort.h>
#include <AK/StdLibExtras.h>
#include <AK/StringBuilder.h>
#include <AK/Utf8View.h>
@ -1049,7 +1050,7 @@ void Painter::draw_line(const Point& p1, const Point& p2, Color color, int thick
}
}
static void draw_split_quadratic_bezier_curve(Painter& painter, const Point& original_control, const Point& p1, const Point& p2, Color color, int thickness, bool dotted)
static void split_quadratic_bezier_curve(const FloatPoint& original_control, const FloatPoint& p1, const FloatPoint& p2, Function<void(const FloatPoint&, const FloatPoint&)>& callback)
{
auto po1_midpoint = original_control + p1;
po1_midpoint /= 2;
@ -1060,11 +1061,11 @@ static void draw_split_quadratic_bezier_curve(Painter& painter, const Point& ori
auto new_segment = po1_midpoint + po2_midpoint;
new_segment /= 2;
painter.draw_quadratic_bezier_curve(po1_midpoint, p1, new_segment, color, thickness, dotted);
painter.draw_quadratic_bezier_curve(po2_midpoint, new_segment, p2, color, thickness, dotted);
Painter::for_each_line_segment_on_bezier_curve(po1_midpoint, p1, new_segment, callback);
Painter::for_each_line_segment_on_bezier_curve(po2_midpoint, new_segment, p2, callback);
}
static bool can_approximate_bezier_curve(const Point& p1, const Point& p2, const Point& control)
static bool can_approximate_bezier_curve(const FloatPoint& p1, const FloatPoint& p2, const FloatPoint& control)
{
constexpr static int tolerance = 15;
@ -1081,15 +1082,27 @@ static bool can_approximate_bezier_curve(const Point& p1, const Point& p2, const
return max(p1x, p2x) + max(p1y, p2y) <= tolerance;
}
void Painter::draw_quadratic_bezier_curve(const Point& control_point, const Point& p1, const Point& p2, Color color, int thickness, bool dotted)
void Painter::for_each_line_segment_on_bezier_curve(const FloatPoint& control_point, const FloatPoint& p1, const FloatPoint& p2, Function<void(const FloatPoint&, const FloatPoint&)>& callback)
{
if (can_approximate_bezier_curve(p1, p2, control_point)) {
draw_line(p1, p2, color, thickness, dotted);
callback(p1, p2);
} else {
draw_split_quadratic_bezier_curve(*this, control_point, p1, p2, color, thickness, dotted);
split_quadratic_bezier_curve(control_point, p1, p2, callback);
}
}
void Painter::for_each_line_segment_on_bezier_curve(const FloatPoint& control_point, const FloatPoint& p1, const FloatPoint& p2, Function<void(const FloatPoint&, const FloatPoint&)>&& callback)
{
for_each_line_segment_on_bezier_curve(control_point, p1, p2, callback);
}
void Painter::draw_quadratic_bezier_curve(const Point& control_point, const Point& p1, const Point& p2, Color color, int thickness, bool dotted)
{
for_each_line_segment_on_bezier_curve(FloatPoint(control_point.x(), control_point.y()), FloatPoint(p1.x(), p1.y()), FloatPoint(p2.x(), p2.y()), [&](const FloatPoint& p1, const FloatPoint& p2) {
draw_line(Point(p1.x(), p1.y()), Point(p2.x(), p2.y()), color, thickness, dotted);
});
}
void Painter::add_clip_rect(const Rect& rect)
{
state().clip_rect.intersect(rect.translated(m_clip_origin.location()));
@ -1137,4 +1150,156 @@ void Painter::stroke_path(const Path& path, Color color, int thickness)
}
}
//#define FILL_PATH_DEBUG
void Painter::fill_path(Path& path, Color color, WindingRule winding_rule)
{
const auto& segments = path.split_lines();
if (segments.size() == 0)
return;
Vector<Path::LineSegment> active_list;
active_list.ensure_capacity(segments.size());
// first, grab the segments for the very first scanline
auto first_y = segments.first().maximum_y;
auto last_y = segments.last().minimum_y;
auto scanline = first_y;
size_t last_active_segment { 0 };
for (auto& segment : segments) {
if (segment.maximum_y != scanline)
break;
active_list.append(segment);
++last_active_segment;
}
auto is_inside_shape = [winding_rule](int winding_number) {
if (winding_rule == WindingRule::Nonzero)
return winding_number != 0;
if (winding_rule == WindingRule::EvenOdd)
return winding_number % 2 == 0;
ASSERT_NOT_REACHED();
};
auto increment_winding = [winding_rule](int& winding_number, const Point& from, const Point& to) {
if (winding_rule == WindingRule::EvenOdd) {
++winding_number;
return;
}
if (winding_rule == WindingRule::Nonzero) {
if (from.dy_relative_to(to) < 0)
++winding_number;
else
--winding_number;
return;
}
ASSERT_NOT_REACHED();
};
while (scanline >= last_y) {
if (active_list.size()) {
// sort the active list by 'x' from right to left
quick_sort(active_list, [](const auto& line0, const auto& line1) {
return line1.x < line0.x;
});
#ifdef FILL_PATH_DEBUG
if ((int)scanline % 10 == 0) {
draw_text(Rect(active_list.last().x - 20, scanline, 20, 10), String::format("%d", (int)scanline));
}
#endif
if (active_list.size() > 1) {
auto winding_number { 0 };
for (size_t i = 1; i < active_list.size(); ++i) {
auto& previous = active_list[i - 1];
auto& current = active_list[i];
int int_distance = fabs(current.x - previous.x);
Point from(previous.x, scanline);
Point to(current.x, scanline);
if (int_distance < 1) {
// the two lines intersect on an int grid
// so they should both be treated as a single line segment
goto skip_drawing;
}
if (int_distance == 1 && is_inside_shape(winding_number)) {
// The two lines form a singluar edge for the shape
// while they do not intersect, they connect together
goto skip_drawing;
}
if (is_inside_shape(winding_number)) {
// The points between this segment and the previous are
// inside the shape
#ifdef FILL_PATH_DEBUG
dbg() << "y=" << scanline << ": " << winding_number << " at " << i << ": " << from << " -- " << to;
#endif
draw_line(from, to, color, 1, false);
}
skip_drawing:;
auto is_passing_through_maxima = scanline == previous.maximum_y
|| scanline == previous.minimum_y
|| scanline == current.maximum_y
|| scanline == current.minimum_y;
auto is_passing_through_vertex = false;
if (is_passing_through_maxima) {
is_passing_through_vertex = previous.x == current.x;
}
if (!is_passing_through_vertex || previous.inverse_slope * current.inverse_slope < 0)
increment_winding(winding_number, from, to);
// update the x coord
active_list[i - 1].x -= active_list[i - 1].inverse_slope;
}
active_list.last().x -= active_list.last().inverse_slope;
} else {
auto point = Point(active_list[0].x, scanline);
draw_line(point, point, color);
// update the x coord
active_list.first().x -= active_list.first().inverse_slope;
}
}
--scanline;
// remove any edge that goes out of bound from the active list
for (size_t i = 0, count = active_list.size(); i < count; ++i) {
if (scanline <= active_list[i].minimum_y) {
active_list.remove(i);
--count;
--i;
}
}
for (size_t j = last_active_segment; j < segments.size(); ++j, ++last_active_segment) {
auto& segment = segments[j];
if (segment.maximum_y < scanline)
break;
if (segment.minimum_y >= scanline)
continue;
active_list.append(segment);
}
}
#ifdef FILL_PATH_DEBUG
size_t i { 0 };
for (auto& segment : segments)
draw_line(segment.from, segment.to, Color::from_hsv(++i / segments.size() * 255, 255, 255), 1);
#endif
}
}

View file

@ -71,8 +71,17 @@ public:
void draw_emoji(const Point&, const Gfx::Bitmap&, const Font&);
void draw_glyph_or_emoji(const Point&, u32 codepoint, const Font&, Color);
static void for_each_line_segment_on_bezier_curve(const FloatPoint& control_point, const FloatPoint& p1, const FloatPoint& p2, Function<void(const FloatPoint&, const FloatPoint&)>&);
static void for_each_line_segment_on_bezier_curve(const FloatPoint& control_point, const FloatPoint& p1, const FloatPoint& p2, Function<void(const FloatPoint&, const FloatPoint&)>&&);
void stroke_path(const Path&, Color, int thickness);
enum class WindingRule {
Nonzero,
EvenOdd,
};
void fill_path(Path&, Color, WindingRule rule = WindingRule::Nonzero);
const Font& font() const { return *state().font; }
void set_font(const Font& font) { state().font = &font; }

View file

@ -24,7 +24,10 @@
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <AK/Function.h>
#include <AK/QuickSort.h>
#include <AK/StringBuilder.h>
#include <LibGfx/Painter.h>
#include <LibGfx/Path.h>
namespace Gfx {
@ -34,6 +37,8 @@ void Path::close()
if (m_segments.size() <= 1)
return;
invalidate_split_lines();
auto& last_point = m_segments.last().point;
for (ssize_t i = m_segments.size() - 1; i >= 0; --i) {
@ -80,4 +85,60 @@ String Path::to_string() const
return builder.to_string();
}
void Path::segmentize_path()
{
Vector<LineSegment> segments;
auto add_line = [&](const auto& p0, const auto& p1) {
if (p0.y() == p1.y())
return; // horizontal lines are not needed (there's nothing to fill inside)
float ymax = p0.y(), ymin = p1.y(), x_of_ymin = p1.x(), x_of_ymax = p0.x();
auto slope = p0.x() == p1.x() ? 0 : ((float)(p0.y() - p1.y())) / ((float)(p0.x() - p1.x()));
if (p0.y() < p1.y()) {
ymin = ymax;
ymax = p1.y();
x_of_ymax = x_of_ymin;
x_of_ymin = p0.x();
}
segments.append({ Point(p0.x(), p0.y()),
Point(p1.x(), p1.y()),
slope == 0 ? 0 : 1 / slope,
x_of_ymin,
ymax, ymin, x_of_ymax });
};
FloatPoint cursor { 0, 0 };
for (auto& segment : m_segments) {
switch (segment.type) {
case Segment::Type::MoveTo:
cursor = segment.point;
break;
case Segment::Type::LineTo: {
add_line(cursor, segment.point);
cursor = segment.point;
break;
}
case Segment::Type::QuadraticBezierCurveTo: {
auto& control = segment.through.value();
Painter::for_each_line_segment_on_bezier_curve(control, cursor, segment.point, [&](const FloatPoint& p0, const FloatPoint& p1) {
add_line(Point(p0.x(), p0.y()), Point(p1.x(), p1.y()));
});
cursor = segment.point;
break;
}
case Segment::Type::Invalid:
ASSERT_NOT_REACHED();
break;
}
}
// sort segments by ymax
quick_sort(segments, [](const auto& line0, const auto& line1) {
return line1.maximum_y < line0.maximum_y;
});
m_split_lines = move(segments);
}
}

View file

@ -57,21 +57,45 @@ public:
void line_to(const FloatPoint& point)
{
m_segments.append({ Segment::Type::LineTo, point });
invalidate_split_lines();
}
void quadratic_bezier_curve_to(const FloatPoint& through, const FloatPoint& point)
{
m_segments.append({ Segment::Type::QuadraticBezierCurveTo, point, through });
invalidate_split_lines();
}
void close();
const Vector<Segment>& segments() const { return m_segments; }
const auto& split_lines()
{
if (m_split_lines.has_value())
return m_split_lines.value();
segmentize_path();
ASSERT(m_split_lines.has_value());
return m_split_lines.value();
}
String to_string() const;
struct LineSegment {
Point from, to;
float inverse_slope;
float x_of_minimum_y;
float maximum_y;
float minimum_y;
float x;
};
private:
void invalidate_split_lines() { m_split_lines.clear(); }
void segmentize_path();
Vector<Segment> m_segments;
Optional<Vector<LineSegment>> m_split_lines {};
};
inline const LogStream& operator<<(const LogStream& stream, const Path& path)