mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-09-26 19:28:59 +00:00
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:
parent
14005f89a6
commit
8057542dea
Notes:
sideshowbarker
2024-07-17 10:54:57 +09:00
Author: https://github.com/MacDue
Commit: 8057542dea
Pull-request: https://github.com/SerenityOS/serenity/pull/23618
9 changed files with 237 additions and 273 deletions
|
@ -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 {};
|
||||
};
|
||||
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue