Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 106 additions & 2 deletions include/tgfx/core/Path.h
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,36 @@ class Path {
*/
void cubicTo(const Point& control1, const Point& control2, const Point& point);

/**
* Adds a conic curve from last point towards (controlX, controlY), ending at (x, y), weighted by
* weight. Conics can represent circular, elliptical, parabolic, or hyperbolic arcs depending on
* the weight value:
* - weight < 1: elliptical arc
* - weight == 1: equivalent to quadTo (parabolic arc)
* - weight > 1: hyperbolic arc
* - weight == sqrt(2)/2 ≈ 0.707: exact 90-degree circular arc
* @param controlX x-coordinate of the control point
* @param controlY y-coordinate of the control point
* @param x x-coordinate of the end point
* @param y y-coordinate of the end point
* @param weight conic weight determining the curve type
*/
void conicTo(float controlX, float controlY, float x, float y, float weight);

/**
* Adds a conic curve from last point towards control, ending at point, weighted by weight. Conics
* can represent circular, elliptical, parabolic, or hyperbolic arcs depending on the weight
* value:
* - weight < 1: elliptical arc
* - weight == 1: equivalent to quadTo (parabolic arc)
* - weight > 1: hyperbolic arc
* - weight == sqrt(2)/2 ≈ 0.707: exact 90-degree circular arc
* @param control the control point
* @param point the end point
* @param weight conic weight determining the curve type
*/
void conicTo(const Point& control, const Point& point, float weight);

/**
* Append a line and arc to the current path. This is the same as the PostScript call "arct".
*/
Expand Down Expand Up @@ -337,9 +367,71 @@ class Path {
void reverse();

/**
* Iterates through verb array and associated Point array.
* Represents a single segment during path iteration.
*/
void decompose(const PathIterator& iterator, void* info = nullptr) const;
struct Segment {
PathVerb verb = PathVerb::Done;
Point points[4] = {};
float conicWeight = 0.0f;
};

/**
* Iterator for traversing path segments. Supports range-based for loops.
* The iterator provides high-performance traversal without virtual function overhead.
*
* Usage example:
* for (auto segment : path) {
* switch (segment.verb) {
* case PathVerb::Move: // segment.points[0] is the move-to point
* case PathVerb::Line: // segment.points[0-1] are line endpoints
* case PathVerb::Quad: // segment.points[0-2] are quad control points
* case PathVerb::Conic: // segment.points[0-2], segment.conicWeight
* case PathVerb::Cubic: // segment.points[0-3] are cubic control points
* case PathVerb::Close: // no points
* }
* }
*/
class Iterator {
public:
~Iterator();
Iterator(const Iterator& other);
Iterator& operator=(const Iterator& other);

Segment operator*() const {
return current;
}

Iterator& operator++();

bool operator!=(const Iterator& other) const {
return isDone != other.isDone;
}

private:
explicit Iterator(const Path* path);
Iterator();

void advance();

friend class Path;

static constexpr size_t kStorageSize = 64;
alignas(8) uint8_t storage[kStorageSize] = {};
Segment current = {};
bool isDone = true;
};

/**
* Returns an iterator to the first segment.
*/
Iterator begin() const;

/**
* Returns an iterator past the last segment.
*/
Iterator end() const {
return Iterator();
}

/**
* Returns the number of points in Path.
Expand All @@ -356,6 +448,18 @@ class Path {
*/
bool getLastPoint(Point* lastPoint) const;

/**
* Converts a conic curve to a series of quadratic Bezier curves.
* @param p0 conic start point
* @param p1 conic control point
* @param p2 conic end point
* @param weight conic weight
* @param pow2 quad count as power of two (0 to 5), e.g., pow2=1 generates 2 quads
* @return quad control points, size = 1 + 2 * numQuads
*/
static std::vector<Point> ConvertConicToQuads(const Point& p0, const Point& p1, const Point& p2,
float weight, int pow2 = 1);

private:
std::shared_ptr<PathRef> pathRef = nullptr;

Expand Down
16 changes: 9 additions & 7 deletions include/tgfx/core/PathTypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@

#pragma once

#include <functional>
#include "tgfx/core/Point.h"

namespace tgfx {
Expand Down Expand Up @@ -90,14 +89,22 @@ enum class PathVerb {
* PathIterator returns 3 points.
*/
Quad,
/**
* PathIterator returns 3 points and a weight.
*/
Conic,
/**
* PathIterator returns 4 points.
*/
Cubic,
/**
* PathIterator returns 0 points.
*/
Close
Close,
/**
* Iteration is complete, no more verbs to return.
*/
Done
};

/**
Expand All @@ -114,9 +121,4 @@ enum class PathArcSize {
Large,
};

/**
* Zero to four Point are stored in points, depending on the returned PathVerb
*/
using PathIterator = std::function<void(PathVerb verb, const Point points[4], void* info)>;

} // namespace tgfx
1 change: 1 addition & 0 deletions include/tgfx/svg/node/SVGText.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

#pragma once

#include <functional>
#include <memory>
#include <vector>
#include "tgfx/core/Path.h"
Expand Down
139 changes: 100 additions & 39 deletions src/core/Path.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,14 @@
/////////////////////////////////////////////////////////////////////////////////////////////////

#include "tgfx/core/Path.h"
#include <functional>
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wfloat-conversion"
#pragma clang diagnostic ignored "-Wimplicit-int-conversion"
#include <include/core/SkPath.h>
#pragma clang diagnostic pop
#include <include/core/SkPathTypes.h>
#include <include/core/SkRect.h>
#include <memory>
#include "core/PathRef.h"
#include "core/utils/AtomicCache.h"
#include "core/utils/MathExtra.h"
Expand Down Expand Up @@ -228,6 +233,14 @@ void Path::cubicTo(const Point& control1, const Point& control2, const Point& po
cubicTo(control1.x, control1.y, control2.x, control2.y, point.x, point.y);
}

void Path::conicTo(float controlX, float controlY, float x, float y, float weight) {
writableRef()->path.conicTo(controlX, controlY, x, y, weight);
}

void Path::conicTo(const Point& control, const Point& point, float weight) {
conicTo(control.x, control.y, point.x, point.y, weight);
}

void Path::arcTo(float x1, float y1, float x2, float y2, float radius) {
writableRef()->path.arcTo(x1, y1, x2, y2, radius);
}
Expand Down Expand Up @@ -554,44 +567,6 @@ void Path::reverse() {
path = tempPath;
}

void Path::decompose(const PathIterator& iterator, void* info) const {
if (iterator == nullptr) {
return;
}
const auto& skPath = pathRef->path;
SkPath::Iter iter(skPath, false);
SkPoint points[4];
SkPoint quads[5];
SkPath::Verb verb;
while ((verb = iter.next(points)) != SkPath::kDone_Verb) {
switch (verb) {
case SkPath::kMove_Verb:
iterator(PathVerb::Move, reinterpret_cast<Point*>(points), info);
break;
case SkPath::kLine_Verb:
iterator(PathVerb::Line, reinterpret_cast<Point*>(points), info);
break;
case SkPath::kQuad_Verb:
iterator(PathVerb::Quad, reinterpret_cast<Point*>(points), info);
break;
case SkPath::kConic_Verb:
// approximate with 2^1=2 quads.
SkPath::ConvertConicToQuads(points[0], points[1], points[2], iter.conicWeight(), quads, 1);
iterator(PathVerb::Quad, reinterpret_cast<Point*>(quads), info);
iterator(PathVerb::Quad, reinterpret_cast<Point*>(quads) + 2, info);
break;
case SkPath::kCubic_Verb:
iterator(PathVerb::Cubic, reinterpret_cast<Point*>(points), info);
break;
case SkPath::kClose_Verb:
iterator(PathVerb::Close, reinterpret_cast<Point*>(points), info);
break;
default:
break;
}
}
}

PathRef* Path::writableRef() {
if (pathRef.use_count() != 1) {
pathRef = std::make_shared<PathRef>(pathRef->path);
Expand Down Expand Up @@ -623,4 +598,90 @@ bool Path::getLastPoint(Point* lastPoint) const {
return false;
};

/////////////////////////////////////////////////////////////////////////////////////////////////
// Path::Iterator implementation
/////////////////////////////////////////////////////////////////////////////////////////////////

static_assert(sizeof(pk::SkPath::Iter) <= 64,
"Path::Iterator storage size is too small for SkPath::Iter");
static_assert(alignof(pk::SkPath::Iter) <= 8,
"Path::Iterator storage alignment is insufficient for SkPath::Iter");

Path::Iterator::Iterator(const Path* path) : isDone(false) {
new (storage) pk::SkPath::Iter(PathRef::ReadAccess(*path), false);
advance();
}

Path::Iterator::Iterator() = default;

Path::Iterator::~Iterator() {
if (!isDone) {
reinterpret_cast<pk::SkPath::Iter*>(storage)->~Iter();
}
}

Path::Iterator::Iterator(const Iterator& other) : current(other.current), isDone(other.isDone) {
if (!isDone) {
new (storage) pk::SkPath::Iter(*reinterpret_cast<const pk::SkPath::Iter*>(other.storage));
}
}

Path::Iterator& Path::Iterator::operator=(const Iterator& other) {
if (this == &other) {
return *this;
}
if (!isDone) {
reinterpret_cast<pk::SkPath::Iter*>(storage)->~Iter();
}
current = other.current;
isDone = other.isDone;
if (!isDone) {
new (storage) pk::SkPath::Iter(*reinterpret_cast<const pk::SkPath::Iter*>(other.storage));
}
return *this;
}

Path::Iterator& Path::Iterator::operator++() {
advance();
return *this;
}

void Path::Iterator::advance() {
auto* iter = reinterpret_cast<pk::SkPath::Iter*>(storage);
pk::SkPoint pts[4];
auto verb = iter->next(pts);
if (verb == pk::SkPath::kDone_Verb) {
isDone = true;
iter->~Iter();
current = {};
} else {
current.verb = static_cast<PathVerb>(verb);
std::memcpy(current.points, pts, sizeof(pts));
if (verb == pk::SkPath::kConic_Verb) {
current.conicWeight = iter->conicWeight();
} else {
current.conicWeight = 0.0f;
}
}
}

Path::Iterator Path::begin() const {
if (isEmpty()) {
return Iterator();
}
return Iterator(this);
}

std::vector<Point> Path::ConvertConicToQuads(const Point& p0, const Point& p1, const Point& p2,
float weight, int pow2) {
size_t maxQuads = static_cast<size_t>(1) << pow2;
std::vector<Point> quads(1 + (2 * maxQuads));
int numQuads = SkPath::ConvertConicToQuads(*reinterpret_cast<const SkPoint*>(&p0),
*reinterpret_cast<const SkPoint*>(&p1),
*reinterpret_cast<const SkPoint*>(&p2), weight,
reinterpret_cast<SkPoint*>(quads.data()), pow2);
quads.resize(1 + (2 * static_cast<size_t>(numQuads)));
return quads;
}

} // namespace tgfx
Loading