mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-09-24 02:08:58 +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
|
@ -144,7 +144,7 @@ void EdgeFlagPathRasterizer<SamplesPerPixel>::fill_internal(Painter& painter, Pa
|
||||||
if (m_clip.is_empty())
|
if (m_clip.is_empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
auto& lines = path.split_lines();
|
auto lines = path.split_lines();
|
||||||
if (lines.is_empty())
|
if (lines.is_empty())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <AK/HashMap.h>
|
||||||
#include <AK/Noncopyable.h>
|
#include <AK/Noncopyable.h>
|
||||||
#include <AK/RefCounted.h>
|
#include <AK/RefCounted.h>
|
||||||
#include <LibGfx/Font/Font.h>
|
#include <LibGfx/Font/Font.h>
|
||||||
|
|
|
@ -279,10 +279,10 @@ public:
|
||||||
path.line_to(TRY(read_point()));
|
path.line_to(TRY(read_point()));
|
||||||
break;
|
break;
|
||||||
case PathCommand::HorizontalLine:
|
case PathCommand::HorizontalLine:
|
||||||
path.line_to({ TRY(read_unit()), path.segments().last()->point().y() });
|
path.line_to({ TRY(read_unit()), path.last_point().y() });
|
||||||
break;
|
break;
|
||||||
case PathCommand::VerticalLine:
|
case PathCommand::VerticalLine:
|
||||||
path.line_to({ path.segments().last()->point().x(), TRY(read_unit()) });
|
path.line_to({ path.last_point().x(), TRY(read_unit()) });
|
||||||
break;
|
break;
|
||||||
case PathCommand::CubicBezier: {
|
case PathCommand::CubicBezier: {
|
||||||
auto control_0 = TRY(read_point());
|
auto control_0 = TRY(read_point());
|
||||||
|
|
|
@ -4,10 +4,7 @@
|
||||||
* SPDX-License-Identifier: BSD-2-Clause
|
* SPDX-License-Identifier: BSD-2-Clause
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include <AK/Function.h>
|
|
||||||
#include <AK/HashTable.h>
|
|
||||||
#include <AK/Math.h>
|
#include <AK/Math.h>
|
||||||
#include <AK/QuickSort.h>
|
|
||||||
#include <AK/StringBuilder.h>
|
#include <AK/StringBuilder.h>
|
||||||
#include <AK/TypeCasts.h>
|
#include <AK/TypeCasts.h>
|
||||||
#include <LibGfx/BoundingBox.h>
|
#include <LibGfx/BoundingBox.h>
|
||||||
|
@ -80,7 +77,7 @@ void Path::elliptical_arc_to(FloatPoint point, FloatSize radii, float x_axis_rot
|
||||||
|
|
||||||
// Step 1 of out-of-range radii correction
|
// Step 1 of out-of-range radii correction
|
||||||
if (rx == 0.0 || ry == 0.0) {
|
if (rx == 0.0 || ry == 0.0) {
|
||||||
append_segment<LineSegment>(next_point);
|
append_segment<PathSegment::LineTo>(next_point);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,7 +189,7 @@ Path Path::place_text_along(Utf8View text, Font const& font) const
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
auto& lines = split_lines();
|
auto lines = split_lines();
|
||||||
auto next_point_for_offset = [&, line_index = 0U, distance_along_path = 0.0f, last_line_length = 0.0f](float offset) mutable -> Optional<FloatPoint> {
|
auto next_point_for_offset = [&, line_index = 0U, distance_along_path = 0.0f, last_line_length = 0.0f](float offset) mutable -> Optional<FloatPoint> {
|
||||||
while (line_index < lines.size() && offset > distance_along_path) {
|
while (line_index < lines.size() && offset > distance_along_path) {
|
||||||
last_line_length = lines[line_index++].length();
|
last_line_length = lines[line_index++].length();
|
||||||
|
@ -247,118 +244,79 @@ Path Path::place_text_along(Utf8View text, Font const& font) const
|
||||||
return result_path;
|
return result_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
FloatPoint Path::last_point()
|
|
||||||
{
|
|
||||||
FloatPoint last_point { 0, 0 };
|
|
||||||
if (!m_segments.is_empty())
|
|
||||||
last_point = m_segments.last()->point();
|
|
||||||
return last_point;
|
|
||||||
}
|
|
||||||
|
|
||||||
void Path::close()
|
void Path::close()
|
||||||
{
|
{
|
||||||
if (m_segments.size() <= 1)
|
// If there's no `moveto` starting this subpath assume the start is (0, 0).
|
||||||
return;
|
FloatPoint first_point_in_subpath = { 0, 0 };
|
||||||
|
for (auto it = end(); it-- != begin();) {
|
||||||
auto last_point = m_segments.last()->point();
|
auto segment = *it;
|
||||||
|
if (segment.command() == PathSegment::MoveTo) {
|
||||||
for (ssize_t i = m_segments.size() - 1; i >= 0; --i) {
|
first_point_in_subpath = segment.point();
|
||||||
auto& segment = m_segments[i];
|
break;
|
||||||
if (segment->type() == Segment::Type::MoveTo) {
|
|
||||||
if (last_point == segment->point())
|
|
||||||
return;
|
|
||||||
append_segment<LineSegment>(segment->point());
|
|
||||||
invalidate_split_lines();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (first_point_in_subpath != last_point())
|
||||||
|
line_to(first_point_in_subpath);
|
||||||
}
|
}
|
||||||
|
|
||||||
void Path::close_all_subpaths()
|
void Path::close_all_subpaths()
|
||||||
{
|
{
|
||||||
if (m_segments.size() <= 1)
|
auto it = begin();
|
||||||
return;
|
// Note: Get the end outside the loop as closing subpaths will move the end.
|
||||||
|
auto end = this->end();
|
||||||
invalidate_split_lines();
|
while (it < end) {
|
||||||
|
// If there's no `moveto` starting this subpath assume the start is (0, 0).
|
||||||
Optional<FloatPoint> cursor, start_of_subpath;
|
FloatPoint first_point_in_subpath = { 0, 0 };
|
||||||
bool is_first_point_in_subpath { false };
|
auto segment = *it;
|
||||||
|
if (segment.command() == PathSegment::MoveTo) {
|
||||||
auto close_previous_subpath = [&] {
|
first_point_in_subpath = segment.point();
|
||||||
if (cursor.has_value() && !is_first_point_in_subpath) {
|
++it;
|
||||||
// This is a move from a subpath to another
|
|
||||||
// connect the two ends of this subpath before
|
|
||||||
// moving on to the next one
|
|
||||||
VERIFY(start_of_subpath.has_value());
|
|
||||||
|
|
||||||
append_segment<MoveSegment>(cursor.value());
|
|
||||||
append_segment<LineSegment>(start_of_subpath.value());
|
|
||||||
}
|
}
|
||||||
};
|
// Find the end of the current subpath.
|
||||||
|
FloatPoint cursor = first_point_in_subpath;
|
||||||
auto segment_count = m_segments.size();
|
while (it < end) {
|
||||||
for (size_t i = 0; i < segment_count; i++) {
|
auto segment = *it;
|
||||||
// Note: We need to use m_segments[i] as append_segment() may invalidate any references.
|
if (segment.command() == PathSegment::MoveTo)
|
||||||
switch (m_segments[i]->type()) {
|
break;
|
||||||
case Segment::Type::MoveTo: {
|
cursor = segment.point();
|
||||||
close_previous_subpath();
|
++it;
|
||||||
is_first_point_in_subpath = true;
|
|
||||||
cursor = m_segments[i]->point();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case Segment::Type::LineTo:
|
// Close the subpath.
|
||||||
case Segment::Type::QuadraticBezierCurveTo:
|
if (first_point_in_subpath != cursor) {
|
||||||
case Segment::Type::CubicBezierCurveTo:
|
move_to(cursor);
|
||||||
if (is_first_point_in_subpath) {
|
line_to(first_point_in_subpath);
|
||||||
start_of_subpath = cursor;
|
|
||||||
is_first_point_in_subpath = false;
|
|
||||||
}
|
|
||||||
cursor = m_segments[i]->point();
|
|
||||||
break;
|
|
||||||
case Segment::Type::Invalid:
|
|
||||||
VERIFY_NOT_REACHED();
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (m_segments.last()->type() != Segment::Type::MoveTo)
|
|
||||||
close_previous_subpath();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ByteString Path::to_byte_string() const
|
ByteString Path::to_byte_string() const
|
||||||
{
|
{
|
||||||
StringBuilder builder;
|
StringBuilder builder;
|
||||||
builder.append("Path { "sv);
|
builder.append("Path { "sv);
|
||||||
for (auto& segment : m_segments) {
|
for (auto segment : *this) {
|
||||||
switch (segment->type()) {
|
switch (segment.command()) {
|
||||||
case Segment::Type::MoveTo:
|
case PathSegment::MoveTo:
|
||||||
builder.append("MoveTo"sv);
|
builder.append("MoveTo"sv);
|
||||||
break;
|
break;
|
||||||
case Segment::Type::LineTo:
|
case PathSegment::LineTo:
|
||||||
builder.append("LineTo"sv);
|
builder.append("LineTo"sv);
|
||||||
break;
|
break;
|
||||||
case Segment::Type::QuadraticBezierCurveTo:
|
case PathSegment::QuadraticBezierCurveTo:
|
||||||
builder.append("QuadraticBezierCurveTo"sv);
|
builder.append("QuadraticBezierCurveTo"sv);
|
||||||
break;
|
break;
|
||||||
case Segment::Type::CubicBezierCurveTo:
|
case PathSegment::CubicBezierCurveTo:
|
||||||
builder.append("CubicBezierCurveTo"sv);
|
builder.append("CubicBezierCurveTo"sv);
|
||||||
break;
|
break;
|
||||||
case Segment::Type::Invalid:
|
|
||||||
builder.append("Invalid"sv);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
builder.appendff("({}", segment->point());
|
builder.appendff("({}", segment.point());
|
||||||
|
|
||||||
switch (segment->type()) {
|
switch (segment.command()) {
|
||||||
case Segment::Type::QuadraticBezierCurveTo:
|
case PathSegment::QuadraticBezierCurveTo:
|
||||||
builder.append(", "sv);
|
builder.appendff(", {}"sv, segment.through());
|
||||||
builder.append(static_cast<QuadraticBezierCurveSegment const&>(*segment).through().to_byte_string());
|
|
||||||
break;
|
break;
|
||||||
case Segment::Type::CubicBezierCurveTo:
|
case PathSegment::CubicBezierCurveTo:
|
||||||
builder.append(", "sv);
|
builder.appendff(", {}"sv, segment.through_0());
|
||||||
builder.append(static_cast<CubicBezierCurveSegment const&>(*segment).through_0().to_byte_string());
|
builder.appendff(", {}"sv, segment.through_1());
|
||||||
builder.append(", "sv);
|
|
||||||
builder.append(static_cast<CubicBezierCurveSegment const&>(*segment).through_1().to_byte_string());
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
@ -381,88 +339,44 @@ void Path::segmentize_path()
|
||||||
};
|
};
|
||||||
|
|
||||||
FloatPoint cursor { 0, 0 };
|
FloatPoint cursor { 0, 0 };
|
||||||
for (auto& segment : m_segments) {
|
for (auto segment : *this) {
|
||||||
switch (segment->type()) {
|
switch (segment.command()) {
|
||||||
case Segment::Type::MoveTo:
|
case PathSegment::MoveTo:
|
||||||
bounding_box.add_point(segment->point());
|
bounding_box.add_point(segment.point());
|
||||||
cursor = segment->point();
|
|
||||||
break;
|
break;
|
||||||
case Segment::Type::LineTo: {
|
case PathSegment::LineTo: {
|
||||||
add_line(cursor, segment->point());
|
add_line(cursor, segment.point());
|
||||||
cursor = segment->point();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Segment::Type::QuadraticBezierCurveTo: {
|
case PathSegment::QuadraticBezierCurveTo: {
|
||||||
auto control = static_cast<QuadraticBezierCurveSegment const&>(*segment).through();
|
Painter::for_each_line_segment_on_bezier_curve(segment.through(), cursor, segment.point(), [&](FloatPoint p0, FloatPoint p1) {
|
||||||
Painter::for_each_line_segment_on_bezier_curve(control, cursor, segment->point(), [&](FloatPoint p0, FloatPoint p1) {
|
|
||||||
add_line(p0, p1);
|
add_line(p0, p1);
|
||||||
});
|
});
|
||||||
cursor = segment->point();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Segment::Type::CubicBezierCurveTo: {
|
case PathSegment::CubicBezierCurveTo: {
|
||||||
auto& curve = static_cast<CubicBezierCurveSegment const&>(*segment);
|
Painter::for_each_line_segment_on_cubic_bezier_curve(segment.through_0(), segment.through_1(), cursor, segment.point(), [&](FloatPoint p0, FloatPoint p1) {
|
||||||
auto control_0 = curve.through_0();
|
|
||||||
auto control_1 = curve.through_1();
|
|
||||||
Painter::for_each_line_segment_on_cubic_bezier_curve(control_0, control_1, cursor, segment->point(), [&](FloatPoint p0, FloatPoint p1) {
|
|
||||||
add_line(p0, p1);
|
add_line(p0, p1);
|
||||||
});
|
});
|
||||||
cursor = segment->point();
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Segment::Type::Invalid:
|
|
||||||
VERIFY_NOT_REACHED();
|
|
||||||
}
|
}
|
||||||
|
cursor = segment.point();
|
||||||
}
|
}
|
||||||
|
|
||||||
m_split_lines = move(segments);
|
m_split_lines = SplitLines { move(segments), bounding_box };
|
||||||
m_bounding_box = bounding_box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Path Path::copy_transformed(Gfx::AffineTransform const& transform) const
|
Path Path::copy_transformed(Gfx::AffineTransform const& transform) const
|
||||||
{
|
{
|
||||||
Path result;
|
Path result;
|
||||||
|
result.m_commands = m_commands;
|
||||||
for (auto const& segment : m_segments) {
|
result.m_points.ensure_capacity(m_points.size());
|
||||||
switch (segment->type()) {
|
for (auto point : m_points)
|
||||||
case Segment::Type::MoveTo:
|
result.m_points.unchecked_append(transform.map(point));
|
||||||
result.move_to(transform.map(segment->point()));
|
|
||||||
break;
|
|
||||||
case Segment::Type::LineTo: {
|
|
||||||
result.line_to(transform.map(segment->point()));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Segment::Type::QuadraticBezierCurveTo: {
|
|
||||||
auto const& quadratic_segment = static_cast<QuadraticBezierCurveSegment const&>(*segment);
|
|
||||||
result.quadratic_bezier_curve_to(transform.map(quadratic_segment.through()), transform.map(segment->point()));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Segment::Type::CubicBezierCurveTo: {
|
|
||||||
auto const& cubic_segment = static_cast<CubicBezierCurveSegment const&>(*segment);
|
|
||||||
result.cubic_bezier_curve_to(transform.map(cubic_segment.through_0()), transform.map(cubic_segment.through_1()), transform.map(segment->point()));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case Segment::Type::Invalid:
|
|
||||||
VERIFY_NOT_REACHED();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Path::add_path(Path const& other)
|
|
||||||
{
|
|
||||||
m_segments.extend(other.m_segments);
|
|
||||||
invalidate_split_lines();
|
|
||||||
}
|
|
||||||
|
|
||||||
void Path::ensure_subpath(FloatPoint point)
|
|
||||||
{
|
|
||||||
if (m_need_new_subpath && m_segments.is_empty()) {
|
|
||||||
move_to(point);
|
|
||||||
m_need_new_subpath = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
template<typename T>
|
template<typename T>
|
||||||
struct RoundTrip {
|
struct RoundTrip {
|
||||||
RoundTrip(ReadonlySpan<T> span)
|
RoundTrip(ReadonlySpan<T> span)
|
||||||
|
@ -498,7 +412,7 @@ Path Path::stroke_to_fill(float thickness) const
|
||||||
|
|
||||||
VERIFY(thickness > 0);
|
VERIFY(thickness > 0);
|
||||||
|
|
||||||
auto& lines = split_lines();
|
auto lines = split_lines();
|
||||||
if (lines.is_empty())
|
if (lines.is_empty())
|
||||||
return Path {};
|
return Path {};
|
||||||
|
|
||||||
|
|
|
@ -7,10 +7,8 @@
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <AK/ByteString.h>
|
#include <AK/ByteString.h>
|
||||||
#include <AK/HashMap.h>
|
|
||||||
#include <AK/Optional.h>
|
#include <AK/Optional.h>
|
||||||
#include <AK/Vector.h>
|
#include <AK/Vector.h>
|
||||||
#include <LibGfx/Font/Font.h>
|
|
||||||
#include <LibGfx/Forward.h>
|
#include <LibGfx/Forward.h>
|
||||||
#include <LibGfx/Line.h>
|
#include <LibGfx/Line.h>
|
||||||
#include <LibGfx/Point.h>
|
#include <LibGfx/Point.h>
|
||||||
|
@ -18,91 +16,127 @@
|
||||||
|
|
||||||
namespace Gfx {
|
namespace Gfx {
|
||||||
|
|
||||||
class Segment : public RefCounted<Segment> {
|
class Path;
|
||||||
|
|
||||||
|
class PathSegment {
|
||||||
public:
|
public:
|
||||||
enum class Type {
|
enum Command : u8 {
|
||||||
Invalid,
|
|
||||||
MoveTo,
|
MoveTo,
|
||||||
LineTo,
|
LineTo,
|
||||||
QuadraticBezierCurveTo,
|
QuadraticBezierCurveTo,
|
||||||
CubicBezierCurveTo,
|
CubicBezierCurveTo,
|
||||||
};
|
};
|
||||||
|
|
||||||
Segment(FloatPoint point)
|
ALWAYS_INLINE Command command() const { return m_command; }
|
||||||
: m_point(point)
|
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;
|
static constexpr int points_per_command(Command command)
|
||||||
|
|
||||||
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)
|
|
||||||
{
|
{
|
||||||
|
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:
|
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:
|
public:
|
||||||
LineSegment(FloatPoint point)
|
int operator<=>(PathSegmentIterator other) const
|
||||||
: Segment(point)
|
{
|
||||||
|
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;
|
// Note: Store reference to vectors from Gfx::Path so appending segments does not invalidate iterators.
|
||||||
|
Vector<FloatPoint> const& m_points;
|
||||||
private:
|
Vector<PathSegment::Command> const& m_commands;
|
||||||
virtual Type type() const override { return Segment::Type::LineTo; }
|
size_t m_point_index { 0 };
|
||||||
};
|
size_t m_command_index { 0 };
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class Path {
|
class Path {
|
||||||
|
@ -111,40 +145,34 @@ public:
|
||||||
|
|
||||||
void move_to(FloatPoint point)
|
void move_to(FloatPoint point)
|
||||||
{
|
{
|
||||||
append_segment<MoveSegment>(point);
|
append_segment<PathSegment::MoveTo>(point);
|
||||||
}
|
}
|
||||||
|
|
||||||
void line_to(FloatPoint point)
|
void line_to(FloatPoint point)
|
||||||
{
|
{
|
||||||
append_segment<LineSegment>(point);
|
append_segment<PathSegment::LineTo>(point);
|
||||||
invalidate_split_lines();
|
invalidate_split_lines();
|
||||||
}
|
}
|
||||||
|
|
||||||
void horizontal_line_to(float x)
|
void horizontal_line_to(float x)
|
||||||
{
|
{
|
||||||
float previous_y = 0;
|
line_to({ x, last_point().y() });
|
||||||
if (!m_segments.is_empty())
|
|
||||||
previous_y = m_segments.last()->point().y();
|
|
||||||
line_to({ x, previous_y });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void vertical_line_to(float y)
|
void vertical_line_to(float y)
|
||||||
{
|
{
|
||||||
float previous_x = 0;
|
line_to({ last_point().x(), y });
|
||||||
if (!m_segments.is_empty())
|
|
||||||
previous_x = m_segments.last()->point().x();
|
|
||||||
line_to({ previous_x, y });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void quadratic_bezier_curve_to(FloatPoint through, FloatPoint point)
|
void quadratic_bezier_curve_to(FloatPoint through, FloatPoint point)
|
||||||
{
|
{
|
||||||
append_segment<QuadraticBezierCurveSegment>(point, through);
|
append_segment<PathSegment::QuadraticBezierCurveTo>(point, through);
|
||||||
invalidate_split_lines();
|
invalidate_split_lines();
|
||||||
}
|
}
|
||||||
|
|
||||||
void cubic_bezier_curve_to(FloatPoint c1, FloatPoint c2, FloatPoint p2)
|
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();
|
invalidate_split_lines();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -156,75 +184,95 @@ public:
|
||||||
|
|
||||||
void text(Utf8View, Font const&);
|
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();
|
||||||
void close_all_subpaths();
|
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()) {
|
if (!m_split_lines.has_value()) {
|
||||||
const_cast<Path*>(this)->segmentize_path();
|
const_cast<Path*>(this)->segmentize_path();
|
||||||
VERIFY(m_split_lines.has_value());
|
VERIFY(m_split_lines.has_value());
|
||||||
}
|
}
|
||||||
return m_split_lines.value();
|
return m_split_lines->lines;
|
||||||
}
|
|
||||||
|
|
||||||
void clear()
|
|
||||||
{
|
|
||||||
m_segments.clear();
|
|
||||||
m_split_lines.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Gfx::FloatRect const& bounding_box() const
|
Gfx::FloatRect const& bounding_box() const
|
||||||
{
|
{
|
||||||
if (!m_bounding_box.has_value()) {
|
(void)split_lines();
|
||||||
const_cast<Path*>(this)->segmentize_path();
|
return m_split_lines->bounding_box;
|
||||||
VERIFY(m_bounding_box.has_value());
|
|
||||||
}
|
|
||||||
return m_bounding_box.value();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void append_path(Path const& path)
|
void append_path(Path const& path)
|
||||||
{
|
{
|
||||||
m_segments.ensure_capacity(m_segments.size() + path.m_segments.size());
|
m_commands.extend(path.m_commands);
|
||||||
for (auto const& segment : path.m_segments)
|
m_points.extend(path.m_points);
|
||||||
m_segments.unchecked_append(segment);
|
|
||||||
invalidate_split_lines();
|
invalidate_split_lines();
|
||||||
}
|
}
|
||||||
|
|
||||||
Path copy_transformed(AffineTransform const&) const;
|
|
||||||
void add_path(Path const&);
|
|
||||||
void ensure_subpath(FloatPoint point);
|
|
||||||
ByteString to_byte_string() const;
|
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:
|
private:
|
||||||
void approximate_elliptical_arc_with_cubic_beziers(FloatPoint center, FloatSize radii, float x_axis_rotation, float theta, float theta_delta);
|
void approximate_elliptical_arc_with_cubic_beziers(FloatPoint center, FloatSize radii, float x_axis_rotation, float theta, float theta_delta);
|
||||||
|
|
||||||
void invalidate_split_lines()
|
void invalidate_split_lines()
|
||||||
{
|
{
|
||||||
m_bounding_box.clear();
|
|
||||||
m_split_lines.clear();
|
m_split_lines.clear();
|
||||||
}
|
}
|
||||||
void segmentize_path();
|
void segmentize_path();
|
||||||
|
|
||||||
template<typename T, typename... Args>
|
template<PathSegment::Command command, typename... Args>
|
||||||
void append_segment(Args&&... 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 {};
|
struct SplitLines {
|
||||||
Optional<Gfx::FloatRect> m_bounding_box;
|
Vector<FloatLine> lines;
|
||||||
bool m_need_new_subpath = { true };
|
Gfx::FloatRect bounding_box;
|
||||||
|
};
|
||||||
|
|
||||||
|
Optional<SplitLines> m_split_lines {};
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -247,7 +247,7 @@ RENDERER_HANDLER(path_move)
|
||||||
|
|
||||||
RENDERER_HANDLER(path_line)
|
RENDERER_HANDLER(path_line)
|
||||||
{
|
{
|
||||||
VERIFY(!m_current_path.segments().is_empty());
|
VERIFY(!m_current_path.is_empty());
|
||||||
m_current_path.line_to(map(args[0].to_float(), args[1].to_float()));
|
m_current_path.line_to(map(args[0].to_float(), args[1].to_float()));
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
@ -265,8 +265,8 @@ RENDERER_HANDLER(path_cubic_bezier_curve)
|
||||||
RENDERER_HANDLER(path_cubic_bezier_curve_no_first_control)
|
RENDERER_HANDLER(path_cubic_bezier_curve_no_first_control)
|
||||||
{
|
{
|
||||||
VERIFY(args.size() == 4);
|
VERIFY(args.size() == 4);
|
||||||
VERIFY(!m_current_path.segments().is_empty());
|
VERIFY(!m_current_path.is_empty());
|
||||||
auto current_point = (*m_current_path.segments().rbegin())->point();
|
auto current_point = m_current_path.last_point();
|
||||||
m_current_path.cubic_bezier_curve_to(
|
m_current_path.cubic_bezier_curve_to(
|
||||||
current_point,
|
current_point,
|
||||||
map(args[0].to_float(), args[1].to_float()),
|
map(args[0].to_float(), args[1].to_float()),
|
||||||
|
@ -277,7 +277,7 @@ RENDERER_HANDLER(path_cubic_bezier_curve_no_first_control)
|
||||||
RENDERER_HANDLER(path_cubic_bezier_curve_no_second_control)
|
RENDERER_HANDLER(path_cubic_bezier_curve_no_second_control)
|
||||||
{
|
{
|
||||||
VERIFY(args.size() == 4);
|
VERIFY(args.size() == 4);
|
||||||
VERIFY(!m_current_path.segments().is_empty());
|
VERIFY(!m_current_path.is_empty());
|
||||||
auto first_control_point = map(args[0].to_float(), args[1].to_float());
|
auto first_control_point = map(args[0].to_float(), args[1].to_float());
|
||||||
auto second_control_point = map(args[2].to_float(), args[3].to_float());
|
auto second_control_point = map(args[2].to_float(), args[3].to_float());
|
||||||
m_current_path.cubic_bezier_curve_to(
|
m_current_path.cubic_bezier_curve_to(
|
||||||
|
|
|
@ -129,7 +129,8 @@ WebIDL::ExceptionOr<void> CanvasPath::arc_to(double x1, double y1, double x2, do
|
||||||
|
|
||||||
// 2. Ensure there is a subpath for (x1, y1).
|
// 2. Ensure there is a subpath for (x1, y1).
|
||||||
auto transform = active_transform();
|
auto transform = active_transform();
|
||||||
m_path.ensure_subpath(transform.map(Gfx::FloatPoint { x1, y1 }));
|
if (m_path.is_empty())
|
||||||
|
m_path.move_to(transform.map(Gfx::FloatPoint { x1, y1 }));
|
||||||
|
|
||||||
// 3. If radius is negative, then throw an "IndexSizeError" DOMException.
|
// 3. If radius is negative, then throw an "IndexSizeError" DOMException.
|
||||||
if (radius < 0)
|
if (radius < 0)
|
||||||
|
|
|
@ -42,9 +42,9 @@ Path2D::Path2D(JS::Realm& realm, Optional<Variant<JS::Handle<Path2D>, String>> c
|
||||||
auto path_instructions = SVG::AttributeParser::parse_path_data(path->get<String>());
|
auto path_instructions = SVG::AttributeParser::parse_path_data(path->get<String>());
|
||||||
auto svg_path = SVG::path_from_path_instructions(path_instructions);
|
auto svg_path = SVG::path_from_path_instructions(path_instructions);
|
||||||
|
|
||||||
if (!svg_path.segments().is_empty()) {
|
if (!svg_path.is_empty()) {
|
||||||
// 5. Let (x, y) be the last point in svgPath.
|
// 5. Let (x, y) be the last point in svgPath.
|
||||||
auto xy = svg_path.segments().last()->point();
|
auto xy = svg_path.last_point();
|
||||||
|
|
||||||
// 6. Add all the subpaths, if any, from svgPath to output.
|
// 6. Add all the subpaths, if any, from svgPath to output.
|
||||||
this->path() = move(svg_path);
|
this->path() = move(svg_path);
|
||||||
|
@ -70,7 +70,7 @@ WebIDL::ExceptionOr<void> Path2D::add_path(JS::NonnullGCPtr<Path2D> path, Geomet
|
||||||
// The addPath(path, transform) method, when invoked on a Path2D object a, must run these steps:
|
// The addPath(path, transform) method, when invoked on a Path2D object a, must run these steps:
|
||||||
|
|
||||||
// 1. If the Path2D object path has no subpaths, then return.
|
// 1. If the Path2D object path has no subpaths, then return.
|
||||||
if (path->path().segments().is_empty())
|
if (path->path().is_empty())
|
||||||
return {};
|
return {};
|
||||||
|
|
||||||
// 2. Let matrix be the result of creating a DOMMatrix from the 2D dictionary transform.
|
// 2. Let matrix be the result of creating a DOMMatrix from the 2D dictionary transform.
|
||||||
|
@ -85,11 +85,11 @@ WebIDL::ExceptionOr<void> Path2D::add_path(JS::NonnullGCPtr<Path2D> path, Geomet
|
||||||
auto copy = path->path().copy_transformed(Gfx::AffineTransform { static_cast<float>(matrix->m11()), static_cast<float>(matrix->m12()), static_cast<float>(matrix->m21()), static_cast<float>(matrix->m22()), static_cast<float>(matrix->m41()), static_cast<float>(matrix->m42()) });
|
auto copy = path->path().copy_transformed(Gfx::AffineTransform { static_cast<float>(matrix->m11()), static_cast<float>(matrix->m12()), static_cast<float>(matrix->m21()), static_cast<float>(matrix->m22()), static_cast<float>(matrix->m41()), static_cast<float>(matrix->m42()) });
|
||||||
|
|
||||||
// 6. Let (x, y) be the last point in the last subpath of c.
|
// 6. Let (x, y) be the last point in the last subpath of c.
|
||||||
auto xy = copy.segments().last()->point();
|
auto xy = copy.last_point();
|
||||||
|
|
||||||
// 7. Add all the subpaths in c to a.
|
// 7. Add all the subpaths in c to a.
|
||||||
// FIXME: Is this correct?
|
// FIXME: Is this correct?
|
||||||
this->path().add_path(copy);
|
this->path().append_path(copy);
|
||||||
|
|
||||||
// 8. Create a new subpath in a with (x, y) as the only point in the subpath.
|
// 8. Create a new subpath in a with (x, y) as the only point in the subpath.
|
||||||
this->move_to(xy.x(), xy.y());
|
this->move_to(xy.x(), xy.y());
|
||||||
|
|
|
@ -111,7 +111,7 @@ Gfx::Path path_from_path_instructions(ReadonlySpan<PathInstruction> instructions
|
||||||
|
|
||||||
for (auto& instruction : instructions) {
|
for (auto& instruction : instructions) {
|
||||||
// If the first path element uses relative coordinates, we treat them as absolute by making them relative to (0, 0).
|
// If the first path element uses relative coordinates, we treat them as absolute by making them relative to (0, 0).
|
||||||
auto last_point = path.segments().is_empty() ? Gfx::FloatPoint { 0, 0 } : path.segments().last()->point();
|
auto last_point = path.last_point();
|
||||||
|
|
||||||
auto& absolute = instruction.absolute;
|
auto& absolute = instruction.absolute;
|
||||||
auto& data = instruction.data;
|
auto& data = instruction.data;
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue