diff --git a/Userland/Libraries/LibGfx/Path.cpp b/Userland/Libraries/LibGfx/Path.cpp index ed94af9cb9..697b77510a 100644 --- a/Userland/Libraries/LibGfx/Path.cpp +++ b/Userland/Libraries/LibGfx/Path.cpp @@ -365,4 +365,164 @@ void Path::add_path(Path const& other) invalidate_split_lines(); } +template +struct RoundTrip { + RoundTrip(ReadonlySpan span) + : m_span(span) + { + } + + size_t size() const + { + return m_span.size() * 2 - 1; + } + + T const& operator[](size_t index) const + { + // Follow the path: + if (index < m_span.size()) + return m_span[index]; + // Then in reverse: + if (index < size()) + return m_span[size() - index - 1]; + // Then wrap around again: + return m_span[index - size() + 1]; + } + +private: + ReadonlySpan m_span; +}; + +Path Path::stroke_to_fill(float thickness) const +{ + // Note: This convolves a polygon with the path using the algorithm described + // in https://keithp.com/~keithp/talks/cairo2003.pdf (3.1 Stroking Splines via Convolution) + + auto& lines = split_lines(); + if (lines.is_empty()) + return Path {}; + + // Paths can be disconnected, which a pain to deal with, so split it up. + Vector> segments; + segments.append({ lines.first().a() }); + for (auto& line : lines) { + if (line.a() == segments.last().last()) { + segments.last().append(line.b()); + } else { + segments.append({ line.a(), line.b() }); + } + } + + // Note: This is the same as the tolerance from bezier curve splitting. + constexpr auto flatness = 0.015f; + auto pen_vertex_count = max( + static_cast(ceilf(AK::Pi / acosf(1 - (2 * flatness) / thickness))), 4); + if (pen_vertex_count % 2 == 1) + pen_vertex_count += 1; + + Vector pen_vertices; + pen_vertices.ensure_capacity(pen_vertex_count); + + // Generate vertices for the pen (going counterclockwise). The pen does not necessarily need + // to be a circle (or an approximation of one), but other shapes are untested. + float theta = 0; + float theta_delta = (AK::Pi * 2) / pen_vertex_count; + for (int i = 0; i < pen_vertex_count; i++) { + float sin_theta; + float cos_theta; + AK::sincos(theta, sin_theta, cos_theta); + pen_vertices.unchecked_append({ cos_theta * thickness / 2, sin_theta * thickness / 2 }); + theta -= theta_delta; + } + + auto wrapping_index = [](auto& vertices, auto index) { + return vertices[(index + vertices.size()) % vertices.size()]; + }; + + auto angle_between = [](auto p1, auto p2) { + auto delta = p2 - p1; + return atan2f(delta.y(), delta.x()); + }; + + struct ActiveRange { + float start; + float end; + + bool in_range(float angle) const + { + // Note: Since active ranges go counterclockwise start > end unless we wrap around at 180 degrees + return ((angle <= start && angle >= end) + || (start < end && angle <= start) + || (start < end && angle >= end)); + } + }; + + Vector active_ranges; + active_ranges.ensure_capacity(pen_vertices.size()); + for (auto i = 0; i < pen_vertex_count; i++) { + active_ranges.unchecked_append({ angle_between(wrapping_index(pen_vertices, i - 1), pen_vertices[i]), + angle_between(pen_vertices[i], wrapping_index(pen_vertices, i + 1)) }); + } + + auto clockwise = [](float current_angle, float target_angle) { + if (target_angle < 0) + target_angle += AK::Pi * 2; + if (current_angle < 0) + current_angle += AK::Pi * 2; + if (target_angle < current_angle) + target_angle += AK::Pi * 2; + return (target_angle - current_angle) <= AK::Pi; + }; + + Path convolution; + for (auto& segment : segments) { + RoundTrip shape { segment }; + + bool first = true; + auto add_vertex = [&](auto v) { + if (first) { + convolution.move_to(v); + first = false; + } else { + convolution.line_to(v); + } + }; + + auto shape_idx = 0u; + + auto slope = [&] { + return angle_between(shape[shape_idx], shape[shape_idx + 1]); + }; + + auto start_slope = slope(); + // Note: At least one range must be active. + auto active = *active_ranges.find_first_index_if([&](auto& range) { + return range.in_range(start_slope); + }); + + while (shape_idx < shape.size()) { + add_vertex(shape[shape_idx] + pen_vertices[active]); + auto slope_now = slope(); + auto range = active_ranges[active]; + if (range.in_range(slope_now)) { + shape_idx++; + } else { + if (clockwise(slope_now, range.end)) { + if (active == static_cast(pen_vertex_count - 1)) + active = 0; + else + active++; + } else { + if (active == 0) + active = pen_vertex_count - 1; + else + active--; + } + } + } + } + + return convolution; +} + } diff --git a/Userland/Libraries/LibGfx/Path.h b/Userland/Libraries/LibGfx/Path.h index 6c31c02e19..d4cf947f00 100644 --- a/Userland/Libraries/LibGfx/Path.h +++ b/Userland/Libraries/LibGfx/Path.h @@ -248,6 +248,8 @@ public: DeprecatedString to_deprecated_string() const; + Path stroke_to_fill(float thickness) const; + private: void invalidate_split_lines() {