LibGfx: Simplify path storage and tidy up APIs

Rather than make path segments virtual and refcounted let's store
`Gfx::Path`s as a list of `FloatPoints` and a separate list of commands.

This reduces the size of paths, for example, a `MoveTo` goes from 24
bytes to 9 bytes (one point + a single byte command), and removes a
layer of indirection when accessing segments. A nice little bonus is
transforming a path can now be done by applying the transform to all
points in the path (without looking at the commands).

Alongside this there's been a few minor API changes:

- `path.segments()` has been removed
  * All current uses could be replaced by a new `path.is_empty()` API
  * There's also now an iterator for looping over `Gfx::Path` segments
- `path.add_path(other_path)` has been removed
  * This was a duplicate of `path.append_path(other_path)`
- `path.ensure_subpath(point)` has been removed
  * Had one use and is equivalent to an `is_empty()` check + `move_to()`
- `path.close()` and `path.close_all_subpaths()` assume an implicit
  `moveto 0,0` if there's no `moveto` at the start of a path (for
  consistency with `path.segmentize_path()`).

Only the last point could change behaviour (though in LibWeb/SVGs all
paths start with a `moveto` as per the spec, it's only possible to
construct a path without a starting `moveto` via LibGfx APIs).
This commit is contained in:
MacDue 2024-03-17 20:23:17 +00:00 committed by Andreas Kling
commit 8057542dea
Notes: sideshowbarker 2024-07-17 10:54:57 +09:00
9 changed files with 237 additions and 273 deletions

View file

@ -7,10 +7,8 @@
#pragma once
#include <AK/ByteString.h>
#include <AK/HashMap.h>
#include <AK/Optional.h>
#include <AK/Vector.h>
#include <LibGfx/Font/Font.h>
#include <LibGfx/Forward.h>
#include <LibGfx/Line.h>
#include <LibGfx/Point.h>
@ -18,91 +16,127 @@
namespace Gfx {
class Segment : public RefCounted<Segment> {
class Path;
class PathSegment {
public:
enum class Type {
Invalid,
enum Command : u8 {
MoveTo,
LineTo,
QuadraticBezierCurveTo,
CubicBezierCurveTo,
};
Segment(FloatPoint point)
: m_point(point)
ALWAYS_INLINE Command command() const { return m_command; }
ALWAYS_INLINE FloatPoint point() const { return m_points.last(); }
ALWAYS_INLINE FloatPoint through() const
{
VERIFY(m_command == Command::QuadraticBezierCurveTo);
return m_points[0];
}
ALWAYS_INLINE FloatPoint through_0() const
{
VERIFY(m_command == Command::CubicBezierCurveTo);
return m_points[0];
}
ALWAYS_INLINE FloatPoint through_1() const
{
VERIFY(m_command == Command::CubicBezierCurveTo);
return m_points[1];
}
virtual ~Segment() = default;
FloatPoint point() const { return m_point; }
virtual Type type() const = 0;
protected:
FloatPoint m_point;
};
class MoveSegment final : public Segment {
public:
MoveSegment(FloatPoint point)
: Segment(point)
static constexpr int points_per_command(Command command)
{
switch (command) {
case Command::MoveTo:
case Command::LineTo:
return 1; // Single point.
case Command::QuadraticBezierCurveTo:
return 2; // Control point + point.
case Command::CubicBezierCurveTo:
return 3; // Two control points + point.
}
VERIFY_NOT_REACHED();
}
PathSegment(Command command, ReadonlySpan<FloatPoint> points)
: m_command(command)
, m_points(points) {};
private:
virtual Type type() const override { return Segment::Type::MoveTo; }
Command m_command;
ReadonlySpan<FloatPoint> m_points;
};
class LineSegment final : public Segment {
class PathSegmentIterator {
public:
LineSegment(FloatPoint point)
: Segment(point)
int operator<=>(PathSegmentIterator other) const
{
if (m_command_index > other.m_command_index)
return 1;
if (m_command_index < other.m_command_index)
return -1;
return 0;
}
bool operator==(PathSegmentIterator other) const { return m_command_index == other.m_command_index; }
bool operator!=(PathSegmentIterator other) const { return m_command_index != other.m_command_index; }
PathSegmentIterator operator++()
{
if (m_command_index < m_commands.size())
m_point_index += PathSegment::points_per_command(m_commands[m_command_index++]);
return *this;
}
PathSegmentIterator operator++(int)
{
PathSegmentIterator old(*this);
++*this;
return old;
}
PathSegmentIterator operator--()
{
if (m_command_index > 0)
m_point_index -= PathSegment::points_per_command(m_commands[--m_command_index]);
return *this;
}
PathSegmentIterator operator--(int)
{
PathSegmentIterator old(*this);
--*this;
return old;
}
PathSegment operator*() const
{
auto command = m_commands[m_command_index];
return PathSegment { command, m_points.span().slice(m_point_index, PathSegment::points_per_command(command)) };
}
PathSegmentIterator& operator=(PathSegmentIterator const& other)
{
m_point_index = other.m_point_index;
m_command_index = other.m_command_index;
return *this;
}
PathSegmentIterator(PathSegmentIterator const&) = default;
friend Path;
private:
PathSegmentIterator(Vector<FloatPoint> const& points, Vector<PathSegment::Command> const& commands, size_t point_index = 0, size_t command_index = 0)
: m_points(points)
, m_commands(commands)
, m_point_index(point_index)
, m_command_index(command_index)
{
}
virtual ~LineSegment() override = default;
private:
virtual Type type() const override { return Segment::Type::LineTo; }
};
class QuadraticBezierCurveSegment final : public Segment {
public:
QuadraticBezierCurveSegment(FloatPoint point, FloatPoint through)
: Segment(point)
, m_through(through)
{
}
virtual ~QuadraticBezierCurveSegment() override = default;
FloatPoint through() const { return m_through; }
private:
virtual Type type() const override { return Segment::Type::QuadraticBezierCurveTo; }
FloatPoint m_through;
};
class CubicBezierCurveSegment final : public Segment {
public:
CubicBezierCurveSegment(FloatPoint point, FloatPoint through_0, FloatPoint through_1)
: Segment(point)
, m_through_0(through_0)
, m_through_1(through_1)
{
}
virtual ~CubicBezierCurveSegment() override = default;
FloatPoint through_0() const { return m_through_0; }
FloatPoint through_1() const { return m_through_1; }
private:
virtual Type type() const override { return Segment::Type::CubicBezierCurveTo; }
FloatPoint m_through_0;
FloatPoint m_through_1;
// Note: Store reference to vectors from Gfx::Path so appending segments does not invalidate iterators.
Vector<FloatPoint> const& m_points;
Vector<PathSegment::Command> const& m_commands;
size_t m_point_index { 0 };
size_t m_command_index { 0 };
};
class Path {
@ -111,40 +145,34 @@ public:
void move_to(FloatPoint point)
{
append_segment<MoveSegment>(point);
append_segment<PathSegment::MoveTo>(point);
}
void line_to(FloatPoint point)
{
append_segment<LineSegment>(point);
append_segment<PathSegment::LineTo>(point);
invalidate_split_lines();
}
void horizontal_line_to(float x)
{
float previous_y = 0;
if (!m_segments.is_empty())
previous_y = m_segments.last()->point().y();
line_to({ x, previous_y });
line_to({ x, last_point().y() });
}
void vertical_line_to(float y)
{
float previous_x = 0;
if (!m_segments.is_empty())
previous_x = m_segments.last()->point().x();
line_to({ previous_x, y });
line_to({ last_point().x(), y });
}
void quadratic_bezier_curve_to(FloatPoint through, FloatPoint point)
{
append_segment<QuadraticBezierCurveSegment>(point, through);
append_segment<PathSegment::QuadraticBezierCurveTo>(point, through);
invalidate_split_lines();
}
void cubic_bezier_curve_to(FloatPoint c1, FloatPoint c2, FloatPoint p2)
{
append_segment<CubicBezierCurveSegment>(p2, c1, c2);
append_segment<PathSegment::CubicBezierCurveTo>(p2, c1, c2);
invalidate_split_lines();
}
@ -156,75 +184,95 @@ public:
void text(Utf8View, Font const&);
FloatPoint last_point();
FloatPoint last_point()
{
if (!m_points.is_empty())
return m_points.last();
return {};
}
void close();
void close_all_subpaths();
Vector<NonnullRefPtr<Segment const>> const& segments() const { return m_segments; }
Path stroke_to_fill(float thickness) const;
auto& split_lines() const
Path place_text_along(Utf8View text, Font const&) const;
Path copy_transformed(AffineTransform const&) const;
ReadonlySpan<FloatLine> split_lines() const
{
if (!m_split_lines.has_value()) {
const_cast<Path*>(this)->segmentize_path();
VERIFY(m_split_lines.has_value());
}
return m_split_lines.value();
}
void clear()
{
m_segments.clear();
m_split_lines.clear();
return m_split_lines->lines;
}
Gfx::FloatRect const& bounding_box() const
{
if (!m_bounding_box.has_value()) {
const_cast<Path*>(this)->segmentize_path();
VERIFY(m_bounding_box.has_value());
}
return m_bounding_box.value();
(void)split_lines();
return m_split_lines->bounding_box;
}
void append_path(Path const& path)
{
m_segments.ensure_capacity(m_segments.size() + path.m_segments.size());
for (auto const& segment : path.m_segments)
m_segments.unchecked_append(segment);
m_commands.extend(path.m_commands);
m_points.extend(path.m_points);
invalidate_split_lines();
}
Path copy_transformed(AffineTransform const&) const;
void add_path(Path const&);
void ensure_subpath(FloatPoint point);
ByteString to_byte_string() const;
Path stroke_to_fill(float thickness) const;
PathSegmentIterator begin() const
{
return PathSegmentIterator(m_points, m_commands);
}
Path place_text_along(Utf8View text, Font const&) const;
PathSegmentIterator end() const
{
return PathSegmentIterator(m_points, m_commands, m_points.size(), m_commands.size());
}
bool is_empty() const
{
return m_commands.is_empty();
}
void clear()
{
*this = Path {};
}
private:
void approximate_elliptical_arc_with_cubic_beziers(FloatPoint center, FloatSize radii, float x_axis_rotation, float theta, float theta_delta);
void invalidate_split_lines()
{
m_bounding_box.clear();
m_split_lines.clear();
}
void segmentize_path();
template<typename T, typename... Args>
void append_segment(Args&&... args)
template<PathSegment::Command command, typename... Args>
void append_segment(FloatPoint point, Args&&... args)
{
m_segments.append(adopt_ref(*new T(forward<Args>(args)...)));
constexpr auto point_count = sizeof...(Args) + 1;
static_assert(point_count == PathSegment::points_per_command(command));
m_commands.append(command);
// Place the current path point after any extra control points so `m_points.last()` is always the last point.
FloatPoint points[] { args..., point };
m_points.append(points, point_count);
}
Vector<NonnullRefPtr<Segment const>> m_segments {};
Vector<FloatPoint> m_points {};
Vector<PathSegment::Command> m_commands {};
Optional<Vector<FloatLine>> m_split_lines {};
Optional<Gfx::FloatRect> m_bounding_box;
bool m_need_new_subpath = { true };
struct SplitLines {
Vector<FloatLine> lines;
Gfx::FloatRect bounding_box;
};
Optional<SplitLines> m_split_lines {};
};
}