Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add an interface to the Path to support constructing Path using SVG #284

Merged
merged 11 commits into from
Oct 30, 2024
27 changes: 27 additions & 0 deletions include/tgfx/core/Path.h
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,28 @@ class Path {
*/
void cubicTo(const Point& control1, const Point& control2, const Point& point);

/**
* Appends an arc to the Path. The arc is represented by one or more conic sections that describe
* part of an oval with radii (rx, ry) rotated by xAxisRotate degrees. The arc curves from the
* last point in the Path to (x, y), choosing one of four possible routes: clockwise or
* counterclockwise, and smaller or larger.
* The arc sweep is always less than 360 degrees. If either radius is zero, or if the last point
* in the Path equals (x, y), a line to (x, y) is appended instead. If both radii are greater
* than zero but too small to fit the arc, they are scaled to fit.
* This method appends up to four conic curves to represent the arc.
* It implements the functionality of the SVG arc, although the SVG sweep-flag value is the
* opposite of the integer value of the sweep parameter; SVG uses 1 for clockwise, while
* counterclockwise is represented by zero.
*
* @param rx x radius before x-axis rotation
* @param ry y radius before x-axis rotation
* @param xAxisRotate x-axis rotation in degrees; positive values are clockwise
* @param largeArc chooses the larger or smaller arc
* @param reversed chooses the rotation direction; false for clockwise
* @param endPoint end point of the arc
*/
void arcTo(float rx, float ry, float xAxisRotate, PathArcSize largeArc, bool reversed,
Point endPoint);
/**
* Closes the current contour of Path. A closed contour connects the first and last Point with
* line, forming a continuous loop.
Expand Down Expand Up @@ -287,6 +309,11 @@ class Path {
*/
int countVerbs() const;

/**
* Returns last point on Path in lastPoint. Returns false if point array is empty.
*/
bool getLastPoint(Point* lastPoint) const;

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

Expand Down
14 changes: 14 additions & 0 deletions include/tgfx/core/PathTypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@ enum class PathVerb {
Close
};

/**
* Specify whether the arc is greater than 180 degrees pair or less than 180 degrees pair.
*/
enum class PathArcSize : uint8_t {
/**
* smaller of arc pair
*/
Small,
/**
* larger of arc pair
*/
Large,
};

/**
* Zero to four Point are stored in points, depending on the returned PathVerb
*/
Expand Down
40 changes: 36 additions & 4 deletions include/tgfx/core/Point.h
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,14 @@ struct Point {
}

/**
* Returns true if fX and fY are both zero.
* Returns true if x and y are both zero.
*/
bool isZero() const {
return (0 == x) && (0 == y);
}

/**
* Sets fX to x and fY to y.
* Sets x to xValue and y to yValue.
*/
void set(float xValue, float yValue) {
x = xValue;
Expand Down Expand Up @@ -104,20 +104,52 @@ struct Point {
}

/**
* Returns a Point from b to a; computed as (a.fX - b.fX, a.fY - b.fY).
* Returns a Point from b to a; computed as (a.x - b.x, a.y - b.y).
*/
friend Point operator-(const Point& a, const Point& b) {
return {a.x - b.x, a.y - b.y};
}

/**
* Subtracts vector Point v from Point. Sets Point to: (x - v.x, y - v.y).
*/
void operator-=(const Point& v) {
x -= v.x;
y -= v.y;
}

/**
* Returns Point resulting from Point a offset by Point b, computed as:
* (a.fX + b.fX, a.fY + b.fY).
* (a.x + b.x, a.y + b.y).
*/
friend Point operator+(const Point& a, const Point& b) {
return {a.x + b.x, a.y + b.y};
}

/**
* offset vector point v from Point. Sets Point to: (x + v.x, y + v.y).
*/
void operator+=(const Point& v) {
x += v.x;
y += v.y;
}

/**
* Returns Point multiplied by scale.
* (x * scale, y * scale)
*/
friend Point operator*(const Point& p, float scale) {
return {p.x * scale, p.y * scale};
}

/**
* Multiplies Point by scale. Sets Point to: (x * scale, y * scale).
*/
void operator*=(float scale) {
x *= scale;
y *= scale;
}

/**
* Returns the Euclidean distance from origin.
*/
Expand Down
140 changes: 140 additions & 0 deletions src/core/Path.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,133 @@ 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);
}

