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 arc to Path. Arc is implemented by one or more conics weighted to describe part oval
* with radii (rx, ry) rotated by xAxisRotate degrees. Arc curves from last Path Point to (x,
* y), choosing one of four possible routes: clockwise or counterclockwise, and smaller or
* larger.
* Arc sweep is always less than 360 degrees. arcTo() appends line to (x, y) if either radii are
* zero, or if last Path Point equals (x, y). arcTo() scales radii (rx, ry) to fit last Path
* Point and (x, y) if both are greater than zero but too small.
* arcTo() appends up to four conic curves.
* arcTo() implements the functionality of SVG arc, although SVG sweep-flag value is opposite the
* integer value of sweep; SVG sweep-flag uses 1 for clockwise,while counter-clockwise direction
Copy link
Collaborator

Choose a reason for hiding this comment

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

参数里已经没有sweep flag了

Copy link
Collaborator Author

@YGaurora YGaurora Oct 29, 2024

Choose a reason for hiding this comment

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

这里应该是指的svg里面的参数
image

Copy link
Collaborator

Choose a reason for hiding this comment

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

用Github Copilot重写一下这段注释吧,prompt:Rewrite it to sound more natural for a native speaker.

* cast to int is zero.
*
* @param rx x radii on axes before x-axis rotation
* @param ry y radii on axes before x-axis rotation
* @param xAxisRotate x-axis rotation in degrees; positive values are clockwise
* @param largeArc chooses smaller or larger arc
* @param reversed Choose the rotation clockwise direction.(clockwise = false)
* @param endPt end of arc
Copy link
Collaborator

Choose a reason for hiding this comment

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

这里endPt和参数命名不一致

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

已修改

*/
void arcTo(float rx, float ry, float xAxisRotate, ArcSize 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 ArcSize : uint8_t {
Copy link
Collaborator

Choose a reason for hiding this comment

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

命名加上Path前缀: PathArcSize

/**
* smaller of arc pair
*/
Small_ArcSize,
/**
* larger of arc pair
*/
Large_ArcSize,
Copy link
Collaborator

Choose a reason for hiding this comment

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

把_ArcSize后缀去掉,Skia里这么命名是因为没有使用enum class,怕重名。

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

已修改

};

/**
* 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
139 changes: 139 additions & 0 deletions src/core/Path.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,135 @@ 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, ArcSize 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);
Point midPointDistance = (srcPoints[0] - srcPoints[1]) * 0.5f;

Matrix pointTransform;
pointTransform.setRotate(-xAxisRotate);
Copy link
Collaborator

Choose a reason for hiding this comment

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

可以合并为 auto pointTransform = Matrix::MakeRotate()


Point transformedMidPoint;
Copy link
Collaborator

Choose a reason for hiding this comment

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

声明变量都记得赋初始值 Point::Zero(), 不然很多隐藏bug。

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

已修改

pointTransform.mapPoints(&transformedMidPoint, &midPointDistance, 1);
float squareRx = rx * rx;
float squareRy = ry * ry;
float squareX = transformedMidPoint.x * transformedMidPoint.x;
float 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
float 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());
Point delta = unitPoints[1] - unitPoints[0];

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

float scaleFactor = std::sqrt(scaleFactorSquared);
if (reversed != static_cast<bool>(largeArc)) { // flipped from the original implementation
scaleFactor = -scaleFactor;
}
delta *= scaleFactor;
Point centerPoint = unitPoints[0] + unitPoints[1];
centerPoint *= 0.5f;
centerPoint.offset(-delta.y, delta.x);
unitPoints[0] -= centerPoint;
unitPoints[1] -= centerPoint;
float theta1 = std::atan2(unitPoints[0].y, unitPoints[0].x);
float theta2 = std::atan2(unitPoints[1].y, unitPoints[1].x);
float 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
float segments = std::ceil(std::abs(thetaArc / (2 * M_PI_F / 3)));
float thetaWidth = thetaArc / segments;
float t = std::tan(0.5f * thetaWidth);
if (!FloatsAreFinite(&t, 1)) {
return;
}
float startTheta = theta1;
float w = std::sqrt(0.5f + std::cos(thetaWidth) * 0.5f);
auto float_is_integer = [](float scalar) -> bool { return scalar == std::floor(scalar); };
bool 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) {
float endTheta = startTheta + thetaWidth;
float sinEndTheta = SkScalarSinSnapToZero(endTheta);
float cosEndTheta = SkScalarCosSnapToZero(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 (Point& 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, w);
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 +544,14 @@ int Path::countPoints() const {
int Path::countVerbs() const {
return pathRef->path.countVerbs();
}

bool Path::getLastPoint(Point* lastPoint) const {
SkPoint skPoint;
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