// This converts the SVG arc to conics based on the SVG standard.
// Code source:
// 1. kdelibs/kdecore/svgicons Niko's code
// 2. webkit/chrome SVGPathNormalizer::decomposeArcToCubic()
// See also SVG implementation notes:
// http://www.w3.org/TR/SVG/implnote.html#ArcConversionEndpointToCenter
// Note that arcSweep bool value is flipped from the original implementation.
void Path::arcTo(float rx, float ry, float xAxisRotate, PathArcSize largeArc, bool reversed,
Point endPoint) {
std::array<Point, 2> srcPoints;
this->getLastPoint(&srcPoints[0]);
// If rx = 0 or ry = 0 then this arc is treated as a straight line segment (a "lineto")
// joining the endpoints.
// http://www.w3.org/TR/SVG/implnote.html#ArcOutOfRangeParameters
if (FloatNearlyZero(rx) && FloatNearlyZero(ry)) {
return this->lineTo(endPoint);
}
// If the current point and target point for the arc are identical, it should be treated as a
// zero length path. This ensures continuity in animations.
srcPoints[1] = endPoint;
if (srcPoints[0] == srcPoints[1]) {
return this->lineTo(endPoint);
}
rx = std::abs(rx);
ry = std::abs(ry);
auto midPointDistance = (srcPoints[0] - srcPoints[1]) * 0.5f;

auto pointTransform = Matrix::MakeRotate(-xAxisRotate);
auto transformedMidPoint = Point::Zero();
pointTransform.mapPoints(&transformedMidPoint, &midPointDistance, 1);
auto squareRx = rx * rx;
auto squareRy = ry * ry;
auto squareX = transformedMidPoint.x * transformedMidPoint.x;
auto squareY = transformedMidPoint.y * transformedMidPoint.y;

// Check if the radii are big enough to draw the arc, scale radii if not.
// http://www.w3.org/TR/SVG/implnote.html#ArcCorrectionOutOfRangeRadii
auto radiiScale = squareX / squareRx + squareY / squareRy;
if (radiiScale > 1) {
radiiScale = std::sqrt(radiiScale);
rx *= radiiScale;
ry *= radiiScale;
}

pointTransform.setScale(1.0f / rx, 1.0f / ry);
pointTransform.preRotate(-xAxisRotate);

std::array<Point, 2> unitPoints;
pointTransform.mapPoints(unitPoints.data(), srcPoints.data(), unitPoints.size());
auto delta = unitPoints[1] - unitPoints[0];

auto d = delta.x * delta.x + delta.y * delta.y;
auto scaleFactorSquared = std::max(1 / d - 0.25f, 0.f);

auto scaleFactor = std::sqrt(scaleFactorSquared);
if (reversed != static_cast<bool>(largeArc)) { // flipped from the original implementation
scaleFactor = -scaleFactor;
}
delta *= scaleFactor;
auto centerPoint = unitPoints[0] + unitPoints[1];
centerPoint *= 0.5f;
centerPoint.offset(-delta.y, delta.x);
unitPoints[0] -= centerPoint;
unitPoints[1] -= centerPoint;
auto theta1 = std::atan2(unitPoints[0].y, unitPoints[0].x);
auto theta2 = std::atan2(unitPoints[1].y, unitPoints[1].x);
auto thetaArc = theta2 - theta1;
if (thetaArc < 0 && !reversed) { // arcSweep flipped from the original implementation
thetaArc += M_PI_F * 2.0f;
} else if (thetaArc > 0 && reversed) { // arcSweep flipped from the original implementation
thetaArc -= M_PI_F * 2.0f;
}

// Very tiny angles cause our subsequent math to go wonky
// but a larger value is probably ok too.
if (std::abs(thetaArc) < (M_PI_F / (1000 * 1000))) {
return this->lineTo(endPoint);
}

pointTransform.setRotate(xAxisRotate);
pointTransform.preScale(rx, ry);

// the arc may be slightly bigger than 1/4 circle, so allow up to 1/3rd
auto segments = std::ceil(std::abs(thetaArc / (2 * M_PI_F / 3)));
auto thetaWidth = thetaArc / segments;
auto t = std::tan(0.5f * thetaWidth);
if (!FloatsAreFinite(&t, 1)) {
return;
}
auto startTheta = theta1;
auto conicW = std::sqrt(0.5f + std::cos(thetaWidth) * 0.5f);
auto float_is_integer = [](float scalar) -> bool { return scalar == std::floor(scalar); };
auto expectIntegers = FloatNearlyZero(M_PI_F * 0.5f - std::abs(thetaWidth)) &&
float_is_integer(rx) && float_is_integer(ry) &&
float_is_integer(endPoint.x) && float_is_integer(endPoint.y);

auto* path = &(writableRef()->path);
for (int i = 0; i < static_cast<int>(segments); ++i) {
auto endTheta = startTheta + thetaWidth;
auto sinEndTheta = SinSnapToZero(endTheta);
auto cosEndTheta = CosSnapToZero(endTheta);

unitPoints[1].set(cosEndTheta, sinEndTheta);
unitPoints[1] += centerPoint;
unitPoints[0] = unitPoints[1];
unitPoints[0].offset(t * sinEndTheta, -t * cosEndTheta);
std::array<Point, 2> mapped;
pointTransform.mapPoints(mapped.data(), unitPoints.data(), unitPoints.size());

// Computing the arc width introduces rounding errors that cause arcs to start outside their
// marks.A round rect may lose convexity as a result.If the input values are on integers,
// place the conic on integers as well.
if (expectIntegers) {
for (auto& point : mapped) {
point.x = std::round(point.x);
point.y = std::round(point.y);
}
}
path->conicTo(mapped[0].x, mapped[0].y, mapped[1].x, mapped[1].y, conicW);
startTheta = endTheta;
}

// The final point should match the input point (by definition); replace it to
// ensure that rounding errors in the above math don't cause any problems.
path->setLastPt(endPoint.x, endPoint.y);
}

void Path::close() {
writableRef()->path.close();
}
Expand Down Expand Up @@ -415,4 +542,17 @@ int Path::countPoints() const {
int Path::countVerbs() const {
return pathRef->path.countVerbs();
}

bool Path::getLastPoint(Point* lastPoint) const {
if (!lastPoint) {
return false;
}
auto skPoint = SkPoint::Make(0, 0);
if (pathRef->path.getLastPt(&skPoint)) {
lastPoint->set(skPoint.fX, skPoint.fY);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里对lastPoint的指针访问是否需要判空?检查一下PathKit里的对应实现是否判空,如果那边判空了,这里也要加上。

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

需要判空,已补充

return true;
}
return false;
};

} // namespace tgfx